reactflow-edge-routing 0.1.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Awais Shah
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,242 @@
1
+ # reactflow-edge-routing
2
+
3
+ Obstacle-aware edge routing for [React Flow](https://reactflow.dev/). Edges automatically route around nodes using orthogonal, polyline, or bezier paths.
4
+
5
+ Powered by [`obstacle-router`](../obstacle-router) (TypeScript port of libavoid).
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install reactflow-edge-routing obstacle-router
11
+ # or
12
+ yarn add reactflow-edge-routing obstacle-router
13
+ ```
14
+
15
+ **Peer dependencies:** `@xyflow/react ^12.0.0`, `react ^18.0.0 || ^19.0.0`
16
+
17
+ ## Quick Start
18
+
19
+ ```tsx
20
+ import { ReactFlow, applyNodeChanges, applyEdgeChanges } from "@xyflow/react";
21
+ import { useEdgeRouting } from "reactflow-edge-routing";
22
+ import { RoutedEdge } from "./RoutedEdge"; // your custom edge component
23
+
24
+ const edgeTypes = { routed: RoutedEdge };
25
+
26
+ function Flow() {
27
+ const [nodes, setNodes] = useState(initialNodes);
28
+ const [edges, setEdges] = useState(initialEdges); // edges should have type: "routed"
29
+
30
+ const { updateRoutingOnNodesChange, resetRouting } = useEdgeRouting(nodes, edges, {
31
+ edgeRounding: 12,
32
+ edgeToEdgeSpacing: 4,
33
+ edgeToNodeSpacing: 8,
34
+ autoBestSideConnection: true,
35
+ });
36
+
37
+ const onNodesChange = useCallback((changes) => {
38
+ setNodes((nds) => applyNodeChanges(changes, nds));
39
+ updateRoutingOnNodesChange(changes);
40
+ }, [updateRoutingOnNodesChange]);
41
+
42
+ return (
43
+ <ReactFlow
44
+ nodes={nodes}
45
+ edges={edges}
46
+ onNodesChange={onNodesChange}
47
+ onNodeDragStop={() => resetRouting()}
48
+ edgeTypes={edgeTypes}
49
+ />
50
+ );
51
+ }
52
+ ```
53
+
54
+ ### Custom Edge Component
55
+
56
+ ```tsx
57
+ import { useRoutedEdgePath } from "reactflow-edge-routing";
58
+ import { BaseEdge, type EdgeProps } from "@xyflow/react";
59
+
60
+ export function RoutedEdge({ id, sourceX, sourceY, targetX, targetY, ...props }: EdgeProps) {
61
+ const [path, labelX, labelY] = useRoutedEdgePath({
62
+ id, sourceX, sourceY, targetX, targetY,
63
+ });
64
+
65
+ return <BaseEdge id={id} path={path} labelX={labelX} labelY={labelY} {...props} />;
66
+ }
67
+ ```
68
+
69
+ ## Architecture
70
+
71
+ ```
72
+ FlowCanvas (React)
73
+ useEdgeRouting(nodes, edges, options)
74
+ enrichNode -> adds _handlePins, _extraHeight
75
+ RoutingEngine / PersistentRouter -> obstacle-router
76
+ store -> routes: { [edgeId]: AvoidRoute }
77
+
78
+ Edge Components
79
+ useRoutedEdgePath(id, sourceX, ...)
80
+ routed path (from store)
81
+ fallback: getSmoothStepPath
82
+
83
+ onNodeDragStop -> resolveCollisions + resetRouting
84
+ ```
85
+
86
+ ## API
87
+
88
+ ### `useEdgeRouting(nodes, edges, options?)`
89
+
90
+ Main hook. Computes routed paths for all edges and stores them in a Zustand store.
91
+
92
+ **Returns:**
93
+
94
+ | Property | Description |
95
+ |---|---|
96
+ | `updateRoutingOnNodesChange(changes)` | Call from `onNodesChange` to trigger re-routing |
97
+ | `resetRouting()` | Full re-route (call after drag stop, layout changes) |
98
+ | `refreshRouting()` | Re-route without rebuilding the router |
99
+ | `updateRoutingForNodeIds(ids)` | Re-route only edges connected to specific nodes |
100
+
101
+ ### `useRoutedEdgePath(params)`
102
+
103
+ Returns the routed SVG path for a single edge.
104
+
105
+ ```typescript
106
+ const [path, labelX, labelY] = useRoutedEdgePath({
107
+ id, sourceX, sourceY, targetX, targetY,
108
+ });
109
+ ```
110
+
111
+ Falls back to a smooth step path if no routed path is available yet.
112
+
113
+ ### `resolveCollisions(nodes, options?)`
114
+
115
+ Pushes overlapping nodes apart after layout or drag.
116
+
117
+ ```typescript
118
+ import { resolveCollisions } from "reactflow-edge-routing";
119
+
120
+ const fixed = resolveCollisions(nodes, {
121
+ maxIterations: 50,
122
+ overlapThreshold: 0.5,
123
+ margin: 20,
124
+ });
125
+ ```
126
+
127
+ ## Options
128
+
129
+ ### Core Spacing
130
+
131
+ | Option | Default | Description |
132
+ |---|---|---|
133
+ | `edgeToEdgeSpacing` | 10 | Distance (px) between parallel edge segments |
134
+ | `edgeToNodeSpacing` | 8 | Buffer distance (px) between edges and node boundaries |
135
+ | `handleSpacing` | 2 | Spacing (px) between edges at shared handles |
136
+
137
+ ### Connector Settings
138
+
139
+ | Option | Default | Description |
140
+ |---|---|---|
141
+ | `connectorType` | `"orthogonal"` | Edge style: `"orthogonal"`, `"polyline"`, or `"bezier"` |
142
+ | `hateCrossings` | `false` | If true, connectors prefer longer paths to avoid crossings |
143
+ | `pinInsideOffset` | 0 | Offset (px) pushing connector start inside shape boundary |
144
+
145
+ ### Rendering
146
+
147
+ | Option | Default | Description |
148
+ |---|---|---|
149
+ | `edgeRounding` | 8 | Corner radius (px) for orthogonal bends |
150
+ | `diagramGridSize` | 0 | Snap waypoints to grid (0 = no grid) |
151
+ | `shouldSplitEdgesNearHandle` | `true` | When true, edges fan out at handles. When false, edges converge to a single point |
152
+ | `stubSize` | 20 | Length (px) of stub segment when `shouldSplitEdgesNearHandle` is off |
153
+ | `autoBestSideConnection` | `true` | Auto-detect best handle side based on relative node positions |
154
+ | `debounceMs` | 0 | Debounce delay (ms) for routing updates |
155
+
156
+ ### Routing Penalties
157
+
158
+ | Option | Default | Description |
159
+ |---|---|---|
160
+ | `segmentPenalty` | 10 | Penalty per path segment. Must be >0 for nudging |
161
+ | `anglePenalty` | 0 | Penalty for non-straight bends |
162
+ | `crossingPenalty` | 0 | Penalty for crossing other connectors |
163
+ | `reverseDirectionPenalty` | 0 | Penalty for routing away from destination |
164
+
165
+ ### Nudging Options
166
+
167
+ | Option | Default | Description |
168
+ |---|---|---|
169
+ | `nudgeOrthogonalSegmentsConnectedToShapes` | `true` | Nudge final segments at shape boundaries |
170
+ | `nudgeSharedPathsWithCommonEndPoint` | `true` | Nudge segments sharing an endpoint |
171
+ | `performUnifyingNudgingPreprocessingStep` | `true` | Unify segments before nudging |
172
+ | `nudgeOrthogonalTouchingColinearSegments` | `false` | Nudge colinear touching segments apart |
173
+
174
+ ### Other
175
+
176
+ | Option | Default | Description |
177
+ |---|---|---|
178
+ | `realTimeRouting` | `false` | Re-route in real time while dragging |
179
+ | `enrichNode` | - | Function to add `_handlePins` and `_extraHeight` to nodes |
180
+
181
+ ## Multi-Handle Nodes
182
+
183
+ For nodes with multiple handles, provide an `enrichNode` function that computes pin positions:
184
+
185
+ ```tsx
186
+ const enrichNode = useCallback((node) => {
187
+ const internal = getInternalNode(node.id);
188
+ if (!internal) return node;
189
+ // compute _handlePins from DOM handle positions
190
+ return { ...node, _handlePins: computePins(internal) };
191
+ }, [getInternalNode]);
192
+
193
+ useEdgeRouting(nodes, edges, { enrichNode });
194
+ ```
195
+
196
+ Each pin describes a handle's proportional position on the node:
197
+
198
+ ```typescript
199
+ type HandlePin = {
200
+ handleId: string; // matches edge.sourceHandle / edge.targetHandle
201
+ xPct: number; // 0-1 from left edge
202
+ yPct: number; // 0-1 from top edge
203
+ side: "left" | "right" | "top" | "bottom";
204
+ };
205
+ ```
206
+
207
+ ## Exports
208
+
209
+ ### Classes
210
+
211
+ - `RoutingEngine` - One-shot routing (builds router, routes, disposes)
212
+ - `PersistentRouter` - Incremental routing (reuses router across updates)
213
+ - `Geometry` - Node bounds, handle positions, best-side detection
214
+ - `PathBuilder` - SVG path generation (orthogonal, bezier, polyline)
215
+ - `HandleSpacing` - Fan-out spacing adjustment at shared handles
216
+
217
+ ### Hooks
218
+
219
+ - `useEdgeRouting` - Main routing hook
220
+ - `useRoutedEdgePath` - Per-edge path hook
221
+ - `useRoutingWorker` - Low-level worker management
222
+
223
+ ### Stores
224
+
225
+ - `useEdgeRoutingStore` - Zustand store for routed paths
226
+ - `useEdgeRoutingActionsStore` - Zustand store for routing actions
227
+
228
+ ### Functions
229
+
230
+ - `resolveCollisions` - Push overlapping nodes apart
231
+ - `attachWorkerListener` - Wire up a Web Worker for background routing
232
+
233
+ ### Types
234
+
235
+ - `AvoidRoute`, `AvoidRouterOptions`, `FlowNode`, `FlowEdge`
236
+ - `HandlePin`, `HandlePosition`, `ConnectorType`
237
+ - `UseEdgeRoutingOptions`, `UseEdgeRoutingResult`
238
+ - `CollisionAlgorithmOptions`, `CollisionAlgorithm`
239
+
240
+ ## License
241
+
242
+ MIT
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "reactflow-edge-routing",
3
+ "version": "0.1.1",
4
+ "description": "Orthogonal edge routing for React Flow using obstacle-router. Edges route around nodes while pins stay fixed at their anchor points.",
5
+ "main": "./src/index.ts",
6
+ "types": "./src/index.ts",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "lint": "tsc --noEmit"
10
+ },
11
+ "dependencies": {
12
+ "obstacle-router": "*",
13
+ "zustand": "^5.0.0"
14
+ },
15
+ "peerDependencies": {
16
+ "@xyflow/react": "^12.0.0",
17
+ "react": "^18.0.0 || ^19.0.0"
18
+ },
19
+ "license": "MIT",
20
+ "devDependencies": {
21
+ "@types/react": "^19.0.0",
22
+ "typescript": "^5.7.0"
23
+ }
24
+ }
@@ -0,0 +1,6 @@
1
+ /** Debounce (ms) before routing runs after diagram changes.
2
+ * Must be > 0 so React has time to apply position state before we read nodesRef. */
3
+ export const DEBOUNCE_ROUTING_MS = 16;
4
+
5
+ /** Default border radius (px) for routed path corners. */
6
+ export const EDGE_BORDER_RADIUS = 0;
@@ -0,0 +1,38 @@
1
+ import { create } from "zustand";
2
+ import type { AvoidRoute, ConnectorType } from "./routing-core";
3
+
4
+ export interface EdgeRoutingState {
5
+ loaded: boolean;
6
+ routes: Record<string, AvoidRoute>;
7
+ connectorType: ConnectorType;
8
+ /** Node IDs currently being dragged — edges connected to these show fallback */
9
+ draggingNodeIds: Set<string>;
10
+ setLoaded: (loaded: boolean) => void;
11
+ setRoutes: (routes: Record<string, AvoidRoute>) => void;
12
+ setConnectorType: (type: ConnectorType) => void;
13
+ setDraggingNodeIds: (ids: Set<string>) => void;
14
+ }
15
+
16
+ export const useEdgeRoutingStore = create<EdgeRoutingState>((set) => ({
17
+ loaded: false,
18
+ routes: {},
19
+ connectorType: "orthogonal",
20
+ draggingNodeIds: new Set(),
21
+ setLoaded: (loaded) => set({ loaded }),
22
+ setRoutes: (routes) => set({ routes }),
23
+ setConnectorType: (connectorType) => set({ connectorType }),
24
+ setDraggingNodeIds: (draggingNodeIds) => set({ draggingNodeIds }),
25
+ }));
26
+
27
+ export interface EdgeRoutingActions {
28
+ resetRouting: () => void;
29
+ updateRoutesForNodeId: (nodeId: string) => void;
30
+ }
31
+
32
+ export const useEdgeRoutingActionsStore = create<{
33
+ actions: EdgeRoutingActions;
34
+ setActions: (a: EdgeRoutingActions) => void;
35
+ }>((set) => ({
36
+ actions: { resetRouting: () => {}, updateRoutesForNodeId: () => {} },
37
+ setActions: (actions) => set({ actions }),
38
+ }));
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Web Worker: handles routing commands using libavoid-js (pure TypeScript).
3
+ * Uses PersistentRouter to prevent heap growth from repeated alloc/free.
4
+ *
5
+ * Pins stay fixed at anchor points — routes go around nodes, not through pins.
6
+ */
7
+
8
+ import {
9
+ type AvoidRoute,
10
+ type AvoidRouterOptions,
11
+ type FlowNode,
12
+ type FlowEdge,
13
+ PersistentRouter,
14
+ } from "./routing-core";
15
+
16
+ // ---- Worker command types ----
17
+
18
+ type WorkerCommand =
19
+ | { command: "reset"; nodes?: FlowNode[]; edges?: FlowEdge[]; options?: AvoidRouterOptions }
20
+ | { command: "change"; cell: FlowNode | FlowEdge }
21
+ | { command: "remove"; id: string }
22
+ | { command: "add"; cell: FlowNode | FlowEdge }
23
+ | { command: "updateNodes"; nodes?: FlowNode[] }
24
+ | { command: "route"; nodes?: FlowNode[]; edges?: FlowEdge[]; options?: AvoidRouterOptions }
25
+ | { command: "close" };
26
+
27
+ // ---- Initialization ----
28
+
29
+ console.log("[edge-routing worker] Worker script started (pure TS, no WASM)");
30
+ postMessage({ command: "loaded", success: true } as const);
31
+
32
+ // ---- Internal model ----
33
+
34
+ let currentNodes: FlowNode[] = [];
35
+ let currentEdges: FlowEdge[] = [];
36
+ let currentOptions: AvoidRouterOptions = {};
37
+ let nodeIndex = new Map<string, number>();
38
+ let edgeIndex = new Map<string, number>();
39
+ let topologyDirty = true;
40
+ let positionDirty = false;
41
+ let pendingNodeUpdates: FlowNode[] = [];
42
+
43
+ const persistentRouter = new PersistentRouter();
44
+
45
+ function rebuildIndices() {
46
+ nodeIndex = new Map(currentNodes.map((n, i) => [n.id, i]));
47
+ edgeIndex = new Map(currentEdges.map((e, i) => [e.id, i]));
48
+ }
49
+
50
+ function isNode(cell: FlowNode | FlowEdge): cell is FlowNode {
51
+ return "position" in cell && ("width" in cell || "measured" in cell || !("source" in cell));
52
+ }
53
+
54
+ function doRoute(): Record<string, AvoidRoute> {
55
+ if (currentEdges.length === 0) return {};
56
+ try {
57
+ if (topologyDirty) {
58
+ topologyDirty = false;
59
+ positionDirty = false;
60
+ pendingNodeUpdates = [];
61
+ return persistentRouter.reset(currentNodes, currentEdges, currentOptions);
62
+ } else if (positionDirty && pendingNodeUpdates.length > 0) {
63
+ positionDirty = false;
64
+ const updates = pendingNodeUpdates;
65
+ pendingNodeUpdates = [];
66
+ return persistentRouter.updateNodes(updates);
67
+ }
68
+ return persistentRouter.reset(currentNodes, currentEdges, currentOptions);
69
+ } catch {
70
+ return {};
71
+ }
72
+ }
73
+
74
+ // ---- Debounce ----
75
+
76
+ const DEFAULT_DEBOUNCE_MS = 0;
77
+
78
+ function getDebounceMs(): number {
79
+ return currentOptions.debounceMs ?? DEFAULT_DEBOUNCE_MS;
80
+ }
81
+
82
+ let debounceTimer: ReturnType<typeof setTimeout> | null = null;
83
+ function isPending() { return debounceTimer != null; }
84
+ function cancelDebounce() {
85
+ if (debounceTimer != null) { clearTimeout(debounceTimer); debounceTimer = null; }
86
+ }
87
+
88
+ function debouncedRoute() {
89
+ cancelDebounce();
90
+ debounceTimer = setTimeout(() => {
91
+ debounceTimer = null;
92
+ const routes = doRoute();
93
+ setTimeout(() => {
94
+ if (!isPending()) {
95
+ postMessage({ command: "routed", routes } as const);
96
+ }
97
+ }, 0);
98
+ }, getDebounceMs());
99
+ }
100
+
101
+ // ---- Message handler ----
102
+
103
+ onmessage = (e: MessageEvent<WorkerCommand>) => {
104
+ const msg = e.data;
105
+ if (!msg || typeof msg !== "object" || !("command" in msg)) return;
106
+
107
+ switch (msg.command) {
108
+ case "reset":
109
+ currentNodes = msg.nodes ?? [];
110
+ currentEdges = msg.edges ?? [];
111
+ if (msg.options) currentOptions = msg.options;
112
+ rebuildIndices();
113
+ topologyDirty = true;
114
+ pendingNodeUpdates = [];
115
+ debouncedRoute();
116
+ break;
117
+
118
+ case "change": {
119
+ const cell = msg.cell;
120
+ if (isNode(cell)) {
121
+ const i = nodeIndex.get(cell.id);
122
+ if (i != null) {
123
+ currentNodes[i] = { ...currentNodes[i], ...cell };
124
+ if (!topologyDirty) { positionDirty = true; pendingNodeUpdates.push(currentNodes[i]); }
125
+ } else {
126
+ nodeIndex.set(cell.id, currentNodes.length);
127
+ currentNodes.push(cell);
128
+ topologyDirty = true;
129
+ }
130
+ } else {
131
+ const i = edgeIndex.get(cell.id);
132
+ if (i != null) currentEdges[i] = { ...currentEdges[i], ...cell };
133
+ else { edgeIndex.set(cell.id, currentEdges.length); currentEdges.push(cell); }
134
+ topologyDirty = true;
135
+ }
136
+ debouncedRoute();
137
+ break;
138
+ }
139
+
140
+ case "remove": {
141
+ const id = msg.id;
142
+ currentNodes = currentNodes.filter((n) => n.id !== id);
143
+ currentEdges = currentEdges.filter((ed) => ed.id !== id);
144
+ rebuildIndices();
145
+ topologyDirty = true;
146
+ pendingNodeUpdates = [];
147
+ debouncedRoute();
148
+ break;
149
+ }
150
+
151
+ case "add": {
152
+ const cell = msg.cell;
153
+ if (isNode(cell)) {
154
+ if (!nodeIndex.has(cell.id)) { nodeIndex.set(cell.id, currentNodes.length); currentNodes.push(cell); }
155
+ } else {
156
+ if (!edgeIndex.has(cell.id)) { edgeIndex.set(cell.id, currentEdges.length); currentEdges.push(cell); }
157
+ }
158
+ topologyDirty = true;
159
+ debouncedRoute();
160
+ break;
161
+ }
162
+
163
+ case "updateNodes": {
164
+ const updatedNodes = msg.nodes ?? [];
165
+ for (const updated of updatedNodes) {
166
+ const i = nodeIndex.get(updated.id);
167
+ if (i != null) {
168
+ currentNodes[i] = { ...currentNodes[i], ...updated };
169
+ if (!topologyDirty) { positionDirty = true; pendingNodeUpdates.push(currentNodes[i]); }
170
+ } else {
171
+ nodeIndex.set(updated.id, currentNodes.length);
172
+ currentNodes.push(updated);
173
+ topologyDirty = true;
174
+ }
175
+ }
176
+ debouncedRoute();
177
+ break;
178
+ }
179
+
180
+ case "route": {
181
+ const routeNodes = msg.nodes ?? [];
182
+ const routeEdges = msg.edges ?? [];
183
+ const routeOptions = msg.options ?? currentOptions;
184
+ if (routeEdges.length === 0) {
185
+ postMessage({ command: "routed", routes: {} } as const);
186
+ break;
187
+ }
188
+ try {
189
+ const routes = persistentRouter.reset(routeNodes, routeEdges, routeOptions);
190
+ postMessage({ command: "routed", routes } as const);
191
+ } catch {
192
+ postMessage({ command: "routed", routes: {} } as const);
193
+ }
194
+ break;
195
+ }
196
+
197
+ case "close":
198
+ cancelDebounce();
199
+ persistentRouter.destroy();
200
+ self.close();
201
+ break;
202
+
203
+ default:
204
+ break;
205
+ }
206
+ };
package/src/index.ts ADDED
@@ -0,0 +1,44 @@
1
+ // Core routing
2
+ export {
3
+ Geometry,
4
+ PathBuilder,
5
+ HandleSpacing,
6
+ RoutingEngine,
7
+ PersistentRouter,
8
+ } from "./routing-core";
9
+ export type {
10
+ AvoidRoute,
11
+ AvoidRouterOptions,
12
+ HandlePosition,
13
+ FlowNode,
14
+ FlowEdge,
15
+ HandlePin,
16
+ ConnectorType,
17
+ } from "./routing-core";
18
+
19
+ // Store
20
+ export { useEdgeRoutingStore, useEdgeRoutingActionsStore } from "./edge-routing-store";
21
+ export type { EdgeRoutingState, EdgeRoutingActions } from "./edge-routing-store";
22
+
23
+ // Worker messages
24
+ export type { EdgeRoutingWorkerCommand, EdgeRoutingWorkerResponse } from "./worker-messages";
25
+
26
+ // Worker listener
27
+ export { attachWorkerListener } from "./worker-listener";
28
+
29
+ // Hooks
30
+ export { useRoutingWorker } from "./use-routing-worker";
31
+ export type { UseRoutingWorkerOptions, UseRoutingWorkerResult } from "./use-routing-worker";
32
+
33
+ export { useEdgeRouting } from "./use-edge-routing";
34
+ export type { UseEdgeRoutingOptions, UseEdgeRoutingResult } from "./use-edge-routing";
35
+
36
+ export { useRoutedEdgePath } from "./use-routed-edge-path";
37
+ export type { UseRoutedEdgePathParams } from "./use-routed-edge-path";
38
+
39
+ // Collision resolution
40
+ export { resolveCollisions } from "./resolve-collisions";
41
+ export type { CollisionAlgorithmOptions, CollisionAlgorithm } from "./resolve-collisions";
42
+
43
+ // Constants
44
+ export { DEBOUNCE_ROUTING_MS, EDGE_BORDER_RADIUS } from "./constants";