dsh-model-everything 0.5.0 → 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 (62) hide show
  1. package/README.en-US.md +12 -0
  2. package/README.md +4 -0
  3. package/lib/client.js +2437 -216
  4. package/lib/render/channels.js +243 -0
  5. package/lib/render/collapse.js +12 -20
  6. package/lib/render/components.js +99 -0
  7. package/lib/render/coords.js +288 -0
  8. package/lib/render/cycle-break.js +147 -0
  9. package/lib/render/dummy.js +43 -0
  10. package/lib/render/from-mermaid.js +34 -10
  11. package/lib/render/from-plantuml.js +318 -46
  12. package/lib/render/layer.js +91 -0
  13. package/lib/render/layout.js +27 -11
  14. package/lib/render/network-simplex.js +272 -0
  15. package/lib/render/order.js +122 -0
  16. package/lib/render/paint.js +764 -67
  17. package/lib/render/place.js +393 -121
  18. package/lib/render/route.js +225 -0
  19. package/lib/render/svg.js +143 -21
  20. package/lib/render/symbol.js +108 -0
  21. package/lib/render/text.js +74 -0
  22. package/lib/render/wrap.js +87 -0
  23. package/lib/types/render/channels.d.ts +63 -0
  24. package/lib/types/render/components.d.ts +57 -0
  25. package/lib/types/render/coords.d.ts +64 -0
  26. package/lib/types/render/cycle-break.d.ts +45 -0
  27. package/lib/types/render/dummy.d.ts +32 -0
  28. package/lib/types/render/from-plantuml.d.ts +12 -6
  29. package/lib/types/render/layer.d.ts +28 -0
  30. package/lib/types/render/layout.d.ts +43 -11
  31. package/lib/types/render/model.d.ts +17 -0
  32. package/lib/types/render/network-simplex.d.ts +54 -0
  33. package/lib/types/render/order.d.ts +27 -0
  34. package/lib/types/render/paint.d.ts +42 -9
  35. package/lib/types/render/place.d.ts +35 -19
  36. package/lib/types/render/route.d.ts +77 -0
  37. package/lib/types/render/svg.d.ts +1 -1
  38. package/lib/types/render/symbol.d.ts +73 -0
  39. package/lib/types/render/text.d.ts +38 -0
  40. package/lib/types/render/wrap.d.ts +27 -0
  41. package/package.json +148 -148
  42. package/skills/model-everything-diagram/SKILL.md +84 -82
  43. package/src/render/channels.ts +309 -0
  44. package/src/render/collapse.ts +150 -155
  45. package/src/render/components.ts +126 -0
  46. package/src/render/coords.ts +328 -0
  47. package/src/render/cycle-break.ts +164 -0
  48. package/src/render/dummy.ts +65 -0
  49. package/src/render/from-mermaid.ts +261 -236
  50. package/src/render/from-plantuml.ts +469 -178
  51. package/src/render/layer.ts +98 -0
  52. package/src/render/layout.ts +101 -62
  53. package/src/render/model.ts +86 -67
  54. package/src/render/network-simplex.ts +290 -0
  55. package/src/render/order.ts +139 -0
  56. package/src/render/paint.ts +1006 -290
  57. package/src/render/place.ts +527 -234
  58. package/src/render/route.ts +269 -0
  59. package/src/render/svg.ts +270 -127
  60. package/src/render/symbol.ts +137 -0
  61. package/src/render/text.ts +86 -0
  62. package/src/render/wrap.ts +103 -0
package/lib/client.js CHANGED
@@ -37,17 +37,844 @@ window.__ModuleLoader__.load({
37
37
  });
38
38
  module.exports = __toCommonJS(client_exports);
39
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
+
40
869
  // src/render/layout.ts
