dsh-model-everything 0.4.1 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/README.en-US.md +75 -19
  2. package/README.md +377 -345
  3. package/cordis.patch.yml +9 -0
  4. package/diagram/package.json +5 -0
  5. package/lib/client.js +3494 -0
  6. package/lib/diagram-diff-tool.js +90 -0
  7. package/lib/diagram-render-tool.js +102 -0
  8. package/lib/diagram-review-tool.js +99 -0
  9. package/lib/diagram.js +43 -0
  10. package/lib/gate.js +14 -12
  11. package/lib/presentation.js +10 -0
  12. package/lib/render/budget.js +99 -0
  13. package/lib/render/channels.js +243 -0
  14. package/lib/render/collapse.js +155 -0
  15. package/lib/render/components.js +99 -0
  16. package/lib/render/coords.js +288 -0
  17. package/lib/render/cycle-break.js +147 -0
  18. package/lib/render/diff.js +138 -0
  19. package/lib/render/dummy.js +43 -0
  20. package/lib/render/from-mermaid.js +235 -0
  21. package/lib/render/from-plantuml.js +430 -0
  22. package/lib/render/index.js +42 -0
  23. package/lib/render/layer.js +91 -0
  24. package/lib/render/layout.js +68 -0
  25. package/lib/render/model.js +17 -0
  26. package/lib/render/network-simplex.js +272 -0
  27. package/lib/render/order.js +122 -0
  28. package/lib/render/paint.js +891 -0
  29. package/lib/render/palette.js +127 -0
  30. package/lib/render/place.js +482 -0
  31. package/lib/render/route.js +225 -0
  32. package/lib/render/svg.js +229 -0
  33. package/lib/render/symbol.js +108 -0
  34. package/lib/render/text.js +74 -0
  35. package/lib/render/viewmodel.js +126 -0
  36. package/lib/render/wrap.js +87 -0
  37. package/lib/render/write.js +14 -0
  38. package/lib/scan.js +248 -8
  39. package/lib/structure-tool.js +2 -2
  40. package/lib/types/client.d.ts +83 -0
  41. package/lib/types/diagram-diff-tool.d.ts +8 -0
  42. package/lib/types/diagram-render-tool.d.ts +8 -0
  43. package/lib/types/diagram-review-tool.d.ts +8 -0
  44. package/lib/types/diagram.d.ts +34 -0
  45. package/lib/types/presentation.d.ts +7 -0
  46. package/lib/types/render/budget.d.ts +55 -0
  47. package/lib/types/render/channels.d.ts +63 -0
  48. package/lib/types/render/collapse.d.ts +23 -0
  49. package/lib/types/render/components.d.ts +57 -0
  50. package/lib/types/render/coords.d.ts +64 -0
  51. package/lib/types/render/cycle-break.d.ts +45 -0
  52. package/lib/types/render/diff.d.ts +44 -0
  53. package/lib/types/render/dummy.d.ts +32 -0
  54. package/lib/types/render/from-mermaid.d.ts +27 -0
  55. package/lib/types/render/from-plantuml.d.ts +33 -0
  56. package/lib/types/render/index.d.ts +38 -0
  57. package/lib/types/render/layer.d.ts +28 -0
  58. package/lib/types/render/layout.d.ts +54 -0
  59. package/lib/types/render/model.d.ts +73 -0
  60. package/lib/types/render/network-simplex.d.ts +54 -0
  61. package/lib/types/render/order.d.ts +27 -0
  62. package/lib/types/render/paint.d.ts +127 -0
  63. package/lib/types/render/palette.d.ts +41 -0
  64. package/lib/types/render/place.d.ts +55 -0
  65. package/lib/types/render/route.d.ts +77 -0
  66. package/lib/types/render/svg.d.ts +19 -0
  67. package/lib/types/render/symbol.d.ts +73 -0
  68. package/lib/types/render/text.d.ts +38 -0
  69. package/lib/types/render/viewmodel.d.ts +20 -0
  70. package/lib/types/render/wrap.d.ts +27 -0
  71. package/lib/types/render/write.d.ts +14 -0
  72. package/lib/types/scan.d.ts +10 -2
  73. package/lib/types/structure.d.ts +1 -0
  74. package/package.json +148 -132
  75. package/skills/model-everything/SKILL.md +454 -454
  76. package/skills/model-everything/references/architecture-domains.md +137 -0
  77. package/skills/model-everything/references/architecture-views.md +119 -0
  78. package/skills/model-everything-diagram/SKILL.md +84 -0
  79. package/src/client.ts +307 -0
  80. package/src/diagram-diff-tool.ts +94 -0
  81. package/src/diagram-render-tool.ts +108 -0
  82. package/src/diagram-review-tool.ts +109 -0
  83. package/src/diagram.ts +61 -0
  84. package/src/gate.ts +14 -12
  85. package/src/presentation.ts +12 -0
  86. package/src/render/budget.ts +135 -0
  87. package/src/render/channels.ts +309 -0
  88. package/src/render/collapse.ts +150 -0
  89. package/src/render/components.ts +126 -0
  90. package/src/render/coords.ts +328 -0
  91. package/src/render/cycle-break.ts +164 -0
  92. package/src/render/diff.ts +162 -0
  93. package/src/render/dummy.ts +65 -0
  94. package/src/render/from-mermaid.ts +261 -0
  95. package/src/render/from-plantuml.ts +469 -0
  96. package/src/render/index.ts +73 -0
  97. package/src/render/layer.ts +98 -0
  98. package/src/render/layout.ts +101 -0
  99. package/src/render/model.ts +86 -0
  100. package/src/render/network-simplex.ts +290 -0
  101. package/src/render/order.ts +139 -0
  102. package/src/render/paint.ts +1006 -0
  103. package/src/render/palette.ts +164 -0
  104. package/src/render/place.ts +527 -0
  105. package/src/render/route.ts +269 -0
  106. package/src/render/svg.ts +270 -0
  107. package/src/render/symbol.ts +137 -0
  108. package/src/render/text.ts +86 -0
  109. package/src/render/viewmodel.ts +136 -0
  110. package/src/render/wrap.ts +103 -0
  111. package/src/render/write.ts +17 -0
  112. package/src/scan.ts +807 -564
  113. package/src/structure-tool.ts +2 -2
  114. package/src/structure.ts +1 -1
