@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
package/src/flowchart.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// ASCII renderer — Flowchart / state diagram
|
|
3
|
+
//
|
|
4
|
+
// Renders flowcharts (graph TD / flowchart LR) and state diagrams
|
|
5
|
+
// (stateDiagram-v2) to ASCII/Unicode box-drawing art via the shared
|
|
6
|
+
// grid-based layout + A* pathfinding pipeline (converter -> grid -> draw).
|
|
7
|
+
//
|
|
8
|
+
// Extracted from the inline sequence that used to live directly in
|
|
9
|
+
// `renderMermaidASCII`'s switch/fallback (src/ascii/index.ts) so flowchart
|
|
10
|
+
// can be registered in `asciiRegistry` (src/ascii/registry.ts) alongside
|
|
11
|
+
// every other diagram type — see
|
|
12
|
+
// docs/decisions/diagram-type-registry-partial.md and issue #745. A pure,
|
|
13
|
+
// behavior-preserving extraction: same functions, same call order, same
|
|
14
|
+
// arguments.
|
|
15
|
+
// ============================================================================
|
|
16
|
+
|
|
17
|
+
import { parseMermaid } from '../../../src/parser.ts'
|
|
18
|
+
import { withDirectionOverride } from '@zombie-mermaid/core'
|
|
19
|
+
import type { Direction } from '@zombie-mermaid/core'
|
|
20
|
+
import { convertToAsciiGraph } from './converter.ts'
|
|
21
|
+
import { createMapping } from './grid.ts'
|
|
22
|
+
import { drawGraph } from './draw.ts'
|
|
23
|
+
import {
|
|
24
|
+
canvasToString,
|
|
25
|
+
flipCanvasVertically,
|
|
26
|
+
flipRoleCanvasVertically,
|
|
27
|
+
} from './canvas.ts'
|
|
28
|
+
import { buildNodeLinkCanvas, flipLinkCanvasVertically } from './hyperlinks.ts'
|
|
29
|
+
import type { AsciiConfig, AsciiTheme, ColorMode } from './types.ts'
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Flowchart-only ASCII extras: `direction` (override the parsed top-level
|
|
33
|
+
* direction, same semantics as `RenderOptions.direction` for SVG — see
|
|
34
|
+
* issue #276) and `hyperlinks` (OSC 8 terminal links). No other registered
|
|
35
|
+
* ASCII type reads `direction`; `hyperlinks` is shared with `class` (see
|
|
36
|
+
* `AsciiRenderExtras` in ./registry.ts).
|
|
37
|
+
*/
|
|
38
|
+
export interface FlowchartAsciiExtras {
|
|
39
|
+
direction?: Direction
|
|
40
|
+
hyperlinks?: boolean
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Render a flowchart or state diagram to ASCII/Unicode text art.
|
|
45
|
+
*
|
|
46
|
+
* Matches every other diagram type's ASCII entry-point shape
|
|
47
|
+
* (`text, config, colorMode, theme, extras`) so it can slot into
|
|
48
|
+
* `asciiRegistry` — see ./registry.ts.
|
|
49
|
+
*/
|
|
50
|
+
export function renderFlowchartAscii(
|
|
51
|
+
text: string,
|
|
52
|
+
config: AsciiConfig,
|
|
53
|
+
colorMode: ColorMode,
|
|
54
|
+
theme: AsciiTheme,
|
|
55
|
+
extras: FlowchartAsciiExtras = {},
|
|
56
|
+
): string {
|
|
57
|
+
// `extras.direction` replaces the parsed top-level direction before
|
|
58
|
+
// layout; see packages/core/src/direction-override.ts.
|
|
59
|
+
const parsed = withDirectionOverride(parseMermaid(text), extras.direction)
|
|
60
|
+
|
|
61
|
+
// Normalize direction for grid layout.
|
|
62
|
+
// BT is laid out as TD then flipped vertically after drawing.
|
|
63
|
+
// RL is treated as LR (full RL support not yet implemented).
|
|
64
|
+
if (parsed.direction === 'LR' || parsed.direction === 'RL') {
|
|
65
|
+
config.graphDirection = 'LR'
|
|
66
|
+
} else {
|
|
67
|
+
config.graphDirection = 'TD'
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const graph = convertToAsciiGraph(parsed, config)
|
|
71
|
+
createMapping(graph)
|
|
72
|
+
drawGraph(graph)
|
|
73
|
+
|
|
74
|
+
// Opt-in OSC 8 hyperlinks: mark each `click`-linked node's label cells
|
|
75
|
+
// now, from the drawn node positions, before any flip below moves them.
|
|
76
|
+
const linkCanvas = extras.hyperlinks
|
|
77
|
+
? buildNodeLinkCanvas(graph, parsed.interactions)
|
|
78
|
+
: undefined
|
|
79
|
+
|
|
80
|
+
// BT: flip the finished canvas vertically so the flow runs bottom→top.
|
|
81
|
+
// The grid layout ran as TD; flipping + character remapping produces BT.
|
|
82
|
+
if (parsed.direction === 'BT') {
|
|
83
|
+
flipCanvasVertically(graph.canvas)
|
|
84
|
+
flipRoleCanvasVertically(graph.roleCanvas)
|
|
85
|
+
if (linkCanvas) flipLinkCanvasVertically(linkCanvas)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return canvasToString(graph.canvas, {
|
|
89
|
+
roleCanvas: graph.roleCanvas,
|
|
90
|
+
colorMode,
|
|
91
|
+
theme,
|
|
92
|
+
linkCanvas,
|
|
93
|
+
})
|
|
94
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// ASCII renderer — grid occupancy map
|
|
3
|
+
//
|
|
4
|
+
// An opaque set of "x,y" keys tracking which logical grid cells are
|
|
5
|
+
// reserved. Before this module existed, `grid.ts`, `pathfinder.ts`, and
|
|
6
|
+
// `edge-routing.ts` each poked at a raw `Map<string, AsciiNode>` directly
|
|
7
|
+
// (`.has(gridKey(c))`, `.set(gridKey(c), node)`), so the "x,y" string-keying
|
|
8
|
+
// scheme had no single owner. Centralizing it here means the keying scheme
|
|
9
|
+
// only needs to be gotten right once, call sites read as grid operations
|
|
10
|
+
// (`isFree`, `placeBlock`) rather than raw Map/Set calls, and — because
|
|
11
|
+
// `Grid` stores its cells behind a real (`#`-private, not just
|
|
12
|
+
// TypeScript-`private`) field — nothing outside this module can bypass
|
|
13
|
+
// `gridKey` and the collision checks by reaching into the underlying
|
|
14
|
+
// collection directly, not even via an `any` cast or plain-JS caller.
|
|
15
|
+
// ============================================================================
|
|
16
|
+
|
|
17
|
+
import type { GridCoord } from './types.ts'
|
|
18
|
+
import { gridKey } from './types.ts'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Grid occupancy map — an opaque set of reserved "x,y" cells. Only tracks
|
|
22
|
+
* *whether* a cell is reserved, not by whom: nothing in this renderer looks
|
|
23
|
+
* up which node owns a given cell (layout code identifies a node's cells via
|
|
24
|
+
* `node.gridCoord`, not via the grid), so there is no per-cell node value to
|
|
25
|
+
* keep honest.
|
|
26
|
+
*
|
|
27
|
+
* `#cells` is a JavaScript private field, not merely a TypeScript `private`
|
|
28
|
+
* annotation: the latter is erased at compile time and still reachable at
|
|
29
|
+
* runtime via `(grid as any).cells` or plain bracket access, which would
|
|
30
|
+
* let a caller add/delete/clear cells directly and bypass `gridKey` and the
|
|
31
|
+
* collision checks in `placeBlock`. A `#`-private field has no such escape
|
|
32
|
+
* hatch — it's enforced by the JS engine itself, so only the methods below
|
|
33
|
+
* can ever touch the underlying `Set`.
|
|
34
|
+
*/
|
|
35
|
+
export class Grid {
|
|
36
|
+
readonly #cells = new Set<string>()
|
|
37
|
+
|
|
38
|
+
/** Whether a single cell is already reserved. */
|
|
39
|
+
has(key: string): boolean {
|
|
40
|
+
return this.#cells.has(key)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Reserve a single cell. Internal — callers go through `placeBlock`. */
|
|
44
|
+
add(key: string): void {
|
|
45
|
+
this.#cells.add(key)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Release a single cell. For temporary reservations only (see
|
|
50
|
+
* `rerouteAroundStyleConflicts` in grid.ts) — every other reservation in
|
|
51
|
+
* this module (node blocks, in particular) is permanent for the life of
|
|
52
|
+
* a render, so a real caller should rarely need this.
|
|
53
|
+
*/
|
|
54
|
+
delete(key: string): void {
|
|
55
|
+
this.#cells.delete(key)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Iterate the reserved cell keys. Read-only — see `cloneGrid`. */
|
|
59
|
+
keys(): IterableIterator<string> {
|
|
60
|
+
return this.#cells.keys()
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Create a fresh, empty grid occupancy map. */
|
|
65
|
+
export function createGrid(): Grid {
|
|
66
|
+
return new Grid()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Copy a grid's current reservations into a brand-new, independent `Grid`.
|
|
71
|
+
*
|
|
72
|
+
* Used to snapshot *node* occupancy before edge routing starts (grid.ts's
|
|
73
|
+
* `createMapping`, right after all `placeBlock` calls and before the
|
|
74
|
+
* edge-routing loop) so that later mutating `graph.grid` — specifically
|
|
75
|
+
* `rerouteAroundStyleConflicts`'s temporary `add`/`delete` of a conflicting
|
|
76
|
+
* cell — can't change what counts as "node-owned" for
|
|
77
|
+
* `edge-cell-styles.ts`'s conflict check. Without this, a temporarily
|
|
78
|
+
* blocked cell looks node-occupied to `findStyleConflict` for exactly as
|
|
79
|
+
* long as it's reserved, which is precisely when a re-route needs to
|
|
80
|
+
* re-check it — see the regression test this snapshot fixes in
|
|
81
|
+
* ascii-edge-cross-style-overlap.test.ts.
|
|
82
|
+
*/
|
|
83
|
+
export function cloneGrid(grid: Grid): Grid {
|
|
84
|
+
const clone = createGrid()
|
|
85
|
+
for (const key of grid.keys()) clone.add(key)
|
|
86
|
+
return clone
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The N x N block of cells a node reserves: border, content, border. */
|
|
90
|
+
export const NODE_BLOCK_SIZE = 3
|
|
91
|
+
|
|
92
|
+
/** Whether a single cell is already reserved. */
|
|
93
|
+
export function isOccupied(grid: Grid, coord: GridCoord): boolean {
|
|
94
|
+
return grid.has(gridKey(coord))
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Whether a cell is free to route/place into: unoccupied and within the
|
|
99
|
+
* non-negative quadrant.
|
|
100
|
+
*
|
|
101
|
+
* The bounds check is not an incidental side benefit of occupancy checking —
|
|
102
|
+
* it is the *only* thing that bounds A*'s search space. `pathfinder.ts`'s
|
|
103
|
+
* neighbor expansion generates `{x:-1,y:0}` / `{x:0,y:-1}` from `MOVE_DIRS`
|
|
104
|
+
* on every iteration; without this check A* explores the infinite negative
|
|
105
|
+
* quadrant on the first edge, burning the entire path budget. Do not remove
|
|
106
|
+
* it as redundant.
|
|
107
|
+
*
|
|
108
|
+
* Note `isFree` is not the logical negation of `isOccupied`: for a negative
|
|
109
|
+
* coordinate, both return `false`. `reserveSpotInGrid` (grid.ts) uses
|
|
110
|
+
* `isOccupied`/`isBlockFree`; `pathfinder.ts` and `edge-routing.ts` use
|
|
111
|
+
* `isFree`. Do not simplify `!isOccupied(...)` to `isFree(...)` (or vice
|
|
112
|
+
* versa) — that silently changes behavior for negative/out-of-range
|
|
113
|
+
* coordinates.
|
|
114
|
+
*/
|
|
115
|
+
export function isFree(grid: Grid, coord: GridCoord): boolean {
|
|
116
|
+
if (coord.x < 0 || coord.y < 0) return false
|
|
117
|
+
return !isOccupied(grid, coord)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Whether every cell in a `size` x `size` block starting at `origin` is
|
|
122
|
+
* free of existing occupants. Use before `placeBlock` to detect a collision
|
|
123
|
+
* without triggering its throw.
|
|
124
|
+
*/
|
|
125
|
+
export function isBlockFree(
|
|
126
|
+
grid: Grid,
|
|
127
|
+
origin: GridCoord,
|
|
128
|
+
size: number = NODE_BLOCK_SIZE,
|
|
129
|
+
): boolean {
|
|
130
|
+
for (let dx = 0; dx < size; dx++) {
|
|
131
|
+
for (let dy = 0; dy < size; dy++) {
|
|
132
|
+
if (isOccupied(grid, { x: origin.x + dx, y: origin.y + dy })) {
|
|
133
|
+
return false
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return true
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Reserve a `size` x `size` block of cells starting at `origin`. Throws if
|
|
142
|
+
* any cell in the block is already occupied rather than silently
|
|
143
|
+
* transferring ownership — callers that want to handle a collision (e.g. by
|
|
144
|
+
* shifting to a different origin) must check `isBlockFree` first.
|
|
145
|
+
*
|
|
146
|
+
* Validates every cell in the block *before* reserving any of them: an
|
|
147
|
+
* earlier version interleaved the occupancy check with the `add` inside a
|
|
148
|
+
* single loop, so a collision on a later cell threw after earlier cells in
|
|
149
|
+
* the same block had already been reserved — leaving a partial, corrupt
|
|
150
|
+
* reservation behind even though the call as a whole failed. Checking the
|
|
151
|
+
* whole block first means this function either reserves all of it or none
|
|
152
|
+
* of it.
|
|
153
|
+
*/
|
|
154
|
+
export function placeBlock(
|
|
155
|
+
grid: Grid,
|
|
156
|
+
origin: GridCoord,
|
|
157
|
+
size: number = NODE_BLOCK_SIZE,
|
|
158
|
+
): void {
|
|
159
|
+
if (!isBlockFree(grid, origin, size)) {
|
|
160
|
+
throw new Error(
|
|
161
|
+
`Grid block at (${origin.x},${origin.y}) is already occupied`,
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
for (let dx = 0; dx < size; dx++) {
|
|
166
|
+
for (let dy = 0; dy < size; dy++) {
|
|
167
|
+
const coord: GridCoord = { x: origin.x + dx, y: origin.y + dy }
|
|
168
|
+
grid.add(gridKey(coord))
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Every cell an edge's routed path passes through — including intermediate
|
|
175
|
+
* cells on each straight segment, not just the corner waypoints `path`
|
|
176
|
+
* itself contains (A* paths are simplified down to corners by `mergePath`).
|
|
177
|
+
*
|
|
178
|
+
* Segments are walked with integer Bresenham, not a naive "step both axes
|
|
179
|
+
* by their sign each iteration": A*-routed segments are always axis-aligned
|
|
180
|
+
* (see `MOVE_DIRS` in pathfinder.ts), but `determinePath`'s Case-4 direct
|
|
181
|
+
* fallback (`edge.path = [prefFrom, prefTo]`, used when A* finds no route
|
|
182
|
+
* at all) can produce an arbitrary, non-45°, non-axis-aligned segment. A
|
|
183
|
+
* naive "x += sign(dx); y += sign(dy)" walk only ever reaches
|
|
184
|
+
* `(to.x, to.y)` when `|dx| === |dy|` — for any other ratio, x and y drift
|
|
185
|
+
* past each other's target on different iterations and the loop's
|
|
186
|
+
* "both must match" exit condition is never satisfied, walking forever
|
|
187
|
+
* (this happened in testing, on a plain 4-edge fan-out, until it blew a
|
|
188
|
+
* `Set`'s max size). `MAX_WALK_STEPS` is a second, independent safety net in
|
|
189
|
+
* case a future caller feeds in a segment absurdly far from the grid's
|
|
190
|
+
* normal handful-of-columns/rows scale — reaching it throws rather than
|
|
191
|
+
* returning a path quietly truncated partway through the segment, which
|
|
192
|
+
* would under-report to any caller checking "every cell this path touches"
|
|
193
|
+
* (`edge-cell-styles.ts`'s conflict detection, in particular): silently
|
|
194
|
+
* dropping the second half of a segment doesn't fail loudly, it just makes
|
|
195
|
+
* conflict detection blind to whatever's past the cutoff.
|
|
196
|
+
*/
|
|
197
|
+
const MAX_WALK_STEPS = 100_000
|
|
198
|
+
|
|
199
|
+
export function pathCells(path: readonly GridCoord[]): GridCoord[] {
|
|
200
|
+
if (path.length === 0) return []
|
|
201
|
+
const cells: GridCoord[] = [path[0]!]
|
|
202
|
+
for (let i = 1; i < path.length; i++) {
|
|
203
|
+
const from = path[i - 1]!
|
|
204
|
+
const to = path[i]!
|
|
205
|
+
let x = from.x
|
|
206
|
+
let y = from.y
|
|
207
|
+
const absDx = Math.abs(to.x - from.x)
|
|
208
|
+
const absDy = Math.abs(to.y - from.y)
|
|
209
|
+
const sx = from.x < to.x ? 1 : -1
|
|
210
|
+
const sy = from.y < to.y ? 1 : -1
|
|
211
|
+
let err = absDx - absDy
|
|
212
|
+
let steps = 0
|
|
213
|
+
while (x !== to.x || y !== to.y) {
|
|
214
|
+
if (steps >= MAX_WALK_STEPS) {
|
|
215
|
+
throw new Error(
|
|
216
|
+
`pathCells: segment from (${from.x},${from.y}) to (${to.x},${to.y}) ` +
|
|
217
|
+
`did not reach its endpoint within ${MAX_WALK_STEPS} steps`,
|
|
218
|
+
)
|
|
219
|
+
}
|
|
220
|
+
const e2 = 2 * err
|
|
221
|
+
if (e2 > -absDy) {
|
|
222
|
+
err -= absDy
|
|
223
|
+
x += sx
|
|
224
|
+
}
|
|
225
|
+
if (e2 < absDx) {
|
|
226
|
+
err += absDx
|
|
227
|
+
y += sy
|
|
228
|
+
}
|
|
229
|
+
cells.push({ x, y })
|
|
230
|
+
steps++
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return cells
|
|
234
|
+
}
|