@statelyai/layout 0.3.0 → 0.4.0
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/NOTICE.md +4 -2
- package/README.md +150 -11
- package/dist/elkjs/index.cjs +2 -2
- package/dist/elkjs/index.d.cts +2 -0
- package/dist/elkjs/index.d.mts +2 -0
- package/dist/elkjs/index.mjs +2 -2
- package/dist/elkjs/main.cjs +1 -1
- package/dist/elkjs/main.mjs +1 -1
- package/dist/elkjs/worker.cjs +2 -2
- package/dist/elkjs/worker.mjs +2 -2
- package/dist/elkjs-UciYx7lD.mjs +2973 -0
- package/dist/elkjs-ggjsBuVP.cjs +3002 -0
- package/dist/{index-BEA_pzeT.d.cts → index-B6cWNrMw.d.cts} +9 -1
- package/dist/{index-DNTLRsDl.d.mts → index-CJQLUKD9.d.mts} +9 -1
- package/dist/{index-DNixtE3h.d.mts → index-DT6lPMfS.d.cts} +85 -5
- package/dist/{index-D9dhxrRl.d.cts → index-F1U-fCTu.d.mts} +85 -5
- package/dist/index.cjs +6 -3
- package/dist/index.d.cts +3 -3
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +4 -4
- package/dist/layered/index.cjs +3 -1
- package/dist/layered/index.d.cts +2 -2
- package/dist/layered/index.d.mts +2 -2
- package/dist/layered/index.mjs +2 -2
- package/dist/{layered-DGFTeQSl.mjs → layered-C7ERzZjR.mjs} +9605 -1655
- package/dist/{layered-B2buJqMf.cjs → layered-CBPg6_6_.cjs} +12481 -4435
- package/dist/{layout-engine-Dn1PiFwP.cjs → layout-engine-DAw06RhN.cjs} +5 -18
- package/dist/{layout-engine-Cs5tatA6.mjs → layout-engine-DC3IYkIy.mjs} +3 -10
- package/dist/routing/index.cjs +2 -1
- package/dist/routing/index.d.cts +2 -2
- package/dist/routing/index.d.mts +2 -2
- package/dist/routing/index.mjs +2 -2
- package/dist/{routing-A2hrK9MP.mjs → routing-CR8flv8b.mjs} +977 -593
- package/dist/{routing-ujEP7Qh6.cjs → routing-QIdGGFBe.cjs} +986 -596
- package/package.json +8 -2
- package/src/authoring/coordinates.ts +28 -8
- package/src/authoring/partial.ts +7 -2
- package/src/authoring/routing.ts +4 -1
- package/src/constraints.ts +174 -6
- package/src/elkjs/index.ts +1392 -449
- package/src/elkjs/layout-quality.ts +219 -0
- package/src/elkjs/opposing-tracks.ts +161 -0
- package/src/elkjs/orthogonal-junctions.ts +174 -0
- package/src/elkjs/statechart.ts +9 -3
- package/src/elkjs/types.ts +2 -0
- package/src/index.ts +2 -0
- package/src/internal/elkjs-compatibility.ts +4 -1
- package/src/internal/layered-trace.ts +48 -0
- package/src/internal/layout-engine.ts +2 -1
- package/src/java-random.ts +6 -0
- package/src/layered/bk-node-placement.ts +403 -92
- package/src/layered/center-labels.ts +398 -0
- package/src/layered/compaction-bounds.ts +26 -0
- package/src/layered/compaction-errors.ts +2 -0
- package/src/layered/compaction-scanline.ts +63 -0
- package/src/layered/compound-boundaries.ts +99 -0
- package/src/layered/compound.ts +35 -7
- package/src/layered/constraint-orientation.ts +141 -0
- package/src/layered/crossing-constraints.ts +243 -0
- package/src/layered/crossing-counter.ts +221 -0
- package/src/layered/crossing-graph.ts +401 -0
- package/src/layered/cycle-random.ts +22 -0
- package/src/layered/elk-port-lists.ts +243 -0
- package/src/layered/end-labels.ts +124 -0
- package/src/layered/external-port-dummy.ts +137 -0
- package/src/layered/feedback-chains.ts +114 -0
- package/src/layered/fixed-self-loop.ts +150 -0
- package/src/layered/greedy-switch-decider.ts +356 -0
- package/src/layered/grouped-compaction.ts +787 -0
- package/src/layered/gwt-sort.ts +53 -0
- package/src/layered/hierarchical-port-phases.ts +242 -0
- package/src/layered/hierarchical-port-restoration.ts +233 -0
- package/src/layered/hierarchical-port-routing.ts +179 -0
- package/src/layered/hierarchy-crossing.ts +121 -0
- package/src/layered/hierarchy-sweepiness.ts +83 -0
- package/src/layered/hints.ts +371 -0
- package/src/layered/hyperedge-dummy-merger.ts +195 -0
- package/src/layered/index.ts +886 -161
- package/src/layered/initial-model-order.ts +404 -0
- package/src/layered/inverted-ports.ts +207 -0
- package/src/layered/label-dummy-switcher.ts +200 -0
- package/src/layered/long-edges.ts +135 -44
- package/src/layered/loop-envelopes.ts +184 -0
- package/src/layered/native-routing.ts +275 -0
- package/src/layered/network-simplex.ts +11 -15
- package/src/layered/node-margins.ts +59 -0
- package/src/layered/north-south-ports.ts +271 -0
- package/src/layered/opposing-tracks.ts +944 -0
- package/src/layered/orthogonal-cycle-order.ts +106 -0
- package/src/layered/orthogonal-hypersegments.ts +92 -0
- package/src/layered/orthogonal-junctions.ts +202 -0
- package/src/layered/orthogonal-segments.ts +243 -0
- package/src/layered/placement-orientation.ts +22 -0
- package/src/layered/port-distributor.ts +192 -0
- package/src/layered/port-edge-order.ts +20 -0
- package/src/layered/replace-routing.ts +45 -0
- package/src/layered/routing-coordinates.ts +32 -0
- package/src/layered/separate-exterior-labels.ts +24 -6
- package/src/layered/spacing.ts +34 -12
- package/src/layered/splitter-order.ts +106 -0
- package/src/layered/strategies.ts +2024 -883
- package/src/layered/types.ts +13 -0
- package/src/layered/weighted-compaction.ts +84 -0
- package/src/routing/adapters.ts +32 -0
- package/src/routing/algorithms.ts +433 -21
- package/src/routing/coordination.ts +8 -3
- package/src/routing/index.ts +1 -0
- package/src/routing/search.ts +93 -11
- package/dist/elkjs-DaHMISnQ.mjs +0 -1677
- package/dist/elkjs-dMghiiWW.cjs +0 -1706
|
@@ -14,6 +14,489 @@ var UnsupportedLayoutError = class extends LayoutError {
|
|
|
14
14
|
}
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region src/authoring/coordinates.ts
|
|
19
|
+
/** Legacy sibling edges are parent-relative; native compound output marks world-space edges. */
|
|
20
|
+
function frames(graph) {
|
|
21
|
+
const nodes = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
22
|
+
const offsets = /* @__PURE__ */ new Map();
|
|
23
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
24
|
+
function offset(id) {
|
|
25
|
+
const cached = offsets.get(id);
|
|
26
|
+
if (cached) return cached;
|
|
27
|
+
if (visiting.has(id)) throw new LayoutError(`Cyclic parent hierarchy at ${id}`, "INVALID_GRAPH");
|
|
28
|
+
visiting.add(id);
|
|
29
|
+
const parentId = nodes.get(id)?.parentId;
|
|
30
|
+
if (parentId == null) {
|
|
31
|
+
const origin = {
|
|
32
|
+
x: 0,
|
|
33
|
+
y: 0
|
|
34
|
+
};
|
|
35
|
+
visiting.delete(id);
|
|
36
|
+
offsets.set(id, origin);
|
|
37
|
+
return origin;
|
|
38
|
+
}
|
|
39
|
+
const parent = nodes.get(parentId);
|
|
40
|
+
const ancestor = offset(parentId);
|
|
41
|
+
const result = {
|
|
42
|
+
x: ancestor.x + parent.x,
|
|
43
|
+
y: ancestor.y + parent.y
|
|
44
|
+
};
|
|
45
|
+
visiting.delete(id);
|
|
46
|
+
offsets.set(id, result);
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
for (const node$1 of graph.nodes) offsets.set(node$1.id, offset(node$1.id));
|
|
50
|
+
return {
|
|
51
|
+
nodes: offsets,
|
|
52
|
+
edges: new Map(graph.edges.map((edge) => {
|
|
53
|
+
const source = nodes.get(edge.sourceId);
|
|
54
|
+
const target = nodes.get(edge.targetId);
|
|
55
|
+
return [edge.id, graph.edgeCoordinateSpace !== "world" && (source.parentId ?? null) === (target.parentId ?? null) ? offset(source.id) : {
|
|
56
|
+
x: 0,
|
|
57
|
+
y: 0
|
|
58
|
+
}];
|
|
59
|
+
}))
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function worldGeometry(graph) {
|
|
63
|
+
const offsets = frames(graph);
|
|
64
|
+
const base = { ...graph };
|
|
65
|
+
delete base.compoundRoutes;
|
|
66
|
+
delete base.compoundGeometry;
|
|
67
|
+
return {
|
|
68
|
+
...base,
|
|
69
|
+
nodes: graph.nodes.map((n) => {
|
|
70
|
+
const p = offsets.nodes.get(n.id);
|
|
71
|
+
return p.x === 0 && p.y === 0 ? n : {
|
|
72
|
+
...n,
|
|
73
|
+
x: n.x + p.x,
|
|
74
|
+
y: n.y + p.y
|
|
75
|
+
};
|
|
76
|
+
}),
|
|
77
|
+
edges: graph.edges.map((e) => {
|
|
78
|
+
const p = offsets.edges.get(e.id);
|
|
79
|
+
return p.x === 0 && p.y === 0 ? e : {
|
|
80
|
+
...e,
|
|
81
|
+
x: e.x + p.x,
|
|
82
|
+
y: e.y + p.y,
|
|
83
|
+
...e.points ? { points: e.points.map((v) => ({
|
|
84
|
+
x: v.x + p.x,
|
|
85
|
+
y: v.y + p.y
|
|
86
|
+
})) } : {}
|
|
87
|
+
};
|
|
88
|
+
})
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Convert solved world geometry back to parent-relative coordinates, using
|
|
93
|
+
* each parent's solved position. Untouched coordinates stay exactly as authored.
|
|
94
|
+
*/
|
|
95
|
+
function nativeGeometry(graph, original) {
|
|
96
|
+
const offsets = frames(original);
|
|
97
|
+
const world = worldGeometry(original);
|
|
98
|
+
const originalNodes = new Map(original.nodes.map((n) => [n.id, n]));
|
|
99
|
+
const originalEdges = new Map(original.edges.map((e) => [e.id, e]));
|
|
100
|
+
const worldNodes = new Map(world.nodes.map((n) => [n.id, n]));
|
|
101
|
+
const worldEdges = new Map(world.edges.map((e) => [e.id, e]));
|
|
102
|
+
const solved = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
103
|
+
const frame = (id) => {
|
|
104
|
+
const parentId = originalNodes.get(id)?.parentId;
|
|
105
|
+
const parent = parentId == null ? void 0 : solved.get(parentId);
|
|
106
|
+
return parent ? {
|
|
107
|
+
x: parent.x,
|
|
108
|
+
y: parent.y
|
|
109
|
+
} : {
|
|
110
|
+
x: 0,
|
|
111
|
+
y: 0
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
const same = (a, b) => a.x === b.x && a.y === b.y;
|
|
115
|
+
return {
|
|
116
|
+
...graph,
|
|
117
|
+
nodes: graph.nodes.map((n) => {
|
|
118
|
+
const p = frame(n.id), before = originalNodes.get(n.id), prior = worldNodes.get(n.id);
|
|
119
|
+
const kept = same(p, offsets.nodes.get(n.id));
|
|
120
|
+
return {
|
|
121
|
+
...n,
|
|
122
|
+
x: kept && n.x === prior.x ? before.x : n.x - p.x,
|
|
123
|
+
y: kept && n.y === prior.y ? before.y : n.y - p.y
|
|
124
|
+
};
|
|
125
|
+
}),
|
|
126
|
+
edges: graph.edges.map((e) => {
|
|
127
|
+
const old = offsets.edges.get(e.id), before = originalEdges.get(e.id), prior = worldEdges.get(e.id);
|
|
128
|
+
const source = originalNodes.get(e.sourceId), target = originalNodes.get(e.targetId);
|
|
129
|
+
const p = original.edgeCoordinateSpace !== "world" && (source.parentId ?? null) === (target.parentId ?? null) ? frame(source.id) : {
|
|
130
|
+
x: 0,
|
|
131
|
+
y: 0
|
|
132
|
+
};
|
|
133
|
+
const kept = same(p, old);
|
|
134
|
+
const points = kept && JSON.stringify(e.points) === JSON.stringify(prior.points) ? before.points : e.points?.map((v) => ({
|
|
135
|
+
x: v.x - p.x,
|
|
136
|
+
y: v.y - p.y
|
|
137
|
+
}));
|
|
138
|
+
return {
|
|
139
|
+
...e,
|
|
140
|
+
x: kept && e.x === prior.x ? before.x : e.x - p.x,
|
|
141
|
+
y: kept && e.y === prior.y ? before.y : e.y - p.y,
|
|
142
|
+
...points === void 0 ? {} : { points }
|
|
143
|
+
};
|
|
144
|
+
})
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
//#endregion
|
|
149
|
+
//#region src/routing/layout-cache.ts
|
|
150
|
+
const geometryByRoutes = /* @__PURE__ */ new WeakMap();
|
|
151
|
+
/** Compare only geometry and topology; application data may contain cycles. */
|
|
152
|
+
function geometryKey(graph) {
|
|
153
|
+
return JSON.stringify({
|
|
154
|
+
nodes: graph.nodes.map(({ id, parentId, x, y, width, height: height$1, ports }) => ({
|
|
155
|
+
id,
|
|
156
|
+
parentId,
|
|
157
|
+
x,
|
|
158
|
+
y,
|
|
159
|
+
width,
|
|
160
|
+
height: height$1,
|
|
161
|
+
ports: ports?.map(({ name, x: x$1, y: y$1, width: width$1, height: height$2 }) => ({
|
|
162
|
+
name,
|
|
163
|
+
x: x$1,
|
|
164
|
+
y: y$1,
|
|
165
|
+
width: width$1,
|
|
166
|
+
height: height$2
|
|
167
|
+
}))
|
|
168
|
+
})),
|
|
169
|
+
edges: graph.edges.map(({ id, sourceId, targetId, sourcePort, targetPort, x, y, width, height: height$1, points, routing }) => ({
|
|
170
|
+
id,
|
|
171
|
+
sourceId,
|
|
172
|
+
targetId,
|
|
173
|
+
sourcePort,
|
|
174
|
+
targetPort,
|
|
175
|
+
x,
|
|
176
|
+
y,
|
|
177
|
+
width,
|
|
178
|
+
height: height$1,
|
|
179
|
+
points,
|
|
180
|
+
routing
|
|
181
|
+
}))
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
/** Associate structured sections with the geometry that produced them. */
|
|
185
|
+
function recordRouteGeometry(graph, routes) {
|
|
186
|
+
geometryByRoutes.set(routes, geometryKey(graph));
|
|
187
|
+
}
|
|
188
|
+
/** Graph edits invalidate cached sections, including edits made by external consumers. */
|
|
189
|
+
function hasCurrentRouteGeometry(graph, routes) {
|
|
190
|
+
return geometryByRoutes.get(routes) === geometryKey(graph);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
//#endregion
|
|
194
|
+
//#region src/routing/persistent.ts
|
|
195
|
+
const height = (n) => n?.height ?? 0;
|
|
196
|
+
const size = (n) => n?.size ?? 0;
|
|
197
|
+
function node(key, value, left, right) {
|
|
198
|
+
return {
|
|
199
|
+
key,
|
|
200
|
+
value,
|
|
201
|
+
left,
|
|
202
|
+
right,
|
|
203
|
+
height: 1 + Math.max(height(left), height(right)),
|
|
204
|
+
size: 1 + size(left) + size(right)
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
function balance(n) {
|
|
208
|
+
if (height(n.left) - height(n.right) > 1) {
|
|
209
|
+
let l = n.left;
|
|
210
|
+
if (height(l.right) > height(l.left)) {
|
|
211
|
+
const r = l.right;
|
|
212
|
+
l = node(r.key, r.value, node(l.key, l.value, l.left, r.left), r.right);
|
|
213
|
+
}
|
|
214
|
+
return node(l.key, l.value, l.left, node(n.key, n.value, l.right, n.right));
|
|
215
|
+
}
|
|
216
|
+
if (height(n.right) - height(n.left) > 1) {
|
|
217
|
+
let r = n.right;
|
|
218
|
+
if (height(r.left) > height(r.right)) {
|
|
219
|
+
const l = r.left;
|
|
220
|
+
r = node(l.key, l.value, l.left, node(r.key, r.value, l.right, r.right));
|
|
221
|
+
}
|
|
222
|
+
return node(r.key, r.value, node(n.key, n.value, n.left, r.left), r.right);
|
|
223
|
+
}
|
|
224
|
+
return n;
|
|
225
|
+
}
|
|
226
|
+
function put(n, key, value) {
|
|
227
|
+
if (!n) return node(key, value);
|
|
228
|
+
if (key === n.key) return Object.is(value, n.value) ? n : node(key, value, n.left, n.right);
|
|
229
|
+
return balance(key < n.key ? node(n.key, n.value, put(n.left, key, value), n.right) : node(n.key, n.value, n.left, put(n.right, key, value)));
|
|
230
|
+
}
|
|
231
|
+
function remove(n, key) {
|
|
232
|
+
if (!n) return n;
|
|
233
|
+
if (key < n.key) return balance(node(n.key, n.value, remove(n.left, key), n.right));
|
|
234
|
+
if (key > n.key) return balance(node(n.key, n.value, n.left, remove(n.right, key)));
|
|
235
|
+
if (!n.left) return n.right;
|
|
236
|
+
if (!n.right) return n.left;
|
|
237
|
+
let successor = n.right;
|
|
238
|
+
while (successor.left) successor = successor.left;
|
|
239
|
+
return balance(node(successor.key, successor.value, n.left, remove(n.right, successor.key)));
|
|
240
|
+
}
|
|
241
|
+
function* entries(n) {
|
|
242
|
+
if (!n) return;
|
|
243
|
+
yield* entries(n.left);
|
|
244
|
+
yield [n.key, n.value];
|
|
245
|
+
yield* entries(n.right);
|
|
246
|
+
}
|
|
247
|
+
var PersistentMap = class PersistentMap {
|
|
248
|
+
#root;
|
|
249
|
+
constructor(root) {
|
|
250
|
+
this.#root = root;
|
|
251
|
+
Object.freeze(this);
|
|
252
|
+
}
|
|
253
|
+
get size() {
|
|
254
|
+
return size(this.#root);
|
|
255
|
+
}
|
|
256
|
+
get [Symbol.toStringTag]() {
|
|
257
|
+
return "PersistentMap";
|
|
258
|
+
}
|
|
259
|
+
get(key) {
|
|
260
|
+
let n = this.#root;
|
|
261
|
+
while (n) {
|
|
262
|
+
if (key === n.key) return n.value;
|
|
263
|
+
n = key < n.key ? n.left : n.right;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
has(key) {
|
|
267
|
+
let n = this.#root;
|
|
268
|
+
while (n) {
|
|
269
|
+
if (key === n.key) return true;
|
|
270
|
+
n = key < n.key ? n.left : n.right;
|
|
271
|
+
}
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
set(key, value) {
|
|
275
|
+
return new PersistentMap(put(this.#root, key, value));
|
|
276
|
+
}
|
|
277
|
+
delete(key) {
|
|
278
|
+
return this.has(key) ? new PersistentMap(remove(this.#root, key)) : this;
|
|
279
|
+
}
|
|
280
|
+
*entries() {
|
|
281
|
+
yield* entries(this.#root);
|
|
282
|
+
}
|
|
283
|
+
*keys() {
|
|
284
|
+
for (const [key] of this) yield key;
|
|
285
|
+
}
|
|
286
|
+
*values() {
|
|
287
|
+
for (const [, value] of this) yield value;
|
|
288
|
+
}
|
|
289
|
+
[Symbol.iterator]() {
|
|
290
|
+
return this.entries();
|
|
291
|
+
}
|
|
292
|
+
forEach(callback, thisArg) {
|
|
293
|
+
for (const [key, value] of this) callback.call(thisArg, value, key, this);
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
//#endregion
|
|
298
|
+
//#region src/routing/spatial.ts
|
|
299
|
+
const intersects = (a, b) => a.x <= b.x + b.width && a.x + a.width >= b.x && a.y <= b.y + b.height && a.y + a.height >= b.y;
|
|
300
|
+
const inflate = (r, amount) => ({
|
|
301
|
+
x: r.x - amount,
|
|
302
|
+
y: r.y - amount,
|
|
303
|
+
width: r.width + amount * 2,
|
|
304
|
+
height: r.height + amount * 2
|
|
305
|
+
});
|
|
306
|
+
function union(a, b) {
|
|
307
|
+
const x = Math.min(a.x, b.x), y = Math.min(a.y, b.y);
|
|
308
|
+
return {
|
|
309
|
+
x,
|
|
310
|
+
y,
|
|
311
|
+
width: Math.max(a.x + a.width, b.x + b.width) - x,
|
|
312
|
+
height: Math.max(a.y + a.height, b.y + b.height) - y
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
function cells(r) {
|
|
316
|
+
const x1 = Math.floor(r.x / 128), x2 = Math.floor((r.x + r.width) / 128), y1 = Math.floor(r.y / 128), y2 = Math.floor((r.y + r.height) / 128);
|
|
317
|
+
if ((x2 - x1 + 1) * (y2 - y1 + 1) > 4096) return void 0;
|
|
318
|
+
const result = [];
|
|
319
|
+
for (let x = 0; x <= x2 - x1; x++) for (let y = 0; y <= y2 - y1; y++) result.push(`${x1 + x}:${y1 + y}`);
|
|
320
|
+
return result;
|
|
321
|
+
}
|
|
322
|
+
/** Persistent uniform-grid broad phase; oversized rectangles live in an overflow index. */
|
|
323
|
+
var SpatialIndex = class SpatialIndex {
|
|
324
|
+
constructor(bounds = new PersistentMap(), buckets = new PersistentMap(), overflow = new PersistentMap()) {
|
|
325
|
+
this.bounds = bounds;
|
|
326
|
+
this.buckets = buckets;
|
|
327
|
+
this.overflow = overflow;
|
|
328
|
+
Object.freeze(this);
|
|
329
|
+
}
|
|
330
|
+
set(id, rect$1) {
|
|
331
|
+
let { bounds, buckets, overflow } = this;
|
|
332
|
+
const previous = bounds.get(id);
|
|
333
|
+
if (previous) {
|
|
334
|
+
const keys = cells(previous);
|
|
335
|
+
if (!keys) overflow = overflow.delete(id);
|
|
336
|
+
else for (const key of keys) {
|
|
337
|
+
const bucket = buckets.get(key).delete(id);
|
|
338
|
+
buckets = bucket.size ? buckets.set(key, bucket) : buckets.delete(key);
|
|
339
|
+
}
|
|
340
|
+
bounds = bounds.delete(id);
|
|
341
|
+
}
|
|
342
|
+
if (rect$1) {
|
|
343
|
+
const keys = cells(rect$1);
|
|
344
|
+
if (!keys) overflow = overflow.set(id, true);
|
|
345
|
+
else for (const key of keys) buckets = buckets.set(key, (buckets.get(key) ?? new PersistentMap()).set(id, true));
|
|
346
|
+
bounds = bounds.set(id, rect$1);
|
|
347
|
+
}
|
|
348
|
+
return new SpatialIndex(bounds, buckets, overflow);
|
|
349
|
+
}
|
|
350
|
+
query(rect$1) {
|
|
351
|
+
const keys = cells(rect$1), found = /* @__PURE__ */ new Set();
|
|
352
|
+
if (!keys) {
|
|
353
|
+
for (const [id, bounds] of this.bounds) if (intersects(rect$1, bounds)) found.add(id);
|
|
354
|
+
} else {
|
|
355
|
+
for (const key of keys) for (const id of this.buckets.get(key)?.keys() ?? []) if (intersects(rect$1, this.bounds.get(id))) found.add(id);
|
|
356
|
+
for (const id of this.overflow.keys()) if (intersects(rect$1, this.bounds.get(id))) found.add(id);
|
|
357
|
+
}
|
|
358
|
+
return [...found].sort();
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
//#endregion
|
|
363
|
+
//#region src/routing/model.ts
|
|
364
|
+
function settings(input = {}) {
|
|
365
|
+
const result = {
|
|
366
|
+
coordinateSpace: input.coordinateSpace ?? "parent",
|
|
367
|
+
clearance: input.clearance ?? 8,
|
|
368
|
+
radius: input.radius ?? 10,
|
|
369
|
+
edgeSpacing: input.edgeSpacing ?? 12,
|
|
370
|
+
bendPenalty: input.bendPenalty ?? 10,
|
|
371
|
+
crossingPenalty: input.crossingPenalty ?? 80,
|
|
372
|
+
overlapPenalty: input.overlapPenalty ?? 8,
|
|
373
|
+
maxSearchNodes: input.maxSearchNodes ?? 4e3,
|
|
374
|
+
organicIterations: input.organicIterations ?? 12,
|
|
375
|
+
edges: structuredClone(input.edges ?? {})
|
|
376
|
+
};
|
|
377
|
+
for (const [key, value] of Object.entries(result)) if (typeof value === "number" && (!Number.isFinite(value) || value < 0)) throw new RangeError(`${key} must be finite and non-negative`);
|
|
378
|
+
if (!Number.isInteger(result.maxSearchNodes) || result.maxSearchNodes < 1 || !Number.isInteger(result.organicIterations)) throw new RangeError("Search and iteration budgets must be integers");
|
|
379
|
+
return freeze(result);
|
|
380
|
+
}
|
|
381
|
+
function freeze(value) {
|
|
382
|
+
if (value !== null && typeof value === "object" && !Object.isFrozen(value)) {
|
|
383
|
+
for (const child of Object.values(value)) freeze(child);
|
|
384
|
+
Object.freeze(value);
|
|
385
|
+
}
|
|
386
|
+
return value;
|
|
387
|
+
}
|
|
388
|
+
function nodeGeometry(n) {
|
|
389
|
+
return freeze({
|
|
390
|
+
id: n.id,
|
|
391
|
+
parentId: n.parentId,
|
|
392
|
+
x: n.x,
|
|
393
|
+
y: n.y,
|
|
394
|
+
width: n.width,
|
|
395
|
+
height: n.height,
|
|
396
|
+
ports: n.ports?.map((p) => ({
|
|
397
|
+
name: p.name,
|
|
398
|
+
x: p.x,
|
|
399
|
+
y: p.y,
|
|
400
|
+
width: p.width,
|
|
401
|
+
height: p.height
|
|
402
|
+
}))
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
function edgeGeometry(e) {
|
|
406
|
+
return freeze({
|
|
407
|
+
id: e.id,
|
|
408
|
+
sourceId: e.sourceId ?? "",
|
|
409
|
+
targetId: e.targetId ?? "",
|
|
410
|
+
sourcePort: e.sourcePort,
|
|
411
|
+
targetPort: e.targetPort,
|
|
412
|
+
x: e.x,
|
|
413
|
+
y: e.y,
|
|
414
|
+
width: e.width,
|
|
415
|
+
height: e.height
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
function rect(value) {
|
|
419
|
+
const { x, y, width, height: height$1 } = value;
|
|
420
|
+
return [
|
|
421
|
+
x,
|
|
422
|
+
y,
|
|
423
|
+
width,
|
|
424
|
+
height$1
|
|
425
|
+
].every((n) => typeof n === "number" && Number.isFinite(n)) && width >= 0 && height$1 >= 0 ? {
|
|
426
|
+
x,
|
|
427
|
+
y,
|
|
428
|
+
width,
|
|
429
|
+
height: height$1
|
|
430
|
+
} : void 0;
|
|
431
|
+
}
|
|
432
|
+
function members(index, key, id, add) {
|
|
433
|
+
const current = index.get(key) ?? new PersistentMap(), next = add ? current.set(id, true) : current.delete(id);
|
|
434
|
+
return next.size ? index.set(key, next) : index.delete(key);
|
|
435
|
+
}
|
|
436
|
+
function ancestors(id, state) {
|
|
437
|
+
const result = [], seen = new Set([id]);
|
|
438
|
+
let parent = state.nodes.get(id)?.parentId;
|
|
439
|
+
while (parent != null) {
|
|
440
|
+
if (seen.has(parent)) throw new Error(`Cyclic routing hierarchy at ${parent}`);
|
|
441
|
+
result.push(parent);
|
|
442
|
+
seen.add(parent);
|
|
443
|
+
parent = state.nodes.get(parent)?.parentId;
|
|
444
|
+
}
|
|
445
|
+
return result;
|
|
446
|
+
}
|
|
447
|
+
function worldRect(node$1, state) {
|
|
448
|
+
const bounds = rect(node$1);
|
|
449
|
+
if (!bounds) return;
|
|
450
|
+
if (state.settings.coordinateSpace === "world") return bounds;
|
|
451
|
+
let x = bounds.x, y = bounds.y;
|
|
452
|
+
for (const id of ancestors(node$1.id, state)) {
|
|
453
|
+
const parent = state.nodes.get(id), p = parent && rect(parent);
|
|
454
|
+
if (!p) return;
|
|
455
|
+
x += p.x;
|
|
456
|
+
y += p.y;
|
|
457
|
+
}
|
|
458
|
+
return {
|
|
459
|
+
...bounds,
|
|
460
|
+
x,
|
|
461
|
+
y
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
function labelRect(edge, state) {
|
|
465
|
+
const bounds = rect(edge);
|
|
466
|
+
if (!bounds || bounds.width <= 0 || bounds.height <= 0) return;
|
|
467
|
+
const source = state.nodes.get(edge.sourceId), target = state.nodes.get(edge.targetId);
|
|
468
|
+
if (state.settings.coordinateSpace === "world" || !source || source.parentId == null || source.parentId !== target?.parentId) return bounds;
|
|
469
|
+
const parent = state.nodes.get(source.parentId), p = parent && worldRect(parent, state);
|
|
470
|
+
return p ? {
|
|
471
|
+
...bounds,
|
|
472
|
+
x: bounds.x + p.x,
|
|
473
|
+
y: bounds.y + p.y
|
|
474
|
+
} : void 0;
|
|
475
|
+
}
|
|
476
|
+
const center = (r) => ({
|
|
477
|
+
x: r.x + r.width / 2,
|
|
478
|
+
y: r.y + r.height / 2
|
|
479
|
+
});
|
|
480
|
+
function sideToward(a, b) {
|
|
481
|
+
return Math.abs(a.x - b.x) >= Math.abs(a.y - b.y) ? b.x >= a.x ? "right" : "left" : b.y >= a.y ? "bottom" : "top";
|
|
482
|
+
}
|
|
483
|
+
const vector = (side) => ({
|
|
484
|
+
x: side === "right" ? 1 : side === "left" ? -1 : 0,
|
|
485
|
+
y: side === "bottom" ? 1 : side === "top" ? -1 : 0
|
|
486
|
+
});
|
|
487
|
+
function anchor(bounds, side) {
|
|
488
|
+
const c = center(bounds), v = vector(side);
|
|
489
|
+
return {
|
|
490
|
+
x: c.x + v.x * bounds.width / 2,
|
|
491
|
+
y: c.y + v.y * bounds.height / 2
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
function groupKey(edge, style, config) {
|
|
495
|
+
const group = config.edges[edge.id]?.group;
|
|
496
|
+
if (style === "bus" || style === "bundle" || style === "fan") return group !== void 0 ? `group:${group}` : `source:${JSON.stringify([edge.sourceId, edge.sourcePort ?? null])}`;
|
|
497
|
+
return `pair:${JSON.stringify([edge.sourceId, edge.targetId].sort())}`;
|
|
498
|
+
}
|
|
499
|
+
|
|
17
500
|
//#endregion
|
|
18
501
|
//#region src/authoring/routing.ts
|
|
19
502
|
function overlaps(a, b, gap = 0) {
|
|
@@ -26,7 +509,7 @@ function crossesRect(a, b, rect$1) {
|
|
|
26
509
|
const delta = b[axis] - a[axis];
|
|
27
510
|
const min = rect$1[axis], max = min + (axis === "x" ? rect$1.width : rect$1.height);
|
|
28
511
|
if (Math.abs(delta) < 1e-10) {
|
|
29
|
-
if (a[axis] <= min || a[axis] >= max) return false;
|
|
512
|
+
if (a[axis] <= min + 1e-10 || a[axis] >= max - 1e-10) return false;
|
|
30
513
|
} else {
|
|
31
514
|
const t1 = (min - a[axis]) / delta, t2 = (max - a[axis]) / delta;
|
|
32
515
|
low = Math.max(low, Math.min(t1, t2));
|
|
@@ -484,173 +967,138 @@ function segmentCrossesRect(start, segment, rect$1) {
|
|
|
484
967
|
}
|
|
485
968
|
|
|
486
969
|
//#endregion
|
|
487
|
-
//#region src/routing/
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
};
|
|
499
|
-
}
|
|
500
|
-
function balance(n) {
|
|
501
|
-
if (height(n.left) - height(n.right) > 1) {
|
|
502
|
-
let l = n.left;
|
|
503
|
-
if (height(l.right) > height(l.left)) {
|
|
504
|
-
const r = l.right;
|
|
505
|
-
l = node(r.key, r.value, node(l.key, l.value, l.left, r.left), r.right);
|
|
506
|
-
}
|
|
507
|
-
return node(l.key, l.value, l.left, node(n.key, n.value, l.right, n.right));
|
|
508
|
-
}
|
|
509
|
-
if (height(n.right) - height(n.left) > 1) {
|
|
510
|
-
let r = n.right;
|
|
511
|
-
if (height(r.left) > height(r.right)) {
|
|
512
|
-
const l = r.left;
|
|
513
|
-
r = node(l.key, l.value, l.left, node(r.key, r.value, l.right, r.right));
|
|
514
|
-
}
|
|
515
|
-
return node(r.key, r.value, node(n.key, n.value, n.left, r.left), r.right);
|
|
516
|
-
}
|
|
517
|
-
return n;
|
|
518
|
-
}
|
|
519
|
-
function put(n, key, value) {
|
|
520
|
-
if (!n) return node(key, value);
|
|
521
|
-
if (key === n.key) return Object.is(value, n.value) ? n : node(key, value, n.left, n.right);
|
|
522
|
-
return balance(key < n.key ? node(n.key, n.value, put(n.left, key, value), n.right) : node(n.key, n.value, n.left, put(n.right, key, value)));
|
|
523
|
-
}
|
|
524
|
-
function remove(n, key) {
|
|
525
|
-
if (!n) return n;
|
|
526
|
-
if (key < n.key) return balance(node(n.key, n.value, remove(n.left, key), n.right));
|
|
527
|
-
if (key > n.key) return balance(node(n.key, n.value, n.left, remove(n.right, key)));
|
|
528
|
-
if (!n.left) return n.right;
|
|
529
|
-
if (!n.right) return n.left;
|
|
530
|
-
let successor = n.right;
|
|
531
|
-
while (successor.left) successor = successor.left;
|
|
532
|
-
return balance(node(successor.key, successor.value, n.left, remove(n.right, successor.key)));
|
|
533
|
-
}
|
|
534
|
-
function* entries(n) {
|
|
535
|
-
if (!n) return;
|
|
536
|
-
yield* entries(n.left);
|
|
537
|
-
yield [n.key, n.value];
|
|
538
|
-
yield* entries(n.right);
|
|
539
|
-
}
|
|
540
|
-
var PersistentMap = class PersistentMap {
|
|
541
|
-
#root;
|
|
542
|
-
constructor(root) {
|
|
543
|
-
this.#root = root;
|
|
544
|
-
Object.freeze(this);
|
|
545
|
-
}
|
|
546
|
-
get size() {
|
|
547
|
-
return size(this.#root);
|
|
548
|
-
}
|
|
549
|
-
get [Symbol.toStringTag]() {
|
|
550
|
-
return "PersistentMap";
|
|
551
|
-
}
|
|
552
|
-
get(key) {
|
|
553
|
-
let n = this.#root;
|
|
554
|
-
while (n) {
|
|
555
|
-
if (key === n.key) return n.value;
|
|
556
|
-
n = key < n.key ? n.left : n.right;
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
has(key) {
|
|
560
|
-
let n = this.#root;
|
|
561
|
-
while (n) {
|
|
562
|
-
if (key === n.key) return true;
|
|
563
|
-
n = key < n.key ? n.left : n.right;
|
|
564
|
-
}
|
|
565
|
-
return false;
|
|
566
|
-
}
|
|
567
|
-
set(key, value) {
|
|
568
|
-
return new PersistentMap(put(this.#root, key, value));
|
|
569
|
-
}
|
|
570
|
-
delete(key) {
|
|
571
|
-
return this.has(key) ? new PersistentMap(remove(this.#root, key)) : this;
|
|
572
|
-
}
|
|
573
|
-
*entries() {
|
|
574
|
-
yield* entries(this.#root);
|
|
575
|
-
}
|
|
576
|
-
*keys() {
|
|
577
|
-
for (const [key] of this) yield key;
|
|
578
|
-
}
|
|
579
|
-
*values() {
|
|
580
|
-
for (const [, value] of this) yield value;
|
|
581
|
-
}
|
|
582
|
-
[Symbol.iterator]() {
|
|
583
|
-
return this.entries();
|
|
584
|
-
}
|
|
585
|
-
forEach(callback, thisArg) {
|
|
586
|
-
for (const [key, value] of this) callback.call(thisArg, value, key, this);
|
|
587
|
-
}
|
|
588
|
-
};
|
|
589
|
-
|
|
590
|
-
//#endregion
|
|
591
|
-
//#region src/routing/spatial.ts
|
|
592
|
-
const intersects = (a, b) => a.x <= b.x + b.width && a.x + a.width >= b.x && a.y <= b.y + b.height && a.y + a.height >= b.y;
|
|
593
|
-
const inflate = (r, amount) => ({
|
|
594
|
-
x: r.x - amount,
|
|
595
|
-
y: r.y - amount,
|
|
596
|
-
width: r.width + amount * 2,
|
|
597
|
-
height: r.height + amount * 2
|
|
598
|
-
});
|
|
599
|
-
function union(a, b) {
|
|
600
|
-
const x = Math.min(a.x, b.x), y = Math.min(a.y, b.y);
|
|
970
|
+
//#region src/routing/adapters.ts
|
|
971
|
+
/** ELK/native spline output: start, then repeating control1/control2/end triples. */
|
|
972
|
+
function pathFromSplinePoints(points) {
|
|
973
|
+
if (points.length < 4 || (points.length - 1) % 3 !== 0) throw new RangeError("Cubic spline points require a start and control1/control2/end triples");
|
|
974
|
+
const segments = [];
|
|
975
|
+
for (let i = 1; i < points.length; i += 3) segments.push({
|
|
976
|
+
kind: "cubic",
|
|
977
|
+
control1: { ...points[i] },
|
|
978
|
+
control2: { ...points[i + 1] },
|
|
979
|
+
to: { ...points[i + 2] }
|
|
980
|
+
});
|
|
601
981
|
return {
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
width: Math.max(a.x + a.width, b.x + b.width) - x,
|
|
605
|
-
height: Math.max(a.y + a.height, b.y + b.height) - y
|
|
982
|
+
start: { ...points[0] },
|
|
983
|
+
segments
|
|
606
984
|
};
|
|
607
985
|
}
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
const bucket = buckets.get(key).delete(id);
|
|
631
|
-
buckets = bucket.size ? buckets.set(key, bucket) : buckets.delete(key);
|
|
632
|
-
}
|
|
633
|
-
bounds = bounds.delete(id);
|
|
634
|
-
}
|
|
635
|
-
if (rect$1) {
|
|
636
|
-
const keys = cells(rect$1);
|
|
637
|
-
if (!keys) overflow = overflow.set(id, true);
|
|
638
|
-
else for (const key of keys) buckets = buckets.set(key, (buckets.get(key) ?? new PersistentMap()).set(id, true));
|
|
639
|
-
bounds = bounds.set(id, rect$1);
|
|
986
|
+
/** Normalize routes from any native layout algorithm, without running layout again. */
|
|
987
|
+
function getLayoutRoutes(graph) {
|
|
988
|
+
const compound = graph;
|
|
989
|
+
if (compound.compoundRoutes?.size && hasCurrentRouteGeometry(graph, compound.compoundRoutes)) return compound.compoundRoutes;
|
|
990
|
+
const world = worldGeometry(graph), nodes = new Map(world.nodes.map((n) => [n.id, n]));
|
|
991
|
+
let result = new PersistentMap();
|
|
992
|
+
for (const edge of world.edges) {
|
|
993
|
+
const points = edge.points;
|
|
994
|
+
let path;
|
|
995
|
+
if (points?.length && points.every((p) => Number.isFinite(p.x) && Number.isFinite(p.y))) if (edge.routing === "splines") {
|
|
996
|
+
if (points.length >= 4 && (points.length - 1) % 3 === 0) path = pathFromSplinePoints(points);
|
|
997
|
+
} else path = pathFromPoints(points);
|
|
998
|
+
const routed = path !== void 0;
|
|
999
|
+
if (!path) {
|
|
1000
|
+
const a = nodes.get(edge.sourceId), b = nodes.get(edge.targetId);
|
|
1001
|
+
path = pathFromPoints([{
|
|
1002
|
+
x: a ? a.x + a.width : 0,
|
|
1003
|
+
y: a ? a.y + a.height / 2 : 0
|
|
1004
|
+
}, {
|
|
1005
|
+
x: b?.x ?? 40,
|
|
1006
|
+
y: b ? b.y + b.height / 2 : 40
|
|
1007
|
+
}]);
|
|
640
1008
|
}
|
|
641
|
-
|
|
1009
|
+
result = result.set(edge.id, freeze({
|
|
1010
|
+
edgeId: edge.id,
|
|
1011
|
+
status: routed ? "routed" : "fallback",
|
|
1012
|
+
diagnostics: routed ? [] : [{
|
|
1013
|
+
code: "MISSING_GEOMETRY",
|
|
1014
|
+
edgeId: edge.id,
|
|
1015
|
+
message: "Layout has no valid route; rendered a fallback connection"
|
|
1016
|
+
}],
|
|
1017
|
+
sections: [{
|
|
1018
|
+
id: `${edge.id}:0`,
|
|
1019
|
+
from: {
|
|
1020
|
+
kind: "node",
|
|
1021
|
+
nodeId: edge.sourceId,
|
|
1022
|
+
...edge.sourcePort === void 0 ? {} : { port: edge.sourcePort }
|
|
1023
|
+
},
|
|
1024
|
+
to: {
|
|
1025
|
+
kind: "node",
|
|
1026
|
+
nodeId: edge.targetId,
|
|
1027
|
+
...edge.targetPort === void 0 ? {} : { port: edge.targetPort }
|
|
1028
|
+
},
|
|
1029
|
+
path
|
|
1030
|
+
}]
|
|
1031
|
+
}));
|
|
642
1032
|
}
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
1033
|
+
return result;
|
|
1034
|
+
}
|
|
1035
|
+
/** One polyline per section preserves label gaps and branching for simple renderers. */
|
|
1036
|
+
function routeToPolylines(route, options) {
|
|
1037
|
+
return route.sections.map((s) => flattenPath(s.path, options));
|
|
1038
|
+
}
|
|
1039
|
+
/**
|
|
1040
|
+
* One point list for consumers of `GraphEdge.points`. Sections are joined
|
|
1041
|
+
* through their gaps (an edge label, for example); between orthogonal
|
|
1042
|
+
* sections the join is orthogonal too, so it never adds a diagonal.
|
|
1043
|
+
*/
|
|
1044
|
+
function routeToPoints(route, options) {
|
|
1045
|
+
const points = [];
|
|
1046
|
+
const axis = (a, b) => Math.abs(a.y - b.y) < 1e-9 ? "horizontal" : Math.abs(a.x - b.x) < 1e-9 ? "vertical" : void 0;
|
|
1047
|
+
for (const polyline of routeToPolylines(route, options)) {
|
|
1048
|
+
const a = points.at(-1), b = polyline[0];
|
|
1049
|
+
if (a && b && axis(a, b) === void 0) {
|
|
1050
|
+
const before = points.length > 1 ? axis(points.at(-2), a) : void 0;
|
|
1051
|
+
const after = polyline.length > 1 ? axis(b, polyline[1]) : void 0;
|
|
1052
|
+
if (before && after) if (before === "horizontal" && after === "horizontal") {
|
|
1053
|
+
const x = (a.x + b.x) / 2;
|
|
1054
|
+
points.push({
|
|
1055
|
+
x,
|
|
1056
|
+
y: a.y
|
|
1057
|
+
}, {
|
|
1058
|
+
x,
|
|
1059
|
+
y: b.y
|
|
1060
|
+
});
|
|
1061
|
+
} else if (before === "vertical" && after === "vertical") {
|
|
1062
|
+
const y = (a.y + b.y) / 2;
|
|
1063
|
+
points.push({
|
|
1064
|
+
x: a.x,
|
|
1065
|
+
y
|
|
1066
|
+
}, {
|
|
1067
|
+
x: b.x,
|
|
1068
|
+
y
|
|
1069
|
+
});
|
|
1070
|
+
} else points.push(before === "horizontal" ? {
|
|
1071
|
+
x: b.x,
|
|
1072
|
+
y: a.y
|
|
1073
|
+
} : {
|
|
1074
|
+
x: a.x,
|
|
1075
|
+
y: b.y
|
|
1076
|
+
});
|
|
650
1077
|
}
|
|
651
|
-
|
|
1078
|
+
points.push(...polyline.map((point) => ({ ...point })));
|
|
652
1079
|
}
|
|
653
|
-
|
|
1080
|
+
return points;
|
|
1081
|
+
}
|
|
1082
|
+
/** Compatibility for single-section native consumers. Refuses to erase gaps or topology. */
|
|
1083
|
+
function routeToGraphPatch(route, options = {}) {
|
|
1084
|
+
if (route.sections.length !== 1 || route.sections[0].sharedId || route.sections[0].from.kind !== "node" || route.sections[0].to.kind !== "node") throw new Error("This route requires section-aware rendering; a single GraphEdge.points array would lose topology");
|
|
1085
|
+
const offset = options.offset ?? {
|
|
1086
|
+
x: 0,
|
|
1087
|
+
y: 0
|
|
1088
|
+
};
|
|
1089
|
+
const points = flattenPath(route.sections[0].path, options).map((p) => ({
|
|
1090
|
+
x: p.x - offset.x,
|
|
1091
|
+
y: p.y - offset.y
|
|
1092
|
+
}));
|
|
1093
|
+
return {
|
|
1094
|
+
op: "updateEdge",
|
|
1095
|
+
id: route.edgeId,
|
|
1096
|
+
data: {
|
|
1097
|
+
points,
|
|
1098
|
+
routing: "polyline"
|
|
1099
|
+
}
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
654
1102
|
|
|
655
1103
|
//#endregion
|
|
656
1104
|
//#region src/routing/search.ts
|
|
@@ -711,6 +1159,11 @@ var Queue = class {
|
|
|
711
1159
|
};
|
|
712
1160
|
/** Visibility graph + A*, with incoming direction in the state for bend costs. */
|
|
713
1161
|
function findPath(start, end, style, context, terminals = {}) {
|
|
1162
|
+
const maxGridNodes = context.maxGridNodes ?? context.maxSearchNodes;
|
|
1163
|
+
const remainingDistance = (point) => {
|
|
1164
|
+
const dx = Math.abs(point.x - end.x), dy = Math.abs(point.y - end.y);
|
|
1165
|
+
return style === "orthogonal" ? dx + dy : style === "octilinear" ? Math.max(dx, dy) + (Math.SQRT2 - 1) * Math.min(dx, dy) : distance(point, end);
|
|
1166
|
+
};
|
|
714
1167
|
const turnCost = (a, b) => {
|
|
715
1168
|
if (!a || !b || !Math.hypot(a.x, a.y) || !Math.hypot(b.x, b.y)) return 0;
|
|
716
1169
|
const dot = a.x * b.x + a.y * b.y, cross = a.x * b.y - a.y * b.x;
|
|
@@ -768,21 +1221,31 @@ function findPath(start, end, style, context, terminals = {}) {
|
|
|
768
1221
|
];
|
|
769
1222
|
neighbors = () => points.map((_, i) => i);
|
|
770
1223
|
} else {
|
|
771
|
-
const
|
|
1224
|
+
const coordinates = (values) => {
|
|
1225
|
+
const endpoints = values.slice(0, 2);
|
|
1226
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
1227
|
+
const unique = [];
|
|
1228
|
+
for (const value of sorted) {
|
|
1229
|
+
if (unique.length && Math.abs(value - unique.at(-1)) < 1e-8) continue;
|
|
1230
|
+
unique.push(value);
|
|
1231
|
+
}
|
|
1232
|
+
return unique.map((value) => endpoints.find((p) => Math.abs(p - value) < 1e-8) ?? value);
|
|
1233
|
+
};
|
|
1234
|
+
const xs = coordinates([
|
|
772
1235
|
start.x,
|
|
773
1236
|
end.x,
|
|
774
1237
|
area.x,
|
|
775
1238
|
area.x + area.width,
|
|
776
1239
|
...corners.map((p) => p.x)
|
|
777
|
-
])
|
|
778
|
-
const ys =
|
|
1240
|
+
]);
|
|
1241
|
+
const ys = coordinates([
|
|
779
1242
|
start.y,
|
|
780
1243
|
end.y,
|
|
781
1244
|
area.y,
|
|
782
1245
|
area.y + area.height,
|
|
783
1246
|
...corners.map((p) => p.y)
|
|
784
|
-
])
|
|
785
|
-
if (xs.length * ys.length >
|
|
1247
|
+
]);
|
|
1248
|
+
if (xs.length * ys.length > maxGridNodes) {
|
|
786
1249
|
context.budgetExceeded = true;
|
|
787
1250
|
return;
|
|
788
1251
|
}
|
|
@@ -818,7 +1281,7 @@ function findPath(start, end, style, context, terminals = {}) {
|
|
|
818
1281
|
const unique = new Map(points.map((p) => [`${p.x}:${p.y}`, p]));
|
|
819
1282
|
for (const p of extra) unique.set(`${p.x}:${p.y}`, p);
|
|
820
1283
|
points = [...unique.values()];
|
|
821
|
-
if (points.length >
|
|
1284
|
+
if (points.length > maxGridNodes) {
|
|
822
1285
|
context.budgetExceeded = true;
|
|
823
1286
|
return;
|
|
824
1287
|
}
|
|
@@ -852,7 +1315,7 @@ function findPath(start, end, style, context, terminals = {}) {
|
|
|
852
1315
|
i + width < points.length ? i + width : -1
|
|
853
1316
|
].filter((v) => v >= 0);
|
|
854
1317
|
}
|
|
855
|
-
if (points.length >
|
|
1318
|
+
if (points.length > maxGridNodes) {
|
|
856
1319
|
context.budgetExceeded = true;
|
|
857
1320
|
return;
|
|
858
1321
|
}
|
|
@@ -876,7 +1339,7 @@ function findPath(start, end, style, context, terminals = {}) {
|
|
|
876
1339
|
previous: -1,
|
|
877
1340
|
key: first,
|
|
878
1341
|
cost: 0
|
|
879
|
-
},
|
|
1342
|
+
}, remainingDistance(start));
|
|
880
1343
|
while (queue.items.length && context.visited < context.maxSearchNodes) {
|
|
881
1344
|
const current = queue.pop();
|
|
882
1345
|
if (costs.get(current.key) !== current.cost) continue;
|
|
@@ -897,6 +1360,8 @@ function findPath(start, end, style, context, terminals = {}) {
|
|
|
897
1360
|
if (!visible(current.index, next)) continue;
|
|
898
1361
|
const before = current.previous < 0 ? terminals.incoming : delta(points[current.previous], a);
|
|
899
1362
|
const step = delta(a, b);
|
|
1363
|
+
const reverses = (incoming, outgoing) => incoming && outgoing && Math.abs(incoming.x * outgoing.y - incoming.y * outgoing.x) < 1e-8 && incoming.x * outgoing.x + incoming.y * outgoing.y < -1e-8;
|
|
1364
|
+
if (reverses(before, step) || next === to && reverses(step, terminals.outgoing)) continue;
|
|
900
1365
|
const bend = turnCost(before, step) + (next === to ? turnCost(step, terminals.outgoing) : 0);
|
|
901
1366
|
const pair = Math.min(current.index, next) * points.length + Math.max(current.index, next);
|
|
902
1367
|
let edgeCost = edgeCosts.get(pair);
|
|
@@ -915,12 +1380,33 @@ function findPath(start, end, style, context, terminals = {}) {
|
|
|
915
1380
|
previous: current.index,
|
|
916
1381
|
key,
|
|
917
1382
|
cost
|
|
918
|
-
}, cost +
|
|
1383
|
+
}, cost + remainingDistance(b));
|
|
919
1384
|
}
|
|
920
1385
|
}
|
|
921
1386
|
}
|
|
922
1387
|
if (context.visited >= context.maxSearchNodes) context.budgetExceeded = true;
|
|
923
1388
|
}
|
|
1389
|
+
const findPathBetweenLeads = (start, end, style, ctx, leads, directions = {}) => {
|
|
1390
|
+
const protectedContext = {
|
|
1391
|
+
...ctx,
|
|
1392
|
+
edgeCost: (p, q) => {
|
|
1393
|
+
for (const [from, to] of leads) {
|
|
1394
|
+
const dx = q.x - p.x, dy = q.y - p.y;
|
|
1395
|
+
const length = Math.hypot(dx, dy);
|
|
1396
|
+
if (length < 1e-8) continue;
|
|
1397
|
+
if (Math.abs(dx * (to.y - from.y) - dy * (to.x - from.x)) > 1e-8 * length || Math.abs(dx * (from.y - p.y) - dy * (from.x - p.x)) > 1e-8 * length) continue;
|
|
1398
|
+
const lo = ((from.x - p.x) * dx + (from.y - p.y) * dy) / length;
|
|
1399
|
+
const hi = ((to.x - p.x) * dx + (to.y - p.y) * dy) / length;
|
|
1400
|
+
if (Math.min(length, Math.max(lo, hi)) - Math.max(0, Math.min(lo, hi)) > 1e-8) return Infinity;
|
|
1401
|
+
}
|
|
1402
|
+
return ctx.edgeCost?.(p, q) ?? 0;
|
|
1403
|
+
}
|
|
1404
|
+
};
|
|
1405
|
+
const result = findPath(start, end, style, protectedContext, directions);
|
|
1406
|
+
ctx.visited = protectedContext.visited;
|
|
1407
|
+
ctx.budgetExceeded = protectedContext.budgetExceeded;
|
|
1408
|
+
return result;
|
|
1409
|
+
};
|
|
924
1410
|
|
|
925
1411
|
//#endregion
|
|
926
1412
|
//#region src/routing/coordination.ts
|
|
@@ -963,7 +1449,7 @@ function conflictCost(a, b, other, config) {
|
|
|
963
1449
|
return config.overlapPenalty * overlap * (1 - separation / spacing);
|
|
964
1450
|
}
|
|
965
1451
|
/** Remove only the common terminal's attachment region from soft reservations. */
|
|
966
|
-
function outsideTerminal(segment, rect$1) {
|
|
1452
|
+
function outsideTerminal(segment, rect$1, closed = true) {
|
|
967
1453
|
let lo = 0, hi = 1;
|
|
968
1454
|
const dx = segment.b.x - segment.a.x, dy = segment.b.y - segment.a.y;
|
|
969
1455
|
for (const [start, delta, min, max] of [[
|
|
@@ -977,13 +1463,13 @@ function outsideTerminal(segment, rect$1) {
|
|
|
977
1463
|
rect$1.y,
|
|
978
1464
|
rect$1.y + rect$1.height
|
|
979
1465
|
]]) if (Math.abs(delta) < 1e-8) {
|
|
980
|
-
if (start < min || start > max) return [segment];
|
|
1466
|
+
if (closed ? start < min || start > max : start <= min + 1e-8 || start >= max - 1e-8) return [segment];
|
|
981
1467
|
} else {
|
|
982
1468
|
const a = (min - start) / delta, b = (max - start) / delta;
|
|
983
1469
|
lo = Math.max(lo, Math.min(a, b));
|
|
984
1470
|
hi = Math.min(hi, Math.max(a, b));
|
|
985
1471
|
}
|
|
986
|
-
if (lo > hi) return [segment];
|
|
1472
|
+
if (closed ? lo > hi : lo >= hi) return [segment];
|
|
987
1473
|
const at = (t) => ({
|
|
988
1474
|
x: segment.a.x + dx * t,
|
|
989
1475
|
y: segment.a.y + dy * t
|
|
@@ -997,151 +1483,14 @@ function outsideTerminal(segment, rect$1) {
|
|
|
997
1483
|
}] : []];
|
|
998
1484
|
}
|
|
999
1485
|
|
|
1000
|
-
//#endregion
|
|
1001
|
-
//#region src/routing/model.ts
|
|
1002
|
-
function settings(input = {}) {
|
|
1003
|
-
const result = {
|
|
1004
|
-
coordinateSpace: input.coordinateSpace ?? "parent",
|
|
1005
|
-
clearance: input.clearance ?? 8,
|
|
1006
|
-
radius: input.radius ?? 10,
|
|
1007
|
-
edgeSpacing: input.edgeSpacing ?? 12,
|
|
1008
|
-
bendPenalty: input.bendPenalty ?? 10,
|
|
1009
|
-
crossingPenalty: input.crossingPenalty ?? 80,
|
|
1010
|
-
overlapPenalty: input.overlapPenalty ?? 8,
|
|
1011
|
-
maxSearchNodes: input.maxSearchNodes ?? 4e3,
|
|
1012
|
-
organicIterations: input.organicIterations ?? 12,
|
|
1013
|
-
edges: structuredClone(input.edges ?? {})
|
|
1014
|
-
};
|
|
1015
|
-
for (const [key, value] of Object.entries(result)) if (typeof value === "number" && (!Number.isFinite(value) || value < 0)) throw new RangeError(`${key} must be finite and non-negative`);
|
|
1016
|
-
if (!Number.isInteger(result.maxSearchNodes) || result.maxSearchNodes < 1 || !Number.isInteger(result.organicIterations)) throw new RangeError("Search and iteration budgets must be integers");
|
|
1017
|
-
return freeze(result);
|
|
1018
|
-
}
|
|
1019
|
-
function freeze(value) {
|
|
1020
|
-
if (value !== null && typeof value === "object" && !Object.isFrozen(value)) {
|
|
1021
|
-
for (const child of Object.values(value)) freeze(child);
|
|
1022
|
-
Object.freeze(value);
|
|
1023
|
-
}
|
|
1024
|
-
return value;
|
|
1025
|
-
}
|
|
1026
|
-
function nodeGeometry(n) {
|
|
1027
|
-
return freeze({
|
|
1028
|
-
id: n.id,
|
|
1029
|
-
parentId: n.parentId,
|
|
1030
|
-
x: n.x,
|
|
1031
|
-
y: n.y,
|
|
1032
|
-
width: n.width,
|
|
1033
|
-
height: n.height,
|
|
1034
|
-
ports: n.ports?.map((p) => ({
|
|
1035
|
-
name: p.name,
|
|
1036
|
-
x: p.x,
|
|
1037
|
-
y: p.y,
|
|
1038
|
-
width: p.width,
|
|
1039
|
-
height: p.height
|
|
1040
|
-
}))
|
|
1041
|
-
});
|
|
1042
|
-
}
|
|
1043
|
-
function edgeGeometry(e) {
|
|
1044
|
-
return freeze({
|
|
1045
|
-
id: e.id,
|
|
1046
|
-
sourceId: e.sourceId ?? "",
|
|
1047
|
-
targetId: e.targetId ?? "",
|
|
1048
|
-
sourcePort: e.sourcePort,
|
|
1049
|
-
targetPort: e.targetPort,
|
|
1050
|
-
x: e.x,
|
|
1051
|
-
y: e.y,
|
|
1052
|
-
width: e.width,
|
|
1053
|
-
height: e.height
|
|
1054
|
-
});
|
|
1055
|
-
}
|
|
1056
|
-
function rect(value) {
|
|
1057
|
-
const { x, y, width, height: height$1 } = value;
|
|
1058
|
-
return [
|
|
1059
|
-
x,
|
|
1060
|
-
y,
|
|
1061
|
-
width,
|
|
1062
|
-
height$1
|
|
1063
|
-
].every((n) => typeof n === "number" && Number.isFinite(n)) && width >= 0 && height$1 >= 0 ? {
|
|
1064
|
-
x,
|
|
1065
|
-
y,
|
|
1066
|
-
width,
|
|
1067
|
-
height: height$1
|
|
1068
|
-
} : void 0;
|
|
1069
|
-
}
|
|
1070
|
-
function members(index, key, id, add) {
|
|
1071
|
-
const current = index.get(key) ?? new PersistentMap(), next = add ? current.set(id, true) : current.delete(id);
|
|
1072
|
-
return next.size ? index.set(key, next) : index.delete(key);
|
|
1073
|
-
}
|
|
1074
|
-
function ancestors(id, state) {
|
|
1075
|
-
const result = [], seen = new Set([id]);
|
|
1076
|
-
let parent = state.nodes.get(id)?.parentId;
|
|
1077
|
-
while (parent != null) {
|
|
1078
|
-
if (seen.has(parent)) throw new Error(`Cyclic routing hierarchy at ${parent}`);
|
|
1079
|
-
result.push(parent);
|
|
1080
|
-
seen.add(parent);
|
|
1081
|
-
parent = state.nodes.get(parent)?.parentId;
|
|
1082
|
-
}
|
|
1083
|
-
return result;
|
|
1084
|
-
}
|
|
1085
|
-
function worldRect(node$1, state) {
|
|
1086
|
-
const bounds = rect(node$1);
|
|
1087
|
-
if (!bounds) return;
|
|
1088
|
-
if (state.settings.coordinateSpace === "world") return bounds;
|
|
1089
|
-
let x = bounds.x, y = bounds.y;
|
|
1090
|
-
for (const id of ancestors(node$1.id, state)) {
|
|
1091
|
-
const parent = state.nodes.get(id), p = parent && rect(parent);
|
|
1092
|
-
if (!p) return;
|
|
1093
|
-
x += p.x;
|
|
1094
|
-
y += p.y;
|
|
1095
|
-
}
|
|
1096
|
-
return {
|
|
1097
|
-
...bounds,
|
|
1098
|
-
x,
|
|
1099
|
-
y
|
|
1100
|
-
};
|
|
1101
|
-
}
|
|
1102
|
-
function labelRect(edge, state) {
|
|
1103
|
-
const bounds = rect(edge);
|
|
1104
|
-
if (!bounds || bounds.width <= 0 || bounds.height <= 0) return;
|
|
1105
|
-
const source = state.nodes.get(edge.sourceId), target = state.nodes.get(edge.targetId);
|
|
1106
|
-
if (state.settings.coordinateSpace === "world" || !source || source.parentId == null || source.parentId !== target?.parentId) return bounds;
|
|
1107
|
-
const parent = state.nodes.get(source.parentId), p = parent && worldRect(parent, state);
|
|
1108
|
-
return p ? {
|
|
1109
|
-
...bounds,
|
|
1110
|
-
x: bounds.x + p.x,
|
|
1111
|
-
y: bounds.y + p.y
|
|
1112
|
-
} : void 0;
|
|
1113
|
-
}
|
|
1114
|
-
const center = (r) => ({
|
|
1115
|
-
x: r.x + r.width / 2,
|
|
1116
|
-
y: r.y + r.height / 2
|
|
1117
|
-
});
|
|
1118
|
-
function sideToward(a, b) {
|
|
1119
|
-
return Math.abs(a.x - b.x) >= Math.abs(a.y - b.y) ? b.x >= a.x ? "right" : "left" : b.y >= a.y ? "bottom" : "top";
|
|
1120
|
-
}
|
|
1121
|
-
const vector = (side) => ({
|
|
1122
|
-
x: side === "right" ? 1 : side === "left" ? -1 : 0,
|
|
1123
|
-
y: side === "bottom" ? 1 : side === "top" ? -1 : 0
|
|
1124
|
-
});
|
|
1125
|
-
function anchor(bounds, side) {
|
|
1126
|
-
const c = center(bounds), v = vector(side);
|
|
1127
|
-
return {
|
|
1128
|
-
x: c.x + v.x * bounds.width / 2,
|
|
1129
|
-
y: c.y + v.y * bounds.height / 2
|
|
1130
|
-
};
|
|
1131
|
-
}
|
|
1132
|
-
function groupKey(edge, style, config) {
|
|
1133
|
-
const group = config.edges[edge.id]?.group;
|
|
1134
|
-
if (style === "bus" || style === "bundle" || style === "fan") return group !== void 0 ? `group:${group}` : `source:${JSON.stringify([edge.sourceId, edge.sourcePort ?? null])}`;
|
|
1135
|
-
return `pair:${JSON.stringify([edge.sourceId, edge.targetId].sort())}`;
|
|
1136
|
-
}
|
|
1137
|
-
|
|
1138
1486
|
//#endregion
|
|
1139
1487
|
//#region src/routing/algorithms.ts
|
|
1140
1488
|
function contextFor(state, excluded, endpoints, metrics$1, batch, padding = 0) {
|
|
1141
1489
|
return {
|
|
1142
1490
|
edgeCost: (a, b) => batch.edgeCost?.(a, b) ?? 0,
|
|
1143
1491
|
guides: (bounds) => batch.guides?.(bounds) ?? [],
|
|
1144
|
-
maxSearchNodes: state.settings.maxSearchNodes,
|
|
1492
|
+
maxSearchNodes: Math.max(1, Math.floor(state.settings.maxSearchNodes / 2)),
|
|
1493
|
+
maxGridNodes: state.settings.maxSearchNodes,
|
|
1145
1494
|
bendPenalty: state.settings.bendPenalty,
|
|
1146
1495
|
visited: 0,
|
|
1147
1496
|
budgetExceeded: false,
|
|
@@ -1158,7 +1507,17 @@ function pathLength(path) {
|
|
|
1158
1507
|
return pathReservations(path).reduce((sum, segment) => sum + distance(segment.a, segment.b), 0);
|
|
1159
1508
|
}
|
|
1160
1509
|
function pathCost(path, context) {
|
|
1161
|
-
|
|
1510
|
+
const segments = pathReservations(path);
|
|
1511
|
+
const lengthAndConflicts = segments.reduce((sum, segment) => sum + distance(segment.a, segment.b) + (context.edgeCost?.(segment.a, segment.b) ?? 0), 0);
|
|
1512
|
+
if (path.segments.some((segment) => segment.kind !== "line")) return lengthAndConflicts;
|
|
1513
|
+
let turns = 0;
|
|
1514
|
+
for (let i = 1; i < segments.length; i++) {
|
|
1515
|
+
const a = segments[i - 1], b = segments[i];
|
|
1516
|
+
const ax = a.b.x - a.a.x, ay = a.b.y - a.a.y;
|
|
1517
|
+
const bx = b.b.x - b.a.x, by = b.b.y - b.a.y;
|
|
1518
|
+
turns += ax * bx + ay * by < -1e-8 ? 4 : Math.abs(ax * by - ay * bx) > 1e-8 ? 1 : 0;
|
|
1519
|
+
}
|
|
1520
|
+
return lengthAndConflicts + context.bendPenalty * turns;
|
|
1162
1521
|
}
|
|
1163
1522
|
function safe(path, context) {
|
|
1164
1523
|
let start = path.start;
|
|
@@ -1427,6 +1786,38 @@ function routeEdge(edge, state, style, metrics$1, batch) {
|
|
|
1427
1786
|
if (!(source$1 ? perEdge.sourceSide : perEdge.targetSide)) side = sideToward(center(outer), point);
|
|
1428
1787
|
} else report("MISSING_PORT", `Port ${name} has no positioned geometry; used the node boundary`);
|
|
1429
1788
|
}
|
|
1789
|
+
if (attachment && name === void 0 && !(source$1 ? perEdge.sourceSide : perEdge.targetSide)) {
|
|
1790
|
+
const excludedParents = new Set([...ancestors(edge.sourceId, state), ...ancestors(edge.targetId, state)].filter((id) => id !== edge.sourceId && id !== edge.targetId));
|
|
1791
|
+
const blocked = (p, s) => {
|
|
1792
|
+
const sign = attachment.facing === "inward" ? -1 : 1, v = vector(s);
|
|
1793
|
+
const lead = {
|
|
1794
|
+
x: p.x + v.x * sign,
|
|
1795
|
+
y: p.y + v.y * sign
|
|
1796
|
+
};
|
|
1797
|
+
return state.obstacles.query(segmentBounds(p, lead)).some((id) => {
|
|
1798
|
+
if (id === `n:${source$1 ? edge.sourceId : edge.targetId}` || id.startsWith("n:") && excludedParents.has(id.slice(2))) return false;
|
|
1799
|
+
return crossesRect(p, lead, state.obstacles.bounds.get(id));
|
|
1800
|
+
});
|
|
1801
|
+
};
|
|
1802
|
+
if (blocked(point, side)) {
|
|
1803
|
+
const toward = center(labelRect(edge, state) ?? other);
|
|
1804
|
+
for (const alternative of [
|
|
1805
|
+
"left",
|
|
1806
|
+
"right",
|
|
1807
|
+
"top",
|
|
1808
|
+
"bottom"
|
|
1809
|
+
]) {
|
|
1810
|
+
const p = { ...anchor(bounds, alternative) };
|
|
1811
|
+
if (alternative === "left" || alternative === "right") p.y = Math.max(bounds.y, Math.min(bounds.y + bounds.height, toward.y));
|
|
1812
|
+
else p.x = Math.max(bounds.x, Math.min(bounds.x + bounds.width, toward.x));
|
|
1813
|
+
if (!blocked(p, alternative)) {
|
|
1814
|
+
side = alternative;
|
|
1815
|
+
point = p;
|
|
1816
|
+
break;
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1430
1821
|
return {
|
|
1431
1822
|
point,
|
|
1432
1823
|
side: attachment?.facing === "inward" ? {
|
|
@@ -1461,6 +1852,30 @@ function routeEdge(edge, state, style, metrics$1, batch) {
|
|
|
1461
1852
|
`n:${edge.targetId}`,
|
|
1462
1853
|
`e:${edge.id}`
|
|
1463
1854
|
]), metrics$1, batch, config.radius * 2 + lane * config.edgeSpacing) : context;
|
|
1855
|
+
let futureTargetCorridor;
|
|
1856
|
+
const protectFutureTarget = (ctx) => {
|
|
1857
|
+
const obstacles = ctx.obstacles;
|
|
1858
|
+
ctx.obstacles = (bounds) => {
|
|
1859
|
+
const result = obstacles(bounds);
|
|
1860
|
+
return futureTargetCorridor && intersects(bounds, futureTargetCorridor) ? [...result, futureTargetCorridor] : result;
|
|
1861
|
+
};
|
|
1862
|
+
return ctx;
|
|
1863
|
+
};
|
|
1864
|
+
for (const ctx of new Set([context, searchContext])) protectFutureTarget(ctx);
|
|
1865
|
+
const drawn = [];
|
|
1866
|
+
const selfCost = (a, b) => drawn.reduce((cost, segment) => {
|
|
1867
|
+
return cost + (conflictCost(a, b, segment, {
|
|
1868
|
+
...config,
|
|
1869
|
+
edgeSpacing: 0,
|
|
1870
|
+
crossingPenalty: 0,
|
|
1871
|
+
overlapPenalty: 1
|
|
1872
|
+
}) > 1e-8 ? Infinity : conflictCost(a, b, segment, config));
|
|
1873
|
+
}, 0);
|
|
1874
|
+
for (const ctx of new Set([context, searchContext])) {
|
|
1875
|
+
const cost = ctx.edgeCost, guides = ctx.guides;
|
|
1876
|
+
ctx.edgeCost = (a, b) => (cost?.(a, b) ?? 0) + selfCost(a, b);
|
|
1877
|
+
ctx.guides = (bounds) => [...guides?.(bounds) ?? [], ...drawn.map((s) => reservationBounds(s, config.edgeSpacing))];
|
|
1878
|
+
}
|
|
1464
1879
|
const labelAnchor = (toward) => {
|
|
1465
1880
|
const side = sideToward(center(label), toward), point = anchor(label, side);
|
|
1466
1881
|
if ((side === "left" || side === "right") && toward.y > label.y && toward.y < label.y + label.height) return {
|
|
@@ -1482,8 +1897,18 @@ function routeEdge(edge, state, style, metrics$1, batch) {
|
|
|
1482
1897
|
}
|
|
1483
1898
|
} : target;
|
|
1484
1899
|
const lastSource = label ? {
|
|
1485
|
-
point:
|
|
1486
|
-
|
|
1900
|
+
point: firstTarget.side === sideToward(center(label), target.point) ? anchor(label, {
|
|
1901
|
+
left: "right",
|
|
1902
|
+
right: "left",
|
|
1903
|
+
top: "bottom",
|
|
1904
|
+
bottom: "top"
|
|
1905
|
+
}[firstTarget.side]) : labelAnchor(target.point),
|
|
1906
|
+
side: firstTarget.side === sideToward(center(label), target.point) ? {
|
|
1907
|
+
left: "right",
|
|
1908
|
+
right: "left",
|
|
1909
|
+
top: "bottom",
|
|
1910
|
+
bottom: "top"
|
|
1911
|
+
}[firstTarget.side] : sideToward(center(label), target.point),
|
|
1487
1912
|
ref: {
|
|
1488
1913
|
kind: "label",
|
|
1489
1914
|
edgeId: edge.id
|
|
@@ -1502,21 +1927,68 @@ function routeEdge(edge, state, style, metrics$1, batch) {
|
|
|
1502
1927
|
function connection(a, b, via$1 = []) {
|
|
1503
1928
|
let path, preferred;
|
|
1504
1929
|
if (distance(a.point, b.point) < 1e-8 && !via$1.length) {
|
|
1505
|
-
|
|
1930
|
+
const v = vector(a.side), tangent = {
|
|
1931
|
+
x: -v.y,
|
|
1932
|
+
y: v.x
|
|
1933
|
+
};
|
|
1506
1934
|
const gap = config.clearance + config.edgeSpacing + 10;
|
|
1935
|
+
const candidates = [];
|
|
1936
|
+
for (const extent of [
|
|
1937
|
+
config.clearance + 1,
|
|
1938
|
+
(config.clearance + 1) * 2,
|
|
1939
|
+
gap,
|
|
1940
|
+
gap * 2,
|
|
1941
|
+
gap * 3
|
|
1942
|
+
]) for (const sign of [1, -1]) {
|
|
1943
|
+
const lead = Math.min(config.clearance + 1, extent / 3);
|
|
1944
|
+
const at = (normal, lateral) => ({
|
|
1945
|
+
x: a.point.x + v.x * normal + tangent.x * lateral,
|
|
1946
|
+
y: a.point.y + v.y * normal + tangent.y * lateral
|
|
1947
|
+
});
|
|
1948
|
+
const candidate = pathFromPoints([
|
|
1949
|
+
a.point,
|
|
1950
|
+
at(lead, 0),
|
|
1951
|
+
at(lead, sign * extent),
|
|
1952
|
+
at(extent, sign * extent),
|
|
1953
|
+
at(extent, 0),
|
|
1954
|
+
b.point
|
|
1955
|
+
]);
|
|
1956
|
+
if (safe(candidate, context)) candidates.push(candidate);
|
|
1957
|
+
}
|
|
1958
|
+
if (!candidates.length) {
|
|
1959
|
+
const hard = contextFor(state, excluded, new Set([
|
|
1960
|
+
`n:${edge.sourceId}`,
|
|
1961
|
+
`n:${edge.targetId}`,
|
|
1962
|
+
`e:${edge.id}`
|
|
1963
|
+
]), metrics$1, batch, -config.clearance);
|
|
1964
|
+
for (const extent of [
|
|
1965
|
+
config.clearance + 1,
|
|
1966
|
+
gap,
|
|
1967
|
+
gap * 2
|
|
1968
|
+
]) for (const sign of [1, -1]) {
|
|
1969
|
+
const at = (normal, lateral) => ({
|
|
1970
|
+
x: a.point.x + v.x * normal + tangent.x * lateral,
|
|
1971
|
+
y: a.point.y + v.y * normal + tangent.y * lateral
|
|
1972
|
+
});
|
|
1973
|
+
const candidate = pathFromPoints([
|
|
1974
|
+
a.point,
|
|
1975
|
+
at(1, 0),
|
|
1976
|
+
at(1, sign * extent),
|
|
1977
|
+
at(extent, sign * extent),
|
|
1978
|
+
at(extent, 0),
|
|
1979
|
+
b.point
|
|
1980
|
+
]);
|
|
1981
|
+
if (safe(candidate, hard)) candidates.push(candidate);
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
candidates.sort((x, y) => pathCost(x, context) - pathCost(y, context));
|
|
1985
|
+
if (candidates.length) return candidates[0];
|
|
1986
|
+
report("CONSTRAINT_VIOLATION", "Coincident terminals; no clear outward loop corridor");
|
|
1507
1987
|
return pathFromPoints([
|
|
1508
1988
|
a.point,
|
|
1509
1989
|
{
|
|
1510
|
-
x: a.point.x + gap,
|
|
1511
|
-
y: a.point.y
|
|
1512
|
-
},
|
|
1513
|
-
{
|
|
1514
|
-
x: a.point.x + gap,
|
|
1515
|
-
y: a.point.y - gap
|
|
1516
|
-
},
|
|
1517
|
-
{
|
|
1518
|
-
x: a.point.x,
|
|
1519
|
-
y: a.point.y - gap
|
|
1990
|
+
x: a.point.x + v.x * gap,
|
|
1991
|
+
y: a.point.y + v.y * gap
|
|
1520
1992
|
},
|
|
1521
1993
|
b.point
|
|
1522
1994
|
]);
|
|
@@ -1555,16 +2027,19 @@ function routeEdge(edge, state, style, metrics$1, batch) {
|
|
|
1555
2027
|
}
|
|
1556
2028
|
}
|
|
1557
2029
|
if (!path) {
|
|
1558
|
-
const
|
|
1559
|
-
|
|
1560
|
-
const
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
2030
|
+
const alignedFacing = (a.point.x === b.point.x || a.point.y === b.point.y) && (b.point.x - a.point.x) * vector(a.side).x + (b.point.y - a.point.y) * vector(a.side).y > 0 && (a.point.x - b.point.x) * vector(b.side).x + (a.point.y - b.point.y) * vector(b.side).y > 0;
|
|
2031
|
+
const boundedStub = (t) => {
|
|
2032
|
+
const p = stub(t), gap = distance(a.point, b.point) / 3;
|
|
2033
|
+
if (!alignedFacing || distance(t.point, p) <= gap) return p;
|
|
2034
|
+
const v = vector(t.side);
|
|
2035
|
+
return {
|
|
2036
|
+
x: t.point.x + v.x * gap,
|
|
2037
|
+
y: t.point.y + v.y * gap
|
|
2038
|
+
};
|
|
2039
|
+
};
|
|
2040
|
+
const start = boundedStub(a), end = boundedStub(b), waypoints = [...via$1];
|
|
1566
2041
|
if (style === "parallel" && peers.length > 1) {
|
|
1567
|
-
const base =
|
|
2042
|
+
const base = findPathBetweenLeads(start, end, "polyline", context, [[a.point, start], [end, b.point]]);
|
|
1568
2043
|
const mid = base && base.length > 2 ? base[Math.floor(base.length / 2)] : center(segmentBounds(start, end));
|
|
1569
2044
|
const d = Math.max(1, distance(start, end)), rank = peers.indexOf(edge.id) + 1;
|
|
1570
2045
|
const orientation = edge.sourceId < edge.targetId ? 1 : -1;
|
|
@@ -1596,27 +2071,154 @@ function routeEdge(edge, state, style, metrics$1, batch) {
|
|
|
1596
2071
|
y: -vector(b.side).y
|
|
1597
2072
|
} : void 0
|
|
1598
2073
|
};
|
|
1599
|
-
let found =
|
|
2074
|
+
let found = findPathBetweenLeads(anchors[i - 1], anchors[i], searchStyle, searchContext, [[a.point, start], [end, b.point]], directions);
|
|
1600
2075
|
if (!found && searchContext !== context && !searchContext.budgetExceeded) {
|
|
1601
2076
|
const narrow = {
|
|
1602
2077
|
...searchContext,
|
|
1603
2078
|
obstacles: context.obstacles
|
|
1604
2079
|
};
|
|
1605
|
-
found =
|
|
2080
|
+
found = findPathBetweenLeads(anchors[i - 1], anchors[i], searchStyle, narrow, [[a.point, start], [end, b.point]], directions);
|
|
1606
2081
|
searchContext.visited = narrow.visited;
|
|
1607
2082
|
searchContext.budgetExceeded = narrow.budgetExceeded;
|
|
1608
2083
|
}
|
|
1609
|
-
if (found) points.push(...found.slice(i === 1 ? 0 : 1));
|
|
1610
|
-
else ok = false;
|
|
1611
|
-
}
|
|
1612
|
-
points.push(b.point);
|
|
1613
|
-
if (ok) {
|
|
1614
|
-
const simple = simplify(points);
|
|
1615
|
-
path = style === "curved" || style === "bundle" || style === "bezier" ? curve(simple, state, context) : style === "organic" ? organic(simple, state, context) : pathFromPoints(simple);
|
|
2084
|
+
if (found) points.push(...found.slice(i === 1 ? 0 : 1));
|
|
2085
|
+
else ok = false;
|
|
2086
|
+
}
|
|
2087
|
+
points.push(b.point);
|
|
2088
|
+
if (ok) {
|
|
2089
|
+
const simple = simplify(points);
|
|
2090
|
+
path = style === "curved" || style === "bundle" || style === "bezier" ? curve(simple, state, context) : style === "organic" ? organic(simple, state, context) : pathFromPoints(simple);
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
if (preferred && Number.isFinite(pathCost(preferred, context)) && (!path || !safe(path, context) || pathCost(preferred, context) <= pathCost(path, context))) return preferred;
|
|
2094
|
+
if (path && safe(path, context) && Number.isFinite(pathCost(path, context))) return path;
|
|
2095
|
+
if (![
|
|
2096
|
+
"straight",
|
|
2097
|
+
"bezier",
|
|
2098
|
+
"polyline",
|
|
2099
|
+
"organic",
|
|
2100
|
+
"parallel"
|
|
2101
|
+
].includes(style)) {
|
|
2102
|
+
const hard = {
|
|
2103
|
+
...contextFor(state, excluded, new Set([
|
|
2104
|
+
`n:${edge.sourceId}`,
|
|
2105
|
+
`n:${edge.targetId}`,
|
|
2106
|
+
`e:${edge.id}`
|
|
2107
|
+
]), metrics$1, batch, -config.clearance),
|
|
2108
|
+
maxSearchNodes: Math.max(0, config.maxSearchNodes - searchContext.visited - (searchContext === context ? 0 : context.visited))
|
|
2109
|
+
};
|
|
2110
|
+
protectFutureTarget(hard);
|
|
2111
|
+
hard.guides = void 0;
|
|
2112
|
+
hard.edgeCost = selfCost;
|
|
2113
|
+
const lead = (t) => {
|
|
2114
|
+
const v = vector(t.side);
|
|
2115
|
+
for (let length = 1; length >= 1e-6; length /= 2) {
|
|
2116
|
+
const point = {
|
|
2117
|
+
x: t.point.x + v.x * length,
|
|
2118
|
+
y: t.point.y + v.y * length
|
|
2119
|
+
};
|
|
2120
|
+
if (clear(t.point, point, hard)) return point;
|
|
2121
|
+
}
|
|
2122
|
+
return t.point;
|
|
2123
|
+
};
|
|
2124
|
+
const start = lead(a), end = lead(b), anchors = [
|
|
2125
|
+
start,
|
|
2126
|
+
...via$1,
|
|
2127
|
+
end
|
|
2128
|
+
];
|
|
2129
|
+
const points = [a.point];
|
|
2130
|
+
let ok = clear(a.point, start, hard) && clear(end, b.point, hard);
|
|
2131
|
+
for (let i = 1; ok && i < anchors.length; i++) {
|
|
2132
|
+
const found = findPathBetweenLeads(anchors[i - 1], anchors[i], style === "octilinear" ? "octilinear" : "orthogonal", hard, [[a.point, start], [end, b.point]], {
|
|
2133
|
+
incoming: i === 1 ? vector(a.side) : void 0,
|
|
2134
|
+
outgoing: i === anchors.length - 1 ? {
|
|
2135
|
+
x: -vector(b.side).x,
|
|
2136
|
+
y: -vector(b.side).y
|
|
2137
|
+
} : void 0
|
|
2138
|
+
});
|
|
2139
|
+
if (found) points.push(...found.slice(i === 1 ? 0 : 1));
|
|
2140
|
+
else ok = false;
|
|
2141
|
+
}
|
|
2142
|
+
points.push(b.point);
|
|
2143
|
+
const retry = pathFromPoints(simplify(points));
|
|
2144
|
+
searchContext.visited += hard.visited;
|
|
2145
|
+
if (ok && safe(retry, hard) && Number.isFinite(pathCost(retry, hard))) {
|
|
2146
|
+
let separated = retry;
|
|
2147
|
+
for (let pass = 0; pass < 6; pass++) {
|
|
2148
|
+
const track = [separated.start, ...separated.segments.map((s) => s.to)];
|
|
2149
|
+
let best = separated, bestCost = pathCost(separated, context);
|
|
2150
|
+
for (let i = 1; i < track.length - 2; i++) {
|
|
2151
|
+
const p = track[i], q = track[i + 1];
|
|
2152
|
+
if ((context.edgeCost?.(p, q) ?? 0) === 0) continue;
|
|
2153
|
+
const horizontal = p.y === q.y;
|
|
2154
|
+
if (!horizontal && p.x !== q.x) continue;
|
|
2155
|
+
for (const offset of [
|
|
2156
|
+
-config.edgeSpacing,
|
|
2157
|
+
config.edgeSpacing,
|
|
2158
|
+
-config.edgeSpacing * 2,
|
|
2159
|
+
config.edgeSpacing * 2,
|
|
2160
|
+
-config.edgeSpacing / 2,
|
|
2161
|
+
config.edgeSpacing / 2,
|
|
2162
|
+
-config.edgeSpacing / 3,
|
|
2163
|
+
config.edgeSpacing / 3,
|
|
2164
|
+
-config.edgeSpacing * 4,
|
|
2165
|
+
config.edgeSpacing * 4
|
|
2166
|
+
]) {
|
|
2167
|
+
const candidatePoints = track.map((r, j) => j === i || j === i + 1 ? horizontal ? {
|
|
2168
|
+
x: r.x,
|
|
2169
|
+
y: r.y + offset
|
|
2170
|
+
} : {
|
|
2171
|
+
x: r.x + offset,
|
|
2172
|
+
y: r.y
|
|
2173
|
+
} : r);
|
|
2174
|
+
if (candidatePoints.slice(1).some((r, j) => r.x !== candidatePoints[j].x && r.y !== candidatePoints[j].y)) continue;
|
|
2175
|
+
const candidate = pathFromPoints(simplify(candidatePoints));
|
|
2176
|
+
const segments = pathReservations(candidate);
|
|
2177
|
+
if (segments.some((s, j) => segments.slice(j + 1).some((t) => {
|
|
2178
|
+
if (s.a.x === s.b.x && t.a.x === t.b.x && s.a.x === t.a.x) return Math.min(Math.max(s.a.y, s.b.y), Math.max(t.a.y, t.b.y)) - Math.max(Math.min(s.a.y, s.b.y), Math.min(t.a.y, t.b.y)) > 1e-8;
|
|
2179
|
+
if (s.a.y === s.b.y && t.a.y === t.b.y && s.a.y === t.a.y) return Math.min(Math.max(s.a.x, s.b.x), Math.max(t.a.x, t.b.x)) - Math.max(Math.min(s.a.x, s.b.x), Math.min(t.a.x, t.b.x)) > 1e-8;
|
|
2180
|
+
return false;
|
|
2181
|
+
})) || !safe(candidate, hard)) continue;
|
|
2182
|
+
const cost = pathCost(candidate, context);
|
|
2183
|
+
if (cost < bestCost) {
|
|
2184
|
+
best = candidate;
|
|
2185
|
+
bestCost = cost;
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
if (best === separated) break;
|
|
2190
|
+
separated = best;
|
|
2191
|
+
}
|
|
2192
|
+
if (!via$1.length && pathCost(retry, context) > pathLength(retry)) {
|
|
2193
|
+
const remaining = Math.max(0, config.maxSearchNodes - searchContext.visited - (searchContext === context ? 0 : context.visited));
|
|
2194
|
+
const optimized = {
|
|
2195
|
+
...hard,
|
|
2196
|
+
visited: 0,
|
|
2197
|
+
budgetExceeded: false,
|
|
2198
|
+
maxSearchNodes: Math.min(2e3, remaining),
|
|
2199
|
+
edgeCost: (a$1, b$1) => (batch.edgeCost?.(a$1, b$1) ?? 0) + selfCost(a$1, b$1),
|
|
2200
|
+
guides: (bounds) => (batch.guides?.(bounds) ?? []).slice(0, 8)
|
|
2201
|
+
};
|
|
2202
|
+
const found = findPathBetweenLeads(start, end, style === "octilinear" ? "octilinear" : "orthogonal", optimized, [[a.point, start], [end, b.point]], {
|
|
2203
|
+
incoming: vector(a.side),
|
|
2204
|
+
outgoing: {
|
|
2205
|
+
x: -vector(b.side).x,
|
|
2206
|
+
y: -vector(b.side).y
|
|
2207
|
+
}
|
|
2208
|
+
});
|
|
2209
|
+
searchContext.visited += optimized.visited;
|
|
2210
|
+
if (found) {
|
|
2211
|
+
const candidate = pathFromPoints(simplify([
|
|
2212
|
+
a.point,
|
|
2213
|
+
...found,
|
|
2214
|
+
b.point
|
|
2215
|
+
]));
|
|
2216
|
+
if (safe(candidate, hard) && pathCost(candidate, context) < pathCost(separated, context)) return candidate;
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
return separated;
|
|
1616
2220
|
}
|
|
1617
2221
|
}
|
|
1618
|
-
if (preferred && (!path || !safe(path, context) || pathCost(preferred, context) <= pathCost(path, context))) return preferred;
|
|
1619
|
-
if (path && safe(path, context)) return path;
|
|
1620
2222
|
report(searchContext.budgetExceeded ? "SEARCH_BUDGET" : "ROUTE_BLOCKED", "Preferred route unavailable; fallback may cross obstacles or violate routing constraints");
|
|
1621
2223
|
if (loop && !via$1.length) {
|
|
1622
2224
|
const r = inflate(sourceBounds, config.clearance + config.edgeSpacing + 1);
|
|
@@ -1645,6 +2247,25 @@ function routeEdge(edge, state, style, metrics$1, batch) {
|
|
|
1645
2247
|
}
|
|
1646
2248
|
const via = perEdge.waypoints ?? [];
|
|
1647
2249
|
if (via.some((p) => !Number.isFinite(p.x) || !Number.isFinite(p.y))) throw new RangeError(`Non-finite waypoint on ${edge.id}`);
|
|
2250
|
+
if (lastSource && !loop) {
|
|
2251
|
+
const hard = contextFor(state, excluded, new Set([
|
|
2252
|
+
`n:${edge.sourceId}`,
|
|
2253
|
+
`n:${edge.targetId}`,
|
|
2254
|
+
`e:${edge.id}`
|
|
2255
|
+
]), metrics$1, batch, -config.clearance);
|
|
2256
|
+
const v = vector(target.side);
|
|
2257
|
+
for (let length = config.clearance + 1; length >= 1e-6; length /= 2) {
|
|
2258
|
+
const lead = {
|
|
2259
|
+
x: target.point.x + v.x * length,
|
|
2260
|
+
y: target.point.y + v.y * length
|
|
2261
|
+
};
|
|
2262
|
+
const corridor = inflate(segmentBounds(target.point, lead), Math.min(.5, length / 4));
|
|
2263
|
+
if (clear(target.point, lead, hard) && !intersects(segmentBounds(source.point, source.point), corridor) && !intersects(segmentBounds(firstTarget.point, firstTarget.point), corridor)) {
|
|
2264
|
+
futureTargetCorridor = corridor;
|
|
2265
|
+
break;
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
1648
2269
|
const plan = grouped ? groupPlan(group, state, metrics$1, batch, style) : void 0;
|
|
1649
2270
|
if (grouped && !plan) report("ROUTE_BLOCKED", "Shared trunk unavailable; used individual fallback routing");
|
|
1650
2271
|
if (plan) {
|
|
@@ -1688,12 +2309,19 @@ function routeEdge(edge, state, style, metrics$1, batch) {
|
|
|
1688
2309
|
to: firstTarget.ref,
|
|
1689
2310
|
path: connection(source, firstTarget, via)
|
|
1690
2311
|
});
|
|
1691
|
-
if (lastSource)
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
2312
|
+
if (lastSource) {
|
|
2313
|
+
futureTargetCorridor = void 0;
|
|
2314
|
+
let segments = sections.flatMap((s) => pathReservations(s.path));
|
|
2315
|
+
segments = segments.flatMap((s) => outsideTerminal(s, label, false));
|
|
2316
|
+
if (loop) segments = segments.flatMap((s) => outsideTerminal(s, inflate(sourceBounds, config.clearance + 1)));
|
|
2317
|
+
drawn.push(...segments);
|
|
2318
|
+
sections.push({
|
|
2319
|
+
id: `${edge.id}:label-target`,
|
|
2320
|
+
from: lastSource.ref,
|
|
2321
|
+
to: target.ref,
|
|
2322
|
+
path: connection(lastSource, target)
|
|
2323
|
+
});
|
|
2324
|
+
}
|
|
1697
2325
|
if (sections.some((section) => pathReservations(section.path).some((segment) => batch.conflicts?.(segment.a, segment.b)))) report("ROUTE_CONFLICT", "Route crosses or overlaps another edge; retained a compact visible path");
|
|
1698
2326
|
metrics$1.searchNodes += searchContext.visited + (searchContext === context ? 0 : context.visited);
|
|
1699
2327
|
return {
|
|
@@ -2120,250 +2748,6 @@ function applyRoutePatches(routes, patches) {
|
|
|
2120
2748
|
return result;
|
|
2121
2749
|
}
|
|
2122
2750
|
|
|
2123
|
-
//#endregion
|
|
2124
|
-
//#region src/routing/layout-cache.ts
|
|
2125
|
-
const geometryByRoutes = /* @__PURE__ */ new WeakMap();
|
|
2126
|
-
/** Compare only geometry and topology; application data may contain cycles. */
|
|
2127
|
-
function geometryKey(graph) {
|
|
2128
|
-
return JSON.stringify({
|
|
2129
|
-
nodes: graph.nodes.map(({ id, parentId, x, y, width, height: height$1, ports }) => ({
|
|
2130
|
-
id,
|
|
2131
|
-
parentId,
|
|
2132
|
-
x,
|
|
2133
|
-
y,
|
|
2134
|
-
width,
|
|
2135
|
-
height: height$1,
|
|
2136
|
-
ports: ports?.map(({ name, x: x$1, y: y$1, width: width$1, height: height$2 }) => ({
|
|
2137
|
-
name,
|
|
2138
|
-
x: x$1,
|
|
2139
|
-
y: y$1,
|
|
2140
|
-
width: width$1,
|
|
2141
|
-
height: height$2
|
|
2142
|
-
}))
|
|
2143
|
-
})),
|
|
2144
|
-
edges: graph.edges.map(({ id, sourceId, targetId, sourcePort, targetPort, x, y, width, height: height$1, points, routing }) => ({
|
|
2145
|
-
id,
|
|
2146
|
-
sourceId,
|
|
2147
|
-
targetId,
|
|
2148
|
-
sourcePort,
|
|
2149
|
-
targetPort,
|
|
2150
|
-
x,
|
|
2151
|
-
y,
|
|
2152
|
-
width,
|
|
2153
|
-
height: height$1,
|
|
2154
|
-
points,
|
|
2155
|
-
routing
|
|
2156
|
-
}))
|
|
2157
|
-
});
|
|
2158
|
-
}
|
|
2159
|
-
/** Associate structured sections with the geometry that produced them. */
|
|
2160
|
-
function recordRouteGeometry(graph, routes) {
|
|
2161
|
-
geometryByRoutes.set(routes, geometryKey(graph));
|
|
2162
|
-
}
|
|
2163
|
-
/** Graph edits invalidate cached sections, including edits made by external consumers. */
|
|
2164
|
-
function hasCurrentRouteGeometry(graph, routes) {
|
|
2165
|
-
return geometryByRoutes.get(routes) === geometryKey(graph);
|
|
2166
|
-
}
|
|
2167
|
-
|
|
2168
|
-
//#endregion
|
|
2169
|
-
//#region src/authoring/coordinates.ts
|
|
2170
|
-
/** Legacy sibling edges are parent-relative; native compound output marks world-space edges. */
|
|
2171
|
-
function frames(graph) {
|
|
2172
|
-
const nodes = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
2173
|
-
const offsets = /* @__PURE__ */ new Map();
|
|
2174
|
-
const visiting = /* @__PURE__ */ new Set();
|
|
2175
|
-
function offset(id) {
|
|
2176
|
-
const cached = offsets.get(id);
|
|
2177
|
-
if (cached) return cached;
|
|
2178
|
-
if (visiting.has(id)) throw new LayoutError(`Cyclic parent hierarchy at ${id}`, "INVALID_GRAPH");
|
|
2179
|
-
visiting.add(id);
|
|
2180
|
-
const parentId = nodes.get(id)?.parentId;
|
|
2181
|
-
if (parentId == null) {
|
|
2182
|
-
const origin = {
|
|
2183
|
-
x: 0,
|
|
2184
|
-
y: 0
|
|
2185
|
-
};
|
|
2186
|
-
visiting.delete(id);
|
|
2187
|
-
offsets.set(id, origin);
|
|
2188
|
-
return origin;
|
|
2189
|
-
}
|
|
2190
|
-
const parent = nodes.get(parentId);
|
|
2191
|
-
const ancestor = offset(parentId);
|
|
2192
|
-
const result = {
|
|
2193
|
-
x: ancestor.x + parent.x,
|
|
2194
|
-
y: ancestor.y + parent.y
|
|
2195
|
-
};
|
|
2196
|
-
visiting.delete(id);
|
|
2197
|
-
offsets.set(id, result);
|
|
2198
|
-
return result;
|
|
2199
|
-
}
|
|
2200
|
-
for (const node$1 of graph.nodes) offsets.set(node$1.id, offset(node$1.id));
|
|
2201
|
-
return {
|
|
2202
|
-
nodes: offsets,
|
|
2203
|
-
edges: new Map(graph.edges.map((edge) => {
|
|
2204
|
-
const source = nodes.get(edge.sourceId);
|
|
2205
|
-
const target = nodes.get(edge.targetId);
|
|
2206
|
-
return [edge.id, graph.edgeCoordinateSpace !== "world" && (source.parentId ?? null) === (target.parentId ?? null) ? offset(source.id) : {
|
|
2207
|
-
x: 0,
|
|
2208
|
-
y: 0
|
|
2209
|
-
}];
|
|
2210
|
-
}))
|
|
2211
|
-
};
|
|
2212
|
-
}
|
|
2213
|
-
function worldGeometry(graph) {
|
|
2214
|
-
const offsets = frames(graph);
|
|
2215
|
-
const base = { ...graph };
|
|
2216
|
-
delete base.compoundRoutes;
|
|
2217
|
-
delete base.compoundGeometry;
|
|
2218
|
-
return {
|
|
2219
|
-
...base,
|
|
2220
|
-
nodes: graph.nodes.map((n) => {
|
|
2221
|
-
const p = offsets.nodes.get(n.id);
|
|
2222
|
-
return p.x === 0 && p.y === 0 ? n : {
|
|
2223
|
-
...n,
|
|
2224
|
-
x: n.x + p.x,
|
|
2225
|
-
y: n.y + p.y
|
|
2226
|
-
};
|
|
2227
|
-
}),
|
|
2228
|
-
edges: graph.edges.map((e) => {
|
|
2229
|
-
const p = offsets.edges.get(e.id);
|
|
2230
|
-
return p.x === 0 && p.y === 0 ? e : {
|
|
2231
|
-
...e,
|
|
2232
|
-
x: e.x + p.x,
|
|
2233
|
-
y: e.y + p.y,
|
|
2234
|
-
...e.points ? { points: e.points.map((v) => ({
|
|
2235
|
-
x: v.x + p.x,
|
|
2236
|
-
y: v.y + p.y
|
|
2237
|
-
})) } : {}
|
|
2238
|
-
};
|
|
2239
|
-
})
|
|
2240
|
-
};
|
|
2241
|
-
}
|
|
2242
|
-
/** Ancestor frames stay fixed during authoring. Preserve untouched coordinates exactly. */
|
|
2243
|
-
function nativeGeometry(graph, original) {
|
|
2244
|
-
const offsets = frames(original);
|
|
2245
|
-
const world = worldGeometry(original);
|
|
2246
|
-
const originalNodes = new Map(original.nodes.map((n) => [n.id, n]));
|
|
2247
|
-
const originalEdges = new Map(original.edges.map((e) => [e.id, e]));
|
|
2248
|
-
const worldNodes = new Map(world.nodes.map((n) => [n.id, n]));
|
|
2249
|
-
const worldEdges = new Map(world.edges.map((e) => [e.id, e]));
|
|
2250
|
-
return {
|
|
2251
|
-
...graph,
|
|
2252
|
-
nodes: graph.nodes.map((n) => {
|
|
2253
|
-
const p = offsets.nodes.get(n.id), before = originalNodes.get(n.id), prior = worldNodes.get(n.id);
|
|
2254
|
-
return {
|
|
2255
|
-
...n,
|
|
2256
|
-
x: n.x === prior.x ? before.x : n.x - p.x,
|
|
2257
|
-
y: n.y === prior.y ? before.y : n.y - p.y
|
|
2258
|
-
};
|
|
2259
|
-
}),
|
|
2260
|
-
edges: graph.edges.map((e) => {
|
|
2261
|
-
const p = offsets.edges.get(e.id), before = originalEdges.get(e.id), prior = worldEdges.get(e.id);
|
|
2262
|
-
const points = JSON.stringify(e.points) === JSON.stringify(prior.points) ? before.points : e.points?.map((v) => ({
|
|
2263
|
-
x: v.x - p.x,
|
|
2264
|
-
y: v.y - p.y
|
|
2265
|
-
}));
|
|
2266
|
-
return {
|
|
2267
|
-
...e,
|
|
2268
|
-
x: e.x === prior.x ? before.x : e.x - p.x,
|
|
2269
|
-
y: e.y === prior.y ? before.y : e.y - p.y,
|
|
2270
|
-
...points === void 0 ? {} : { points }
|
|
2271
|
-
};
|
|
2272
|
-
})
|
|
2273
|
-
};
|
|
2274
|
-
}
|
|
2275
|
-
|
|
2276
|
-
//#endregion
|
|
2277
|
-
//#region src/routing/adapters.ts
|
|
2278
|
-
/** ELK/native spline output: start, then repeating control1/control2/end triples. */
|
|
2279
|
-
function pathFromSplinePoints(points) {
|
|
2280
|
-
if (points.length < 4 || (points.length - 1) % 3 !== 0) throw new RangeError("Cubic spline points require a start and control1/control2/end triples");
|
|
2281
|
-
const segments = [];
|
|
2282
|
-
for (let i = 1; i < points.length; i += 3) segments.push({
|
|
2283
|
-
kind: "cubic",
|
|
2284
|
-
control1: { ...points[i] },
|
|
2285
|
-
control2: { ...points[i + 1] },
|
|
2286
|
-
to: { ...points[i + 2] }
|
|
2287
|
-
});
|
|
2288
|
-
return {
|
|
2289
|
-
start: { ...points[0] },
|
|
2290
|
-
segments
|
|
2291
|
-
};
|
|
2292
|
-
}
|
|
2293
|
-
/** Normalize routes from any native layout algorithm, without running layout again. */
|
|
2294
|
-
function getLayoutRoutes(graph) {
|
|
2295
|
-
const compound = graph;
|
|
2296
|
-
if (compound.compoundRoutes?.size && hasCurrentRouteGeometry(graph, compound.compoundRoutes)) return compound.compoundRoutes;
|
|
2297
|
-
const world = worldGeometry(graph), nodes = new Map(world.nodes.map((n) => [n.id, n]));
|
|
2298
|
-
let result = new PersistentMap();
|
|
2299
|
-
for (const edge of world.edges) {
|
|
2300
|
-
const points = edge.points;
|
|
2301
|
-
let path;
|
|
2302
|
-
if (points?.length && points.every((p) => Number.isFinite(p.x) && Number.isFinite(p.y))) if (edge.routing === "splines") {
|
|
2303
|
-
if (points.length >= 4 && (points.length - 1) % 3 === 0) path = pathFromSplinePoints(points);
|
|
2304
|
-
} else path = pathFromPoints(points);
|
|
2305
|
-
const routed = path !== void 0;
|
|
2306
|
-
if (!path) {
|
|
2307
|
-
const a = nodes.get(edge.sourceId), b = nodes.get(edge.targetId);
|
|
2308
|
-
path = pathFromPoints([{
|
|
2309
|
-
x: a ? a.x + a.width : 0,
|
|
2310
|
-
y: a ? a.y + a.height / 2 : 0
|
|
2311
|
-
}, {
|
|
2312
|
-
x: b?.x ?? 40,
|
|
2313
|
-
y: b ? b.y + b.height / 2 : 40
|
|
2314
|
-
}]);
|
|
2315
|
-
}
|
|
2316
|
-
result = result.set(edge.id, freeze({
|
|
2317
|
-
edgeId: edge.id,
|
|
2318
|
-
status: routed ? "routed" : "fallback",
|
|
2319
|
-
diagnostics: routed ? [] : [{
|
|
2320
|
-
code: "MISSING_GEOMETRY",
|
|
2321
|
-
edgeId: edge.id,
|
|
2322
|
-
message: "Layout has no valid route; rendered a fallback connection"
|
|
2323
|
-
}],
|
|
2324
|
-
sections: [{
|
|
2325
|
-
id: `${edge.id}:0`,
|
|
2326
|
-
from: {
|
|
2327
|
-
kind: "node",
|
|
2328
|
-
nodeId: edge.sourceId,
|
|
2329
|
-
...edge.sourcePort === void 0 ? {} : { port: edge.sourcePort }
|
|
2330
|
-
},
|
|
2331
|
-
to: {
|
|
2332
|
-
kind: "node",
|
|
2333
|
-
nodeId: edge.targetId,
|
|
2334
|
-
...edge.targetPort === void 0 ? {} : { port: edge.targetPort }
|
|
2335
|
-
},
|
|
2336
|
-
path
|
|
2337
|
-
}]
|
|
2338
|
-
}));
|
|
2339
|
-
}
|
|
2340
|
-
return result;
|
|
2341
|
-
}
|
|
2342
|
-
/** One polyline per section preserves label gaps and branching for simple renderers. */
|
|
2343
|
-
function routeToPolylines(route, options) {
|
|
2344
|
-
return route.sections.map((s) => flattenPath(s.path, options));
|
|
2345
|
-
}
|
|
2346
|
-
/** Compatibility for single-section native consumers. Refuses to erase gaps or topology. */
|
|
2347
|
-
function routeToGraphPatch(route, options = {}) {
|
|
2348
|
-
if (route.sections.length !== 1 || route.sections[0].sharedId || route.sections[0].from.kind !== "node" || route.sections[0].to.kind !== "node") throw new Error("This route requires section-aware rendering; a single GraphEdge.points array would lose topology");
|
|
2349
|
-
const offset = options.offset ?? {
|
|
2350
|
-
x: 0,
|
|
2351
|
-
y: 0
|
|
2352
|
-
};
|
|
2353
|
-
const points = flattenPath(route.sections[0].path, options).map((p) => ({
|
|
2354
|
-
x: p.x - offset.x,
|
|
2355
|
-
y: p.y - offset.y
|
|
2356
|
-
}));
|
|
2357
|
-
return {
|
|
2358
|
-
op: "updateEdge",
|
|
2359
|
-
id: route.edgeId,
|
|
2360
|
-
data: {
|
|
2361
|
-
points,
|
|
2362
|
-
routing: "polyline"
|
|
2363
|
-
}
|
|
2364
|
-
};
|
|
2365
|
-
}
|
|
2366
|
-
|
|
2367
2751
|
//#endregion
|
|
2368
2752
|
//#region src/routing/index.ts
|
|
2369
2753
|
const straightRouting = createRoutingStrategy("straight");
|
|
@@ -2568,6 +2952,12 @@ Object.defineProperty(exports, 'routeToGraphPatch', {
|
|
|
2568
2952
|
return routeToGraphPatch;
|
|
2569
2953
|
}
|
|
2570
2954
|
});
|
|
2955
|
+
Object.defineProperty(exports, 'routeToPoints', {
|
|
2956
|
+
enumerable: true,
|
|
2957
|
+
get: function () {
|
|
2958
|
+
return routeToPoints;
|
|
2959
|
+
}
|
|
2960
|
+
});
|
|
2571
2961
|
Object.defineProperty(exports, 'routeToPolylines', {
|
|
2572
2962
|
enumerable: true,
|
|
2573
2963
|
get: function () {
|