@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,31 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Circle shape renderer — uses corner decorators instead of curves
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
import type { ShapeRenderer } from './types.ts'
|
|
6
|
+
import {
|
|
7
|
+
getBoxDimensions,
|
|
8
|
+
renderBox,
|
|
9
|
+
getBoxAttachmentPoint,
|
|
10
|
+
} from './rectangle.ts'
|
|
11
|
+
import { getCorners } from './corners.ts'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Circle shape renderer.
|
|
15
|
+
* Uses circle markers (◯) at corners to indicate circular shape semantics.
|
|
16
|
+
*
|
|
17
|
+
* Renders as:
|
|
18
|
+
* ◯─────────◯
|
|
19
|
+
* │ Label │
|
|
20
|
+
* ◯─────────◯
|
|
21
|
+
*/
|
|
22
|
+
export const circleRenderer: ShapeRenderer = {
|
|
23
|
+
getDimensions: getBoxDimensions,
|
|
24
|
+
|
|
25
|
+
render(label, dimensions, options) {
|
|
26
|
+
const corners = getCorners('circle', options.useAscii)
|
|
27
|
+
return renderBox(label, dimensions, corners, options.useAscii)
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
getAttachmentPoint: getBoxAttachmentPoint,
|
|
31
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Corner character lookup table for shape rendering
|
|
3
|
+
// ============================================================================
|
|
4
|
+
//
|
|
5
|
+
// All shapes are rendered as rectangles with distinctive corner characters
|
|
6
|
+
// to indicate shape type. This eliminates diagonal characters while keeping
|
|
7
|
+
// shapes visually distinguishable.
|
|
8
|
+
|
|
9
|
+
import type { AsciiNodeShape } from '../types.ts'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Corner characters for a shape in both Unicode and ASCII modes.
|
|
13
|
+
*/
|
|
14
|
+
export interface CornerChars {
|
|
15
|
+
/** Top-left corner */
|
|
16
|
+
tl: string
|
|
17
|
+
/** Top-right corner */
|
|
18
|
+
tr: string
|
|
19
|
+
/** Bottom-left corner */
|
|
20
|
+
bl: string
|
|
21
|
+
/** Bottom-right corner */
|
|
22
|
+
br: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Shape corner configuration with both Unicode and ASCII variants.
|
|
27
|
+
*/
|
|
28
|
+
export interface ShapeCorners {
|
|
29
|
+
unicode: CornerChars
|
|
30
|
+
ascii: CornerChars
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Corner character lookup table for all shape types.
|
|
35
|
+
*
|
|
36
|
+
* Design principles:
|
|
37
|
+
* - All shapes use orthogonal box structure (no diagonals)
|
|
38
|
+
* - Corner characters indicate shape semantics
|
|
39
|
+
* - ASCII fallbacks use available punctuation
|
|
40
|
+
*/
|
|
41
|
+
export const SHAPE_CORNERS: Record<AsciiNodeShape, ShapeCorners> = {
|
|
42
|
+
// Standard rectangular shapes
|
|
43
|
+
rectangle: {
|
|
44
|
+
unicode: { tl: '┌', tr: '┐', bl: '└', br: '┘' },
|
|
45
|
+
ascii: { tl: '+', tr: '+', bl: '+', br: '+' },
|
|
46
|
+
},
|
|
47
|
+
rounded: {
|
|
48
|
+
unicode: { tl: '╭', tr: '╮', bl: '╰', br: '╯' },
|
|
49
|
+
ascii: { tl: '.', tr: '.', bl: "'", br: "'" },
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
// Circular shapes - use circle markers at corners
|
|
53
|
+
circle: {
|
|
54
|
+
unicode: { tl: '◯', tr: '◯', bl: '◯', br: '◯' },
|
|
55
|
+
ascii: { tl: 'o', tr: 'o', bl: 'o', br: 'o' },
|
|
56
|
+
},
|
|
57
|
+
doublecircle: {
|
|
58
|
+
unicode: { tl: '◎', tr: '◎', bl: '◎', br: '◎' },
|
|
59
|
+
ascii: { tl: '@', tr: '@', bl: '@', br: '@' },
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
// Diamond - decision nodes
|
|
63
|
+
diamond: {
|
|
64
|
+
unicode: { tl: '◇', tr: '◇', bl: '◇', br: '◇' },
|
|
65
|
+
ascii: { tl: '<', tr: '>', bl: '<', br: '>' },
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
// Hexagon - process nodes (crop corners — monospace-safe, distinct from rectangle)
|
|
69
|
+
hexagon: {
|
|
70
|
+
unicode: { tl: '⌜', tr: '⌝', bl: '⌞', br: '⌟' },
|
|
71
|
+
ascii: { tl: '*', tr: '*', bl: '*', br: '*' },
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
// Stadium/pill shape
|
|
75
|
+
stadium: {
|
|
76
|
+
unicode: { tl: '(', tr: ')', bl: '(', br: ')' },
|
|
77
|
+
ascii: { tl: '(', tr: ')', bl: '(', br: ')' },
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
// Subroutine - double vertical bars
|
|
81
|
+
subroutine: {
|
|
82
|
+
unicode: { tl: '╟', tr: '╢', bl: '╟', br: '╢' },
|
|
83
|
+
ascii: { tl: '|', tr: '|', bl: '|', br: '|' },
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
// Cylinder/database
|
|
87
|
+
cylinder: {
|
|
88
|
+
unicode: { tl: '╭', tr: '╮', bl: '╰', br: '╯' },
|
|
89
|
+
ascii: { tl: '.', tr: '.', bl: "'", br: "'" },
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
// Asymmetric/flag - pointer on left side
|
|
93
|
+
asymmetric: {
|
|
94
|
+
unicode: { tl: '▷', tr: '┐', bl: '▷', br: '┘' },
|
|
95
|
+
ascii: { tl: '>', tr: '+', bl: '>', br: '+' },
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
// Trapezoid - wider at bottom (top corners slope inward)
|
|
99
|
+
trapezoid: {
|
|
100
|
+
unicode: { tl: '/', tr: '\\', bl: '└', br: '┘' },
|
|
101
|
+
ascii: { tl: '/', tr: '\\', bl: '+', br: '+' },
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
// Trapezoid-alt - wider at top (bottom corners slope inward)
|
|
105
|
+
'trapezoid-alt': {
|
|
106
|
+
unicode: { tl: '┌', tr: '┐', bl: '\\', br: '/' },
|
|
107
|
+
ascii: { tl: '+', tr: '+', bl: '\\', br: '/' },
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
// Parallelogram - leans right (both sides slope the same way)
|
|
111
|
+
parallelogram: {
|
|
112
|
+
unicode: { tl: '/', tr: '/', bl: '/', br: '/' },
|
|
113
|
+
ascii: { tl: '/', tr: '/', bl: '/', br: '/' },
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
// Parallelogram-alt - leans left
|
|
117
|
+
'parallelogram-alt': {
|
|
118
|
+
unicode: { tl: '\\', tr: '\\', bl: '\\', br: '\\' },
|
|
119
|
+
ascii: { tl: '\\', tr: '\\', bl: '\\', br: '\\' },
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
// State diagram pseudostates (special handling, not corner-based)
|
|
123
|
+
'state-start': {
|
|
124
|
+
unicode: { tl: '●', tr: '●', bl: '●', br: '●' },
|
|
125
|
+
ascii: { tl: '*', tr: '*', bl: '*', br: '*' },
|
|
126
|
+
},
|
|
127
|
+
'state-end': {
|
|
128
|
+
unicode: { tl: '◉', tr: '◉', bl: '◉', br: '◉' },
|
|
129
|
+
ascii: { tl: '@', tr: '@', bl: '@', br: '@' },
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
// --------------------------------------------------------------------
|
|
133
|
+
// Expanded-syntax shapes (`A@{ shape: ... }`).
|
|
134
|
+
//
|
|
135
|
+
// The ASCII grid has no diagonals and one glyph per corner, so these are
|
|
136
|
+
// distinguished by corner character alone. Shapes whose defining feature
|
|
137
|
+
// is interior (a rule, a cross, a notch) or whose outline the grid cannot
|
|
138
|
+
// express keep box corners and rely on their label — the alternative is a
|
|
139
|
+
// misleading outline, and docs/diagrams.md records which shapes render
|
|
140
|
+
// distinctly in ASCII.
|
|
141
|
+
// --------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
// Document family — wavy bottom edge suggested by curve glyphs
|
|
144
|
+
document: {
|
|
145
|
+
unicode: { tl: '┌', tr: '┐', bl: '╰', br: '╮' },
|
|
146
|
+
ascii: { tl: '+', tr: '+', bl: "'", br: '~' },
|
|
147
|
+
},
|
|
148
|
+
'stacked-document': {
|
|
149
|
+
unicode: { tl: '┌', tr: '╗', bl: '╰', br: '╮' },
|
|
150
|
+
ascii: { tl: '+', tr: '#', bl: "'", br: '~' },
|
|
151
|
+
},
|
|
152
|
+
'stacked-process': {
|
|
153
|
+
unicode: { tl: '┌', tr: '╗', bl: '└', br: '╝' },
|
|
154
|
+
ascii: { tl: '+', tr: '#', bl: '+', br: '#' },
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
// Card — notched top-left corner
|
|
158
|
+
card: {
|
|
159
|
+
unicode: { tl: '╱', tr: '┐', bl: '└', br: '┘' },
|
|
160
|
+
ascii: { tl: '/', tr: '+', bl: '+', br: '+' },
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
// Interior-feature rectangles — box corners, feature not expressible
|
|
164
|
+
'lined-process': {
|
|
165
|
+
unicode: { tl: '┌', tr: '┐', bl: '└', br: '┘' },
|
|
166
|
+
ascii: { tl: '+', tr: '+', bl: '+', br: '+' },
|
|
167
|
+
},
|
|
168
|
+
'divided-process': {
|
|
169
|
+
unicode: { tl: '┌', tr: '┐', bl: '└', br: '┘' },
|
|
170
|
+
ascii: { tl: '+', tr: '+', bl: '+', br: '+' },
|
|
171
|
+
},
|
|
172
|
+
'window-pane': {
|
|
173
|
+
unicode: { tl: '┌', tr: '┐', bl: '└', br: '┘' },
|
|
174
|
+
ascii: { tl: '+', tr: '+', bl: '+', br: '+' },
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
// Triangles
|
|
178
|
+
triangle: {
|
|
179
|
+
unicode: { tl: '╱', tr: '╲', bl: '└', br: '┘' },
|
|
180
|
+
ascii: { tl: '/', tr: '\\', bl: '+', br: '+' },
|
|
181
|
+
},
|
|
182
|
+
'flipped-triangle': {
|
|
183
|
+
unicode: { tl: '┌', tr: '┐', bl: '╲', br: '╱' },
|
|
184
|
+
ascii: { tl: '+', tr: '+', bl: '\\', br: '/' },
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
// Circles
|
|
188
|
+
'filled-circle': {
|
|
189
|
+
unicode: { tl: '●', tr: '●', bl: '●', br: '●' },
|
|
190
|
+
ascii: { tl: '*', tr: '*', bl: '*', br: '*' },
|
|
191
|
+
},
|
|
192
|
+
'crossed-circle': {
|
|
193
|
+
unicode: { tl: '╳', tr: '╳', bl: '╳', br: '╳' },
|
|
194
|
+
ascii: { tl: 'X', tr: 'X', bl: 'X', br: 'X' },
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
// Fork/join bar
|
|
198
|
+
'fork-join': {
|
|
199
|
+
unicode: { tl: '━', tr: '━', bl: '━', br: '━' },
|
|
200
|
+
ascii: { tl: '=', tr: '=', bl: '=', br: '=' },
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
// Notched pentagon — clipped top corners
|
|
204
|
+
'notched-pentagon': {
|
|
205
|
+
unicode: { tl: '╱', tr: '╲', bl: '└', br: '┘' },
|
|
206
|
+
ascii: { tl: '/', tr: '\\', bl: '+', br: '+' },
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
// Sloped rectangle (manual input) — sloped top edge
|
|
210
|
+
'sloped-rectangle': {
|
|
211
|
+
unicode: { tl: '╱', tr: '┐', bl: '└', br: '┘' },
|
|
212
|
+
ascii: { tl: '/', tr: '+', bl: '+', br: '+' },
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
// Flag / paper tape — wavy top and bottom
|
|
216
|
+
flag: {
|
|
217
|
+
unicode: { tl: '╭', tr: '╮', bl: '╰', br: '╯' },
|
|
218
|
+
ascii: { tl: '~', tr: '~', bl: '~', br: '~' },
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
// Bow-tie rectangle (stored data) — concave sides
|
|
222
|
+
'bow-tie-rectangle': {
|
|
223
|
+
unicode: { tl: '╲', tr: '╱', bl: '╱', br: '╲' },
|
|
224
|
+
ascii: { tl: '\\', tr: '/', bl: '/', br: '\\' },
|
|
225
|
+
},
|
|
226
|
+
|
|
227
|
+
// Delay — one rounded end
|
|
228
|
+
'half-rounded-rectangle': {
|
|
229
|
+
unicode: { tl: '┌', tr: '╮', bl: '└', br: '╯' },
|
|
230
|
+
ascii: { tl: '+', tr: '.', bl: '+', br: "'" },
|
|
231
|
+
},
|
|
232
|
+
|
|
233
|
+
// Braces
|
|
234
|
+
brace: {
|
|
235
|
+
unicode: { tl: '╭', tr: '┐', bl: '╰', br: '┘' },
|
|
236
|
+
ascii: { tl: '{', tr: '+', bl: '{', br: '+' },
|
|
237
|
+
},
|
|
238
|
+
'brace-right': {
|
|
239
|
+
unicode: { tl: '┌', tr: '╮', bl: '└', br: '╯' },
|
|
240
|
+
ascii: { tl: '+', tr: '}', bl: '+', br: '}' },
|
|
241
|
+
},
|
|
242
|
+
braces: {
|
|
243
|
+
unicode: { tl: '╭', tr: '╮', bl: '╰', br: '╯' },
|
|
244
|
+
ascii: { tl: '{', tr: '}', bl: '{', br: '}' },
|
|
245
|
+
},
|
|
246
|
+
|
|
247
|
+
// Lightning bolt (communication link)
|
|
248
|
+
bolt: {
|
|
249
|
+
unicode: { tl: '╱', tr: '╲', bl: '╲', br: '╱' },
|
|
250
|
+
ascii: { tl: '/', tr: '\\', bl: '\\', br: '/' },
|
|
251
|
+
},
|
|
252
|
+
|
|
253
|
+
// Bare text and anchor — no visible outline
|
|
254
|
+
text: {
|
|
255
|
+
unicode: { tl: ' ', tr: ' ', bl: ' ', br: ' ' },
|
|
256
|
+
ascii: { tl: ' ', tr: ' ', bl: ' ', br: ' ' },
|
|
257
|
+
},
|
|
258
|
+
anchor: {
|
|
259
|
+
unicode: { tl: ' ', tr: ' ', bl: ' ', br: ' ' },
|
|
260
|
+
ascii: { tl: ' ', tr: ' ', bl: ' ', br: ' ' },
|
|
261
|
+
},
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Get corner characters for a shape type.
|
|
266
|
+
*/
|
|
267
|
+
export function getCorners(
|
|
268
|
+
shape: AsciiNodeShape,
|
|
269
|
+
useAscii: boolean,
|
|
270
|
+
): CornerChars {
|
|
271
|
+
const corners = SHAPE_CORNERS[shape] ?? SHAPE_CORNERS.rectangle
|
|
272
|
+
return useAscii ? corners.ascii : corners.unicode
|
|
273
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Diamond shape renderer — uses corner decorators instead of diagonals
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
import type { ShapeRenderer } from './types.ts'
|
|
6
|
+
import {
|
|
7
|
+
getBoxDimensions,
|
|
8
|
+
renderBox,
|
|
9
|
+
getBoxAttachmentPoint,
|
|
10
|
+
} from './rectangle.ts'
|
|
11
|
+
import { getCorners } from './corners.ts'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Diamond shape renderer.
|
|
15
|
+
* Uses diamond markers (◇) at corners to indicate decision node semantics.
|
|
16
|
+
*
|
|
17
|
+
* Renders as:
|
|
18
|
+
* ◇─────────◇
|
|
19
|
+
* │ Label │
|
|
20
|
+
* ◇─────────◇
|
|
21
|
+
*/
|
|
22
|
+
export const diamondRenderer: ShapeRenderer = {
|
|
23
|
+
getDimensions: getBoxDimensions,
|
|
24
|
+
|
|
25
|
+
render(label, dimensions, options) {
|
|
26
|
+
const corners = getCorners('diamond', options.useAscii)
|
|
27
|
+
return renderBox(label, dimensions, corners, options.useAscii)
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
getAttachmentPoint: getBoxAttachmentPoint,
|
|
31
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Hexagon shape renderer — uses corner decorators instead of diagonals
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
import type { ShapeRenderer } from './types.ts'
|
|
6
|
+
import {
|
|
7
|
+
getBoxDimensions,
|
|
8
|
+
renderBox,
|
|
9
|
+
getBoxAttachmentPoint,
|
|
10
|
+
} from './rectangle.ts'
|
|
11
|
+
import { getCorners } from './corners.ts'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Hexagon shape renderer.
|
|
15
|
+
* Uses crop-corner markers (⌜⌝⌞⌟, see corners.ts's `SHAPE_CORNERS.hexagon`)
|
|
16
|
+
* to indicate process node semantics — not the hexagon glyph ⬡ (U+2B21)
|
|
17
|
+
* this comment used to claim: JetBrains Mono NL has no glyph for ⬡ at all,
|
|
18
|
+
* and the renderer hasn't actually emitted it since corners.ts moved to
|
|
19
|
+
* the monospace-safe crop-corner style. See issue #1062.
|
|
20
|
+
*
|
|
21
|
+
* Renders as:
|
|
22
|
+
* ⌜─────────⌝
|
|
23
|
+
* │ Label │
|
|
24
|
+
* ⌞─────────⌟
|
|
25
|
+
*/
|
|
26
|
+
export const hexagonRenderer: ShapeRenderer = {
|
|
27
|
+
getDimensions: getBoxDimensions,
|
|
28
|
+
|
|
29
|
+
render(label, dimensions, options) {
|
|
30
|
+
const corners = getCorners('hexagon', options.useAscii)
|
|
31
|
+
return renderBox(label, dimensions, corners, options.useAscii)
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
getAttachmentPoint: getBoxAttachmentPoint,
|
|
35
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Shape registry — pluggable ASCII shape renderers
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
AsciiNodeShape,
|
|
7
|
+
Canvas,
|
|
8
|
+
DrawingCoord,
|
|
9
|
+
Direction,
|
|
10
|
+
} from '../types.ts'
|
|
11
|
+
import type {
|
|
12
|
+
ShapeRenderer,
|
|
13
|
+
ShapeDimensions,
|
|
14
|
+
ShapeRenderOptions,
|
|
15
|
+
ShapeRegistry,
|
|
16
|
+
} from './types.ts'
|
|
17
|
+
|
|
18
|
+
// Import all shape renderers
|
|
19
|
+
import { rectangleRenderer } from './rectangle.ts'
|
|
20
|
+
import { diamondRenderer } from './diamond.ts'
|
|
21
|
+
import { circleRenderer } from './circle.ts'
|
|
22
|
+
import { stateStartRenderer, stateEndRenderer } from './state.ts'
|
|
23
|
+
import { roundedRenderer } from './rounded.ts'
|
|
24
|
+
import { stadiumRenderer } from './stadium.ts'
|
|
25
|
+
import { hexagonRenderer } from './hexagon.ts'
|
|
26
|
+
import {
|
|
27
|
+
subroutineRenderer,
|
|
28
|
+
doublecircleRenderer,
|
|
29
|
+
cylinderRenderer,
|
|
30
|
+
asymmetricRenderer,
|
|
31
|
+
trapezoidRenderer,
|
|
32
|
+
trapezoidAltRenderer,
|
|
33
|
+
parallelogramRenderer,
|
|
34
|
+
parallelogramAltRenderer,
|
|
35
|
+
} from './special.ts'
|
|
36
|
+
|
|
37
|
+
// Re-export types
|
|
38
|
+
export type {
|
|
39
|
+
ShapeRenderer,
|
|
40
|
+
ShapeDimensions,
|
|
41
|
+
ShapeRenderOptions,
|
|
42
|
+
ShapeRegistry,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Global shape registry — maps shape types to their renderers.
|
|
47
|
+
* Rectangle is the default fallback for unregistered shapes.
|
|
48
|
+
*/
|
|
49
|
+
export const shapeRegistry: ShapeRegistry = new Map<
|
|
50
|
+
AsciiNodeShape,
|
|
51
|
+
ShapeRenderer
|
|
52
|
+
>([
|
|
53
|
+
// Core shapes
|
|
54
|
+
['rectangle', rectangleRenderer],
|
|
55
|
+
['rounded', roundedRenderer],
|
|
56
|
+
['diamond', diamondRenderer],
|
|
57
|
+
['stadium', stadiumRenderer],
|
|
58
|
+
['circle', circleRenderer],
|
|
59
|
+
|
|
60
|
+
// Batch 1 additions
|
|
61
|
+
['subroutine', subroutineRenderer],
|
|
62
|
+
['doublecircle', doublecircleRenderer],
|
|
63
|
+
['hexagon', hexagonRenderer],
|
|
64
|
+
|
|
65
|
+
// Batch 2 additions
|
|
66
|
+
['cylinder', cylinderRenderer],
|
|
67
|
+
['asymmetric', asymmetricRenderer],
|
|
68
|
+
['trapezoid', trapezoidRenderer],
|
|
69
|
+
['trapezoid-alt', trapezoidAltRenderer],
|
|
70
|
+
['parallelogram', parallelogramRenderer],
|
|
71
|
+
['parallelogram-alt', parallelogramAltRenderer],
|
|
72
|
+
|
|
73
|
+
// State diagram pseudo-states
|
|
74
|
+
['state-start', stateStartRenderer],
|
|
75
|
+
['state-end', stateEndRenderer],
|
|
76
|
+
])
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get the renderer for a shape type, falling back to rectangle.
|
|
80
|
+
*/
|
|
81
|
+
export function getShapeRenderer(shape: AsciiNodeShape): ShapeRenderer {
|
|
82
|
+
return shapeRegistry.get(shape) ?? rectangleRenderer
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Render a node shape to a canvas.
|
|
87
|
+
* This is the main entry point for shape rendering.
|
|
88
|
+
*/
|
|
89
|
+
export function renderShape(
|
|
90
|
+
shape: AsciiNodeShape,
|
|
91
|
+
label: string,
|
|
92
|
+
options: ShapeRenderOptions,
|
|
93
|
+
): Canvas {
|
|
94
|
+
const renderer = getShapeRenderer(shape)
|
|
95
|
+
const dimensions = renderer.getDimensions(label, options)
|
|
96
|
+
return renderer.render(label, dimensions, options)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get dimensions for a shape given a label.
|
|
101
|
+
* Used during layout to determine node size.
|
|
102
|
+
*/
|
|
103
|
+
export function getShapeDimensions(
|
|
104
|
+
shape: AsciiNodeShape,
|
|
105
|
+
label: string,
|
|
106
|
+
options: ShapeRenderOptions,
|
|
107
|
+
): ShapeDimensions {
|
|
108
|
+
const renderer = getShapeRenderer(shape)
|
|
109
|
+
return renderer.getDimensions(label, options)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Get edge attachment point for a shape.
|
|
114
|
+
*/
|
|
115
|
+
export function getShapeAttachmentPoint(
|
|
116
|
+
shape: AsciiNodeShape,
|
|
117
|
+
dir: Direction,
|
|
118
|
+
dimensions: ShapeDimensions,
|
|
119
|
+
baseCoord: DrawingCoord,
|
|
120
|
+
): DrawingCoord {
|
|
121
|
+
const renderer = getShapeRenderer(shape)
|
|
122
|
+
return renderer.getAttachmentPoint(dir, dimensions, baseCoord)
|
|
123
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Rectangle shape renderer — standard box with corners
|
|
3
|
+
// ============================================================================
|
|
4
|
+
//
|
|
5
|
+
// This module provides the base box rendering used by all rectangular shapes.
|
|
6
|
+
// The renderBox() function accepts custom corner characters, allowing different
|
|
7
|
+
// shapes to reuse the same rendering logic with different visual markers.
|
|
8
|
+
|
|
9
|
+
import type { Canvas, DrawingCoord, Direction } from '../types.ts'
|
|
10
|
+
import {
|
|
11
|
+
Up,
|
|
12
|
+
Down,
|
|
13
|
+
Left,
|
|
14
|
+
Right,
|
|
15
|
+
UpperLeft,
|
|
16
|
+
UpperRight,
|
|
17
|
+
LowerLeft,
|
|
18
|
+
LowerRight,
|
|
19
|
+
} from '../types.ts'
|
|
20
|
+
import { mkCanvas } from '../canvas.ts'
|
|
21
|
+
import { splitLines } from '../multiline-utils.ts'
|
|
22
|
+
import { displayWidth, toDisplayCells } from '../display-width.ts'
|
|
23
|
+
import type {
|
|
24
|
+
ShapeRenderer,
|
|
25
|
+
ShapeDimensions,
|
|
26
|
+
ShapeRenderOptions,
|
|
27
|
+
} from './types.ts'
|
|
28
|
+
import { dirEquals } from '../edge-routing.ts'
|
|
29
|
+
import { type CornerChars, getCorners } from './corners.ts'
|
|
30
|
+
|
|
31
|
+
// ============================================================================
|
|
32
|
+
// Shared dimension calculation
|
|
33
|
+
// ============================================================================
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Calculate standard box dimensions for any rectangular shape.
|
|
37
|
+
* Used by rectangle, circle, diamond, hexagon, etc.
|
|
38
|
+
*/
|
|
39
|
+
export function getBoxDimensions(
|
|
40
|
+
label: string,
|
|
41
|
+
options: ShapeRenderOptions,
|
|
42
|
+
): ShapeDimensions {
|
|
43
|
+
const lines = splitLines(label)
|
|
44
|
+
const maxLineWidth = Math.max(...lines.map((l) => displayWidth(l)), 0)
|
|
45
|
+
const lineCount = lines.length
|
|
46
|
+
|
|
47
|
+
// Width: 2*padding + maxLineWidth + 2 border chars
|
|
48
|
+
const innerWidth = 2 * options.padding + maxLineWidth
|
|
49
|
+
const width = innerWidth + 2
|
|
50
|
+
|
|
51
|
+
// Height: lineCount + 2*padding + 2 border chars
|
|
52
|
+
// Ensure innerHeight is odd for symmetric vertical centering
|
|
53
|
+
const rawInnerHeight = lineCount + 2 * options.padding
|
|
54
|
+
const innerHeight =
|
|
55
|
+
rawInnerHeight % 2 === 0 ? rawInnerHeight + 1 : rawInnerHeight
|
|
56
|
+
const height = innerHeight + 2
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
width,
|
|
60
|
+
height,
|
|
61
|
+
labelArea: {
|
|
62
|
+
x: 1 + options.padding,
|
|
63
|
+
y: 1 + options.padding,
|
|
64
|
+
width: maxLineWidth,
|
|
65
|
+
height: lineCount,
|
|
66
|
+
},
|
|
67
|
+
// Grid layout: [border=1, content, border=1]
|
|
68
|
+
gridColumns: [1, innerWidth, 1],
|
|
69
|
+
gridRows: [1, innerHeight, 1],
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ============================================================================
|
|
74
|
+
// Shared box rendering
|
|
75
|
+
// ============================================================================
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Render a box with custom corner characters.
|
|
79
|
+
* This is the core rendering function used by all rectangular shapes.
|
|
80
|
+
*
|
|
81
|
+
* @param label - Text to display in the box
|
|
82
|
+
* @param dimensions - Pre-calculated dimensions
|
|
83
|
+
* @param corners - Corner characters (tl, tr, bl, br)
|
|
84
|
+
* @param useAscii - Whether to use ASCII or Unicode for lines
|
|
85
|
+
*/
|
|
86
|
+
export function renderBox(
|
|
87
|
+
label: string,
|
|
88
|
+
dimensions: ShapeDimensions,
|
|
89
|
+
corners: CornerChars,
|
|
90
|
+
useAscii: boolean,
|
|
91
|
+
): Canvas {
|
|
92
|
+
const { width, height } = dimensions
|
|
93
|
+
const canvas = mkCanvas(width - 1, height - 1)
|
|
94
|
+
|
|
95
|
+
const from = { x: 0, y: 0 }
|
|
96
|
+
const to = { x: width - 1, y: height - 1 }
|
|
97
|
+
|
|
98
|
+
// Line characters
|
|
99
|
+
const hLine = useAscii ? '-' : '─'
|
|
100
|
+
const vLine = useAscii ? '|' : '│'
|
|
101
|
+
|
|
102
|
+
// Draw horizontal lines (top and bottom)
|
|
103
|
+
for (let x = from.x + 1; x < to.x; x++) {
|
|
104
|
+
canvas[x]![from.y] = hLine
|
|
105
|
+
canvas[x]![to.y] = hLine
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Draw vertical lines (left and right)
|
|
109
|
+
for (let y = from.y + 1; y < to.y; y++) {
|
|
110
|
+
canvas[from.x]![y] = vLine
|
|
111
|
+
canvas[to.x]![y] = vLine
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Draw corners
|
|
115
|
+
canvas[from.x]![from.y] = corners.tl
|
|
116
|
+
canvas[to.x]![from.y] = corners.tr
|
|
117
|
+
canvas[from.x]![to.y] = corners.bl
|
|
118
|
+
canvas[to.x]![to.y] = corners.br
|
|
119
|
+
|
|
120
|
+
// Center the multi-line label
|
|
121
|
+
const lines = splitLines(label)
|
|
122
|
+
const w = width - 1 // Match original grid-based width calculation
|
|
123
|
+
const h = height - 1
|
|
124
|
+
const centerY = Math.floor(h / 2)
|
|
125
|
+
const startY = centerY - Math.floor((lines.length - 1) / 2)
|
|
126
|
+
|
|
127
|
+
for (let i = 0; i < lines.length; i++) {
|
|
128
|
+
const line = lines[i]!
|
|
129
|
+
const textX = Math.floor(w / 2) - Math.ceil(displayWidth(line) / 2) + 1
|
|
130
|
+
const cells = toDisplayCells(line)
|
|
131
|
+
for (let j = 0; j < cells.length; j++) {
|
|
132
|
+
const x = textX + j
|
|
133
|
+
const y = startY + i
|
|
134
|
+
if (x >= 0 && x < canvas.length && y >= 0 && y < canvas[0]!.length) {
|
|
135
|
+
canvas[x]![y] = cells[j]!
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return canvas
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ============================================================================
|
|
144
|
+
// Shared attachment point calculation
|
|
145
|
+
// ============================================================================
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Calculate edge attachment point for rectangular shapes.
|
|
149
|
+
* All box-based shapes use the same attachment logic.
|
|
150
|
+
*/
|
|
151
|
+
export function getBoxAttachmentPoint(
|
|
152
|
+
dir: Direction,
|
|
153
|
+
dimensions: ShapeDimensions,
|
|
154
|
+
baseCoord: DrawingCoord,
|
|
155
|
+
): DrawingCoord {
|
|
156
|
+
const { width, height } = dimensions
|
|
157
|
+
const centerX = baseCoord.x + Math.floor(width / 2)
|
|
158
|
+
const centerY = baseCoord.y + Math.floor(height / 2)
|
|
159
|
+
|
|
160
|
+
if (dirEquals(dir, Up)) return { x: centerX, y: baseCoord.y }
|
|
161
|
+
if (dirEquals(dir, Down)) return { x: centerX, y: baseCoord.y + height - 1 }
|
|
162
|
+
if (dirEquals(dir, Left)) return { x: baseCoord.x, y: centerY }
|
|
163
|
+
if (dirEquals(dir, Right)) return { x: baseCoord.x + width - 1, y: centerY }
|
|
164
|
+
if (dirEquals(dir, UpperLeft)) return { x: baseCoord.x, y: baseCoord.y }
|
|
165
|
+
if (dirEquals(dir, UpperRight))
|
|
166
|
+
return { x: baseCoord.x + width - 1, y: baseCoord.y }
|
|
167
|
+
if (dirEquals(dir, LowerLeft))
|
|
168
|
+
return { x: baseCoord.x, y: baseCoord.y + height - 1 }
|
|
169
|
+
if (dirEquals(dir, LowerRight))
|
|
170
|
+
return { x: baseCoord.x + width - 1, y: baseCoord.y + height - 1 }
|
|
171
|
+
// Middle
|
|
172
|
+
return { x: centerX, y: centerY }
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ============================================================================
|
|
176
|
+
// Rectangle renderer
|
|
177
|
+
// ============================================================================
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Rectangle shape renderer — the default box shape.
|
|
181
|
+
* Renders as:
|
|
182
|
+
* ┌─────────┐
|
|
183
|
+
* │ Label │
|
|
184
|
+
* └─────────┘
|
|
185
|
+
*/
|
|
186
|
+
export const rectangleRenderer: ShapeRenderer = {
|
|
187
|
+
getDimensions: getBoxDimensions,
|
|
188
|
+
|
|
189
|
+
render(
|
|
190
|
+
label: string,
|
|
191
|
+
dimensions: ShapeDimensions,
|
|
192
|
+
options: ShapeRenderOptions,
|
|
193
|
+
): Canvas {
|
|
194
|
+
const corners = getCorners('rectangle', options.useAscii)
|
|
195
|
+
return renderBox(label, dimensions, corners, options.useAscii)
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
getAttachmentPoint: getBoxAttachmentPoint,
|
|
199
|
+
}
|