41
870
  function rankStates(nodes, edges, init) {
42
871
  const ids = nodes.map((node) => node.id);
43
872
  const known = new Set(ids);
44
873
  const start = init !== void 0 && known.has(init) ? init : ids[0];
45
874
  const outgoing = /* @__PURE__ */ new Map();
46
- const incoming = /* @__PURE__ */ new Map();
47
875
  for (const edge of edges) {
48
876
  if (!known.has(edge.from) || !known.has(edge.to)) continue;
49
877
  outgoing.set(edge.from, [...outgoing.get(edge.from) ?? [], edge.to]);
50
- incoming.set(edge.to, [...incoming.get(edge.to) ?? [], edge.from]);
51
878
  }
52
879
  const depth = /* @__PURE__ */ new Map();
53
880
  if (start !== void 0) {
@@ -72,40 +899,224 @@ window.__ModuleLoader__.load({
72
899
  }
73
900
  return { rows, unreachable: ids.filter((id) => !depth.has(id)) };
74
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
+ }
75
1107
 
76
1108
  // src/render/place.ts
77
1109
  var MARGIN = 20;
78
- var NODE_HEIGHT = 34;
79
- var ROW_GAP = 58;
80
- var COLUMN_GAP = 30;
81
- var MIN_NODE_WIDTH = 88;
82
- var MAX_NODE_WIDTH = 220;
83
- var CHAR_WIDTH = 7.2;
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;
84
1116
  var GROUP_PADDING = 10;
85
1117
  var GROUP_HEADER = 16;
86
- var BLOCK_GAP = 14;
87
- var nodeWidth = (label) => Math.min(MAX_NODE_WIDTH, Math.max(MIN_NODE_WIDTH, label.length * CHAR_WIDTH + 26));
88
- function placeByRanks(diagram, nodes, top) {
89
- const ranked = rankStates(diagram.nodes, diagram.edges, diagram.init);
90
- const rows = [...ranked.rows];
91
- if (ranked.unreachable.length > 0) rows.push(ranked.unreachable);
92
- const boxes = /* @__PURE__ */ new Map();
93
- let widest = 0;
94
- rows.forEach((row, index) => {
95
- const widths = row.map((id) => nodeWidth(id));
96
- widest = Math.max(widest, widths.reduce((total, width) => total + width, 0) + Math.max(0, row.length - 1) * COLUMN_GAP);
97
- let x = 0;
98
- row.forEach((id, position) => {
99
- boxes.set(id, { x, y: top + index * (NODE_HEIGHT + ROW_GAP), width: widths[position], height: NODE_HEIGHT });
100
- x += widths[position] + COLUMN_GAP;
101
- });
102
- });
103
- rows.forEach((row) => {
104
- const rowWidth = row.reduce((total, id) => total + boxes.get(id).width, 0) + Math.max(0, row.length - 1) * COLUMN_GAP;
105
- const offset = (widest - rowWidth) / 2;
106
- for (const id of row) boxes.get(id).x += offset + MARGIN;
107
- });
108
- return { boxes, groupRects: /* @__PURE__ */ new Map(), width: widest + MARGIN * 2 };
1118
+ function textOf(node) {
1119
+ return measure(node.label !== void 0 && node.label !== "" ? node.label : node.id);
109
1120
  }
110
1121
  function groupTree(groups) {
111
1122
  const byId = new Map(groups.map((group) => [group.id, group]));
@@ -162,69 +1173,511 @@ window.__ModuleLoader__.load({
162
1173
  if (parent === void 0) roots.push(group.id);
163
1174
  else children.set(parent, [...children.get(parent) ?? [], group.id]);
164
1175
  }
165
- return { owner, children, roots };
1176
+ return { owner, parentOf, children, roots };
166
1177
  }
167
- function placeByGroups(diagram, nodes, top) {
168
- const groups = diagram.groups;
169
- const byId = new Map(groups.map((group) => [group.id, group]));
170
- const { owner, children, roots } = groupTree(groups);
171
- const order = new Map(nodes.map((node, index) => [node.id, index]));
172
- const loose = nodes.filter((node) => !owner.has(node.id)).map((node) => node.id);
173
- const nodeIds = new Set(nodes.map((node) => node.id));
174
- const rawContents = (groupId) => {
175
- const group = byId.get(groupId);
176
- const own = [...new Set(group.members)].filter((member) => owner.get(member) === groupId && nodeIds.has(member));
177
- const nested = children.get(groupId) ?? [];
178
- return [...own, ...nested];
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;
179
1217
  };
180
- const contents = (groupId) => rawContents(groupId).filter((child) => !byId.has(child) || rawContents(child).length > 0);
181
- const measure = (id) => {
182
- if (!byId.has(id)) return { width: nodeWidth(id), height: NODE_HEIGHT };
183
- const items = contents(id);
184
- const sizes2 = items.map(measure);
185
- return {
186
- width: Math.max(MIN_NODE_WIDTH, ...sizes2.map((size) => size.width)) + GROUP_PADDING * 2,
187
- height: GROUP_HEADER + GROUP_PADDING * 2 + sizes2.reduce((total, size) => total + size.height, 0) + Math.max(0, items.length - 1) * BLOCK_GAP
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 }
188
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 };
189
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);
190
1448
  const boxes = /* @__PURE__ */ new Map();
191
1449
  const groupRects = /* @__PURE__ */ new Map();
192
- const place2 = (id, x, y) => {
193
- const size = measure(id);
194
- if (!byId.has(id)) {
195
- boxes.set(id, { x, y, width: size.width, height: NODE_HEIGHT });
196
- return size;
197
- }
198
- groupRects.set(id, { x, y, width: size.width, height: size.height });
199
- let cursor2 = y + GROUP_HEADER + GROUP_PADDING;
200
- const inner = size.width - GROUP_PADDING * 2;
201
- const ordered = [...contents(id)].sort((left, right) => firstIndex(left) - firstIndex(right));
202
- for (const child of ordered) {
203
- const childSize = measure(child);
204
- place2(child, x + GROUP_PADDING + (inner - childSize.width) / 2, cursor2);
205
- cursor2 += childSize.height + BLOCK_GAP;
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);
206
1464
  }
207
- return size;
208
- };
209
- const visibleRoots = roots.filter((id) => contents(id).length > 0);
210
- const firstIndex = (id) => {
211
- if (!byId.has(id)) return order.get(id);
212
- return Math.min(...contents(id).map(firstIndex));
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);
213
1469
  };
214
- const topItems = [...loose, ...visibleRoots].sort((left, right) => firstIndex(left) - firstIndex(right));
215
- const sizes = topItems.map(measure);
216
- const width = Math.max(MIN_NODE_WIDTH, ...sizes.map((size) => size.width));
217
- let cursor = top;
218
- for (const item of topItems) {
219
- const size = measure(item);
220
- place2(item, MARGIN + (width - size.width) / 2, cursor);
221
- cursor += size.height + BLOCK_GAP;
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 };
222
1490
  }
223
- return { boxes, groupRects, width: width + MARGIN * 2 };
224
1491
  }
