@statelyai/layout 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/NOTICE.md +4 -2
  2. package/README.md +150 -11
  3. package/dist/elkjs/index.cjs +2 -2
  4. package/dist/elkjs/index.d.cts +2 -0
  5. package/dist/elkjs/index.d.mts +2 -0
  6. package/dist/elkjs/index.mjs +2 -2
  7. package/dist/elkjs/main.cjs +1 -1
  8. package/dist/elkjs/main.mjs +1 -1
  9. package/dist/elkjs/worker.cjs +2 -2
  10. package/dist/elkjs/worker.mjs +2 -2
  11. package/dist/elkjs-BBfKN4Dw.mjs +3075 -0
  12. package/dist/elkjs-DNfXDPup.cjs +3104 -0
  13. package/dist/{index-BEA_pzeT.d.cts → index-B6cWNrMw.d.cts} +9 -1
  14. package/dist/{index-DNTLRsDl.d.mts → index-CJQLUKD9.d.mts} +9 -1
  15. package/dist/{index-DNixtE3h.d.mts → index-DT6lPMfS.d.cts} +85 -5
  16. package/dist/{index-D9dhxrRl.d.cts → index-F1U-fCTu.d.mts} +85 -5
  17. package/dist/index.cjs +6 -3
  18. package/dist/index.d.cts +3 -3
  19. package/dist/index.d.mts +3 -3
  20. package/dist/index.mjs +4 -4
  21. package/dist/layered/index.cjs +3 -1
  22. package/dist/layered/index.d.cts +2 -2
  23. package/dist/layered/index.d.mts +2 -2
  24. package/dist/layered/index.mjs +2 -2
  25. package/dist/{layered-B2buJqMf.cjs → layered-5RxQhryb.cjs} +11184 -2932
  26. package/dist/{layered-DGFTeQSl.mjs → layered-DSesbctm.mjs} +12059 -3903
  27. package/dist/{layout-engine-Dn1PiFwP.cjs → layout-engine-CLFySz3R.cjs} +5 -18
  28. package/dist/{layout-engine-Cs5tatA6.mjs → layout-engine-GpBIX6ng.mjs} +3 -10
  29. package/dist/routing/index.cjs +2 -1
  30. package/dist/routing/index.d.cts +2 -2
  31. package/dist/routing/index.d.mts +2 -2
  32. package/dist/routing/index.mjs +2 -2
  33. package/dist/{routing-A2hrK9MP.mjs → routing-CR8flv8b.mjs} +977 -593
  34. package/dist/{routing-ujEP7Qh6.cjs → routing-QIdGGFBe.cjs} +986 -596
  35. package/package.json +8 -2
  36. package/src/authoring/coordinates.ts +28 -8
  37. package/src/authoring/partial.ts +7 -2
  38. package/src/authoring/routing.ts +4 -1
  39. package/src/constraints.ts +174 -6
  40. package/src/elkjs/index.ts +1476 -449
  41. package/src/elkjs/layout-quality.ts +267 -0
  42. package/src/elkjs/opposing-tracks.ts +161 -0
  43. package/src/elkjs/orthogonal-junctions.ts +174 -0
  44. package/src/elkjs/statechart.ts +9 -3
  45. package/src/elkjs/types.ts +2 -0
  46. package/src/index.ts +2 -0
  47. package/src/internal/elkjs-compatibility.ts +4 -1
  48. package/src/internal/layered-trace.ts +48 -0
  49. package/src/internal/layout-engine.ts +2 -1
  50. package/src/java-random.ts +6 -0
  51. package/src/layered/bk-node-placement.ts +403 -92
  52. package/src/layered/center-labels.ts +398 -0
  53. package/src/layered/compaction-bounds.ts +26 -0
  54. package/src/layered/compaction-errors.ts +2 -0
  55. package/src/layered/compaction-scanline.ts +63 -0
  56. package/src/layered/compound-boundaries.ts +99 -0
  57. package/src/layered/compound.ts +35 -7
  58. package/src/layered/constraint-orientation.ts +141 -0
  59. package/src/layered/crossing-constraints.ts +243 -0
  60. package/src/layered/crossing-counter.ts +233 -0
  61. package/src/layered/crossing-graph.ts +401 -0
  62. package/src/layered/cycle-random.ts +22 -0
  63. package/src/layered/elk-port-lists.ts +243 -0
  64. package/src/layered/end-labels.ts +124 -0
  65. package/src/layered/external-port-dummy.ts +137 -0
  66. package/src/layered/feedback-chains.ts +114 -0
  67. package/src/layered/fixed-self-loop.ts +150 -0
  68. package/src/layered/greedy-switch-decider.ts +356 -0
  69. package/src/layered/grouped-compaction.ts +787 -0
  70. package/src/layered/gwt-sort.ts +53 -0
  71. package/src/layered/hierarchical-port-phases.ts +242 -0
  72. package/src/layered/hierarchical-port-restoration.ts +233 -0
  73. package/src/layered/hierarchical-port-routing.ts +179 -0
  74. package/src/layered/hierarchy-crossing.ts +121 -0
  75. package/src/layered/hierarchy-sweepiness.ts +79 -0
  76. package/src/layered/hints.ts +371 -0
  77. package/src/layered/hyperedge-dummy-merger.ts +195 -0
  78. package/src/layered/index.ts +875 -161
  79. package/src/layered/initial-model-order.ts +404 -0
  80. package/src/layered/inverted-ports.ts +207 -0
  81. package/src/layered/label-dummy-switcher.ts +200 -0
  82. package/src/layered/long-edges.ts +135 -44
  83. package/src/layered/loop-envelopes.ts +184 -0
  84. package/src/layered/native-routing.ts +275 -0
  85. package/src/layered/network-simplex.ts +11 -15
  86. package/src/layered/node-margins.ts +59 -0
  87. package/src/layered/north-south-ports.ts +271 -0
  88. package/src/layered/opposing-tracks.ts +1044 -0
  89. package/src/layered/orthogonal-cycle-order.ts +106 -0
  90. package/src/layered/orthogonal-hypersegments.ts +154 -0
  91. package/src/layered/orthogonal-junctions.ts +202 -0
  92. package/src/layered/orthogonal-segments.ts +243 -0
  93. package/src/layered/placement-orientation.ts +22 -0
  94. package/src/layered/port-distributor.ts +192 -0
  95. package/src/layered/port-edge-order.ts +20 -0
  96. package/src/layered/replace-routing.ts +45 -0
  97. package/src/layered/routing-coordinates.ts +32 -0
  98. package/src/layered/separate-exterior-labels.ts +24 -6
  99. package/src/layered/spacing.ts +34 -12
  100. package/src/layered/splitter-order.ts +106 -0
  101. package/src/layered/strategies.ts +2124 -886
  102. package/src/layered/types.ts +13 -0
  103. package/src/layered/weighted-compaction.ts +84 -0
  104. package/src/routing/adapters.ts +32 -0
  105. package/src/routing/algorithms.ts +433 -21
  106. package/src/routing/coordination.ts +8 -3
  107. package/src/routing/index.ts +1 -0
  108. package/src/routing/search.ts +93 -11
  109. package/dist/elkjs-DaHMISnQ.mjs +0 -1677
  110. 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/persistent.ts
