@oh-my-pi/pi-utils 16.0.7 → 16.0.9
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,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
|
+
}
|