Skip to content

Commit 2a1fec5

Browse files
kumilingusclaude
andauthored
fix(routers.rightAngle): include anchor in bbox union (#3288)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ed09e06 commit 2a1fec5

2 files changed

Lines changed: 42 additions & 13 deletions

File tree

packages/joint-core/src/routers/rightAngle.mjs

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ function getDirectionForLinkConnection(linkOrigin, connectionPoint, linkView) {
256256
}
257257
}
258258

259-
function pointDataFromAnchor(view, point, bbox, direction, isPort, fallBackAnchor, margin) {
259+
function pointDataFromAnchor(view, anchor, bbox, direction, isPort, margin) {
260260
if (direction === Directions.AUTO) {
261261
direction = isPort ? Directions.MAGNET_SIDE : Directions.ANCHOR_SIDE;
262262
}
@@ -268,10 +268,16 @@ function pointDataFromAnchor(view, point, bbox, direction, isPort, fallBackAncho
268268
y: y0,
269269
width = 0,
270270
height = 0
271-
} = isElement ? g.Rect.fromRectUnion(bbox, view.model.getBBox()) : fallBackAnchor;
271+
} = isElement
272+
// Find the union of:
273+
// - the element bbox
274+
// - the ports may overlap the element body
275+
// - the anchor point may be outside the element body and port
276+
? g.Rect.fromRectUnion(anchor, bbox, view.model.getBBox())
277+
: anchor;
272278

273279
return {
274-
point,
280+
point: anchor,
275281
x0,
276282
y0,
277283
view,
@@ -422,7 +428,7 @@ function getVerticalDistance(source, target) {
422428
const boundaryDefiningShape = source.direction === Directions.LEFT ? leftShape : rightShape;
423429

424430
topBoundary = boundaryDefiningShape.y0;
425-
bottomBoundary = boundaryDefiningShape.y1;
431+
bottomBoundary = boundaryDefiningShape.y1;
426432
}
427433

428434
const { y: soy } = sourcePoint;
@@ -527,7 +533,7 @@ function routeBetweenPoints(source, target, opt = {}) {
527533
// Use S-shaped connection
528534
if (isPointInsideSource || isPointInsideTarget) {
529535
const middleOfAnchors = (soy + toy) / 2;
530-
536+
531537
return [
532538
{ x: sox, y: soy },
533539
{ x: sox, y: middleOfAnchors },
@@ -556,7 +562,7 @@ function routeBetweenPoints(source, target, opt = {}) {
556562
// the case when the source is to the left of the target element.
557563
x1 = Math.min(sox, tmx0);
558564
x2 = Math.max(tox, smx1);
559-
565+
560566
// This is an edge case when the source and target intersect and
561567
if ((isUpwardsShorter && soy < ty0) || (!isUpwardsShorter && soy > ty1)) {
562568
// the path should no longer rely on minimal x boundary in `x1`
@@ -587,7 +593,7 @@ function routeBetweenPoints(source, target, opt = {}) {
587593
// Use S-shaped connection
588594
if (isPointInsideSource || isPointInsideTarget) {
589595
const middleOfAnchors = (soy + toy) / 2;
590-
596+
591597
return [
592598
{ x: sox, y: soy },
593599
{ x: sox, y: middleOfAnchors },
@@ -720,7 +726,7 @@ function routeBetweenPoints(source, target, opt = {}) {
720726
let x = middleOfVerticalSides;
721727
let y1 = soy;
722728
let y2 = toy;
723-
729+
724730
const isLeftShorter = leftD < rightD;
725731

726732
// If the source and target elements overlap, we need to make sure the connection
@@ -841,7 +847,7 @@ function routeBetweenPoints(source, target, opt = {}) {
841847
{ x: tox, y: y1 }
842848
];
843849
} else if (sourceSide === 'left' && targetSide === 'left') {
844-
const useUShapeConnection =
850+
const useUShapeConnection =
845851
targetInSourceBBox ||
846852
g.intersection.rectWithRect(inflatedSourceBBox, targetBBox) ||
847853
(sox <= tox && (inflatedSourceBBox.bottomRight().y <= toy || inflatedSourceBBox.topRight().y >= toy)) ||
@@ -1513,10 +1519,10 @@ function rightAngleRouter(vertices, opt, linkView) {
15131519
const useVertices = opt.useVertices || false;
15141520

15151521
const isSourcePort = !!linkView.model.source().port;
1516-
const sourcePoint = pointDataFromAnchor(linkView.sourceView, linkView.sourceAnchor, linkView.sourceBBox, sourceDirection, isSourcePort, linkView.sourceAnchor, margin);
1522+
const sourcePoint = pointDataFromAnchor(linkView.sourceView, linkView.sourceAnchor, linkView.sourceBBox, sourceDirection, isSourcePort, margin);
15171523

15181524
const isTargetPort = !!linkView.model.target().port;
1519-
const targetPoint = pointDataFromAnchor(linkView.targetView, linkView.targetAnchor, linkView.targetBBox, targetDirection, isTargetPort, linkView.targetAnchor, margin);
1525+
const targetPoint = pointDataFromAnchor(linkView.targetView, linkView.targetAnchor, linkView.targetBBox, targetDirection, isTargetPort, margin);
15201526

15211527
const resultVertices = [];
15221528

@@ -1547,7 +1553,7 @@ function rightAngleRouter(vertices, opt, linkView) {
15471553

15481554
const isVertexAlignedAndInside = isVertexInside && (isHorizontalAndAligns || isVerticalAndAligns);
15491555

1550-
1556+
15511557

15521558
if (firstPointOverlap) {
15531559
resultVertices.push(sourcePoint.point, firstVertex.point);
@@ -1666,7 +1672,7 @@ function rightAngleRouter(vertices, opt, linkView) {
16661672
const isVerticalAndAligns = alignsVertically && (resolvedTargetDirection === Directions.TOP || resolvedTargetDirection === Directions.BOTTOM);
16671673
const isHorizontalAndAligns = alignsHorizontally && (resolvedTargetDirection === Directions.LEFT || resolvedTargetDirection === Directions.RIGHT);
16681674

1669-
1675+
16701676
if (!lastPointOverlap && isVertexInside && (isHorizontalAndAligns || isVerticalAndAligns)) {
16711677
// Handle special cases when the last vertex is inside the target element
16721678
// and in is aligned with the connection point => construct a loop

packages/joint-core/test/jointjs/routers.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2624,4 +2624,27 @@ QUnit.module('routers', function(hooks) {
26242624

26252625
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');
26262626
});
2627+
2628+
QUnit.test('rightAngle routing with source anchor outside the element bbox', function(assert) {
2629+
// Source `top` anchor offset above the element — outside the element bbox.
2630+
// The router must include the anchor in its source bbox union so the routing
2631+
// area reflects where the anchor actually sits, not just the element body.
2632+
const [r1, , l] = this.addTestSubjects('top', 'top', rightAngleRouter, {
2633+
sourceAnchor: { dy: -50 },
2634+
targetAnchor: {}
2635+
});
2636+
2637+
const linkView = this.paper.findViewByModel(l);
2638+
assert.notOk(r1.getBBox().containsPoint(linkView.sourceAnchor), 'Source anchor is outside the element bbox');
2639+
assert.checkDataPath(linkView.metrics.data, 'M 25 -50 L 25 -78 L 78 -78 L 78 100 L 25 100 L 25 150', 'Source anchor 50px above the element');
2640+
2641+
// Move the anchor 1px further up. The segments derived from the source bbox
2642+
// union (the first three points) must shift by 1px as well.
2643+
l.source(l.getSourceCell(), {
2644+
anchor: { name: 'top', args: { dy: -51 }}
2645+
});
2646+
2647+
assert.notOk(r1.getBBox().containsPoint(linkView.sourceAnchor), 'Source anchor is still outside the element bbox');
2648+
assert.checkDataPath(linkView.metrics.data, 'M 25 -51 L 25 -79 L 78 -79 L 78 100 L 25 100 L 25 150', 'Source anchor 51px above the element — path shifts by 1px');
2649+
});
26272650
});

0 commit comments

Comments
 (0)