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