225
- function place(diagram, top) {
226
- const groups = diagram.groups ?? [];
227
- return groups.length > 0 ? placeByGroups(diagram, diagram.nodes, top) : placeByRanks(diagram, diagram.nodes, top);
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);
228
1681
  }
229
1682
 
230
1683
  // src/render/palette.ts
@@ -327,12 +1780,28 @@ window.__ModuleLoader__.load({
327
1780
 
328
1781
  // src/render/paint.ts
329
1782
  var TITLE_HEIGHT = 26;
330
- var NODE_HEIGHT2 = 34;
331
- var CHANNEL_GAP = 16;
332
1783
  var CAPTION_CHARS = 78;
333
1784
  var CAPTION_LINE = 15;
334
1785
  var CAPTION_GAP = 16;
335
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
+ }
336
1805
  function roleOfNode(node) {
337
1806
  if (node.role === "error") return "fail";
338
1807
  if (node.role === "warning") return "findings";
@@ -363,6 +1832,76 @@ window.__ModuleLoader__.load({
363
1832
  function captionRole(caption) {
364
1833
  return roleForVerdict(caption.split(" \u2014 ")[0]);
365
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
+ }
366
1905
  function paintWith(diagram, overrides = {}, options = {}) {
367
1906
  const colours = palette(overrides, options);
368
1907
  const shapes = [];
@@ -370,16 +1909,23 @@ window.__ModuleLoader__.load({
370
1909
  const topOffset = MARGIN + TITLE_HEIGHT;
371
1910
  const placement = place(diagram, topOffset);
372
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";
373
1916
  for (const group of groups) {
374
1917
  const rect = groupRects.get(group.id);
375
1918
  if (rect === void 0) continue;
376
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" });
377
1920
  }
378
1921
  const drawn = /* @__PURE__ */ new Set();
1922
+ const boxList = Array.from(boxes.values());
1923
+ const routes = [];
379
1924
  for (const node of diagram.nodes) {
380
1925
  if (drawn.has(node.id)) continue;
381
1926
  const box = boxes.get(node.id);
382
1927
  drawn.add(node.id);
1928
+ const text2 = textOf(node);
383
1929
  shapes.push({
384
1930
  kind: "box",
385
1931
  x: box.x,
@@ -387,19 +1933,370 @@ window.__ModuleLoader__.load({
387
1933
  width: box.width,
388
1934
  height: box.height,
389
1935
  label: labelOf(node),
1936
+ lines: text2.lines,
1937
+ ...node.shape === void 0 ? {} : { shape: node.shape },
390
1938
  colour: roleOfNode(node),
391
1939
  initial: diagram.init === node.id
392
1940
  });
393
1941
  }
394
1942
  const roleById = new Map(diagram.nodes.map((node) => [node.id, roleOfNode(node)]));
395
- let channel = Math.max(placement.width, ...Array.from(boxes.values(), (box) => box.x + box.width)) + CHANNEL_GAP;
396
- let channelUses = 0;
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();
397
1962
  for (const edge of diagram.edges) {
398
- const source = boxes.get(edge.from);
399
- const target = boxes.get(edge.to);
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);
400
2298
  if (source === void 0 || target === void 0) continue;
401
- const colour = roleById.get(edge.from);
402
- const sameRow = Math.abs(target.y - source.y) < NODE_HEIGHT2 / 2;
2299
+ const colour = roleById.get(edge.from) ?? "muted";
403
2300
  if (edge.from === edge.to) {
404
2301
  const x = source.x + source.width;
405
2302
  const y = source.y + source.height / 2;
@@ -412,57 +2309,57 @@ window.__ModuleLoader__.load({
412
2309
  { x, y: y + 9 }
413
2310
  ],
414
2311
  label: edge.label,
415
- labelX: x + 20,
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,
416
2315
  labelY: y,
417
- colour
2316
+ colour,
2317
+ ...edge.dashed === void 0 ? {} : { dashed: edge.dashed },
2318
+ ...edge.arrow === void 0 ? {} : { arrow: edge.arrow }
418
2319
  });
419
2320
  continue;
420
2321
  }
421
- if (target.y > source.y && !sameRow) {
422
- const x1 = source.x + source.width / 2;
423
- const y1 = source.y + source.height;
424
- const x2 = target.x + target.width / 2;
425
- const y2 = target.y;
426
- shapes.push({ kind: "link", x1, y1, x2, y2, label: edge.label, labelX: (x1 + x2) / 2 + 7, labelY: (y1 + y2) / 2, colour });
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);
427
2338
  continue;
428
2339
  }
429
- if (sameRow) {
430
- const right = source.x + source.width;
431
- const left = target.x;
432
- const y = source.y + source.height / 2;
433
- const forward = right <= left;
434
- shapes.push({
435
- kind: "link",
436
- x1: forward ? right : source.x,
437
- y1: y,
438
- x2: forward ? left : target.x + target.width,
439
- y2: y,
440
- label: edge.label,
441
- labelX: (right + left) / 2,
442
- labelY: y - 7,
443
- colour
444
- });
445
- continue;
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
+ }
446
2348
  }
447
- const lane = channel + channelUses % 3 * 8;
448
- channelUses += 1;
449
- channel = Math.max(channel, lane);
450
- const startY = source.y + source.height / 2;
451
- const endY = target.y + target.height / 2;
452
- shapes.push({
453
- kind: "path",
454
- points: [
455
- { x: source.x + source.width, y: startY },
456
- { x: lane, y: startY },
457
- { x: lane, y: endY },
458
- { x: target.x + target.width, y: endY }
459
- ],
460
- label: edge.label,
461
- labelX: lane + 6,
462
- labelY: (startY + endY) / 2,
463
- colour
464
- });
2349
+ paintRoute(edge, routeEdge(source, target, boxList, anchors).points, colour);
465
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
+ });
466
2363
  shapes.push({ kind: "title", x: MARGIN, y: MARGIN + 12, text: diagram.title, colour: "text" });
467
2364
  const contentRight = Math.max(
468
2365
  MARGIN,
@@ -471,7 +2368,7 @@ window.__ModuleLoader__.load({
471
2368
  );
472
2369
  const captionLines = wrap(diagram.caption, CAPTION_CHARS);
473
2370
  const captionWidth = Math.max(...captionLines.map((line) => line.length * CAPTION_CHAR_WIDTH)) + MARGIN * 2;
474
- const width = Math.max(contentRight + MARGIN + (channelUses > 0 ? CHANNEL_GAP * 3 : 0), captionWidth, 240);
2371
+ const width = Math.max(contentRight + MARGIN, captionWidth, 240);
475
2372
  const contentBottom = Math.max(
476
2373
  topOffset,
477
2374
  ...Array.from(boxes.values(), (box) => box.y + box.height),
@@ -480,7 +2377,10 @@ window.__ModuleLoader__.load({
480
2377
  const captionTop = contentBottom + CAPTION_GAP;
481
2378
  shapes.push({ kind: "caption", x: MARGIN, y: captionTop, lines: captionLines, colour: captionRole(diagram.caption) });
482
2379
  const height = captionTop + captionLines.length * CAPTION_LINE + MARGIN / 2;
483
- return { canvas: { width: Math.round(width), height: Math.round(height), shapes }, colours };
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 };
484
2384
  }
485
2385
 
486
2386
  // src/render/from-mermaid.ts
@@ -505,10 +2405,15 @@ window.__ModuleLoader__.load({
505
2405
  "requirementdiagram",
506
2406
  "sankey-beta"
507
2407
  ]);
508
- function ensure(builder, id, label) {
2408
+ function ensure(builder, id, label, shape) {
509
2409
  const existing = builder.nodes.get(id);
510
2410
  if (existing === void 0) {
511
- builder.nodes.set(id, { id, role: "normal", ...label === void 0 ? {} : { label } });
2411
+ builder.nodes.set(id, {
2412
+ id,
2413
+ role: "normal",
2414
+ ...label === void 0 ? {} : { label },
2415
+ ...shape === void 0 ? {} : { shape }
2416
+ });
512
2417
  const innermost = builder.open[builder.open.length - 1];
513
2418
  if (innermost !== void 0 && !innermost.members.includes(id)) innermost.members.push(id);
514
2419
  return;
@@ -520,7 +2425,12 @@ window.__ModuleLoader__.load({
520
2425
  if (match === null) return void 0;
521
2426
  const id = match[1];
522
2427
  const label = [match[2], match[3], match[4], match[5], match[6], match[7]].find((value) => value !== void 0 && value.trim() !== "");
523
- return { id, ...label === void 0 ? {} : { label: label.trim().replace(/^["']|["']$/g, "") } };
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
+ };
524
2434
  }
525
2435
  function fromMermaid(source, title = "mermaid") {
526
2436
  const lines = source.split(/\r?\n/);
@@ -537,13 +2447,13 @@ window.__ModuleLoader__.load({
537
2447
  if (line === "") return;
538
2448
  const where = "line " + String(index + 1) + ": ";
539
2449
  const lower = line.toLowerCase();
540
- const keyword = lower.split(/[\s{}]/)[0] ?? "";
2450
+ const keyword = lower.split(/[\s{}]/)[0];
541
2451
  if (UNSUPPORTED.has(keyword)) {
542
2452
  refused.push(where + "`" + keyword + "` is not a diagram this renderer draws");
543
2453
  return;
544
2454
  }
545
2455
  if (DIRECTIVE.test(line)) {
546
- refused.push(where + "`" + (line.split(/\s/)[0] ?? "") + "` is a directive this renderer ignores");
2456
+ refused.push(where + "`" + line.split(/\s/)[0] + "` is a directive this renderer ignores");
547
2457
  return;
548
2458
  }
549
2459
  if (/^(graph|flowchart)\b/.test(lower)) {
@@ -615,8 +2525,8 @@ window.__ModuleLoader__.load({
615
2525
  refused.push(where + "`" + line + "` does not name two nodes");
616
2526
  return;
617
2527
  }
618
- ensure(builder, left.id, left.label);
619
- ensure(builder, right.id, right.label);
2528
+ ensure(builder, left.id, left.label, left.shape);
2529
+ ensure(builder, right.id, right.label, right.shape);
620
2530
  push({ from: left.id, to: right.id, label: labelled[2].trim() });
621
2531
  return;
622
2532
  }
@@ -628,8 +2538,8 @@ window.__ModuleLoader__.load({
628
2538
  refused.push(where + "`" + line + "` does not name two nodes");
629
2539
  return;
630
2540
  }
631
- ensure(builder, left.id, left.label);
632
- ensure(builder, right.id, right.label);
2541
+ ensure(builder, left.id, left.label, left.shape);
2542
+ ensure(builder, right.id, right.label, right.shape);
633
2543
  push({
634
2544
  from: left.id,
635
2545
  to: right.id,
@@ -640,7 +2550,7 @@ window.__ModuleLoader__.load({
640
2550
  }
641
2551
  const single = nodeFrom(line);
642
2552
  if (single !== void 0) {
643
- ensure(builder, single.id, single.label);
2553
+ ensure(builder, single.id, single.label, single.shape);
644
2554
  return;
645
2555
  }
646
2556
  refused.push(where + "`" + line + "` is not something this renderer draws");
@@ -664,17 +2574,15 @@ window.__ModuleLoader__.load({
664
2574
  }
665
2575
 
666
2576
  // src/render/collapse.ts
667
- function subtreeSize(diagram, groupId, seen) {
2577
+ function subtreeSize(groups, groupId, seen) {
668
2578
  if (seen.has(groupId)) return 0;
669
2579
  seen.add(groupId);
670
- const groups = diagram.groups ?? [];
671
2580
  const group = groups.find((entry) => entry.id === groupId);
672
- if (group === void 0) return 0;
673
2581
  const children = groups.filter((entry) => entry.parent === groupId);
674
- return group.members.length + children.reduce((total, child) => total + subtreeSize(diagram, child.id, seen), 0);
2582
+ return group.members.length + children.reduce((total, child) => total + subtreeSize(groups, child.id, seen), 0);
675
2583
  }
676
- function ownerOf(diagram, nodeId) {
677
- return (diagram.groups ?? []).find((group) => group.members.includes(nodeId))?.id;
2584
+ function ownerOf(groups, nodeId) {
2585
+ return groups.find((group) => group.members.includes(nodeId))?.id;
678
2586
  }
679
2587
  function collapse(diagram, folded) {
680
2588
  const asked = new Set(folded);
@@ -684,7 +2592,7 @@ window.__ModuleLoader__.load({
684
2592
  const targets = /* @__PURE__ */ new Set();
685
2593
  for (const id of asked) {
686
2594
  const group = byId.get(id);
687
- if (group !== void 0 && subtreeSize(diagram, id, /* @__PURE__ */ new Set()) > 0) targets.add(id);
2595
+ if (group !== void 0 && subtreeSize(groups, id, /* @__PURE__ */ new Set()) > 0) targets.add(id);
688
2596
  }
689
2597
  if (targets.size === 0) return diagram;
690
2598
  for (const id of [...targets]) {
@@ -701,16 +2609,13 @@ window.__ModuleLoader__.load({
701
2609
  }
702
2610
  const removedNodes = /* @__PURE__ */ new Set();
703
2611
  const removedGroups = /* @__PURE__ */ new Set();
704
- const descend = (groupId, seen) => {
705
- if (seen.has(groupId)) return;
706
- seen.add(groupId);
2612
+ const descend = (groupId) => {
707
2613
  removedGroups.add(groupId);
708
2614
  const group = byId.get(groupId);
709
- if (group === void 0) return;
710
2615
  for (const member of group.members) removedNodes.add(member);
711
- for (const child of groups.filter((entry) => entry.parent === groupId)) descend(child.id, seen);
2616
+ for (const child of groups.filter((entry) => entry.parent === groupId)) descend(child.id);
712
2617
  };
713
- for (const id of targets) descend(id, /* @__PURE__ */ new Set());
2618
+ for (const id of targets) descend(id);
714
2619
  const foldOf = (id) => {
715
2620
  for (const target of targets) {
716
2621
  const inSubtree = (groupId, seen) => {
@@ -720,7 +2625,7 @@ window.__ModuleLoader__.load({
720
2625
  return groups.filter((entry) => entry.parent === groupId).some((child) => inSubtree(child.id, seen));
721
2626
  };
722
2627
  const group = byId.get(target);
723
- if (group.members.includes(id) || inSubtree(ownerOf(diagram, id) ?? "", /* @__PURE__ */ new Set())) return target;
2628
+ if (group.members.includes(id) || inSubtree(ownerOf(groups, id), /* @__PURE__ */ new Set())) return target;
724
2629
  }
725
2630
  return void 0;
726
2631
  };
@@ -735,12 +2640,12 @@ window.__ModuleLoader__.load({
735
2640
  if (fold === void 0 || emitted.has(fold)) continue;
736
2641
  emitted.add(fold);
737
2642
  const group = byId.get(fold);
738
- nodes.push({ id: fold, label: group.label + " (" + String(subtreeSize(diagram, fold, /* @__PURE__ */ new Set())) + ")", role: "normal" });
2643
+ nodes.push({ id: fold, label: group.label + " (" + String(subtreeSize(groups, fold, /* @__PURE__ */ new Set())) + ")", role: "normal" });
739
2644
  }
740
2645
  for (const id of targets) {
741
2646
  if (emitted.has(id)) continue;
742
2647
  const group = byId.get(id);
743
- nodes.push({ id, label: group.label + " (" + String(subtreeSize(diagram, id, /* @__PURE__ */ new Set())) + ")", role: "normal" });
2648
+ nodes.push({ id, label: group.label + " (" + String(subtreeSize(groups, id, /* @__PURE__ */ new Set())) + ")", role: "normal" });
744
2649
  }
745
2650
  const edges = [];
746
2651
  const seenEdges = /* @__PURE__ */ new Set();
@@ -757,7 +2662,7 @@ window.__ModuleLoader__.load({
757
2662
  const adopted = /* @__PURE__ */ new Map();
758
2663
  for (const id of targets) {
759
2664
  const parent = byId.get(id)?.parent;
760
- if (parent === void 0 || targets.has(parent) || removedGroups.has(parent)) continue;
2665
+ if (parent === void 0) continue;
761
2666
  adopted.set(parent, [...adopted.get(parent) ?? [], id]);
762
2667
  }
763
2668
  const remaining = groups.filter((group) => !removedGroups.has(group.id) && targets.has(group.id) === false).map((group) => {
@@ -776,38 +2681,139 @@ window.__ModuleLoader__.load({
776
2681
  }
777
2682
 
778
2683
  // src/render/from-plantuml.ts
779
- var ARROW = /^(.+?)\s*(<?)(-{2,}|\.{2,})(>?)\s*(.+?)(?:\s*:\s*(.+))?$/;
780
- var DECLARED = /^(component|package|node|folder|cloud|database|state|artifact|frame|rectangle)\s+(.+)$/;
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+(.+)$");
781
2725
  var QUOTED_ALIAS = /^["'](.+?)["']\s+as\s+([A-Za-z_][\w-]*)$/;
782
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-]*)$/;
783
2728
  var BRACKET = /^\[(.+?)\]$/;
784
- var UNSUPPORTED2 = /^(actor|participant|usecase|class|object|note|legend|alt|opt|loop|par|group\s|ref\s|activate|deactivate|destroy|create|newpage)\b/;
785
- var IGNORED = /^(skinparam|!theme|!define|!include|hide|show|scale|left to right direction|top to bottom direction|together)\b/;
786
- function ensure2(builder, id, label) {
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) {
787
2731
  const existing = builder.nodes.get(id);
788
2732
  if (existing === void 0) {
789
- builder.nodes.set(id, { id, role: "normal", ...label === void 0 ? {} : { label } });
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);
790
2740
  const innermost = builder.open[builder.open.length - 1];
791
2741
  if (innermost !== void 0 && !innermost.members.includes(id)) innermost.members.push(id);
792
2742
  return;
793
2743
  }
2744
+ if (!implicit) builder.implicit.delete(id);
794
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;
795
2747
  }
796
2748
  function reference(token) {
797
2749
  const text2 = token.trim();
798
2750
  const bracket = BRACKET.exec(text2);
799
2751
  if (bracket !== null) {
800
2752
  const label = bracket[1].trim();
801
- return { id: label, label };
2753
+ return { id: label, label, shape: "rect" };
802
2754
  }
803
2755
  const quoted = /^["'](.+?)["']$/.exec(text2);
804
2756
  if (quoted !== null) return { id: quoted[1], label: quoted[1] };
805
2757
  return { id: text2 };
806
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
+ }
807
2813
  function fromPlantUml(source, title = "plantuml") {
808
2814
  const lines = source.split(/\r?\n/);
809
2815
  const refused = [];
810
- const builder = { nodes: /* @__PURE__ */ new Map(), edges: [], groups: [], open: [] };
2816
+ const builder = { nodes: /* @__PURE__ */ new Map(), edges: [], edgeLines: [], edgeScopes: [], groups: [], open: [], implicit: /* @__PURE__ */ new Set() };
811
2817
  let startId;
812
2818
  let seenAny = false;
813
2819
  let groupCount = 0;
@@ -819,8 +2825,14 @@ window.__ModuleLoader__.load({
819
2825
  seenAny = true;
820
2826
  if (/^title\b/.test(line)) return;
821
2827
  if (IGNORED.test(line)) return;
822
- if (UNSUPPORTED2.test(line)) {
823
- refused.push(where + "`" + (line.split(/\s/)[0] ?? "") + "` is not a diagram this renderer draws");
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");
824
2836
  return;
825
2837
  }
826
2838
  if (line === "}") {
@@ -834,56 +2846,176 @@ window.__ModuleLoader__.load({
834
2846
  }
835
2847
  if (line.endsWith("{")) {
836
2848
  const declaration = line.slice(0, -1).trim();
837
- const declared2 = DECLARED.exec(declaration);
838
- const label = declared2 === null ? declaration.replace(/^["']|["']$/g, "") : declared2[2].replace(/^["']|["']$/g, "");
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
+ }
839
2855
  groupCount += 1;
840
- builder.open.push({ id: "group-" + String(groupCount), label, members: [] });
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;
841
2866
  return;
842
2867
  }
843
2868
  const declared = DECLARED.exec(line);
844
2869
  if (declared !== null) {
2870
+ const shape = SHAPES[declared[1]];
845
2871
  const rest = declared[2].trim();
846
2872
  const quoted = QUOTED_ALIAS.exec(rest);
847
2873
  if (quoted !== null) {
848
- ensure2(builder, quoted[2], quoted[1]);
2874
+ ensure2(builder, quoted[2], quoted[1], shape);
849
2875
  return;
850
2876
  }
851
2877
  const bracketed = BRACKET_ALIAS.exec(rest);
852
2878
  if (bracketed !== null) {
853
- ensure2(builder, bracketed[2], bracketed[1]);
2879
+ ensure2(builder, bracketed[2], bracketed[1], shape);
854
2880
  return;
855
2881
  }
856
- if (/^["'](.+?)["']$/.test(rest)) return;
857
- const reference_ = reference(rest);
858
- ensure2(builder, reference_.id, reference_.label);
859
- return;
860
- }
861
- if (line === "[*] --> [*]") return;
862
- const startMarker = /^\[\*\]\s*-->\s*(.+)$/.exec(line);
863
- if (startMarker !== null) {
864
- const target = reference(startMarker[1]);
865
- ensure2(builder, target.id, target.label);
866
- if (startId === void 0) startId = target.id;
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);
867
2889
  return;
868
2890
  }
869
- const arrow = ARROW.exec(line);
870
- if (arrow !== null) {
871
- const left = arrow[2] === "<" ? arrow[5] : arrow[1];
872
- const right = arrow[2] === "<" ? arrow[1] : arrow[5];
873
- const from = reference(left);
874
- const to = reference(right);
875
- ensure2(builder, from.id, from.label);
876
- ensure2(builder, to.id, to.label);
877
- builder.edges.push({ from: from.id, to: to.id, label: (arrow[6] ?? "").trim(), ...arrow[3].startsWith(".") ? { dashed: true } : {} });
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);
878
2908
  return;
879
2909
  }
880
2910
  const single = BRACKET.exec(line);
881
2911
  if (single !== null) {
882
- ensure2(builder, single[1].trim(), single[1].trim());
2912
+ ensure2(builder, single[1].trim(), single[1].trim(), "rect");
883
2913
  return;
884
2914
  }
885
2915
  refused.push(where + "`" + line + "` is not something this renderer draws");
886
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;
887
3019
  if (!seenAny || builder.nodes.size === 0) {
888
3020
  return { diagram: emptyDiagram(title, "no nodes found"), refused };
889
3021
  }
@@ -900,6 +3032,13 @@ window.__ModuleLoader__.load({
900
3032
  }
901
3033
 
902
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
+ }
903
3042
  var n = (value) => {
904
3043
  const rounded = Math.round(value * 10) / 10;
905
3044
  return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1);
@@ -914,19 +3053,59 @@ window.__ModuleLoader__.load({
914
3053
  box: (shape, colours) => {
915
3054
  const stroke = colours[shape.colour];
916
3055
  const width = shape.initial ? 2.5 : 1;
917
- return '<rect x="' + n(shape.x) + '" y="' + n(shape.y) + '" width="' + n(shape.width) + '" height="' + n(shape.height) + '" rx="6" fill="' + colours.surface + '" stroke="' + stroke + '" stroke-width="' + n(width) + '" /><text x="' + n(shape.x + shape.width / 2) + '" y="' + n(shape.y + shape.height / 2 + 4) + '" font-size="11" text-anchor="middle" fill="' + stroke + '">' + escape(shape.label) + "</text>";
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
+ }
918
3087
  },
919
3088
  link: (shape, colours) => {
920
3089
  const stroke = colours[shape.colour];
921
- const arrow = arrowHead(shape.x1, shape.y1, shape.x2, shape.y2, stroke);
922
- return '<line x1="' + n(shape.x1) + '" y1="' + n(shape.y1) + '" x2="' + n(shape.x2) + '" y2="' + n(shape.y2) + '" stroke="' + stroke + '" stroke-width="1" />' + arrow + edgeLabel(shape.label, shape.labelX, shape.labelY, colours);
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);
923
3100
  },
924
3101
  path: (shape, colours) => {
925
3102
  const stroke = colours[shape.colour];
926
3103
  const points = shape.points.map((point) => n(point.x) + "," + n(point.y)).join(" ");
927
3104
  const last = shape.points[shape.points.length - 1];
928
3105
  const before = shape.points[shape.points.length - 2] ?? last;
929
- return '<polyline points="' + points + '" fill="none" stroke="' + stroke + '" stroke-width="1" stroke-linejoin="round" />' + arrowHead(before.x, before.y, last.x, last.y, stroke) + edgeLabel(shape.label, shape.labelX, shape.labelY, colours);
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);
930
3109
  },
931
3110
  title: (shape, colours) => {
932
3111
  return '<text x="' + n(shape.x) + '" y="' + n(shape.y) + '" font-size="12" fill="' + colours[shape.colour] + '">' + escape(shape.text) + "</text>";
@@ -934,25 +3113,67 @@ window.__ModuleLoader__.load({
934
3113
  caption: (shape, colours) => {
935
3114
  const colour = colours[shape.colour];
936
3115
  const lines = shape.lines.map((line, index) => '<tspan x="' + n(shape.x) + '" dy="' + (index === 0 ? 0 : 15) + '">' + escape(line) + "</tspan>").join("");
937
- return '<text x="' + n(shape.x) + '" y="' + n(shape.y + 11) + '" font-size="10" fill="' + colour + '">' + lines + "</text>";
3116
+ return '<text x="' + n(shape.x) + '" y="' + n(shape.y + 11) + '" font-size="10" text-anchor="middle" fill="' + colour + '">' + lines + "</text>";
938
3117
  }
939
3118
  };
940
- function arrowHead(x1, y1, x2, y2, stroke) {
941
- const length = Math.hypot(x2 - x1, y2 - y1);
942
- if (length < 1) return "";
943
- const ux = (x2 - x1) / length;
944
- const uy = (y2 - y1) / length;
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;
945
3136
  const size = 7;
946
3137
  const half = 3.2;
947
- const baseX = x2 - ux * size;
948
- const baseY = y2 - uy * size;
949
- const left = n(baseX - uy * half) + "," + n(baseY + ux * half);
950
- const right = n(baseX + uy * half) + "," + n(baseY - ux * half);
951
- return '<polygon points="' + n(x2) + "," + n(y2) + " " + left + " " + right + '" fill="' + stroke + '" />';
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" />';
952
3169
  }
953
3170
  function edgeLabel(label, x, y, colours) {
954
3171
  if (typeof label !== "string" || label === "") return "";
955
- return '<text x="' + n(x) + '" y="' + n(y) + '" font-size="10" fill="' + colours.muted + '">' + escape(label) + "</text>";
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>";
956
3177
  }
957
3178
  function toSvg(canvas, colours, label) {
958
3179
  let dropped = 0;