@hncode/hncode 0.5.5 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/package.json +37 -38
  2. package/src/agent.js +67 -2
  3. package/src/image-row.js +111 -0
  4. package/src/latex.js +1557 -0
  5. package/src/llm.js +19 -4
  6. package/src/mermaid/ansi.js +55 -0
  7. package/src/mermaid/canvas.js +449 -0
  8. package/src/mermaid/class-style.js +57 -0
  9. package/src/mermaid/css-colors.js +153 -0
  10. package/src/mermaid/diagrams/class.js +289 -0
  11. package/src/mermaid/diagrams/er.js +201 -0
  12. package/src/mermaid/diagrams/flowchart.js +488 -0
  13. package/src/mermaid/diagrams/gitgraph.js +205 -0
  14. package/src/mermaid/diagrams/mindmap.js +101 -0
  15. package/src/mermaid/diagrams/pie.js +104 -0
  16. package/src/mermaid/diagrams/sequence.js +282 -0
  17. package/src/mermaid/diagrams/state.js +292 -0
  18. package/src/mermaid/diagrams/timeline.js +99 -0
  19. package/src/mermaid/graph.js +138 -0
  20. package/src/mermaid/index.js +59 -0
  21. package/src/mermaid/labels.js +339 -0
  22. package/src/mermaid/layout-seq.js +225 -0
  23. package/src/mermaid/layout.js +1454 -0
  24. package/src/mermaid/registry.js +54 -0
  25. package/src/mermaid/source-box.js +102 -0
  26. package/src/mermaid/statements.js +235 -0
  27. package/src/mermaid/types.js +1 -0
  28. package/src/mermaid/width.js +29 -0
  29. package/src/screen-diff.js +512 -0
  30. package/src/screen-paint.js +128 -0
  31. package/src/screen.js +277 -0
  32. package/src/subagent-types.js +136 -147
  33. package/src/term-caps.js +10 -4
  34. package/src/tex/font.js +292 -0
  35. package/src/tex/fonts/LICENSE.txt +11 -0
  36. package/src/tex/fonts/STIX2Math.ttf +0 -0
  37. package/src/tex/index.js +293 -0
  38. package/src/tex/kitty.js +186 -0
  39. package/src/tex/layout.js +446 -0
  40. package/src/tex/parse.js +331 -0
  41. package/src/tex/raster.js +216 -0
  42. package/src/tex/render.js +167 -0
  43. package/src/tex/sixel.js +326 -0
  44. package/src/tex/svg.js +237 -0
  45. package/src/tool-result.js +2 -2
  46. package/src/tools/agent-swarm.js +3 -15
  47. package/src/tools/agent.js +454 -464
  48. package/src/tools/ask-user-question.js +195 -206
  49. package/src/tools/bash.js +39 -3
  50. package/src/tools/edit.js +10 -112
  51. package/src/tools/fetch-url.js +130 -120
  52. package/src/tools/memory.js +80 -88
  53. package/src/tools/read.js +27 -23
  54. package/src/tools/todo.js +8 -13
  55. package/src/tools/validate-args.js +78 -0
  56. package/src/tui.js +1346 -496
