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