dsh-model-everything 0.5.0 → 0.5.2

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 (65) hide show
  1. package/README.en-US.md +12 -0
  2. package/README.md +4 -0
  3. package/lib/client.js +2829 -224
  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 +1227 -69
  17. package/lib/render/place.js +393 -121
  18. package/lib/render/route.js +225 -0
  19. package/lib/render/svg.js +146 -21
  20. package/lib/render/symbol.js +108 -0
  21. package/lib/render/text.js +74 -0
  22. package/lib/render/trunk.js +213 -0
  23. package/lib/render/wrap.js +87 -0
  24. package/lib/types/render/channels.d.ts +63 -0
  25. package/lib/types/render/components.d.ts +57 -0
  26. package/lib/types/render/coords.d.ts +64 -0
  27. package/lib/types/render/cycle-break.d.ts +45 -0
  28. package/lib/types/render/dummy.d.ts +32 -0
  29. package/lib/types/render/from-plantuml.d.ts +12 -6
  30. package/lib/types/render/layer.d.ts +28 -0
  31. package/lib/types/render/layout.d.ts +43 -11
  32. package/lib/types/render/model.d.ts +17 -0
  33. package/lib/types/render/network-simplex.d.ts +54 -0
  34. package/lib/types/render/order.d.ts +27 -0
  35. package/lib/types/render/paint.d.ts +157 -10
  36. package/lib/types/render/place.d.ts +40 -19
  37. package/lib/types/render/route.d.ts +77 -0
  38. package/lib/types/render/svg.d.ts +1 -1
  39. package/lib/types/render/symbol.d.ts +73 -0
  40. package/lib/types/render/text.d.ts +38 -0
  41. package/lib/types/render/trunk.d.ts +104 -0
  42. package/lib/types/render/wrap.d.ts +27 -0
  43. package/package.json +148 -148
  44. package/skills/model-everything-diagram/SKILL.md +85 -82
  45. package/src/render/channels.ts +309 -0
  46. package/src/render/collapse.ts +150 -155
  47. package/src/render/components.ts +126 -0
  48. package/src/render/coords.ts +328 -0
  49. package/src/render/cycle-break.ts +164 -0
  50. package/src/render/dummy.ts +65 -0
  51. package/src/render/from-mermaid.ts +261 -236
  52. package/src/render/from-plantuml.ts +469 -178
  53. package/src/render/layer.ts +98 -0
  54. package/src/render/layout.ts +101 -62
  55. package/src/render/model.ts +86 -67
  56. package/src/render/network-simplex.ts +290 -0
  57. package/src/render/order.ts +139 -0
  58. package/src/render/paint.ts +1570 -290
  59. package/src/render/place.ts +532 -234
  60. package/src/render/route.ts +269 -0
  61. package/src/render/svg.ts +274 -127
  62. package/src/render/symbol.ts +137 -0
  63. package/src/render/text.ts +86 -0
  64. package/src/render/trunk.ts +275 -0
  65. 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,619 @@ 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 };
