@dskripchenko/flow 0.0.0-stage → 1.0.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.
@@ -0,0 +1,4121 @@
1
+ // src/model/types.ts
2
+ /**
3
+ * The graph model: plain JSON, versioned, independent of layout and of the
4
+ * monitoring state. A host stores it as is; the viewer and the editor read
5
+ * and write exactly this shape.
6
+ */
7
+ /** The model format this package reads and writes. */
8
+ var GRAPH_VERSION = 1;
9
+ /** `enabled` treats a missing flag as enabled. */
10
+ function isEnabled(el) {
11
+ return el.enabled !== false;
12
+ }
13
+ // src/model/parse.ts
14
+ /**
15
+ * Reading a model from untrusted JSON: shape checks, defaults, and the
16
+ * version gate. Semantic problems (unknown references, unreachable nodes)
17
+ * are left to `validateGraph`, which reports instead of throwing.
18
+ */
19
+ var GraphError = class extends Error {
20
+ pointer;
21
+ name = "GraphError";
22
+ constructor(message, pointer = "") {
23
+ super(pointer ? `${message} (at ${pointer})` : message);
24
+ this.pointer = pointer;
25
+ }
26
+ };
27
+ var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
28
+ function str(o, key, at, required = false) {
29
+ const v = o[key];
30
+ if (v === void 0 || v === null) {
31
+ if (required) throw new GraphError(`"${key}" is required`, `${at}/${key}`);
32
+ return;
33
+ }
34
+ if (typeof v === "number") return String(v);
35
+ if (typeof v !== "string") throw new GraphError(`"${key}" must be a string`, `${at}/${key}`);
36
+ return v;
37
+ }
38
+ function bool(o, key, at) {
39
+ const v = o[key];
40
+ if (v === void 0 || v === null) return void 0;
41
+ if (typeof v !== "boolean") throw new GraphError(`"${key}" must be true or false`, `${at}/${key}`);
42
+ return v;
43
+ }
44
+ function point(v, at) {
45
+ if (v === void 0 || v === null) return void 0;
46
+ if (!isObj(v) || typeof v.x !== "number" || typeof v.y !== "number" || !Number.isFinite(v.x) || !Number.isFinite(v.y)) throw new GraphError("\"position\" must be { x: number, y: number }", at);
47
+ return {
48
+ x: v.x,
49
+ y: v.y
50
+ };
51
+ }
52
+ var SIDES = /* @__PURE__ */ new Set([
53
+ "top",
54
+ "right",
55
+ "bottom",
56
+ "left"
57
+ ]);
58
+ function ports(v, at) {
59
+ if (v === void 0 || v === null) return void 0;
60
+ if (!Array.isArray(v)) throw new GraphError("\"ports\" must be an array", at);
61
+ return v.map((p, i) => {
62
+ const here = `${at}/${i}`;
63
+ if (!isObj(p)) throw new GraphError("A port must be an object", here);
64
+ const side = str(p, "side", here, true);
65
+ if (!SIDES.has(side)) throw new GraphError(`"side" must be one of top, right, bottom, left`, `${here}/side`);
66
+ const port = {
67
+ id: str(p, "id", here, true),
68
+ side
69
+ };
70
+ if (typeof p.offset === "number") port.offset = Math.min(1, Math.max(0, p.offset));
71
+ if (p.kind === "in" || p.kind === "out" || p.kind === "both") port.kind = p.kind;
72
+ const label = str(p, "label", here);
73
+ if (label !== void 0) port.label = label;
74
+ return port;
75
+ });
76
+ }
77
+ function data(v, at) {
78
+ if (v === void 0 || v === null) return void 0;
79
+ if (!isObj(v)) throw new GraphError("\"data\" must be an object", at);
80
+ return v;
81
+ }
82
+ function node(v, at) {
83
+ if (!isObj(v)) throw new GraphError("A node must be an object", at);
84
+ const n = {
85
+ id: str(v, "id", at, true),
86
+ type: str(v, "type", at) ?? "state"
87
+ };
88
+ const label = str(v, "label", at);
89
+ if (label !== void 0) n.label = label;
90
+ const description = str(v, "description", at);
91
+ if (description !== void 0) n.description = description;
92
+ const position = point(v.position, `${at}/position`);
93
+ if (position) n.position = position;
94
+ const group = str(v, "group", at);
95
+ if (group !== void 0) n.group = group;
96
+ const d = data(v.data, `${at}/data`);
97
+ if (d) n.data = d;
98
+ const enabled = bool(v, "enabled", at);
99
+ if (enabled !== void 0) n.enabled = enabled;
100
+ if (v.subgraph !== void 0 && v.subgraph !== null) n.subgraph = typeof v.subgraph === "string" ? v.subgraph : parseGraph(v.subgraph, `${at}/subgraph`);
101
+ const p = ports(v.ports, `${at}/ports`);
102
+ if (p) n.ports = p;
103
+ return n;
104
+ }
105
+ function edge(v, at) {
106
+ if (!isObj(v)) throw new GraphError("An edge must be an object", at);
107
+ const e = {
108
+ id: str(v, "id", at, true),
109
+ from: str(v, "from", at, true),
110
+ to: str(v, "to", at, true)
111
+ };
112
+ for (const key of [
113
+ "fromPort",
114
+ "toPort",
115
+ "label",
116
+ "condition"
117
+ ]) {
118
+ const s = str(v, key, at);
119
+ if (s !== void 0) e[key] = s;
120
+ }
121
+ const enabled = bool(v, "enabled", at);
122
+ if (enabled !== void 0) e.enabled = enabled;
123
+ if (v.kind === "association" || v.kind === "transition") e.kind = v.kind;
124
+ const d = data(v.data, `${at}/data`);
125
+ if (d) e.data = d;
126
+ return e;
127
+ }
128
+ function group(v, at) {
129
+ if (!isObj(v)) throw new GraphError("A group must be an object", at);
130
+ const g = { id: str(v, "id", at, true) };
131
+ const label = str(v, "label", at);
132
+ if (label !== void 0) g.label = label;
133
+ const parent = str(v, "parent", at);
134
+ if (parent !== void 0) g.parent = parent;
135
+ return g;
136
+ }
137
+ /**
138
+ * Reads a model from parsed JSON (or a JSON string). Fills defaults, drops
139
+ * unknown fields of nodes and edges, and throws `GraphError` with a JSON
140
+ * pointer when the shape is wrong.
141
+ */
142
+ function parseGraph(input, at = "") {
143
+ let v = input;
144
+ if (typeof v === "string") try {
145
+ v = JSON.parse(v);
146
+ } catch (e) {
147
+ throw new GraphError(`Not valid JSON: ${e.message}`, at);
148
+ }
149
+ if (!isObj(v)) throw new GraphError("A graph must be an object", at);
150
+ const version = v.version ?? 1;
151
+ if (version !== 1) throw new GraphError(`Unsupported model version ${String(version)}; this package reads version 1`, `${at}/version`);
152
+ if (!Array.isArray(v.nodes)) throw new GraphError("\"nodes\" must be an array", `${at}/nodes`);
153
+ const edges = v.edges ?? [];
154
+ if (!Array.isArray(edges)) throw new GraphError("\"edges\" must be an array", `${at}/edges`);
155
+ const g = {
156
+ version: 1,
157
+ nodes: v.nodes.map((n, i) => node(n, `${at}/nodes/${i}`)),
158
+ edges: edges.map((e, i) => edge(e, `${at}/edges/${i}`))
159
+ };
160
+ if (v.groups !== void 0 && v.groups !== null) {
161
+ if (!Array.isArray(v.groups)) throw new GraphError("\"groups\" must be an array", `${at}/groups`);
162
+ g.groups = v.groups.map((x, i) => group(x, `${at}/groups/${i}`));
163
+ }
164
+ if (isObj(v.meta)) {
165
+ const meta = { ...v.meta };
166
+ if (meta.direction !== void 0 && meta.direction !== "TB" && meta.direction !== "LR") delete meta.direction;
167
+ g.meta = meta;
168
+ }
169
+ return g;
170
+ }
171
+ /** The model as stable JSON text (2-space indent). */
172
+ function stringifyGraph(graph) {
173
+ return JSON.stringify(graph, null, 2);
174
+ }
175
+ /** An empty model. */
176
+ function emptyGraph(meta) {
177
+ return meta ? {
178
+ version: 1,
179
+ nodes: [],
180
+ edges: [],
181
+ meta
182
+ } : {
183
+ version: 1,
184
+ nodes: [],
185
+ edges: []
186
+ };
187
+ }
188
+ // src/model/registry.ts
189
+ var BUILTIN_TYPES = Object.freeze({
190
+ start: {
191
+ title: "Start",
192
+ shape: "circle",
193
+ size: {
194
+ width: 40,
195
+ height: 40
196
+ },
197
+ icon: "play",
198
+ labelOutside: true,
199
+ noIncoming: true
200
+ },
201
+ end: {
202
+ title: "End",
203
+ shape: "ring",
204
+ size: {
205
+ width: 40,
206
+ height: 40
207
+ },
208
+ icon: "stop",
209
+ labelOutside: true,
210
+ noOutgoing: true
211
+ },
212
+ state: {
213
+ title: "State",
214
+ shape: "rounded",
215
+ minWidth: 160,
216
+ maxWidth: 240,
217
+ height: 44
218
+ },
219
+ task: {
220
+ title: "Task",
221
+ shape: "rect",
222
+ minWidth: 176,
223
+ maxWidth: 260,
224
+ height: 60,
225
+ icon: "task"
226
+ },
227
+ decision: {
228
+ title: "Decision",
229
+ shape: "diamond",
230
+ minWidth: 124,
231
+ maxWidth: 200,
232
+ height: 80,
233
+ icon: "decision"
234
+ },
235
+ subprocess: {
236
+ title: "Sub-process",
237
+ shape: "rect",
238
+ minWidth: 192,
239
+ maxWidth: 260,
240
+ height: 64,
241
+ icon: "subprocess",
242
+ drillable: true
243
+ },
244
+ note: {
245
+ title: "Note",
246
+ shape: "note",
247
+ minWidth: 140,
248
+ maxWidth: 240,
249
+ height: 48,
250
+ annotation: true
251
+ }
252
+ });
253
+ /** A registry with the built-in types plus `custom` (which may override them). */
254
+ function createRegistry(custom = {}) {
255
+ return new Map(Object.entries({
256
+ ...BUILTIN_TYPES,
257
+ ...custom
258
+ }));
259
+ }
260
+ var defaultRegistry = createRegistry();
261
+ /** The definition for a node's type; an unknown type is drawn as a state. */
262
+ function typeOf(registry, type) {
263
+ return registry.get(type) ?? registry.get("state") ?? BUILTIN_TYPES.state;
264
+ }
265
+ /** The ports a node offers: its own, else its type's. */
266
+ function portsOf(registry, node) {
267
+ return node.ports ?? typeOf(registry, node.type).ports ?? [];
268
+ }
269
+ /** Whether the editor must leave a node alone (its type is locked). */
270
+ function isLocked(registry, node) {
271
+ return typeOf(registry, node.type).locked === true;
272
+ }
273
+ // src/model/validate.ts
274
+ /**
275
+ * Semantic checks of a model. Cycles are legal (state machines loop); what
276
+ * is reported is what makes a process wrong (errors) or suspicious
277
+ * (warnings). The viewer renders a graph with errors as far as it can; the
278
+ * editor lists the issues and highlights the elements.
279
+ */
280
+ /** All issues of a graph, errors first, then in model order. */
281
+ function validateGraph(graph, options = {}) {
282
+ const registry = options.registry ?? defaultRegistry;
283
+ const issues = [];
284
+ const add = (severity, code, message, kind, id) => issues.push(kind && id !== void 0 ? {
285
+ severity,
286
+ code,
287
+ message,
288
+ target: {
289
+ kind,
290
+ id
291
+ }
292
+ } : {
293
+ severity,
294
+ code,
295
+ message
296
+ });
297
+ const seen = /* @__PURE__ */ new Map();
298
+ const claim = (kind, id) => {
299
+ if (seen.has(id)) add("error", "duplicate-id", `The id "${id}" is used more than once`, kind, id);
300
+ else seen.set(id, kind);
301
+ };
302
+ const nodes = new Map(graph.nodes.map((n) => [n.id, n]));
303
+ const groups = new Map((graph.groups ?? []).map((g) => [g.id, g]));
304
+ for (const n of graph.nodes) claim("node", n.id);
305
+ for (const e of graph.edges) claim("edge", e.id);
306
+ for (const g of graph.groups ?? []) claim("group", g.id);
307
+ for (const g of graph.groups ?? []) {
308
+ if (g.parent !== void 0 && !groups.has(g.parent)) add("error", "unknown-group", `Group "${g.id}" is inside an unknown group "${g.parent}"`, "group", g.id);
309
+ let p = g.parent;
310
+ let steps = 0;
311
+ while (p !== void 0 && groups.has(p)) {
312
+ if (p === g.id || ++steps > groups.size) {
313
+ add("error", "group-cycle", `Group "${g.id}" contains itself`, "group", g.id);
314
+ break;
315
+ }
316
+ p = groups.get(p).parent;
317
+ }
318
+ }
319
+ for (const n of graph.nodes) {
320
+ if (!registry.has(n.type)) add("warning", "unknown-type", `Node "${n.id}" has an unknown type "${n.type}"; it is drawn as a state`, "node", n.id);
321
+ if (n.group !== void 0 && !groups.has(n.group)) add("error", "unknown-group", `Node "${n.id}" is in an unknown group "${n.group}"`, "node", n.id);
322
+ const def = typeOf(registry, n.type);
323
+ if (!def.labelOutside && !def.annotation && !(n.label ?? "").trim() && n.type !== "decision") add("warning", "empty-label", `Node "${n.id}" has no label`, "node", n.id);
324
+ }
325
+ const live = graph.edges.filter((e) => e.kind !== "association");
326
+ for (const e of graph.edges) for (const end of ["from", "to"]) {
327
+ const id = e[end];
328
+ const n = nodes.get(id);
329
+ if (!n) {
330
+ add("error", "unknown-node", `Edge "${e.id}" ${end === "from" ? "starts at" : "ends at"} an unknown node "${id}"`, "edge", e.id);
331
+ continue;
332
+ }
333
+ const portId = end === "from" ? e.fromPort : e.toPort;
334
+ if (portId !== void 0) {
335
+ const port = portsOf(registry, n).find((p) => p.id === portId);
336
+ if (!port) add("error", "unknown-port", `Edge "${e.id}" uses an unknown port "${portId}" of "${id}"`, "edge", e.id);
337
+ else if (end === "from" && port.kind === "in" || end === "to" && port.kind === "out") add("error", "unknown-port", `Port "${portId}" of "${id}" does not take ${end === "from" ? "outgoing" : "incoming"} edges`, "edge", e.id);
338
+ }
339
+ }
340
+ const flowNodes = graph.nodes.filter((n) => !typeOf(registry, n.type).annotation);
341
+ const starts = flowNodes.filter((n) => typeOf(registry, n.type).noIncoming);
342
+ const ends = flowNodes.filter((n) => typeOf(registry, n.type).noOutgoing);
343
+ if ((options.requireStartEnd ?? true) && flowNodes.length > 0) {
344
+ if (starts.length === 0) add("warning", "missing-start", "The process has no start node");
345
+ if (ends.length === 0) add("warning", "missing-end", "The process has no end node");
346
+ }
347
+ for (const e of live) {
348
+ const to = nodes.get(e.to);
349
+ const from = nodes.get(e.from);
350
+ if (to && typeOf(registry, to.type).noIncoming) add("warning", "start-incoming", `Edge "${e.id}" leads into the start node "${to.id}"`, "edge", e.id);
351
+ if (from && typeOf(registry, from.type).noOutgoing) add("warning", "end-outgoing", `Edge "${e.id}" leaves the end node "${from.id}"`, "edge", e.id);
352
+ }
353
+ if (starts.length > 0) {
354
+ const out = /* @__PURE__ */ new Map();
355
+ for (const e of live) {
356
+ if (!nodes.has(e.from) || !nodes.has(e.to)) continue;
357
+ const list = out.get(e.from) ?? [];
358
+ list.push(e.to);
359
+ out.set(e.from, list);
360
+ }
361
+ const reached = /* @__PURE__ */ new Set();
362
+ const queue = starts.map((s) => s.id);
363
+ for (const id of queue) reached.add(id);
364
+ while (queue.length) {
365
+ const id = queue.shift();
366
+ for (const next of out.get(id) ?? []) if (!reached.has(next)) {
367
+ reached.add(next);
368
+ queue.push(next);
369
+ }
370
+ }
371
+ for (const n of flowNodes) if (!reached.has(n.id)) add("warning", "unreachable", `Node "${n.id}" cannot be reached from the start`, "node", n.id);
372
+ }
373
+ const hasOut = new Set(live.filter((e) => nodes.has(e.to)).map((e) => e.from));
374
+ for (const n of flowNodes) {
375
+ if (typeOf(registry, n.type).noOutgoing) continue;
376
+ if (!hasOut.has(n.id)) add("warning", "dead-end", `Node "${n.id}" has no outgoing transition`, "node", n.id);
377
+ }
378
+ return issues.sort((a, b) => a.severity === b.severity ? 0 : a.severity === "error" ? -1 : 1);
379
+ }
380
+ /** Issues grouped by element id, for highlighting. */
381
+ function issuesByElement(issues) {
382
+ const map = /* @__PURE__ */ new Map();
383
+ for (const i of issues) {
384
+ if (!i.target) continue;
385
+ const list = map.get(i.target.id) ?? [];
386
+ list.push(i);
387
+ map.set(i.target.id, list);
388
+ }
389
+ return map;
390
+ }
391
+ // src/layout/layered/errors.ts
392
+ /**
393
+ * Errors of the layered layout. Copied from @dskripchenko/diagrams
394
+ * (src/core/errors.ts) together with the layout; the name stays so that a
395
+ * later switch to the diagrams export changes nothing for callers.
396
+ */
397
+ var DiagramError = class extends Error {
398
+ line;
399
+ column;
400
+ hint;
401
+ name = "DiagramError";
402
+ constructor(message, line = 0, column = 0, hint) {
403
+ super(line > 0 ? `${message} (line ${line}${column > 0 ? `, column ${column}` : ""})` : message);
404
+ this.line = line;
405
+ this.column = column;
406
+ this.hint = hint;
407
+ }
408
+ };
409
+ /** Raised when a graph is too large to lay out in predictable time. */
410
+ function tooLarge(what, count, limit) {
411
+ return new DiagramError(`The graph has ${count} ${what}; the limit is ${limit}.`, 0, 0, "Split it into sub-processes.");
412
+ }
413
+ // src/layout/layered/graph.ts
414
+ var Graph = class {
415
+ nodes = /* @__PURE__ */ new Map();
416
+ edges = [];
417
+ outMap = /* @__PURE__ */ new Map();
418
+ inMap = /* @__PURE__ */ new Map();
419
+ addNode(node) {
420
+ const full = {
421
+ ...node,
422
+ rank: 0,
423
+ order: 0,
424
+ x: 0,
425
+ y: 0
426
+ };
427
+ this.nodes.set(full.id, full);
428
+ this.outMap.set(full.id, []);
429
+ this.inMap.set(full.id, []);
430
+ return full;
431
+ }
432
+ node(id) {
433
+ const n = this.nodes.get(id);
434
+ if (!n) throw new Error(`Unknown node ${id}`);
435
+ return n;
436
+ }
437
+ addEdge(edge) {
438
+ this.edges.push(edge);
439
+ this.outMap.get(edge.v).push(edge);
440
+ this.inMap.get(edge.w).push(edge);
441
+ return edge;
442
+ }
443
+ removeEdge(edge) {
444
+ const remove = (list) => {
445
+ const i = list.indexOf(edge);
446
+ if (i >= 0) list.splice(i, 1);
447
+ };
448
+ remove(this.edges);
449
+ remove(this.outMap.get(edge.v));
450
+ remove(this.inMap.get(edge.w));
451
+ }
452
+ out(id) {
453
+ return this.outMap.get(id) ?? [];
454
+ }
455
+ in(id) {
456
+ return this.inMap.get(id) ?? [];
457
+ }
458
+ /** Re-indexes after edges were mutated in place (reversed). */
459
+ reindex() {
460
+ for (const id of this.nodes.keys()) {
461
+ this.outMap.set(id, []);
462
+ this.inMap.set(id, []);
463
+ }
464
+ for (const e of this.edges) {
465
+ this.outMap.get(e.v).push(e);
466
+ this.inMap.get(e.w).push(e);
467
+ }
468
+ }
469
+ };
470
+ // src/layout/layered/order.ts
471
+ /** The clusters containing a node, outermost first. */
472
+ function chain(cluster, parents) {
473
+ const out = [];
474
+ let c = cluster;
475
+ while (c) {
476
+ out.unshift(c);
477
+ c = parents.get(c) ?? null;
478
+ }
479
+ return out;
480
+ }
481
+ function buildLayers(g) {
482
+ const layers = [];
483
+ for (const n of g.nodes.values()) (layers[n.rank] ??= []).push(n);
484
+ for (let i = 0; i < layers.length; i++) layers[i] ??= [];
485
+ return layers;
486
+ }
487
+ /** Initial order: depth-first from the top, so connected nodes start close. */
488
+ function initialOrder(g, layers) {
489
+ const visited = /* @__PURE__ */ new Set();
490
+ const next = layers.map(() => 0);
491
+ const ordered = [...g.nodes.values()].sort((a, b) => a.rank - b.rank);
492
+ for (const start of ordered) {
493
+ if (visited.has(start.id)) continue;
494
+ const stack = [start];
495
+ while (stack.length > 0) {
496
+ const n = stack.pop();
497
+ if (visited.has(n.id)) continue;
498
+ visited.add(n.id);
499
+ n.order = next[n.rank]++;
500
+ const succ = g.out(n.id).map((e) => g.node(e.w));
501
+ for (let i = succ.length - 1; i >= 0; i--) if (!visited.has(succ[i].id)) stack.push(succ[i]);
502
+ }
503
+ }
504
+ for (const layer of layers) layer.sort((a, b) => a.order - b.order);
505
+ }
506
+ /**
507
+ * Sorts one layer by `key`, keeping clusters contiguous and sibling clusters
508
+ * in their canonical order. Ties keep their current order, or are reversed
509
+ * with `biasRight`.
510
+ */
511
+ function sortLayer(layer, key, parents, canonical, biasRight = false) {
512
+ const root = {
513
+ cluster: null,
514
+ nodes: [],
515
+ children: /* @__PURE__ */ new Map(),
516
+ items: []
517
+ };
518
+ for (const n of layer) {
519
+ let group = root;
520
+ for (const c of chain(n.cluster, parents)) {
521
+ let child = group.children.get(c);
522
+ if (!child) {
523
+ child = {
524
+ cluster: c,
525
+ nodes: [],
526
+ children: /* @__PURE__ */ new Map(),
527
+ items: []
528
+ };
529
+ group.children.set(c, child);
530
+ group.items.push(c);
531
+ }
532
+ group = child;
533
+ }
534
+ group.nodes.push(n);
535
+ group.items.push(n);
536
+ }
537
+ const groupKey = /* @__PURE__ */ new Map();
538
+ const keyOf = (group) => {
539
+ const cached = groupKey.get(group);
540
+ if (cached !== void 0) return cached;
541
+ let sum = 0;
542
+ let count = 0;
543
+ const walk = (g) => {
544
+ for (const n of g.nodes) {
545
+ sum += key.get(n.id);
546
+ count++;
547
+ }
548
+ for (const c of g.children.values()) walk(c);
549
+ };
550
+ walk(group);
551
+ const value = count > 0 ? sum / count : 0;
552
+ groupKey.set(group, value);
553
+ return value;
554
+ };
555
+ const flatten = (group) => {
556
+ const indexed = group.items.map((item, index) => ({
557
+ item,
558
+ index,
559
+ k: typeof item === "string" ? keyOf(group.children.get(item)) : key.get(item.id)
560
+ }));
561
+ indexed.sort((a, b) => a.k - b.k || (biasRight ? b.index - a.index : a.index - b.index));
562
+ const slots = indexed.flatMap((entry, i) => typeof entry.item === "string" ? [i] : []);
563
+ const clusters = slots.map((i) => indexed[i].item).sort((a, b) => (canonical.get(a) ?? 0) - (canonical.get(b) ?? 0));
564
+ slots.forEach((slot, i) => {
565
+ indexed[slot].item = clusters[i];
566
+ });
567
+ const out = [];
568
+ for (const entry of indexed) if (typeof entry.item === "string") out.push(...flatten(group.children.get(entry.item)));
569
+ else out.push(entry.item);
570
+ return out;
571
+ };
572
+ return flatten(root);
573
+ }
574
+ /** Crossings between two adjacent layers (Barth, Jünger and Mutzel's accumulator tree). */
575
+ function crossingsBetween(g, upper, lower) {
576
+ const pos = new Map(lower.map((n, i) => [n.id, i]));
577
+ const targets = [];
578
+ upper.forEach((n, ui) => {
579
+ for (const e of g.out(n.id)) {
580
+ const l = pos.get(e.w);
581
+ if (l !== void 0) targets.push({
582
+ u: ui,
583
+ l,
584
+ w: e.weight || 1
585
+ });
586
+ }
587
+ });
588
+ targets.sort((a, b) => a.u - b.u || a.l - b.l);
589
+ let first = 1;
590
+ while (first < lower.length) first <<= 1;
591
+ const tree = new Array(2 * first - 1).fill(0);
592
+ first -= 1;
593
+ let crossings = 0;
594
+ for (const t of targets) {
595
+ let index = t.l + first;
596
+ tree[index] += t.w;
597
+ let weightSum = 0;
598
+ while (index > 0) {
599
+ if (index % 2) weightSum += tree[index + 1];
600
+ index = index - 1 >> 1;
601
+ tree[index] += t.w;
602
+ }
603
+ crossings += t.w * weightSum;
604
+ }
605
+ return crossings;
606
+ }
607
+ /**
608
+ * Edges that pass through a cluster they do not belong to: from the left of
609
+ * its block in one rank to the right of it in the next, or back. Counted as
610
+ * crossings, since that is what the reader sees — a line across a frame.
611
+ */
612
+ function frameCrossingsBetween(g, upper, lower, parents) {
613
+ const ranges = (layer) => {
614
+ const out = /* @__PURE__ */ new Map();
615
+ layer.forEach((n, i) => {
616
+ for (const c of chain(n.cluster, parents)) {
617
+ const r = out.get(c);
618
+ if (r) r.to = i;
619
+ else out.set(c, {
620
+ from: i,
621
+ to: i
622
+ });
623
+ }
624
+ });
625
+ return out;
626
+ };
627
+ const up = ranges(upper);
628
+ const down = ranges(lower);
629
+ if (up.size === 0 || down.size === 0) return 0;
630
+ const pos = new Map(lower.map((n, i) => [n.id, i]));
631
+ let count = 0;
632
+ upper.forEach((n, ui) => {
633
+ for (const e of g.out(n.id)) {
634
+ const li = pos.get(e.w);
635
+ if (li === void 0) continue;
636
+ for (const [c, a] of up) {
637
+ const b = down.get(c);
638
+ if (!b) continue;
639
+ if (ui < a.from && li > b.to || ui > a.to && li < b.from) count += 2;
640
+ }
641
+ }
642
+ });
643
+ return count;
644
+ }
645
+ function totalCrossings(g, layers, parents = /* @__PURE__ */ new Map()) {
646
+ let total = 0;
647
+ for (let i = 0; i + 1 < layers.length; i++) {
648
+ total += crossingsBetween(g, layers[i], layers[i + 1]);
649
+ if (parents.size > 0) total += frameCrossingsBetween(g, layers[i], layers[i + 1], parents);
650
+ }
651
+ return total;
652
+ }
653
+ function canonicalClusterOrder(layers, parents) {
654
+ const sum = /* @__PURE__ */ new Map();
655
+ const count = /* @__PURE__ */ new Map();
656
+ for (const layer of layers) layer.forEach((n, i) => {
657
+ const relative = layer.length > 1 ? i / (layer.length - 1) : .5;
658
+ for (const c of chain(n.cluster, parents)) {
659
+ sum.set(c, (sum.get(c) ?? 0) + relative);
660
+ count.set(c, (count.get(c) ?? 0) + 1);
661
+ }
662
+ });
663
+ const out = /* @__PURE__ */ new Map();
664
+ for (const [c, s] of sum) out.set(c, s / count.get(c));
665
+ return out;
666
+ }
667
+ /**
668
+ * Transpose: swaps neighbours in a layer while that removes crossings with
669
+ * the layers above and below. The barycenter sweeps settle in local minima
670
+ * a single swap gets out of — typically a node with one edge that ties with
671
+ * a node pulled two ways. Only nodes in the same cluster are swapped, so
672
+ * clusters stay contiguous.
673
+ */
674
+ function transpose(g, layers) {
675
+ const pairs = (us, vs) => {
676
+ let c = 0;
677
+ for (const a of us) for (const b of vs) if (a.order > b.order) c += a.w * b.w;
678
+ return c;
679
+ };
680
+ const ends = (n) => ({
681
+ up: g.in(n.id).map((e) => ({
682
+ order: g.node(e.v).order,
683
+ w: e.weight || 1
684
+ })),
685
+ down: g.out(n.id).map((e) => ({
686
+ order: g.node(e.w).order,
687
+ w: e.weight || 1
688
+ }))
689
+ });
690
+ const cost = (u, v) => pairs(u.up, v.up) + pairs(u.down, v.down);
691
+ for (let round = 0; round < 8; round++) {
692
+ let improved = false;
693
+ for (const layer of layers) {
694
+ if (layer.length < 2) continue;
695
+ for (let j = 0; j + 1 < layer.length; j++) {
696
+ const a = layer[j];
697
+ const b = layer[j + 1];
698
+ if (a.cluster !== b.cluster) continue;
699
+ const ea = ends(a);
700
+ const eb = ends(b);
701
+ if (cost(eb, ea) < cost(ea, eb)) {
702
+ layer[j] = b;
703
+ layer[j + 1] = a;
704
+ improved = true;
705
+ }
706
+ }
707
+ layer.forEach((n, k) => n.order = k);
708
+ }
709
+ if (!improved) break;
710
+ }
711
+ }
712
+ /**
713
+ * Orders every layer in place and sets `node.order`.
714
+ * Returns the layers, each in its final order. With `refine`, every sweep
715
+ * is followed by a transpose pass.
716
+ */
717
+ function order(g, parents, iterations = 24, refine = false) {
718
+ const layers = buildLayers(g);
719
+ initialOrder(g, layers);
720
+ let canonical = canonicalClusterOrder(layers, parents);
721
+ for (let i = 0; i < layers.length; i++) {
722
+ const key = new Map(layers[i].map((n, j) => [n.id, j]));
723
+ layers[i] = sortLayer(layers[i], key, parents, canonical);
724
+ }
725
+ const assign = (ls) => ls.forEach((layer) => layer.forEach((n, i) => n.order = i));
726
+ assign(layers);
727
+ if (refine) transpose(g, layers);
728
+ let best = layers.map((l) => l.slice());
729
+ let bestCrossings = totalCrossings(g, layers, parents);
730
+ let sinceImprovement = 0;
731
+ for (let it = 0; it < iterations && bestCrossings > 0 && sinceImprovement < 4; it++) {
732
+ const down = it % 2 === 0;
733
+ const biasRight = refine && it % 4 >= 2;
734
+ const range = down ? Array.from({ length: layers.length - 1 }, (_, i) => i + 1) : Array.from({ length: layers.length - 1 }, (_, i) => layers.length - 2 - i);
735
+ const fixed = down ? 0 : layers.length - 1;
736
+ const identity = new Map(layers[fixed].map((n, j) => [n.id, j]));
737
+ layers[fixed] = sortLayer(layers[fixed], identity, parents, canonical);
738
+ layers[fixed].forEach((n, j) => n.order = j);
739
+ for (const i of range) {
740
+ const layer = layers[i];
741
+ const key = /* @__PURE__ */ new Map();
742
+ for (const n of layer) {
743
+ const neighbours = down ? g.in(n.id).map((e) => ({
744
+ order: g.node(e.v).order,
745
+ w: e.weight || 1
746
+ })) : g.out(n.id).map((e) => ({
747
+ order: g.node(e.w).order,
748
+ w: e.weight || 1
749
+ }));
750
+ if (neighbours.length === 0) key.set(n.id, n.order);
751
+ else {
752
+ let s = 0;
753
+ let ws = 0;
754
+ for (const nb of neighbours) {
755
+ s += nb.order * nb.w;
756
+ ws += nb.w;
757
+ }
758
+ key.set(n.id, s / ws);
759
+ }
760
+ }
761
+ layers[i] = sortLayer(layer, key, parents, canonical, biasRight);
762
+ layers[i].forEach((n, j) => n.order = j);
763
+ }
764
+ if (refine) transpose(g, layers);
765
+ canonical = canonicalClusterOrder(layers, parents);
766
+ const crossings = totalCrossings(g, layers, parents);
767
+ if (crossings < bestCrossings) {
768
+ bestCrossings = crossings;
769
+ best = layers.map((l) => l.slice());
770
+ sinceImprovement = 0;
771
+ } else sinceImprovement++;
772
+ }
773
+ assign(best);
774
+ return best;
775
+ }
776
+ // src/layout/layered/position.ts
777
+ function isDummy(n) {
778
+ return n.dummy === "edge" || n.dummy === "label" || n.dummy === "spine";
779
+ }
780
+ function separation(a, b, s) {
781
+ const gap = isDummy(a) && isDummy(b) ? s.edgeSep : isDummy(a) || isDummy(b) ? (s.nodeSep + s.edgeSep) / 2 : s.nodeSep;
782
+ return a.width / 2 + gap + b.width / 2;
783
+ }
784
+ function packLeft(layer, s) {
785
+ let x = 0;
786
+ layer.forEach((n, i) => {
787
+ x = i === 0 ? n.width / 2 : x + separation(layer[i - 1], n, s);
788
+ n.x = x;
789
+ });
790
+ }
791
+ function median(values) {
792
+ const sorted = values.slice().sort((a, b) => a - b);
793
+ const mid = sorted.length >> 1;
794
+ return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
795
+ }
796
+ /**
797
+ * One rank of the priority method: nodes move towards `desired` in order of
798
+ * priority; a node may push lower-priority neighbours along, never higher.
799
+ */
800
+ function placeLayer(layer, desired, priority, s) {
801
+ const fixed = new Array(layer.length).fill(false);
802
+ const order = layer.map((_, i) => i).sort((a, b) => priority.get(layer[b].id) - priority.get(layer[a].id) || a - b);
803
+ for (const i of order) {
804
+ const node = layer[i];
805
+ const target = desired.get(node.id);
806
+ if (target === void 0) {
807
+ fixed[i] = true;
808
+ continue;
809
+ }
810
+ if (target > node.x) {
811
+ let limit = Infinity;
812
+ let span = 0;
813
+ for (let j = i + 1; j < layer.length; j++) {
814
+ span += separation(layer[j - 1], layer[j], s);
815
+ if (fixed[j]) {
816
+ limit = layer[j].x - span;
817
+ break;
818
+ }
819
+ }
820
+ const x = Math.min(target, limit);
821
+ if (x > node.x) {
822
+ node.x = x;
823
+ for (let j = i + 1; j < layer.length; j++) {
824
+ const min = layer[j - 1].x + separation(layer[j - 1], layer[j], s);
825
+ if (layer[j].x >= min) break;
826
+ layer[j].x = min;
827
+ }
828
+ }
829
+ } else if (target < node.x) {
830
+ let limit = -Infinity;
831
+ let span = 0;
832
+ for (let j = i - 1; j >= 0; j--) {
833
+ span += separation(layer[j], layer[j + 1], s);
834
+ if (fixed[j]) {
835
+ limit = layer[j].x + span;
836
+ break;
837
+ }
838
+ }
839
+ const x = Math.max(target, limit);
840
+ if (x < node.x) {
841
+ node.x = x;
842
+ for (let j = i - 1; j >= 0; j--) {
843
+ const max = layer[j + 1].x - separation(layer[j], layer[j + 1], s);
844
+ if (layer[j].x <= max) break;
845
+ layer[j].x = max;
846
+ }
847
+ }
848
+ }
849
+ fixed[i] = true;
850
+ }
851
+ }
852
+ function priorityX(g, layers, s, rounds = 4) {
853
+ for (const layer of layers) packLeft(layer, s);
854
+ const sweep = (down) => {
855
+ const indices = down ? Array.from({ length: layers.length - 1 }, (_, i) => i + 1) : Array.from({ length: layers.length - 1 }, (_, i) => layers.length - 2 - i);
856
+ for (const i of indices) {
857
+ const layer = layers[i];
858
+ const desired = /* @__PURE__ */ new Map();
859
+ const priority = /* @__PURE__ */ new Map();
860
+ for (const n of layer) {
861
+ const neighbours = down ? g.in(n.id).map((e) => g.node(e.v)) : g.out(n.id).map((e) => g.node(e.w));
862
+ if (neighbours.length > 0) desired.set(n.id, median(neighbours.map((m) => m.x)));
863
+ const p = isDummy(n) && neighbours.some(isDummy) ? 1e6 : neighbours.length;
864
+ priority.set(n.id, p);
865
+ }
866
+ placeLayer(layer, desired, priority, s);
867
+ }
868
+ };
869
+ for (let r = 0; r < rounds; r++) {
870
+ sweep(true);
871
+ sweep(false);
872
+ }
873
+ for (let i = 0; i < layers.length; i++) {
874
+ const layer = layers[i];
875
+ const desired = /* @__PURE__ */ new Map();
876
+ const priority = /* @__PURE__ */ new Map();
877
+ for (const n of layer) {
878
+ const neighbours = [...g.in(n.id).map((e) => g.node(e.v)), ...g.out(n.id).map((e) => g.node(e.w))];
879
+ if (neighbours.length > 0) desired.set(n.id, median(neighbours.map((m) => m.x)));
880
+ priority.set(n.id, isDummy(n) ? 1e6 : neighbours.length);
881
+ }
882
+ placeLayer(layer, desired, priority, s);
883
+ }
884
+ }
885
+ /**
886
+ * Assigns x and y to every node.
887
+ */
888
+ function position(g, layers, parents, s) {
889
+ priorityX(g, layers, s);
890
+ compactClusters(g, layers, parents, s);
891
+ let min = Infinity;
892
+ for (const n of g.nodes.values()) min = Math.min(min, n.x - n.width / 2);
893
+ if (Number.isFinite(min)) for (const n of g.nodes.values()) n.x -= min;
894
+ let y = 0;
895
+ layers.forEach((layer, i) => {
896
+ let h = 0;
897
+ for (const n of layer) h = Math.max(h, n.height);
898
+ if (i > 0) y += s.rankSep / 2;
899
+ for (const n of layer) n.y = y + h / 2;
900
+ y += h;
901
+ });
902
+ }
903
+ /** Members of every cluster (all descendants), and the root's under null. */
904
+ function membership(g, parents) {
905
+ const members = /* @__PURE__ */ new Map();
906
+ for (const n of g.nodes.values()) {
907
+ let c = n.cluster;
908
+ for (;;) {
909
+ const list = members.get(c) ?? [];
910
+ list.push(n);
911
+ members.set(c, list);
912
+ if (c === null) break;
913
+ c = parents.get(c) ?? null;
914
+ }
915
+ }
916
+ return members;
917
+ }
918
+ function childClusters(parents) {
919
+ const children = /* @__PURE__ */ new Map();
920
+ for (const [c, p] of parents) {
921
+ const list = children.get(p) ?? [];
922
+ list.push(c);
923
+ children.set(p, list);
924
+ }
925
+ return children;
926
+ }
927
+ /**
928
+ * The horizontal frame of every cluster from the current node positions:
929
+ * its direct nodes and its child frames, plus padding.
930
+ */
931
+ function clusterFrames(g, parents, padding, frameLeft) {
932
+ const children = childClusters(parents);
933
+ const direct = /* @__PURE__ */ new Map();
934
+ for (const n of g.nodes.values()) {
935
+ if (n.cluster === null) continue;
936
+ const list = direct.get(n.cluster) ?? [];
937
+ list.push(n);
938
+ direct.set(n.cluster, list);
939
+ }
940
+ const frames = /* @__PURE__ */ new Map();
941
+ const visit = (c) => {
942
+ const cached = frames.get(c);
943
+ if (cached) return cached;
944
+ let left = Infinity;
945
+ let right = -Infinity;
946
+ for (const n of direct.get(c) ?? []) {
947
+ left = Math.min(left, n.x - n.width / 2);
948
+ right = Math.max(right, n.x + n.width / 2);
949
+ }
950
+ for (const child of children.get(c) ?? []) {
951
+ const f = visit(child);
952
+ left = Math.min(left, f.left);
953
+ right = Math.max(right, f.right);
954
+ }
955
+ if (!Number.isFinite(left)) {
956
+ left = 0;
957
+ right = 0;
958
+ }
959
+ const frame = {
960
+ left: left - padding - (frameLeft?.get(c) ?? 0),
961
+ right: right + padding
962
+ };
963
+ frames.set(c, frame);
964
+ return frame;
965
+ };
966
+ for (const c of parents.keys()) visit(c);
967
+ return frames;
968
+ }
969
+ /**
970
+ * Pushes clusters apart as rigid blocks, bottom-up: a cluster's content is
971
+ * settled first, then the cluster is a single unit to its parent. In every
972
+ * rank, consecutive units must not overlap; units only ever move right, so
973
+ * the separation established before is kept.
974
+ */
975
+ function compactClusters(g, layers, parents, s) {
976
+ if (parents.size === 0) return;
977
+ const children = childClusters(parents);
978
+ const members = membership(g, parents);
979
+ const unitOf = (n, level) => {
980
+ let c = n.cluster;
981
+ let prev = null;
982
+ while (c !== level) {
983
+ if (c === null) return null;
984
+ prev = c;
985
+ c = parents.get(c) ?? null;
986
+ }
987
+ return prev ?? n.id;
988
+ };
989
+ const compact = (level) => {
990
+ for (const child of children.get(level) ?? []) compact(child);
991
+ const frames = clusterFrames(g, parents, s.clusterPadding, s.frameLeft);
992
+ const units = /* @__PURE__ */ new Map();
993
+ for (const n of members.get(level) ?? []) {
994
+ const id = unitOf(n, level);
995
+ if (id === null || units.has(id)) continue;
996
+ if (id === n.id) units.set(id, {
997
+ cluster: false,
998
+ left: n.x - n.width / 2,
999
+ right: n.x + n.width / 2,
1000
+ dummy: isDummy(n)
1001
+ });
1002
+ else {
1003
+ const f = frames.get(id);
1004
+ units.set(id, {
1005
+ cluster: true,
1006
+ left: f.left,
1007
+ right: f.right,
1008
+ dummy: false
1009
+ });
1010
+ }
1011
+ }
1012
+ const edges = /* @__PURE__ */ new Map();
1013
+ const indegree = new Map([...units.keys()].map((id) => [id, 0]));
1014
+ for (const layer of layers) {
1015
+ let prev = null;
1016
+ for (const n of layer) {
1017
+ const id = unitOf(n, level);
1018
+ if (id === null || id === prev) continue;
1019
+ if (prev !== null) {
1020
+ const a = units.get(prev);
1021
+ const b = units.get(id);
1022
+ const gap = a.dummy && b.dummy ? s.edgeSep : a.dummy || b.dummy ? (s.nodeSep + s.edgeSep) / 2 : s.nodeSep;
1023
+ const out = edges.get(prev) ?? /* @__PURE__ */ new Map();
1024
+ if (!out.has(id)) indegree.set(id, indegree.get(id) + 1);
1025
+ out.set(id, Math.max(out.get(id) ?? -Infinity, a.right - a.left + gap));
1026
+ edges.set(prev, out);
1027
+ }
1028
+ prev = id;
1029
+ }
1030
+ }
1031
+ const left = new Map([...units].map(([id, u]) => [id, u.left]));
1032
+ const queue = [...units.keys()].filter((id) => indegree.get(id) === 0);
1033
+ while (queue.length > 0) {
1034
+ const id = queue.shift();
1035
+ for (const [to, dist] of edges.get(id) ?? []) {
1036
+ left.set(to, Math.max(left.get(to), left.get(id) + dist));
1037
+ indegree.set(to, indegree.get(to) - 1);
1038
+ if (indegree.get(to) === 0) queue.push(to);
1039
+ }
1040
+ }
1041
+ for (const [id, u] of units) {
1042
+ const dx = left.get(id) - u.left;
1043
+ if (dx === 0) continue;
1044
+ if (u.cluster) for (const m of members.get(id) ?? []) m.x += dx;
1045
+ else g.node(id).x += dx;
1046
+ }
1047
+ };
1048
+ compact(null);
1049
+ }
1050
+ // src/layout/layered/rank.ts
1051
+ /**
1052
+ * Turns around the edges that close cycles, found by depth-first search in
1053
+ * insertion order — so the edge reversed is the one the author wrote last in
1054
+ * the loop, which is usually the "back" edge they meant.
1055
+ */
1056
+ function removeCycles(g) {
1057
+ const state = /* @__PURE__ */ new Map();
1058
+ const reverse = [];
1059
+ for (const start of g.nodes.keys()) {
1060
+ if (state.get(start)) continue;
1061
+ const stack = [{
1062
+ id: start,
1063
+ i: 0
1064
+ }];
1065
+ state.set(start, 1);
1066
+ while (stack.length > 0) {
1067
+ const top = stack[stack.length - 1];
1068
+ const out = g.out(top.id);
1069
+ if (top.i < out.length) {
1070
+ const e = out[top.i++];
1071
+ const s = state.get(e.w) ?? 0;
1072
+ if (s === 1) reverse.push(e);
1073
+ else if (s === 0) {
1074
+ state.set(e.w, 1);
1075
+ stack.push({
1076
+ id: e.w,
1077
+ i: 0
1078
+ });
1079
+ }
1080
+ } else {
1081
+ state.set(top.id, 2);
1082
+ stack.pop();
1083
+ }
1084
+ }
1085
+ }
1086
+ for (const e of reverse) {
1087
+ const v = e.v;
1088
+ e.v = e.w;
1089
+ e.w = v;
1090
+ e.reversed = !e.reversed;
1091
+ }
1092
+ if (reverse.length > 0) g.reindex();
1093
+ }
1094
+ /** A simple graph for ranking: parallel edges merged, as network simplex expects. */
1095
+ function simplify$1(g, ids) {
1096
+ const inside = new Set(ids);
1097
+ const merged = /* @__PURE__ */ new Map();
1098
+ for (const e of g.edges) {
1099
+ if (!inside.has(e.v) || e.v === e.w) continue;
1100
+ const key = `${e.v}\u0000${e.w}`;
1101
+ const existing = merged.get(key);
1102
+ if (existing) {
1103
+ existing.weight += e.weight;
1104
+ existing.minlen = Math.max(existing.minlen, e.minlen);
1105
+ } else merged.set(key, {
1106
+ v: e.v,
1107
+ w: e.w,
1108
+ minlen: e.minlen,
1109
+ weight: e.weight
1110
+ });
1111
+ }
1112
+ return {
1113
+ nodes: [...ids],
1114
+ edges: [...merged.values()]
1115
+ };
1116
+ }
1117
+ /** Weakly connected components, each in insertion order. */
1118
+ function components(g) {
1119
+ const seen = /* @__PURE__ */ new Set();
1120
+ const out = [];
1121
+ for (const start of g.nodes.keys()) {
1122
+ if (seen.has(start)) continue;
1123
+ const comp = [];
1124
+ const queue = [start];
1125
+ seen.add(start);
1126
+ while (queue.length > 0) {
1127
+ const id = queue.shift();
1128
+ comp.push(id);
1129
+ for (const e of [...g.out(id), ...g.in(id)]) {
1130
+ const other = e.v === id ? e.w : e.v;
1131
+ if (!seen.has(other)) {
1132
+ seen.add(other);
1133
+ queue.push(other);
1134
+ }
1135
+ }
1136
+ }
1137
+ out.push(comp);
1138
+ }
1139
+ return out;
1140
+ }
1141
+ /**
1142
+ * Network simplex leaves ties: a node whose incoming weight equals its
1143
+ * outgoing weight costs the same anywhere between its neighbours, and the
1144
+ * initial ranking puts it as low as it can go — far from where it is drawn
1145
+ * from. Moving such a node up never makes the total longer (it shortens the
1146
+ * incoming edges at least as much as it lengthens the outgoing ones), so
1147
+ * every node with at least as much weight coming in as going out is pulled
1148
+ * up against its predecessors, in topological order.
1149
+ */
1150
+ function pullUp(g, ids) {
1151
+ const indegree = new Map(ids.map((id) => [id, g.in(id).length]));
1152
+ const queue = ids.filter((id) => indegree.get(id) === 0);
1153
+ while (queue.length > 0) {
1154
+ const id = queue.shift();
1155
+ const ins = g.in(id);
1156
+ const outs = g.out(id);
1157
+ if (ins.length > 0) {
1158
+ if (ins.reduce((sum, e) => sum + e.weight, 0) >= outs.reduce((sum, e) => sum + e.weight, 0)) {
1159
+ let target = -Infinity;
1160
+ for (const e of ins) target = Math.max(target, g.node(e.v).rank + e.minlen);
1161
+ const n = g.node(id);
1162
+ if (target < n.rank) n.rank = target;
1163
+ }
1164
+ }
1165
+ for (const e of outs) {
1166
+ indegree.set(e.w, indegree.get(e.w) - 1);
1167
+ if (indegree.get(e.w) === 0) queue.push(e.w);
1168
+ }
1169
+ }
1170
+ }
1171
+ /** Assigns `rank` to every node: 0 is the top, each component starts at 0. */
1172
+ function rank(g, maxIterations = 2e3) {
1173
+ for (const comp of components(g)) {
1174
+ const { nodes, edges } = simplify$1(g, comp);
1175
+ const ranks = networkSimplex(nodes, edges, maxIterations);
1176
+ let min = Infinity;
1177
+ for (const r of ranks.values()) min = Math.min(min, r);
1178
+ for (const [id, r] of ranks) g.node(id).rank = r - min;
1179
+ pullUp(g, comp);
1180
+ let low = Infinity;
1181
+ for (const id of comp) low = Math.min(low, g.node(id).rank);
1182
+ for (const id of comp) g.node(id).rank -= low;
1183
+ }
1184
+ }
1185
+ /**
1186
+ * Network simplex over one connected component.
1187
+ * Returns ranks (not normalised).
1188
+ */
1189
+ function networkSimplex(nodes, edges, maxIterations) {
1190
+ const out = /* @__PURE__ */ new Map();
1191
+ const inn = /* @__PURE__ */ new Map();
1192
+ for (const n of nodes) {
1193
+ out.set(n, []);
1194
+ inn.set(n, []);
1195
+ }
1196
+ for (const e of edges) {
1197
+ out.get(e.v).push(e);
1198
+ inn.get(e.w).push(e);
1199
+ }
1200
+ const edgeKey = (v, w) => `${v}\u0000${w}`;
1201
+ const byKey = new Map(edges.map((e) => [edgeKey(e.v, e.w), e]));
1202
+ const graphEdge = (v, w) => byKey.get(edgeKey(v, w));
1203
+ const ranks = /* @__PURE__ */ new Map();
1204
+ const visit = (start) => {
1205
+ const stack = [{
1206
+ id: start,
1207
+ i: 0
1208
+ }];
1209
+ const onStack = /* @__PURE__ */ new Set([start]);
1210
+ while (stack.length > 0) {
1211
+ const top = stack[stack.length - 1];
1212
+ const list = out.get(top.id);
1213
+ if (top.i < list.length) {
1214
+ const e = list[top.i++];
1215
+ if (!ranks.has(e.w) && !onStack.has(e.w)) {
1216
+ onStack.add(e.w);
1217
+ stack.push({
1218
+ id: e.w,
1219
+ i: 0
1220
+ });
1221
+ }
1222
+ } else {
1223
+ let r = Infinity;
1224
+ for (const e of list) r = Math.min(r, ranks.get(e.w) - e.minlen);
1225
+ ranks.set(top.id, r === Infinity ? 0 : r);
1226
+ stack.pop();
1227
+ }
1228
+ }
1229
+ };
1230
+ for (const n of nodes) if (!ranks.has(n)) visit(n);
1231
+ if (nodes.length <= 1 || edges.length === 0) return ranks;
1232
+ const slack = (e) => ranks.get(e.w) - ranks.get(e.v) - e.minlen;
1233
+ const treeAdj = /* @__PURE__ */ new Map();
1234
+ const addTreeEdge = (v, w) => {
1235
+ treeAdj.get(v).add(w);
1236
+ treeAdj.get(w).add(v);
1237
+ };
1238
+ const growTight = () => {
1239
+ const stack = [...treeAdj.keys()];
1240
+ while (stack.length > 0) {
1241
+ const v = stack.pop();
1242
+ for (const e of [...out.get(v), ...inn.get(v)]) {
1243
+ const w = e.v === v ? e.w : e.v;
1244
+ if (!treeAdj.has(w) && slack(e) === 0) {
1245
+ treeAdj.set(w, /* @__PURE__ */ new Set());
1246
+ addTreeEdge(v, w);
1247
+ stack.push(w);
1248
+ }
1249
+ }
1250
+ }
1251
+ return treeAdj.size;
1252
+ };
1253
+ treeAdj.set(nodes[0], /* @__PURE__ */ new Set());
1254
+ while (growTight() < nodes.length) {
1255
+ let best = null;
1256
+ let bestSlack = Infinity;
1257
+ for (const e of edges) if (treeAdj.has(e.v) !== treeAdj.has(e.w)) {
1258
+ const s = slack(e);
1259
+ if (s < bestSlack) {
1260
+ bestSlack = s;
1261
+ best = e;
1262
+ }
1263
+ }
1264
+ if (!best) break;
1265
+ const delta = treeAdj.has(best.v) ? bestSlack : -bestSlack;
1266
+ for (const v of treeAdj.keys()) ranks.set(v, ranks.get(v) + delta);
1267
+ }
1268
+ const root = nodes[0];
1269
+ const low = /* @__PURE__ */ new Map();
1270
+ const lim = /* @__PURE__ */ new Map();
1271
+ const parent = /* @__PURE__ */ new Map();
1272
+ const cut = /* @__PURE__ */ new Map();
1273
+ const treeKey = (a, b) => a < b ? edgeKey(a, b) : edgeKey(b, a);
1274
+ const initLowLim = () => {
1275
+ low.clear();
1276
+ lim.clear();
1277
+ parent.clear();
1278
+ let next = 1;
1279
+ const stack = [];
1280
+ parent.set(root, void 0);
1281
+ stack.push({
1282
+ id: root,
1283
+ it: treeAdj.get(root).values(),
1284
+ low: next
1285
+ });
1286
+ const visited = /* @__PURE__ */ new Set([root]);
1287
+ while (stack.length > 0) {
1288
+ const top = stack[stack.length - 1];
1289
+ const step = top.it.next();
1290
+ if (!step.done) {
1291
+ const w = step.value;
1292
+ if (!visited.has(w)) {
1293
+ visited.add(w);
1294
+ parent.set(w, top.id);
1295
+ stack.push({
1296
+ id: w,
1297
+ it: treeAdj.get(w).values(),
1298
+ low: next
1299
+ });
1300
+ }
1301
+ } else {
1302
+ low.set(top.id, top.low);
1303
+ lim.set(top.id, next++);
1304
+ stack.pop();
1305
+ }
1306
+ }
1307
+ };
1308
+ const postorder = () => [...lim.entries()].sort((a, b) => a[1] - b[1]).map(([id]) => id);
1309
+ const calcCut = (child) => {
1310
+ const par = parent.get(child);
1311
+ let childIsTail = true;
1312
+ let ge = graphEdge(child, par);
1313
+ if (!ge) {
1314
+ childIsTail = false;
1315
+ ge = graphEdge(par, child);
1316
+ }
1317
+ let value = ge.weight;
1318
+ for (const e of [...out.get(child), ...inn.get(child)]) {
1319
+ const isOut = e.v === child;
1320
+ const other = isOut ? e.w : e.v;
1321
+ if (other === par) continue;
1322
+ const pointsToHead = isOut === childIsTail;
1323
+ value += pointsToHead ? e.weight : -e.weight;
1324
+ if (treeAdj.get(child).has(other)) {
1325
+ const otherCut = cut.get(treeKey(child, other)) ?? 0;
1326
+ value += pointsToHead ? -otherCut : otherCut;
1327
+ }
1328
+ }
1329
+ return value;
1330
+ };
1331
+ const initCutValues = () => {
1332
+ cut.clear();
1333
+ for (const v of postorder()) {
1334
+ const par = parent.get(v);
1335
+ if (par === void 0) continue;
1336
+ cut.set(treeKey(v, par), calcCut(v));
1337
+ }
1338
+ };
1339
+ const updateRanks = () => {
1340
+ const order = [...lim.entries()].sort((a, b) => b[1] - a[1]).map(([id]) => id);
1341
+ for (const v of order) {
1342
+ const par = parent.get(v);
1343
+ if (par === void 0) continue;
1344
+ const e = graphEdge(v, par);
1345
+ if (e) ranks.set(v, ranks.get(par) - e.minlen);
1346
+ else ranks.set(v, ranks.get(par) + graphEdge(par, v).minlen);
1347
+ }
1348
+ };
1349
+ const isDescendant = (v, rootOf) => low.get(rootOf) <= lim.get(v) && lim.get(v) <= lim.get(rootOf);
1350
+ initLowLim();
1351
+ initCutValues();
1352
+ for (let iteration = 0; iteration < maxIterations; iteration++) {
1353
+ let leave = null;
1354
+ for (const [v, par] of parent) {
1355
+ if (par === void 0) continue;
1356
+ if ((cut.get(treeKey(v, par)) ?? 0) < 0) {
1357
+ leave = graphEdge(v, par) ? {
1358
+ v,
1359
+ w: par
1360
+ } : {
1361
+ v: par,
1362
+ w: v
1363
+ };
1364
+ break;
1365
+ }
1366
+ }
1367
+ if (!leave) break;
1368
+ let tail = leave.v;
1369
+ let flip = false;
1370
+ if (lim.get(leave.v) > lim.get(leave.w)) {
1371
+ tail = leave.w;
1372
+ flip = true;
1373
+ }
1374
+ let enter = null;
1375
+ let best = Infinity;
1376
+ for (const e of edges) if (flip === isDescendant(e.v, tail) && flip !== isDescendant(e.w, tail)) {
1377
+ const s = slack(e);
1378
+ if (s < best) {
1379
+ best = s;
1380
+ enter = e;
1381
+ }
1382
+ }
1383
+ if (!enter) break;
1384
+ treeAdj.get(leave.v).delete(leave.w);
1385
+ treeAdj.get(leave.w).delete(leave.v);
1386
+ addTreeEdge(enter.v, enter.w);
1387
+ initLowLim();
1388
+ initCutValues();
1389
+ updateRanks();
1390
+ }
1391
+ return ranks;
1392
+ }
1393
+ // src/layout/layered/index.ts
1394
+ /**
1395
+ * Vendored from @dskripchenko/diagrams 1.1.0 (src/layout/layered), which does
1396
+ * not export it yet; see docs/internal/design.md. Keep in sync; do not edit
1397
+ * here beyond import paths.
1398
+ *
1399
+ * Layered ("Sugiyama") layout with clusters.
1400
+ *
1401
+ * 1. Clusters get invisible top and bottom border nodes, tied to their
1402
+ * members, so a cluster occupies a contiguous band of ranks with room
1403
+ * for its title.
1404
+ * 2. Cycles are broken by reversing edges.
1405
+ * 3. Ranks by network simplex. Every real edge's minimum length is doubled
1406
+ * so that the middle rank of each edge is free for its label.
1407
+ * 4. Long edges are split into chains of dummy nodes, one per rank.
1408
+ * 5. Ranks are ordered to reduce crossings, keeping clusters contiguous.
1409
+ * 6. Coordinates: priority method, then clusters pushed apart as blocks.
1410
+ * 7. Everything is computed top-to-bottom and turned at the end for
1411
+ * BT, LR and RL.
1412
+ */
1413
+ var MAX_NODES = 500;
1414
+ var MAX_EDGES = 2e3;
1415
+ var SELF_LOOP = 22;
1416
+ function layoutCore(input) {
1417
+ const direction = input.direction ?? "TB";
1418
+ const horizontal = direction === "LR" || direction === "RL";
1419
+ const spacing = {
1420
+ nodeSep: input.nodeSep ?? 50,
1421
+ rankSep: input.rankSep ?? 50,
1422
+ edgeSep: input.edgeSep ?? 20,
1423
+ clusterPadding: input.clusterPadding ?? 12
1424
+ };
1425
+ if (input.nodes.length > 500) throw tooLarge("nodes", input.nodes.length, 500);
1426
+ if (input.edges.length > 2e3) throw tooLarge("edges", input.edges.length, MAX_EDGES);
1427
+ const dims = (w, h) => horizontal ? {
1428
+ width: h,
1429
+ height: w
1430
+ } : {
1431
+ width: w,
1432
+ height: h
1433
+ };
1434
+ const g = new Graph();
1435
+ const clusters = new Map((input.clusters ?? []).map((c) => [c.id, c]));
1436
+ const parents = /* @__PURE__ */ new Map();
1437
+ for (const c of clusters.values()) {
1438
+ if (c.parent !== void 0 && !clusters.has(c.parent)) throw new DiagramError(`Cluster "${c.id}" is inside an unknown cluster "${c.parent}"`);
1439
+ parents.set(c.id, c.parent ?? null);
1440
+ }
1441
+ for (const c of clusters.keys()) {
1442
+ let p = parents.get(c) ?? null;
1443
+ let steps = 0;
1444
+ while (p !== null) {
1445
+ if (p === c || ++steps > clusters.size) throw new DiagramError(`Cluster "${c}" contains itself`);
1446
+ p = parents.get(p) ?? null;
1447
+ }
1448
+ }
1449
+ const selfLoops = input.edges.filter((e) => e.from === e.to && !clusters.has(e.from));
1450
+ const loopRoom = /* @__PURE__ */ new Map();
1451
+ for (const e of selfLoops) {
1452
+ const labelRoom = e.label ? (horizontal ? e.label.height : e.label.width) + 6 : 0;
1453
+ loopRoom.set(e.from, Math.max(loopRoom.get(e.from) ?? 0, SELF_LOOP + labelRoom));
1454
+ }
1455
+ for (const n of input.nodes) {
1456
+ if (g.nodes.has(n.id)) continue;
1457
+ const d = dims(n.width, n.height);
1458
+ const room = loopRoom.get(n.id) ?? 0;
1459
+ if (n.cluster !== void 0 && !clusters.has(n.cluster)) throw new DiagramError(`Node "${n.id}" is inside an unknown cluster "${n.cluster}"`);
1460
+ g.addNode({
1461
+ id: n.id,
1462
+ width: d.width + room * 2,
1463
+ height: d.height,
1464
+ cluster: n.cluster ?? null
1465
+ });
1466
+ }
1467
+ const top = (c) => `\u0000top:${c}`;
1468
+ const bottom = (c) => `\u0000bottom:${c}`;
1469
+ const titleRoom = /* @__PURE__ */ new Map();
1470
+ const frameLeft = /* @__PURE__ */ new Map();
1471
+ spacing.frameLeft = frameLeft;
1472
+ for (const c of clusters.values()) {
1473
+ const label = c.label ?? {
1474
+ width: 0,
1475
+ height: 0
1476
+ };
1477
+ const titleHeight = label.height > 0 ? label.height + 6 : 0;
1478
+ titleRoom.set(c.id, titleHeight);
1479
+ if (horizontal) frameLeft.set(c.id, titleHeight);
1480
+ const titled = direction === "TB" ? "top" : direction === "BT" ? "bottom" : null;
1481
+ g.addNode({
1482
+ id: top(c.id),
1483
+ width: titled === "top" ? label.width + 2 * spacing.clusterPadding : 0,
1484
+ height: titled === "top" ? titleHeight : 0,
1485
+ cluster: c.id,
1486
+ dummy: "border-top"
1487
+ });
1488
+ g.addNode({
1489
+ id: bottom(c.id),
1490
+ width: titled === "bottom" ? label.width + 2 * spacing.clusterPadding : 0,
1491
+ height: titled === "bottom" ? titleHeight : 0,
1492
+ cluster: c.id,
1493
+ dummy: "border-bottom"
1494
+ });
1495
+ }
1496
+ let structuralId = 0;
1497
+ const structural = (v, w) => g.addEdge({
1498
+ id: `\u0000s${structuralId++}`,
1499
+ v,
1500
+ w,
1501
+ minlen: 1,
1502
+ weight: 1,
1503
+ original: "",
1504
+ reversed: false,
1505
+ structural: true
1506
+ });
1507
+ for (const c of clusters.keys()) {
1508
+ const directNodes = [...g.nodes.values()].filter((n) => n.cluster === c && !n.dummy);
1509
+ const directClusters = [...clusters.values()].filter((k) => (k.parent ?? null) === c);
1510
+ for (const n of directNodes) {
1511
+ structural(top(c), n.id);
1512
+ structural(n.id, bottom(c));
1513
+ }
1514
+ for (const k of directClusters) {
1515
+ structural(top(c), top(k.id));
1516
+ structural(bottom(k.id), bottom(c));
1517
+ }
1518
+ if (directNodes.length === 0 && directClusters.length === 0) structural(top(c), bottom(c));
1519
+ }
1520
+ const leaves = (c) => {
1521
+ const out = [];
1522
+ for (const n of input.nodes) if (n.cluster === c) out.push(n.id);
1523
+ for (const k of clusters.values()) if ((k.parent ?? null) === c) out.push(...leaves(k.id));
1524
+ return out;
1525
+ };
1526
+ const endpoint = (id, asSource) => {
1527
+ if (clusters.has(id)) {
1528
+ const inside = leaves(id);
1529
+ if (inside.length === 0) return asSource ? bottom(id) : top(id);
1530
+ return asSource ? inside[inside.length - 1] : inside[0];
1531
+ }
1532
+ if (!g.nodes.has(id)) throw new DiagramError(`Edge refers to an unknown node "${id}"`);
1533
+ return id;
1534
+ };
1535
+ const labels = /* @__PURE__ */ new Map();
1536
+ for (const e of input.edges) {
1537
+ if (e.from === e.to && !clusters.has(e.from)) continue;
1538
+ const v = endpoint(e.from, true);
1539
+ const w = endpoint(e.to, false);
1540
+ if (v === w) continue;
1541
+ if (e.label) labels.set(e.id, dims(e.label.width, e.label.height));
1542
+ g.addEdge({
1543
+ id: e.id,
1544
+ v,
1545
+ w,
1546
+ minlen: Math.max(1, e.minlen ?? 1) * 2,
1547
+ weight: e.weight ?? 1,
1548
+ original: e.id,
1549
+ reversed: false
1550
+ });
1551
+ }
1552
+ removeCycles(g);
1553
+ rank(g);
1554
+ pullBordersTight(g, clusters.keys(), top, bottom);
1555
+ for (const e of g.edges.slice()) if (e.structural && g.node(e.w).rank - g.node(e.v).rank !== 1) g.removeEdge(e);
1556
+ const chains = /* @__PURE__ */ new Map();
1557
+ let dummyId = 0;
1558
+ const clusterOfEnd = (id) => {
1559
+ const n = g.node(id);
1560
+ if (n.dummy === "border-top" || n.dummy === "border-bottom") return parents.get(n.cluster) ?? null;
1561
+ return n.cluster;
1562
+ };
1563
+ for (const e of g.edges.slice()) {
1564
+ if (e.structural) continue;
1565
+ const v = g.node(e.v);
1566
+ const w = g.node(e.w);
1567
+ const owner = commonCluster(clusterOfEnd(v.id), clusterOfEnd(w.id), parents);
1568
+ const chain = [v.id];
1569
+ const span = w.rank - v.rank;
1570
+ const label = labels.get(e.original);
1571
+ const labelRank = v.rank + Math.floor(span / 2);
1572
+ g.removeEdge(e);
1573
+ let prev = v.id;
1574
+ for (let r = v.rank + 1; r < w.rank; r++) {
1575
+ const isLabel = !!label && r === labelRank;
1576
+ const d = g.addNode({
1577
+ id: `\u0000d${dummyId++}`,
1578
+ width: isLabel ? label.width : 0,
1579
+ height: isLabel ? label.height : 0,
1580
+ cluster: owner,
1581
+ dummy: isLabel ? "label" : "edge",
1582
+ edge: e.original
1583
+ });
1584
+ d.rank = r;
1585
+ g.addEdge({
1586
+ id: `${e.id}#${r}`,
1587
+ v: prev,
1588
+ w: d.id,
1589
+ minlen: 1,
1590
+ weight: e.weight,
1591
+ original: e.original,
1592
+ reversed: e.reversed
1593
+ });
1594
+ chain.push(d.id);
1595
+ prev = d.id;
1596
+ }
1597
+ g.addEdge({
1598
+ id: `${e.id}#end`,
1599
+ v: prev,
1600
+ w: w.id,
1601
+ minlen: 1,
1602
+ weight: e.weight,
1603
+ original: e.original,
1604
+ reversed: e.reversed
1605
+ });
1606
+ chain.push(w.id);
1607
+ chains.set(e.original, e.reversed ? chain.reverse() : chain);
1608
+ }
1609
+ addSpines(g, clusters.keys(), parents, top, bottom);
1610
+ position(g, order(g, parents, 24, input.transpose ?? false), parents, spacing);
1611
+ const frames = clusterFrames(g, parents, spacing.clusterPadding, spacing.frameLeft);
1612
+ const clusterBoxes = /* @__PURE__ */ new Map();
1613
+ const descendants = /* @__PURE__ */ new Map();
1614
+ for (const n of g.nodes.values()) {
1615
+ let c = n.cluster;
1616
+ while (c !== null) {
1617
+ const list = descendants.get(c) ?? [];
1618
+ list.push(n);
1619
+ descendants.set(c, list);
1620
+ c = parents.get(c) ?? null;
1621
+ }
1622
+ }
1623
+ const depth = (c) => {
1624
+ let d = 0;
1625
+ let p = parents.get(c) ?? null;
1626
+ while (p !== null) {
1627
+ d++;
1628
+ p = parents.get(p) ?? null;
1629
+ }
1630
+ return d;
1631
+ };
1632
+ const byDepth = [...clusters.keys()].sort((a, b) => depth(b) - depth(a));
1633
+ const vertical = /* @__PURE__ */ new Map();
1634
+ for (const c of byDepth) {
1635
+ let t = Infinity;
1636
+ let b = -Infinity;
1637
+ for (const n of descendants.get(c) ?? []) {
1638
+ t = Math.min(t, n.y - n.height / 2);
1639
+ b = Math.max(b, n.y + n.height / 2);
1640
+ }
1641
+ for (const k of clusters.values()) {
1642
+ if ((k.parent ?? null) !== c) continue;
1643
+ const inner = vertical.get(k.id);
1644
+ if (inner) {
1645
+ t = Math.min(t, inner.top);
1646
+ b = Math.max(b, inner.bottom);
1647
+ }
1648
+ }
1649
+ const pad = spacing.clusterPadding;
1650
+ vertical.set(c, {
1651
+ top: t - (direction === "TB" ? 0 : pad),
1652
+ bottom: b + (direction === "BT" ? 0 : pad)
1653
+ });
1654
+ }
1655
+ for (const c of clusters.keys()) {
1656
+ const f = frames.get(c);
1657
+ const v = vertical.get(c);
1658
+ const left = f.left;
1659
+ clusterBoxes.set(c, {
1660
+ x: (left + f.right) / 2,
1661
+ y: (v.top + v.bottom) / 2,
1662
+ width: f.right - left,
1663
+ height: v.bottom - v.top
1664
+ });
1665
+ }
1666
+ let maxX = 0;
1667
+ let maxY = 0;
1668
+ let minX = 0;
1669
+ let minY = 0;
1670
+ for (const n of g.nodes.values()) {
1671
+ maxX = Math.max(maxX, n.x + n.width / 2);
1672
+ maxY = Math.max(maxY, n.y + n.height / 2);
1673
+ }
1674
+ for (const b of clusterBoxes.values()) {
1675
+ minX = Math.min(minX, b.x - b.width / 2);
1676
+ minY = Math.min(minY, b.y - b.height / 2);
1677
+ maxX = Math.max(maxX, b.x + b.width / 2);
1678
+ maxY = Math.max(maxY, b.y + b.height / 2);
1679
+ }
1680
+ const W = maxX - minX;
1681
+ const H = maxY - minY;
1682
+ const turn = (p) => {
1683
+ const x = p.x - minX;
1684
+ const y = p.y - minY;
1685
+ switch (direction) {
1686
+ case "BT": return {
1687
+ x,
1688
+ y: H - y
1689
+ };
1690
+ case "LR": return {
1691
+ x: y,
1692
+ y: x
1693
+ };
1694
+ case "RL": return {
1695
+ x: H - y,
1696
+ y: x
1697
+ };
1698
+ default: return {
1699
+ x,
1700
+ y
1701
+ };
1702
+ }
1703
+ };
1704
+ const turnBox = (b) => {
1705
+ const c = turn(b);
1706
+ return horizontal ? {
1707
+ x: c.x,
1708
+ y: c.y,
1709
+ width: b.height,
1710
+ height: b.width
1711
+ } : {
1712
+ ...c,
1713
+ width: b.width,
1714
+ height: b.height
1715
+ };
1716
+ };
1717
+ const nodes = /* @__PURE__ */ new Map();
1718
+ for (const n of input.nodes) {
1719
+ const c = turn(g.node(n.id));
1720
+ nodes.set(n.id, {
1721
+ x: c.x,
1722
+ y: c.y,
1723
+ width: n.width,
1724
+ height: n.height
1725
+ });
1726
+ }
1727
+ const clustersOut = /* @__PURE__ */ new Map();
1728
+ for (const [id, b] of clusterBoxes) clustersOut.set(id, turnBox(b));
1729
+ const edges = /* @__PURE__ */ new Map();
1730
+ for (const e of input.edges) {
1731
+ if (e.from === e.to && !clusters.has(e.from)) {
1732
+ edges.set(e.id, selfLoopRoute(nodes.get(e.from), direction, e.label));
1733
+ continue;
1734
+ }
1735
+ const chain = chains.get(e.id);
1736
+ if (!chain) {
1737
+ edges.set(e.id, { points: [] });
1738
+ continue;
1739
+ }
1740
+ const inner = chain.slice(1, -1).map((id) => g.node(id));
1741
+ const route = { points: inner.map((n) => turn(n)) };
1742
+ const labelNode = inner.find((n) => n.dummy === "label");
1743
+ if (labelNode) route.label = turn(labelNode);
1744
+ if (clusters.has(e.from) || clusters.has(e.to)) clipAtClusters(route, turn(g.node(chain[0])), turn(g.node(chain[chain.length - 1])), {
1745
+ from: clusters.has(e.from) ? clustersOut.get(e.from) : void 0,
1746
+ to: clusters.has(e.to) ? clustersOut.get(e.to) : void 0
1747
+ });
1748
+ edges.set(e.id, route);
1749
+ }
1750
+ return {
1751
+ width: horizontal ? H : W,
1752
+ height: horizontal ? W : H,
1753
+ nodes,
1754
+ clusters: clustersOut,
1755
+ edges
1756
+ };
1757
+ }
1758
+ function inside(box, p) {
1759
+ return Math.abs(p.x - box.x) <= box.width / 2 && Math.abs(p.y - box.y) <= box.height / 2;
1760
+ }
1761
+ /** Where the segment from `a` (outside) to `b` (inside) enters the box. */
1762
+ function entry(box, a, b) {
1763
+ let lo = 0;
1764
+ let hi = 1;
1765
+ for (let i = 0; i < 30; i++) {
1766
+ const mid = (lo + hi) / 2;
1767
+ if (inside(box, {
1768
+ x: a.x + (b.x - a.x) * mid,
1769
+ y: a.y + (b.y - a.y) * mid
1770
+ })) hi = mid;
1771
+ else lo = mid;
1772
+ }
1773
+ return {
1774
+ x: a.x + (b.x - a.x) * hi,
1775
+ y: a.y + (b.y - a.y) * hi
1776
+ };
1777
+ }
1778
+ /**
1779
+ * Cuts an edge laid out to a node inside a cluster at the cluster's frame,
1780
+ * and records the cut as the edge's explicit end.
1781
+ */
1782
+ function clipAtClusters(route, start, end, frames) {
1783
+ let line = [
1784
+ start,
1785
+ ...route.points,
1786
+ end
1787
+ ];
1788
+ if (frames.to && !inside(frames.to, start)) {
1789
+ for (let i = 1; i < line.length; i++) if (inside(frames.to, line[i])) {
1790
+ const p = entry(frames.to, line[i - 1], line[i]);
1791
+ line = [...line.slice(0, i), p];
1792
+ break;
1793
+ }
1794
+ route.end = line[line.length - 1];
1795
+ }
1796
+ if (frames.from && !inside(frames.from, line[line.length - 1])) {
1797
+ for (let i = line.length - 2; i >= 0; i--) if (inside(frames.from, line[i])) {
1798
+ line = [entry(frames.from, line[i + 1], line[i]), ...line.slice(i + 1)];
1799
+ break;
1800
+ }
1801
+ route.start = line[0];
1802
+ }
1803
+ route.points = line.slice(1, -1);
1804
+ }
1805
+ /**
1806
+ * Lays out a graph, first taking out every cluster that no node outside it
1807
+ * links into: such a cluster is laid out on its own — in its own direction,
1808
+ * if it has one — and placed in its parent as a single box. This is what
1809
+ * Mermaid does, and it is what makes `direction` inside a subgraph work.
1810
+ * A cluster with edges crossing its frame stays part of the whole layout.
1811
+ */
1812
+ function layout(input) {
1813
+ const clusters = input.clusters ?? [];
1814
+ if (clusters.length === 0) return layoutCore(input);
1815
+ const parent = new Map(clusters.map((c) => [c.id, c.parent ?? null]));
1816
+ const within = (cluster, root) => {
1817
+ let c = cluster ?? null;
1818
+ let guard = 0;
1819
+ while (c !== null && guard++ <= clusters.length) {
1820
+ if (c === root) return true;
1821
+ c = parent.get(c) ?? null;
1822
+ }
1823
+ return false;
1824
+ };
1825
+ const nodeCluster = new Map(input.nodes.map((n) => [n.id, n.cluster ?? null]));
1826
+ const home = (id) => parent.has(id) ? {
1827
+ cluster: id,
1828
+ isCluster: true
1829
+ } : {
1830
+ cluster: nodeCluster.get(id) ?? null,
1831
+ isCluster: false
1832
+ };
1833
+ const isolated = (c) => {
1834
+ for (const e of input.edges) {
1835
+ const a = home(e.from);
1836
+ const b = home(e.to);
1837
+ const aIn = a.isCluster ? within(a.cluster, c) && a.cluster !== c : within(a.cluster, c);
1838
+ if (aIn !== (b.isCluster ? within(b.cluster, c) && b.cluster !== c : within(b.cluster, c))) {
1839
+ const outsideEnd = aIn ? b : a;
1840
+ if (!(outsideEnd.isCluster && outsideEnd.cluster === c)) return false;
1841
+ }
1842
+ }
1843
+ return true;
1844
+ };
1845
+ const extracted = clusters.filter((c) => isolated(c.id) && !clusters.some((o) => o.id !== c.id && within(c.id, o.id) && isolated(o.id)));
1846
+ if (extracted.length === 0) return layoutCore(input);
1847
+ const pad = input.clusterPadding ?? 12;
1848
+ const parentDirection = input.direction ?? "TB";
1849
+ const inner = /* @__PURE__ */ new Map();
1850
+ const removedNodes = /* @__PURE__ */ new Set();
1851
+ const removedClusters = /* @__PURE__ */ new Set();
1852
+ const removedEdges = /* @__PURE__ */ new Set();
1853
+ for (const c of extracted) {
1854
+ const sub = {
1855
+ ...input,
1856
+ direction: c.direction ?? parentDirection,
1857
+ nodes: input.nodes.filter((n) => within(n.cluster, c.id)).map((n) => ({
1858
+ ...n,
1859
+ cluster: n.cluster === c.id ? void 0 : n.cluster
1860
+ })),
1861
+ clusters: clusters.filter((k) => k.id !== c.id && within(k.id, c.id)).map((k) => ({
1862
+ ...k,
1863
+ parent: k.parent === c.id ? void 0 : k.parent
1864
+ })),
1865
+ edges: input.edges.filter((e) => {
1866
+ const a = home(e.from);
1867
+ const b = home(e.to);
1868
+ const aIn = within(a.cluster, c.id) && !(a.isCluster && a.cluster === c.id);
1869
+ const bIn = within(b.cluster, c.id) && !(b.isCluster && b.cluster === c.id);
1870
+ return aIn && bIn;
1871
+ })
1872
+ };
1873
+ for (const n of sub.nodes) removedNodes.add(n.id);
1874
+ for (const k of sub.clusters ?? []) removedClusters.add(k.id);
1875
+ removedClusters.add(c.id);
1876
+ for (const e of sub.edges) removedEdges.add(e.id);
1877
+ const result = layout(sub);
1878
+ const title = c.label && c.label.height > 0 ? c.label.height + 6 : 0;
1879
+ const width = Math.max(result.width, c.label?.width ?? 0) + pad * 2;
1880
+ const height = result.height + pad * 2 + title;
1881
+ inner.set(c.id, {
1882
+ result,
1883
+ title,
1884
+ width,
1885
+ height
1886
+ });
1887
+ }
1888
+ const result = layout({
1889
+ ...input,
1890
+ nodes: [...input.nodes.filter((n) => !removedNodes.has(n.id)), ...extracted.map((c) => ({
1891
+ id: c.id,
1892
+ width: inner.get(c.id).width,
1893
+ height: inner.get(c.id).height,
1894
+ cluster: c.parent
1895
+ }))],
1896
+ clusters: clusters.filter((k) => !removedClusters.has(k.id)),
1897
+ edges: input.edges.filter((e) => !removedEdges.has(e.id))
1898
+ });
1899
+ for (const c of extracted) {
1900
+ const box = result.nodes.get(c.id);
1901
+ result.nodes.delete(c.id);
1902
+ result.clusters.set(c.id, box);
1903
+ const { result: r, title } = inner.get(c.id);
1904
+ const dx = box.x - box.width / 2 + (box.width - r.width) / 2;
1905
+ const dy = box.y - box.height / 2 + pad + title;
1906
+ const move = (p) => ({
1907
+ x: p.x + dx,
1908
+ y: p.y + dy
1909
+ });
1910
+ for (const [id, b] of r.nodes) result.nodes.set(id, {
1911
+ ...b,
1912
+ ...move(b)
1913
+ });
1914
+ for (const [id, b] of r.clusters) result.clusters.set(id, {
1915
+ ...b,
1916
+ ...move(b)
1917
+ });
1918
+ for (const [id, route] of r.edges) result.edges.set(id, {
1919
+ ...route,
1920
+ points: route.points.map(move),
1921
+ label: route.label && move(route.label),
1922
+ start: route.start && move(route.start),
1923
+ end: route.end && move(route.end)
1924
+ });
1925
+ }
1926
+ return result;
1927
+ }
1928
+ /** The innermost cluster containing both, or null. */
1929
+ function commonCluster(a, b, parents) {
1930
+ const chainOf = (c) => {
1931
+ const out = [];
1932
+ while (c !== null) {
1933
+ out.push(c);
1934
+ c = parents.get(c) ?? null;
1935
+ }
1936
+ return out;
1937
+ };
1938
+ const ca = new Set(chainOf(a));
1939
+ for (const c of chainOf(b)) if (ca.has(c)) return c;
1940
+ return null;
1941
+ }
1942
+ /**
1943
+ * Network simplex leaves a border node anywhere its zero-cost slack allows;
1944
+ * pull each top border down to just above the cluster and each bottom one
1945
+ * up to just below it, so frames are no taller than their content.
1946
+ */
1947
+ function pullBordersTight(g, clusterIds, top, bottom) {
1948
+ const ids = [...clusterIds];
1949
+ for (let pass = 0; pass < ids.length + 1; pass++) {
1950
+ let changed = false;
1951
+ for (const c of ids) {
1952
+ const t = g.node(top(c));
1953
+ const b = g.node(bottom(c));
1954
+ const below = g.out(t.id).map((e) => g.node(e.w).rank);
1955
+ const above = g.in(b.id).map((e) => g.node(e.v).rank);
1956
+ const incoming = g.in(t.id).map((e) => g.node(e.v).rank + e.minlen);
1957
+ const outgoing = g.out(b.id).map((e) => g.node(e.w).rank - e.minlen);
1958
+ if (below.length > 0) {
1959
+ const want = Math.min(...below) - 1;
1960
+ const limit = incoming.length > 0 ? Math.max(...incoming) : -Infinity;
1961
+ const r = Math.max(want, limit);
1962
+ if (r !== t.rank && r <= Math.min(...below) - 1) {
1963
+ t.rank = r;
1964
+ changed = true;
1965
+ }
1966
+ }
1967
+ if (above.length > 0) {
1968
+ const want = Math.max(...above) + 1;
1969
+ const limit = outgoing.length > 0 ? Math.min(...outgoing) : Infinity;
1970
+ const r = Math.min(want, limit);
1971
+ if (r !== b.rank && r >= Math.max(...above) + 1) {
1972
+ b.rank = r;
1973
+ changed = true;
1974
+ }
1975
+ }
1976
+ }
1977
+ if (!changed) break;
1978
+ }
1979
+ }
1980
+ function addSpines(g, clusterIds, parents, top, bottom) {
1981
+ const present = /* @__PURE__ */ new Map();
1982
+ for (const n of g.nodes.values()) {
1983
+ let c = n.cluster;
1984
+ while (c !== null) {
1985
+ let set = present.get(c);
1986
+ if (!set) present.set(c, set = /* @__PURE__ */ new Set());
1987
+ set.add(n.rank);
1988
+ c = parents.get(c) ?? null;
1989
+ }
1990
+ }
1991
+ let id = 0;
1992
+ for (const c of clusterIds) {
1993
+ const from = g.node(top(c)).rank;
1994
+ const to = g.node(bottom(c)).rank;
1995
+ const set = present.get(c) ?? /* @__PURE__ */ new Set();
1996
+ for (let r = from + 1; r < to; r++) {
1997
+ if (set.has(r)) continue;
1998
+ const s = g.addNode({
1999
+ id: `\u0000spine${id++}`,
2000
+ width: 0,
2001
+ height: 0,
2002
+ cluster: c,
2003
+ dummy: "spine"
2004
+ });
2005
+ s.rank = r;
2006
+ let p = c;
2007
+ while (p !== null) {
2008
+ let ps = present.get(p);
2009
+ if (!ps) present.set(p, ps = /* @__PURE__ */ new Set());
2010
+ ps.add(r);
2011
+ p = parents.get(p) ?? null;
2012
+ }
2013
+ }
2014
+ }
2015
+ }
2016
+ /** A loop on the side of the node that faces away from the flow. */
2017
+ function selfLoopRoute(box, direction, label) {
2018
+ const horizontal = direction === "LR" || direction === "RL";
2019
+ const d = SELF_LOOP;
2020
+ if (!horizontal) {
2021
+ const xr = box.x + box.width / 2;
2022
+ const y0 = box.y - box.height / 4;
2023
+ const y1 = box.y + box.height / 4;
2024
+ return {
2025
+ selfLoop: true,
2026
+ points: [
2027
+ {
2028
+ x: xr,
2029
+ y: y0
2030
+ },
2031
+ {
2032
+ x: xr + d,
2033
+ y: y0 - 4
2034
+ },
2035
+ {
2036
+ x: xr + d,
2037
+ y: y1 + 4
2038
+ },
2039
+ {
2040
+ x: xr,
2041
+ y: y1
2042
+ }
2043
+ ],
2044
+ label: label ? {
2045
+ x: xr + d + 4 + label.width / 2,
2046
+ y: box.y
2047
+ } : void 0
2048
+ };
2049
+ }
2050
+ const yb = box.y + box.height / 2;
2051
+ const x0 = box.x - box.width / 4;
2052
+ const x1 = box.x + box.width / 4;
2053
+ return {
2054
+ selfLoop: true,
2055
+ points: [
2056
+ {
2057
+ x: x0,
2058
+ y: yb
2059
+ },
2060
+ {
2061
+ x: x0 - 4,
2062
+ y: yb + d
2063
+ },
2064
+ {
2065
+ x: x1 + 4,
2066
+ y: yb + d
2067
+ },
2068
+ {
2069
+ x: x1,
2070
+ y: yb
2071
+ }
2072
+ ],
2073
+ label: label ? {
2074
+ x: box.x,
2075
+ y: yb + d + 4 + label.height / 2
2076
+ } : void 0
2077
+ };
2078
+ }
2079
+ // src/layout/text.ts
2080
+ var NARROW = /* @__PURE__ */ new Set([...`il.,:;'|!()[]{}ftjrI1 `]);
2081
+ var WIDE = /* @__PURE__ */ new Set([..."mwMW@%ЖШЩЮжшщю—"]);
2082
+ /** Width in em of one character, by class; tuned on system sans fonts. */
2083
+ function glyph(ch) {
2084
+ if (NARROW.has(ch)) return ch === " " ? .28 : .3;
2085
+ if (WIDE.has(ch)) return .88;
2086
+ const code = ch.codePointAt(0);
2087
+ if (code >= 11904) return 1;
2088
+ if (ch >= "0" && ch <= "9") return .57;
2089
+ if (ch !== ch.toLowerCase()) return .68;
2090
+ if (code >= 1024 && code <= 1279) return .58;
2091
+ return .54;
2092
+ }
2093
+ var approximateMeasurer = (text, font) => {
2094
+ let em = 0;
2095
+ for (const ch of text) em += glyph(ch);
2096
+ return em * font.size * ((font.weight ?? 400) >= 600 ? 1.06 : 1);
2097
+ };
2098
+ /**
2099
+ * Splits text into lines no wider than `max`, at spaces; a word longer than
2100
+ * a line is cut with an ellipsis. At most `maxLines` lines; the last one
2101
+ * ends with "…" when text is left over.
2102
+ */
2103
+ function wrapText(text, max, font, measure, maxLines = 2) {
2104
+ const words = text.trim().split(/\s+/).filter(Boolean);
2105
+ if (words.length === 0) return [];
2106
+ const lines = [];
2107
+ let line = "";
2108
+ let i = 0;
2109
+ for (; i < words.length; i++) {
2110
+ const w = words[i];
2111
+ const next = line ? `${line} ${w}` : w;
2112
+ if (measure(next, font) <= max) {
2113
+ line = next;
2114
+ continue;
2115
+ }
2116
+ if (line) {
2117
+ lines.push(line);
2118
+ if (lines.length === maxLines) break;
2119
+ }
2120
+ line = measure(w, font) <= max ? w : truncate(w, max, font, measure);
2121
+ }
2122
+ if (lines.length < maxLines && line) {
2123
+ lines.push(line);
2124
+ i = words.length;
2125
+ }
2126
+ if (i < words.length) {
2127
+ const last = lines[lines.length - 1];
2128
+ lines[lines.length - 1] = truncate(`${last}…`, max, font, measure, true);
2129
+ }
2130
+ return lines;
2131
+ }
2132
+ function truncate(text, max, font, measure, hasEllipsis = false) {
2133
+ let t = hasEllipsis ? text.slice(0, -1) : text;
2134
+ while (t.length > 1 && measure(`${t}…`, font) > max) t = t.slice(0, -1);
2135
+ return measure(text, font) <= max && !hasEllipsis ? text : `${t.trimEnd()}…`;
2136
+ }
2137
+ // src/layout/route.ts
2138
+ var NORMAL = {
2139
+ top: {
2140
+ x: 0,
2141
+ y: -1
2142
+ },
2143
+ right: {
2144
+ x: 1,
2145
+ y: 0
2146
+ },
2147
+ bottom: {
2148
+ x: 0,
2149
+ y: 1
2150
+ },
2151
+ left: {
2152
+ x: -1,
2153
+ y: 0
2154
+ }
2155
+ };
2156
+ function sideNormal(side) {
2157
+ return NORMAL[side];
2158
+ }
2159
+ /** A point on a box side, `offset` 0…1 along it (left→right, top→bottom). */
2160
+ function sidePoint(box, side, offset = .5) {
2161
+ const left = box.x - box.width / 2;
2162
+ const top = box.y - box.height / 2;
2163
+ switch (side) {
2164
+ case "top": return {
2165
+ x: left + box.width * offset,
2166
+ y: top
2167
+ };
2168
+ case "bottom": return {
2169
+ x: left + box.width * offset,
2170
+ y: top + box.height
2171
+ };
2172
+ case "left": return {
2173
+ x: left,
2174
+ y: top + box.height * offset
2175
+ };
2176
+ default: return {
2177
+ x: left + box.width,
2178
+ y: top + box.height * offset
2179
+ };
2180
+ }
2181
+ }
2182
+ /**
2183
+ * The side of `box` that faces `toward`. `prefer` biases ties to the flow's
2184
+ * axis: in a TB layout an edge leaves through top/bottom unless the other
2185
+ * end is clearly beside the node.
2186
+ */
2187
+ function facingSide(box, toward, prefer) {
2188
+ const dx = toward.x - box.x;
2189
+ const dy = toward.y - box.y;
2190
+ const nx = Math.abs(dx) / Math.max(1, box.width / 2);
2191
+ const ny = Math.abs(dy) / Math.max(1, box.height / 2);
2192
+ const bias = .6;
2193
+ if (prefer === "vertical" ? ny >= nx * bias : ny > nx / bias) return dy < 0 ? "top" : "bottom";
2194
+ return dx < 0 ? "left" : "right";
2195
+ }
2196
+ /** Whether anchors on this outline may be spread along a side. */
2197
+ function spreadable(shape) {
2198
+ return shape === "rect" || shape === "rounded" || shape === "note" || shape === "pill";
2199
+ }
2200
+ var STUB = 18;
2201
+ var fmt = (n) => (Math.round(n * 10) / 10).toString();
2202
+ var P = (p) => `${fmt(p.x)} ${fmt(p.y)}`;
2203
+ var add = (a, b, k = 1) => ({
2204
+ x: a.x + b.x * k,
2205
+ y: a.y + b.y * k
2206
+ });
2207
+ var dist = (a, b) => Math.hypot(b.x - a.x, b.y - a.y);
2208
+ function route(input) {
2209
+ return input.routing === "orthogonal" ? orthogonal(input) : spline(input);
2210
+ }
2211
+ function spline({ from, to, waypoints = [] }) {
2212
+ const n0 = NORMAL[from.side];
2213
+ const n1 = NORMAL[to.side];
2214
+ const a = from.point;
2215
+ const b = to.point;
2216
+ if (waypoints.length === 0) {
2217
+ const k = Math.max(28, Math.min(120, dist(a, b) * .45));
2218
+ const c0 = add(a, n0, k);
2219
+ const c1 = add(b, n1, k);
2220
+ return {
2221
+ d: `M ${P(a)} C ${P(c0)} ${P(c1)} ${P(b)}`,
2222
+ points: [
2223
+ a,
2224
+ c0,
2225
+ c1,
2226
+ b
2227
+ ]
2228
+ };
2229
+ }
2230
+ const pts = [
2231
+ a,
2232
+ ...waypoints,
2233
+ b
2234
+ ];
2235
+ return {
2236
+ d: basisPath(pts),
2237
+ points: pts
2238
+ };
2239
+ }
2240
+ /** d3's curveBasis written out: smooth, starts and ends exactly on the end points. */
2241
+ function basisPath(points) {
2242
+ const pts = [];
2243
+ for (const p of points) {
2244
+ const last = pts[pts.length - 1];
2245
+ if (!last || dist(last, p) > .5) pts.push(p);
2246
+ }
2247
+ if (pts.length === 0) return "";
2248
+ if (pts.length === 2) return `M ${P(pts[0])} L ${P(pts[1])}`;
2249
+ let d = "";
2250
+ let x0 = 0;
2251
+ let y0 = 0;
2252
+ let x1 = 0;
2253
+ let y1 = 0;
2254
+ const bezier = (x, y) => {
2255
+ d += ` C ${fmt((2 * x0 + x1) / 3)} ${fmt((2 * y0 + y1) / 3)} ${fmt((x0 + 2 * x1) / 3)} ${fmt((y0 + 2 * y1) / 3)} ${fmt((x0 + 4 * x1 + x) / 6)} ${fmt((y0 + 4 * y1 + y) / 6)}`;
2256
+ };
2257
+ let state = 0;
2258
+ for (const p of pts) {
2259
+ if (state === 0) {
2260
+ state = 1;
2261
+ d = `M ${P(p)}`;
2262
+ } else if (state === 1) state = 2;
2263
+ else if (state === 2) {
2264
+ state = 3;
2265
+ d += ` L ${fmt((5 * x0 + x1) / 6)} ${fmt((5 * y0 + y1) / 6)}`;
2266
+ bezier(p.x, p.y);
2267
+ } else bezier(p.x, p.y);
2268
+ x0 = x1;
2269
+ x1 = p.x;
2270
+ y0 = y1;
2271
+ y1 = p.y;
2272
+ }
2273
+ if (state === 3) bezier(x1, y1);
2274
+ d += ` L ${P({
2275
+ x: x1,
2276
+ y: y1
2277
+ })}`;
2278
+ return d;
2279
+ }
2280
+ function orthogonal({ from, to, waypoints = [], radius = 8 }) {
2281
+ const a = from.point;
2282
+ const b = to.point;
2283
+ const s0 = add(a, NORMAL[from.side], STUB);
2284
+ const s1 = add(b, NORMAL[to.side], STUB);
2285
+ const verticalStart = from.side === "top" || from.side === "bottom";
2286
+ const ctrl = [
2287
+ s0,
2288
+ ...waypoints,
2289
+ s1
2290
+ ];
2291
+ const pts = [a, s0];
2292
+ let vertical = verticalStart;
2293
+ for (let i = 1; i < ctrl.length; i++) {
2294
+ const p = pts[pts.length - 1];
2295
+ let q = ctrl[i];
2296
+ const last = i === ctrl.length - 1;
2297
+ if (!last) {
2298
+ if (Math.abs(p.y - q.y) < 3) q = {
2299
+ x: q.x,
2300
+ y: p.y
2301
+ };
2302
+ else if (Math.abs(p.x - q.x) < 3) q = {
2303
+ x: p.x,
2304
+ y: q.y
2305
+ };
2306
+ }
2307
+ if (Math.abs(p.x - q.x) < .5 || Math.abs(p.y - q.y) < .5) {
2308
+ pts.push(q);
2309
+ vertical = Math.abs(p.x - q.x) < .5;
2310
+ continue;
2311
+ }
2312
+ const endVertical = to.side === "top" || to.side === "bottom";
2313
+ if (last && vertical !== endVertical) pts.push(vertical ? {
2314
+ x: p.x,
2315
+ y: q.y
2316
+ } : {
2317
+ x: q.x,
2318
+ y: p.y
2319
+ });
2320
+ else if (vertical) {
2321
+ const my = (p.y + q.y) / 2;
2322
+ pts.push({
2323
+ x: p.x,
2324
+ y: my
2325
+ }, {
2326
+ x: q.x,
2327
+ y: my
2328
+ });
2329
+ } else {
2330
+ const mx = (p.x + q.x) / 2;
2331
+ pts.push({
2332
+ x: mx,
2333
+ y: p.y
2334
+ }, {
2335
+ x: mx,
2336
+ y: q.y
2337
+ });
2338
+ }
2339
+ pts.push(q);
2340
+ }
2341
+ pts.push(b);
2342
+ const clean = simplify(pts);
2343
+ return {
2344
+ d: roundedPath(clean, radius),
2345
+ points: clean
2346
+ };
2347
+ }
2348
+ /** Drops repeated and collinear points. */
2349
+ function simplify(pts) {
2350
+ const out = [];
2351
+ for (const p of pts) {
2352
+ const last = out[out.length - 1];
2353
+ if (last && Math.abs(last.x - p.x) < .5 && Math.abs(last.y - p.y) < .5) continue;
2354
+ const prev = out[out.length - 2];
2355
+ if (prev && last) {
2356
+ if (Math.abs(prev.x - last.x) < .5 && Math.abs(last.x - p.x) < .5 || Math.abs(prev.y - last.y) < .5 && Math.abs(last.y - p.y) < .5) out.pop();
2357
+ }
2358
+ out.push(p);
2359
+ }
2360
+ return out;
2361
+ }
2362
+ /** Straight segments with rounded corners. */
2363
+ function roundedPath(pts, radius) {
2364
+ let d = `M ${P(pts[0])}`;
2365
+ for (let i = 1; i < pts.length - 1; i++) {
2366
+ const p = pts[i - 1];
2367
+ const c = pts[i];
2368
+ const n = pts[i + 1];
2369
+ const r = Math.min(radius, dist(p, c) / 2, dist(c, n) / 2);
2370
+ const inP = add(c, {
2371
+ x: Math.sign(p.x - c.x),
2372
+ y: Math.sign(p.y - c.y)
2373
+ }, r);
2374
+ const outP = add(c, {
2375
+ x: Math.sign(n.x - c.x),
2376
+ y: Math.sign(n.y - c.y)
2377
+ }, r);
2378
+ d += ` L ${P(inP)} Q ${P(c)} ${P(outP)}`;
2379
+ }
2380
+ d += ` L ${P(pts[pts.length - 1])}`;
2381
+ return d;
2382
+ }
2383
+ /** The point halfway along a polyline: where a label goes without a layout hint. */
2384
+ function midpoint(points) {
2385
+ if (points.length === 0) return {
2386
+ x: 0,
2387
+ y: 0
2388
+ };
2389
+ let total = 0;
2390
+ for (let i = 1; i < points.length; i++) total += dist(points[i - 1], points[i]);
2391
+ let left = total / 2;
2392
+ for (let i = 1; i < points.length; i++) {
2393
+ const l = dist(points[i - 1], points[i]);
2394
+ if (left <= l && l > 0) {
2395
+ const t = left / l;
2396
+ return {
2397
+ x: points[i - 1].x + (points[i].x - points[i - 1].x) * t,
2398
+ y: points[i - 1].y + (points[i].y - points[i - 1].y) * t
2399
+ };
2400
+ }
2401
+ left -= l;
2402
+ }
2403
+ return points[points.length - 1];
2404
+ }
2405
+ /** A cubic Bézier point, for the label of a direct spline. */
2406
+ function bezierPoint(p0, p1, p2, p3, t) {
2407
+ const u = 1 - t;
2408
+ return {
2409
+ x: u * u * u * p0.x + 3 * u * u * t * p1.x + 3 * u * t * t * p2.x + t * t * t * p3.x,
2410
+ y: u * u * u * p0.y + 3 * u * u * t * p1.y + 3 * u * t * t * p2.y + t * t * t * p3.y
2411
+ };
2412
+ }
2413
+ /** Distance from a point to a polyline: hit testing edges in the editor. */
2414
+ function distanceToPolyline(p, points) {
2415
+ let best = Infinity;
2416
+ for (let i = 1; i < points.length; i++) {
2417
+ const a = points[i - 1];
2418
+ const b = points[i];
2419
+ const l2 = (b.x - a.x) ** 2 + (b.y - a.y) ** 2;
2420
+ const t = l2 === 0 ? 0 : Math.max(0, Math.min(1, ((p.x - a.x) * (b.x - a.x) + (p.y - a.y) * (b.y - a.y)) / l2));
2421
+ best = Math.min(best, dist(p, {
2422
+ x: a.x + t * (b.x - a.x),
2423
+ y: a.y + t * (b.y - a.y)
2424
+ }));
2425
+ }
2426
+ return best;
2427
+ }
2428
+ /**
2429
+ * Orthogonal routing through the layout's bends with as few corners as the
2430
+ * obstacles allow: the route keeps its lane while `clear` says the way along
2431
+ * the flow is free, and changes lane in the gap between two ranks (where no
2432
+ * node stands) only when the next bend would be blocked. `mainX` is true when
2433
+ * the flow runs along x (LR).
2434
+ */
2435
+ function orthogonalThrough(from, to, waypoints, mainX, clear, radius = 10, through) {
2436
+ const m = (p) => mainX ? p.x : p.y;
2437
+ const c = (p) => mainX ? p.y : p.x;
2438
+ const at = (mv, cv) => mainX ? {
2439
+ x: mv,
2440
+ y: cv
2441
+ } : {
2442
+ x: cv,
2443
+ y: mv
2444
+ };
2445
+ const s0 = add(from.point, NORMAL[from.side], STUB);
2446
+ const s1 = add(to.point, NORMAL[to.side], STUB);
2447
+ const pts = [from.point, s0];
2448
+ let cur = s0;
2449
+ let prevM = m(from.point) + Math.sign(m(s0) - m(from.point) || 1) * 6;
2450
+ /** Changes lane to `cv` somewhere between prevM and nextM, preferring the middle of the gap. */
2451
+ const shift = (cv, nextM) => {
2452
+ let bend = (prevM + nextM) / 2;
2453
+ for (const t of [
2454
+ .5,
2455
+ .3,
2456
+ .7,
2457
+ .15,
2458
+ .85,
2459
+ .05,
2460
+ .95
2461
+ ]) {
2462
+ const b = prevM + (nextM - prevM) * t;
2463
+ if (clear(at(b, c(cur)), at(b, cv)) && clear(at(m(cur), c(cur)), at(b, c(cur)))) {
2464
+ bend = b;
2465
+ break;
2466
+ }
2467
+ }
2468
+ pts.push(at(bend, c(cur)), at(bend, cv));
2469
+ cur = at(bend, cv);
2470
+ };
2471
+ for (const w of waypoints) {
2472
+ const required = !!through && Math.abs(w.x - through.x) < .5 && Math.abs(w.y - through.y) < .5;
2473
+ if (Math.abs(c(w) - c(cur)) < 1 || !required && clear(cur, at(m(w), c(cur)))) {
2474
+ prevM = m(w);
2475
+ continue;
2476
+ }
2477
+ shift(c(w), m(w));
2478
+ prevM = m(w);
2479
+ }
2480
+ if (Math.abs(c(cur) - c(s1)) >= 1) shift(c(s1), m(s1));
2481
+ pts.push(s1, to.point);
2482
+ const clean = simplify(pts);
2483
+ return {
2484
+ d: roundedPath(clean, radius),
2485
+ points: clean
2486
+ };
2487
+ }
2488
+ /** Whether an axis-aligned segment passes through a box (grown by `pad`). */
2489
+ function segmentHitsBox(a, b, box, pad = 0) {
2490
+ const x0 = Math.min(a.x, b.x);
2491
+ const x1 = Math.max(a.x, b.x);
2492
+ const y0 = Math.min(a.y, b.y);
2493
+ const y1 = Math.max(a.y, b.y);
2494
+ const bx0 = box.x - box.width / 2 - pad;
2495
+ const bx1 = box.x + box.width / 2 + pad;
2496
+ const by0 = box.y - box.height / 2 - pad;
2497
+ const by1 = box.y + box.height / 2 + pad;
2498
+ return x1 > bx0 && x0 < bx1 && y1 > by0 && y0 < by1;
2499
+ }
2500
+ /** The point of an orthogonal route at main-axis coordinate `mv` (for its label), if a segment along the flow covers it. */
2501
+ function pointAtMain(points, mv, mainX) {
2502
+ for (let i = 1; i < points.length; i++) {
2503
+ const a = points[i - 1];
2504
+ const b = points[i];
2505
+ if (!(mainX ? Math.abs(a.y - b.y) < .5 : Math.abs(a.x - b.x) < .5)) continue;
2506
+ const lo = Math.min(mainX ? a.x : a.y, mainX ? b.x : b.y);
2507
+ const hi = Math.max(mainX ? a.x : a.y, mainX ? b.x : b.y);
2508
+ if (mv >= lo && mv <= hi) return mainX ? {
2509
+ x: mv,
2510
+ y: a.y
2511
+ } : {
2512
+ x: a.x,
2513
+ y: mv
2514
+ };
2515
+ }
2516
+ }
2517
+ // src/layout/channels.ts
2518
+ /**
2519
+ * Back edges — transitions against the flow (retry, resend, give up…) — are
2520
+ * routed through a channel beside the graph instead of across it: out of the
2521
+ * source's side facing the channel, along a lane of their own, and back into
2522
+ * the target from the same side. Lanes are assigned like intervals on a line,
2523
+ * so parallel back edges never share one; the shortest take the inner lanes.
2524
+ * The side (below/above in LR, right/left in TB) is picked per edge so its
2525
+ * drops do not cut through other nodes or foreign groups.
2526
+ */
2527
+ var GAP = 30;
2528
+ var LANE = 16;
2529
+ var LANE_LABELLED = 30;
2530
+ /** Whether `target` lies wholly behind `source` along the flow. */
2531
+ function isBehind(source, target, direction) {
2532
+ return direction === "LR" ? target.x + target.width / 2 <= source.x - source.width / 2 : target.y + target.height / 2 <= source.y - source.height / 2;
2533
+ }
2534
+ function routeChannels(edges, ctx) {
2535
+ const out = /* @__PURE__ */ new Map();
2536
+ if (edges.length === 0) return out;
2537
+ const lr = ctx.direction === "LR";
2538
+ const m = (p) => lr ? p.x : p.y;
2539
+ const c = (p) => lr ? p.y : p.x;
2540
+ const mHalf = (b) => (lr ? b.width : b.height) / 2;
2541
+ const cHalf = (b) => (lr ? b.height : b.width) / 2;
2542
+ const at = (mv, cv) => lr ? {
2543
+ x: mv,
2544
+ y: cv
2545
+ } : {
2546
+ x: cv,
2547
+ y: mv
2548
+ };
2549
+ let cMin = Infinity;
2550
+ let cMax = -Infinity;
2551
+ for (const [id, b] of ctx.nodes) {
2552
+ cMin = Math.min(cMin, c(b) - cHalf(b));
2553
+ cMax = Math.max(cMax, c(b) + cHalf(b) + (ctx.extraBelow?.(id) ?? 0));
2554
+ }
2555
+ for (const g of ctx.groups.values()) {
2556
+ cMin = Math.min(cMin, c(g) - cHalf(g));
2557
+ cMax = Math.max(cMax, c(g) + cHalf(g));
2558
+ }
2559
+ /** Things a straight segment along the cross axis at `mv`, from `c0` to the graph's edge on side `s`, would cut through. */
2560
+ const blockedFrom = (skip, mv, c0, s, own) => {
2561
+ let n = 0;
2562
+ const hit = (b) => Math.abs(m(b) - mv) < mHalf(b) + 6 && (s > 0 ? c(b) + cHalf(b) > c0 + 1 : c(b) - cHalf(b) < c0 - 1);
2563
+ for (const [other, b] of ctx.nodes) if (other !== skip && hit(b)) n++;
2564
+ for (const [gid, g] of ctx.groups) if (!own.has(gid) && hit(g)) n += 2;
2565
+ return n;
2566
+ };
2567
+ const used = /* @__PURE__ */ new Map();
2568
+ /**
2569
+ * How one end reaches the channel on side `s`: straight out of the side
2570
+ * facing the channel at the first clear offset, else out of the side facing
2571
+ * along the flow into the gap between ranks, else straight (blocked).
2572
+ */
2573
+ const endPlan = (id, box, own, s, isSource, spread, commit) => {
2574
+ const facing = lr ? s > 0 ? "bottom" : "top" : s > 0 ? "right" : "left";
2575
+ const key = `${id}\u0000${facing}`;
2576
+ const taken = used.get(key) ?? [];
2577
+ const offsets = spread ? [
2578
+ .5,
2579
+ .3,
2580
+ .7,
2581
+ .15,
2582
+ .85
2583
+ ] : [.5];
2584
+ const edgeC = c(box) + s * cHalf(box);
2585
+ for (const o of offsets) {
2586
+ if (taken.some((t) => Math.abs(t - o) < .1)) continue;
2587
+ const p = sidePoint(box, facing, o);
2588
+ if (blockedFrom(id, m(p), edgeC, s, own) === 0) {
2589
+ if (commit) used.set(key, [...taken, o]);
2590
+ return {
2591
+ point: p,
2592
+ side: facing,
2593
+ dropM: m(p),
2594
+ cost: 0
2595
+ };
2596
+ }
2597
+ }
2598
+ const gapSides = lr ? isSource ? ["left", "right"] : ["right", "left"] : isSource ? ["top", "bottom"] : ["bottom", "top"];
2599
+ let best;
2600
+ for (const gapSide of gapSides) {
2601
+ const p = sidePoint(box, gapSide, .5);
2602
+ const dir = gapSide === "left" || gapSide === "top" ? -1 : 1;
2603
+ const dropM = m(p) + dir * 18;
2604
+ const cost = blockedFrom(id, dropM, c(box), s, own);
2605
+ const plan = {
2606
+ point: p,
2607
+ side: gapSide,
2608
+ dropM,
2609
+ stub: at(dropM, c(p)),
2610
+ cost
2611
+ };
2612
+ if (cost === 0) return plan;
2613
+ if (!best || cost < best.cost) best = plan;
2614
+ }
2615
+ const p0 = sidePoint(box, facing, .5);
2616
+ const straight = {
2617
+ point: p0,
2618
+ side: facing,
2619
+ dropM: m(p0),
2620
+ cost: blockedFrom(id, m(p0), edgeC, s, own)
2621
+ };
2622
+ return best && best.cost < straight.cost ? best : straight;
2623
+ };
2624
+ const plans = edges.map((e) => {
2625
+ const cost = (s) => endPlan(e.from, e.source, e.sourceGroups, s, true, e.spreadSource, false).cost + endPlan(e.to, e.target, e.targetGroups, s, false, e.spreadTarget, false).cost;
2626
+ const side = cost(-1) < cost(1) ? -1 : 1;
2627
+ const src = endPlan(e.from, e.source, e.sourceGroups, side, true, e.spreadSource, true);
2628
+ const dst = endPlan(e.to, e.target, e.targetGroups, side, false, e.spreadTarget, true);
2629
+ return {
2630
+ e,
2631
+ side,
2632
+ lane: 0,
2633
+ lo: Math.min(src.dropM, dst.dropM),
2634
+ hi: Math.max(src.dropM, dst.dropM),
2635
+ src,
2636
+ dst
2637
+ };
2638
+ });
2639
+ for (const s of [1, -1]) {
2640
+ const side = plans.filter((p) => p.side === s).sort((a, b) => a.hi - a.lo - (b.hi - b.lo) || a.lo - b.lo);
2641
+ const lanes = [];
2642
+ for (const p of side) {
2643
+ let i = lanes.findIndex((lane) => lane.every((r) => p.hi + 12 < r.lo || p.lo - 12 > r.hi));
2644
+ if (i < 0) {
2645
+ lanes.push([]);
2646
+ i = lanes.length - 1;
2647
+ }
2648
+ lanes[i].push({
2649
+ lo: p.lo,
2650
+ hi: p.hi
2651
+ });
2652
+ p.lane = i;
2653
+ }
2654
+ }
2655
+ const labelled = (s) => plans.some((p) => p.side === s && p.e.label);
2656
+ const gap = (s) => lr && labelled(s) ? LANE_LABELLED : LANE;
2657
+ const base = (s) => s > 0 ? cMax + GAP : cMin - GAP;
2658
+ for (const p of plans) {
2659
+ const s = p.side;
2660
+ const laneC = base(s) + s * p.lane * gap(s);
2661
+ const points = [p.src.point];
2662
+ if (p.src.stub) points.push(p.src.stub);
2663
+ points.push(at(p.src.dropM, laneC), at(p.dst.dropM, laneC));
2664
+ if (p.dst.stub) points.push(p.dst.stub);
2665
+ points.push(p.dst.point);
2666
+ const route = {
2667
+ d: roundedPath(points, 10),
2668
+ points,
2669
+ from: {
2670
+ point: p.src.point,
2671
+ side: p.src.side
2672
+ },
2673
+ to: {
2674
+ point: p.dst.point,
2675
+ side: p.dst.side
2676
+ }
2677
+ };
2678
+ if (p.e.label) route.label = at((p.src.dropM + p.dst.dropM) / 2, laneC);
2679
+ out.set(p.e.id, route);
2680
+ }
2681
+ return out;
2682
+ }
2683
+ // src/layout/labels.ts
2684
+ var hits = (a, b, pad) => Math.abs(a.x - b.x) < (a.width + b.width) / 2 + pad && Math.abs(a.y - b.y) < (a.height + b.height) / 2 + pad;
2685
+ /** Candidate centres along a polyline (longest segment first), then beside it. */
2686
+ function candidates(points, label) {
2687
+ const segs = [];
2688
+ for (let i = 1; i < points.length; i++) {
2689
+ const a = points[i - 1];
2690
+ const b = points[i];
2691
+ segs.push({
2692
+ a,
2693
+ b,
2694
+ len: Math.hypot(b.x - a.x, b.y - a.y)
2695
+ });
2696
+ }
2697
+ segs.sort((u, v) => v.len - u.len);
2698
+ const out = [];
2699
+ for (const s of segs) for (const t of [
2700
+ .5,
2701
+ .35,
2702
+ .65,
2703
+ .2,
2704
+ .8,
2705
+ .1,
2706
+ .9
2707
+ ]) out.push({
2708
+ x: s.a.x + (s.b.x - s.a.x) * t,
2709
+ y: s.a.y + (s.b.y - s.a.y) * t
2710
+ });
2711
+ const beside = [];
2712
+ for (const s of segs) {
2713
+ const horizontal = Math.abs(s.a.y - s.b.y) < .5;
2714
+ const dx = horizontal ? 0 : label.width / 2 + 3;
2715
+ const dy = horizontal ? label.height / 2 + 3 : 0;
2716
+ for (const t of [
2717
+ .5,
2718
+ .3,
2719
+ .7
2720
+ ]) {
2721
+ const p = {
2722
+ x: s.a.x + (s.b.x - s.a.x) * t,
2723
+ y: s.a.y + (s.b.y - s.a.y) * t
2724
+ };
2725
+ beside.push({
2726
+ x: p.x - dx,
2727
+ y: p.y - dy
2728
+ }, {
2729
+ x: p.x + dx,
2730
+ y: p.y + dy
2731
+ });
2732
+ }
2733
+ }
2734
+ return [...out, ...beside];
2735
+ }
2736
+ /**
2737
+ * Moves labels off nodes and off each other, in the given order (earlier
2738
+ * labels keep their place). Mutates the labels' `x`/`y`.
2739
+ */
2740
+ function placeLabels(items, nodes) {
2741
+ const boxes = [...nodes];
2742
+ const placed = [];
2743
+ const free = (l) => !boxes.some((b) => hits(l, b, 4)) && !placed.some((p) => hits(l, p, 3));
2744
+ for (const { label, points } of items) {
2745
+ if (!free(label)) {
2746
+ for (const c of candidates(points, label)) if (free({
2747
+ ...label,
2748
+ x: c.x,
2749
+ y: c.y
2750
+ })) {
2751
+ label.x = c.x;
2752
+ label.y = c.y;
2753
+ break;
2754
+ }
2755
+ }
2756
+ placed.push(label);
2757
+ }
2758
+ }
2759
+ // src/layout/index.ts
2760
+ /**
2761
+ * Graph layout: node sizes from types and labels, positions from the layered
2762
+ * layout (or the model's own positions), and routed edges with label boxes.
2763
+ * Deterministic: the same model and options give the same geometry.
2764
+ */
2765
+ var LABEL_FONT = {
2766
+ size: 14,
2767
+ weight: 500
2768
+ };
2769
+ var DESCRIPTION_FONT = { size: 12 };
2770
+ var EDGE_FONT = {
2771
+ size: 12,
2772
+ weight: 500
2773
+ };
2774
+ /** The label beside a start/end circle. */
2775
+ var OUTSIDE_FONT = { size: 13 };
2776
+ var ICON_ROOM = 30;
2777
+ /** A node's size and wrapped label, before it is placed. */
2778
+ function measureNode(node, registry, measure) {
2779
+ const def = typeOf(registry, node.type);
2780
+ const label = node.label ?? "";
2781
+ if (def.size) return {
2782
+ id: node.id,
2783
+ width: def.size.width,
2784
+ height: def.size.height,
2785
+ lines: label ? wrapText(label, 160, OUTSIDE_FONT, measure, 2) : [],
2786
+ descriptionLines: []
2787
+ };
2788
+ const min = def.minWidth ?? 120;
2789
+ const max = def.maxWidth ?? 220;
2790
+ const icon = def.icon && def.shape !== "diamond" ? ICON_ROOM : 0;
2791
+ const diamond = def.shape === "diamond";
2792
+ const room = diamond ? max * .62 : max - 32 - icon - 10;
2793
+ const lines = wrapText(label, room, LABEL_FONT, measure, diamond ? 2 : 3);
2794
+ const descriptionLines = node.description && !diamond ? wrapText(node.description, room, DESCRIPTION_FONT, measure, 2) : [];
2795
+ const textWidth = Math.max(0, ...lines.map((l) => measure(l, LABEL_FONT)), ...descriptionLines.map((l) => measure(l, DESCRIPTION_FONT)));
2796
+ const width = diamond ? Math.min(max, Math.max(min, textWidth / .62 + 8)) : Math.min(max, Math.max(min, Math.ceil(textWidth + 32 + icon + 10)));
2797
+ const textHeight = lines.length * 18 + descriptionLines.length * 15;
2798
+ const height = Math.max(def.height ?? 44, diamond ? textHeight * 2 + 20 : textHeight + 24);
2799
+ return {
2800
+ id: node.id,
2801
+ width: Math.round(width),
2802
+ height: Math.round(height),
2803
+ lines,
2804
+ descriptionLines
2805
+ };
2806
+ }
2807
+ function edgeLabelText(e) {
2808
+ return e.label ?? e.condition ?? "";
2809
+ }
2810
+ function measureEdgeLabel(e, measure, extra = 0) {
2811
+ const text = edgeLabelText(e);
2812
+ if (!text) return void 0;
2813
+ const lines = wrapText(text, 160, EDGE_FONT, measure, 2);
2814
+ const width = Math.max(...lines.map((l) => measure(l, EDGE_FONT))) + 16 + extra;
2815
+ return {
2816
+ width: Math.ceil(width),
2817
+ height: lines.length * 15 + 8,
2818
+ lines
2819
+ };
2820
+ }
2821
+ /** Lays out a graph. */
2822
+ function layoutGraph(graph, options = {}) {
2823
+ const registry = options.registry ?? defaultRegistry;
2824
+ const measure = options.measure ?? approximateMeasurer;
2825
+ const direction = options.direction ?? graph.meta?.direction ?? "LR";
2826
+ const routing = options.routing ?? "orthogonal";
2827
+ const nodeIds = new Set(graph.nodes.map((n) => n.id));
2828
+ const groupIds = new Set((graph.groups ?? []).map((g) => g.id));
2829
+ const sized = new Map(graph.nodes.map((n) => [n.id, measureNode(n, registry, measure)]));
2830
+ const edges = graph.edges.filter((e) => nodeIds.has(e.from) && nodeIds.has(e.to));
2831
+ const labels = new Map(edges.map((e) => [e.id, measureEdgeLabel(e, measure, options.edgeLabelExtra ?? 0)]));
2832
+ const fixed = options.ignorePositions ? [] : graph.nodes.filter((n) => n.position);
2833
+ const centres = /* @__PURE__ */ new Map();
2834
+ let waypoints = /* @__PURE__ */ new Map();
2835
+ let labelAt = /* @__PURE__ */ new Map();
2836
+ let groupBoxes = /* @__PURE__ */ new Map();
2837
+ if (fixed.length < graph.nodes.length) {
2838
+ let result = runLayered(graph, sized, edges, labels, direction, groupIds, options);
2839
+ const back = new Set(edges.filter((e) => channelled(e, graph) && result.nodes.has(e.from) && result.nodes.has(e.to)).filter((e) => isBehind(result.nodes.get(e.from), result.nodes.get(e.to), direction)).map((e) => e.id));
2840
+ if (back.size > 0) result = runLayered(graph, sized, edges.filter((e) => !back.has(e.id)), labels, direction, groupIds, options);
2841
+ for (const [id, b] of result.nodes) centres.set(id, {
2842
+ x: b.x,
2843
+ y: b.y
2844
+ });
2845
+ for (const [id, r] of result.edges) {
2846
+ waypoints.set(id, r.points);
2847
+ if (r.label) labelAt.set(id, r.label);
2848
+ }
2849
+ groupBoxes = new Map([...result.clusters].map(([id, b]) => [id, b]));
2850
+ if (fixed.length > 0) {
2851
+ let dx = 0;
2852
+ let dy = 0;
2853
+ for (const n of fixed) {
2854
+ const c = centres.get(n.id);
2855
+ dx += n.position.x - c.x;
2856
+ dy += n.position.y - c.y;
2857
+ }
2858
+ dx /= fixed.length;
2859
+ dy /= fixed.length;
2860
+ const placed = [];
2861
+ for (const n of fixed) {
2862
+ centres.set(n.id, { ...n.position });
2863
+ placed.push({
2864
+ ...n.position,
2865
+ width: sized.get(n.id).width,
2866
+ height: sized.get(n.id).height
2867
+ });
2868
+ }
2869
+ for (const n of graph.nodes) {
2870
+ if (n.position) continue;
2871
+ const c = centres.get(n.id);
2872
+ const box = {
2873
+ x: c.x + dx,
2874
+ y: c.y + dy,
2875
+ width: sized.get(n.id).width,
2876
+ height: sized.get(n.id).height
2877
+ };
2878
+ let guard = 0;
2879
+ while (placed.some((p) => overlaps(p, box, 16)) && guard++ < 400) if (direction === "LR") box.y += 12;
2880
+ else box.x += 12;
2881
+ centres.set(n.id, {
2882
+ x: box.x,
2883
+ y: box.y
2884
+ });
2885
+ placed.push(box);
2886
+ }
2887
+ waypoints = /* @__PURE__ */ new Map();
2888
+ labelAt = /* @__PURE__ */ new Map();
2889
+ groupBoxes = /* @__PURE__ */ new Map();
2890
+ }
2891
+ } else for (const n of graph.nodes) centres.set(n.id, { ...n.position });
2892
+ const nodes = /* @__PURE__ */ new Map();
2893
+ for (const n of graph.nodes) {
2894
+ const s = sized.get(n.id);
2895
+ const c = centres.get(n.id);
2896
+ nodes.set(n.id, {
2897
+ ...s,
2898
+ x: c.x,
2899
+ y: c.y
2900
+ });
2901
+ }
2902
+ const groups = /* @__PURE__ */ new Map();
2903
+ for (const g of graph.groups ?? []) {
2904
+ const box = groupBoxes.get(g.id) ?? frameAround(g.id, graph, nodes);
2905
+ if (box) groups.set(g.id, {
2906
+ id: g.id,
2907
+ label: g.label,
2908
+ ...box
2909
+ });
2910
+ }
2911
+ const routed = routeEdges(graph, nodes, {
2912
+ direction,
2913
+ routing,
2914
+ registry,
2915
+ waypoints,
2916
+ labelAt,
2917
+ labels,
2918
+ groups
2919
+ });
2920
+ return {
2921
+ direction,
2922
+ routing,
2923
+ nodes,
2924
+ edges: routed,
2925
+ groups,
2926
+ waypoints,
2927
+ bounds: boundsOf(nodes, routed, groups)
2928
+ };
2929
+ }
2930
+ function runLayered(graph, sized, edges, labels, direction, groupIds, options) {
2931
+ const registry = options.registry ?? defaultRegistry;
2932
+ const nodes = graph.nodes.map((n) => {
2933
+ const s = sized.get(n.id);
2934
+ const def = typeOf(registry, n.type);
2935
+ let w = s.width;
2936
+ let h = s.height;
2937
+ if (def.labelOutside && s.lines.length) {
2938
+ if (direction === "LR") h += (s.lines.length * 18 + 4) * 2;
2939
+ else w += (Math.max(...s.lines.map((l) => (options.measure ?? approximateMeasurer)(l, OUTSIDE_FONT))) + 10) * 2;
2940
+ }
2941
+ const node = {
2942
+ id: n.id,
2943
+ width: w,
2944
+ height: h
2945
+ };
2946
+ if (n.group && groupIds.has(n.group)) node.cluster = n.group;
2947
+ return node;
2948
+ });
2949
+ const layoutEdges = edges.map((e) => {
2950
+ const l = labels.get(e.id);
2951
+ const le = {
2952
+ id: e.id,
2953
+ from: e.from,
2954
+ to: e.to
2955
+ };
2956
+ if (l) le.label = {
2957
+ width: l.width,
2958
+ height: l.height
2959
+ };
2960
+ if (e.kind === "association") le.weight = .2;
2961
+ return le;
2962
+ });
2963
+ const clusters = (graph.groups ?? []).map((g) => {
2964
+ const c = { id: g.id };
2965
+ if (g.parent && groupIds.has(g.parent)) c.parent = g.parent;
2966
+ if (g.label) c.label = {
2967
+ width: approximateMeasurer(g.label, {
2968
+ size: 12,
2969
+ weight: 600
2970
+ }) + 8,
2971
+ height: 18
2972
+ };
2973
+ return c;
2974
+ });
2975
+ try {
2976
+ return layout({
2977
+ direction,
2978
+ nodes,
2979
+ edges: layoutEdges,
2980
+ clusters,
2981
+ nodeSep: options.nodeSep ?? 36,
2982
+ rankSep: options.rankSep ?? (direction === "LR" ? 26 : 22),
2983
+ edgeSep: 16,
2984
+ clusterPadding: 18,
2985
+ transpose: true
2986
+ });
2987
+ } catch {
2988
+ return layout({
2989
+ direction,
2990
+ nodes: nodes.map((n) => ({
2991
+ id: n.id,
2992
+ width: n.width,
2993
+ height: n.height
2994
+ })),
2995
+ edges: layoutEdges,
2996
+ nodeSep: 36,
2997
+ rankSep: 24,
2998
+ transpose: true
2999
+ });
3000
+ }
3001
+ }
3002
+ /**
3003
+ * Routes edges between placed nodes. Anchors on one side of a rectangular
3004
+ * node are spread along it so parallel edges do not merge.
3005
+ */
3006
+ function routeEdges(graph, nodes, ctx, only) {
3007
+ const byId = new Map(graph.nodes.map((n) => [n.id, n]));
3008
+ const prefer = ctx.direction === "TB" ? "vertical" : "horizontal";
3009
+ const parentOf = new Map((graph.groups ?? []).map((g) => [g.id, g.parent]));
3010
+ const groupsOf = (id) => {
3011
+ const set = /* @__PURE__ */ new Set();
3012
+ let g = byId.get(id)?.group;
3013
+ while (g !== void 0 && !set.has(g)) {
3014
+ set.add(g);
3015
+ g = parentOf.get(g);
3016
+ }
3017
+ return set;
3018
+ };
3019
+ const clearOf = (e) => {
3020
+ const own = /* @__PURE__ */ new Set([...groupsOf(e.from), ...groupsOf(e.to)]);
3021
+ const boxes = [];
3022
+ for (const [id, b] of nodes) if (id !== e.from && id !== e.to) boxes.push(b);
3023
+ for (const [gid, g] of ctx.groups ?? []) if (!own.has(gid)) boxes.push(g);
3024
+ return (u, v) => !boxes.some((b) => segmentHitsBox(u, v, b, 6));
3025
+ };
3026
+ const pending = [];
3027
+ const backs = [];
3028
+ const out = /* @__PURE__ */ new Map();
3029
+ for (const e of graph.edges) {
3030
+ const a = byId.get(e.from);
3031
+ const b = byId.get(e.to);
3032
+ const ab = nodes.get(e.from);
3033
+ const bb = nodes.get(e.to);
3034
+ if (!a || !b || !ab || !bb) continue;
3035
+ if (e.from === e.to) {
3036
+ if (only && !only.has(e.id)) continue;
3037
+ out.set(e.id, selfLoop(e, ab, ctx));
3038
+ continue;
3039
+ }
3040
+ if (channelled(e, graph) && isBehind(ab, bb, ctx.direction)) {
3041
+ backs.push({
3042
+ id: e.id,
3043
+ from: e.from,
3044
+ to: e.to,
3045
+ source: ab,
3046
+ target: bb,
3047
+ sourceGroups: groupsOf(e.from),
3048
+ targetGroups: groupsOf(e.to),
3049
+ label: ctx.labels.get(e.id),
3050
+ spreadSource: spreadable(typeOf(ctx.registry, a.type).shape),
3051
+ spreadTarget: spreadable(typeOf(ctx.registry, b.type).shape)
3052
+ });
3053
+ continue;
3054
+ }
3055
+ const wps = ctx.waypoints.get(e.id) ?? [];
3056
+ const end = (node, box, portId, toward) => {
3057
+ const port = portId ? portsOf(ctx.registry, node).find((p) => p.id === portId) : void 0;
3058
+ if (port) return {
3059
+ node,
3060
+ box,
3061
+ side: port.side,
3062
+ offset: port.offset ?? .5,
3063
+ fixed: true,
3064
+ toward
3065
+ };
3066
+ return {
3067
+ node,
3068
+ box,
3069
+ side: facingSide(box, toward, prefer),
3070
+ fixed: false,
3071
+ toward
3072
+ };
3073
+ };
3074
+ pending.push({
3075
+ edge: e,
3076
+ ends: [end(a, ab, e.fromPort, wps[0] ?? bb), end(b, bb, e.toPort, wps[wps.length - 1] ?? ab)],
3077
+ wps
3078
+ });
3079
+ }
3080
+ const slots = /* @__PURE__ */ new Map();
3081
+ for (const p of pending) for (const i of [0, 1]) {
3082
+ const end = p.ends[i];
3083
+ if (end.fixed) continue;
3084
+ const key = `${end.node.id}\u0000${end.side}`;
3085
+ const list = slots.get(key) ?? [];
3086
+ list.push({
3087
+ p,
3088
+ i
3089
+ });
3090
+ slots.set(key, list);
3091
+ }
3092
+ for (const list of slots.values()) {
3093
+ const first = list[0].p.ends[list[0].i];
3094
+ const shape = typeOf(ctx.registry, first.node.type).shape;
3095
+ if (list.length === 1 || !spreadable(shape)) {
3096
+ for (const { p, i } of list) p.ends[i].offset = .5;
3097
+ continue;
3098
+ }
3099
+ const horizontalSide = first.side === "top" || first.side === "bottom";
3100
+ list.sort((u, v) => {
3101
+ const a = u.p.ends[u.i].toward;
3102
+ const b = v.p.ends[v.i].toward;
3103
+ return horizontalSide ? a.x - b.x || a.y - b.y : a.y - b.y || a.x - b.x;
3104
+ });
3105
+ const span = Math.min(.7, .18 * (list.length - 1));
3106
+ list.forEach(({ p, i }, k) => p.ends[i].offset = .5 - span / 2 + span * k / (list.length - 1));
3107
+ }
3108
+ for (const p of pending) {
3109
+ if (only && !only.has(p.edge.id)) continue;
3110
+ const [s, t] = p.ends;
3111
+ const from = {
3112
+ point: anchorPoint(s.box, s.side, s.offset ?? .5, ctx, s.node),
3113
+ side: s.side
3114
+ };
3115
+ const to = {
3116
+ point: anchorPoint(t.box, t.side, t.offset ?? .5, ctx, t.node),
3117
+ side: t.side
3118
+ };
3119
+ const r = p.wps.length > 0 ? orthogonalThrough(from, to, p.wps, ctx.direction === "LR", clearOf(p.edge), ctx.routing === "spline" ? 28 : 10, ctx.labelAt.get(p.edge.id)) : route({
3120
+ from,
3121
+ to,
3122
+ waypoints: p.wps,
3123
+ routing: ctx.routing
3124
+ });
3125
+ const geo = {
3126
+ id: p.edge.id,
3127
+ d: r.d,
3128
+ points: r.points,
3129
+ from,
3130
+ to
3131
+ };
3132
+ const l = ctx.labels.get(p.edge.id);
3133
+ if (l) {
3134
+ let at = ctx.labelAt.get(p.edge.id);
3135
+ if (at) at = pointAtMain(r.points, ctx.direction === "LR" ? at.x : at.y, ctx.direction === "LR") ?? midpoint(r.points);
3136
+ if (!at) at = ctx.routing === "spline" && p.wps.length === 0 && r.points.length === 4 ? bezierPoint(r.points[0], r.points[1], r.points[2], r.points[3], .5) : midpoint(r.points);
3137
+ geo.label = {
3138
+ x: at.x,
3139
+ y: at.y,
3140
+ width: l.width,
3141
+ height: l.height,
3142
+ lines: l.lines
3143
+ };
3144
+ }
3145
+ out.set(p.edge.id, geo);
3146
+ }
3147
+ const channels = routeChannels(backs, {
3148
+ direction: ctx.direction,
3149
+ nodes,
3150
+ groups: ctx.groups ?? /* @__PURE__ */ new Map(),
3151
+ extraBelow: (id) => {
3152
+ const n = byId.get(id);
3153
+ const def = n && typeOf(ctx.registry, n.type);
3154
+ return ctx.direction === "LR" && def?.labelOutside && n?.label ? 42 : 0;
3155
+ }
3156
+ });
3157
+ for (const b of backs) {
3158
+ if (only && !only.has(b.id)) continue;
3159
+ const r = channels.get(b.id);
3160
+ const geo = {
3161
+ id: b.id,
3162
+ d: r.d,
3163
+ points: r.points,
3164
+ from: r.from,
3165
+ to: r.to
3166
+ };
3167
+ const l = ctx.labels.get(b.id);
3168
+ if (l && r.label) geo.label = {
3169
+ x: r.label.x,
3170
+ y: r.label.y,
3171
+ width: l.width,
3172
+ height: l.height,
3173
+ lines: l.lines
3174
+ };
3175
+ out.set(b.id, geo);
3176
+ }
3177
+ if (!only) placeLabels([...out.values()].filter((g) => g.label).sort((a, b) => Number(backs.some((x) => x.id === a.id)) - Number(backs.some((x) => x.id === b.id))).map((g) => ({
3178
+ label: g.label,
3179
+ points: g.points
3180
+ })), nodes.values());
3181
+ return out;
3182
+ }
3183
+ /** Edges that go through the back-edge channel when they run against the flow. */
3184
+ function channelled(e, graph) {
3185
+ return e.from !== e.to && e.kind !== "association" && !e.fromPort && !e.toPort;
3186
+ }
3187
+ function anchorPoint(box, side, offset, ctx, node) {
3188
+ const shape = typeOf(ctx.registry, node.type).shape;
3189
+ if (!spreadable(shape)) return sidePoint(box, side, .5);
3190
+ return sidePoint(box, side, offset);
3191
+ }
3192
+ function selfLoop(e, box, ctx) {
3193
+ const lr = ctx.direction === "LR";
3194
+ const fromSide = lr ? "top" : "right";
3195
+ const toSide = lr ? "top" : "right";
3196
+ const a = sidePoint(box, fromSide, .7);
3197
+ const b = sidePoint(box, toSide, .3);
3198
+ const k = 34;
3199
+ const d = lr ? `M ${a.x} ${a.y} C ${a.x} ${a.y - k} ${b.x} ${b.y - k} ${b.x} ${b.y}` : `M ${a.x} ${a.y} C ${a.x + k} ${a.y} ${b.x + k} ${b.y} ${b.x} ${b.y}`;
3200
+ const apex = lr ? {
3201
+ x: (a.x + b.x) / 2,
3202
+ y: a.y - k * .75
3203
+ } : {
3204
+ x: a.x + k * .75,
3205
+ y: (a.y + b.y) / 2
3206
+ };
3207
+ const geo = {
3208
+ id: e.id,
3209
+ d,
3210
+ points: [
3211
+ a,
3212
+ apex,
3213
+ b
3214
+ ],
3215
+ from: {
3216
+ point: a,
3217
+ side: fromSide
3218
+ },
3219
+ to: {
3220
+ point: b,
3221
+ side: toSide
3222
+ },
3223
+ selfLoop: true
3224
+ };
3225
+ const l = ctx.labels.get(e.id);
3226
+ if (l) geo.label = {
3227
+ x: lr ? apex.x : apex.x + l.width / 2 + 4,
3228
+ y: lr ? apex.y - l.height / 2 - 4 : apex.y,
3229
+ width: l.width,
3230
+ height: l.height,
3231
+ lines: l.lines
3232
+ };
3233
+ return geo;
3234
+ }
3235
+ function overlaps(a, b, gap = 0) {
3236
+ return Math.abs(a.x - b.x) < (a.width + b.width) / 2 + gap && Math.abs(a.y - b.y) < (a.height + b.height) / 2 + gap;
3237
+ }
3238
+ /** A group frame around its member nodes and nested groups. */
3239
+ function frameAround(groupId, graph, nodes, pad = 18, title = 24) {
3240
+ const members = /* @__PURE__ */ new Set([groupId]);
3241
+ let grew = true;
3242
+ while (grew) {
3243
+ grew = false;
3244
+ for (const g of graph.groups ?? []) if (g.parent && members.has(g.parent) && !members.has(g.id)) {
3245
+ members.add(g.id);
3246
+ grew = true;
3247
+ }
3248
+ }
3249
+ let x0 = Infinity;
3250
+ let y0 = Infinity;
3251
+ let x1 = -Infinity;
3252
+ let y1 = -Infinity;
3253
+ for (const n of graph.nodes) {
3254
+ if (!n.group || !members.has(n.group)) continue;
3255
+ const b = nodes.get(n.id);
3256
+ if (!b) continue;
3257
+ x0 = Math.min(x0, b.x - b.width / 2);
3258
+ y0 = Math.min(y0, b.y - b.height / 2);
3259
+ x1 = Math.max(x1, b.x + b.width / 2);
3260
+ y1 = Math.max(y1, b.y + b.height / 2);
3261
+ }
3262
+ if (!Number.isFinite(x0)) return void 0;
3263
+ const nested = [...members].filter((m) => m !== groupId).length > 0 ? 10 : 0;
3264
+ x0 -= pad + nested;
3265
+ x1 += pad + nested;
3266
+ y0 -= pad + title + nested;
3267
+ y1 += pad + nested;
3268
+ return {
3269
+ x: (x0 + x1) / 2,
3270
+ y: (y0 + y1) / 2,
3271
+ width: x1 - x0,
3272
+ height: y1 - y0
3273
+ };
3274
+ }
3275
+ /** Everything drawn: nodes, edge bends and labels, frames. */
3276
+ function boundsOf(nodes, edges, groups, outsideLabels = 40) {
3277
+ let x0 = Infinity;
3278
+ let y0 = Infinity;
3279
+ let x1 = -Infinity;
3280
+ let y1 = -Infinity;
3281
+ const box = (b, extraBottom = 0) => {
3282
+ x0 = Math.min(x0, b.x - b.width / 2);
3283
+ y0 = Math.min(y0, b.y - b.height / 2);
3284
+ x1 = Math.max(x1, b.x + b.width / 2);
3285
+ y1 = Math.max(y1, b.y + b.height / 2 + extraBottom);
3286
+ };
3287
+ for (const b of nodes.values()) {
3288
+ box(b, b.width <= 40 ? outsideLabels : 0);
3289
+ if (b.width <= 40 && "lines" in b && b.lines.length) {
3290
+ box({
3291
+ ...b,
3292
+ width: 110
3293
+ });
3294
+ box({
3295
+ ...b,
3296
+ x: b.x + 60,
3297
+ width: b.width + 120
3298
+ });
3299
+ }
3300
+ }
3301
+ for (const g of groups.values()) box(g);
3302
+ for (const e of edges.values()) {
3303
+ for (const p of e.points) box({
3304
+ x: p.x,
3305
+ y: p.y,
3306
+ width: 0,
3307
+ height: 0
3308
+ });
3309
+ if (e.label) box(e.label);
3310
+ }
3311
+ if (!Number.isFinite(x0)) return {
3312
+ x: 0,
3313
+ y: 0,
3314
+ width: 0,
3315
+ height: 0
3316
+ };
3317
+ return {
3318
+ x: x0,
3319
+ y: y0,
3320
+ width: x1 - x0,
3321
+ height: y1 - y0
3322
+ };
3323
+ }
3324
+ // src/monitor/state.ts
3325
+ function emptyMonitorState() {
3326
+ return {
3327
+ nodes: {},
3328
+ edges: {},
3329
+ failures: 0
3330
+ };
3331
+ }
3332
+ function toEpoch(v) {
3333
+ if (v === void 0) return void 0;
3334
+ if (typeof v === "number") return Number.isFinite(v) ? v : void 0;
3335
+ const t = Date.parse(v);
3336
+ return Number.isNaN(t) ? void 0 : t;
3337
+ }
3338
+ function mergeRecord(prev, next) {
3339
+ if (!next) return prev;
3340
+ const out = { ...prev };
3341
+ for (const [id, v] of Object.entries(next)) out[id] = {
3342
+ ...prev[id] ?? {},
3343
+ ...v
3344
+ };
3345
+ return out;
3346
+ }
3347
+ /** The pure reducer. Never mutates `state`. */
3348
+ function reduceMonitor(state, action) {
3349
+ switch (action.type) {
3350
+ case "reset": return emptyMonitorState();
3351
+ case "error": return {
3352
+ ...state,
3353
+ error: action.error,
3354
+ failures: state.failures + 1
3355
+ };
3356
+ case "snapshot": {
3357
+ const s = action.snapshot;
3358
+ const next = {
3359
+ ...action.merge ? {
3360
+ ...state,
3361
+ nodes: mergeRecord(state.nodes, s.nodes),
3362
+ edges: mergeRecord(state.edges, s.edges)
3363
+ } : {
3364
+ nodes: { ...s.nodes ?? {} },
3365
+ edges: { ...s.edges ?? {} },
3366
+ failures: 0
3367
+ },
3368
+ failures: 0,
3369
+ receivedAt: action.at
3370
+ };
3371
+ delete next.error;
3372
+ const stamped = toEpoch(s.updatedAt);
3373
+ if (stamped !== void 0) next.updatedAt = stamped;
3374
+ else delete next.updatedAt;
3375
+ if (s.path !== void 0 || !action.merge) {
3376
+ if (s.path) next.path = [...s.path];
3377
+ else delete next.path;
3378
+ }
3379
+ if (s.current !== void 0 || !action.merge) {
3380
+ if (s.current) next.current = [...s.current];
3381
+ else delete next.current;
3382
+ }
3383
+ return next;
3384
+ }
3385
+ }
3386
+ }
3387
+ /** Folds a snapshot into a state: the shorthand for the common case. */
3388
+ function mergeSnapshot(state, snapshot, at = Date.now(), merge = false) {
3389
+ return reduceMonitor(state ?? emptyMonitorState(), {
3390
+ type: "snapshot",
3391
+ snapshot,
3392
+ at,
3393
+ merge
3394
+ });
3395
+ }
3396
+ /**
3397
+ * Whether the state is too old to trust: nothing received for `staleAfterMs`.
3398
+ * A state that never received anything is not stale (it is loading).
3399
+ */
3400
+ function isStale(state, now, staleAfterMs) {
3401
+ if (!state?.receivedAt) return false;
3402
+ return now - state.receivedAt > staleAfterMs;
3403
+ }
3404
+ /**
3405
+ * The edges of a path: for each consecutive pair of nodes, the first enabled
3406
+ * edge between them (then any edge). Pairs without an edge are skipped.
3407
+ */
3408
+ function pathEdges(graph, path) {
3409
+ if (!path || path.length < 2) return [];
3410
+ const out = [];
3411
+ for (let i = 1; i < path.length; i++) {
3412
+ const from = path[i - 1];
3413
+ const to = path[i];
3414
+ const candidates = graph.edges.filter((e) => e.from === from && e.to === to);
3415
+ const pick = candidates.find((e) => e.enabled !== false) ?? candidates[0];
3416
+ if (pick) out.push(pick.id);
3417
+ }
3418
+ return out;
3419
+ }
3420
+ /** Built-in statuses and the kit tone each is drawn with. */
3421
+ var STATUS_TONES = Object.freeze({
3422
+ active: "accent",
3423
+ done: "success",
3424
+ error: "danger",
3425
+ waiting: "warning",
3426
+ skipped: "neutral"
3427
+ });
3428
+ /** Counts of nodes by status: the summary line of a monitor. */
3429
+ function statusSummary(state) {
3430
+ const out = {};
3431
+ for (const n of Object.values(state?.nodes ?? {})) {
3432
+ if (!n.status) continue;
3433
+ out[n.status] = (out[n.status] ?? 0) + 1;
3434
+ }
3435
+ return out;
3436
+ }
3437
+ // src/monitor/poller.ts
3438
+ function documentVisibility() {
3439
+ const doc = globalThis.document;
3440
+ if (!doc || typeof doc.addEventListener !== "function") return void 0;
3441
+ return {
3442
+ hidden: () => doc.visibilityState === "hidden",
3443
+ subscribe(listener) {
3444
+ doc.addEventListener("visibilitychange", listener);
3445
+ return () => doc.removeEventListener("visibilitychange", listener);
3446
+ }
3447
+ };
3448
+ }
3449
+ /** The delay after `failures` errors in a row. */
3450
+ function backoffDelay(intervalMs, failures, backoff = {}) {
3451
+ if (backoff === false || failures === 0) return intervalMs;
3452
+ const factor = backoff.factor ?? 2;
3453
+ const max = backoff.maxMs ?? 6e4;
3454
+ return Math.min(max, intervalMs * factor ** failures);
3455
+ }
3456
+ function createPoller(options) {
3457
+ const visibility = options.visibilityPause === false ? void 0 : options.visibilityPause === true || options.visibilityPause === void 0 ? documentVisibility() : options.visibilityPause;
3458
+ let status = "idle";
3459
+ const current = () => status;
3460
+ let timer;
3461
+ let controller;
3462
+ let failures = 0;
3463
+ let unsubscribe;
3464
+ let nextDelay = options.intervalMs;
3465
+ /** Bumped on every stop/refresh, so a late response of an old run is ignored. */
3466
+ let generation = 0;
3467
+ const clear = () => {
3468
+ if (timer !== void 0) clearTimeout(timer);
3469
+ timer = void 0;
3470
+ };
3471
+ const abort = () => {
3472
+ controller?.abort();
3473
+ controller = void 0;
3474
+ };
3475
+ const schedule = (delay) => {
3476
+ clear();
3477
+ nextDelay = delay;
3478
+ if (status !== "running") return;
3479
+ timer = setTimeout(tick, delay);
3480
+ };
3481
+ const tick = async () => {
3482
+ timer = void 0;
3483
+ if (status !== "running") return;
3484
+ const gen = ++generation;
3485
+ abort();
3486
+ const c = new AbortController();
3487
+ controller = c;
3488
+ try {
3489
+ const data = await options.fetch(c.signal);
3490
+ if (gen !== generation || current() === "idle") return;
3491
+ failures = 0;
3492
+ options.onData?.(data);
3493
+ } catch (error) {
3494
+ if (gen !== generation || current() === "idle" || c.signal.aborted) return;
3495
+ failures++;
3496
+ options.onError?.(error, failures);
3497
+ } finally {
3498
+ if (controller === c) controller = void 0;
3499
+ }
3500
+ if (gen === generation && status === "running") schedule(backoffDelay(options.intervalMs, failures, options.backoff));
3501
+ };
3502
+ const onVisibility = () => {
3503
+ if (!visibility || status === "idle") return;
3504
+ if (visibility.hidden()) {
3505
+ if (status === "running") {
3506
+ status = "paused";
3507
+ generation++;
3508
+ clear();
3509
+ abort();
3510
+ }
3511
+ } else if (status === "paused") {
3512
+ status = "running";
3513
+ tick();
3514
+ }
3515
+ };
3516
+ return {
3517
+ start() {
3518
+ if (status !== "idle") return;
3519
+ unsubscribe = visibility?.subscribe(onVisibility);
3520
+ if (visibility?.hidden()) {
3521
+ status = "paused";
3522
+ return;
3523
+ }
3524
+ status = "running";
3525
+ if (options.immediate ?? true) tick();
3526
+ else schedule(options.intervalMs);
3527
+ },
3528
+ stop() {
3529
+ status = "idle";
3530
+ generation++;
3531
+ clear();
3532
+ abort();
3533
+ unsubscribe?.();
3534
+ unsubscribe = void 0;
3535
+ },
3536
+ refresh() {
3537
+ if (status !== "running") return;
3538
+ clear();
3539
+ tick();
3540
+ },
3541
+ get status() {
3542
+ return status;
3543
+ },
3544
+ get failures() {
3545
+ return failures;
3546
+ },
3547
+ get nextDelay() {
3548
+ return nextDelay;
3549
+ }
3550
+ };
3551
+ }
3552
+ // src/edit/commands.ts
3553
+ /** Thrown when a command does not fit the graph (unknown id, duplicate id…). */
3554
+ var CommandError = class extends Error {
3555
+ name = "CommandError";
3556
+ };
3557
+ function patched(obj, patch) {
3558
+ const out = { ...obj };
3559
+ for (const [k, v] of Object.entries(patch)) {
3560
+ if (k === "id") continue;
3561
+ if (v === null || v === void 0) delete out[k];
3562
+ else out[k] = v;
3563
+ }
3564
+ return out;
3565
+ }
3566
+ function ids(graph) {
3567
+ return /* @__PURE__ */ new Set([
3568
+ ...graph.nodes.map((n) => n.id),
3569
+ ...graph.edges.map((e) => e.id),
3570
+ ...(graph.groups ?? []).map((g) => g.id)
3571
+ ]);
3572
+ }
3573
+ /** Applies a command; returns a new graph and leaves the old one untouched. */
3574
+ function applyCommand(graph, command) {
3575
+ switch (command.type) {
3576
+ case "addNodes": {
3577
+ const taken = ids(graph);
3578
+ for (const el of [...command.nodes, ...command.edges ?? []]) {
3579
+ if (taken.has(el.id)) throw new CommandError(`The id "${el.id}" is already used`);
3580
+ taken.add(el.id);
3581
+ }
3582
+ return {
3583
+ ...graph,
3584
+ nodes: [...graph.nodes, ...command.nodes],
3585
+ edges: command.edges?.length ? [...graph.edges, ...command.edges] : graph.edges
3586
+ };
3587
+ }
3588
+ case "removeElements": {
3589
+ const gone = new Set(command.ids);
3590
+ const nodes = graph.nodes.filter((n) => !gone.has(n.id));
3591
+ const edges = graph.edges.filter((e) => !gone.has(e.id) && !gone.has(e.from) && !gone.has(e.to));
3592
+ const next = {
3593
+ ...graph,
3594
+ nodes,
3595
+ edges
3596
+ };
3597
+ if (graph.groups) {
3598
+ const parentOf = new Map(graph.groups.map((g) => [g.id, g.parent]));
3599
+ const lift = (id) => {
3600
+ let p = id;
3601
+ let guard = 0;
3602
+ while (p !== void 0 && gone.has(p) && guard++ < 100) p = parentOf.get(p);
3603
+ return p;
3604
+ };
3605
+ next.groups = graph.groups.filter((g) => !gone.has(g.id)).map((g) => g.parent && gone.has(g.parent) ? patched(g, { parent: lift(g.parent) ?? null }) : g);
3606
+ next.nodes = nodes.map((n) => n.group && gone.has(n.group) ? patched(n, { group: lift(n.group) ?? null }) : n);
3607
+ }
3608
+ return next;
3609
+ }
3610
+ case "updateNode": {
3611
+ const i = graph.nodes.findIndex((n) => n.id === command.id);
3612
+ if (i < 0) throw new CommandError(`No node "${command.id}"`);
3613
+ const nodes = graph.nodes.slice();
3614
+ nodes[i] = patched(nodes[i], command.patch);
3615
+ return {
3616
+ ...graph,
3617
+ nodes
3618
+ };
3619
+ }
3620
+ case "moveNodes": {
3621
+ const to = new Map(command.moves.map((m) => [m.id, m.position]));
3622
+ return {
3623
+ ...graph,
3624
+ nodes: graph.nodes.map((n) => to.has(n.id) ? {
3625
+ ...n,
3626
+ position: { ...to.get(n.id) }
3627
+ } : n)
3628
+ };
3629
+ }
3630
+ case "addEdge":
3631
+ if (ids(graph).has(command.edge.id)) throw new CommandError(`The id "${command.edge.id}" is already used`);
3632
+ return {
3633
+ ...graph,
3634
+ edges: [...graph.edges, command.edge]
3635
+ };
3636
+ case "updateEdge": {
3637
+ const i = graph.edges.findIndex((e) => e.id === command.id);
3638
+ if (i < 0) throw new CommandError(`No edge "${command.id}"`);
3639
+ const edges = graph.edges.slice();
3640
+ edges[i] = patched(edges[i], command.patch);
3641
+ return {
3642
+ ...graph,
3643
+ edges
3644
+ };
3645
+ }
3646
+ case "setEnabled": {
3647
+ const patch = { enabled: command.enabled ? null : false };
3648
+ return command.kind === "node" ? applyCommand(graph, {
3649
+ type: "updateNode",
3650
+ id: command.id,
3651
+ patch
3652
+ }) : applyCommand(graph, {
3653
+ type: "updateEdge",
3654
+ id: command.id,
3655
+ patch
3656
+ });
3657
+ }
3658
+ case "addGroup":
3659
+ if (ids(graph).has(command.group.id)) throw new CommandError(`The id "${command.group.id}" is already used`);
3660
+ return {
3661
+ ...graph,
3662
+ groups: [...graph.groups ?? [], command.group]
3663
+ };
3664
+ case "updateGroup": {
3665
+ const groups = graph.groups ?? [];
3666
+ const i = groups.findIndex((g) => g.id === command.id);
3667
+ if (i < 0) throw new CommandError(`No group "${command.id}"`);
3668
+ const next = groups.slice();
3669
+ next[i] = patched(next[i], command.patch);
3670
+ return {
3671
+ ...graph,
3672
+ groups: next
3673
+ };
3674
+ }
3675
+ case "updateMeta": return {
3676
+ ...graph,
3677
+ meta: patched(graph.meta ?? {}, command.patch)
3678
+ };
3679
+ case "replace": return command.graph;
3680
+ case "batch": return command.commands.reduce(applyCommand, graph);
3681
+ }
3682
+ }
3683
+ /** A fresh id with a prefix: `state-3`. */
3684
+ function nextId(graph, prefix, taken = ids(graph)) {
3685
+ const base = prefix.replace(/[^\p{L}\p{N}_-]+/gu, "-").replace(/^-+|-+$/g, "") || "n";
3686
+ for (let i = 1;; i++) {
3687
+ const id = `${base}-${i}`;
3688
+ if (!taken.has(id)) return id;
3689
+ }
3690
+ }
3691
+ /** Snaps a point to a grid. */
3692
+ function snap(p, grid) {
3693
+ if (grid <= 0) return p;
3694
+ return {
3695
+ x: Math.round(p.x / grid) * grid,
3696
+ y: Math.round(p.y / grid) * grid
3697
+ };
3698
+ }
3699
+ /** A short human description of a command, for undo tooltips and logs. */
3700
+ function describeCommand(c) {
3701
+ switch (c.type) {
3702
+ case "addNodes": return c.nodes.length === 1 ? `add ${c.nodes[0].id}` : `add ${c.nodes.length} nodes`;
3703
+ case "removeElements": return c.ids.length === 1 ? `delete ${c.ids[0]}` : `delete ${c.ids.length} elements`;
3704
+ case "updateNode":
3705
+ case "updateEdge":
3706
+ case "updateGroup": return `edit ${c.id}`;
3707
+ case "moveNodes": return c.moves.length === 1 ? `move ${c.moves[0].id}` : `move ${c.moves.length} nodes`;
3708
+ case "addEdge": return `connect ${c.edge.from} → ${c.edge.to}`;
3709
+ case "setEnabled": return `${c.enabled ? "enable" : "disable"} ${c.id}`;
3710
+ case "addGroup": return `add group ${c.group.id}`;
3711
+ case "updateMeta": return "edit settings";
3712
+ case "replace": return "replace graph";
3713
+ case "batch": return c.commands.length === 1 ? describeCommand(c.commands[0]) : `${c.commands.length} changes`;
3714
+ }
3715
+ }
3716
+ // src/edit/history.ts
3717
+ /**
3718
+ * Undo/redo over commands. Each entry keeps the graph before and after the
3719
+ * command: graphs are immutable and share unchanged nodes, so this costs
3720
+ * little and undo never depends on computing an inverse correctly.
3721
+ */
3722
+ var History = class {
3723
+ current;
3724
+ past = [];
3725
+ future = [];
3726
+ limit;
3727
+ mergeWindow;
3728
+ now;
3729
+ constructor(current, options = {}) {
3730
+ this.current = current;
3731
+ this.limit = options.limit ?? 200;
3732
+ this.mergeWindow = options.mergeWindowMs ?? 800;
3733
+ this.now = options.now ?? Date.now;
3734
+ }
3735
+ get graph() {
3736
+ return this.current;
3737
+ }
3738
+ get canUndo() {
3739
+ return this.past.length > 0;
3740
+ }
3741
+ get canRedo() {
3742
+ return this.future.length > 0;
3743
+ }
3744
+ /** The entry `undo` would revert. */
3745
+ get last() {
3746
+ return this.past[this.past.length - 1];
3747
+ }
3748
+ get next() {
3749
+ return this.future[this.future.length - 1];
3750
+ }
3751
+ /** Applies a command and records it. Throws CommandError and records nothing when it does not apply. */
3752
+ execute(command, mergeKey) {
3753
+ const before = this.current;
3754
+ const after = applyCommand(before, command);
3755
+ const at = this.now();
3756
+ const last = this.last;
3757
+ if (mergeKey && last && last.mergeKey === mergeKey && at - last.at <= this.mergeWindow && this.future.length === 0) {
3758
+ last.after = after;
3759
+ last.command = last.command.type === "batch" ? {
3760
+ type: "batch",
3761
+ commands: [...last.command.commands, command]
3762
+ } : {
3763
+ type: "batch",
3764
+ commands: [last.command, command]
3765
+ };
3766
+ last.at = at;
3767
+ } else {
3768
+ this.past.push({
3769
+ command,
3770
+ before,
3771
+ after,
3772
+ mergeKey,
3773
+ at
3774
+ });
3775
+ if (this.past.length > this.limit) this.past.shift();
3776
+ }
3777
+ this.future = [];
3778
+ this.current = after;
3779
+ return after;
3780
+ }
3781
+ undo() {
3782
+ const entry = this.past.pop();
3783
+ if (!entry) return void 0;
3784
+ this.future.push(entry);
3785
+ this.current = entry.before;
3786
+ return entry;
3787
+ }
3788
+ redo() {
3789
+ const entry = this.future.pop();
3790
+ if (!entry) return void 0;
3791
+ this.past.push(entry);
3792
+ this.current = entry.after;
3793
+ return entry;
3794
+ }
3795
+ /**
3796
+ * Takes a graph from outside (the host changed the model). When it is the
3797
+ * one history already holds, nothing happens; otherwise history restarts.
3798
+ */
3799
+ reset(graph) {
3800
+ if (graph === this.current) return;
3801
+ this.current = graph;
3802
+ this.past = [];
3803
+ this.future = [];
3804
+ }
3805
+ /** Ends merging: the next command starts a new entry even with the same key. */
3806
+ seal() {
3807
+ const last = this.last;
3808
+ if (last) last.mergeKey = void 0;
3809
+ }
3810
+ };
3811
+ // src/edit/selection.ts
3812
+ /**
3813
+ * Operations over a selection that produce commands: copy/paste/duplicate,
3814
+ * align, distribute, and applying an auto layout as one undoable move.
3815
+ */
3816
+ function isFragment(v) {
3817
+ return typeof v === "object" && v !== null && v.kind === "dskripchenko-flow/fragment" && Array.isArray(v.nodes);
3818
+ }
3819
+ /**
3820
+ * Copies the selected nodes, with positions from the layout, and the edges
3821
+ * whose both ends are selected (plus selected edges between them).
3822
+ */
3823
+ function copyFragment(graph, selected, layout) {
3824
+ const set = new Set(selected);
3825
+ const nodes = graph.nodes.filter((n) => set.has(n.id)).map((n) => {
3826
+ const box = layout?.nodes.get(n.id);
3827
+ const copy = structuredClone(n);
3828
+ if (box) copy.position = {
3829
+ x: box.x,
3830
+ y: box.y
3831
+ };
3832
+ return copy;
3833
+ });
3834
+ const inside = new Set(nodes.map((n) => n.id));
3835
+ return {
3836
+ kind: "dskripchenko-flow/fragment",
3837
+ version: 1,
3838
+ nodes,
3839
+ edges: graph.edges.filter((e) => inside.has(e.from) && inside.has(e.to)).map((e) => structuredClone(e))
3840
+ };
3841
+ }
3842
+ /**
3843
+ * The command that pastes a fragment with fresh ids, shifted by `offset`
3844
+ * (or centred on `at`). Returns the new node ids for the selection.
3845
+ */
3846
+ function pasteFragment(graph, fragment, place = {}) {
3847
+ const taken = /* @__PURE__ */ new Set([
3848
+ ...graph.nodes.map((n) => n.id),
3849
+ ...graph.edges.map((e) => e.id),
3850
+ ...(graph.groups ?? []).map((g) => g.id)
3851
+ ]);
3852
+ const groups = new Set((graph.groups ?? []).map((g) => g.id));
3853
+ const map = /* @__PURE__ */ new Map();
3854
+ let shift = place.offset ?? {
3855
+ x: 32,
3856
+ y: 32
3857
+ };
3858
+ if (place.at) {
3859
+ const ps = fragment.nodes.map((n) => n.position).filter((p) => !!p);
3860
+ if (ps.length) {
3861
+ const cx = (Math.min(...ps.map((p) => p.x)) + Math.max(...ps.map((p) => p.x))) / 2;
3862
+ const cy = (Math.min(...ps.map((p) => p.y)) + Math.max(...ps.map((p) => p.y))) / 2;
3863
+ shift = {
3864
+ x: place.at.x - cx,
3865
+ y: place.at.y - cy
3866
+ };
3867
+ }
3868
+ }
3869
+ const nodes = fragment.nodes.map((n) => {
3870
+ const id = nextId(graph, n.type === "state" ? "state" : n.type, taken);
3871
+ taken.add(id);
3872
+ map.set(n.id, id);
3873
+ const copy = {
3874
+ ...structuredClone(n),
3875
+ id
3876
+ };
3877
+ if (n.position) copy.position = {
3878
+ x: n.position.x + shift.x,
3879
+ y: n.position.y + shift.y
3880
+ };
3881
+ if (copy.group && !groups.has(copy.group)) delete copy.group;
3882
+ return copy;
3883
+ });
3884
+ return {
3885
+ command: {
3886
+ type: "addNodes",
3887
+ nodes,
3888
+ edges: fragment.edges.filter((e) => map.has(e.from) && map.has(e.to)).map((e) => {
3889
+ const id = nextId(graph, "e", taken);
3890
+ taken.add(id);
3891
+ return {
3892
+ ...structuredClone(e),
3893
+ id,
3894
+ from: map.get(e.from),
3895
+ to: map.get(e.to)
3896
+ };
3897
+ })
3898
+ },
3899
+ ids: nodes.map((n) => n.id)
3900
+ };
3901
+ }
3902
+ /** Aligns the boxes' edges or centres; returns a move command (or null for < 2 nodes). */
3903
+ function alignNodes(boxes, ids, how) {
3904
+ const list = ids.map((id) => [id, boxes.get(id)]).filter((x) => !!x[1]);
3905
+ if (list.length < 2) return null;
3906
+ const lefts = list.map(([, b]) => b.x - b.width / 2);
3907
+ const rights = list.map(([, b]) => b.x + b.width / 2);
3908
+ const tops = list.map(([, b]) => b.y - b.height / 2);
3909
+ const bottoms = list.map(([, b]) => b.y + b.height / 2);
3910
+ return {
3911
+ type: "moveNodes",
3912
+ moves: list.map(([id, b]) => {
3913
+ let { x, y } = b;
3914
+ switch (how) {
3915
+ case "left":
3916
+ x = Math.min(...lefts) + b.width / 2;
3917
+ break;
3918
+ case "right":
3919
+ x = Math.max(...rights) - b.width / 2;
3920
+ break;
3921
+ case "center":
3922
+ x = (Math.min(...lefts) + Math.max(...rights)) / 2;
3923
+ break;
3924
+ case "top":
3925
+ y = Math.min(...tops) + b.height / 2;
3926
+ break;
3927
+ case "bottom":
3928
+ y = Math.max(...bottoms) - b.height / 2;
3929
+ break;
3930
+ case "middle": y = (Math.min(...tops) + Math.max(...bottoms)) / 2;
3931
+ }
3932
+ return {
3933
+ id,
3934
+ position: {
3935
+ x,
3936
+ y
3937
+ }
3938
+ };
3939
+ })
3940
+ };
3941
+ }
3942
+ /** Spaces the boxes evenly between the outermost two (gaps, not centres). */
3943
+ function distributeNodes(boxes, ids, axis) {
3944
+ const list = ids.map((id) => [id, boxes.get(id)]).filter((x) => !!x[1]);
3945
+ if (list.length < 3) return null;
3946
+ const h = axis === "horizontal";
3947
+ const size = (b) => h ? b.width : b.height;
3948
+ const pos = (b) => h ? b.x : b.y;
3949
+ list.sort((a, b) => pos(a[1]) - pos(b[1]));
3950
+ const first = list[0][1];
3951
+ const last = list[list.length - 1][1];
3952
+ const start = pos(first) - size(first) / 2;
3953
+ const end = pos(last) + size(last) / 2;
3954
+ const total = list.reduce((s, [, b]) => s + size(b), 0);
3955
+ const gap = (end - start - total) / (list.length - 1);
3956
+ let cursor = start;
3957
+ return {
3958
+ type: "moveNodes",
3959
+ moves: list.map(([id, b]) => {
3960
+ const c = cursor + size(b) / 2;
3961
+ cursor += size(b) + gap;
3962
+ return {
3963
+ id,
3964
+ position: h ? {
3965
+ x: c,
3966
+ y: b.y
3967
+ } : {
3968
+ x: b.x,
3969
+ y: c
3970
+ }
3971
+ };
3972
+ })
3973
+ };
3974
+ }
3975
+ /** Fixes every node where a layout put it: auto layout as one undoable command. */
3976
+ function layoutCommand(layout) {
3977
+ return {
3978
+ type: "moveNodes",
3979
+ moves: [...layout.nodes.values()].map((b) => ({
3980
+ id: b.id,
3981
+ position: {
3982
+ x: Math.round(b.x),
3983
+ y: Math.round(b.y)
3984
+ }
3985
+ }))
3986
+ };
3987
+ }
3988
+ /**
3989
+ * The centre nearest to `near` where a box of `size` overlaps no box in
3990
+ * `boxes` (with `padding`), searching outward ring by ring on the grid.
3991
+ * Used to place nodes added from the palette.
3992
+ */
3993
+ function findFreeSpot(boxes, size, near, options = {}) {
3994
+ const grid = options.grid ?? 0;
3995
+ const pad = options.padding ?? 24;
3996
+ const list = [...boxes];
3997
+ const snapTo = (v) => (grid > 0 ? Math.round(v / grid) * grid : Math.round(v)) + 0;
3998
+ const free = (p) => list.every((b) => Math.abs(b.x - p.x) >= (b.width + size.width) / 2 + pad || Math.abs(b.y - p.y) >= (b.height + size.height) / 2 + pad);
3999
+ const start = {
4000
+ x: snapTo(near.x),
4001
+ y: snapTo(near.y)
4002
+ };
4003
+ if (free(start)) return start;
4004
+ const step = Math.max(grid, 20);
4005
+ const rings = options.maxRings ?? 60;
4006
+ for (let r = 1; r <= rings; r++) {
4007
+ const ring = [];
4008
+ for (let i = -r; i <= r; i++) {
4009
+ ring.push({
4010
+ x: start.x + i * step,
4011
+ y: start.y - r * step
4012
+ }, {
4013
+ x: start.x + i * step,
4014
+ y: start.y + r * step
4015
+ });
4016
+ if (i > -r && i < r) ring.push({
4017
+ x: start.x - r * step,
4018
+ y: start.y + i * step
4019
+ }, {
4020
+ x: start.x + r * step,
4021
+ y: start.y + i * step
4022
+ });
4023
+ }
4024
+ ring.sort((a, b) => Math.hypot(a.x - near.x, a.y - near.y) - Math.hypot(b.x - near.x, b.y - near.y));
4025
+ const hit = ring.find(free);
4026
+ if (hit) return {
4027
+ x: snapTo(hit.x),
4028
+ y: snapTo(hit.y)
4029
+ };
4030
+ }
4031
+ return start;
4032
+ }
4033
+ // src/view/viewport.ts
4034
+ var DEFAULT_LIMITS = {
4035
+ min: .2,
4036
+ max: 2.5
4037
+ };
4038
+ function clampZoom(k, limits = DEFAULT_LIMITS) {
4039
+ return Math.min(limits.max, Math.max(limits.min, k));
4040
+ }
4041
+ function toScreen(v, p) {
4042
+ return {
4043
+ x: p.x * v.k + v.x,
4044
+ y: p.y * v.k + v.y
4045
+ };
4046
+ }
4047
+ function toGraph(v, p) {
4048
+ return {
4049
+ x: (p.x - v.x) / v.k,
4050
+ y: (p.y - v.y) / v.k
4051
+ };
4052
+ }
4053
+ /** Zooms by `factor` keeping the graph point under `at` (screen) in place. */
4054
+ function zoomAt(v, factor, at, limits = DEFAULT_LIMITS) {
4055
+ const k = clampZoom(v.k * factor, limits);
4056
+ const g = toGraph(v, at);
4057
+ return {
4058
+ k,
4059
+ x: at.x - g.x * k,
4060
+ y: at.y - g.y * k
4061
+ };
4062
+ }
4063
+ /**
4064
+ * The viewport that shows `bounds` whole and centred in a `width × height`
4065
+ * screen with `padding`; never zooms in beyond `maxFit` (small graphs stay
4066
+ * at a readable size instead of blowing up).
4067
+ */
4068
+ function fitView(bounds, width, height, padding = 32, limits = DEFAULT_LIMITS, maxFit = 1) {
4069
+ if (bounds.width <= 0 || bounds.height <= 0 || width <= 0 || height <= 0) return {
4070
+ x: width / 2 - bounds.x,
4071
+ y: height / 2 - bounds.y,
4072
+ k: 1
4073
+ };
4074
+ const k = clampZoom(Math.min((width - padding * 2) / bounds.width, (height - padding * 2) / bounds.height, maxFit), limits);
4075
+ return {
4076
+ k,
4077
+ x: width / 2 - (bounds.x + bounds.width / 2) * k,
4078
+ y: height / 2 - (bounds.y + bounds.height / 2) * k
4079
+ };
4080
+ }
4081
+ /** Pans so that a graph point sits in the screen centre, keeping the zoom. */
4082
+ function centreOn(v, p, width, height) {
4083
+ return {
4084
+ k: v.k,
4085
+ x: width / 2 - p.x * v.k,
4086
+ y: height / 2 - p.y * v.k
4087
+ };
4088
+ }
4089
+ /** Whether a graph-space box is (mostly) inside the visible area. */
4090
+ function isVisible(v, box, width, height, margin = 24) {
4091
+ const a = toScreen(v, {
4092
+ x: box.x - box.width / 2,
4093
+ y: box.y - box.height / 2
4094
+ });
4095
+ const b = toScreen(v, {
4096
+ x: box.x + box.width / 2,
4097
+ y: box.y + box.height / 2
4098
+ });
4099
+ return a.x >= margin && a.y >= margin && b.x <= width - margin && b.y <= height - margin;
4100
+ }
4101
+ /** The node nearest to `from` in a direction: arrow-key navigation. */
4102
+ function nearestInDirection(from, candidates, dir) {
4103
+ let best;
4104
+ let bestScore = Infinity;
4105
+ for (const c of candidates) {
4106
+ const dx = c.x - from.x;
4107
+ const dy = c.y - from.y;
4108
+ const along = dir === "right" ? dx : dir === "left" ? -dx : dir === "down" ? dy : -dy;
4109
+ const across = dir === "left" || dir === "right" ? Math.abs(dy) : Math.abs(dx);
4110
+ if (along <= 1) continue;
4111
+ const score = along + across * 2;
4112
+ if (score < bestScore) {
4113
+ bestScore = score;
4114
+ best = c.id;
4115
+ }
4116
+ }
4117
+ return best;
4118
+ }
4119
+ export { DiagramError as $, reduceMonitor as A, measureNode as B, backoffDelay as C, isStale as D, emptyMonitorState as E, LABEL_FONT as F, midpoint as G, bezierPoint as H, boundsOf as I, sidePoint as J, route as K, frameAround as L, toEpoch as M, DESCRIPTION_FONT as N, mergeSnapshot as O, EDGE_FONT as P, MAX_NODES as Q, layoutGraph as R, snap as S, STATUS_TONES as T, distanceToPolyline as U, routeEdges as V, facingSide as W, wrapText as X, approximateMeasurer as Y, MAX_EDGES as Z, History as _, isVisible as a, isLocked as at, describeCommand as b, toScreen as c, GraphError as ct, copyFragment as d, stringifyGraph as dt, issuesByElement as et, distributeNodes as f, GRAPH_VERSION as ft, pasteFragment as g, layoutCommand as h, fitView as i, defaultRegistry as it, statusSummary as j, pathEdges as k, zoomAt as l, emptyGraph as lt, isFragment as m, centreOn as n, BUILTIN_TYPES as nt, nearestInDirection as o, portsOf as ot, findFreeSpot as p, isEnabled as pt, sideNormal as q, clampZoom as r, createRegistry as rt, toGraph as s, typeOf as st, DEFAULT_LIMITS as t, validateGraph as tt, alignNodes as u, parseGraph as ut, CommandError as v, createPoller as w, nextId as x, applyCommand as y, measureEdgeLabel as z };
4120
+
4121
+ //# sourceMappingURL=core-DfJMfClV.js.map