diff --git a/demo/right-angle-playground/css/style.css b/demo/right-angle-playground/css/style.css new file mode 100644 index 000000000..947734566 --- /dev/null +++ b/demo/right-angle-playground/css/style.css @@ -0,0 +1,9 @@ +body { + margin: 0; + padding: 0; +} + +#paper { + position: absolute; + inset: 0 0 0 0; +} diff --git a/demo/right-angle-playground/index.html b/demo/right-angle-playground/index.html new file mode 100644 index 000000000..85ba8a3cf --- /dev/null +++ b/demo/right-angle-playground/index.html @@ -0,0 +1,18 @@ + + + + + Right angle router playground + + + + +
+ + + + + + + + \ No newline at end of file diff --git a/demo/right-angle-playground/src/index.js b/demo/right-angle-playground/src/index.js new file mode 100644 index 000000000..70bb8825b --- /dev/null +++ b/demo/right-angle-playground/src/index.js @@ -0,0 +1,170 @@ +const { dia, shapes, linkTools, elementTools } = joint; +class ResizeTool extends elementTools.Control { + getPosition(view) { + const model = view.model; + const { width, height } = model.size(); + return { x: width, y: height }; + } + + setPosition(view, coordinates) { + const model = view.model; + model.resize( + Math.max(Math.round(coordinates.x / 2) * 2, 10), + Math.max(Math.round(coordinates.y / 2) * 2, 10) + ); + } +} + +const graph = new dia.Graph(); + +const paper = new dia.Paper({ + el: document.getElementById('paper'), + width: '100%', + height: '100%', + gridSize: 10, + async: true, + frozen: true, + model: graph, + defaultRouter: { name: 'rightAngle', args: { useVertices: true }}, + defaultConnector: { name: 'rounded' }, + background: { + color: '#151D29' + }, + defaultLinkAnchor: { + name: 'connectionRatio', + args: { + ratio: 0.25 + } + } +}); + +const rect = new shapes.standard.Rectangle({ + position: { x: 120, y: 120 }, + size: { width: 220, height: 60 }, + attrs: { + body: { + stroke: 'none', + fill: '#DF423D', + rx: 10, + ry: 10, + } + } +}); + +const rect2 = rect.clone(); + +rect2.resize(60, 220); +rect2.position(400, 700); + +const link = new shapes.standard.Link({ + attrs: { + line: { + stroke: 'white' + } + } +}); + +const link2 = link.clone(); + +link.source({ id: rect.id, anchor: { name: 'top' }}); +link.target({ id: rect2.id, anchor: { name: 'right' }}); +link.vertices([ + { x: 370, y: 420 }, + { x: 500, y: 500 } +]); + +link2.source({ x: 670, y: 100 }); +link2.target({ x: 800, y: 800 }); +link2.vertices([ + { x: 670, y: 420 }, + { x: 800, y: 500 }, +]); + +const link3 = link.clone(); +link3.attr('line/stroke', '#DF423D'); +link3.source({ x: 1000, y: 600 }); +link3.target({ id: link2.id }); +link3.vertices([{ x: 900, y: 400 }]); + +graph.addCells([rect, rect2, link, link2, link3]); + +rect.findView(paper).addTools( + new dia.ToolsView({ + tools: [ + new ResizeTool({ + selector: 'body' + }) + ] + }) +); + +rect2.findView(paper).addTools( + new dia.ToolsView({ + tools: [ + new ResizeTool({ + selector: 'body' + }) + ] + }) +); + +const linkToolsView = new dia.ToolsView({ + tools: [ + new linkTools.Vertices({ + focusOpacity: 0.5, + }), + new linkTools.TargetAnchor({ + focusOpacity: 0.5, + scale: 1.2 + }), + new linkTools.SourceAnchor({ + focusOpacity: 0.5, + scale: 1.2 + }), + ] +}); + +link.findView(paper).addTools(linkToolsView); + +const link2ToolsView = new dia.ToolsView({ + tools: [ + new linkTools.Vertices({ + focusOpacity: 0.5 + }), + new linkTools.SourceArrowhead({ + focusOpacity: 0.5 + }), + new linkTools.TargetArrowhead({ + focusOpacity: 0.5 + }) + ] +}); + +link2.findView(paper).addTools(link2ToolsView); + +const link3ToolsView = new dia.ToolsView({ + tools: [ + new linkTools.Vertices({ + focusOpacity: 0.5 + }) + ] +}); + +link3.findView(paper).addTools(link3ToolsView); + +function scaleToFit() { + const graphBBox = graph.getBBox(); + paper.scaleContentToFit({ + contentArea: graphBBox.clone().inflate(0, 100) + }); + const { sy } = paper.scale(); + const area = paper.getArea(); + const yTop = area.height / 2 - graphBBox.y - graphBBox.height / 2; + const xLeft = area.width / 2 - graphBBox.x - graphBBox.width / 2; + paper.translate(xLeft * sy, yTop * sy); +} + +window.addEventListener('resize', () => scaleToFit()); +scaleToFit(); + +paper.unfreeze(); \ No newline at end of file diff --git a/src/routers/rightAngle.mjs b/src/routers/rightAngle.mjs index 2ff1e19ea..f8f1be26d 100644 --- a/src/routers/rightAngle.mjs +++ b/src/routers/rightAngle.mjs @@ -12,75 +12,36 @@ const Directions = { const DEFINED_DIRECTIONS = [Directions.LEFT, Directions.RIGHT, Directions.TOP, Directions.BOTTOM]; -function getDirectionForLinkConnection(linkOrigin, connectionPoint, linkView) { - const tangent = linkView.getTangentAtLength(linkView.getClosestPointLength(connectionPoint)); - const roundedAngle = Math.round(tangent.angle() / 90) * 90; - - switch (roundedAngle) { - case 0: - case 360: - return linkOrigin.y < connectionPoint.y ? Directions.TOP : Directions.BOTTOM; - case 90: - return linkOrigin.x < connectionPoint.x ? Directions.LEFT : Directions.RIGHT; - case 180: - return linkOrigin.y < connectionPoint.y ? Directions.TOP : Directions.BOTTOM; - case 270: - return linkOrigin.x < connectionPoint.x ? Directions.LEFT : Directions.RIGHT; - } -} - -function rightAngleRouter(_vertices, opt, linkView) { - const margin = opt.margin || 20; - let { sourceDirection = Directions.AUTO, targetDirection = Directions.AUTO } = opt; - - const sourceView = linkView.sourceView; - const targetView = linkView.targetView; - - const isSourcePort = !!linkView.model.source().port; - const isTargetPort = !!linkView.model.target().port; - - if (sourceDirection === Directions.AUTO) { - sourceDirection = isSourcePort ? Directions.MAGNET_SIDE : Directions.ANCHOR_SIDE; - } +const OPPOSITE_DIRECTIONS = { + [Directions.LEFT]: Directions.RIGHT, + [Directions.RIGHT]: Directions.LEFT, + [Directions.TOP]: Directions.BOTTOM, + [Directions.BOTTOM]: Directions.TOP +}; - if (targetDirection === Directions.AUTO) { - targetDirection = isTargetPort ? Directions.MAGNET_SIDE : Directions.ANCHOR_SIDE; - } +const VERTICAL_DIRECTIONS = [Directions.TOP, Directions.BOTTOM]; - const sourceBBox = linkView.sourceBBox; - const targetBBox = linkView.targetBBox; - const sourcePoint = linkView.sourceAnchor; - const targetPoint = linkView.targetAnchor; - let { - x: sx0, - y: sy0, - width: sourceWidth = 0, - height: sourceHeight = 0 - } = sourceView && sourceView.model.isElement() ? g.Rect.fromRectUnion(sourceBBox, sourceView.model.getBBox()) : linkView.sourceAnchor; - - let { - x: tx0, - y: ty0, - width: targetWidth = 0, - height: targetHeight = 0 - } = targetView && targetView.model.isElement() ? g.Rect.fromRectUnion(targetBBox, targetView.model.getBBox()) : linkView.targetAnchor; +const ANGLE_DIRECTION_MAP = { + 0: Directions.RIGHT, + 180: Directions.LEFT, + 270: Directions.TOP, + 90: Directions.BOTTOM +}; - const tx1 = tx0 + targetWidth; - const ty1 = ty0 + targetHeight; - const sx1 = sx0 + sourceWidth; - const sy1 = sy0 + sourceHeight; +function getSegmentAngle(line) { + // TODO: the angle() method is general and therefore unnecessarily heavy for orthogonal links + return line.angle(); +} - // Key coordinates including the margin - const smx0 = sx0 - margin; - const smx1 = sx1 + margin; - const smy0 = sy0 - margin; - const smy1 = sy1 + margin; - const tmx0 = tx0 - margin; - const tmx1 = tx1 + margin; - const tmy0 = ty0 - margin; - const tmy1 = ty1 + margin; +function simplifyPoints(points) { + // TODO: use own more efficient implementation (filter points that do not change direction). + // To simplify segments that are almost aligned (start and end points differ by e.g. 0.5px), use a threshold of 1. + return new g.Polyline(points).simplify({ threshold: 1 }).points; +} - const sourceOutsidePoint = sourcePoint.clone(); +function resolveSides(source, target) { + const { point: sourcePoint, x0: sx0, y0: sy0, view: sourceView, bbox: sourceBBox, direction: sourceDirection } = source; + const { point: targetPoint, x0: tx0, y0: ty0, view: targetView, bbox: targetBBox, direction: targetDirection } = target; let sourceSide; @@ -97,26 +58,8 @@ function rightAngleRouter(_vertices, opt, linkView) { sourceSide = sourceDirection; } - switch (sourceSide) { - case 'left': - sourceOutsidePoint.x = smx0; - break; - case 'right': - sourceOutsidePoint.x = smx1; - break; - case 'top': - sourceOutsidePoint.y = smy0; - break; - case 'bottom': - sourceOutsidePoint.y = smy1; - break; - } - const targetOutsidePoint = targetPoint.clone(); - - let targetSide; - if (!targetView) { const targetLinkAnchorBBox = new g.Rect(tx0, ty0, 0, 0); targetSide = DEFINED_DIRECTIONS.includes(targetDirection) ? targetDirection : targetLinkAnchorBBox.sideNearestToPoint(sourcePoint); @@ -130,21 +73,274 @@ function rightAngleRouter(_vertices, opt, linkView) { targetSide = targetDirection; } - switch (targetSide) { + return [sourceSide, targetSide]; +} + +function resolveForTopSourceSide(source, target, nextInLine) { + const { x0: sx0, y0: sy0, width, height, point: anchor, margin } = source; + const sx1 = sx0 + width; + const sy1 = sy0 + height; + const smx0 = sx0 - margin; + const smx1 = sx1 + margin; + const smy0 = sy0 - margin; + + const { x: ax } = anchor; + const { x0: tx, y0: ty } = target; + + if (tx === ax && ty < sy0) return Directions.BOTTOM; + if (tx < ax && ty < smy0) return Directions.RIGHT; + if (tx > ax && ty < smy0) return Directions.LEFT; + if (tx < smx0 && ty >= sy0) return Directions.TOP; + if (tx > smx1 && ty >= sy0) return Directions.TOP; + if (tx >= smx0 && tx <= ax && ty > sy1) { + if (nextInLine.point.x < tx) { + return Directions.RIGHT; + } + + return Directions.LEFT; + } + if (tx <= smx1 && tx >= ax && ty > sy1) { + if (nextInLine.point.x < tx) { + return Directions.RIGHT; + } + + return Directions.LEFT; + } + + return Directions.TOP; +} + +function resolveForBottomSourceSide(source, target, nextInLine) { + const { x0: sx0, y0: sy0, width, height, point: anchor, margin } = source; + const sx1 = sx0 + width; + const sy1 = sy0 + height; + const smx0 = sx0 - margin; + const smx1 = sx1 + margin; + const smy1 = sy1 + margin; + + const { x: ax } = anchor; + const { x0: tx, y0: ty } = target; + + if (tx === ax && ty > sy1) return Directions.TOP; + if (tx < ax && ty > smy1) return Directions.RIGHT; + if (tx > ax && ty > smy1) return Directions.LEFT; + if (tx < smx0 && ty <= sy1) return Directions.BOTTOM; + if (tx > smx1 && ty <= sy1) return Directions.BOTTOM; + if (tx >= smx0 && tx <= ax && ty < sy0) { + if (nextInLine.point.x < tx) { + return Directions.RIGHT; + } + + return Directions.LEFT; + } + if (tx <= smx1 && tx >= ax && ty < sy0) { + if (nextInLine.point.x < tx) { + return Directions.RIGHT; + } + + return Directions.LEFT; + } + + return Directions.BOTTOM; +} + +function resolveForLeftSourceSide(source, target, nextInLine) { + const { y0: sy0, x0: sx0, width, height, point: anchor, margin } = source; + const sx1 = sx0 + width; + const sy1 = sy0 + height; + const smx0 = sx0 - margin; + const smy0 = sy0 - margin; + const smy1 = sy1 + margin; + + const { x: ax, y: ay } = anchor; + const { x0: tx, y0: ty } = target; + + if (tx < ax && ty === ay) return Directions.RIGHT; + if (tx <= smx0 && ty < ay) return Directions.BOTTOM; + if (tx <= smx0 && ty > ay) return Directions.TOP; + if (tx >= sx0 && ty <= smy0) return Directions.LEFT; + if (tx >= sx0 && ty >= smy1) return Directions.LEFT; + if (tx > sx1 && ty >= smy0 && ty <= ay) { + if (nextInLine.point.y < ty) { + return Directions.BOTTOM; + } + + return Directions.TOP; + } + if (tx > sx1 && ty <= smy1 && ty >= ay) { + if (nextInLine.point.y < ty) { + return Directions.BOTTOM; + } + + return Directions.TOP; + } + + return Directions.LEFT; +} + +function resolveForRightSourceSide(source, target, nextInLine) { + const { y0: sy0, x0: sx0, width, height, point: anchor, margin } = source; + const sx1 = sx0 + width; + const sy1 = sy0 + height; + const smx1 = sx1 + margin; + const smy0 = sy0 - margin; + const smy1 = sy1 + margin; + + const { x: ax, y: ay } = anchor; + const { x0: tx, y0: ty } = target; + + if (tx > ax && ty === ay) return Directions.LEFT; + if (tx >= smx1 && ty < ay) return Directions.BOTTOM; + if (tx >= smx1 && ty > ay) return Directions.TOP; + if (tx <= sx1 && ty <= smy0) return Directions.RIGHT; + if (tx <= sx1 && ty >= smy1) return Directions.RIGHT; + if (tx < sx0 && ty >= smy0 && ty <= ay) { + if (nextInLine.point.y < ty) { + return Directions.BOTTOM; + } + + return Directions.TOP; + } + if (tx < sx0 && ty <= smy1 && ty >= ay) { + if (nextInLine.point.y < ty) { + return Directions.BOTTOM; + } + + return Directions.TOP; + } + + return Directions.RIGHT; +} + +function resolveInitialDirection(source, target, nextInLine) { + const [sourceSide] = resolveSides(source, target); + + switch (sourceSide) { + case Directions.TOP: + return resolveForTopSourceSide(source, target, nextInLine); + case Directions.RIGHT: + return resolveForRightSourceSide(source, target, nextInLine); + case Directions.BOTTOM: + return resolveForBottomSourceSide(source, target, nextInLine); + case Directions.LEFT: + return resolveForLeftSourceSide(source, target, nextInLine); + } +} + +function getDirectionForLinkConnection(linkOrigin, connectionPoint, linkView) { + const tangent = linkView.getTangentAtLength(linkView.getClosestPointLength(connectionPoint)); + const roundedAngle = Math.round(getSegmentAngle(tangent) / 90) * 90; + + if (roundedAngle % 180 === 0 && linkOrigin.y === connectionPoint.y) { + return linkOrigin.x < connectionPoint.x ? Directions.LEFT : Directions.RIGHT; + } else if (linkOrigin.x === connectionPoint.x) { + return linkOrigin.y < connectionPoint.y ? Directions.TOP : Directions.BOTTOM; + } + + switch (roundedAngle) { + case 0: + case 180: + case 360: + return linkOrigin.y < connectionPoint.y ? Directions.TOP : Directions.BOTTOM; + case 90: + case 270: + return linkOrigin.x < connectionPoint.x ? Directions.LEFT : Directions.RIGHT; + } +} + +function pointDataFromAnchor(view, point, bbox, direction, isPort, fallBackAnchor, margin) { + if (direction === Directions.AUTO) { + direction = isPort ? Directions.MAGNET_SIDE : Directions.ANCHOR_SIDE; + } + + const isElement = view && view.model.isElement(); + + const { + x: x0, + y: y0, + width = 0, + height = 0 + } = isElement ? g.Rect.fromRectUnion(bbox, view.model.getBBox()) : fallBackAnchor; + + return { + point, + x0, + y0, + view, + bbox, + width, + height, + direction, + margin: isElement ? margin : 0 + }; +} + +function pointDataFromVertex({ x, y }) { + const point = new g.Point(x, y); + + return { + point, + x0: point.x, + y0: point.y, + view: null, + bbox: new g.Rect(x, y, 0, 0), + width: 0, + height: 0, + direction: null, + margin: 0 + }; +} + +function getOutsidePoint(side, pointData, margin) { + const outsidePoint = pointData.point.clone(); + + const { x0, y0, width, height } = pointData; + + switch (side) { case 'left': - targetOutsidePoint.x = tmx0; + outsidePoint.x = x0 - margin; break; case 'right': - targetOutsidePoint.x = tmx1; + outsidePoint.x = x0 + width + margin; break; case 'top': - targetOutsidePoint.y = tmy0; + outsidePoint.y = y0 - margin; break; case 'bottom': - targetOutsidePoint.y = tmy1; + outsidePoint.y = y0 + height + margin; break; } + return outsidePoint; +} + +function routeBetweenPoints(source, target) { + const { point: sourcePoint, x0: sx0, y0: sy0, view: sourceView, width: sourceWidth, height: sourceHeight, margin: sourceMargin } = source; + const { point: targetPoint, x0: tx0, y0: ty0, width: targetWidth, height: targetHeight, margin: targetMargin } = target; + + const tx1 = tx0 + targetWidth; + const ty1 = ty0 + targetHeight; + const sx1 = sx0 + sourceWidth; + const sy1 = sy0 + sourceHeight; + + const isSourceEl = sourceView && sourceView.model.isElement(); + + // Key coordinates including the margin + const smx0 = sx0 - sourceMargin; + const smx1 = sx1 + sourceMargin; + const smy0 = sy0 - sourceMargin; + const smy1 = sy1 + sourceMargin; + + const tmx0 = tx0 - targetMargin; + const tmx1 = tx1 + targetMargin; + const tmy0 = ty0 - targetMargin; + const tmy1 = ty1 + targetMargin; + + const [sourceSide, targetSide] = resolveSides(source, target); + + const sourceOutsidePoint = getOutsidePoint(sourceSide, { point: sourcePoint, x0: sx0, y0: sy0, width: sourceWidth, height: sourceHeight }, sourceMargin); + const targetOutsidePoint = getOutsidePoint(targetSide, { point: targetPoint, x0: tx0, y0: ty0, width: targetWidth, height: targetHeight }, targetMargin); + const { x: sox, y: soy } = sourceOutsidePoint; const { x: tox, y: toy } = targetOutsidePoint; const tcx = (tx0 + tx1) / 2; @@ -155,7 +351,7 @@ function rightAngleRouter(_vertices, opt, linkView) { const middleOfHorizontalSides = (scy < tcy ? (sy1 + ty0) : (ty1 + sy0)) / 2; if (sourceSide === 'left' && targetSide === 'right') { - if (smx0 <= tx1) { + if (smx0 <= tmx1) { let y = middleOfHorizontalSides; if (sx1 <= tx0) { if (ty1 >= smy0 && toy < soy) { @@ -178,7 +374,7 @@ function rightAngleRouter(_vertices, opt, linkView) { { x, y: toy } ]; } else if (sourceSide === 'right' && targetSide === 'left') { - if (smx1 >= tx0) { + if (smx1 >= tmx0) { let y = middleOfHorizontalSides; if (sox > tx1) { if (ty1 >= smy0 && toy < soy) { @@ -227,7 +423,7 @@ function rightAngleRouter(_vertices, opt, linkView) { { x: tox, y } ]; } else if (sourceSide === 'bottom' && targetSide === 'top') { - if (soy - margin > toy) { + if (soy - sourceMargin > toy) { let x = middleOfVerticalSides; let y = soy; @@ -259,8 +455,8 @@ function rightAngleRouter(_vertices, opt, linkView) { if (toy < soy) { if (sox >= tmx1 || sox <= tmx0) { return [ - { x: sox, y: Math.min(soy,toy) }, - { x: tox, y: Math.min(soy,toy) } + { x: sox, y: Math.min(soy, toy) }, + { x: tox, y: Math.min(soy, toy) } ]; } else if (tox > sox) { x = Math.min(sox, tmx0); @@ -270,8 +466,8 @@ function rightAngleRouter(_vertices, opt, linkView) { } else { if (tox >= smx1 || tox <= smx0) { return [ - { x: sox, y: Math.min(soy,toy) }, - { x: tox, y: Math.min(soy,toy) } + { x: sox, y: Math.min(soy, toy) }, + { x: tox, y: Math.min(soy, toy) } ]; } else if (tox >= sox) { x = Math.max(tox, smx1); @@ -287,34 +483,31 @@ function rightAngleRouter(_vertices, opt, linkView) { { x: tox, y: y1 } ]; } else if (sourceSide === 'bottom' && targetSide === 'bottom') { - if (tx0 >= sox + margin || tx1 <= sox - margin) { - return [ - { x: sox, y: Math.max(soy, toy) }, - { x: tox, y: Math.max(soy, toy) } - ]; - } - let x; - let y1; - let y2; + let y1 = Math.max((sy0 + ty1) / 2, toy); + let y2 = Math.max((sy1 + ty0) / 2, soy); if (toy > soy) { - y1 = Math.max((sy1 + ty0) / 2, toy); - y2 = Math.max((sy1 + ty0) / 2, soy); - - if (tox > sox) { + if (sox >= tmx1 || sox <= tmx0) { + return [ + { x: sox, y: Math.max(soy, toy) }, + { x: tox, y: Math.max(soy, toy) } + ]; + } else if (tox > sox) { x = Math.min(sox, tmx0); } else { x = Math.max(sox, tmx1); } } else { - y1 = Math.max((sy0 + ty1) / 2, toy); - y2 = Math.max((sy0 + ty1) / 2, soy); - - if (tox > sox) { - x = Math.min(tox, smx0); - } else { + if (tox >= smx1 || tox <= smx0) { + return [ + { x: sox, y: Math.max(soy, toy) }, + { x: tox, y: Math.max(soy, toy) } + ]; + } else if (tox >= sox) { x = Math.max(tox, smx1); + } else { + x = Math.min(tox, smx0); } } @@ -377,8 +570,9 @@ function rightAngleRouter(_vertices, opt, linkView) { } else if (sourceSide === 'top' && targetSide === 'right') { if (soy > toy) { if (sox < tox) { - let y = (sy0 + ty1) / 2; - if (y > tcy && y < tmy1 && sox < tmx0) { + let y = middleOfHorizontalSides; + + if ((y > tcy || !isSourceEl) && y < tmy1 && sox < tx0) { y = tmy0; } return [ @@ -387,37 +581,41 @@ function rightAngleRouter(_vertices, opt, linkView) { { x: tox, y: toy } ]; } + return [{ x: sox, y: toy }]; } - const x = (sx0 + tx1) / 2; + const x = Math.max(middleOfVerticalSides, tmx1); - if (sox > tox && sy1 >= toy) { + if (tox < sox && toy > sy0 && toy < sy1) { return [ { x: sox, y: soy }, - { x, y: soy }, - { x, y: toy }]; + { x: x, y: soy }, + { x: x, y: toy } + ]; } - if (x > smx0 && soy < ty1) { - const y = Math.min(sy0, ty0) - margin; - const x = Math.max(sx1, tx1) + margin; + if ((x > smx0 && toy > sy0) || tx0 > sx1) { + const y = Math.min(sy0 - sourceMargin, ty0 - targetMargin); + const x = Math.max(sx1 + sourceMargin, tx1 + targetMargin); return [ { x: sox, y }, { x, y }, { x, y: toy } ]; } + return [ { x: sox, y: soy }, - { x, y: soy }, - { x, y: toy } + { x: Math.max(x, tox), y: soy }, + { x: Math.max(x, tox), y: toy } ]; } else if (sourceSide === 'top' && targetSide === 'left') { if (soy > toy) { if (sox > tox) { - let y = (sy0 + ty1) / 2; - if (y > tcy && y < tmy1 && sox > tmx1) { + let y = middleOfHorizontalSides; + + if ((y > tcy || !isSourceEl) && y < tmy1 && sox > tx1) { y = tmy0; } return [ @@ -429,7 +627,7 @@ function rightAngleRouter(_vertices, opt, linkView) { return [{ x: sox, y: toy }]; } - const x = (sx1 + tx0) / 2; + const x = Math.min(tmx0, middleOfVerticalSides); if (sox < tox && sy1 >= toy) { return [ @@ -439,8 +637,8 @@ function rightAngleRouter(_vertices, opt, linkView) { } if (x < smx1 && soy < ty1) { - const y = Math.min(sy0, ty0) - margin; - const x = Math.min(sx0, tx0) - margin; + const y = Math.min(smy0, tmy0); + const x = Math.min(smx0, tmx0); return [ { x: sox, y }, { x, y }, @@ -455,8 +653,9 @@ function rightAngleRouter(_vertices, opt, linkView) { } else if (sourceSide === 'bottom' && targetSide === 'right') { if (soy < toy) { if (sox < tox) { - let y = (sy1 + ty0) / 2; - if (y < tcy && y > tmy0 && sox < tmx0) { + let y = middleOfHorizontalSides; + + if ((y < tcy || !isSourceEl) && y > tmy0 && sox < tx0) { y = tmy1; } return [ @@ -468,8 +667,8 @@ function rightAngleRouter(_vertices, opt, linkView) { return [{ x: sox, y: toy }]; } else { if (sx0 < tox) { - const y = Math.max(sy1, ty1) + margin; - const x = Math.max(sx1, tx1) + margin; + const y = Math.max(smy1, tmy1); + const x = Math.max(smx1, tmx1); return [ { x: sox, y }, { x, y }, @@ -488,8 +687,9 @@ function rightAngleRouter(_vertices, opt, linkView) { } else if (sourceSide === 'bottom' && targetSide === 'left') { if (soy < toy) { if (sox > tox) { - let y = (sy1 + ty0) / 2; - if (y < tcy && y > tmy0 && sox > tmx1) { + let y = middleOfHorizontalSides; + + if ((y < tcy || !isSourceEl) && y > tmy0 && sox > tx1) { y = tmy1; } return [ @@ -501,8 +701,8 @@ function rightAngleRouter(_vertices, opt, linkView) { return [{ x: sox, y: toy }]; } else { if (sx1 > tox) { - const y = Math.max(sy1, ty1) + margin; - const x = Math.min(sx0, tx0) - margin; + const y = Math.max(smy1, tmy1); + const x = Math.min(smx0, tmx0); return [ { x: sox, y }, { x, y }, @@ -518,13 +718,15 @@ function rightAngleRouter(_vertices, opt, linkView) { { x, y: soy }, { x, y: toy } ]; - } else if (sourceSide === 'left' && targetSide === 'bottom') { - if (sox > tox && soy >= tmy1) { + } + else if (sourceSide === 'left' && targetSide === 'bottom') { + if (sox >= tox && soy >= tmy1) { return [{ x: tox, y: soy }]; } if (sox >= tx1 && soy < toy) { - const x = (sx1 + tx0) / 2; + const x = middleOfVerticalSides; + return [ { x, y: soy }, { x, y: toy }, @@ -533,7 +735,7 @@ function rightAngleRouter(_vertices, opt, linkView) { } if (tox < sx1 && ty1 <= sy0) { - const y = (sy0 + ty1) / 2; + const y = middleOfHorizontalSides; return [ { x: sox, y: soy }, @@ -543,7 +745,7 @@ function rightAngleRouter(_vertices, opt, linkView) { } const x = Math.min(tmx0, sox); - const y = Math.max(sy1, ty1) + margin; + const y = Math.max(smy1, tmy1); return [ { x, y: soy }, @@ -557,7 +759,8 @@ function rightAngleRouter(_vertices, opt, linkView) { if (sox >= tx1) { if (soy > toy) { - const x = (sx0 + tx1) / 2; + const x = middleOfVerticalSides; + return [ { x, y: soy }, { x, y: toy }, @@ -567,7 +770,7 @@ function rightAngleRouter(_vertices, opt, linkView) { } if (tox <= sx1 && toy > soy) { - const y = (ty0 + sy1) / 2; + const y = middleOfHorizontalSides; return [ { x: sox, y: soy }, @@ -576,8 +779,8 @@ function rightAngleRouter(_vertices, opt, linkView) { ]; } - const x = toy < soy ? Math.min(sx0, tx0) - margin : smx0; - const y = Math.min(sy0, ty0) - margin; + const x = toy < soy ? Math.min(smx0, tmx0) : smx0; + const y = Math.min(smy0, tmy0); return [ { x, y: soy }, @@ -586,12 +789,13 @@ function rightAngleRouter(_vertices, opt, linkView) { ]; } else if (sourceSide === 'right' && targetSide === 'top') { - if (sox < tox && soy < tmy0) { + if (sox <= tox && soy < tmy0) { return [{ x: tox, y: soy }]; } if (sx1 < tx0 && soy > toy) { - let x = (sx1 + tx0) / 2; + let x = middleOfVerticalSides; + return [ { x, y: soy }, { x, y: toy }, @@ -600,7 +804,7 @@ function rightAngleRouter(_vertices, opt, linkView) { } if (tox < sox && ty0 > sy1) { - const y = (sy1 + ty0) / 2; + const y = middleOfHorizontalSides; return [ { x: sox, y: soy }, @@ -609,20 +813,22 @@ function rightAngleRouter(_vertices, opt, linkView) { ]; } - const x = Math.max(sx1, tx1) + margin; - const y = Math.min(sy0, ty0) - margin; + const x = Math.max(smx1, tmx1); + const y = Math.min(smy0, tmy0); + return [ { x, y: soy }, { x, y }, { x: tox, y } ]; } else if (sourceSide === 'right' && targetSide === 'bottom') { - if (sox < tox && soy >= tmy1) { + if (sox <= tox && soy >= tmy1) { return [{ x: tox, y: soy }]; } - if (sox <= tx0 && soy < toy) { - const x = (sx1 + tx0) / 2; + if (sox <= tmx0 && soy < toy) { + const x = middleOfVerticalSides; + return [ { x, y: soy }, { x, y: toy }, @@ -631,7 +837,7 @@ function rightAngleRouter(_vertices, opt, linkView) { } if (tox > sx0 && ty1 < sy0) { - const y = (sy0 + ty1) / 2; + const y = middleOfHorizontalSides; return [ { x: sox, y: soy }, @@ -641,7 +847,7 @@ function rightAngleRouter(_vertices, opt, linkView) { } const x = Math.max(tmx1, sox); - const y = Math.max(sy1, ty1) + margin; + const y = Math.max(smy1, tmy1); return [ { x, y: soy }, @@ -651,6 +857,200 @@ function rightAngleRouter(_vertices, opt, linkView) { } } +function rightAngleRouter(vertices, opt, linkView) { + const { sourceDirection = Directions.AUTO, targetDirection = Directions.AUTO } = opt; + const margin = opt.margin || 20; + const useVertices = opt.useVertices || false; + + const isSourcePort = !!linkView.model.source().port; + const sourcePoint = pointDataFromAnchor(linkView.sourceView, linkView.sourceAnchor, linkView.sourceBBox, sourceDirection, isSourcePort, linkView.sourceAnchor, margin); + + const isTargetPort = !!linkView.model.target().port; + const targetPoint = pointDataFromAnchor(linkView.targetView, linkView.targetAnchor, linkView.targetBBox, targetDirection, isTargetPort, linkView.targetAnchor, margin); + + let resultVertices = []; + + if (!useVertices || vertices.length === 0) { + return simplifyPoints(routeBetweenPoints(sourcePoint, targetPoint)); + } + + const verticesData = vertices.map((v) => pointDataFromVertex(v)); + const [firstVertex] = verticesData; + + if (sourcePoint.view && sourcePoint.view.model.isElement() && sourcePoint.view.model.getBBox().inflate(margin).containsPoint(firstVertex.point)) { + const [fromDirection] = resolveSides(sourcePoint, firstVertex); + const toDirection = fromDirection; + const dummySource = pointDataFromVertex(sourcePoint.point); + // Points do not usually have margin. Here we create a point with a margin. + dummySource.margin = margin; + dummySource.direction = fromDirection; + firstVertex.direction = toDirection; + + resultVertices.push(...routeBetweenPoints(dummySource, firstVertex), firstVertex.point); + } else { + // The first point responsible for the initial direction of the route + const next = verticesData[1] || targetPoint; + const direction = resolveInitialDirection(sourcePoint, firstVertex, next); + firstVertex.direction = direction; + + resultVertices.push(...routeBetweenPoints(sourcePoint, firstVertex), firstVertex.point); + } + + for (let i = 0; i < verticesData.length - 1; i++) { + const from = verticesData[i]; + const to = verticesData[i + 1]; + + const segment = new g.Line(from.point, to.point); + const segmentAngle = getSegmentAngle(segment); + if (segmentAngle % 90 === 0) { + // Since the segment is horizontal or vertical, we can skip the routing and just connect them with a straight line + const toDirection = ANGLE_DIRECTION_MAP[segmentAngle]; + const accessDirection = OPPOSITE_DIRECTIONS[toDirection]; + + if (toDirection !== from.direction) { + resultVertices.push(from.point, to.point); + to.direction = accessDirection; + } else { + const angle = g.normalizeAngle(segmentAngle - 90); + + let dx = 0; + let dy = 0; + + if (angle === 90) { + dy = -margin; + } else if (angle === 180) { + dx = -margin; + } else if (angle === 270) { + dy = margin; + } else if (angle === 0) { + dx = margin; + } + + const p1 = { x: from.point.x + dx, y: from.point.y + dy }; + const p2 = { x: to.point.x + dx, y: to.point.y + dy }; + + const segment2 = new g.Line(to.point, p2); + to.direction = ANGLE_DIRECTION_MAP[getSegmentAngle(segment2)]; + + // Constructing a loop + resultVertices.push(from.point, p1, p2, to.point); + } + + continue; + } + + const [fromDirection, toDirection] = resolveDirection(from, to); + + from.direction = fromDirection; + to.direction = toDirection; + + resultVertices.push(...routeBetweenPoints(from, to), to.point); + } + + const lastVertex = verticesData[verticesData.length - 1]; + + if (targetPoint.view && targetPoint.view.model.isElement()) { + if (targetPoint.view.model.getBBox().inflate(margin).containsPoint(lastVertex.point)) { + const [fromDirection] = resolveDirection(lastVertex, targetPoint); + const dummyTarget = pointDataFromVertex(targetPoint.point); + const [, toDirection] = resolveSides(lastVertex, targetPoint); + // we are creating a point that has a margin + dummyTarget.margin = margin; + dummyTarget.direction = toDirection; + lastVertex.direction = fromDirection; + + resultVertices.push(...routeBetweenPoints(lastVertex, dummyTarget)); + } else { + // the last point of `simplified` array is the last defined vertex + // grab the penultimate point and construct a line segment from it to the last vertex + // this will ensure that the last segment continues in a straight line + + const simplified = simplifyPoints(resultVertices); + const segment = new g.Line(simplified[simplified.length - 2], lastVertex.point); + const definedDirection = ANGLE_DIRECTION_MAP[Math.round(getSegmentAngle(segment))]; + lastVertex.direction = definedDirection; + + let lastSegmentRoute = routeBetweenPoints(lastVertex, targetPoint); + const [p1, p2] = simplifyPoints([...lastSegmentRoute, targetPoint.point]); + + const lastSegment = new g.Line(p1, p2); + const roundedLastSegmentAngle = Math.round(getSegmentAngle(lastSegment)); + const lastSegmentDirection = ANGLE_DIRECTION_MAP[roundedLastSegmentAngle]; + + if (lastSegmentDirection !== definedDirection && definedDirection === OPPOSITE_DIRECTIONS[lastSegmentDirection]) { + lastVertex.margin = margin; + lastSegmentRoute = routeBetweenPoints(lastVertex, targetPoint); + } + + resultVertices.push(...lastSegmentRoute); + } + } else { + // since the target is only a point we can apply the same logic as if we connected two verticesData + const [vertexDirection] = resolveDirection(lastVertex, targetPoint); + lastVertex.direction = vertexDirection; + + resultVertices.push(...routeBetweenPoints(lastVertex, targetPoint)); + } + + return simplifyPoints(resultVertices); +} + +function resolveDirection(from, to) { + const accessDirection = from.direction; + const isDirectionVertical = VERTICAL_DIRECTIONS.includes(accessDirection); + + let sourceDirection = from.direction; + let targetDirection = to.direction; + + if (isDirectionVertical) { + const isToAbove = from.point.y > to.point.y; + const dx = to.point.x - from.point.x; + + if (accessDirection === Directions.BOTTOM) { + // If isToAbove === false and we need figure out if to go left or right + sourceDirection = isToAbove ? OPPOSITE_DIRECTIONS[accessDirection] : dx >= 0 ? Directions.RIGHT : Directions.LEFT; + + if (dx > 0) { + targetDirection = isToAbove ? Directions.LEFT : Directions.TOP; + } else if (dx < 0) { + targetDirection = isToAbove ? Directions.RIGHT : Directions.TOP; + } + } else { + // If isToAbove === true and we need figure out if to go left or right + sourceDirection = isToAbove ? dx >= 0 ? Directions.RIGHT : Directions.LEFT : OPPOSITE_DIRECTIONS[accessDirection]; + + if (dx > 0) { + targetDirection = isToAbove ? Directions.BOTTOM : Directions.LEFT; + } else if (dx < 0) { + targetDirection = isToAbove ? Directions.BOTTOM : Directions.RIGHT; + } + } + } else { + const isToLeft = from.point.x > to.point.x; + const dy = to.point.y - from.point.y; + + if (accessDirection === Directions.RIGHT) { + sourceDirection = isToLeft ? OPPOSITE_DIRECTIONS[accessDirection] : dy >= 0 ? Directions.BOTTOM : Directions.TOP; + + if (dy > 0) { + targetDirection = isToLeft ? Directions.TOP : Directions.LEFT; + } else if (dy < 0) { + targetDirection = isToLeft ? Directions.BOTTOM : Directions.LEFT; + } + } else { + sourceDirection = isToLeft ? dy >= 0 ? Directions.BOTTOM : Directions.TOP : OPPOSITE_DIRECTIONS[accessDirection]; + + if (dy > 0) { + targetDirection = isToLeft ? Directions.RIGHT : Directions.TOP; + } else if (dy < 0) { + targetDirection = isToLeft ? Directions.RIGHT : Directions.BOTTOM; + } + } + } + + return [sourceDirection, targetDirection]; +} + rightAngleRouter.Directions = Directions; export const rightAngle = rightAngleRouter; diff --git a/test/jointjs/routers.js b/test/jointjs/routers.js index 658f76432..daea95066 100644 --- a/test/jointjs/routers.js +++ b/test/jointjs/routers.js @@ -333,7 +333,7 @@ QUnit.module('routers', function(hooks) { assert.ok(spyIsPointObstacle.called); assert.ok(spyIsPointObstacle.alwaysCalledWithExactly(sinon.match.instanceOf(g.Point))); - assert.checkDataPath(d, 'M 140 70 L 620 70','isPointObstacle option is taken into account'); + assert.checkDataPath(d, 'M 140 70 L 620 70', 'isPointObstacle option is taken into account'); }); @@ -443,10 +443,10 @@ QUnit.module('routers', function(hooks) { const height = 50; const size = { width, height }; const margin = 28; - const router = { name: 'rightAngle', args: { margin }}; + const rightAngleRouter = { name: 'rightAngle', args: { margin }}; const position = { x: 0, y: 150 }; - this.addTestSubjects = function(sourceSide, targetSide ){ + this.addTestSubjects = function(sourceSide, targetSide, router = rightAngleRouter) { const r1 = new joint.shapes.standard.Rectangle({ size }); const r2 = r1.clone().position(position.x, position.y); const l = new joint.shapes.standard.Link({ source: { id: r1.id, anchor: { name: sourceSide }}, target: { id: r2.id, anchor: { name: targetSide }}, router }); @@ -455,6 +455,12 @@ QUnit.module('routers', function(hooks) { return [r1, r2, l]; }; + this.addTestSubjectsWithVertices = function(sourceSide, targetSide, vertices) { + const [r1, r2, l] = this.addTestSubjects(sourceSide, targetSide, { ...rightAngleRouter, args: { margin, useVertices: true }}); + l.vertices(vertices); + return [r1, r2, l]; + }; + const topVerticalPathSegments = [ `${width / 2} 0`, `${width / 2} -${margin}`, @@ -502,7 +508,7 @@ QUnit.module('routers', function(hooks) { path = `M ${moveSegment} L ${segments.join(' L ')}`; assert.checkDataPath(d, path, 'Source on the left of target'); - + position1 = r1.position(); position2 = r2.position(); @@ -812,12 +818,22 @@ QUnit.module('routers', function(hooks) { `${position.y + width} ${height / 2}` ]; + const rightVerticalPathSegments = [ + `${width} ${height / 2}`, + `${width + margin} ${height / 2}`, + `${width + margin} ${height / 2 + position.y}`, + `${width} ${height / 2 + position.y}`, + ]; + QUnit.test('rightAngle routing - source: right, target: right', function(assert) { const [r1, r2, l] = this.addTestSubjects('right', 'right'); let d = this.paper.findViewByModel(l).metrics.data; + let segments = joint.util.cloneDeep(rightVerticalPathSegments); + let moveSegment = segments.shift(); + let path = `M ${moveSegment} L ${segments.join(' L ')}`; - assert.checkDataPath(d, 'M 50 25 L 78 25 L 78 25 L 78 25 L 78 175 L 50 175', 'Source above target'); + assert.checkDataPath(d, path, 'Source above target'); let position1 = r1.position(); let position2 = r2.position(); @@ -826,16 +842,19 @@ QUnit.module('routers', function(hooks) { r2.position(position1.x, position1.y); d = this.paper.findViewByModel(l).metrics.data; + segments = joint.util.cloneDeep(rightVerticalPathSegments).reverse(); + moveSegment = segments.shift(); + path = `M ${moveSegment} L ${segments.join(' L ')}`; - assert.checkDataPath(d, 'M 50 175 L 78 175 L 78 175 L 78 175 L 78 25 L 50 25', 'Target above source'); + assert.checkDataPath(d, path, 'Target above source'); r1.position(0, 0); r2.position(position.y, position.x); d = this.paper.findViewByModel(l).metrics.data; - let segments = joint.util.cloneDeep(rightHorizontalPathSegments); - let moveSegment = segments.shift(); - let path = `M ${moveSegment} L ${segments.join(' L ')}`; + segments = joint.util.cloneDeep(rightHorizontalPathSegments); + moveSegment = segments.shift(); + path = `M ${moveSegment} L ${segments.join(' L ')}`; assert.checkDataPath(d, path, 'Source on the left of target'); @@ -1399,12 +1418,22 @@ QUnit.module('routers', function(hooks) { `${position.y} ${height / 2}` ]; + const leftVerticalPathSegments = [ + `0 ${height / 2}`, + `-${margin} ${height / 2}`, + `-${margin} ${height / 2 + position.y}`, + `0 ${height / 2 + position.y}` + ]; + QUnit.test('rightAngle routing - source: left, target: left', function(assert) { const [r1, r2, l] = this.addTestSubjects('left', 'left'); let d = this.paper.findViewByModel(l).metrics.data; + let segments = joint.util.cloneDeep(leftVerticalPathSegments); + let moveSegment = segments.shift(); + let path = `M ${moveSegment} L ${segments.join(' L ')}`; - assert.checkDataPath(d, 'M 0 25 L -28 25 L -28 25 L -28 25 L -28 175 L 0 175', 'Source above target'); + assert.checkDataPath(d, path, 'Source above target'); let position1 = r1.position(); let position2 = r2.position(); @@ -1413,16 +1442,19 @@ QUnit.module('routers', function(hooks) { r2.position(position1.x, position1.y); d = this.paper.findViewByModel(l).metrics.data; + segments = joint.util.cloneDeep(leftVerticalPathSegments).reverse(); + moveSegment = segments.shift(); + path = `M ${moveSegment} L ${segments.join(' L ')}`; - assert.checkDataPath(d, 'M 0 175 L -28 175 L -28 175 L -28 175 L -28 25 L 0 25', 'Target above source'); + assert.checkDataPath(d, path, 'Target above source'); r1.position(0, 0); r2.position(position.y, position.x); d = this.paper.findViewByModel(l).metrics.data; - let segments = joint.util.cloneDeep(leftHorizontalPathSegments); - let moveSegment = segments.shift(); - let path = `M ${moveSegment} L ${segments.join(' L ')}`; + segments = joint.util.cloneDeep(leftHorizontalPathSegments); + moveSegment = segments.shift(); + path = `M ${moveSegment} L ${segments.join(' L ')}`; assert.checkDataPath(d, path, 'Source on the left of target'); @@ -1439,4 +1471,645 @@ QUnit.module('routers', function(hooks) { assert.checkDataPath(d, path, 'Target on the left of source'); }); + + QUnit.test('rightAngle routing with vertex - source: top, target: top', function(assert) { + const vertex = { x: 100, y: 100 }; + const [r1, r2, l] = this.addTestSubjectsWithVertices('top', 'top', [vertex]); + + let d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 25 0 L 25 -28 L 100 -28 L 100 111 L 25 111 L 25 150', 'Source above target with vertex'); + + let position1 = r1.position(); + let position2 = r2.position(); + + r1.position(position2.x, position2.y); + r2.position(position1.x, position1.y); + + d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 25 150 L 25 100 L 100 100 L 100 -28 L 25 -28 L 25 0', 'Target above source with vertex'); + + r1.position(0, 0); + r2.position(position.y, position.x); + + d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 25 0 L 25 -28 L 100 -28 L 100 100 L 125 100 L 125 -28 L 175 -28 L 175 0', 'Source on the left of target with vertex'); + + position1 = r1.position(); + position2 = r2.position(); + + r1.position(position2.x, position2.y); + r2.position(position1.x, position1.y); + + d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 175 0 L 175 -28 L 100 -28 L 100 100 L 75 100 L 75 -28 L 25 -28 L 25 0', 'Target on the left of source with vertex'); + }); + + QUnit.test('rightAngle routing with vertex - source: top, target: right', function(assert) { + const vertex = { x: 100, y: 100 }; + const [r1, r2, l] = this.addTestSubjectsWithVertices('top', 'right', [vertex]); + + let d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 25 0 L 25 -28 L 100 -28 L 100 175 L 50 175', 'Source above target with vertex'); + + let position1 = r1.position(); + let position2 = r2.position(); + + r1.position(position2.x, position2.y); + r2.position(position1.x, position1.y); + + d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 25 150 L 25 100 L 100 100 L 100 25 L 78 25 L 50 25', 'Target above source with vertex'); + + r1.position(0, 0); + r2.position(position.y, position.x); + + d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 25 0 L 25 -28 L 100 -28 L 100 100 L 228 100 L 228 25 L 200 25', 'Source on the left of target with vertex'); + + position1 = r1.position(); + position2 = r2.position(); + + r1.position(position2.x, position2.y); + r2.position(position1.x, position1.y); + + d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 175 0 L 175 -28 L 100 -28 L 100 100 L 75 100 L 75 25 L 50 25', 'Target on the left of source with vertex'); + }); + + QUnit.test('rightAngle routing with vertex - source: top, target: bottom', function(assert) { + const vertex = { x: 100, y: 100 }; + const [r1, r2, l] = this.addTestSubjectsWithVertices('top', 'bottom', [vertex]); + + let d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 25 0 L 25 -28 L 100 -28 L 100 228 L 25 228 L 25 200', 'Source above target with vertex'); + + let position1 = r1.position(); + let position2 = r2.position(); + + r1.position(position2.x, position2.y); + r2.position(position1.x, position1.y); + + d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 25 150 L 25 100 L 128 100 L 128 128 L 25 128 L 25 50', 'Target above source with vertex'); + + r1.position(0, 0); + r2.position(position.y, position.x); + + d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 25 0 L 25 -28 L 100 -28 L 100 100 L 175 100 L 175 50', 'Source on the left of target with vertex'); + + position1 = r1.position(); + position2 = r2.position(); + + r1.position(position2.x, position2.y); + r2.position(position1.x, position1.y); + + d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 175 0 L 175 -28 L 100 -28 L 100 100 L 25 100 L 25 50', 'Target on the left of source with vertex'); + }); + + QUnit.test('rightAngle routing with vertex - source: top, target: left', function(assert) { + const vertex = { x: 100, y: 100 }; + const [r1, r2, l] = this.addTestSubjectsWithVertices('top', 'left', [vertex]); + + let d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 25 0 L 25 -28 L 100 -28 L 100 228 L -28 228 L -28 175 L 0 175', 'Source above target with vertex'); + + let position1 = r1.position(); + let position2 = r2.position(); + + r1.position(position2.x, position2.y); + r2.position(position1.x, position1.y); + + d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 25 150 L 25 100 L 100 100 L 100 75 L -28 75 L -28 25 L 0 25', 'Target above source with vertex'); + + r1.position(0, 0); + r2.position(position.y, position.x); + + d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 25 0 L 25 -28 L 100 -28 L 100 100 L 125 100 L 125 25 L 150 25', 'Source on the left of target with vertex'); + + position1 = r1.position(); + position2 = r2.position(); + + r1.position(position2.x, position2.y); + r2.position(position1.x, position1.y); + + d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 175 0 L 175 -28 L 100 -28 L 100 100 L -28 100 L -28 25 L 0 25', 'Target on the left of source with vertex'); + }); + + QUnit.test('rightAngle routing with vertex - source: right, target: top', function(assert) { + const vertex = { x: 100, y: 100 }; + const [r1, r2, l] = this.addTestSubjectsWithVertices('right', 'top', [vertex]); + + let d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 50 25 L 100 25 L 100 111 L 25 111 L 25 150', 'Source above target with vertex'); + + let position1 = r1.position(); + let position2 = r2.position(); + + r1.position(position2.x, position2.y); + r2.position(position1.x, position1.y); + + d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 50 175 L 100 175 L 100 -28 L 25 -28 L 25 0', 'Target above source with vertex'); + + r1.position(0, 0); + r2.position(position.y, position.x); + + d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 50 25 L 100 25 L 100 100 L 125 100 L 125 -28 L 175 -28 L 175 0', 'Source on the left of target with vertex'); + + position1 = r1.position(); + position2 = r2.position(); + + r1.position(position2.x, position2.y); + r2.position(position1.x, position1.y); + + d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 200 25 L 228 25 L 228 100 L 75 100 L 75 -28 L 25 -28 L 25 0', 'Target on the left of source with vertex'); + }); + + QUnit.test('rightAngle routing with vertex - source: right, target: right', function(assert) { + const vertex = { x: 100, y: 100 }; + const [r1, r2, l] = this.addTestSubjectsWithVertices('right', 'right', [vertex]); + + let d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 50 25 L 100 25 L 100 175 L 50 175', 'Source above target with vertex'); + + let position1 = r1.position(); + let position2 = r2.position(); + + r1.position(position2.x, position2.y); + r2.position(position1.x, position1.y); + + d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 50 175 L 100 175 L 100 25 L 50 25', 'Target above source with vertex'); + + r1.position(0, 0); + r2.position(position.y, position.x); + + d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 50 25 L 100 25 L 100 100 L 228 100 L 228 25 L 200 25', 'Source on the left of target with vertex'); + + position1 = r1.position(); + position2 = r2.position(); + + r1.position(position2.x, position2.y); + r2.position(position1.x, position1.y); + + d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 200 25 L 228 25 L 228 100 L 89 100 L 89 25 L 50 25', 'Target on the left of source with vertex'); + }); + + QUnit.test('rightAngle routing with vertex - source: right, target: bottom', function(assert) { + const vertex = { x: 100, y: 100 }; + const [r1, r2, l] = this.addTestSubjectsWithVertices('right', 'bottom', [vertex]); + + let d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 50 25 L 100 25 L 100 228 L 25 228 L 25 200', 'Source above target with vertex'); + + let position1 = r1.position(); + let position2 = r2.position(); + + r1.position(position2.x, position2.y); + r2.position(position1.x, position1.y); + + d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 50 175 L 100 175 L 100 89 L 25 89 L 25 50', 'Target above source with vertex'); + + r1.position(0, 0); + r2.position(position.y, position.x); + + d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 50 25 L 100 25 L 100 100 L 175 100 L 175 50', 'Source on the left of target with vertex'); + + position1 = r1.position(); + position2 = r2.position(); + + r1.position(position2.x, position2.y); + r2.position(position1.x, position1.y); + + d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 200 25 L 228 25 L 228 100 L 25 100 L 25 50', 'Target on the left of source with vertex'); + }); + + QUnit.test('rightAngle routing with vertex - source: right, target: left', function(assert) { + const vertex = { x: 100, y: 100 }; + + const [r1, r2, l] = this.addTestSubjectsWithVertices('right', 'left', [vertex]); + + let d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 50 25 L 100 25 L 100 228 L -28 228 L -28 175 L 0 175', 'Source above target with vertex'); + + let position1 = r1.position(); + let position2 = r2.position(); + + r1.position(position2.x, position2.y); + r2.position(position1.x, position1.y); + + d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 50 175 L 100 175 L 100 -28 L -28 -28 L -28 25 L 0 25', 'Target above source with vertex'); + + r1.position(0, 0); + r2.position(position.y, position.x); + + d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 50 25 L 100 25 L 100 100 L 125 100 L 125 25 L 150 25', 'Source on the left of target with vertex'); + + position1 = r1.position(); + position2 = r2.position(); + + r1.position(position2.x, position2.y); + r2.position(position1.x, position1.y); + + d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 200 25 L 228 25 L 228 100 L -28 100 L -28 25 L 0 25', 'Target on the left of source with vertex'); + }); + + QUnit.test('rightAngle routing with vertex - source: bottom, target: top', function(assert) { + const vertex = { x: 100, y: 100 }; + const [r1, r2, l] = this.addTestSubjectsWithVertices('bottom', 'top', [vertex]); + + let d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 25 50 L 25 100 L 100 100 L 100 125 L 25 125 L 25 150', 'Source above target with vertex'); + + let position1 = r1.position(); + let position2 = r2.position(); + + r1.position(position2.x, position2.y); + r2.position(position1.x, position1.y); + + d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 25 200 L 25 228 L 100 228 L 100 -28 L 25 -28 L 25 0', 'Target above source with vertex'); + + r1.position(0, 0); + r2.position(position.y, position.x); + + d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 25 50 L 25 100 L 125 100 L 125 -28 L 175 -28 L 175 0', 'Source on the left of target with vertex'); + + position1 = r1.position(); + position2 = r2.position(); + + r1.position(position2.x, position2.y); + r2.position(position1.x, position1.y); + + d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 175 50 L 175 100 L 75 100 L 75 -28 L 25 -28 L 25 0', 'Target on the left of source with vertex'); + }); + + QUnit.test('rightAngle routing with vertex - source: bottom, target: right', function(assert) { + const vertex = { x: 100, y: 100 }; + const [r1, r2, l] = this.addTestSubjectsWithVertices('bottom', 'right', [vertex]); + + let d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 25 50 L 25 100 L 100 100 L 100 175 L 78 175 L 50 175', 'Source above target with vertex'); + + let position1 = r1.position(); + let position2 = r2.position(); + + r1.position(position2.x, position2.y); + r2.position(position1.x, position1.y); + + d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 25 200 L 25 228 L 100 228 L 100 25 L 50 25', 'Target above source with vertex'); + + r1.position(0, 0); + r2.position(position.y, position.x); + + d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 25 50 L 25 100 L 228 100 L 228 25 L 200 25', 'Source on the left of target with vertex'); + + position1 = r1.position(); + position2 = r2.position(); + + r1.position(position2.x, position2.y); + r2.position(position1.x, position1.y); + + d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 175 50 L 175 100 L 89 100 L 89 25 L 50 25', 'Target on the left of source with vertex'); + }); + + QUnit.test('rightAngle routing with vertex - source: bottom, target: bottom', function(assert) { + const vertex = { x: 100, y: 100 }; + const [r1, r2, l] = this.addTestSubjectsWithVertices('bottom', 'bottom', [vertex]); + + let d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 25 50 L 25 100 L 100 100 L 100 228 L 25 228 L 25 200', 'Source above target with vertex'); + + let position1 = r1.position(); + let position2 = r2.position(); + + r1.position(position2.x, position2.y); + r2.position(position1.x, position1.y); + + d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 25 200 L 25 228 L 100 228 L 100 89 L 25 89 L 25 50', 'Target above source with vertex'); + + r1.position(0, 0); + r2.position(position.y, position.x); + + d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 25 50 L 25 100 L 175 100 L 175 50', 'Source on the left of target with vertex'); + + position1 = r1.position(); + position2 = r2.position(); + + r1.position(position2.x, position2.y); + r2.position(position1.x, position1.y); + + d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 175 50 L 175 100 L 25 100 L 25 50', 'Target on the left of source with vertex'); + }); + + QUnit.test('rightAngle routing with vertex - source: bottom, target: left', function(assert) { + const vertex = { x: 100, y: 100 }; + const [r1, r2, l] = this.addTestSubjectsWithVertices('bottom', 'left', [vertex]); + + let d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 25 50 L 25 100 L 100 100 L 100 125 L -28 125 L -28 175 L 0 175', 'Source above target with vertex'); + + let position1 = r1.position(); + let position2 = r2.position(); + + r1.position(position2.x, position2.y); + r2.position(position1.x, position1.y); + + d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 25 200 L 25 228 L 100 228 L 100 -28 L -28 -28 L -28 25 L 0 25', 'Target above source with vertex'); + + r1.position(0, 0); + r2.position(position.y, position.x); + + d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 25 50 L 25 100 L 111 100 L 111 25 L 150 25', 'Source on the left of target with vertex'); + + position1 = r1.position(); + position2 = r2.position(); + + r1.position(position2.x, position2.y); + r2.position(position1.x, position1.y); + + d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 175 50 L 175 100 L -28 100 L -28 25 L 0 25', 'Target on the left of source with vertex'); + }); + + QUnit.test('rightAngle routing with vertex - source: left, target: top', function(assert) { + const vertex = { x: 100, y: 100 }; + const [r1, r2, l] = this.addTestSubjectsWithVertices('left', 'top', [vertex]); + + let d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 0 25 L -28 25 L -28 100 L 100 100 L 100 125 L 25 125 L 25 150', 'Source above target with vertex'); + + let position1 = r1.position(); + let position2 = r2.position(); + + r1.position(position2.x, position2.y); + r2.position(position1.x, position1.y); + + d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 0 175 L -28 175 L -28 100 L 100 100 L 100 -28 L 25 -28 L 25 0', 'Target above source with vertex'); + + r1.position(0, 0); + r2.position(position.y, position.x); + + d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 0 25 L -28 25 L -28 100 L 125 100 L 125 -28 L 175 -28 L 175 0', 'Source on the left of target with vertex'); + + position1 = r1.position(); + position2 = r2.position(); + + r1.position(position2.x, position2.y); + r2.position(position1.x, position1.y); + + d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 150 25 L 100 25 L 100 100 L 75 100 L 75 -28 L 25 -28 L 25 0', 'Target on the left of source with vertex'); + }); + + QUnit.test('rightAngle routing with vertex - source: left, target: right', function(assert) { + const vertex = { x: 100, y: 100 }; + const [r1, r2, l] = this.addTestSubjectsWithVertices('left', 'right', [vertex]); + + let d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 0 25 L -28 25 L -28 100 L 100 100 L 100 175 L 78 175 L 50 175', 'Source above target with vertex'); + + let position1 = r1.position(); + let position2 = r2.position(); + + r1.position(position2.x, position2.y); + r2.position(position1.x, position1.y); + + d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 0 175 L -28 175 L -28 100 L 100 100 L 100 25 L 78 25 L 50 25', 'Target above source with vertex'); + + r1.position(0, 0); + r2.position(position.y, position.x); + + d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 0 25 L -28 25 L -28 100 L 228 100 L 228 25 L 200 25', 'Source on the left of target with vertex'); + + position1 = r1.position(); + position2 = r2.position(); + + r1.position(position2.x, position2.y); + r2.position(position1.x, position1.y); + + d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 150 25 L 100 25 L 100 100 L 75 100 L 75 25 L 50 25', 'Target on the left of source with vertex'); + }); + + QUnit.test('rightAngle routing with vertex - source: left, target: bottom', function(assert) { + const vertex = { x: 100, y: 100 }; + const [r1, r2, l] = this.addTestSubjectsWithVertices('left', 'bottom', [vertex]); + + let d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 0 25 L -28 25 L -28 100 L 100 100 L 100 228 L 25 228 L 25 200', 'Source above target with vertex'); + + let position1 = r1.position(); + let position2 = r2.position(); + + r1.position(position2.x, position2.y); + r2.position(position1.x, position1.y); + + d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 0 175 L -28 175 L -28 100 L 128 100 L 128 128 L 25 128 L 25 50', 'Target above source with vertex'); + + r1.position(0, 0); + r2.position(position.y, position.x); + + d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 0 25 L -28 25 L -28 100 L 175 100 L 175 50', 'Source on the left of target with vertex'); + + position1 = r1.position(); + position2 = r2.position(); + + r1.position(position2.x, position2.y); + r2.position(position1.x, position1.y); + + d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 150 25 L 100 25 L 100 100 L 25 100 L 25 50', 'Target on the left of source with vertex'); + }); + + QUnit.test('rightAngle routing with vertex - source: left, target: left', function(assert) { + const vertex = { x: 100, y: 100 }; + const [r1, r2, l] = this.addTestSubjectsWithVertices('left', 'left', [vertex]); + + let d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 0 25 L -28 25 L -28 100 L 100 100 L 100 125 L -28 125 L -28 175 L 0 175', 'Source above target with vertex'); + + let position1 = r1.position(); + let position2 = r2.position(); + + r1.position(position2.x, position2.y); + r2.position(position1.x, position1.y); + + d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 0 175 L -28 175 L -28 100 L 100 100 L 100 75 L -28 75 L -28 25 L 0 25', 'Target above source with vertex'); + + r1.position(0, 0); + r2.position(position.y, position.x); + + d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 0 25 L -28 25 L -28 100 L 111 100 L 111 25 L 150 25', 'Source on the left of target with vertex'); + + position1 = r1.position(); + position2 = r2.position(); + + r1.position(position2.x, position2.y); + r2.position(position1.x, position1.y); + + d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 150 25 L 100 25 L 100 100 L -28 100 L -28 25 L 0 25', 'Target on the left of source with vertex'); + }); + + QUnit.test('rightAngle routing with vertex inside the source element bbox - source: top', function(assert) { + const vertices = [{ x: size.width * 0.75, y: size.height / 2 }, { x: 100, y: 100 }]; + const [, , l] = this.addTestSubjectsWithVertices('top', 'top', vertices); + let d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 25 0 L 25 -28 L 53 -28 L 53 12.5 L 37.5 12.5 L 37.5 100 L 100 100 L 100 125 L 25 125 L 25 150', 'Source above target with vertex inside the source element bbox'); + }); + + QUnit.test('rightAngle routing with vertex inside the source element bbox - source: right', function(assert) { + const vertices = [{ x: 0, y: size.y }, { x: 100, y: 100 }]; + const [, , l] = this.addTestSubjectsWithVertices('right', 'top', vertices); + let d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 50 25 L 78 25 L 78 -3 L 25 -3 L 25 0 L 0 0 L 0 100 L 100 100 L 100 125 L 25 125 L 25 150', 'Source above target with vertex inside the source element bbox'); + }); + + QUnit.test('rightAngle routing with vertex inside the source element bbox - source: bottom', function(assert) { + const vertices = [{ x: size.width, y: size.y }, { x: 100, y: 100 }]; + const [, , l] = this.addTestSubjectsWithVertices('bottom', 'top', vertices); + let d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 25 50 L 25 78 L 53 78 L 53 25 L 50 25 L 50 0 L 100 0 L 100 111 L 25 111 L 25 150', 'Source above target with vertex inside the source element bbox'); + }); + + QUnit.test('rightAngle routing with vertex inside the source element bbox - source: left', function(assert) { + const vertices = [{ x: size.width, y: size.y }, { x: 100, y: 100 }]; + const [, , l] = this.addTestSubjectsWithVertices('left', 'top', vertices); + let d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 0 25 L -28 25 L -28 -3 L 25 -3 L 25 0 L 100 0 L 100 111 L 25 111 L 25 150', 'Source above target with vertex inside the source element bbox'); + }); + + QUnit.test('rightAngle routing with vertex inside the target element bbox - target: top', function(assert) { + const vertices = [{ x: 100, y: 100 }, { x: position.x + size.width * 0.75, y: position.y + size.height / 2 }]; + const [, , l] = this.addTestSubjectsWithVertices('top', 'top', vertices); + let d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 25 0 L 25 -28 L 100 -28 L 100 175 L 31.25 175 L 31.25 122 L 25 122 L 25 150', 'Source above target with vertex inside the target element bbox'); + }); + + QUnit.test('rightAngle routing with vertex inside the target element bbox - target: right', function(assert) { + const vertices = [{ x: 100, y: 100 }, { x: position.x, y: position.y }]; + const [, , l] = this.addTestSubjectsWithVertices('top', 'right', vertices); + let d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 25 0 L 25 -28 L 100 -28 L 100 150 L 0 150 L 0 203 L 78 203 L 78 175 L 50 175', 'Source above target with vertex inside the target element bbox'); + }); + + QUnit.test('rightAngle routing with vertex inside the target element bbox - target: bottom', function(assert) { + const vertices = [{ x: 100, y: 100 }, { x: position.x + size.width, y: position.y }]; + const [, , l] = this.addTestSubjectsWithVertices('top', 'bottom', vertices); + let d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 25 0 L 25 -28 L 100 -28 L 100 150 L 37.5 150 L 37.5 228 L 25 228 L 25 200', 'Source above target with vertex inside the target element bbox'); + }); + + QUnit.test('rightAngle routing with vertex inside the target element bbox - target: left', function(assert) { + const vertices = [{ x: 100, y: 100 }, { x: position.x + size.width, y: position.y }]; + const [, , l] = this.addTestSubjectsWithVertices('top', 'left', vertices); + let d = this.paper.findViewByModel(l).metrics.data; + + assert.checkDataPath(d, 'M 25 0 L 25 -28 L 100 -28 L 100 150 L 25 150 L 25 147 L -28 147 L -28 175 L 0 175', 'Source above target with vertex inside the target element bbox'); + }); }); diff --git a/types/joint.d.ts b/types/joint.d.ts index d58ad2cbe..5ee543d81 100644 --- a/types/joint.d.ts +++ b/types/joint.d.ts @@ -3535,6 +3535,8 @@ export namespace routers { interface RightAngleRouterArguments { margin?: number; + /** @experimental before version 4.0 */ + useVertices?: boolean; sourceDirection?: RightAngleDirections; targetDirection?: RightAngleDirections; }