@oh-my-pi/pi-utils 16.0.7 → 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.
- package/CHANGELOG.md +10 -0
- package/dist/types/mermaid-ascii.d.ts +1 -1
- package/dist/types/vendor/mermaid-ascii/ascii/ansi.d.ts +41 -0
- package/dist/types/vendor/mermaid-ascii/ascii/canvas.d.ts +89 -0
- package/dist/types/vendor/mermaid-ascii/ascii/class-diagram.d.ts +7 -0
- package/dist/types/vendor/mermaid-ascii/ascii/converter.d.ts +12 -0
- package/dist/types/vendor/mermaid-ascii/ascii/draw.d.ts +66 -0
- package/dist/types/vendor/mermaid-ascii/ascii/edge-bundling.d.ts +48 -0
- package/dist/types/vendor/mermaid-ascii/ascii/edge-routing.d.ts +43 -0
- package/dist/types/vendor/mermaid-ascii/ascii/er-diagram.d.ts +7 -0
- package/dist/types/vendor/mermaid-ascii/ascii/grid.d.ts +56 -0
- package/dist/types/vendor/mermaid-ascii/ascii/index.d.ts +65 -0
- package/dist/types/vendor/mermaid-ascii/ascii/multiline-utils.d.ts +27 -0
- package/dist/types/vendor/mermaid-ascii/ascii/pathfinder.d.ts +17 -0
- package/dist/types/vendor/mermaid-ascii/ascii/sequence.d.ts +7 -0
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/circle.d.ts +11 -0
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/corners.d.ts +34 -0
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/diamond.d.ts +11 -0
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/hexagon.d.ts +11 -0
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/index.d.ts +26 -0
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/rectangle.d.ts +31 -0
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/rounded.d.ts +11 -0
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/special.d.ts +59 -0
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/stadium.d.ts +17 -0
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/state.d.ts +30 -0
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/types.d.ts +55 -0
- package/dist/types/vendor/mermaid-ascii/ascii/types.d.ts +206 -0
- package/dist/types/vendor/mermaid-ascii/ascii/validate.d.ts +51 -0
- package/dist/types/vendor/mermaid-ascii/ascii/xychart.d.ts +2 -0
- package/dist/types/vendor/mermaid-ascii/class/parser.d.ts +6 -0
- package/dist/types/vendor/mermaid-ascii/class/types.d.ts +102 -0
- package/dist/types/vendor/mermaid-ascii/er/parser.d.ts +6 -0
- package/dist/types/vendor/mermaid-ascii/er/types.d.ts +76 -0
- package/dist/types/vendor/mermaid-ascii/index.d.ts +1 -0
- package/dist/types/vendor/mermaid-ascii/multiline-utils.d.ts +9 -0
- package/dist/types/vendor/mermaid-ascii/parser.d.ts +7 -0
- package/dist/types/vendor/mermaid-ascii/sequence/parser.d.ts +6 -0
- package/dist/types/vendor/mermaid-ascii/sequence/types.d.ts +130 -0
- package/dist/types/vendor/mermaid-ascii/text-metrics.d.ts +21 -0
- package/dist/types/vendor/mermaid-ascii/types.d.ts +114 -0
- package/dist/types/vendor/mermaid-ascii/xychart/colors.d.ts +25 -0
- package/dist/types/vendor/mermaid-ascii/xychart/parser.d.ts +6 -0
- package/dist/types/vendor/mermaid-ascii/xychart/types.d.ts +145 -0
- package/package.json +2 -3
- package/src/mermaid-ascii.ts +1 -1
- package/src/vendor/mermaid-ascii/NOTICE +33 -0
- package/src/vendor/mermaid-ascii/ascii/ansi.ts +409 -0
- package/src/vendor/mermaid-ascii/ascii/canvas.ts +476 -0
- package/src/vendor/mermaid-ascii/ascii/class-diagram.ts +699 -0
- package/src/vendor/mermaid-ascii/ascii/converter.ts +271 -0
- package/src/vendor/mermaid-ascii/ascii/draw.ts +1382 -0
- package/src/vendor/mermaid-ascii/ascii/edge-bundling.ts +328 -0
- package/src/vendor/mermaid-ascii/ascii/edge-routing.ts +297 -0
- package/src/vendor/mermaid-ascii/ascii/er-diagram.ts +441 -0
- package/src/vendor/mermaid-ascii/ascii/grid.ts +578 -0
- package/src/vendor/mermaid-ascii/ascii/index.ts +187 -0
- package/src/vendor/mermaid-ascii/ascii/multiline-utils.ts +78 -0
- package/src/vendor/mermaid-ascii/ascii/pathfinder.ts +215 -0
- package/src/vendor/mermaid-ascii/ascii/sequence.ts +460 -0
- package/src/vendor/mermaid-ascii/ascii/shapes/circle.ts +27 -0
- package/src/vendor/mermaid-ascii/ascii/shapes/corners.ts +127 -0
- package/src/vendor/mermaid-ascii/ascii/shapes/diamond.ts +27 -0
- package/src/vendor/mermaid-ascii/ascii/shapes/hexagon.ts +27 -0
- package/src/vendor/mermaid-ascii/ascii/shapes/index.ts +101 -0
- package/src/vendor/mermaid-ascii/ascii/shapes/rectangle.ts +175 -0
- package/src/vendor/mermaid-ascii/ascii/shapes/rounded.ts +27 -0
- package/src/vendor/mermaid-ascii/ascii/shapes/special.ts +296 -0
- package/src/vendor/mermaid-ascii/ascii/shapes/stadium.ts +114 -0
- package/src/vendor/mermaid-ascii/ascii/shapes/state.ts +192 -0
- package/src/vendor/mermaid-ascii/ascii/shapes/types.ts +73 -0
- package/src/vendor/mermaid-ascii/ascii/types.ts +273 -0
- package/src/vendor/mermaid-ascii/ascii/validate.ts +120 -0
- package/src/vendor/mermaid-ascii/ascii/xychart.ts +875 -0
- package/src/vendor/mermaid-ascii/class/parser.ts +290 -0
- package/src/vendor/mermaid-ascii/class/types.ts +121 -0
- package/src/vendor/mermaid-ascii/er/parser.ts +181 -0
- package/src/vendor/mermaid-ascii/er/types.ts +91 -0
- package/src/vendor/mermaid-ascii/index.ts +14 -0
- package/src/vendor/mermaid-ascii/multiline-utils.ts +30 -0
- package/src/vendor/mermaid-ascii/parser.ts +645 -0
- package/src/vendor/mermaid-ascii/sequence/parser.ts +207 -0
- package/src/vendor/mermaid-ascii/sequence/types.ts +146 -0
- package/src/vendor/mermaid-ascii/text-metrics.ts +71 -0
- package/src/vendor/mermaid-ascii/types.ts +164 -0
- package/src/vendor/mermaid-ascii/xychart/colors.ts +140 -0
- package/src/vendor/mermaid-ascii/xychart/parser.ts +115 -0
- 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
|
+
}
|