@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.
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,476 @@
1
+ // ============================================================================
2
+ // ASCII renderer — 2D text canvas
3
+ //
4
+ // Ported from AlexanderGrooff/mermaid-ascii cmd/draw.go.
5
+ // The canvas is a column-major 2D array of single-character strings.
6
+ // canvas[x][y] gives the character at column x, row y.
7
+ // ============================================================================
8
+
9
+ import type { Canvas, DrawingCoord, RoleCanvas, CharRole, AsciiTheme, ColorMode } from './types'
10
+ import { colorizeLine, DEFAULT_ASCII_THEME } from './ansi'
11
+ import { displayWidth, toCells, WIDE_PAD } from '../text-metrics'
12
+
13
+ /**
14
+ * Create a blank canvas filled with spaces.
15
+ * Dimensions are inclusive: mkCanvas(3, 2) creates a 4x3 grid (indices 0..3, 0..2).
16
+ */
17
+ export function mkCanvas(x: number, y: number): Canvas {
18
+ const canvas: Canvas = []
19
+ for (let i = 0; i <= x; i++) {
20
+ const col: string[] = []
21
+ for (let j = 0; j <= y; j++) {
22
+ col.push(' ')
23
+ }
24
+ canvas.push(col)
25
+ }
26
+ return canvas
27
+ }
28
+
29
+ /** Create a blank canvas with the same dimensions as the given canvas. */
30
+ export function copyCanvas(source: Canvas): Canvas {
31
+ const [maxX, maxY] = getCanvasSize(source)
32
+ return mkCanvas(maxX, maxY)
33
+ }
34
+
35
+ // ============================================================================
36
+ // Role canvas creation and management
37
+ // ============================================================================
38
+
39
+ /**
40
+ * Create a blank role canvas filled with nulls.
41
+ * Same dimensions as mkCanvas — column-major, roleCanvas[x][y].
42
+ */
43
+ export function mkRoleCanvas(x: number, y: number): RoleCanvas {
44
+ const roleCanvas: RoleCanvas = []
45
+ for (let i = 0; i <= x; i++) {
46
+ const col: (CharRole | null)[] = []
47
+ for (let j = 0; j <= y; j++) {
48
+ col.push(null)
49
+ }
50
+ roleCanvas.push(col)
51
+ }
52
+ return roleCanvas
53
+ }
54
+
55
+ /** Create a blank role canvas with the same dimensions as the given role canvas. */
56
+ export function copyRoleCanvas(source: RoleCanvas): RoleCanvas {
57
+ const maxX = source.length - 1
58
+ const maxY = (source[0]?.length ?? 1) - 1
59
+ return mkRoleCanvas(maxX, maxY)
60
+ }
61
+
62
+ /**
63
+ * Grow the role canvas to fit at least (newX, newY), preserving existing roles.
64
+ * Mutates the role canvas in place and returns it.
65
+ */
66
+ export function increaseRoleCanvasSize(roleCanvas: RoleCanvas, newX: number, newY: number): RoleCanvas {
67
+ const currX = roleCanvas.length - 1
68
+ const currY = (roleCanvas[0]?.length ?? 1) - 1
69
+ const targetX = Math.max(newX, currX)
70
+ const targetY = Math.max(newY, currY)
71
+ const grown = mkRoleCanvas(targetX, targetY)
72
+ for (let x = 0; x < grown.length; x++) {
73
+ for (let y = 0; y < grown[0]!.length; y++) {
74
+ if (x < roleCanvas.length && y < roleCanvas[0]!.length) {
75
+ grown[x]![y] = roleCanvas[x]![y]!
76
+ }
77
+ }
78
+ }
79
+ roleCanvas.length = 0
80
+ roleCanvas.push(...grown)
81
+ return roleCanvas
82
+ }
83
+
84
+ /**
85
+ * Set a role at a specific coordinate.
86
+ * Expands the role canvas if necessary.
87
+ */
88
+ export function setRole(roleCanvas: RoleCanvas, x: number, y: number, role: CharRole): void {
89
+ if (x >= roleCanvas.length || y >= (roleCanvas[0]?.length ?? 0)) {
90
+ increaseRoleCanvasSize(roleCanvas, x, y)
91
+ }
92
+ roleCanvas[x]![y] = role
93
+ }
94
+
95
+ /**
96
+ * Merge role canvases — same logic as mergeCanvases but for roles.
97
+ * Non-null roles in overlays overwrite null roles in base.
98
+ */
99
+ export function mergeRoleCanvases(
100
+ base: RoleCanvas,
101
+ offset: DrawingCoord,
102
+ ...overlays: RoleCanvas[]
103
+ ): RoleCanvas {
104
+ let maxX = base.length - 1
105
+ let maxY = (base[0]?.length ?? 1) - 1
106
+
107
+ for (const overlay of overlays) {
108
+ const oX = overlay.length - 1
109
+ const oY = (overlay[0]?.length ?? 1) - 1
110
+ maxX = Math.max(maxX, oX + offset.x)
111
+ maxY = Math.max(maxY, oY + offset.y)
112
+ }
113
+
114
+ const merged = mkRoleCanvas(maxX, maxY)
115
+
116
+ // Copy base
117
+ for (let x = 0; x <= maxX; x++) {
118
+ for (let y = 0; y <= maxY; y++) {
119
+ if (x < base.length && y < base[0]!.length) {
120
+ merged[x]![y] = base[x]![y]!
121
+ }
122
+ }
123
+ }
124
+
125
+ // Apply overlays
126
+ for (const overlay of overlays) {
127
+ for (let x = 0; x < overlay.length; x++) {
128
+ for (let y = 0; y < overlay[0]!.length; y++) {
129
+ const role = overlay[x]?.[y]
130
+ if (role !== null && role !== undefined) {
131
+ const mx = x + offset.x
132
+ const my = y + offset.y
133
+ merged[mx]![my] = role
134
+ }
135
+ }
136
+ }
137
+ }
138
+
139
+ return merged
140
+ }
141
+
142
+ /** Returns [maxX, maxY] — the highest valid indices in each dimension. */
143
+ export function getCanvasSize(canvas: Canvas): [number, number] {
144
+ return [canvas.length - 1, (canvas[0]?.length ?? 1) - 1]
145
+ }
146
+
147
+ /**
148
+ * Grow the canvas to fit at least (newX, newY), preserving existing content.
149
+ * Mutates the canvas in place and returns it.
150
+ */
151
+ export function increaseSize(canvas: Canvas, newX: number, newY: number): Canvas {
152
+ const [currX, currY] = getCanvasSize(canvas)
153
+ const targetX = Math.max(newX, currX)
154
+ const targetY = Math.max(newY, currY)
155
+ const grown = mkCanvas(targetX, targetY)
156
+ for (let x = 0; x < grown.length; x++) {
157
+ for (let y = 0; y < grown[0]!.length; y++) {
158
+ if (x < canvas.length && y < canvas[0]!.length) {
159
+ grown[x]![y] = canvas[x]![y]!
160
+ }
161
+ }
162
+ }
163
+ // Mutate in place: splice old contents and replace with grown
164
+ canvas.length = 0
165
+ canvas.push(...grown)
166
+ return canvas
167
+ }
168
+
169
+ // ============================================================================
170
+ // Junction merging — Unicode box-drawing character compositing
171
+ // ============================================================================
172
+
173
+ /** All Unicode box-drawing characters that participate in junction merging. */
174
+ const JUNCTION_CHARS = new Set([
175
+ '─', '│', '┌', '┐', '└', '┘', '├', '┤', '┬', '┴', '┼', '╴', '╵', '╶', '╷',
176
+ ])
177
+
178
+ export function isJunctionChar(c: string): boolean {
179
+ return JUNCTION_CHARS.has(c)
180
+ }
181
+
182
+ /**
183
+ * Check if a cell holds label content for first-label-wins collision
184
+ * handling during merges: letters/digits in any script, the continuation
185
+ * cell of a wide glyph, or any 2-column glyph. Wide glyphs are only ever
186
+ * produced by labels (CJK ideographs, Hangul, emoji) — the renderer's own
187
+ * structural glyphs (borders, the narrow arrowheads ◀▶) are 1 column — so
188
+ * width 2 is a sufficient signal for emoji labels (🚀, 🇨🇳, 👍🏽) that the
189
+ * letter/digit test misses.
190
+ */
191
+ function isLabelChar(c: string): boolean {
192
+ return c === WIDE_PAD || displayWidth(c) === 2 || /[\p{L}\p{N}]/u.test(c)
193
+ }
194
+
195
+ /**
196
+ * Write one cell, dissolving any wide-glyph pair the write would split:
197
+ * overwriting a WIDE_PAD orphans its lead, and overwriting a lead orphans
198
+ * its pad — the orphaned half becomes a space so serialized rows keep
199
+ * exactly one column per cell.
200
+ */
201
+ function writeCell(canvas: Canvas, x: number, y: number, c: string): void {
202
+ const current = canvas[x]![y]!
203
+ if (current === WIDE_PAD && x > 0 && c !== WIDE_PAD) {
204
+ canvas[x - 1]![y] = ' '
205
+ } else if (current !== WIDE_PAD && canvas[x + 1]?.[y] === WIDE_PAD && c !== current) {
206
+ canvas[x + 1]![y] = ' '
207
+ }
208
+ canvas[x]![y] = c
209
+ }
210
+
211
+ /**
212
+ * When two junction characters overlap during canvas merging,
213
+ * resolve them to the correct combined junction.
214
+ * E.g., '─' overlapping '│' becomes '┼'.
215
+ */
216
+ const JUNCTION_MAP: Record<string, Record<string, string>> = {
217
+ '─': { '│': '┼', '┌': '┬', '┐': '┬', '└': '┴', '┘': '┴', '├': '┼', '┤': '┼', '┬': '┬', '┴': '┴' },
218
+ '│': { '─': '┼', '┌': '├', '┐': '┤', '└': '├', '┘': '┤', '├': '├', '┤': '┤', '┬': '┼', '┴': '┼' },
219
+ '┌': { '─': '┬', '│': '├', '┐': '┬', '└': '├', '┘': '┼', '├': '├', '┤': '┼', '┬': '┬', '┴': '┼' },
220
+ '┐': { '─': '┬', '│': '┤', '┌': '┬', '└': '┼', '┘': '┤', '├': '┼', '┤': '┤', '┬': '┬', '┴': '┼' },
221
+ '└': { '─': '┴', '│': '├', '┌': '├', '┐': '┼', '┘': '┴', '├': '├', '┤': '┼', '┬': '┼', '┴': '┴' },
222
+ '┘': { '─': '┴', '│': '┤', '┌': '┼', '┐': '┤', '└': '┴', '├': '┼', '┤': '┤', '┬': '┼', '┴': '┴' },
223
+ '├': { '─': '┼', '│': '├', '┌': '├', '┐': '┼', '└': '├', '┘': '┼', '┤': '┼', '┬': '┼', '┴': '┼' },
224
+ '┤': { '─': '┼', '│': '┤', '┌': '┼', '┐': '┤', '└': '┼', '┘': '┤', '├': '┼', '┬': '┼', '┴': '┼' },
225
+ '┬': { '─': '┬', '│': '┼', '┌': '┬', '┐': '┬', '└': '┼', '┘': '┼', '├': '┼', '┤': '┼', '┴': '┼' },
226
+ '┴': { '─': '┴', '│': '┼', '┌': '┼', '┐': '┼', '└': '┴', '┘': '┴', '├': '┼', '┤': '┼', '┬': '┼' },
227
+ }
228
+
229
+ export function mergeJunctions(c1: string, c2: string): string {
230
+ return JUNCTION_MAP[c1]?.[c2] ?? c1
231
+ }
232
+
233
+ // ============================================================================
234
+ // Canvas merging — composite multiple canvases with offset
235
+ // ============================================================================
236
+
237
+ /**
238
+ * Merge overlay canvases onto a base canvas at the given offset.
239
+ * Non-space characters in overlays overwrite the base.
240
+ * When both characters are Unicode junction chars, they're merged intelligently.
241
+ */
242
+ export function mergeCanvases(
243
+ base: Canvas,
244
+ offset: DrawingCoord,
245
+ useAscii: boolean,
246
+ ...overlays: Canvas[]
247
+ ): Canvas {
248
+ let [maxX, maxY] = getCanvasSize(base)
249
+ for (const overlay of overlays) {
250
+ const [oX, oY] = getCanvasSize(overlay)
251
+ maxX = Math.max(maxX, oX + offset.x)
252
+ maxY = Math.max(maxY, oY + offset.y)
253
+ }
254
+
255
+ const merged = mkCanvas(maxX, maxY)
256
+
257
+ // Copy base
258
+ for (let x = 0; x <= maxX; x++) {
259
+ for (let y = 0; y <= maxY; y++) {
260
+ if (x < base.length && y < base[0]!.length) {
261
+ merged[x]![y] = base[x]![y]!
262
+ }
263
+ }
264
+ }
265
+
266
+ // Apply overlays
267
+ for (const overlay of overlays) {
268
+ for (let x = 0; x < overlay.length; x++) {
269
+ for (let y = 0; y < overlay[0]!.length; y++) {
270
+ const c = overlay[x]![y]!
271
+ // WIDE_PAD cells are written atomically with their lead below
272
+ if (c === ' ' || c === WIDE_PAD) continue
273
+ const mx = x + offset.x
274
+ const my = y + offset.y
275
+ const current = merged[mx]![my]!
276
+ const isWide = overlay[x + 1]?.[y] === WIDE_PAD
277
+ if (!useAscii && isJunctionChar(c) && isJunctionChar(current)) {
278
+ merged[mx]![my] = mergeJunctions(current, c)
279
+ } else if (isWide) {
280
+ // Wide glyphs land or yield as a whole pair (first label wins)
281
+ if (!isLabelChar(current) && !isLabelChar(merged[mx + 1]?.[my] ?? ' ')) {
282
+ writeCell(merged, mx, my, c)
283
+ writeCell(merged, mx + 1, my, WIDE_PAD)
284
+ }
285
+ } else if (isLabelChar(current) && isLabelChar(c)) {
286
+ // Don't overwrite existing label text with new label text
287
+ // This prevents label collisions (first label wins)
288
+ } else {
289
+ writeCell(merged, mx, my, c)
290
+ }
291
+ }
292
+ }
293
+ }
294
+
295
+ return merged
296
+ }
297
+
298
+ // ============================================================================
299
+ // Canvas → string conversion
300
+ // ============================================================================
301
+
302
+ /** Options for converting canvas to string with optional coloring. */
303
+ export interface CanvasToStringOptions {
304
+ /** Role canvas for applying colors. If not provided, output is plain text. */
305
+ roleCanvas?: RoleCanvas
306
+ /** Color mode for terminal output. Default: 'none' */
307
+ colorMode?: ColorMode
308
+ /** Theme colors for ASCII output. Uses default theme if not provided. */
309
+ theme?: AsciiTheme
310
+ }
311
+
312
+ /**
313
+ * Convert the canvas to a multi-line string (row by row, left to right).
314
+ * Optionally applies ANSI color codes based on character roles.
315
+ */
316
+ export function canvasToString(canvas: Canvas, options?: CanvasToStringOptions): string {
317
+ const [maxX, maxY] = getCanvasSize(canvas)
318
+ const lines: string[] = []
319
+
320
+ const roleCanvas = options?.roleCanvas
321
+ const colorMode = options?.colorMode ?? 'none'
322
+ const theme = options?.theme ?? DEFAULT_ASCII_THEME
323
+
324
+ for (let y = 0; y <= maxY; y++) {
325
+ if (colorMode === 'none' || !roleCanvas) {
326
+ // Plain text output — no colors
327
+ let line = ''
328
+ for (let x = 0; x <= maxX; x++) {
329
+ const c = canvas[x]![y]!
330
+ // Skip wide-glyph continuation cells: the glyph itself spans 2 columns
331
+ if (c !== WIDE_PAD) line += c
332
+ }
333
+ lines.push(line)
334
+ } else {
335
+ // Colored output — collect chars and roles for this row
336
+ const chars: string[] = []
337
+ const roles: (CharRole | null)[] = []
338
+ for (let x = 0; x <= maxX; x++) {
339
+ const c = canvas[x]![y]!
340
+ if (c === WIDE_PAD) continue
341
+ chars.push(c)
342
+ roles.push(roleCanvas[x]?.[y] ?? null)
343
+ }
344
+ lines.push(colorizeLine(chars, roles, theme, colorMode))
345
+ }
346
+ }
347
+
348
+ return lines.join('\n')
349
+ }
350
+
351
+ // ============================================================================
352
+ // Canvas vertical flip — used for BT (bottom-to-top) direction support.
353
+ //
354
+ // The ASCII renderer lays out graphs top-down (TD). For BT direction, we
355
+ // flip the finished canvas vertically and remap directional characters so
356
+ // arrows point upward and corners are mirrored correctly.
357
+ // ============================================================================
358
+
359
+ /**
360
+ * Characters that change meaning when the Y-axis is flipped.
361
+ * Symmetric characters (─, │, ├, ┤, ┼) are unchanged.
362
+ */
363
+ const VERTICAL_FLIP_MAP: Record<string, string> = {
364
+ // Unicode arrows
365
+ '▲': '▼', '▼': '▲',
366
+ '◤': '◣', '◣': '◤',
367
+ '◥': '◢', '◢': '◥',
368
+ // ASCII arrows
369
+ '^': 'v', 'v': '^',
370
+ // Unicode corners
371
+ '┌': '└', '└': '┌',
372
+ '┐': '┘', '┘': '┐',
373
+ // Unicode junctions (T-pieces flip vertically)
374
+ '┬': '┴', '┴': '┬',
375
+ // Box-start junctions (exit points from node boxes)
376
+ '╵': '╷', '╷': '╵',
377
+ }
378
+
379
+ /**
380
+ * Flip the canvas vertically (mirror across the horizontal center).
381
+ * Reverses row order within each column and remaps directional characters
382
+ * (arrows, corners, junctions) so they point the correct way after flip.
383
+ *
384
+ * Used to transform a TD-rendered canvas into BT output.
385
+ * Mutates the canvas in place and returns it.
386
+ */
387
+ export function flipCanvasVertically(canvas: Canvas): Canvas {
388
+ // Reverse each column array (Y-axis flip in column-major layout)
389
+ for (const col of canvas) {
390
+ col.reverse()
391
+ }
392
+
393
+ // Remap directional characters that change meaning after vertical flip
394
+ for (const col of canvas) {
395
+ for (let y = 0; y < col.length; y++) {
396
+ const flipped = VERTICAL_FLIP_MAP[col[y]!]
397
+ if (flipped) col[y] = flipped
398
+ }
399
+ }
400
+
401
+ return canvas
402
+ }
403
+
404
+ /**
405
+ * Flip the role canvas vertically to match flipCanvasVertically.
406
+ * Mutates the role canvas in place and returns it.
407
+ */
408
+ export function flipRoleCanvasVertically(roleCanvas: RoleCanvas): RoleCanvas {
409
+ for (const col of roleCanvas) {
410
+ col.reverse()
411
+ }
412
+ return roleCanvas
413
+ }
414
+
415
+ /**
416
+ * Draw text string onto the canvas starting at the given coordinate.
417
+ * By default, preserves existing non-space characters (labels don't overwrite each other).
418
+ * Set forceOverwrite=true to always overwrite (for box content).
419
+ */
420
+ export function drawText(
421
+ canvas: Canvas,
422
+ start: DrawingCoord,
423
+ text: string,
424
+ forceOverwrite = false
425
+ ): void {
426
+ const cells = toCells(text)
427
+ increaseSize(canvas, start.x + cells.length, start.y)
428
+ for (let i = 0; i < cells.length; i++) {
429
+ const cell = cells[i]!
430
+ // WIDE_PAD cells are written atomically with their lead below
431
+ if (cell === WIDE_PAD) continue
432
+ const x = start.x + i
433
+ if (cells[i + 1] === WIDE_PAD) {
434
+ // Wide glyph: needs both its cells free (or forced) to land
435
+ const pairFree = canvas[x]![start.y] === ' ' && canvas[x + 1]![start.y] === ' '
436
+ if (forceOverwrite || pairFree) {
437
+ writeCell(canvas, x, start.y, cell)
438
+ writeCell(canvas, x + 1, start.y, WIDE_PAD)
439
+ }
440
+ } else if (forceOverwrite || canvas[x]![start.y] === ' ') {
441
+ writeCell(canvas, x, start.y, cell)
442
+ }
443
+ }
444
+ }
445
+
446
+ /**
447
+ * Set the canvas size to fit all grid columns and rows.
448
+ * Called after layout to ensure the canvas covers the full drawing area.
449
+ */
450
+ export function setCanvasSizeToGrid(
451
+ canvas: Canvas,
452
+ columnWidth: Map<number, number>,
453
+ rowHeight: Map<number, number>,
454
+ ): void {
455
+ let maxX = 0
456
+ let maxY = 0
457
+ for (const w of columnWidth.values()) maxX += w
458
+ for (const h of rowHeight.values()) maxY += h
459
+ increaseSize(canvas, maxX - 1, maxY - 1)
460
+ }
461
+
462
+ /**
463
+ * Set the role canvas size to match the grid dimensions.
464
+ * Should be called alongside setCanvasSizeToGrid.
465
+ */
466
+ export function setRoleCanvasSizeToGrid(
467
+ roleCanvas: RoleCanvas,
468
+ columnWidth: Map<number, number>,
469
+ rowHeight: Map<number, number>,
470
+ ): void {
471
+ let maxX = 0
472
+ let maxY = 0
473
+ for (const w of columnWidth.values()) maxX += w
474
+ for (const h of rowHeight.values()) maxY += h
475
+ increaseRoleCanvasSize(roleCanvas, maxX - 1, maxY - 1)
476
+ }