@zombie-mermaid/ascii-renderer 2.2.1
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/LICENSE +22 -0
- package/dist/index.cjs +8 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +146 -0
- package/dist/index.d.ts +146 -0
- package/dist/index.js +5392 -0
- package/dist/index.js.map +1 -0
- package/package.json +36 -0
- package/src/__tests__/ascii-arrowhead-direction-1083.test.ts +51 -0
- package/src/__tests__/ascii-canvas-first-claim-wins-1093.test.ts +89 -0
- package/src/__tests__/ascii-canvas-size-offset-1093.test.ts +95 -0
- package/src/__tests__/ascii-canvas-write.test.ts +132 -0
- package/src/__tests__/ascii-chain-edge-overlap-1067.test.ts +100 -0
- package/src/__tests__/ascii-charset-border-junctions.test.ts +63 -0
- package/src/__tests__/ascii-cjk-width.test.ts +150 -0
- package/src/__tests__/ascii-class-box-occupancy.test.ts +462 -0
- package/src/__tests__/ascii-class-column-width-488-489.test.ts +639 -0
- package/src/__tests__/ascii-class-cross-level-jog-corruption.test.ts +187 -0
- package/src/__tests__/ascii-class-detour-label-routing-487.test.ts +114 -0
- package/src/__tests__/ascii-class-diagram-compartments.test.ts +70 -0
- package/src/__tests__/ascii-class-label-box-collision.test.ts +60 -0
- package/src/__tests__/ascii-class-label-row-collision-531.test.ts +135 -0
- package/src/__tests__/ascii-class-label-territory-row-awareness.test.ts +57 -0
- package/src/__tests__/ascii-class-padding.test.ts +104 -0
- package/src/__tests__/ascii-class-parent-alignment-971.test.ts +139 -0
- package/src/__tests__/ascii-class-parent-alignment-972.test.ts +208 -0
- package/src/__tests__/ascii-class-reciprocal-relationships-448.test.ts +169 -0
- package/src/__tests__/ascii-combining-mark-width.test.ts +70 -0
- package/src/__tests__/ascii-coords-overlay.test.ts +64 -0
- package/src/__tests__/ascii-decision-lr-box-start.test.ts +106 -0
- package/src/__tests__/ascii-display-width-unit.test.ts +154 -0
- package/src/__tests__/ascii-draw-arrows-coverage.test.ts +224 -0
- package/src/__tests__/ascii-draw-arrows-single-point-path.test.ts +105 -0
- package/src/__tests__/ascii-edge-bundling-rank-violation-454.test.ts +72 -0
- package/src/__tests__/ascii-edge-ending-glyphs.test.ts +206 -0
- package/src/__tests__/ascii-edge-label-diagonal-fallback-418.test.ts +95 -0
- package/src/__tests__/ascii-edge-routing-fixes.test.ts +178 -0
- package/src/__tests__/ascii-edge-routing-single-point-path.test.ts +103 -0
- package/src/__tests__/ascii-edge-style-consistency-1067.test.ts +89 -0
- package/src/__tests__/ascii-edge-styles.test.ts +149 -0
- package/src/__tests__/ascii-emoji-cluster-width.test.ts +101 -0
- package/src/__tests__/ascii-er-box-occupancy.test.ts +251 -0
- package/src/__tests__/ascii-er-cardinality.test.ts +40 -0
- package/src/__tests__/ascii-er-corner-glyphs.test.ts +249 -0
- package/src/__tests__/ascii-er-jog-stray-line.test.ts +111 -0
- package/src/__tests__/ascii-er-label-padding.test.ts +118 -0
- package/src/__tests__/ascii-er-padding.test.ts +114 -0
- package/src/__tests__/ascii-er-relationship-label-corruption-350.test.ts +458 -0
- package/src/__tests__/ascii-er-relationship-overwrite.test.ts +325 -0
- package/src/__tests__/ascii-er-stray-connectors.test.ts +344 -0
- package/src/__tests__/ascii-er-unrelated-stem-separation-411.test.ts +98 -0
- package/src/__tests__/ascii-er-vertical-one-marker.test.ts +110 -0
- package/src/__tests__/ascii-label-line-terminal-fallback.test.ts +168 -0
- package/src/__tests__/ascii-lane-search.test.ts +207 -0
- package/src/__tests__/ascii-multibox-cjk-width.test.ts +184 -0
- package/src/__tests__/ascii-multiline.test.ts +288 -0
- package/src/__tests__/ascii-padding-edge-cases.test.ts +154 -0
- package/src/__tests__/ascii-pathfinder-route-edge.test.ts +184 -0
- package/src/__tests__/ascii-sequence-alt-else-label.test.ts +236 -0
- package/src/__tests__/ascii-sequence-block-wall-clearance.test.ts +217 -0
- package/src/__tests__/ascii-sequence-box-group.test.ts +159 -0
- package/src/__tests__/ascii-sequence-cjk-width.test.ts +235 -0
- package/src/__tests__/ascii-sequence-create-destroy.test.ts +114 -0
- package/src/__tests__/ascii-sequence-form-invariants.test.ts +432 -0
- package/src/__tests__/ascii-sequence-mermaid-parity.test.ts +219 -0
- package/src/__tests__/ascii-sequence-notes.test.ts +61 -0
- package/src/__tests__/ascii-sequence-padding.test.ts +119 -0
- package/src/__tests__/ascii-sequence-self-arrow.test.ts +206 -0
- package/src/__tests__/ascii-shape-diamond.test.ts +38 -0
- package/src/__tests__/ascii-shape-rectangle.test.ts +258 -0
- package/src/__tests__/ascii-shape-rounded.test.ts +36 -0
- package/src/__tests__/ascii-shapes-circle.test.ts +41 -0
- package/src/__tests__/ascii-shapes-hexagon.test.ts +42 -0
- package/src/__tests__/ascii-shapes-special.test.ts +344 -0
- package/src/__tests__/ascii-shapes-stadium.test.ts +217 -0
- package/src/__tests__/ascii-shapes-state.test.ts +224 -0
- package/src/__tests__/ascii-state-bidirectional-label-swap-530.test.ts +130 -0
- package/src/__tests__/ascii-subgraph-direction-honored-445.test.ts +90 -0
- package/src/__tests__/ascii-subgraph-label-border-clip.test.ts +152 -0
- package/src/__tests__/ascii-subgraph-title-padding.test.ts +77 -0
- package/src/__tests__/ascii-territory-unit.test.ts +219 -0
- package/src/__tests__/ascii-validate.test.ts +220 -0
- package/src/__tests__/ascii.test.ts +325 -0
- package/src/__tests__/class-arrow-directions.test.ts +505 -0
- package/src/__tests__/draw-lines.test.ts +93 -0
- package/src/__tests__/edge-cell-styles.test.ts +278 -0
- package/src/__tests__/grid-occupancy.test.ts +240 -0
- package/src/__tests__/helpers/ascii-form.ts +142 -0
- package/src/__tests__/helpers/terminal-display-width.ts +74 -0
- package/src/__tests__/pathfinder.test.ts +239 -0
- package/src/__tests__/testdata/ascii/ampersand_lhs.txt +18 -0
- package/src/__tests__/testdata/ascii/ampersand_lhs_and_rhs.txt +18 -0
- package/src/__tests__/testdata/ascii/ampersand_rhs.txt +18 -0
- package/src/__tests__/testdata/ascii/ampersand_td_fanin.txt +18 -0
- package/src/__tests__/testdata/ascii/ampersand_td_fanout.txt +18 -0
- package/src/__tests__/testdata/ascii/ampersand_without_edge.txt +18 -0
- package/src/__tests__/testdata/ascii/back_reference_from_child.txt +10 -0
- package/src/__tests__/testdata/ascii/backlink_from_bottom.txt +22 -0
- package/src/__tests__/testdata/ascii/backlink_from_top.txt +22 -0
- package/src/__tests__/testdata/ascii/backlink_with_short_y_padding.txt +20 -0
- package/src/__tests__/testdata/ascii/cls_all_relationships.txt +19 -0
- package/src/__tests__/testdata/ascii/cls_annotation.txt +29 -0
- package/src/__tests__/testdata/ascii/cls_association.txt +14 -0
- package/src/__tests__/testdata/ascii/cls_basic.txt +15 -0
- package/src/__tests__/testdata/ascii/cls_dependency.txt +14 -0
- package/src/__tests__/testdata/ascii/cls_inheritance.txt +20 -0
- package/src/__tests__/testdata/ascii/cls_methods.txt +21 -0
- package/src/__tests__/testdata/ascii/comments.txt +23 -0
- package/src/__tests__/testdata/ascii/custom_padding.txt +10 -0
- package/src/__tests__/testdata/ascii/duplicate_labels.txt +19 -0
- package/src/__tests__/testdata/ascii/er_attributes.txt +21 -0
- package/src/__tests__/testdata/ascii/er_basic.txt +8 -0
- package/src/__tests__/testdata/ascii/er_identifying.txt +18 -0
- package/src/__tests__/testdata/ascii/flowchart_tb_simple.txt +29 -0
- package/src/__tests__/testdata/ascii/graph_bt_direction.txt +28 -0
- package/src/__tests__/testdata/ascii/graph_tb_direction.txt +26 -0
- package/src/__tests__/testdata/ascii/nested_subgraphs_with_labels.txt +36 -0
- package/src/__tests__/testdata/ascii/preserve_order_of_definition.txt +23 -0
- package/src/__tests__/testdata/ascii/self_reference.txt +10 -0
- package/src/__tests__/testdata/ascii/self_reference_with_edge.txt +10 -0
- package/src/__tests__/testdata/ascii/seq_basic.txt +17 -0
- package/src/__tests__/testdata/ascii/seq_multiple_messages.txt +25 -0
- package/src/__tests__/testdata/ascii/seq_self_message.txt +18 -0
- package/src/__tests__/testdata/ascii/single_node.txt +8 -0
- package/src/__tests__/testdata/ascii/single_node_longer_name.txt +8 -0
- package/src/__tests__/testdata/ascii/subgraph_complex_mixed.txt +38 -0
- package/src/__tests__/testdata/ascii/subgraph_complex_nested.txt +49 -0
- package/src/__tests__/testdata/ascii/subgraph_direction_override.txt +47 -0
- package/src/__tests__/testdata/ascii/subgraph_empty.txt +10 -0
- package/src/__tests__/testdata/ascii/subgraph_mixed_nodes.txt +20 -0
- package/src/__tests__/testdata/ascii/subgraph_mixed_nodes_td.txt +48 -0
- package/src/__tests__/testdata/ascii/subgraph_multiple_edges.txt +32 -0
- package/src/__tests__/testdata/ascii/subgraph_multiple_nodes.txt +16 -0
- package/src/__tests__/testdata/ascii/subgraph_nested.txt +24 -0
- package/src/__tests__/testdata/ascii/subgraph_nested_with_external.txt +30 -0
- package/src/__tests__/testdata/ascii/subgraph_node_outside_lr.txt +17 -0
- package/src/__tests__/testdata/ascii/subgraph_single_node.txt +16 -0
- package/src/__tests__/testdata/ascii/subgraph_td_direction.txt +26 -0
- package/src/__tests__/testdata/ascii/subgraph_td_multiple.txt +44 -0
- package/src/__tests__/testdata/ascii/subgraph_td_multiple_paddingy.txt +42 -0
- package/src/__tests__/testdata/ascii/subgraph_three_levels_nested.txt +32 -0
- package/src/__tests__/testdata/ascii/subgraph_three_separate.txt +24 -0
- package/src/__tests__/testdata/ascii/subgraph_two_separate.txt +20 -0
- package/src/__tests__/testdata/ascii/subgraph_with_labels.txt +20 -0
- package/src/__tests__/testdata/ascii/three_nodes.txt +9 -0
- package/src/__tests__/testdata/ascii/three_nodes_single_line.txt +8 -0
- package/src/__tests__/testdata/ascii/two_layer_single_graph.txt +19 -0
- package/src/__tests__/testdata/ascii/two_layer_single_graph_longer_names.txt +19 -0
- package/src/__tests__/testdata/ascii/two_nodes_linked.txt +8 -0
- package/src/__tests__/testdata/ascii/two_nodes_longer_names.txt +8 -0
- package/src/__tests__/testdata/ascii/two_root_nodes.txt +19 -0
- package/src/__tests__/testdata/ascii/two_root_nodes_longer_names.txt +19 -0
- package/src/__tests__/testdata/ascii/two_single_root_nodes.txt +19 -0
- package/src/__tests__/testdata/unicode/ampersand_lhs.txt +18 -0
- package/src/__tests__/testdata/unicode/ampersand_lhs_and_rhs.txt +18 -0
- package/src/__tests__/testdata/unicode/ampersand_rhs.txt +18 -0
- package/src/__tests__/testdata/unicode/ampersand_without_edge.txt +18 -0
- package/src/__tests__/testdata/unicode/back_reference_from_child.txt +10 -0
- package/src/__tests__/testdata/unicode/backlink_from_bottom.txt +22 -0
- package/src/__tests__/testdata/unicode/backlink_from_top.txt +22 -0
- package/src/__tests__/testdata/unicode/cls_all_relationships.txt +19 -0
- package/src/__tests__/testdata/unicode/cls_annotation.txt +29 -0
- package/src/__tests__/testdata/unicode/cls_association.txt +14 -0
- package/src/__tests__/testdata/unicode/cls_basic.txt +15 -0
- package/src/__tests__/testdata/unicode/cls_dependency.txt +14 -0
- package/src/__tests__/testdata/unicode/cls_inheritance.txt +20 -0
- package/src/__tests__/testdata/unicode/cls_methods.txt +21 -0
- package/src/__tests__/testdata/unicode/comments.txt +23 -0
- package/src/__tests__/testdata/unicode/duplicate_labels.txt +19 -0
- package/src/__tests__/testdata/unicode/er_attributes.txt +21 -0
- package/src/__tests__/testdata/unicode/er_basic.txt +8 -0
- package/src/__tests__/testdata/unicode/er_identifying.txt +18 -0
- package/src/__tests__/testdata/unicode/graph_bt_direction.txt +28 -0
- package/src/__tests__/testdata/unicode/preserve_order_of_definition.txt +23 -0
- package/src/__tests__/testdata/unicode/self_reference.txt +10 -0
- package/src/__tests__/testdata/unicode/self_reference_with_edge.txt +10 -0
- package/src/__tests__/testdata/unicode/seq_basic.txt +17 -0
- package/src/__tests__/testdata/unicode/seq_multiple_messages.txt +25 -0
- package/src/__tests__/testdata/unicode/seq_self_message.txt +18 -0
- package/src/__tests__/testdata/unicode/single_node.txt +8 -0
- package/src/__tests__/testdata/unicode/single_node_longer_name.txt +8 -0
- package/src/__tests__/testdata/unicode/three_nodes.txt +9 -0
- package/src/__tests__/testdata/unicode/three_nodes_single_line.txt +8 -0
- package/src/__tests__/testdata/unicode/two_layer_single_graph.txt +19 -0
- package/src/__tests__/testdata/unicode/two_layer_single_graph_longer_names.txt +19 -0
- package/src/__tests__/testdata/unicode/two_nodes_linked.txt +8 -0
- package/src/__tests__/testdata/unicode/two_nodes_longer_names.txt +8 -0
- package/src/__tests__/testdata/unicode/two_root_nodes.txt +19 -0
- package/src/__tests__/testdata/unicode/two_root_nodes_longer_names.txt +19 -0
- package/src/__tests__/testdata/unicode/two_single_root_nodes.txt +19 -0
- package/src/__tests__/xychart-ascii.test.ts +376 -0
- package/src/ansi.ts +490 -0
- package/src/canvas.ts +757 -0
- package/src/class-diagram.ts +2001 -0
- package/src/converter.ts +446 -0
- package/src/coords.ts +58 -0
- package/src/display-width.ts +151 -0
- package/src/draw-arrows.ts +593 -0
- package/src/draw-boxes.ts +267 -0
- package/src/draw-bundles.ts +611 -0
- package/src/draw-lines.ts +174 -0
- package/src/draw-subgraphs.ts +108 -0
- package/src/draw.ts +350 -0
- package/src/edge-bundling.ts +435 -0
- package/src/edge-cell-styles.ts +209 -0
- package/src/edge-routing.ts +1070 -0
- package/src/er-diagram.ts +1488 -0
- package/src/flowchart.ts +94 -0
- package/src/grid-occupancy.ts +234 -0
- package/src/grid.ts +1309 -0
- package/src/hyperlinks.ts +248 -0
- package/src/index.ts +163 -0
- package/src/lane-search.ts +68 -0
- package/src/multiline-utils.ts +82 -0
- package/src/pathfinder.ts +448 -0
- package/src/registry.ts +88 -0
- package/src/sequence.ts +1318 -0
- package/src/shapes/circle.ts +31 -0
- package/src/shapes/corners.ts +273 -0
- package/src/shapes/diamond.ts +31 -0
- package/src/shapes/hexagon.ts +35 -0
- package/src/shapes/index.ts +123 -0
- package/src/shapes/rectangle.ts +199 -0
- package/src/shapes/rounded.ts +31 -0
- package/src/shapes/special.ts +360 -0
- package/src/shapes/stadium.ts +122 -0
- package/src/shapes/state.ts +204 -0
- package/src/shapes/types.ts +78 -0
- package/src/territory.ts +136 -0
- package/src/types.ts +454 -0
- package/src/validate.ts +189 -0
- package/src/xychart.ts +1085 -0
|
@@ -0,0 +1,448 @@
|
|
|
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 {
|
|
10
|
+
GridCoord,
|
|
11
|
+
PathBudget,
|
|
12
|
+
AsciiGraph,
|
|
13
|
+
CardinalDirection,
|
|
14
|
+
} from './types.ts'
|
|
15
|
+
import { gridKey, gridCoordEquals, dirEquals, Left, Right } from './types.ts'
|
|
16
|
+
import { isFree, type Grid } from './grid-occupancy.ts'
|
|
17
|
+
|
|
18
|
+
// ============================================================================
|
|
19
|
+
// Priority queue (min-heap) for A* open set
|
|
20
|
+
// ============================================================================
|
|
21
|
+
|
|
22
|
+
interface PQItem {
|
|
23
|
+
coord: GridCoord
|
|
24
|
+
priority: number
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Simple min-heap priority queue.
|
|
29
|
+
* For the grid sizes we handle (~100s of cells), this is more than fast enough.
|
|
30
|
+
*/
|
|
31
|
+
class MinHeap {
|
|
32
|
+
private items: PQItem[] = []
|
|
33
|
+
|
|
34
|
+
get length(): number {
|
|
35
|
+
return this.items.length
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
push(item: PQItem): void {
|
|
39
|
+
this.items.push(item)
|
|
40
|
+
this.bubbleUp(this.items.length - 1)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
pop(): PQItem | undefined {
|
|
44
|
+
if (this.items.length === 0) return undefined
|
|
45
|
+
const top = this.items[0]!
|
|
46
|
+
const last = this.items.pop()!
|
|
47
|
+
if (this.items.length > 0) {
|
|
48
|
+
this.items[0] = last
|
|
49
|
+
this.sinkDown(0)
|
|
50
|
+
}
|
|
51
|
+
return top
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private bubbleUp(start: number): void {
|
|
55
|
+
let i = start
|
|
56
|
+
while (i > 0) {
|
|
57
|
+
const parent = (i - 1) >> 1
|
|
58
|
+
if (this.items[i]!.priority < this.items[parent]!.priority) {
|
|
59
|
+
;[this.items[i], this.items[parent]] = [
|
|
60
|
+
this.items[parent]!,
|
|
61
|
+
this.items[i]!,
|
|
62
|
+
]
|
|
63
|
+
i = parent
|
|
64
|
+
} else {
|
|
65
|
+
break
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private sinkDown(start: number): void {
|
|
71
|
+
const n = this.items.length
|
|
72
|
+
let i = start
|
|
73
|
+
while (true) {
|
|
74
|
+
let smallest = i
|
|
75
|
+
const left = 2 * i + 1
|
|
76
|
+
const right = 2 * i + 2
|
|
77
|
+
if (
|
|
78
|
+
left < n &&
|
|
79
|
+
this.items[left]!.priority < this.items[smallest]!.priority
|
|
80
|
+
) {
|
|
81
|
+
smallest = left
|
|
82
|
+
}
|
|
83
|
+
if (
|
|
84
|
+
right < n &&
|
|
85
|
+
this.items[right]!.priority < this.items[smallest]!.priority
|
|
86
|
+
) {
|
|
87
|
+
smallest = right
|
|
88
|
+
}
|
|
89
|
+
if (smallest !== i) {
|
|
90
|
+
;[this.items[i], this.items[smallest]] = [
|
|
91
|
+
this.items[smallest]!,
|
|
92
|
+
this.items[i]!,
|
|
93
|
+
]
|
|
94
|
+
i = smallest
|
|
95
|
+
} else {
|
|
96
|
+
break
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ============================================================================
|
|
103
|
+
// A* heuristic
|
|
104
|
+
// ============================================================================
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Manhattan distance with a +1 penalty when both dx and dy are non-zero.
|
|
108
|
+
* This encourages the pathfinder to prefer straight lines and minimize corners.
|
|
109
|
+
*/
|
|
110
|
+
export function heuristic(a: GridCoord, b: GridCoord): number {
|
|
111
|
+
const absX = Math.abs(a.x - b.x)
|
|
112
|
+
const absY = Math.abs(a.y - b.y)
|
|
113
|
+
if (absX === 0 || absY === 0) {
|
|
114
|
+
return absX + absY
|
|
115
|
+
}
|
|
116
|
+
return absX + absY + 1
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ============================================================================
|
|
120
|
+
// A* pathfinding
|
|
121
|
+
// ============================================================================
|
|
122
|
+
|
|
123
|
+
/** 4-directional movement (no diagonals in grid pathfinding). */
|
|
124
|
+
const MOVE_DIRS: GridCoord[] = [
|
|
125
|
+
{ x: 1, y: 0 },
|
|
126
|
+
{ x: -1, y: 0 },
|
|
127
|
+
{ x: 0, y: 1 },
|
|
128
|
+
{ x: 0, y: -1 },
|
|
129
|
+
]
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Maximum number of A* iterations before giving up on a *single* getPath
|
|
133
|
+
* call. Prevents unbounded memory growth when the destination is
|
|
134
|
+
* unreachable through free cells (the grid has no positive upper-bound
|
|
135
|
+
* check).
|
|
136
|
+
*
|
|
137
|
+
* This alone is not sufficient to bound a whole render: a dense fan-in/out
|
|
138
|
+
* graph can call getPath hundreds of times (once per edge, sometimes twice
|
|
139
|
+
* — see determinePath's preferred + alternative attempts), and each call is
|
|
140
|
+
* independently allowed to spend up to MAX_ITERATIONS work searching a grid
|
|
141
|
+
* that's mostly occupied by other nodes on the same row. That per-call cap
|
|
142
|
+
* bounds each search but not their sum, so total time/memory across a
|
|
143
|
+
* render still scales with (edge count × MAX_ITERATIONS) and can exhaust
|
|
144
|
+
* the heap well before any single call hits its own limit. See PathBudget
|
|
145
|
+
* below for the render-wide bound that fixes this.
|
|
146
|
+
*/
|
|
147
|
+
const MAX_ITERATIONS = 50_000
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* A mutable, render-wide budget shared across every getPath call made while
|
|
151
|
+
* laying out one graph (see grid.ts's createMapping, which creates one
|
|
152
|
+
* fresh budget per render and threads it through edge-routing.ts and
|
|
153
|
+
* edge-bundling.ts). Each A* iteration — in any call — decrements
|
|
154
|
+
* `remaining`; once it hits zero, all subsequent getPath calls return null
|
|
155
|
+
* immediately (falling back to the direct-path logic already used when A*
|
|
156
|
+
* can't find a route at all), which hard-bounds total pathfinding work for
|
|
157
|
+
* the whole render regardless of how many edges it has.
|
|
158
|
+
*
|
|
159
|
+
* (Type defined in types.ts alongside the other shared ASCII-renderer
|
|
160
|
+
* types, to avoid a circular import between this module and types.ts.)
|
|
161
|
+
*/
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Default render-wide iteration budget. Generous enough to fully route
|
|
165
|
+
* ordinary diagrams (tens to low hundreds of edges) without ever being
|
|
166
|
+
* hit, while still keeping total pathfinding work — and thus memory use —
|
|
167
|
+
* bounded for pathological dense fan-in/out graphs with hundreds of edges.
|
|
168
|
+
*/
|
|
169
|
+
export const DEFAULT_PATH_BUDGET = 200_000
|
|
170
|
+
|
|
171
|
+
/** Create a fresh render-wide path budget. */
|
|
172
|
+
export function createPathBudget(
|
|
173
|
+
total: number = DEFAULT_PATH_BUDGET,
|
|
174
|
+
): PathBudget {
|
|
175
|
+
return { remaining: total }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Find a path from `from` to `to` on the grid using A*.
|
|
180
|
+
* Returns the path as an array of GridCoords, or null if no path exists.
|
|
181
|
+
*
|
|
182
|
+
* `budget`, if provided, is a render-wide iteration budget shared across
|
|
183
|
+
* all getPath calls for the current layout — see PathBudget above. Once
|
|
184
|
+
* exhausted, this (and every subsequent call sharing it) returns null
|
|
185
|
+
* right away instead of searching.
|
|
186
|
+
*/
|
|
187
|
+
export function getPath(
|
|
188
|
+
grid: Grid,
|
|
189
|
+
from: GridCoord,
|
|
190
|
+
to: GridCoord,
|
|
191
|
+
budget?: PathBudget,
|
|
192
|
+
): GridCoord[] | null {
|
|
193
|
+
if (budget && budget.remaining <= 0) {
|
|
194
|
+
return null
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const pq = new MinHeap()
|
|
198
|
+
pq.push({ coord: from, priority: 0 })
|
|
199
|
+
|
|
200
|
+
const costSoFar = new Map<string, number>()
|
|
201
|
+
costSoFar.set(gridKey(from), 0)
|
|
202
|
+
|
|
203
|
+
const cameFrom = new Map<string, GridCoord | null>()
|
|
204
|
+
cameFrom.set(gridKey(from), null)
|
|
205
|
+
|
|
206
|
+
let iterations = 0
|
|
207
|
+
while (pq.length > 0) {
|
|
208
|
+
if (++iterations > MAX_ITERATIONS) {
|
|
209
|
+
return null
|
|
210
|
+
}
|
|
211
|
+
if (budget) {
|
|
212
|
+
if (budget.remaining <= 0) return null
|
|
213
|
+
budget.remaining--
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const current = pq.pop()!.coord
|
|
217
|
+
|
|
218
|
+
if (gridCoordEquals(current, to)) {
|
|
219
|
+
// Reconstruct path by walking backwards through cameFrom
|
|
220
|
+
const path: GridCoord[] = []
|
|
221
|
+
let c: GridCoord | null = current
|
|
222
|
+
while (c !== null) {
|
|
223
|
+
path.unshift(c)
|
|
224
|
+
c = cameFrom.get(gridKey(c)) ?? null
|
|
225
|
+
}
|
|
226
|
+
return path
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Every coord ever pushed onto `pq` has its costSoFar entry set
|
|
230
|
+
// immediately beforehand — either the initial `from` push above, or in
|
|
231
|
+
// the neighbor-expansion loop below (`costSoFar.set` precedes
|
|
232
|
+
// `pq.push`) — so a coord just popped from `pq` always has a recorded
|
|
233
|
+
// cost. That invariant lives in this function's control flow, not in
|
|
234
|
+
// the Map's type, so it's checked explicitly rather than trusted via
|
|
235
|
+
// `!`.
|
|
236
|
+
const currentCost = costSoFar.get(gridKey(current))
|
|
237
|
+
if (currentCost === undefined) {
|
|
238
|
+
/* v8 ignore next */
|
|
239
|
+
throw new Error(
|
|
240
|
+
`A* pathfinding: missing cost for visited cell ${gridKey(current)}`,
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
for (const dir of MOVE_DIRS) {
|
|
245
|
+
const next: GridCoord = { x: current.x + dir.x, y: current.y + dir.y }
|
|
246
|
+
|
|
247
|
+
// Allow moving to the destination even if it's occupied (it's a node boundary)
|
|
248
|
+
if (!isFree(grid, next) && !gridCoordEquals(next, to)) {
|
|
249
|
+
continue
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const newCost = currentCost + 1
|
|
253
|
+
const nextKey = gridKey(next)
|
|
254
|
+
const existingCost = costSoFar.get(nextKey)
|
|
255
|
+
|
|
256
|
+
if (existingCost === undefined || newCost < existingCost) {
|
|
257
|
+
costSoFar.set(nextKey, newCost)
|
|
258
|
+
const priority = newCost + heuristic(next, to)
|
|
259
|
+
pq.push({ coord: next, priority })
|
|
260
|
+
cameFrom.set(nextKey, current)
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return null // No path found
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Simplify a path by removing intermediate waypoints on straight segments.
|
|
270
|
+
* E.g., [(0,0), (1,0), (2,0), (2,1)] becomes [(0,0), (2,0), (2,1)].
|
|
271
|
+
* This reduces the number of line-drawing operations.
|
|
272
|
+
*/
|
|
273
|
+
export function mergePath(path: GridCoord[]): GridCoord[] {
|
|
274
|
+
if (path.length <= 2) return path
|
|
275
|
+
|
|
276
|
+
const toRemove = new Set<number>()
|
|
277
|
+
let step0 = path[0]!
|
|
278
|
+
let step1 = path[1]!
|
|
279
|
+
|
|
280
|
+
for (let idx = 2; idx < path.length; idx++) {
|
|
281
|
+
const step2 = path[idx]!
|
|
282
|
+
const prevDx = step1.x - step0.x
|
|
283
|
+
const prevDy = step1.y - step0.y
|
|
284
|
+
const dx = step2.x - step1.x
|
|
285
|
+
const dy = step2.y - step1.y
|
|
286
|
+
|
|
287
|
+
// Same direction — the middle point is redundant
|
|
288
|
+
if (prevDx === dx && prevDy === dy) {
|
|
289
|
+
// In Go: indexToRemove = append(indexToRemove, idx+1) but idx is 0-based from path[2:]
|
|
290
|
+
// which corresponds to index idx in the full path. Go uses idx+1 because idx iterates
|
|
291
|
+
// from 0 in the [2:] slice, mapping to full-array index idx+1.
|
|
292
|
+
// Actually re-checking Go code: the loop is `for idx, step2 := range path[2:]`
|
|
293
|
+
// so idx=0 → path[2], and it removes idx+1 which is index 1 in the full array.
|
|
294
|
+
// Wait, that doesn't look right. Let me re-read:
|
|
295
|
+
// step0 = path[0], step1 = path[1]
|
|
296
|
+
// for idx, step2 := range path[2:] { ... indexToRemove = append(indexToRemove, idx+1) ... }
|
|
297
|
+
// When idx=0, step2=path[2], and it removes index 1 (step1 = path[1]) if directions match
|
|
298
|
+
// So it removes the middle point (step1) which is at index idx+1 in the original array
|
|
299
|
+
// when counting from the 2-ahead loop. Let me just track which middle indices to remove.
|
|
300
|
+
toRemove.add(idx - 1) // Remove the middle point (step1's position)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
step0 = step1
|
|
304
|
+
step1 = step2
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return path.filter((_, i) => !toRemove.has(i))
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ============================================================================
|
|
311
|
+
// Direct (unobstructed L-shape) path — shared fast path for edge-routing.ts
|
|
312
|
+
// and edge-bundling.ts
|
|
313
|
+
// ============================================================================
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Check every cell strictly after `from` up to (and optionally including)
|
|
317
|
+
* `to` along a single axis-aligned run. `from` and `to` must share an x or
|
|
318
|
+
* a y coordinate. `includeTo` should be false when `to` is the edge's
|
|
319
|
+
* final destination point (which is expected to sit on/adjacent to the
|
|
320
|
+
* target node's own border and so may legitimately be "occupied" in
|
|
321
|
+
* graph.grid — mirroring the same allowance A*'s getPath makes for its
|
|
322
|
+
* destination cell).
|
|
323
|
+
*/
|
|
324
|
+
function isAxisRunFree(
|
|
325
|
+
grid: Grid,
|
|
326
|
+
from: GridCoord,
|
|
327
|
+
to: GridCoord,
|
|
328
|
+
includeTo: boolean,
|
|
329
|
+
): boolean {
|
|
330
|
+
if (from.x === to.x && from.y === to.y) return true
|
|
331
|
+
const alongY = from.x === to.x
|
|
332
|
+
if (!alongY && from.y !== to.y) return false // not axis-aligned
|
|
333
|
+
|
|
334
|
+
const step = alongY ? (to.y > from.y ? 1 : -1) : to.x > from.x ? 1 : -1
|
|
335
|
+
let pos = alongY ? from.y : from.x
|
|
336
|
+
const end = alongY ? to.y : to.x
|
|
337
|
+
|
|
338
|
+
for (pos += step; ; pos += step) {
|
|
339
|
+
const isLast = pos === end
|
|
340
|
+
const cell: GridCoord = alongY
|
|
341
|
+
? { x: from.x, y: pos }
|
|
342
|
+
: { x: pos, y: from.y }
|
|
343
|
+
if (!(isLast && !includeTo) && !isFree(grid, cell)) return false
|
|
344
|
+
if (isLast) break
|
|
345
|
+
}
|
|
346
|
+
return true
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** Try one L-shaped corner order; null if any leg is obstructed. */
|
|
350
|
+
function tryCornerPath(
|
|
351
|
+
grid: Grid,
|
|
352
|
+
from: GridCoord,
|
|
353
|
+
to: GridCoord,
|
|
354
|
+
horizontalFirst: boolean,
|
|
355
|
+
): GridCoord[] | null {
|
|
356
|
+
const corner: GridCoord = horizontalFirst
|
|
357
|
+
? { x: to.x, y: from.y }
|
|
358
|
+
: { x: from.x, y: to.y }
|
|
359
|
+
|
|
360
|
+
if (!isAxisRunFree(grid, from, corner, true)) return null
|
|
361
|
+
if (!isAxisRunFree(grid, corner, to, false)) return null
|
|
362
|
+
return [from, corner, to]
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Try a direct two-segment (L-shaped) route between `from` and `to`. `dir`
|
|
367
|
+
* indicates the preferred corner order (horizontal-first for Left/Right,
|
|
368
|
+
* vertical-first for Up/Down), but when that orientation is blocked the
|
|
369
|
+
* *other* orientation is tried too before giving up.
|
|
370
|
+
*
|
|
371
|
+
* Trying both orientations (rather than trusting `dir` alone) matters
|
|
372
|
+
* because `dir` is not always the true geometric departure direction from
|
|
373
|
+
* `from`: edge-routing.ts's determinePath always passes it correctly, but
|
|
374
|
+
* edge-bundling.ts's junction segments sometimes pass the *arrival* anchor
|
|
375
|
+
* at `to` instead (the two only coincide when `from`/`to` happen to already
|
|
376
|
+
* be axis-aligned, which is common but not guaranteed — see the PR #178
|
|
377
|
+
* review that found the fast path dead at 3 of 4 bundled call sites because
|
|
378
|
+
* of exactly this mix-up). Trying both orientations makes the result
|
|
379
|
+
* correct regardless of which direction a caller actually knows, instead of
|
|
380
|
+
* requiring every caller to recompute true geometric departure.
|
|
381
|
+
*
|
|
382
|
+
* Returns null when `from` and `to` are already axis-aligned (no corner
|
|
383
|
+
* needed — A*'s result is already optimal there) or when both orientations
|
|
384
|
+
* are blocked.
|
|
385
|
+
*
|
|
386
|
+
* A* always finds a *shortest* path, but when several equally-short routes
|
|
387
|
+
* exist — e.g. a straight line and a zigzag with the same total step count
|
|
388
|
+
* — which one it returns depends on priority-queue tie-breaking order, not
|
|
389
|
+
* on which "looks" straighter. That's why sibling edges from the same
|
|
390
|
+
* source can end up with visually inconsistent routing (one goes straight,
|
|
391
|
+
* another zigzags) even though nothing actually blocks a straight route
|
|
392
|
+
* for either. A direct L-shaped route is, by construction, exactly as
|
|
393
|
+
* short as any route between two points can be, so whenever it's
|
|
394
|
+
* unobstructed we prefer it outright over whatever A* found — this also
|
|
395
|
+
* skips the A* search entirely for the common unobstructed case.
|
|
396
|
+
*/
|
|
397
|
+
function tryDirectPath(
|
|
398
|
+
graph: AsciiGraph,
|
|
399
|
+
from: GridCoord,
|
|
400
|
+
to: GridCoord,
|
|
401
|
+
dir: CardinalDirection,
|
|
402
|
+
): GridCoord[] | null {
|
|
403
|
+
if (from.x === to.x || from.y === to.y) return null
|
|
404
|
+
|
|
405
|
+
const preferHorizontalFirst = dirEquals(dir, Left) || dirEquals(dir, Right)
|
|
406
|
+
return (
|
|
407
|
+
tryCornerPath(graph.grid, from, to, preferHorizontalFirst) ??
|
|
408
|
+
tryCornerPath(graph.grid, from, to, !preferHorizontalFirst)
|
|
409
|
+
)
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Route a single directed edge segment from `from` to `to`: try an
|
|
414
|
+
* unobstructed direct L-shaped route first (tryDirectPath, above), falling
|
|
415
|
+
* back to A* search (getPath) when every direct orientation is blocked or
|
|
416
|
+
* unavailable (from/to already axis-aligned). Returns the merged path, or
|
|
417
|
+
* null when neither strategy finds a route at all (e.g. the destination
|
|
418
|
+
* is unreachable through free cells).
|
|
419
|
+
*
|
|
420
|
+
* This is the single seam both edge-routing.ts (determinePath's
|
|
421
|
+
* preferred/alternative candidates) and edge-bundling.ts
|
|
422
|
+
* (routeBundledEdges' junction-based segments) route through, so regular
|
|
423
|
+
* and bundled edges share the same fast-path-then-A* behavior instead of
|
|
424
|
+
* each independently calling getPath.
|
|
425
|
+
*
|
|
426
|
+
* `graph.pathBudget` is required, not silently optional: it's the
|
|
427
|
+
* render-wide bound against pathological/hostile diagrams (see PathBudget
|
|
428
|
+
* above), and routeEdge is the shared entry point for two modules now — an
|
|
429
|
+
* absent budget here would silently disable that bound for both instead of
|
|
430
|
+
* failing loudly, the way requireGridCoord fails loudly for a missing grid
|
|
431
|
+
* coordinate rather than treating it as some default.
|
|
432
|
+
*/
|
|
433
|
+
export function routeEdge(
|
|
434
|
+
graph: AsciiGraph,
|
|
435
|
+
from: GridCoord,
|
|
436
|
+
to: GridCoord,
|
|
437
|
+
dir: CardinalDirection,
|
|
438
|
+
): GridCoord[] | null {
|
|
439
|
+
if (!graph.pathBudget) {
|
|
440
|
+
throw new Error(
|
|
441
|
+
'routeEdge requires graph.pathBudget to be set; call createPathBudget() (see grid.ts createMapping) before routing edges',
|
|
442
|
+
)
|
|
443
|
+
}
|
|
444
|
+
const path =
|
|
445
|
+
tryDirectPath(graph, from, to, dir) ??
|
|
446
|
+
getPath(graph.grid, from, to, graph.pathBudget)
|
|
447
|
+
return path ? mergePath(path) : null
|
|
448
|
+
}
|
package/src/registry.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// zombie-mermaid — ASCII half of the per-diagram-type registry (issue #533)
|
|
3
|
+
//
|
|
4
|
+
// `src/diagram-registry.ts` used to hold both halves of each registered
|
|
5
|
+
// diagram type: `renderSvg` (plus `parse`/`layoutForSvg`) AND `renderAscii`.
|
|
6
|
+
// That made the SVG-side module import `renderXYChartAscii`/`renderErAscii`
|
|
7
|
+
// out of `src/ascii/`, while `src/ascii/index.ts` imported `diagramRegistry`
|
|
8
|
+
// back out of `src/`. A cycle:
|
|
9
|
+
//
|
|
10
|
+
// src/ascii/index.ts -> src/diagram-registry.ts -> src/ascii/xychart.ts
|
|
11
|
+
// -> src/ascii/er-diagram.ts
|
|
12
|
+
//
|
|
13
|
+
// Benign while both halves live in one package; fatal to the monorepo split
|
|
14
|
+
// scoped in docs/decisions/monorepo-conversion-scoping.md (#416/#620), where
|
|
15
|
+
// `src/ascii/**` becomes `@zombie-mermaid/ascii-renderer` and the SVG side
|
|
16
|
+
// stays behind — a package-level import cycle, not just a module-level one.
|
|
17
|
+
// It also had a measurable cost today: `dist/ascii.js` carried
|
|
18
|
+
// `import "elkjs/lib/elk.bundled.js"` purely because the registry dragged
|
|
19
|
+
// `src/er/layout.ts` -> `src/elk-instance.ts` into the ASCII entry's module
|
|
20
|
+
// graph, which is precisely what the `./ascii` subpath export (#300) exists
|
|
21
|
+
// to avoid.
|
|
22
|
+
//
|
|
23
|
+
// Splitting the table by renderer removes the back-edge: the ASCII side owns
|
|
24
|
+
// its own dispatch here, and `src/diagram-registry.ts` keeps the SVG side and
|
|
25
|
+
// no longer names anything under `src/ascii/`. Both directions are now
|
|
26
|
+
// one-way, and the set of registered types stays a single decision per
|
|
27
|
+
// renderer rather than a `switch` re-listed at each front door — the point of
|
|
28
|
+
// #533. Adding a type to one renderer's table without the other is
|
|
29
|
+
// expressible in principle, though in practice 'xychart'/'er'/'sequence'/
|
|
30
|
+
// 'class'/'flowchart' now move together on both sides.
|
|
31
|
+
// ============================================================================
|
|
32
|
+
|
|
33
|
+
import type { DiagramType, Direction } from '@zombie-mermaid/core'
|
|
34
|
+
import type { AsciiConfig, AsciiTheme, ColorMode } from './types.ts'
|
|
35
|
+
import { renderXYChartAscii } from './xychart.ts'
|
|
36
|
+
import { renderErAscii } from './er-diagram.ts'
|
|
37
|
+
import { renderSequenceAscii } from './sequence.ts'
|
|
38
|
+
import { renderClassAscii } from './class-diagram.ts'
|
|
39
|
+
import { renderFlowchartAscii } from './flowchart.ts'
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Small, closed set of ASCII-only extras not every type needs — `class`
|
|
43
|
+
* reads `hyperlinks` (see `ClassAsciiOptions` in
|
|
44
|
+
* src/ascii/class-diagram.ts), `flowchart` reads both `direction` and
|
|
45
|
+
* `hyperlinks` (see `FlowchartAsciiExtras` in src/ascii/flowchart.ts). Kept
|
|
46
|
+
* as its own type rather than reusing `AsciiRenderOptions` from
|
|
47
|
+
* src/ascii/index.ts so this module stays a leaf of the ASCII tree: index.ts
|
|
48
|
+
* imports it, never the other way round.
|
|
49
|
+
*/
|
|
50
|
+
export interface AsciiRenderExtras {
|
|
51
|
+
hyperlinks?: boolean
|
|
52
|
+
direction?: Direction
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* One registered diagram type's ASCII entry point. Every ASCII renderer
|
|
57
|
+
* reruns its own parse + grid layout + draw from raw text — there is no
|
|
58
|
+
* shared positioned model to hand it, unlike the SVG side's
|
|
59
|
+
* `parse`/`layoutForSvg` split (see `DiagramModule` in
|
|
60
|
+
* src/diagram-registry.ts for why a shared layout step would be fiction
|
|
61
|
+
* here).
|
|
62
|
+
*/
|
|
63
|
+
export type AsciiRenderer = (
|
|
64
|
+
text: string,
|
|
65
|
+
config: AsciiConfig,
|
|
66
|
+
colorMode: ColorMode,
|
|
67
|
+
theme: AsciiTheme,
|
|
68
|
+
extras: AsciiRenderExtras,
|
|
69
|
+
) => string
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Every diagram type `renderMermaidASCII` supports, dispatched through this
|
|
73
|
+
* table — see that function in src/ascii/index.ts, which has no fallback
|
|
74
|
+
* switch left now that 'flowchart' (the last holdout — see
|
|
75
|
+
* docs/decisions/diagram-type-registry-partial.md) is registered here too.
|
|
76
|
+
*/
|
|
77
|
+
export const asciiRegistry: Record<DiagramType, AsciiRenderer> = {
|
|
78
|
+
xychart: (text, config, colorMode, theme) =>
|
|
79
|
+
renderXYChartAscii(text, config, colorMode, theme),
|
|
80
|
+
er: (text, config, colorMode, theme) =>
|
|
81
|
+
renderErAscii(text, config, colorMode, theme),
|
|
82
|
+
sequence: (text, config, colorMode, theme) =>
|
|
83
|
+
renderSequenceAscii(text, config, colorMode, theme),
|
|
84
|
+
class: (text, config, colorMode, theme, extras) =>
|
|
85
|
+
renderClassAscii(text, config, colorMode, theme, extras),
|
|
86
|
+
flowchart: (text, config, colorMode, theme, extras) =>
|
|
87
|
+
renderFlowchartAscii(text, config, colorMode, theme, extras),
|
|
88
|
+
}
|