@oh-my-pi/pi-utils 16.0.6 → 16.0.8

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 (87) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/types/mermaid-ascii.d.ts +1 -1
  3. package/dist/types/vendor/mermaid-ascii/ascii/ansi.d.ts +41 -0
  4. package/dist/types/vendor/mermaid-ascii/ascii/canvas.d.ts +89 -0
  5. package/dist/types/vendor/mermaid-ascii/ascii/class-diagram.d.ts +7 -0
  6. package/dist/types/vendor/mermaid-ascii/ascii/converter.d.ts +12 -0
  7. package/dist/types/vendor/mermaid-ascii/ascii/draw.d.ts +66 -0
  8. package/dist/types/vendor/mermaid-ascii/ascii/edge-bundling.d.ts +48 -0
  9. package/dist/types/vendor/mermaid-ascii/ascii/edge-routing.d.ts +43 -0
  10. package/dist/types/vendor/mermaid-ascii/ascii/er-diagram.d.ts +7 -0
  11. package/dist/types/vendor/mermaid-ascii/ascii/grid.d.ts +56 -0
  12. package/dist/types/vendor/mermaid-ascii/ascii/index.d.ts +65 -0
  13. package/dist/types/vendor/mermaid-ascii/ascii/multiline-utils.d.ts +27 -0
  14. package/dist/types/vendor/mermaid-ascii/ascii/pathfinder.d.ts +17 -0
  15. package/dist/types/vendor/mermaid-ascii/ascii/sequence.d.ts +7 -0
  16. package/dist/types/vendor/mermaid-ascii/ascii/shapes/circle.d.ts +11 -0
  17. package/dist/types/vendor/mermaid-ascii/ascii/shapes/corners.d.ts +34 -0
  18. package/dist/types/vendor/mermaid-ascii/ascii/shapes/diamond.d.ts +11 -0
  19. package/dist/types/vendor/mermaid-ascii/ascii/shapes/hexagon.d.ts +11 -0
  20. package/dist/types/vendor/mermaid-ascii/ascii/shapes/index.d.ts +26 -0
  21. package/dist/types/vendor/mermaid-ascii/ascii/shapes/rectangle.d.ts +31 -0
  22. package/dist/types/vendor/mermaid-ascii/ascii/shapes/rounded.d.ts +11 -0
  23. package/dist/types/vendor/mermaid-ascii/ascii/shapes/special.d.ts +59 -0
  24. package/dist/types/vendor/mermaid-ascii/ascii/shapes/stadium.d.ts +17 -0
  25. package/dist/types/vendor/mermaid-ascii/ascii/shapes/state.d.ts +30 -0
  26. package/dist/types/vendor/mermaid-ascii/ascii/shapes/types.d.ts +55 -0
  27. package/dist/types/vendor/mermaid-ascii/ascii/types.d.ts +206 -0
  28. package/dist/types/vendor/mermaid-ascii/ascii/validate.d.ts +51 -0
  29. package/dist/types/vendor/mermaid-ascii/ascii/xychart.d.ts +2 -0
  30. package/dist/types/vendor/mermaid-ascii/class/parser.d.ts +6 -0
  31. package/dist/types/vendor/mermaid-ascii/class/types.d.ts +102 -0
  32. package/dist/types/vendor/mermaid-ascii/er/parser.d.ts +6 -0
  33. package/dist/types/vendor/mermaid-ascii/er/types.d.ts +76 -0
  34. package/dist/types/vendor/mermaid-ascii/index.d.ts +1 -0
  35. package/dist/types/vendor/mermaid-ascii/multiline-utils.d.ts +9 -0
  36. package/dist/types/vendor/mermaid-ascii/parser.d.ts +7 -0
  37. package/dist/types/vendor/mermaid-ascii/sequence/parser.d.ts +6 -0
  38. package/dist/types/vendor/mermaid-ascii/sequence/types.d.ts +130 -0
  39. package/dist/types/vendor/mermaid-ascii/text-metrics.d.ts +21 -0
  40. package/dist/types/vendor/mermaid-ascii/types.d.ts +114 -0
  41. package/dist/types/vendor/mermaid-ascii/xychart/colors.d.ts +25 -0
  42. package/dist/types/vendor/mermaid-ascii/xychart/parser.d.ts +6 -0
  43. package/dist/types/vendor/mermaid-ascii/xychart/types.d.ts +145 -0
  44. package/package.json +2 -3
  45. package/src/mermaid-ascii.ts +1 -1
  46. package/src/vendor/mermaid-ascii/NOTICE +33 -0
  47. package/src/vendor/mermaid-ascii/ascii/ansi.ts +409 -0
  48. package/src/vendor/mermaid-ascii/ascii/canvas.ts +476 -0
  49. package/src/vendor/mermaid-ascii/ascii/class-diagram.ts +699 -0
  50. package/src/vendor/mermaid-ascii/ascii/converter.ts +271 -0
  51. package/src/vendor/mermaid-ascii/ascii/draw.ts +1382 -0
  52. package/src/vendor/mermaid-ascii/ascii/edge-bundling.ts +328 -0
  53. package/src/vendor/mermaid-ascii/ascii/edge-routing.ts +297 -0
  54. package/src/vendor/mermaid-ascii/ascii/er-diagram.ts +441 -0
  55. package/src/vendor/mermaid-ascii/ascii/grid.ts +578 -0
  56. package/src/vendor/mermaid-ascii/ascii/index.ts +187 -0
  57. package/src/vendor/mermaid-ascii/ascii/multiline-utils.ts +78 -0
  58. package/src/vendor/mermaid-ascii/ascii/pathfinder.ts +215 -0
  59. package/src/vendor/mermaid-ascii/ascii/sequence.ts +460 -0
  60. package/src/vendor/mermaid-ascii/ascii/shapes/circle.ts +27 -0
  61. package/src/vendor/mermaid-ascii/ascii/shapes/corners.ts +127 -0
  62. package/src/vendor/mermaid-ascii/ascii/shapes/diamond.ts +27 -0
  63. package/src/vendor/mermaid-ascii/ascii/shapes/hexagon.ts +27 -0
  64. package/src/vendor/mermaid-ascii/ascii/shapes/index.ts +101 -0
  65. package/src/vendor/mermaid-ascii/ascii/shapes/rectangle.ts +175 -0
  66. package/src/vendor/mermaid-ascii/ascii/shapes/rounded.ts +27 -0
  67. package/src/vendor/mermaid-ascii/ascii/shapes/special.ts +296 -0
  68. package/src/vendor/mermaid-ascii/ascii/shapes/stadium.ts +114 -0
  69. package/src/vendor/mermaid-ascii/ascii/shapes/state.ts +192 -0
  70. package/src/vendor/mermaid-ascii/ascii/shapes/types.ts +73 -0
  71. package/src/vendor/mermaid-ascii/ascii/types.ts +273 -0
  72. package/src/vendor/mermaid-ascii/ascii/validate.ts +120 -0
  73. package/src/vendor/mermaid-ascii/ascii/xychart.ts +875 -0
  74. package/src/vendor/mermaid-ascii/class/parser.ts +290 -0
  75. package/src/vendor/mermaid-ascii/class/types.ts +121 -0
  76. package/src/vendor/mermaid-ascii/er/parser.ts +181 -0
  77. package/src/vendor/mermaid-ascii/er/types.ts +91 -0
  78. package/src/vendor/mermaid-ascii/index.ts +14 -0
  79. package/src/vendor/mermaid-ascii/multiline-utils.ts +30 -0
  80. package/src/vendor/mermaid-ascii/parser.ts +645 -0
  81. package/src/vendor/mermaid-ascii/sequence/parser.ts +207 -0
  82. package/src/vendor/mermaid-ascii/sequence/types.ts +146 -0
  83. package/src/vendor/mermaid-ascii/text-metrics.ts +71 -0
  84. package/src/vendor/mermaid-ascii/types.ts +164 -0
  85. package/src/vendor/mermaid-ascii/xychart/colors.ts +140 -0
  86. package/src/vendor/mermaid-ascii/xychart/parser.ts +115 -0
  87. package/src/vendor/mermaid-ascii/xychart/types.ts +150 -0
