@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,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>
|
package/src/territory.ts
ADDED
|
@@ -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
|
+
}
|