@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,204 @@
1
+ // ============================================================================
2
+ // State pseudo-state renderers — UML start and end states
3
+ // ============================================================================
4
+
5
+ import type { Canvas, DrawingCoord, Direction } from '../types.ts'
6
+ import { Up, Down, Left, Right } from '../types.ts'
7
+ import { mkCanvas } from '../canvas.ts'
8
+ import type {
9
+ ShapeRenderer,
10
+ ShapeDimensions,
11
+ ShapeRenderOptions,
12
+ } from './types.ts'
13
+ import { dirEquals } from '../edge-routing.ts'
14
+
15
+ /**
16
+ * State start pseudo-state renderer — filled circle in rounded box.
17
+ * Renders as:
18
+ * ╭───╮
19
+ * │ ● │ (Unicode)
20
+ * ╰───╯
21
+ *
22
+ * .---.
23
+ * | * | (ASCII)
24
+ * '---'
25
+ *
26
+ * This represents the UML initial pseudo-state.
27
+ */
28
+ export const stateStartRenderer: ShapeRenderer = {
29
+ getDimensions(_label: string, _options: ShapeRenderOptions): ShapeDimensions {
30
+ // Start state is a 5x3 rounded box with centered symbol
31
+ const width = 5
32
+ const height = 3
33
+
34
+ return {
35
+ width,
36
+ height,
37
+ labelArea: { x: 2, y: 1, width: 1, height: 1 },
38
+ gridColumns: [1, 3, 1],
39
+ gridRows: [1, 1, 1],
40
+ }
41
+ },
42
+
43
+ render(
44
+ _label: string,
45
+ dimensions: ShapeDimensions,
46
+ options: ShapeRenderOptions,
47
+ ): Canvas {
48
+ const { width, height } = dimensions
49
+ const canvas = mkCanvas(width - 1, height - 1)
50
+
51
+ const centerX = Math.floor(width / 2) // = 2
52
+
53
+ if (!options.useAscii) {
54
+ // Unicode rounded box with filled circle: ╭───╮ │ ● │ ╰───╯
55
+ canvas[0]![0] = '╭'
56
+ canvas[1]![0] = '─'
57
+ canvas[2]![0] = '─'
58
+ canvas[3]![0] = '─'
59
+ canvas[4]![0] = '╮'
60
+
61
+ canvas[0]![1] = '│'
62
+ canvas[centerX]![1] = '●'
63
+ canvas[4]![1] = '│'
64
+
65
+ canvas[0]![2] = '╰'
66
+ canvas[1]![2] = '─'
67
+ canvas[2]![2] = '─'
68
+ canvas[3]![2] = '─'
69
+ canvas[4]![2] = '╯'
70
+ } else {
71
+ // ASCII rounded box: .---. | * | '---'
72
+ canvas[0]![0] = '.'
73
+ canvas[1]![0] = '-'
74
+ canvas[2]![0] = '-'
75
+ canvas[3]![0] = '-'
76
+ canvas[4]![0] = '.'
77
+
78
+ canvas[0]![1] = '|'
79
+ canvas[centerX]![1] = '*'
80
+ canvas[4]![1] = '|'
81
+
82
+ canvas[0]![2] = "'"
83
+ canvas[1]![2] = '-'
84
+ canvas[2]![2] = '-'
85
+ canvas[3]![2] = '-'
86
+ canvas[4]![2] = "'"
87
+ }
88
+
89
+ return canvas
90
+ },
91
+
92
+ getAttachmentPoint(
93
+ dir: Direction,
94
+ dimensions: ShapeDimensions,
95
+ baseCoord: DrawingCoord,
96
+ ): DrawingCoord {
97
+ const { width, height } = dimensions
98
+ const centerX = baseCoord.x + Math.floor(width / 2)
99
+ const centerY = baseCoord.y + Math.floor(height / 2)
100
+
101
+ if (dirEquals(dir, Up)) return { x: centerX, y: baseCoord.y }
102
+ if (dirEquals(dir, Down)) return { x: centerX, y: baseCoord.y + height - 1 }
103
+ if (dirEquals(dir, Left)) return { x: baseCoord.x, y: centerY }
104
+ if (dirEquals(dir, Right)) return { x: baseCoord.x + width - 1, y: centerY }
105
+ // All diagonals and middle point to center
106
+ return { x: centerX, y: centerY }
107
+ },
108
+ }
109
+
110
+ /**
111
+ * State end pseudo-state renderer — bullseye in double-bordered box.
112
+ * Renders as:
113
+ * ╔═══╗
114
+ * ║ ◎ ║ (Unicode)
115
+ * ╚═══╝
116
+ *
117
+ * #===#
118
+ * # * # (ASCII)
119
+ * #===#
120
+ *
121
+ * This represents the UML final state. The double border distinguishes it
122
+ * from the start state's single rounded border.
123
+ */
124
+ export const stateEndRenderer: ShapeRenderer = {
125
+ getDimensions(_label: string, _options: ShapeRenderOptions): ShapeDimensions {
126
+ // End state is a 5x3 double-bordered box with centered symbol
127
+ const width = 5
128
+ const height = 3
129
+
130
+ return {
131
+ width,
132
+ height,
133
+ labelArea: { x: 2, y: 1, width: 1, height: 1 },
134
+ gridColumns: [1, 3, 1],
135
+ gridRows: [1, 1, 1],
136
+ }
137
+ },
138
+
139
+ render(
140
+ _label: string,
141
+ dimensions: ShapeDimensions,
142
+ options: ShapeRenderOptions,
143
+ ): Canvas {
144
+ const { width, height } = dimensions
145
+ const canvas = mkCanvas(width - 1, height - 1)
146
+
147
+ const centerX = Math.floor(width / 2) // = 2
148
+
149
+ if (!options.useAscii) {
150
+ // Unicode double-bordered box with bullseye: ╔═══╗ ║ ◎ ║ ╚═══╝
151
+ canvas[0]![0] = '╔'
152
+ canvas[1]![0] = '═'
153
+ canvas[2]![0] = '═'
154
+ canvas[3]![0] = '═'
155
+ canvas[4]![0] = '╗'
156
+
157
+ canvas[0]![1] = '║'
158
+ canvas[centerX]![1] = '◎'
159
+ canvas[4]![1] = '║'
160
+
161
+ canvas[0]![2] = '╚'
162
+ canvas[1]![2] = '═'
163
+ canvas[2]![2] = '═'
164
+ canvas[3]![2] = '═'
165
+ canvas[4]![2] = '╝'
166
+ } else {
167
+ // ASCII double-bordered box: #===# # * # #===#
168
+ canvas[0]![0] = '#'
169
+ canvas[1]![0] = '='
170
+ canvas[2]![0] = '='
171
+ canvas[3]![0] = '='
172
+ canvas[4]![0] = '#'
173
+
174
+ canvas[0]![1] = '#'
175
+ canvas[centerX]![1] = '*'
176
+ canvas[4]![1] = '#'
177
+
178
+ canvas[0]![2] = '#'
179
+ canvas[1]![2] = '='
180
+ canvas[2]![2] = '='
181
+ canvas[3]![2] = '='
182
+ canvas[4]![2] = '#'
183
+ }
184
+
185
+ return canvas
186
+ },
187
+
188
+ getAttachmentPoint(
189
+ dir: Direction,
190
+ dimensions: ShapeDimensions,
191
+ baseCoord: DrawingCoord,
192
+ ): DrawingCoord {
193
+ const { width, height } = dimensions
194
+ const centerX = baseCoord.x + Math.floor(width / 2)
195
+ const centerY = baseCoord.y + Math.floor(height / 2)
196
+
197
+ if (dirEquals(dir, Up)) return { x: centerX, y: baseCoord.y }
198
+ if (dirEquals(dir, Down)) return { x: centerX, y: baseCoord.y + height - 1 }
199
+ if (dirEquals(dir, Left)) return { x: baseCoord.x, y: centerY }
200
+ if (dirEquals(dir, Right)) return { x: baseCoord.x + width - 1, y: centerY }
201
+ // All diagonals and middle point to center
202
+ return { x: centerX, y: centerY }
203
+ },
204
+ }
@@ -0,0 +1,78 @@
1
+ // ============================================================================
2
+ // Shape renderer types — interface for pluggable ASCII shape renderers
3
+ // ============================================================================
4
+
5
+ import type {
6
+ Canvas,
7
+ DrawingCoord,
8
+ Direction,
9
+ AsciiNodeShape,
10
+ } from '../types.ts'
11
+
12
+ /**
13
+ * Dimensions calculated for a shape, used by layout and rendering.
14
+ */
15
+ export interface ShapeDimensions {
16
+ /** Total width in characters including borders */
17
+ width: number
18
+ /** Total height in characters including borders */
19
+ height: number
20
+ /** Label area bounds (where text can be placed) */
21
+ labelArea: {
22
+ x: number
23
+ y: number
24
+ width: number
25
+ height: number
26
+ }
27
+ /** Grid column widths for the 3-column layout [left, center, right] */
28
+ gridColumns: [number, number, number]
29
+ /** Grid row heights for the 3-row layout [top, middle, bottom] */
30
+ gridRows: [number, number, number]
31
+ }
32
+
33
+ /**
34
+ * Options passed to shape renderers.
35
+ */
36
+ export interface ShapeRenderOptions {
37
+ /** Use ASCII chars (+,-,|) vs Unicode box-drawing (┌,─,│) */
38
+ useAscii: boolean
39
+ /** Padding inside the shape */
40
+ padding: number
41
+ }
42
+
43
+ /**
44
+ * Interface for pluggable shape renderers.
45
+ * Each shape type implements this interface.
46
+ */
47
+ export interface ShapeRenderer {
48
+ /**
49
+ * Calculate dimensions for this shape given a label.
50
+ * Used during layout to determine node size.
51
+ */
52
+ getDimensions(label: string, options: ShapeRenderOptions): ShapeDimensions
53
+
54
+ /**
55
+ * Render the shape to a canvas.
56
+ * Returns a standalone canvas containing just the shape.
57
+ */
58
+ render(
59
+ label: string,
60
+ dimensions: ShapeDimensions,
61
+ options: ShapeRenderOptions,
62
+ ): Canvas
63
+
64
+ /**
65
+ * Get the edge attachment point for a given direction.
66
+ * Used by edge routing to determine where edges connect.
67
+ */
68
+ getAttachmentPoint(
69
+ dir: Direction,
70
+ dimensions: ShapeDimensions,
71
+ baseCoord: DrawingCoord,
72
+ ): DrawingCoord
73
+ }
74
+
75
+ /**
76
+ * Registry of shape renderers keyed by shape type.
77
+ */
78
+ export type ShapeRegistry = Map<AsciiNodeShape, ShapeRenderer>
@@ -0,0 +1,136 @@
1
+ // ============================================================================
2
+ // ASCII renderer — 1-D territory allocation
3
+ //
4
+ // Splits contested horizontal space between items (labels) whose row spans
5
+ // overlap, giving each a mutually exclusive [left, right] column range
6
+ // derived purely from geometry — never from draw order or from what an
7
+ // earlier item happened to render as.
8
+ //
9
+ // Extracted from `class-diagram.ts`'s `territoryByRel` precompute (issue
10
+ // #618). It stayed out of the shared `Grid` occupancy model (issue #532)
11
+ // deliberately: this is a 1-D interval-scheduling problem over item
12
+ // midpoints, not a 2-D occupancy map, so there is nothing to reserve and
13
+ // nothing to look up per cell. Keeping it storage-agnostic — the caller
14
+ // supplies geometry, this module supplies only the allocation — means a
15
+ // second renderer can adopt it without first adopting a grid.
16
+ // ============================================================================
17
+
18
+ /**
19
+ * One item's placement geometry, in whatever units the caller works in
20
+ * (columns and rows, for the ASCII renderers).
21
+ *
22
+ * `start`/`end` describe the item's *natural* span — where it would sit if
23
+ * nothing else competed for the space — and must already be clamped exactly
24
+ * the way the caller's own draw pass clamps it. Passing an unclamped span
25
+ * under-detects real collisions: an item pinned at the canvas edge draws at
26
+ * column 0 but would be compared here as if it were still off-canvas at its
27
+ * raw negative position, so the overlap with its neighbour is missed
28
+ * entirely (issue #447).
29
+ *
30
+ * `rowStart`/`rowEnd` are inclusive and must likewise be the item's *final*
31
+ * resolved rows, not a pre-fallback ideal: two items whose ideal rows don't
32
+ * overlap can still resolve onto the same actual row, and a territory
33
+ * computed from the ideal rows leaves them unsplit — so the later one's
34
+ * draw silently overwrites the earlier one's (issue #531).
35
+ */
36
+ export interface TerritoryGeometry {
37
+ /** The item's preferred centre along the allocated axis. */
38
+ idealMid: number
39
+ /** Inclusive first column of the item's natural (uncontested) span. */
40
+ start: number
41
+ /** Inclusive last column of the item's natural (uncontested) span. */
42
+ end: number
43
+ /** Inclusive first row the item occupies. */
44
+ rowStart: number
45
+ /** Inclusive last row the item occupies. */
46
+ rowEnd: number
47
+ }
48
+
49
+ /**
50
+ * The columns an item may draw into, inclusive on both sides. An
51
+ * unconstrained side is `-Infinity` / `Infinity` rather than a sentinel
52
+ * number, so a caller can clamp or compare without special-casing.
53
+ */
54
+ export interface Territory {
55
+ left: number
56
+ right: number
57
+ }
58
+
59
+ /** Whether two items' row spans actually intersect. */
60
+ function rowsOverlap(a: TerritoryGeometry, b: TerritoryGeometry): boolean {
61
+ return a.rowStart <= b.rowEnd && b.rowStart <= a.rowEnd
62
+ }
63
+
64
+ /**
65
+ * Allocate each item a horizontal territory, splitting contested space
66
+ * evenly at the midpoint between two competing items' `idealMid` values.
67
+ *
68
+ * Two items are competitors only when *both* their row spans intersect and
69
+ * their natural spans overlap; anything else leaves both sides unconstrained
70
+ * (`±Infinity`). Competitors are found by scanning outward in `idealMid`
71
+ * order for the nearest row-overlapping item on each side — not by stopping
72
+ * at the immediately adjacent one, since a non-colliding item can sit
73
+ * between two that genuinely do collide (issue #531).
74
+ *
75
+ * Allocating up front, before anything is drawn, is what makes placement
76
+ * independent of iteration order. A *reactive* approach — only avoiding
77
+ * cells an earlier iteration already wrote — cascades: one item shifted into
78
+ * view at the canvas edge eats its neighbour's rightful columns, that
79
+ * neighbour then has nothing left to claim and vanishes, and everything
80
+ * after it drifts based on the leftovers. With fixed, mutually exclusive
81
+ * territories, a genuinely insufficient width truncates every competitor
82
+ * consistently instead of destroying an arbitrary subset (issue #447).
83
+ *
84
+ * `geometry` is called exactly once per item, in `items` order. Items
85
+ * tying on `idealMid` keep their `items` order (the sort is stable).
86
+ *
87
+ * @param items Items to allocate for, in any order.
88
+ * @param geometry Extracts an item's placement geometry.
89
+ * @returns Each item mapped to its allocated territory. Every item in
90
+ * `items` is present; passing the same item reference twice yields one
91
+ * entry (the last one allocated).
92
+ */
93
+ export function allocateTerritory<T>(
94
+ items: readonly T[],
95
+ geometry: (item: T) => TerritoryGeometry,
96
+ ): Map<T, Territory> {
97
+ const ordered = items
98
+ .map((item) => ({ item, geom: geometry(item) }))
99
+ .sort((a, b) => a.geom.idealMid - b.geom.idealMid)
100
+
101
+ /**
102
+ * The nearest entry in `dir` (-1 left, +1 right) from `idx`, by `idealMid`
103
+ * order, whose rows actually overlap `ordered[idx]`'s — skipping over any
104
+ * immediately-adjacent entries that don't.
105
+ */
106
+ const nearestRowOverlapping = (
107
+ idx: number,
108
+ dir: -1 | 1,
109
+ ): TerritoryGeometry | undefined => {
110
+ const g = ordered[idx]
111
+ if (!g) return undefined
112
+ for (let j = idx + dir; j >= 0 && j < ordered.length; j += dir) {
113
+ const candidate = ordered[j]
114
+ if (candidate && rowsOverlap(candidate.geom, g.geom))
115
+ return candidate.geom
116
+ }
117
+ return undefined
118
+ }
119
+
120
+ const territories = new Map<T, Territory>()
121
+ for (const [i, entry] of ordered.entries()) {
122
+ const g = entry.geom
123
+ const prev = nearestRowOverlapping(i, -1)
124
+ const next = nearestRowOverlapping(i, 1)
125
+ const left =
126
+ prev && prev.end >= g.start
127
+ ? Math.floor((prev.idealMid + g.idealMid) / 2) + 1
128
+ : -Infinity
129
+ const right =
130
+ next && g.end >= next.start
131
+ ? Math.floor((g.idealMid + next.idealMid) / 2)
132
+ : Infinity
133
+ territories.set(entry.item, { left, right })
134
+ }
135
+ return territories
136
+ }