Skip to content

Commit fc06111

Browse files
fix(library): a dragged bend arm rests on the adjacent parallel arm, not through it
Squeezing a loop's arms together (e.g. diagram 83) did not collapse cleanly: dragging an arm toward the parallel arm two segments away let it sail straight through and grow a fresh zig-zag on the far side, which read as the geometry "pushing apart" instead of merging. The lane had walls only at the endpoint stubs, none at the neighbouring arm. getBendLaneBounds now also clamps a dragged arm's lane at each parallel arm two segments away (joined to it by the perpendicular connectors i±1), on whichever side that arm currently sits. The lane comes to rest exactly where the connector collapses to zero, so the loop merges to the clean route and further dragging in the same direction does nothing — matching the existing "follow the cursor, stop at a wall" model. A neighbour already coincident imposes no bound (already merged). Unit tests cover the clamp toward each side; the U-merge release tests, bend-drag e2e, interactions, zoom and the routing-quality gate are unaffected (290 unit + gate green). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 5313620 commit fc06111

2 files changed

Lines changed: 64 additions & 0 deletions

File tree

library/lib/utils/edgeUtils.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2968,6 +2968,23 @@ export function getBendLaneBounds(
29682968
constrainBy(targetPoint, targetPosition)
29692969
}
29702970

2971+
// The two PARALLEL arms flanking this segment (two away, joined to it by the
2972+
// perpendicular connectors i±1) are walls too: a lane must come to REST on an
2973+
// adjacent arm — where the connector between them collapses to zero and the loop
2974+
// merges — not sail through it and grow a fresh zig-zag on the far side. This is
2975+
// what makes "drag the arms together" collapse the loop instead of pushing it
2976+
// apart. Clamp toward whichever side each neighbour currently sits on; a neighbour
2977+
// already coincident with this lane imposes no bound (the loop is already merged).
2978+
const laneCoord = points[segmentIndex][laneAxis]
2979+
const clampAgainstArm = (armIndex: number): void => {
2980+
if (armIndex < 0 || armIndex > lastSegmentIndex) return
2981+
const armCoord = points[armIndex][laneAxis]
2982+
if (armCoord > laneCoord) bounds.max = Math.min(bounds.max, armCoord)
2983+
else if (armCoord < laneCoord) bounds.min = Math.max(bounds.min, armCoord)
2984+
}
2985+
clampAgainstArm(segmentIndex - 2)
2986+
clampAgainstArm(segmentIndex + 2)
2987+
29712988
return bounds
29722989
}
29732990

library/tests/unit/edgeUtils.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
simplifySvgPath,
3333
stubsWouldOverlap,
3434
getEffectiveStubLength,
35+
getBendLaneBounds,
3536
} from "@/utils/edgeUtils"
3637
import { ConnectionLineType, Position } from "@xyflow/react"
3738
import { describe, expect, it } from "vitest"
@@ -1533,6 +1534,52 @@ describe("preserveOrthogonalEdgePoints", () => {
15331534
})
15341535
})
15351536

1537+
// A dragged arm must come to REST on the parallel arm two segments away (where the
1538+
// connector between them collapses and the loop merges), not sail through it and grow
1539+
// a fresh zig-zag on the far side — which read as the geometry "pushing apart".
1540+
describe("clamps a dragged arm's lane at the adjacent parallel arm", () => {
1541+
// A loop like diagram 83: source stub → up → over → down into target. The two
1542+
// vertical arms are at x=615 and x=645; the target down-run (seg 3) is dragged left.
1543+
const loop = [
1544+
{ x: 565, y: 335 }, // source
1545+
{ x: 615, y: 335 },
1546+
{ x: 615, y: 140 }, // seg1: left arm (x=615)
1547+
{ x: 645, y: 140 },
1548+
{ x: 645, y: 410 }, // seg3: right arm (x=645) — the dragged terminal down-run
1549+
]
1550+
const S = { x: 565, y: 335 }
1551+
const T = { x: 645, y: 410 }
1552+
1553+
it("stops the right arm at the left arm (x=615) instead of crossing it", () => {
1554+
const bounds = getBendLaneBounds(
1555+
loop,
1556+
3, // the target down-run
1557+
"V",
1558+
S,
1559+
T,
1560+
Position.Right,
1561+
Position.Top
1562+
)
1563+
// Dragging the x=645 arm LEFT, its lane may not go below x=615 (the parallel arm).
1564+
expect(bounds.min).toBe(615)
1565+
})
1566+
1567+
it("clamps the left arm at the right arm (x=645) when dragged toward it", () => {
1568+
// seg1 (left arm, x=615) has its parallel neighbour on the target side (seg3 at
1569+
// x=645), so dragging it RIGHT stops at 645 rather than crossing.
1570+
const bounds = getBendLaneBounds(
1571+
loop,
1572+
1,
1573+
"V",
1574+
S,
1575+
T,
1576+
Position.Right,
1577+
Position.Top
1578+
)
1579+
expect(bounds.max).toBe(645)
1580+
})
1581+
})
1582+
15361583
it("detects mirrored horizontal and vertical stub collisions", () => {
15371584
expect(
15381585
stubsWouldOverlap(

0 commit comments

Comments
 (0)