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 +21 -0
- package/README.md +242 -0
- package/package.json +24 -0
- package/src/constants.ts +6 -0
- package/src/edge-routing-store.ts +38 -0
- package/src/edge-routing.worker.ts +206 -0
- package/src/index.ts +44 -0
- package/src/resolve-collisions.ts +215 -0
- package/src/routing-core.ts +1050 -0
- package/src/use-edge-routing.ts +303 -0
- package/src/use-routed-edge-path.ts +99 -0
- package/src/use-routing-worker.ts +110 -0
- package/src/worker-listener.ts +48 -0
- package/src/worker-messages.ts +21 -0
- package/src/worker-polyfill.ts +8 -0
- package/tsconfig.json +16 -0
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
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -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";
|