488
- const height = (n) => n?.height ?? 0;
489
- const size = (n) => n?.size ?? 0;
490
- function node(key, value, left, right) {
491
- return {
492
- key,
493
- value,
494
- left,
495
- right,
496
- height: 1 + Math.max(height(left), height(right)),
497
- size: 1 + size(left) + size(right)
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
- x,
603
- y,
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
- function cells(r) {
609
- 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);
610
- if ((x2 - x1 + 1) * (y2 - y1 + 1) > 4096) return void 0;
611
- const result = [];
612
- for (let x = 0; x <= x2 - x1; x++) for (let y = 0; y <= y2 - y1; y++) result.push(`${x1 + x}:${y1 + y}`);
613
- return result;
614
- }
615
- /** Persistent uniform-grid broad phase; oversized rectangles live in an overflow index. */
616
- var SpatialIndex = class SpatialIndex {
617
- constructor(bounds = new PersistentMap(), buckets = new PersistentMap(), overflow = new PersistentMap()) {
618
- this.bounds = bounds;
619
- this.buckets = buckets;
620
- this.overflow = overflow;
621
- Object.freeze(this);
622
- }
623
- set(id, rect$1) {
624
- let { bounds, buckets, overflow } = this;
625
- const previous = bounds.get(id);
626
- if (previous) {
627
- const keys = cells(previous);
628
- if (!keys) overflow = overflow.delete(id);
629
- else for (const key of keys) {
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
- return new SpatialIndex(bounds, buckets, overflow);
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
- query(rect$1) {
644
- const keys = cells(rect$1), found = /* @__PURE__ */ new Set();
645
- if (!keys) {
646
- for (const [id, bounds] of this.bounds) if (intersects(rect$1, bounds)) found.add(id);
647
- } else {
648
- 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);
649
- for (const id of this.overflow.keys()) if (intersects(rect$1, this.bounds.get(id))) found.add(id);
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
- return [...found].sort();
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 xs = [...new Set([
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
- ])].sort((a, b) => a - b);
778
- const ys = [...new Set([
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
- ])].sort((a, b) => a - b);
785
- if (xs.length * ys.length > context.maxSearchNodes) {
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 > context.maxSearchNodes) {
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 > context.maxSearchNodes) {
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
- }, distance(start, end));
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 + distance(b, end));
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
- return pathReservations(path).reduce((sum, segment) => sum + distance(segment.a, segment.b) + (context.edgeCost?.(segment.a, segment.b) ?? 0), 0);
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: labelAnchor(target.point),
1486
- side: sideToward(center(label), target.point),
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
- report("CONSTRAINT_VIOLATION", "Coincident terminals; rendered a visible loop fallback");
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 start = stub(a), end = stub(b), waypoints = [...via$1];
1559
- if (loop && !waypoints.length) {
1560
- const r = inflate(sourceBounds, config.clearance + config.edgeSpacing + 1);
1561
- waypoints.push({
1562
- x: r.x + r.width,
1563
- y: r.y
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 = findPath(start, end, "polyline", context);
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 = findPath(anchors[i - 1], anchors[i], searchStyle, searchContext, directions);
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 = findPath(anchors[i - 1], anchors[i], searchStyle, narrow, directions);
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) sections.push({
1692
- id: `${edge.id}:label-target`,
1693
- from: lastSource.ref,
1694
- to: target.ref,
1695
- path: connection(lastSource, target)
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 () {