@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,187 @@
1
+ // ============================================================================
2
+ // beautiful-mermaid — ASCII renderer public API
3
+ //
4
+ // Renders Mermaid diagrams to ASCII or Unicode box-drawing art.
5
+ // No external dependencies — pure TypeScript.
6
+ //
7
+ // Supported diagram types:
8
+ // - Flowcharts (graph TD / flowchart LR) — grid-based layout with A* pathfinding
9
+ // - State diagrams (stateDiagram-v2) — same pipeline as flowcharts
10
+ // - Sequence diagrams (sequenceDiagram) — column-based timeline layout
11
+ // - Class diagrams (classDiagram) — level-based UML layout
12
+ // - ER diagrams (erDiagram) — grid layout with crow's foot notation
13
+ //
14
+ // Usage:
15
+ // import { renderMermaidASCII } from 'beautiful-mermaid'
16
+ // const ascii = renderMermaidASCII('graph LR\n A --> B')
17
+ // ============================================================================
18
+
19
+ import { parseMermaid } from '../parser'
20
+ import { convertToAsciiGraph } from './converter'
21
+ import { createMapping } from './grid'
22
+ import { drawGraph } from './draw'
23
+ import { canvasToString, flipCanvasVertically, flipRoleCanvasVertically } from './canvas'
24
+ import { renderSequenceAscii } from './sequence'
25
+ import { renderClassAscii } from './class-diagram'
26
+ import { renderErAscii } from './er-diagram'
27
+ import { renderXYChartAscii } from './xychart'
28
+ import { detectColorMode, DEFAULT_ASCII_THEME } from './ansi'
29
+ import type { AsciiConfig, AsciiTheme, ColorMode } from './types'
30
+ import type { Direction } from '../types'
31
+
32
+ // Re-export types for external use
33
+ export type { AsciiTheme, ColorMode }
34
+ export { DEFAULT_ASCII_THEME, detectColorMode }
35
+
36
+ export interface AsciiRenderOptions {
37
+ /** true = ASCII chars (+,-,|,>), false = Unicode box-drawing (┌,─,│,►). Default: false */
38
+ useAscii?: boolean
39
+ /** Horizontal spacing between nodes. Default: 5 */
40
+ paddingX?: number
41
+ /** Vertical spacing between nodes. Default: 5 */
42
+ paddingY?: number
43
+ /** Padding inside node boxes. Default: 1 */
44
+ boxBorderPadding?: number
45
+ /**
46
+ * Force the layout direction, overriding the direction parsed from the
47
+ * diagram source. Applies to the flowchart + state-diagram grid pipeline
48
+ * (sequence/class/ER/xychart renderers ignore it). Useful for re-fitting a
49
+ * wide `LR` graph into a narrow viewport by laying it out top-down.
50
+ * Default: undefined (use the source's own direction).
51
+ */
52
+ direction?: Direction
53
+ /**
54
+ * Color mode for output.
55
+ * - 'none': No colors (plain text)
56
+ * - 'auto': Auto-detect (terminal ANSI capabilities, or HTML in browsers)
57
+ * - 'ansi16': 16-color ANSI
58
+ * - 'ansi256': 256-color xterm
59
+ * - 'truecolor': 24-bit RGB
60
+ * - 'html': HTML <span> tags with inline color styles (for browser rendering)
61
+ * Default: 'auto'
62
+ */
63
+ colorMode?: ColorMode | 'auto'
64
+ /** Theme colors for ASCII output. Uses default theme if not provided. */
65
+ theme?: Partial<AsciiTheme>
66
+ }
67
+
68
+ /**
69
+ * Detect the diagram type from the mermaid source text.
70
+ * Mirrors the detection logic in src/index.ts for the SVG renderer.
71
+ */
72
+ function detectDiagramType(text: string): 'flowchart' | 'sequence' | 'class' | 'er' | 'xychart' {
73
+ const firstLine = text.trim().split('\n')[0]?.trim().toLowerCase() ?? ''
74
+
75
+ if (/^xychart(-beta)?\b/.test(firstLine)) return 'xychart'
76
+ if (/^sequencediagram\s*$/.test(firstLine)) return 'sequence'
77
+ if (/^classdiagram\s*$/.test(firstLine)) return 'class'
78
+ if (/^erdiagram\s*$/.test(firstLine)) return 'er'
79
+
80
+ // Default: flowchart/state (handled by parseMermaid internally)
81
+ return 'flowchart'
82
+ }
83
+
84
+ /**
85
+ * Render Mermaid diagram text to an ASCII/Unicode string.
86
+ *
87
+ * Synchronous — no async layout engine needed (unlike the SVG renderer).
88
+ * Auto-detects diagram type from the header line and dispatches to
89
+ * the appropriate renderer.
90
+ *
91
+ * @param text - Mermaid source text (any supported diagram type)
92
+ * @param options - Rendering options
93
+ * @returns Multi-line ASCII/Unicode string
94
+ *
95
+ * @example
96
+ * ```ts
97
+ * const result = renderMermaidAscii(`
98
+ * graph LR
99
+ * A --> B --> C
100
+ * `, { useAscii: true })
101
+ *
102
+ * // Output:
103
+ * // +---+ +---+ +---+
104
+ * // | | | | | |
105
+ * // | A |---->| B |---->| C |
106
+ * // | | | | | |
107
+ * // +---+ +---+ +---+
108
+ * ```
109
+ */
110
+ export function renderMermaidASCII(
111
+ text: string,
112
+ options: AsciiRenderOptions = {},
113
+ ): string {
114
+ const config: AsciiConfig = {
115
+ useAscii: options.useAscii ?? false,
116
+ paddingX: options.paddingX ?? 5,
117
+ paddingY: options.paddingY ?? 5,
118
+ boxBorderPadding: options.boxBorderPadding ?? 1,
119
+ graphDirection: 'TD', // default, overridden for flowcharts below
120
+ }
121
+
122
+ // Resolve color mode ('auto' or unset → detect environment, otherwise use specified mode)
123
+ const colorMode: ColorMode = options.colorMode === 'auto' || options.colorMode === undefined
124
+ ? detectColorMode()
125
+ : options.colorMode
126
+
127
+ // Merge user theme with defaults
128
+ const theme: AsciiTheme = { ...DEFAULT_ASCII_THEME, ...options.theme }
129
+
130
+ const diagramType = detectDiagramType(text)
131
+
132
+ switch (diagramType) {
133
+ case 'xychart':
134
+ return renderXYChartAscii(text, config, colorMode, theme)
135
+
136
+ case 'sequence':
137
+ return renderSequenceAscii(text, config, colorMode, theme)
138
+
139
+ case 'class':
140
+ return renderClassAscii(text, config, colorMode, theme)
141
+
142
+ case 'er':
143
+ return renderErAscii(text, config, colorMode, theme)
144
+
145
+ case 'flowchart':
146
+ default: {
147
+ // Flowchart + state diagram pipeline (original)
148
+ const parsed = parseMermaid(text)
149
+
150
+ // Honor an explicit direction override. Applied before normalization so
151
+ // the LR/TD split and the BT vertical-flip below behave exactly as if the
152
+ // source had authored this direction.
153
+ if (options.direction) {
154
+ parsed.direction = options.direction
155
+ }
156
+
157
+ // Normalize direction for grid layout.
158
+ // BT is laid out as TD then flipped vertically after drawing.
159
+ // RL is treated as LR (full RL support not yet implemented).
160
+ if (parsed.direction === 'LR' || parsed.direction === 'RL') {
161
+ config.graphDirection = 'LR'
162
+ } else {
163
+ config.graphDirection = 'TD'
164
+ }
165
+
166
+ const graph = convertToAsciiGraph(parsed, config)
167
+ createMapping(graph)
168
+ drawGraph(graph)
169
+
170
+ // BT: flip the finished canvas vertically so the flow runs bottom→top.
171
+ // The grid layout ran as TD; flipping + character remapping produces BT.
172
+ if (parsed.direction === 'BT') {
173
+ flipCanvasVertically(graph.canvas)
174
+ flipRoleCanvasVertically(graph.roleCanvas)
175
+ }
176
+
177
+ return canvasToString(graph.canvas, {
178
+ roleCanvas: graph.roleCanvas,
179
+ colorMode,
180
+ theme,
181
+ })
182
+ }
183
+ }
184
+ }
185
+
186
+ /** Lowercase alias kept as the public name used by the pi-utils wrapper. */
187
+ export const renderMermaidAscii = renderMermaidASCII
@@ -0,0 +1,78 @@
1
+ // ============================================================================
2
+ // ASCII renderer — multi-line text utilities
3
+ //
4
+ // Shared utilities for handling multi-line labels (containing \n from <br> tags)
5
+ // in ASCII/Unicode rendering. Provides consistent text splitting, sizing, and
6
+ // centered rendering across all diagram types.
7
+ // ============================================================================
8
+
9
+ import type { Canvas } from './types'
10
+ import { drawText } from './canvas'
11
+ import { displayWidth } from '../text-metrics'
12
+
13
+ /**
14
+ * Split a label into lines.
15
+ * Labels are already normalized by parsers (br tags → \n).
16
+ */
17
+ export function splitLines(label: string): string[] {
18
+ return label.split('\n')
19
+ }
20
+
21
+ /**
22
+ * Get the maximum line width for sizing calculations.
23
+ * Used to determine column widths for multi-line labels.
24
+ */
25
+ export function maxLineWidth(label: string): number {
26
+ const lines = splitLines(label)
27
+ return Math.max(...lines.map(l => displayWidth(l)), 0)
28
+ }
29
+
30
+ /**
31
+ * Get the number of lines for height calculations.
32
+ * Used to determine row heights for multi-line labels.
33
+ */
34
+ export function lineCount(label: string): number {
35
+ return splitLines(label).length
36
+ }
37
+
38
+ /**
39
+ * Draw multi-line text centered at (cx, cy).
40
+ * Expands vertically from the center point.
41
+ * Each line is horizontally centered independently.
42
+ */
43
+ export function drawMultilineTextCentered(
44
+ canvas: Canvas,
45
+ label: string,
46
+ cx: number,
47
+ cy: number
48
+ ): void {
49
+ const lines = splitLines(label)
50
+ const totalHeight = lines.length
51
+ // Center vertically: start y positions lines evenly around cy
52
+ const startY = cy - Math.floor((totalHeight - 1) / 2)
53
+
54
+ for (let i = 0; i < lines.length; i++) {
55
+ const line = lines[i]!
56
+ // Center each line horizontally
57
+ const startX = cx - Math.floor(displayWidth(line) / 2)
58
+ // Force overwrite for node labels (they take priority)
59
+ drawText(canvas, { x: startX, y: startY + i }, line, true)
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Draw multi-line text left-aligned starting at (x, y).
65
+ * Each subsequent line is placed one row below.
66
+ */
67
+ export function drawMultilineTextLeft(
68
+ canvas: Canvas,
69
+ label: string,
70
+ x: number,
71
+ y: number
72
+ ): void {
73
+ const lines = splitLines(label)
74
+ for (let i = 0; i < lines.length; i++) {
75
+ // Force overwrite for node labels (they take priority)
76
+ drawText(canvas, { x, y: y + i }, lines[i]!, true)
77
+ }
78
+ }
@@ -0,0 +1,215 @@
1
+ // ============================================================================
2
+ // ASCII renderer — A* pathfinding for edge routing
3
+ //
4
+ // Ported from AlexanderGrooff/mermaid-ascii cmd/arrow.go.
5
+ // Uses A* search with a corner-penalizing heuristic to find clean
6
+ // paths between nodes on the grid. Prefers straight lines over zigzags.
7
+ // ============================================================================
8
+
9
+ import type { GridCoord, AsciiNode } from './types'
10
+ import { gridKey, gridCoordEquals } from './types'
11
+
12
+ // ============================================================================
13
+ // Priority queue (min-heap) for A* open set
14
+ // ============================================================================
15
+
16
+ interface PQItem {
17
+ coord: GridCoord
18
+ priority: number
19
+ }
20
+
21
+ /**
22
+ * Simple min-heap priority queue.
23
+ * For the grid sizes we handle (~100s of cells), this is more than fast enough.
24
+ */
25
+ class MinHeap {
26
+ private items: PQItem[] = []
27
+
28
+ get length(): number {
29
+ return this.items.length
30
+ }
31
+
32
+ push(item: PQItem): void {
33
+ this.items.push(item)
34
+ this.bubbleUp(this.items.length - 1)
35
+ }
36
+
37
+ pop(): PQItem | undefined {
38
+ if (this.items.length === 0) return undefined
39
+ const top = this.items[0]!
40
+ const last = this.items.pop()!
41
+ if (this.items.length > 0) {
42
+ this.items[0] = last
43
+ this.sinkDown(0)
44
+ }
45
+ return top
46
+ }
47
+
48
+ private bubbleUp(i: number): void {
49
+ while (i > 0) {
50
+ const parent = (i - 1) >> 1
51
+ if (this.items[i]!.priority < this.items[parent]!.priority) {
52
+ ;[this.items[i], this.items[parent]] = [this.items[parent]!, this.items[i]!]
53
+ i = parent
54
+ } else {
55
+ break
56
+ }
57
+ }
58
+ }
59
+
60
+ private sinkDown(i: number): void {
61
+ const n = this.items.length
62
+ while (true) {
63
+ let smallest = i
64
+ const left = 2 * i + 1
65
+ const right = 2 * i + 2
66
+ if (left < n && this.items[left]!.priority < this.items[smallest]!.priority) {
67
+ smallest = left
68
+ }
69
+ if (right < n && this.items[right]!.priority < this.items[smallest]!.priority) {
70
+ smallest = right
71
+ }
72
+ if (smallest !== i) {
73
+ ;[this.items[i], this.items[smallest]] = [this.items[smallest]!, this.items[i]!]
74
+ i = smallest
75
+ } else {
76
+ break
77
+ }
78
+ }
79
+ }
80
+ }
81
+
82
+ // ============================================================================
83
+ // A* heuristic
84
+ // ============================================================================
85
+
86
+ /**
87
+ * Manhattan distance with a +1 penalty when both dx and dy are non-zero.
88
+ * This encourages the pathfinder to prefer straight lines and minimize corners.
89
+ */
90
+ export function heuristic(a: GridCoord, b: GridCoord): number {
91
+ const absX = Math.abs(a.x - b.x)
92
+ const absY = Math.abs(a.y - b.y)
93
+ if (absX === 0 || absY === 0) {
94
+ return absX + absY
95
+ }
96
+ return absX + absY + 1
97
+ }
98
+
99
+ // ============================================================================
100
+ // A* pathfinding
101
+ // ============================================================================
102
+
103
+ /** 4-directional movement (no diagonals in grid pathfinding). */
104
+ const MOVE_DIRS: GridCoord[] = [
105
+ { x: 1, y: 0 },
106
+ { x: -1, y: 0 },
107
+ { x: 0, y: 1 },
108
+ { x: 0, y: -1 },
109
+ ]
110
+
111
+ /** Check if a grid cell is unoccupied and has non-negative coordinates. */
112
+ function isFreeInGrid(grid: Map<string, AsciiNode>, c: GridCoord): boolean {
113
+ if (c.x < 0 || c.y < 0) return false
114
+ return !grid.has(gridKey(c))
115
+ }
116
+
117
+ /**
118
+ * Find a path from `from` to `to` on the grid using A*.
119
+ * Returns the path as an array of GridCoords, or null if no path exists.
120
+ */
121
+ export function getPath(
122
+ grid: Map<string, AsciiNode>,
123
+ from: GridCoord,
124
+ to: GridCoord,
125
+ ): GridCoord[] | null {
126
+ const pq = new MinHeap()
127
+ pq.push({ coord: from, priority: 0 })
128
+
129
+ const costSoFar = new Map<string, number>()
130
+ costSoFar.set(gridKey(from), 0)
131
+
132
+ const cameFrom = new Map<string, GridCoord | null>()
133
+ cameFrom.set(gridKey(from), null)
134
+
135
+ while (pq.length > 0) {
136
+ const current = pq.pop()!.coord
137
+
138
+ if (gridCoordEquals(current, to)) {
139
+ // Reconstruct path by walking backwards through cameFrom
140
+ const path: GridCoord[] = []
141
+ let c: GridCoord | null = current
142
+ while (c !== null) {
143
+ path.unshift(c)
144
+ c = cameFrom.get(gridKey(c)) ?? null
145
+ }
146
+ return path
147
+ }
148
+
149
+ const currentCost = costSoFar.get(gridKey(current))!
150
+
151
+ for (const dir of MOVE_DIRS) {
152
+ const next: GridCoord = { x: current.x + dir.x, y: current.y + dir.y }
153
+
154
+ // Allow moving to the destination even if it's occupied (it's a node boundary)
155
+ if (!isFreeInGrid(grid, next) && !gridCoordEquals(next, to)) {
156
+ continue
157
+ }
158
+
159
+ const newCost = currentCost + 1
160
+ const nextKey = gridKey(next)
161
+ const existingCost = costSoFar.get(nextKey)
162
+
163
+ if (existingCost === undefined || newCost < existingCost) {
164
+ costSoFar.set(nextKey, newCost)
165
+ const priority = newCost + heuristic(next, to)
166
+ pq.push({ coord: next, priority })
167
+ cameFrom.set(nextKey, current)
168
+ }
169
+ }
170
+ }
171
+
172
+ return null // No path found
173
+ }
174
+
175
+ /**
176
+ * Simplify a path by removing intermediate waypoints on straight segments.
177
+ * E.g., [(0,0), (1,0), (2,0), (2,1)] becomes [(0,0), (2,0), (2,1)].
178
+ * This reduces the number of line-drawing operations.
179
+ */
180
+ export function mergePath(path: GridCoord[]): GridCoord[] {
181
+ if (path.length <= 2) return path
182
+
183
+ const toRemove = new Set<number>()
184
+ let step0 = path[0]!
185
+ let step1 = path[1]!
186
+
187
+ for (let idx = 2; idx < path.length; idx++) {
188
+ const step2 = path[idx]!
189
+ const prevDx = step1.x - step0.x
190
+ const prevDy = step1.y - step0.y
191
+ const dx = step2.x - step1.x
192
+ const dy = step2.y - step1.y
193
+
194
+ // Same direction — the middle point is redundant
195
+ if (prevDx === dx && prevDy === dy) {
196
+ // In Go: indexToRemove = append(indexToRemove, idx+1) but idx is 0-based from path[2:]
197
+ // which corresponds to index idx in the full path. Go uses idx+1 because idx iterates
198
+ // from 0 in the [2:] slice, mapping to full-array index idx+1.
199
+ // Actually re-checking Go code: the loop is `for idx, step2 := range path[2:]`
200
+ // so idx=0 → path[2], and it removes idx+1 which is index 1 in the full array.
201
+ // Wait, that doesn't look right. Let me re-read:
202
+ // step0 = path[0], step1 = path[1]
203
+ // for idx, step2 := range path[2:] { ... indexToRemove = append(indexToRemove, idx+1) ... }
204
+ // When idx=0, step2=path[2], and it removes index 1 (step1 = path[1]) if directions match
205
+ // So it removes the middle point (step1) which is at index idx+1 in the original array
206
+ // when counting from the 2-ahead loop. Let me just track which middle indices to remove.
207
+ toRemove.add(idx - 1) // Remove the middle point (step1's position)
208
+ }
209
+
210
+ step0 = step1
211
+ step1 = step2
212
+ }
213
+
214
+ return path.filter((_, i) => !toRemove.has(i))
215
+ }