package/lib/client.js ADDED
@@ -0,0 +1,3494 @@
1
+ // Generated by scripts/build-client.mjs - do not edit. Source: src/client.ts and src/render/*.ts.
2
+ window.__ModuleLoader__.load({
3
+ id: "dsh-model-everything",
4
+ factory(require) {
5
+ const module = { exports: {} };
6
+ const exports = module.exports;
7
+ "use strict";
8
+ var __defProp = Object.defineProperty;
9
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
10
+ var __getOwnPropNames = Object.getOwnPropertyNames;
11
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
12
+ var __export = (target, all) => {
13
+ for (var name in all)
14
+ __defProp(target, name, { get: all[name], enumerable: true });
15
+ };
16
+ var __copyProps = (to, from, except, desc) => {
17
+ if (from && typeof from === "object" || typeof from === "function") {
18
+ for (let key of __getOwnPropNames(from))
19
+ if (!__hasOwnProp.call(to, key) && key !== except)
20
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
21
+ }
22
+ return to;
23
+ };
24
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
25
+
26
+ // src/client.ts
27
+ var client_exports = {};
28
+ __export(client_exports, {
29
+ TOOL_NAMES: () => TOOL_NAMES,
30
+ WHITEBOARD_ID: () => WHITEBOARD_ID,
31
+ WHITEBOARD_SAMPLE: () => WHITEBOARD_SAMPLE,
32
+ default: () => factory,
33
+ mountWhiteboard: () => mountWhiteboard,
34
+ themeMode: () => themeMode,
35
+ viewFor: () => viewFor,
36
+ whiteboardFor: () => whiteboardFor
37
+ });
38
+ module.exports = __toCommonJS(client_exports);
39
+
40
+ // src/render/components.ts
41
+ var PAGE_ASPECT = 1.6;
42
+ function weakComponents(nodes, edges) {
43
+ const known = new Set(nodes.map((node) => node.id));
44
+ const neighbours = /* @__PURE__ */ new Map();
45
+ const link = (from, to) => {
46
+ neighbours.set(from, [...neighbours.get(from) ?? [], to]);
47
+ };
48
+ for (const edge of edges) {
49
+ if (!known.has(edge.from) || !known.has(edge.to) || edge.from === edge.to) continue;
50
+ link(edge.from, edge.to);
51
+ link(edge.to, edge.from);
52
+ }
53
+ const seen = /* @__PURE__ */ new Set();
54
+ const components = [];
55
+ for (const node of nodes) {
56
+ if (seen.has(node.id)) continue;
57
+ const members = [];
58
+ const queue = [node.id];
59
+ seen.add(node.id);
60
+ for (let head = 0; head < queue.length; head += 1) {
61
+ const id = queue[head];
62
+ members.push(id);
63
+ for (const next of neighbours.get(id) ?? []) {
64
+ if (seen.has(next)) continue;
65
+ seen.add(next);
66
+ queue.push(next);
67
+ }
68
+ }
69
+ components.push(members);
70
+ }
71
+ return components;
72
+ }
73
+ function packRectangles(rectangles, options = {}) {
74
+ const gap = options.gap ?? 30;
75
+ const aspect = options.aspect ?? PAGE_ASPECT;
76
+ if (rectangles.length === 0) return { placements: [], width: 0, height: 0 };
77
+ const area = rectangles.reduce((total, rectangle) => total + (rectangle.width + gap) * (rectangle.height + gap), 0);
78
+ const targetWidth = Math.max(gap, Math.sqrt(area * aspect));
79
+ const order2 = rectangles.map((rectangle, index) => ({ rectangle, index })).sort((left, right) => right.rectangle.height - left.rectangle.height || right.rectangle.width - left.rectangle.width || left.index - right.index);
80
+ const placements = rectangles.map((rectangle) => ({ x: 0, y: 0, width: rectangle.width, height: rectangle.height }));
81
+ let x = 0;
82
+ let y = 0;
83
+ let shelfHeight = 0;
84
+ let width = 0;
85
+ for (const entry of order2) {
86
+ const rectangle = entry.rectangle;
87
+ if (x > 0 && x + rectangle.width > targetWidth) {
88
+ y += shelfHeight + gap;
89
+ x = 0;
90
+ shelfHeight = 0;
91
+ }
92
+ placements[entry.index] = { x, y, width: rectangle.width, height: rectangle.height };
93
+ x += rectangle.width + gap;
94
+ width = Math.max(width, x - gap);
95
+ shelfHeight = Math.max(shelfHeight, rectangle.height);
96
+ }
97
+ return { placements, width, height: y + shelfHeight };
98
+ }
99
+
100
+ // src/render/channels.ts
101
+ var PORT_INSET = 10;
102
+ function spanOf(box) {
103
+ const lo = box.x + PORT_INSET;
104
+ const hi = box.x + box.width - PORT_INSET;
105
+ return hi > lo ? { lo, hi } : { lo: box.x + box.width / 2, hi: box.x + box.width / 2 };
106
+ }
107
+ function clamp(value, span) {
108
+ return Math.min(span.hi, Math.max(span.lo, value));
109
+ }
110
+ function inside(value, span) {
111
+ return value >= span.lo - 1e-3 && value <= span.hi + 1e-3;
112
+ }
113
+ function cut(free, blocked) {
114
+ const out = [];
115
+ for (const span of free) {
116
+ if (blocked.hi <= span.lo || blocked.lo >= span.hi) {
117
+ out.push(span);
118
+ continue;
119
+ }
120
+ if (blocked.lo > span.lo) out.push({ lo: span.lo, hi: blocked.lo });
121
+ if (blocked.hi < span.hi) out.push({ lo: blocked.hi, hi: span.hi });
122
+ }
123
+ return out;
124
+ }
125
+ function atOrAfter(value, free) {
126
+ let found = value;
127
+ for (const span of free) {
128
+ if (span.hi < value) continue;
129
+ found = Math.max(value, span.lo);
130
+ break;
131
+ }
132
+ return found;
133
+ }
134
+ function choose(entry) {
135
+ const candidates = [entry.proposal, entry.source.lo, entry.source.hi, entry.target.lo, entry.target.hi];
136
+ candidates.push((clamp(entry.proposal, entry.source) + clamp(entry.proposal, entry.target)) / 2);
137
+ let best = entry.proposal;
138
+ let bestBends = Infinity;
139
+ let bestFar = Infinity;
140
+ let bestReach = Infinity;
141
+ for (const span of entry.free) {
142
+ for (const value of candidates) {
143
+ const x = clamp(value, span);
144
+ const bends = (inside(x, entry.source) ? 0 : 1) + (inside(x, entry.target) ? 0 : 1);
145
+ const far = Math.abs(x - entry.proposal);
146
+ const reach = Math.abs(x - clamp(x, entry.source)) + Math.abs(x - clamp(x, entry.target));
147
+ const better = bends < bestBends || bends === bestBends && (far < bestFar - 1e-3 || Math.abs(far - bestFar) <= 1e-3 && reach < bestReach);
148
+ if (!better) continue;
149
+ best = x;
150
+ bestBends = bends;
151
+ bestFar = far;
152
+ bestReach = reach;
153
+ }
154
+ }
155
+ return best;
156
+ }
157
+ function adjacentPorts(source, target) {
158
+ const fromMiddle = source.x + source.width / 2;
159
+ const toMiddle = target.x + target.width / 2;
160
+ const fromSpan = spanOf(source);
161
+ const toSpan = spanOf(target);
162
+ const chosen = inside(fromMiddle, toSpan) ? fromMiddle : clamp(toMiddle, fromSpan);
163
+ return { from: clamp(chosen, fromSpan), to: clamp(chosen, toSpan) };
164
+ }
165
+ function assignChannels(input, options) {
166
+ const { layers, chains, chainOf, boxes, layerY, layerHeight, laneX } = input;
167
+ const rowOf = /* @__PURE__ */ new Map();
168
+ layers.forEach((row, depth) => {
169
+ for (const id of row) rowOf.set(id, depth);
170
+ });
171
+ const long = [];
172
+ for (const chain of chains) {
173
+ if (chain.path.length <= 2) continue;
174
+ const head = chain.path[0];
175
+ const tail = chain.path[chain.path.length - 1];
176
+ const from = rowOf.get(head);
177
+ const to = rowOf.get(tail);
178
+ if (from === void 0 || to === void 0 || to <= from) continue;
179
+ const lane = chainOf.get(chain.path[1]);
180
+ const source = boxes.get(head);
181
+ const target = boxes.get(tail);
182
+ if (lane === void 0 || source === void 0 || target === void 0) continue;
183
+ let free = [{ lo: -Infinity, hi: Infinity }];
184
+ for (let depth = from + 1; depth < to; depth += 1) {
185
+ for (const id of layers[depth]) {
186
+ const box = boxes.get(id);
187
+ if (box === void 0) continue;
188
+ free = cut(free, { lo: box.x - options.clearance, hi: box.x + box.width + options.clearance });
189
+ }
190
+ }
191
+ const centre = (source.x + source.width / 2 + target.x + target.width / 2) / 2;
192
+ long.push({
193
+ edge: chain.index,
194
+ lane,
195
+ path: chain.path,
196
+ from,
197
+ to,
198
+ free,
199
+ source: spanOf(source),
200
+ target: spanOf(target),
201
+ proposal: laneX.get(lane) ?? centre,
202
+ x: 0
203
+ });
204
+ }
205
+ for (const entry of long) entry.x = choose(entry);
206
+ const byGap = /* @__PURE__ */ new Map();
207
+ for (const entry of long) {
208
+ for (let gap = entry.from; gap < entry.to; gap += 1) {
209
+ const row = gap + 1 < entry.to ? gap + 1 : gap;
210
+ const id = entry.path[row - entry.from];
211
+ const index = layers[row].indexOf(id);
212
+ byGap.set(gap, [...byGap.get(gap) ?? [], { entry, index }]);
213
+ }
214
+ }
215
+ for (const [, list] of byGap) list.sort((left, right) => left.index - right.index);
216
+ for (let round = 0; round < 4; round += 1) {
217
+ let moved = false;
218
+ for (const [, list] of byGap) {
219
+ let previous = -Infinity;
220
+ for (const item of list) {
221
+ const x = item.entry.x < previous + options.spacing ? atOrAfter(previous + options.spacing, item.entry.free) : item.entry.x;
222
+ if (x !== item.entry.x) {
223
+ item.entry.x = x;
224
+ moved = true;
225
+ }
226
+ previous = item.entry.x;
227
+ }
228
+ }
229
+ if (!moved) break;
230
+ }
231
+ const stacks = /* @__PURE__ */ new Map();
232
+ const want = (gap, edge, left) => {
233
+ stacks.set(gap, [...stacks.get(gap) ?? [], { edge, gap, left }]);
234
+ };
235
+ for (const entry of long) {
236
+ const head = clamp(entry.x, entry.source);
237
+ const tail = clamp(entry.x, entry.target);
238
+ if (Math.abs(head - entry.x) > 0.5) want(entry.from, entry.edge, Math.min(head, entry.x));
239
+ if (Math.abs(tail - entry.x) > 0.5) want(entry.to - 1, entry.edge, Math.min(tail, entry.x));
240
+ }
241
+ for (const chain of chains) {
242
+ if (chain.path.length > 2) continue;
243
+ const head = chain.path[0];
244
+ const tail = chain.path[chain.path.length - 1];
245
+ const from = rowOf.get(head);
246
+ const to = rowOf.get(tail);
247
+ if (from === void 0 || to === void 0 || Math.abs(from - to) !== 1) continue;
248
+ const source = boxes.get(head);
249
+ const target = boxes.get(tail);
250
+ if (source === void 0 || target === void 0) continue;
251
+ const ports = adjacentPorts(source, target);
252
+ want(Math.min(from, to), chain.index, Math.min(ports.from, ports.to));
253
+ }
254
+ const midOf = (gap) => (layerY[gap] + layerHeight[gap] + layerY[gap + 1]) / 2;
255
+ const heightOf = /* @__PURE__ */ new Map();
256
+ for (const [gap, list] of stacks) {
257
+ const top = layerY[gap] + layerHeight[gap];
258
+ const bottom = layerY[gap + 1];
259
+ const pad = Math.min(8, Math.max(1, (bottom - top) / 3));
260
+ const lo = top + pad;
261
+ const hi = Math.max(lo, bottom - pad);
262
+ const sorted = [...list].sort((left, right) => left.left - right.left || left.edge - right.edge);
263
+ sorted.forEach((item, index) => {
264
+ const y = sorted.length === 1 ? (lo + hi) / 2 : lo + (hi - lo) * (index + 1) / (sorted.length + 1);
265
+ const heights = heightOf.get(item.edge) ?? /* @__PURE__ */ new Map();
266
+ heights.set(item.gap, y);
267
+ heightOf.set(item.edge, heights);
268
+ });
269
+ }
270
+ const lanes2 = /* @__PURE__ */ new Map();
271
+ for (const entry of long) {
272
+ const y = [];
273
+ for (let gap = entry.from; gap < entry.to; gap += 1) y.push(heightOf.get(entry.edge)?.get(gap) ?? midOf(gap));
274
+ lanes2.set(entry.edge, { x: entry.x, y });
275
+ }
276
+ const runs = /* @__PURE__ */ new Map();
277
+ for (const [edge, heights] of heightOf) {
278
+ if (lanes2.has(edge)) continue;
279
+ runs.set(edge, [...heights.values()][0]);
280
+ }
281
+ return { lanes: lanes2, runs };
282
+ }
283
+
284
+ // src/render/coords.ts
285
+ function positions(rows) {
286
+ const at = /* @__PURE__ */ new Map();
287
+ for (const row of rows) row.forEach((id, index) => at.set(id, index));
288
+ return at;
289
+ }
290
+ function alignBlocks(frame, at) {
291
+ const root = /* @__PURE__ */ new Map();
292
+ const joined = /* @__PURE__ */ new Map();
293
+ for (const row of frame.rows) {
294
+ for (const id of row) {
295
+ root.set(id, id);
296
+ joined.set(id, id);
297
+ }
298
+ }
299
+ for (let depth = 1; depth < frame.rows.length; depth += 1) {
300
+ let previous = -1;
301
+ for (const id of frame.rows[depth]) {
302
+ const close = frame.near.get(id) ?? [];
303
+ if (close.length === 0) continue;
304
+ const sorted = [...close].sort((left, right) => at.get(left) - at.get(right));
305
+ const middle = (sorted.length - 1) / 2;
306
+ for (let cursor = Math.floor(middle); cursor <= Math.ceil(middle); cursor += 1) {
307
+ if (joined.get(id) !== id) break;
308
+ const target = sorted[cursor];
309
+ if (previous >= at.get(target)) continue;
310
+ joined.set(target, id);
311
+ root.set(id, root.get(target));
312
+ joined.set(id, root.get(id));
313
+ previous = at.get(target);
314
+ }
315
+ }
316
+ }
317
+ return root;
318
+ }
319
+ function compactBlocks(rows, root, widthOf, gap) {
320
+ const clears = /* @__PURE__ */ new Map();
321
+ for (const row of rows) {
322
+ for (let index = 1; index < row.length; index += 1) {
323
+ const left = row[index - 1];
324
+ const right = row[index];
325
+ const leftBlock = root.get(left);
326
+ const rightBlock = root.get(right);
327
+ const space = (widthOf(left) + widthOf(right)) / 2 + gap;
328
+ clears.set(rightBlock, [...clears.get(rightBlock) ?? [], { block: leftBlock, space }]);
329
+ }
330
+ }
331
+ const placed = /* @__PURE__ */ new Map();
332
+ const place2 = (block) => {
333
+ const known = placed.get(block);
334
+ if (known !== void 0) return known;
335
+ let value = 0;
336
+ for (const step of clears.get(block) ?? []) value = Math.max(value, place2(step.block) + step.space);
337
+ placed.set(block, value);
338
+ return value;
339
+ };
340
+ const x = /* @__PURE__ */ new Map();
341
+ for (const row of rows) {
342
+ for (const id of row) {
343
+ const block = root.get(id);
344
+ if (!placed.has(block)) place2(block);
345
+ x.set(id, placed.get(block));
346
+ }
347
+ }
348
+ return x;
349
+ }
350
+ function extentOf(x, rows, widthOf) {
351
+ let left = Infinity;
352
+ let right = -Infinity;
353
+ for (const row of rows) {
354
+ for (const id of row) {
355
+ const value = x.get(id);
356
+ left = Math.min(left, value - widthOf(id) / 2);
357
+ right = Math.max(right, value + widthOf(id) / 2);
358
+ }
359
+ }
360
+ return { left, right };
361
+ }
362
+ function onePass(frame, widthOf, gap) {
363
+ const at = positions(frame.rows);
364
+ const root = alignBlocks(frame, at);
365
+ const x = compactBlocks(frame.rows, root, widthOf, gap);
366
+ if (!frame.mirrored) return x;
367
+ const mirrored = /* @__PURE__ */ new Map();
368
+ for (const [id, value] of x) mirrored.set(id, -value);
369
+ return mirrored;
370
+ }
371
+ function frames(layers, above, below) {
372
+ const fromTop = layers;
373
+ const fromBottom = [...layers].reverse();
374
+ const flip = (rows) => rows.map((row) => [...row].reverse());
375
+ return [
376
+ { rows: fromTop, near: above, mirrored: false },
377
+ { rows: flip(fromTop), near: above, mirrored: true },
378
+ { rows: fromBottom, near: below, mirrored: false },
379
+ { rows: flip(fromBottom), near: below, mirrored: true }
380
+ ];
381
+ }
382
+ function assignCoordinates(input, options) {
383
+ const { layers, chains, sizeOf, chainOf } = input;
384
+ const gap = options.gap;
385
+ const laneIds = new Set(chainOf.values());
386
+ const isDummy = (id) => chainOf.has(id);
387
+ const isPoint = (id) => isDummy(id) || laneIds.has(id);
388
+ const widthOf = (id) => isPoint(id) ? 0 : sizeOf(id).width;
389
+ const heightOf = (id) => isPoint(id) ? 0 : sizeOf(id).height;
390
+ const laneOf = (id) => chainOf.get(id) ?? id;
391
+ const x = /* @__PURE__ */ new Map();
392
+ const read = (id) => x.get(laneOf(id));
393
+ const above = /* @__PURE__ */ new Map();
394
+ const below = /* @__PURE__ */ new Map();
395
+ const laneEnds = /* @__PURE__ */ new Map();
396
+ const link = (into, id, other) => {
397
+ into.set(id, [...into.get(id) ?? [], other]);
398
+ };
399
+ for (const chain of chains) {
400
+ for (let cursor = 0; cursor + 1 < chain.path.length; cursor += 1) {
401
+ const top2 = chain.path[cursor];
402
+ const bottom = chain.path[cursor + 1];
403
+ if (!isDummy(top2) && !isDummy(bottom)) {
404
+ link(below, top2, bottom);
405
+ link(above, bottom, top2);
406
+ }
407
+ }
408
+ if (chain.path.length > 2) {
409
+ const lane = chainOf.get(chain.path[1]);
410
+ if (lane !== void 0) laneEnds.set(lane, [chain.path[0], chain.path[chain.path.length - 1]]);
411
+ }
412
+ }
413
+ const candidates = frames(layers, above, below).map((frame) => onePass(frame, widthOf, gap));
414
+ const leftOrigin = extentOf(candidates[0], layers, widthOf).left;
415
+ const rightOrigin = extentOf(candidates[1], layers, widthOf).right;
416
+ for (const [index, candidate] of candidates.entries()) {
417
+ const extent = extentOf(candidate, layers, widthOf);
418
+ const shift = index % 2 === 0 ? leftOrigin - extent.left : rightOrigin - extent.right;
419
+ for (const [id, value] of candidate) candidate.set(id, value + shift);
420
+ }
421
+ for (const row of layers) {
422
+ for (const id of row) {
423
+ let total = 0;
424
+ for (const candidate of candidates) total += candidate.get(id);
425
+ x.set(id, total / candidates.length);
426
+ }
427
+ }
428
+ const reserved = layers.map((layer2) => {
429
+ let lowest = Infinity;
430
+ let highest = -Infinity;
431
+ for (const id of layer2) {
432
+ const value = x.get(id);
433
+ lowest = Math.min(lowest, value - widthOf(id) / 2);
434
+ highest = Math.max(highest, value + widthOf(id) / 2);
435
+ }
436
+ return { lowest, highest };
437
+ });
438
+ for (const [lane, ends] of laneEnds) x.set(lane, (x.get(ends[0]) + x.get(ends[1])) / 2);
439
+ for (const id of chainOf.keys()) x.delete(id);
440
+ for (const row of layers) {
441
+ let cursor = -Infinity;
442
+ for (const id of row) {
443
+ if (isDummy(id)) {
444
+ cursor = Math.max(cursor, read(id));
445
+ continue;
446
+ }
447
+ const width2 = widthOf(id);
448
+ const centre = Math.max(x.get(id), cursor + width2 / 2);
449
+ x.set(id, centre);
450
+ cursor = centre + width2 / 2 + gap;
451
+ }
452
+ }
453
+ let left = 0;
454
+ for (const [id, value] of x) left = Math.min(left, value - widthOf(id) / 2);
455
+ for (const [id, value] of x) x.set(id, value - left);
456
+ const layerHeight = layers.map((layer2) => Math.max(0, ...layer2.map((id) => heightOf(id))));
457
+ const layerWidth = layers.map((layer2, depth) => {
458
+ let lowest = reserved[depth].lowest - left;
459
+ let highest = reserved[depth].highest - left;
460
+ for (const id of layer2) {
461
+ const value = read(id);
462
+ lowest = Math.min(lowest, value - widthOf(id) / 2);
463
+ highest = Math.max(highest, value + widthOf(id) / 2);
464
+ }
465
+ return highest - lowest;
466
+ });
467
+ const layerY = [];
468
+ let top = 0;
469
+ for (const [depth, height] of layerHeight.entries()) {
470
+ layerY.push(top);
471
+ top += height + options.layerGap;
472
+ }
473
+ let width = 0;
474
+ for (const [id, value] of x) width = Math.max(width, value + widthOf(id) / 2);
475
+ return {
476
+ x,
477
+ layerY,
478
+ layerHeight,
479
+ layerWidth,
480
+ width,
481
+ height: layerY.length === 0 ? 0 : layerY[layerY.length - 1] + layerHeight[layerHeight.length - 1]
482
+ };
483
+ }
484
+
485
+ // src/render/cycle-break.ts
486
+ function tarjan(nodes, outgoing) {
487
+ let counter = 0;
488
+ const index = /* @__PURE__ */ new Map();
489
+ const low = /* @__PURE__ */ new Map();
490
+ const stack = [];
491
+ const onStack = /* @__PURE__ */ new Set();
492
+ const found = [];
493
+ const visit = (node) => {
494
+ index.set(node, counter);
495
+ low.set(node, counter);
496
+ counter += 1;
497
+ stack.push(node);
498
+ onStack.add(node);
499
+ for (const next of outgoing.get(node) ?? []) {
500
+ if (!index.has(next)) {
501
+ visit(next);
502
+ low.set(node, Math.min(low.get(node), low.get(next)));
503
+ continue;
504
+ }
505
+ if (onStack.has(next)) low.set(node, Math.min(low.get(node), index.get(next)));
506
+ }
507
+ if (low.get(node) !== index.get(node)) return;
508
+ const component = [];
509
+ let member;
510
+ do {
511
+ member = stack.pop();
512
+ onStack.delete(member);
513
+ component.push(member);
514
+ } while (member !== node);
515
+ found.push(component);
516
+ };
517
+ for (const node of nodes) if (!index.has(node)) visit(node);
518
+ return found;
519
+ }
520
+ function breakCycles(graph) {
521
+ const known = new Set(graph.nodes.map((node) => node.id));
522
+ const position = new Map(graph.nodes.map((node, offset) => [node.id, offset]));
523
+ const edges = graph.edges.map((edge) => ({ ...edge }));
524
+ const reversed = /* @__PURE__ */ new Set();
525
+ const flip = (index) => {
526
+ const edge = edges[index];
527
+ const next = edge.from;
528
+ edge.from = edge.to;
529
+ edge.to = next;
530
+ };
531
+ if (graph.init !== void 0 && known.has(graph.init)) {
532
+ for (const [index, edge] of edges.entries()) {
533
+ if (edge.to === graph.init && edge.from !== graph.init) flip(index);
534
+ }
535
+ }
536
+ const outgoing = /* @__PURE__ */ new Map();
537
+ for (const edge of edges) {
538
+ if (!known.has(edge.from) || !known.has(edge.to) || edge.from === edge.to) continue;
539
+ outgoing.set(edge.from, [...outgoing.get(edge.from) ?? [], edge.to]);
540
+ }
541
+ const nodes = graph.nodes.map((node) => node.id);
542
+ const raw = tarjan(nodes, outgoing);
543
+ raw.reverse();
544
+ const componentOf = /* @__PURE__ */ new Map();
545
+ const components = raw.map((members, id) => {
546
+ for (const member of members) componentOf.set(member, id);
547
+ return { id, members };
548
+ });
549
+ const condensation = [];
550
+ const seen = /* @__PURE__ */ new Set();
551
+ for (const edge of edges) {
552
+ const from = componentOf.get(edge.from);
553
+ const to = componentOf.get(edge.to);
554
+ if (from === void 0 || to === void 0 || from === to) continue;
555
+ const key = String(from) + ">" + String(to);
556
+ if (seen.has(key)) continue;
557
+ seen.add(key);
558
+ condensation.push({ from, to });
559
+ }
560
+ const rank = /* @__PURE__ */ new Map();
561
+ for (const component of components) {
562
+ if (component.members.length < 2) {
563
+ rank.set(component.members[0], 0);
564
+ continue;
565
+ }
566
+ const inside2 = new Set(component.members);
567
+ const score = /* @__PURE__ */ new Map();
568
+ for (const member of component.members) score.set(member, 0);
569
+ for (const edge of edges) {
570
+ if (!inside2.has(edge.from) || !inside2.has(edge.to) || edge.from === edge.to) continue;
571
+ score.set(edge.from, score.get(edge.from) + 1);
572
+ score.set(edge.to, score.get(edge.to) - 1);
573
+ }
574
+ const ordered = [...component.members].sort((left, right) => {
575
+ const difference = score.get(right) - score.get(left);
576
+ return difference !== 0 ? difference : position.get(left) - position.get(right);
577
+ });
578
+ ordered.forEach((member, offset) => rank.set(member, offset));
579
+ }
580
+ for (const [index, edge] of edges.entries()) {
581
+ const from = rank.get(edge.from);
582
+ const to = rank.get(edge.to);
583
+ if (from === void 0 || to === void 0 || edge.from === edge.to) continue;
584
+ if (componentOf.get(edge.from) !== componentOf.get(edge.to)) continue;
585
+ if (from > to) flip(index);
586
+ }
587
+ edges.forEach((edge, index) => {
588
+ if (graph.edges[index].from !== edge.from) reversed.add(edge.index);
589
+ });
590
+ return { edges, reversed, components, componentOf, rank, condensation };
591
+ }
592
+
593
+ // src/render/dummy.ts
594
+ function addDummies(layering, edges) {
595
+ const layers = layering.layers.map((layer2) => [...layer2]);
596
+ const chains = [];
597
+ const chainOf = /* @__PURE__ */ new Map();
598
+ const laneOf = /* @__PURE__ */ new Map();
599
+ for (const edge of edges) {
600
+ if (edge.from === edge.to) {
601
+ chains.push({ index: edge.index, path: [edge.from, edge.to] });
602
+ continue;
603
+ }
604
+ const from = layering.layerOf.get(edge.from);
605
+ const to = layering.layerOf.get(edge.to);
606
+ if (from === void 0 || to === void 0 || to <= from) {
607
+ chains.push({ index: edge.index, path: [edge.from, edge.to] });
608
+ continue;
609
+ }
610
+ const path = [edge.from];
611
+ for (let cursor = from + 1; cursor < to; cursor += 1) {
612
+ const instance = "#d" + String(edge.index) + "_" + String(cursor);
613
+ const lane = "#e" + String(edge.index);
614
+ chainOf.set(instance, lane);
615
+ laneOf.set(edge.index, lane);
616
+ layers[cursor].push(instance);
617
+ path.push(instance);
618
+ }
619
+ path.push(edge.to);
620
+ chains.push({ index: edge.index, path });
621
+ }
622
+ return { layers, chains, chainOf, laneOf };
623
+ }
624
+
625
+ // src/render/network-simplex.ts
626
+ function slackOf(edge, rank) {
627
+ return rank[edge.to] - rank[edge.from] - edge.minLength;
628
+ }
629
+ function componentsOf(count, edges) {
630
+ const neighbours = [];
631
+ for (let node = 0; node < count; node += 1) neighbours.push([]);
632
+ for (const edge of edges) {
633
+ neighbours[edge.from].push(edge.to);
634
+ neighbours[edge.to].push(edge.from);
635
+ }
636
+ const seen = new Array(count).fill(false);
637
+ const components = [];
638
+ for (let node = 0; node < count; node += 1) {
639
+ if (seen[node]) continue;
640
+ seen[node] = true;
641
+ const members = [node];
642
+ for (let head = 0; head < members.length; head += 1) {
643
+ for (const next of neighbours[members[head]]) {
644
+ if (seen[next]) continue;
645
+ seen[next] = true;
646
+ members.push(next);
647
+ }
648
+ }
649
+ members.sort((left, right) => left - right);
650
+ components.push(members);
651
+ }
652
+ return components;
653
+ }
654
+ function buildTightTree(members, edges, rank, tree) {
655
+ const inside2 = /* @__PURE__ */ new Set([members[0]]);
656
+ while (inside2.size < members.length) {
657
+ let tight = -1;
658
+ for (const [index, edge] of edges.entries()) {
659
+ if (tree.has(index)) continue;
660
+ if (inside2.has(edge.from) === inside2.has(edge.to)) continue;
661
+ if (slackOf(edge, rank) === 0) {
662
+ tight = index;
663
+ break;
664
+ }
665
+ }
666
+ if (tight >= 0) {
667
+ tree.add(tight);
668
+ inside2.add(edges[tight].from);
669
+ inside2.add(edges[tight].to);
670
+ continue;
671
+ }
672
+ let lowest = -Infinity;
673
+ let highest = Infinity;
674
+ for (const edge of edges) {
675
+ const from = inside2.has(edge.from);
676
+ const to = inside2.has(edge.to);
677
+ if (from === to) continue;
678
+ const slack = slackOf(edge, rank);
679
+ if (from) lowest = Math.max(lowest, -slack);
680
+ else highest = Math.min(highest, slack);
681
+ }
682
+ const step = lowest > -Infinity ? lowest : highest;
683
+ for (const node of members) if (!inside2.has(node)) rank[node] += step;
684
+ }
685
+ }
686
+ function walkTree(members, edges, tree) {
687
+ const adjacency = /* @__PURE__ */ new Map();
688
+ const link = (node, other) => {
689
+ adjacency.set(node, [...adjacency.get(node) ?? [], { node: other }]);
690
+ };
691
+ for (const index of tree) {
692
+ const edge = edges[index];
693
+ link(edge.from, edge.to);
694
+ link(edge.to, edge.from);
695
+ }
696
+ const parent = /* @__PURE__ */ new Map();
697
+ const first = /* @__PURE__ */ new Map();
698
+ const size = /* @__PURE__ */ new Map();
699
+ const order2 = [];
700
+ const seen = /* @__PURE__ */ new Set();
701
+ const root = members[0];
702
+ const stack = [root];
703
+ seen.add(root);
704
+ parent.set(root, -1);
705
+ while (stack.length > 0) {
706
+ const node = stack.pop();
707
+ first.set(node, order2.length);
708
+ order2.push(node);
709
+ for (const step of adjacency.get(node)) {
710
+ if (seen.has(step.node)) continue;
711
+ seen.add(step.node);
712
+ parent.set(step.node, node);
713
+ stack.push(step.node);
714
+ }
715
+ }
716
+ for (let index = order2.length - 1; index >= 0; index -= 1) {
717
+ const node = order2[index];
718
+ size.set(node, (size.get(node) ?? 0) + 1);
719
+ const above = parent.get(node);
720
+ if (above !== void 0 && above >= 0) size.set(above, (size.get(above) ?? 0) + size.get(node));
721
+ }
722
+ return { parent, first, size, order: order2 };
723
+ }
724
+ function solveComponent(size, members, edges, rank) {
725
+ if (members.length < 2) {
726
+ if (members.length === 1) rank[members[0]] = 0;
727
+ return;
728
+ }
729
+ const tree = /* @__PURE__ */ new Set();
730
+ buildTightTree(members, edges, rank, tree);
731
+ const spanning = tree.size === members.length - 1;
732
+ if (spanning) improve(size, members, edges, tree, rank);
733
+ let top = Infinity;
734
+ for (const node of members) top = Math.min(top, rank[node]);
735
+ for (const node of members) rank[node] -= top;
736
+ }
737
+ function improve(size, members, edges, tree, rank) {
738
+ const incoming = new Float64Array(size);
739
+ const outgoing = new Float64Array(size);
740
+ for (const edge of edges) {
741
+ outgoing[edge.from] += edge.weight;
742
+ incoming[edge.to] += edge.weight;
743
+ }
744
+ const cap = Math.max(50, 8 * members.length + 4 * edges.length);
745
+ for (let iteration = 0; iteration < cap; iteration += 1) {
746
+ const walk = walkTree(members, edges, tree);
747
+ const entering = /* @__PURE__ */ new Map();
748
+ const leaving = /* @__PURE__ */ new Map();
749
+ for (let index = walk.order.length - 1; index >= 0; index -= 1) {
750
+ const node = walk.order[index];
751
+ entering.set(node, incoming[node] + (entering.get(node) ?? 0));
752
+ leaving.set(node, outgoing[node] + (leaving.get(node) ?? 0));
753
+ const above = walk.parent.get(node);
754
+ if (above === void 0 || above < 0) continue;
755
+ entering.set(above, (entering.get(above) ?? 0) + entering.get(node));
756
+ leaving.set(above, (leaving.get(above) ?? 0) + leaving.get(node));
757
+ }
758
+ const holds = (node, child) => {
759
+ const start = walk.first.get(child);
760
+ const at = walk.first.get(node);
761
+ return at >= start && at < start + walk.size.get(child);
762
+ };
763
+ let worst = -1;
764
+ let worstEdge = -1;
765
+ let worstChild = -1;
766
+ for (const index of tree) {
767
+ const edge = edges[index];
768
+ const child = walk.parent.get(edge.from) === edge.to ? edge.from : edge.to;
769
+ const tailInside = holds(edge.from, child);
770
+ const value = tailInside ? leaving.get(child) - entering.get(child) : entering.get(child) - leaving.get(child);
771
+ if (value < 0 && (worstEdge < 0 || value < worst)) {
772
+ worst = value;
773
+ worstEdge = index;
774
+ worstChild = child;
775
+ }
776
+ }
777
+ if (worstEdge < 0) return;
778
+ const leavingEdge = edges[worstEdge];
779
+ const headInside = !holds(leavingEdge.from, worstChild);
780
+ const inHead = (node) => headInside ? holds(node, worstChild) : !holds(node, worstChild);
781
+ let enteringEdge = -1;
782
+ let step = Infinity;
783
+ for (const [index, edge] of edges.entries()) {
784
+ if (tree.has(index)) continue;
785
+ if (!inHead(edge.from) || inHead(edge.to)) continue;
786
+ const slack = slackOf(edge, rank);
787
+ if (slack < step || slack === step && (enteringEdge < 0 || index < enteringEdge)) {
788
+ step = slack;
789
+ enteringEdge = index;
790
+ }
791
+ }
792
+ if (enteringEdge < 0 || step === Infinity) return;
793
+ if (step !== 0) {
794
+ for (const node of members) if (inHead(node)) rank[node] += step;
795
+ }
796
+ tree.delete(worstEdge);
797
+ tree.add(enteringEdge);
798
+ }
799
+ }
800
+ function minimumLength(problem) {
801
+ const rank = problem.start.slice();
802
+ for (const members of componentsOf(problem.count, problem.edges)) {
803
+ const inside2 = new Set(members);
804
+ const local = problem.edges.filter((edge) => inside2.has(edge.from) && inside2.has(edge.to));
805
+ const pinned = problem.pinned !== void 0 && inside2.has(problem.pinned) ? problem.pinned : void 0;
806
+ const pinnedRank = pinned === void 0 ? void 0 : rank[pinned];
807
+ if (pinned !== void 0 && pinnedRank !== void 0 && members.every((node) => rank[node] >= pinnedRank)) {
808
+ for (const node of members) if (node !== pinned) local.push({ from: pinned, to: node, minLength: 0, weight: 0 });
809
+ }
810
+ solveComponent(problem.count, members, local, rank);
811
+ }
812
+ return rank;
813
+ }
814
+
815
+ // src/render/layer.ts
816
+ function longestPath(ids, edges, index) {
817
+ const rank = new Array(ids.length).fill(0);
818
+ const pending = new Array(ids.length).fill(0);
819
+ const outgoing = ids.map(() => []);
820
+ for (const edge of edges) {
821
+ outgoing[index.get(edge.from)].push(index.get(edge.to));
822
+ pending[index.get(edge.to)] += 1;
823
+ }
824
+ const queue = [];
825
+ for (let node = 0; node < ids.length; node += 1) if (pending[node] === 0) queue.push(node);
826
+ for (let head = 0; head < queue.length; head += 1) {
827
+ const node = queue[head];
828
+ for (const target of outgoing[node]) {
829
+ rank[target] = Math.max(rank[target], rank[node] + 1);
830
+ pending[target] -= 1;
831
+ if (pending[target] === 0) queue.push(target);
832
+ }
833
+ }
834
+ return rank;
835
+ }
836
+ function layer(graph, edges) {
837
+ const ids = [];
838
+ const seen = /* @__PURE__ */ new Set();
839
+ for (const node of graph.nodes) {
840
+ if (seen.has(node.id)) continue;
841
+ seen.add(node.id);
842
+ ids.push(node.id);
843
+ }
844
+ const index = new Map(ids.map((id, offset) => [id, offset]));
845
+ const forward = edges.filter((edge) => index.has(edge.from) && index.has(edge.to) && edge.from !== edge.to);
846
+ const rankEdges = forward.map((edge) => ({
847
+ from: index.get(edge.from),
848
+ to: index.get(edge.to),
849
+ minLength: 1,
850
+ weight: 1
851
+ }));
852
+ let pinned;
853
+ if (graph.init !== void 0 && index.has(graph.init)) {
854
+ const start = index.get(graph.init);
855
+ const forced = forward.some((edge) => edge.to === graph.init);
856
+ if (!forced) pinned = start;
857
+ }
858
+ const rank = minimumLength({ count: ids.length, edges: rankEdges, start: longestPath(ids, forward, index), ...pinned === void 0 ? {} : { pinned } });
859
+ const layers = [];
860
+ for (const [offset, id] of ids.entries()) {
861
+ const depth = rank[offset];
862
+ while (layers.length <= depth) layers.push([]);
863
+ layers[depth].push(id);
864
+ }
865
+ const layerOf = new Map(ids.map((id, offset) => [id, rank[offset]]));
866
+ return { layers, layerOf };
867
+ }
868
+
869
+ // src/render/layout.ts
870
+ function rankStates(nodes, edges, init) {
871
+ const ids = nodes.map((node) => node.id);
872
+ const known = new Set(ids);
873
+ const start = init !== void 0 && known.has(init) ? init : ids[0];
874
+ const outgoing = /* @__PURE__ */ new Map();
875
+ for (const edge of edges) {
876
+ if (!known.has(edge.from) || !known.has(edge.to)) continue;
877
+ outgoing.set(edge.from, [...outgoing.get(edge.from) ?? [], edge.to]);
878
+ }
879
+ const depth = /* @__PURE__ */ new Map();
880
+ if (start !== void 0) {
881
+ depth.set(start, 0);
882
+ const queue = [start];
883
+ while (queue.length > 0) {
884
+ const id = queue.shift();
885
+ const level = depth.get(id);
886
+ for (const target of outgoing.get(id) ?? []) {
887
+ if (depth.has(target)) continue;
888
+ depth.set(target, level + 1);
889
+ queue.push(target);
890
+ }
891
+ }
892
+ }
893
+ const rows = [];
894
+ for (const id of ids) {
895
+ const level = depth.get(id);
896
+ if (level === void 0) continue;
897
+ while (rows.length <= level) rows.push([]);
898
+ rows[level].push(id);
899
+ }
900
+ return { rows, unreachable: ids.filter((id) => !depth.has(id)) };
901
+ }
902
+ function seedOrder(graph) {
903
+ const ranked = rankStates(graph.nodes, graph.edges, graph.init);
904
+ const order2 = /* @__PURE__ */ new Map();
905
+ let index = 0;
906
+ for (const row of ranked.rows) for (const id of row) order2.set(id, index++);
907
+ for (const id of ranked.unreachable) order2.set(id, index++);
908
+ return order2;
909
+ }
910
+
911
+ // src/render/order.ts
912
+ function locate(layers) {
913
+ const found = /* @__PURE__ */ new Map();
914
+ layers.forEach((layer2, depth) => layer2.forEach((id, index) => found.set(id, { layer: depth, index })));
915
+ return found;
916
+ }
917
+ function medianOf(id, neighbours, reference2, positions2) {
918
+ const found = (neighbours.get(id) ?? []).filter((other) => positions2.get(other)?.layer === reference2).map((other) => positions2.get(other).index).sort((left, right) => left - right);
919
+ if (found.length === 0) return positions2.get(id).index;
920
+ const middle = Math.floor(found.length / 2);
921
+ return found.length % 2 === 1 ? found[middle] : (found[middle - 1] + found[middle]) / 2;
922
+ }
923
+ function countCrossings(layers, chains) {
924
+ const positions2 = locate(layers);
925
+ const segments = /* @__PURE__ */ new Map();
926
+ for (const chain of chains) {
927
+ for (let cursor = 0; cursor + 1 < chain.path.length; cursor += 1) {
928
+ const top = positions2.get(chain.path[cursor]);
929
+ const bottom = positions2.get(chain.path[cursor + 1]);
930
+ if (top === void 0 || bottom === void 0 || bottom.layer !== top.layer + 1) continue;
931
+ const list = segments.get(top.layer) ?? [];
932
+ list.push({ top: top.index, bottom: bottom.index });
933
+ segments.set(top.layer, list);
934
+ }
935
+ }
936
+ let crossings = 0;
937
+ for (const list of segments.values()) {
938
+ for (let left = 0; left < list.length; left += 1) {
939
+ for (let right = left + 1; right < list.length; right += 1) {
940
+ if ((list[left].top - list[right].top) * (list[left].bottom - list[right].bottom) < 0) crossings += 1;
941
+ }
942
+ }
943
+ }
944
+ return crossings;
945
+ }
946
+ function order(seedLayers, chains, seed) {
947
+ const layers = seedLayers.map(
948
+ (layer2) => layer2.map((id, index) => ({ id, index })).sort((left, right) => (seed?.get(left.id) ?? Infinity) - (seed?.get(right.id) ?? Infinity) || left.index - right.index).map((entry) => entry.id)
949
+ );
950
+ const down = /* @__PURE__ */ new Map();
951
+ const up = /* @__PURE__ */ new Map();
952
+ for (const chain of chains) {
953
+ for (let cursor = 0; cursor + 1 < chain.path.length; cursor += 1) {
954
+ const top = chain.path[cursor];
955
+ const bottom = chain.path[cursor + 1];
956
+ down.set(top, [...down.get(top) ?? [], bottom]);
957
+ up.set(bottom, [...up.get(bottom) ?? [], top]);
958
+ }
959
+ }
960
+ let positions2 = locate(layers);
961
+ for (let round = 0; round < 4; round += 1) {
962
+ for (let depth = 1; depth < layers.length; depth += 1) {
963
+ const layer2 = layers[depth];
964
+ const sorted = layer2.map((id, index) => ({ id, index, median: medianOf(id, up, depth - 1, positions2) })).sort((left, right) => left.median - right.median || left.index - right.index).map((entry) => entry.id);
965
+ layers[depth] = sorted;
966
+ positions2 = locate(layers);
967
+ }
968
+ for (let depth = layers.length - 2; depth >= 0; depth -= 1) {
969
+ const layer2 = layers[depth];
970
+ const sorted = layer2.map((id, index) => ({ id, index, median: medianOf(id, down, depth + 1, positions2) })).sort((left, right) => left.median - right.median || left.index - right.index).map((entry) => entry.id);
971
+ layers[depth] = sorted;
972
+ positions2 = locate(layers);
973
+ }
974
+ }
975
+ let crossings = countCrossings(layers, chains);
976
+ for (let pass = 0; pass < 4; pass += 1) {
977
+ let improved = false;
978
+ for (const layer2 of layers) {
979
+ for (let index = 0; index + 1 < layer2.length; index += 1) {
980
+ const before = layer2[index];
981
+ layer2[index] = layer2[index + 1];
982
+ layer2[index + 1] = before;
983
+ const next = countCrossings(layers, chains);
984
+ if (next < crossings) {
985
+ crossings = next;
986
+ improved = true;
987
+ } else {
988
+ layer2[index + 1] = layer2[index];
989
+ layer2[index] = before;
990
+ }
991
+ }
992
+ }
993
+ if (!improved) break;
994
+ }
995
+ return { layers, crossings };
996
+ }
997
+
998
+ // src/render/text.ts
999
+ var CHAR_WIDTH = 6.6;
1000
+ var MAX_LINE_CHARS = 30;
1001
+ var LINE_HEIGHT = 15;
1002
+ var TEXT_PADDING = 12;
1003
+ var MIN_NODE_WIDTH = 88;
1004
+ var MAX_NODE_WIDTH = 260;
1005
+ var BREAK = /<br\s*\/?>|\r?\n/gi;
1006
+ function explicitLines(label) {
1007
+ return label.split(BREAK).map((line) => line.trim());
1008
+ }
1009
+ function wrapLine(line, limit = MAX_LINE_CHARS) {
1010
+ if (line === "") return [""];
1011
+ const out = [];
1012
+ let current = "";
1013
+ for (const word of line.split(/\s+/).filter((part) => part !== "")) {
1014
+ const candidate = current === "" ? word : current + " " + word;
1015
+ if (candidate.length <= limit) {
1016
+ current = candidate;
1017
+ continue;
1018
+ }
1019
+ if (current !== "") out.push(current);
1020
+ if (word.length <= limit) {
1021
+ current = word;
1022
+ continue;
1023
+ }
1024
+ let rest = word;
1025
+ while (rest.length > limit) {
1026
+ out.push(rest.slice(0, limit));
1027
+ rest = rest.slice(limit);
1028
+ }
1029
+ current = rest;
1030
+ }
1031
+ if (current !== "") out.push(current);
1032
+ return out.length === 0 ? [""] : out;
1033
+ }
1034
+ function measure(label) {
1035
+ const lines = explicitLines(label).flatMap((line) => wrapLine(line));
1036
+ const longest = lines.reduce((max, line) => Math.max(max, line.length), 0);
1037
+ const width = Math.min(MAX_NODE_WIDTH, Math.max(MIN_NODE_WIDTH, longest * CHAR_WIDTH + TEXT_PADDING * 2));
1038
+ const height = lines.length * LINE_HEIGHT + TEXT_PADDING;
1039
+ return { lines, width, height };
1040
+ }
1041
+ function textWidth(text2) {
1042
+ return text2.length * CHAR_WIDTH;
1043
+ }
1044
+
1045
+ // src/render/wrap.ts
1046
+ var PAGE_ASPECT2 = 1.6;
1047
+ var MIN_LAYERS_TO_WRAP = 4;
1048
+ var STRIP_RATIO = 2;
1049
+ function runHeight(layerHeight, first, last, gap) {
1050
+ let total = 0;
1051
+ for (let index = first; index <= last; index += 1) {
1052
+ total += layerHeight[index];
1053
+ if (index < last) total += gap;
1054
+ }
1055
+ return total;
1056
+ }
1057
+ function runWidth(layerWidth, first, last) {
1058
+ let widest = 0;
1059
+ for (let index = first; index <= last; index += 1) widest = Math.max(widest, layerWidth[index]);
1060
+ return widest;
1061
+ }
1062
+ function shapeError(width, height, aspect) {
1063
+ if (width <= 0 || height <= 0) return Infinity;
1064
+ const ratio = Math.max(width, height) / Math.min(width, height);
1065
+ return Math.abs(Math.log(ratio / aspect));
1066
+ }
1067
+ function planBands(layerWidth, layerHeight, gap, aspect = PAGE_ASPECT2) {
1068
+ const count = layerWidth.length;
1069
+ if (count === 0) return { bands: [], width: 0, height: 0 };
1070
+ const single = {
1071
+ bands: [{ first: 0, last: count - 1, offsetX: 0 }],
1072
+ width: runWidth(layerWidth, 0, count - 1),
1073
+ height: runHeight(layerHeight, 0, count - 1, gap)
1074
+ };
1075
+ if (count < MIN_LAYERS_TO_WRAP || single.height <= single.width * STRIP_RATIO) return single;
1076
+ const columnGap = gap * 2;
1077
+ let best = single;
1078
+ let bestError = shapeError(single.width, single.height, aspect);
1079
+ for (let bands = 2; bands < count; bands += 1) {
1080
+ const base = Math.floor(count / bands);
1081
+ const extra = count % bands;
1082
+ const ranges = [];
1083
+ let cursor = 0;
1084
+ for (let band = 0; band < bands; band += 1) {
1085
+ const size = base + (band < extra ? 1 : 0);
1086
+ ranges.push({ first: cursor, last: cursor + size - 1 });
1087
+ cursor += size;
1088
+ }
1089
+ let width = columnGap * (bands - 1);
1090
+ let height = 0;
1091
+ for (const range of ranges) {
1092
+ width += runWidth(layerWidth, range.first, range.last);
1093
+ height = Math.max(height, runHeight(layerHeight, range.first, range.last, gap));
1094
+ }
1095
+ if (shapeError(width, height, aspect) >= bestError) continue;
1096
+ const planned = [];
1097
+ let offsetX = 0;
1098
+ for (const range of ranges) {
1099
+ planned.push({ first: range.first, last: range.last, offsetX });
1100
+ offsetX += runWidth(layerWidth, range.first, range.last) + columnGap;
1101
+ }
1102
+ best = { bands: planned, width, height };
1103
+ bestError = shapeError(width, height, aspect);
1104
+ }
1105
+ return best;
1106
+ }
1107
+
1108
+ // src/render/place.ts
1109
+ var MARGIN = 20;
1110
+ var LAYER_GAP = 44;
1111
+ var LABELLED_LAYER_GAP = 54;
1112
+ var NODE_GAP = 30;
1113
+ var LANE_CLEARANCE = 12;
1114
+ var LANE_SPACING = 14;
1115
+ var COMPONENT_GAP = 30;
1116
+ var GROUP_PADDING = 10;
1117
+ var GROUP_HEADER = 16;
1118
+ function textOf(node) {
1119
+ return measure(node.label !== void 0 && node.label !== "" ? node.label : node.id);
1120
+ }
1121
+ function groupTree(groups) {
1122
+ const byId = new Map(groups.map((group) => [group.id, group]));
1123
+ const owner = /* @__PURE__ */ new Map();
1124
+ const bySize = [...groups].sort((left, right) => left.members.length - right.members.length);
1125
+ for (const group of bySize) {
1126
+ for (const member of group.members) if (!owner.has(member)) owner.set(member, group.id);
1127
+ }
1128
+ const declared = (group) => {
1129
+ const parent = group.parent;
1130
+ if (parent === void 0 || parent === group.id) return void 0;
1131
+ return byId.has(parent) ? parent : void 0;
1132
+ };
1133
+ const members = new Map(groups.map((group) => [group.id, new Set(group.members)]));
1134
+ const infer = (group) => {
1135
+ let best;
1136
+ for (const other of groups) {
1137
+ if (other.id === group.id) continue;
1138
+ const outer = members.get(other.id);
1139
+ const inner = members.get(group.id);
1140
+ if (inner.size === 0 || inner.size >= outer.size) continue;
1141
+ let contained = true;
1142
+ for (const member of inner) {
1143
+ if (!outer.has(member)) {
1144
+ contained = false;
1145
+ break;
1146
+ }
1147
+ }
1148
+ if (contained && (best === void 0 || outer.size < best.size)) best = { id: other.id, size: outer.size };
1149
+ }
1150
+ return best?.id;
1151
+ };
1152
+ const parentOf = /* @__PURE__ */ new Map();
1153
+ for (const group of groups) {
1154
+ const parent = declared(group) ?? infer(group);
1155
+ if (parent !== void 0) parentOf.set(group.id, parent);
1156
+ }
1157
+ for (const group of groups) {
1158
+ const seen = /* @__PURE__ */ new Set([group.id]);
1159
+ let cursor = parentOf.get(group.id);
1160
+ while (cursor !== void 0) {
1161
+ if (seen.has(cursor)) {
1162
+ parentOf.delete(cursor);
1163
+ break;
1164
+ }
1165
+ seen.add(cursor);
1166
+ cursor = parentOf.get(cursor);
1167
+ }
1168
+ }
1169
+ const children = /* @__PURE__ */ new Map();
1170
+ const roots = [];
1171
+ for (const group of groups) {
1172
+ const parent = parentOf.get(group.id);
1173
+ if (parent === void 0) roots.push(group.id);
1174
+ else children.set(parent, [...children.get(parent) ?? [], group.id]);
1175
+ }
1176
+ return { owner, parentOf, children, roots };
1177
+ }
1178
+ function place(diagram, top) {
1179
+ const groups = diagram.groups ?? [];
1180
+ const nodes = [];
1181
+ const seenNode = /* @__PURE__ */ new Set();
1182
+ for (const node of diagram.nodes) {
1183
+ if (seenNode.has(node.id)) continue;
1184
+ seenNode.add(node.id);
1185
+ nodes.push(node);
1186
+ }
1187
+ const nodeById = new Map(nodes.map((node) => [node.id, node]));
1188
+ const groupById = new Map(groups.map((group) => [group.id, group]));
1189
+ const tree = groupTree(groups);
1190
+ const appearance = /* @__PURE__ */ new Map();
1191
+ for (const node of nodes) if (!appearance.has(node.id)) appearance.set(node.id, appearance.size);
1192
+ for (const group of groups) if (!appearance.has(group.id)) appearance.set(group.id, appearance.size);
1193
+ const visible = /* @__PURE__ */ new Set();
1194
+ const asked = /* @__PURE__ */ new Map();
1195
+ const isVisible = (id) => {
1196
+ const known = asked.get(id);
1197
+ if (known !== void 0) return known;
1198
+ const group = groupById.get(id);
1199
+ let found = false;
1200
+ for (const member of group.members) {
1201
+ if (tree.owner.get(member) === id && nodeById.has(member)) {
1202
+ found = true;
1203
+ break;
1204
+ }
1205
+ }
1206
+ if (!found) {
1207
+ for (const child of tree.children.get(id) ?? []) {
1208
+ if (isVisible(child)) {
1209
+ found = true;
1210
+ break;
1211
+ }
1212
+ }
1213
+ }
1214
+ asked.set(id, found);
1215
+ if (found) visible.add(id);
1216
+ return found;
1217
+ };
1218
+ for (const group of groups) isVisible(group.id);
1219
+ const directNodes = (scope) => nodes.filter((node) => tree.owner.get(node.id) === scope).map((node) => node.id);
1220
+ const directGroups = (scope) => (tree.children.get(scope) ?? []).filter((child) => visible.has(child));
1221
+ const firstIndexCache = /* @__PURE__ */ new Map();
1222
+ const firstIndexOf = (id) => {
1223
+ const cached = firstIndexCache.get(id);
1224
+ if (cached !== void 0) return cached;
1225
+ if (nodeById.has(id)) return appearance.get(id);
1226
+ const members = groupById.get(id).members.filter((member) => tree.owner.get(member) === id && nodeById.has(member)).map((member) => appearance.get(member));
1227
+ const children = directGroups(id).map(firstIndexOf);
1228
+ const all = [...members, ...children];
1229
+ const found = Math.min(...all);
1230
+ firstIndexCache.set(id, found);
1231
+ return found;
1232
+ };
1233
+ const entityAt = (scope, id) => {
1234
+ const boundary = groupById.get(id);
1235
+ if (boundary !== void 0) {
1236
+ if (!visible.has(id)) return void 0;
1237
+ if (scope === void 0) {
1238
+ let cursor = id;
1239
+ while (tree.parentOf.has(cursor)) cursor = tree.parentOf.get(cursor);
1240
+ return cursor;
1241
+ }
1242
+ return tree.parentOf.get(id) === scope ? id : void 0;
1243
+ }
1244
+ if (!nodeById.has(id)) return void 0;
1245
+ const group = tree.owner.get(id);
1246
+ if (scope === void 0) {
1247
+ if (group === void 0 || !visible.has(group)) return id;
1248
+ let cursor = group;
1249
+ while (tree.parentOf.has(cursor)) cursor = tree.parentOf.get(cursor);
1250
+ return cursor;
1251
+ }
1252
+ if (group === scope) return id;
1253
+ if (group === void 0) return void 0;
1254
+ return tree.parentOf.get(group) === scope && visible.has(group) ? group : void 0;
1255
+ };
1256
+ const groupLayouts = /* @__PURE__ */ new Map();
1257
+ const measureGroup = (id) => {
1258
+ const inside2 = buildLevel(id);
1259
+ const size = {
1260
+ width: Math.max(MIN_NODE_WIDTH, inside2.width) + GROUP_PADDING * 2,
1261
+ height: inside2.height + GROUP_HEADER + GROUP_PADDING * 2
1262
+ };
1263
+ groupLayouts.set(id, inside2);
1264
+ return size;
1265
+ };
1266
+ const entityOf = (id) => {
1267
+ const node = nodeById.get(id);
1268
+ if (node !== void 0) {
1269
+ const text2 = textOf(node);
1270
+ return { id, group: false, width: text2.width, height: text2.height };
1271
+ }
1272
+ const size = measureGroup(id);
1273
+ return { id, group: true, width: size.width, height: size.height };
1274
+ };
1275
+ const entitiesIn = (scope) => {
1276
+ const found = [];
1277
+ if (scope === void 0) {
1278
+ for (const node of nodes) {
1279
+ const group = tree.owner.get(node.id);
1280
+ if (group === void 0 || !visible.has(group)) found.push(entityOf(node.id));
1281
+ }
1282
+ for (const root2 of tree.roots) if (visible.has(root2)) found.push(entityOf(root2));
1283
+ } else {
1284
+ for (const id of directNodes(scope)) found.push(entityOf(id));
1285
+ for (const id of directGroups(scope)) found.push(entityOf(id));
1286
+ }
1287
+ return found.sort((left, right2) => firstIndexOf(left.id) - firstIndexOf(right2.id));
1288
+ };
1289
+ const edgesIn = (scope) => {
1290
+ const found = [];
1291
+ diagram.edges.forEach((edge, index) => {
1292
+ if (edge.from === edge.to) return;
1293
+ const from = entityAt(scope, edge.from);
1294
+ const to = entityAt(scope, edge.to);
1295
+ if (from === void 0 || to === void 0 || from === to) return;
1296
+ found.push({ index, from, to });
1297
+ });
1298
+ return found;
1299
+ };
1300
+ const pinnedAt = (scope) => {
1301
+ if (diagram.init === void 0) return void 0;
1302
+ const entity = entityAt(scope, diagram.init);
1303
+ return entity === diagram.init || entity !== void 0 && groupById.has(entity) ? entity : void 0;
1304
+ };
1305
+ const layoutPiece = (entities, edges, pinned, seed) => {
1306
+ const byId = new Map(entities.map((entity) => [entity.id, entity]));
1307
+ const graph = {
1308
+ nodes: entities.map((entity) => ({ id: entity.id, width: entity.width, height: entity.height })),
1309
+ edges,
1310
+ ...pinned === void 0 ? {} : { init: pinned }
1311
+ };
1312
+ const broken = breakCycles(graph);
1313
+ const layered = layer(graph, broken.edges);
1314
+ const augmented = addDummies(layered, broken.edges);
1315
+ const ordered = order(augmented.layers, augmented.chains, seed);
1316
+ const gap = edges.some((edge) => diagram.edges[edge.index].label !== "") ? LABELLED_LAYER_GAP : LAYER_GAP;
1317
+ const coordinates = assignCoordinates(
1318
+ {
1319
+ layers: ordered.layers,
1320
+ chains: augmented.chains,
1321
+ sizeOf: (id) => {
1322
+ const entity = byId.get(id);
1323
+ return { width: entity.width, height: entity.height };
1324
+ },
1325
+ chainOf: augmented.chainOf
1326
+ },
1327
+ { gap: NODE_GAP, layerGap: gap }
1328
+ );
1329
+ const bands = planBands(coordinates.layerWidth, coordinates.layerHeight, gap);
1330
+ const bandOfLayer = new Array(coordinates.layerHeight.length).fill(0);
1331
+ bands.bands.forEach((band, index) => {
1332
+ for (let depth = band.first; depth <= band.last; depth += 1) bandOfLayer[depth] = index;
1333
+ });
1334
+ const layerTop = [];
1335
+ for (const band of bands.bands) {
1336
+ let y = 0;
1337
+ for (let depth = band.first; depth <= band.last; depth += 1) {
1338
+ layerTop[depth] = y;
1339
+ y += coordinates.layerHeight[depth] + gap;
1340
+ }
1341
+ }
1342
+ const items = /* @__PURE__ */ new Map();
1343
+ ordered.layers.forEach((row, depth) => {
1344
+ const shift = bands.bands[bandOfLayer[depth]].offsetX;
1345
+ const top2 = layerTop[depth];
1346
+ const rowHeight = coordinates.layerHeight[depth];
1347
+ for (const id of row) {
1348
+ const entity = byId.get(id);
1349
+ if (entity === void 0) continue;
1350
+ const centre = coordinates.x.get(id);
1351
+ items.set(id, {
1352
+ box: {
1353
+ x: centre + shift - entity.width / 2,
1354
+ y: top2 + (rowHeight - entity.height) / 2,
1355
+ width: entity.width,
1356
+ height: entity.height
1357
+ },
1358
+ group: entity.group
1359
+ });
1360
+ }
1361
+ });
1362
+ const across2 = /* @__PURE__ */ new Set();
1363
+ for (const edge of edges) {
1364
+ const from = layered.layerOf.get(edge.from);
1365
+ const to = layered.layerOf.get(edge.to);
1366
+ if (bandOfLayer[from] !== bandOfLayer[to]) across2.add(edge.index);
1367
+ }
1368
+ const laneProposal = /* @__PURE__ */ new Map();
1369
+ for (const chain of augmented.chains) {
1370
+ if (chain.path.length <= 2) continue;
1371
+ const lane = augmented.chainOf.get(chain.path[1]);
1372
+ const fromLayer = layered.layerOf.get(chain.path[0]);
1373
+ const proposed = coordinates.x.get(lane);
1374
+ laneProposal.set(lane, proposed + bands.bands[bandOfLayer[fromLayer]].offsetX);
1375
+ }
1376
+ const itemBoxes = /* @__PURE__ */ new Map();
1377
+ for (const [id, item] of items) itemBoxes.set(id, item.box);
1378
+ const channels = assignChannels(
1379
+ {
1380
+ layers: ordered.layers,
1381
+ chains: augmented.chains,
1382
+ chainOf: augmented.chainOf,
1383
+ boxes: itemBoxes,
1384
+ layerY: layerTop,
1385
+ layerHeight: coordinates.layerHeight,
1386
+ laneX: laneProposal
1387
+ },
1388
+ { clearance: LANE_CLEARANCE, spacing: LANE_SPACING }
1389
+ );
1390
+ const lanes3 = /* @__PURE__ */ new Map();
1391
+ for (const chain of augmented.chains) {
1392
+ if (chain.path.length <= 2) continue;
1393
+ const start = byId.get(chain.path[0]);
1394
+ const end = byId.get(chain.path[chain.path.length - 1]);
1395
+ if (start.group || end.group) continue;
1396
+ const fromLayer = layered.layerOf.get(chain.path[0]);
1397
+ const toLayer = layered.layerOf.get(chain.path[chain.path.length - 1]);
1398
+ if (bandOfLayer[fromLayer] !== bandOfLayer[toLayer]) continue;
1399
+ const channel = channels.lanes.get(chain.index);
1400
+ lanes3.set(chain.index, {
1401
+ points: channel.y.map((y) => ({ x: channel.x, y })),
1402
+ reversed: broken.reversed.has(chain.index)
1403
+ });
1404
+ }
1405
+ const runs2 = /* @__PURE__ */ new Map();
1406
+ for (const [edgeIndex, y] of channels.runs) if (!across2.has(edgeIndex)) runs2.set(edgeIndex, y);
1407
+ let right2 = 0;
1408
+ let bottom = 0;
1409
+ for (const entry of items.values()) {
1410
+ right2 = Math.max(right2, entry.box.x + entry.box.width);
1411
+ bottom = Math.max(bottom, entry.box.y + entry.box.height);
1412
+ }
1413
+ return { width: right2, height: bottom, items, lanes: lanes3, runs: runs2, across: across2 };
1414
+ };
1415
+ function buildLevel(scope) {
1416
+ const entities = entitiesIn(scope);
1417
+ if (entities.length === 0) {
1418
+ return { width: 0, height: 0, items: /* @__PURE__ */ new Map(), lanes: /* @__PURE__ */ new Map(), runs: /* @__PURE__ */ new Map(), across: /* @__PURE__ */ new Set() };
1419
+ }
1420
+ const edges = edgesIn(scope);
1421
+ const pinned = pinnedAt(scope);
1422
+ const seed = seedOrder({ nodes: entities.map((entity) => ({ id: entity.id, width: entity.width, height: entity.height })), edges, ...pinned === void 0 ? {} : { init: pinned } });
1423
+ const components = weakComponents(entities, edges);
1424
+ const pieces = [];
1425
+ for (const members of components) {
1426
+ const inside2 = new Set(members);
1427
+ const pieceEntities = entities.filter((entity) => inside2.has(entity.id));
1428
+ const pieceEdges = edges.filter((edge) => inside2.has(edge.from) && inside2.has(edge.to));
1429
+ const pin = pinned !== void 0 && inside2.has(pinned) ? pinned : void 0;
1430
+ pieces.push(layoutPiece(pieceEntities, pieceEdges, pin, seed));
1431
+ }
1432
+ const packed = packRectangles(pieces.map((piece) => ({ width: piece.width, height: piece.height })), { gap: COMPONENT_GAP });
1433
+ const layout = { width: packed.width, height: packed.height, items: /* @__PURE__ */ new Map(), lanes: /* @__PURE__ */ new Map(), runs: /* @__PURE__ */ new Map(), across: /* @__PURE__ */ new Set() };
1434
+ pieces.forEach((piece, index) => {
1435
+ for (const edgeIndex of piece.across) layout.across.add(edgeIndex);
1436
+ const at = packed.placements[index];
1437
+ for (const [id, item] of piece.items) {
1438
+ layout.items.set(id, { box: { x: item.box.x + at.x, y: item.box.y + at.y, width: item.box.width, height: item.box.height }, group: item.group });
1439
+ }
1440
+ for (const [edgeIndex, lane] of piece.lanes) {
1441
+ layout.lanes.set(edgeIndex, { points: lane.points.map((point) => ({ x: point.x + at.x, y: point.y + at.y })), reversed: lane.reversed });
1442
+ }
1443
+ for (const [edgeIndex, y] of piece.runs) layout.runs.set(edgeIndex, y + at.y);
1444
+ });
1445
+ return layout;
1446
+ }
1447
+ const root = buildLevel(void 0);
1448
+ const boxes = /* @__PURE__ */ new Map();
1449
+ const groupRects = /* @__PURE__ */ new Map();
1450
+ const lanes2 = /* @__PURE__ */ new Map();
1451
+ const runs = /* @__PURE__ */ new Map();
1452
+ const across = /* @__PURE__ */ new Set();
1453
+ const placeLevel = (layout, originX, originY) => {
1454
+ for (const edgeIndex of layout.across) across.add(edgeIndex);
1455
+ for (const [id, item] of layout.items) {
1456
+ const box = { x: originX + item.box.x, y: originY + item.box.y, width: item.box.width, height: item.box.height };
1457
+ if (item.group) {
1458
+ groupRects.set(id, box);
1459
+ const inside2 = groupLayouts.get(id);
1460
+ placeLevel(inside2, box.x + (box.width - inside2.width) / 2, box.y + GROUP_HEADER + GROUP_PADDING);
1461
+ continue;
1462
+ }
1463
+ boxes.set(id, box);
1464
+ }
1465
+ for (const [edgeIndex, lane] of layout.lanes) {
1466
+ lanes2.set(edgeIndex, { points: lane.points.map((point) => ({ x: point.x + originX, y: point.y + originY })), reversed: lane.reversed });
1467
+ }
1468
+ for (const [edgeIndex, y] of layout.runs) runs.set(edgeIndex, y + originY);
1469
+ };
1470
+ placeLevel(root, MARGIN, top);
1471
+ let right = MARGIN;
1472
+ for (const box of boxes.values()) right = Math.max(right, box.x + box.width);
1473
+ for (const box of groupRects.values()) right = Math.max(right, box.x + box.width);
1474
+ return { boxes, groupRects, width: right + MARGIN, lanes: lanes2, runs, across };
1475
+ }
1476
+
1477
+ // src/render/route.ts
1478
+ var LANE_GAP = 10;
1479
+ var ROUTE_STUB = 16;
1480
+ function normalOf(side) {
1481
+ switch (side) {
1482
+ case "top":
1483
+ return { x: 0, y: -1 };
1484
+ case "bottom":
1485
+ return { x: 0, y: 1 };
1486
+ case "left":
1487
+ return { x: -1, y: 0 };
1488
+ case "right":
1489
+ return { x: 1, y: 0 };
1490
+ }
1491
+ }
1492
+ var BEND_COST = 24;
1493
+ function crossesBox(x1, y1, x2, y2, box) {
1494
+ const minX = box.x + 0.5;
1495
+ const maxX = box.x + box.width - 0.5;
1496
+ const minY = box.y + 0.5;
1497
+ const maxY = box.y + box.height - 0.5;
1498
+ if (Math.abs(y1 - y2) < 1e-3) {
1499
+ const y = y1;
1500
+ if (y <= minY || y >= maxY) return false;
1501
+ return Math.max(x1, x2) > minX && Math.min(x1, x2) < maxX;
1502
+ }
1503
+ if (Math.abs(x1 - x2) < 1e-3) {
1504
+ const x = x1;
1505
+ if (x <= minX || x >= maxX) return false;
1506
+ return Math.max(y1, y2) > minY && Math.min(y1, y2) < maxY;
1507
+ }
1508
+ const steps = Math.max(8, Math.round(Math.hypot(x2 - x1, y2 - y1)));
1509
+ for (let index = 0; index <= steps; index += 1) {
1510
+ const x = x1 + (x2 - x1) * index / steps;
1511
+ const y = y1 + (y2 - y1) * index / steps;
1512
+ if (x > minX && x < maxX && y > minY && y < maxY) return true;
1513
+ }
1514
+ return false;
1515
+ }
1516
+ function anchor(box, side) {
1517
+ switch (side) {
1518
+ case "top":
1519
+ return { x: box.x + box.width / 2, y: box.y };
1520
+ case "bottom":
1521
+ return { x: box.x + box.width / 2, y: box.y + box.height };
1522
+ case "left":
1523
+ return { x: box.x, y: box.y + box.height / 2 };
1524
+ case "right":
1525
+ return { x: box.x + box.width, y: box.y + box.height / 2 };
1526
+ }
1527
+ }
1528
+ function approachSides(from, to) {
1529
+ const fromCy = from.y + from.height / 2;
1530
+ const toCy = to.y + to.height / 2;
1531
+ if (Math.abs(toCy - fromCy) > 0.5) return toCy > fromCy ? { exit: "bottom", entry: "top" } : { exit: "top", entry: "bottom" };
1532
+ const fromCx = from.x + from.width / 2;
1533
+ const toCx = to.x + to.width / 2;
1534
+ return toCx >= fromCx ? { exit: "right", entry: "left" } : { exit: "left", entry: "right" };
1535
+ }
1536
+ function lanes(obstacles, extra) {
1537
+ const values = new Set(extra);
1538
+ for (const box of obstacles) {
1539
+ values.add(box.x - LANE_GAP);
1540
+ values.add(box.x + box.width + LANE_GAP);
1541
+ values.add(box.y - LANE_GAP);
1542
+ values.add(box.y + box.height + LANE_GAP);
1543
+ }
1544
+ return [...values].sort((left, right) => left - right);
1545
+ }
1546
+ function facesAway(side, port, other) {
1547
+ const normal = normalOf(side);
1548
+ return normal.x * (other.x + other.width / 2 - port.x) + normal.y * (other.y + other.height / 2 - port.y) < 0;
1549
+ }
1550
+ function routeEdge(from, to, obstacles, anchors) {
1551
+ const sides = approachSides(from, to);
1552
+ const exitSide = anchors?.exitSide ?? sides.exit;
1553
+ const entrySide = anchors?.entrySide ?? sides.entry;
1554
+ const exit = anchors?.exit ?? anchor(from, exitSide);
1555
+ const entry = anchors?.entry ?? anchor(to, entrySide);
1556
+ const others = obstacles.filter((box) => box !== from && box !== to);
1557
+ const own = [];
1558
+ if (facesAway(exitSide, exit, to)) own.push(from);
1559
+ if (facesAway(entrySide, entry, from)) own.push(to);
1560
+ const blockers = own.length === 0 ? others : [...others, ...own];
1561
+ const blockedStub = (point) => blockers.some((box) => point.x > box.x && point.x < box.x + box.width && point.y > box.y && point.y < box.y + box.height);
1562
+ const outNormal = normalOf(exitSide);
1563
+ const inNormal = normalOf(entrySide);
1564
+ let exitStub = { x: exit.x + outNormal.x * ROUTE_STUB, y: exit.y + outNormal.y * ROUTE_STUB };
1565
+ let entryStub = { x: entry.x + inNormal.x * ROUTE_STUB, y: entry.y + inNormal.y * ROUTE_STUB };
1566
+ if (blockedStub(exitStub) || blockedStub(entryStub)) {
1567
+ exitStub = exit;
1568
+ entryStub = entry;
1569
+ }
1570
+ const straight = [exit, exitStub, entryStub, entry];
1571
+ const axisAligned = Math.abs(exitStub.x - entryStub.x) < 0.5 || Math.abs(exitStub.y - entryStub.y) < 0.5;
1572
+ if (axisAligned && !blockers.some((box) => crossesBox(exitStub.x, exitStub.y, entryStub.x, entryStub.y, box))) {
1573
+ return { points: simplify(straight), routed: false };
1574
+ }
1575
+ const xs = lanes(blockers, [exitStub.x, entryStub.x]);
1576
+ const ys = lanes(blockers, [exitStub.y, entryStub.y]);
1577
+ const blocked = (x1, y1, x2, y2) => blockers.some((box) => crossesBox(x1, y1, x2, y2, box));
1578
+ const start = { point: exitStub, axis: -1, cost: 0, bends: 0, path: exitStub === exit ? [exit] : [exit, exitStub] };
1579
+ const queue = [start];
1580
+ const best = /* @__PURE__ */ new Map();
1581
+ const key = (point) => String(Math.round(point.x)) + "," + String(Math.round(point.y));
1582
+ while (queue.length > 0) {
1583
+ let index = 0;
1584
+ for (let cursor = 1; cursor < queue.length; cursor += 1) if (queue[cursor].cost < queue[index].cost) index = cursor;
1585
+ const current = queue.splice(index, 1)[0];
1586
+ if (current.cost > (best.get(key(current.point)) ?? Infinity)) continue;
1587
+ if (Math.abs(current.point.x - entryStub.x) < 0.5 && Math.abs(current.point.y - entryStub.y) < 0.5) {
1588
+ return { points: simplify([...current.path, entryStub, entry]), routed: true };
1589
+ }
1590
+ const options = [];
1591
+ for (const x of xs) if (Math.abs(x - current.point.x) > 0.5) options.push({ x, y: current.point.y });
1592
+ for (const y of ys) if (Math.abs(y - current.point.y) > 0.5) options.push({ x: current.point.x, y });
1593
+ for (const next of options) {
1594
+ if (blocked(current.point.x, current.point.y, next.x, next.y)) continue;
1595
+ const axis = Math.abs(next.y - current.point.y) < 0.5 ? 0 : 1;
1596
+ const bend = current.axis === -1 || current.axis === axis ? 0 : 1;
1597
+ const cost = current.cost + Math.abs(next.x - current.point.x) + Math.abs(next.y - current.point.y) + bend * BEND_COST;
1598
+ const state = key(next);
1599
+ if (cost >= (best.get(state) ?? Infinity)) continue;
1600
+ best.set(state, cost);
1601
+ queue.push({ point: next, axis, cost, bends: current.bends + bend, path: [...current.path, next] });
1602
+ }
1603
+ }
1604
+ return { points: simplify(straight), routed: false };
1605
+ }
1606
+ function simplify(points) {
1607
+ if (points.length <= 2) return points;
1608
+ const out = [points[0]];
1609
+ for (let index = 1; index < points.length - 1; index += 1) {
1610
+ const previous = out[out.length - 1];
1611
+ const current = points[index];
1612
+ const next = points[index + 1];
1613
+ const collinear = Math.abs(previous.x - current.x) < 0.5 && Math.abs(current.x - next.x) < 0.5 || Math.abs(previous.y - current.y) < 0.5 && Math.abs(current.y - next.y) < 0.5;
1614
+ if (!collinear) out.push(current);
1615
+ }
1616
+ out.push(points[points.length - 1]);
1617
+ return out;
1618
+ }
1619
+
1620
+ // src/render/symbol.ts
1621
+ function cubicPoint(curve, t) {
1622
+ const u = 1 - t;
1623
+ const a = u * u * u;
1624
+ const b = 3 * u * u * t;
1625
+ const c = 3 * u * t * t;
1626
+ const d = t * t * t;
1627
+ return {
1628
+ x: a * curve.from.x + b * curve.c1.x + c * curve.c2.x + d * curve.to.x,
1629
+ y: a * curve.from.y + b * curve.c1.y + c * curve.c2.y + d * curve.to.y
1630
+ };
1631
+ }
1632
+ function diamondPoints(box) {
1633
+ return [
1634
+ { x: box.x + box.width / 2, y: box.y },
1635
+ { x: box.x + box.width, y: box.y + box.height / 2 },
1636
+ { x: box.x + box.width / 2, y: box.y + box.height },
1637
+ { x: box.x, y: box.y + box.height / 2 }
1638
+ ];
1639
+ }
1640
+ function hasDiscretePorts(shape) {
1641
+ return shape === "diamond" || shape === "circle";
1642
+ }
1643
+ function hexagonCut(width) {
1644
+ return Math.min(12, width / 4);
1645
+ }
1646
+ function hexagonPoints(box) {
1647
+ const cut2 = hexagonCut(box.width);
1648
+ return [
1649
+ { x: box.x + cut2, y: box.y },
1650
+ { x: box.x + box.width - cut2, y: box.y },
1651
+ { x: box.x + box.width, y: box.y + box.height / 2 },
1652
+ { x: box.x + box.width - cut2, y: box.y + box.height },
1653
+ { x: box.x + cut2, y: box.y + box.height },
1654
+ { x: box.x, y: box.y + box.height / 2 }
1655
+ ];
1656
+ }
1657
+ function stadiumRadius(box) {
1658
+ return Math.min(box.height / 2, box.width / 2);
1659
+ }
1660
+ function cylinderCaps(box) {
1661
+ const cap = Math.min(10, box.height / 3);
1662
+ return {
1663
+ cap,
1664
+ top: {
1665
+ from: { x: box.x, y: box.y + cap },
1666
+ c1: { x: box.x, y: box.y },
1667
+ c2: { x: box.x + box.width, y: box.y },
1668
+ to: { x: box.x + box.width, y: box.y + cap }
1669
+ },
1670
+ bottom: {
1671
+ from: { x: box.x + box.width, y: box.y + box.height - cap },
1672
+ c1: { x: box.x + box.width, y: box.y + box.height },
1673
+ c2: { x: box.x, y: box.y + box.height },
1674
+ to: { x: box.x, y: box.y + box.height - cap }
1675
+ }
1676
+ };
1677
+ }
1678
+ function cylinderApex(box, side) {
1679
+ const caps = cylinderCaps(box);
1680
+ return cubicPoint(side === "top" ? caps.top : caps.bottom, 0.5);
1681
+ }
1682
+
1683
+ // src/render/palette.ts
1684
+ var NEUTRAL_LIGHT = {
1685
+ pass: "#2f8f5b",
1686
+ findings: "#b7791f",
1687
+ fail: "#c0392b",
1688
+ undecided: "#6b7280",
1689
+ muted: "#9aa3ad",
1690
+ text: "#1f2933",
1691
+ edge: "#6b7280",
1692
+ surface: "transparent",
1693
+ background: "#ffffff"
1694
+ };
1695
+ var NEUTRAL_DARK = {
1696
+ pass: "#4ac486",
1697
+ findings: "#e0b050",
1698
+ fail: "#f2705f",
1699
+ undecided: "#9aa3ad",
1700
+ muted: "#7d8794",
1701
+ text: "#e6e8eb",
1702
+ edge: "#8b949e",
1703
+ surface: "transparent",
1704
+ background: "#0d1117"
1705
+ };
1706
+ var GITHUB_LIGHT = {
1707
+ pass: "#1a7f37",
1708
+ findings: "#9a6700",
1709
+ fail: "#cf222e",
1710
+ undecided: "#656d76",
1711
+ muted: "#8c959f",
1712
+ text: "#1f2328",
1713
+ edge: "#656d76",
1714
+ surface: "transparent",
1715
+ background: "#ffffff"
1716
+ };
1717
+ var GITHUB_DARK = {
1718
+ pass: "#3fb950",
1719
+ findings: "#d29922",
1720
+ fail: "#f85149",
1721
+ undecided: "#7d8590",
1722
+ muted: "#6e7681",
1723
+ text: "#e6edf3",
1724
+ edge: "#7d8590",
1725
+ surface: "transparent",
1726
+ background: "#0d1117"
1727
+ };
1728
+ var HIGH_CONTRAST = {
1729
+ pass: "#0a5c35",
1730
+ findings: "#7a4300",
1731
+ fail: "#a30000",
1732
+ undecided: "#333333",
1733
+ muted: "#444444",
1734
+ text: "#000000",
1735
+ edge: "#000000",
1736
+ surface: "transparent",
1737
+ background: "#ffffff"
1738
+ };
1739
+ var PRINT = {
1740
+ pass: "#333333",
1741
+ findings: "#555555",
1742
+ fail: "#000000",
1743
+ undecided: "#666666",
1744
+ muted: "#777777",
1745
+ text: "#000000",
1746
+ edge: "#333333",
1747
+ surface: "transparent",
1748
+ background: "#ffffff"
1749
+ };
1750
+ var SCHEMES = {
1751
+ default: { light: NEUTRAL_LIGHT, dark: NEUTRAL_DARK },
1752
+ github: { light: GITHUB_LIGHT, dark: GITHUB_DARK },
1753
+ "high-contrast": { light: HIGH_CONTRAST },
1754
+ print: { light: PRINT }
1755
+ };
1756
+ function palette(overrides = {}, options = {}) {
1757
+ const scheme = options.scheme ?? "default";
1758
+ const mode = options.mode ?? "light";
1759
+ const chosen = SCHEMES[scheme] ?? SCHEMES.default;
1760
+ const base = mode === "dark" ? chosen.dark ?? chosen.light : chosen.light;
1761
+ return { ...base, ...overrides };
1762
+ }
1763
+ function roleForVerdict(verdict) {
1764
+ if (verdict === "pass") return "pass";
1765
+ if (verdict === "pass_with_findings") return "findings";
1766
+ if (verdict === "fail") return "fail";
1767
+ return "undecided";
1768
+ }
1769
+ function schemeNames() {
1770
+ return Object.keys(SCHEMES);
1771
+ }
1772
+
1773
+ // src/render/model.ts
1774
+ function emptyDiagram(title, caption) {
1775
+ return { kind: "none", title, nodes: [], edges: [], caption };
1776
+ }
1777
+ function labelOf(node) {
1778
+ return node.label !== void 0 && node.label !== "" ? node.label : node.id;
1779
+ }
1780
+
1781
+ // src/render/paint.ts
1782
+ var TITLE_HEIGHT = 26;
1783
+ var CAPTION_CHARS = 78;
1784
+ var CAPTION_LINE = 15;
1785
+ var CAPTION_GAP = 16;
1786
+ var CAPTION_CHAR_WIDTH = 6.2;
1787
+ var PORT_SEPARATION = 14;
1788
+ var PORT_SIDES = ["top", "right", "bottom", "left"];
1789
+ var isVerticalSide = (side) => side === "top" || side === "bottom";
1790
+ var MULTI_EDGE_SPACING = 20;
1791
+ function packPorts(wanted, low, high) {
1792
+ const placed = [];
1793
+ let previous = low - PORT_SEPARATION;
1794
+ for (const value of wanted) {
1795
+ previous = Math.min(high, Math.max(value, previous + PORT_SEPARATION));
1796
+ placed.push(previous);
1797
+ }
1798
+ let next = high + PORT_SEPARATION;
1799
+ for (let index = placed.length - 1; index >= 0; index -= 1) {
1800
+ next = Math.max(low, Math.min(placed[index], next - PORT_SEPARATION));
1801
+ placed[index] = next;
1802
+ }
1803
+ return placed;
1804
+ }
1805
+ function roleOfNode(node) {
1806
+ if (node.role === "error") return "fail";
1807
+ if (node.role === "warning") return "findings";
1808
+ if (node.role === "muted") return "muted";
1809
+ return "edge";
1810
+ }
1811
+ function wrap(text2, limit) {
1812
+ const lines = [];
1813
+ let line = "";
1814
+ for (const word of text2.split(/\s+/).filter((part) => part !== "")) {
1815
+ const candidate = line === "" ? word : line + " " + word;
1816
+ if (candidate.length <= limit) {
1817
+ line = candidate;
1818
+ continue;
1819
+ }
1820
+ if (line !== "") lines.push(line);
1821
+ line = "";
1822
+ let rest = word;
1823
+ while (rest.length > limit) {
1824
+ lines.push(rest.slice(0, limit));
1825
+ rest = rest.slice(limit);
1826
+ }
1827
+ line = rest;
1828
+ }
1829
+ if (line !== "") lines.push(line);
1830
+ return lines.length === 0 ? [""] : lines;
1831
+ }
1832
+ function captionRole(caption) {
1833
+ return roleForVerdict(caption.split(" \u2014 ")[0]);
1834
+ }
1835
+ var LABEL_HEIGHT = 12;
1836
+ var LABEL_PAD = 2;
1837
+ var LABEL_STEP = 3;
1838
+ function placeLabel(label, from, to, boxes, placed, segments) {
1839
+ const width = textWidth(label);
1840
+ const height = LABEL_HEIGHT;
1841
+ const spotAt = (x, y) => ({ x, y, width, height });
1842
+ const hitsBox = (spot, box) => spot.x - spot.width / 2 < box.x + box.width + LABEL_PAD && spot.x + spot.width / 2 > box.x - LABEL_PAD && spot.y - spot.height / 2 < box.y + box.height + LABEL_PAD && spot.y + spot.height / 2 > box.y - LABEL_PAD;
1843
+ const hitsSpot = (spot, other) => spot.x - spot.width / 2 < other.x + other.width / 2 + LABEL_PAD && spot.x + spot.width / 2 > other.x - other.width / 2 - LABEL_PAD && spot.y - spot.height / 2 < other.y + other.height / 2 + LABEL_PAD && spot.y + spot.height / 2 > other.y - other.height / 2 - LABEL_PAD;
1844
+ const hitsSegment = (spot, segment) => spot.x + spot.width / 2 > Math.min(segment.x1, segment.x2) - LABEL_PAD && spot.x - spot.width / 2 < Math.max(segment.x1, segment.x2) + LABEL_PAD && spot.y + spot.height / 2 > Math.min(segment.y1, segment.y2) - LABEL_PAD && spot.y - spot.height / 2 < Math.max(segment.y1, segment.y2) + LABEL_PAD;
1845
+ const clearOfBoxes = (spot) => {
1846
+ for (const box of boxes) if (hitsBox(spot, box)) return false;
1847
+ for (const earlier of placed) if (hitsSpot(spot, earlier)) return false;
1848
+ return true;
1849
+ };
1850
+ const clearOfEdges = (spot) => {
1851
+ for (const segment of segments) if (hitsSegment(spot, segment)) return false;
1852
+ return true;
1853
+ };
1854
+ const middle = { x: (from.x + to.x) / 2, y: (from.y + to.y) / 2 };
1855
+ const dx = to.x - from.x;
1856
+ const dy = to.y - from.y;
1857
+ const length = Math.abs(dx) + Math.abs(dy);
1858
+ const ux = dx / length;
1859
+ const uy = dy / length;
1860
+ const along = Math.abs(ux) * width / 2 + Math.abs(uy) * height / 2;
1861
+ const bound = Math.max(0, length / 2 - along);
1862
+ const walk = [0];
1863
+ for (let step = LABEL_STEP; step <= bound + 1e-3; step += LABEL_STEP) walk.push(step, -step);
1864
+ const at = (offset) => spotAt(middle.x + ux * offset, middle.y + uy * offset);
1865
+ for (const offset of walk) {
1866
+ const spot = at(offset);
1867
+ if (clearOfBoxes(spot) && clearOfEdges(spot)) return spot;
1868
+ }
1869
+ for (const offset of walk) {
1870
+ const spot = at(offset);
1871
+ if (clearOfBoxes(spot)) return spot;
1872
+ }
1873
+ return at(0);
1874
+ }
1875
+ function drawnExtent(shapes) {
1876
+ let width = 0;
1877
+ let height = 0;
1878
+ const consider = (x, y) => {
1879
+ if (x > width) width = x;
1880
+ if (y > height) height = y;
1881
+ };
1882
+ for (const shape of shapes) {
1883
+ switch (shape.kind) {
1884
+ case "box":
1885
+ case "group":
1886
+ consider(shape.x + shape.width, shape.y + shape.height);
1887
+ break;
1888
+ case "link":
1889
+ consider(Math.max(shape.x1, shape.x2), Math.max(shape.y1, shape.y2));
1890
+ if (shape.label !== "") consider(shape.labelX + textWidth(shape.label) / 2, shape.labelY + LABEL_HEIGHT / 2);
1891
+ break;
1892
+ case "path": {
1893
+ for (const point of shape.points) consider(point.x, point.y);
1894
+ if (shape.label !== "") consider(shape.labelX + textWidth(shape.label) / 2, shape.labelY + LABEL_HEIGHT / 2);
1895
+ break;
1896
+ }
1897
+ case "caption":
1898
+ case "title":
1899
+ consider(textWidth(shape.lines?.[0] ?? "") + shape.x, shape.y + 12);
1900
+ break;
1901
+ }
1902
+ }
1903
+ return { width, height };
1904
+ }
1905
+ function paintWith(diagram, overrides = {}, options = {}) {
1906
+ const colours = palette(overrides, options);
1907
+ const shapes = [];
1908
+ const groups = diagram.groups ?? [];
1909
+ const topOffset = MARGIN + TITLE_HEIGHT;
1910
+ const placement = place(diagram, topOffset);
1911
+ const { boxes, groupRects } = placement;
1912
+ const endBox = (id) => boxes.get(id) ?? groupRects.get(id);
1913
+ const shapeById = /* @__PURE__ */ new Map();
1914
+ for (const node of diagram.nodes) if (node.shape !== void 0 && !shapeById.has(node.id)) shapeById.set(node.id, node.shape);
1915
+ const shapeOf = (id) => shapeById.get(id) ?? "rect";
1916
+ for (const group of groups) {
1917
+ const rect = groupRects.get(group.id);
1918
+ if (rect === void 0) continue;
1919
+ shapes.push({ kind: "group", id: group.id, x: rect.x, y: rect.y, width: rect.width, height: rect.height, label: group.label, colour: "muted" });
1920
+ }
1921
+ const drawn = /* @__PURE__ */ new Set();
1922
+ const boxList = Array.from(boxes.values());
1923
+ const routes = [];
1924
+ for (const node of diagram.nodes) {
1925
+ if (drawn.has(node.id)) continue;
1926
+ const box = boxes.get(node.id);
1927
+ drawn.add(node.id);
1928
+ const text2 = textOf(node);
1929
+ shapes.push({
1930
+ kind: "box",
1931
+ x: box.x,
1932
+ y: box.y,
1933
+ width: box.width,
1934
+ height: box.height,
1935
+ label: labelOf(node),
1936
+ lines: text2.lines,
1937
+ ...node.shape === void 0 ? {} : { shape: node.shape },
1938
+ colour: roleOfNode(node),
1939
+ initial: diagram.init === node.id
1940
+ });
1941
+ }
1942
+ const roleById = new Map(diagram.nodes.map((node) => [node.id, roleOfNode(node)]));
1943
+ const multiEdgeSets = /* @__PURE__ */ new Map();
1944
+ diagram.edges.forEach((edge, index) => {
1945
+ if (edge.from === edge.to) return;
1946
+ if (endBox(edge.from) === void 0 || endBox(edge.to) === void 0) return;
1947
+ const key = edge.from < edge.to ? edge.from + "\0" + edge.to : edge.to + "\0" + edge.from;
1948
+ const list = multiEdgeSets.get(key);
1949
+ if (list === void 0) multiEdgeSets.set(key, [index]);
1950
+ else list.push(index);
1951
+ });
1952
+ const fanOf = /* @__PURE__ */ new Map();
1953
+ for (const list of multiEdgeSets.values()) {
1954
+ if (list.length < 2) continue;
1955
+ list.forEach((index, position) => {
1956
+ if (position === 0) return;
1957
+ fanOf.set(index, (position % 2 === 1 ? 1 : -1) * Math.ceil(position / 2) * MULTI_EDGE_SPACING);
1958
+ });
1959
+ }
1960
+ const endKey = (index, end) => String(index) + ":" + end;
1961
+ const endsAt = /* @__PURE__ */ new Map();
1962
+ for (const edge of diagram.edges) {
1963
+ if (edge.from === edge.to) continue;
1964
+ if (endBox(edge.from) === void 0 || endBox(edge.to) === void 0) continue;
1965
+ endsAt.set(edge.from, (endsAt.get(edge.from) ?? 0) + 1);
1966
+ endsAt.set(edge.to, (endsAt.get(edge.to) ?? 0) + 1);
1967
+ }
1968
+ const claimed = /* @__PURE__ */ new Map();
1969
+ const claimedAt = /* @__PURE__ */ new Map();
1970
+ const claim = (id, index, end, side) => {
1971
+ claimed.set(endKey(index, end), side);
1972
+ claimedAt.set(id, [...claimedAt.get(id) ?? [], side]);
1973
+ };
1974
+ const bestSide = (id, other) => {
1975
+ const box = endBox(id);
1976
+ const centre = { x: box.x + box.width / 2, y: box.y + box.height / 2 };
1977
+ const toward = { x: other.x + other.width / 2 - centre.x, y: other.y + other.height / 2 - centre.y };
1978
+ const reach = Math.hypot(toward.x, toward.y);
1979
+ const taken = claimedAt.get(id) ?? [];
1980
+ let best;
1981
+ for (const side of PORT_SIDES) {
1982
+ if (taken.includes(side)) continue;
1983
+ const normal = normalOf(side);
1984
+ const score = reach === 0 ? 0 : (normal.x * toward.x + normal.y * toward.y) / reach;
1985
+ const point = anchor(box, side);
1986
+ const near = Math.hypot(
1987
+ Math.max(other.x - point.x, 0, point.x - (other.x + other.width)),
1988
+ Math.max(other.y - point.y, 0, point.y - (other.y + other.height))
1989
+ );
1990
+ const better = best === void 0 || score > best.score + 1e-9 || Math.abs(score - best.score) <= 1e-9 && near < best.near - 1e-9;
1991
+ if (better) best = { side, score, near };
1992
+ }
1993
+ return best?.side;
1994
+ };
1995
+ diagram.edges.forEach((edge, index) => {
1996
+ if (edge.from === edge.to) return;
1997
+ const source = endBox(edge.from);
1998
+ const target = endBox(edge.to);
1999
+ if (source === void 0 || target === void 0) return;
2000
+ if (placement.across.has(index)) {
2001
+ claim(edge.from, index, "exit", "right");
2002
+ claim(edge.to, index, "entry", "left");
2003
+ return;
2004
+ }
2005
+ const lane = placement.lanes.get(index);
2006
+ if (lane === void 0 || lane.points.length === 0) return;
2007
+ const down = target.y > source.y;
2008
+ claim(edge.from, index, "exit", down ? "bottom" : "top");
2009
+ claim(edge.to, index, "entry", down ? "top" : "bottom");
2010
+ });
2011
+ diagram.edges.forEach((edge, index) => {
2012
+ if (edge.from === edge.to) return;
2013
+ const source = endBox(edge.from);
2014
+ const target = endBox(edge.to);
2015
+ if (source === void 0 || target === void 0) return;
2016
+ for (const end of ["exit", "entry"]) {
2017
+ if (claimed.has(endKey(index, end))) continue;
2018
+ const id = end === "exit" ? edge.from : edge.to;
2019
+ if ((endsAt.get(id) ?? 0) < 2) continue;
2020
+ if (!hasDiscretePorts(shapeOf(id))) continue;
2021
+ const side = bestSide(id, end === "exit" ? target : source);
2022
+ if (side === void 0) continue;
2023
+ claim(id, index, end, side);
2024
+ }
2025
+ });
2026
+ const facing = diagram.edges.map((edge, index) => {
2027
+ const source = endBox(edge.from);
2028
+ const target = endBox(edge.to);
2029
+ if (source === void 0 || target === void 0 || edge.from === edge.to) return void 0;
2030
+ const sides2 = placement.across.has(index) ? { exit: "right", entry: "left" } : approachSides(source, target);
2031
+ const lane = placement.lanes.get(index);
2032
+ if (lane === void 0 || lane.points.length === 0) {
2033
+ return {
2034
+ exit: claimed.get(endKey(index, "exit")) ?? sides2.exit,
2035
+ entry: claimed.get(endKey(index, "entry")) ?? sides2.entry
2036
+ };
2037
+ }
2038
+ const down = target.y > source.y;
2039
+ return {
2040
+ exit: claimed.get(endKey(index, "exit")) ?? (down ? "bottom" : "top"),
2041
+ entry: claimed.get(endKey(index, "entry")) ?? (down ? "top" : "bottom"),
2042
+ at: lane.points[0].x
2043
+ };
2044
+ });
2045
+ const laneClear = (points, x, from, to) => {
2046
+ for (let at = 0; at + 1 < points.length; at += 1) {
2047
+ for (const box of boxList) {
2048
+ if (box === from || box === to) continue;
2049
+ if (crossesBox(x, points[at].y, x, points[at + 1].y, box)) return false;
2050
+ }
2051
+ }
2052
+ return true;
2053
+ };
2054
+ const laneShift = /* @__PURE__ */ new Map();
2055
+ for (const list of multiEdgeSets.values()) {
2056
+ if (list.length < 2) continue;
2057
+ const lead = placement.lanes.get(list[0]);
2058
+ if (lead === void 0 || lead.points.length === 0) continue;
2059
+ const base = lead.points[0].x;
2060
+ const taken = /* @__PURE__ */ new Set();
2061
+ for (const index of list) {
2062
+ const lane = placement.lanes.get(index);
2063
+ const own = lane.points[0].x;
2064
+ const offset = fanOf.get(index) ?? 0;
2065
+ const edge = diagram.edges[index];
2066
+ const from = endBox(edge.from);
2067
+ const to = endBox(edge.to);
2068
+ let target = own;
2069
+ if (offset !== 0) {
2070
+ for (const candidate of [base + offset, own + offset]) {
2071
+ if (taken.has(candidate) || !laneClear(lane.points, candidate, from, to)) continue;
2072
+ target = candidate;
2073
+ break;
2074
+ }
2075
+ }
2076
+ taken.add(target);
2077
+ if (target !== own) laneShift.set(index, target - own);
2078
+ }
2079
+ }
2080
+ const wanted = /* @__PURE__ */ new Map();
2081
+ const slotKey = (boxId, side, index) => boxId + ":" + side + ":" + String(index);
2082
+ const spanOf2 = (boxId, side, corner) => {
2083
+ const box = endBox(boxId);
2084
+ const vertical = side === "top" || side === "bottom";
2085
+ const middle = vertical ? box.x + box.width / 2 : box.y + box.height / 2;
2086
+ const within = (low, high) => ({ low, high: Math.max(low, high) });
2087
+ switch (shapeOf(boxId)) {
2088
+ // The four vertices of a diamond, and the four cardinal points of an ellipse, are the middles of its sides.
2089
+ case "diamond":
2090
+ case "circle":
2091
+ return within(middle, middle);
2092
+ case "hexagon":
2093
+ return vertical ? within(box.x + hexagonCut(box.width), box.x + box.width - hexagonCut(box.width)) : within(middle, middle);
2094
+ case "stadium": {
2095
+ const radius = stadiumRadius(box);
2096
+ return vertical ? within(box.x + radius, box.x + box.width - radius) : within(middle, middle);
2097
+ }
2098
+ case "cylinder": {
2099
+ if (vertical) return within(middle, middle);
2100
+ const cap = cylinderCaps(box).cap;
2101
+ return within(box.y + cap, box.y + box.height - cap);
2102
+ }
2103
+ default: {
2104
+ const inset = corner ? 2 : vertical ? PORT_INSET : 8;
2105
+ return vertical ? within(box.x + inset, box.x + box.width - inset) : within(box.y + inset, box.y + box.height - inset);
2106
+ }
2107
+ }
2108
+ };
2109
+ const clampTo = (boxId, side, value, corner) => {
2110
+ const span = spanOf2(boxId, side, corner);
2111
+ return Math.min(span.high, Math.max(span.low, value));
2112
+ };
2113
+ const pointOn = (boxId, side, along) => {
2114
+ const box = endBox(boxId);
2115
+ if (side === "left") return { x: box.x, y: along };
2116
+ if (side === "right") return { x: box.x + box.width, y: along };
2117
+ if (side === "top") return { x: along, y: shapeOf(boxId) === "cylinder" ? cylinderApex(box, "top").y : box.y };
2118
+ return { x: along, y: shapeOf(boxId) === "cylinder" ? cylinderApex(box, "bottom").y : box.y + box.height };
2119
+ };
2120
+ for (const [index, entry] of facing.entries()) {
2121
+ if (entry === void 0) continue;
2122
+ const edge = diagram.edges[index];
2123
+ const from = endBox(edge.from);
2124
+ const to = endBox(edge.to);
2125
+ const middle = (box, side) => isVerticalSide(side) ? box.x + box.width / 2 : box.y + box.height / 2;
2126
+ const fromMiddle = middle(from, entry.exit);
2127
+ const toMiddle = middle(to, entry.entry);
2128
+ const corner = entry.at !== void 0;
2129
+ const offset = fanOf.get(index) ?? 0;
2130
+ let exitWanted;
2131
+ let entryWanted;
2132
+ if (entry.at !== void 0) {
2133
+ const chosen = entry.at + (laneShift.get(index) ?? 0);
2134
+ exitWanted = clampTo(edge.from, entry.exit, chosen, corner);
2135
+ entryWanted = clampTo(edge.to, entry.entry, chosen, corner);
2136
+ } else if (isVerticalSide(entry.exit) === isVerticalSide(entry.entry)) {
2137
+ const target = spanOf2(edge.to, entry.entry, false);
2138
+ const chosen = (fromMiddle >= target.low && fromMiddle <= target.high ? fromMiddle : clampTo(edge.from, entry.exit, toMiddle, false)) + offset;
2139
+ exitWanted = clampTo(edge.from, entry.exit, chosen, corner);
2140
+ entryWanted = clampTo(edge.to, entry.entry, chosen, corner);
2141
+ } else {
2142
+ exitWanted = hasDiscretePorts(shapeOf(edge.from)) ? fromMiddle : clampTo(edge.from, entry.exit, anchor(to, entry.entry).x + normalOf(entry.entry).x * ROUTE_STUB, false) + offset;
2143
+ entryWanted = hasDiscretePorts(shapeOf(edge.to)) ? toMiddle : clampTo(edge.to, entry.entry, anchor(from, entry.exit).x + normalOf(entry.exit).x * ROUTE_STUB, false) + offset;
2144
+ }
2145
+ wanted.set(slotKey(edge.from, entry.exit, index), exitWanted);
2146
+ wanted.set(slotKey(edge.to, entry.entry, index), entryWanted);
2147
+ }
2148
+ const sides = /* @__PURE__ */ new Map();
2149
+ for (const [index, entry] of facing.entries()) {
2150
+ if (entry === void 0) continue;
2151
+ const edge = diagram.edges[index];
2152
+ sides.set(edge.from + ":" + entry.exit, [
2153
+ ...sides.get(edge.from + ":" + entry.exit) ?? [],
2154
+ { index, wanted: wanted.get(slotKey(edge.from, entry.exit, index)) }
2155
+ ]);
2156
+ sides.set(edge.to + ":" + entry.entry, [
2157
+ ...sides.get(edge.to + ":" + entry.entry) ?? [],
2158
+ { index, wanted: wanted.get(slotKey(edge.to, entry.entry, index)) }
2159
+ ]);
2160
+ }
2161
+ let placedPort = /* @__PURE__ */ new Map();
2162
+ for (let round = 0; round < 3; round += 1) {
2163
+ placedPort = /* @__PURE__ */ new Map();
2164
+ for (const [key, list] of sides) {
2165
+ const [boxId, side] = key.split(":");
2166
+ const span = spanOf2(boxId, side, true);
2167
+ const order2 = [...list].sort((left, right) => left.wanted - right.wanted || left.index - right.index);
2168
+ const packed = packPorts(
2169
+ order2.map((entry) => clampTo(boxId, side, entry.wanted, true)),
2170
+ span.low,
2171
+ span.high
2172
+ );
2173
+ order2.forEach((entry, at) => placedPort.set(slotKey(boxId, side, entry.index), packed[at]));
2174
+ }
2175
+ let moved = false;
2176
+ for (const [index, entry] of facing.entries()) {
2177
+ if (entry === void 0) continue;
2178
+ const edge = diagram.edges[index];
2179
+ const vertical = entry.exit === "bottom" || entry.exit === "top";
2180
+ if (!vertical) continue;
2181
+ const exit = placedPort.get(slotKey(edge.from, entry.exit, index));
2182
+ if (Math.abs(exit - placedPort.get(slotKey(edge.to, entry.entry, index))) < 0.01) continue;
2183
+ if (Math.abs(clampTo(edge.to, entry.entry, exit, true) - exit) > 0.01) continue;
2184
+ for (const slot of sides.get(edge.to + ":" + entry.entry)) {
2185
+ if (slot.index !== index) continue;
2186
+ slot.wanted = exit;
2187
+ moved = true;
2188
+ }
2189
+ }
2190
+ if (!moved) break;
2191
+ }
2192
+ const portOf = (boxId, side, edgeIndex) => {
2193
+ const coordinate = placedPort.get(slotKey(boxId, side, edgeIndex));
2194
+ return pointOn(boxId, side, coordinate);
2195
+ };
2196
+ const anchorsFor = /* @__PURE__ */ new Map();
2197
+ for (const [index, entry] of facing.entries()) {
2198
+ if (entry === void 0) continue;
2199
+ const edge = diagram.edges[index];
2200
+ anchorsFor.set(index, {
2201
+ exit: portOf(edge.from, entry.exit, index),
2202
+ entry: portOf(edge.to, entry.entry, index),
2203
+ exitSide: entry.exit,
2204
+ entrySide: entry.entry
2205
+ });
2206
+ }
2207
+ const longestRun = (points) => {
2208
+ let best = { from: points[0], to: points[points.length - 1], length: -1 };
2209
+ for (let index = 0; index + 1 < points.length; index += 1) {
2210
+ const from = points[index];
2211
+ const to = points[index + 1];
2212
+ const length = Math.abs(to.x - from.x) + Math.abs(to.y - from.y);
2213
+ if (length > best.length) best = { from, to, length };
2214
+ }
2215
+ return best;
2216
+ };
2217
+ const pointsOfRoute = (shape) => shape.kind === "path" ? shape.points : [{ x: shape.x1, y: shape.y1 }, { x: shape.x2, y: shape.y2 }];
2218
+ const segmentsOfRoute = (shape) => {
2219
+ const points = pointsOfRoute(shape);
2220
+ const out = [];
2221
+ for (let index = 0; index + 1 < points.length; index += 1) {
2222
+ const from = points[index];
2223
+ const to = points[index + 1];
2224
+ out.push({ x1: from.x, y1: from.y, x2: to.x, y2: to.y });
2225
+ }
2226
+ return out;
2227
+ };
2228
+ const paintPath = (edge, points, colour) => {
2229
+ const shape = {
2230
+ kind: "path",
2231
+ points,
2232
+ label: edge.label,
2233
+ labelX: 0,
2234
+ labelY: 0,
2235
+ colour,
2236
+ ...edge.dashed === void 0 ? {} : { dashed: edge.dashed },
2237
+ ...edge.arrow === void 0 ? {} : { arrow: edge.arrow }
2238
+ };
2239
+ shapes.push(shape);
2240
+ routes.push(shape);
2241
+ };
2242
+ const paintRoute = (edge, points, colour) => {
2243
+ if (points.length > 2) {
2244
+ paintPath(edge, points, colour);
2245
+ return;
2246
+ }
2247
+ const exit = points[0];
2248
+ const entry = points[points.length - 1];
2249
+ const shape = {
2250
+ kind: "link",
2251
+ x1: exit.x,
2252
+ y1: exit.y,
2253
+ x2: entry.x,
2254
+ y2: entry.y,
2255
+ label: edge.label,
2256
+ labelX: 0,
2257
+ labelY: 0,
2258
+ colour,
2259
+ ...edge.dashed === void 0 ? {} : { dashed: edge.dashed },
2260
+ ...edge.arrow === void 0 ? {} : { arrow: edge.arrow }
2261
+ };
2262
+ shapes.push(shape);
2263
+ routes.push(shape);
2264
+ };
2265
+ const runRoute = (ports, runY) => {
2266
+ const out = isVerticalSide(ports.exitSide) ? 0 : normalOf(ports.exitSide).x * ROUTE_STUB;
2267
+ const into = isVerticalSide(ports.entrySide) ? 0 : normalOf(ports.entrySide).x * ROUTE_STUB;
2268
+ const points = [ports.exit];
2269
+ if (out !== 0) points.push({ x: ports.exit.x + out, y: ports.exit.y });
2270
+ points.push({ x: ports.exit.x + out, y: runY }, { x: ports.entry.x + into, y: runY });
2271
+ if (into !== 0) points.push({ x: ports.entry.x + into, y: ports.entry.y });
2272
+ points.push(ports.entry);
2273
+ return simplify(points);
2274
+ };
2275
+ const runFits = (ports, runY) => {
2276
+ if (isVerticalSide(ports.exitSide) && Math.abs(runY - ports.exit.y) > 0.5) {
2277
+ if (Math.sign(runY - ports.exit.y) !== Math.sign(normalOf(ports.exitSide).y)) return false;
2278
+ }
2279
+ if (isVerticalSide(ports.entrySide) && Math.abs(ports.entry.y - runY) > 0.5) {
2280
+ if (Math.sign(ports.entry.y - runY) !== -Math.sign(normalOf(ports.entrySide).y)) return false;
2281
+ }
2282
+ return true;
2283
+ };
2284
+ const clearsBoxes = (points, from, to) => {
2285
+ for (let at = 0; at + 1 < points.length; at += 1) {
2286
+ const a = points[at];
2287
+ const b = points[at + 1];
2288
+ for (const box of boxList) {
2289
+ if (box === from || box === to) continue;
2290
+ if (crossesBox(a.x, a.y, b.x, b.y, box)) return false;
2291
+ }
2292
+ }
2293
+ return true;
2294
+ };
2295
+ for (const [edgeIndex, edge] of diagram.edges.entries()) {
2296
+ const source = endBox(edge.from);
2297
+ const target = endBox(edge.to);
2298
+ if (source === void 0 || target === void 0) continue;
2299
+ const colour = roleById.get(edge.from) ?? "muted";
2300
+ if (edge.from === edge.to) {
2301
+ const x = source.x + source.width;
2302
+ const y = source.y + source.height / 2;
2303
+ shapes.push({
2304
+ kind: "path",
2305
+ points: [
2306
+ { x, y: y - 9 },
2307
+ { x: x + 14, y: y - 9 },
2308
+ { x: x + 14, y: y + 9 },
2309
+ { x, y: y + 9 }
2310
+ ],
2311
+ label: edge.label,
2312
+ // The one label that is not on its own edge: a self loop's longest run is eighteen points, and its name
2313
+ // does not fit on that. It sits beside the loop, where it belongs to the edge and touches nothing.
2314
+ labelX: x + 20 + textWidth(edge.label) / 2,
2315
+ labelY: y,
2316
+ colour,
2317
+ ...edge.dashed === void 0 ? {} : { dashed: edge.dashed },
2318
+ ...edge.arrow === void 0 ? {} : { arrow: edge.arrow }
2319
+ });
2320
+ continue;
2321
+ }
2322
+ const anchors = anchorsFor.get(edgeIndex);
2323
+ const lane = placement.lanes.get(edgeIndex);
2324
+ if (lane !== void 0 && lane.points.length > 0 && anchors !== void 0) {
2325
+ const shift = laneShift.get(edgeIndex) ?? 0;
2326
+ const ordered = lane.reversed ? [...lane.points].reverse() : lane.points;
2327
+ const waypoints = shift === 0 ? ordered : ordered.map((point) => ({ x: point.x + shift, y: point.y }));
2328
+ const points = [anchors.exit];
2329
+ let previous = anchors.exit;
2330
+ for (const waypoint of waypoints) {
2331
+ if (Math.abs(previous.x - waypoint.x) > 0.5 && Math.abs(previous.y - waypoint.y) > 0.5) points.push({ x: previous.x, y: waypoint.y });
2332
+ points.push(waypoint);
2333
+ previous = waypoint;
2334
+ }
2335
+ if (Math.abs(previous.x - anchors.entry.x) > 0.5 && Math.abs(previous.y - anchors.entry.y) > 0.5) points.push({ x: anchors.entry.x, y: previous.y });
2336
+ points.push(anchors.entry);
2337
+ paintPath(edge, simplify(points), colour);
2338
+ continue;
2339
+ }
2340
+ const runY = placement.runs.get(edgeIndex);
2341
+ if (runY !== void 0 && anchors !== void 0 && runFits(anchors, runY)) {
2342
+ const points = runRoute(anchors, runY);
2343
+ const mixed = isVerticalSide(anchors.exitSide) !== isVerticalSide(anchors.entrySide);
2344
+ if (!mixed || clearsBoxes(points, source, target)) {
2345
+ paintRoute(edge, points, colour);
2346
+ continue;
2347
+ }
2348
+ }
2349
+ paintRoute(edge, routeEdge(source, target, boxList, anchors).points, colour);
2350
+ }
2351
+ const routeSegments = routes.map((shape) => segmentsOfRoute(shape));
2352
+ const placedLabels = [];
2353
+ routes.forEach((shape, index) => {
2354
+ const run = longestRun(pointsOfRoute(shape));
2355
+ let spot = { x: (run.from.x + run.to.x) / 2, y: (run.from.y + run.to.y) / 2, width: textWidth(shape.label), height: LABEL_HEIGHT };
2356
+ if (shape.label !== "") {
2357
+ spot = placeLabel(shape.label, run.from, run.to, boxList, placedLabels, routeSegments.flatMap((list, at) => at === index ? [] : list));
2358
+ placedLabels.push(spot);
2359
+ }
2360
+ shape.labelX = spot.x;
2361
+ shape.labelY = spot.y;
2362
+ });
2363
+ shapes.push({ kind: "title", x: MARGIN, y: MARGIN + 12, text: diagram.title, colour: "text" });
2364
+ const contentRight = Math.max(
2365
+ MARGIN,
2366
+ ...Array.from(boxes.values(), (box) => box.x + box.width),
2367
+ ...Array.from(groupRects.values(), (rect) => rect.x + rect.width)
2368
+ );
2369
+ const captionLines = wrap(diagram.caption, CAPTION_CHARS);
2370
+ const captionWidth = Math.max(...captionLines.map((line) => line.length * CAPTION_CHAR_WIDTH)) + MARGIN * 2;
2371
+ const width = Math.max(contentRight + MARGIN, captionWidth, 240);
2372
+ const contentBottom = Math.max(
2373
+ topOffset,
2374
+ ...Array.from(boxes.values(), (box) => box.y + box.height),
2375
+ ...Array.from(groupRects.values(), (rect) => rect.y + rect.height)
2376
+ );
2377
+ const captionTop = contentBottom + CAPTION_GAP;
2378
+ shapes.push({ kind: "caption", x: MARGIN, y: captionTop, lines: captionLines, colour: captionRole(diagram.caption) });
2379
+ const height = captionTop + captionLines.length * CAPTION_LINE + MARGIN / 2;
2380
+ const extent = drawnExtent(shapes);
2381
+ const canvasWidth = Math.max(width, extent.width + MARGIN);
2382
+ const canvasHeight = Math.max(height, extent.height + MARGIN);
2383
+ return { canvas: { width: Math.round(canvasWidth), height: Math.round(canvasHeight), shapes }, colours };
2384
+ }
2385
+
2386
+ // src/render/from-mermaid.ts
2387
+ var CURVED_EDGE = /^(.+?)\s*(-->|---|-\.->|==>)\s*(?:\|([^|]*)\|\s*)?(.+)$/;
2388
+ var LABELLED_EDGE = /^(.+?)\s*--\s*([^->]+?)\s*-->\s*(.+)$/;
2389
+ var NODE = /^([A-Za-z_][\w-]*)\s*(?:\[\((.*)\)\]|\[\[(.*)\]\]|\[(.*)\]|\((.*)\)|\{(.*)\}|<<(.*)>>)?$/;
2390
+ var TRANSITION = /^([A-Za-z_][\w-]*|\[\*\])\s*-->\s*([A-Za-z_][\w-]*|\[\*\])\s*(?::\s*(.*))?$/;
2391
+ var STATE_ALIAS = /^state\s+["'](.+?)["']\s+as\s+([A-Za-z_][\w-]*)$/;
2392
+ var DIRECTIVE = /^(click|style|classDef|class|linkStyle|accTitle|accDescr)\b/;
2393
+ var UNSUPPORTED = /* @__PURE__ */ new Set([
2394
+ "sequencediagram",
2395
+ "classdiagram",
2396
+ "erdiagram",
2397
+ "gantt",
2398
+ "pie",
2399
+ "journey",
2400
+ "mindmap",
2401
+ "timeline",
2402
+ "quadrantchart",
2403
+ "gitgraph",
2404
+ "c4context",
2405
+ "requirementdiagram",
2406
+ "sankey-beta"
2407
+ ]);
2408
+ function ensure(builder, id, label, shape) {
2409
+ const existing = builder.nodes.get(id);
2410
+ if (existing === void 0) {
2411
+ builder.nodes.set(id, {
2412
+ id,
2413
+ role: "normal",
2414
+ ...label === void 0 ? {} : { label },
2415
+ ...shape === void 0 ? {} : { shape }
2416
+ });
2417
+ const innermost = builder.open[builder.open.length - 1];
2418
+ if (innermost !== void 0 && !innermost.members.includes(id)) innermost.members.push(id);
2419
+ return;
2420
+ }
2421
+ if (label !== void 0 && (existing.label === void 0 || existing.label === "")) existing.label = label;
2422
+ }
2423
+ function nodeFrom(token) {
2424
+ const match = NODE.exec(token.trim());
2425
+ if (match === null) return void 0;
2426
+ const id = match[1];
2427
+ const label = [match[2], match[3], match[4], match[5], match[6], match[7]].find((value) => value !== void 0 && value.trim() !== "");
2428
+ const shape = match[2] !== void 0 ? "cylinder" : match[3] !== void 0 ? "subroutine" : match[4] !== void 0 ? "rect" : match[5] !== void 0 ? "stadium" : match[6] !== void 0 ? "diamond" : match[7] !== void 0 ? "hexagon" : void 0;
2429
+ return {
2430
+ id,
2431
+ ...label === void 0 ? {} : { label: label.trim().replace(/^["']|["']$/g, "") },
2432
+ ...shape === void 0 ? {} : { shape }
2433
+ };
2434
+ }
2435
+ function fromMermaid(source, title = "mermaid") {
2436
+ const lines = source.split(/\r?\n/);
2437
+ const refused = [];
2438
+ const builder = { nodes: /* @__PURE__ */ new Map(), edges: [], groups: [], open: [] };
2439
+ let mode = "none";
2440
+ let startId;
2441
+ let endCount = 0;
2442
+ const push = (edge) => {
2443
+ builder.edges.push(edge);
2444
+ };
2445
+ lines.forEach((raw, index) => {
2446
+ const line = raw.replace(/%%(.*)$/, "").trim();
2447
+ if (line === "") return;
2448
+ const where = "line " + String(index + 1) + ": ";
2449
+ const lower = line.toLowerCase();
2450
+ const keyword = lower.split(/[\s{}]/)[0];
2451
+ if (UNSUPPORTED.has(keyword)) {
2452
+ refused.push(where + "`" + keyword + "` is not a diagram this renderer draws");
2453
+ return;
2454
+ }
2455
+ if (DIRECTIVE.test(line)) {
2456
+ refused.push(where + "`" + line.split(/\s/)[0] + "` is a directive this renderer ignores");
2457
+ return;
2458
+ }
2459
+ if (/^(graph|flowchart)\b/.test(lower)) {
2460
+ mode = "graph";
2461
+ return;
2462
+ }
2463
+ if (/^statediagram(-v2)?\b/.test(lower)) {
2464
+ mode = "state";
2465
+ return;
2466
+ }
2467
+ if (/^subgraph\b/.test(lower)) {
2468
+ const label = line.replace(/^subgraph\b/i, "").trim().replace(/^["']|["']$/g, "");
2469
+ const id = "subgraph-" + String(++endCount);
2470
+ builder.open.push({ id, label: label === "" ? id : label, members: [] });
2471
+ return;
2472
+ }
2473
+ if (lower === "end") {
2474
+ const closed = builder.open.pop();
2475
+ if (closed !== void 0) {
2476
+ const container = builder.open[builder.open.length - 1];
2477
+ if (container !== void 0) closed.parent = container.id;
2478
+ builder.groups.push(closed);
2479
+ }
2480
+ return;
2481
+ }
2482
+ if (mode === "state") {
2483
+ const alias = STATE_ALIAS.exec(line);
2484
+ if (alias !== null) {
2485
+ ensure(builder, alias[2], alias[1]);
2486
+ return;
2487
+ }
2488
+ const transition = TRANSITION.exec(line);
2489
+ if (transition === null) {
2490
+ const bare = /^state\s+([A-Za-z_][\w-]*)$/.exec(line);
2491
+ if (bare !== null) {
2492
+ ensure(builder, bare[1]);
2493
+ return;
2494
+ }
2495
+ if (/^[A-Za-z_][\w-]*$/.test(line)) {
2496
+ ensure(builder, line);
2497
+ return;
2498
+ }
2499
+ refused.push(where + "`" + line + "` is not a state transition this renderer draws");
2500
+ return;
2501
+ }
2502
+ const from = transition[1];
2503
+ const to = transition[2];
2504
+ const label = (transition[3] ?? "").trim();
2505
+ if (from === "[*]") {
2506
+ ensure(builder, to);
2507
+ if (startId === void 0) startId = to;
2508
+ return;
2509
+ }
2510
+ if (to === "[*]") {
2511
+ ensure(builder, from);
2512
+ push({ from, to: from, label: label === "" ? "end" : label, dashed: true });
2513
+ return;
2514
+ }
2515
+ ensure(builder, from);
2516
+ ensure(builder, to);
2517
+ push({ from, to, label });
2518
+ return;
2519
+ }
2520
+ const labelled = LABELLED_EDGE.exec(line);
2521
+ if (labelled !== null) {
2522
+ const left = nodeFrom(labelled[1]);
2523
+ const right = nodeFrom(labelled[3]);
2524
+ if (left === void 0 || right === void 0) {
2525
+ refused.push(where + "`" + line + "` does not name two nodes");
2526
+ return;
2527
+ }
2528
+ ensure(builder, left.id, left.label, left.shape);
2529
+ ensure(builder, right.id, right.label, right.shape);
2530
+ push({ from: left.id, to: right.id, label: labelled[2].trim() });
2531
+ return;
2532
+ }
2533
+ const edge = CURVED_EDGE.exec(line);
2534
+ if (edge !== null) {
2535
+ const left = nodeFrom(edge[1]);
2536
+ const right = nodeFrom(edge[4]);
2537
+ if (left === void 0 || right === void 0) {
2538
+ refused.push(where + "`" + line + "` does not name two nodes");
2539
+ return;
2540
+ }
2541
+ ensure(builder, left.id, left.label, left.shape);
2542
+ ensure(builder, right.id, right.label, right.shape);
2543
+ push({
2544
+ from: left.id,
2545
+ to: right.id,
2546
+ label: (edge[3] ?? "").trim(),
2547
+ ...edge[2] === "-.->" || edge[2] === "==>" ? { dashed: true } : {}
2548
+ });
2549
+ return;
2550
+ }
2551
+ const single = nodeFrom(line);
2552
+ if (single !== void 0) {
2553
+ ensure(builder, single.id, single.label, single.shape);
2554
+ return;
2555
+ }
2556
+ refused.push(where + "`" + line + "` is not something this renderer draws");
2557
+ });
2558
+ if (mode === "none") {
2559
+ return { diagram: emptyDiagram(title, "no mermaid diagram found"), refused };
2560
+ }
2561
+ if (builder.nodes.size === 0) {
2562
+ return { diagram: emptyDiagram(title, "no nodes found"), refused };
2563
+ }
2564
+ const diagram = {
2565
+ kind: mode === "state" ? "machine" : "graph",
2566
+ title,
2567
+ ...startId === void 0 ? {} : { init: startId },
2568
+ nodes: [...builder.nodes.values()],
2569
+ edges: builder.edges,
2570
+ ...builder.groups.length === 0 ? {} : { groups: builder.groups },
2571
+ caption: mode + ", " + String(builder.nodes.size) + " box" + (builder.nodes.size === 1 ? "" : "es") + ", " + String(builder.edges.length) + " arrow" + (builder.edges.length === 1 ? "" : "s") + (refused.length === 0 ? "" : ", " + String(refused.length) + " line(s) refused")
2572
+ };
2573
+ return { diagram, refused };
2574
+ }
2575
+
2576
+ // src/render/collapse.ts
2577
+ function subtreeSize(groups, groupId, seen) {
2578
+ if (seen.has(groupId)) return 0;
2579
+ seen.add(groupId);
2580
+ const group = groups.find((entry) => entry.id === groupId);
2581
+ const children = groups.filter((entry) => entry.parent === groupId);
2582
+ return group.members.length + children.reduce((total, child) => total + subtreeSize(groups, child.id, seen), 0);
2583
+ }
2584
+ function ownerOf(groups, nodeId) {
2585
+ return groups.find((group) => group.members.includes(nodeId))?.id;
2586
+ }
2587
+ function collapse(diagram, folded) {
2588
+ const asked = new Set(folded);
2589
+ const groups = diagram.groups ?? [];
2590
+ if (asked.size === 0 || groups.length === 0) return diagram;
2591
+ const byId = new Map(groups.map((group) => [group.id, group]));
2592
+ const targets = /* @__PURE__ */ new Set();
2593
+ for (const id of asked) {
2594
+ const group = byId.get(id);
2595
+ if (group !== void 0 && subtreeSize(groups, id, /* @__PURE__ */ new Set()) > 0) targets.add(id);
2596
+ }
2597
+ if (targets.size === 0) return diagram;
2598
+ for (const id of [...targets]) {
2599
+ let cursor = byId.get(id)?.parent;
2600
+ const seen = /* @__PURE__ */ new Set();
2601
+ while (cursor !== void 0 && !seen.has(cursor)) {
2602
+ seen.add(cursor);
2603
+ if (targets.has(cursor)) {
2604
+ targets.delete(id);
2605
+ break;
2606
+ }
2607
+ cursor = byId.get(cursor)?.parent;
2608
+ }
2609
+ }
2610
+ const removedNodes = /* @__PURE__ */ new Set();
2611
+ const removedGroups = /* @__PURE__ */ new Set();
2612
+ const descend = (groupId) => {
2613
+ removedGroups.add(groupId);
2614
+ const group = byId.get(groupId);
2615
+ for (const member of group.members) removedNodes.add(member);
2616
+ for (const child of groups.filter((entry) => entry.parent === groupId)) descend(child.id);
2617
+ };
2618
+ for (const id of targets) descend(id);
2619
+ const foldOf = (id) => {
2620
+ for (const target of targets) {
2621
+ const inSubtree = (groupId, seen) => {
2622
+ if (seen.has(groupId)) return false;
2623
+ seen.add(groupId);
2624
+ if (groupId === target) return true;
2625
+ return groups.filter((entry) => entry.parent === groupId).some((child) => inSubtree(child.id, seen));
2626
+ };
2627
+ const group = byId.get(target);
2628
+ if (group.members.includes(id) || inSubtree(ownerOf(groups, id), /* @__PURE__ */ new Set())) return target;
2629
+ }
2630
+ return void 0;
2631
+ };
2632
+ const nodes = [];
2633
+ const emitted = /* @__PURE__ */ new Set();
2634
+ for (const node of diagram.nodes) {
2635
+ if (!removedNodes.has(node.id)) {
2636
+ nodes.push(node);
2637
+ continue;
2638
+ }
2639
+ const fold = foldOf(node.id);
2640
+ if (fold === void 0 || emitted.has(fold)) continue;
2641
+ emitted.add(fold);
2642
+ const group = byId.get(fold);
2643
+ nodes.push({ id: fold, label: group.label + " (" + String(subtreeSize(groups, fold, /* @__PURE__ */ new Set())) + ")", role: "normal" });
2644
+ }
2645
+ for (const id of targets) {
2646
+ if (emitted.has(id)) continue;
2647
+ const group = byId.get(id);
2648
+ nodes.push({ id, label: group.label + " (" + String(subtreeSize(groups, id, /* @__PURE__ */ new Set())) + ")", role: "normal" });
2649
+ }
2650
+ const edges = [];
2651
+ const seenEdges = /* @__PURE__ */ new Set();
2652
+ for (const edge of diagram.edges) {
2653
+ const from = removedNodes.has(edge.from) ? foldOf(edge.from) : edge.from;
2654
+ const to = removedNodes.has(edge.to) ? foldOf(edge.to) : edge.to;
2655
+ if (from === void 0 || to === void 0) continue;
2656
+ if (from === to) continue;
2657
+ const key = from + ">" + to + ":" + edge.label;
2658
+ if (seenEdges.has(key)) continue;
2659
+ seenEdges.add(key);
2660
+ edges.push({ ...edge, from, to });
2661
+ }
2662
+ const adopted = /* @__PURE__ */ new Map();
2663
+ for (const id of targets) {
2664
+ const parent = byId.get(id)?.parent;
2665
+ if (parent === void 0) continue;
2666
+ adopted.set(parent, [...adopted.get(parent) ?? [], id]);
2667
+ }
2668
+ const remaining = groups.filter((group) => !removedGroups.has(group.id) && targets.has(group.id) === false).map((group) => {
2669
+ const extra = adopted.get(group.id);
2670
+ if (extra === void 0) return group;
2671
+ return { ...group, members: [...group.members.filter((member) => !removedNodes.has(member)), ...extra] };
2672
+ });
2673
+ const caption = diagram.caption + (targets.size === 1 ? ", 1 group folded" : ", " + String(targets.size) + " groups folded");
2674
+ return {
2675
+ ...diagram,
2676
+ nodes,
2677
+ edges,
2678
+ ...remaining.length === 0 ? { groups: void 0 } : { groups: remaining },
2679
+ caption
2680
+ };
2681
+ }
2682
+
2683
+ // src/render/from-plantuml.ts
2684
+ var SHAPES = {
2685
+ component: "rect",
2686
+ node: "rect",
2687
+ rectangle: "rect",
2688
+ artifact: "rect",
2689
+ state: "rect",
2690
+ database: "cylinder",
2691
+ // PlantUML draws an entity as a cylinder in a component diagram; `entity` in an ER diagram is a different
2692
+ // symbol, and that diagram family is not one this renderer accepts.
2693
+ entity: "cylinder",
2694
+ // A use case is an ellipse. The `circle` symbol is written as an ellipse whose radii follow the box, so it
2695
+ // reads as the familiar use-case oval; `stadium` is the pill a terminal state is drawn as.
2696
+ usecase: "circle"
2697
+ };
2698
+ var BOUNDARY = /* @__PURE__ */ new Set(["package", "folder", "node", "component", "rectangle", "artifact"]);
2699
+ var NOT_DRAWN = {
2700
+ actor: "an actor is a stick figure, not one of the shapes this renderer draws",
2701
+ participant: "a participant is a lifeline, not one of the shapes this renderer draws",
2702
+ class: "a class box has compartments this renderer does not draw",
2703
+ object: "an object box has compartments this renderer does not draw",
2704
+ interface: "an interface is a circle or a boxed stereotype, neither of which this renderer draws",
2705
+ enum: "an enum box has compartments this renderer does not draw",
2706
+ annotation: "an annotation is not one of the shapes this renderer draws",
2707
+ cloud: "a cloud outline is not one of the shapes this renderer draws",
2708
+ frame: "a frame tab is not one of the shapes this renderer draws",
2709
+ card: "a card is not one of the shapes this renderer draws",
2710
+ file: "a file outline is not one of the shapes this renderer draws",
2711
+ agent: "an agent is not one of the shapes this renderer draws",
2712
+ boundary: "the boundary symbol is not one of the shapes this renderer draws",
2713
+ control: "the control symbol is not one of the shapes this renderer draws",
2714
+ collections: "a collections symbol is not one of the shapes this renderer draws",
2715
+ queue: "a queue is not one of the shapes this renderer draws",
2716
+ stack: "a stack is not one of the shapes this renderer draws",
2717
+ storage: "a storage symbol is not one of the shapes this renderer draws",
2718
+ note: "a note is not one of the shapes this renderer draws",
2719
+ legend: "a legend is not one of the shapes this renderer draws"
2720
+ };
2721
+ var FRAGMENT = /* @__PURE__ */ new Set(["sequence", "alt", "opt", "loop", "par", "group", "ref", "break", "critical", "activate", "deactivate", "destroy", "create", "newpage", "autonumber"]);
2722
+ var ARROW = /^(.+?)\s*(<\|?|[*o])?(-{2,}|\.{2,})(\|?>|[*o])?\s*(.+?)(?:\s*:\s*(.+))?$/;
2723
+ var SHAPE_KEYWORDS = Object.keys(SHAPES);
2724
+ var DECLARED = new RegExp("^(" + SHAPE_KEYWORDS.join("|") + ")\\s+(.+)$");
2725
+ var QUOTED_ALIAS = /^["'](.+?)["']\s+as\s+([A-Za-z_][\w-]*)$/;
2726
+ var BRACKET_ALIAS = /^\[(.+?)\]\s+as\s+([A-Za-z_][\w-]*)$/;
2727
+ var PLAIN_ALIAS = /^([A-Za-z_][\w.-]*)\s+as\s+([A-Za-z_][\w-]*)$/;
2728
+ var BRACKET = /^\[(.+?)\]$/;
2729
+ var IGNORED = /^(skinparam|!theme|!define|!include|!pragma|hide|show|scale|left to right direction|top to bottom direction|together|allowmixing)\b/;
2730
+ function ensure2(builder, id, label, shape, implicit = false) {
2731
+ const existing = builder.nodes.get(id);
2732
+ if (existing === void 0) {
2733
+ builder.nodes.set(id, {
2734
+ id,
2735
+ role: "normal",
2736
+ ...label === void 0 ? {} : { label },
2737
+ ...shape === void 0 ? {} : { shape }
2738
+ });
2739
+ if (implicit) builder.implicit.add(id);
2740
+ const innermost = builder.open[builder.open.length - 1];
2741
+ if (innermost !== void 0 && !innermost.members.includes(id)) innermost.members.push(id);
2742
+ return;
2743
+ }
2744
+ if (!implicit) builder.implicit.delete(id);
2745
+ if (label !== void 0 && (existing.label === void 0 || existing.label === "")) existing.label = label;
2746
+ if (shape !== void 0 && existing.shape === void 0) existing.shape = shape;
2747
+ }
2748
+ function reference(token) {
2749
+ const text2 = token.trim();
2750
+ const bracket = BRACKET.exec(text2);
2751
+ if (bracket !== null) {
2752
+ const label = bracket[1].trim();
2753
+ return { id: label, label, shape: "rect" };
2754
+ }
2755
+ const quoted = /^["'](.+?)["']$/.exec(text2);
2756
+ if (quoted !== null) return { id: quoted[1], label: quoted[1] };
2757
+ return { id: text2 };
2758
+ }
2759
+ function keywordOf(line) {
2760
+ return line.split(/[\s{]/)[0].toLowerCase();
2761
+ }
2762
+ function boundaryOf(declaration, keyword, ordinal) {
2763
+ const rest = declaration.slice(keyword.length).trim();
2764
+ const alias = QUOTED_ALIAS.exec(rest) ?? BRACKET_ALIAS.exec(rest) ?? PLAIN_ALIAS.exec(rest);
2765
+ if (alias !== null) return { id: alias[2], label: alias[1] };
2766
+ const label = rest.replace(/^["']|["']$/g, "");
2767
+ return { id: "group-" + String(ordinal), label: label === "" ? keyword : label };
2768
+ }
2769
+ function readArrow(line) {
2770
+ const match = ARROW.exec(line);
2771
+ if (match === null) return void 0;
2772
+ const left = reference(match[1]);
2773
+ const right = reference(match[5]);
2774
+ const leftMark = match[2];
2775
+ const rightMark = match[4];
2776
+ const dotted = match[3].startsWith(".");
2777
+ const label = (match[6] ?? "").trim();
2778
+ if (leftMark !== void 0 && rightMark !== void 0) {
2779
+ return "an edge marked at both ends is not one this renderer draws";
2780
+ }
2781
+ if (leftMark === "*" || leftMark === "o" || rightMark === "*" || rightMark === "o") {
2782
+ if (dotted) return "a dotted composition or aggregation is not one this renderer draws";
2783
+ const atRight = rightMark === "*" || rightMark === "o";
2784
+ const mark = atRight ? rightMark : leftMark;
2785
+ return {
2786
+ from: atRight ? right : left,
2787
+ to: atRight ? left : right,
2788
+ label,
2789
+ dashed: false,
2790
+ arrow: mark === "*" ? "composition" : "aggregation"
2791
+ };
2792
+ }
2793
+ if (leftMark !== void 0) {
2794
+ return {
2795
+ from: right,
2796
+ to: left,
2797
+ label,
2798
+ dashed: dotted,
2799
+ arrow: leftMark === "<|" ? dotted ? "realization" : "generalization" : dotted ? "dependency" : "association"
2800
+ };
2801
+ }
2802
+ if (rightMark !== void 0) {
2803
+ return {
2804
+ from: left,
2805
+ to: right,
2806
+ label,
2807
+ dashed: dotted,
2808
+ arrow: rightMark === "|>" ? dotted ? "realization" : "generalization" : dotted ? "dependency" : "association"
2809
+ };
2810
+ }
2811
+ return "a line with no arrow end is not one this renderer draws; name the end it points at";
2812
+ }
2813
+ function fromPlantUml(source, title = "plantuml") {
2814
+ const lines = source.split(/\r?\n/);
2815
+ const refused = [];
2816
+ const builder = { nodes: /* @__PURE__ */ new Map(), edges: [], edgeLines: [], edgeScopes: [], groups: [], open: [], implicit: /* @__PURE__ */ new Set() };
2817
+ let startId;
2818
+ let seenAny = false;
2819
+ let groupCount = 0;
2820
+ lines.forEach((raw, index) => {
2821
+ const line = raw.replace(/'.*$/, "").trim();
2822
+ if (line === "") return;
2823
+ const where = "line " + String(index + 1) + ": ";
2824
+ if (/^@(start|end)uml\b/.test(line)) return;
2825
+ seenAny = true;
2826
+ if (/^title\b/.test(line)) return;
2827
+ if (IGNORED.test(line)) return;
2828
+ const keyword = keywordOf(line);
2829
+ const notDrawn = NOT_DRAWN[keyword];
2830
+ if (notDrawn !== void 0) {
2831
+ refused.push(where + "`" + keyword + "`: " + notDrawn);
2832
+ return;
2833
+ }
2834
+ if (FRAGMENT.has(keyword)) {
2835
+ refused.push(where + "`" + keyword + "`: a sequence statement is not part of a drawing this renderer makes");
2836
+ return;
2837
+ }
2838
+ if (line === "}") {
2839
+ const closing = builder.open.pop();
2840
+ if (closing !== void 0) {
2841
+ const container = builder.open[builder.open.length - 1];
2842
+ if (container !== void 0) closing.parent = container.id;
2843
+ builder.groups.push(closing);
2844
+ }
2845
+ return;
2846
+ }
2847
+ if (line.endsWith("{")) {
2848
+ const declaration = line.slice(0, -1).trim();
2849
+ const opener = keywordOf(declaration);
2850
+ if (!BOUNDARY.has(opener)) {
2851
+ const reason = SHAPES[opener] === void 0 ? "`" + declaration + "` is not a boundary this renderer draws" : "a `" + opener + "` block is not a boundary this renderer draws";
2852
+ refused.push(where + reason);
2853
+ return;
2854
+ }
2855
+ groupCount += 1;
2856
+ const boundary = boundaryOf(declaration, opener, groupCount);
2857
+ builder.open.push({ id: boundary.id, label: boundary.label, members: [] });
2858
+ return;
2859
+ }
2860
+ if (line === "[*] --> [*]") return;
2861
+ const startMarker = /^\[\*\]\s*-->\s*(.+)$/.exec(line);
2862
+ if (startMarker !== null) {
2863
+ const target = reference(startMarker[1]);
2864
+ ensure2(builder, target.id, target.label, target.shape);
2865
+ if (startId === void 0) startId = target.id;
2866
+ return;
2867
+ }
2868
+ const declared = DECLARED.exec(line);
2869
+ if (declared !== null) {
2870
+ const shape = SHAPES[declared[1]];
2871
+ const rest = declared[2].trim();
2872
+ const quoted = QUOTED_ALIAS.exec(rest);
2873
+ if (quoted !== null) {
2874
+ ensure2(builder, quoted[2], quoted[1], shape);
2875
+ return;
2876
+ }
2877
+ const bracketed = BRACKET_ALIAS.exec(rest);
2878
+ if (bracketed !== null) {
2879
+ ensure2(builder, bracketed[2], bracketed[1], shape);
2880
+ return;
2881
+ }
2882
+ const plain = PLAIN_ALIAS.exec(rest);
2883
+ if (plain !== null) {
2884
+ ensure2(builder, plain[2], plain[1], shape);
2885
+ return;
2886
+ }
2887
+ const written = reference(rest);
2888
+ ensure2(builder, written.id, written.label, shape);
2889
+ return;
2890
+ }
2891
+ const arrow = readArrow(line);
2892
+ if (arrow !== void 0) {
2893
+ if (typeof arrow === "string") {
2894
+ refused.push(where + arrow);
2895
+ return;
2896
+ }
2897
+ ensure2(builder, arrow.from.id, arrow.from.label, arrow.from.shape, true);
2898
+ ensure2(builder, arrow.to.id, arrow.to.label, arrow.to.shape, true);
2899
+ builder.edges.push({
2900
+ from: arrow.from.id,
2901
+ to: arrow.to.id,
2902
+ label: arrow.label,
2903
+ ...arrow.dashed ? { dashed: true } : {},
2904
+ arrow: arrow.arrow
2905
+ });
2906
+ builder.edgeLines.push(index + 1);
2907
+ builder.edgeScopes.push(builder.open[builder.open.length - 1]?.id);
2908
+ return;
2909
+ }
2910
+ const single = BRACKET.exec(line);
2911
+ if (single !== null) {
2912
+ ensure2(builder, single[1].trim(), single[1].trim(), "rect");
2913
+ return;
2914
+ }
2915
+ refused.push(where + "`" + line + "` is not something this renderer draws");
2916
+ });
2917
+ const boundaryNames = /* @__PURE__ */ new Map();
2918
+ const disputedNames = /* @__PURE__ */ new Set();
2919
+ for (const group of builder.groups) {
2920
+ for (const name of [group.id, group.label]) {
2921
+ const existing = boundaryNames.get(name);
2922
+ if (existing !== void 0 && existing !== group.id) disputedNames.add(name);
2923
+ else boundaryNames.set(name, group.id);
2924
+ }
2925
+ }
2926
+ for (const name of disputedNames) boundaryNames.delete(name);
2927
+ const boundaryAt = /* @__PURE__ */ new Map();
2928
+ const ambiguousAt = /* @__PURE__ */ new Set();
2929
+ for (const id of builder.implicit) {
2930
+ if (disputedNames.has(id)) {
2931
+ ambiguousAt.add(id);
2932
+ builder.nodes.delete(id);
2933
+ } else {
2934
+ const groupId = boundaryNames.get(id);
2935
+ if (groupId === void 0) continue;
2936
+ boundaryAt.set(id, groupId);
2937
+ builder.nodes.delete(id);
2938
+ }
2939
+ for (const group of builder.groups) {
2940
+ const at = group.members.indexOf(id);
2941
+ if (at >= 0) group.members.splice(at, 1);
2942
+ }
2943
+ }
2944
+ const groupById = new Map(builder.groups.map((group) => [group.id, group]));
2945
+ const childrenOf = /* @__PURE__ */ new Map();
2946
+ for (const group of builder.groups) {
2947
+ if (group.parent === void 0) continue;
2948
+ childrenOf.set(group.parent, [...childrenOf.get(group.parent) ?? [], group.id]);
2949
+ }
2950
+ const drawable = /* @__PURE__ */ new Map();
2951
+ const hasContents = (id) => {
2952
+ const known = drawable.get(id);
2953
+ if (known !== void 0) return known;
2954
+ const group = groupById.get(id);
2955
+ let found = false;
2956
+ for (const member of group.members) {
2957
+ if (builder.nodes.has(member)) {
2958
+ found = true;
2959
+ break;
2960
+ }
2961
+ }
2962
+ if (!found) {
2963
+ for (const child of childrenOf.get(id) ?? []) {
2964
+ if (hasContents(child)) {
2965
+ found = true;
2966
+ break;
2967
+ }
2968
+ }
2969
+ }
2970
+ drawable.set(id, found);
2971
+ return found;
2972
+ };
2973
+ const boundaryLabel = (id) => groupById.get(id).label;
2974
+ const within = (scope, boundary) => {
2975
+ let cursor = scope;
2976
+ while (cursor !== void 0) {
2977
+ if (cursor === boundary) return true;
2978
+ cursor = groupById.get(cursor).parent;
2979
+ }
2980
+ return false;
2981
+ };
2982
+ const keptEdges = [];
2983
+ builder.edges.forEach((edge, index) => {
2984
+ const where = "line " + String(builder.edgeLines[index]) + ": ";
2985
+ const fromBoundary = boundaryAt.get(edge.from);
2986
+ const toBoundary = boundaryAt.get(edge.to);
2987
+ if (ambiguousAt.has(edge.from) || ambiguousAt.has(edge.to)) {
2988
+ const name = ambiguousAt.has(edge.from) ? edge.from : edge.to;
2989
+ refused.push(where + "`" + name + "` is the name of more than one block; an edge cannot say which boundary it means");
2990
+ return;
2991
+ }
2992
+ if (fromBoundary !== void 0 && toBoundary !== void 0) {
2993
+ refused.push(where + "`" + boundaryLabel(fromBoundary) + "` and `" + boundaryLabel(toBoundary) + "` are boundaries, not elements; an edge between two boundaries is not one this renderer draws");
2994
+ return;
2995
+ }
2996
+ const boundary = fromBoundary ?? toBoundary;
2997
+ if (boundary !== void 0) {
2998
+ const group = groupById.get(boundary);
2999
+ if (group.parent !== void 0) {
3000
+ refused.push(where + "`" + group.label + "` is a block inside `" + boundaryLabel(group.parent) + "`, not an element; an edge to a nested boundary would have to enter another boundary");
3001
+ return;
3002
+ }
3003
+ if (within(builder.edgeScopes[index], boundary)) {
3004
+ refused.push(where + "`" + group.label + "` is the block this edge is written inside, not an element; an edge cannot leave a boundary and arrive at the same boundary");
3005
+ return;
3006
+ }
3007
+ if (!hasContents(boundary)) {
3008
+ refused.push(where + "`" + group.label + "` is a block with nothing drawn inside it, so there is no rectangle to attach the edge to");
3009
+ return;
3010
+ }
3011
+ }
3012
+ keptEdges.push({
3013
+ ...edge,
3014
+ ...fromBoundary === void 0 ? {} : { from: fromBoundary },
3015
+ ...toBoundary === void 0 ? {} : { to: toBoundary }
3016
+ });
3017
+ });
3018
+ builder.edges = keptEdges;
3019
+ if (!seenAny || builder.nodes.size === 0) {
3020
+ return { diagram: emptyDiagram(title, "no nodes found"), refused };
3021
+ }
3022
+ const diagram = {
3023
+ kind: startId === void 0 ? "graph" : "machine",
3024
+ title,
3025
+ ...startId === void 0 ? {} : { init: startId },
3026
+ nodes: [...builder.nodes.values()],
3027
+ edges: builder.edges,
3028
+ ...builder.groups.length === 0 ? {} : { groups: builder.groups },
3029
+ caption: "plantuml, " + String(builder.nodes.size) + " box" + (builder.nodes.size === 1 ? "" : "es") + ", " + String(builder.edges.length) + " arrow" + (builder.edges.length === 1 ? "" : "s") + (refused.length === 0 ? "" : ", " + String(refused.length) + " line(s) refused")
3030
+ };
3031
+ return { diagram, refused };
3032
+ }
3033
+
3034
+ // src/render/svg.ts
3035
+ function boxText(shape, stroke) {
3036
+ const lines = shape.lines === void 0 || shape.lines.length === 0 ? [shape.label] : shape.lines;
3037
+ const lineHeight = 15;
3038
+ const first = shape.y + shape.height / 2 - (lines.length - 1) * lineHeight / 2 + 4;
3039
+ const spans = lines.map((line, index) => '<tspan x="' + n(shape.x + shape.width / 2) + '" y="' + n(first + index * lineHeight) + '">' + escape(line) + "</tspan>").join("");
3040
+ return '<text font-size="11" text-anchor="middle" fill="' + stroke + '">' + spans + "</text>";
3041
+ }
3042
+ var n = (value) => {
3043
+ const rounded = Math.round(value * 10) / 10;
3044
+ return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1);
3045
+ };
3046
+ var escape = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
3047
+ var writers = {
3048
+ // A boundary is a dashed rectangle with its name in the top-left corner, drawn under everything else.
3049
+ group: (shape, colours) => {
3050
+ const stroke = colours[shape.colour];
3051
+ return '<g data-group-id="' + escape(shape.id) + '" style="cursor:pointer"><rect x="' + n(shape.x) + '" y="' + n(shape.y) + '" width="' + n(shape.width) + '" height="' + n(shape.height) + '" rx="8" fill="none" stroke="' + stroke + '" stroke-width="1" stroke-dasharray="4 3" /><text x="' + n(shape.x + 6) + '" y="' + n(shape.y + 12) + '" font-size="10" fill="' + stroke + '">' + escape(shape.label) + "</text></g>";
3052
+ },
3053
+ box: (shape, colours) => {
3054
+ const stroke = colours[shape.colour];
3055
+ const width = shape.initial ? 2.5 : 1;
3056
+ const tail = '" fill="' + colours.surface + '" stroke="' + stroke + '" stroke-width="' + n(width) + '" />';
3057
+ const x = shape.x;
3058
+ const y = shape.y;
3059
+ const w = shape.width;
3060
+ const h = shape.height;
3061
+ const at = (point) => n(point.x) + " " + n(point.y);
3062
+ const points = (list) => list.map((point) => n(point.x) + "," + n(point.y)).join(",");
3063
+ const curve = (segment) => " C" + at(segment.c1) + " " + at(segment.c2) + " " + at(segment.to);
3064
+ switch (shape.shape) {
3065
+ // A decision: the notation wrote `{…}`, and a rounded rectangle would say "step".
3066
+ case "diamond":
3067
+ return '<polygon points="' + points(diamondPoints(shape)) + tail + boxText(shape, stroke);
3068
+ case "hexagon":
3069
+ return '<polygon points="' + points(hexagonPoints(shape)) + tail + boxText(shape, stroke);
3070
+ // A datastore: the cylinder UML draws for a stored collection.
3071
+ case "cylinder": {
3072
+ const caps = cylinderCaps(shape);
3073
+ const path = "M" + at(caps.top.from) + curve(caps.top) + " L" + at(caps.bottom.from) + curve(caps.bottom) + " Z";
3074
+ return '<path d="' + path + tail + boxText(shape, stroke);
3075
+ }
3076
+ // A start or end state: the stadium a terminal node is drawn as.
3077
+ case "stadium":
3078
+ return '<rect x="' + n(x) + '" y="' + n(y) + '" width="' + n(w) + '" height="' + n(h) + '" rx="' + n(stadiumRadius(shape)) + tail + boxText(shape, stroke);
3079
+ case "circle":
3080
+ return '<ellipse cx="' + n(x + w / 2) + '" cy="' + n(y + h / 2) + '" rx="' + n(w / 2) + '" ry="' + n(h / 2) + tail + boxText(shape, stroke);
3081
+ // A subroutine carries its double side lines.
3082
+ case "subroutine":
3083
+ return '<rect x="' + n(x) + '" y="' + n(y) + '" width="' + n(w) + '" height="' + n(h) + tail + '<line x1="' + n(x + 6) + '" y1="' + n(y) + '" x2="' + n(x + 6) + '" y2="' + n(y + h) + '" stroke="' + stroke + '" stroke-width="1" /><line x1="' + n(x + w - 6) + '" y1="' + n(y) + '" x2="' + n(x + w - 6) + '" y2="' + n(y + h) + '" stroke="' + stroke + '" stroke-width="1" />' + boxText(shape, stroke);
3084
+ default:
3085
+ return '<rect x="' + n(x) + '" y="' + n(y) + '" width="' + n(w) + '" height="' + n(h) + '" rx="6' + tail + boxText(shape, stroke);
3086
+ }
3087
+ },
3088
+ link: (shape, colours) => {
3089
+ const stroke = colours[shape.colour];
3090
+ const marks = edgeMarks(
3091
+ { x: shape.x1, y: shape.y1 },
3092
+ { x: shape.x2, y: shape.y2 },
3093
+ { x: shape.x1, y: shape.y1 },
3094
+ { x: shape.x2, y: shape.y2 },
3095
+ shape.arrow,
3096
+ stroke,
3097
+ colours.background
3098
+ );
3099
+ return '<line x1="' + n(shape.x1) + '" y1="' + n(shape.y1) + '" x2="' + n(shape.x2) + '" y2="' + n(shape.y2) + '" stroke="' + stroke + '" stroke-width="1"' + dash(shape) + " />" + marks + edgeLabel(shape.label, shape.labelX, shape.labelY, colours);
3100
+ },
3101
+ path: (shape, colours) => {
3102
+ const stroke = colours[shape.colour];
3103
+ const points = shape.points.map((point) => n(point.x) + "," + n(point.y)).join(" ");
3104
+ const last = shape.points[shape.points.length - 1];
3105
+ const before = shape.points[shape.points.length - 2] ?? last;
3106
+ const first = shape.points[0];
3107
+ const after = shape.points[1] ?? first;
3108
+ return '<polyline points="' + points + '" fill="none" stroke="' + stroke + '" stroke-width="1" stroke-linejoin="round"' + dash(shape) + " />" + edgeMarks(first, after, before, last, shape.arrow, stroke, colours.background) + edgeLabel(shape.label, shape.labelX, shape.labelY, colours);
3109
+ },
3110
+ title: (shape, colours) => {
3111
+ return '<text x="' + n(shape.x) + '" y="' + n(shape.y) + '" font-size="12" fill="' + colours[shape.colour] + '">' + escape(shape.text) + "</text>";
3112
+ },
3113
+ caption: (shape, colours) => {
3114
+ const colour = colours[shape.colour];
3115
+ const lines = shape.lines.map((line, index) => '<tspan x="' + n(shape.x) + '" dy="' + (index === 0 ? 0 : 15) + '">' + escape(line) + "</tspan>").join("");
3116
+ return '<text x="' + n(shape.x) + '" y="' + n(shape.y + 11) + '" font-size="10" text-anchor="middle" fill="' + colour + '">' + lines + "</text>";
3117
+ }
3118
+ };
3119
+ function dash(shape) {
3120
+ return shape.dashed === true ? ' stroke-dasharray="4 3"' : "";
3121
+ }
3122
+ function edgeMarks(source, sourceNext, targetPrev, target, arrow, stroke, background) {
3123
+ if (arrow === "composition" || arrow === "aggregation") {
3124
+ return diamond(source, sourceNext, stroke, arrow === "composition" ? stroke : background);
3125
+ }
3126
+ if (arrow === "generalization" || arrow === "realization") {
3127
+ return hollowTriangle(targetPrev, target, stroke, background);
3128
+ }
3129
+ return arrowHead(targetPrev.x, targetPrev.y, target.x, target.y, stroke);
3130
+ }
3131
+ function tipAndBase(from, to) {
3132
+ const length = Math.hypot(to.x - from.x, to.y - from.y);
3133
+ if (length < 1) return void 0;
3134
+ const ux = (to.x - from.x) / length;
3135
+ const uy = (to.y - from.y) / length;
3136
+ const size = 7;
3137
+ const half = 3.2;
3138
+ const baseX = to.x - ux * size;
3139
+ const baseY = to.y - uy * size;
3140
+ return {
3141
+ tip: n(to.x) + "," + n(to.y),
3142
+ left: n(baseX - uy * half) + "," + n(baseY + ux * half),
3143
+ right: n(baseX + uy * half) + "," + n(baseY - ux * half)
3144
+ };
3145
+ }
3146
+ function arrowHead(x1, y1, x2, y2, stroke) {
3147
+ const points = tipAndBase({ x: x1, y: y1 }, { x: x2, y: y2 });
3148
+ if (points === void 0) return "";
3149
+ return '<polygon points="' + points.tip + " " + points.left + " " + points.right + '" fill="' + stroke + '" />';
3150
+ }
3151
+ function hollowTriangle(from, to, stroke, background) {
3152
+ const points = tipAndBase(from, to);
3153
+ if (points === void 0) return "";
3154
+ return '<polygon points="' + points.tip + " " + points.left + " " + points.right + '" fill="' + background + '" stroke="' + stroke + '" stroke-width="1" />';
3155
+ }
3156
+ function diamond(at, next, stroke, fill) {
3157
+ const length = Math.hypot(next.x - at.x, next.y - at.y);
3158
+ if (length < 1) return "";
3159
+ const ux = (next.x - at.x) / length;
3160
+ const uy = (next.y - at.y) / length;
3161
+ const size = 12;
3162
+ const half = 4;
3163
+ const back = { x: at.x + ux * size, y: at.y + uy * size };
3164
+ const middle = { x: at.x + ux * size / 2, y: at.y + uy * size / 2 };
3165
+ const left = { x: middle.x - uy * half, y: middle.y + ux * half };
3166
+ const right = { x: middle.x + uy * half, y: middle.y - ux * half };
3167
+ const points = [at, left, back, right].map((point) => n(point.x) + "," + n(point.y)).join(" ");
3168
+ return '<polygon points="' + points + '" fill="' + fill + '" stroke="' + stroke + '" stroke-width="1" />';
3169
+ }
3170
+ function edgeLabel(label, x, y, colours) {
3171
+ if (typeof label !== "string" || label === "") return "";
3172
+ const width = textWidth(label);
3173
+ const left = x - width / 2;
3174
+ const top = y - LABEL_HEIGHT / 2;
3175
+ const baseline = y + 3.5;
3176
+ return '<rect x="' + n(left) + '" y="' + n(top) + '" width="' + n(width) + '" height="' + n(LABEL_HEIGHT) + '" fill="' + colours.background + '" /><text x="' + n(x) + '" y="' + n(baseline) + '" font-size="10" text-anchor="middle" fill="' + colours.muted + '">' + escape(label) + "</text>";
3177
+ }
3178
+ function toSvg(canvas, colours, label) {
3179
+ let dropped = 0;
3180
+ const body = [];
3181
+ for (const shape of canvas.shapes) {
3182
+ const writer = writers[shape.kind];
3183
+ if (writer === void 0) {
3184
+ dropped += 1;
3185
+ continue;
3186
+ }
3187
+ body.push(writer(shape, colours));
3188
+ }
3189
+ const header = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ' + n(canvas.width) + " " + n(canvas.height) + '" width="' + n(canvas.width) + '" height="' + n(canvas.height) + '" role="img"' + (label === void 0 ? "" : ' aria-label="' + escape(label) + '"') + ' font-family="system-ui, -apple-system, Segoe UI, sans-serif">';
3190
+ const background = '<rect x="0" y="0" width="' + n(canvas.width) + '" height="' + n(canvas.height) + '" fill="' + colours.background + '" />';
3191
+ return { markup: header + background + body.join("") + "</svg>", dropped };
3192
+ }
3193
+
3194
+ // src/render/index.ts
3195
+ function detectNotation(source) {
3196
+ const head = source.split(/\r?\n/).map((line) => line.trim()).find((line) => line !== "" && !line.startsWith("'") && !line.startsWith("//"));
3197
+ if (head === void 0) return "mermaid";
3198
+ return /^@start/.test(head) ? "plantuml" : "mermaid";
3199
+ }
3200
+ function render(input, overrides = {}, options = {}) {
3201
+ const notation = input.notation === void 0 || input.notation === "auto" ? detectNotation(input.source) : input.notation;
3202
+ const parsed = notation === "plantuml" ? fromPlantUml(input.source, input.title ?? "plantuml") : fromMermaid(input.source, input.title ?? "mermaid");
3203
+ const diagram = input.folded === void 0 ? parsed.diagram : collapse(parsed.diagram, input.folded);
3204
+ const { canvas, colours } = paintWith(diagram, overrides, options);
3205
+ const written = toSvg(canvas, colours, diagram.title + ": " + diagram.caption);
3206
+ return {
3207
+ diagram,
3208
+ canvas,
3209
+ colours,
3210
+ markup: written.markup,
3211
+ notation,
3212
+ refused: parsed.refused,
3213
+ dropped: written.dropped
3214
+ };
3215
+ }
3216
+
3217
+ // src/render/viewmodel.ts
3218
+ var record = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
3219
+ var array = (value) => Array.isArray(value) ? value : [];
3220
+ var text = (value) => typeof value === "string" && value !== "" ? value : void 0;
3221
+ function reportText(content) {
3222
+ const parts = [];
3223
+ for (const entry of array(content)) {
3224
+ const block = record(entry);
3225
+ const body = text(block?.text);
3226
+ if (body !== void 0) parts.push(body);
3227
+ }
3228
+ return parts.join(String.fromCharCode(10));
3229
+ }
3230
+ function readReport(content) {
3231
+ const body = reportText(content);
3232
+ if (body === "") return {};
3233
+ const verdict = /verdict=(\w+)/.exec(body)?.[1];
3234
+ const named = /\[error\/([A-Z0-9_]+)\]\s*(.*)/.exec(body);
3235
+ if (named !== null) return { verdict, code: named[1], message: named[2] };
3236
+ const first = /first error:\s*([A-Z0-9_]+)/.exec(body);
3237
+ return first === null ? { verdict } : { verdict, code: first[1] };
3238
+ }
3239
+ function readModel(args) {
3240
+ const view = record(args);
3241
+ if (view === void 0) return void 0;
3242
+ const read = view.value;
3243
+ if (typeof read === "function") return record(read("model"));
3244
+ return record(view.model);
3245
+ }
3246
+ function machineOf(model) {
3247
+ if (model === void 0) return void 0;
3248
+ const states = array(model.states).map(record).filter((entry) => entry !== void 0 && typeof entry.id === "string");
3249
+ if (states.length === 0) return void 0;
3250
+ const edges = array(model.transitions).map(record).filter((entry) => entry !== void 0 && typeof entry.from === "string" && typeof entry.to === "string").map((entry) => ({ from: String(entry.from), to: String(entry.to), label: text(entry.event) ?? "" }));
3251
+ return {
3252
+ nodes: states.map((state) => ({ id: String(state.id), role: "normal" })),
3253
+ edges,
3254
+ ...typeof model.init === "string" ? { init: model.init } : {}
3255
+ };
3256
+ }
3257
+ function captionOf(report) {
3258
+ const verdict = report.verdict ?? "no report yet";
3259
+ if (report.code === void 0) return verdict;
3260
+ const message = report.message ?? "";
3261
+ return verdict + " \u2014 " + report.code + (message === "" ? "" : ": " + message);
3262
+ }
3263
+ function blame(nodes, report) {
3264
+ if (report.message === void 0) return nodes;
3265
+ const message = report.message;
3266
+ const named = nodes.filter((node) => {
3267
+ const escaped = node.id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3268
+ return new RegExp("(?<![A-Za-z0-9_])" + escaped + "(?![A-Za-z0-9_])").test(message);
3269
+ }).map((node) => node.id);
3270
+ if (named.length === 0) return nodes;
3271
+ return nodes.map((node) => named.includes(node.id) ? { ...node, role: "error" } : node);
3272
+ }
3273
+ function viewModel(input) {
3274
+ const block = record(input.block);
3275
+ const title = text(block?.name) ?? text(input.toolName) ?? "tool";
3276
+ const report = readReport(block?.content);
3277
+ const parsed = record(block?.result);
3278
+ const findings = array(parsed?.findings).map(record);
3279
+ const firstError = findings.find((finding) => finding?.severity === "error");
3280
+ const merged = {
3281
+ verdict: text(parsed?.verdict) ?? report.verdict,
3282
+ ...report.code === void 0 && typeof firstError?.code === "string" ? { code: firstError.code } : { code: report.code },
3283
+ ...report.message === void 0 && typeof firstError?.message === "string" ? { message: firstError.message } : { message: report.message }
3284
+ };
3285
+ const caption = captionOf(merged);
3286
+ const machine = machineOf(readModel(block?.args));
3287
+ if (machine === void 0) {
3288
+ return { kind: "none", title, nodes: [], edges: [], caption };
3289
+ }
3290
+ return {
3291
+ kind: "machine",
3292
+ title,
3293
+ ...machine.init === void 0 ? {} : { init: machine.init },
3294
+ nodes: blame(machine.nodes, merged),
3295
+ edges: machine.edges,
3296
+ caption
3297
+ };
3298
+ }
3299
+
3300
+ // src/client.ts
3301
+ var TOOL_NAMES = ["model_verify"];
3302
+ function themeMode() {
3303
+ try {
3304
+ const scope = globalThis;
3305
+ const body = scope.document?.body;
3306
+ const computed = typeof scope.getComputedStyle === "function" && body !== void 0 ? scope.getComputedStyle(body) : void 0;
3307
+ const colour = computed?.backgroundColor ?? "";
3308
+ const rgb = /rgba?\((\d+),\s*(\d+),\s*(\d+)/.exec(colour);
3309
+ if (rgb !== null) {
3310
+ const [r, g, b] = [Number(rgb[1]), Number(rgb[2]), Number(rgb[3])];
3311
+ return (0.299 * r + 0.587 * g + 0.114 * b) / 255 < 0.5 ? "dark" : "light";
3312
+ }
3313
+ } catch {
3314
+ }
3315
+ return "light";
3316
+ }
3317
+ function surfaceStyle(colours) {
3318
+ return { background: colours.background, color: colours.text, padding: "4px 0", overflowX: "auto" };
3319
+ }
3320
+ var WHITEBOARD_ID = "model-everything-diagram";
3321
+ var WHITEBOARD_SAMPLE = ["graph TD", " a[Idea] --> b{Feasible?}", " b -->|yes| c[Build]", " b -->|no| d[Revise]", " d --> a"].join("\n");
3322
+ function viewFor(React, overrides = {}) {
3323
+ return (props) => {
3324
+ const diagram = viewModel({ phase: props.phase, toolName: props.toolName, block: props.block });
3325
+ const { canvas, colours } = paintWith(diagram, overrides, { mode: themeMode() });
3326
+ const written = toSvg(canvas, colours, diagram.kind === "none" ? diagram.caption : diagram.title + ": " + diagram.caption);
3327
+ return React.createElement("div", {
3328
+ style: surfaceStyle(colours),
3329
+ dangerouslySetInnerHTML: { __html: written.markup }
3330
+ });
3331
+ };
3332
+ }
3333
+ function whiteboardFor(React, overrides = {}) {
3334
+ const h = (type, props, ...children) => React.createElement(type, props, ...children);
3335
+ return function Whiteboard() {
3336
+ const useState = React.useState;
3337
+ if (useState === void 0) {
3338
+ return h(
3339
+ "div",
3340
+ { style: { padding: "8px" } },
3341
+ h("textarea", {
3342
+ value: WHITEBOARD_SAMPLE,
3343
+ readOnly: true,
3344
+ spellcheck: false,
3345
+ rows: WHITEBOARD_SAMPLE.split("\n").length,
3346
+ style: { width: "100%", font: "12px ui-monospace, SFMono-Regular, Menlo, monospace" }
3347
+ }),
3348
+ // The picker is present but inert: a host without hooks cannot hold the choice, and hiding the
3349
+ // option would make the board look like it has none.
3350
+ h(
3351
+ "select",
3352
+ { disabled: true, style: { margin: "4px 0", fontSize: "12px" } },
3353
+ ...schemeNames().map((name) => h("option", { value: name, selected: name === "default" }, name))
3354
+ ),
3355
+ whiteboardBody(React, overrides, WHITEBOARD_SAMPLE, 1)
3356
+ );
3357
+ }
3358
+ const [source, setSource] = useState(WHITEBOARD_SAMPLE);
3359
+ const [zoom, setZoom] = useState(1);
3360
+ const [scheme, setScheme] = useState("default");
3361
+ const [fit, setFit] = useState(true);
3362
+ const [folded, setFolded] = useState([]);
3363
+ const drawn = drawSample(React, overrides, source, fit ? 0 : zoom, scheme, folded);
3364
+ const textarea = h("textarea", {
3365
+ value: source,
3366
+ spellcheck: false,
3367
+ rows: Math.min(12, Math.max(4, String(source).split("\n").length)),
3368
+ style: { width: "100%", font: "12px ui-monospace, SFMono-Regular, Menlo, monospace", padding: "6px", resize: "vertical" },
3369
+ onInput: (event) => setSource(String(event?.target?.value ?? ""))
3370
+ });
3371
+ const controls = h(
3372
+ "div",
3373
+ { style: { display: "flex", gap: "6px", alignItems: "center", padding: "4px 0", flexWrap: "wrap" } },
3374
+ h("button", { type: "button", onClick: () => {
3375
+ setFit(false);
3376
+ setZoom(Math.max(0.5, Math.round((Number(zoom) - 0.25) * 100) / 100));
3377
+ } }, "\u2212"),
3378
+ h("span", { style: { fontSize: "12px", opacity: 0.7 } }, fit ? "fit" : String(Math.round(Number(zoom) * 100)) + "%"),
3379
+ h("button", { type: "button", onClick: () => {
3380
+ setFit(false);
3381
+ setZoom(Math.min(3, Math.round((Number(zoom) + 0.25) * 100) / 100));
3382
+ } }, "+"),
3383
+ h("button", { type: "button", onClick: () => {
3384
+ setFit(false);
3385
+ setZoom(1);
3386
+ } }, "reset"),
3387
+ h("button", { type: "button", title: "Scale to the pane width", onClick: () => setFit(!fit) }, fit ? "fit: on" : "fit: off"),
3388
+ folded.length === 0 ? null : h("button", { type: "button", title: "Unfold every group", onClick: () => setFolded([]) }, "unfold " + String(folded.length)),
3389
+ h(
3390
+ "select",
3391
+ {
3392
+ value: scheme,
3393
+ title: "Colour scheme",
3394
+ style: { marginLeft: "auto", fontSize: "12px" },
3395
+ onChange: (event) => setScheme(String(event?.target?.value ?? "default"))
3396
+ },
3397
+ ...schemeNames().map((name) => h("option", { value: name, selected: name === scheme }, name))
3398
+ )
3399
+ );
3400
+ return h(
3401
+ "div",
3402
+ { style: { display: "flex", flexDirection: "column", gap: "4px", height: "100%", boxSizing: "border-box", padding: "8px" } },
3403
+ textarea,
3404
+ controls,
3405
+ // The drawing takes the rest of the pane and scrolls inside it, so an expanded tab uses its room.
3406
+ h(
3407
+ "div",
3408
+ {
3409
+ style: { flex: "1 1 auto", minHeight: "0", overflow: "auto" },
3410
+ // A boundary carries its id in the markup, so one listener on the container is enough: no per-rect
3411
+ // handler, and the handling survives a re-render that replaced the whole SVG.
3412
+ onClick: (event) => {
3413
+ const hit = event?.target?.closest?.("[data-group-id]");
3414
+ const id = hit?.getAttribute?.("data-group-id");
3415
+ if (typeof id !== "string" || id === "") return;
3416
+ setFolded(folded.includes(id) ? folded.filter((entry) => entry !== id) : [...folded, id]);
3417
+ }
3418
+ },
3419
+ drawn
3420
+ )
3421
+ );
3422
+ };
3423
+ }
3424
+ function drawSample(React, overrides, source, zoom, scheme, folded = []) {
3425
+ return whiteboardBody(React, overrides, source, zoom, scheme, folded);
3426
+ }
3427
+ function whiteboardBody(React, overrides, source, zoom, scheme = "default", folded = []) {
3428
+ if (String(source).trim() === "") {
3429
+ return React.createElement("p", { style: { fontSize: "12px", opacity: 0.7 } }, "Paste Mermaid or PlantUML above.");
3430
+ }
3431
+ const drawn = render({ source: String(source), folded }, overrides, { mode: themeMode(), scheme });
3432
+ const zoomed = zoom === 0 ? drawn.markup.replace("<svg ", '<svg style="width: 100%; height: auto; display: block" ') : drawn.markup.replace("<svg ", '<svg style="zoom: ' + String(zoom) + '; display: block" ');
3433
+ const refused = drawn.refused.length === 0 ? null : React.createElement("pre", {
3434
+ style: { fontSize: "11px", color: "#b7791f", whiteSpace: "pre-wrap", margin: "6px 0" }
3435
+ }, drawn.refused.join("\n"));
3436
+ return React.createElement(
3437
+ "div",
3438
+ { style: { background: drawn.colours.background, color: drawn.colours.text, borderRadius: "6px" } },
3439
+ React.createElement("div", { dangerouslySetInnerHTML: { __html: zoomed } }),
3440
+ refused
3441
+ );
3442
+ }
3443
+ var SIDEBAR_POLL_MS = 250;
3444
+ var SIDEBAR_POLL_ATTEMPTS = 40;
3445
+ function mountWhiteboard(React, ctx, options = {}) {
3446
+ const schedule = options.schedule ?? ((run, delay) => {
3447
+ const timer = globalThis.setTimeout;
3448
+ return typeof timer === "function" ? timer(run, delay) : void 0;
3449
+ });
3450
+ let attempts = 0;
3451
+ const attempt = () => {
3452
+ attempts += 1;
3453
+ const tabs = typeof ctx.get === "function" ? ctx.get("sidebarRightTabs") : void 0;
3454
+ if (tabs === void 0 || typeof tabs.register !== "function") {
3455
+ if (attempts < SIDEBAR_POLL_ATTEMPTS) schedule(attempt, SIDEBAR_POLL_MS);
3456
+ return void 0;
3457
+ }
3458
+ tabs.register({
3459
+ id: WHITEBOARD_ID,
3460
+ kind: "panel",
3461
+ keepMounted: true,
3462
+ title: () => "Diagram",
3463
+ guide: [
3464
+ {
3465
+ id: "diagram-whiteboard",
3466
+ order: 120,
3467
+ title: () => "Diagram",
3468
+ description: () => "Paste Mermaid or PlantUML and read the drawing"
3469
+ }
3470
+ ]
3471
+ });
3472
+ return ctx.slots.register({ name: "sidebar.right.pane.tab", key: WHITEBOARD_ID }, whiteboardFor(React));
3473
+ };
3474
+ return attempt();
3475
+ }
3476
+ function factory(require2) {
3477
+ const hostRequire = require2;
3478
+ const React = hostRequire("react");
3479
+ return {
3480
+ inject: ["slots"],
3481
+ mount: (ctx, options = {}) => mountWhiteboard(React, ctx, options),
3482
+ apply(ctx) {
3483
+ for (const name of TOOL_NAMES) {
3484
+ ctx.slots.inject("tool.call.toolview", () => ctx.slots.register({ name: "tool.call.toolview", key: name }, viewFor(React)));
3485
+ }
3486
+ ctx.slots.inject("sidebar.right.pane.tab", () => mountWhiteboard(React, ctx));
3487
+ }
3488
+ };
3489
+ }
3490
+
3491
+ const create = module.exports.default ?? module.exports;
3492
+ return create(require);
3493
+ },
3494
+ });