@statelyai/layout 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +71 -2
- package/dist/elkjs/index.cjs +3 -2
- package/dist/elkjs/index.d.cts +11 -2
- package/dist/elkjs/index.d.mts +11 -2
- package/dist/elkjs/index.mjs +3 -3
- package/dist/elkjs/main.cjs +2 -2
- package/dist/elkjs/main.d.cts +2 -2
- package/dist/elkjs/main.d.mts +2 -2
- package/dist/elkjs/main.mjs +2 -2
- package/dist/elkjs/worker-api.cjs +1 -1
- package/dist/elkjs/worker-api.d.cts +2 -2
- package/dist/elkjs/worker-api.d.mts +2 -2
- package/dist/elkjs/worker-api.mjs +1 -1
- package/dist/elkjs/worker.cjs +2 -2
- package/dist/elkjs/worker.mjs +2 -2
- package/dist/{elkjs-CFlfkEBz.mjs → elkjs-DaHMISnQ.mjs} +152 -3
- package/dist/{elkjs-BVnDwnBQ.cjs → elkjs-dMghiiWW.cjs} +157 -2
- package/dist/index-BEA_pzeT.d.cts +72 -0
- package/dist/{index-BSH2bloA.d.mts → index-D9dhxrRl.d.cts} +35 -3
- package/dist/index-DNTLRsDl.d.mts +72 -0
- package/dist/{index-CG712jzV.d.cts → index-DNixtE3h.d.mts} +35 -3
- package/dist/index.cjs +36 -9
- package/dist/index.d.cts +4 -3
- package/dist/index.d.mts +4 -3
- package/dist/index.mjs +4 -3
- package/dist/layered/index.cjs +1 -1
- package/dist/layered/index.d.cts +3 -2
- package/dist/layered/index.d.mts +3 -2
- package/dist/layered/index.mjs +1 -1
- package/dist/{layered-Cf9Rn8l7.cjs → layered-B2buJqMf.cjs} +417 -469
- package/dist/{layered-Bz2nWmVp.mjs → layered-DGFTeQSl.mjs} +391 -431
- package/dist/{layout-engine-WbC0YjjT.mjs → layout-engine-Cs5tatA6.mjs} +1 -1
- package/dist/{layout-engine-BNex9jxg.cjs → layout-engine-Dn1PiFwP.cjs} +1 -1
- package/dist/routing/index.cjs +28 -0
- package/dist/routing/index.d.cts +3 -0
- package/dist/routing/index.d.mts +3 -0
- package/dist/routing/index.mjs +3 -0
- package/dist/routing-A2hrK9MP.mjs +2396 -0
- package/dist/routing-ujEP7Qh6.cjs +2606 -0
- package/dist/types-CT2akdK0.d.mts +133 -0
- package/dist/types-uxPlGRof.d.cts +133 -0
- package/dist/{worker-client-DAT_z2OR.d.mts → worker-client-BwGGRD5y.d.mts} +1 -1
- package/dist/{worker-client-DVhklF6A.d.cts → worker-client-D4zNyPEI.d.cts} +1 -1
- package/package.json +7 -2
- package/src/authoring/coordinates.ts +10 -2
- package/src/elkjs/index.ts +5 -3
- package/src/elkjs/routes.ts +147 -0
- package/src/index.ts +6 -0
- package/src/layered/compound.ts +417 -0
- package/src/layered/index.ts +27 -102
- package/src/layered/types.ts +21 -0
- package/src/routing/adapters.ts +111 -0
- package/src/routing/algorithms.ts +676 -0
- package/src/routing/coordination.ts +86 -0
- package/src/routing/engine.ts +473 -0
- package/src/routing/index.ts +68 -0
- package/src/routing/layout-cache.ts +65 -0
- package/src/routing/model.ts +218 -0
- package/src/routing/path.ts +324 -0
- package/src/routing/persistent.ts +117 -0
- package/src/routing/search.ts +276 -0
- package/src/routing/spatial.ts +78 -0
- package/src/routing/types.ts +146 -0
- /package/dist/{public-types-B8rAALJq.d.mts → public-types-9NCPoqbp.d.mts} +0 -0
- /package/dist/{public-types-CVB0hXGk.d.cts → public-types-BcRZoJE3.d.cts} +0 -0
- /package/dist/{worker-client-DkwS4sHx.mjs → worker-client-BNjwT1AJ.mjs} +0 -0
- /package/dist/{worker-client-D3HMK33r.cjs → worker-client-D4oEvhAl.cjs} +0 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { crossesRect } from "../authoring/routing";
|
|
2
|
+
import { distance } from "./path";
|
|
3
|
+
import { inflate, union } from "./spatial";
|
|
4
|
+
import type { RouteBounds, RoutePoint } from "./types";
|
|
5
|
+
|
|
6
|
+
export interface SearchContext {
|
|
7
|
+
obstacles(bounds: RouteBounds): readonly RouteBounds[];
|
|
8
|
+
edgeCost?(a: RoutePoint, b: RoutePoint): number;
|
|
9
|
+
guides?(bounds: RouteBounds): readonly RouteBounds[];
|
|
10
|
+
readonly maxSearchNodes: number;
|
|
11
|
+
readonly bendPenalty: number;
|
|
12
|
+
visited: number;
|
|
13
|
+
budgetExceeded: boolean;
|
|
14
|
+
}
|
|
15
|
+
export const segmentBounds = (a: RoutePoint, b: RoutePoint): RouteBounds => ({
|
|
16
|
+
x: Math.min(a.x, b.x),
|
|
17
|
+
y: Math.min(a.y, b.y),
|
|
18
|
+
width: Math.abs(b.x - a.x),
|
|
19
|
+
height: Math.abs(b.y - a.y),
|
|
20
|
+
});
|
|
21
|
+
export function clear(a: RoutePoint, b: RoutePoint, context: SearchContext): boolean {
|
|
22
|
+
return !context.obstacles(segmentBounds(a, b)).some((r) => crossesRect(a, b, r));
|
|
23
|
+
}
|
|
24
|
+
export function simplify(points: readonly RoutePoint[]): RoutePoint[] {
|
|
25
|
+
const result: RoutePoint[] = [];
|
|
26
|
+
for (const p of points) {
|
|
27
|
+
if (result.length && distance(result.at(-1)!, p) < 1e-9) continue;
|
|
28
|
+
while (result.length > 1) {
|
|
29
|
+
const a = result.at(-2)!,
|
|
30
|
+
b = result.at(-1)!;
|
|
31
|
+
if (
|
|
32
|
+
Math.abs((b.x - a.x) * (p.y - b.y) - (b.y - a.y) * (p.x - b.x)) > 1e-8 ||
|
|
33
|
+
(b.x - a.x) * (p.x - b.x) + (b.y - a.y) * (p.y - b.y) < 0
|
|
34
|
+
)
|
|
35
|
+
break;
|
|
36
|
+
result.pop();
|
|
37
|
+
}
|
|
38
|
+
result.push(p);
|
|
39
|
+
}
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
class Queue<T> {
|
|
43
|
+
items: { value: T; priority: number }[] = [];
|
|
44
|
+
push(value: T, priority: number) {
|
|
45
|
+
const item = { value, priority };
|
|
46
|
+
let i = this.items.length;
|
|
47
|
+
this.items.push(item);
|
|
48
|
+
while (i > 0) {
|
|
49
|
+
const p = (i - 1) >> 1;
|
|
50
|
+
if (this.items[p]!.priority <= priority) break;
|
|
51
|
+
this.items[i] = this.items[p]!;
|
|
52
|
+
i = p;
|
|
53
|
+
}
|
|
54
|
+
this.items[i] = item;
|
|
55
|
+
}
|
|
56
|
+
pop(): T {
|
|
57
|
+
const first = this.items[0]!,
|
|
58
|
+
last = this.items.pop()!;
|
|
59
|
+
if (this.items.length) {
|
|
60
|
+
let i = 0;
|
|
61
|
+
while (i * 2 + 1 < this.items.length) {
|
|
62
|
+
let child = i * 2 + 1;
|
|
63
|
+
if (
|
|
64
|
+
child + 1 < this.items.length &&
|
|
65
|
+
this.items[child + 1]!.priority < this.items[child]!.priority
|
|
66
|
+
)
|
|
67
|
+
child++;
|
|
68
|
+
if (last.priority <= this.items[child]!.priority) break;
|
|
69
|
+
this.items[i] = this.items[child]!;
|
|
70
|
+
i = child;
|
|
71
|
+
}
|
|
72
|
+
this.items[i] = last;
|
|
73
|
+
}
|
|
74
|
+
return first.value;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/** Visibility graph + A*, with incoming direction in the state for bend costs. */
|
|
78
|
+
export function findPath(
|
|
79
|
+
start: RoutePoint,
|
|
80
|
+
end: RoutePoint,
|
|
81
|
+
style: "orthogonal" | "polyline" | "octilinear",
|
|
82
|
+
context: SearchContext,
|
|
83
|
+
terminals: { incoming?: RoutePoint; outgoing?: RoutePoint } = {},
|
|
84
|
+
): RoutePoint[] | undefined {
|
|
85
|
+
const turnCost = (a: RoutePoint | undefined, b: RoutePoint | undefined) => {
|
|
86
|
+
if (!a || !b || !Math.hypot(a.x, a.y) || !Math.hypot(b.x, b.y)) return 0;
|
|
87
|
+
const dot = a.x * b.x + a.y * b.y,
|
|
88
|
+
cross = a.x * b.y - a.y * b.x;
|
|
89
|
+
return dot < -1e-8 ? context.bendPenalty * 4 : Math.abs(cross) > 1e-8 ? context.bendPenalty : 0;
|
|
90
|
+
};
|
|
91
|
+
const delta = (a: RoutePoint, b: RoutePoint) => ({ x: b.x - a.x, y: b.y - a.y });
|
|
92
|
+
const attachmentCost = (points: RoutePoint[]) =>
|
|
93
|
+
turnCost(terminals.incoming, delta(points[0]!, points[1]!)) +
|
|
94
|
+
turnCost(delta(points.at(-2)!, points.at(-1)!), terminals.outgoing);
|
|
95
|
+
const aligned = start.x === end.x || start.y === end.y;
|
|
96
|
+
if (
|
|
97
|
+
(style === "polyline" ||
|
|
98
|
+
aligned ||
|
|
99
|
+
(style === "octilinear" && Math.abs(start.x - end.x) === Math.abs(start.y - end.y))) &&
|
|
100
|
+
clear(start, end, context) &&
|
|
101
|
+
attachmentCost([start, end]) === 0 &&
|
|
102
|
+
(context.edgeCost?.(start, end) ?? 0) === 0
|
|
103
|
+
)
|
|
104
|
+
return [start, end];
|
|
105
|
+
if (style === "orthogonal") {
|
|
106
|
+
const candidates = [
|
|
107
|
+
{ x: start.x, y: end.y },
|
|
108
|
+
{ x: end.x, y: start.y },
|
|
109
|
+
]
|
|
110
|
+
.filter((bend) => clear(start, bend, context) && clear(bend, end, context))
|
|
111
|
+
.map((bend) => simplify([start, bend, end]));
|
|
112
|
+
candidates.sort((a, b) => attachmentCost(a) - attachmentCost(b));
|
|
113
|
+
if (
|
|
114
|
+
candidates.length &&
|
|
115
|
+
attachmentCost(candidates[0]!) === 0 &&
|
|
116
|
+
candidates[0]!
|
|
117
|
+
.slice(1)
|
|
118
|
+
.every((p, i) => (context.edgeCost?.(candidates[0]![i]!, p) ?? 0) === 0)
|
|
119
|
+
)
|
|
120
|
+
return candidates[0];
|
|
121
|
+
}
|
|
122
|
+
for (let attempt = 0; attempt < 5 && context.visited < context.maxSearchNodes; attempt++) {
|
|
123
|
+
const area = inflate(segmentBounds(start, end), 32 * 2 ** attempt);
|
|
124
|
+
const guides = context.guides?.(area) ?? [];
|
|
125
|
+
const obstacles = context.obstacles(guides.reduce(union, area));
|
|
126
|
+
const corners = [...obstacles, ...guides].flatMap((r) => [
|
|
127
|
+
{ x: r.x, y: r.y },
|
|
128
|
+
{ x: r.x + r.width, y: r.y },
|
|
129
|
+
{ x: r.x, y: r.y + r.height },
|
|
130
|
+
{ x: r.x + r.width, y: r.y + r.height },
|
|
131
|
+
]);
|
|
132
|
+
let points: RoutePoint[], neighbors: (i: number) => number[];
|
|
133
|
+
if (style === "polyline") {
|
|
134
|
+
points = [start, end, ...corners];
|
|
135
|
+
neighbors = () => points.map((_, i) => i);
|
|
136
|
+
} else {
|
|
137
|
+
const xs = [
|
|
138
|
+
...new Set([start.x, end.x, area.x, area.x + area.width, ...corners.map((p) => p.x)]),
|
|
139
|
+
].sort((a, b) => a - b);
|
|
140
|
+
const ys = [
|
|
141
|
+
...new Set([start.y, end.y, area.y, area.y + area.height, ...corners.map((p) => p.y)]),
|
|
142
|
+
].sort((a, b) => a - b);
|
|
143
|
+
if (xs.length * ys.length > context.maxSearchNodes) {
|
|
144
|
+
context.budgetExceeded = true;
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
points = ys.flatMap((y) => xs.map((x) => ({ x, y })));
|
|
148
|
+
const width = xs.length;
|
|
149
|
+
if (style === "octilinear") {
|
|
150
|
+
// Add intersections between endpoint/corner diagonals and grid lines.
|
|
151
|
+
const extra: RoutePoint[] = [];
|
|
152
|
+
for (const p of [start, end, ...corners])
|
|
153
|
+
for (const x of xs)
|
|
154
|
+
for (const sign of [-1, 1]) {
|
|
155
|
+
const y = p.y + sign * (x - p.x);
|
|
156
|
+
if (y >= area.y && y <= area.y + area.height) extra.push({ x, y });
|
|
157
|
+
}
|
|
158
|
+
for (const p of [start, end, ...corners])
|
|
159
|
+
for (const y of ys)
|
|
160
|
+
for (const sign of [-1, 1]) {
|
|
161
|
+
const x = p.x + sign * (y - p.y);
|
|
162
|
+
if (x >= area.x && x <= area.x + area.width) extra.push({ x, y });
|
|
163
|
+
}
|
|
164
|
+
const unique = new Map(points.map((p) => [`${p.x}:${p.y}`, p]));
|
|
165
|
+
for (const p of extra) unique.set(`${p.x}:${p.y}`, p);
|
|
166
|
+
points = [...unique.values()];
|
|
167
|
+
if (points.length > context.maxSearchNodes) {
|
|
168
|
+
context.budgetExceeded = true;
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
const groups = new Map<string, number[]>();
|
|
172
|
+
const keys = (p: RoutePoint) => [
|
|
173
|
+
`x:${p.x}`,
|
|
174
|
+
`y:${p.y}`,
|
|
175
|
+
`a:${(p.x - p.y).toFixed(8)}`,
|
|
176
|
+
`b:${(p.x + p.y).toFixed(8)}`,
|
|
177
|
+
];
|
|
178
|
+
points.forEach((p, i) => {
|
|
179
|
+
for (const key of keys(p)) {
|
|
180
|
+
const list = groups.get(key) ?? [];
|
|
181
|
+
list.push(i);
|
|
182
|
+
groups.set(key, list);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
const adjacent = points.map(() => new Set<number>());
|
|
186
|
+
for (const ids of groups.values()) {
|
|
187
|
+
ids.sort((a, b) => points[a]!.x - points[b]!.x || points[a]!.y - points[b]!.y);
|
|
188
|
+
for (let i = 1; i < ids.length; i++) {
|
|
189
|
+
adjacent[ids[i - 1]!]!.add(ids[i]!);
|
|
190
|
+
adjacent[ids[i]!]!.add(ids[i - 1]!);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
neighbors = (i) => [...adjacent[i]!];
|
|
194
|
+
} else
|
|
195
|
+
neighbors = (i) =>
|
|
196
|
+
[
|
|
197
|
+
i % width ? i - 1 : -1,
|
|
198
|
+
i % width < width - 1 ? i + 1 : -1,
|
|
199
|
+
i >= width ? i - width : -1,
|
|
200
|
+
i + width < points.length ? i + width : -1,
|
|
201
|
+
].filter((v) => v >= 0);
|
|
202
|
+
}
|
|
203
|
+
if (points.length > context.maxSearchNodes) {
|
|
204
|
+
context.budgetExceeded = true;
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
const from = points.findIndex((p) => distance(p, start) < 1e-8),
|
|
208
|
+
to = points.findIndex((p) => distance(p, end) < 1e-8);
|
|
209
|
+
const queue = new Queue<{ index: number; previous: number; key: string; cost: number }>();
|
|
210
|
+
const costs = new Map<string, number>(),
|
|
211
|
+
parents = new Map<string, string>(),
|
|
212
|
+
vertices = new Map<string, number>(),
|
|
213
|
+
visibility = new Map<number, boolean>(),
|
|
214
|
+
edgeCosts = new Map<number, number>();
|
|
215
|
+
const visible = (a: number, b: number) => {
|
|
216
|
+
const key = a < b ? a * points.length + b : b * points.length + a;
|
|
217
|
+
let result = visibility.get(key);
|
|
218
|
+
if (result === undefined) {
|
|
219
|
+
result = clear(points[a]!, points[b]!, context);
|
|
220
|
+
visibility.set(key, result);
|
|
221
|
+
}
|
|
222
|
+
return result;
|
|
223
|
+
};
|
|
224
|
+
const first = `${from}:-1`;
|
|
225
|
+
costs.set(first, 0);
|
|
226
|
+
vertices.set(first, from);
|
|
227
|
+
queue.push({ index: from, previous: -1, key: first, cost: 0 }, distance(start, end));
|
|
228
|
+
while (queue.items.length && context.visited < context.maxSearchNodes) {
|
|
229
|
+
const current = queue.pop();
|
|
230
|
+
if (costs.get(current.key) !== current.cost) continue;
|
|
231
|
+
context.visited++;
|
|
232
|
+
if (current.index === to) {
|
|
233
|
+
const result: RoutePoint[] = [];
|
|
234
|
+
let key: string | undefined = current.key;
|
|
235
|
+
while (key !== undefined) {
|
|
236
|
+
result.push(points[vertices.get(key)!]!);
|
|
237
|
+
key = parents.get(key);
|
|
238
|
+
}
|
|
239
|
+
return simplify(result.reverse());
|
|
240
|
+
}
|
|
241
|
+
const a = points[current.index]!;
|
|
242
|
+
for (const next of neighbors(current.index)) {
|
|
243
|
+
if (next === current.index || next === current.previous) continue;
|
|
244
|
+
const b = points[next]!;
|
|
245
|
+
if (!visible(current.index, next)) continue;
|
|
246
|
+
const before =
|
|
247
|
+
current.previous < 0 ? terminals.incoming : delta(points[current.previous]!, a);
|
|
248
|
+
const step = delta(a, b);
|
|
249
|
+
const bend =
|
|
250
|
+
turnCost(before, step) + (next === to ? turnCost(step, terminals.outgoing) : 0);
|
|
251
|
+
const pair = Math.min(current.index, next) * points.length + Math.max(current.index, next);
|
|
252
|
+
let edgeCost = edgeCosts.get(pair);
|
|
253
|
+
if (edgeCost === undefined) {
|
|
254
|
+
edgeCost = context.edgeCost?.(a, b) ?? 0;
|
|
255
|
+
edgeCosts.set(pair, edgeCost);
|
|
256
|
+
}
|
|
257
|
+
const cost = current.cost + distance(a, b) + bend + edgeCost;
|
|
258
|
+
const key = `${next}:${current.index}`;
|
|
259
|
+
if (cost >= (costs.get(key) ?? Infinity)) continue;
|
|
260
|
+
costs.set(key, cost);
|
|
261
|
+
parents.set(key, current.key);
|
|
262
|
+
vertices.set(key, next);
|
|
263
|
+
queue.push({ index: next, previous: current.index, key, cost }, cost + distance(b, end));
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (context.visited >= context.maxSearchNodes) context.budgetExceeded = true;
|
|
268
|
+
return undefined;
|
|
269
|
+
}
|
|
270
|
+
export function pointsBounds(points: readonly RoutePoint[]): RouteBounds {
|
|
271
|
+
return points.slice(1).reduce<RouteBounds>((r, p) => union(r, { ...p, width: 0, height: 0 }), {
|
|
272
|
+
...points[0]!,
|
|
273
|
+
width: 0,
|
|
274
|
+
height: 0,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { PersistentMap } from "./persistent";
|
|
2
|
+
import type { RouteBounds } from "./types";
|
|
3
|
+
export const intersects = (a: RouteBounds, b: RouteBounds): boolean =>
|
|
4
|
+
a.x <= b.x + b.width && a.x + a.width >= b.x && a.y <= b.y + b.height && a.y + a.height >= b.y;
|
|
5
|
+
export const inflate = (r: RouteBounds, amount: number): RouteBounds => ({
|
|
6
|
+
x: r.x - amount,
|
|
7
|
+
y: r.y - amount,
|
|
8
|
+
width: r.width + amount * 2,
|
|
9
|
+
height: r.height + amount * 2,
|
|
10
|
+
});
|
|
11
|
+
export function union(a: RouteBounds, b: RouteBounds): RouteBounds {
|
|
12
|
+
const x = Math.min(a.x, b.x),
|
|
13
|
+
y = Math.min(a.y, b.y);
|
|
14
|
+
return {
|
|
15
|
+
x,
|
|
16
|
+
y,
|
|
17
|
+
width: Math.max(a.x + a.width, b.x + b.width) - x,
|
|
18
|
+
height: Math.max(a.y + a.height, b.y + b.height) - y,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function cells(r: RouteBounds): string[] | undefined {
|
|
22
|
+
const x1 = Math.floor(r.x / 128),
|
|
23
|
+
x2 = Math.floor((r.x + r.width) / 128),
|
|
24
|
+
y1 = Math.floor(r.y / 128),
|
|
25
|
+
y2 = Math.floor((r.y + r.height) / 128);
|
|
26
|
+
if ((x2 - x1 + 1) * (y2 - y1 + 1) > 4096) return undefined;
|
|
27
|
+
const result: string[] = [];
|
|
28
|
+
for (let x = 0; x <= x2 - x1; x++)
|
|
29
|
+
for (let y = 0; y <= y2 - y1; y++) result.push(`${x1 + x}:${y1 + y}`);
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
/** Persistent uniform-grid broad phase; oversized rectangles live in an overflow index. */
|
|
33
|
+
export class SpatialIndex {
|
|
34
|
+
constructor(
|
|
35
|
+
readonly bounds = new PersistentMap<RouteBounds>(),
|
|
36
|
+
private readonly buckets = new PersistentMap<PersistentMap<true>>(),
|
|
37
|
+
private readonly overflow = new PersistentMap<true>(),
|
|
38
|
+
) {
|
|
39
|
+
Object.freeze(this);
|
|
40
|
+
}
|
|
41
|
+
set(id: string, rect?: RouteBounds): SpatialIndex {
|
|
42
|
+
let { bounds, buckets, overflow } = this;
|
|
43
|
+
const previous = bounds.get(id);
|
|
44
|
+
if (previous) {
|
|
45
|
+
const keys = cells(previous);
|
|
46
|
+
if (!keys) overflow = overflow.delete(id);
|
|
47
|
+
else
|
|
48
|
+
for (const key of keys) {
|
|
49
|
+
const bucket = buckets.get(key)!.delete(id);
|
|
50
|
+
buckets = bucket.size ? buckets.set(key, bucket) : buckets.delete(key);
|
|
51
|
+
}
|
|
52
|
+
bounds = bounds.delete(id);
|
|
53
|
+
}
|
|
54
|
+
if (rect) {
|
|
55
|
+
const keys = cells(rect);
|
|
56
|
+
if (!keys) overflow = overflow.set(id, true);
|
|
57
|
+
else
|
|
58
|
+
for (const key of keys)
|
|
59
|
+
buckets = buckets.set(key, (buckets.get(key) ?? new PersistentMap<true>()).set(id, true));
|
|
60
|
+
bounds = bounds.set(id, rect);
|
|
61
|
+
}
|
|
62
|
+
return new SpatialIndex(bounds, buckets, overflow);
|
|
63
|
+
}
|
|
64
|
+
query(rect: RouteBounds): string[] {
|
|
65
|
+
const keys = cells(rect),
|
|
66
|
+
found = new Set<string>();
|
|
67
|
+
if (!keys) {
|
|
68
|
+
for (const [id, bounds] of this.bounds) if (intersects(rect, bounds)) found.add(id);
|
|
69
|
+
} else {
|
|
70
|
+
for (const key of keys)
|
|
71
|
+
for (const id of this.buckets.get(key)?.keys() ?? [])
|
|
72
|
+
if (intersects(rect, this.bounds.get(id)!)) found.add(id);
|
|
73
|
+
for (const id of this.overflow.keys())
|
|
74
|
+
if (intersects(rect, this.bounds.get(id)!)) found.add(id);
|
|
75
|
+
}
|
|
76
|
+
return [...found].sort();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import type { EntityRect, Graph, GraphDiff, Point } from "@statelyai/graph";
|
|
2
|
+
|
|
3
|
+
export type RoutePoint = Readonly<Point>;
|
|
4
|
+
export type RouteSegment =
|
|
5
|
+
| { readonly kind: "line"; readonly to: RoutePoint }
|
|
6
|
+
| { readonly kind: "quadratic"; readonly control: RoutePoint; readonly to: RoutePoint }
|
|
7
|
+
| {
|
|
8
|
+
readonly kind: "cubic";
|
|
9
|
+
readonly control1: RoutePoint;
|
|
10
|
+
readonly control2: RoutePoint;
|
|
11
|
+
readonly to: RoutePoint;
|
|
12
|
+
}
|
|
13
|
+
| {
|
|
14
|
+
readonly kind: "arc";
|
|
15
|
+
readonly rx: number;
|
|
16
|
+
readonly ry: number;
|
|
17
|
+
readonly rotation: number;
|
|
18
|
+
readonly largeArc: boolean;
|
|
19
|
+
readonly sweep: boolean;
|
|
20
|
+
readonly to: RoutePoint;
|
|
21
|
+
};
|
|
22
|
+
export interface RoutePath {
|
|
23
|
+
readonly start: RoutePoint;
|
|
24
|
+
readonly segments: readonly RouteSegment[];
|
|
25
|
+
}
|
|
26
|
+
export type RouteEndpoint =
|
|
27
|
+
| { readonly kind: "node"; readonly nodeId: string; readonly port?: string }
|
|
28
|
+
| { readonly kind: "label"; readonly edgeId: string }
|
|
29
|
+
| { readonly kind: "junction"; readonly id: string };
|
|
30
|
+
export interface RouteSection {
|
|
31
|
+
readonly id: string;
|
|
32
|
+
readonly from: RouteEndpoint;
|
|
33
|
+
readonly to: RouteEndpoint;
|
|
34
|
+
readonly path: RoutePath;
|
|
35
|
+
/** Same ID denotes a shared trunk; render it once. */
|
|
36
|
+
readonly sharedId?: string;
|
|
37
|
+
}
|
|
38
|
+
export type RoutingDiagnosticCode =
|
|
39
|
+
| "MISSING_GEOMETRY"
|
|
40
|
+
| "MISSING_PORT"
|
|
41
|
+
| "ROUTE_CONFLICT"
|
|
42
|
+
| "ROUTE_BLOCKED"
|
|
43
|
+
| "SEARCH_BUDGET"
|
|
44
|
+
| "CONSTRAINT_VIOLATION";
|
|
45
|
+
export interface RoutingDiagnostic {
|
|
46
|
+
readonly code: RoutingDiagnosticCode;
|
|
47
|
+
readonly edgeId: string;
|
|
48
|
+
readonly message: string;
|
|
49
|
+
}
|
|
50
|
+
export interface Route {
|
|
51
|
+
readonly edgeId: string;
|
|
52
|
+
readonly sections: readonly RouteSection[];
|
|
53
|
+
readonly status: "routed" | "fallback";
|
|
54
|
+
readonly diagnostics: readonly RoutingDiagnostic[];
|
|
55
|
+
}
|
|
56
|
+
export type RoutePatch =
|
|
57
|
+
| { readonly op: "set"; readonly edgeId: string; readonly route: Route }
|
|
58
|
+
| { readonly op: "delete"; readonly edgeId: string };
|
|
59
|
+
export type RouteStyle =
|
|
60
|
+
| "straight"
|
|
61
|
+
| "bezier"
|
|
62
|
+
| "orthogonal"
|
|
63
|
+
| "polyline"
|
|
64
|
+
| "octilinear"
|
|
65
|
+
| "curved"
|
|
66
|
+
| "organic"
|
|
67
|
+
| "parallel"
|
|
68
|
+
| "self-loop"
|
|
69
|
+
| "fan"
|
|
70
|
+
| "bus"
|
|
71
|
+
| "bundle";
|
|
72
|
+
export type RouteSide = "left" | "right" | "top" | "bottom";
|
|
73
|
+
export interface RouteAttachment {
|
|
74
|
+
/** Node-local attachment rectangle, independent of its outer obstacle bounds. */
|
|
75
|
+
readonly bounds: RouteBounds;
|
|
76
|
+
readonly facing?: "inward" | "outward";
|
|
77
|
+
}
|
|
78
|
+
export interface EdgeRoutingSettings {
|
|
79
|
+
readonly sourceAttachment?: RouteAttachment;
|
|
80
|
+
readonly targetAttachment?: RouteAttachment;
|
|
81
|
+
readonly sourceSide?: RouteSide;
|
|
82
|
+
readonly targetSide?: RouteSide;
|
|
83
|
+
readonly waypoints?: readonly RoutePoint[];
|
|
84
|
+
/** Explicit shared routing group. Native binary edges describe branches of a net. */
|
|
85
|
+
readonly group?: string;
|
|
86
|
+
}
|
|
87
|
+
export interface RoutingSettings {
|
|
88
|
+
/** Native compound geometry is parent-relative; adapters may supply resolved world geometry. */
|
|
89
|
+
readonly coordinateSpace?: "parent" | "world";
|
|
90
|
+
readonly clearance?: number;
|
|
91
|
+
readonly radius?: number;
|
|
92
|
+
readonly edgeSpacing?: number;
|
|
93
|
+
readonly bendPenalty?: number;
|
|
94
|
+
/** Soft cost per crossing; finite costs keep detours proportionate. Default 80. */
|
|
95
|
+
readonly crossingPenalty?: number;
|
|
96
|
+
/** Cost per unit of parallel overlap within edgeSpacing. Default 8. */
|
|
97
|
+
readonly overlapPenalty?: number;
|
|
98
|
+
readonly maxSearchNodes?: number;
|
|
99
|
+
readonly organicIterations?: number;
|
|
100
|
+
readonly edges?: Readonly<Record<string, EdgeRoutingSettings>>;
|
|
101
|
+
}
|
|
102
|
+
export interface RoutingMetrics {
|
|
103
|
+
readonly routedEdges: number;
|
|
104
|
+
readonly reusedEdges: number;
|
|
105
|
+
readonly affectedEdges: number;
|
|
106
|
+
readonly searchNodes: number;
|
|
107
|
+
readonly obstacleCandidates: number;
|
|
108
|
+
}
|
|
109
|
+
/** Immutable value. Retain it per canvas; the private state uses structural sharing. */
|
|
110
|
+
export interface RoutingSnapshot {
|
|
111
|
+
readonly strategy: string;
|
|
112
|
+
readonly revision: number;
|
|
113
|
+
readonly routes: ReadonlyMap<string, Route>;
|
|
114
|
+
readonly metrics: RoutingMetrics;
|
|
115
|
+
}
|
|
116
|
+
export interface RoutingUpdate {
|
|
117
|
+
readonly snapshot: RoutingSnapshot;
|
|
118
|
+
readonly patches: readonly RoutePatch[];
|
|
119
|
+
/** Compare identity before accepting an asynchronous result. */
|
|
120
|
+
readonly base: RoutingSnapshot;
|
|
121
|
+
}
|
|
122
|
+
export type RoutingGraph = Graph<unknown, unknown, unknown, unknown>;
|
|
123
|
+
export interface RoutingStrategy {
|
|
124
|
+
readonly id: string;
|
|
125
|
+
route(
|
|
126
|
+
graph: RoutingGraph,
|
|
127
|
+
settings?: RoutingSettings,
|
|
128
|
+
): RoutingSnapshot | Promise<RoutingSnapshot>;
|
|
129
|
+
update(
|
|
130
|
+
graph: RoutingGraph,
|
|
131
|
+
previous: RoutingSnapshot,
|
|
132
|
+
diff: GraphDiff<unknown, unknown>,
|
|
133
|
+
settings?: RoutingSettings,
|
|
134
|
+
): RoutingUpdate | Promise<RoutingUpdate>;
|
|
135
|
+
}
|
|
136
|
+
export interface NativeRoutingStrategy extends RoutingStrategy {
|
|
137
|
+
readonly id: RouteStyle;
|
|
138
|
+
route(graph: RoutingGraph, settings?: RoutingSettings): RoutingSnapshot;
|
|
139
|
+
update(
|
|
140
|
+
graph: RoutingGraph,
|
|
141
|
+
previous: RoutingSnapshot,
|
|
142
|
+
diff: GraphDiff<unknown, unknown>,
|
|
143
|
+
settings?: RoutingSettings,
|
|
144
|
+
): RoutingUpdate;
|
|
145
|
+
}
|
|
146
|
+
export type RouteBounds = Readonly<EntityRect>;
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|