1177
+ }
1178
+ function place(diagram, top) {
1179
+ const groups = diagram.groups ?? [];
1180
+ const nodes = [];
1181
+ const seenNode = /* @__PURE__ */ new Set();
1182
+ for (const node of diagram.nodes) {
1183
+ if (seenNode.has(node.id)) continue;
1184
+ seenNode.add(node.id);
1185
+ nodes.push(node);
1186
+ }
1187
+ const nodeById = new Map(nodes.map((node) => [node.id, node]));
1188
+ const groupById = new Map(groups.map((group) => [group.id, group]));
1189
+ const tree = groupTree(groups);
1190
+ const appearance = /* @__PURE__ */ new Map();
1191
+ for (const node of nodes) if (!appearance.has(node.id)) appearance.set(node.id, appearance.size);
1192
+ for (const group of groups) if (!appearance.has(group.id)) appearance.set(group.id, appearance.size);
1193
+ const visible = /* @__PURE__ */ new Set();
1194
+ const asked = /* @__PURE__ */ new Map();
1195
+ const isVisible = (id) => {
1196
+ const known = asked.get(id);
1197
+ if (known !== void 0) return known;
1198
+ const group = groupById.get(id);
1199
+ let found = false;
1200
+ for (const member of group.members) {
1201
+ if (tree.owner.get(member) === id && nodeById.has(member)) {
1202
+ found = true;
1203
+ break;
1204
+ }
1205
+ }
1206
+ if (!found) {
1207
+ for (const child of tree.children.get(id) ?? []) {
1208
+ if (isVisible(child)) {
1209
+ found = true;
1210
+ break;
1211
+ }
1212
+ }
1213
+ }
1214
+ asked.set(id, found);
1215
+ if (found) visible.add(id);
1216
+ return found;
1217
+ };
1218
+ for (const group of groups) isVisible(group.id);
1219
+ const directNodes = (scope) => nodes.filter((node) => tree.owner.get(node.id) === scope).map((node) => node.id);
1220
+ const directGroups = (scope) => (tree.children.get(scope) ?? []).filter((child) => visible.has(child));
1221
+ const firstIndexCache = /* @__PURE__ */ new Map();
1222
+ const firstIndexOf = (id) => {
1223
+ const cached = firstIndexCache.get(id);
1224
+ if (cached !== void 0) return cached;
1225
+ if (nodeById.has(id)) return appearance.get(id);
1226
+ const members = groupById.get(id).members.filter((member) => tree.owner.get(member) === id && nodeById.has(member)).map((member) => appearance.get(member));
1227
+ const children = directGroups(id).map(firstIndexOf);
1228
+ const all = [...members, ...children];
1229
+ const found = Math.min(...all);
1230
+ firstIndexCache.set(id, found);
1231
+ return found;
1232
+ };
1233
+ const entityAt = (scope, id) => {
1234
+ const boundary = groupById.get(id);
1235
+ if (boundary !== void 0) {
1236
+ if (!visible.has(id)) return void 0;
1237
+ if (scope === void 0) {
1238
+ let cursor = id;
1239
+ while (tree.parentOf.has(cursor)) cursor = tree.parentOf.get(cursor);
1240
+ return cursor;
1241
+ }
1242
+ return tree.parentOf.get(id) === scope ? id : void 0;
1243
+ }
1244
+ if (!nodeById.has(id)) return void 0;
1245
+ const group = tree.owner.get(id);
1246
+ if (scope === void 0) {
1247
+ if (group === void 0 || !visible.has(group)) return id;
1248
+ let cursor = group;
1249
+ while (tree.parentOf.has(cursor)) cursor = tree.parentOf.get(cursor);
1250
+ return cursor;
1251
+ }
1252
+ if (group === scope) return id;
1253
+ if (group === void 0) return void 0;
1254
+ return tree.parentOf.get(group) === scope && visible.has(group) ? group : void 0;
1255
+ };
1256
+ const groupLayouts = /* @__PURE__ */ new Map();
1257
+ const measureGroup = (id) => {
1258
+ const inside2 = buildLevel(id);
1259
+ const size = {
1260
+ width: Math.max(MIN_NODE_WIDTH, inside2.width) + GROUP_PADDING * 2,
1261
+ height: inside2.height + GROUP_HEADER + GROUP_PADDING * 2
1262
+ };
1263
+ groupLayouts.set(id, inside2);
1264
+ return size;
1265
+ };
1266
+ const entityOf = (id) => {
1267
+ const node = nodeById.get(id);
1268
+ if (node !== void 0) {
1269
+ const text2 = textOf(node);
1270
+ return { id, group: false, width: text2.width, height: text2.height };
1271
+ }
1272
+ const size = measureGroup(id);
1273
+ return { id, group: true, width: size.width, height: size.height };
1274
+ };
1275
+ const entitiesIn = (scope) => {
1276
+ const found = [];
1277
+ if (scope === void 0) {
1278
+ for (const node of nodes) {
1279
+ const group = tree.owner.get(node.id);
1280
+ if (group === void 0 || !visible.has(group)) found.push(entityOf(node.id));
1281
+ }
1282
+ for (const root2 of tree.roots) if (visible.has(root2)) found.push(entityOf(root2));
1283
+ } else {
1284
+ for (const id of directNodes(scope)) found.push(entityOf(id));
1285
+ for (const id of directGroups(scope)) found.push(entityOf(id));
1286
+ }
1287
+ return found.sort((left, right2) => firstIndexOf(left.id) - firstIndexOf(right2.id));
1288
+ };
1289
+ const edgesIn = (scope) => {
1290
+ const found = [];
1291
+ diagram.edges.forEach((edge, index) => {
1292
+ if (edge.from === edge.to) return;
1293
+ const from = entityAt(scope, edge.from);
1294
+ const to = entityAt(scope, edge.to);
1295
+ if (from === void 0 || to === void 0 || from === to) return;
1296
+ found.push({ index, from, to });
1297
+ });
1298
+ return found;
1299
+ };
1300
+ const pinnedAt = (scope) => {
1301
+ if (diagram.init === void 0) return void 0;
1302
+ const entity = entityAt(scope, diagram.init);
1303
+ return entity === diagram.init || entity !== void 0 && groupById.has(entity) ? entity : void 0;
1304
+ };
1305
+ const layoutPiece = (entities, edges, pinned, seed) => {
1306
+ const byId = new Map(entities.map((entity) => [entity.id, entity]));
1307
+ const graph = {
1308
+ nodes: entities.map((entity) => ({ id: entity.id, width: entity.width, height: entity.height })),
1309
+ edges,
1310
+ ...pinned === void 0 ? {} : { init: pinned }
1311
+ };
1312
+ const broken = breakCycles(graph);
1313
+ const layered = layer(graph, broken.edges);
1314
+ const augmented = addDummies(layered, broken.edges);
1315
+ const ordered = order(augmented.layers, augmented.chains, seed);
1316
+ const gap = edges.some((edge) => diagram.edges[edge.index].label !== "") ? LABELLED_LAYER_GAP : LAYER_GAP;
1317
+ const coordinates = assignCoordinates(
1318
+ {
1319
+ layers: ordered.layers,
1320
+ chains: augmented.chains,
1321
+ sizeOf: (id) => {
1322
+ const entity = byId.get(id);
1323
+ return { width: entity.width, height: entity.height };
1324
+ },
1325
+ chainOf: augmented.chainOf
1326
+ },
1327
+ { gap: NODE_GAP, layerGap: gap }
1328
+ );
1329
+ const bands = planBands(coordinates.layerWidth, coordinates.layerHeight, gap);
1330
+ const bandOfLayer = new Array(coordinates.layerHeight.length).fill(0);
1331
+ bands.bands.forEach((band, index) => {
1332
+ for (let depth = band.first; depth <= band.last; depth += 1) bandOfLayer[depth] = index;
1333
+ });
1334
+ const layerTop = [];
1335
+ for (const band of bands.bands) {
1336
+ let y = 0;
1337
+ for (let depth = band.first; depth <= band.last; depth += 1) {
1338
+ layerTop[depth] = y;
1339
+ y += coordinates.layerHeight[depth] + gap;
1340
+ }
1341
+ }
1342
+ const items = /* @__PURE__ */ new Map();
1343
+ ordered.layers.forEach((row, depth) => {
1344
+ const shift = bands.bands[bandOfLayer[depth]].offsetX;
1345
+ const top2 = layerTop[depth];
1346
+ const rowHeight = coordinates.layerHeight[depth];
1347
+ for (const id of row) {
1348
+ const entity = byId.get(id);
1349
+ if (entity === void 0) continue;
1350
+ const centre = coordinates.x.get(id);
1351
+ items.set(id, {
1352
+ box: {
1353
+ x: centre + shift - entity.width / 2,
1354
+ y: top2 + (rowHeight - entity.height) / 2,
1355
+ width: entity.width,
1356
+ height: entity.height
1357
+ },
1358
+ group: entity.group
1359
+ });
1360
+ }
1361
+ });
1362
+ const across2 = /* @__PURE__ */ new Set();
1363
+ for (const edge of edges) {
1364
+ const from = layered.layerOf.get(edge.from);
1365
+ const to = layered.layerOf.get(edge.to);
1366
+ if (bandOfLayer[from] !== bandOfLayer[to]) across2.add(edge.index);
1367
+ }
1368
+ const laneProposal = /* @__PURE__ */ new Map();
1369
+ for (const chain of augmented.chains) {
1370
+ if (chain.path.length <= 2) continue;
1371
+ const lane = augmented.chainOf.get(chain.path[1]);
1372
+ const fromLayer = layered.layerOf.get(chain.path[0]);
1373
+ const proposed = coordinates.x.get(lane);
1374
+ laneProposal.set(lane, proposed + bands.bands[bandOfLayer[fromLayer]].offsetX);
1375
+ }
1376
+ const itemBoxes = /* @__PURE__ */ new Map();
1377
+ for (const [id, item] of items) itemBoxes.set(id, item.box);
1378
+ const channels = assignChannels(
1379
+ {
1380
+ layers: ordered.layers,
1381
+ chains: augmented.chains,
1382
+ chainOf: augmented.chainOf,
1383
+ boxes: itemBoxes,
1384
+ layerY: layerTop,
1385
+ layerHeight: coordinates.layerHeight,
1386
+ laneX: laneProposal
1387
+ },
1388
+ { clearance: LANE_CLEARANCE, spacing: LANE_SPACING }
1389
+ );
1390
+ const lanes3 = /* @__PURE__ */ new Map();
1391
+ for (const chain of augmented.chains) {
1392
+ if (chain.path.length <= 2) continue;
1393
+ const start = byId.get(chain.path[0]);
1394
+ const end = byId.get(chain.path[chain.path.length - 1]);
1395
+ if (start.group || end.group) continue;
1396
+ const fromLayer = layered.layerOf.get(chain.path[0]);
1397
+ const toLayer = layered.layerOf.get(chain.path[chain.path.length - 1]);
1398
+ if (bandOfLayer[fromLayer] !== bandOfLayer[toLayer]) continue;
1399
+ const channel = channels.lanes.get(chain.index);
1400
+ lanes3.set(chain.index, {
1401
+ points: channel.y.map((y) => ({ x: channel.x, y })),
1402
+ reversed: broken.reversed.has(chain.index)
1403
+ });
1404
+ }
1405
+ const runs2 = /* @__PURE__ */ new Map();
1406
+ for (const [edgeIndex, y] of channels.runs) if (!across2.has(edgeIndex)) runs2.set(edgeIndex, y);
1407
+ let right2 = 0;
1408
+ let bottom = 0;
1409
+ for (const entry of items.values()) {
1410
+ right2 = Math.max(right2, entry.box.x + entry.box.width);
1411
+ bottom = Math.max(bottom, entry.box.y + entry.box.height);
1412
+ }
1413
+ return { width: right2, height: bottom, items, lanes: lanes3, runs: runs2, across: across2 };
1414
+ };
1415
+ function buildLevel(scope) {
1416
+ const entities = entitiesIn(scope);
1417
+ if (entities.length === 0) {
1418
+ return { width: 0, height: 0, items: /* @__PURE__ */ new Map(), lanes: /* @__PURE__ */ new Map(), runs: /* @__PURE__ */ new Map(), across: /* @__PURE__ */ new Set() };
1419
+ }
1420
+ const edges = edgesIn(scope);
1421
+ const pinned = pinnedAt(scope);
1422
+ const seed = seedOrder({ nodes: entities.map((entity) => ({ id: entity.id, width: entity.width, height: entity.height })), edges, ...pinned === void 0 ? {} : { init: pinned } });
1423
+ const components = weakComponents(entities, edges);
1424
+ const pieces = [];
1425
+ for (const members of components) {
1426
+ const inside2 = new Set(members);
1427
+ const pieceEntities = entities.filter((entity) => inside2.has(entity.id));
1428
+ const pieceEdges = edges.filter((edge) => inside2.has(edge.from) && inside2.has(edge.to));
1429
+ const pin = pinned !== void 0 && inside2.has(pinned) ? pinned : void 0;
1430
+ pieces.push(layoutPiece(pieceEntities, pieceEdges, pin, seed));
1431
+ }
1432
+ const packed = packRectangles(pieces.map((piece) => ({ width: piece.width, height: piece.height })), { gap: COMPONENT_GAP });
1433
+ const layout = { width: packed.width, height: packed.height, items: /* @__PURE__ */ new Map(), lanes: /* @__PURE__ */ new Map(), runs: /* @__PURE__ */ new Map(), across: /* @__PURE__ */ new Set() };
1434
+ pieces.forEach((piece, index) => {
1435
+ for (const edgeIndex of piece.across) layout.across.add(edgeIndex);
1436
+ const at = packed.placements[index];
1437
+ for (const [id, item] of piece.items) {
1438
+ layout.items.set(id, { box: { x: item.box.x + at.x, y: item.box.y + at.y, width: item.box.width, height: item.box.height }, group: item.group });
1439
+ }
1440
+ for (const [edgeIndex, lane] of piece.lanes) {
1441
+ layout.lanes.set(edgeIndex, { points: lane.points.map((point) => ({ x: point.x + at.x, y: point.y + at.y })), reversed: lane.reversed });
1442
+ }
1443
+ for (const [edgeIndex, y] of piece.runs) layout.runs.set(edgeIndex, y + at.y);
1444
+ });
1445
+ return layout;
1446
+ }
1447
+ const root = buildLevel(void 0);
1448
+ const boxes = /* @__PURE__ */ new Map();
1449
+ const groupRects = /* @__PURE__ */ new Map();
1450
+ const lanes2 = /* @__PURE__ */ new Map();
1451
+ const runs = /* @__PURE__ */ new Map();
1452
+ const across = /* @__PURE__ */ new Set();
1453
+ const placeLevel = (layout, originX, originY) => {
1454
+ for (const edgeIndex of layout.across) across.add(edgeIndex);
1455
+ for (const [id, item] of layout.items) {
1456
+ const box = { x: originX + item.box.x, y: originY + item.box.y, width: item.box.width, height: item.box.height };
1457
+ if (item.group) {
1458
+ groupRects.set(id, box);
1459
+ const inside2 = groupLayouts.get(id);
1460
+ placeLevel(inside2, box.x + (box.width - inside2.width) / 2, box.y + GROUP_HEADER + GROUP_PADDING);
1461
+ continue;
1462
+ }
1463
+ boxes.set(id, box);
1464
+ }
1465
+ for (const [edgeIndex, lane] of layout.lanes) {
1466
+ lanes2.set(edgeIndex, { points: lane.points.map((point) => ({ x: point.x + originX, y: point.y + originY })), reversed: lane.reversed });
1467
+ }
1468
+ for (const [edgeIndex, y] of layout.runs) runs.set(edgeIndex, y + originY);
1469
+ };
1470
+ placeLevel(root, MARGIN, top);
1471
+ let right = MARGIN;
1472
+ for (const box of boxes.values()) right = Math.max(right, box.x + box.width);
1473
+ for (const box of groupRects.values()) right = Math.max(right, box.x + box.width);
1474
+ return { boxes, groupRects, width: right + MARGIN, lanes: lanes2, runs, across };
1475
+ }
1476
+
1477
+ // src/render/route.ts
1478
+ var LANE_GAP = 10;
1479
+ var ROUTE_STUB = 16;
1480
+ function normalOf(side) {
1481
+ switch (side) {
1482
+ case "top":
1483
+ return { x: 0, y: -1 };
1484
+ case "bottom":
1485
+ return { x: 0, y: 1 };
1486
+ case "left":
1487
+ return { x: -1, y: 0 };
1488
+ case "right":
1489
+ return { x: 1, y: 0 };
1490
+ }
1491
+ }
1492
+ var BEND_COST = 24;
1493
+ function crossesBox(x1, y1, x2, y2, box) {
1494
+ const minX = box.x + 0.5;
1495
+ const maxX = box.x + box.width - 0.5;
1496
+ const minY = box.y + 0.5;
1497
+ const maxY = box.y + box.height - 0.5;
1498
+ if (Math.abs(y1 - y2) < 1e-3) {
1499
+ const y = y1;
1500
+ if (y <= minY || y >= maxY) return false;
1501
+ return Math.max(x1, x2) > minX && Math.min(x1, x2) < maxX;
1502
+ }
1503
+ if (Math.abs(x1 - x2) < 1e-3) {
1504
+ const x = x1;
1505
+ if (x <= minX || x >= maxX) return false;
1506
+ return Math.max(y1, y2) > minY && Math.min(y1, y2) < maxY;
1507
+ }
1508
+ const steps = Math.max(8, Math.round(Math.hypot(x2 - x1, y2 - y1)));
1509
+ for (let index = 0; index <= steps; index += 1) {
1510
+ const x = x1 + (x2 - x1) * index / steps;
1511
+ const y = y1 + (y2 - y1) * index / steps;
1512
+ if (x > minX && x < maxX && y > minY && y < maxY) return true;
1513
+ }
1514
+ return false;
1515
+ }
1516
+ function anchor(box, side) {
1517
+ switch (side) {
1518
+ case "top":
1519
+ return { x: box.x + box.width / 2, y: box.y };
1520
+ case "bottom":
1521
+ return { x: box.x + box.width / 2, y: box.y + box.height };
1522
+ case "left":
1523
+ return { x: box.x, y: box.y + box.height / 2 };
1524
+ case "right":
1525
+ return { x: box.x + box.width, y: box.y + box.height / 2 };
1526
+ }
1527
+ }
1528
+ function approachSides(from, to) {
1529
+ const fromCy = from.y + from.height / 2;
1530
+ const toCy = to.y + to.height / 2;
1531
+ if (Math.abs(toCy - fromCy) > 0.5) return toCy > fromCy ? { exit: "bottom", entry: "top" } : { exit: "top", entry: "bottom" };
1532
+ const fromCx = from.x + from.width / 2;
1533
+ const toCx = to.x + to.width / 2;
1534
+ return toCx >= fromCx ? { exit: "right", entry: "left" } : { exit: "left", entry: "right" };
1535
+ }
1536
+ function lanes(obstacles, extra) {
1537
+ const values = new Set(extra);
1538
+ for (const box of obstacles) {
1539
+ values.add(box.x - LANE_GAP);
1540
+ values.add(box.x + box.width + LANE_GAP);
1541
+ values.add(box.y - LANE_GAP);
1542
+ values.add(box.y + box.height + LANE_GAP);
1543
+ }
1544
+ return [...values].sort((left, right) => left - right);
1545
+ }
1546
+ function facesAway(side, port, other) {
1547
+ const normal = normalOf(side);
1548
+ return normal.x * (other.x + other.width / 2 - port.x) + normal.y * (other.y + other.height / 2 - port.y) < 0;
1549
+ }
1550
+ function routeEdge(from, to, obstacles, anchors) {
1551
+ const sides = approachSides(from, to);
1552
+ const exitSide = anchors?.exitSide ?? sides.exit;
1553
+ const entrySide = anchors?.entrySide ?? sides.entry;
1554
+ const exit = anchors?.exit ?? anchor(from, exitSide);
1555
+ const entry = anchors?.entry ?? anchor(to, entrySide);
1556
+ const others = obstacles.filter((box) => box !== from && box !== to);
1557
+ const own = [];
1558
+ if (facesAway(exitSide, exit, to)) own.push(from);
1559
+ if (facesAway(entrySide, entry, from)) own.push(to);
1560
+ const blockers = own.length === 0 ? others : [...others, ...own];
1561
+ const blockedStub = (point) => blockers.some((box) => point.x > box.x && point.x < box.x + box.width && point.y > box.y && point.y < box.y + box.height);
1562
+ const outNormal = normalOf(exitSide);
1563
+ const inNormal = normalOf(entrySide);
1564
+ let exitStub = { x: exit.x + outNormal.x * ROUTE_STUB, y: exit.y + outNormal.y * ROUTE_STUB };
1565
+ let entryStub = { x: entry.x + inNormal.x * ROUTE_STUB, y: entry.y + inNormal.y * ROUTE_STUB };
1566
+ if (blockedStub(exitStub) || blockedStub(entryStub)) {
1567
+ exitStub = exit;
1568
+ entryStub = entry;
1569
+ }
1570
+ const straight = [exit, exitStub, entryStub, entry];
1571
+ const axisAligned = Math.abs(exitStub.x - entryStub.x) < 0.5 || Math.abs(exitStub.y - entryStub.y) < 0.5;
1572
+ if (axisAligned && !blockers.some((box) => crossesBox(exitStub.x, exitStub.y, entryStub.x, entryStub.y, box))) {
1573
+ return { points: simplify(straight), routed: false };
1574
+ }
1575
+ const xs = lanes(blockers, [exitStub.x, entryStub.x]);
1576
+ const ys = lanes(blockers, [exitStub.y, entryStub.y]);
1577
+ const blocked = (x1, y1, x2, y2) => blockers.some((box) => crossesBox(x1, y1, x2, y2, box));
1578
+ const start = { point: exitStub, axis: -1, cost: 0, bends: 0, path: exitStub === exit ? [exit] : [exit, exitStub] };
1579
+ const queue = [start];
1580
+ const best = /* @__PURE__ */ new Map();
1581
+ const key = (point) => String(Math.round(point.x)) + "," + String(Math.round(point.y));
1582
+ while (queue.length > 0) {
1583
+ let index = 0;
1584
+ for (let cursor = 1; cursor < queue.length; cursor += 1) if (queue[cursor].cost < queue[index].cost) index = cursor;
1585
+ const current = queue.splice(index, 1)[0];
1586
+ if (current.cost > (best.get(key(current.point)) ?? Infinity)) continue;
1587
+ if (Math.abs(current.point.x - entryStub.x) < 0.5 && Math.abs(current.point.y - entryStub.y) < 0.5) {
1588
+ return { points: simplify([...current.path, entryStub, entry]), routed: true };
1589
+ }
1590
+ const options = [];
1591
+ for (const x of xs) if (Math.abs(x - current.point.x) > 0.5) options.push({ x, y: current.point.y });
1592
+ for (const y of ys) if (Math.abs(y - current.point.y) > 0.5) options.push({ x: current.point.x, y });
1593
+ for (const next of options) {
1594
+ if (blocked(current.point.x, current.point.y, next.x, next.y)) continue;
1595
+ const axis = Math.abs(next.y - current.point.y) < 0.5 ? 0 : 1;
1596
+ const bend = current.axis === -1 || current.axis === axis ? 0 : 1;
1597
+ const cost = current.cost + Math.abs(next.x - current.point.x) + Math.abs(next.y - current.point.y) + bend * BEND_COST;
1598
+ const state = key(next);
1599
+ if (cost >= (best.get(state) ?? Infinity)) continue;
1600
+ best.set(state, cost);
1601
+ queue.push({ point: next, axis, cost, bends: current.bends + bend, path: [...current.path, next] });
1602
+ }
1603
+ }
1604
+ return { points: simplify(straight), routed: false };
1605
+ }
1606
+ function simplify(points) {
1607
+ if (points.length <= 2) return points;
1608
+ const out = [points[0]];
1609
+ for (let index = 1; index < points.length - 1; index += 1) {
1610
+ const previous = out[out.length - 1];
1611
+ const current = points[index];
1612
+ const next = points[index + 1];
1613
+ const collinear = Math.abs(previous.x - current.x) < 0.5 && Math.abs(current.x - next.x) < 0.5 || Math.abs(previous.y - current.y) < 0.5 && Math.abs(current.y - next.y) < 0.5;
1614
+ if (!collinear) out.push(current);
1615
+ }
1616
+ out.push(points[points.length - 1]);
1617
+ return out;
1618
+ }
1619
+
1620
+ // src/render/trunk.ts
1621
+ var TRUNK_MIN_EDGES = 3;
1622
+ function isVerticalSide(side) {
1623
+ return side === "top" || side === "bottom";
1624
+ }
1625
+ function trunkAt(box, side) {
1626
+ if (side === "top") return box.y - ROUTE_STUB;
1627
+ if (side === "bottom") return box.y + box.height + ROUTE_STUB;
1628
+ if (side === "left") return box.x - ROUTE_STUB;
1629
+ return box.x + box.width + ROUTE_STUB;
1630
+ }
1631
+ function alongSide(point, side) {
1632
+ return isVerticalSide(side) ? point.x : point.y;
1633
+ }
1634
+ function alongNormal(point, side) {
1635
+ return isVerticalSide(side) ? point.y : point.x;
1636
+ }
1637
+ function facesTrunk(point, side, at) {
1638
+ const normal = normalOf(side);
1639
+ const outward = isVerticalSide(side) ? normal.y : normal.x;
1640
+ return (at - alongNormal(point, side)) * outward > 0.5;
1641
+ }
1642
+ function keyOf(end) {
1643
+ return end.to + "\0" + end.entry + "\0" + (end.arrow ?? "");
1644
+ }
1645
+ function clearSegment(boxes, skip, x1, y1, x2, y2) {
1646
+ for (const box of boxes) {
1647
+ if (skip.includes(box)) continue;
1648
+ if (crossesBox(x1, y1, x2, y2, box)) return false;
1649
+ }
1650
+ return true;
1651
+ }
1652
+ function onOneLine(group, side, at, low, high) {
1653
+ if (isVerticalSide(group.side) !== isVerticalSide(side)) return false;
1654
+ if (group.at !== at) return false;
1655
+ return Math.min(group.to, high) - Math.max(group.from, low) > 0.5;
1656
+ }
1657
+ function planTrunks(input) {
1658
+ const groups = [];
1659
+ const groupOf = /* @__PURE__ */ new Map();
1660
+ const buckets = /* @__PURE__ */ new Map();
1661
+ for (const end of input.ends) {
1662
+ if (end.runY === void 0) continue;
1663
+ if (isVerticalSide(end.exit) !== isVerticalSide(end.entry)) continue;
1664
+ const source = input.boxOf(end.from);
1665
+ const target = input.boxOf(end.to);
1666
+ if (source === void 0 || target === void 0) continue;
1667
+ if (source === target) continue;
1668
+ const exit = input.exitPortOf(end.index);
1669
+ if (exit === void 0) continue;
1670
+ const at = trunkAt(target, end.entry);
1671
+ if (!facesTrunk(exit, end.exit, at)) continue;
1672
+ const candidate = { end, source, target, at, exit, branch: alongSide(exit, end.exit) };
1673
+ const key = keyOf(end);
1674
+ const list = buckets.get(key);
1675
+ if (list === void 0) buckets.set(key, [candidate]);
1676
+ else list.push(candidate);
1677
+ }
1678
+ for (const list of buckets.values()) {
1679
+ const bySource = /* @__PURE__ */ new Map();
1680
+ for (const candidate of list) {
1681
+ const seen = bySource.get(candidate.end.from);
1682
+ if (seen === void 0) bySource.set(candidate.end.from, [candidate]);
1683
+ else seen.push(candidate);
1684
+ }
1685
+ const members = list.filter((candidate) => bySource.get(candidate.end.from).length === 1);
1686
+ if (members.length < TRUNK_MIN_EDGES) continue;
1687
+ const target = members[0].target;
1688
+ const side = members[0].end.entry;
1689
+ const at = members[0].at;
1690
+ const entryAlong = isVerticalSide(side) ? target.x + target.width / 2 : target.y + target.height / 2;
1691
+ let lead = members[0];
1692
+ for (const candidate of members) {
1693
+ if (Math.abs(candidate.branch - entryAlong) < Math.abs(lead.branch - entryAlong) - 1e-9) lead = candidate;
1694
+ }
1695
+ let low = entryAlong;
1696
+ let high = entryAlong;
1697
+ for (const candidate of members) {
1698
+ low = Math.min(low, candidate.branch);
1699
+ high = Math.max(high, candidate.branch);
1700
+ }
1701
+ const vertical = isVerticalSide(side);
1702
+ const run = vertical ? [low, at, high, at] : [at, low, at, high];
1703
+ if (!clearSegment(input.boxes, [target], run[0], run[1], run[2], run[3])) continue;
1704
+ let blocked = false;
1705
+ for (const candidate of members) {
1706
+ const drop = vertical ? [candidate.branch, candidate.exit.y, candidate.branch, at] : [candidate.exit.x, candidate.branch, at, candidate.branch];
1707
+ if (!clearSegment(input.boxes, [candidate.source, target], drop[0], drop[1], drop[2], drop[3])) blocked = true;
1708
+ }
1709
+ if (blocked) continue;
1710
+ if (groups.some((group) => onOneLine(group, side, at, low, high))) continue;
1711
+ const id = groups.length;
1712
+ groups.push({
1713
+ id,
1714
+ target: members[0].end.to,
1715
+ side,
1716
+ at,
1717
+ entryAlong,
1718
+ from: low,
1719
+ to: high,
1720
+ lead: lead.end.index,
1721
+ members: members.map((candidate) => candidate.end.index)
1722
+ });
1723
+ for (const candidate of members) groupOf.set(candidate.end.index, id);
1724
+ }
1725
+ return { groups, groupOf };
166
1726
  }
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];
179
- };
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
188
- };
1727
+
1728
+ // src/render/symbol.ts
1729
+ function cubicPoint(curve, t) {
1730
+ const u = 1 - t;
1731
+ const a = u * u * u;
1732
+ const b = 3 * u * u * t;
1733
+ const c = 3 * u * t * t;
1734
+ const d = t * t * t;
1735
+ return {
1736
+ x: a * curve.from.x + b * curve.c1.x + c * curve.c2.x + d * curve.to.x,
1737
+ y: a * curve.from.y + b * curve.c1.y + c * curve.c2.y + d * curve.to.y
189
1738
  };
190
- const boxes = /* @__PURE__ */ new Map();
191
- 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;
1739
+ }
1740
+ function diamondPoints(box) {
1741
+ return [
1742
+ { x: box.x + box.width / 2, y: box.y },
1743
+ { x: box.x + box.width, y: box.y + box.height / 2 },
1744
+ { x: box.x + box.width / 2, y: box.y + box.height },
1745
+ { x: box.x, y: box.y + box.height / 2 }
1746
+ ];
1747
+ }
1748
+ function hasDiscretePorts(shape) {
1749
+ return shape === "diamond" || shape === "circle";
1750
+ }
1751
+ function hexagonCut(width) {
1752
+ return Math.min(12, width / 4);
1753
+ }
1754
+ function hexagonPoints(box) {
1755
+ const cut2 = hexagonCut(box.width);
1756
+ return [
1757
+ { x: box.x + cut2, y: box.y },
1758
+ { x: box.x + box.width - cut2, y: box.y },
1759
+ { x: box.x + box.width, y: box.y + box.height / 2 },
1760
+ { x: box.x + box.width - cut2, y: box.y + box.height },
1761
+ { x: box.x + cut2, y: box.y + box.height },
1762
+ { x: box.x, y: box.y + box.height / 2 }
1763
+ ];
1764
+ }
1765
+ function stadiumRadius(box) {
1766
+ return Math.min(box.height / 2, box.width / 2);
1767
+ }
1768
+ function cylinderCaps(box) {
1769
+ const cap = Math.min(10, box.height / 3);
1770
+ return {
1771
+ cap,
1772
+ top: {
1773
+ from: { x: box.x, y: box.y + cap },
1774
+ c1: { x: box.x, y: box.y },
1775
+ c2: { x: box.x + box.width, y: box.y },
1776
+ to: { x: box.x + box.width, y: box.y + cap }
1777
+ },
1778
+ bottom: {
1779
+ from: { x: box.x + box.width, y: box.y + box.height - cap },
1780
+ c1: { x: box.x + box.width, y: box.y + box.height },
1781
+ c2: { x: box.x, y: box.y + box.height },
1782
+ to: { x: box.x, y: box.y + box.height - cap }
206
1783
  }
207
- return size;
208
1784
  };
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));
213
- };
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;
222
- }
223
- return { boxes, groupRects, width: width + MARGIN * 2 };
224
1785
  }
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);
1786
+ function cylinderApex(box, side) {
1787
+ const caps = cylinderCaps(box);
1788
+ return cubicPoint(side === "top" ? caps.top : caps.bottom, 0.5);
228
1789
  }