@@ -0,0 +1,578 @@
1
+ // ============================================================================
2
+ // ASCII renderer — grid-based layout
3
+ //
4
+ // Ported from AlexanderGrooff/mermaid-ascii cmd/graph.go + cmd/mapping_node.go.
5
+ // Places nodes on a logical grid, computes column/row sizes,
6
+ // converts grid coordinates to character-level drawing coordinates,
7
+ // and handles subgraph bounding boxes.
8
+ // ============================================================================
9
+
10
+ import type {
11
+ GridCoord, DrawingCoord, Direction, AsciiGraph, AsciiNode, AsciiSubgraph,
12
+ } from './types'
13
+ import { gridKey } from './types'
14
+ import { mkCanvas, setCanvasSizeToGrid, setRoleCanvasSizeToGrid } from './canvas'
15
+ import { determinePath, determineLabelLine } from './edge-routing'
16
+ import { analyzeEdgeBundles, processBundles } from './edge-bundling'
17
+ import { drawBox } from './draw'
18
+ import { maxLineWidth, lineCount } from './multiline-utils'
19
+ import { getShapeDimensions } from './shapes/index'
20
+
21
+ // ============================================================================
22
+ // Grid coordinate → drawing coordinate conversion
23
+ // ============================================================================
24
+
25
+ /**
26
+ * Convert a grid coordinate to a drawing (character) coordinate.
27
+ * Sums column widths up to the target column, and row heights up to the target row,
28
+ * then centers within the cell.
29
+ */
30
+ export function gridToDrawingCoord(
31
+ graph: AsciiGraph,
32
+ c: GridCoord,
33
+ dir?: Direction,
34
+ ): DrawingCoord {
35
+ const target: GridCoord = dir
36
+ ? { x: c.x + dir.x, y: c.y + dir.y }
37
+ : c
38
+
39
+ let x = 0
40
+ for (let col = 0; col < target.x; col++) {
41
+ x += graph.columnWidth.get(col) ?? 0
42
+ }
43
+
44
+ let y = 0
45
+ for (let row = 0; row < target.y; row++) {
46
+ y += graph.rowHeight.get(row) ?? 0
47
+ }
48
+
49
+ const colW = graph.columnWidth.get(target.x) ?? 0
50
+ const rowH = graph.rowHeight.get(target.y) ?? 0
51
+ return {
52
+ x: x + Math.floor(colW / 2) + graph.offsetX,
53
+ y: y + Math.floor(rowH / 2) + graph.offsetY,
54
+ }
55
+ }
56
+
57
+ /** Convert a path of grid coords to drawing coords. */
58
+ export function lineToDrawing(graph: AsciiGraph, line: GridCoord[]): DrawingCoord[] {
59
+ return line.map(c => gridToDrawingCoord(graph, c))
60
+ }
61
+
62
+ // ============================================================================
63
+ // Node placement on the grid
64
+ // ============================================================================
65
+
66
+ /**
67
+ * Reserve a 3x3 block in the grid for a node.
68
+ * If the requested position is occupied, recursively shift by 4 grid units
69
+ * (in the perpendicular direction based on effective direction) until a free spot is found.
70
+ *
71
+ * @param effectiveDir - Optional direction override. If not provided, uses the node's
72
+ * effective direction (subgraph direction if in a subgraph with override,
73
+ * otherwise graph direction).
74
+ */
75
+ export function reserveSpotInGrid(
76
+ graph: AsciiGraph,
77
+ node: AsciiNode,
78
+ requested: GridCoord,
79
+ effectiveDir?: 'LR' | 'TD',
80
+ ): GridCoord {
81
+ // Determine direction for collision handling
82
+ const dir = effectiveDir ?? getEffectiveDirection(graph, node)
83
+
84
+ if (graph.grid.has(gridKey(requested))) {
85
+ // Collision — shift perpendicular to main flow direction
86
+ if (dir === 'LR') {
87
+ return reserveSpotInGrid(graph, node, { x: requested.x, y: requested.y + 4 }, dir)
88
+ } else {
89
+ return reserveSpotInGrid(graph, node, { x: requested.x + 4, y: requested.y }, dir)
90
+ }
91
+ }
92
+
93
+ // Reserve the 3x3 block
94
+ for (let dx = 0; dx < 3; dx++) {
95
+ for (let dy = 0; dy < 3; dy++) {
96
+ const reserved: GridCoord = { x: requested.x + dx, y: requested.y + dy }
97
+ graph.grid.set(gridKey(reserved), node)
98
+ }
99
+ }
100
+
101
+ node.gridCoord = requested
102
+ return requested
103
+ }
104
+
105
+ // ============================================================================
106
+ // Column width / row height computation
107
+ // ============================================================================
108
+
109
+ /**
110
+ * Set column widths and row heights for a node's 3x3 grid block.
111
+ * Each node occupies 3 columns (border, content, border) and 3 rows.
112
+ * Uses shape-aware dimensions to properly size non-rectangular shapes.
113
+ */
114
+ export function setColumnWidth(graph: AsciiGraph, node: AsciiNode): void {
115
+ const gc = node.gridCoord!
116
+ const padding = graph.config.boxBorderPadding
117
+
118
+ // Get shape-aware dimensions
119
+ const shapeDims = getShapeDimensions(node.shape, node.displayLabel, {
120
+ useAscii: graph.config.useAscii,
121
+ padding,
122
+ })
123
+
124
+ // Use shape-provided grid dimensions
125
+ const colWidths = shapeDims.gridColumns
126
+ const rowHeights = shapeDims.gridRows
127
+
128
+ for (let idx = 0; idx < colWidths.length; idx++) {
129
+ const xCoord = gc.x + idx
130
+ const current = graph.columnWidth.get(xCoord) ?? 0
131
+ graph.columnWidth.set(xCoord, Math.max(current, colWidths[idx]!))
132
+ }
133
+
134
+ for (let idx = 0; idx < rowHeights.length; idx++) {
135
+ const yCoord = gc.y + idx
136
+ const current = graph.rowHeight.get(yCoord) ?? 0
137
+ graph.rowHeight.set(yCoord, Math.max(current, rowHeights[idx]!))
138
+ }
139
+
140
+ // Padding column/row before the node (spacing between nodes)
141
+ if (gc.x > 0) {
142
+ const current = graph.columnWidth.get(gc.x - 1) ?? 0
143
+ graph.columnWidth.set(gc.x - 1, Math.max(current, graph.config.paddingX))
144
+ }
145
+
146
+ if (gc.y > 0) {
147
+ let basePadding = graph.config.paddingY
148
+ // Extra vertical padding for nodes with incoming edges from outside their subgraph
149
+ if (hasIncomingEdgeFromOutsideSubgraph(graph, node)) {
150
+ const subgraphOverhead = 4
151
+ basePadding += subgraphOverhead
152
+ }
153
+ const current = graph.rowHeight.get(gc.y - 1) ?? 0
154
+ graph.rowHeight.set(gc.y - 1, Math.max(current, basePadding))
155
+ }
156
+ }
157
+
158
+ /** Ensure grid has width/height entries for all cells along an edge path. */
159
+ export function increaseGridSizeForPath(graph: AsciiGraph, path: GridCoord[]): void {
160
+ for (const c of path) {
161
+ if (!graph.columnWidth.has(c.x)) {
162
+ graph.columnWidth.set(c.x, Math.floor(graph.config.paddingX / 2))
163
+ }
164
+ if (!graph.rowHeight.has(c.y)) {
165
+ graph.rowHeight.set(c.y, Math.floor(graph.config.paddingY / 2))
166
+ }
167
+ }
168
+ }
169
+
170
+ // ============================================================================
171
+ // Subgraph helpers
172
+ // ============================================================================
173
+
174
+ function isNodeInAnySubgraph(graph: AsciiGraph, node: AsciiNode): boolean {
175
+ return graph.subgraphs.some(sg => sg.nodes.includes(node))
176
+ }
177
+
178
+ /**
179
+ * Get the innermost subgraph that directly contains this node.
180
+ * Returns null if node is not in any subgraph.
181
+ */
182
+ export function getNodeSubgraph(graph: AsciiGraph, node: AsciiNode): AsciiSubgraph | null {
183
+ // Find the innermost (most deeply nested) subgraph containing the node
184
+ let innermost: AsciiSubgraph | null = null
185
+ for (const sg of graph.subgraphs) {
186
+ if (sg.nodes.includes(node)) {
187
+ // Check if this subgraph is deeper (more nested) than current innermost
188
+ if (!innermost || isAncestorOrSelf(innermost, sg)) {
189
+ innermost = sg
190
+ }
191
+ }
192
+ }
193
+ return innermost
194
+ }
195
+
196
+ /** Check if `candidate` is the same as or an ancestor of `target`. */
197
+ function isAncestorOrSelf(candidate: AsciiSubgraph, target: AsciiSubgraph): boolean {
198
+ let current: AsciiSubgraph | null = target
199
+ while (current !== null) {
200
+ if (current === candidate) return true
201
+ current = current.parent
202
+ }
203
+ return false
204
+ }
205
+
206
+ /**
207
+ * Get the effective direction for a node's layout.
208
+ * Returns the subgraph's direction override if the node is in a subgraph with one,
209
+ * otherwise returns the graph-level direction.
210
+ */
211
+ export function getEffectiveDirection(graph: AsciiGraph, node: AsciiNode): 'LR' | 'TD' {
212
+ const sg = getNodeSubgraph(graph, node)
213
+ if (sg?.direction) {
214
+ return sg.direction
215
+ }
216
+ return graph.config.graphDirection
217
+ }
218
+
219
+ /**
220
+ * Check if a node has an incoming edge from outside its subgraph
221
+ * AND is the topmost such node in its subgraph.
222
+ * Used to add extra vertical padding for subgraph borders.
223
+ */
224
+ function hasIncomingEdgeFromOutsideSubgraph(graph: AsciiGraph, node: AsciiNode): boolean {
225
+ const nodeSg = getNodeSubgraph(graph, node)
226
+ if (!nodeSg) return false
227
+
228
+ let hasExternalEdge = false
229
+ for (const edge of graph.edges) {
230
+ if (edge.to === node) {
231
+ const sourceSg = getNodeSubgraph(graph, edge.from)
232
+ if (sourceSg !== nodeSg) {
233
+ hasExternalEdge = true
234
+ break
235
+ }
236
+ }
237
+ }
238
+
239
+ if (!hasExternalEdge) return false
240
+
241
+ // Only return true for the topmost node with an external incoming edge
242
+ for (const otherNode of nodeSg.nodes) {
243
+ if (otherNode === node || !otherNode.gridCoord) continue
244
+ let otherHasExternal = false
245
+ for (const edge of graph.edges) {
246
+ if (edge.to === otherNode) {
247
+ const sourceSg = getNodeSubgraph(graph, edge.from)
248
+ if (sourceSg !== nodeSg) {
249
+ otherHasExternal = true
250
+ break
251
+ }
252
+ }
253
+ }
254
+ if (otherHasExternal && otherNode.gridCoord.y < node.gridCoord!.y) {
255
+ return false
256
+ }
257
+ }
258
+
259
+ return true
260
+ }
261
+
262
+ // ============================================================================
263
+ // Subgraph bounding boxes
264
+ // ============================================================================
265
+
266
+ function calculateSubgraphBoundingBox(graph: AsciiGraph, sg: AsciiSubgraph): void {
267
+ if (sg.nodes.length === 0) return
268
+
269
+ let minX = 1_000_000
270
+ let minY = 1_000_000
271
+ let maxX = -1_000_000
272
+ let maxY = -1_000_000
273
+
274
+ // Include children's bounding boxes
275
+ for (const child of sg.children) {
276
+ calculateSubgraphBoundingBox(graph, child)
277
+ if (child.nodes.length > 0) {
278
+ minX = Math.min(minX, child.minX)
279
+ minY = Math.min(minY, child.minY)
280
+ maxX = Math.max(maxX, child.maxX)
281
+ maxY = Math.max(maxY, child.maxY)
282
+ }
283
+ }
284
+
285
+ // Include node positions
286
+ for (const node of sg.nodes) {
287
+ if (!node.drawingCoord || !node.drawing) continue
288
+ const nodeMinX = node.drawingCoord.x
289
+ const nodeMinY = node.drawingCoord.y
290
+ const nodeMaxX = nodeMinX + node.drawing.length - 1
291
+ const nodeMaxY = nodeMinY + node.drawing[0]!.length - 1
292
+ minX = Math.min(minX, nodeMinX)
293
+ minY = Math.min(minY, nodeMinY)
294
+ maxX = Math.max(maxX, nodeMaxX)
295
+ maxY = Math.max(maxY, nodeMaxY)
296
+ }
297
+
298
+ const subgraphPadding = 2
299
+ const subgraphLabelSpace = 2
300
+ sg.minX = minX - subgraphPadding
301
+ sg.minY = minY - subgraphPadding - subgraphLabelSpace
302
+ sg.maxX = maxX + subgraphPadding
303
+ sg.maxY = maxY + subgraphPadding
304
+ }
305
+
306
+ /** Ensure non-overlapping root subgraphs have minimum spacing. */
307
+ function ensureSubgraphSpacing(graph: AsciiGraph): void {
308
+ const minSpacing = 1
309
+ const rootSubgraphs = graph.subgraphs.filter(sg => sg.parent === null && sg.nodes.length > 0)
310
+
311
+ for (let i = 0; i < rootSubgraphs.length; i++) {
312
+ for (let j = i + 1; j < rootSubgraphs.length; j++) {
313
+ const sg1 = rootSubgraphs[i]!
314
+ const sg2 = rootSubgraphs[j]!
315
+
316
+ // Horizontal overlap → adjust vertical
317
+ if (sg1.minX < sg2.maxX && sg1.maxX > sg2.minX) {
318
+ if (sg1.maxY >= sg2.minY - minSpacing && sg1.minY < sg2.minY) {
319
+ sg2.minY = sg1.maxY + minSpacing + 1
320
+ } else if (sg2.maxY >= sg1.minY - minSpacing && sg2.minY < sg1.minY) {
321
+ sg1.minY = sg2.maxY + minSpacing + 1
322
+ }
323
+ }
324
+ // Vertical overlap → adjust horizontal
325
+ if (sg1.minY < sg2.maxY && sg1.maxY > sg2.minY) {
326
+ if (sg1.maxX >= sg2.minX - minSpacing && sg1.minX < sg2.minX) {
327
+ sg2.minX = sg1.maxX + minSpacing + 1
328
+ } else if (sg2.maxX >= sg1.minX - minSpacing && sg2.minX < sg1.minX) {
329
+ sg1.minX = sg2.maxX + minSpacing + 1
330
+ }
331
+ }
332
+ }
333
+ }
334
+ }
335
+
336
+ export function calculateSubgraphBoundingBoxes(graph: AsciiGraph): void {
337
+ for (const sg of graph.subgraphs) {
338
+ calculateSubgraphBoundingBox(graph, sg)
339
+ }
340
+ ensureSubgraphSpacing(graph)
341
+ }
342
+
343
+ /**
344
+ * Offset all drawing coordinates so subgraph borders don't go negative.
345
+ * If any subgraph has negative min coordinates, shift everything positive.
346
+ */
347
+ export function offsetDrawingForSubgraphs(graph: AsciiGraph): void {
348
+ if (graph.subgraphs.length === 0) return
349
+
350
+ let minX = 0
351
+ let minY = 0
352
+ for (const sg of graph.subgraphs) {
353
+ minX = Math.min(minX, sg.minX)
354
+ minY = Math.min(minY, sg.minY)
355
+ }
356
+
357
+ const offsetX = -minX
358
+ const offsetY = -minY
359
+ if (offsetX === 0 && offsetY === 0) return
360
+
361
+ graph.offsetX = offsetX
362
+ graph.offsetY = offsetY
363
+
364
+ for (const sg of graph.subgraphs) {
365
+ sg.minX += offsetX
366
+ sg.minY += offsetY
367
+ sg.maxX += offsetX
368
+ sg.maxY += offsetY
369
+ }
370
+
371
+ for (const node of graph.nodes) {
372
+ if (node.drawingCoord) {
373
+ node.drawingCoord.x += offsetX
374
+ node.drawingCoord.y += offsetY
375
+ }
376
+ }
377
+ }
378
+
379
+ // ============================================================================
380
+ // Main layout orchestrator
381
+ // ============================================================================
382
+
383
+ /**
384
+ * createMapping performs the full grid layout:
385
+ * 1. Place root nodes on the grid
386
+ * 2. Place child nodes level by level
387
+ * 3. Compute column widths and row heights
388
+ * 4. Run A* pathfinding for all edges
389
+ * 5. Determine label placement
390
+ * 6. Convert grid coords → drawing coords
391
+ * 7. Generate node box drawings
392
+ * 8. Calculate subgraph bounding boxes
393
+ */
394
+ export function createMapping(graph: AsciiGraph): void {
395
+ const dir = graph.config.graphDirection
396
+ const highestPositionPerLevel: number[] = new Array(100).fill(0)
397
+
398
+ // Identify root nodes — nodes that aren't the target of any edge
399
+ const nodesFound = new Set<string>()
400
+ const initialRoots: AsciiNode[] = []
401
+
402
+ for (const node of graph.nodes) {
403
+ if (!nodesFound.has(node.name)) {
404
+ initialRoots.push(node)
405
+ }
406
+ nodesFound.add(node.name)
407
+ for (const child of getChildren(graph, node)) {
408
+ nodesFound.add(child.name)
409
+ }
410
+ }
411
+
412
+ // Filter out subgraph nodes that have incoming edges from external sources.
413
+ // This handles the case where subgraph is declared before external nodes
414
+ // (e.g., `subgraph s; A-->B; end; X-->A` - A shouldn't be a root, X should).
415
+ const rootNodes = initialRoots.filter(node => {
416
+ const nodeSg = getNodeSubgraph(graph, node)
417
+ if (!nodeSg) return true // external nodes: keep as roots
418
+
419
+ // Check if this subgraph node has incoming edges from outside its subgraph
420
+ for (const edge of graph.edges) {
421
+ if (edge.to === node) {
422
+ const sourceSg = getNodeSubgraph(graph, edge.from)
423
+ if (sourceSg !== nodeSg) {
424
+ return false // has external incoming edge → not a root
425
+ }
426
+ }
427
+ }
428
+ return true
429
+ })
430
+
431
+ // In LR mode with both external and subgraph roots, separate them
432
+ // so subgraph roots are placed one level deeper
433
+ let hasExternalRoots = false
434
+ let hasSubgraphRootsWithEdges = false
435
+ for (const node of rootNodes) {
436
+ if (isNodeInAnySubgraph(graph, node)) {
437
+ if (getChildren(graph, node).length > 0) hasSubgraphRootsWithEdges = true
438
+ } else {
439
+ hasExternalRoots = true
440
+ }
441
+ }
442
+ const shouldSeparate = dir === 'LR' && hasExternalRoots && hasSubgraphRootsWithEdges
443
+
444
+ let externalRootNodes: AsciiNode[]
445
+ let subgraphRootNodes: AsciiNode[] = []
446
+
447
+ if (shouldSeparate) {
448
+ externalRootNodes = rootNodes.filter(n => !isNodeInAnySubgraph(graph, n))
449
+ subgraphRootNodes = rootNodes.filter(n => isNodeInAnySubgraph(graph, n))
450
+ } else {
451
+ externalRootNodes = rootNodes
452
+ }
453
+
454
+ // Place external root nodes
455
+ for (const node of externalRootNodes) {
456
+ const requested: GridCoord = dir === 'LR'
457
+ ? { x: 0, y: highestPositionPerLevel[0]! }
458
+ : { x: highestPositionPerLevel[0]!, y: 0 }
459
+ reserveSpotInGrid(graph, graph.nodes[node.index]!, requested)
460
+ highestPositionPerLevel[0] = highestPositionPerLevel[0]! + 4
461
+ }
462
+
463
+ // Place subgraph root nodes at level 4 (one level in from the edge)
464
+ if (shouldSeparate && subgraphRootNodes.length > 0) {
465
+ const subgraphLevel = 4
466
+ for (const node of subgraphRootNodes) {
467
+ const requested: GridCoord = dir === 'LR'
468
+ ? { x: subgraphLevel, y: highestPositionPerLevel[subgraphLevel]! }
469
+ : { x: highestPositionPerLevel[subgraphLevel]!, y: subgraphLevel }
470
+ reserveSpotInGrid(graph, graph.nodes[node.index]!, requested)
471
+ highestPositionPerLevel[subgraphLevel] = highestPositionPerLevel[subgraphLevel]! + 4
472
+ }
473
+ }
474
+
475
+ // Place child nodes level by level
476
+ // Use subgraph direction only when both parent and child are in the same subgraph
477
+ // Multi-pass: iterate until all nodes are placed (handles non-topological node order)
478
+ // Note: when shouldSeparate, externalRootNodes + subgraphRootNodes = rootNodes
479
+ // otherwise, externalRootNodes = rootNodes and subgraphRootNodes is empty
480
+ let placedCount = externalRootNodes.length + subgraphRootNodes.length
481
+ while (placedCount < graph.nodes.length) {
482
+ const prevCount = placedCount
483
+ for (const node of graph.nodes) {
484
+ if (node.gridCoord === null) continue // skip unplaced nodes
485
+ const gc = node.gridCoord
486
+
487
+ for (const child of getChildren(graph, node)) {
488
+ if (child.gridCoord !== null) continue // already placed
489
+
490
+ // Determine direction for this edge (parent -> child)
491
+ // Use subgraph direction only if both are in the same subgraph with override
492
+ const parentSg = getNodeSubgraph(graph, node)
493
+ const childSg = getNodeSubgraph(graph, child)
494
+ const edgeDir = (parentSg && parentSg === childSg && parentSg.direction)
495
+ ? parentSg.direction
496
+ : graph.config.graphDirection
497
+
498
+ const childLevel = edgeDir === 'LR' ? gc.x + 4 : gc.y + 4
499
+
500
+ // Determine position based on direction context
501
+ let highestPosition: number
502
+ if (edgeDir !== graph.config.graphDirection) {
503
+ // Cross-direction: use parent's perpendicular coordinate
504
+ // This keeps children aligned with parent when direction changes
505
+ highestPosition = edgeDir === 'LR' ? gc.y : gc.x
506
+ } else {
507
+ // Same direction: use level tracker
508
+ highestPosition = highestPositionPerLevel[childLevel]!
509
+ }
510
+
511
+ const requested: GridCoord = edgeDir === 'LR'
512
+ ? { x: childLevel, y: highestPosition }
513
+ : { x: highestPosition, y: childLevel }
514
+ reserveSpotInGrid(graph, graph.nodes[child.index]!, requested, edgeDir)
515
+
516
+ // Only update level tracker for same-direction placements
517
+ if (edgeDir === graph.config.graphDirection) {
518
+ highestPositionPerLevel[childLevel] = highestPosition + 4
519
+ }
520
+ placedCount++
521
+ }
522
+ }
523
+ // Safety: break if no progress made (handles disconnected nodes)
524
+ if (placedCount === prevCount) break
525
+ }
526
+
527
+ // Compute column widths and row heights
528
+ for (const node of graph.nodes) {
529
+ setColumnWidth(graph, node)
530
+ }
531
+
532
+ // Analyze edges for bundling (parallel links like A & B --> C)
533
+ // This groups edges that share sources or targets for cleaner visualization
534
+ graph.bundles = analyzeEdgeBundles(graph)
535
+
536
+ // Route bundled edges through junction points
537
+ processBundles(graph)
538
+
539
+ // Route non-bundled edges via A* and determine label positions
540
+ for (const edge of graph.edges) {
541
+ // Skip edges that were already routed as part of a bundle
542
+ if (edge.bundle && edge.path.length > 0) {
543
+ increaseGridSizeForPath(graph, edge.path)
544
+ determineLabelLine(graph, edge)
545
+ continue
546
+ }
547
+
548
+ determinePath(graph, edge)
549
+ increaseGridSizeForPath(graph, edge.path)
550
+ determineLabelLine(graph, edge)
551
+ }
552
+
553
+ // Convert grid coords → drawing coords and generate box drawings
554
+ for (const node of graph.nodes) {
555
+ node.drawingCoord = gridToDrawingCoord(graph, node.gridCoord!)
556
+ node.drawing = drawBox(node, graph)
557
+ }
558
+
559
+ // Set canvas size and compute subgraph bounding boxes
560
+ setCanvasSizeToGrid(graph.canvas, graph.columnWidth, graph.rowHeight)
561
+ setRoleCanvasSizeToGrid(graph.roleCanvas, graph.columnWidth, graph.rowHeight)
562
+ calculateSubgraphBoundingBoxes(graph)
563
+ offsetDrawingForSubgraphs(graph)
564
+ }
565
+
566
+ // ============================================================================
567
+ // Graph traversal helpers
568
+ // ============================================================================
569
+
570
+ /** Get all edges originating from a node. */
571
+ function getEdgesFromNode(graph: AsciiGraph, node: AsciiNode): AsciiGraph['edges'] {
572
+ return graph.edges.filter(e => e.from.name === node.name)
573
+ }
574
+
575
+ /** Get all direct children of a node (targets of outgoing edges). */
576
+ function getChildren(graph: AsciiGraph, node: AsciiNode): AsciiNode[] {
577
+ return getEdgesFromNode(graph, node).map(e => e.to)
578
+ }