@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,174 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// ASCII renderer — line drawing (8-directional lines on the canvas)
|
|
3
|
+
//
|
|
4
|
+
// Split out of draw.ts.
|
|
5
|
+
// ============================================================================
|
|
6
|
+
|
|
7
|
+
import type { Canvas, DrawingCoord, AsciiEdgeStyle } from './types.ts'
|
|
8
|
+
import {
|
|
9
|
+
Up,
|
|
10
|
+
Down,
|
|
11
|
+
Left,
|
|
12
|
+
Right,
|
|
13
|
+
UpperLeft,
|
|
14
|
+
UpperRight,
|
|
15
|
+
LowerLeft,
|
|
16
|
+
LowerRight,
|
|
17
|
+
} from './types.ts'
|
|
18
|
+
import { determineDirection, dirEquals } from './edge-routing.ts'
|
|
19
|
+
import { write } from './canvas.ts'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Line character sets for different edge styles.
|
|
23
|
+
* Each style has horizontal, vertical, and diagonal characters for both
|
|
24
|
+
* Unicode (box-drawing) and ASCII (basic punctuation) modes.
|
|
25
|
+
*
|
|
26
|
+
* Unicode dotted: ┄ (horizontal), ┆ (vertical) — U+2504, U+2506
|
|
27
|
+
* Unicode thick: ━ (horizontal), ┃ (vertical) — U+2501, U+2503
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* Line character sets for different edge styles.
|
|
31
|
+
* Only horizontal and vertical characters - no diagonals.
|
|
32
|
+
* All edges use orthogonal Manhattan routing (90° bends only).
|
|
33
|
+
*/
|
|
34
|
+
const LINE_CHARS = {
|
|
35
|
+
solid: {
|
|
36
|
+
h: { unicode: '─', ascii: '-' },
|
|
37
|
+
v: { unicode: '│', ascii: '|' },
|
|
38
|
+
},
|
|
39
|
+
dotted: {
|
|
40
|
+
h: { unicode: '┄', ascii: '.' },
|
|
41
|
+
v: { unicode: '┆', ascii: ':' },
|
|
42
|
+
},
|
|
43
|
+
thick: {
|
|
44
|
+
h: { unicode: '━', ascii: '=' },
|
|
45
|
+
v: { unicode: '┃', ascii: '‖' },
|
|
46
|
+
},
|
|
47
|
+
/*
|
|
48
|
+
* `A ~~~ B` — an invisible link. It routes and reserves grid cells like any
|
|
49
|
+
* other edge (so the nodes it connects stay ranked apart), but every cell it
|
|
50
|
+
* would occupy is written as a space, leaving no visible line.
|
|
51
|
+
*/
|
|
52
|
+
invisible: {
|
|
53
|
+
h: { unicode: ' ', ascii: ' ' },
|
|
54
|
+
v: { unicode: ' ', ascii: ' ' },
|
|
55
|
+
},
|
|
56
|
+
} as const
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Draw a line between two drawing coordinates using orthogonal Manhattan routing.
|
|
60
|
+
* Returns the list of coordinates that were drawn on.
|
|
61
|
+
* offsetFrom/offsetTo control how many cells to skip at the start/end.
|
|
62
|
+
*
|
|
63
|
+
* All lines use 90° bends only - no diagonal lines are produced.
|
|
64
|
+
* For diagonal directions, uses horizontal-first routing (draws horizontal
|
|
65
|
+
* segment, then vertical segment).
|
|
66
|
+
*/
|
|
67
|
+
export function drawLine(
|
|
68
|
+
canvas: Canvas,
|
|
69
|
+
from: DrawingCoord,
|
|
70
|
+
to: DrawingCoord,
|
|
71
|
+
offsetFrom: number,
|
|
72
|
+
offsetTo: number,
|
|
73
|
+
useAscii: boolean,
|
|
74
|
+
style: AsciiEdgeStyle = 'solid',
|
|
75
|
+
): DrawingCoord[] {
|
|
76
|
+
const dir = determineDirection(from, to)
|
|
77
|
+
const drawnCoords: DrawingCoord[] = []
|
|
78
|
+
|
|
79
|
+
// Select character set based on style (horizontal and vertical only)
|
|
80
|
+
const chars = LINE_CHARS[style]
|
|
81
|
+
const hChar = useAscii ? chars.h.ascii : chars.h.unicode
|
|
82
|
+
const vChar = useAscii ? chars.v.ascii : chars.v.unicode
|
|
83
|
+
|
|
84
|
+
// Pure vertical directions
|
|
85
|
+
if (dirEquals(dir, Up)) {
|
|
86
|
+
for (let y = from.y - offsetFrom; y >= to.y - offsetTo; y--) {
|
|
87
|
+
drawnCoords.push({ x: from.x, y })
|
|
88
|
+
write(canvas, from.x, y, vChar)
|
|
89
|
+
}
|
|
90
|
+
} else if (dirEquals(dir, Down)) {
|
|
91
|
+
for (let y = from.y + offsetFrom; y <= to.y + offsetTo; y++) {
|
|
92
|
+
drawnCoords.push({ x: from.x, y })
|
|
93
|
+
write(canvas, from.x, y, vChar)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Pure horizontal directions
|
|
97
|
+
else if (dirEquals(dir, Left)) {
|
|
98
|
+
for (let x = from.x - offsetFrom; x >= to.x - offsetTo; x--) {
|
|
99
|
+
drawnCoords.push({ x, y: from.y })
|
|
100
|
+
write(canvas, x, from.y, hChar)
|
|
101
|
+
}
|
|
102
|
+
} else if (dirEquals(dir, Right)) {
|
|
103
|
+
for (let x = from.x + offsetFrom; x <= to.x + offsetTo; x++) {
|
|
104
|
+
drawnCoords.push({ x, y: from.y })
|
|
105
|
+
write(canvas, x, from.y, hChar)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Diagonal directions: use Manhattan routing (horizontal-first, then vertical)
|
|
109
|
+
// UpperLeft: go left first, then up
|
|
110
|
+
else if (dirEquals(dir, UpperLeft)) {
|
|
111
|
+
// Horizontal segment: from.x -> to.x (going left)
|
|
112
|
+
for (let x = from.x - offsetFrom; x >= to.x; x--) {
|
|
113
|
+
drawnCoords.push({ x, y: from.y })
|
|
114
|
+
write(canvas, x, from.y, hChar)
|
|
115
|
+
}
|
|
116
|
+
// Vertical segment: from.y -> to.y (going up)
|
|
117
|
+
for (let y = from.y - 1; y >= to.y - offsetTo; y--) {
|
|
118
|
+
drawnCoords.push({ x: to.x, y })
|
|
119
|
+
write(canvas, to.x, y, vChar)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// UpperRight: go right first, then up
|
|
123
|
+
else if (dirEquals(dir, UpperRight)) {
|
|
124
|
+
// Horizontal segment: from.x -> to.x (going right)
|
|
125
|
+
for (let x = from.x + offsetFrom; x <= to.x; x++) {
|
|
126
|
+
drawnCoords.push({ x, y: from.y })
|
|
127
|
+
write(canvas, x, from.y, hChar)
|
|
128
|
+
}
|
|
129
|
+
// Vertical segment: from.y -> to.y (going up)
|
|
130
|
+
for (let y = from.y - 1; y >= to.y - offsetTo; y--) {
|
|
131
|
+
drawnCoords.push({ x: to.x, y })
|
|
132
|
+
write(canvas, to.x, y, vChar)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// LowerLeft: go left first, then down
|
|
136
|
+
else if (dirEquals(dir, LowerLeft)) {
|
|
137
|
+
// Horizontal segment: from.x -> to.x (going left)
|
|
138
|
+
for (let x = from.x - offsetFrom; x >= to.x; x--) {
|
|
139
|
+
drawnCoords.push({ x, y: from.y })
|
|
140
|
+
write(canvas, x, from.y, hChar)
|
|
141
|
+
}
|
|
142
|
+
// Vertical segment: from.y -> to.y (going down)
|
|
143
|
+
for (let y = from.y + 1; y <= to.y + offsetTo; y++) {
|
|
144
|
+
drawnCoords.push({ x: to.x, y })
|
|
145
|
+
write(canvas, to.x, y, vChar)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// LowerRight: go right first, then down
|
|
149
|
+
// Special case: if x difference is small (1), draw straight vertical at from.x
|
|
150
|
+
// This keeps edges visually aligned with the source node
|
|
151
|
+
else if (dirEquals(dir, LowerRight)) {
|
|
152
|
+
const dx = to.x - from.x
|
|
153
|
+
if (dx <= 1) {
|
|
154
|
+
// Draw vertical line at from.x (source's x-coordinate)
|
|
155
|
+
for (let y = from.y + offsetFrom; y <= to.y + offsetTo; y++) {
|
|
156
|
+
drawnCoords.push({ x: from.x, y })
|
|
157
|
+
write(canvas, from.x, y, vChar)
|
|
158
|
+
}
|
|
159
|
+
} else {
|
|
160
|
+
// Horizontal segment: from.x -> to.x (going right)
|
|
161
|
+
for (let x = from.x + offsetFrom; x <= to.x; x++) {
|
|
162
|
+
drawnCoords.push({ x, y: from.y })
|
|
163
|
+
write(canvas, x, from.y, hChar)
|
|
164
|
+
}
|
|
165
|
+
// Vertical segment: from.y -> to.y (going down)
|
|
166
|
+
for (let y = from.y + 1; y <= to.y + offsetTo; y++) {
|
|
167
|
+
drawnCoords.push({ x: to.x, y })
|
|
168
|
+
write(canvas, to.x, y, vChar)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return drawnCoords
|
|
174
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// ASCII renderer — subgraph drawing (border rectangle + label)
|
|
3
|
+
//
|
|
4
|
+
// Split out of draw.ts.
|
|
5
|
+
// ============================================================================
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
Canvas,
|
|
9
|
+
DrawingCoord,
|
|
10
|
+
AsciiGraph,
|
|
11
|
+
AsciiSubgraph,
|
|
12
|
+
} from './types.ts'
|
|
13
|
+
import { mkCanvas, write } from './canvas.ts'
|
|
14
|
+
import { splitLines } from './multiline-utils.ts'
|
|
15
|
+
import { displayWidth, toDisplayCells } from './display-width.ts'
|
|
16
|
+
|
|
17
|
+
/** Draw a subgraph border rectangle. */
|
|
18
|
+
export function drawSubgraphBox(sg: AsciiSubgraph, graph: AsciiGraph): Canvas {
|
|
19
|
+
const width = sg.maxX - sg.minX
|
|
20
|
+
const height = sg.maxY - sg.minY
|
|
21
|
+
if (width <= 0 || height <= 0) return mkCanvas(0, 0)
|
|
22
|
+
|
|
23
|
+
const from: DrawingCoord = { x: 0, y: 0 }
|
|
24
|
+
const to: DrawingCoord = { x: width, y: height }
|
|
25
|
+
const canvas = mkCanvas(width, height)
|
|
26
|
+
|
|
27
|
+
if (!graph.config.useAscii) {
|
|
28
|
+
for (let x = from.x + 1; x < to.x; x++) write(canvas, x, from.y, '─')
|
|
29
|
+
for (let x = from.x + 1; x < to.x; x++) write(canvas, x, to.y, '─')
|
|
30
|
+
for (let y = from.y + 1; y < to.y; y++) write(canvas, from.x, y, '│')
|
|
31
|
+
for (let y = from.y + 1; y < to.y; y++) write(canvas, to.x, y, '│')
|
|
32
|
+
write(canvas, from.x, from.y, '┌')
|
|
33
|
+
write(canvas, to.x, from.y, '┐')
|
|
34
|
+
write(canvas, from.x, to.y, '└')
|
|
35
|
+
write(canvas, to.x, to.y, '┘')
|
|
36
|
+
} else {
|
|
37
|
+
for (let x = from.x + 1; x < to.x; x++) write(canvas, x, from.y, '-')
|
|
38
|
+
for (let x = from.x + 1; x < to.x; x++) write(canvas, x, to.y, '-')
|
|
39
|
+
for (let y = from.y + 1; y < to.y; y++) write(canvas, from.x, y, '|')
|
|
40
|
+
for (let y = from.y + 1; y < to.y; y++) write(canvas, to.x, y, '|')
|
|
41
|
+
write(canvas, from.x, from.y, '+')
|
|
42
|
+
write(canvas, to.x, from.y, '+')
|
|
43
|
+
write(canvas, from.x, to.y, '+')
|
|
44
|
+
write(canvas, to.x, to.y, '+')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return canvas
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A single cell of a subgraph title's own footprint — including cells whose
|
|
52
|
+
* character is a literal space (e.g. the space inside "US West Region").
|
|
53
|
+
* Callers use this to force those cells to win over whatever was already
|
|
54
|
+
* drawn underneath (see the caller in `draw.ts` for why: `mergeCanvases`
|
|
55
|
+
* treats overlay spaces as transparent, which lets a connector line drawn
|
|
56
|
+
* earlier show through a label's internal spaces — issue #447).
|
|
57
|
+
*/
|
|
58
|
+
export interface SubgraphLabelCell {
|
|
59
|
+
x: number
|
|
60
|
+
y: number
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Draw a subgraph label centered in its header area. Supports multi-line labels. */
|
|
64
|
+
// `_graph` isn't read here but is kept to match the `(sg, graph)` signature
|
|
65
|
+
// shared by the other `draw*` subgraph helpers in this file.
|
|
66
|
+
export function drawSubgraphLabel(
|
|
67
|
+
sg: AsciiSubgraph,
|
|
68
|
+
_graph: AsciiGraph,
|
|
69
|
+
): [Canvas, DrawingCoord, SubgraphLabelCell[]] {
|
|
70
|
+
const width = sg.maxX - sg.minX
|
|
71
|
+
const height = sg.maxY - sg.minY
|
|
72
|
+
if (width <= 0 || height <= 0) return [mkCanvas(0, 0), { x: 0, y: 0 }, []]
|
|
73
|
+
|
|
74
|
+
const canvas = mkCanvas(width, height)
|
|
75
|
+
const footprint: SubgraphLabelCell[] = []
|
|
76
|
+
|
|
77
|
+
// Support multi-line subgraph labels
|
|
78
|
+
const lines = splitLines(sg.name)
|
|
79
|
+
|
|
80
|
+
// Start at row 1 inside subgraph, expand downward for multiple lines
|
|
81
|
+
for (let i = 0; i < lines.length; i++) {
|
|
82
|
+
const line = lines[i]!
|
|
83
|
+
const labelY = 1 + i
|
|
84
|
+
// Center the label within the interior columns (1..width-1, i.e.
|
|
85
|
+
// excluding both border columns). When the interior width minus the
|
|
86
|
+
// label width is odd, the leftover column can't be split evenly — bias
|
|
87
|
+
// it to the right (keep >=1 space on the left) so the title never hugs
|
|
88
|
+
// the left border. Using floor(width/2) - floor(label/2) here instead
|
|
89
|
+
// biases the leftover to the left, which can zero out the left padding
|
|
90
|
+
// entirely for even-length labels in an odd-width interior.
|
|
91
|
+
let labelX = 1 + Math.ceil((width - 1 - displayWidth(line)) / 2)
|
|
92
|
+
if (labelX < 1) labelX = 1
|
|
93
|
+
|
|
94
|
+
// Unlike `write()`'s own bounds (inclusive of the canvas edge — correct
|
|
95
|
+
// for the border-drawing calls in `drawSubgraphBox` above, which write
|
|
96
|
+
// the border itself at x == width / y == height), the label must stay
|
|
97
|
+
// strictly inside the border, so this loop enforces the tighter,
|
|
98
|
+
// exclusive bound itself rather than relying on `write()`'s clip.
|
|
99
|
+
const cells = toDisplayCells(line)
|
|
100
|
+
for (let j = 0; j < cells.length; j++) {
|
|
101
|
+
if (labelX + j >= width || labelY >= height) continue
|
|
102
|
+
write(canvas, labelX + j, labelY, cells[j]!)
|
|
103
|
+
footprint.push({ x: labelX + j, y: labelY })
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return [canvas, { x: sg.minX, y: sg.minY }, footprint]
|
|
108
|
+
}
|
package/src/draw.ts
ADDED
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// ASCII renderer — drawing operations
|
|
3
|
+
//
|
|
4
|
+
// Ported from AlexanderGrooff/mermaid-ascii cmd/draw.go + cmd/arrow.go.
|
|
5
|
+
// Contains the top-level draw orchestrator (drawGraph) that layers node,
|
|
6
|
+
// edge, bundle, and subgraph canvases together, plus the character-role
|
|
7
|
+
// tracking used for colored output.
|
|
8
|
+
//
|
|
9
|
+
// The individual drawing operations live in sibling modules, split out for
|
|
10
|
+
// navigability:
|
|
11
|
+
// - draw-boxes.ts node/box drawing, multi-section boxes
|
|
12
|
+
// - draw-lines.ts 8-directional line drawing
|
|
13
|
+
// - draw-arrows.ts arrow paths, corners, arrowheads, labels
|
|
14
|
+
// - draw-bundles.ts bundled (fan-in/fan-out) edge drawing
|
|
15
|
+
// - draw-subgraphs.ts subgraph border + label drawing
|
|
16
|
+
// This file re-exports their public entry points so `./draw.ts` remains a
|
|
17
|
+
// stable import path for the rest of the ASCII renderer.
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
import type {
|
|
21
|
+
Canvas,
|
|
22
|
+
DrawingCoord,
|
|
23
|
+
AsciiGraph,
|
|
24
|
+
AsciiSubgraph,
|
|
25
|
+
EdgeBundle,
|
|
26
|
+
} from './types.ts'
|
|
27
|
+
import { mergeCanvases, firstClaimWins, write } from './canvas.ts'
|
|
28
|
+
import type { RoleCanvas, CharRole } from './types.ts'
|
|
29
|
+
import { setRole } from './canvas.ts'
|
|
30
|
+
import { drawArrow } from './draw-arrows.ts'
|
|
31
|
+
import {
|
|
32
|
+
drawBundledEdgeSegment,
|
|
33
|
+
drawBundleSharedPath,
|
|
34
|
+
drawBundleArrowhead,
|
|
35
|
+
drawBundleArrowheadStart,
|
|
36
|
+
drawBundledEdgeArrowhead,
|
|
37
|
+
drawBundledEdgeArrowheadStart,
|
|
38
|
+
drawJunctionCharacter,
|
|
39
|
+
} from './draw-bundles.ts'
|
|
40
|
+
import { drawSubgraphBox, drawSubgraphLabel } from './draw-subgraphs.ts'
|
|
41
|
+
|
|
42
|
+
export {
|
|
43
|
+
drawNode,
|
|
44
|
+
drawBox,
|
|
45
|
+
drawMultiBox,
|
|
46
|
+
measureMultiBox,
|
|
47
|
+
classifyBoxChar,
|
|
48
|
+
} from './draw-boxes.ts'
|
|
49
|
+
export { drawLine } from './draw-lines.ts'
|
|
50
|
+
export { drawArrow, drawSubgraphBox, drawSubgraphLabel }
|
|
51
|
+
|
|
52
|
+
// ============================================================================
|
|
53
|
+
// Top-level draw orchestrator
|
|
54
|
+
// ============================================================================
|
|
55
|
+
|
|
56
|
+
/** Sort subgraphs by nesting depth (shallowest first) for correct layered rendering. */
|
|
57
|
+
function sortSubgraphsByDepth(subgraphs: AsciiSubgraph[]): AsciiSubgraph[] {
|
|
58
|
+
function getDepth(sg: AsciiSubgraph): number {
|
|
59
|
+
return sg.parent === null ? 0 : 1 + getDepth(sg.parent)
|
|
60
|
+
}
|
|
61
|
+
const sorted = [...subgraphs]
|
|
62
|
+
sorted.sort((a, b) => getDepth(a) - getDepth(b))
|
|
63
|
+
return sorted
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ============================================================================
|
|
67
|
+
// Role tracking helpers for colored output
|
|
68
|
+
// ============================================================================
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Fill roles for all non-space characters in a canvas region.
|
|
72
|
+
* Used after drawing a layer to record what role those characters have.
|
|
73
|
+
*/
|
|
74
|
+
function fillRolesFromCanvas(
|
|
75
|
+
roleCanvas: RoleCanvas,
|
|
76
|
+
canvas: Canvas,
|
|
77
|
+
offset: DrawingCoord,
|
|
78
|
+
role: CharRole,
|
|
79
|
+
): void {
|
|
80
|
+
for (let x = 0; x < canvas.length; x++) {
|
|
81
|
+
for (let y = 0; y < (canvas[0]?.length ?? 0); y++) {
|
|
82
|
+
const char = canvas[x]?.[y]
|
|
83
|
+
if (char && char !== ' ') {
|
|
84
|
+
const rx = x + offset.x
|
|
85
|
+
const ry = y + offset.y
|
|
86
|
+
// Use setRole which auto-expands the role canvas if needed
|
|
87
|
+
if (rx >= 0 && ry >= 0) {
|
|
88
|
+
setRole(roleCanvas, rx, ry, role)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Fill roles for multiple canvases with the same role.
|
|
97
|
+
*/
|
|
98
|
+
function fillRolesFromCanvases(
|
|
99
|
+
roleCanvas: RoleCanvas,
|
|
100
|
+
canvases: Canvas[],
|
|
101
|
+
offset: DrawingCoord,
|
|
102
|
+
role: CharRole,
|
|
103
|
+
): void {
|
|
104
|
+
for (const canvas of canvases) {
|
|
105
|
+
fillRolesFromCanvas(roleCanvas, canvas, offset, role)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Special handling for node boxes: border chars get 'border' role, text gets 'text' role.
|
|
111
|
+
* Detects text by checking if character is alphanumeric or common punctuation.
|
|
112
|
+
*/
|
|
113
|
+
function fillRolesForNodeBox(
|
|
114
|
+
roleCanvas: RoleCanvas,
|
|
115
|
+
canvas: Canvas,
|
|
116
|
+
offset: DrawingCoord,
|
|
117
|
+
): void {
|
|
118
|
+
const isBorderChar = (c: string) => /^[┌┐└┘├┤┬┴┼│─╭╮╰╯+\-|.':]$/.test(c)
|
|
119
|
+
|
|
120
|
+
for (let x = 0; x < canvas.length; x++) {
|
|
121
|
+
for (let y = 0; y < (canvas[0]?.length ?? 0); y++) {
|
|
122
|
+
const char = canvas[x]?.[y]
|
|
123
|
+
if (char && char !== ' ') {
|
|
124
|
+
const rx = x + offset.x
|
|
125
|
+
const ry = y + offset.y
|
|
126
|
+
// Use setRole which auto-expands the role canvas if needed
|
|
127
|
+
if (rx >= 0 && ry >= 0) {
|
|
128
|
+
setRole(roleCanvas, rx, ry, isBorderChar(char) ? 'border' : 'text')
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Main draw function — renders the entire graph onto the canvas.
|
|
137
|
+
* Drawing order matters for correct layering:
|
|
138
|
+
* 1. Subgraph borders (bottom layer)
|
|
139
|
+
* 2. Node boxes
|
|
140
|
+
* 3. Edge paths (lines)
|
|
141
|
+
* 4. Edge corners
|
|
142
|
+
* 5. Arrowheads
|
|
143
|
+
* 6. Box-start junctions
|
|
144
|
+
* 7. Edge labels
|
|
145
|
+
* 8. Subgraph labels (top layer)
|
|
146
|
+
*
|
|
147
|
+
* Also fills the roleCanvas with character roles for colored output.
|
|
148
|
+
*/
|
|
149
|
+
export function drawGraph(graph: AsciiGraph): Canvas {
|
|
150
|
+
const useAscii = graph.config.useAscii
|
|
151
|
+
const zero: DrawingCoord = { x: 0, y: 0 }
|
|
152
|
+
|
|
153
|
+
// Draw subgraph borders
|
|
154
|
+
const sortedSgs = sortSubgraphsByDepth(graph.subgraphs)
|
|
155
|
+
for (const sg of sortedSgs) {
|
|
156
|
+
const sgCanvas = drawSubgraphBox(sg, graph)
|
|
157
|
+
const offset: DrawingCoord = { x: sg.minX, y: sg.minY }
|
|
158
|
+
graph.canvas = mergeCanvases(graph.canvas, offset, useAscii, sgCanvas)
|
|
159
|
+
// Subgraph borders get 'border' role
|
|
160
|
+
fillRolesFromCanvas(graph.roleCanvas, sgCanvas, offset, 'border')
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Draw node boxes
|
|
164
|
+
for (const node of graph.nodes) {
|
|
165
|
+
if (!node.drawn && node.drawingCoord && node.drawing) {
|
|
166
|
+
graph.canvas = mergeCanvases(
|
|
167
|
+
graph.canvas,
|
|
168
|
+
node.drawingCoord,
|
|
169
|
+
useAscii,
|
|
170
|
+
node.drawing,
|
|
171
|
+
)
|
|
172
|
+
// Node boxes: detect border vs text characters
|
|
173
|
+
fillRolesForNodeBox(graph.roleCanvas, node.drawing, node.drawingCoord)
|
|
174
|
+
node.drawn = true
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Collect all edge drawing layers
|
|
179
|
+
const lineCanvases: Canvas[] = []
|
|
180
|
+
const cornerCanvases: Canvas[] = []
|
|
181
|
+
const arrowHeadEndCanvases: Canvas[] = []
|
|
182
|
+
const arrowHeadStartCanvases: Canvas[] = []
|
|
183
|
+
const boxStartCanvases: Canvas[] = []
|
|
184
|
+
const labelCanvases: Canvas[] = []
|
|
185
|
+
const junctionCanvases: Canvas[] = []
|
|
186
|
+
|
|
187
|
+
// Track which bundles have been processed (to draw shared paths only once)
|
|
188
|
+
const processedBundles = new Set<EdgeBundle>()
|
|
189
|
+
|
|
190
|
+
for (const edge of graph.edges) {
|
|
191
|
+
// Handle bundled edges specially
|
|
192
|
+
if (edge.bundle && edge.pathToJunction) {
|
|
193
|
+
const bundle = edge.bundle
|
|
194
|
+
|
|
195
|
+
// Draw this edge's individual path (source → junction for fan-in, junction → target for fan-out)
|
|
196
|
+
const [pathC, boxStartC, , , cornersC, labelC] = drawBundledEdgeSegment(
|
|
197
|
+
graph,
|
|
198
|
+
edge,
|
|
199
|
+
bundle,
|
|
200
|
+
)
|
|
201
|
+
lineCanvases.push(pathC)
|
|
202
|
+
cornerCanvases.push(cornersC)
|
|
203
|
+
boxStartCanvases.push(boxStartC)
|
|
204
|
+
labelCanvases.push(labelC)
|
|
205
|
+
|
|
206
|
+
// For fan-in bundles, draw a start arrowhead at each individual source
|
|
207
|
+
if (bundle.type === 'fan-in' && edge.hasArrowStart) {
|
|
208
|
+
const arrowHeadStartC = drawBundledEdgeArrowheadStart(graph, edge)
|
|
209
|
+
arrowHeadStartCanvases.push(arrowHeadStartC)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Draw the bundle's shared path and arrowhead only once
|
|
213
|
+
if (!processedBundles.has(bundle)) {
|
|
214
|
+
processedBundles.add(bundle)
|
|
215
|
+
|
|
216
|
+
// Draw shared path (junction → target for fan-in, source → junction for fan-out)
|
|
217
|
+
const [sharedPathC, sharedCornersC] = drawBundleSharedPath(
|
|
218
|
+
graph,
|
|
219
|
+
bundle,
|
|
220
|
+
)
|
|
221
|
+
lineCanvases.push(sharedPathC)
|
|
222
|
+
cornerCanvases.push(sharedCornersC)
|
|
223
|
+
|
|
224
|
+
// Draw arrowhead at target for fan-in (once for all edges in bundle)
|
|
225
|
+
if (bundle.type === 'fan-in') {
|
|
226
|
+
const arrowHeadC = drawBundleArrowhead(graph, bundle)
|
|
227
|
+
arrowHeadEndCanvases.push(arrowHeadC)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Draw start arrowhead at the shared source for fan-out (once for all
|
|
231
|
+
// edges in the bundle — the trunk leaving the source is drawn once,
|
|
232
|
+
// so it only gets a start arrowhead when every edge agrees on one).
|
|
233
|
+
if (
|
|
234
|
+
bundle.type === 'fan-out' &&
|
|
235
|
+
bundle.edges.every((e) => e.hasArrowStart)
|
|
236
|
+
) {
|
|
237
|
+
const arrowHeadStartC = drawBundleArrowheadStart(graph, bundle)
|
|
238
|
+
arrowHeadStartCanvases.push(arrowHeadStartC)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Draw junction character
|
|
242
|
+
const junctionC = drawJunctionCharacter(graph, bundle)
|
|
243
|
+
junctionCanvases.push(junctionC)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// For fan-out bundles, draw arrowhead at each target
|
|
247
|
+
if (bundle.type === 'fan-out' && edge.hasArrowEnd) {
|
|
248
|
+
const arrowHeadC = drawBundledEdgeArrowhead(graph, edge)
|
|
249
|
+
arrowHeadEndCanvases.push(arrowHeadC)
|
|
250
|
+
}
|
|
251
|
+
} else {
|
|
252
|
+
// Non-bundled edge: use standard drawing
|
|
253
|
+
const [
|
|
254
|
+
pathC,
|
|
255
|
+
boxStartC,
|
|
256
|
+
arrowHeadEndC,
|
|
257
|
+
arrowHeadStartC,
|
|
258
|
+
cornersC,
|
|
259
|
+
labelC,
|
|
260
|
+
] = drawArrow(graph, edge)
|
|
261
|
+
lineCanvases.push(pathC)
|
|
262
|
+
cornerCanvases.push(cornersC)
|
|
263
|
+
arrowHeadEndCanvases.push(arrowHeadEndC)
|
|
264
|
+
arrowHeadStartCanvases.push(arrowHeadStartC)
|
|
265
|
+
boxStartCanvases.push(boxStartC)
|
|
266
|
+
labelCanvases.push(labelC)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Merge edge layers in order and track roles
|
|
271
|
+
// Note: arrowHeadStart is merged AFTER boxStart so bidirectional arrows
|
|
272
|
+
// properly overwrite the box connector at the source end
|
|
273
|
+
//
|
|
274
|
+
// `firstClaimWins` resolves any leftover cross-style overlap between two
|
|
275
|
+
// edges' own line canvases (see its doc) before they're merged onto the
|
|
276
|
+
// shared canvas — applied here, and not to the other edge layers below,
|
|
277
|
+
// because only the line layer has this "first claim wins" rule; corners,
|
|
278
|
+
// arrowheads, box-start connectors and labels keep mergeCanvases' normal
|
|
279
|
+
// last-overlay-wins behavior.
|
|
280
|
+
const resolvedLineCanvases = firstClaimWins(lineCanvases)
|
|
281
|
+
graph.canvas = mergeCanvases(
|
|
282
|
+
graph.canvas,
|
|
283
|
+
zero,
|
|
284
|
+
useAscii,
|
|
285
|
+
...resolvedLineCanvases,
|
|
286
|
+
)
|
|
287
|
+
fillRolesFromCanvases(graph.roleCanvas, resolvedLineCanvases, zero, 'line')
|
|
288
|
+
|
|
289
|
+
graph.canvas = mergeCanvases(graph.canvas, zero, useAscii, ...cornerCanvases)
|
|
290
|
+
fillRolesFromCanvases(graph.roleCanvas, cornerCanvases, zero, 'corner')
|
|
291
|
+
|
|
292
|
+
graph.canvas = mergeCanvases(
|
|
293
|
+
graph.canvas,
|
|
294
|
+
zero,
|
|
295
|
+
useAscii,
|
|
296
|
+
...junctionCanvases,
|
|
297
|
+
)
|
|
298
|
+
fillRolesFromCanvases(graph.roleCanvas, junctionCanvases, zero, 'junction')
|
|
299
|
+
|
|
300
|
+
graph.canvas = mergeCanvases(
|
|
301
|
+
graph.canvas,
|
|
302
|
+
zero,
|
|
303
|
+
useAscii,
|
|
304
|
+
...arrowHeadEndCanvases,
|
|
305
|
+
)
|
|
306
|
+
fillRolesFromCanvases(graph.roleCanvas, arrowHeadEndCanvases, zero, 'arrow')
|
|
307
|
+
|
|
308
|
+
graph.canvas = mergeCanvases(
|
|
309
|
+
graph.canvas,
|
|
310
|
+
zero,
|
|
311
|
+
useAscii,
|
|
312
|
+
...boxStartCanvases,
|
|
313
|
+
)
|
|
314
|
+
fillRolesFromCanvases(graph.roleCanvas, boxStartCanvases, zero, 'junction')
|
|
315
|
+
|
|
316
|
+
graph.canvas = mergeCanvases(
|
|
317
|
+
graph.canvas,
|
|
318
|
+
zero,
|
|
319
|
+
useAscii,
|
|
320
|
+
...arrowHeadStartCanvases,
|
|
321
|
+
)
|
|
322
|
+
fillRolesFromCanvases(graph.roleCanvas, arrowHeadStartCanvases, zero, 'arrow')
|
|
323
|
+
|
|
324
|
+
graph.canvas = mergeCanvases(graph.canvas, zero, useAscii, ...labelCanvases)
|
|
325
|
+
fillRolesFromCanvases(graph.roleCanvas, labelCanvases, zero, 'text')
|
|
326
|
+
|
|
327
|
+
// Draw subgraph labels last (on top)
|
|
328
|
+
for (const sg of graph.subgraphs) {
|
|
329
|
+
if (sg.nodes.length === 0) continue
|
|
330
|
+
const [labelCanvas, offset, footprint] = drawSubgraphLabel(sg, graph)
|
|
331
|
+
graph.canvas = mergeCanvases(graph.canvas, offset, useAscii, labelCanvas)
|
|
332
|
+
fillRolesFromCanvas(graph.roleCanvas, labelCanvas, offset, 'text')
|
|
333
|
+
// mergeCanvases treats an overlay's space characters as transparent (so
|
|
334
|
+
// sparse edge/arrow layers don't blank each other out) — but the title's
|
|
335
|
+
// own footprint should win outright, including cells whose character is
|
|
336
|
+
// a literal space (e.g. "US West Region"). Otherwise a connector line
|
|
337
|
+
// already drawn at that cell shows through the label's internal space
|
|
338
|
+
// (issue #447: rendered as "US West│Region"). Force those specific
|
|
339
|
+
// cells, bypassing mergeCanvases's space-skip.
|
|
340
|
+
for (const { x, y } of footprint) {
|
|
341
|
+
const ch = labelCanvas[x]?.[y]
|
|
342
|
+
if (ch === ' ') {
|
|
343
|
+
write(graph.canvas, x + offset.x, y + offset.y, ch)
|
|
344
|
+
setRole(graph.roleCanvas, x + offset.x, y + offset.y, 'text')
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return graph.canvas
|
|
350
|
+
}
|