229
1790
 
230
1791
  // src/render/palette.ts
@@ -327,12 +1888,36 @@ window.__ModuleLoader__.load({
327
1888
 
328
1889
  // src/render/paint.ts
329
1890
  var TITLE_HEIGHT = 26;
330
- var NODE_HEIGHT2 = 34;
331
- var CHANNEL_GAP = 16;
332
1891
  var CAPTION_CHARS = 78;
333
1892
  var CAPTION_LINE = 15;
334
1893
  var CAPTION_GAP = 16;
335
1894
  var CAPTION_CHAR_WIDTH = 6.2;
1895
+ var PORT_SEPARATION = 14;
1896
+ var PORT_SIDES = ["top", "right", "bottom", "left"];
1897
+ var isVerticalSide2 = (side) => side === "top" || side === "bottom";
1898
+ var MULTI_EDGE_SPACING = 20;
1899
+ function packPorts(wanted, low, high) {
1900
+ const placed = [];
1901
+ let previous = low - PORT_SEPARATION;
1902
+ for (const value of wanted) {
1903
+ previous = Math.min(high, Math.max(value, previous + PORT_SEPARATION));
1904
+ placed.push(previous);
1905
+ }
1906
+ let next = high + PORT_SEPARATION;
1907
+ for (let index = placed.length - 1; index >= 0; index -= 1) {
1908
+ next = Math.max(low, Math.min(placed[index], next - PORT_SEPARATION));
1909
+ placed[index] = next;
1910
+ }
1911
+ return placed;
1912
+ }
1913
+ function centreRun(placed, low, high) {
1914
+ if (placed.length === 0) return placed;
1915
+ const first = placed[0];
1916
+ const last = placed[placed.length - 1];
1917
+ const middle = (low + high) / 2;
1918
+ const shift = Math.min(high - last, Math.max(low - first, middle - (first + last) / 2));
1919
+ return shift === 0 ? placed : placed.map((value) => value + shift);
1920
+ }
336
1921
  function roleOfNode(node) {
337
1922
  if (node.role === "error") return "fail";
338
1923
  if (node.role === "warning") return "findings";
@@ -363,6 +1948,113 @@ window.__ModuleLoader__.load({
363
1948
  function captionRole(caption) {
364
1949
  return roleForVerdict(caption.split(" \u2014 ")[0]);
365
1950
  }
1951
+ var LABEL_HEIGHT = 12;
1952
+ var LABEL_PAD = 2;
1953
+ var LABEL_STEP = 3;
1954
+ function runsOf(points) {
1955
+ const runs = [];
1956
+ for (let index = 0; index + 1 < points.length; index += 1) {
1957
+ const from = points[index];
1958
+ const to = points[index + 1];
1959
+ const length = Math.abs(to.x - from.x) + Math.abs(to.y - from.y);
1960
+ if (length < 1) continue;
1961
+ runs.push({ from, to, length, index });
1962
+ }
1963
+ return runs.sort((left, right) => right.length - left.length || left.index - right.index);
1964
+ }
1965
+ function placeLabel(label, points, boxes, placed, otherRoutes) {
1966
+ const width = textWidth(label);
1967
+ const height = LABEL_HEIGHT;
1968
+ const spotAt = (x, y) => ({ x, y, width, height });
1969
+ 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;
1970
+ 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;
1971
+ 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;
1972
+ const collisionsAt = (spot, run) => {
1973
+ let boxHits = 0;
1974
+ let labelHits = 0;
1975
+ let edgeHits = 0;
1976
+ for (const box of boxes) if (hitsBox(spot, box)) boxHits += 1;
1977
+ for (const earlier of placed) if (hitsSpot(spot, earlier)) labelHits += 1;
1978
+ for (const route of otherRoutes) {
1979
+ for (const segment of route) {
1980
+ if (!hitsSegment(spot, segment)) continue;
1981
+ edgeHits += 1;
1982
+ break;
1983
+ }
1984
+ }
1985
+ return { run, collisions: boxHits + labelHits + edgeHits, boxes: boxHits, labels: labelHits, edges: edgeHits };
1986
+ };
1987
+ const runs = runsOf(points);
1988
+ if (runs.length === 0) {
1989
+ const point = points[0] ?? { x: 0, y: 0 };
1990
+ return { ...spotAt(point.x, point.y), run: -1, collisions: 0, boxes: 0, labels: 0, edges: 0 };
1991
+ }
1992
+ let best;
1993
+ for (const [runIndex, run] of runs.entries()) {
1994
+ const dx = run.to.x - run.from.x;
1995
+ const dy = run.to.y - run.from.y;
1996
+ const ux = dx / run.length;
1997
+ const uy = dy / run.length;
1998
+ const middle = { x: (run.from.x + run.to.x) / 2, y: (run.from.y + run.to.y) / 2 };
1999
+ const along = Math.abs(ux) * width / 2 + Math.abs(uy) * height / 2;
2000
+ const bound = Math.max(0, run.length / 2 - along);
2001
+ const walk = [0];
2002
+ for (let step = LABEL_STEP; step <= bound + 1e-3; step += LABEL_STEP) walk.push(step, -step);
2003
+ for (const offset of walk) {
2004
+ const spot = spotAt(middle.x + ux * offset, middle.y + uy * offset);
2005
+ const placement = collisionsAt(spot, runIndex);
2006
+ if (placement.collisions === 0) return { ...spot, ...placement };
2007
+ if (best === void 0 || placement.collisions < best.collisions || placement.collisions === best.collisions && placement.boxes + placement.labels < best.boxes + best.labels) {
2008
+ best = { ...spot, ...placement };
2009
+ }
2010
+ }
2011
+ }
2012
+ return best;
2013
+ }
2014
+ function drawnExtent(shapes) {
2015
+ let width = 0;
2016
+ let height = 0;
2017
+ const consider = (x, y) => {
2018
+ if (x > width) width = x;
2019
+ if (y > height) height = y;
2020
+ };
2021
+ for (const shape of shapes) {
2022
+ switch (shape.kind) {
2023
+ case "box":
2024
+ case "group":
2025
+ consider(shape.x + shape.width, shape.y + shape.height);
2026
+ break;
2027
+ case "link":
2028
+ consider(Math.max(shape.x1, shape.x2), Math.max(shape.y1, shape.y2));
2029
+ if (shape.label !== "") consider(shape.labelX + textWidth(shape.label) / 2, shape.labelY + LABEL_HEIGHT / 2);
2030
+ break;
2031
+ case "path": {
2032
+ for (const point of shape.points) consider(point.x, point.y);
2033
+ if (shape.label !== "") consider(shape.labelX + textWidth(shape.label) / 2, shape.labelY + LABEL_HEIGHT / 2);
2034
+ break;
2035
+ }
2036
+ case "caption":
2037
+ case "title":
2038
+ consider(textWidth(shape.lines?.[0] ?? "") + shape.x, shape.y + 12);
2039
+ break;
2040
+ case "junction":
2041
+ break;
2042
+ }
2043
+ }
2044
+ return { width, height };
2045
+ }
2046
+ function boxesCrossedBy(points, from, to, boxes) {
2047
+ const crossed = [];
2048
+ for (let index = 0; index + 1 < points.length; index += 1) {
2049
+ const a = points[index];
2050
+ const b = points[index + 1];
2051
+ for (const box of boxes) {
2052
+ if (box === from || box === to) continue;
2053
+ if (crossesBox(a.x, a.y, b.x, b.y, box)) crossed.push(box);
2054
+ }
2055
+ }
2056
+ return crossed;
2057
+ }
366
2058
  function paintWith(diagram, overrides = {}, options = {}) {
367
2059
  const colours = palette(overrides, options);
368
2060
  const shapes = [];
@@ -370,16 +2062,23 @@ window.__ModuleLoader__.load({
370
2062
  const topOffset = MARGIN + TITLE_HEIGHT;
371
2063
  const placement = place(diagram, topOffset);
372
2064
  const { boxes, groupRects } = placement;
2065
+ const endBox = (id) => boxes.get(id) ?? groupRects.get(id);
2066
+ const shapeById = /* @__PURE__ */ new Map();
2067
+ for (const node of diagram.nodes) if (node.shape !== void 0 && !shapeById.has(node.id)) shapeById.set(node.id, node.shape);
2068
+ const shapeOf = (id) => shapeById.get(id) ?? "rect";
373
2069
  for (const group of groups) {
374
2070
  const rect = groupRects.get(group.id);
375
2071
  if (rect === void 0) continue;
376
2072
  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
2073
  }
378
2074
  const drawn = /* @__PURE__ */ new Set();
2075
+ const boxList = Array.from(boxes.values());
2076
+ const routes = [];
379
2077
  for (const node of diagram.nodes) {
380
2078
  if (drawn.has(node.id)) continue;
381
2079
  const box = boxes.get(node.id);
382
2080
  drawn.add(node.id);
2081
+ const text2 = textOf(node);
383
2082
  shapes.push({
384
2083
  kind: "box",
385
2084
  x: box.x,
@@ -387,23 +2086,540 @@ window.__ModuleLoader__.load({
387
2086
  width: box.width,
388
2087
  height: box.height,
389
2088
  label: labelOf(node),
2089
+ lines: text2.lines,
2090
+ ...node.shape === void 0 ? {} : { shape: node.shape },
390
2091
  colour: roleOfNode(node),
391
2092
  initial: diagram.init === node.id
392
2093
  });
393
2094
  }
394
2095
  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;
2096
+ const multiEdgeSets = /* @__PURE__ */ new Map();
2097
+ diagram.edges.forEach((edge, index) => {
2098
+ if (edge.from === edge.to) return;
2099
+ if (endBox(edge.from) === void 0 || endBox(edge.to) === void 0) return;
2100
+ const key = edge.from < edge.to ? edge.from + "\0" + edge.to : edge.to + "\0" + edge.from;
2101
+ const list = multiEdgeSets.get(key);
2102
+ if (list === void 0) multiEdgeSets.set(key, [index]);
2103
+ else list.push(index);
2104
+ });
2105
+ const fanOf = /* @__PURE__ */ new Map();
2106
+ for (const list of multiEdgeSets.values()) {
2107
+ if (list.length < 2) continue;
2108
+ list.forEach((index, position) => {
2109
+ if (position === 0) return;
2110
+ fanOf.set(index, (position % 2 === 1 ? 1 : -1) * Math.ceil(position / 2) * MULTI_EDGE_SPACING);
2111
+ });
2112
+ }
2113
+ const endKey = (index, end) => String(index) + ":" + end;
2114
+ const endsAt = /* @__PURE__ */ new Map();
397
2115
  for (const edge of diagram.edges) {
398
- const source = boxes.get(edge.from);
399
- const target = boxes.get(edge.to);
2116
+ if (edge.from === edge.to) continue;
2117
+ if (endBox(edge.from) === void 0 || endBox(edge.to) === void 0) continue;
2118
+ endsAt.set(edge.from, (endsAt.get(edge.from) ?? 0) + 1);
2119
+ endsAt.set(edge.to, (endsAt.get(edge.to) ?? 0) + 1);
2120
+ }
2121
+ const spanOf2 = (boxId, side, corner) => {
2122
+ const box = endBox(boxId);
2123
+ const vertical = side === "top" || side === "bottom";
2124
+ const middle = vertical ? box.x + box.width / 2 : box.y + box.height / 2;
2125
+ const within = (low, high) => ({ low, high: Math.max(low, high) });
2126
+ switch (shapeOf(boxId)) {
2127
+ // The four vertices of a diamond, and the four cardinal points of an ellipse, are the middles of its sides.
2128
+ case "diamond":
2129
+ case "circle":
2130
+ return within(middle, middle);
2131
+ case "hexagon":
2132
+ return vertical ? within(box.x + hexagonCut(box.width), box.x + box.width - hexagonCut(box.width)) : within(middle, middle);
2133
+ case "stadium": {
2134
+ const radius = stadiumRadius(box);
2135
+ return vertical ? within(box.x + radius, box.x + box.width - radius) : within(middle, middle);
2136
+ }
2137
+ case "cylinder": {
2138
+ if (vertical) return within(middle, middle);
2139
+ const cap = cylinderCaps(box).cap;
2140
+ return within(box.y + cap, box.y + box.height - cap);
2141
+ }
2142
+ default: {
2143
+ const inset = corner ? 2 : vertical ? PORT_INSET : 8;
2144
+ return vertical ? within(box.x + inset, box.x + box.width - inset) : within(box.y + inset, box.y + box.height - inset);
2145
+ }
2146
+ }
2147
+ };
2148
+ const clampTo = (boxId, side, value, corner) => {
2149
+ const span = spanOf2(boxId, side, corner);
2150
+ return Math.min(span.high, Math.max(span.low, value));
2151
+ };
2152
+ const pointOn = (boxId, side, along) => {
2153
+ const box = endBox(boxId);
2154
+ if (side === "left") return { x: box.x, y: along };
2155
+ if (side === "right") return { x: box.x + box.width, y: along };
2156
+ if (side === "top") return { x: along, y: shapeOf(boxId) === "cylinder" ? cylinderApex(box, "top").y : box.y };
2157
+ return { x: along, y: shapeOf(boxId) === "cylinder" ? cylinderApex(box, "bottom").y : box.y + box.height };
2158
+ };
2159
+ const claimed = /* @__PURE__ */ new Map();
2160
+ const claimedAt = /* @__PURE__ */ new Map();
2161
+ const claim = (id, index, end, side) => {
2162
+ claimed.set(endKey(index, end), side);
2163
+ claimedAt.set(id, [...claimedAt.get(id) ?? [], { index, side }]);
2164
+ };
2165
+ const takenSides = (id, except) => (claimedAt.get(id) ?? []).filter((entry) => entry.index !== except).map((entry) => entry.side);
2166
+ const sideMiddle = (box, side) => isVerticalSide2(side) ? box.x + box.width / 2 : box.y + box.height / 2;
2167
+ const sidePoint = (boxId, side) => pointOn(boxId, side, sideMiddle(endBox(boxId), side));
2168
+ const facesAway2 = (box, other, side) => {
2169
+ const out = normalOf(side);
2170
+ const dx = other.x + other.width / 2 - (box.x + box.width / 2);
2171
+ const dy = other.y + other.height / 2 - (box.y + box.height / 2);
2172
+ return out.x * dx + out.y * dy < 0;
2173
+ };
2174
+ const stubAlong = (pinnedId, pinnedSide, freeSide) => {
2175
+ const point = sidePoint(pinnedId, pinnedSide);
2176
+ const out = normalOf(pinnedSide);
2177
+ return isVerticalSide2(freeSide) ? point.x + out.x * ROUTE_STUB : point.y + out.y * ROUTE_STUB;
2178
+ };
2179
+ const offers = (boxId, side, value) => {
2180
+ const span = spanOf2(boxId, side, true);
2181
+ return value >= span.low - 0.01 && value <= span.high + 0.01;
2182
+ };
2183
+ const bendBudget = (fromId, toId, exit, entry, index) => {
2184
+ const from = endBox(fromId);
2185
+ const to = endBox(toId);
2186
+ if (facesAway2(from, to, exit) || facesAway2(to, from, entry)) return 4;
2187
+ if (isVerticalSide2(exit) === isVerticalSide2(entry)) {
2188
+ const out = normalOf(exit);
2189
+ const into = normalOf(entry);
2190
+ const sameWay = out.x === -into.x && out.y === -into.y;
2191
+ const fromSpan = spanOf2(fromId, exit, true);
2192
+ const toSpan = spanOf2(toId, entry, true);
2193
+ const meet = fromSpan.low <= toSpan.high + 0.01 && toSpan.low <= fromSpan.high + 0.01;
2194
+ return sameWay && meet ? 0 : 2;
2195
+ }
2196
+ const horizontalId = isVerticalSide2(exit) ? toId : fromId;
2197
+ const horizontalSide = isVerticalSide2(exit) ? entry : exit;
2198
+ const verticalId = isVerticalSide2(exit) ? fromId : toId;
2199
+ const verticalSide = isVerticalSide2(exit) ? exit : entry;
2200
+ if (!offers(verticalId, verticalSide, stubAlong(horizontalId, horizontalSide, verticalSide))) return 2;
2201
+ const crowded = takenSides(fromId, index).includes(exit) || takenSides(toId, index).includes(entry);
2202
+ return crowded ? 2 : 1;
2203
+ };
2204
+ const bestSide = (id, otherId, end, partner, index) => {
2205
+ const box = endBox(id);
2206
+ const other = endBox(otherId);
2207
+ const centre = { x: box.x + box.width / 2, y: box.y + box.height / 2 };
2208
+ const toward = { x: other.x + other.width / 2 - centre.x, y: other.y + other.height / 2 - centre.y };
2209
+ const reach = Math.hypot(toward.x, toward.y);
2210
+ const taken = takenSides(id, index);
2211
+ let best;
2212
+ for (const side of PORT_SIDES) {
2213
+ if (taken.includes(side)) continue;
2214
+ const bends = end === "exit" ? bendBudget(id, otherId, side, partner, index) : bendBudget(otherId, id, partner, side, index);
2215
+ const normal = normalOf(side);
2216
+ const score = reach === 0 ? 0 : (normal.x * toward.x + normal.y * toward.y) / reach;
2217
+ const point = anchor(box, side);
2218
+ const near = Math.hypot(
2219
+ Math.max(other.x - point.x, 0, point.x - (other.x + other.width)),
2220
+ Math.max(other.y - point.y, 0, point.y - (other.y + other.height))
2221
+ );
2222
+ const partnerPoint = anchor(other, partner);
2223
+ const ground = Math.abs(point.x - partnerPoint.x) + Math.abs(point.y - partnerPoint.y);
2224
+ const better = best === void 0 || bends < best.bends || bends === best.bends && (score > best.score + 1e-9 || Math.abs(score - best.score) <= 1e-9 && (near < best.near - 1e-9 || Math.abs(near - best.near) <= 1e-9 && ground < best.ground - 1e-9));
2225
+ if (better) best = { side, bends, score, near, ground };
2226
+ }
2227
+ return best?.side;
2228
+ };
2229
+ const bestPair = (fromId, toId, index) => {
2230
+ const approach = approachSides(endBox(fromId), endBox(toId));
2231
+ const pairs = [
2232
+ { exit: "bottom", entry: "top" },
2233
+ { exit: "top", entry: "bottom" },
2234
+ { exit: "right", entry: "left" },
2235
+ { exit: "left", entry: "right" }
2236
+ ];
2237
+ let best = approach;
2238
+ let bestBends = bendBudget(fromId, toId, approach.exit, approach.entry, index);
2239
+ for (const pair of pairs) {
2240
+ const bends = bendBudget(fromId, toId, pair.exit, pair.entry, index);
2241
+ if (bends >= bestBends) continue;
2242
+ best = pair;
2243
+ bestBends = bends;
2244
+ }
2245
+ return best;
2246
+ };
2247
+ const bestEnd = (id, otherId, end, partner, index) => {
2248
+ const approach = end === "exit" ? approachSides(endBox(id), endBox(otherId)).exit : approachSides(endBox(otherId), endBox(id)).entry;
2249
+ let best = approach;
2250
+ let bestBends = end === "exit" ? bendBudget(id, otherId, approach, partner, index) : bendBudget(otherId, id, partner, approach, index);
2251
+ for (const side of PORT_SIDES) {
2252
+ const bends = end === "exit" ? bendBudget(id, otherId, side, partner, index) : bendBudget(otherId, id, partner, side, index);
2253
+ if (bends >= bestBends) continue;
2254
+ best = side;
2255
+ bestBends = bends;
2256
+ }
2257
+ return best;
2258
+ };
2259
+ diagram.edges.forEach((edge, index) => {
2260
+ if (edge.from === edge.to) return;
2261
+ const source = endBox(edge.from);
2262
+ const target = endBox(edge.to);
2263
+ if (source === void 0 || target === void 0) return;
2264
+ if (placement.across.has(index)) {
2265
+ claim(edge.from, index, "exit", "right");
2266
+ claim(edge.to, index, "entry", "left");
2267
+ return;
2268
+ }
2269
+ const lane = placement.lanes.get(index);
2270
+ if (lane === void 0 || lane.points.length === 0) return;
2271
+ const down = target.y > source.y;
2272
+ claim(edge.from, index, "exit", down ? "bottom" : "top");
2273
+ claim(edge.to, index, "entry", down ? "top" : "bottom");
2274
+ });
2275
+ diagram.edges.forEach((edge, index) => {
2276
+ if (edge.from === edge.to) return;
2277
+ const source = endBox(edge.from);
2278
+ const target = endBox(edge.to);
2279
+ if (source === void 0 || target === void 0) return;
2280
+ for (const end of ["exit", "entry"]) {
2281
+ if (claimed.has(endKey(index, end))) continue;
2282
+ const id = end === "exit" ? edge.from : edge.to;
2283
+ if ((endsAt.get(id) ?? 0) < 2) continue;
2284
+ if (!hasDiscretePorts(shapeOf(id))) continue;
2285
+ const approached = approachSides(source, target);
2286
+ const partner = claimed.get(endKey(index, end === "exit" ? "entry" : "exit")) ?? (end === "exit" ? approached.entry : approached.exit);
2287
+ const side = bestSide(id, end === "exit" ? edge.to : edge.from, end, partner, index);
2288
+ if (side === void 0) continue;
2289
+ claim(id, index, end, side);
2290
+ }
2291
+ });
2292
+ const facing = diagram.edges.map((edge, index) => {
2293
+ const source = endBox(edge.from);
2294
+ const target = endBox(edge.to);
2295
+ if (source === void 0 || target === void 0 || edge.from === edge.to) return void 0;
2296
+ if (placement.across.has(index)) return { exit: "right", entry: "left" };
2297
+ const lane = placement.lanes.get(index);
2298
+ const down = target.y > source.y;
2299
+ if (lane !== void 0 && lane.points.length > 0) {
2300
+ return {
2301
+ exit: claimed.get(endKey(index, "exit")) ?? (down ? "bottom" : "top"),
2302
+ entry: claimed.get(endKey(index, "entry")) ?? (down ? "top" : "bottom"),
2303
+ at: lane.points[0].x
2304
+ };
2305
+ }
2306
+ const exit = claimed.get(endKey(index, "exit"));
2307
+ const entry = claimed.get(endKey(index, "entry"));
2308
+ if (exit !== void 0 && entry !== void 0) return { exit, entry };
2309
+ if (exit !== void 0) return { exit, entry: bestEnd(edge.to, edge.from, "entry", exit, index) };
2310
+ if (entry !== void 0) return { exit: bestEnd(edge.from, edge.to, "exit", entry, index), entry };
2311
+ return bestPair(edge.from, edge.to, index);
2312
+ });
2313
+ const laneClear = (points, x, from, to) => {
2314
+ for (let at = 0; at + 1 < points.length; at += 1) {
2315
+ for (const box of boxList) {
2316
+ if (box === from || box === to) continue;
2317
+ if (crossesBox(x, points[at].y, x, points[at + 1].y, box)) return false;
2318
+ }
2319
+ }
2320
+ return true;
2321
+ };
2322
+ const laneShift = /* @__PURE__ */ new Map();
2323
+ for (const list of multiEdgeSets.values()) {
2324
+ if (list.length < 2) continue;
2325
+ const lead = placement.lanes.get(list[0]);
2326
+ if (lead === void 0 || lead.points.length === 0) continue;
2327
+ const base = lead.points[0].x;
2328
+ const taken = /* @__PURE__ */ new Set();
2329
+ for (const index of list) {
2330
+ const lane = placement.lanes.get(index);
2331
+ const own = lane.points[0].x;
2332
+ const offset = fanOf.get(index) ?? 0;
2333
+ const edge = diagram.edges[index];
2334
+ const from = endBox(edge.from);
2335
+ const to = endBox(edge.to);
2336
+ let target = own;
2337
+ if (offset !== 0) {
2338
+ for (const candidate of [base + offset, own + offset]) {
2339
+ if (taken.has(candidate) || !laneClear(lane.points, candidate, from, to)) continue;
2340
+ target = candidate;
2341
+ break;
2342
+ }
2343
+ }
2344
+ taken.add(target);
2345
+ if (target !== own) laneShift.set(index, target - own);
2346
+ }
2347
+ }
2348
+ const wanted = /* @__PURE__ */ new Map();
2349
+ const slotKey = (boxId, side, index) => boxId + ":" + side + ":" + String(index);
2350
+ for (const [index, entry] of facing.entries()) {
2351
+ if (entry === void 0) continue;
2352
+ const edge = diagram.edges[index];
2353
+ const from = endBox(edge.from);
2354
+ const to = endBox(edge.to);
2355
+ const middle = (box, side) => isVerticalSide2(side) ? box.x + box.width / 2 : box.y + box.height / 2;
2356
+ const fromMiddle = middle(from, entry.exit);
2357
+ const toMiddle = middle(to, entry.entry);
2358
+ const corner = entry.at !== void 0;
2359
+ const offset = fanOf.get(index) ?? 0;
2360
+ let exitWanted;
2361
+ let entryWanted;
2362
+ if (entry.at !== void 0) {
2363
+ const chosen = entry.at + (laneShift.get(index) ?? 0);
2364
+ exitWanted = clampTo(edge.from, entry.exit, chosen, corner);
2365
+ entryWanted = clampTo(edge.to, entry.entry, chosen, corner);
2366
+ } else if (isVerticalSide2(entry.exit) === isVerticalSide2(entry.entry)) {
2367
+ const target = spanOf2(edge.to, entry.entry, false);
2368
+ const chosen = (fromMiddle >= target.low && fromMiddle <= target.high ? fromMiddle : clampTo(edge.from, entry.exit, toMiddle, false)) + offset;
2369
+ exitWanted = clampTo(edge.from, entry.exit, chosen, corner);
2370
+ entryWanted = clampTo(edge.to, entry.entry, chosen, corner);
2371
+ } else {
2372
+ exitWanted = hasDiscretePorts(shapeOf(edge.from)) ? fromMiddle : clampTo(edge.from, entry.exit, stubAlong(edge.to, entry.entry, entry.exit), false) + offset;
2373
+ entryWanted = hasDiscretePorts(shapeOf(edge.to)) ? toMiddle : clampTo(edge.to, entry.entry, stubAlong(edge.from, entry.exit, entry.entry), false) + offset;
2374
+ }
2375
+ wanted.set(slotKey(edge.from, entry.exit, index), exitWanted);
2376
+ wanted.set(slotKey(edge.to, entry.entry, index), entryWanted);
2377
+ }
2378
+ const sides = /* @__PURE__ */ new Map();
2379
+ for (const [index, entry] of facing.entries()) {
2380
+ if (entry === void 0) continue;
2381
+ const edge = diagram.edges[index];
2382
+ sides.set(edge.from + ":" + entry.exit, [
2383
+ ...sides.get(edge.from + ":" + entry.exit) ?? [],
2384
+ { index, wanted: wanted.get(slotKey(edge.from, entry.exit, index)) }
2385
+ ]);
2386
+ sides.set(edge.to + ":" + entry.entry, [
2387
+ ...sides.get(edge.to + ":" + entry.entry) ?? [],
2388
+ { index, wanted: wanted.get(slotKey(edge.to, entry.entry, index)) }
2389
+ ]);
2390
+ }
2391
+ let placedPort = /* @__PURE__ */ new Map();
2392
+ for (let round = 0; round < 3; round += 1) {
2393
+ placedPort = /* @__PURE__ */ new Map();
2394
+ for (const [key, list] of sides) {
2395
+ const [boxId, side] = key.split(":");
2396
+ const span = spanOf2(boxId, side, true);
2397
+ const order2 = [...list].sort((left, right) => left.wanted - right.wanted || left.index - right.index);
2398
+ const packed = packPorts(
2399
+ order2.map((entry) => clampTo(boxId, side, entry.wanted, true)),
2400
+ span.low,
2401
+ span.high
2402
+ );
2403
+ order2.forEach((entry, at) => placedPort.set(slotKey(boxId, side, entry.index), packed[at]));
2404
+ }
2405
+ let moved = false;
2406
+ for (const [index, entry] of facing.entries()) {
2407
+ if (entry === void 0) continue;
2408
+ const edge = diagram.edges[index];
2409
+ const vertical = entry.exit === "bottom" || entry.exit === "top";
2410
+ if (!vertical) continue;
2411
+ const exit = placedPort.get(slotKey(edge.from, entry.exit, index));
2412
+ if (Math.abs(exit - placedPort.get(slotKey(edge.to, entry.entry, index))) < 0.01) continue;
2413
+ if (Math.abs(clampTo(edge.to, entry.entry, exit, true) - exit) > 0.01) continue;
2414
+ for (const slot of sides.get(edge.to + ":" + entry.entry)) {
2415
+ if (slot.index !== index) continue;
2416
+ slot.wanted = exit;
2417
+ moved = true;
2418
+ }
2419
+ }
2420
+ if (!moved) break;
2421
+ }
2422
+ const trunkEnds = [];
2423
+ diagram.edges.forEach((edge, index) => {
2424
+ const entry = facing[index];
2425
+ if (entry === void 0) return;
2426
+ const runY = placement.runs.get(index);
2427
+ trunkEnds.push({
2428
+ index,
2429
+ from: edge.from,
2430
+ to: edge.to,
2431
+ exit: entry.exit,
2432
+ entry: entry.entry,
2433
+ ...edge.arrow === void 0 ? {} : { arrow: edge.arrow },
2434
+ ...runY === void 0 ? {} : { runY }
2435
+ });
2436
+ });
2437
+ const planFor = (exitPortOf) => planTrunks({
2438
+ ends: trunkEnds,
2439
+ // Only a node's own box is a trunk target: an arrow into a boundary points past it, not at it.
2440
+ boxOf: (id) => boxes.get(id),
2441
+ boxes: boxList,
2442
+ exitPortOf
2443
+ });
2444
+ const packedExit = (index) => {
2445
+ const entry = facing[index];
2446
+ if (entry === void 0) return void 0;
2447
+ const edge = diagram.edges[index];
2448
+ const coordinate = placedPort.get(slotKey(edge.from, entry.exit, index));
2449
+ return coordinate === void 0 ? void 0 : pointOn(edge.from, entry.exit, coordinate);
2450
+ };
2451
+ const planned = planFor(packedExit);
2452
+ const onLane = (index, coordinate) => {
2453
+ if (planned.groupOf.has(index)) return false;
2454
+ const lane = placement.lanes.get(index);
2455
+ if (lane === void 0 || lane.points.length === 0) return false;
2456
+ return Math.abs(lane.points[0].x - coordinate) <= 0.5;
2457
+ };
2458
+ const rows = [];
2459
+ const rowOf = /* @__PURE__ */ new Map();
2460
+ for (const [key, list] of sides) {
2461
+ const [boxId, side] = key.split(":");
2462
+ const span = spanOf2(boxId, side, true);
2463
+ const slots = [...list].map((entry) => ({ index: entry.index, at: placedPort.get(slotKey(boxId, side, entry.index)) })).sort((left, right) => left.at - right.at || left.index - right.index);
2464
+ const centred = centreRun(
2465
+ slots.map((slot) => slot.at),
2466
+ span.low,
2467
+ span.high
2468
+ );
2469
+ const first = slots[0];
2470
+ const row = {
2471
+ boxId,
2472
+ side,
2473
+ indexes: slots.map((slot) => slot.index),
2474
+ at: slots.map((slot) => slot.at),
2475
+ position: new Map(slots.map((slot, at) => [slot.index, at])),
2476
+ shift: centred[0] - first.at,
2477
+ allowed: true
2478
+ };
2479
+ rows.push(row);
2480
+ rowOf.set(key, row);
2481
+ }
2482
+ const movedAt = (row, at) => row.at[at] + (row.allowed ? row.shift : 0);
2483
+ const partnerOf = (boxId, side, index) => {
2484
+ const entry = facing[index];
2485
+ if (entry === void 0) return void 0;
2486
+ const edge = diagram.edges[index];
2487
+ const isExit = edge.from === boxId && entry.exit === side;
2488
+ const groupId = planned.groupOf.get(index);
2489
+ if (groupId !== void 0) {
2490
+ if (!isExit) return void 0;
2491
+ const group = planned.groups[groupId];
2492
+ if (isVerticalSide2(side) !== isVerticalSide2(group.side)) return void 0;
2493
+ return { coordinate: group.entryAlong };
2494
+ }
2495
+ const partnerId = isExit ? edge.to : edge.from;
2496
+ const partnerSide = isExit ? entry.entry : entry.exit;
2497
+ if (isVerticalSide2(side) !== isVerticalSide2(partnerSide)) return void 0;
2498
+ const coordinate = placedPort.get(slotKey(partnerId, partnerSide, index));
2499
+ if (coordinate === void 0) return void 0;
2500
+ const row = rowOf.get(partnerId + ":" + partnerSide);
2501
+ return { coordinate, ...row === void 0 ? {} : { row, at: row.position.get(index) } };
2502
+ };
2503
+ for (let pass = 0; pass < rows.length; pass += 1) {
2504
+ let refused = false;
2505
+ for (const row of rows) {
2506
+ if (!row.allowed || row.shift === 0) continue;
2507
+ const pinned = row.indexes.some((index, at) => {
2508
+ if (onLane(index, row.at[at])) return true;
2509
+ const partner = partnerOf(row.boxId, row.side, index);
2510
+ if (partner === void 0) return false;
2511
+ if (Math.abs(row.at[at] - partner.coordinate) > 0.5) return false;
2512
+ const partnerAt = partner.row === void 0 ? partner.coordinate : movedAt(partner.row, partner.at);
2513
+ return Math.abs(movedAt(row, at) - partnerAt) > 0.5;
2514
+ });
2515
+ if (!pinned) continue;
2516
+ row.allowed = false;
2517
+ refused = true;
2518
+ }
2519
+ if (!refused) break;
2520
+ }
2521
+ for (const row of rows) {
2522
+ if (!row.allowed || row.shift === 0) continue;
2523
+ row.indexes.forEach((index, at) => placedPort.set(slotKey(row.boxId, row.side, index), movedAt(row, at)));
2524
+ }
2525
+ const portOf = (boxId, side, edgeIndex) => {
2526
+ const coordinate = placedPort.get(slotKey(boxId, side, edgeIndex));
2527
+ return pointOn(boxId, side, coordinate);
2528
+ };
2529
+ const anchorsFor = /* @__PURE__ */ new Map();
2530
+ for (const [index, entry] of facing.entries()) {
2531
+ if (entry === void 0) continue;
2532
+ const edge = diagram.edges[index];
2533
+ anchorsFor.set(index, {
2534
+ exit: portOf(edge.from, entry.exit, index),
2535
+ entry: portOf(edge.to, entry.entry, index),
2536
+ exitSide: entry.exit,
2537
+ entrySide: entry.entry
2538
+ });
2539
+ }
2540
+ const trunks = planFor((index) => anchorsFor.get(index)?.exit);
2541
+ const sharedEntry = /* @__PURE__ */ new Map();
2542
+ for (const group of trunks.groups) sharedEntry.set(group.id, pointOn(group.target, group.side, group.entryAlong));
2543
+ const pointsOfRoute = (shape) => shape.kind === "path" ? shape.points : [{ x: shape.x1, y: shape.y1 }, { x: shape.x2, y: shape.y2 }];
2544
+ const segmentsOfRoute = (shape) => {
2545
+ const points = pointsOfRoute(shape);
2546
+ const out = [];
2547
+ for (let index = 0; index + 1 < points.length; index += 1) {
2548
+ const from = points[index];
2549
+ const to = points[index + 1];
2550
+ out.push({ x1: from.x, y1: from.y, x2: to.x, y2: to.y });
2551
+ }
2552
+ return out;
2553
+ };
2554
+ const paintPath = (edgeIndex, edge, points, colour) => {
2555
+ const shape = {
2556
+ kind: "path",
2557
+ points,
2558
+ label: edge.label,
2559
+ labelX: 0,
2560
+ labelY: 0,
2561
+ colour,
2562
+ ...edge.dashed === void 0 ? {} : { dashed: edge.dashed },
2563
+ ...edge.arrow === void 0 ? {} : { arrow: edge.arrow }
2564
+ };
2565
+ shapes.push(shape);
2566
+ routes.push({ shape, edge: edgeIndex, fixed: false });
2567
+ };
2568
+ const paintRoute = (edgeIndex, edge, points, colour) => {
2569
+ if (points.length > 2) {
2570
+ paintPath(edgeIndex, edge, points, colour);
2571
+ return;
2572
+ }
2573
+ const exit = points[0];
2574
+ const entry = points[points.length - 1];
2575
+ const shape = {
2576
+ kind: "link",
2577
+ x1: exit.x,
2578
+ y1: exit.y,
2579
+ x2: entry.x,
2580
+ y2: entry.y,
2581
+ label: edge.label,
2582
+ labelX: 0,
2583
+ labelY: 0,
2584
+ colour,
2585
+ ...edge.dashed === void 0 ? {} : { dashed: edge.dashed },
2586
+ ...edge.arrow === void 0 ? {} : { arrow: edge.arrow }
2587
+ };
2588
+ shapes.push(shape);
2589
+ routes.push({ shape, edge: edgeIndex, fixed: false });
2590
+ };
2591
+ const clearOfBoxes = (points, from, to) => boxesCrossedBy(points, from, to, boxList).length === 0;
2592
+ const runRoute = (ports, runY) => {
2593
+ const out = isVerticalSide2(ports.exitSide) ? 0 : normalOf(ports.exitSide).x * ROUTE_STUB;
2594
+ const into = isVerticalSide2(ports.entrySide) ? 0 : normalOf(ports.entrySide).x * ROUTE_STUB;
2595
+ const exitRun = out !== 0 && into === 0 && Math.sign(ports.entry.x - ports.exit.x) === Math.sign(out) ? ports.entry.x : ports.exit.x + out;
2596
+ const entryRun = into !== 0 && out === 0 && Math.sign(ports.exit.x - ports.entry.x) === Math.sign(into) ? ports.exit.x : ports.entry.x + into;
2597
+ const points = [ports.exit];
2598
+ if (out !== 0) points.push({ x: exitRun, y: ports.exit.y });
2599
+ points.push({ x: exitRun, y: runY }, { x: entryRun, y: runY });
2600
+ if (into !== 0) points.push({ x: entryRun, y: ports.entry.y });
2601
+ points.push(ports.entry);
2602
+ return simplify(points);
2603
+ };
2604
+ const runFits = (ports, runY) => {
2605
+ if (isVerticalSide2(ports.exitSide) && Math.abs(runY - ports.exit.y) > 0.5) {
2606
+ if (Math.sign(runY - ports.exit.y) !== Math.sign(normalOf(ports.exitSide).y)) return false;
2607
+ }
2608
+ if (isVerticalSide2(ports.entrySide) && Math.abs(ports.entry.y - runY) > 0.5) {
2609
+ if (Math.sign(ports.entry.y - runY) !== -Math.sign(normalOf(ports.entrySide).y)) return false;
2610
+ }
2611
+ return true;
2612
+ };
2613
+ const junctions = [];
2614
+ for (const [edgeIndex, edge] of diagram.edges.entries()) {
2615
+ const source = endBox(edge.from);
2616
+ const target = endBox(edge.to);
400
2617
  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;
2618
+ const colour = roleById.get(edge.from) ?? "muted";
403
2619
  if (edge.from === edge.to) {
404
2620
  const x = source.x + source.width;
405
2621
  const y = source.y + source.height / 2;
406
- shapes.push({
2622
+ const shape = {
407
2623
  kind: "path",
408
2624
  points: [
409
2625
  { x, y: y - 9 },
@@ -412,57 +2628,116 @@ window.__ModuleLoader__.load({
412
2628
  { x, y: y + 9 }
413
2629
  ],
414
2630
  label: edge.label,
415
- labelX: x + 20,
2631
+ // The one label that is not on its own edge: a self loop's longest run is eighteen points, and its name
2632
+ // does not fit on that. It sits beside the loop, where it belongs to the edge and touches nothing.
2633
+ labelX: x + 20 + textWidth(edge.label) / 2,
416
2634
  labelY: y,
417
- colour
418
- });
2635
+ colour,
2636
+ ...edge.dashed === void 0 ? {} : { dashed: edge.dashed },
2637
+ ...edge.arrow === void 0 ? {} : { arrow: edge.arrow }
2638
+ };
2639
+ shapes.push(shape);
2640
+ routes.push({ shape, edge: edgeIndex, fixed: true });
419
2641
  continue;
420
2642
  }
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 });
427
- continue;
2643
+ const anchors = anchorsFor.get(edgeIndex);
2644
+ const groupId = trunks.groupOf.get(edgeIndex);
2645
+ if (groupId !== void 0 && anchors !== void 0) {
2646
+ const group = trunks.groups[groupId];
2647
+ const entry = sharedEntry.get(groupId);
2648
+ const branch = isVerticalSide2(group.side) ? [anchors.exit, { x: anchors.exit.x, y: group.at }, { x: entry.x, y: group.at }, entry] : [anchors.exit, { x: group.at, y: anchors.exit.y }, { x: group.at, y: entry.y }, entry];
2649
+ const points = simplify(branch);
2650
+ if (clearOfBoxes(points, source, target)) {
2651
+ paintRoute(edgeIndex, edge, points, colour);
2652
+ const meet = isVerticalSide2(group.side) ? { x: anchors.exit.x, y: group.at } : { x: group.at, y: anchors.exit.y };
2653
+ junctions.push({ kind: "junction", x: meet.x, y: meet.y, colour, trunk: group.id });
2654
+ continue;
2655
+ }
428
2656
  }
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;
2657
+ const lane = placement.lanes.get(edgeIndex);
2658
+ if (lane !== void 0 && lane.points.length > 0 && anchors !== void 0) {
2659
+ const shift = laneShift.get(edgeIndex) ?? 0;
2660
+ const ordered = lane.reversed ? [...lane.points].reverse() : lane.points;
2661
+ const waypoints = shift === 0 ? ordered : ordered.map((point) => ({ x: point.x + shift, y: point.y }));
2662
+ const points = [anchors.exit];
2663
+ let previous = anchors.exit;
2664
+ for (const waypoint of waypoints) {
2665
+ 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 });
2666
+ points.push(waypoint);
2667
+ previous = waypoint;
2668
+ }
2669
+ 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 });
2670
+ points.push(anchors.entry);
2671
+ const simplified = simplify(points);
2672
+ if (clearOfBoxes(simplified, source, target)) {
2673
+ paintPath(edgeIndex, edge, simplified, colour);
2674
+ continue;
2675
+ }
446
2676
  }
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
- });
2677
+ const runY = placement.runs.get(edgeIndex);
2678
+ if (runY !== void 0 && anchors !== void 0 && runFits(anchors, runY)) {
2679
+ const points = runRoute(anchors, runY);
2680
+ if (clearOfBoxes(points, source, target)) {
2681
+ paintRoute(edgeIndex, edge, points, colour);
2682
+ continue;
2683
+ }
2684
+ }
2685
+ paintRoute(edgeIndex, edge, routeEdge(source, target, boxList, anchors).points, colour);
465
2686
  }
2687
+ for (const dot of junctions) shapes.push(dot);
2688
+ const boxEntries = Array.from(boxes.entries());
2689
+ const routingFailures = [];
2690
+ for (const { shape, edge } of routes) {
2691
+ const diagramEdge = diagram.edges[edge];
2692
+ const source = endBox(diagramEdge.from);
2693
+ const target = endBox(diagramEdge.to);
2694
+ if (source === void 0 || target === void 0) continue;
2695
+ for (const segment of segmentsOfRoute(shape)) {
2696
+ for (const [id, box] of boxEntries) {
2697
+ if (box === source || box === target) continue;
2698
+ if (!crossesBox(segment.x1, segment.y1, segment.x2, segment.y2, box)) continue;
2699
+ routingFailures.push({
2700
+ edge,
2701
+ from: diagramEdge.from,
2702
+ to: diagramEdge.to,
2703
+ box: id,
2704
+ x1: segment.x1,
2705
+ y1: segment.y1,
2706
+ x2: segment.x2,
2707
+ y2: segment.y2
2708
+ });
2709
+ }
2710
+ }
2711
+ }
2712
+ const routeSegments = routes.map(({ shape }) => segmentsOfRoute(shape));
2713
+ const placedLabels = [];
2714
+ routes.forEach(({ shape, fixed }, at) => {
2715
+ if (fixed) {
2716
+ if (shape.label !== "") {
2717
+ placedLabels.push({ x: shape.labelX, y: shape.labelY, width: textWidth(shape.label), height: LABEL_HEIGHT });
2718
+ }
2719
+ return;
2720
+ }
2721
+ if (shape.label === "") return;
2722
+ const placement2 = placeLabel(
2723
+ shape.label,
2724
+ pointsOfRoute(shape),
2725
+ boxList,
2726
+ placedLabels,
2727
+ routeSegments.filter((_, other) => other !== at)
2728
+ );
2729
+ shape.labelX = placement2.x;
2730
+ shape.labelY = placement2.y;
2731
+ shape.labelPlacement = {
2732
+ run: placement2.run,
2733
+ collisions: placement2.collisions,
2734
+ boxes: placement2.boxes,
2735
+ labels: placement2.labels,
2736
+ edges: placement2.edges
2737
+ };
2738
+ placedLabels.push({ x: placement2.x, y: placement2.y, width: placement2.width, height: placement2.height });
2739
+ });
2740
+ const labelCollisions = routes.reduce((total, { shape }) => total + ((shape.labelPlacement?.collisions ?? 0) > 0 ? 1 : 0), 0);
466
2741
  shapes.push({ kind: "title", x: MARGIN, y: MARGIN + 12, text: diagram.title, colour: "text" });
467
2742
  const contentRight = Math.max(
468
2743
  MARGIN,
@@ -471,7 +2746,7 @@ window.__ModuleLoader__.load({
471
2746
  );
472
2747
  const captionLines = wrap(diagram.caption, CAPTION_CHARS);
473
2748
  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);
2749
+ const width = Math.max(contentRight + MARGIN, captionWidth, 240);
475
2750
  const contentBottom = Math.max(
476
2751
  topOffset,
477
2752
  ...Array.from(boxes.values(), (box) => box.y + box.height),
@@ -480,7 +2755,13 @@ window.__ModuleLoader__.load({
480
2755
  const captionTop = contentBottom + CAPTION_GAP;
481
2756
  shapes.push({ kind: "caption", x: MARGIN, y: captionTop, lines: captionLines, colour: captionRole(diagram.caption) });
482
2757
  const height = captionTop + captionLines.length * CAPTION_LINE + MARGIN / 2;
483
- return { canvas: { width: Math.round(width), height: Math.round(height), shapes }, colours };
2758
+ const extent = drawnExtent(shapes);
2759
+ const canvasWidth = Math.max(width, extent.width + MARGIN);
2760
+ const canvasHeight = Math.max(height, extent.height + MARGIN);
2761
+ return {
2762
+ canvas: { width: Math.round(canvasWidth), height: Math.round(canvasHeight), shapes, routingFailures, labelCollisions },
2763
+ colours
2764
+ };
484
2765
  }
485
2766
 
486
2767
  // src/render/from-mermaid.ts
@@ -505,10 +2786,15 @@ window.__ModuleLoader__.load({
505
2786
  "requirementdiagram",
506
2787
  "sankey-beta"
507
2788
  ]);
508
- function ensure(builder, id, label) {
2789
+ function ensure(builder, id, label, shape) {
509
2790
  const existing = builder.nodes.get(id);
510
2791
  if (existing === void 0) {
511
- builder.nodes.set(id, { id, role: "normal", ...label === void 0 ? {} : { label } });
2792
+ builder.nodes.set(id, {
2793
+ id,
2794
+ role: "normal",
2795
+ ...label === void 0 ? {} : { label },
2796
+ ...shape === void 0 ? {} : { shape }
2797
+ });
512
2798
  const innermost = builder.open[builder.open.length - 1];
513
2799
  if (innermost !== void 0 && !innermost.members.includes(id)) innermost.members.push(id);
514
2800
  return;
@@ -520,7 +2806,12 @@ window.__ModuleLoader__.load({
520
2806
  if (match === null) return void 0;
521
2807
  const id = match[1];
522
2808
  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, "") } };
2809
+ 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;
2810
+ return {
2811
+ id,
2812
+ ...label === void 0 ? {} : { label: label.trim().replace(/^["']|["']$/g, "") },
2813
+ ...shape === void 0 ? {} : { shape }
2814
+ };
524
2815
  }
525
2816
  function fromMermaid(source, title = "mermaid") {
526
2817
  const lines = source.split(/\r?\n/);
@@ -537,13 +2828,13 @@ window.__ModuleLoader__.load({
537
2828
  if (line === "") return;
538
2829
  const where = "line " + String(index + 1) + ": ";
539
2830
  const lower = line.toLowerCase();
540
- const keyword = lower.split(/[\s{}]/)[0] ?? "";
2831
+ const keyword = lower.split(/[\s{}]/)[0];
541
2832
  if (UNSUPPORTED.has(keyword)) {
542
2833
  refused.push(where + "`" + keyword + "` is not a diagram this renderer draws");
543
2834
  return;
544
2835
  }
545
2836
  if (DIRECTIVE.test(line)) {
546
- refused.push(where + "`" + (line.split(/\s/)[0] ?? "") + "` is a directive this renderer ignores");
2837
+ refused.push(where + "`" + line.split(/\s/)[0] + "` is a directive this renderer ignores");
547
2838
  return;
548
2839
  }
549
2840
  if (/^(graph|flowchart)\b/.test(lower)) {
@@ -615,8 +2906,8 @@ window.__ModuleLoader__.load({
615
2906
  refused.push(where + "`" + line + "` does not name two nodes");
616
2907
  return;
617
2908
  }
618
- ensure(builder, left.id, left.label);
619
- ensure(builder, right.id, right.label);
2909
+ ensure(builder, left.id, left.label, left.shape);
2910
+ ensure(builder, right.id, right.label, right.shape);
620
2911
  push({ from: left.id, to: right.id, label: labelled[2].trim() });
621
2912
  return;
622
2913
  }
@@ -628,8 +2919,8 @@ window.__ModuleLoader__.load({
628
2919
  refused.push(where + "`" + line + "` does not name two nodes");
629
2920
  return;
630
2921
  }
631
- ensure(builder, left.id, left.label);
632
- ensure(builder, right.id, right.label);
2922
+ ensure(builder, left.id, left.label, left.shape);
2923
+ ensure(builder, right.id, right.label, right.shape);
633
2924
  push({
634
2925
  from: left.id,
635
2926
  to: right.id,
@@ -640,7 +2931,7 @@ window.__ModuleLoader__.load({
640
2931
  }
641
2932
  const single = nodeFrom(line);
642
2933
  if (single !== void 0) {
643
- ensure(builder, single.id, single.label);
2934
+ ensure(builder, single.id, single.label, single.shape);
644
2935
  return;
645
2936
  }
646
2937
  refused.push(where + "`" + line + "` is not something this renderer draws");
@@ -664,17 +2955,15 @@ window.__ModuleLoader__.load({
664
2955
  }
665
2956
 
666
2957
  // src/render/collapse.ts
667
- function subtreeSize(diagram, groupId, seen) {
2958
+ function subtreeSize(groups, groupId, seen) {
668
2959
  if (seen.has(groupId)) return 0;
669
2960
  seen.add(groupId);
670
- const groups = diagram.groups ?? [];
671
2961
  const group = groups.find((entry) => entry.id === groupId);
672
- if (group === void 0) return 0;
673
2962
  const children = groups.filter((entry) => entry.parent === groupId);
674
- return group.members.length + children.reduce((total, child) => total + subtreeSize(diagram, child.id, seen), 0);
2963
+ return group.members.length + children.reduce((total, child) => total + subtreeSize(groups, child.id, seen), 0);
675
2964
  }
676
- function ownerOf(diagram, nodeId) {
677
- return (diagram.groups ?? []).find((group) => group.members.includes(nodeId))?.id;
2965
+ function ownerOf(groups, nodeId) {
2966
+ return groups.find((group) => group.members.includes(nodeId))?.id;
678
2967
  }
679
2968
  function collapse(diagram, folded) {
680
2969
  const asked = new Set(folded);
@@ -684,7 +2973,7 @@ window.__ModuleLoader__.load({
684
2973
  const targets = /* @__PURE__ */ new Set();
685
2974
  for (const id of asked) {
686
2975
  const group = byId.get(id);
687
- if (group !== void 0 && subtreeSize(diagram, id, /* @__PURE__ */ new Set()) > 0) targets.add(id);
2976
+ if (group !== void 0 && subtreeSize(groups, id, /* @__PURE__ */ new Set()) > 0) targets.add(id);
688
2977
  }
689
2978
  if (targets.size === 0) return diagram;
690
2979
  for (const id of [...targets]) {
@@ -701,16 +2990,13 @@ window.__ModuleLoader__.load({
701
2990
  }
702
2991
  const removedNodes = /* @__PURE__ */ new Set();
703
2992
  const removedGroups = /* @__PURE__ */ new Set();
704
- const descend = (groupId, seen) => {
705
- if (seen.has(groupId)) return;
706
- seen.add(groupId);
2993
+ const descend = (groupId) => {
707
2994
  removedGroups.add(groupId);
708
2995
  const group = byId.get(groupId);
709
- if (group === void 0) return;
710
2996
  for (const member of group.members) removedNodes.add(member);
711
- for (const child of groups.filter((entry) => entry.parent === groupId)) descend(child.id, seen);
2997
+ for (const child of groups.filter((entry) => entry.parent === groupId)) descend(child.id);
712
2998
  };
713
- for (const id of targets) descend(id, /* @__PURE__ */ new Set());
2999
+ for (const id of targets) descend(id);
714
3000
  const foldOf = (id) => {
715
3001
  for (const target of targets) {
716
3002
  const inSubtree = (groupId, seen) => {
@@ -720,7 +3006,7 @@ window.__ModuleLoader__.load({
720
3006
  return groups.filter((entry) => entry.parent === groupId).some((child) => inSubtree(child.id, seen));
721
3007
  };
722
3008
  const group = byId.get(target);
723
- if (group.members.includes(id) || inSubtree(ownerOf(diagram, id) ?? "", /* @__PURE__ */ new Set())) return target;
3009
+ if (group.members.includes(id) || inSubtree(ownerOf(groups, id), /* @__PURE__ */ new Set())) return target;
724
3010
  }
725
3011
  return void 0;
726
3012
  };
@@ -735,12 +3021,12 @@ window.__ModuleLoader__.load({
735
3021
  if (fold === void 0 || emitted.has(fold)) continue;
736
3022
  emitted.add(fold);
737
3023
  const group = byId.get(fold);
738
- nodes.push({ id: fold, label: group.label + " (" + String(subtreeSize(diagram, fold, /* @__PURE__ */ new Set())) + ")", role: "normal" });
3024
+ nodes.push({ id: fold, label: group.label + " (" + String(subtreeSize(groups, fold, /* @__PURE__ */ new Set())) + ")", role: "normal" });
739
3025
  }
740
3026
  for (const id of targets) {
741
3027
  if (emitted.has(id)) continue;
742
3028
  const group = byId.get(id);
743
- nodes.push({ id, label: group.label + " (" + String(subtreeSize(diagram, id, /* @__PURE__ */ new Set())) + ")", role: "normal" });
3029
+ nodes.push({ id, label: group.label + " (" + String(subtreeSize(groups, id, /* @__PURE__ */ new Set())) + ")", role: "normal" });
744
3030
  }
745
3031
  const edges = [];
746
3032
  const seenEdges = /* @__PURE__ */ new Set();
@@ -757,7 +3043,7 @@ window.__ModuleLoader__.load({
757
3043
  const adopted = /* @__PURE__ */ new Map();
758
3044
  for (const id of targets) {
759
3045
  const parent = byId.get(id)?.parent;
760
- if (parent === void 0 || targets.has(parent) || removedGroups.has(parent)) continue;
3046
+ if (parent === void 0) continue;
761
3047
  adopted.set(parent, [...adopted.get(parent) ?? [], id]);
762
3048
  }
763
3049
  const remaining = groups.filter((group) => !removedGroups.has(group.id) && targets.has(group.id) === false).map((group) => {
@@ -776,38 +3062,139 @@ window.__ModuleLoader__.load({
776
3062
  }
777
3063
 
778
3064
  // 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+(.+)$/;
3065
+ var SHAPES = {
3066
+ component: "rect",
3067
+ node: "rect",
3068
+ rectangle: "rect",
3069
+ artifact: "rect",
3070
+ state: "rect",
3071
+ database: "cylinder",
3072
+ // PlantUML draws an entity as a cylinder in a component diagram; `entity` in an ER diagram is a different
3073
+ // symbol, and that diagram family is not one this renderer accepts.
3074
+ entity: "cylinder",
3075
+ // A use case is an ellipse. The `circle` symbol is written as an ellipse whose radii follow the box, so it
3076
+ // reads as the familiar use-case oval; `stadium` is the pill a terminal state is drawn as.
3077
+ usecase: "circle"
3078
+ };
3079
+ var BOUNDARY = /* @__PURE__ */ new Set(["package", "folder", "node", "component", "rectangle", "artifact"]);
3080
+ var NOT_DRAWN = {
3081
+ actor: "an actor is a stick figure, not one of the shapes this renderer draws",
3082
+ participant: "a participant is a lifeline, not one of the shapes this renderer draws",
3083
+ class: "a class box has compartments this renderer does not draw",
3084
+ object: "an object box has compartments this renderer does not draw",
3085
+ interface: "an interface is a circle or a boxed stereotype, neither of which this renderer draws",
3086
+ enum: "an enum box has compartments this renderer does not draw",
3087
+ annotation: "an annotation is not one of the shapes this renderer draws",
3088
+ cloud: "a cloud outline is not one of the shapes this renderer draws",
3089
+ frame: "a frame tab is not one of the shapes this renderer draws",
3090
+ card: "a card is not one of the shapes this renderer draws",
3091
+ file: "a file outline is not one of the shapes this renderer draws",
3092
+ agent: "an agent is not one of the shapes this renderer draws",
3093
+ boundary: "the boundary symbol is not one of the shapes this renderer draws",
3094
+ control: "the control symbol is not one of the shapes this renderer draws",
3095
+ collections: "a collections symbol is not one of the shapes this renderer draws",
3096
+ queue: "a queue is not one of the shapes this renderer draws",
3097
+ stack: "a stack is not one of the shapes this renderer draws",
3098
+ storage: "a storage symbol is not one of the shapes this renderer draws",
3099
+ note: "a note is not one of the shapes this renderer draws",
3100
+ legend: "a legend is not one of the shapes this renderer draws"
3101
+ };
3102
+ var FRAGMENT = /* @__PURE__ */ new Set(["sequence", "alt", "opt", "loop", "par", "group", "ref", "break", "critical", "activate", "deactivate", "destroy", "create", "newpage", "autonumber"]);
3103
+ var ARROW = /^(.+?)\s*(<\|?|[*o])?(-{2,}|\.{2,})(\|?>|[*o])?\s*(.+?)(?:\s*:\s*(.+))?$/;
3104
+ var SHAPE_KEYWORDS = Object.keys(SHAPES);
3105
+ var DECLARED = new RegExp("^(" + SHAPE_KEYWORDS.join("|") + ")\\s+(.+)$");
781
3106
  var QUOTED_ALIAS = /^["'](.+?)["']\s+as\s+([A-Za-z_][\w-]*)$/;
782
3107
  var BRACKET_ALIAS = /^\[(.+?)\]\s+as\s+([A-Za-z_][\w-]*)$/;
3108
+ var PLAIN_ALIAS = /^([A-Za-z_][\w.-]*)\s+as\s+([A-Za-z_][\w-]*)$/;
783
3109
  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) {
3110
+ var IGNORED = /^(skinparam|!theme|!define|!include|!pragma|hide|show|scale|left to right direction|top to bottom direction|together|allowmixing)\b/;
3111
+ function ensure2(builder, id, label, shape, implicit = false) {
787
3112
  const existing = builder.nodes.get(id);
788
3113
  if (existing === void 0) {
789
- builder.nodes.set(id, { id, role: "normal", ...label === void 0 ? {} : { label } });
3114
+ builder.nodes.set(id, {
3115
+ id,
3116
+ role: "normal",
3117
+ ...label === void 0 ? {} : { label },
3118
+ ...shape === void 0 ? {} : { shape }
3119
+ });
3120
+ if (implicit) builder.implicit.add(id);
790
3121
  const innermost = builder.open[builder.open.length - 1];
791
3122
  if (innermost !== void 0 && !innermost.members.includes(id)) innermost.members.push(id);
792
3123
  return;
793
3124
  }
3125
+ if (!implicit) builder.implicit.delete(id);
794
3126
  if (label !== void 0 && (existing.label === void 0 || existing.label === "")) existing.label = label;
3127
+ if (shape !== void 0 && existing.shape === void 0) existing.shape = shape;
795
3128
  }
796
3129
  function reference(token) {
797
3130
  const text2 = token.trim();
798
3131
  const bracket = BRACKET.exec(text2);
799
3132
  if (bracket !== null) {
800
3133
  const label = bracket[1].trim();
801
- return { id: label, label };
3134
+ return { id: label, label, shape: "rect" };
802
3135
  }
803
3136
  const quoted = /^["'](.+?)["']$/.exec(text2);
804
3137
  if (quoted !== null) return { id: quoted[1], label: quoted[1] };
805
3138
  return { id: text2 };
806
3139
  }
3140
+ function keywordOf(line) {
3141
+ return line.split(/[\s{]/)[0].toLowerCase();
3142
+ }
3143
+ function boundaryOf(declaration, keyword, ordinal) {
3144
+ const rest = declaration.slice(keyword.length).trim();
3145
+ const alias = QUOTED_ALIAS.exec(rest) ?? BRACKET_ALIAS.exec(rest) ?? PLAIN_ALIAS.exec(rest);
3146
+ if (alias !== null) return { id: alias[2], label: alias[1] };
3147
+ const label = rest.replace(/^["']|["']$/g, "");
3148
+ return { id: "group-" + String(ordinal), label: label === "" ? keyword : label };
3149
+ }
3150
+ function readArrow(line) {
3151
+ const match = ARROW.exec(line);
3152
+ if (match === null) return void 0;
3153
+ const left = reference(match[1]);
3154
+ const right = reference(match[5]);
3155
+ const leftMark = match[2];
3156
+ const rightMark = match[4];
3157
+ const dotted = match[3].startsWith(".");
3158
+ const label = (match[6] ?? "").trim();
3159
+ if (leftMark !== void 0 && rightMark !== void 0) {
3160
+ return "an edge marked at both ends is not one this renderer draws";
3161
+ }
3162
+ if (leftMark === "*" || leftMark === "o" || rightMark === "*" || rightMark === "o") {
3163
+ if (dotted) return "a dotted composition or aggregation is not one this renderer draws";
3164
+ const atRight = rightMark === "*" || rightMark === "o";
3165
+ const mark = atRight ? rightMark : leftMark;
3166
+ return {
3167
+ from: atRight ? right : left,
3168
+ to: atRight ? left : right,
3169
+ label,
3170
+ dashed: false,
3171
+ arrow: mark === "*" ? "composition" : "aggregation"
3172
+ };
3173
+ }
3174
+ if (leftMark !== void 0) {
3175
+ return {
3176
+ from: right,
3177
+ to: left,
3178
+ label,
3179
+ dashed: dotted,
3180
+ arrow: leftMark === "<|" ? dotted ? "realization" : "generalization" : dotted ? "dependency" : "association"
3181
+ };
3182
+ }
3183
+ if (rightMark !== void 0) {
3184
+ return {
3185
+ from: left,
3186
+ to: right,
3187
+ label,
3188
+ dashed: dotted,
3189
+ arrow: rightMark === "|>" ? dotted ? "realization" : "generalization" : dotted ? "dependency" : "association"
3190
+ };
3191
+ }
3192
+ return "a line with no arrow end is not one this renderer draws; name the end it points at";
3193
+ }
807
3194
  function fromPlantUml(source, title = "plantuml") {
808
3195
  const lines = source.split(/\r?\n/);
809
3196
  const refused = [];
810
- const builder = { nodes: /* @__PURE__ */ new Map(), edges: [], groups: [], open: [] };
3197
+ const builder = { nodes: /* @__PURE__ */ new Map(), edges: [], edgeLines: [], edgeScopes: [], groups: [], open: [], implicit: /* @__PURE__ */ new Set() };
811
3198
  let startId;
812
3199
  let seenAny = false;
813
3200
  let groupCount = 0;
@@ -819,8 +3206,14 @@ window.__ModuleLoader__.load({
819
3206
  seenAny = true;
820
3207
  if (/^title\b/.test(line)) return;
821
3208
  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");
3209
+ const keyword = keywordOf(line);
3210
+ const notDrawn = NOT_DRAWN[keyword];
3211
+ if (notDrawn !== void 0) {
3212
+ refused.push(where + "`" + keyword + "`: " + notDrawn);
3213
+ return;
3214
+ }
3215
+ if (FRAGMENT.has(keyword)) {
3216
+ refused.push(where + "`" + keyword + "`: a sequence statement is not part of a drawing this renderer makes");
824
3217
  return;
825
3218
  }
826
3219
  if (line === "}") {
@@ -834,56 +3227,176 @@ window.__ModuleLoader__.load({
834
3227
  }
835
3228
  if (line.endsWith("{")) {
836
3229
  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, "");
3230
+ const opener = keywordOf(declaration);
3231
+ if (!BOUNDARY.has(opener)) {
3232
+ const reason = SHAPES[opener] === void 0 ? "`" + declaration + "` is not a boundary this renderer draws" : "a `" + opener + "` block is not a boundary this renderer draws";
3233
+ refused.push(where + reason);
3234
+ return;
3235
+ }
839
3236
  groupCount += 1;
840
- builder.open.push({ id: "group-" + String(groupCount), label, members: [] });
3237
+ const boundary = boundaryOf(declaration, opener, groupCount);
3238
+ builder.open.push({ id: boundary.id, label: boundary.label, members: [] });
3239
+ return;
3240
+ }
3241
+ if (line === "[*] --> [*]") return;
3242
+ const startMarker = /^\[\*\]\s*-->\s*(.+)$/.exec(line);
3243
+ if (startMarker !== null) {
3244
+ const target = reference(startMarker[1]);
3245
+ ensure2(builder, target.id, target.label, target.shape);
3246
+ if (startId === void 0) startId = target.id;
841
3247
  return;
842
3248
  }
843
3249
  const declared = DECLARED.exec(line);
844
3250
  if (declared !== null) {
3251
+ const shape = SHAPES[declared[1]];
845
3252
  const rest = declared[2].trim();
846
3253
  const quoted = QUOTED_ALIAS.exec(rest);
847
3254
  if (quoted !== null) {
848
- ensure2(builder, quoted[2], quoted[1]);
3255
+ ensure2(builder, quoted[2], quoted[1], shape);
849
3256
  return;
850
3257
  }
851
3258
  const bracketed = BRACKET_ALIAS.exec(rest);
852
3259
  if (bracketed !== null) {
853
- ensure2(builder, bracketed[2], bracketed[1]);
3260
+ ensure2(builder, bracketed[2], bracketed[1], shape);
854
3261
  return;
855
3262
  }
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;
3263
+ const plain = PLAIN_ALIAS.exec(rest);
3264
+ if (plain !== null) {
3265
+ ensure2(builder, plain[2], plain[1], shape);
3266
+ return;
3267
+ }
3268
+ const written = reference(rest);
3269
+ ensure2(builder, written.id, written.label, shape);
867
3270
  return;
868
3271
  }
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 } : {} });
3272
+ const arrow = readArrow(line);
3273
+ if (arrow !== void 0) {
3274
+ if (typeof arrow === "string") {
3275
+ refused.push(where + arrow);
3276
+ return;
3277
+ }
3278
+ ensure2(builder, arrow.from.id, arrow.from.label, arrow.from.shape, true);
3279
+ ensure2(builder, arrow.to.id, arrow.to.label, arrow.to.shape, true);
3280
+ builder.edges.push({
3281
+ from: arrow.from.id,
3282
+ to: arrow.to.id,
3283
+ label: arrow.label,
3284
+ ...arrow.dashed ? { dashed: true } : {},
3285
+ arrow: arrow.arrow
3286
+ });
3287
+ builder.edgeLines.push(index + 1);
3288
+ builder.edgeScopes.push(builder.open[builder.open.length - 1]?.id);
878
3289
  return;
879
3290
  }
880
3291
  const single = BRACKET.exec(line);
881
3292
  if (single !== null) {
882
- ensure2(builder, single[1].trim(), single[1].trim());
3293
+ ensure2(builder, single[1].trim(), single[1].trim(), "rect");
883
3294
  return;
884
3295
  }
885
3296
  refused.push(where + "`" + line + "` is not something this renderer draws");
886
3297
  });
3298
+ const boundaryNames = /* @__PURE__ */ new Map();
3299
+ const disputedNames = /* @__PURE__ */ new Set();
3300
+ for (const group of builder.groups) {
3301
+ for (const name of [group.id, group.label]) {
3302
+ const existing = boundaryNames.get(name);
3303
+ if (existing !== void 0 && existing !== group.id) disputedNames.add(name);
3304
+ else boundaryNames.set(name, group.id);
3305
+ }
3306
+ }
3307
+ for (const name of disputedNames) boundaryNames.delete(name);
3308
+ const boundaryAt = /* @__PURE__ */ new Map();
3309
+ const ambiguousAt = /* @__PURE__ */ new Set();
3310
+ for (const id of builder.implicit) {
3311
+ if (disputedNames.has(id)) {
3312
+ ambiguousAt.add(id);
3313
+ builder.nodes.delete(id);
3314
+ } else {
3315
+ const groupId = boundaryNames.get(id);
3316
+ if (groupId === void 0) continue;
3317
+ boundaryAt.set(id, groupId);
3318
+ builder.nodes.delete(id);
3319
+ }
3320
+ for (const group of builder.groups) {
3321
+ const at = group.members.indexOf(id);
3322
+ if (at >= 0) group.members.splice(at, 1);
3323
+ }
3324
+ }
3325
+ const groupById = new Map(builder.groups.map((group) => [group.id, group]));
3326
+ const childrenOf = /* @__PURE__ */ new Map();
3327
+ for (const group of builder.groups) {
3328
+ if (group.parent === void 0) continue;
3329
+ childrenOf.set(group.parent, [...childrenOf.get(group.parent) ?? [], group.id]);
3330
+ }
3331
+ const drawable = /* @__PURE__ */ new Map();
3332
+ const hasContents = (id) => {
3333
+ const known = drawable.get(id);
3334
+ if (known !== void 0) return known;
3335
+ const group = groupById.get(id);
3336
+ let found = false;
3337
+ for (const member of group.members) {
3338
+ if (builder.nodes.has(member)) {
3339
+ found = true;
3340
+ break;
3341
+ }
3342
+ }
3343
+ if (!found) {
3344
+ for (const child of childrenOf.get(id) ?? []) {
3345
+ if (hasContents(child)) {
3346
+ found = true;
3347
+ break;
3348
+ }
3349
+ }
3350
+ }
3351
+ drawable.set(id, found);
3352
+ return found;
3353
+ };
3354
+ const boundaryLabel = (id) => groupById.get(id).label;
3355
+ const within = (scope, boundary) => {
3356
+ let cursor = scope;
3357
+ while (cursor !== void 0) {
3358
+ if (cursor === boundary) return true;
3359
+ cursor = groupById.get(cursor).parent;
3360
+ }
3361
+ return false;
3362
+ };
3363
+ const keptEdges = [];
3364
+ builder.edges.forEach((edge, index) => {
3365
+ const where = "line " + String(builder.edgeLines[index]) + ": ";
3366
+ const fromBoundary = boundaryAt.get(edge.from);
3367
+ const toBoundary = boundaryAt.get(edge.to);
3368
+ if (ambiguousAt.has(edge.from) || ambiguousAt.has(edge.to)) {
3369
+ const name = ambiguousAt.has(edge.from) ? edge.from : edge.to;
3370
+ refused.push(where + "`" + name + "` is the name of more than one block; an edge cannot say which boundary it means");
3371
+ return;
3372
+ }
3373
+ if (fromBoundary !== void 0 && toBoundary !== void 0) {
3374
+ refused.push(where + "`" + boundaryLabel(fromBoundary) + "` and `" + boundaryLabel(toBoundary) + "` are boundaries, not elements; an edge between two boundaries is not one this renderer draws");
3375
+ return;
3376
+ }
3377
+ const boundary = fromBoundary ?? toBoundary;
3378
+ if (boundary !== void 0) {
3379
+ const group = groupById.get(boundary);
3380
+ if (group.parent !== void 0) {
3381
+ 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");
3382
+ return;
3383
+ }
3384
+ if (within(builder.edgeScopes[index], boundary)) {
3385
+ 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");
3386
+ return;
3387
+ }
3388
+ if (!hasContents(boundary)) {
3389
+ refused.push(where + "`" + group.label + "` is a block with nothing drawn inside it, so there is no rectangle to attach the edge to");
3390
+ return;
3391
+ }
3392
+ }
3393
+ keptEdges.push({
3394
+ ...edge,
3395
+ ...fromBoundary === void 0 ? {} : { from: fromBoundary },
3396
+ ...toBoundary === void 0 ? {} : { to: toBoundary }
3397
+ });
3398
+ });
3399
+ builder.edges = keptEdges;
887
3400
  if (!seenAny || builder.nodes.size === 0) {
888
3401
  return { diagram: emptyDiagram(title, "no nodes found"), refused };
889
3402
  }
@@ -900,6 +3413,13 @@ window.__ModuleLoader__.load({
900
3413
  }
901
3414
 
902
3415
  // src/render/svg.ts
3416
+ function boxText(shape, stroke) {
3417
+ const lines = shape.lines === void 0 || shape.lines.length === 0 ? [shape.label] : shape.lines;
3418
+ const lineHeight = 15;
3419
+ const first = shape.y + shape.height / 2 - (lines.length - 1) * lineHeight / 2 + 4;
3420
+ const spans = lines.map((line, index) => '<tspan x="' + n(shape.x + shape.width / 2) + '" y="' + n(first + index * lineHeight) + '">' + escape(line) + "</tspan>").join("");
3421
+ return '<text font-size="11" text-anchor="middle" fill="' + stroke + '">' + spans + "</text>";
3422
+ }
903
3423
  var n = (value) => {
904
3424
  const rounded = Math.round(value * 10) / 10;
905
3425
  return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1);
@@ -914,45 +3434,130 @@ window.__ModuleLoader__.load({
914
3434
  box: (shape, colours) => {
915
3435
  const stroke = colours[shape.colour];
916
3436
  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>";
3437
+ const tail = '" fill="' + colours.surface + '" stroke="' + stroke + '" stroke-width="' + n(width) + '" />';
3438
+ const x = shape.x;
3439
+ const y = shape.y;
3440
+ const w = shape.width;
3441
+ const h = shape.height;
3442
+ const at = (point) => n(point.x) + " " + n(point.y);
3443
+ const points = (list) => list.map((point) => n(point.x) + "," + n(point.y)).join(",");
3444
+ const curve = (segment) => " C" + at(segment.c1) + " " + at(segment.c2) + " " + at(segment.to);
3445
+ switch (shape.shape) {
3446
+ // A decision: the notation wrote `{…}`, and a rounded rectangle would say "step".
3447
+ case "diamond":
3448
+ return '<polygon points="' + points(diamondPoints(shape)) + tail + boxText(shape, stroke);
3449
+ case "hexagon":
3450
+ return '<polygon points="' + points(hexagonPoints(shape)) + tail + boxText(shape, stroke);
3451
+ // A datastore: the cylinder UML draws for a stored collection.
3452
+ case "cylinder": {
3453
+ const caps = cylinderCaps(shape);
3454
+ const path = "M" + at(caps.top.from) + curve(caps.top) + " L" + at(caps.bottom.from) + curve(caps.bottom) + " Z";
3455
+ return '<path d="' + path + tail + boxText(shape, stroke);
3456
+ }
3457
+ // A start or end state: the stadium a terminal node is drawn as.
3458
+ case "stadium":
3459
+ return '<rect x="' + n(x) + '" y="' + n(y) + '" width="' + n(w) + '" height="' + n(h) + '" rx="' + n(stadiumRadius(shape)) + tail + boxText(shape, stroke);
3460
+ case "circle":
3461
+ return '<ellipse cx="' + n(x + w / 2) + '" cy="' + n(y + h / 2) + '" rx="' + n(w / 2) + '" ry="' + n(h / 2) + tail + boxText(shape, stroke);
3462
+ // A subroutine carries its double side lines.
3463
+ case "subroutine":
3464
+ 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);
3465
+ default:
3466
+ return '<rect x="' + n(x) + '" y="' + n(y) + '" width="' + n(w) + '" height="' + n(h) + '" rx="6' + tail + boxText(shape, stroke);
3467
+ }
918
3468
  },
919
3469
  link: (shape, colours) => {
920
3470
  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);
3471
+ const marks = edgeMarks(
3472
+ { x: shape.x1, y: shape.y1 },
3473
+ { x: shape.x2, y: shape.y2 },
3474
+ { x: shape.x1, y: shape.y1 },
3475
+ { x: shape.x2, y: shape.y2 },
3476
+ shape.arrow,
3477
+ stroke,
3478
+ colours.background
3479
+ );
3480
+ 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
3481
  },
924
3482
  path: (shape, colours) => {
925
3483
  const stroke = colours[shape.colour];
926
3484
  const points = shape.points.map((point) => n(point.x) + "," + n(point.y)).join(" ");
927
3485
  const last = shape.points[shape.points.length - 1];
928
3486
  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);
3487
+ const first = shape.points[0];
3488
+ const after = shape.points[1] ?? first;
3489
+ 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
3490
  },
3491
+ // A junction dot: the visible answer to "is this a join or a crossing?". Filled in the line's own colour, so it
3492
+ // reads as the line thickening where a branch meets it rather than as a new mark on the page.
3493
+ junction: (shape, colours) => '<circle cx="' + n(shape.x) + '" cy="' + n(shape.y) + '" r="2.5" fill="' + colours[shape.colour] + '" />',
931
3494
  title: (shape, colours) => {
932
3495
  return '<text x="' + n(shape.x) + '" y="' + n(shape.y) + '" font-size="12" fill="' + colours[shape.colour] + '">' + escape(shape.text) + "</text>";
933
3496
  },
934
3497
  caption: (shape, colours) => {
935
3498
  const colour = colours[shape.colour];
936
3499
  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>";
3500
+ return '<text x="' + n(shape.x) + '" y="' + n(shape.y + 11) + '" font-size="10" text-anchor="middle" fill="' + colour + '">' + lines + "</text>";
938
3501
  }
939
3502
  };
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;
3503
+ function dash(shape) {
3504
+ return shape.dashed === true ? ' stroke-dasharray="4 3"' : "";
3505
+ }
3506
+ function edgeMarks(source, sourceNext, targetPrev, target, arrow, stroke, background) {
3507
+ if (arrow === "composition" || arrow === "aggregation") {
3508
+ return diamond(source, sourceNext, stroke, arrow === "composition" ? stroke : background);
3509
+ }
3510
+ if (arrow === "generalization" || arrow === "realization") {
3511
+ return hollowTriangle(targetPrev, target, stroke, background);
3512
+ }
3513
+ return arrowHead(targetPrev.x, targetPrev.y, target.x, target.y, stroke);
3514
+ }
3515
+ function tipAndBase(from, to) {
3516
+ const length = Math.hypot(to.x - from.x, to.y - from.y);
3517
+ if (length < 1) return void 0;
3518
+ const ux = (to.x - from.x) / length;
3519
+ const uy = (to.y - from.y) / length;
945
3520
  const size = 7;
946
3521
  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 + '" />';
3522
+ const baseX = to.x - ux * size;
3523
+ const baseY = to.y - uy * size;
3524
+ return {
3525
+ tip: n(to.x) + "," + n(to.y),
3526
+ left: n(baseX - uy * half) + "," + n(baseY + ux * half),
3527
+ right: n(baseX + uy * half) + "," + n(baseY - ux * half)
3528
+ };
3529
+ }
3530
+ function arrowHead(x1, y1, x2, y2, stroke) {
3531
+ const points = tipAndBase({ x: x1, y: y1 }, { x: x2, y: y2 });
3532
+ if (points === void 0) return "";
3533
+ return '<polygon points="' + points.tip + " " + points.left + " " + points.right + '" fill="' + stroke + '" />';
3534
+ }
3535
+ function hollowTriangle(from, to, stroke, background) {
3536
+ const points = tipAndBase(from, to);
3537
+ if (points === void 0) return "";
3538
+ return '<polygon points="' + points.tip + " " + points.left + " " + points.right + '" fill="' + background + '" stroke="' + stroke + '" stroke-width="1" />';
3539
+ }
3540
+ function diamond(at, next, stroke, fill) {
3541
+ const length = Math.hypot(next.x - at.x, next.y - at.y);
3542
+ if (length < 1) return "";
3543
+ const ux = (next.x - at.x) / length;
3544
+ const uy = (next.y - at.y) / length;
3545
+ const size = 12;
3546
+ const half = 4;
3547
+ const back = { x: at.x + ux * size, y: at.y + uy * size };
3548
+ const middle = { x: at.x + ux * size / 2, y: at.y + uy * size / 2 };
3549
+ const left = { x: middle.x - uy * half, y: middle.y + ux * half };
3550
+ const right = { x: middle.x + uy * half, y: middle.y - ux * half };
3551
+ const points = [at, left, back, right].map((point) => n(point.x) + "," + n(point.y)).join(" ");
3552
+ return '<polygon points="' + points + '" fill="' + fill + '" stroke="' + stroke + '" stroke-width="1" />';
952
3553
  }
953
3554
  function edgeLabel(label, x, y, colours) {
954
3555
  if (typeof label !== "string" || label === "") return "";
955
- return '<text x="' + n(x) + '" y="' + n(y) + '" font-size="10" fill="' + colours.muted + '">' + escape(label) + "</text>";
3556
+ const width = textWidth(label);
3557
+ const left = x - width / 2;
3558
+ const top = y - LABEL_HEIGHT / 2;
3559
+ const baseline = y + 3.5;
3560
+ 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
3561
  }
957
3562
  function toSvg(canvas, colours, label) {
958
3563
  let dropped = 0;