@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.
- 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,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
|
+
}
|