reactflow-edge-routing 0.1.7 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reactflow-edge-routing",
3
- "version": "0.1.7",
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",
@@ -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";
@@ -522,53 +524,62 @@ export class PathBuilder {
522
524
  }
523
525
 
524
526
  /**
525
- * Convert routed waypoints to a smooth bezier spline.
527
+ * Convert routed waypoints to a smooth cubic Bezier spline using
528
+ * Catmull-Rom → Bezier conversion with adaptive tension.
526
529
  *
527
- * Takes the orthogonal waypoints from libavoid, keeps only corners
528
- * (where direction changes), then draws a smooth cubic bezier spline
529
- * through them. The result is a flowing curve not orthogonal at all —
530
- * that still follows the obstacle-avoiding route.
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.
531
535
  */
532
536
  static routedBezierPath(
533
537
  points: { x: number; y: number }[],
534
- options: { gridSize?: number } = {}
538
+ options: { gridSize?: number; baseTension?: number } = {}
535
539
  ): string {
536
540
  if (points.length < 2) return "";
537
541
  const gridSize = options.gridSize ?? 0;
542
+ const baseTension = options.baseTension ?? 0.2;
538
543
  const snap = (p: { x: number; y: number }) =>
539
544
  gridSize > 0 ? Geometry.snapToGrid(p.x, p.y, gridSize) : p;
540
545
 
541
546
  const raw = points.map(snap);
542
547
 
543
- // Deduplicate
548
+ // Deduplicate near-identical consecutive points
544
549
  const deduped: { x: number; y: number }[] = [raw[0]];
545
550
  for (let i = 1; i < raw.length; i++) {
546
551
  const prev = deduped[deduped.length - 1];
547
- if (Math.abs(raw[i].x - prev.x) > 0.5 || Math.abs(raw[i].y - prev.y) > 0.5) {
552
+ if (Math.abs(raw[i].x - prev.x) > 1 || Math.abs(raw[i].y - prev.y) > 1) {
548
553
  deduped.push(raw[i]);
549
554
  }
550
555
  }
551
556
 
552
557
  // Remove collinear midpoints — keep only corners + endpoints
553
- const pts: { x: number; y: number }[] = [deduped[0]];
554
- for (let i = 1; i < deduped.length - 1; i++) {
555
- const prev = deduped[i - 1];
556
- const curr = deduped[i];
557
- const next = deduped[i + 1];
558
- const sameX = Math.abs(prev.x - curr.x) < 1 && Math.abs(curr.x - next.x) < 1;
559
- const sameY = Math.abs(prev.y - curr.y) < 1 && Math.abs(curr.y - next.y) < 1;
560
- if (!sameX || !sameY) pts.push(curr);
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]);
561
572
  }
562
- pts.push(deduped[deduped.length - 1]);
563
573
 
564
- if (pts.length < 2) return `M ${pts[0].x} ${pts[0].y}`;
574
+ if (pts.length === 0) return "";
575
+ if (pts.length === 1) return `M ${pts[0].x} ${pts[0].y}`;
565
576
 
566
- // 2 points: simple bezier with offset control points
577
+ // 2 points: simple bezier with offset control points scaled to distance
567
578
  if (pts.length === 2) {
568
579
  const [s, t] = pts;
569
580
  const dx = t.x - s.x;
570
581
  const dy = t.y - s.y;
571
- const offset = Math.max(50, Math.max(Math.abs(dx), Math.abs(dy)) * 0.5);
582
+ const offset = Math.max(30, Math.max(Math.abs(dx), Math.abs(dy)) * 0.4);
572
583
  if (Math.abs(dx) >= Math.abs(dy)) {
573
584
  const sign = dx >= 0 ? 1 : -1;
574
585
  return `M ${s.x} ${s.y} C ${s.x + offset * sign} ${s.y}, ${t.x - offset * sign} ${t.y}, ${t.x} ${t.y}`;
@@ -577,10 +588,6 @@ export class PathBuilder {
577
588
  return `M ${s.x} ${s.y} C ${s.x} ${s.y + offset * sign}, ${t.x} ${t.y - offset * sign}, ${t.x} ${t.y}`;
578
589
  }
579
590
 
580
- // 3+ points: smooth cubic bezier spline through all corner points.
581
- // For each segment i→i+1, compute tangent-based control points using
582
- // neighbors (Catmull-Rom style, tension 0.3).
583
- const tension = 0.3;
584
591
  const dist = (a: { x: number; y: number }, b: { x: number; y: number }) =>
585
592
  Math.hypot(b.x - a.x, b.y - a.y);
586
593
 
@@ -594,10 +601,21 @@ export class PathBuilder {
594
601
 
595
602
  const segLen = dist(p1, p2);
596
603
 
597
- // Tangent at p1: direction from p0 to p2
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
598
617
  let cp1x = p1.x + (p2.x - p0.x) * tension;
599
618
  let cp1y = p1.y + (p2.y - p0.y) * tension;
600
- // Tangent at p2: direction from p1 to p3
601
619
  let cp2x = p2.x - (p3.x - p1.x) * tension;
602
620
  let cp2y = p2.y - (p3.y - p1.y) * tension;
603
621
 
@@ -1023,7 +1041,7 @@ export class RoutingEngine {
1023
1041
  const labelP = gridSize > 0 ? Geometry.snapToGrid(midP.x, midP.y, gridSize) : midP;
1024
1042
  const first = points[0];
1025
1043
  const last = points[points.length - 1];
1026
- 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 };
1027
1045
  }
1028
1046
 
1029
1047
  RoutingEngine.cleanup(router, connRefs, shapeRefList);
@@ -1229,7 +1247,7 @@ export class PersistentRouter {
1229
1247
  const labelP = gridSize > 0 ? Geometry.snapToGrid(midP.x, midP.y, gridSize) : midP;
1230
1248
  const first = points[0];
1231
1249
  const last = points[points.length - 1];
1232
- 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 };
1233
1251
  }
1234
1252
  return result;
1235
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
- return [route.path, route.labelX, route.labelY, true];
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
  }