reactflow-edge-routing 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/routing-core.ts +49 -47
- package/src/use-routed-edge-path.ts +15 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reactflow-edge-routing",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "Orthogonal edge routing for React Flow using obstacle-router. Edges route around nodes while pins stay fixed at their anchor points.",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
package/src/routing-core.ts
CHANGED
|
@@ -60,6 +60,8 @@ export type AvoidRoute = {
|
|
|
60
60
|
sourceY: number;
|
|
61
61
|
targetX: number;
|
|
62
62
|
targetY: number;
|
|
63
|
+
/** Raw routed waypoints (including handle stubs). Use slice(1,-1) for editable edge control points. */
|
|
64
|
+
points?: { x: number; y: number }[];
|
|
63
65
|
};
|
|
64
66
|
|
|
65
67
|
export type ConnectorType = "orthogonal" | "polyline" | "bezier";
|
|
@@ -287,8 +289,7 @@ function segmentIntersectsRect(
|
|
|
287
289
|
return tMin <= tMax;
|
|
288
290
|
}
|
|
289
291
|
|
|
290
|
-
/** Returns true if the direct line from srcPt to tgtPt is blocked by any node
|
|
291
|
-
* including the source/target nodes themselves (e.g. edge going backward through its own node). */
|
|
292
|
+
/** Returns true if the direct line from srcPt to tgtPt is blocked by any node except the source/target nodes. */
|
|
292
293
|
function isEdgeDirectPathBlocked(
|
|
293
294
|
srcPt: { x: number; y: number },
|
|
294
295
|
tgtPt: { x: number; y: number },
|
|
@@ -298,25 +299,10 @@ function isEdgeDirectPathBlocked(
|
|
|
298
299
|
nodeById: Map<string, FlowNode>,
|
|
299
300
|
buffer: number
|
|
300
301
|
): boolean {
|
|
301
|
-
const dx = tgtPt.x - srcPt.x, dy = tgtPt.y - srcPt.y;
|
|
302
|
-
const len = Math.sqrt(dx * dx + dy * dy);
|
|
303
302
|
for (const node of nodes) {
|
|
304
303
|
const bounds = Geometry.getNodeBoundsAbsolute(node, nodeById);
|
|
305
|
-
if (node.id === srcNodeId || node.id === tgtNodeId)
|
|
306
|
-
|
|
307
|
-
// always trigger. Nudge 1 px along the segment away from the handle and check
|
|
308
|
-
// from there — avoids the border false-positive while still catching lines that
|
|
309
|
-
// go backward through the node body.
|
|
310
|
-
if (len < 1e-6) continue;
|
|
311
|
-
const isSrc = node.id === srcNodeId;
|
|
312
|
-
const nudged = isSrc
|
|
313
|
-
? { x: srcPt.x + dx / len, y: srcPt.y + dy / len }
|
|
314
|
-
: { x: tgtPt.x - dx / len, y: tgtPt.y - dy / len };
|
|
315
|
-
const otherEnd = isSrc ? tgtPt : srcPt;
|
|
316
|
-
if (segmentIntersectsRect(nudged, otherEnd, bounds, buffer)) return true;
|
|
317
|
-
} else {
|
|
318
|
-
if (segmentIntersectsRect(srcPt, tgtPt, bounds, buffer)) return true;
|
|
319
|
-
}
|
|
304
|
+
if (node.id === srcNodeId || node.id === tgtNodeId) continue;
|
|
305
|
+
if (segmentIntersectsRect(srcPt, tgtPt, bounds, buffer)) return true;
|
|
320
306
|
}
|
|
321
307
|
return false;
|
|
322
308
|
}
|
|
@@ -538,53 +524,62 @@ export class PathBuilder {
|
|
|
538
524
|
}
|
|
539
525
|
|
|
540
526
|
/**
|
|
541
|
-
* Convert routed waypoints to a smooth
|
|
527
|
+
* Convert routed waypoints to a smooth cubic Bezier spline using
|
|
528
|
+
* Catmull-Rom → Bezier conversion with adaptive tension.
|
|
542
529
|
*
|
|
543
|
-
*
|
|
544
|
-
*
|
|
545
|
-
*
|
|
546
|
-
*
|
|
530
|
+
* Steps:
|
|
531
|
+
* 1. Deduplicate and remove collinear intermediate points (keep corners only).
|
|
532
|
+
* 2. For each segment, compute control points from neighboring points.
|
|
533
|
+
* 3. Clamp control-point reach to prevent overshooting / edge crossings.
|
|
534
|
+
* 4. Adapt tension based on segment length — short segments get less curvature.
|
|
547
535
|
*/
|
|
548
536
|
static routedBezierPath(
|
|
549
537
|
points: { x: number; y: number }[],
|
|
550
|
-
options: { gridSize?: number } = {}
|
|
538
|
+
options: { gridSize?: number; baseTension?: number } = {}
|
|
551
539
|
): string {
|
|
552
540
|
if (points.length < 2) return "";
|
|
553
541
|
const gridSize = options.gridSize ?? 0;
|
|
542
|
+
const baseTension = options.baseTension ?? 0.2;
|
|
554
543
|
const snap = (p: { x: number; y: number }) =>
|
|
555
544
|
gridSize > 0 ? Geometry.snapToGrid(p.x, p.y, gridSize) : p;
|
|
556
545
|
|
|
557
546
|
const raw = points.map(snap);
|
|
558
547
|
|
|
559
|
-
// Deduplicate
|
|
548
|
+
// Deduplicate near-identical consecutive points
|
|
560
549
|
const deduped: { x: number; y: number }[] = [raw[0]];
|
|
561
550
|
for (let i = 1; i < raw.length; i++) {
|
|
562
551
|
const prev = deduped[deduped.length - 1];
|
|
563
|
-
if (Math.abs(raw[i].x - prev.x) >
|
|
552
|
+
if (Math.abs(raw[i].x - prev.x) > 1 || Math.abs(raw[i].y - prev.y) > 1) {
|
|
564
553
|
deduped.push(raw[i]);
|
|
565
554
|
}
|
|
566
555
|
}
|
|
567
556
|
|
|
568
557
|
// Remove collinear midpoints — keep only corners + endpoints
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
558
|
+
let pts: { x: number; y: number }[];
|
|
559
|
+
if (deduped.length <= 2) {
|
|
560
|
+
pts = deduped;
|
|
561
|
+
} else {
|
|
562
|
+
pts = [deduped[0]];
|
|
563
|
+
for (let i = 1; i < deduped.length - 1; i++) {
|
|
564
|
+
const prev = deduped[i - 1];
|
|
565
|
+
const curr = deduped[i];
|
|
566
|
+
const next = deduped[i + 1];
|
|
567
|
+
const sameX = Math.abs(prev.x - curr.x) < 1 && Math.abs(curr.x - next.x) < 1;
|
|
568
|
+
const sameY = Math.abs(prev.y - curr.y) < 1 && Math.abs(curr.y - next.y) < 1;
|
|
569
|
+
if (!sameX || !sameY) pts.push(curr);
|
|
570
|
+
}
|
|
571
|
+
pts.push(deduped[deduped.length - 1]);
|
|
577
572
|
}
|
|
578
|
-
pts.push(deduped[deduped.length - 1]);
|
|
579
573
|
|
|
580
|
-
if (pts.length
|
|
574
|
+
if (pts.length === 0) return "";
|
|
575
|
+
if (pts.length === 1) return `M ${pts[0].x} ${pts[0].y}`;
|
|
581
576
|
|
|
582
|
-
// 2 points: simple bezier with offset control points
|
|
577
|
+
// 2 points: simple bezier with offset control points scaled to distance
|
|
583
578
|
if (pts.length === 2) {
|
|
584
579
|
const [s, t] = pts;
|
|
585
580
|
const dx = t.x - s.x;
|
|
586
581
|
const dy = t.y - s.y;
|
|
587
|
-
const offset = Math.max(
|
|
582
|
+
const offset = Math.max(30, Math.max(Math.abs(dx), Math.abs(dy)) * 0.4);
|
|
588
583
|
if (Math.abs(dx) >= Math.abs(dy)) {
|
|
589
584
|
const sign = dx >= 0 ? 1 : -1;
|
|
590
585
|
return `M ${s.x} ${s.y} C ${s.x + offset * sign} ${s.y}, ${t.x - offset * sign} ${t.y}, ${t.x} ${t.y}`;
|
|
@@ -593,10 +588,6 @@ export class PathBuilder {
|
|
|
593
588
|
return `M ${s.x} ${s.y} C ${s.x} ${s.y + offset * sign}, ${t.x} ${t.y - offset * sign}, ${t.x} ${t.y}`;
|
|
594
589
|
}
|
|
595
590
|
|
|
596
|
-
// 3+ points: smooth cubic bezier spline through all corner points.
|
|
597
|
-
// For each segment i→i+1, compute tangent-based control points using
|
|
598
|
-
// neighbors (Catmull-Rom style, tension 0.3).
|
|
599
|
-
const tension = 0.3;
|
|
600
591
|
const dist = (a: { x: number; y: number }, b: { x: number; y: number }) =>
|
|
601
592
|
Math.hypot(b.x - a.x, b.y - a.y);
|
|
602
593
|
|
|
@@ -610,10 +601,21 @@ export class PathBuilder {
|
|
|
610
601
|
|
|
611
602
|
const segLen = dist(p1, p2);
|
|
612
603
|
|
|
613
|
-
//
|
|
604
|
+
// Skip curvature for very short segments
|
|
605
|
+
if (segLen < 5) {
|
|
606
|
+
d += ` L ${p2.x} ${p2.y}`;
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// Adaptive tension: shorter segments get less curvature to avoid overshooting
|
|
611
|
+
const tension =
|
|
612
|
+
segLen < 40 ? baseTension * 0.3 :
|
|
613
|
+
segLen < 80 ? baseTension * 0.6 :
|
|
614
|
+
baseTension;
|
|
615
|
+
|
|
616
|
+
// Catmull-Rom to cubic bezier control points
|
|
614
617
|
let cp1x = p1.x + (p2.x - p0.x) * tension;
|
|
615
618
|
let cp1y = p1.y + (p2.y - p0.y) * tension;
|
|
616
|
-
// Tangent at p2: direction from p1 to p3
|
|
617
619
|
let cp2x = p2.x - (p3.x - p1.x) * tension;
|
|
618
620
|
let cp2y = p2.y - (p3.y - p1.y) * tension;
|
|
619
621
|
|
|
@@ -1039,7 +1041,7 @@ export class RoutingEngine {
|
|
|
1039
1041
|
const labelP = gridSize > 0 ? Geometry.snapToGrid(midP.x, midP.y, gridSize) : midP;
|
|
1040
1042
|
const first = points[0];
|
|
1041
1043
|
const last = points[points.length - 1];
|
|
1042
|
-
result[edgeId] = { path, labelX: labelP.x, labelY: labelP.y, sourceX: first.x, sourceY: first.y, targetX: last.x, targetY: last.y };
|
|
1044
|
+
result[edgeId] = { path, labelX: labelP.x, labelY: labelP.y, sourceX: first.x, sourceY: first.y, targetX: last.x, targetY: last.y, points };
|
|
1043
1045
|
}
|
|
1044
1046
|
|
|
1045
1047
|
RoutingEngine.cleanup(router, connRefs, shapeRefList);
|
|
@@ -1245,7 +1247,7 @@ export class PersistentRouter {
|
|
|
1245
1247
|
const labelP = gridSize > 0 ? Geometry.snapToGrid(midP.x, midP.y, gridSize) : midP;
|
|
1246
1248
|
const first = points[0];
|
|
1247
1249
|
const last = points[points.length - 1];
|
|
1248
|
-
result[edgeId] = { path, labelX: labelP.x, labelY: labelP.y, sourceX: first.x, sourceY: first.y, targetX: last.x, targetY: last.y };
|
|
1250
|
+
result[edgeId] = { path, labelX: labelP.x, labelY: labelP.y, sourceX: first.x, sourceY: first.y, targetX: last.x, targetY: last.y, points };
|
|
1249
1251
|
}
|
|
1250
1252
|
return result;
|
|
1251
1253
|
}
|
|
@@ -57,15 +57,20 @@ function getFallback(
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
/**
|
|
60
|
-
* Returns [path, labelX, labelY, wasRouted] for a routed edge.
|
|
60
|
+
* Returns [path, labelX, labelY, wasRouted, controlPoints] for a routed edge.
|
|
61
61
|
*
|
|
62
62
|
* - While a connected node is being dragged → dashed fallback
|
|
63
63
|
* - If a routed path exists for this edge → solid routed path
|
|
64
64
|
* - No route yet (worker hasn't responded) → dashed fallback
|
|
65
|
+
*
|
|
66
|
+
* `controlPoints` are the intermediate waypoints of the routed path (excluding
|
|
67
|
+
* the source and target anchor points). Use them to build an editable edge on
|
|
68
|
+
* top of auto-routing — e.g. render draggable handles at each waypoint.
|
|
69
|
+
* When no route is available (fallback), `controlPoints` is an empty array.
|
|
65
70
|
*/
|
|
66
71
|
export function useRoutedEdgePath(
|
|
67
72
|
params: UseRoutedEdgePathParams
|
|
68
|
-
): [path: string, labelX: number, labelY: number, wasRouted: boolean] {
|
|
73
|
+
): [path: string, labelX: number, labelY: number, wasRouted: boolean, controlPoints: { x: number; y: number }[]] {
|
|
69
74
|
const {
|
|
70
75
|
id, sourceX, sourceY, targetX, targetY,
|
|
71
76
|
sourcePosition, targetPosition,
|
|
@@ -84,16 +89,21 @@ export function useRoutedEdgePath(
|
|
|
84
89
|
|
|
85
90
|
if (isDragging) {
|
|
86
91
|
const [path, lx, ly] = getFallback(sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, connectorType, params.borderRadius, offset);
|
|
87
|
-
return [path, lx, ly, false];
|
|
92
|
+
return [path, lx, ly, false, []];
|
|
88
93
|
}
|
|
89
94
|
|
|
90
95
|
// If we have a routed path, use it (no stale check — trust the router)
|
|
91
96
|
if (route?.path) {
|
|
92
|
-
|
|
97
|
+
// Strip the first (source anchor) and last (target anchor) points — the
|
|
98
|
+
// middle points are the editable control points for the connector.
|
|
99
|
+
const controlPoints = route.points && route.points.length > 2
|
|
100
|
+
? route.points.slice(1, -1)
|
|
101
|
+
: [];
|
|
102
|
+
return [route.path, route.labelX, route.labelY, true, controlPoints];
|
|
93
103
|
}
|
|
94
104
|
|
|
95
105
|
// No route yet — show fallback
|
|
96
106
|
const [path, lx, ly] = getFallback(sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, connectorType, params.borderRadius, offset);
|
|
97
|
-
return [path, lx, ly, false];
|
|
107
|
+
return [path, lx, ly, false, []];
|
|
98
108
|
}, [route, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, offset, connectorType, params.borderRadius, source, target, draggingNodeIds]);
|
|
99
109
|
}
|