@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.
Files changed (232) hide show
  1. package/LICENSE +22 -0
  2. package/dist/index.cjs +8 -0
  3. package/dist/index.cjs.map +1 -0
  4. package/dist/index.d.cts +146 -0
  5. package/dist/index.d.ts +146 -0
  6. package/dist/index.js +5392 -0
  7. package/dist/index.js.map +1 -0
  8. package/package.json +36 -0
  9. package/src/__tests__/ascii-arrowhead-direction-1083.test.ts +51 -0
  10. package/src/__tests__/ascii-canvas-first-claim-wins-1093.test.ts +89 -0
  11. package/src/__tests__/ascii-canvas-size-offset-1093.test.ts +95 -0
  12. package/src/__tests__/ascii-canvas-write.test.ts +132 -0
  13. package/src/__tests__/ascii-chain-edge-overlap-1067.test.ts +100 -0
  14. package/src/__tests__/ascii-charset-border-junctions.test.ts +63 -0
  15. package/src/__tests__/ascii-cjk-width.test.ts +150 -0
  16. package/src/__tests__/ascii-class-box-occupancy.test.ts +462 -0
  17. package/src/__tests__/ascii-class-column-width-488-489.test.ts +639 -0
  18. package/src/__tests__/ascii-class-cross-level-jog-corruption.test.ts +187 -0
  19. package/src/__tests__/ascii-class-detour-label-routing-487.test.ts +114 -0
  20. package/src/__tests__/ascii-class-diagram-compartments.test.ts +70 -0
  21. package/src/__tests__/ascii-class-label-box-collision.test.ts +60 -0
  22. package/src/__tests__/ascii-class-label-row-collision-531.test.ts +135 -0
  23. package/src/__tests__/ascii-class-label-territory-row-awareness.test.ts +57 -0
  24. package/src/__tests__/ascii-class-padding.test.ts +104 -0
  25. package/src/__tests__/ascii-class-parent-alignment-971.test.ts +139 -0
  26. package/src/__tests__/ascii-class-parent-alignment-972.test.ts +208 -0
  27. package/src/__tests__/ascii-class-reciprocal-relationships-448.test.ts +169 -0
  28. package/src/__tests__/ascii-combining-mark-width.test.ts +70 -0
  29. package/src/__tests__/ascii-coords-overlay.test.ts +64 -0
  30. package/src/__tests__/ascii-decision-lr-box-start.test.ts +106 -0
  31. package/src/__tests__/ascii-display-width-unit.test.ts +154 -0
  32. package/src/__tests__/ascii-draw-arrows-coverage.test.ts +224 -0
  33. package/src/__tests__/ascii-draw-arrows-single-point-path.test.ts +105 -0
  34. package/src/__tests__/ascii-edge-bundling-rank-violation-454.test.ts +72 -0
  35. package/src/__tests__/ascii-edge-ending-glyphs.test.ts +206 -0
  36. package/src/__tests__/ascii-edge-label-diagonal-fallback-418.test.ts +95 -0
  37. package/src/__tests__/ascii-edge-routing-fixes.test.ts +178 -0
  38. package/src/__tests__/ascii-edge-routing-single-point-path.test.ts +103 -0
  39. package/src/__tests__/ascii-edge-style-consistency-1067.test.ts +89 -0
  40. package/src/__tests__/ascii-edge-styles.test.ts +149 -0
  41. package/src/__tests__/ascii-emoji-cluster-width.test.ts +101 -0
  42. package/src/__tests__/ascii-er-box-occupancy.test.ts +251 -0
  43. package/src/__tests__/ascii-er-cardinality.test.ts +40 -0
  44. package/src/__tests__/ascii-er-corner-glyphs.test.ts +249 -0
  45. package/src/__tests__/ascii-er-jog-stray-line.test.ts +111 -0
  46. package/src/__tests__/ascii-er-label-padding.test.ts +118 -0
  47. package/src/__tests__/ascii-er-padding.test.ts +114 -0
  48. package/src/__tests__/ascii-er-relationship-label-corruption-350.test.ts +458 -0
  49. package/src/__tests__/ascii-er-relationship-overwrite.test.ts +325 -0
  50. package/src/__tests__/ascii-er-stray-connectors.test.ts +344 -0
  51. package/src/__tests__/ascii-er-unrelated-stem-separation-411.test.ts +98 -0
  52. package/src/__tests__/ascii-er-vertical-one-marker.test.ts +110 -0
  53. package/src/__tests__/ascii-label-line-terminal-fallback.test.ts +168 -0
  54. package/src/__tests__/ascii-lane-search.test.ts +207 -0
  55. package/src/__tests__/ascii-multibox-cjk-width.test.ts +184 -0
  56. package/src/__tests__/ascii-multiline.test.ts +288 -0
  57. package/src/__tests__/ascii-padding-edge-cases.test.ts +154 -0
  58. package/src/__tests__/ascii-pathfinder-route-edge.test.ts +184 -0
  59. package/src/__tests__/ascii-sequence-alt-else-label.test.ts +236 -0
  60. package/src/__tests__/ascii-sequence-block-wall-clearance.test.ts +217 -0
  61. package/src/__tests__/ascii-sequence-box-group.test.ts +159 -0
  62. package/src/__tests__/ascii-sequence-cjk-width.test.ts +235 -0
  63. package/src/__tests__/ascii-sequence-create-destroy.test.ts +114 -0
  64. package/src/__tests__/ascii-sequence-form-invariants.test.ts +432 -0
  65. package/src/__tests__/ascii-sequence-mermaid-parity.test.ts +219 -0
  66. package/src/__tests__/ascii-sequence-notes.test.ts +61 -0
  67. package/src/__tests__/ascii-sequence-padding.test.ts +119 -0
  68. package/src/__tests__/ascii-sequence-self-arrow.test.ts +206 -0
  69. package/src/__tests__/ascii-shape-diamond.test.ts +38 -0
  70. package/src/__tests__/ascii-shape-rectangle.test.ts +258 -0
  71. package/src/__tests__/ascii-shape-rounded.test.ts +36 -0
  72. package/src/__tests__/ascii-shapes-circle.test.ts +41 -0
  73. package/src/__tests__/ascii-shapes-hexagon.test.ts +42 -0
  74. package/src/__tests__/ascii-shapes-special.test.ts +344 -0
  75. package/src/__tests__/ascii-shapes-stadium.test.ts +217 -0
  76. package/src/__tests__/ascii-shapes-state.test.ts +224 -0
  77. package/src/__tests__/ascii-state-bidirectional-label-swap-530.test.ts +130 -0
  78. package/src/__tests__/ascii-subgraph-direction-honored-445.test.ts +90 -0
  79. package/src/__tests__/ascii-subgraph-label-border-clip.test.ts +152 -0
  80. package/src/__tests__/ascii-subgraph-title-padding.test.ts +77 -0
  81. package/src/__tests__/ascii-territory-unit.test.ts +219 -0
  82. package/src/__tests__/ascii-validate.test.ts +220 -0
  83. package/src/__tests__/ascii.test.ts +325 -0
  84. package/src/__tests__/class-arrow-directions.test.ts +505 -0
  85. package/src/__tests__/draw-lines.test.ts +93 -0
  86. package/src/__tests__/edge-cell-styles.test.ts +278 -0
  87. package/src/__tests__/grid-occupancy.test.ts +240 -0
  88. package/src/__tests__/helpers/ascii-form.ts +142 -0
  89. package/src/__tests__/helpers/terminal-display-width.ts +74 -0
  90. package/src/__tests__/pathfinder.test.ts +239 -0
  91. package/src/__tests__/testdata/ascii/ampersand_lhs.txt +18 -0
  92. package/src/__tests__/testdata/ascii/ampersand_lhs_and_rhs.txt +18 -0
  93. package/src/__tests__/testdata/ascii/ampersand_rhs.txt +18 -0
  94. package/src/__tests__/testdata/ascii/ampersand_td_fanin.txt +18 -0
  95. package/src/__tests__/testdata/ascii/ampersand_td_fanout.txt +18 -0
  96. package/src/__tests__/testdata/ascii/ampersand_without_edge.txt +18 -0
  97. package/src/__tests__/testdata/ascii/back_reference_from_child.txt +10 -0
  98. package/src/__tests__/testdata/ascii/backlink_from_bottom.txt +22 -0
  99. package/src/__tests__/testdata/ascii/backlink_from_top.txt +22 -0
  100. package/src/__tests__/testdata/ascii/backlink_with_short_y_padding.txt +20 -0
  101. package/src/__tests__/testdata/ascii/cls_all_relationships.txt +19 -0
  102. package/src/__tests__/testdata/ascii/cls_annotation.txt +29 -0
  103. package/src/__tests__/testdata/ascii/cls_association.txt +14 -0
  104. package/src/__tests__/testdata/ascii/cls_basic.txt +15 -0
  105. package/src/__tests__/testdata/ascii/cls_dependency.txt +14 -0
  106. package/src/__tests__/testdata/ascii/cls_inheritance.txt +20 -0
  107. package/src/__tests__/testdata/ascii/cls_methods.txt +21 -0
  108. package/src/__tests__/testdata/ascii/comments.txt +23 -0
  109. package/src/__tests__/testdata/ascii/custom_padding.txt +10 -0
  110. package/src/__tests__/testdata/ascii/duplicate_labels.txt +19 -0
  111. package/src/__tests__/testdata/ascii/er_attributes.txt +21 -0
  112. package/src/__tests__/testdata/ascii/er_basic.txt +8 -0
  113. package/src/__tests__/testdata/ascii/er_identifying.txt +18 -0
  114. package/src/__tests__/testdata/ascii/flowchart_tb_simple.txt +29 -0
  115. package/src/__tests__/testdata/ascii/graph_bt_direction.txt +28 -0
  116. package/src/__tests__/testdata/ascii/graph_tb_direction.txt +26 -0
  117. package/src/__tests__/testdata/ascii/nested_subgraphs_with_labels.txt +36 -0
  118. package/src/__tests__/testdata/ascii/preserve_order_of_definition.txt +23 -0
  119. package/src/__tests__/testdata/ascii/self_reference.txt +10 -0
  120. package/src/__tests__/testdata/ascii/self_reference_with_edge.txt +10 -0
  121. package/src/__tests__/testdata/ascii/seq_basic.txt +17 -0
  122. package/src/__tests__/testdata/ascii/seq_multiple_messages.txt +25 -0
  123. package/src/__tests__/testdata/ascii/seq_self_message.txt +18 -0
  124. package/src/__tests__/testdata/ascii/single_node.txt +8 -0
  125. package/src/__tests__/testdata/ascii/single_node_longer_name.txt +8 -0
  126. package/src/__tests__/testdata/ascii/subgraph_complex_mixed.txt +38 -0
  127. package/src/__tests__/testdata/ascii/subgraph_complex_nested.txt +49 -0
  128. package/src/__tests__/testdata/ascii/subgraph_direction_override.txt +47 -0
  129. package/src/__tests__/testdata/ascii/subgraph_empty.txt +10 -0
  130. package/src/__tests__/testdata/ascii/subgraph_mixed_nodes.txt +20 -0
  131. package/src/__tests__/testdata/ascii/subgraph_mixed_nodes_td.txt +48 -0
  132. package/src/__tests__/testdata/ascii/subgraph_multiple_edges.txt +32 -0
  133. package/src/__tests__/testdata/ascii/subgraph_multiple_nodes.txt +16 -0
  134. package/src/__tests__/testdata/ascii/subgraph_nested.txt +24 -0
  135. package/src/__tests__/testdata/ascii/subgraph_nested_with_external.txt +30 -0
  136. package/src/__tests__/testdata/ascii/subgraph_node_outside_lr.txt +17 -0
  137. package/src/__tests__/testdata/ascii/subgraph_single_node.txt +16 -0
  138. package/src/__tests__/testdata/ascii/subgraph_td_direction.txt +26 -0
  139. package/src/__tests__/testdata/ascii/subgraph_td_multiple.txt +44 -0
  140. package/src/__tests__/testdata/ascii/subgraph_td_multiple_paddingy.txt +42 -0
  141. package/src/__tests__/testdata/ascii/subgraph_three_levels_nested.txt +32 -0
  142. package/src/__tests__/testdata/ascii/subgraph_three_separate.txt +24 -0
  143. package/src/__tests__/testdata/ascii/subgraph_two_separate.txt +20 -0
  144. package/src/__tests__/testdata/ascii/subgraph_with_labels.txt +20 -0
  145. package/src/__tests__/testdata/ascii/three_nodes.txt +9 -0
  146. package/src/__tests__/testdata/ascii/three_nodes_single_line.txt +8 -0
  147. package/src/__tests__/testdata/ascii/two_layer_single_graph.txt +19 -0
  148. package/src/__tests__/testdata/ascii/two_layer_single_graph_longer_names.txt +19 -0
  149. package/src/__tests__/testdata/ascii/two_nodes_linked.txt +8 -0
  150. package/src/__tests__/testdata/ascii/two_nodes_longer_names.txt +8 -0
  151. package/src/__tests__/testdata/ascii/two_root_nodes.txt +19 -0
  152. package/src/__tests__/testdata/ascii/two_root_nodes_longer_names.txt +19 -0
  153. package/src/__tests__/testdata/ascii/two_single_root_nodes.txt +19 -0
  154. package/src/__tests__/testdata/unicode/ampersand_lhs.txt +18 -0
  155. package/src/__tests__/testdata/unicode/ampersand_lhs_and_rhs.txt +18 -0
  156. package/src/__tests__/testdata/unicode/ampersand_rhs.txt +18 -0
  157. package/src/__tests__/testdata/unicode/ampersand_without_edge.txt +18 -0
  158. package/src/__tests__/testdata/unicode/back_reference_from_child.txt +10 -0
  159. package/src/__tests__/testdata/unicode/backlink_from_bottom.txt +22 -0
  160. package/src/__tests__/testdata/unicode/backlink_from_top.txt +22 -0
  161. package/src/__tests__/testdata/unicode/cls_all_relationships.txt +19 -0
  162. package/src/__tests__/testdata/unicode/cls_annotation.txt +29 -0
  163. package/src/__tests__/testdata/unicode/cls_association.txt +14 -0
  164. package/src/__tests__/testdata/unicode/cls_basic.txt +15 -0
  165. package/src/__tests__/testdata/unicode/cls_dependency.txt +14 -0
  166. package/src/__tests__/testdata/unicode/cls_inheritance.txt +20 -0
  167. package/src/__tests__/testdata/unicode/cls_methods.txt +21 -0
  168. package/src/__tests__/testdata/unicode/comments.txt +23 -0
  169. package/src/__tests__/testdata/unicode/duplicate_labels.txt +19 -0
  170. package/src/__tests__/testdata/unicode/er_attributes.txt +21 -0
  171. package/src/__tests__/testdata/unicode/er_basic.txt +8 -0
  172. package/src/__tests__/testdata/unicode/er_identifying.txt +18 -0
  173. package/src/__tests__/testdata/unicode/graph_bt_direction.txt +28 -0
  174. package/src/__tests__/testdata/unicode/preserve_order_of_definition.txt +23 -0
  175. package/src/__tests__/testdata/unicode/self_reference.txt +10 -0
  176. package/src/__tests__/testdata/unicode/self_reference_with_edge.txt +10 -0
  177. package/src/__tests__/testdata/unicode/seq_basic.txt +17 -0
  178. package/src/__tests__/testdata/unicode/seq_multiple_messages.txt +25 -0
  179. package/src/__tests__/testdata/unicode/seq_self_message.txt +18 -0
  180. package/src/__tests__/testdata/unicode/single_node.txt +8 -0
  181. package/src/__tests__/testdata/unicode/single_node_longer_name.txt +8 -0
  182. package/src/__tests__/testdata/unicode/three_nodes.txt +9 -0
  183. package/src/__tests__/testdata/unicode/three_nodes_single_line.txt +8 -0
  184. package/src/__tests__/testdata/unicode/two_layer_single_graph.txt +19 -0
  185. package/src/__tests__/testdata/unicode/two_layer_single_graph_longer_names.txt +19 -0
  186. package/src/__tests__/testdata/unicode/two_nodes_linked.txt +8 -0
  187. package/src/__tests__/testdata/unicode/two_nodes_longer_names.txt +8 -0
  188. package/src/__tests__/testdata/unicode/two_root_nodes.txt +19 -0
  189. package/src/__tests__/testdata/unicode/two_root_nodes_longer_names.txt +19 -0
  190. package/src/__tests__/testdata/unicode/two_single_root_nodes.txt +19 -0
  191. package/src/__tests__/xychart-ascii.test.ts +376 -0
  192. package/src/ansi.ts +490 -0
  193. package/src/canvas.ts +757 -0
  194. package/src/class-diagram.ts +2001 -0
  195. package/src/converter.ts +446 -0
  196. package/src/coords.ts +58 -0
  197. package/src/display-width.ts +151 -0
  198. package/src/draw-arrows.ts +593 -0
  199. package/src/draw-boxes.ts +267 -0
  200. package/src/draw-bundles.ts +611 -0
  201. package/src/draw-lines.ts +174 -0
  202. package/src/draw-subgraphs.ts +108 -0
  203. package/src/draw.ts +350 -0
  204. package/src/edge-bundling.ts +435 -0
  205. package/src/edge-cell-styles.ts +209 -0
  206. package/src/edge-routing.ts +1070 -0
  207. package/src/er-diagram.ts +1488 -0
  208. package/src/flowchart.ts +94 -0
  209. package/src/grid-occupancy.ts +234 -0
  210. package/src/grid.ts +1309 -0
  211. package/src/hyperlinks.ts +248 -0
  212. package/src/index.ts +163 -0
  213. package/src/lane-search.ts +68 -0
  214. package/src/multiline-utils.ts +82 -0
  215. package/src/pathfinder.ts +448 -0
  216. package/src/registry.ts +88 -0
  217. package/src/sequence.ts +1318 -0
  218. package/src/shapes/circle.ts +31 -0
  219. package/src/shapes/corners.ts +273 -0
  220. package/src/shapes/diamond.ts +31 -0
  221. package/src/shapes/hexagon.ts +35 -0
  222. package/src/shapes/index.ts +123 -0
  223. package/src/shapes/rectangle.ts +199 -0
  224. package/src/shapes/rounded.ts +31 -0
  225. package/src/shapes/special.ts +360 -0
  226. package/src/shapes/stadium.ts +122 -0
  227. package/src/shapes/state.ts +204 -0
  228. package/src/shapes/types.ts +78 -0
  229. package/src/territory.ts +136 -0
  230. package/src/types.ts +454 -0
  231. package/src/validate.ts +189 -0
  232. package/src/xychart.ts +1085 -0
@@ -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
+ }