@statelyai/layout 0.4.3 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -3
- package/dist/elkjs/index.cjs +2 -2
- package/dist/elkjs/index.d.cts +1 -1
- package/dist/elkjs/index.d.mts +1 -1
- 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-Cj-lWiK1.cjs → elkjs-Bh_E59N_.cjs} +3 -3
- package/dist/{elkjs-B5kN_zS6.mjs → elkjs-Dk9yOk9U.mjs} +3 -3
- package/dist/{index-B6cWNrMw.d.cts → index-0YVSvMvA.d.mts} +13 -3
- package/dist/{index-CJQLUKD9.d.mts → index-C0J4ttZe.d.cts} +13 -3
- package/dist/{index-DT6lPMfS.d.cts → index-Xc4R6y3z.d.cts} +9 -2
- package/dist/{index-F1U-fCTu.d.mts → index-lSp0oCJC.d.mts} +9 -2
- package/dist/index.cjs +7 -4
- package/dist/index.d.cts +4 -4
- package/dist/index.d.mts +4 -4
- package/dist/index.mjs +4 -4
- package/dist/layered/index.cjs +1 -1
- package/dist/layered/index.d.cts +2 -2
- package/dist/layered/index.d.mts +2 -2
- package/dist/layered/index.mjs +1 -1
- package/dist/{layered-BsAdm_u8.mjs → layered-CUwft7on.mjs} +488 -91
- package/dist/{layered-BJu8A21s.cjs → layered-QzYtokgN.cjs} +488 -91
- package/dist/{layout-engine-BFP1TZbF.cjs → layout-engine-CQ5FcBPC.cjs} +1 -1
- package/dist/{layout-engine-DnlmGxHL.mjs → layout-engine-DvDkMQM3.mjs} +1 -1
- package/dist/routing/index.cjs +5 -2
- package/dist/routing/index.d.cts +3 -3
- package/dist/routing/index.d.mts +3 -3
- package/dist/routing/index.mjs +2 -2
- package/dist/routing-D-LfMXOF.mjs +5189 -0
- package/dist/routing-Phxu1R0x.cjs +5423 -0
- package/dist/{types-CT2akdK0.d.mts → types-D8pYSn8I.d.cts} +112 -2
- package/dist/{types-uxPlGRof.d.cts → types-MAUtJfKQ.d.mts} +112 -2
- package/package.json +4 -1
- package/src/authoring/partial.ts +1 -7
- package/src/authoring/routing.ts +64 -19
- package/src/layered/feedback-chains.ts +22 -4
- package/src/layered/index.ts +186 -42
- package/src/layered/loop-envelopes.ts +96 -26
- package/src/layered/north-south-ports.ts +17 -4
- package/src/layered/self-loop-ports.ts +158 -0
- package/src/layered/strategies.ts +183 -32
- package/src/routing/algorithms.ts +1436 -148
- package/src/routing/bezier-fit.ts +605 -0
- package/src/routing/bezier-visibility.ts +107 -0
- package/src/routing/bezier.ts +596 -0
- package/src/routing/engine.ts +65 -13
- package/src/routing/index.ts +14 -0
- package/src/routing/model.ts +185 -1
- package/src/routing/path.ts +1 -1
- package/src/routing/policy-geometry.ts +536 -0
- package/src/routing/search.ts +10 -2
- package/src/routing/types.ts +77 -1
- package/dist/routing-CR8flv8b.mjs +0 -2780
- package/dist/routing-QIdGGFBe.cjs +0 -2996
|
@@ -1,2780 +0,0 @@
|
|
|
1
|
-
//#region src/errors.ts
|
|
2
|
-
var LayoutError = class extends Error {
|
|
3
|
-
constructor(message, code) {
|
|
4
|
-
super(message);
|
|
5
|
-
this.code = code;
|
|
6
|
-
this.name = "LayoutError";
|
|
7
|
-
}
|
|
8
|
-
};
|
|
9
|
-
var UnsupportedLayoutError = class extends LayoutError {
|
|
10
|
-
constructor(message) {
|
|
11
|
-
super(message, "UNSUPPORTED_LAYOUT");
|
|
12
|
-
this.name = "UnsupportedLayoutError";
|
|
13
|
-
}
|
|
14
|
-
};
|
|
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
|
-
|
|
499
|
-
//#endregion
|
|
500
|
-
//#region src/authoring/routing.ts
|
|
501
|
-
function overlaps(a, b, gap = 0) {
|
|
502
|
-
return a.width > 0 && a.height > 0 && b.width > 0 && b.height > 0 && a.x < b.x + b.width + gap && a.x + a.width + gap > b.x && a.y < b.y + b.height + gap && a.y + a.height + gap > b.y;
|
|
503
|
-
}
|
|
504
|
-
/** Open-interior intersection: travelling along a rectangle boundary is allowed. */
|
|
505
|
-
function crossesRect(a, b, rect$1) {
|
|
506
|
-
let low = 0, high = 1;
|
|
507
|
-
for (const axis of ["x", "y"]) {
|
|
508
|
-
const delta = b[axis] - a[axis];
|
|
509
|
-
const min = rect$1[axis], max = min + (axis === "x" ? rect$1.width : rect$1.height);
|
|
510
|
-
if (Math.abs(delta) < 1e-10) {
|
|
511
|
-
if (a[axis] <= min + 1e-10 || a[axis] >= max - 1e-10) return false;
|
|
512
|
-
} else {
|
|
513
|
-
const t1 = (min - a[axis]) / delta, t2 = (max - a[axis]) / delta;
|
|
514
|
-
low = Math.max(low, Math.min(t1, t2));
|
|
515
|
-
high = Math.min(high, Math.max(t1, t2));
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
return low < high - 1e-10;
|
|
519
|
-
}
|
|
520
|
-
function routeCrosses(points, rect$1) {
|
|
521
|
-
return points?.some((point, i) => i > 0 && crossesRect(points[i - 1], point, rect$1)) ?? false;
|
|
522
|
-
}
|
|
523
|
-
function endpoint(node$1, portName, direction, source) {
|
|
524
|
-
if (portName !== void 0) {
|
|
525
|
-
const port = node$1.ports?.find((p) => p.name === portName);
|
|
526
|
-
if (port?.x !== void 0 && port.y !== void 0) return {
|
|
527
|
-
x: node$1.x + port.x + (port.width ?? 0) / 2,
|
|
528
|
-
y: node$1.y + port.y + (port.height ?? 0) / 2
|
|
529
|
-
};
|
|
530
|
-
throw new LayoutError(`Port ${portName} on ${node$1.id} requires positioned geometry`, "MISSING_GEOMETRY");
|
|
531
|
-
}
|
|
532
|
-
const horizontal = direction === "left" || direction === "right";
|
|
533
|
-
const far = source === (direction === "right" || direction === "down");
|
|
534
|
-
return horizontal ? {
|
|
535
|
-
x: node$1.x + (far ? node$1.width : 0),
|
|
536
|
-
y: node$1.y + node$1.height / 2
|
|
537
|
-
} : {
|
|
538
|
-
x: node$1.x + node$1.width / 2,
|
|
539
|
-
y: node$1.y + (far ? node$1.height : 0)
|
|
540
|
-
};
|
|
541
|
-
}
|
|
542
|
-
/** Deterministic orthogonal visibility-grid search around fixed rectangles. */
|
|
543
|
-
function connect(start, end, obstacles, check) {
|
|
544
|
-
if (start.x === end.x && start.y === end.y) return [start];
|
|
545
|
-
for (let attempt = 0; attempt < 8; attempt++) {
|
|
546
|
-
check();
|
|
547
|
-
const margin = 64 * 2 ** attempt;
|
|
548
|
-
const left = Math.min(start.x, end.x) - margin;
|
|
549
|
-
const right = Math.max(start.x, end.x) + margin;
|
|
550
|
-
const top = Math.min(start.y, end.y) - margin;
|
|
551
|
-
const bottom = Math.max(start.y, end.y) + margin;
|
|
552
|
-
const local = obstacles.filter((r) => r.x <= right && r.x + r.width >= left && r.y <= bottom && r.y + r.height >= top);
|
|
553
|
-
const result = search(start, end, local, obstacles, check);
|
|
554
|
-
if (result || local.length === obstacles.length) return result;
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
function search(start, end, gridObstacles, obstacles, check) {
|
|
558
|
-
const xs = [...new Set([
|
|
559
|
-
start.x,
|
|
560
|
-
end.x,
|
|
561
|
-
...gridObstacles.flatMap((r) => [r.x, r.x + r.width])
|
|
562
|
-
])].sort((a, b) => a - b);
|
|
563
|
-
const ys = [...new Set([
|
|
564
|
-
start.y,
|
|
565
|
-
end.y,
|
|
566
|
-
...gridObstacles.flatMap((r) => [r.y, r.y + r.height])
|
|
567
|
-
])].sort((a, b) => a - b);
|
|
568
|
-
if (xs.length * ys.length > 4e4) return void 0;
|
|
569
|
-
const width = xs.length;
|
|
570
|
-
const point = (id) => ({
|
|
571
|
-
x: xs[id % width],
|
|
572
|
-
y: ys[Math.floor(id / width)]
|
|
573
|
-
});
|
|
574
|
-
const from = ys.indexOf(start.y) * width + xs.indexOf(start.x);
|
|
575
|
-
const to = ys.indexOf(end.y) * width + xs.indexOf(end.x);
|
|
576
|
-
const distances = new Map([[from, 0]]);
|
|
577
|
-
const previous = /* @__PURE__ */ new Map();
|
|
578
|
-
const queue = [];
|
|
579
|
-
function push(item) {
|
|
580
|
-
queue.push(item);
|
|
581
|
-
let i = queue.length - 1;
|
|
582
|
-
while (i > 0) {
|
|
583
|
-
const parent = i - 1 >> 1;
|
|
584
|
-
if (queue[parent].cost <= item.cost) break;
|
|
585
|
-
queue[i] = queue[parent];
|
|
586
|
-
i = parent;
|
|
587
|
-
}
|
|
588
|
-
queue[i] = item;
|
|
589
|
-
}
|
|
590
|
-
function pop() {
|
|
591
|
-
const first = queue[0];
|
|
592
|
-
const last = queue.pop();
|
|
593
|
-
if (queue.length) {
|
|
594
|
-
let i = 0;
|
|
595
|
-
while (i * 2 + 1 < queue.length) {
|
|
596
|
-
let child = i * 2 + 1;
|
|
597
|
-
if (child + 1 < queue.length && queue[child + 1].cost < queue[child].cost) child++;
|
|
598
|
-
if (last.cost <= queue[child].cost) break;
|
|
599
|
-
queue[i] = queue[child];
|
|
600
|
-
i = child;
|
|
601
|
-
}
|
|
602
|
-
queue[i] = last;
|
|
603
|
-
}
|
|
604
|
-
return first;
|
|
605
|
-
}
|
|
606
|
-
push({
|
|
607
|
-
id: from,
|
|
608
|
-
cost: 0
|
|
609
|
-
});
|
|
610
|
-
let visits = 0;
|
|
611
|
-
while (queue.length) {
|
|
612
|
-
if (++visits % 128 === 0) check();
|
|
613
|
-
const current = pop();
|
|
614
|
-
if (current.cost !== distances.get(current.id)) continue;
|
|
615
|
-
if (current.id === to) {
|
|
616
|
-
const result = [end];
|
|
617
|
-
let id = to;
|
|
618
|
-
while (id !== from) {
|
|
619
|
-
id = previous.get(id);
|
|
620
|
-
result.push(point(id));
|
|
621
|
-
}
|
|
622
|
-
return result.reverse();
|
|
623
|
-
}
|
|
624
|
-
const x = current.id % width, y = Math.floor(current.id / width);
|
|
625
|
-
const neighbors = [
|
|
626
|
-
x > 0 ? current.id - 1 : -1,
|
|
627
|
-
x + 1 < width ? current.id + 1 : -1,
|
|
628
|
-
y > 0 ? current.id - width : -1,
|
|
629
|
-
y + 1 < ys.length ? current.id + width : -1
|
|
630
|
-
];
|
|
631
|
-
const a = point(current.id);
|
|
632
|
-
for (const next of neighbors) {
|
|
633
|
-
if (next < 0) continue;
|
|
634
|
-
const b = point(next);
|
|
635
|
-
if (obstacles.some((rect$1) => crossesRect(a, b, rect$1))) continue;
|
|
636
|
-
const cost = current.cost + Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
|
|
637
|
-
if (cost >= (distances.get(next) ?? Infinity)) continue;
|
|
638
|
-
distances.set(next, cost);
|
|
639
|
-
previous.set(next, current.id);
|
|
640
|
-
push({
|
|
641
|
-
id: next,
|
|
642
|
-
cost
|
|
643
|
-
});
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
function routeOrthogonal(edge, source, target, direction, obstacles, waypoints, check) {
|
|
648
|
-
const anchors = [
|
|
649
|
-
endpoint(source, edge.sourcePort, direction, true),
|
|
650
|
-
...waypoints,
|
|
651
|
-
endpoint(target, edge.targetPort, direction, false)
|
|
652
|
-
];
|
|
653
|
-
const points = [];
|
|
654
|
-
for (let i = 1; i < anchors.length; i++) {
|
|
655
|
-
const segment = connect(anchors[i - 1], anchors[i], obstacles, check);
|
|
656
|
-
if (!segment) return void 0;
|
|
657
|
-
points.push(...points.length ? segment.slice(1) : segment);
|
|
658
|
-
}
|
|
659
|
-
return points.filter((p, i) => {
|
|
660
|
-
if (i === 0 || i === points.length - 1) return true;
|
|
661
|
-
const before = points[i - 1], after = points[i + 1];
|
|
662
|
-
return !(before.x === p.x && p.x === after.x && (p.y - before.y) * (after.y - p.y) >= 0 || before.y === p.y && p.y === after.y && (p.x - before.x) * (after.x - p.x) >= 0);
|
|
663
|
-
});
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
//#endregion
|
|
667
|
-
//#region src/routing/path.ts
|
|
668
|
-
const distance = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
|
|
669
|
-
const lerp = (a, b, t) => ({
|
|
670
|
-
x: a.x + (b.x - a.x) * t,
|
|
671
|
-
y: a.y + (b.y - a.y) * t
|
|
672
|
-
});
|
|
673
|
-
function pathFromPoints(points) {
|
|
674
|
-
if (!points.length) throw new RangeError("A path needs at least one point");
|
|
675
|
-
return {
|
|
676
|
-
start: { ...points[0] },
|
|
677
|
-
segments: points.slice(1).map((to) => ({
|
|
678
|
-
kind: "line",
|
|
679
|
-
to: { ...to }
|
|
680
|
-
}))
|
|
681
|
-
};
|
|
682
|
-
}
|
|
683
|
-
function pointLineDistance(p, a, b) {
|
|
684
|
-
const squared = (b.x - a.x) ** 2 + (b.y - a.y) ** 2;
|
|
685
|
-
return distance(p, lerp(a, b, squared ? Math.max(0, Math.min(1, ((p.x - a.x) * (b.x - a.x) + (p.y - a.y) * (b.y - a.y)) / squared)) : 0));
|
|
686
|
-
}
|
|
687
|
-
/** SVG endpoint-to-center conversion, including SVG's radius correction. */
|
|
688
|
-
function arcCenter(start, arc) {
|
|
689
|
-
let rx = Math.abs(arc.rx), ry = Math.abs(arc.ry);
|
|
690
|
-
if (!rx || !ry || distance(start, arc.to) === 0) return void 0;
|
|
691
|
-
const phi = arc.rotation * Math.PI / 180, c = Math.cos(phi), s = Math.sin(phi);
|
|
692
|
-
const dx = (start.x - arc.to.x) / 2, dy = (start.y - arc.to.y) / 2;
|
|
693
|
-
const x = c * dx + s * dy, y = -s * dx + c * dy;
|
|
694
|
-
const scale = Math.sqrt(Math.max(1, x * x / (rx * rx) + y * y / (ry * ry)));
|
|
695
|
-
rx *= scale;
|
|
696
|
-
ry *= scale;
|
|
697
|
-
const factor = (arc.largeArc === arc.sweep ? -1 : 1) * Math.sqrt(Math.max(0, (rx * rx * ry * ry - rx * rx * y * y - ry * ry * x * x) / (rx * rx * y * y + ry * ry * x * x)));
|
|
698
|
-
const cx = factor * rx * y / ry, cy = -factor * ry * x / rx;
|
|
699
|
-
const center$1 = {
|
|
700
|
-
x: c * cx - s * cy + (start.x + arc.to.x) / 2,
|
|
701
|
-
y: s * cx + c * cy + (start.y + arc.to.y) / 2
|
|
702
|
-
};
|
|
703
|
-
const theta = Math.atan2((y - cy) / ry, (x - cx) / rx);
|
|
704
|
-
let delta = Math.atan2((-y - cy) / ry, (-x - cx) / rx) - theta;
|
|
705
|
-
if (arc.sweep && delta < 0) delta += 2 * Math.PI;
|
|
706
|
-
if (!arc.sweep && delta > 0) delta -= 2 * Math.PI;
|
|
707
|
-
return {
|
|
708
|
-
rx,
|
|
709
|
-
ry,
|
|
710
|
-
c,
|
|
711
|
-
s,
|
|
712
|
-
center: center$1,
|
|
713
|
-
theta,
|
|
714
|
-
delta
|
|
715
|
-
};
|
|
716
|
-
}
|
|
717
|
-
/** Adaptive, bounded-error polyline approximation. Throws rather than silently exceeding tolerance. */
|
|
718
|
-
function flattenPath(path, options = {}) {
|
|
719
|
-
const tolerance = options.tolerance ?? .5, max = options.maxSegments ?? 65536;
|
|
720
|
-
if (!(tolerance > 0 && Number.isFinite(tolerance)) || !Number.isInteger(max) || max < 1) throw new RangeError("Invalid flattening tolerance or segment budget");
|
|
721
|
-
const points = [{ ...path.start }];
|
|
722
|
-
const add = (point) => {
|
|
723
|
-
if (points.length > max) throw new RangeError("Path flattening exceeded maxSegments");
|
|
724
|
-
points.push({ ...point });
|
|
725
|
-
};
|
|
726
|
-
function bezier(controls, depth = 0) {
|
|
727
|
-
const first = controls[0], last = controls.at(-1);
|
|
728
|
-
if (controls.slice(1, -1).every((p) => pointLineDistance(p, first, last) <= tolerance)) {
|
|
729
|
-
add(last);
|
|
730
|
-
return;
|
|
731
|
-
}
|
|
732
|
-
if (depth >= 32) throw new RangeError("Cannot flatten path at requested tolerance");
|
|
733
|
-
const left = [first], right = [last];
|
|
734
|
-
let row = [...controls];
|
|
735
|
-
while (row.length > 1) {
|
|
736
|
-
row = row.slice(1).map((p, i) => lerp(row[i], p, .5));
|
|
737
|
-
left.push(row[0]);
|
|
738
|
-
right.unshift(row.at(-1));
|
|
739
|
-
}
|
|
740
|
-
bezier(left, depth + 1);
|
|
741
|
-
bezier(right, depth + 1);
|
|
742
|
-
}
|
|
743
|
-
let from = path.start;
|
|
744
|
-
for (const segment of path.segments) {
|
|
745
|
-
if (segment.kind === "line") add(segment.to);
|
|
746
|
-
else if (segment.kind === "quadratic") bezier([
|
|
747
|
-
from,
|
|
748
|
-
segment.control,
|
|
749
|
-
segment.to
|
|
750
|
-
]);
|
|
751
|
-
else if (segment.kind === "cubic") bezier([
|
|
752
|
-
from,
|
|
753
|
-
segment.control1,
|
|
754
|
-
segment.control2,
|
|
755
|
-
segment.to
|
|
756
|
-
]);
|
|
757
|
-
else {
|
|
758
|
-
const arc = arcCenter(from, segment);
|
|
759
|
-
if (!arc) add(segment.to);
|
|
760
|
-
else {
|
|
761
|
-
const step = Math.min(Math.PI / 2, 2 * Math.acos(Math.max(-1, 1 - tolerance / Math.max(arc.rx, arc.ry))));
|
|
762
|
-
const count = Math.max(1, Math.ceil(Math.abs(arc.delta) / step));
|
|
763
|
-
if (!Number.isFinite(count) || points.length + count > max + 1) throw new RangeError("Path flattening exceeded maxSegments");
|
|
764
|
-
for (let i = 1; i < count; i++) {
|
|
765
|
-
const t = arc.theta + arc.delta * i / count;
|
|
766
|
-
add({
|
|
767
|
-
x: arc.center.x + arc.c * arc.rx * Math.cos(t) - arc.s * arc.ry * Math.sin(t),
|
|
768
|
-
y: arc.center.y + arc.s * arc.rx * Math.cos(t) + arc.c * arc.ry * Math.sin(t)
|
|
769
|
-
});
|
|
770
|
-
}
|
|
771
|
-
add(segment.to);
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
from = segment.to;
|
|
775
|
-
}
|
|
776
|
-
return points;
|
|
777
|
-
}
|
|
778
|
-
/** Conservative bounds: control hulls and full ellipse envelopes contain every curve. */
|
|
779
|
-
function getPathBounds(path) {
|
|
780
|
-
const points = [path.start];
|
|
781
|
-
let from = path.start;
|
|
782
|
-
for (const segment of path.segments) {
|
|
783
|
-
points.push(segment.to);
|
|
784
|
-
if (segment.kind === "quadratic") points.push(segment.control);
|
|
785
|
-
if (segment.kind === "cubic") points.push(segment.control1, segment.control2);
|
|
786
|
-
if (segment.kind === "arc") {
|
|
787
|
-
const arc = arcCenter(from, segment);
|
|
788
|
-
if (arc) {
|
|
789
|
-
const dx = Math.hypot(arc.rx * arc.c, arc.ry * arc.s), dy = Math.hypot(arc.rx * arc.s, arc.ry * arc.c);
|
|
790
|
-
points.push({
|
|
791
|
-
x: arc.center.x - dx,
|
|
792
|
-
y: arc.center.y - dy
|
|
793
|
-
}, {
|
|
794
|
-
x: arc.center.x + dx,
|
|
795
|
-
y: arc.center.y + dy
|
|
796
|
-
});
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
from = segment.to;
|
|
800
|
-
}
|
|
801
|
-
const xs = points.map((p) => p.x), ys = points.map((p) => p.y);
|
|
802
|
-
const x = Math.min(...xs), y = Math.min(...ys);
|
|
803
|
-
return {
|
|
804
|
-
x,
|
|
805
|
-
y,
|
|
806
|
-
width: Math.max(...xs) - x,
|
|
807
|
-
height: Math.max(...ys) - y
|
|
808
|
-
};
|
|
809
|
-
}
|
|
810
|
-
function getPathLength(path, options) {
|
|
811
|
-
const points = flattenPath(path, options);
|
|
812
|
-
return points.reduce((sum, p, i) => sum + (i ? distance(points[i - 1], p) : 0), 0);
|
|
813
|
-
}
|
|
814
|
-
function locate(path, length, options) {
|
|
815
|
-
const points = flattenPath(path, options);
|
|
816
|
-
let remaining = Math.max(0, length);
|
|
817
|
-
for (let i = 1; i < points.length; i++) {
|
|
818
|
-
const a = points[i - 1], b = points[i], size$1 = distance(a, b);
|
|
819
|
-
if (size$1 && (remaining <= size$1 || i === points.length - 1)) return {
|
|
820
|
-
a,
|
|
821
|
-
b,
|
|
822
|
-
t: Math.min(1, remaining / size$1)
|
|
823
|
-
};
|
|
824
|
-
remaining -= size$1;
|
|
825
|
-
}
|
|
826
|
-
return {
|
|
827
|
-
a: points.at(-1),
|
|
828
|
-
b: points.at(-1),
|
|
829
|
-
t: 0
|
|
830
|
-
};
|
|
831
|
-
}
|
|
832
|
-
function getPointAtLength(path, length, options) {
|
|
833
|
-
const { a, b, t } = locate(path, length, options);
|
|
834
|
-
return lerp(a, b, t);
|
|
835
|
-
}
|
|
836
|
-
function getTangentAtLength(path, length, options) {
|
|
837
|
-
const { a, b } = locate(path, length, options), d = distance(a, b);
|
|
838
|
-
return d ? {
|
|
839
|
-
x: (b.x - a.x) / d,
|
|
840
|
-
y: (b.y - a.y) / d
|
|
841
|
-
} : {
|
|
842
|
-
x: 0,
|
|
843
|
-
y: 0
|
|
844
|
-
};
|
|
845
|
-
}
|
|
846
|
-
/** Round line-to-line corners only; existing curves and path endpoints are retained. */
|
|
847
|
-
function roundCorners(path, options) {
|
|
848
|
-
const radius = options.radius;
|
|
849
|
-
if (!Number.isFinite(radius) || radius < 0) throw new RangeError("radius must be non-negative and finite");
|
|
850
|
-
if (!radius) return path;
|
|
851
|
-
const result = [];
|
|
852
|
-
let previous = path.start;
|
|
853
|
-
for (let i = 0; i < path.segments.length; i++) {
|
|
854
|
-
const segment = path.segments[i], next = path.segments[i + 1];
|
|
855
|
-
const corner = segment.to;
|
|
856
|
-
if (segment.kind === "line" && next?.kind === "line") {
|
|
857
|
-
const l1 = distance(previous, corner), l2 = distance(corner, next.to);
|
|
858
|
-
if (l1 > 0 && l2 > 0) {
|
|
859
|
-
const u = {
|
|
860
|
-
x: (corner.x - previous.x) / l1,
|
|
861
|
-
y: (corner.y - previous.y) / l1
|
|
862
|
-
}, v = {
|
|
863
|
-
x: (next.to.x - corner.x) / l2,
|
|
864
|
-
y: (next.to.y - corner.y) / l2
|
|
865
|
-
};
|
|
866
|
-
const angle = Math.acos(Math.max(-1, Math.min(1, u.x * v.x + u.y * v.y)));
|
|
867
|
-
if (angle > 1e-8 && angle < Math.PI - 1e-8) {
|
|
868
|
-
const tangent = Math.tan(angle / 2), trim = Math.min(radius * tangent, l1 / 2, l2 / 2), r = trim / tangent;
|
|
869
|
-
result.push({
|
|
870
|
-
kind: "line",
|
|
871
|
-
to: {
|
|
872
|
-
x: corner.x - u.x * trim,
|
|
873
|
-
y: corner.y - u.y * trim
|
|
874
|
-
}
|
|
875
|
-
}, {
|
|
876
|
-
kind: "arc",
|
|
877
|
-
rx: r,
|
|
878
|
-
ry: r,
|
|
879
|
-
rotation: 0,
|
|
880
|
-
largeArc: false,
|
|
881
|
-
sweep: u.x * v.y - u.y * v.x > 0,
|
|
882
|
-
to: {
|
|
883
|
-
x: corner.x + v.x * trim,
|
|
884
|
-
y: corner.y + v.y * trim
|
|
885
|
-
}
|
|
886
|
-
});
|
|
887
|
-
previous = corner;
|
|
888
|
-
continue;
|
|
889
|
-
}
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
result.push(segment);
|
|
893
|
-
previous = corner;
|
|
894
|
-
}
|
|
895
|
-
return {
|
|
896
|
-
start: path.start,
|
|
897
|
-
segments: result
|
|
898
|
-
};
|
|
899
|
-
}
|
|
900
|
-
function toSvgPath(path, options = {}) {
|
|
901
|
-
const shaped = options.radius ? roundCorners(path, { radius: options.radius }) : path;
|
|
902
|
-
const precision = options.precision ?? 6;
|
|
903
|
-
if (!Number.isInteger(precision) || precision < 0 || precision > 15) throw new RangeError("precision must be an integer from 0 to 15");
|
|
904
|
-
const n = (value) => {
|
|
905
|
-
if (!Number.isFinite(value)) throw new RangeError("Path coordinates must be finite");
|
|
906
|
-
return String(Number(value.toFixed(precision)));
|
|
907
|
-
};
|
|
908
|
-
const p = (point) => `${n(point.x)} ${n(point.y)}`;
|
|
909
|
-
return [`M ${p(shaped.start)}`, ...shaped.segments.map((s) => {
|
|
910
|
-
switch (s.kind) {
|
|
911
|
-
case "line": return `L ${p(s.to)}`;
|
|
912
|
-
case "quadratic": return `Q ${p(s.control)} ${p(s.to)}`;
|
|
913
|
-
case "cubic": return `C ${p(s.control1)} ${p(s.control2)} ${p(s.to)}`;
|
|
914
|
-
case "arc": return `A ${n(Math.abs(s.rx))} ${n(Math.abs(s.ry))} ${n(s.rotation)} ${Number(s.largeArc)} ${Number(s.sweep)} ${p(s.to)}`;
|
|
915
|
-
}
|
|
916
|
-
})].join(" ");
|
|
917
|
-
}
|
|
918
|
-
/** Test curve geometry, subdividing conservative bounds only where they overlap. */
|
|
919
|
-
function segmentCrossesRect(start, segment, rect$1) {
|
|
920
|
-
const overlaps$1 = (points) => {
|
|
921
|
-
const xs = points.map((p) => p.x), ys = points.map((p) => p.y);
|
|
922
|
-
return Math.min(...xs) < rect$1.x + rect$1.width && Math.max(...xs) > rect$1.x && Math.min(...ys) < rect$1.y + rect$1.height && Math.max(...ys) > rect$1.y;
|
|
923
|
-
};
|
|
924
|
-
const inside = (p) => p.x > rect$1.x && p.x < rect$1.x + rect$1.width && p.y > rect$1.y && p.y < rect$1.y + rect$1.height;
|
|
925
|
-
function bezier(controls, depth) {
|
|
926
|
-
if (!overlaps$1(controls)) return false;
|
|
927
|
-
if (inside(controls[0]) || inside(controls.at(-1))) return true;
|
|
928
|
-
if (depth === 32) return true;
|
|
929
|
-
const left = [controls[0]], right = [controls.at(-1)];
|
|
930
|
-
let row = [...controls];
|
|
931
|
-
while (row.length > 1) {
|
|
932
|
-
row = row.slice(1).map((p, i) => lerp(row[i], p, .5));
|
|
933
|
-
left.push(row[0]);
|
|
934
|
-
right.unshift(row.at(-1));
|
|
935
|
-
}
|
|
936
|
-
return bezier(left, depth + 1) || bezier(right, depth + 1);
|
|
937
|
-
}
|
|
938
|
-
if (segment.kind === "line") return crossesRect(start, segment.to, rect$1);
|
|
939
|
-
if (segment.kind === "quadratic") return bezier([
|
|
940
|
-
start,
|
|
941
|
-
segment.control,
|
|
942
|
-
segment.to
|
|
943
|
-
], 0);
|
|
944
|
-
if (segment.kind === "cubic") return bezier([
|
|
945
|
-
start,
|
|
946
|
-
segment.control1,
|
|
947
|
-
segment.control2,
|
|
948
|
-
segment.to
|
|
949
|
-
], 0);
|
|
950
|
-
const arc = arcCenter(start, segment);
|
|
951
|
-
if (!arc) return crossesRect(start, segment.to, rect$1);
|
|
952
|
-
const point = (t) => ({
|
|
953
|
-
x: arc.center.x + arc.c * arc.rx * Math.cos(t) - arc.s * arc.ry * Math.sin(t),
|
|
954
|
-
y: arc.center.y + arc.s * arc.rx * Math.cos(t) + arc.c * arc.ry * Math.sin(t)
|
|
955
|
-
});
|
|
956
|
-
function interval(a, b, depth) {
|
|
957
|
-
const points = [point(a), point(b)];
|
|
958
|
-
for (const base of [Math.atan2(-arc.s * arc.ry, arc.c * arc.rx), Math.atan2(arc.c * arc.ry, arc.s * arc.rx)]) for (let k = Math.ceil((a - base) / Math.PI); base + k * Math.PI < b; k++) points.push(point(base + k * Math.PI));
|
|
959
|
-
if (!overlaps$1(points)) return false;
|
|
960
|
-
if (inside(points[0]) || inside(points[1])) return true;
|
|
961
|
-
if (depth === 32) return true;
|
|
962
|
-
const middle = (a + b) / 2;
|
|
963
|
-
return interval(a, middle, depth + 1) || interval(middle, b, depth + 1);
|
|
964
|
-
}
|
|
965
|
-
return interval(Math.min(arc.theta, arc.theta + arc.delta), Math.max(arc.theta, arc.theta + arc.delta), 0);
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
//#endregion
|
|
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
|
-
});
|
|
980
|
-
return {
|
|
981
|
-
start: { ...points[0] },
|
|
982
|
-
segments
|
|
983
|
-
};
|
|
984
|
-
}
|
|
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
|
-
}]);
|
|
1007
|
-
}
|
|
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
|
-
}));
|
|
1031
|
-
}
|
|
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
|
-
});
|
|
1076
|
-
}
|
|
1077
|
-
points.push(...polyline.map((point) => ({ ...point })));
|
|
1078
|
-
}
|
|
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
|
-
}
|
|
1101
|
-
|
|
1102
|
-
//#endregion
|
|
1103
|
-
//#region src/routing/search.ts
|
|
1104
|
-
const segmentBounds = (a, b) => ({
|
|
1105
|
-
x: Math.min(a.x, b.x),
|
|
1106
|
-
y: Math.min(a.y, b.y),
|
|
1107
|
-
width: Math.abs(b.x - a.x),
|
|
1108
|
-
height: Math.abs(b.y - a.y)
|
|
1109
|
-
});
|
|
1110
|
-
function clear(a, b, context) {
|
|
1111
|
-
return !context.obstacles(segmentBounds(a, b)).some((r) => crossesRect(a, b, r));
|
|
1112
|
-
}
|
|
1113
|
-
function simplify(points) {
|
|
1114
|
-
const result = [];
|
|
1115
|
-
for (const p of points) {
|
|
1116
|
-
if (result.length && distance(result.at(-1), p) < 1e-9) continue;
|
|
1117
|
-
while (result.length > 1) {
|
|
1118
|
-
const a = result.at(-2), b = result.at(-1);
|
|
1119
|
-
if (Math.abs((b.x - a.x) * (p.y - b.y) - (b.y - a.y) * (p.x - b.x)) > 1e-8 || (b.x - a.x) * (p.x - b.x) + (b.y - a.y) * (p.y - b.y) < 0) break;
|
|
1120
|
-
result.pop();
|
|
1121
|
-
}
|
|
1122
|
-
result.push(p);
|
|
1123
|
-
}
|
|
1124
|
-
return result;
|
|
1125
|
-
}
|
|
1126
|
-
var Queue = class {
|
|
1127
|
-
items = [];
|
|
1128
|
-
push(value, priority) {
|
|
1129
|
-
const item = {
|
|
1130
|
-
value,
|
|
1131
|
-
priority
|
|
1132
|
-
};
|
|
1133
|
-
let i = this.items.length;
|
|
1134
|
-
this.items.push(item);
|
|
1135
|
-
while (i > 0) {
|
|
1136
|
-
const p = i - 1 >> 1;
|
|
1137
|
-
if (this.items[p].priority <= priority) break;
|
|
1138
|
-
this.items[i] = this.items[p];
|
|
1139
|
-
i = p;
|
|
1140
|
-
}
|
|
1141
|
-
this.items[i] = item;
|
|
1142
|
-
}
|
|
1143
|
-
pop() {
|
|
1144
|
-
const first = this.items[0], last = this.items.pop();
|
|
1145
|
-
if (this.items.length) {
|
|
1146
|
-
let i = 0;
|
|
1147
|
-
while (i * 2 + 1 < this.items.length) {
|
|
1148
|
-
let child = i * 2 + 1;
|
|
1149
|
-
if (child + 1 < this.items.length && this.items[child + 1].priority < this.items[child].priority) child++;
|
|
1150
|
-
if (last.priority <= this.items[child].priority) break;
|
|
1151
|
-
this.items[i] = this.items[child];
|
|
1152
|
-
i = child;
|
|
1153
|
-
}
|
|
1154
|
-
this.items[i] = last;
|
|
1155
|
-
}
|
|
1156
|
-
return first.value;
|
|
1157
|
-
}
|
|
1158
|
-
};
|
|
1159
|
-
/** Visibility graph + A*, with incoming direction in the state for bend costs. */
|
|
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
|
-
};
|
|
1166
|
-
const turnCost = (a, b) => {
|
|
1167
|
-
if (!a || !b || !Math.hypot(a.x, a.y) || !Math.hypot(b.x, b.y)) return 0;
|
|
1168
|
-
const dot = a.x * b.x + a.y * b.y, cross = a.x * b.y - a.y * b.x;
|
|
1169
|
-
return dot < -1e-8 ? context.bendPenalty * 4 : Math.abs(cross) > 1e-8 ? context.bendPenalty : 0;
|
|
1170
|
-
};
|
|
1171
|
-
const delta = (a, b) => ({
|
|
1172
|
-
x: b.x - a.x,
|
|
1173
|
-
y: b.y - a.y
|
|
1174
|
-
});
|
|
1175
|
-
const attachmentCost = (points) => turnCost(terminals.incoming, delta(points[0], points[1])) + turnCost(delta(points.at(-2), points.at(-1)), terminals.outgoing);
|
|
1176
|
-
const aligned = start.x === end.x || start.y === end.y;
|
|
1177
|
-
if ((style === "polyline" || aligned || style === "octilinear" && Math.abs(start.x - end.x) === Math.abs(start.y - end.y)) && clear(start, end, context) && attachmentCost([start, end]) === 0 && (context.edgeCost?.(start, end) ?? 0) === 0) return [start, end];
|
|
1178
|
-
if (style === "orthogonal") {
|
|
1179
|
-
const candidates = [{
|
|
1180
|
-
x: start.x,
|
|
1181
|
-
y: end.y
|
|
1182
|
-
}, {
|
|
1183
|
-
x: end.x,
|
|
1184
|
-
y: start.y
|
|
1185
|
-
}].filter((bend) => clear(start, bend, context) && clear(bend, end, context)).map((bend) => simplify([
|
|
1186
|
-
start,
|
|
1187
|
-
bend,
|
|
1188
|
-
end
|
|
1189
|
-
]));
|
|
1190
|
-
candidates.sort((a, b) => attachmentCost(a) - attachmentCost(b));
|
|
1191
|
-
if (candidates.length && attachmentCost(candidates[0]) === 0 && candidates[0].slice(1).every((p, i) => (context.edgeCost?.(candidates[0][i], p) ?? 0) === 0)) return candidates[0];
|
|
1192
|
-
}
|
|
1193
|
-
for (let attempt = 0; attempt < 5 && context.visited < context.maxSearchNodes; attempt++) {
|
|
1194
|
-
const area = inflate(segmentBounds(start, end), 32 * 2 ** attempt);
|
|
1195
|
-
const guides = context.guides?.(area) ?? [];
|
|
1196
|
-
const corners = [...context.obstacles(guides.reduce(union, area)), ...guides].flatMap((r) => [
|
|
1197
|
-
{
|
|
1198
|
-
x: r.x,
|
|
1199
|
-
y: r.y
|
|
1200
|
-
},
|
|
1201
|
-
{
|
|
1202
|
-
x: r.x + r.width,
|
|
1203
|
-
y: r.y
|
|
1204
|
-
},
|
|
1205
|
-
{
|
|
1206
|
-
x: r.x,
|
|
1207
|
-
y: r.y + r.height
|
|
1208
|
-
},
|
|
1209
|
-
{
|
|
1210
|
-
x: r.x + r.width,
|
|
1211
|
-
y: r.y + r.height
|
|
1212
|
-
}
|
|
1213
|
-
]);
|
|
1214
|
-
let points, neighbors;
|
|
1215
|
-
if (style === "polyline") {
|
|
1216
|
-
points = [
|
|
1217
|
-
start,
|
|
1218
|
-
end,
|
|
1219
|
-
...corners
|
|
1220
|
-
];
|
|
1221
|
-
neighbors = () => points.map((_, i) => i);
|
|
1222
|
-
} else {
|
|
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([
|
|
1234
|
-
start.x,
|
|
1235
|
-
end.x,
|
|
1236
|
-
area.x,
|
|
1237
|
-
area.x + area.width,
|
|
1238
|
-
...corners.map((p) => p.x)
|
|
1239
|
-
]);
|
|
1240
|
-
const ys = coordinates([
|
|
1241
|
-
start.y,
|
|
1242
|
-
end.y,
|
|
1243
|
-
area.y,
|
|
1244
|
-
area.y + area.height,
|
|
1245
|
-
...corners.map((p) => p.y)
|
|
1246
|
-
]);
|
|
1247
|
-
if (xs.length * ys.length > maxGridNodes) {
|
|
1248
|
-
context.budgetExceeded = true;
|
|
1249
|
-
return;
|
|
1250
|
-
}
|
|
1251
|
-
points = ys.flatMap((y) => xs.map((x) => ({
|
|
1252
|
-
x,
|
|
1253
|
-
y
|
|
1254
|
-
})));
|
|
1255
|
-
const width = xs.length;
|
|
1256
|
-
if (style === "octilinear") {
|
|
1257
|
-
const extra = [];
|
|
1258
|
-
for (const p of [
|
|
1259
|
-
start,
|
|
1260
|
-
end,
|
|
1261
|
-
...corners
|
|
1262
|
-
]) for (const x of xs) for (const sign of [-1, 1]) {
|
|
1263
|
-
const y = p.y + sign * (x - p.x);
|
|
1264
|
-
if (y >= area.y && y <= area.y + area.height) extra.push({
|
|
1265
|
-
x,
|
|
1266
|
-
y
|
|
1267
|
-
});
|
|
1268
|
-
}
|
|
1269
|
-
for (const p of [
|
|
1270
|
-
start,
|
|
1271
|
-
end,
|
|
1272
|
-
...corners
|
|
1273
|
-
]) for (const y of ys) for (const sign of [-1, 1]) {
|
|
1274
|
-
const x = p.x + sign * (y - p.y);
|
|
1275
|
-
if (x >= area.x && x <= area.x + area.width) extra.push({
|
|
1276
|
-
x,
|
|
1277
|
-
y
|
|
1278
|
-
});
|
|
1279
|
-
}
|
|
1280
|
-
const unique = new Map(points.map((p) => [`${p.x}:${p.y}`, p]));
|
|
1281
|
-
for (const p of extra) unique.set(`${p.x}:${p.y}`, p);
|
|
1282
|
-
points = [...unique.values()];
|
|
1283
|
-
if (points.length > maxGridNodes) {
|
|
1284
|
-
context.budgetExceeded = true;
|
|
1285
|
-
return;
|
|
1286
|
-
}
|
|
1287
|
-
const groups = /* @__PURE__ */ new Map();
|
|
1288
|
-
const keys = (p) => [
|
|
1289
|
-
`x:${p.x}`,
|
|
1290
|
-
`y:${p.y}`,
|
|
1291
|
-
`a:${(p.x - p.y).toFixed(8)}`,
|
|
1292
|
-
`b:${(p.x + p.y).toFixed(8)}`
|
|
1293
|
-
];
|
|
1294
|
-
points.forEach((p, i) => {
|
|
1295
|
-
for (const key of keys(p)) {
|
|
1296
|
-
const list = groups.get(key) ?? [];
|
|
1297
|
-
list.push(i);
|
|
1298
|
-
groups.set(key, list);
|
|
1299
|
-
}
|
|
1300
|
-
});
|
|
1301
|
-
const adjacent = points.map(() => /* @__PURE__ */ new Set());
|
|
1302
|
-
for (const ids of groups.values()) {
|
|
1303
|
-
ids.sort((a, b) => points[a].x - points[b].x || points[a].y - points[b].y);
|
|
1304
|
-
for (let i = 1; i < ids.length; i++) {
|
|
1305
|
-
adjacent[ids[i - 1]].add(ids[i]);
|
|
1306
|
-
adjacent[ids[i]].add(ids[i - 1]);
|
|
1307
|
-
}
|
|
1308
|
-
}
|
|
1309
|
-
neighbors = (i) => [...adjacent[i]];
|
|
1310
|
-
} else neighbors = (i) => [
|
|
1311
|
-
i % width ? i - 1 : -1,
|
|
1312
|
-
i % width < width - 1 ? i + 1 : -1,
|
|
1313
|
-
i >= width ? i - width : -1,
|
|
1314
|
-
i + width < points.length ? i + width : -1
|
|
1315
|
-
].filter((v) => v >= 0);
|
|
1316
|
-
}
|
|
1317
|
-
if (points.length > maxGridNodes) {
|
|
1318
|
-
context.budgetExceeded = true;
|
|
1319
|
-
return;
|
|
1320
|
-
}
|
|
1321
|
-
const from = points.findIndex((p) => distance(p, start) < 1e-8), to = points.findIndex((p) => distance(p, end) < 1e-8);
|
|
1322
|
-
const queue = new Queue();
|
|
1323
|
-
const costs = /* @__PURE__ */ new Map(), parents = /* @__PURE__ */ new Map(), vertices = /* @__PURE__ */ new Map(), visibility = /* @__PURE__ */ new Map(), edgeCosts = /* @__PURE__ */ new Map();
|
|
1324
|
-
const visible = (a, b) => {
|
|
1325
|
-
const key = a < b ? a * points.length + b : b * points.length + a;
|
|
1326
|
-
let result = visibility.get(key);
|
|
1327
|
-
if (result === void 0) {
|
|
1328
|
-
result = clear(points[a], points[b], context);
|
|
1329
|
-
visibility.set(key, result);
|
|
1330
|
-
}
|
|
1331
|
-
return result;
|
|
1332
|
-
};
|
|
1333
|
-
const first = `${from}:-1`;
|
|
1334
|
-
costs.set(first, 0);
|
|
1335
|
-
vertices.set(first, from);
|
|
1336
|
-
queue.push({
|
|
1337
|
-
index: from,
|
|
1338
|
-
previous: -1,
|
|
1339
|
-
key: first,
|
|
1340
|
-
cost: 0
|
|
1341
|
-
}, remainingDistance(start));
|
|
1342
|
-
while (queue.items.length && context.visited < context.maxSearchNodes) {
|
|
1343
|
-
const current = queue.pop();
|
|
1344
|
-
if (costs.get(current.key) !== current.cost) continue;
|
|
1345
|
-
context.visited++;
|
|
1346
|
-
if (current.index === to) {
|
|
1347
|
-
const result = [];
|
|
1348
|
-
let key = current.key;
|
|
1349
|
-
while (key !== void 0) {
|
|
1350
|
-
result.push(points[vertices.get(key)]);
|
|
1351
|
-
key = parents.get(key);
|
|
1352
|
-
}
|
|
1353
|
-
return simplify(result.reverse());
|
|
1354
|
-
}
|
|
1355
|
-
const a = points[current.index];
|
|
1356
|
-
for (const next of neighbors(current.index)) {
|
|
1357
|
-
if (next === current.index || next === current.previous) continue;
|
|
1358
|
-
const b = points[next];
|
|
1359
|
-
if (!visible(current.index, next)) continue;
|
|
1360
|
-
const before = current.previous < 0 ? terminals.incoming : delta(points[current.previous], a);
|
|
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;
|
|
1364
|
-
const bend = turnCost(before, step) + (next === to ? turnCost(step, terminals.outgoing) : 0);
|
|
1365
|
-
const pair = Math.min(current.index, next) * points.length + Math.max(current.index, next);
|
|
1366
|
-
let edgeCost = edgeCosts.get(pair);
|
|
1367
|
-
if (edgeCost === void 0) {
|
|
1368
|
-
edgeCost = context.edgeCost?.(a, b) ?? 0;
|
|
1369
|
-
edgeCosts.set(pair, edgeCost);
|
|
1370
|
-
}
|
|
1371
|
-
const cost = current.cost + distance(a, b) + bend + edgeCost;
|
|
1372
|
-
const key = `${next}:${current.index}`;
|
|
1373
|
-
if (cost >= (costs.get(key) ?? Infinity)) continue;
|
|
1374
|
-
costs.set(key, cost);
|
|
1375
|
-
parents.set(key, current.key);
|
|
1376
|
-
vertices.set(key, next);
|
|
1377
|
-
queue.push({
|
|
1378
|
-
index: next,
|
|
1379
|
-
previous: current.index,
|
|
1380
|
-
key,
|
|
1381
|
-
cost
|
|
1382
|
-
}, cost + remainingDistance(b));
|
|
1383
|
-
}
|
|
1384
|
-
}
|
|
1385
|
-
}
|
|
1386
|
-
if (context.visited >= context.maxSearchNodes) context.budgetExceeded = true;
|
|
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
|
-
};
|
|
1409
|
-
|
|
1410
|
-
//#endregion
|
|
1411
|
-
//#region src/routing/coordination.ts
|
|
1412
|
-
function reservations(route) {
|
|
1413
|
-
return route.sections.flatMap((section) => pathReservations(section.path).map((segment) => ({
|
|
1414
|
-
...segment,
|
|
1415
|
-
sharedId: section.sharedId
|
|
1416
|
-
})));
|
|
1417
|
-
}
|
|
1418
|
-
function pathReservations(path) {
|
|
1419
|
-
const points = flattenPath(path, { tolerance: .5 });
|
|
1420
|
-
return points.slice(1).map((b, i) => ({
|
|
1421
|
-
a: points[i],
|
|
1422
|
-
b
|
|
1423
|
-
}));
|
|
1424
|
-
}
|
|
1425
|
-
function reservationBounds(segment, spacing) {
|
|
1426
|
-
return inflate(segmentBounds(segment.a, segment.b), Math.max(1, spacing));
|
|
1427
|
-
}
|
|
1428
|
-
/** Finite penalties: a crossing is preferable to an arbitrarily long detour. */
|
|
1429
|
-
function conflictCost(a, b, other, config) {
|
|
1430
|
-
const dx = b.x - a.x, dy = b.y - a.y;
|
|
1431
|
-
const ex = other.b.x - other.a.x, ey = other.b.y - other.a.y;
|
|
1432
|
-
const length = Math.hypot(dx, dy), otherLength = Math.hypot(ex, ey);
|
|
1433
|
-
if (length < 1e-8 || otherLength < 1e-8) return 0;
|
|
1434
|
-
const cross = dx * ey - dy * ex;
|
|
1435
|
-
const ox = other.a.x - a.x, oy = other.a.y - a.y;
|
|
1436
|
-
if (Math.abs(cross) > 1e-8 * length * otherLength) {
|
|
1437
|
-
const t = (ox * ey - oy * ex) / cross;
|
|
1438
|
-
const u = (ox * dy - oy * dx) / cross;
|
|
1439
|
-
if (t < -1e-8 || t > 1.00000001 || u < -1e-8 || u > 1.00000001) return 0;
|
|
1440
|
-
return config.crossingPenalty * (t < 1e-8 || t > .99999999 ? .5 : 1) * (u < 1e-8 || u > .99999999 ? .5 : 1);
|
|
1441
|
-
}
|
|
1442
|
-
const separation = Math.abs(ox * dy - oy * dx) / length;
|
|
1443
|
-
const spacing = Math.max(1e-8, config.edgeSpacing);
|
|
1444
|
-
if (separation >= spacing) return 0;
|
|
1445
|
-
const lo = (ox * dx + oy * dy) / length;
|
|
1446
|
-
const hi = lo + (ex * dx + ey * dy) / length;
|
|
1447
|
-
const overlap = Math.max(0, Math.min(length, Math.max(lo, hi)) - Math.max(0, Math.min(lo, hi)));
|
|
1448
|
-
return config.overlapPenalty * overlap * (1 - separation / spacing);
|
|
1449
|
-
}
|
|
1450
|
-
/** Remove only the common terminal's attachment region from soft reservations. */
|
|
1451
|
-
function outsideTerminal(segment, rect$1, closed = true) {
|
|
1452
|
-
let lo = 0, hi = 1;
|
|
1453
|
-
const dx = segment.b.x - segment.a.x, dy = segment.b.y - segment.a.y;
|
|
1454
|
-
for (const [start, delta, min, max] of [[
|
|
1455
|
-
segment.a.x,
|
|
1456
|
-
dx,
|
|
1457
|
-
rect$1.x,
|
|
1458
|
-
rect$1.x + rect$1.width
|
|
1459
|
-
], [
|
|
1460
|
-
segment.a.y,
|
|
1461
|
-
dy,
|
|
1462
|
-
rect$1.y,
|
|
1463
|
-
rect$1.y + rect$1.height
|
|
1464
|
-
]]) if (Math.abs(delta) < 1e-8) {
|
|
1465
|
-
if (closed ? start < min || start > max : start <= min + 1e-8 || start >= max - 1e-8) return [segment];
|
|
1466
|
-
} else {
|
|
1467
|
-
const a = (min - start) / delta, b = (max - start) / delta;
|
|
1468
|
-
lo = Math.max(lo, Math.min(a, b));
|
|
1469
|
-
hi = Math.min(hi, Math.max(a, b));
|
|
1470
|
-
}
|
|
1471
|
-
if (closed ? lo > hi : lo >= hi) return [segment];
|
|
1472
|
-
const at = (t) => ({
|
|
1473
|
-
x: segment.a.x + dx * t,
|
|
1474
|
-
y: segment.a.y + dy * t
|
|
1475
|
-
});
|
|
1476
|
-
return [...lo > 1e-8 ? [{
|
|
1477
|
-
...segment,
|
|
1478
|
-
b: at(lo)
|
|
1479
|
-
}] : [], ...hi < .99999999 ? [{
|
|
1480
|
-
...segment,
|
|
1481
|
-
a: at(hi)
|
|
1482
|
-
}] : []];
|
|
1483
|
-
}
|
|
1484
|
-
|
|
1485
|
-
//#endregion
|
|
1486
|
-
//#region src/routing/algorithms.ts
|
|
1487
|
-
function contextFor(state, excluded, endpoints, metrics$1, batch, padding = 0) {
|
|
1488
|
-
return {
|
|
1489
|
-
edgeCost: (a, b) => batch.edgeCost?.(a, b) ?? 0,
|
|
1490
|
-
guides: (bounds) => batch.guides?.(bounds) ?? [],
|
|
1491
|
-
maxSearchNodes: Math.max(1, Math.floor(state.settings.maxSearchNodes / 2)),
|
|
1492
|
-
maxGridNodes: state.settings.maxSearchNodes,
|
|
1493
|
-
bendPenalty: state.settings.bendPenalty,
|
|
1494
|
-
visited: 0,
|
|
1495
|
-
budgetExceeded: false,
|
|
1496
|
-
obstacles(bounds) {
|
|
1497
|
-
const queried = inflate(bounds, state.settings.clearance + padding);
|
|
1498
|
-
batch.dependencyBounds = batch.dependencyBounds ? union(batch.dependencyBounds, queried) : queried;
|
|
1499
|
-
const ids = state.obstacles.query(queried);
|
|
1500
|
-
metrics$1.obstacleCandidates += ids.length;
|
|
1501
|
-
return ids.filter((id) => !excluded.has(id)).map((id) => inflate(state.obstacles.bounds.get(id), endpoints.has(id) ? 0 : state.settings.clearance + padding));
|
|
1502
|
-
}
|
|
1503
|
-
};
|
|
1504
|
-
}
|
|
1505
|
-
function pathLength(path) {
|
|
1506
|
-
return pathReservations(path).reduce((sum, segment) => sum + distance(segment.a, segment.b), 0);
|
|
1507
|
-
}
|
|
1508
|
-
function pathCost(path, context) {
|
|
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;
|
|
1520
|
-
}
|
|
1521
|
-
function safe(path, context) {
|
|
1522
|
-
let start = path.start;
|
|
1523
|
-
for (const segment of path.segments) {
|
|
1524
|
-
const bounds = getPathBounds({
|
|
1525
|
-
start,
|
|
1526
|
-
segments: [segment]
|
|
1527
|
-
});
|
|
1528
|
-
if (context.obstacles(bounds).some((rect$1) => segmentCrossesRect(start, segment, rect$1))) return false;
|
|
1529
|
-
start = segment.to;
|
|
1530
|
-
}
|
|
1531
|
-
return true;
|
|
1532
|
-
}
|
|
1533
|
-
function cubic(start, end) {
|
|
1534
|
-
const amount = Math.max(20, distance(start.point, end.point) / 2), a = vector(start.side), b = vector(end.side);
|
|
1535
|
-
return {
|
|
1536
|
-
start: start.point,
|
|
1537
|
-
segments: [{
|
|
1538
|
-
kind: "cubic",
|
|
1539
|
-
control1: {
|
|
1540
|
-
x: start.point.x + a.x * amount,
|
|
1541
|
-
y: start.point.y + a.y * amount
|
|
1542
|
-
},
|
|
1543
|
-
control2: {
|
|
1544
|
-
x: end.point.x + b.x * amount,
|
|
1545
|
-
y: end.point.y + b.y * amount
|
|
1546
|
-
},
|
|
1547
|
-
to: end.point
|
|
1548
|
-
}]
|
|
1549
|
-
};
|
|
1550
|
-
}
|
|
1551
|
-
function curve(points, state, context) {
|
|
1552
|
-
const path = pathFromPoints(points);
|
|
1553
|
-
for (let radius = state.settings.radius; radius > .01; radius /= 2) {
|
|
1554
|
-
const rounded = roundCorners(path, { radius });
|
|
1555
|
-
if (safe(rounded, context)) return rounded;
|
|
1556
|
-
}
|
|
1557
|
-
return path;
|
|
1558
|
-
}
|
|
1559
|
-
/** Elastic string relaxation, constrained by collision checks on each proposed move. */
|
|
1560
|
-
function organic(points, state, context) {
|
|
1561
|
-
let samples = [];
|
|
1562
|
-
for (let i = 1; i < points.length; i++) {
|
|
1563
|
-
const a = points[i - 1], b = points[i], count = Math.min(16, Math.max(2, Math.ceil(distance(a, b) / 32)));
|
|
1564
|
-
for (let j = 0; j < count; j++) samples.push({
|
|
1565
|
-
x: a.x + (b.x - a.x) * j / count,
|
|
1566
|
-
y: a.y + (b.y - a.y) * j / count
|
|
1567
|
-
});
|
|
1568
|
-
}
|
|
1569
|
-
samples.push(points.at(-1));
|
|
1570
|
-
for (let iteration = 0; iteration < state.settings.organicIterations; iteration++) {
|
|
1571
|
-
const next = [...samples];
|
|
1572
|
-
for (let i = 1; i < samples.length - 1; i++) {
|
|
1573
|
-
const p = samples[i], a = samples[i - 1], b = samples[i + 1];
|
|
1574
|
-
let x = (a.x + b.x - 2 * p.x) * .2, y = (a.y + b.y - 2 * p.y) * .2;
|
|
1575
|
-
for (const r of context.obstacles(inflate({
|
|
1576
|
-
...p,
|
|
1577
|
-
width: 0,
|
|
1578
|
-
height: 0
|
|
1579
|
-
}, 24))) {
|
|
1580
|
-
const q = {
|
|
1581
|
-
x: Math.max(r.x, Math.min(r.x + r.width, p.x)),
|
|
1582
|
-
y: Math.max(r.y, Math.min(r.y + r.height, p.y))
|
|
1583
|
-
}, d = distance(p, q);
|
|
1584
|
-
if (d > 0 && d < 24) {
|
|
1585
|
-
x += (p.x - q.x) / d * (24 - d) * .2;
|
|
1586
|
-
y += (p.y - q.y) / d * (24 - d) * .2;
|
|
1587
|
-
}
|
|
1588
|
-
}
|
|
1589
|
-
const candidate = {
|
|
1590
|
-
x: p.x + x,
|
|
1591
|
-
y: p.y + y
|
|
1592
|
-
};
|
|
1593
|
-
if (clear(next[i - 1], candidate, context) && clear(candidate, b, context)) next[i] = candidate;
|
|
1594
|
-
}
|
|
1595
|
-
samples = next;
|
|
1596
|
-
}
|
|
1597
|
-
return curve(simplify(samples), state, context);
|
|
1598
|
-
}
|
|
1599
|
-
function groupPlan(key, state, metrics$1, batch, style) {
|
|
1600
|
-
if (batch.plans.has(key)) return batch.plans.get(key);
|
|
1601
|
-
const edges = [...state.groups.get(key)?.keys() ?? []].map((id) => state.edges.get(id));
|
|
1602
|
-
const nodes = [...new Set(edges.flatMap((e) => [e.sourceId, e.targetId]))];
|
|
1603
|
-
const rectangles = nodes.map((id) => state.nodes.get(id)).map((n) => n && worldRect(n, state)).filter((r) => r !== void 0);
|
|
1604
|
-
if (rectangles.length !== nodes.length || !rectangles.length) {
|
|
1605
|
-
batch.plans.set(key, null);
|
|
1606
|
-
return null;
|
|
1607
|
-
}
|
|
1608
|
-
const common = new Set(ancestors(nodes[0], state));
|
|
1609
|
-
for (const id of nodes.slice(1)) {
|
|
1610
|
-
const set = new Set(ancestors(id, state));
|
|
1611
|
-
for (const parent of common) if (!set.has(parent)) common.delete(parent);
|
|
1612
|
-
}
|
|
1613
|
-
const context = contextFor(state, new Set([...common].map((id) => `n:${id}`)), /* @__PURE__ */ new Set(), metrics$1, batch);
|
|
1614
|
-
const bounds = rectangles.reduce(union), spacing = state.settings.clearance + state.settings.edgeSpacing + 1;
|
|
1615
|
-
let result = null;
|
|
1616
|
-
const targets = edges.map((e) => labelRect(e, state) ?? worldRect(state.nodes.get(e.targetId), state));
|
|
1617
|
-
const sources = edges.map((e) => worldRect(state.nodes.get(e.sourceId), state));
|
|
1618
|
-
const candidates = [];
|
|
1619
|
-
if (new Set(edges.map((e) => e.sourceId)).size === 1) {
|
|
1620
|
-
const source = sources[0], target = targets.reduce(union);
|
|
1621
|
-
const side = sideToward(center(source), center(target)), v = vector(side);
|
|
1622
|
-
const port = edges[0].sourcePort;
|
|
1623
|
-
const named = port === void 0 ? void 0 : state.nodes.get(edges[0].sourceId).ports?.find((p) => p.name === port);
|
|
1624
|
-
const attachment = named && Number.isFinite(named.x) && Number.isFinite(named.y) ? {
|
|
1625
|
-
x: source.x + named.x + (named.width ?? 0) / 2,
|
|
1626
|
-
y: source.y + named.y + (named.height ?? 0) / 2
|
|
1627
|
-
} : anchor(source, side);
|
|
1628
|
-
const start = {
|
|
1629
|
-
x: attachment.x + v.x * spacing,
|
|
1630
|
-
y: attachment.y + v.y * spacing
|
|
1631
|
-
};
|
|
1632
|
-
const end = side === "right" ? {
|
|
1633
|
-
x: target.x - spacing,
|
|
1634
|
-
y: start.y
|
|
1635
|
-
} : side === "left" ? {
|
|
1636
|
-
x: target.x + target.width + spacing,
|
|
1637
|
-
y: start.y
|
|
1638
|
-
} : side === "bottom" ? {
|
|
1639
|
-
x: start.x,
|
|
1640
|
-
y: target.y - spacing
|
|
1641
|
-
} : {
|
|
1642
|
-
x: start.x,
|
|
1643
|
-
y: target.y + target.height + spacing
|
|
1644
|
-
};
|
|
1645
|
-
if ((end.x - start.x) * v.x + (end.y - start.y) * v.y > 1e-8) candidates.push({
|
|
1646
|
-
start,
|
|
1647
|
-
end
|
|
1648
|
-
});
|
|
1649
|
-
}
|
|
1650
|
-
const xs = [bounds.x - spacing, bounds.x + bounds.width + spacing];
|
|
1651
|
-
const ys = [bounds.y - spacing, bounds.y + bounds.height + spacing];
|
|
1652
|
-
const exterior = [...xs.map((x) => ({
|
|
1653
|
-
start: {
|
|
1654
|
-
x,
|
|
1655
|
-
y: ys[0]
|
|
1656
|
-
},
|
|
1657
|
-
end: {
|
|
1658
|
-
x,
|
|
1659
|
-
y: ys[1]
|
|
1660
|
-
}
|
|
1661
|
-
})), ...ys.map((y) => ({
|
|
1662
|
-
start: {
|
|
1663
|
-
x: xs[0],
|
|
1664
|
-
y
|
|
1665
|
-
},
|
|
1666
|
-
end: {
|
|
1667
|
-
x: xs[1],
|
|
1668
|
-
y
|
|
1669
|
-
}
|
|
1670
|
-
}))].flatMap((p) => [p, {
|
|
1671
|
-
start: p.end,
|
|
1672
|
-
end: p.start
|
|
1673
|
-
}]);
|
|
1674
|
-
const score = (p) => distance(p.start, p.end) + sources.reduce((sum, r) => sum + distance(center(r), p.start), 0) + targets.reduce((sum, r) => sum + distance(p.end, center(r)), 0);
|
|
1675
|
-
exterior.sort((a, b) => score(a) - score(b));
|
|
1676
|
-
candidates.push(...exterior);
|
|
1677
|
-
for (const candidate of candidates) {
|
|
1678
|
-
const points = findPath(candidate.start, candidate.end, "orthogonal", context);
|
|
1679
|
-
if (!points) continue;
|
|
1680
|
-
result = {
|
|
1681
|
-
id: `trunk:${key}`,
|
|
1682
|
-
path: style === "bundle" ? curve(points, state, context) : pathFromPoints(points)
|
|
1683
|
-
};
|
|
1684
|
-
break;
|
|
1685
|
-
}
|
|
1686
|
-
metrics$1.searchNodes += context.visited;
|
|
1687
|
-
batch.plans.set(key, result);
|
|
1688
|
-
return result;
|
|
1689
|
-
}
|
|
1690
|
-
function routeEdge(edge, state, style, metrics$1, batch) {
|
|
1691
|
-
const config = state.settings, perEdge = config.edges[edge.id] ?? {}, diagnostics = [];
|
|
1692
|
-
const report = (code, message) => {
|
|
1693
|
-
if (!diagnostics.some((d) => d.code === code)) diagnostics.push({
|
|
1694
|
-
code,
|
|
1695
|
-
message,
|
|
1696
|
-
edgeId: edge.id
|
|
1697
|
-
});
|
|
1698
|
-
};
|
|
1699
|
-
const sourceNode = state.nodes.get(edge.sourceId), targetNode = state.nodes.get(edge.targetId);
|
|
1700
|
-
const sourceRect = sourceNode && worldRect(sourceNode, state), targetRect = targetNode && worldRect(targetNode, state);
|
|
1701
|
-
if (!sourceRect || !targetRect) report("MISSING_GEOMETRY", "Missing endpoint geometry; rendered a fallback connection");
|
|
1702
|
-
const sourceBounds = sourceRect ?? {
|
|
1703
|
-
x: 0,
|
|
1704
|
-
y: 0,
|
|
1705
|
-
width: 0,
|
|
1706
|
-
height: 0
|
|
1707
|
-
}, targetBounds = targetRect ?? {
|
|
1708
|
-
x: sourceBounds.x + 40,
|
|
1709
|
-
y: sourceBounds.y + 40,
|
|
1710
|
-
width: 0,
|
|
1711
|
-
height: 0
|
|
1712
|
-
};
|
|
1713
|
-
const loop = edge.sourceId === edge.targetId;
|
|
1714
|
-
const group = groupKey(edge, style, config), peers = group ? [...state.groups.get(group)?.keys() ?? []] : [];
|
|
1715
|
-
const grouped = peers.length > 1 && [
|
|
1716
|
-
"bus",
|
|
1717
|
-
"bundle",
|
|
1718
|
-
"fan"
|
|
1719
|
-
].includes(style);
|
|
1720
|
-
const lane = !grouped && peers.length > 1 ? peers.length - 1 - peers.indexOf(edge.id) : 0;
|
|
1721
|
-
function terminal(source$1) {
|
|
1722
|
-
const node$1 = source$1 ? sourceNode : targetNode, outer = source$1 ? sourceBounds : targetBounds, attachment = source$1 ? perEdge.sourceAttachment : perEdge.targetAttachment, bounds = attachment ? {
|
|
1723
|
-
...attachment.bounds,
|
|
1724
|
-
x: outer.x + attachment.bounds.x,
|
|
1725
|
-
y: outer.y + attachment.bounds.y
|
|
1726
|
-
} : outer, other = source$1 ? targetBounds : sourceBounds;
|
|
1727
|
-
const name = source$1 ? edge.sourcePort : edge.targetPort;
|
|
1728
|
-
let side = (source$1 ? perEdge.sourceSide : perEdge.targetSide) ?? (loop ? source$1 ? "right" : "top" : sideToward(center(bounds), center(other)));
|
|
1729
|
-
let point = anchor(bounds, side);
|
|
1730
|
-
if (attachment) {
|
|
1731
|
-
const toward = labelRect(edge, state) ?? other;
|
|
1732
|
-
side = (source$1 ? perEdge.sourceSide : perEdge.targetSide) ?? sideToward(center(bounds), center(toward));
|
|
1733
|
-
point = anchor(bounds, side);
|
|
1734
|
-
const margin = Math.min(config.clearance, Math.min(bounds.width, bounds.height) / 2);
|
|
1735
|
-
if (side === "left" || side === "right") point = {
|
|
1736
|
-
...point,
|
|
1737
|
-
y: Math.max(bounds.y + margin, Math.min(bounds.y + bounds.height - margin, center(toward).y))
|
|
1738
|
-
};
|
|
1739
|
-
else point = {
|
|
1740
|
-
...point,
|
|
1741
|
-
x: Math.max(bounds.x + margin, Math.min(bounds.x + bounds.width - margin, center(toward).x))
|
|
1742
|
-
};
|
|
1743
|
-
}
|
|
1744
|
-
if (!attachment && name === void 0 && ![
|
|
1745
|
-
"bus",
|
|
1746
|
-
"fan",
|
|
1747
|
-
"bundle"
|
|
1748
|
-
].includes(style) && !loop) {
|
|
1749
|
-
const nodeId = source$1 ? edge.sourceId : edge.targetId;
|
|
1750
|
-
const adjacent = [...state.incident.get(nodeId)?.keys() ?? []].filter((id) => {
|
|
1751
|
-
const e = state.edges.get(id);
|
|
1752
|
-
if (e.sourceId === e.targetId) return false;
|
|
1753
|
-
const from = e.sourceId === nodeId;
|
|
1754
|
-
if ((from ? e.sourcePort : e.targetPort) !== void 0) return false;
|
|
1755
|
-
const otherNode = state.nodes.get(from ? e.targetId : e.sourceId);
|
|
1756
|
-
const rect$1 = otherNode && worldRect(otherNode, state);
|
|
1757
|
-
if (!rect$1) return false;
|
|
1758
|
-
return ((from ? config.edges[id]?.sourceSide : config.edges[id]?.targetSide) ?? sideToward(center(bounds), center(rect$1))) === side;
|
|
1759
|
-
});
|
|
1760
|
-
const horizontal = side === "left" || side === "right";
|
|
1761
|
-
const projected = (id) => {
|
|
1762
|
-
const e = state.edges.get(id);
|
|
1763
|
-
const p = center(worldRect(state.nodes.get(e.sourceId === nodeId ? e.targetId : e.sourceId), state));
|
|
1764
|
-
return horizontal ? p.y : p.x;
|
|
1765
|
-
};
|
|
1766
|
-
adjacent.sort((a, b) => projected(a) - projected(b) || (a < b ? -1 : a > b ? 1 : 0));
|
|
1767
|
-
const span = horizontal ? bounds.height : bounds.width;
|
|
1768
|
-
const spacing = Math.min(config.edgeSpacing, span * .7 / Math.max(1, adjacent.length - 1));
|
|
1769
|
-
const shift = (adjacent.indexOf(edge.id) - (adjacent.length - 1) / 2) * spacing;
|
|
1770
|
-
point = horizontal ? {
|
|
1771
|
-
x: point.x,
|
|
1772
|
-
y: point.y + shift
|
|
1773
|
-
} : {
|
|
1774
|
-
x: point.x + shift,
|
|
1775
|
-
y: point.y
|
|
1776
|
-
};
|
|
1777
|
-
}
|
|
1778
|
-
if (name !== void 0) {
|
|
1779
|
-
const port = node$1?.ports?.find((p) => p.name === name);
|
|
1780
|
-
if (port && Number.isFinite(port.x) && Number.isFinite(port.y) && Number.isFinite(port.width ?? 0) && Number.isFinite(port.height ?? 0)) {
|
|
1781
|
-
point = {
|
|
1782
|
-
x: outer.x + port.x + (port.width ?? 0) / 2,
|
|
1783
|
-
y: outer.y + port.y + (port.height ?? 0) / 2
|
|
1784
|
-
};
|
|
1785
|
-
if (!(source$1 ? perEdge.sourceSide : perEdge.targetSide)) side = sideToward(center(outer), point);
|
|
1786
|
-
} else report("MISSING_PORT", `Port ${name} has no positioned geometry; used the node boundary`);
|
|
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
|
-
}
|
|
1820
|
-
return {
|
|
1821
|
-
point,
|
|
1822
|
-
side: attachment?.facing === "inward" ? {
|
|
1823
|
-
left: "right",
|
|
1824
|
-
right: "left",
|
|
1825
|
-
top: "bottom",
|
|
1826
|
-
bottom: "top"
|
|
1827
|
-
}[side] : side,
|
|
1828
|
-
ref: {
|
|
1829
|
-
kind: "node",
|
|
1830
|
-
nodeId: source$1 ? edge.sourceId : edge.targetId,
|
|
1831
|
-
...name === void 0 ? {} : { port: name }
|
|
1832
|
-
}
|
|
1833
|
-
};
|
|
1834
|
-
}
|
|
1835
|
-
const source = terminal(true), target = terminal(false), label = labelRect(edge, state);
|
|
1836
|
-
const excluded = new Set([...ancestors(edge.sourceId, state), ...ancestors(edge.targetId, state)].filter((id) => id !== edge.sourceId && id !== edge.targetId).map((id) => `n:${id}`));
|
|
1837
|
-
if (perEdge.sourceAttachment !== void 0) excluded.add(`n:${edge.sourceId}`);
|
|
1838
|
-
if (perEdge.targetAttachment !== void 0) excluded.add(`n:${edge.targetId}`);
|
|
1839
|
-
const context = contextFor(state, excluded, new Set([
|
|
1840
|
-
`n:${edge.sourceId}`,
|
|
1841
|
-
`n:${edge.targetId}`,
|
|
1842
|
-
`e:${edge.id}`
|
|
1843
|
-
]), metrics$1, batch, lane * config.edgeSpacing);
|
|
1844
|
-
const searchContext = [
|
|
1845
|
-
"curved",
|
|
1846
|
-
"organic",
|
|
1847
|
-
"bundle",
|
|
1848
|
-
"bezier"
|
|
1849
|
-
].includes(style) ? contextFor(state, excluded, new Set([
|
|
1850
|
-
`n:${edge.sourceId}`,
|
|
1851
|
-
`n:${edge.targetId}`,
|
|
1852
|
-
`e:${edge.id}`
|
|
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
|
-
}
|
|
1878
|
-
const labelAnchor = (toward) => {
|
|
1879
|
-
const side = sideToward(center(label), toward), point = anchor(label, side);
|
|
1880
|
-
if ((side === "left" || side === "right") && toward.y > label.y && toward.y < label.y + label.height) return {
|
|
1881
|
-
x: point.x,
|
|
1882
|
-
y: toward.y
|
|
1883
|
-
};
|
|
1884
|
-
if ((side === "top" || side === "bottom") && toward.x > label.x && toward.x < label.x + label.width) return {
|
|
1885
|
-
x: toward.x,
|
|
1886
|
-
y: point.y
|
|
1887
|
-
};
|
|
1888
|
-
return point;
|
|
1889
|
-
};
|
|
1890
|
-
const firstTarget = label ? {
|
|
1891
|
-
point: labelAnchor(source.point),
|
|
1892
|
-
side: sideToward(center(label), source.point),
|
|
1893
|
-
ref: {
|
|
1894
|
-
kind: "label",
|
|
1895
|
-
edgeId: edge.id
|
|
1896
|
-
}
|
|
1897
|
-
} : target;
|
|
1898
|
-
const lastSource = label ? {
|
|
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),
|
|
1911
|
-
ref: {
|
|
1912
|
-
kind: "label",
|
|
1913
|
-
edgeId: edge.id
|
|
1914
|
-
}
|
|
1915
|
-
} : void 0;
|
|
1916
|
-
metrics$1.routedEdges++;
|
|
1917
|
-
const sections = [];
|
|
1918
|
-
const stub = (t) => {
|
|
1919
|
-
if (t.ref.kind === "junction") return t.point;
|
|
1920
|
-
const v = vector(t.side), length = config.clearance + 1 + (!grouped && peers.length > 1 ? peers.indexOf(edge.id) : 0) * config.edgeSpacing;
|
|
1921
|
-
return {
|
|
1922
|
-
x: t.point.x + v.x * length,
|
|
1923
|
-
y: t.point.y + v.y * length
|
|
1924
|
-
};
|
|
1925
|
-
};
|
|
1926
|
-
function connection(a, b, via$1 = []) {
|
|
1927
|
-
let path, preferred;
|
|
1928
|
-
if (distance(a.point, b.point) < 1e-8 && !via$1.length) {
|
|
1929
|
-
const v = vector(a.side), tangent = {
|
|
1930
|
-
x: -v.y,
|
|
1931
|
-
y: v.x
|
|
1932
|
-
};
|
|
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");
|
|
1986
|
-
return pathFromPoints([
|
|
1987
|
-
a.point,
|
|
1988
|
-
{
|
|
1989
|
-
x: a.point.x + v.x * gap,
|
|
1990
|
-
y: a.point.y + v.y * gap
|
|
1991
|
-
},
|
|
1992
|
-
b.point
|
|
1993
|
-
]);
|
|
1994
|
-
}
|
|
1995
|
-
if (loop && !via$1.length && a.ref.kind === "node" && b.ref.kind === "node" && a.side === "right" && b.side === "top") {
|
|
1996
|
-
const r = inflate(sourceBounds, config.clearance + config.edgeSpacing + 1);
|
|
1997
|
-
const points = [
|
|
1998
|
-
a.point,
|
|
1999
|
-
{
|
|
2000
|
-
x: r.x + r.width,
|
|
2001
|
-
y: a.point.y
|
|
2002
|
-
},
|
|
2003
|
-
{
|
|
2004
|
-
x: r.x + r.width,
|
|
2005
|
-
y: r.y
|
|
2006
|
-
},
|
|
2007
|
-
{
|
|
2008
|
-
x: b.point.x,
|
|
2009
|
-
y: r.y
|
|
2010
|
-
},
|
|
2011
|
-
b.point
|
|
2012
|
-
];
|
|
2013
|
-
const loopPath = pathFromPoints(points);
|
|
2014
|
-
if (safe(loopPath, context)) return [
|
|
2015
|
-
"curved",
|
|
2016
|
-
"bezier",
|
|
2017
|
-
"bundle"
|
|
2018
|
-
].includes(style) ? curve(points, state, context) : style === "organic" ? organic(points, state, context) : loopPath;
|
|
2019
|
-
}
|
|
2020
|
-
if (style === "straight" && !via$1.length && !loop) path = pathFromPoints([a.point, b.point]);
|
|
2021
|
-
else if (style === "bezier" && !via$1.length && !loop) {
|
|
2022
|
-
const direct = cubic(a, b);
|
|
2023
|
-
if (safe(direct, context)) {
|
|
2024
|
-
if (pathCost(direct, context) === pathLength(direct)) return direct;
|
|
2025
|
-
preferred = direct;
|
|
2026
|
-
}
|
|
2027
|
-
}
|
|
2028
|
-
if (!path) {
|
|
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];
|
|
2040
|
-
if (style === "parallel" && peers.length > 1) {
|
|
2041
|
-
const base = findPathBetweenLeads(start, end, "polyline", context, [[a.point, start], [end, b.point]]);
|
|
2042
|
-
const mid = base && base.length > 2 ? base[Math.floor(base.length / 2)] : center(segmentBounds(start, end));
|
|
2043
|
-
const d = Math.max(1, distance(start, end)), rank = peers.indexOf(edge.id) + 1;
|
|
2044
|
-
const orientation = edge.sourceId < edge.targetId ? 1 : -1;
|
|
2045
|
-
if (base && base.length > 2) waypoints.push(mid);
|
|
2046
|
-
for (const sign of base && base.length > 2 ? [] : [orientation, -orientation]) {
|
|
2047
|
-
const lane$1 = {
|
|
2048
|
-
x: mid.x - (end.y - start.y) / d * config.edgeSpacing * rank * sign,
|
|
2049
|
-
y: mid.y + (end.x - start.x) / d * config.edgeSpacing * rank * sign
|
|
2050
|
-
};
|
|
2051
|
-
if (clear(lane$1, lane$1, context)) {
|
|
2052
|
-
waypoints.push(lane$1);
|
|
2053
|
-
break;
|
|
2054
|
-
}
|
|
2055
|
-
}
|
|
2056
|
-
if (waypoints.length === via$1.length) report("CONSTRAINT_VIOLATION", "No clear parallel lane; rendered an individual route");
|
|
2057
|
-
}
|
|
2058
|
-
const points = [a.point], anchors = [
|
|
2059
|
-
start,
|
|
2060
|
-
...waypoints,
|
|
2061
|
-
end
|
|
2062
|
-
];
|
|
2063
|
-
const searchStyle = style === "polyline" || style === "organic" || style === "parallel" ? "polyline" : style === "octilinear" ? "octilinear" : "orthogonal";
|
|
2064
|
-
let ok = clear(a.point, start, context) && clear(end, b.point, context);
|
|
2065
|
-
for (let i = 1; ok && i < anchors.length; i++) {
|
|
2066
|
-
const directions = {
|
|
2067
|
-
incoming: i === 1 && a.ref.kind !== "junction" ? vector(a.side) : void 0,
|
|
2068
|
-
outgoing: i === anchors.length - 1 ? {
|
|
2069
|
-
x: -vector(b.side).x,
|
|
2070
|
-
y: -vector(b.side).y
|
|
2071
|
-
} : void 0
|
|
2072
|
-
};
|
|
2073
|
-
let found = findPathBetweenLeads(anchors[i - 1], anchors[i], searchStyle, searchContext, [[a.point, start], [end, b.point]], directions);
|
|
2074
|
-
if (!found && searchContext !== context && !searchContext.budgetExceeded) {
|
|
2075
|
-
const narrow = {
|
|
2076
|
-
...searchContext,
|
|
2077
|
-
obstacles: context.obstacles
|
|
2078
|
-
};
|
|
2079
|
-
found = findPathBetweenLeads(anchors[i - 1], anchors[i], searchStyle, narrow, [[a.point, start], [end, b.point]], directions);
|
|
2080
|
-
searchContext.visited = narrow.visited;
|
|
2081
|
-
searchContext.budgetExceeded = narrow.budgetExceeded;
|
|
2082
|
-
}
|
|
2083
|
-
if (found) points.push(...found.slice(i === 1 ? 0 : 1));
|
|
2084
|
-
else ok = false;
|
|
2085
|
-
}
|
|
2086
|
-
points.push(b.point);
|
|
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;
|
|
2219
|
-
}
|
|
2220
|
-
}
|
|
2221
|
-
report(searchContext.budgetExceeded ? "SEARCH_BUDGET" : "ROUTE_BLOCKED", "Preferred route unavailable; fallback may cross obstacles or violate routing constraints");
|
|
2222
|
-
if (loop && !via$1.length) {
|
|
2223
|
-
const r = inflate(sourceBounds, config.clearance + config.edgeSpacing + 1);
|
|
2224
|
-
return pathFromPoints([
|
|
2225
|
-
a.point,
|
|
2226
|
-
{
|
|
2227
|
-
x: r.x + r.width,
|
|
2228
|
-
y: a.point.y
|
|
2229
|
-
},
|
|
2230
|
-
{
|
|
2231
|
-
x: r.x + r.width,
|
|
2232
|
-
y: r.y
|
|
2233
|
-
},
|
|
2234
|
-
{
|
|
2235
|
-
x: b.point.x,
|
|
2236
|
-
y: r.y
|
|
2237
|
-
},
|
|
2238
|
-
b.point
|
|
2239
|
-
]);
|
|
2240
|
-
}
|
|
2241
|
-
return pathFromPoints([
|
|
2242
|
-
a.point,
|
|
2243
|
-
...via$1,
|
|
2244
|
-
b.point
|
|
2245
|
-
]);
|
|
2246
|
-
}
|
|
2247
|
-
const via = perEdge.waypoints ?? [];
|
|
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
|
-
}
|
|
2268
|
-
const plan = grouped ? groupPlan(group, state, metrics$1, batch, style) : void 0;
|
|
2269
|
-
if (grouped && !plan) report("ROUTE_BLOCKED", "Shared trunk unavailable; used individual fallback routing");
|
|
2270
|
-
if (plan) {
|
|
2271
|
-
const start = plan.path.start, end = plan.path.segments.at(-1)?.to ?? start;
|
|
2272
|
-
const j0 = {
|
|
2273
|
-
point: start,
|
|
2274
|
-
side: sideToward(start, source.point),
|
|
2275
|
-
ref: {
|
|
2276
|
-
kind: "junction",
|
|
2277
|
-
id: `${plan.id}:start`
|
|
2278
|
-
}
|
|
2279
|
-
};
|
|
2280
|
-
const j1 = {
|
|
2281
|
-
point: end,
|
|
2282
|
-
side: sideToward(end, firstTarget.point),
|
|
2283
|
-
ref: {
|
|
2284
|
-
kind: "junction",
|
|
2285
|
-
id: `${plan.id}:end`
|
|
2286
|
-
}
|
|
2287
|
-
};
|
|
2288
|
-
sections.push({
|
|
2289
|
-
id: `${edge.id}:source`,
|
|
2290
|
-
from: source.ref,
|
|
2291
|
-
to: j0.ref,
|
|
2292
|
-
path: connection(source, j0, via)
|
|
2293
|
-
}, {
|
|
2294
|
-
id: `${edge.id}:trunk`,
|
|
2295
|
-
sharedId: plan.id,
|
|
2296
|
-
from: j0.ref,
|
|
2297
|
-
to: j1.ref,
|
|
2298
|
-
path: plan.path
|
|
2299
|
-
}, {
|
|
2300
|
-
id: `${edge.id}:target`,
|
|
2301
|
-
from: j1.ref,
|
|
2302
|
-
to: firstTarget.ref,
|
|
2303
|
-
path: connection(j1, firstTarget)
|
|
2304
|
-
});
|
|
2305
|
-
} else sections.push({
|
|
2306
|
-
id: `${edge.id}:0`,
|
|
2307
|
-
from: source.ref,
|
|
2308
|
-
to: firstTarget.ref,
|
|
2309
|
-
path: connection(source, firstTarget, via)
|
|
2310
|
-
});
|
|
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
|
-
}
|
|
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");
|
|
2325
|
-
metrics$1.searchNodes += searchContext.visited + (searchContext === context ? 0 : context.visited);
|
|
2326
|
-
return {
|
|
2327
|
-
edgeId: edge.id,
|
|
2328
|
-
sections,
|
|
2329
|
-
status: diagnostics.length ? "fallback" : "routed",
|
|
2330
|
-
diagnostics
|
|
2331
|
-
};
|
|
2332
|
-
}
|
|
2333
|
-
function routeBounds(route) {
|
|
2334
|
-
return route.sections.map((s) => getPathBounds(s.path)).reduce(union);
|
|
2335
|
-
}
|
|
2336
|
-
|
|
2337
|
-
//#endregion
|
|
2338
|
-
//#region src/routing/engine.ts
|
|
2339
|
-
const STATE = Symbol("routingState");
|
|
2340
|
-
const metrics = () => ({
|
|
2341
|
-
routedEdges: 0,
|
|
2342
|
-
reusedEdges: 0,
|
|
2343
|
-
affectedEdges: 0,
|
|
2344
|
-
searchNodes: 0,
|
|
2345
|
-
obstacleCandidates: 0
|
|
2346
|
-
});
|
|
2347
|
-
function snapshot(state, style, revision, counts) {
|
|
2348
|
-
return Object.freeze({
|
|
2349
|
-
strategy: style,
|
|
2350
|
-
revision,
|
|
2351
|
-
routes: state.routes,
|
|
2352
|
-
metrics: Object.freeze(counts),
|
|
2353
|
-
[STATE]: Object.freeze(state)
|
|
2354
|
-
});
|
|
2355
|
-
}
|
|
2356
|
-
function edgeMembership(state, edge, style, add) {
|
|
2357
|
-
let incident = state.incident, groups = state.groups;
|
|
2358
|
-
for (const id of new Set([edge.sourceId, edge.targetId])) incident = members(incident, id, edge.id, add);
|
|
2359
|
-
const key = groupKey(edge, style, state.settings);
|
|
2360
|
-
if (key !== void 0) groups = members(groups, key, edge.id, add);
|
|
2361
|
-
return {
|
|
2362
|
-
...state,
|
|
2363
|
-
incident,
|
|
2364
|
-
groups
|
|
2365
|
-
};
|
|
2366
|
-
}
|
|
2367
|
-
function initialState(graph, config, style) {
|
|
2368
|
-
let nodes = new PersistentMap(), edges = new PersistentMap(), children = new PersistentMap();
|
|
2369
|
-
for (const n of graph.nodes) {
|
|
2370
|
-
if (nodes.has(n.id)) throw new Error(`Duplicate routing node ${n.id}`);
|
|
2371
|
-
nodes = nodes.set(n.id, nodeGeometry(n));
|
|
2372
|
-
if (n.parentId != null) children = members(children, n.parentId, n.id, true);
|
|
2373
|
-
}
|
|
2374
|
-
for (const e of graph.edges) {
|
|
2375
|
-
if (edges.has(e.id)) throw new Error(`Duplicate routing edge ${e.id}`);
|
|
2376
|
-
edges = edges.set(e.id, edgeGeometry(e));
|
|
2377
|
-
}
|
|
2378
|
-
let state = {
|
|
2379
|
-
graphId: graph.id,
|
|
2380
|
-
direction: graph.direction,
|
|
2381
|
-
settings: settings(config),
|
|
2382
|
-
nodes,
|
|
2383
|
-
edges,
|
|
2384
|
-
children,
|
|
2385
|
-
incident: new PersistentMap(),
|
|
2386
|
-
groups: new PersistentMap(),
|
|
2387
|
-
obstacles: new SpatialIndex(),
|
|
2388
|
-
routeIndex: new SpatialIndex(),
|
|
2389
|
-
routes: new PersistentMap()
|
|
2390
|
-
};
|
|
2391
|
-
for (const [id, n] of nodes) state = {
|
|
2392
|
-
...state,
|
|
2393
|
-
obstacles: state.obstacles.set(`n:${id}`, worldRect(n, state))
|
|
2394
|
-
};
|
|
2395
|
-
for (const [id, e] of edges) {
|
|
2396
|
-
state = edgeMembership(state, e, style, true);
|
|
2397
|
-
state = {
|
|
2398
|
-
...state,
|
|
2399
|
-
obstacles: state.obstacles.set(`e:${id}`, labelRect(e, state))
|
|
2400
|
-
};
|
|
2401
|
-
}
|
|
2402
|
-
return state;
|
|
2403
|
-
}
|
|
2404
|
-
function calculate(state, style, affected, counts, previous) {
|
|
2405
|
-
let routes = state.routes, routeIndex = state.routeIndex;
|
|
2406
|
-
const input = {
|
|
2407
|
-
nodes: state.nodes,
|
|
2408
|
-
incident: state.incident,
|
|
2409
|
-
edges: state.edges,
|
|
2410
|
-
groups: state.groups,
|
|
2411
|
-
obstacles: state.obstacles,
|
|
2412
|
-
settings: state.settings
|
|
2413
|
-
};
|
|
2414
|
-
const patches = [], batch = { plans: /* @__PURE__ */ new Map() };
|
|
2415
|
-
const pending = new Set(affected);
|
|
2416
|
-
const processed = /* @__PURE__ */ new Set();
|
|
2417
|
-
const priorities = /* @__PURE__ */ new Map();
|
|
2418
|
-
const priority = (id) => {
|
|
2419
|
-
let result = priorities.get(id);
|
|
2420
|
-
if (result !== void 0) return result;
|
|
2421
|
-
const edge = state.edges.get(id) ?? previous?.edges.get(id);
|
|
2422
|
-
const key = edge && groupKey(edge, style, state.settings);
|
|
2423
|
-
result = key ? (state.edges.has(id) ? state.groups : previous?.groups)?.get(key)?.keys().next().value ?? id : id;
|
|
2424
|
-
priorities.set(id, result);
|
|
2425
|
-
return result;
|
|
2426
|
-
};
|
|
2427
|
-
const later = (candidate, id) => priority(candidate) > priority(id) || priority(candidate) === priority(id) && candidate > id;
|
|
2428
|
-
const invalidate = (id, bounds) => {
|
|
2429
|
-
for (const candidate of routeIndex.query(inflate(bounds, state.settings.edgeSpacing))) {
|
|
2430
|
-
if (!later(candidate, id) || processed.has(candidate)) continue;
|
|
2431
|
-
pending.add(candidate);
|
|
2432
|
-
const edge = state.edges.get(candidate);
|
|
2433
|
-
const key = edge && groupKey(edge, style, state.settings);
|
|
2434
|
-
if (key) {
|
|
2435
|
-
for (const peer of state.groups.get(key)?.keys() ?? []) if (!processed.has(peer)) pending.add(peer);
|
|
2436
|
-
}
|
|
2437
|
-
}
|
|
2438
|
-
};
|
|
2439
|
-
if (previous) for (const id of affected) {
|
|
2440
|
-
const edge = previous.edges.get(id);
|
|
2441
|
-
if (!edge) continue;
|
|
2442
|
-
const key = groupKey(edge, style, previous.settings);
|
|
2443
|
-
if ((key ? previous.groups.get(key)?.keys().next().value ?? id : id) === priority(id)) continue;
|
|
2444
|
-
const oldRoute = previous.routes.get(id);
|
|
2445
|
-
if (oldRoute) for (const candidate of previous.routeIndex.query(inflate(routeBounds(oldRoute), state.settings.edgeSpacing))) {
|
|
2446
|
-
pending.add(candidate);
|
|
2447
|
-
const current = state.edges.get(candidate);
|
|
2448
|
-
const group = current && groupKey(current, style, state.settings);
|
|
2449
|
-
if (group) for (const peer of state.groups.get(group)?.keys() ?? []) pending.add(peer);
|
|
2450
|
-
}
|
|
2451
|
-
}
|
|
2452
|
-
const segmentCache = /* @__PURE__ */ new Map();
|
|
2453
|
-
while (pending.size) {
|
|
2454
|
-
const id = [...pending].sort((a, b) => (priority(a) < priority(b) ? -1 : priority(a) > priority(b) ? 1 : 0) || (a < b ? -1 : a > b ? 1 : 0))[0];
|
|
2455
|
-
pending.delete(id);
|
|
2456
|
-
processed.add(id);
|
|
2457
|
-
const edge = state.edges.get(id), oldRoute = previous?.routes.get(id);
|
|
2458
|
-
if (!edge) {
|
|
2459
|
-
if (routes.has(id)) {
|
|
2460
|
-
invalidate(id, routeBounds(routes.get(id)));
|
|
2461
|
-
routes = routes.delete(id);
|
|
2462
|
-
routeIndex = routeIndex.set(id);
|
|
2463
|
-
patches.push({
|
|
2464
|
-
op: "delete",
|
|
2465
|
-
edgeId: id
|
|
2466
|
-
});
|
|
2467
|
-
}
|
|
2468
|
-
continue;
|
|
2469
|
-
}
|
|
2470
|
-
batch.dependencyBounds = void 0;
|
|
2471
|
-
const query = (bounds) => {
|
|
2472
|
-
const area = inflate(bounds, state.settings.edgeSpacing);
|
|
2473
|
-
batch.dependencyBounds = batch.dependencyBounds ? union(batch.dependencyBounds, area) : area;
|
|
2474
|
-
const result = [];
|
|
2475
|
-
const shared = /* @__PURE__ */ new Set();
|
|
2476
|
-
for (const otherId of routeIndex.query(area)) {
|
|
2477
|
-
if (!later(id, otherId)) continue;
|
|
2478
|
-
const other = state.edges.get(otherId), route$1 = routes.get(otherId);
|
|
2479
|
-
if (!other || !route$1) continue;
|
|
2480
|
-
if ([
|
|
2481
|
-
"bus",
|
|
2482
|
-
"fan",
|
|
2483
|
-
"bundle"
|
|
2484
|
-
].includes(style) && groupKey(other, style, state.settings) === groupKey(edge, style, state.settings)) continue;
|
|
2485
|
-
const terminalRegions = [edge.sourceId, edge.targetId].filter((node$1) => node$1 === other.sourceId || node$1 === other.targetId).flatMap((id$1) => {
|
|
2486
|
-
const node$1 = state.nodes.get(id$1), bounds$1 = node$1 && worldRect(node$1, state);
|
|
2487
|
-
return bounds$1 ? [inflate(bounds$1, state.settings.clearance + state.settings.edgeSpacing * 2 + 1)] : [];
|
|
2488
|
-
});
|
|
2489
|
-
let segments = segmentCache.get(otherId);
|
|
2490
|
-
if (!segments) {
|
|
2491
|
-
segments = reservations(route$1);
|
|
2492
|
-
segmentCache.set(otherId, segments);
|
|
2493
|
-
}
|
|
2494
|
-
const outside = terminalRegions.reduce((segments$1, rect$1) => segments$1.flatMap((segment) => outsideTerminal(segment, rect$1)), segments);
|
|
2495
|
-
for (const segment of outside) {
|
|
2496
|
-
if (!intersects(segmentBounds(segment.a, segment.b), area)) continue;
|
|
2497
|
-
if (segment.sharedId !== void 0) {
|
|
2498
|
-
const key = JSON.stringify([
|
|
2499
|
-
segment.sharedId,
|
|
2500
|
-
segment.a,
|
|
2501
|
-
segment.b
|
|
2502
|
-
]);
|
|
2503
|
-
if (shared.has(key)) continue;
|
|
2504
|
-
shared.add(key);
|
|
2505
|
-
}
|
|
2506
|
-
result.push(segment);
|
|
2507
|
-
}
|
|
2508
|
-
}
|
|
2509
|
-
return result;
|
|
2510
|
-
};
|
|
2511
|
-
batch.edgeCost = (a, b) => query(segmentBounds(a, b)).reduce((cost, other) => cost + conflictCost(a, b, other, state.settings), 0);
|
|
2512
|
-
batch.conflicts = (a, b) => query(segmentBounds(a, b)).some((other) => conflictCost(a, b, other, {
|
|
2513
|
-
...state.settings,
|
|
2514
|
-
crossingPenalty: 1,
|
|
2515
|
-
overlapPenalty: 1
|
|
2516
|
-
}) > 1e-8);
|
|
2517
|
-
batch.guides = (bounds) => query(bounds).map((segment) => reservationBounds(segment, state.settings.edgeSpacing));
|
|
2518
|
-
let route = routeEdge(edge, input, style, counts, batch);
|
|
2519
|
-
if (oldRoute && JSON.stringify(oldRoute) === JSON.stringify(route)) route = oldRoute;
|
|
2520
|
-
const drawnBounds = routeBounds(route);
|
|
2521
|
-
if (route !== oldRoute) {
|
|
2522
|
-
if (oldRoute) invalidate(id, routeBounds(oldRoute));
|
|
2523
|
-
invalidate(id, drawnBounds);
|
|
2524
|
-
}
|
|
2525
|
-
routeIndex = routeIndex.set(id, batch.dependencyBounds ? union(batch.dependencyBounds, drawnBounds) : drawnBounds);
|
|
2526
|
-
if (route === oldRoute) continue;
|
|
2527
|
-
route = freeze(route);
|
|
2528
|
-
routes = routes.set(id, route);
|
|
2529
|
-
segmentCache.delete(id);
|
|
2530
|
-
patches.push({
|
|
2531
|
-
op: "set",
|
|
2532
|
-
edgeId: id,
|
|
2533
|
-
route
|
|
2534
|
-
});
|
|
2535
|
-
}
|
|
2536
|
-
counts.affectedEdges = processed.size;
|
|
2537
|
-
counts.reusedEdges = Math.max(0, state.routes.size - [...processed].filter((id) => state.routes.has(id)).length);
|
|
2538
|
-
patches.sort((a, b) => a.edgeId < b.edgeId ? -1 : a.edgeId > b.edgeId ? 1 : 0);
|
|
2539
|
-
return {
|
|
2540
|
-
state: {
|
|
2541
|
-
...state,
|
|
2542
|
-
routes,
|
|
2543
|
-
routeIndex
|
|
2544
|
-
},
|
|
2545
|
-
patches
|
|
2546
|
-
};
|
|
2547
|
-
}
|
|
2548
|
-
function descendants(state, ids) {
|
|
2549
|
-
const queue = [...ids];
|
|
2550
|
-
for (let i = 0; i < queue.length; i++) for (const child of state.children.get(queue[i])?.keys() ?? []) if (!ids.has(child)) {
|
|
2551
|
-
ids.add(child);
|
|
2552
|
-
queue.push(child);
|
|
2553
|
-
}
|
|
2554
|
-
}
|
|
2555
|
-
function verifyOld(record, old, id) {
|
|
2556
|
-
if (!record) throw new Error(`Routing diff updates missing entity ${id}`);
|
|
2557
|
-
for (const [key, value] of Object.entries(old)) if (key in record && JSON.stringify(record[key]) !== JSON.stringify(key === "ports" ? nodeGeometry({
|
|
2558
|
-
id,
|
|
2559
|
-
ports: value
|
|
2560
|
-
}).ports : value)) throw new Error(`Stale routing diff for ${id}.${key}`);
|
|
2561
|
-
}
|
|
2562
|
-
function updateState(graph, prior, diff, style, config) {
|
|
2563
|
-
if (graph.id !== prior.graphId) throw new Error("A routing snapshot belongs to a different graph");
|
|
2564
|
-
const candidateSettings = config === void 0 ? prior.settings : settings(config);
|
|
2565
|
-
const nextSettings = JSON.stringify(candidateSettings) === JSON.stringify(prior.settings) ? prior.settings : candidateSettings;
|
|
2566
|
-
const settingsChanged = JSON.stringify(nextSettings) !== JSON.stringify(prior.settings) || prior.direction !== graph.direction;
|
|
2567
|
-
let state = {
|
|
2568
|
-
...prior,
|
|
2569
|
-
settings: nextSettings,
|
|
2570
|
-
direction: graph.direction
|
|
2571
|
-
};
|
|
2572
|
-
const changedNodes = /* @__PURE__ */ new Set(), changedEdges = /* @__PURE__ */ new Set(), affected = /* @__PURE__ */ new Set();
|
|
2573
|
-
for (const n of diff.nodes.removed) {
|
|
2574
|
-
if (!state.nodes.has(n.id)) throw new Error(`Routing diff removes missing node ${n.id}`);
|
|
2575
|
-
const old = state.nodes.get(n.id);
|
|
2576
|
-
if (old.parentId != null) state = {
|
|
2577
|
-
...state,
|
|
2578
|
-
children: members(state.children, old.parentId, n.id, false)
|
|
2579
|
-
};
|
|
2580
|
-
state = {
|
|
2581
|
-
...state,
|
|
2582
|
-
nodes: state.nodes.delete(n.id)
|
|
2583
|
-
};
|
|
2584
|
-
changedNodes.add(n.id);
|
|
2585
|
-
}
|
|
2586
|
-
for (const n of diff.nodes.added) {
|
|
2587
|
-
if (state.nodes.has(n.id)) throw new Error(`Routing diff adds existing node ${n.id}`);
|
|
2588
|
-
state = {
|
|
2589
|
-
...state,
|
|
2590
|
-
nodes: state.nodes.set(n.id, nodeGeometry(n))
|
|
2591
|
-
};
|
|
2592
|
-
if (n.parentId != null) state = {
|
|
2593
|
-
...state,
|
|
2594
|
-
children: members(state.children, n.parentId, n.id, true)
|
|
2595
|
-
};
|
|
2596
|
-
changedNodes.add(n.id);
|
|
2597
|
-
}
|
|
2598
|
-
for (const change of diff.nodes.updated) {
|
|
2599
|
-
const old = state.nodes.get(change.id);
|
|
2600
|
-
verifyOld(old, change.old, change.id);
|
|
2601
|
-
const next = nodeGeometry({
|
|
2602
|
-
...old,
|
|
2603
|
-
...change.new,
|
|
2604
|
-
id: change.id
|
|
2605
|
-
});
|
|
2606
|
-
if (JSON.stringify(old) === JSON.stringify(next)) continue;
|
|
2607
|
-
if (old.parentId != null) state = {
|
|
2608
|
-
...state,
|
|
2609
|
-
children: members(state.children, old.parentId, change.id, false)
|
|
2610
|
-
};
|
|
2611
|
-
if (next.parentId != null) state = {
|
|
2612
|
-
...state,
|
|
2613
|
-
children: members(state.children, next.parentId, change.id, true)
|
|
2614
|
-
};
|
|
2615
|
-
state = {
|
|
2616
|
-
...state,
|
|
2617
|
-
nodes: state.nodes.set(change.id, next)
|
|
2618
|
-
};
|
|
2619
|
-
changedNodes.add(change.id);
|
|
2620
|
-
}
|
|
2621
|
-
for (const e of diff.edges.removed) {
|
|
2622
|
-
const old = state.edges.get(e.id);
|
|
2623
|
-
if (!old) throw new Error(`Routing diff removes missing edge ${e.id}`);
|
|
2624
|
-
state = edgeMembership(state, old, style, false);
|
|
2625
|
-
state = {
|
|
2626
|
-
...state,
|
|
2627
|
-
edges: state.edges.delete(e.id)
|
|
2628
|
-
};
|
|
2629
|
-
changedEdges.add(e.id);
|
|
2630
|
-
}
|
|
2631
|
-
for (const e of diff.edges.added) {
|
|
2632
|
-
if (state.edges.has(e.id)) throw new Error(`Routing diff adds existing edge ${e.id}`);
|
|
2633
|
-
const next = edgeGeometry(e);
|
|
2634
|
-
state = {
|
|
2635
|
-
...state,
|
|
2636
|
-
edges: state.edges.set(e.id, next)
|
|
2637
|
-
};
|
|
2638
|
-
state = edgeMembership(state, next, style, true);
|
|
2639
|
-
changedEdges.add(e.id);
|
|
2640
|
-
}
|
|
2641
|
-
for (const change of diff.edges.updated) {
|
|
2642
|
-
const old = state.edges.get(change.id);
|
|
2643
|
-
verifyOld(old, change.old, change.id);
|
|
2644
|
-
const next = edgeGeometry({
|
|
2645
|
-
...old,
|
|
2646
|
-
...change.new,
|
|
2647
|
-
id: change.id
|
|
2648
|
-
});
|
|
2649
|
-
if (JSON.stringify(old) === JSON.stringify(next)) continue;
|
|
2650
|
-
state = edgeMembership(state, old, style, false);
|
|
2651
|
-
state = {
|
|
2652
|
-
...state,
|
|
2653
|
-
edges: state.edges.set(change.id, next)
|
|
2654
|
-
};
|
|
2655
|
-
state = edgeMembership(state, next, style, true);
|
|
2656
|
-
changedEdges.add(change.id);
|
|
2657
|
-
}
|
|
2658
|
-
if (settingsChanged) {
|
|
2659
|
-
state = {
|
|
2660
|
-
...state,
|
|
2661
|
-
groups: new PersistentMap()
|
|
2662
|
-
};
|
|
2663
|
-
for (const [id, e] of state.edges) {
|
|
2664
|
-
const key = groupKey(e, style, nextSettings);
|
|
2665
|
-
if (key !== void 0) state = {
|
|
2666
|
-
...state,
|
|
2667
|
-
groups: members(state.groups, key, id, true)
|
|
2668
|
-
};
|
|
2669
|
-
changedEdges.add(id);
|
|
2670
|
-
}
|
|
2671
|
-
for (const id of state.nodes.keys()) changedNodes.add(id);
|
|
2672
|
-
for (const id of prior.routes.keys()) affected.add(id);
|
|
2673
|
-
}
|
|
2674
|
-
descendants(prior, changedNodes);
|
|
2675
|
-
descendants(state, changedNodes);
|
|
2676
|
-
function invalidateObstacle(id, bounds) {
|
|
2677
|
-
const old = prior.obstacles.bounds.get(id);
|
|
2678
|
-
for (const rect$1 of [old, bounds]) if (rect$1) for (const edgeId of prior.routeIndex.query(inflate(rect$1, Math.max(prior.settings.clearance, nextSettings.clearance)))) affected.add(edgeId);
|
|
2679
|
-
state = {
|
|
2680
|
-
...state,
|
|
2681
|
-
obstacles: state.obstacles.set(id, bounds)
|
|
2682
|
-
};
|
|
2683
|
-
}
|
|
2684
|
-
for (const id of changedNodes) {
|
|
2685
|
-
const node$1 = state.nodes.get(id);
|
|
2686
|
-
invalidateObstacle(`n:${id}`, node$1 && worldRect(node$1, state));
|
|
2687
|
-
for (const index of [prior.incident, state.incident]) for (const edgeId of index.get(id)?.keys() ?? []) changedEdges.add(edgeId);
|
|
2688
|
-
}
|
|
2689
|
-
for (const id of changedEdges) {
|
|
2690
|
-
const edge = state.edges.get(id);
|
|
2691
|
-
invalidateObstacle(`e:${id}`, edge && labelRect(edge, state));
|
|
2692
|
-
affected.add(id);
|
|
2693
|
-
}
|
|
2694
|
-
if (![
|
|
2695
|
-
"bus",
|
|
2696
|
-
"fan",
|
|
2697
|
-
"bundle"
|
|
2698
|
-
].includes(style)) for (const id of changedEdges) {
|
|
2699
|
-
const before = prior.edges.get(id), after = state.edges.get(id);
|
|
2700
|
-
if (!(!before || !after || before.sourceId !== after.sourceId || before.targetId !== after.targetId || before.sourcePort !== after.sourcePort || before.targetPort !== after.targetPort || [
|
|
2701
|
-
before.sourceId,
|
|
2702
|
-
before.targetId,
|
|
2703
|
-
after.sourceId,
|
|
2704
|
-
after.targetId
|
|
2705
|
-
].some((nodeId) => changedNodes.has(nodeId)))) continue;
|
|
2706
|
-
for (const version of [prior, state]) {
|
|
2707
|
-
const edge = version.edges.get(id);
|
|
2708
|
-
if (!edge) continue;
|
|
2709
|
-
for (const nodeId of [edge.sourceId, edge.targetId]) for (const peer of version.incident.get(nodeId)?.keys() ?? []) affected.add(peer);
|
|
2710
|
-
}
|
|
2711
|
-
}
|
|
2712
|
-
for (const id of [...affected]) for (const version of [prior, state]) {
|
|
2713
|
-
const edge = version.edges.get(id);
|
|
2714
|
-
if (!edge) continue;
|
|
2715
|
-
const key = groupKey(edge, style, version.settings);
|
|
2716
|
-
if (key !== void 0) for (const peer of version.groups.get(key)?.keys() ?? []) affected.add(peer);
|
|
2717
|
-
}
|
|
2718
|
-
return {
|
|
2719
|
-
state,
|
|
2720
|
-
affected
|
|
2721
|
-
};
|
|
2722
|
-
}
|
|
2723
|
-
function createRoutingStrategy(id) {
|
|
2724
|
-
return Object.freeze({
|
|
2725
|
-
id,
|
|
2726
|
-
route(graph, config = {}) {
|
|
2727
|
-
const initial = initialState(graph, config, id), counts = metrics();
|
|
2728
|
-
return snapshot(calculate(initial, id, new Set(initial.edges.keys()), counts).state, id, 0, counts);
|
|
2729
|
-
},
|
|
2730
|
-
update(graph, previous, diff, config) {
|
|
2731
|
-
const prior = previous[STATE];
|
|
2732
|
-
if (!prior || previous.strategy !== id) throw new Error("Update requires a snapshot from the same routing strategy; route again after serialization or strategy changes");
|
|
2733
|
-
const { state, affected } = updateState(graph, prior, diff, id, config), counts = metrics();
|
|
2734
|
-
const result = calculate(state, id, affected, counts, prior);
|
|
2735
|
-
return Object.freeze({
|
|
2736
|
-
base: previous,
|
|
2737
|
-
snapshot: snapshot(result.state, id, previous.revision + 1, counts),
|
|
2738
|
-
patches: Object.freeze(result.patches.map((patch) => Object.freeze(patch)))
|
|
2739
|
-
});
|
|
2740
|
-
}
|
|
2741
|
-
});
|
|
2742
|
-
}
|
|
2743
|
-
function applyRoutePatches(routes, patches) {
|
|
2744
|
-
let result = routes instanceof PersistentMap ? routes : new PersistentMap();
|
|
2745
|
-
if (!(routes instanceof PersistentMap)) for (const [id, route] of routes) result = result.set(id, route);
|
|
2746
|
-
for (const patch of patches) result = patch.op === "set" ? result.set(patch.edgeId, patch.route) : result.delete(patch.edgeId);
|
|
2747
|
-
return result;
|
|
2748
|
-
}
|
|
2749
|
-
|
|
2750
|
-
//#endregion
|
|
2751
|
-
//#region src/routing/index.ts
|
|
2752
|
-
const straightRouting = createRoutingStrategy("straight");
|
|
2753
|
-
const bezierRouting = createRoutingStrategy("bezier");
|
|
2754
|
-
const orthogonalRouting = createRoutingStrategy("orthogonal");
|
|
2755
|
-
const polylineRouting = createRoutingStrategy("polyline");
|
|
2756
|
-
const octilinearRouting = createRoutingStrategy("octilinear");
|
|
2757
|
-
const curvedRouting = createRoutingStrategy("curved");
|
|
2758
|
-
const organicRouting = createRoutingStrategy("organic");
|
|
2759
|
-
const parallelRouting = createRoutingStrategy("parallel");
|
|
2760
|
-
const selfLoopRouting = createRoutingStrategy("self-loop");
|
|
2761
|
-
const fanRouting = createRoutingStrategy("fan");
|
|
2762
|
-
const busRouting = createRoutingStrategy("bus");
|
|
2763
|
-
const bundleRouting = createRoutingStrategy("bundle");
|
|
2764
|
-
const routingStrategies = Object.freeze({
|
|
2765
|
-
straight: straightRouting,
|
|
2766
|
-
bezier: bezierRouting,
|
|
2767
|
-
orthogonal: orthogonalRouting,
|
|
2768
|
-
polyline: polylineRouting,
|
|
2769
|
-
octilinear: octilinearRouting,
|
|
2770
|
-
curved: curvedRouting,
|
|
2771
|
-
organic: organicRouting,
|
|
2772
|
-
parallel: parallelRouting,
|
|
2773
|
-
"self-loop": selfLoopRouting,
|
|
2774
|
-
fan: fanRouting,
|
|
2775
|
-
bus: busRouting,
|
|
2776
|
-
bundle: bundleRouting
|
|
2777
|
-
});
|
|
2778
|
-
|
|
2779
|
-
//#endregion
|
|
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 };
|