@@ -0,0 +1,1454 @@
1
+ /**
2
+ * Graph layout: rank, order, place, route, draw.
3
+ *
4
+ * Follows the Sugiyama outline — assign ranks along the flow axis, reorder
5
+ * within ranks to cut crossings, then relax positions on the cross axis so
6
+ * chains stay straight. Edges between adjacent ranks share horizontal "bus"
7
+ * rows; everything else is routed around the diagram through vertical "lanes".
8
+ *
9
+ * `BT` and `RL` reuse the `TD`/`LR` layouts and flip the finished canvas, so
10
+ * text never ends up mirrored.
11
+ */
12
+ import { Canvas, CONT, D, drawText, drawTextOverEdges, L, R, STY_DOT, STY_SOLID, STY_THICK, U, } from './canvas.js';
13
+ import { Graph } from './graph.js';
14
+ import { fitLabel, MAX_LABEL, MAX_LINES, WRAP_WIDTH, wrapLabel } from './labels.js';
15
+ import { measured, stringWidth } from './width.js';
16
+ /** Cells of padding between a box border and its text. */
17
+ export const PAD = 1;
18
+ /** Minimum horizontal / vertical space between boxes. */
19
+ const GAP_X = 3;
20
+ const GAP_Y = 2;
21
+ /** Refuse to allocate a canvas larger than this many cells. */
22
+ export const MAX_CANVAS_CELLS = 1 << 21;
23
+ /** Saturating subtraction; Rust's `usize` arithmetic never goes negative. */
24
+ export const sat = (a, b) => Math.max(0, a - b);
25
+ export const half = (n) => Math.floor(n / 2);
26
+ /**
27
+ * Everything an edge says, joined — the fallback for routes that have no
28
+ * per-end placement (lanes, self-loops). Forward routes place `cardFrom` /
29
+ * `cardTo` at their own ends instead.
30
+ */
31
+ function edgeText(edge) {
32
+ const joined = [edge.cardFrom ?? '', edge.label ?? '', edge.cardTo ?? '']
33
+ .filter((part) => part !== '')
34
+ .join(' ');
35
+ return joined === '' ? null : joined;
36
+ }
37
+ // ------------------------------------------------------------------ ranking
38
+ /**
39
+ * Longest-path ranking over the graph's DAG.
40
+ *
41
+ * Back edges (those closing a cycle) are excluded by a DFS colouring pass, so
42
+ * `A --> B --> C --> A` still ranks 0, 1, 2 rather than diverging.
43
+ */
44
+ export function computeRanks(graph) {
45
+ const n = graph.nodes.length;
46
+ const children = Array.from({ length: n }, () => []);
47
+ const indeg = new Array(n).fill(0);
48
+ for (const e of graph.edges) {
49
+ if (e.from !== e.to) {
50
+ children[e.from].push(e.to);
51
+ indeg[e.to]++;
52
+ }
53
+ }
54
+ const color = new Uint8Array(n);
55
+ const dag = Array.from({ length: n }, () => []);
56
+ const order = [];
57
+ // Roots first so ranks grow from natural entry points, then any leftovers.
58
+ const roots = [...Array(n).keys()].filter((i) => indeg[i] === 0);
59
+ for (const start of [...roots, ...Array(n).keys()]) {
60
+ if (color[start] === 0)
61
+ dfsDag(start, children, color, dag, order);
62
+ }
63
+ const rank = new Array(n).fill(0);
64
+ for (let i = order.length - 1; i >= 0; i--) {
65
+ const u = order[i];
66
+ for (const v of dag[u])
67
+ rank[v] = Math.max(rank[v], rank[u] + 1);
68
+ }
69
+ return rank;
70
+ }
71
+ /** Iterative DFS recording postorder and skipping edges back into the stack. */
72
+ function dfsDag(start, children, color, dag, order) {
73
+ const stack = [{ u: start, i: 0 }];
74
+ color[start] = 1;
75
+ while (stack.length > 0) {
76
+ const frame = stack[stack.length - 1];
77
+ const u = frame.u;
78
+ if (frame.i < children[u].length) {
79
+ const v = children[u][frame.i];
80
+ frame.i++;
81
+ if (color[v] === 1)
82
+ continue; // grey: a back edge, ignore it
83
+ dag[u].push(v);
84
+ if (color[v] === 0) {
85
+ color[v] = 1;
86
+ stack.push({ u: v, i: 0 });
87
+ }
88
+ }
89
+ else {
90
+ color[u] = 2;
91
+ order.push(u);
92
+ stack.pop();
93
+ }
94
+ }
95
+ }
96
+ /**
97
+ * Reorder nodes within each rank to minimise edge crossings (barycenter
98
+ * sweeps): alternate down/up passes sort each rank by the mean position of its
99
+ * neighbours, keeping whichever ordering crossed least.
100
+ */
101
+ export function orderRanks(byRank, edges, ranks) {
102
+ const n = ranks.length;
103
+ if (byRank.length < 2 || n < 3)
104
+ return;
105
+ const parents = Array.from({ length: n }, () => []);
106
+ const children = Array.from({ length: n }, () => []);
107
+ for (const e of edges) {
108
+ if (e.from !== e.to && ranks[e.to] > ranks[e.from]) {
109
+ parents[e.to].push(e.from);
110
+ children[e.from].push(e.to);
111
+ }
112
+ }
113
+ const pos = new Array(n).fill(0);
114
+ const reindex = (row) => {
115
+ for (let i = 0; i < row.length; i++)
116
+ pos[row[i]] = i;
117
+ };
118
+ for (const row of byRank)
119
+ reindex(row);
120
+ let best = byRank.map((row) => [...row]);
121
+ let bestCrossings = countCrossings(edges, ranks, pos);
122
+ if (bestCrossings === 0)
123
+ return;
124
+ for (let it = 0; it < 8; it++) {
125
+ // Alternate sweeping down (sort by parents) and up (sort by children).
126
+ const rows = it % 2 === 0 ? byRank.slice(1) : byRank.slice(0, -1).reverse();
127
+ const neigh = it % 2 === 0 ? parents : children;
128
+ for (const row of rows) {
129
+ sortByBarycenter(row, neigh, pos);
130
+ reindex(row);
131
+ }
132
+ const crossings = countCrossings(edges, ranks, pos);
133
+ if (crossings < bestCrossings) {
134
+ bestCrossings = crossings;
135
+ best = byRank.map((row) => [...row]);
136
+ }
137
+ if (bestCrossings === 0)
138
+ break;
139
+ }
140
+ for (let i = 0; i < byRank.length; i++)
141
+ byRank[i].splice(0, byRank[i].length, ...best[i]);
142
+ }
143
+ function sortByBarycenter(row, neigh, pos) {
144
+ const keyed = row.map((v) => ({
145
+ key: neigh[v].length === 0 ? pos[v] : neigh[v].reduce((s, u) => s + pos[u], 0) / neigh[v].length,
146
+ v,
147
+ }));
148
+ keyed.sort((a, b) => a.key - b.key);
149
+ for (let i = 0; i < keyed.length; i++)
150
+ row[i] = keyed[i].v;
151
+ }
152
+ export function countCrossings(edges, ranks, pos) {
153
+ const adjacent = edges
154
+ .filter((e) => e.from !== e.to && ranks[e.to] === ranks[e.from] + 1)
155
+ .map((e) => [ranks[e.from], pos[e.from], pos[e.to]]);
156
+ let crossings = 0;
157
+ for (let i = 0; i < adjacent.length; i++) {
158
+ const a = adjacent[i];
159
+ for (let j = i + 1; j < adjacent.length; j++) {
160
+ const b = adjacent[j];
161
+ if (a[0] === b[0] && ((a[1] < b[1] && a[2] > b[2]) || (a[1] > b[1] && a[2] < b[2]))) {
162
+ crossings++;
163
+ }
164
+ }
165
+ }
166
+ return crossings;
167
+ }
168
+ /**
169
+ * Assign a cross-axis centre to every node so nodes line up under their
170
+ * neighbours: each node drifts toward the average of its neighbours while
171
+ * ranks keep their order and boxes keep `sep` between them.
172
+ */
173
+ export function assignPositions(byRank, size, sep, edges, ranks) {
174
+ const n = size.length;
175
+ const parents = Array.from({ length: n }, () => []);
176
+ const children = Array.from({ length: n }, () => []);
177
+ for (const e of edges) {
178
+ if (e.from !== e.to && ranks[e.to] > ranks[e.from]) {
179
+ parents[e.to].push(e.from);
180
+ children[e.from].push(e.to);
181
+ }
182
+ }
183
+ const pos = new Array(n).fill(0);
184
+ for (const row of byRank) {
185
+ let x = 0;
186
+ for (const v of row) {
187
+ const h = size[v] / 2;
188
+ x += h;
189
+ pos[v] = x;
190
+ x += h + sep;
191
+ }
192
+ }
193
+ for (let it = 0; it < 10; it++) {
194
+ const rows = it % 2 === 0 ? byRank : [...byRank].reverse();
195
+ const neigh = it % 2 === 0 ? parents : children;
196
+ for (const row of rows)
197
+ relaxRank(row, neigh, pos, size, sep);
198
+ }
199
+ let minLeft = Number.POSITIVE_INFINITY;
200
+ for (let v = 0; v < n; v++)
201
+ minLeft = Math.min(minLeft, pos[v] - size[v] / 2);
202
+ if (!Number.isFinite(minLeft))
203
+ minLeft = 0;
204
+ return Array.from({ length: n }, (_, v) => Math.max(0, Math.round(pos[v] - minLeft)));
205
+ }
206
+ function relaxRank(nodes, neigh, pos, size, sep) {
207
+ const n = nodes.length;
208
+ if (n === 0)
209
+ return;
210
+ const desired = nodes.map((v) => neigh[v].length === 0 ? pos[v] : neigh[v].reduce((s, u) => s + pos[u], 0) / neigh[v].length);
211
+ const halfOf = (i) => size[nodes[i]] / 2;
212
+ // Sweep right then left, then take the midpoint: this centres a node between
213
+ // the tightest packing that respects order from either side.
214
+ const left = new Array(n);
215
+ for (let i = 0; i < n; i++) {
216
+ left[i] =
217
+ i === 0 ? desired[i] : Math.max(desired[i], left[i - 1] + halfOf(i - 1) + sep + halfOf(i));
218
+ }
219
+ const right = new Array(n);
220
+ for (let i = n - 1; i >= 0; i--) {
221
+ right[i] =
222
+ i === n - 1
223
+ ? desired[i]
224
+ : Math.min(desired[i], right[i + 1] - halfOf(i + 1) - sep - halfOf(i));
225
+ }
226
+ for (let i = 0; i < n; i++)
227
+ pos[nodes[i]] = (left[i] + right[i]) / 2;
228
+ for (let i = 1; i < n; i++) {
229
+ const minP = pos[nodes[i - 1]] + halfOf(i - 1) + sep + halfOf(i);
230
+ if (pos[nodes[i]] < minP)
231
+ pos[nodes[i]] = minP;
232
+ }
233
+ }
234
+ /**
235
+ * Pack spans into as few parallel tracks as possible.
236
+ *
237
+ * Two spans share a track when they are two cells apart, or when they share an
238
+ * endpoint — edges fanning out of one node deliberately reuse a single row so
239
+ * a merge draws one arrowhead rather than a stack of them.
240
+ */
241
+ export function assignTracks(spans, shortestFirst = false) {
242
+ // Lanes pack shortest-first: a span contained in another takes the inner
243
+ // track, so exits and entries at rows the inner lane never reaches cross
244
+ // nothing. Buses keep the start-ordered packing.
245
+ const sorted = [...spans].sort((a, b) => (shortestFirst ? a.end - a.start - (b.end - b.start) : 0) ||
246
+ a.start - b.start ||
247
+ a.end - b.end ||
248
+ a.from - b.from ||
249
+ a.to - b.to ||
250
+ a.edge - b.edge);
251
+ const tracks = [];
252
+ const assigned = [];
253
+ for (const span of sorted) {
254
+ let slot = tracks.findIndex((members) => members.every((m) => m.end + 2 <= span.start ||
255
+ span.end + 2 <= m.start ||
256
+ ((m.from === span.from || m.to === span.to) && !m.labeled && !span.labeled)));
257
+ if (slot === -1) {
258
+ tracks.push([]);
259
+ slot = tracks.length - 1;
260
+ }
261
+ tracks[slot].push(span);
262
+ assigned.push([span.edge, slot]);
263
+ }
264
+ return { assigned, count: tracks.length };
265
+ }
266
+ /**
267
+ * An edge between adjacent ranks running against the direction. In top-down
268
+ * layouts these route locally through the same band as their forward
269
+ * siblings — the short return arrow mermaid draws — rather than around the
270
+ * diagram. Left-to-right boxes are three rows tall, leaving no room to
271
+ * offset the return off the centre row, so LR/RL keep the lane.
272
+ */
273
+ export const isAdjacentBack = (ranks, e) => e.from !== e.to && ranks[e.from] === ranks[e.to] + 1;
274
+ /** Edges crossing the band between rank `r` and `r + 1` that must jog
275
+ * sideways, so need a bus row. `withBack` admits adjacent back edges. */
276
+ function busSpans(graph, ranks, centers, r, exact, withBack) {
277
+ const out = [];
278
+ graph.edges.forEach((e, i) => {
279
+ const jogs = exact
280
+ ? centers[e.from] !== centers[e.to]
281
+ : Math.abs(centers[e.from] - centers[e.to]) > 1;
282
+ const lo = Math.min(ranks[e.from], ranks[e.to]);
283
+ const adjacent = withBack
284
+ ? Math.abs(ranks[e.from] - ranks[e.to]) === 1
285
+ : ranks[e.to] === ranks[e.from] + 1;
286
+ if (e.from !== e.to && adjacent && lo === r && jogs) {
287
+ out.push({
288
+ start: Math.min(centers[e.from], centers[e.to]),
289
+ end: Math.max(centers[e.from], centers[e.to]),
290
+ from: e.from,
291
+ to: e.to,
292
+ edge: i,
293
+ });
294
+ }
295
+ });
296
+ return out;
297
+ }
298
+ /** Edges skipping a rank or running backwards; these go around in a lane. */
299
+ function laneSpans(graph, ranks, placed, vertical) {
300
+ const out = [];
301
+ graph.edges.forEach((e, i) => {
302
+ if (e.from === e.to || ranks[e.to] === ranks[e.from] + 1)
303
+ return;
304
+ if (vertical && isAdjacentBack(ranks, e))
305
+ return;
306
+ const pf = placed[e.from];
307
+ const pt = placed[e.to];
308
+ const a = vertical ? Math.min(pf.cy, pt.cy) : Math.min(pf.cx, pt.cx);
309
+ const b = vertical ? Math.max(pf.cy, pt.cy) : Math.max(pf.cx, pt.cx);
310
+ out.push({ start: a, end: b, from: e.from, to: e.to, edge: i, labeled: edgeText(e) !== null });
311
+ });
312
+ return out;
313
+ }
314
+ // ----------------------------------------------------------------- placement
315
+ function placeTd(ranks, maxRank, byRank, sizes, graph, placed) {
316
+ const centers = assignPositions(byRank, sizes.layW, GAP_X, graph.edges, ranks);
317
+ // Top-entry geometry, derivable before placement. A node's entries — the
318
+ // merged forward cluster plus each skip — spread evenly across the box top
319
+ // (two entries land at ~1/3 and ~2/3). A forward arrival aligned with its
320
+ // source stays at centre so chains keep no kinks, and the skips spread
321
+ // over the right half instead. A label that does not fit before the next
322
+ // entry renders left of its arrow. A skip whose entry column crosses no
323
+ // box on any intermediate rank drops straight instead of taking a lane.
324
+ const boxL = (j) => sat(centers[j], half(sizes.boxW[j]));
325
+ const boxR = (j) => boxL(j) + sizes.boxW[j] - 1;
326
+ const isSkip = (e) => e.from !== e.to && ranks[e.to] - ranks[e.from] > 1;
327
+ const edgeEntryX = new Array(graph.edges.length).fill(-1);
328
+ const edgeStraight = new Array(graph.edges.length).fill(false);
329
+ const fwdEntryX = new Array(graph.nodes.length).fill(-1);
330
+ const edgeLabelLeft = new Array(graph.edges.length).fill(false);
331
+ const labelW = (e) => {
332
+ const parts = [e.label, e.cardTo].filter((p) => p != null);
333
+ return parts.length === 0
334
+ ? -1
335
+ : Math.max(...parts.map((p) => Math.min(stringWidth(p), MAX_LABEL)));
336
+ };
337
+ const skipsInto = graph.nodes.map(() => []);
338
+ const fwdsInto = graph.nodes.map(() => []);
339
+ graph.edges.forEach((e, i) => {
340
+ if (e.from === e.to)
341
+ return;
342
+ if (isSkip(e))
343
+ skipsInto[e.to].push(i);
344
+ else if (ranks[e.to] === ranks[e.from] + 1)
345
+ fwdsInto[e.to].push(i);
346
+ });
347
+ graph.nodes.forEach((_, t) => {
348
+ const skips = skipsInto[t];
349
+ if (skips.length === 0)
350
+ return;
351
+ const fwds = fwdsInto[t];
352
+ const cx = centers[t];
353
+ const left = boxL(t);
354
+ const right = boxR(t);
355
+ const hasFwd = fwds.length > 0;
356
+ const aligned = hasFwd && fwds.some((i) => Math.abs(centers[graph.edges[i].from] - cx) <= 1);
357
+ const spreadL = hasFwd && aligned ? cx : left;
358
+ const slots = hasFwd && aligned ? skips.length : (hasFwd ? 1 : 0) + skips.length;
359
+ const slot = (i) => spreadL + Math.round(((right - spreadL) * (i + 1)) / (slots + 1));
360
+ /** Ordered entries: the forward cluster (if any), then each skip. */
361
+ const items = [];
362
+ if (hasFwd) {
363
+ items.push({
364
+ slot: aligned ? cx : slot(0),
365
+ w: Math.max(...fwds.map((i) => labelW(graph.edges[i]))),
366
+ skip: null,
367
+ });
368
+ }
369
+ skips.forEach((si, j) => {
370
+ items.push({
371
+ slot: slot(hasFwd && aligned ? j : j + (hasFwd ? 1 : 0)),
372
+ w: labelW(graph.edges[si]),
373
+ skip: si,
374
+ });
375
+ });
376
+ // Walk left to right with a cursor over the free head-row cells: each
377
+ // entry lands at its slot (or past the previous label), its own label
378
+ // going right when the next slot leaves room, else left when the cells
379
+ // behind the cursor allow. Overflow falls back to the legacy rule.
380
+ const cols = [];
381
+ const lefts = [];
382
+ let cursor = left;
383
+ let fits = true;
384
+ for (const [i, item] of items.entries()) {
385
+ const x = Math.max(item.slot, cursor);
386
+ if (x > right - 1) {
387
+ fits = false;
388
+ break;
389
+ }
390
+ const next = items[i + 1]?.slot ?? Number.MAX_SAFE_INTEGER;
391
+ const w = item.w;
392
+ if (w >= 0 && x + w + 2 > next && x - cursor >= w) {
393
+ lefts.push(true);
394
+ cursor = x + 2;
395
+ }
396
+ else {
397
+ lefts.push(false);
398
+ cursor = w >= 0 ? x + w + 2 : x + 2;
399
+ }
400
+ cols.push(x);
401
+ }
402
+ if (fits) {
403
+ items.forEach((item, i) => {
404
+ if (item.skip === null) {
405
+ fwdEntryX[t] = cols[i];
406
+ if (lefts[i])
407
+ for (const fi of fwds)
408
+ edgeLabelLeft[fi] = true;
409
+ }
410
+ else {
411
+ edgeEntryX[item.skip] = cols[i];
412
+ edgeLabelLeft[item.skip] = lefts[i];
413
+ }
414
+ });
415
+ return;
416
+ }
417
+ // Legacy: forward stays centred; a skip lands past the arrival labels,
418
+ // or left of centre with its own label flipped left.
419
+ const reach = hasFwd ? Math.max(cx, ...fwds.map((i) => cx + 1 + labelW(graph.edges[i]))) : -1;
420
+ for (const si of skips) {
421
+ const clear = reach === -1 ? cx + 2 : reach + 2;
422
+ const capped = Math.min(clear, right - 1);
423
+ if (capped > Math.max(cx, reach)) {
424
+ edgeEntryX[si] = capped;
425
+ }
426
+ else {
427
+ edgeEntryX[si] = Math.max(left + 1, cx - 2);
428
+ edgeLabelLeft[si] = true;
429
+ }
430
+ }
431
+ });
432
+ graph.edges.forEach((e, i) => {
433
+ if (!isSkip(e))
434
+ return;
435
+ const entryX = edgeEntryX[i];
436
+ edgeStraight[i] = graph.nodes.every((_, j) => ranks[j] <= ranks[e.from] ||
437
+ ranks[j] >= ranks[e.to] ||
438
+ entryX < boxL(j) ||
439
+ entryX > boxR(j));
440
+ });
441
+ const edgeBus = new Array(graph.edges.length).fill(0);
442
+ const busTracks = new Array(maxRank + 1).fill(0);
443
+ for (let r = 0; r < maxRank; r++) {
444
+ const spans = busSpans(graph, ranks, centers, r, false, true);
445
+ // Skip departures ride the bus tracks of their own band: endpoint
446
+ // sharing folds them onto their siblings' row, so a node's forward fan
447
+ // and its skips split from one `┴` origin.
448
+ graph.edges.forEach((e, i) => {
449
+ if (!isSkip(e) || ranks[e.from] !== r)
450
+ return;
451
+ const endX = edgeStraight[i] ? edgeEntryX[i] : 1 << 29;
452
+ spans.push({
453
+ start: Math.min(centers[e.from], endX),
454
+ end: Math.max(centers[e.from], endX),
455
+ from: e.from,
456
+ to: e.to,
457
+ edge: i,
458
+ });
459
+ });
460
+ if (spans.length === 0)
461
+ continue;
462
+ // Back-edge arrowheads sit on the first band row, back buses right under
463
+ // it, forward buses below those: with the attach columns offset right of
464
+ // centre, a reciprocal pair then runs as two parallel staircases whose
465
+ // verticals fall outside each other's horizontal spans — no crossings.
466
+ const back = spans.filter((s) => isAdjacentBack(ranks, graph.edges[s.edge]));
467
+ const fwd = spans.filter((s) => !isAdjacentBack(ranks, graph.edges[s.edge]));
468
+ const base = graph.edges.some((e) => isAdjacentBack(ranks, e) && ranks[e.to] === r) ? 1 : 0;
469
+ const b = assignTracks(back);
470
+ for (const [idx, slot] of b.assigned)
471
+ edgeBus[idx] = base + slot;
472
+ const f = assignTracks(fwd);
473
+ for (const [idx, slot] of f.assigned)
474
+ edgeBus[idx] = base + b.count + slot;
475
+ busTracks[r] = base + b.count + f.count;
476
+ }
477
+ // A laned skip re-enters along a reserved approach row above the target's
478
+ // rank, over the arrival heads and their labels; straight drops need none.
479
+ const entersInto = new Array(maxRank + 1).fill(false);
480
+ graph.edges.forEach((e, i) => {
481
+ if (isSkip(e) && !edgeStraight[i])
482
+ entersInto[ranks[e.to]] = true;
483
+ });
484
+ for (let r = 0; r < maxRank; r++) {
485
+ if (entersInto[r + 1])
486
+ busTracks[r] += 1;
487
+ }
488
+ const rankH = byRank.map((row) => row.length === 0 ? 3 : Math.max(...row.map((i) => sizes.boxH[i] + sizes.extraH[i])));
489
+ // Per-end cardinalities want a row each around the verb: source card,
490
+ // label, arrow-and-target-card.
491
+ const hasCards = graph.edges.some((e) => e.cardFrom !== undefined || e.cardTo !== undefined);
492
+ const gapY = hasCards ? Math.max(GAP_Y, 3) : GAP_Y;
493
+ const rankY = new Array(maxRank + 1).fill(0);
494
+ for (let r = 1; r <= maxRank; r++) {
495
+ rankY[r] = rankY[r - 1] + rankH[r - 1] + Math.max(gapY, busTracks[r - 1] + 1);
496
+ }
497
+ const canvasH = rankY[maxRank] + rankH[maxRank];
498
+ const bandEnd = Array.from({ length: maxRank + 1 }, (_, r) => rankY[r] + rankH[r]);
499
+ const rankStart = rankY;
500
+ // Approach rows sit just above each band's arrival-head row.
501
+ const skipApproach = new Array(maxRank + 1).fill(-1);
502
+ for (let r = 0; r < maxRank; r++) {
503
+ if (entersInto[r + 1])
504
+ skipApproach[r + 1] = rankY[r + 1] - 2;
505
+ }
506
+ let diagramW = 1;
507
+ byRank.forEach((row, r) => {
508
+ for (const idx of row) {
509
+ const w = sizes.boxW[idx];
510
+ const h = sizes.boxH[idx];
511
+ const cx = centers[idx];
512
+ const x = sat(cx, half(w));
513
+ const y = rankY[r] + half(rankH[r] - h - sizes.extraH[idx]);
514
+ placed[idx] = { x, y, w, h, cx, cy: y + half(h), rank: r };
515
+ diagramW = Math.max(diagramW, x + w);
516
+ if (sizes.extraH[idx] > 0 && sizes.selfLabelW[idx] > 0) {
517
+ diagramW = Math.max(diagramW, x + w + 2 + sizes.selfLabelW[idx]);
518
+ }
519
+ }
520
+ });
521
+ let contentW = diagramW;
522
+ for (const e of graph.edges) {
523
+ if (e.from === e.to)
524
+ continue;
525
+ if (ranks[e.to] === ranks[e.from] + 1) {
526
+ const parts = [e.label, e.cardTo].filter((part) => part != null);
527
+ for (const part of parts) {
528
+ const lw = Math.min(stringWidth(part), MAX_LABEL);
529
+ contentW = Math.max(contentW, placed[e.to].cx + 2 + lw);
530
+ }
531
+ if (e.cardFrom !== undefined) {
532
+ contentW = Math.max(contentW, placed[e.from].cx + 2 + stringWidth(e.cardFrom));
533
+ }
534
+ }
535
+ else if (isAdjacentBack(ranks, e)) {
536
+ const text = edgeText(e);
537
+ if (text !== null) {
538
+ contentW = Math.max(contentW, placed[e.to].cx + 2 + Math.min(stringWidth(text), MAX_LABEL));
539
+ }
540
+ }
541
+ else {
542
+ const text = edgeText(e);
543
+ if (text !== null) {
544
+ contentW = Math.max(contentW, diagramW + Math.min(stringWidth(text), MAX_LABEL) + 1);
545
+ }
546
+ }
547
+ }
548
+ const edgeLane = new Array(graph.edges.length).fill(0);
549
+ const lanes = laneSpans(graph, ranks, placed, true).filter((s) => !edgeStraight[s.edge]);
550
+ let canvasW = contentW;
551
+ let laneBase = 0;
552
+ if (lanes.length > 0) {
553
+ const { assigned, count } = assignTracks(lanes, true);
554
+ for (const [idx, slot] of assigned)
555
+ edgeLane[idx] = slot;
556
+ canvasW = contentW + 1 + count;
557
+ laneBase = contentW + 1;
558
+ }
559
+ return {
560
+ canvasW,
561
+ canvasH,
562
+ bandEnd,
563
+ rankStart,
564
+ edgeBus,
565
+ laneBase,
566
+ edgeLane,
567
+ skipApproach,
568
+ edgeEntryX,
569
+ edgeStraight,
570
+ fwdEntryX,
571
+ edgeLabelLeft,
572
+ };
573
+ }
574
+ function placeLr(ranks, maxRank, byRank, sizes, graph, placed) {
575
+ const colW = byRank.map((row) => row.length === 0 ? 0 : Math.max(...row.map((i) => sizes.boxW[i])));
576
+ const centers = assignPositions(byRank, sizes.layH, 1, graph.edges, ranks);
577
+ // A skip whose target entry row crosses no box on any intermediate rank
578
+ // runs straight through the diagram into the target's left side, exiting
579
+ // through the source's right-side fan; the bottom lane is the fallback.
580
+ // (No entry spreading or local returns here: LR boxes are three rows tall,
581
+ // so the centre row is the only usable port on a side.)
582
+ const isSkip = (e) => e.from !== e.to && ranks[e.to] - ranks[e.from] > 1;
583
+ // A back-edge target's bottom-entry `▲` stub sits one row below its box;
584
+ // a straight run through that cell would appear to carry the arrival.
585
+ const stubRows = new Set();
586
+ for (const e of graph.edges) {
587
+ if (e.from === e.to || ranks[e.to] >= ranks[e.from])
588
+ continue;
589
+ const t = e.to;
590
+ stubRows.add(sat(centers[t], half(sizes.boxH[t] + sizes.extraH[t])) + sizes.boxH[t]);
591
+ }
592
+ const edgeStraight = new Array(graph.edges.length).fill(false);
593
+ graph.edges.forEach((e, i) => {
594
+ if (!isSkip(e))
595
+ return;
596
+ const row = centers[e.to];
597
+ edgeStraight[i] =
598
+ !stubRows.has(row) &&
599
+ graph.nodes.every((_, j) => ranks[j] <= ranks[e.from] ||
600
+ ranks[j] >= ranks[e.to] ||
601
+ Math.abs(centers[j] - row) > half(sizes.boxH[j] + sizes.extraH[j]));
602
+ });
603
+ // Left-to-right edge labels sit in the gap after their source's column, so
604
+ // each gap sizes to the widest label *leaving through it* — one long label
605
+ // widens its own band, not the whole diagram. Straight skips label there
606
+ // too; a self-loop's label hangs beside its own box (selfLabelW).
607
+ const bandLabel = new Array(maxRank + 1).fill(0);
608
+ graph.edges.forEach((e, i) => {
609
+ if (e.from === e.to)
610
+ return;
611
+ if (ranks[e.to] !== ranks[e.from] + 1 && !edgeStraight[i])
612
+ return;
613
+ const verb = e.label === null ? 0 : Math.min(stringWidth(e.label), MAX_LABEL);
614
+ const cards = [e.cardFrom, e.cardTo]
615
+ .filter((c) => c !== undefined)
616
+ .reduce((w, c) => w + stringWidth(c) + 1, 0);
617
+ bandLabel[ranks[e.from]] = Math.max(bandLabel[ranks[e.from]], verb + cards);
618
+ });
619
+ const edgeBus = new Array(graph.edges.length).fill(0);
620
+ const busTracks = new Array(maxRank + 1).fill(0);
621
+ for (let r = 0; r < maxRank; r++) {
622
+ const spans = busSpans(graph, ranks, centers, r, true, false);
623
+ // Straight-skip departures ride their own band's bus tracks: endpoint
624
+ // sharing folds them onto their siblings' column — one fan origin.
625
+ graph.edges.forEach((e, i) => {
626
+ if (!isSkip(e) || !edgeStraight[i] || ranks[e.from] !== r)
627
+ return;
628
+ spans.push({
629
+ start: Math.min(centers[e.from], centers[e.to]),
630
+ end: Math.max(centers[e.from], centers[e.to]),
631
+ from: e.from,
632
+ to: e.to,
633
+ edge: i,
634
+ });
635
+ });
636
+ if (spans.length === 0)
637
+ continue;
638
+ const { assigned, count } = assignTracks(spans);
639
+ for (const [idx, slot] of assigned)
640
+ edgeBus[idx] = slot;
641
+ busTracks[r] = count;
642
+ }
643
+ const rankX = new Array(maxRank + 1).fill(0);
644
+ for (let r = 1; r <= maxRank; r++) {
645
+ const gap = Math.max(GAP_X + 1, bandLabel[r - 1] + 3, busTracks[r - 1] + 1);
646
+ rankX[r] = rankX[r - 1] + colW[r - 1] + gap;
647
+ }
648
+ const selfTails = byRank[maxRank]
649
+ .filter((i) => sizes.extraH[i] > 0 && sizes.selfLabelW[i] > 0)
650
+ .map((i) => 2 + sizes.selfLabelW[i]);
651
+ const canvasW = rankX[maxRank] + colW[maxRank] + (selfTails.length === 0 ? 0 : Math.max(...selfTails));
652
+ const bandEnd = Array.from({ length: maxRank + 1 }, (_, r) => rankX[r] + colW[r]);
653
+ const rankStart = rankX;
654
+ let diagramH = 1;
655
+ byRank.forEach((row, r) => {
656
+ const x = rankX[r];
657
+ for (const idx of row) {
658
+ const w = sizes.boxW[idx];
659
+ const h = sizes.boxH[idx];
660
+ const cy = centers[idx];
661
+ const y = sat(cy, half(h + sizes.extraH[idx]));
662
+ placed[idx] = { x, y, w, h, cx: x + half(w), cy: y + half(h), rank: r };
663
+ diagramH = Math.max(diagramH, y + h + sizes.extraH[idx]);
664
+ }
665
+ });
666
+ const edgeLane = new Array(graph.edges.length).fill(0);
667
+ const lanes = laneSpans(graph, ranks, placed, false).filter((s) => !edgeStraight[s.edge]);
668
+ let canvasH = diagramH;
669
+ let laneBase = 0;
670
+ if (lanes.length > 0) {
671
+ const { assigned, count } = assignTracks(lanes, true);
672
+ for (const [idx, slot] of assigned)
673
+ edgeLane[idx] = slot;
674
+ canvasH = diagramH + 1 + count;
675
+ laneBase = diagramH + 1;
676
+ }
677
+ return {
678
+ canvasW,
679
+ canvasH,
680
+ bandEnd,
681
+ rankStart,
682
+ edgeBus,
683
+ laneBase,
684
+ edgeLane,
685
+ skipApproach: new Array(maxRank + 1).fill(-1),
686
+ edgeEntryX: new Array(graph.edges.length).fill(-1),
687
+ edgeStraight,
688
+ fwdEntryX: new Array(graph.nodes.length).fill(-1),
689
+ edgeLabelLeft: new Array(graph.edges.length).fill(false),
690
+ };
691
+ }
692
+ // -------------------------------------------------------------------- canvas
693
+ /** Rank, place, draw and route a graph onto a fresh canvas. */
694
+ export function layoutCanvas(graph, extras) {
695
+ const n = graph.nodes.length;
696
+ if (n === 0)
697
+ return null;
698
+ // Parallel edges ride the same cells, so all labels after the first were
699
+ // silently lost — join them onto the first instead. Done before sizing so
700
+ // the joined label gets its room.
701
+ const firstOf = new Map();
702
+ graph.edges.forEach((e, i) => {
703
+ if (e.from === e.to)
704
+ return;
705
+ const key = `${e.from}>${e.to}`;
706
+ const first = firstOf.get(key);
707
+ if (first === undefined) {
708
+ firstOf.set(key, i);
709
+ return;
710
+ }
711
+ if (e.label !== null) {
712
+ const head = graph.edges[first].label;
713
+ graph.edges[first].label = head === null ? e.label : `${head} / ${e.label}`;
714
+ e.label = null;
715
+ }
716
+ });
717
+ const ranks = computeRanks(graph);
718
+ const maxRank = Math.max(...ranks, 0);
719
+ const byRank = Array.from({ length: maxRank + 1 }, () => []);
720
+ for (let idx = 0; idx < ranks.length; idx++)
721
+ byRank[ranks[idx]].push(idx);
722
+ orderRanks(byRank, graph.edges, ranks);
723
+ // Lane edges exit through the rank's trailing side toward the lane strip;
724
+ // their endpoints go last within the rank, or whatever the ordering put
725
+ // beyond them would sit in that corridor and be cut through.
726
+ const vertical = graph.dir === 'down' || graph.dir === 'up';
727
+ const inLane = new Array(graph.nodes.length).fill(false);
728
+ for (const e of graph.edges) {
729
+ if (vertical && isAdjacentBack(ranks, e))
730
+ continue;
731
+ if (e.from !== e.to && ranks[e.to] !== ranks[e.from] + 1) {
732
+ inLane[e.from] = true;
733
+ inLane[e.to] = true;
734
+ }
735
+ }
736
+ for (const row of byRank)
737
+ row.sort((a, b) => Number(inLane[a]) - Number(inLane[b]));
738
+ const wrapped = graph.nodes.map((node) => wrapLabel(node.label, WRAP_WIDTH, MAX_LINES));
739
+ const widest = (lines) => Math.max(1, lines.length === 0 ? 1 : Math.max(...lines.map(stringWidth)));
740
+ const boxW = extras.map((extra, i) => {
741
+ if (extra.kind === 'frame') {
742
+ return Math.max(extra.sub.w + 2, stringWidth(fitLabel(graph.nodes[i].label, WRAP_WIDTH)) + 4);
743
+ }
744
+ if (extra.kind === 'compartments')
745
+ return widest(extra.sections.flat()) + 2 * PAD + 2;
746
+ return widest(wrapped[i]) + 2 * PAD + 2;
747
+ });
748
+ const boxH = extras.map((extra, i) => {
749
+ if (extra.kind === 'frame')
750
+ return extra.sub.h + 2;
751
+ if (extra.kind === 'compartments') {
752
+ const filled = extra.sections.filter((s) => s.length > 0).length;
753
+ return extra.sections.reduce((s, sec) => s + sec.length, 0) + sat(filled, 1) + 2;
754
+ }
755
+ return wrapped[i].length + 2;
756
+ });
757
+ // A self-edge needs two rows below its box, and room beside it for a label.
758
+ const extraH = new Array(n).fill(0);
759
+ const selfLabelW = new Array(n).fill(0);
760
+ for (const e of graph.edges) {
761
+ if (e.from !== e.to)
762
+ continue;
763
+ extraH[e.from] = 2;
764
+ const text = edgeText(e);
765
+ if (text !== null) {
766
+ selfLabelW[e.from] = Math.max(selfLabelW[e.from], Math.min(stringWidth(text), MAX_LABEL));
767
+ }
768
+ }
769
+ for (let i = 0; i < n; i++)
770
+ if (extraH[i] > 0)
771
+ boxW[i] = Math.max(boxW[i], 7);
772
+ const sizes = {
773
+ boxW,
774
+ boxH,
775
+ layW: boxW.map((w, i) => w + (selfLabelW[i] > 0 ? 2 * (selfLabelW[i] + 3) : 0)),
776
+ layH: boxH.map((h, i) => h + extraH[i]),
777
+ extraH,
778
+ selfLabelW,
779
+ };
780
+ const placed = Array.from({ length: n }, () => ({
781
+ x: 0,
782
+ y: 0,
783
+ w: 0,
784
+ h: 0,
785
+ cx: 0,
786
+ cy: 0,
787
+ rank: 0,
788
+ }));
789
+ const plan = vertical
790
+ ? placeTd(ranks, maxRank, byRank, sizes, graph, placed)
791
+ : placeLr(ranks, maxRank, byRank, sizes, graph, placed);
792
+ if (plan.canvasW * plan.canvasH > MAX_CANVAS_CELLS)
793
+ return null;
794
+ const canvas = new Canvas(plan.canvasW, plan.canvasH);
795
+ for (let idx = 0; idx < n; idx++) {
796
+ const extra = extras[idx];
797
+ canvas.curTag = graph.nodes[idx].classes?.join(' ');
798
+ canvas.curHref = graph.nodes[idx].href;
799
+ // `BT` mirrors the finished canvas; multi-row content draws upside down so
800
+ // the flip restores reading order (flipHorizontal's text runs, vertically).
801
+ const mirrored = graph.dir === 'up';
802
+ if (extra.kind === 'frame')
803
+ drawFrame(canvas, placed[idx], graph.nodes[idx].label, extra.sub, mirrored);
804
+ else if (extra.kind === 'compartments')
805
+ drawClassBox(canvas, placed[idx], extra.sections, mirrored);
806
+ else
807
+ drawBox(canvas, placed[idx], wrapped[idx], graph.nodes[idx].shape, mirrored);
808
+ }
809
+ canvas.curTag = undefined;
810
+ canvas.curHref = undefined;
811
+ const laneLabels = [];
812
+ graph.edges.forEach((edge, i) => {
813
+ canvas.curStyle =
814
+ edge.line === 'dotted' ? STY_DOT : edge.line === 'thick' ? STY_THICK : STY_SOLID;
815
+ if (edge.from === edge.to) {
816
+ routeSelf(canvas, placed[edge.from], edge);
817
+ return;
818
+ }
819
+ const from = placed[edge.from];
820
+ const to = placed[edge.to];
821
+ const adjacent = to.rank === from.rank + 1;
822
+ const adjacentBack = from.rank === to.rank + 1;
823
+ // A back edge crosses the band below its *target's* rank.
824
+ const bus = plan.bandEnd[(adjacentBack ? to : from).rank] + plan.edgeBus[i];
825
+ const lane = plan.laneBase + plan.edgeLane[i];
826
+ if (vertical) {
827
+ if (adjacent)
828
+ routeForward(canvas, from, to, edge, bus, plan.fwdEntryX[edge.to], plan.edgeLabelLeft[i]);
829
+ else if (adjacentBack)
830
+ routeBackAdjacent(canvas, from, to, edge, bus);
831
+ else if (to.rank > from.rank) {
832
+ routeSkip(canvas, from, to, edge, {
833
+ laneX: lane,
834
+ entryX: plan.edgeEntryX[i],
835
+ busY: bus,
836
+ approachY: plan.skipApproach[to.rank],
837
+ straight: plan.edgeStraight[i],
838
+ labelLeft: plan.edgeLabelLeft[i],
839
+ });
840
+ }
841
+ else
842
+ routeBack(canvas, from, to, edge, lane);
843
+ }
844
+ else if (adjacent) {
845
+ routeForwardLr(canvas, from, to, edge, bus);
846
+ }
847
+ else if (to.rank > from.rank && plan.edgeStraight[i]) {
848
+ routeSkipLr(canvas, from, to, edge, bus);
849
+ }
850
+ else {
851
+ routeBackLr(canvas, from, to, edge, lane, laneLabels);
852
+ }
853
+ });
854
+ placeLaneLabels(canvas, laneLabels);
855
+ canvas.finalizeMask();
856
+ return canvas;
857
+ }
858
+ /** Apply the direction flip a finished canvas needs for `BT` / `RL`. */
859
+ export function orient(canvas, graph) {
860
+ if (graph.dir === 'up')
861
+ canvas.flipVertical();
862
+ else if (graph.dir === 'left')
863
+ canvas.flipHorizontal();
864
+ return canvas;
865
+ }
866
+ /** Flowchart and state diagrams: plain boxes, no extra content. */
867
+ export function layoutFlowchart(graph) {
868
+ const extras = graph.nodes.map(() => ({ kind: 'plain' }));
869
+ const canvas = layoutCanvas(graph, extras);
870
+ return canvas && orient(canvas, graph);
871
+ }
872
+ /** Class and ER diagrams: boxes divided into title / attribute / method rows. */
873
+ export function layoutClass(graph) {
874
+ const extras = graph.nodes.map((node) => ({
875
+ kind: 'compartments',
876
+ sections: node.sections ?? [[node.label]],
877
+ }));
878
+ const canvas = layoutCanvas(graph, extras);
879
+ return canvas && orient(canvas, graph);
880
+ }
881
+ /**
882
+ * Lay out a flowchart that uses `subgraph`.
883
+ *
884
+ * Each subgraph becomes a framed box holding its own independently laid-out
885
+ * canvas. An edge is drawn in the innermost scope containing both endpoints;
886
+ * one crossing a subgraph boundary attaches to the frame instead of the node.
887
+ */
888
+ export function layoutGrouped(graph) {
889
+ // A node whose id matches a subgraph id stands in for that subgraph.
890
+ const proxy = new Map();
891
+ graph.groups.forEach((g, gi) => {
892
+ const ni = graph.index.get(g.id);
893
+ if (ni !== undefined)
894
+ proxy.set(ni, gi);
895
+ });
896
+ const groupChain = (g) => {
897
+ const chain = [];
898
+ let cur = g;
899
+ while (cur !== null) {
900
+ chain.push(cur);
901
+ cur = graph.groups[cur].parent;
902
+ }
903
+ return chain.reverse();
904
+ };
905
+ const endpoint = (n) => {
906
+ const gi = proxy.get(n);
907
+ return gi === undefined
908
+ ? { item: { group: false, i: n }, chain: groupChain(graph.nodeGroup[n]) }
909
+ : { item: { group: true, i: gi }, chain: groupChain(graph.groups[gi].parent) };
910
+ };
911
+ /** Edges bucketed by the scope that draws them; `null` is the top level. */
912
+ const scopeEdges = new Map();
913
+ const referenced = new Array(graph.groups.length).fill(false);
914
+ graph.edges.forEach((e, ei) => {
915
+ const f = endpoint(e.from);
916
+ const t = endpoint(e.to);
917
+ let k = 0;
918
+ while (k < f.chain.length && k < t.chain.length && f.chain[k] === t.chain[k])
919
+ k++;
920
+ const scope = k === 0 ? null : f.chain[k - 1];
921
+ const fItem = f.chain.length > k ? { group: true, i: f.chain[k] } : f.item;
922
+ const tItem = t.chain.length > k ? { group: true, i: t.chain[k] } : t.item;
923
+ for (const item of [fItem, tItem]) {
924
+ if (item.group)
925
+ referenced[item.i] = true;
926
+ }
927
+ const list = scopeEdges.get(scope);
928
+ if (list)
929
+ list.push([fItem, tItem, ei]);
930
+ else
931
+ scopeEdges.set(scope, [[fItem, tItem, ei]]);
932
+ });
933
+ const directNodes = new Map();
934
+ graph.nodeGroup.forEach((g, ni) => {
935
+ if (proxy.has(ni))
936
+ return;
937
+ const list = directNodes.get(g);
938
+ if (list)
939
+ list.push(ni);
940
+ else
941
+ directNodes.set(g, [ni]);
942
+ });
943
+ // Drop empty subgraphs, but keep any that an edge attaches to. Walked by
944
+ // the actual child relation: state `--` regions reparent earlier groups
945
+ // under later ones, so index order says nothing about depth.
946
+ const childGroups = graph.groups.map(() => []);
947
+ graph.groups.forEach((g, gi) => {
948
+ if (g.parent !== null)
949
+ childGroups[g.parent].push(gi);
950
+ });
951
+ const keep = new Array(graph.groups.length).fill(false);
952
+ const visit = (gi) => {
953
+ let kept = referenced[gi] || (directNodes.get(gi) ?? []).length > 0;
954
+ for (const c of childGroups[gi])
955
+ if (visit(c))
956
+ kept = true;
957
+ keep[gi] = kept;
958
+ return kept;
959
+ };
960
+ graph.groups.forEach((g, gi) => {
961
+ if (g.parent === null)
962
+ visit(gi);
963
+ });
964
+ const canvas = buildScope(graph, null, scopeEdges, directNodes, keep);
965
+ return canvas && orient(canvas, graph);
966
+ }
967
+ function buildScope(graph, scope, scopeEdges, directNodes, keep) {
968
+ const items = (directNodes.get(scope) ?? []).map((i) => ({ group: false, i }));
969
+ const childGroups = graph.groups
970
+ .map((_, gi) => gi)
971
+ .filter((gi) => graph.groups[gi].parent === scope && keep[gi]);
972
+ items.push(...childGroups.map((i) => ({ group: true, i })));
973
+ if (items.length === 0)
974
+ return new Canvas(1, 1);
975
+ const nodeAt = new Map();
976
+ const groupAt = new Map();
977
+ const nodes = [];
978
+ const extras = [];
979
+ for (const item of items) {
980
+ ;
981
+ (item.group ? groupAt : nodeAt).set(item.i, nodes.length);
982
+ if (!item.group) {
983
+ nodes.push({
984
+ label: graph.nodes[item.i].label,
985
+ shape: graph.nodes[item.i].shape,
986
+ classes: graph.nodes[item.i].classes,
987
+ href: graph.nodes[item.i].href,
988
+ });
989
+ extras.push({ kind: 'plain' });
990
+ }
991
+ else {
992
+ const sub = buildScope(graph, item.i, scopeEdges, directNodes, keep);
993
+ if (sub === null)
994
+ return null;
995
+ nodes.push({ label: graph.groups[item.i].label, shape: 'rect' });
996
+ extras.push({ kind: 'frame', sub });
997
+ }
998
+ }
999
+ const edges = [];
1000
+ for (const [f, t, ei] of scopeEdges.get(scope) ?? []) {
1001
+ const fi = (f.group ? groupAt : nodeAt).get(f.i);
1002
+ const ti = (t.group ? groupAt : nodeAt).get(t.i);
1003
+ if (fi === undefined || ti === undefined)
1004
+ continue;
1005
+ const e = graph.edges[ei];
1006
+ edges.push({
1007
+ from: fi,
1008
+ to: ti,
1009
+ label: e.label,
1010
+ headTo: e.headTo,
1011
+ headFrom: e.headFrom,
1012
+ line: e.line,
1013
+ });
1014
+ }
1015
+ // Layout only reads nodes/edges/dir, so a bare Graph carrying those is enough.
1016
+ const synth = new Graph(graph.dir);
1017
+ synth.nodes = nodes;
1018
+ synth.edges = edges;
1019
+ return layoutCanvas(synth, extras);
1020
+ }
1021
+ // ------------------------------------------------------------------- drawing
1022
+ export function drawBox(canvas, p, lines, shape, mirrored = false) {
1023
+ const { x, y, w, h } = p;
1024
+ const right = x + w - 1;
1025
+ const bottom = y + h - 1;
1026
+ // A diamond is a double-line box — the terminal's nod to `A{...}`.
1027
+ const [tl, tr, bl, br] = shape === 'diamond'
1028
+ ? ['╔', '╗', '╚', '╝']
1029
+ : shape === 'round'
1030
+ ? ['╭', '╮', '╰', '╯']
1031
+ : ['┌', '┐', '└', '┘'];
1032
+ canvas.set(x, y, tl, 'border');
1033
+ canvas.set(right, y, tr, 'border');
1034
+ canvas.set(x, bottom, bl, 'border');
1035
+ canvas.set(right, bottom, br, 'border');
1036
+ if (shape === 'diamond') {
1037
+ // Double lines have no direction bits; edges tee into them through the
1038
+ // mixed junctions (`╤` `╧` `╟` `╢`) that `finalizeMask` resolves.
1039
+ for (let cx = x + 1; cx < right; cx++) {
1040
+ canvas.set(cx, y, '═', 'border');
1041
+ canvas.set(cx, bottom, '═', 'border');
1042
+ }
1043
+ for (let cy = y + 1; cy < bottom; cy++) {
1044
+ canvas.set(x, cy, '║', 'border');
1045
+ canvas.set(right, cy, '║', 'border');
1046
+ }
1047
+ }
1048
+ else {
1049
+ // The perimeter is drawn as bits so edges can tee into it, but it is the
1050
+ // box outline, so it claims `border` rather than `edge`.
1051
+ for (let cx = x + 1; cx < right; cx++) {
1052
+ canvas.addBits(cx, y, L | R, 'border');
1053
+ canvas.addBits(cx, bottom, L | R, 'border');
1054
+ }
1055
+ for (let cy = y + 1; cy < bottom; cy++) {
1056
+ canvas.addBits(x, cy, U | D, 'border');
1057
+ canvas.addBits(right, cy, U | D, 'border');
1058
+ }
1059
+ }
1060
+ for (let cy = y; cy <= bottom; cy++) {
1061
+ for (let cx = x; cx <= right; cx++) {
1062
+ const i = canvas.idx(cx, cy);
1063
+ canvas.occupied[i] = 1;
1064
+ if (canvas.curTag !== undefined)
1065
+ canvas.tag[i] = canvas.curTag;
1066
+ if (canvas.curHref !== undefined)
1067
+ canvas.href[i] = canvas.curHref;
1068
+ }
1069
+ }
1070
+ const inner = Math.max(1, sat(w, 2 * PAD + 2));
1071
+ const ordered = mirrored ? [...lines].reverse() : lines;
1072
+ ordered.forEach((line, li) => {
1073
+ const text = fitLabel(line, inner);
1074
+ const textX = x + 1 + PAD + half(sat(inner, stringWidth(text)));
1075
+ drawText(canvas, text, textX, y + 1 + li, 'text');
1076
+ });
1077
+ }
1078
+ /** A class or ER box: sections separated by horizontal rules, title centred. */
1079
+ function drawClassBox(canvas, p, sections, mirrored = false) {
1080
+ drawBox(canvas, p, [], 'rect');
1081
+ const inner = Math.max(1, sat(p.w, 2 * PAD + 2));
1082
+ const rows = [];
1083
+ sections.forEach((section, si) => {
1084
+ if (section.length === 0)
1085
+ return;
1086
+ if (rows.length > 0)
1087
+ rows.push({ sep: true });
1088
+ for (const line of section)
1089
+ rows.push({ text: fitLabel(line, inner), center: si === 0 });
1090
+ });
1091
+ if (mirrored)
1092
+ rows.reverse();
1093
+ rows.forEach((r, ri) => {
1094
+ const row = p.y + 1 + ri;
1095
+ if (r.sep) {
1096
+ canvas.set(p.x, row, '├', 'border');
1097
+ for (let x = p.x + 1; x < p.x + p.w - 1; x++)
1098
+ canvas.set(x, row, '─', 'border');
1099
+ canvas.set(p.x + p.w - 1, row, '┤', 'border');
1100
+ }
1101
+ else {
1102
+ const tx = r.center ? p.x + 1 + PAD + half(sat(inner, stringWidth(r.text))) : p.x + 1 + PAD;
1103
+ drawTextOverEdges(canvas, r.text, tx, row, 'text');
1104
+ }
1105
+ });
1106
+ }
1107
+ /** A subgraph frame: a titled box with a finished sub-canvas centred inside. */
1108
+ function drawFrame(canvas, p, title, sub, mirrored = false) {
1109
+ drawBox(canvas, p, [], 'rect');
1110
+ // An unlabelled frame (a state `--` region) keeps its border unbroken.
1111
+ if (title !== '') {
1112
+ const t = fitLabel(title, sat(p.w, 4));
1113
+ // Mirrored: the bottom border becomes the top after the flip.
1114
+ drawTextOverEdges(canvas, ` ${t} `, p.x + 1, mirrored ? p.y + p.h - 1 : p.y, 'text');
1115
+ }
1116
+ canvas.blit(sub, p.x + 1 + half(p.w - 2 - sub.w), p.y + 1 + half(p.h - 2 - sub.h));
1117
+ }
1118
+ // ------------------------------------------------------------------- routing
1119
+ function headGlyph(head, arrow) {
1120
+ switch (head) {
1121
+ case 'circle':
1122
+ return 'o';
1123
+ case 'cross':
1124
+ return '×';
1125
+ case 'diamondFill':
1126
+ return '◆';
1127
+ case 'diamondOpen':
1128
+ return '◇';
1129
+ case 'triangle':
1130
+ return { '▼': '▽', '▲': '△', '◄': '◁', '▶': '▷' }[arrow] ?? arrow;
1131
+ default:
1132
+ return arrow;
1133
+ }
1134
+ }
1135
+ /** Adjacent ranks, top-down: drop, jog along the bus row, drop into the head.
1136
+ * `entryX` overrides the centre entry column when the target's top is shared
1137
+ * with skip entries; `labelLeft` renders the label left of the arrowhead. */
1138
+ function routeForward(canvas, from, to, edge, bus, entryX = -1, labelLeft = false) {
1139
+ const tx = entryX === -1 ? to.cx : entryX;
1140
+ // A jog of one column reads as a kink; snap straight instead.
1141
+ const bx = Math.abs(from.cx - tx) <= 1 ? tx : from.cx;
1142
+ const by = from.y + from.h - 1;
1143
+ const headRow = to.y - 1;
1144
+ canvas.junction(bx, by, D);
1145
+ canvas.segV(bx, by, bus);
1146
+ if (bx === tx) {
1147
+ canvas.segV(bx, bus, headRow);
1148
+ }
1149
+ else {
1150
+ canvas.segH(bus, bx, tx);
1151
+ canvas.segV(tx, bus, headRow);
1152
+ }
1153
+ if (edge.headTo === 'none')
1154
+ canvas.addBits(tx, headRow, U);
1155
+ else
1156
+ canvas.set(tx, headRow, headGlyph(edge.headTo, '▼'), 'edge');
1157
+ if (edge.headFrom !== 'none')
1158
+ canvas.set(bx, by, headGlyph(edge.headFrom, '▲'), 'edge');
1159
+ if (edge.cardFrom === undefined && edge.cardTo === undefined) {
1160
+ if (edge.label !== null) {
1161
+ const start = labelLeft ? sat(tx, Math.min(stringWidth(edge.label), MAX_LABEL)) : tx + 1;
1162
+ placeLabel(canvas, edge.label, headRow, start);
1163
+ }
1164
+ return;
1165
+ }
1166
+ // Cardinalities sit at their own ends; the verb takes the row above the
1167
+ // head, falling back beside the target card when the gap has no spare row.
1168
+ const srcRow = by + 1;
1169
+ if (edge.cardFrom !== undefined)
1170
+ placeLabel(canvas, edge.cardFrom, srcRow, bx + 1);
1171
+ if (edge.cardTo !== undefined)
1172
+ placeLabel(canvas, edge.cardTo, headRow, tx + 1);
1173
+ if (edge.label !== null) {
1174
+ const midRow = headRow - 1;
1175
+ if (midRow > srcRow) {
1176
+ const lineX = midRow > bus ? tx : bx;
1177
+ placeLabel(canvas, edge.label, midRow, lineX + 1);
1178
+ }
1179
+ else {
1180
+ placeLabel(canvas, edge.label, headRow, tx + 1 + (edge.cardTo === undefined ? 0 : stringWidth(edge.cardTo) + 1));
1181
+ }
1182
+ }
1183
+ }
1184
+ /**
1185
+ * Adjacent ranks, top-down, against the flow: up out of the source's top,
1186
+ * jog along the band, arrow into the target's bottom. The short local return
1187
+ * mermaid draws — not a trip around the diagram.
1188
+ */
1189
+ function routeBackAdjacent(canvas, from, to, edge, bus) {
1190
+ // Attach right of centre so the return does not merge with the forward
1191
+ // exits and arrivals that own the centre column.
1192
+ const tx = Math.min(to.x + to.w - 2, to.cx + 2);
1193
+ const bx0 = Math.min(from.x + from.w - 2, from.cx + 2);
1194
+ const bx = Math.abs(bx0 - tx) <= 1 ? tx : bx0;
1195
+ const fy = from.y;
1196
+ const headRow = to.y + to.h;
1197
+ canvas.junction(bx, fy, U);
1198
+ if (bx === tx) {
1199
+ canvas.segV(bx, headRow, fy);
1200
+ }
1201
+ else {
1202
+ canvas.segV(bx, bus, fy);
1203
+ canvas.segH(bus, bx, tx);
1204
+ canvas.segV(tx, headRow, bus);
1205
+ }
1206
+ if (edge.headTo === 'none')
1207
+ canvas.addBits(tx, headRow, D);
1208
+ else
1209
+ canvas.set(tx, headRow, headGlyph(edge.headTo, '▲'), 'edge');
1210
+ if (edge.headFrom !== 'none')
1211
+ canvas.set(bx, fy, headGlyph(edge.headFrom, '▼'), 'edge');
1212
+ const text = edgeText(edge);
1213
+ if (text !== null)
1214
+ placeLabel(canvas, text, headRow, tx + 1);
1215
+ }
1216
+ /** A self-edge: a stub loop hanging below the box. */
1217
+ function routeSelf(canvas, p, edge) {
1218
+ const bottom = p.y + p.h - 1;
1219
+ const exitX = p.cx + 1;
1220
+ const retX = p.x + p.w - 2;
1221
+ if (retX <= exitX || bottom + 2 >= canvas.h)
1222
+ return;
1223
+ const [v, h, bl, br] = edge.line === 'dotted'
1224
+ ? ['╎', '╌', '╰', '╯']
1225
+ : edge.line === 'thick'
1226
+ ? ['┃', '━', '┗', '┛']
1227
+ : ['│', '─', '╰', '╯'];
1228
+ canvas.junction(exitX, bottom, D);
1229
+ canvas.set(exitX, bottom + 1, v, 'edge');
1230
+ canvas.set(exitX, bottom + 2, bl, 'edge');
1231
+ for (let x = exitX + 1; x < retX; x++)
1232
+ canvas.set(x, bottom + 2, h, 'edge');
1233
+ canvas.set(retX, bottom + 2, br, 'edge');
1234
+ canvas.set(retX, bottom + 1, headGlyph(edge.headTo, '▲'), 'edge');
1235
+ const selfText = edgeText(edge);
1236
+ if (selfText !== null)
1237
+ placeLabel(canvas, selfText, bottom + 1, p.x + p.w + 1);
1238
+ }
1239
+ /**
1240
+ * Forward skip edge, top-down: out the source's *bottom* onto its band's bus
1241
+ * row (endpoint sharing folds it onto its siblings' row — one `┴` origin
1242
+ * split), then either straight down an unobstructed column into the
1243
+ * target's *top*, or around: down the lane and in along the reserved
1244
+ * approach row above the target's rank, over the arrival heads and labels.
1245
+ */
1246
+ function routeSkip(canvas, from, to, edge, at) {
1247
+ const { laneX, entryX, busY, approachY, straight, labelLeft } = at;
1248
+ const bx = from.cx;
1249
+ const bottom = from.y + from.h - 1;
1250
+ canvas.junction(bx, bottom, D);
1251
+ canvas.segV(bx, bottom, busY);
1252
+ if (straight) {
1253
+ canvas.segH(busY, bx, entryX);
1254
+ canvas.segV(entryX, busY, to.y - 1);
1255
+ }
1256
+ else {
1257
+ canvas.segH(busY, bx, laneX);
1258
+ canvas.segV(laneX, busY, approachY);
1259
+ canvas.segH(approachY, entryX, laneX);
1260
+ canvas.segV(entryX, approachY, to.y - 1);
1261
+ }
1262
+ if (edge.headTo === 'none')
1263
+ canvas.addBits(entryX, to.y - 1, D);
1264
+ else
1265
+ canvas.set(entryX, to.y - 1, headGlyph(edge.headTo, '▼'), 'edge');
1266
+ if (edge.headFrom !== 'none')
1267
+ canvas.set(bx, bottom, headGlyph(edge.headFrom, '▲'), 'edge');
1268
+ const text = edgeText(edge);
1269
+ if (text !== null) {
1270
+ const start = labelLeft ? sat(entryX, Math.min(stringWidth(text), MAX_LABEL)) : entryX + 1;
1271
+ placeLabel(canvas, text, to.y - 1, start);
1272
+ }
1273
+ }
1274
+ /**
1275
+ * Multi-rank back edge, top-down: out the right side, up a lane, back in
1276
+ * through the target's right side — side entry keeps returns recognisable
1277
+ * at a glance.
1278
+ */
1279
+ function routeBack(canvas, from, to, edge, laneX) {
1280
+ const sx = from.x + from.w - 1;
1281
+ const sy = from.cy;
1282
+ const backText = edgeText(edge);
1283
+ canvas.junction(sx, sy, R);
1284
+ canvas.segH(sy, sx, laneX);
1285
+ if (edge.headFrom !== 'none')
1286
+ canvas.set(sx, sy, headGlyph(edge.headFrom, '◄'), 'edge');
1287
+ const tx = to.x + to.w - 1;
1288
+ const tyc = to.cy;
1289
+ canvas.segV(laneX, sy, tyc);
1290
+ canvas.segH(tyc, tx + 1, laneX);
1291
+ if (edge.headTo === 'none')
1292
+ canvas.addBits(tx + 1, tyc, R);
1293
+ else
1294
+ canvas.set(tx + 1, tyc, headGlyph(edge.headTo, '◄'), 'edge');
1295
+ if (backText !== null) {
1296
+ placeLabel(canvas, backText, sat(tyc, 1), sat(laneX, stringWidth(backText) + 1));
1297
+ }
1298
+ }
1299
+ /** Adjacent ranks, left-to-right: out the right side, jog on the bus column. */
1300
+ function routeForwardLr(canvas, from, to, edge, bus) {
1301
+ const rx = from.x + from.w - 1;
1302
+ const ry = from.cy;
1303
+ const ly = to.cy;
1304
+ const headCol = to.x - 1;
1305
+ canvas.junction(rx, ry, R);
1306
+ canvas.segH(ry, rx, bus);
1307
+ if (ry === ly) {
1308
+ canvas.segH(ry, bus, headCol);
1309
+ }
1310
+ else {
1311
+ canvas.segV(bus, ry, ly);
1312
+ canvas.segH(ly, bus, headCol);
1313
+ }
1314
+ if (edge.headTo === 'none')
1315
+ canvas.addBits(headCol, ly, R);
1316
+ else
1317
+ canvas.set(headCol, ly, headGlyph(edge.headTo, '▶'), 'edge');
1318
+ if (edge.headFrom !== 'none')
1319
+ canvas.set(rx, ry, headGlyph(edge.headFrom, '◄'), 'edge');
1320
+ // The verb keeps its usual spot above the line; cardinalities hug their
1321
+ // own ends on the rows above the departure and arrival cells.
1322
+ if (edge.label !== null)
1323
+ placeLabel(canvas, edge.label, sat(ly, 1), bus + 1);
1324
+ if (edge.cardFrom !== undefined)
1325
+ placeLabel(canvas, edge.cardFrom, sat(ry, 1), rx + 1);
1326
+ if (edge.cardTo !== undefined) {
1327
+ placeLabel(canvas, edge.cardTo, sat(ly, 1), sat(headCol, stringWidth(edge.cardTo)));
1328
+ }
1329
+ }
1330
+ /**
1331
+ * Straight forward skip, left-to-right: out the source's right-side fan, jog
1332
+ * on its own band's bus column, then straight along the target's entry row
1333
+ * into its left side — merging with the forward arrivals' head. Only used
1334
+ * when that row crosses no intermediate box; the bottom lane is the fallback.
1335
+ */
1336
+ function routeSkipLr(canvas, from, to, edge, bus) {
1337
+ const ry = from.cy;
1338
+ const ty = to.cy;
1339
+ const rx = from.x + from.w - 1;
1340
+ const headCol = to.x - 1;
1341
+ canvas.junction(rx, ry, R);
1342
+ canvas.segH(ry, rx, bus);
1343
+ if (ry !== ty)
1344
+ canvas.segV(bus, ry, ty);
1345
+ canvas.segH(ty, bus, headCol);
1346
+ if (edge.headTo === 'none')
1347
+ canvas.addBits(headCol, ty, R);
1348
+ else
1349
+ canvas.set(headCol, ty, headGlyph(edge.headTo, '▶'), 'edge');
1350
+ if (edge.headFrom !== 'none')
1351
+ canvas.set(rx, ry, headGlyph(edge.headFrom, '◄'), 'edge');
1352
+ // Label after the bus jog, where forward labels sit — the gap before the
1353
+ // target belongs to the arrivals that end there.
1354
+ const text = edgeText(edge);
1355
+ if (text !== null)
1356
+ placeLabel(canvas, text, sat(ty, 1), bus + 1);
1357
+ }
1358
+ /** Skip or back edge, left-to-right: down out the bottom, along a lane, back up. */
1359
+ function routeBackLr(canvas, from, to, edge, laneY, laneLabels) {
1360
+ const sx = from.cx;
1361
+ const sy = from.y + from.h - 1;
1362
+ const tx = to.cx;
1363
+ const ty = to.y + to.h - 1;
1364
+ canvas.junction(sx, sy, D);
1365
+ canvas.segV(sx, sy, laneY);
1366
+ canvas.segH(laneY, sx, tx);
1367
+ canvas.segV(tx, laneY, ty + 1);
1368
+ if (edge.headTo === 'none')
1369
+ canvas.addBits(tx, ty + 1, D);
1370
+ else
1371
+ canvas.set(tx, ty + 1, headGlyph(edge.headTo, '▲'), 'edge');
1372
+ if (edge.headFrom !== 'none')
1373
+ canvas.set(sx, sy, headGlyph(edge.headFrom, '▲'), 'edge');
1374
+ // The label interrupts its own lane row — the row above belongs to the
1375
+ // neighbouring lane once several stack. Deferred until all edges landed,
1376
+ // so it can dodge the verticals that cross this row.
1377
+ const backText = edgeText(edge);
1378
+ if (backText !== null) {
1379
+ laneLabels.push({
1380
+ text: ` ${fitLabel(backText, MAX_LABEL)} `,
1381
+ y: laneY,
1382
+ lo: Math.min(sx, tx),
1383
+ hi: Math.max(sx, tx),
1384
+ });
1385
+ }
1386
+ }
1387
+ /**
1388
+ * Write each lane label onto its own row, centred on the run but slid to
1389
+ * the nearest stretch free of crossing verticals, arrowheads and earlier
1390
+ * labels — clearing a crossing line under a label would sever it.
1391
+ */
1392
+ function placeLaneLabels(canvas, labels) {
1393
+ for (const { text, y, lo, hi } of labels) {
1394
+ const tw = stringWidth(text);
1395
+ const lastStart = hi - 1 - tw;
1396
+ if (lastStart < lo + 1 || y >= canvas.h)
1397
+ continue;
1398
+ const clear = (start) => {
1399
+ for (let x = start; x < start + tw; x++) {
1400
+ const i = canvas.idx(x, y);
1401
+ if (canvas.occupied[i] === 1)
1402
+ return false;
1403
+ if ((canvas.mask[i] & (U | D)) !== 0)
1404
+ return false;
1405
+ if (canvas.ch[i] !== ' ')
1406
+ return false;
1407
+ }
1408
+ return true;
1409
+ };
1410
+ const mid = Math.min(Math.max(half(lo + hi) - half(tw), lo + 1), lastStart);
1411
+ let at = mid;
1412
+ for (let d = 0;; d++) {
1413
+ const left = mid - d;
1414
+ const right = mid + d;
1415
+ if (left < lo + 1 && right > lastStart)
1416
+ break;
1417
+ if (left >= lo + 1 && clear(left)) {
1418
+ at = left;
1419
+ break;
1420
+ }
1421
+ if (right <= lastStart && clear(right)) {
1422
+ at = right;
1423
+ break;
1424
+ }
1425
+ }
1426
+ drawTextOverEdges(canvas, text, at, y, 'edgeLabel');
1427
+ }
1428
+ }
1429
+ /** Write an edge label, stopping at the first cell already occupied. */
1430
+ function placeLabel(canvas, label, row, startX) {
1431
+ if (row >= canvas.h)
1432
+ return;
1433
+ const text = fitLabel(label, MAX_LABEL);
1434
+ let x = startX;
1435
+ for (const [c, cw] of measured(text)) {
1436
+ if (cw === 0)
1437
+ continue;
1438
+ if (x + cw > canvas.w)
1439
+ break;
1440
+ let blocked = false;
1441
+ for (let k = 0; k < cw; k++) {
1442
+ const i = canvas.idx(x + k, row);
1443
+ if (canvas.ch[i] !== ' ' || canvas.mask[i] !== 0 || canvas.occupied[i])
1444
+ blocked = true;
1445
+ }
1446
+ if (blocked)
1447
+ break;
1448
+ canvas.set(x, row, c, 'edgeLabel');
1449
+ for (let k = 1; k < cw; k++)
1450
+ canvas.set(x + k, row, CONT, 'edgeLabel');
1451
+ x += cw;
1452
+ }
1453
+ }
1454
+ //# sourceMappingURL=layout.js.map