diff --git a/examples/right-angle-playground-js/index.html b/examples/right-angle-playground-js/index.html new file mode 100644 index 0000000000..4560fbd8d7 --- /dev/null +++ b/examples/right-angle-playground-js/index.html @@ -0,0 +1,17 @@ + + + + + + + + JointJS: Right Angle Router Playground + + + +
+ + + + + diff --git a/examples/right-angle-playground-js/package.json b/examples/right-angle-playground-js/package.json new file mode 100644 index 0000000000..6be75782fc --- /dev/null +++ b/examples/right-angle-playground-js/package.json @@ -0,0 +1,17 @@ +{ + "name": "@joint/demo-right-angle-playground-js", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "devDependencies": { + "vite": "^7.3.1" + }, + "dependencies": { + "@joint/core": "workspace:^" + } +} diff --git a/examples/right-angle-playground-js/src/main.js b/examples/right-angle-playground-js/src/main.js new file mode 100644 index 0000000000..c01802b70c --- /dev/null +++ b/examples/right-angle-playground-js/src/main.js @@ -0,0 +1,137 @@ +import { dia, shapes, linkTools, elementTools } from '@joint/core'; +import './styles.css'; + +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 / 10) * 10, 50), + Math.max(Math.round(coordinates.y / 10) * 10, 50) + ); + } +} + +const graph = new dia.Graph({}, { cellNamespace: shapes }); + +const paper = new dia.Paper({ + el: document.getElementById('paper-container'), + width: '100%', + height: '100%', + gridSize: 10, + async: true, + frozen: true, + model: graph, + cellViewNamespace: shapes, + defaultRouter: { name: 'rightAngle', args: { + useVertices: true, + margin: 40, + minPathMargin: 10, + //sourceMargin: 40, + //targetMargin: 30 + }}, + defaultConnector: { name: 'rounded' }, + background: { + color: '#151D29' + }, + defaultLinkAnchor: { + name: 'connectionRatio', + args: { + ratio: 0.25 + } + } +}); + +paper.setGrid({ name: 'dot'}); + +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(300, 250); + +const link = new shapes.standard.Link({ + attrs: { + line: { + stroke: 'white' + } + } +}); + +link.source({ id: rect.id, anchor: { name: 'top' }}); +link.target({ id: rect2.id, anchor: { name: 'right' }}); + +graph.addCells([rect, rect2, link]); + +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); + +function scaleToFit() { + const graphBBox = graph.getBBox(); + paper.transformToFitContent({ + 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(); diff --git a/examples/right-angle-playground-js/src/styles.css b/examples/right-angle-playground-js/src/styles.css new file mode 100644 index 0000000000..be510ea6b0 --- /dev/null +++ b/examples/right-angle-playground-js/src/styles.css @@ -0,0 +1,17 @@ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + background-color: #151D29; + width: 100%; + height: 100vh; +} + +#paper-container { + position: absolute; + inset: 0; + overflow: hidden; +} diff --git a/packages/joint-core/src/routers/rightAngle.mjs b/packages/joint-core/src/routers/rightAngle.mjs index fb1e3c5ae1..92d5311f51 100644 --- a/packages/joint-core/src/routers/rightAngle.mjs +++ b/packages/joint-core/src/routers/rightAngle.mjs @@ -80,31 +80,31 @@ 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 sMarginX0 = sx0 - margin; + const sMarginX1 = sx1 + margin; + const sMarginY0 = 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) { + if (tx < ax && ty < sMarginY0) { if (nextInLine.point.x === ax) return Directions.BOTTOM; return Directions.RIGHT; } - if (tx > ax && ty < smy0) { + if (tx > ax && ty < sMarginY0) { if (nextInLine.point.x === ax) return Directions.BOTTOM; return Directions.LEFT; } - if (tx < smx0 && ty > smy0) return Directions.TOP; - if (tx > smx1 && ty > smy0) return Directions.TOP; - if (tx >= smx0 && tx <= ax && ty > sy1) { + if (tx < sMarginX0 && ty > sMarginY0) return Directions.TOP; + if (tx > sMarginX1 && ty > sMarginY0) return Directions.TOP; + if (tx >= sMarginX0 && tx <= ax && ty > sy1) { if (nextInLine.point.x < tx) { return Directions.RIGHT; } return Directions.LEFT; } - if (tx <= smx1 && tx >= ax && ty > sy1) { + if (tx <= sMarginX1 && tx >= ax && ty > sy1) { if (nextInLine.point.x < tx) { return Directions.RIGHT; } @@ -118,31 +118,31 @@ 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 sMarginX0 = sx0 - margin; + const sMarginX1 = sx1 + margin; + const sMarginY1 = 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) { + if (tx < ax && ty > sMarginY1) { if (nextInLine.point.x === ax) return Directions.TOP; return Directions.RIGHT; } - if (tx > ax && ty > smy1) { + if (tx > ax && ty > sMarginY1) { if (nextInLine.point.x === ax) return Directions.TOP; return Directions.LEFT; } - if (tx < smx0 && ty < smy1) return Directions.BOTTOM; - if (tx > smx1 && ty < smy1) return Directions.BOTTOM; - if (tx >= smx0 && tx <= ax && ty < sy0) { + if (tx < sMarginX0 && ty < sMarginY1) return Directions.BOTTOM; + if (tx > sMarginX1 && ty < sMarginY1) return Directions.BOTTOM; + if (tx >= sMarginX0 && tx <= ax && ty < sy0) { if (nextInLine.point.x < tx) { return Directions.RIGHT; } return Directions.LEFT; } - if (tx <= smx1 && tx >= ax && ty < sy0) { + if (tx <= sMarginX1 && tx >= ax && ty < sy0) { if (nextInLine.point.x < tx) { return Directions.RIGHT; } @@ -156,26 +156,26 @@ 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 sMarginX0 = sx0 - margin; + const sMarginY0 = sy0 - margin; + const sMarginY1 = 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 >= smx0 && ty < smy0) return Directions.LEFT; - if (tx >= smx0 && ty > smy1) return Directions.LEFT; - if (tx > sx1 && ty >= smy0 && ty <= ay) { + if (tx <= sMarginX0 && ty < ay) return Directions.BOTTOM; + if (tx <= sMarginX0 && ty > ay) return Directions.TOP; + if (tx >= sMarginX0 && ty < sMarginY0) return Directions.LEFT; + if (tx >= sMarginX0 && ty > sMarginY1) return Directions.LEFT; + if (tx > sx1 && ty >= sMarginY0 && ty <= ay) { if (nextInLine.point.y < ty) { return Directions.BOTTOM; } return Directions.TOP; } - if (tx > sx1 && ty <= smy1 && ty >= ay) { + if (tx > sx1 && ty <= sMarginY1 && ty >= ay) { if (nextInLine.point.y < ty) { return Directions.BOTTOM; } @@ -190,26 +190,26 @@ 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 sMarginX1 = sx1 + margin; + const sMarginY0 = sy0 - margin; + const sMarginY1 = 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 <= smx1 && ty < smy0) return Directions.RIGHT; - if (tx <= smx1 && ty > smy1) return Directions.RIGHT; - if (tx < sx0 && ty >= smy0 && ty <= ay) { + if (tx >= sMarginX1 && ty < ay) return Directions.BOTTOM; + if (tx >= sMarginX1 && ty > ay) return Directions.TOP; + if (tx <= sMarginX1 && ty < sMarginY0) return Directions.RIGHT; + if (tx <= sMarginX1 && ty > sMarginY1) return Directions.RIGHT; + if (tx < sx0 && ty >= sMarginY0 && ty <= ay) { if (nextInLine.point.y < ty) { return Directions.BOTTOM; } return Directions.TOP; } - if (tx < sx0 && ty <= smy1 && ty >= ay) { + if (tx < sx0 && ty <= sMarginY1 && ty >= ay) { if (nextInLine.point.y < ty) { return Directions.BOTTOM; } @@ -469,1024 +469,1375 @@ function moveAndExpandBBox(bbox, direction, margin) { } function routeBetweenPoints(source, target, opt = {}) { - const { point: sourcePoint, x0: sx0, y0: sy0, width: sourceWidth, height: sourceHeight, margin: sourceMargin } = source; - const { point: targetPoint, x0: tx0, y0: ty0, width: targetWidth, height: targetHeight, margin: targetMargin } = target; + const { + point: sourcePoint, + x0: sBoxX0, + y0: sBoxY0, + width: sourceWidth, + height: sourceHeight, + margin: sourceMargin + } = source; + const { + point: targetPoint, + x0: tBoxX0, + y0: tBoxY0, + width: targetWidth, + height: targetHeight, + margin: targetMargin + } = target; + const { targetInSourceBBox = false } = opt; - const tx1 = tx0 + targetWidth; - const ty1 = ty0 + targetHeight; - const sx1 = sx0 + sourceWidth; - const sy1 = sy0 + sourceHeight; + const minSourceMargin = opt.minPathMargin != null ? Math.min(opt.minPathMargin, sourceMargin) : sourceMargin; + const minTargetMargin = opt.minPathMargin != null ? Math.min(opt.minPathMargin, targetMargin) : targetMargin; + + const tBoxX1 = tBoxX0 + targetWidth; + const tBoxY1 = tBoxY0 + targetHeight; + const sBoxX1 = sBoxX0 + sourceWidth; + const sBoxY1 = sBoxY0 + sourceHeight; - // Key coordinates including the margin - const smx0 = sx0 - sourceMargin; - const smx1 = sx1 + sourceMargin; - const smy0 = sy0 - sourceMargin; - const smy1 = sy1 + sourceMargin; + const sMarginX0 = sBoxX0 - sourceMargin; + const sMarginX1 = sBoxX1 + sourceMargin; + const sMarginY0 = sBoxY0 - sourceMargin; + const sMarginY1 = sBoxY1 + sourceMargin; - const tmx0 = tx0 - targetMargin; - const tmx1 = tx1 + targetMargin; - const tmy0 = ty0 - targetMargin; - const tmy1 = ty1 + targetMargin; + const tMarginX0 = tBoxX0 - targetMargin; + const tMarginX1 = tBoxX1 + targetMargin; + const tMarginY0 = tBoxY0 - targetMargin; + const tMarginY1 = tBoxY1 + targetMargin; + + const sMinMarginX0 = sBoxX0 - minSourceMargin; + const sMinMarginX1 = sBoxX1 + minSourceMargin; + const tMinMarginX0 = tBoxX0 - minTargetMargin; + const tMinMarginX1 = tBoxX1 + minTargetMargin; + + const sMinMarginY0 = sBoxY0 - minSourceMargin; + const sMinMarginY1 = sBoxY1 + minSourceMargin; + const tMinMarginY0 = tBoxY0 - minTargetMargin; + const tMinMarginY1 = tBoxY1 + minTargetMargin; 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 sourceOffsetPoint = getOutsidePoint(sourceSide, { point: sourcePoint, x0: sBoxX0, y0: sBoxY0, width: sourceWidth, height: sourceHeight }, sourceMargin); + const targetOffsetPoint = getOutsidePoint(targetSide, { point: targetPoint, x0: tBoxX0, y0: tBoxY0, width: targetWidth, height: targetHeight }, targetMargin); - const { x: sox, y: soy } = sourceOutsidePoint; - const { x: tox, y: toy } = targetOutsidePoint; - const tcx = (tx0 + tx1) / 2; - const tcy = (ty0 + ty1) / 2; - const scx = (sx0 + sx1) / 2; - const scy = (sy0 + sy1) / 2; - const middleOfVerticalSides = (scx < tcx ? (sx1 + tx0) : (tx1 + sx0)) / 2; - const middleOfHorizontalSides = (scy < tcy ? (sy1 + ty0) : (ty1 + sy0)) / 2; + const { x: sOffsetX, y: sOffsetY } = sourceOffsetPoint; + const { x: tOffsetX, y: tOffsetY } = targetOffsetPoint; + const tCenterX = (tBoxX0 + tBoxX1) / 2; + const tCenterY = (tBoxY0 + tBoxY1) / 2; + const sCenterX = (sBoxX0 + sBoxX1) / 2; + const sCenterY = (sBoxY0 + sBoxY1) / 2; + const middleOfVerticalSides = (sCenterX < tCenterX ? (sBoxX1 + tBoxX0) : (tBoxX1 + sBoxX0)) / 2; + const middleOfHorizontalSides = (sCenterY < tCenterY ? (sBoxY1 + tBoxY0) : (tBoxY1 + sBoxY0)) / 2; - const sourceBBox = new g.Rect(sx0, sy0, sourceWidth, sourceHeight); - const targetBBox = new g.Rect(tx0, ty0, targetWidth, targetHeight); + const sourceBBox = new g.Rect(sBoxX0, sBoxY0, sourceWidth, sourceHeight); + const targetBBox = new g.Rect(tBoxX0, tBoxY0, targetWidth, targetHeight); const inflatedSourceBBox = sourceBBox.clone().inflate(sourceMargin); const inflatedTargetBBox = targetBBox.clone().inflate(targetMargin); - const sourceForDistance = Object.assign({}, source, { x1: sx1, y1: sy1, outsidePoint: sourceOutsidePoint, direction: sourceSide }); - const targetForDistance = Object.assign({}, target, { x1: tx1, y1: ty1, outsidePoint: targetOutsidePoint, direction: targetSide }); + const sourceForDistance = Object.assign({}, source, { x1: sBoxX1, y1: sBoxY1, outsidePoint: sourceOffsetPoint, direction: sourceSide }); + const targetForDistance = Object.assign({}, target, { x1: tBoxX1, y1: tBoxY1, outsidePoint: targetOffsetPoint, direction: targetSide }); // Distances used to determine the shortest route along the connections on horizontal sides for // bottom => bottom // top => bottom // bottom => top // top => top - const [leftD, rightD] = getHorizontalDistance(sourceForDistance, targetForDistance); + const [leftDistance, rightDistance] = getHorizontalDistance(sourceForDistance, targetForDistance); // Distances used to determine the shortest route along the connection on vertical sides for // left => left // left => right // right => right // right => left - const [topD, bottomD] = getVerticalDistance(sourceForDistance, targetForDistance); + const [topDistance, bottomDistance] = getVerticalDistance(sourceForDistance, targetForDistance); // All possible combinations of source and target sides if (sourceSide === 'left' && targetSide === 'right') { - const isPointInsideSource = inflatedSourceBBox.containsPoint(targetOutsidePoint); - const isPointInsideTarget = inflatedTargetBBox.containsPoint(sourceOutsidePoint); + const isPointInsideSource = inflatedSourceBBox.containsPoint(targetOffsetPoint); + const isPointInsideTarget = inflatedTargetBBox.containsPoint(sourceOffsetPoint); // Use S-shaped connection if (isPointInsideSource || isPointInsideTarget) { - const middleOfAnchors = (soy + toy) / 2; + const middleY = (sOffsetY + tOffsetY) / 2; - return [ - { x: sox, y: soy }, - { x: sox, y: middleOfAnchors }, - { x: tox, y: middleOfAnchors }, - { x: tox, y: toy } - ]; + if (sOffsetX < tMinMarginX1) { + return [ + { x: sOffsetX, y: sOffsetY }, + { x: sOffsetX, y: middleY }, + { x: tOffsetX, y: middleY }, + { x: tOffsetX, y: tOffsetY } + ]; + } else { + const middleX = (sOffsetX + tOffsetX) / 2; + return [ + { x: middleX, y: sOffsetY }, + { x: middleX, y: middleY }, + { x: middleX, y: middleY }, + { x: middleX, y: tOffsetY } + ]; + } } - if (smx0 < tox) { + if (sOffsetX < tOffsetX) { let y = middleOfHorizontalSides; - let x1 = sox; - let x2 = tox; + let x1 = sOffsetX; + let x2 = tOffsetX; - const isUpwardsShorter = topD < bottomD; + const isUpwardsShorter = topDistance < bottomDistance; // If the source and target elements overlap, we need to make sure the connection // goes around the target element. - if ((y >= smy0 && y <= smy1) || (y >= tmy0 && y <= tmy1)) { - if (smy1 >= tmy0 && isUpwardsShorter) { - y = Math.min(tmy0, smy0); - } else if (smy0 <= tmy1 && !isUpwardsShorter) { - y = Math.max(tmy1, smy1); + if (((y >= sMinMarginY0 && y <= sMinMarginY1) || (y >= tMinMarginY0 && y <= tMinMarginY1))) { + + if (sMinMarginX0 > tMinMarginX1) { + const middleY = (sOffsetY + tOffsetY) / 2; + return [ + { x: sOffsetX, y: sOffsetY }, + { x: sOffsetX, y: middleY }, + { x: tOffsetX, y: middleY }, + { x: tOffsetX, y: tOffsetY } + ]; + } + + if (sMinMarginY1 >= tMinMarginY0 && isUpwardsShorter) { + y = Math.min(tMarginY0, sMarginY0); + } else if (sMinMarginY0 <= tMinMarginY1 && !isUpwardsShorter) { + y = Math.max(tMarginY1, sMarginY1); } // This handles the case when the source and target elements overlap as well as // the case when the source is to the left of the target element. - x1 = Math.min(sox, tmx0); - x2 = Math.max(tox, smx1); + x1 = Math.min(sOffsetX, tBoxX0 - targetMargin); + x2 = Math.max(tOffsetX, sBoxX1 + sourceMargin); // This is an edge case when the source and target intersect and - if ((isUpwardsShorter && soy < ty0) || (!isUpwardsShorter && soy > ty1)) { + if ((isUpwardsShorter && sOffsetY < tBoxY0) || (!isUpwardsShorter && sOffsetY > tBoxY1)) { // the path should no longer rely on minimal x boundary in `x1` - x1 = sox; - } else if ((isUpwardsShorter && toy < sy0) || (!isUpwardsShorter && toy > sy1)) { + x1 = sOffsetX; + } else if ((isUpwardsShorter && tOffsetY < sBoxY0) || (!isUpwardsShorter && tOffsetY > sBoxY1)) { // the path should no longer rely on maximal x boundary in `x2` - x2 = tox; + x2 = tOffsetX; } } return [ - { x: x1, y: soy }, + { x: x1, y: sOffsetY }, { x: x1, y }, { x: x2, y }, - { x: x2, y: toy } + { x: x2, y: tOffsetY } ]; } - const x = (sox + tox) / 2; + const x = (sOffsetX + tOffsetX) / 2; return [ - { x, y: soy }, - { x, y: toy }, + { x, y: sOffsetY }, + { x, y: tOffsetY }, ]; } else if (sourceSide === 'right' && targetSide === 'left') { - const isPointInsideSource = inflatedSourceBBox.containsPoint(targetOutsidePoint); - const isPointInsideTarget = inflatedTargetBBox.containsPoint(sourceOutsidePoint); + const isPointInsideSource = inflatedSourceBBox.containsPoint(targetOffsetPoint); + const isPointInsideTarget = inflatedTargetBBox.containsPoint(sourceOffsetPoint); // Use S-shaped connection if (isPointInsideSource || isPointInsideTarget) { - const middleOfAnchors = (soy + toy) / 2; + const middleY = (sOffsetY + tOffsetY) / 2; - return [ - { x: sox, y: soy }, - { x: sox, y: middleOfAnchors }, - { x: tox, y: middleOfAnchors }, - { x: tox, y: toy } - ]; + if (sOffsetX > tMinMarginX0) { + return [ + { x: sOffsetX, y: sOffsetY }, + { x: sOffsetX, y: middleY }, + { x: tOffsetX, y: middleY }, + { x: tOffsetX, y: tOffsetY } + ]; + } else { + const middleX = (sOffsetX + tOffsetX) / 2; + return [ + { x: middleX, y: sOffsetY }, + { x: middleX, y: middleY }, + { x: middleX, y: middleY }, + { x: middleX, y: tOffsetY } + ]; + } } - if (smx1 > tox) { + if (sOffsetX > tOffsetX) { let y = middleOfHorizontalSides; - let x1 = sox; - let x2 = tox; + let x1 = sOffsetX; + let x2 = tOffsetX; - const isUpwardsShorter = topD < bottomD; + const isUpwardsShorter = topDistance < bottomDistance; // If the source and target elements overlap, we need to make sure the connection // goes around the target element. - if ((y >= smy0 && y <= smy1) || (y >= tmy0 && y <= tmy1)) { - if (smy1 >= tmy0 && isUpwardsShorter) { - y = Math.min(tmy0, smy0); - } else if (smy0 <= tmy1 && !isUpwardsShorter) { - y = Math.max(tmy1, smy1); + if ((y >= sMinMarginY0 && y <= sMinMarginY1) || (y >= tMinMarginY0 && y <= tMinMarginY1)) { + if (sMinMarginX1 < tMinMarginX0) { + const middleY = (sOffsetY + tOffsetY) / 2; + return [ + { x: sOffsetX, y: sOffsetY }, + { x: sOffsetX, y: middleY }, + { x: tOffsetX, y: middleY }, + { x: tOffsetX, y: tOffsetY } + ]; + } + + if (sMinMarginY1 >= tMinMarginY0 && isUpwardsShorter) { + y = Math.min(tMarginY0, sMarginY0); + } else if (sMinMarginY0 <= tMinMarginY1 && !isUpwardsShorter) { + y = Math.max(tMarginY1, sMarginY1); } // This handles the case when the source and target elements overlap as well as // the case when the source is to the left of the target element. - x1 = Math.max(sox, tmx1); - x2 = Math.min(tox, smx0); + x1 = Math.max(sOffsetX, tBoxX1 + targetMargin); + x2 = Math.min(tOffsetX, sBoxX0 - sourceMargin); // This is an edge case when the source and target intersect and - if ((isUpwardsShorter && soy < ty0) || (!isUpwardsShorter && soy > ty1)) { + if ((isUpwardsShorter && sOffsetY < tBoxY0) || (!isUpwardsShorter && sOffsetY > tBoxY1)) { // the path should no longer rely on maximal x boundary in `x1` - x1 = sox; - } else if ((isUpwardsShorter && toy < sy0) || (!isUpwardsShorter && toy > sy1)) { + x1 = sOffsetX; + } else if ((isUpwardsShorter && tOffsetY < sBoxY0) || (!isUpwardsShorter && tOffsetY > sBoxY1)) { // the path should no longer rely on minimal x boundary in `x2` - x2 = tox; + x2 = tOffsetX; } } return [ - { x: x1, y: soy }, + { x: x1, y: sOffsetY }, { x: x1, y }, { x: x2, y }, - { x: x2, y: toy } + { x: x2, y: tOffsetY } ]; } - const x = (sox + tox) / 2; + const x = (sOffsetX + tOffsetX) / 2; return [ - { x, y: soy }, - { x, y: toy } + { x, y: sOffsetY }, + { x, y: tOffsetY } ]; } else if (sourceSide === 'top' && targetSide === 'bottom') { - const isPointInsideSource = inflatedSourceBBox.containsPoint(targetOutsidePoint); - const isPointInsideTarget = inflatedTargetBBox.containsPoint(sourceOutsidePoint); + const isPointInsideSource = inflatedSourceBBox.containsPoint(targetOffsetPoint); + const isPointInsideTarget = inflatedTargetBBox.containsPoint(sourceOffsetPoint); // Use S-shaped connection if (isPointInsideSource || isPointInsideTarget) { - const middleOfAnchors = (sox + tox) / 2; + const middleX = (sOffsetX + tOffsetX) / 2; - return [ - { x: sox, y: soy }, - { x: middleOfAnchors, y: soy }, - { x: middleOfAnchors, y: toy }, - { x: tox, y: toy } - ]; + if (sOffsetY < tMinMarginY1) { + return [ + { x: sOffsetX, y: sOffsetY }, + { x: middleX, y: sOffsetY }, + { x: middleX, y: tOffsetY }, + { x: tOffsetX, y: tOffsetY } + ]; + } else { + const middleY = (sOffsetY + tOffsetY) / 2; + return [ + { x: sOffsetX, y: middleY }, + { x: middleX, y: middleY }, + { x: middleX, y: middleY }, + { x: tOffsetX, y: middleY } + ]; + } } - if (smy0 < toy) { + if (sMarginY0 < tOffsetY) { let x = middleOfVerticalSides; - let y1 = soy; - let y2 = toy; + let y1 = sOffsetY; + let y2 = tOffsetY; - const isLeftShorter = leftD < rightD; + const isLeftShorter = leftDistance < rightDistance; // If the source and target elements overlap, we need to make sure the connection // goes around the target element. - if ((x >= smx0 && x <= smx1) || (x >= tmx0 && x <= tmx1)) { - if (smx1 >= tmx0 && isLeftShorter) { - x = Math.min(tmx0, smx0); - } else if (smx0 <= tmx1 && !isLeftShorter) { - x = Math.max(tmx1, smx1); + if ((x >= sMinMarginX0 && x <= sMinMarginX1) || (x >= tMinMarginX0 && x <= tMinMarginX1)) { + if (sMinMarginY0 > tMinMarginY1) { + const middleX = (sOffsetX + tOffsetX) / 2; + return [ + { x: sOffsetX, y: sOffsetY }, + { x: middleX, y: sOffsetY }, + { x: middleX, y: tOffsetY }, + { x: tOffsetX, y: tOffsetY } + ]; + } + + if (sMinMarginX1 >= tMinMarginX0 && isLeftShorter) { + x = Math.min(tMarginX0, sMarginX0); + } else if (sMinMarginX0 <= tMinMarginX1 && !isLeftShorter) { + x = Math.max(tMarginX1, sMarginX1); } // This handles the case when the source and target elements overlap as well as // the case when the source is to the left of the target element. - y1 = Math.min(soy, tmy0); - y2 = Math.max(toy, smy1); + y1 = Math.min(sOffsetY, tBoxY0 - targetMargin); + y2 = Math.max(tOffsetY, sBoxY1 + sourceMargin); // This is an edge case when the source and target intersect and - if ((isLeftShorter && sox < tx0) || (!isLeftShorter && sox > tx1)) { + if ((isLeftShorter && sOffsetX < tBoxX0) || (!isLeftShorter && sOffsetX > tBoxX1)) { // the path should no longer rely on minimal y boundary in `y1` - y1 = soy; - } else if ((isLeftShorter && tox < sx0) || (!isLeftShorter && tox > sx1)) { + y1 = sOffsetY; + } else if ((isLeftShorter && tOffsetX < sBoxX0) || (!isLeftShorter && tOffsetX > sBoxX1)) { // the path should no longer rely on maximal y boundary in `y2` - y2 = toy; + y2 = tOffsetY; } } return [ - { x: sox, y: y1 }, + { x: sOffsetX, y: y1 }, { x, y: y1 }, { x, y: y2 }, - { x: tox, y: y2 } + { x: tOffsetX, y: y2 } ]; } - const y = (soy + toy) / 2; + const y = (sOffsetY + tOffsetY) / 2; return [ - { x: sox, y }, - { x: tox, y } + { x: sOffsetX, y }, + { x: tOffsetX, y } ]; } else if (sourceSide === 'bottom' && targetSide === 'top') { - const isPointInsideSource = inflatedSourceBBox.containsPoint(targetOutsidePoint); - const isPointInsideTarget = inflatedTargetBBox.containsPoint(sourceOutsidePoint); + const isPointInsideSource = inflatedSourceBBox.containsPoint(targetOffsetPoint); + const isPointInsideTarget = inflatedTargetBBox.containsPoint(sourceOffsetPoint); // Use S-shaped connection if (isPointInsideSource || isPointInsideTarget) { - const middleOfAnchors = (sox + tox) / 2; + const middleX = (sOffsetX + tOffsetX) / 2; - return [ - { x: sox, y: soy }, - { x: middleOfAnchors, y: soy }, - { x: middleOfAnchors, y: toy }, - { x: tox, y: toy } - ]; + if (sOffsetY > tMinMarginY0) { + return [ + { x: sOffsetX, y: sOffsetY }, + { x: middleX, y: sOffsetY }, + { x: middleX, y: tOffsetY }, + { x: tOffsetX, y: tOffsetY } + ]; + } else { + const middleY = (sOffsetY + tOffsetY) / 2; + return [ + { x: sOffsetX, y: middleY }, + { x: middleX, y: middleY }, + { x: middleX, y: middleY }, + { x: tOffsetX, y: middleY } + ]; + } } - if (smy1 > toy) { + if (sMarginY1 > tOffsetY) { let x = middleOfVerticalSides; - let y1 = soy; - let y2 = toy; + let y1 = sOffsetY; + let y2 = tOffsetY; - const isLeftShorter = leftD < rightD; + const isLeftShorter = leftDistance < rightDistance; // If the source and target elements overlap, we need to make sure the connection // goes around the target element. - if ((x >= smx0 && x <= smx1) || (x >= tmx0 && x <= tmx1)) { - if (smx1 >= tmx0 && isLeftShorter) { - x = Math.min(tmx0, smx0); - } else if (smx0 <= tmx1 && !isLeftShorter) { - x = Math.max(tmx1, smx1); + if ((x >= sMinMarginX0 && x <= sMinMarginX1) || (x >= tMinMarginX0 && x <= tMinMarginX1)) { + if (sMinMarginY1 < tMinMarginY0) { + const middleX = (sOffsetX + tOffsetX) / 2; + return [ + { x: sOffsetX, y: sOffsetY }, + { x: middleX, y: sOffsetY }, + { x: middleX, y: tOffsetY }, + { x: tOffsetX, y: tOffsetY } + ]; + } + + if (sMinMarginX1 >= tMinMarginX0 && isLeftShorter) { + x = Math.min(tMarginX0, sMarginX0); + } else if (sMinMarginX0 <= tMinMarginX1 && !isLeftShorter) { + x = Math.max(tMarginX1, sMarginX1); } // This handles the case when the source and target elements overlap as well as // the case when the source is to the left of the target element. - y1 = Math.max(soy, tmy1); - y2 = Math.min(toy, smy0); + y1 = Math.max(sOffsetY, tBoxY1 + targetMargin); + y2 = Math.min(tOffsetY, sBoxY0 - sourceMargin); // This is an edge case when the source and target intersect and - if ((isLeftShorter && sox < tx0) || (!isLeftShorter && sox > tx1)) { + if ((isLeftShorter && sOffsetX < tBoxX0) || (!isLeftShorter && sOffsetX > tBoxX1)) { // the path should no longer rely on maximal y boundary in `y1` - y1 = soy; - } else if ((isLeftShorter && tox < sx0) || (!isLeftShorter && tox > sx1)) { + y1 = sOffsetY; + } else if ((isLeftShorter && tOffsetX < sBoxX0) || (!isLeftShorter && tOffsetX > sBoxX1)) { // the path should no longer rely on minimal y boundary in `y2` - y2 = toy; + y2 = tOffsetY; } } return [ - { x: sox, y: y1 }, + { x: sOffsetX, y: y1 }, { x, y: y1 }, { x, y: y2 }, - { x: tox, y: y2 } + { x: tOffsetX, y: y2 } ]; } - const y = (soy + toy) / 2; + const y = (sOffsetY + tOffsetY) / 2; return [ - { x: sox, y }, - { x: tox, y } + { x: sOffsetX, y }, + { x: tOffsetX, y } ]; } else if (sourceSide === 'top' && targetSide === 'top') { const useUShapeConnection = targetInSourceBBox || g.intersection.rectWithRect(inflatedSourceBBox, targetBBox) || - (soy <= ty0 && (inflatedSourceBBox.bottomRight().x <= tox || inflatedSourceBBox.bottomLeft().x >= tox)) || - (soy >= ty0 && (inflatedTargetBBox.bottomRight().x <= sox || inflatedTargetBBox.bottomLeft().x >= sox)); + (sOffsetY <= tBoxY0 && (inflatedSourceBBox.bottomRight().x <= tOffsetX || inflatedSourceBBox.bottomLeft().x >= tOffsetX)) || + (sOffsetY >= tBoxY0 && (inflatedTargetBBox.bottomRight().x <= sOffsetX || inflatedTargetBBox.bottomLeft().x >= sOffsetX)); // U-shape connection is a straight line if `sox` and `tox` are the same - if (useUShapeConnection && sox !== tox) { + if (useUShapeConnection && sOffsetX !== tOffsetX) { return [ - { x: sox, y: Math.min(soy, toy) }, - { x: tox, y: Math.min(soy, toy) } + { x: sOffsetX, y: Math.min(sOffsetY, tOffsetY) }, + { x: tOffsetX, y: Math.min(sOffsetY, tOffsetY) } ]; } let x; - const y1 = Math.min((sy1 + ty0) / 2, toy); - const y2 = Math.min((sy0 + ty1) / 2, soy); + const y1 = Math.min((sBoxY1 + tBoxY0) / 2, tOffsetY); + const y2 = Math.min((sBoxY0 + tBoxY1) / 2, sOffsetY); - if (toy < soy) { + if (tOffsetY < sOffsetY) { // Use the shortest path along the connections on horizontal sides - if (rightD > leftD) { - x = Math.min(sox, tmx0); + if (rightDistance > leftDistance) { + x = Math.min(sOffsetX, tMarginX0); } else { - x = Math.max(sox, tmx1); + x = Math.max(sOffsetX, tMarginX1); } } else { - if (rightD > leftD) { - x = Math.min(tox, smx0); + if (rightDistance > leftDistance) { + x = Math.min(tOffsetX, sMarginX0); } else { - x = Math.max(tox, smx1); + x = Math.max(tOffsetX, sMarginX1); } } return [ - { x: sox, y: y2 }, + { x: sOffsetX, y: y2 }, { x, y: y2 }, { x, y: y1 }, - { x: tox, y: y1 } + { x: tOffsetX, y: y1 } ]; } else if (sourceSide === 'bottom' && targetSide === 'bottom') { const useUShapeConnection = targetInSourceBBox || g.intersection.rectWithRect(inflatedSourceBBox, targetBBox) || - (soy >= toy && (inflatedSourceBBox.topRight().x <= tox || inflatedSourceBBox.topLeft().x >= tox)) || - (soy <= toy && (inflatedTargetBBox.topRight().x <= sox || inflatedTargetBBox.topLeft().x >= sox)); + (sOffsetY >= tOffsetY && (inflatedSourceBBox.topRight().x <= tOffsetX || inflatedSourceBBox.topLeft().x >= tOffsetX)) || + (sOffsetY <= tOffsetY && (inflatedTargetBBox.topRight().x <= sOffsetX || inflatedTargetBBox.topLeft().x >= sOffsetX)); // U-shape connection is a straight line if `sox` and `tox` are the same - if (useUShapeConnection && sox !== tox) { + if (useUShapeConnection && sOffsetX !== tOffsetX) { return [ - { x: sox, y: Math.max(soy, toy) }, - { x: tox, y: Math.max(soy, toy) } + { x: sOffsetX, y: Math.max(sOffsetY, tOffsetY) }, + { x: tOffsetX, y: Math.max(sOffsetY, tOffsetY) } ]; } let x; - const y1 = Math.max((sy0 + ty1) / 2, toy); - const y2 = Math.max((sy1 + ty0) / 2, soy); + const y1 = Math.max((sBoxY0 + tBoxY1) / 2, tOffsetY); + const y2 = Math.max((sBoxY1 + tBoxY0) / 2, sOffsetY); - if (toy > soy) { + if (tOffsetY > sOffsetY) { // Use the shortest path along the connections on horizontal sides - if (rightD > leftD) { - x = Math.min(sox, tmx0); + if (rightDistance > leftDistance) { + x = Math.min(sOffsetX, tMarginX0); } else { - x = Math.max(sox, tmx1); + x = Math.max(sOffsetX, tMarginX1); } } else { - if (rightD > leftD) { - x = Math.min(tox, smx0); + if (rightDistance > leftDistance) { + x = Math.min(tOffsetX, sMarginX0); } else { - x = Math.max(tox, smx1); + x = Math.max(tOffsetX, sMarginX1); } } return [ - { x: sox, y: y2 }, + { x: sOffsetX, y: y2 }, { x, y: y2 }, { x, y: y1 }, - { x: tox, y: y1 } + { x: tOffsetX, y: y1 } ]; } else if (sourceSide === 'left' && targetSide === 'left') { const useUShapeConnection = targetInSourceBBox || g.intersection.rectWithRect(inflatedSourceBBox, targetBBox) || - (sox <= tox && (inflatedSourceBBox.bottomRight().y <= toy || inflatedSourceBBox.topRight().y >= toy)) || - (sox >= tox && (inflatedTargetBBox.bottomRight().y <= soy || inflatedTargetBBox.topRight().y >= soy)); + (sOffsetX <= tOffsetX && (inflatedSourceBBox.bottomRight().y <= tOffsetY || inflatedSourceBBox.topRight().y >= tOffsetY)) || + (sOffsetX >= tOffsetX && (inflatedTargetBBox.bottomRight().y <= sOffsetY || inflatedTargetBBox.topRight().y >= sOffsetY)); // U-shape connection is a straight line if `soy` and `toy` are the same - if (useUShapeConnection && soy !== toy) { + if (useUShapeConnection && sOffsetY !== tOffsetY) { return [ - { x: Math.min(sox, tox), y: soy }, - { x: Math.min(sox, tox), y: toy } + { x: Math.min(sOffsetX, tOffsetX), y: sOffsetY }, + { x: Math.min(sOffsetX, tOffsetX), y: tOffsetY } ]; } let y; - const x1 = Math.min((sx1 + tx0) / 2, tox); - const x2 = Math.min((sx0 + tx1) / 2, sox); + const x1 = Math.min((sBoxX1 + tBoxX0) / 2, tOffsetX); + const x2 = Math.min((sBoxX0 + tBoxX1) / 2, sOffsetX); - if (tox > sox) { - if (topD <= bottomD) { - y = Math.min(smy0, toy); + if (tOffsetX > sOffsetX) { + if (topDistance <= bottomDistance) { + y = Math.min(sMarginY0, tOffsetY); } else { - y = Math.max(smy1, toy); + y = Math.max(sMarginY1, tOffsetY); } } else { - if (topD <= bottomD) { - y = Math.min(tmy0, soy); + if (topDistance <= bottomDistance) { + y = Math.min(tMarginY0, sOffsetY); } else { - y = Math.max(tmy1, soy); + y = Math.max(tMarginY1, sOffsetY); } } return [ - { x: x2, y: soy }, + { x: x2, y: sOffsetY }, { x: x2, y }, { x: x1, y }, - { x: x1, y: toy } + { x: x1, y: tOffsetY } ]; } else if (sourceSide === 'right' && targetSide === 'right') { const useUShapeConnection = targetInSourceBBox || g.intersection.rectWithRect(inflatedSourceBBox, targetBBox) || - (sox >= tox && (inflatedSourceBBox.bottomLeft().y <= toy || inflatedSourceBBox.topLeft().y >= toy)) || - (sox <= tox && (inflatedTargetBBox.bottomLeft().y <= soy || inflatedTargetBBox.topLeft().y >= soy)); + (sOffsetX >= tOffsetX && (inflatedSourceBBox.bottomLeft().y <= tOffsetY || inflatedSourceBBox.topLeft().y >= tOffsetY)) || + (sOffsetX <= tOffsetX && (inflatedTargetBBox.bottomLeft().y <= sOffsetY || inflatedTargetBBox.topLeft().y >= sOffsetY)); // U-shape connection is a straight line if `soy` and `toy` are the same - if (useUShapeConnection && soy !== toy) { + if (useUShapeConnection && sOffsetY !== tOffsetY) { return [ - { x: Math.max(sox, tox), y: soy }, - { x: Math.max(sox, tox), y: toy } + { x: Math.max(sOffsetX, tOffsetX), y: sOffsetY }, + { x: Math.max(sOffsetX, tOffsetX), y: tOffsetY } ]; } let y; - const x1 = Math.max((sx0 + tx1) / 2, tox); - const x2 = Math.max((sx1 + tx0) / 2, sox); + const x1 = Math.max((sBoxX0 + tBoxX1) / 2, tOffsetX); + const x2 = Math.max((sBoxX1 + tBoxX0) / 2, sOffsetX); - if (tox <= sox) { - if (topD <= bottomD) { - y = Math.min(smy0, toy); + if (tOffsetX <= sOffsetX) { + if (topDistance <= bottomDistance) { + y = Math.min(sMarginY0, tOffsetY); } else { - y = Math.max(smy1, toy); + y = Math.max(sMarginY1, tOffsetY); } } else { - if (topD <= bottomD) { - y = Math.min(tmy0, soy); + if (topDistance <= bottomDistance) { + y = Math.min(tMarginY0, sOffsetY); } else { - y = Math.max(tmy1, soy); + y = Math.max(tMarginY1, sOffsetY); } } return [ - { x: x2, y: soy }, + { x: x2, y: sOffsetY }, { x: x2, y }, { x: x1, y }, - { x: x1, y: toy } + { x: x1, y: tOffsetY } ]; } else if (sourceSide === 'top' && targetSide === 'right') { const isPointInsideSource = inflatedSourceBBox.containsPoint(targetPoint); // The target point is inside the source element if (isPointInsideSource) { - if (sox <= tmx1) { - const x = Math.max(sox + sourceMargin, tox); - const y = Math.min(smy0, tmy0); + if (sOffsetX <= tOffsetX - sourceMargin) { + const x = Math.max(sMarginX1, tOffsetX); + const y = Math.min(sMarginY0, tMarginY0); // Target anchor is on the right side of the source anchor return [ - { x: sox, y }, + { x: sOffsetX, y }, { x: x, y }, - { x: x, y: toy } + { x: x, y: tOffsetY } ]; } // Target anchor is on the left side of the source anchor // Subtract the `sourceMargin` since the source anchor is on the right side of the target anchor - const anchorMiddleX = (sox - sourceMargin + tox) / 2; + const anchorMiddleX = (sOffsetX + tOffsetX) / 2; return [ - { x: sox, y: soy }, - { x: anchorMiddleX, y: soy }, - { x: anchorMiddleX, y: toy } + { x: sOffsetX, y: sOffsetY }, + { x: anchorMiddleX, y: sOffsetY }, + { x: anchorMiddleX, y: tOffsetY } ]; } - if (smy0 > toy) { - if (sox < tox) { - let y = tmy0; + if (sMarginY0 > tOffsetY) { + if (sOffsetX < tOffsetX) { + let y = tMarginY0; - if (tmy1 <= smy0 && tmx1 >= sox) { + if (tMinMarginY1 <= sMinMarginY0 && tMarginX1 >= sOffsetX) { y = middleOfHorizontalSides; + + if (sOffsetY < tMinMarginY1) { + + if (sOffsetX + sourceMargin > tBoxX1) { + + return [ + { x: sOffsetX, y: sOffsetY }, + { x: tOffsetX, y: sOffsetY }, + { x: tOffsetX, y: tOffsetY } + ]; + } + + return [ + { x: sOffsetX, y: sOffsetY }, + { x: sOffsetX + sourceMargin, y: sOffsetY }, + { x: sOffsetX + sourceMargin, y }, + { x: tOffsetX, y }, + { x: tOffsetX, y: tOffsetY } + ]; + } } return [ - { x: sox, y }, - { x: tox, y }, - { x: tox, y: toy } + { x: sOffsetX, y }, + { x: tOffsetX, y }, + { x: tOffsetX, y: tOffsetY } ]; } - return [{ x: sox, y: toy }]; + return [{ x: sOffsetX, y: tOffsetY }]; } - const x = Math.max(middleOfVerticalSides, tmx1); + const x = Math.max(middleOfVerticalSides, tMinMarginX1); - if (sox > tox && sy1 >= toy) { + if (sOffsetX > tOffsetX && sBoxY1 >= tOffsetY) { return [ - { x: sox, y: soy }, - { x, y: soy }, - { x, y: toy } + { x: sOffsetX, y: sOffsetY }, + { x, y: sOffsetY }, + { x, y: tOffsetY } ]; } - if (x > smx0 && soy < ty1) { - const y = Math.min(smy0, tmy0); - const x = Math.max(smx1, tmx1); + if (x > sMinMarginX0 && sOffsetY < tBoxY1) { + const y = Math.min(sMarginY0, tMarginY0); + const x = Math.max(sMarginX1, tMarginX1); return [ - { x: sox, y }, + { x: sOffsetX, y }, { x, y }, - { x, y: toy } + { x, y: tOffsetY } + ]; + } + + if (tOffsetX > sMinMarginX0) { + return [ + { x: sOffsetX, y: sOffsetY }, + { x, y: sOffsetY }, + { x, y: tOffsetY - targetMargin }, + { x: tOffsetX, y: tOffsetY - targetMargin }, + { x: tOffsetX, y: tOffsetY } ]; } return [ - { x: sox, y: soy }, - { x, y: soy }, - { x, y: toy } + { x: sOffsetX, y: sOffsetY }, + { x, y: sOffsetY }, + { x, y: tOffsetY } ]; } else if (sourceSide === 'top' && targetSide === 'left') { const isPointInsideSource = inflatedSourceBBox.containsPoint(targetPoint); // The target point is inside the source element if (isPointInsideSource) { - if (sox >= tmx0) { - const x = Math.min(sox - sourceMargin, tox); - const y = Math.min(smy0, tmy0); + if (sOffsetX >= tOffsetX + sourceMargin) { + const x = Math.min(sMarginX0, tOffsetX); + const y = Math.min(sMarginY0, tMarginY0); // Target anchor is on the left side of the source anchor return [ - { x: sox, y }, + { x: sOffsetX, y }, { x: x, y }, - { x: x, y: toy } + { x: x, y: tOffsetY } ]; } // Target anchor is on the right side of the source anchor // Add the `sourceMargin` since the source anchor is on the left side of the target anchor - const anchorMiddleX = (sox + sourceMargin + tox) / 2; + const anchorMiddleX = (sOffsetX + tOffsetX) / 2; return [ - { x: sox, y: soy }, - { x: anchorMiddleX, y: soy }, - { x: anchorMiddleX, y: toy } + { x: sOffsetX, y: sOffsetY }, + { x: anchorMiddleX, y: sOffsetY }, + { x: anchorMiddleX, y: tOffsetY } ]; } - if (smy0 > toy) { - if (sox > tox) { - let y = tmy0; + if (sMarginY0 > tOffsetY) { + if (sOffsetX > tOffsetX) { + let y = tMarginY0; - if (tmy1 <= smy0 && tmx0 <= sox) { + if (tMinMarginY1 <= sMinMarginY0 && tMarginX0 <= sOffsetX) { y = middleOfHorizontalSides; + + if (sOffsetY < tMinMarginY1) { + + if (sOffsetX - sourceMargin < tBoxX0) { + + return [ + { x: sOffsetX, y: sOffsetY }, + { x: tOffsetX, y: sOffsetY }, + { x: tOffsetX, y: tOffsetY } + ]; + } + + return [ + { x: sOffsetX, y: sOffsetY }, + { x: sOffsetX - sourceMargin, y: sOffsetY }, + { x: sOffsetX - sourceMargin, y }, + { x: tOffsetX, y }, + { x: tOffsetX, y: tOffsetY } + ]; + } } return [ - { x: sox, y }, - { x: tox, y }, - { x: tox, y: toy } + { x: sOffsetX, y }, + { x: tOffsetX, y }, + { x: tOffsetX, y: tOffsetY } ]; } - return [{ x: sox, y: toy }]; + return [{ x: sOffsetX, y: tOffsetY }]; } - const x = Math.min(tmx0, middleOfVerticalSides); + const x = Math.min(tMinMarginX0, middleOfVerticalSides); - if (sox < tox && sy1 >= toy) { + if (sOffsetX < tOffsetX && sBoxY1 >= tOffsetY) { return [ - { x: sox, y: soy }, - { x, y: soy }, - { x, y: toy }]; + { x: sOffsetX, y: sOffsetY }, + { x, y: sOffsetY }, + { x, y: tOffsetY }]; } - if (x < smx1 && soy < ty1) { - const y = Math.min(smy0, tmy0); - const x = Math.min(smx0, tmx0); + if (x < sMinMarginX1 && sOffsetY < tBoxY1) { + const y = Math.min(sMarginY0, tMarginY0); + const x = Math.min(sMarginX0, tMarginX0); return [ - { x: sox, y }, + { x: sOffsetX, y }, { x, y }, - { x, y: toy } + { x, y: tOffsetY } + ]; + } + + if (tOffsetX < sMinMarginX1) { + return [ + { x: sOffsetX, y: sOffsetY }, + { x, y: sOffsetY }, + { x, y: tOffsetY - targetMargin }, + { x: tOffsetX, y: tOffsetY - targetMargin }, + { x: tOffsetX, y: tOffsetY } ]; } return [ - { x: sox, y: soy }, - { x, y: soy }, - { x, y: toy } + { x: sOffsetX, y: sOffsetY }, + { x, y: sOffsetY }, + { x, y: tOffsetY } ]; } else if (sourceSide === 'bottom' && targetSide === 'right') { const isPointInsideSource = inflatedSourceBBox.containsPoint(targetPoint); // The target point is inside the source element if (isPointInsideSource) { - if (sox <= tmx1) { - const x = Math.max(sox + sourceMargin, tox); - const y = Math.max(smy1, tmy1); + if (sOffsetX <= tOffsetX - sourceMargin) { + const x = Math.max(sMarginX1, tOffsetX); + const y = Math.max(sMarginY1, tMarginY1); // Target anchor is on the right side of the source anchor return [ - { x: sox, y }, + { x: sOffsetX, y }, { x, y }, - { x, y: toy } + { x, y: tOffsetY } ]; } // Target anchor is on the left side of the source anchor // Subtract the `sourceMargin` since the source anchor is on the right side of the target anchor - const anchorMiddleX = (sox - sourceMargin + tox) / 2; + const anchorMiddleX = (sOffsetX + tOffsetX) / 2; return [ - { x: sox, y: soy }, - { x: anchorMiddleX, y: soy }, - { x: anchorMiddleX, y: toy } + { x: sOffsetX, y: sOffsetY }, + { x: anchorMiddleX, y: sOffsetY }, + { x: anchorMiddleX, y: tOffsetY } ]; } - if (smy1 < toy) { - if (sox < tox) { - let y = tmy1; + if (sMarginY1 < tOffsetY) { + if (sOffsetX < tOffsetX) { + let y = tMarginY1; - if (tmy0 >= smy1 && tmx1 >= sox) { + if (tMinMarginY0 >= sMinMarginY1 && tMarginX1 >= sOffsetX) { y = middleOfHorizontalSides; + + if (sOffsetY > tMinMarginY0) { + + if (sOffsetX + sourceMargin > tBoxX1) { + + return [ + { x: sOffsetX, y: sOffsetY }, + { x: tOffsetX, y: sOffsetY }, + { x: tOffsetX, y: tOffsetY } + ]; + } + + return [ + { x: sOffsetX, y: sOffsetY }, + { x: sOffsetX + sourceMargin, y: sOffsetY }, + { x: sOffsetX + sourceMargin, y }, + { x: tOffsetX, y }, + { x: tOffsetX, y: tOffsetY } + ]; + } + } return [ - { x: sox, y }, - { x: tox, y }, - { x: tox, y: toy } + { x: sOffsetX, y }, + { x: tOffsetX, y }, + { x: tOffsetX, y: tOffsetY } ]; } - return [{ x: sox, y: toy }]; + return [{ x: sOffsetX, y: tOffsetY }]; } - const x = Math.max(middleOfVerticalSides, tmx1); + const x = Math.max(middleOfVerticalSides, tMinMarginX1); - if (sox > tox && sy0 <= toy) { + if (sOffsetX > tOffsetX && sBoxY0 <= tOffsetY) { return [ - { x: sox, y: soy }, - { x, y: soy }, - { x, y: toy } + { x: sOffsetX, y: sOffsetY }, + { x, y: sOffsetY }, + { x, y: tOffsetY } ]; } - if (x > smx0 && soy > ty0) { - const y = Math.max(smy1, tmy1); - const x = Math.max(smx1, tmx1); + if (x > sMinMarginX0 && sOffsetY > tBoxY0) { + const y = Math.max(sMarginY1, tMarginY1); + const x = Math.max(sMarginX1, tMarginX1); return [ - { x: sox, y }, + { x: sOffsetX, y }, { x, y }, - { x, y: toy } + { x, y: tOffsetY } + ]; + } + + if (tOffsetX > sMinMarginX0) { + return [ + { x: sOffsetX, y: sOffsetY }, + { x, y: sOffsetY }, + { x, y: tOffsetY + targetMargin }, + { x: tOffsetX, y: tOffsetY + targetMargin }, + { x: tOffsetX, y: tOffsetY } ]; } return [ - { x: sox, y: soy }, - { x, y: soy }, - { x, y: toy } + { x: sOffsetX, y: sOffsetY }, + { x, y: sOffsetY }, + { x, y: tOffsetY } ]; } else if (sourceSide === 'bottom' && targetSide === 'left') { const isPointInsideSource = inflatedSourceBBox.containsPoint(targetPoint); // The target point is inside the source element if (isPointInsideSource) { - if (sox >= tmx0) { - const x = Math.min(sox - sourceMargin, tox); - const y = Math.max(smy1, tmy1); + if (sOffsetX >= tOffsetX + sourceMargin) { + const x = Math.min(sOffsetX - sourceMargin, tOffsetX); + const y = Math.max(sMarginY1, tMarginY1); // Target anchor is on the left side of the source anchor return [ - { x: sox, y }, + { x: sOffsetX, y }, { x, y }, - { x, y: toy } + { x, y: tOffsetY } ]; } // Target anchor is on the right side of the source anchor // Add the `sourceMargin` since the source anchor is on the left side of the target anchor - const anchorMiddleX = (sox + sourceMargin + tox) / 2; + const anchorMiddleX = (sOffsetX + tOffsetX) / 2; return [ - { x: sox, y: soy }, - { x: anchorMiddleX, y: soy }, - { x: anchorMiddleX, y: toy } + { x: sOffsetX, y: sOffsetY }, + { x: anchorMiddleX, y: sOffsetY }, + { x: anchorMiddleX, y: tOffsetY } ]; } - if (smy1 < toy) { - if (sox > tox) { - let y = tmy1; + if (sMarginY1 < tOffsetY) { + if (sOffsetX > tOffsetX) { + let y = tMarginY1; - if (tmy0 >= smy1 && tmx0 <= sox) { + if (tMinMarginY0 >= sMinMarginY1 && tMarginX0 <= sOffsetX) { y = middleOfHorizontalSides; + + if (sOffsetY > tMinMarginY0) { + + if (sOffsetX - sourceMargin < tBoxX0) { + + return [ + { x: sOffsetX, y: sOffsetY }, + { x: tOffsetX, y: sOffsetY }, + { x: tOffsetX, y: tOffsetY } + ]; + } + + return [ + { x: sOffsetX, y: sOffsetY }, + { x: sOffsetX - sourceMargin, y: sOffsetY }, + { x: sOffsetX - sourceMargin, y }, + { x: tOffsetX, y }, + { x: tOffsetX, y: tOffsetY } + ]; + } } return [ - { x: sox, y }, - { x: tox, y }, - { x: tox, y: toy } + { x: sOffsetX, y }, + { x: tOffsetX, y }, + { x: tOffsetX, y: tOffsetY } ]; } - return [{ x: sox, y: toy }]; + return [{ x: sOffsetX, y: tOffsetY }]; } - const x = Math.min(tmx0, middleOfVerticalSides); + const x = Math.min(tMinMarginX0, middleOfVerticalSides); - if (sox < tox && sy0 <= toy) { + if (sOffsetX < tOffsetX && sBoxY0 <= tOffsetY) { return [ - { x: sox, y: soy }, - { x, y: soy }, - { x, y: toy } + { x: sOffsetX, y: sOffsetY }, + { x, y: sOffsetY }, + { x, y: tOffsetY } ]; } - if (x < smx1 && soy > ty0) { - const y = Math.max(smy1, tmy1); - const x = Math.min(smx0, tmx0); + if (x < sMinMarginX1 && sOffsetY > tBoxY0) { + const y = Math.max(sMarginY1, tMarginY1); + const x = Math.min(sMarginX0, tMarginX0); return [ - { x: sox, y }, + { x: sOffsetX, y }, { x, y }, - { x, y: toy } + { x, y: tOffsetY } + ]; + } + + if (tOffsetX < sMinMarginX1) { + return [ + { x: sOffsetX, y: sOffsetY }, + { x, y: sOffsetY }, + { x, y: tOffsetY + targetMargin }, + { x: tOffsetX, y: tOffsetY + targetMargin }, + { x: tOffsetX, y: tOffsetY } ]; } return [ - { x: sox, y: soy }, - { x, y: soy }, - { x, y: toy } + { x: sOffsetX, y: sOffsetY }, + { x, y: sOffsetY }, + { x, y: tOffsetY } ]; } else if (sourceSide === 'left' && targetSide === 'bottom') { const isPointInsideSource = inflatedSourceBBox.containsPoint(targetPoint); // The target point is inside the source element if (isPointInsideSource) { - if (soy <= tmy1) { - const x = Math.min(smx0, tmx0); - const y = Math.max(soy + sourceMargin, toy); + if (sOffsetY <= tMinMarginY1) { + const x = Math.min(sMarginX0, tMarginX0); + const y = Math.max(sOffsetY, tOffsetY); return [ - { x, y: soy }, + { x, y: sOffsetY }, { x, y }, - { x: tox, y } + { x: tOffsetX, y } ]; } // Target anchor is above the source anchor - const anchorMiddleY = (soy - sourceMargin + toy) / 2; + const anchorMiddleY = (sOffsetY + tOffsetY) / 2; return [ - { x: sox, y: soy }, - { x: sox, y: anchorMiddleY }, - { x: tox, y: anchorMiddleY } + { x: sOffsetX, y: sOffsetY }, + { x: sOffsetX, y: anchorMiddleY }, + { x: tOffsetX, y: anchorMiddleY } ]; } - if (smx0 > tox) { - if (soy < toy) { - let x = tmx0; + if (sMarginX0 > tOffsetX) { + if (sOffsetY < tOffsetY) { + let x = tMarginX0; - if (tmx1 <= smx0 && tmy1 >= soy) { + if (tMinMarginX1 <= sMinMarginX0 && tMarginY1 >= sOffsetY) { x = middleOfVerticalSides; + + if (sOffsetX < tMinMarginX1) { + + if (sOffsetY + sourceMargin > tBoxY1) { + + return [ + { x: sOffsetX, y: sOffsetY }, + { x: sOffsetX, y: tOffsetY }, + { x: tOffsetX, y: tOffsetY } + ]; + } + + return [ + { x: sOffsetX, y: sOffsetY }, + { x: sOffsetX, y: sOffsetY + sourceMargin }, + { x, y: sOffsetY + sourceMargin }, + { x, y: tOffsetY }, + { x: tOffsetX, y: tOffsetY } + ]; + } + } return [ - { x, y: soy }, - { x, y: toy }, - { x: tox, y: toy } + { x, y: sOffsetY }, + { x, y: tOffsetY }, + { x: tOffsetX, y: tOffsetY } ]; } - return [{ x: tox, y: soy }]; + return [{ x: tOffsetX, y: sOffsetY }]; } - const y = Math.max(tmy1, middleOfHorizontalSides); + const y = Math.max(tMinMarginY1, middleOfHorizontalSides); - if (soy > toy && sx1 >= tox) { + if (sOffsetY > tOffsetY && sBoxX1 >= tOffsetX) { return [ - { x: sox, y: soy }, - { x: sox, y }, - { x: tox, y } + { x: sOffsetX, y: sOffsetY }, + { x: sOffsetX, y }, + { x: tOffsetX, y } ]; } - if (y > smy0 && sox < tx1) { - const x = Math.min(smx0, tmx0); - const y = Math.max(smy1, tmy1); + if (y > sMinMarginY0 && sOffsetX < tBoxX1) { + const x = Math.min(sMarginX0, tMarginX0); + const y = Math.max(sMarginY1, tMarginY1); return [ - { x, y: soy }, + { x, y: sOffsetY }, { x, y }, - { x: tox, y } + { x: tOffsetX, y } + ]; + } + + if (tOffsetY > sMinMarginY0) { + return [ + { x: sOffsetX, y: sOffsetY }, + { x: sOffsetX, y }, + { x: tOffsetX - sourceMargin, y }, + { x: tOffsetX - sourceMargin, y: tOffsetY }, + { x: tOffsetX, y: tOffsetY } ]; } return [ - { x: sox, y: soy }, - { x: sox, y }, - { x: tox, y } + { x: sOffsetX, y: sOffsetY }, + { x: sOffsetX, y }, + { x: tOffsetX, y } ]; } else if (sourceSide === 'left' && targetSide === 'top') { const isPointInsideSource = inflatedSourceBBox.containsPoint(targetPoint); // The target point is inside the source element if (isPointInsideSource) { - if (soy >= tmy0) { - const y = Math.min(soy - sourceMargin, toy); - const x = Math.min(smx0, tmx0); + if (sOffsetY >= tMarginY0) { + const y = Math.min(sMarginY0, tOffsetY); + const x = Math.min(sMarginX0, tMarginX0); // Target anchor is on the top side of the source anchor return [ - { x, y: soy }, + { x, y: sOffsetY }, { x, y }, - { x: tox, y } + { x: tOffsetX, y } ]; } // Target anchor is below the source anchor // Add the `sourceMargin` since the source anchor is above the target anchor - const anchorMiddleY = (soy + sourceMargin + toy) / 2; + const anchorMiddleY = (sOffsetY + tOffsetY) / 2; return [ - { x: sox, y: soy }, - { x: sox, y: anchorMiddleY }, - { x: tox, y: anchorMiddleY } + { x: sOffsetX, y: sOffsetY }, + { x: sOffsetX, y: anchorMiddleY }, + { x: tOffsetX, y: anchorMiddleY } ]; } - if (smx0 > tox) { - if (soy > toy) { - let x = tmx0; + if (sMarginX0 > tOffsetX) { + if (sOffsetY > tOffsetY) { + let x = tMarginX0; - if (tmx1 <= smx0 && tmy0 <= soy) { + if (tMinMarginX1 <= sMinMarginX0 && tMarginY0 <= sOffsetY) { x = middleOfVerticalSides; + + if (sOffsetX < tMinMarginX1) { + + if (sOffsetY - sourceMargin < tBoxY0) { + + return [ + { x: sOffsetX, y: sOffsetY }, + { x: sOffsetX, y: tOffsetY }, + { x: tOffsetX, y: tOffsetY } + ]; + } + + return [ + { x: sOffsetX, y: sOffsetY }, + { x: sOffsetX, y: sOffsetY - sourceMargin }, + { x, y: sOffsetY - sourceMargin }, + { x, y: tOffsetY }, + { x: tOffsetX, y: tOffsetY } + ]; + } } return [ - { x, y: soy }, - { x, y: toy }, - { x: tox, y: toy } + { x, y: sOffsetY }, + { x, y: tOffsetY }, + { x: tOffsetX, y: tOffsetY } ]; } - return [{ x: tox, y: soy }]; + return [{ x: tOffsetX, y: sOffsetY }]; } - const y = Math.min(tmy0, middleOfHorizontalSides); + const y = Math.min(tMinMarginY0, middleOfHorizontalSides); - if (soy < toy && sx1 >= tox) { + if (sOffsetY < tOffsetY && sBoxX1 >= tOffsetX) { return [ - { x: sox, y: soy }, - { x: sox, y }, - { x: tox, y }]; + { x: sOffsetX, y: sOffsetY }, + { x: sOffsetX, y }, + { x: tOffsetX, y }]; } - if (y < smy1 && sox < tx1) { - const x = Math.min(smx0, tmx0); - const y = Math.min(smy0, tmy0); + if (y < sMinMarginY1 && sOffsetX < tBoxX1) { + const x = Math.min(sMarginX0, tMarginX0); + const y = Math.min(sMarginY0, tMarginY0); return [ - { x, y: soy }, + { x, y: sOffsetY }, { x, y }, - { x: tox, y } + { x: tOffsetX, y } + ]; + } + + if (tOffsetY < sMinMarginY1) { + return [ + { x: sOffsetX, y: sOffsetY }, + { x: sOffsetX, y }, + { x: tOffsetX - sourceMargin, y }, + { x: tOffsetX - sourceMargin, y: tOffsetY }, + { x: tOffsetX, y: tOffsetY } ]; } return [ - { x: sox, y: soy }, - { x: sox, y }, - { x: tox, y } + { x: sOffsetX, y: sOffsetY }, + { x: sOffsetX, y }, + { x: tOffsetX, y } ]; } else if (sourceSide === 'right' && targetSide === 'top') { const isPointInsideSource = inflatedSourceBBox.containsPoint(targetPoint); // The target point is inside the source element if (isPointInsideSource) { - if (soy >= tmy0) { - const x = Math.max(smx1, tmx1); - const y = Math.min(soy - sourceMargin, toy); + if (sOffsetY >= tMarginY0) { + const x = Math.max(sMarginX1, tMarginX1); + const y = Math.min(sOffsetY - sourceMargin, tOffsetY); // Target anchor is on the top side of the source anchor return [ - { x, y: soy }, + { x, y: sOffsetY }, { x, y }, // Path adjustment for right side start - { x: tox, y } + { x: tOffsetX, y } ]; } // Target anchor is below the source anchor // Adjust sourceMargin calculation since the source anchor is now on the right - const anchorMiddleY = (soy + sourceMargin + toy) / 2; + const anchorMiddleY = (sOffsetY + tOffsetY) / 2; return [ - { x: sox, y: soy }, - { x: sox, y: anchorMiddleY }, - { x: tox, y: anchorMiddleY } + { x: sOffsetX, y: sOffsetY }, + { x: sOffsetX, y: anchorMiddleY }, + { x: tOffsetX, y: anchorMiddleY } ]; } - if (smx1 < tox) { - if (soy > toy) { - let x = tmx1; + if (sMarginX1 < tOffsetX) { + if (sOffsetY > tOffsetY) { + let x = tMarginX1; - if (tmx0 >= smx1 && tmy0 <= soy) { + if (tMinMarginX0 >= sMinMarginX1 && tMarginY0 <= sOffsetY) { x = middleOfVerticalSides; + + if (sOffsetX > tMinMarginX0) { + + if (sOffsetY - sourceMargin < tBoxY0) { + + return [ + { x: sOffsetX, y: sOffsetY }, + { x: sOffsetX, y: tOffsetY }, + { x: tOffsetX, y: tOffsetY } + ]; + } + + return [ + { x: sOffsetX, y: sOffsetY }, + { x: sOffsetX, y: sOffsetY - sourceMargin }, + { x, y: sOffsetY - sourceMargin }, + { x, y: tOffsetY }, + { x: tOffsetX, y: tOffsetY } + ]; + } } return [ - { x, y: soy }, - { x, y: toy }, - { x: tox, y: toy } + { x, y: sOffsetY }, + { x, y: tOffsetY }, + { x: tOffsetX, y: tOffsetY } ]; } - return [{ x: tox, y: soy }]; + return [{ x: tOffsetX, y: sOffsetY }]; } - const y = Math.min(tmy0, middleOfHorizontalSides); + const y = Math.min(tMinMarginY0, middleOfHorizontalSides); - if (soy < toy && sx0 <= tox) { + if (sOffsetY < tOffsetY && sBoxX0 <= tOffsetX) { return [ - { x: sox, y: soy }, - { x: sox, y }, - { x: tox, y }]; + { x: sOffsetX, y: sOffsetY }, + { x: sOffsetX, y }, + { x: tOffsetX, y }]; } - if (y < smy1 && sox > tx0) { - const x = Math.max(smx1, tmx1); - const y = Math.min(smy0, tmy0); + if (y < sMinMarginY1 && sOffsetX > tBoxX0) { + const x = Math.max(sMarginX1, tMarginX1); + const y = Math.min(sMarginY0, tMarginY0); return [ - { x, y: soy }, + { x, y: sOffsetY }, { x, y }, - { x: tox, y } + { x: tOffsetX, y } + ]; + } + + if (tOffsetY < sMinMarginY1) { + return [ + { x: sOffsetX, y: sOffsetY }, + { x: sOffsetX, y }, + { x: tOffsetX + sourceMargin, y }, + { x: tOffsetX + sourceMargin, y: tOffsetY }, + { x: tOffsetX, y: tOffsetY } ]; } return [ - { x: sox, y: soy }, - { x: sox, y }, - { x: tox, y } + { x: sOffsetX, y: sOffsetY }, + { x: sOffsetX, y }, + { x: tOffsetX, y } ]; } else if (sourceSide === 'right' && targetSide === 'bottom') { const isPointInsideSource = inflatedSourceBBox.containsPoint(targetPoint); // The target point is inside the source element if (isPointInsideSource) { - if (soy <= tmy1) { - const x = Math.max(smx1, tmx1); - const y = Math.max(soy + sourceMargin, toy); + if (sOffsetY <= tMinMarginY1) { + const x = Math.max(sMarginX1, tMarginX1); + const y = Math.max(sOffsetY, tOffsetY); return [ - { x, y: soy }, + { x, y: sOffsetY }, { x, y }, - { x: tox, y } + { x: tOffsetX, y } ]; } // Target anchor is above the source anchor - const anchorMiddleY = (soy - sourceMargin + toy) / 2; + const anchorMiddleY = (sOffsetY + tOffsetY) / 2; return [ - { x: sox, y: soy }, - { x: sox, y: anchorMiddleY }, - { x: tox, y: anchorMiddleY } + { x: sOffsetX, y: sOffsetY }, + { x: sOffsetX, y: anchorMiddleY }, + { x: tOffsetX, y: anchorMiddleY } ]; } - if (smx1 < tox) { - if (soy < toy) { - let x = tmx1; + if (sMarginX1 < tOffsetX) { + if (sOffsetY < tOffsetY) { + let x = tMarginX1; - if (tmx0 >= smx1 && tmy1 >= soy) { + if (tMinMarginX0 >= sMinMarginX1 && tMarginY1 >= sOffsetY) { x = middleOfVerticalSides; + + if (sOffsetX > tMinMarginX0) { + + if (sOffsetY + sourceMargin > tBoxY1) { + + return [ + { x: sOffsetX, y: sOffsetY }, + { x: sOffsetX, y: tOffsetY }, + { x: tOffsetX, y: tOffsetY } + ]; + } + + return [ + { x: sOffsetX, y: sOffsetY }, + { x: sOffsetX, y: sOffsetY + sourceMargin }, + { x, y: sOffsetY + sourceMargin }, + { x, y: tOffsetY }, + { x: tOffsetX, y: tOffsetY } + ]; + } + } return [ - { x, y: soy }, - { x, y: toy }, - { x: tox, y: toy } + { x, y: sOffsetY }, + { x, y: tOffsetY }, + { x: tOffsetX, y: tOffsetY } ]; } - return [{ x: tox, y: soy }]; + return [{ x: tOffsetX, y: sOffsetY }]; } - const y = Math.max(tmy1, middleOfHorizontalSides); + const y = Math.max(tMinMarginY1, middleOfHorizontalSides); - if (soy > toy && sx0 <= tox) { + if (sOffsetY > tOffsetY && sBoxX0 <= tOffsetX) { return [ - { x: sox, y: soy }, - { x: sox, y }, - { x: tox, y } + { x: sOffsetX, y: sOffsetY }, + { x: sOffsetX, y }, + { x: tOffsetX, y } ]; } - if (y > smy0 && sox > tx0) { - const x = Math.max(smx1, tmx1); - const y = Math.max(smy1, tmy1); + if (y > sMinMarginY0 && sOffsetX > tBoxX0) { + const x = Math.max(sMarginX1, tMarginX1); + const y = Math.max(sMarginY1, tMarginY1); return [ - { x, y: soy }, + { x, y: sOffsetY }, { x, y }, - { x: tox, y } + { x: tOffsetX, y } + ]; + } + + if (tOffsetY > sMinMarginY0) { + return [ + { x: sOffsetX, y: sOffsetY }, + { x: sOffsetX, y }, + { x: tOffsetX + sourceMargin, y }, + { x: tOffsetX + sourceMargin, y: tOffsetY }, + { x: tOffsetX, y: tOffsetY } ]; } return [ - { x: sox, y: soy }, - { x: sox, y }, - { x: tox, y } + { x: sOffsetX, y: sOffsetY }, + { x: sOffsetX, y }, + { x: tOffsetX, y } ]; } } @@ -1514,20 +1865,24 @@ function getLoopCoordinates(direction, angle, margin) { } function rightAngleRouter(vertices, opt, linkView) { - const { sourceDirection = Directions.AUTO, targetDirection = Directions.AUTO } = opt; + const { sourceDirection = Directions.AUTO, targetDirection = Directions.AUTO, minPathMargin = null } = opt; const margin = opt.margin || 20; + + const sourceMargin = opt.sourceMargin || margin; + const targetMargin = opt.targetMargin || margin; + const useVertices = opt.useVertices || false; const isSourcePort = !!linkView.model.source().port; - const sourcePoint = pointDataFromAnchor(linkView.sourceView, linkView.sourceAnchor, linkView.sourceBBox, sourceDirection, isSourcePort, margin); + const sourcePoint = pointDataFromAnchor(linkView.sourceView, linkView.sourceAnchor, linkView.sourceBBox, sourceDirection, isSourcePort, sourceMargin); const isTargetPort = !!linkView.model.target().port; - const targetPoint = pointDataFromAnchor(linkView.targetView, linkView.targetAnchor, linkView.targetBBox, targetDirection, isTargetPort, margin); + const targetPoint = pointDataFromAnchor(linkView.targetView, linkView.targetAnchor, linkView.targetBBox, targetDirection, isTargetPort, targetMargin); const resultVertices = []; if (!useVertices || vertices.length === 0) { - return simplifyPoints(routeBetweenPoints(sourcePoint, targetPoint)); + return simplifyPoints(routeBetweenPoints(sourcePoint, targetPoint, { minPathMargin })); } const verticesData = vertices.map((v) => pointDataFromVertex(v)); @@ -1535,11 +1890,11 @@ function rightAngleRouter(vertices, opt, linkView) { const [resolvedSourceDirection] = resolveSides(sourcePoint, firstVertex); const isElement = sourcePoint.view && sourcePoint.view.model.isElement(); - const sourceBBox = isElement ? moveAndExpandBBox(sourcePoint.view.model.getBBox(), resolvedSourceDirection, margin) : null; + const sourceBBox = isElement ? moveAndExpandBBox(sourcePoint.view.model.getBBox(), resolvedSourceDirection, sourceMargin) : null; const isVertexInside = isElement ? sourceBBox.containsPoint(firstVertex.point) : false; if (isVertexInside) { - const outsidePoint = getOutsidePoint(resolvedSourceDirection, sourcePoint, margin); + const outsidePoint = getOutsidePoint(resolvedSourceDirection, sourcePoint, sourceMargin); const firstPointOverlap = outsidePoint.equals(firstVertex.point); const alignsVertically = sourcePoint.point.x === firstVertex.point.x; @@ -1553,8 +1908,6 @@ function rightAngleRouter(vertices, opt, linkView) { const isVertexAlignedAndInside = isVertexInside && (isHorizontalAndAligns || isVerticalAndAligns); - - if (firstPointOverlap) { resultVertices.push(sourcePoint.point, firstVertex.point); // Set the access direction as the opposite of the source direction that will be used to connect the route with the next vertex @@ -1575,7 +1928,7 @@ function rightAngleRouter(vertices, opt, linkView) { // No need to create a route, use the `routeBetweenPoints` to construct a route firstVertex.direction = resolvedSourceDirection; firstVertex.margin = margin; - resultVertices.push(...routeBetweenPoints(sourcePoint, firstVertex, { targetInSourceBBox: true }), firstVertex.point); + resultVertices.push(...routeBetweenPoints(sourcePoint, firstVertex, { targetInSourceBBox: true, minPathMargin }), firstVertex.point); } } else { // The first point responsible for the initial direction of the route @@ -1583,7 +1936,7 @@ function rightAngleRouter(vertices, opt, linkView) { const direction = resolveInitialDirection(sourcePoint, firstVertex, next); firstVertex.direction = direction; - resultVertices.push(...routeBetweenPoints(sourcePoint, firstVertex), firstVertex.point); + resultVertices.push(...routeBetweenPoints(sourcePoint, firstVertex, { minPathMargin }), firstVertex.point); } for (let i = 0; i < verticesData.length - 1; i++) { @@ -1634,7 +1987,7 @@ function rightAngleRouter(vertices, opt, linkView) { from.direction = fromDirection; to.direction = toDirection; - resultVertices.push(...routeBetweenPoints(from, to), to.point); + resultVertices.push(...routeBetweenPoints(from, to, { minPathMargin }), to.point); } const lastVertex = verticesData[verticesData.length - 1]; @@ -1656,7 +2009,7 @@ function rightAngleRouter(vertices, opt, linkView) { lastVertex.direction = definedDirection; - let lastSegmentRoute = routeBetweenPoints(lastVertex, targetPoint); + let lastSegmentRoute = routeBetweenPoints(lastVertex, targetPoint, { minPathMargin }); const [p1, p2] = simplifyPoints([...lastSegmentRoute, targetPoint.point]); const lastSegment = new g.Line(p1, p2); @@ -1681,10 +2034,10 @@ function rightAngleRouter(vertices, opt, linkView) { } else if (isVertexInside && resolvedTargetDirection !== OPPOSITE_DIRECTIONS[definedDirection]) { lastVertex.margin = margin; lastVertex.direction = resolvedTargetDirection; - lastSegmentRoute = routeBetweenPoints(lastVertex, targetPoint); + lastSegmentRoute = routeBetweenPoints(lastVertex, targetPoint, { minPathMargin }); } else if (lastSegmentDirection !== definedDirection && definedDirection === OPPOSITE_DIRECTIONS[lastSegmentDirection]) { lastVertex.margin = margin; - lastSegmentRoute = routeBetweenPoints(lastVertex, targetPoint); + lastSegmentRoute = routeBetweenPoints(lastVertex, targetPoint, { minPathMargin }); } resultVertices.push(...lastSegmentRoute); @@ -1725,7 +2078,7 @@ function rightAngleRouter(vertices, opt, linkView) { from.direction = fromDirection; to.direction = toDirection; - resultVertices.push(...routeBetweenPoints(from, to)); + resultVertices.push(...routeBetweenPoints(from, to, { minPathMargin })); } } diff --git a/packages/joint-core/test/jointjs/routers.js b/packages/joint-core/test/jointjs/routers.js index c2ed58f605..fc66aeeb68 100644 --- a/packages/joint-core/test/jointjs/routers.js +++ b/packages/joint-core/test/jointjs/routers.js @@ -2625,6 +2625,100 @@ QUnit.module('routers', function(hooks) { assert.checkDataPath(d, 'M 25 0 L 25 -28 L 100 -28 L 100 150 L -28 150 L -28 175 L 0 175', 'Source above target with vertex inside the target element bbox'); }); + // minMargin tests + // r1 at (0,0), r2 repositioned so their margin zones overlap. + // margin=28, minMargin=5 → minSourceMargin=minTargetMargin=5, used as tighter routing boundaries. + + QUnit.test('rightAngle routing - minMargin - source: bottom, target: top', function(assert) { + // r1 at (0,0), r2 at (60,20) — elements offset horizontally so their x-margin zones barely touch. + // source outside point: (25, 78); target outside point: (85, -8) + const [, r2, l] = this.addTestSubjects('bottom', 'top', { name: 'rightAngle', args: { margin, minMargin: 5 }}); + r2.position(60, 20); + + let d = this.paper.findViewByModel(l).metrics.data; + assert.checkDataPath(d, 'M 25 50 L 25 98 L 138 98 L 138 -8 L 85 -8 L 85 20', 'minMargin - source: bottom, target: top'); + + l.router({ name: 'rightAngle', args: { margin }}); + d = this.paper.findViewByModel(l).metrics.data; + assert.checkDataPath(d, 'M 25 50 L 25 98 L 138 98 L 138 -8 L 85 -8 L 85 20', 'Without minMargin, route detours around margin zones'); + }); + + QUnit.test('rightAngle routing - minMargin - source: top, target: bottom', function(assert) { + // r1 at (0,0), r2 at (60,20) — elements offset horizontally so their x-margin zones barely touch. + // source outside point: (25, -28); target outside point: (85, 98) + const [, r2, l] = this.addTestSubjects('top', 'bottom', { name: 'rightAngle', args: { margin, minMargin: 5 }}); + r2.position(60, 20); + + let d = this.paper.findViewByModel(l).metrics.data; + assert.checkDataPath(d, 'M 25 0 L 25 -28 L 138 -28 L 138 98 L 85 98 L 85 70', 'minMargin - source: top, target: bottom'); + + l.router({ name: 'rightAngle', args: { margin }}); + d = this.paper.findViewByModel(l).metrics.data; + assert.checkDataPath(d, 'M 25 0 L 25 -28 L 138 -28 L 138 98 L 85 98 L 85 70', 'Without minMargin, route detours around margin zones'); + }); + + QUnit.test('rightAngle routing - minMargin - source: right, target: left', function(assert) { + // r1 at (0,0), r2 at (20,60) — elements offset vertically so their y-margin zones barely touch. + // source outside point: (78, 25); target outside point: (-8, 85) + const [, r2, l] = this.addTestSubjects('right', 'left', { name: 'rightAngle', args: { margin, minMargin: 5 }}); + r2.position(20, 60); + + let d = this.paper.findViewByModel(l).metrics.data; + assert.checkDataPath(d, 'M 50 25 L 98 25 L 98 138 L -8 138 L -8 85 L 20 85', 'minMargin - source: right, target: left'); + + l.router({ name: 'rightAngle', args: { margin }}); + d = this.paper.findViewByModel(l).metrics.data; + assert.checkDataPath(d, 'M 50 25 L 98 25 L 98 138 L -8 138 L -8 85 L 20 85', 'Without minMargin, route detours around margin zones'); + }); + + QUnit.test('rightAngle routing - minMargin - source: left, target: right', function(assert) { + // r1 at (60,0), r2 at (0,60) — elements offset vertically so their y-margin zones barely touch. + // source outside point: (32, 25); target outside point: (78, 85) + const [r1, r2, l] = this.addTestSubjects('left', 'right', { name: 'rightAngle', args: { margin, minMargin: 5 }}); + r1.position(60, 0); + r2.position(0, 60); + + let d = this.paper.findViewByModel(l).metrics.data; + assert.checkDataPath(d, 'M 60 25 L -28 25 L -28 138 L 78 138 L 78 85 L 50 85', 'minMargin - source: left, target: right'); + + l.router({ name: 'rightAngle', args: { margin }}); + d = this.paper.findViewByModel(l).metrics.data; + assert.checkDataPath(d, 'M 60 25 L -28 25 L -28 138 L 78 138 L 78 85 L 50 85', 'Without minMargin, route detours around margin zones'); + }); + + // The facing-elements condition: source left anchor facing a target right anchor (and the reverse). + // Positions are chosen so the anchors' outside points land outside the inflated bboxes (no S-shape). + // r1 at (100,0), r2 at (0,60), minMargin=25: minSourceMargin=minTargetMargin=25. + + QUnit.test('rightAngle routing - minMargin facing - source: left, target: right', function(assert) { + // r1 at (100,0), r2 at (0,60) — anchors face each other with a 50px gap (tox−smx0=6=ignoreOverlappingMargin). + // source outside point: (72, 25); target outside point: (78, 85) + const [r1, r2, l] = this.addTestSubjects('left', 'right', { name: 'rightAngle', args: { margin, minMargin: 25 }}); + r1.position(100, 0); + r2.position(0, 60); + + let d = this.paper.findViewByModel(l).metrics.data; + assert.checkDataPath(d, 'M 100 25 L -28 25 L -28 138 L 78 138 L 78 85 L 50 85', 'minMargin facing - source: left, target: right'); + + l.router({ name: 'rightAngle', args: { margin }}); + d = this.paper.findViewByModel(l).metrics.data; + assert.checkDataPath(d, 'M 100 25 L -28 25 L -28 138 L 78 138 L 78 85 L 50 85', 'Without minMargin, route detours around margin zones'); + }); + + QUnit.test('rightAngle routing - minMargin facing - source: right, target: left', function(assert) { + // r1 at (0,0), r2 at (100,60) — anchors face each other with a 50px gap (smx1−tox=6=ignoreOverlappingMargin). + // source outside point: (78, 25); target outside point: (72, 85) + const [, r2, l] = this.addTestSubjects('right', 'left', { name: 'rightAngle', args: { margin, minMargin: 25 }}); + r2.position(100, 60); + + let d = this.paper.findViewByModel(l).metrics.data; + assert.checkDataPath(d, 'M 50 25 L 178 25 L 178 138 L 72 138 L 72 85 L 100 85', 'minMargin facing - source: right, target: left'); + + l.router({ name: 'rightAngle', args: { margin }}); + d = this.paper.findViewByModel(l).metrics.data; + assert.checkDataPath(d, 'M 50 25 L 178 25 L 178 138 L 72 138 L 72 85 L 100 85', 'Without minMargin, route detours around margin zones'); + }); + QUnit.test('rightAngle routing with source anchor outside the element bbox', function(assert) { // Source `top` anchor offset above the element — outside the element bbox. // The router must include the anchor in its source bbox union so the routing diff --git a/packages/joint-core/types/routers.d.ts b/packages/joint-core/types/routers.d.ts index 25aa447693..123395e9df 100644 --- a/packages/joint-core/types/routers.d.ts +++ b/packages/joint-core/types/routers.d.ts @@ -87,6 +87,9 @@ export enum RightAngleDirections { export interface RightAngleRouterArguments { margin?: number; + sourceMargin?: number | null; + targetMargin?: number | null; + minPathMargin?: number | null; /** @experimental before version 4.0 */ useVertices?: boolean; sourceDirection?: RightAngleDirections; diff --git a/yarn.lock b/yarn.lock index 6518b60360..258368fbc8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4735,6 +4735,15 @@ __metadata: languageName: unknown linkType: soft +"@joint/demo-right-angle-playground-js@workspace:examples/right-angle-playground-js": + version: 0.0.0-use.local + resolution: "@joint/demo-right-angle-playground-js@workspace:examples/right-angle-playground-js" + dependencies: + "@joint/core": "workspace:^" + vite: "npm:^7.3.1" + languageName: unknown + linkType: soft + "@joint/demo-roi-calculator-js@workspace:examples/roi-calculator-js": version: 0.0.0-use.local resolution: "@joint/demo-roi-calculator-js@workspace:examples/roi-calculator-js"