@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,593 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// ASCII renderer — arrow drawing (path, corners, arrowheads, box-start
|
|
3
|
+
// junctions, labels)
|
|
4
|
+
//
|
|
5
|
+
// Split out of draw.ts.
|
|
6
|
+
// ============================================================================
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
Canvas,
|
|
10
|
+
DrawingCoord,
|
|
11
|
+
GridCoord,
|
|
12
|
+
Direction,
|
|
13
|
+
AsciiGraph,
|
|
14
|
+
AsciiEdge,
|
|
15
|
+
AsciiEdgeStyle,
|
|
16
|
+
AsciiNode,
|
|
17
|
+
} from './types.ts'
|
|
18
|
+
import {
|
|
19
|
+
Up,
|
|
20
|
+
Down,
|
|
21
|
+
Left,
|
|
22
|
+
Right,
|
|
23
|
+
UpperLeft,
|
|
24
|
+
UpperRight,
|
|
25
|
+
LowerLeft,
|
|
26
|
+
LowerRight,
|
|
27
|
+
Middle,
|
|
28
|
+
drawingCoordEquals,
|
|
29
|
+
} from './types.ts'
|
|
30
|
+
import { copyCanvas, drawText, write } from './canvas.ts'
|
|
31
|
+
import { determineDirection, dirEquals } from './edge-routing.ts'
|
|
32
|
+
import { displayWidth } from './display-width.ts'
|
|
33
|
+
import { gridToDrawingCoord, lineToDrawing } from './grid.ts'
|
|
34
|
+
import { splitLines } from './multiline-utils.ts'
|
|
35
|
+
import { drawLine } from './draw-lines.ts'
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Draw a complete arrow (edge) between two nodes.
|
|
39
|
+
* Returns 6 separate canvases for layered compositing:
|
|
40
|
+
* [path, boxStart, arrowHeadEnd, arrowHeadStart, corners, label]
|
|
41
|
+
*
|
|
42
|
+
* Supports bidirectional arrows via edge.hasArrowStart and edge.hasArrowEnd.
|
|
43
|
+
*/
|
|
44
|
+
export function drawArrow(
|
|
45
|
+
graph: AsciiGraph,
|
|
46
|
+
edge: AsciiEdge,
|
|
47
|
+
): [Canvas, Canvas, Canvas, Canvas, Canvas, Canvas] {
|
|
48
|
+
if (edge.path.length === 0) {
|
|
49
|
+
const empty = copyCanvas(graph.canvas)
|
|
50
|
+
return [empty, empty, empty, empty, empty, empty]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const labelCanvas = drawArrowLabel(graph, edge)
|
|
54
|
+
const [pathCanvas, linesDrawn, lineDirs] = drawPath(
|
|
55
|
+
graph,
|
|
56
|
+
edge.path,
|
|
57
|
+
edge.style,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
// A routed path can collapse to zero drawn line segments when every grid
|
|
61
|
+
// point maps to the same drawing coordinate — e.g. a routed edge whose
|
|
62
|
+
// preferred from/to grid coordinates coincide for closely-spaced/adjacent
|
|
63
|
+
// nodes (pathfinder.ts's getPath can legitimately return a single-point
|
|
64
|
+
// path in that case; see #153). With no segment to anchor to, there's
|
|
65
|
+
// nothing for a box-start connector or an end arrowhead to attach to, so
|
|
66
|
+
// both are skipped instead of indexing into the empty linesDrawn/lineDirs
|
|
67
|
+
// arrays.
|
|
68
|
+
const hasSegments = linesDrawn.length > 0
|
|
69
|
+
|
|
70
|
+
/*
|
|
71
|
+
* An invisible link (`A ~~~ B`) routes and reserves space like any other
|
|
72
|
+
* edge, but must leave no mark. drawPath already writes spaces for its
|
|
73
|
+
* segments; the box connector and corner glyphs are drawn by separate
|
|
74
|
+
* passes that don't consult the line character set, so they're suppressed
|
|
75
|
+
* explicitly here. Arrowheads need no guard — `~~~` carries no `>`/`o`/`x`
|
|
76
|
+
* marker, so hasArrowStart/hasArrowEnd are already false.
|
|
77
|
+
*/
|
|
78
|
+
const invisible = edge.style === 'invisible'
|
|
79
|
+
|
|
80
|
+
const boxStartCanvas =
|
|
81
|
+
hasSegments && !invisible
|
|
82
|
+
? drawBoxStart(graph, edge.path, linesDrawn[0]!, edge.from)
|
|
83
|
+
: copyCanvas(graph.canvas)
|
|
84
|
+
|
|
85
|
+
// Draw end arrowhead only if hasArrowEnd is true (default behavior)
|
|
86
|
+
let arrowHeadEndCanvas: Canvas
|
|
87
|
+
if (edge.hasArrowEnd && hasSegments) {
|
|
88
|
+
arrowHeadEndCanvas = drawArrowHead(
|
|
89
|
+
graph,
|
|
90
|
+
linesDrawn[linesDrawn.length - 1]!,
|
|
91
|
+
lineDirs[lineDirs.length - 1]!,
|
|
92
|
+
edge.endMarker,
|
|
93
|
+
)
|
|
94
|
+
} else {
|
|
95
|
+
arrowHeadEndCanvas = copyCanvas(graph.canvas)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Draw start arrowhead for bidirectional edges
|
|
99
|
+
// The start arrowhead needs to be at the box connector position (one step back
|
|
100
|
+
// from the first line point), pointing into the source node.
|
|
101
|
+
let arrowHeadStartCanvas: Canvas
|
|
102
|
+
if (edge.hasArrowStart && hasSegments) {
|
|
103
|
+
const firstLine = linesDrawn[0]!
|
|
104
|
+
const firstPoint = firstLine[0]!
|
|
105
|
+
const startDir = reverseDirection(lineDirs[0]!)
|
|
106
|
+
|
|
107
|
+
// Calculate the box connector position (one step back from first point)
|
|
108
|
+
const arrowPos: DrawingCoord = { x: firstPoint.x, y: firstPoint.y }
|
|
109
|
+
if (dirEquals(lineDirs[0]!, Right)) arrowPos.x = firstPoint.x - 1
|
|
110
|
+
else if (dirEquals(lineDirs[0]!, Left)) arrowPos.x = firstPoint.x + 1
|
|
111
|
+
else if (dirEquals(lineDirs[0]!, Down)) arrowPos.y = firstPoint.y - 1
|
|
112
|
+
else if (dirEquals(lineDirs[0]!, Up)) arrowPos.y = firstPoint.y + 1
|
|
113
|
+
|
|
114
|
+
// Create a synthetic line ending at the arrow position for drawArrowHead
|
|
115
|
+
const syntheticLine: DrawingCoord[] = [firstPoint, arrowPos]
|
|
116
|
+
arrowHeadStartCanvas = drawArrowHead(
|
|
117
|
+
graph,
|
|
118
|
+
syntheticLine,
|
|
119
|
+
startDir,
|
|
120
|
+
edge.startMarker,
|
|
121
|
+
)
|
|
122
|
+
} else {
|
|
123
|
+
arrowHeadStartCanvas = copyCanvas(graph.canvas)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const cornersCanvas = invisible
|
|
127
|
+
? copyCanvas(graph.canvas)
|
|
128
|
+
: drawCorners(graph, edge.path)
|
|
129
|
+
|
|
130
|
+
return [
|
|
131
|
+
pathCanvas,
|
|
132
|
+
boxStartCanvas,
|
|
133
|
+
arrowHeadEndCanvas,
|
|
134
|
+
arrowHeadStartCanvas,
|
|
135
|
+
cornersCanvas,
|
|
136
|
+
labelCanvas,
|
|
137
|
+
]
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Reverse a direction (for bidirectional arrow start heads).
|
|
142
|
+
*/
|
|
143
|
+
function reverseDirection(dir: Direction): Direction {
|
|
144
|
+
if (dirEquals(dir, Up)) return Down
|
|
145
|
+
if (dirEquals(dir, Down)) return Up
|
|
146
|
+
if (dirEquals(dir, Left)) return Right
|
|
147
|
+
if (dirEquals(dir, Right)) return Left
|
|
148
|
+
if (dirEquals(dir, UpperLeft)) return LowerRight
|
|
149
|
+
if (dirEquals(dir, UpperRight)) return LowerLeft
|
|
150
|
+
if (dirEquals(dir, LowerLeft)) return UpperRight
|
|
151
|
+
if (dirEquals(dir, LowerRight)) return UpperLeft
|
|
152
|
+
return Middle
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Draw the path lines for an edge.
|
|
157
|
+
* Returns the canvas, the coordinates drawn for each segment, and the direction of each segment.
|
|
158
|
+
*/
|
|
159
|
+
function drawPath(
|
|
160
|
+
graph: AsciiGraph,
|
|
161
|
+
path: GridCoord[],
|
|
162
|
+
style: AsciiEdgeStyle = 'solid',
|
|
163
|
+
): [Canvas, DrawingCoord[][], Direction[]] {
|
|
164
|
+
const canvas = copyCanvas(graph.canvas)
|
|
165
|
+
// path is non-empty: drawArrow (drawPath's sole caller) already returns
|
|
166
|
+
// early when edge.path.length === 0.
|
|
167
|
+
let previousCoord = path[0]!
|
|
168
|
+
const linesDrawn: DrawingCoord[][] = []
|
|
169
|
+
const lineDirs: Direction[] = []
|
|
170
|
+
|
|
171
|
+
for (let i = 1; i < path.length; i++) {
|
|
172
|
+
const nextCoord = path[i]!
|
|
173
|
+
const prevDC = gridToDrawingCoord(graph, previousCoord)
|
|
174
|
+
const nextDC = gridToDrawingCoord(graph, nextCoord)
|
|
175
|
+
|
|
176
|
+
if (drawingCoordEquals(prevDC, nextDC)) {
|
|
177
|
+
previousCoord = nextCoord
|
|
178
|
+
continue
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const dir = determineDirection(previousCoord, nextCoord)
|
|
182
|
+
const segment = drawLine(
|
|
183
|
+
canvas,
|
|
184
|
+
prevDC,
|
|
185
|
+
nextDC,
|
|
186
|
+
1,
|
|
187
|
+
-1,
|
|
188
|
+
graph.config.useAscii,
|
|
189
|
+
style,
|
|
190
|
+
)
|
|
191
|
+
if (segment.length === 0) segment.push(prevDC)
|
|
192
|
+
linesDrawn.push(segment)
|
|
193
|
+
lineDirs.push(dir)
|
|
194
|
+
previousCoord = nextCoord
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return [canvas, linesDrawn, lineDirs]
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Vertical box-border characters a horizontal connector can genuinely merge with. */
|
|
201
|
+
const VERTICAL_BORDER_CHARS = new Set(['│', '┃', '║', '┆', '┊', '|', '‖'])
|
|
202
|
+
/** Horizontal box-border characters a vertical connector can genuinely merge with. */
|
|
203
|
+
const HORIZONTAL_BORDER_CHARS = new Set(['─', '━', '═', '╌', '┄', '-', '='])
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Draw the junction character where an edge exits the source node's box.
|
|
207
|
+
* Unicode mode uses shape-specific T-junction glyphs; ASCII mode uses the
|
|
208
|
+
* same universal '+' already used for corners in draw-boxes.ts.
|
|
209
|
+
* Skips drawing for state pseudo-states which have their own visual borders.
|
|
210
|
+
*
|
|
211
|
+
* A tee/junction character (┬┴├┤) is only correct when it's actually merging
|
|
212
|
+
* with a real perpendicular border line at that exact cell — e.g. a grid
|
|
213
|
+
* column widened by a sibling edge's label can push the computed box-start
|
|
214
|
+
* position away from the node's actual border, landing on a blank cell with
|
|
215
|
+
* no vertical (or horizontal) line to justify a junction. In that case fall
|
|
216
|
+
* back to a plain line character matching the edge's own direction, instead
|
|
217
|
+
* of fabricating a stray tee (see issue #86).
|
|
218
|
+
*/
|
|
219
|
+
function drawBoxStart(
|
|
220
|
+
graph: AsciiGraph,
|
|
221
|
+
path: GridCoord[],
|
|
222
|
+
firstLine: DrawingCoord[],
|
|
223
|
+
sourceNode: AsciiNode,
|
|
224
|
+
): Canvas {
|
|
225
|
+
const canvas = copyCanvas(graph.canvas)
|
|
226
|
+
const useAscii = graph.config.useAscii
|
|
227
|
+
|
|
228
|
+
// Skip box start connectors for state pseudo-states (they have their own bordered design)
|
|
229
|
+
if (sourceNode.shape === 'state-start' || sourceNode.shape === 'state-end') {
|
|
230
|
+
return canvas
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// firstLine is guaranteed non-empty and path has >= 2 points: the sole
|
|
234
|
+
// caller (drawArrow) only invokes drawBoxStart when drawPath produced at
|
|
235
|
+
// least one line segment (see the `hasSegments` guard there), and
|
|
236
|
+
// drawPath never pushes an empty segment into linesDrawn.
|
|
237
|
+
const from = firstLine[0]!
|
|
238
|
+
const dir = determineDirection(path[0]!, path[1]!)
|
|
239
|
+
const junction = useAscii ? '+' : null
|
|
240
|
+
|
|
241
|
+
// `canvas` (from copyCanvas) is a blank overlay layer — it never has
|
|
242
|
+
// content of its own to check. Whether a genuine perpendicular border
|
|
243
|
+
// line already occupies the target cell has to be read from
|
|
244
|
+
// `graph.canvas`, which at this point in drawGraph already has node
|
|
245
|
+
// boxes (and subgraph borders) merged onto it, before any edge layers.
|
|
246
|
+
const existingOnBox = (x: number, y: number): string | undefined =>
|
|
247
|
+
graph.canvas[x]?.[y]
|
|
248
|
+
|
|
249
|
+
if (dirEquals(dir, Up)) {
|
|
250
|
+
const x = from.x
|
|
251
|
+
const y = from.y + 1
|
|
252
|
+
const existing = existingOnBox(x, y)
|
|
253
|
+
const hasBorder =
|
|
254
|
+
existing !== undefined && HORIZONTAL_BORDER_CHARS.has(existing)
|
|
255
|
+
write(canvas, x, y, hasBorder ? (junction ?? '┴') : useAscii ? '|' : '│')
|
|
256
|
+
} else if (dirEquals(dir, Down)) {
|
|
257
|
+
const x = from.x
|
|
258
|
+
const y = from.y - 1
|
|
259
|
+
const existing = existingOnBox(x, y)
|
|
260
|
+
const hasBorder =
|
|
261
|
+
existing !== undefined && HORIZONTAL_BORDER_CHARS.has(existing)
|
|
262
|
+
write(canvas, x, y, hasBorder ? (junction ?? '┬') : useAscii ? '|' : '│')
|
|
263
|
+
} else if (dirEquals(dir, Left) || dirEquals(dir, Right)) {
|
|
264
|
+
// Anchor horizontal connectors to the source node's *own* rendered
|
|
265
|
+
// border column, not to gridToDrawingCoord's grid-column-centered
|
|
266
|
+
// position. That position is centered within the node's border grid
|
|
267
|
+
// column using that column's *allocated* width — but a sibling edge's
|
|
268
|
+
// label can land on that same column (its labelLine's chosen segment
|
|
269
|
+
// just happens to pass through the node's border column on its way
|
|
270
|
+
// elsewhere) and widen it well past the 1-character width the border
|
|
271
|
+
// itself needs. Centering on the inflated width then drags the
|
|
272
|
+
// connector away from the box's actual border character, which stays
|
|
273
|
+
// put at a position that only depends on the box's own dimensions.
|
|
274
|
+
//
|
|
275
|
+
// Node dimensions are set before edge routing/drawing (see
|
|
276
|
+
// createMapping in grid.ts), so drawingCoord/drawing should always be
|
|
277
|
+
// present here — but that's a cross-module invariant the type checker
|
|
278
|
+
// can't see, so it's validated explicitly rather than trusted silently.
|
|
279
|
+
const dc = sourceNode.drawingCoord
|
|
280
|
+
const drawing = sourceNode.drawing
|
|
281
|
+
if (dc === null || drawing === null) {
|
|
282
|
+
/* v8 ignore next */
|
|
283
|
+
throw new Error(
|
|
284
|
+
`drawBoxStart: node "${sourceNode.name}" has no drawingCoord/drawing assigned`,
|
|
285
|
+
)
|
|
286
|
+
}
|
|
287
|
+
const boxWidth = drawing.length
|
|
288
|
+
const x = dirEquals(dir, Left) ? dc.x : dc.x + boxWidth - 1
|
|
289
|
+
const y = from.y
|
|
290
|
+
const existing = existingOnBox(x, y)
|
|
291
|
+
const hasBorder =
|
|
292
|
+
existing !== undefined && VERTICAL_BORDER_CHARS.has(existing)
|
|
293
|
+
write(
|
|
294
|
+
canvas,
|
|
295
|
+
x,
|
|
296
|
+
y,
|
|
297
|
+
hasBorder
|
|
298
|
+
? (junction ?? (dirEquals(dir, Left) ? '┤' : '├'))
|
|
299
|
+
: useAscii
|
|
300
|
+
? '-'
|
|
301
|
+
: '─',
|
|
302
|
+
)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return canvas
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Fixed glyph for a `--o`/`--x` circle/cross terminator — direction-
|
|
310
|
+
* independent, unlike the triangular arrowheads below, so callers don't
|
|
311
|
+
* need to know which way the edge points to pick it. Returns undefined for
|
|
312
|
+
* a plain arrowhead (no marker), so a caller can fall back to its own
|
|
313
|
+
* directional glyph selection. Shared by drawArrowHead below (single-edge
|
|
314
|
+
* arrowheads) and draw-bundles.ts (fan-in/fan-out bundled arrowheads) so
|
|
315
|
+
* the glyph choice can't drift between the two. See issue #330.
|
|
316
|
+
*/
|
|
317
|
+
export function markerArrowChar(
|
|
318
|
+
useAscii: boolean,
|
|
319
|
+
marker: 'circle' | 'cross' | undefined,
|
|
320
|
+
): string | undefined {
|
|
321
|
+
if (marker === 'circle') return useAscii ? 'o' : '○'
|
|
322
|
+
if (marker === 'cross') return useAscii ? 'x' : '✕'
|
|
323
|
+
return undefined
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Maps a direction to its Unicode arrowhead glyph, or `undefined` for a
|
|
328
|
+
* direction with no arrowhead (`Middle`, or any other value this switch
|
|
329
|
+
* doesn't recognize) — callers decide the ultimate fallback.
|
|
330
|
+
*
|
|
331
|
+
* Uses triangular Unicode symbols (▲▼◄►) for the four orthogonal
|
|
332
|
+
* directions and diagonal arrow symbols (↗↖↘↙) for the four diagonal ones.
|
|
333
|
+
* The diagonal glyphs are plain Arrows-block characters (U+2196–U+2199),
|
|
334
|
+
* not filled triangles like the orthogonal set — JetBrains Mono NL (this
|
|
335
|
+
* site's self-hosted ASCII font, see scripts/build-mono-font-subset.ts)
|
|
336
|
+
* has no glyph at all for the filled diagonal triangles (◢◣◤◥, U+25E2–
|
|
337
|
+
* U+25E5) that would otherwise match the orthogonal style; U+2196–U+2199
|
|
338
|
+
* are real, correctly-directional glyphs the font does have, which beats
|
|
339
|
+
* falling back to an unpinned system font for just these four characters.
|
|
340
|
+
* See issue #1062.
|
|
341
|
+
*
|
|
342
|
+
* Exported (alongside {@link asciiArrowChar}) so tests can exercise every
|
|
343
|
+
* direction directly — the ELK/pathfinder layout this module actually
|
|
344
|
+
* draws from rarely if ever produces some direction combinations (e.g.
|
|
345
|
+
* `LowerLeft` only arises from determinePath's rare Case-4 diagonal
|
|
346
|
+
* fallback, and only for a specific relative source/target position no
|
|
347
|
+
* existing sample or hand-written diagram happens to trigger), which would
|
|
348
|
+
* otherwise leave this mapping's correctness for those directions unverified.
|
|
349
|
+
*/
|
|
350
|
+
export function unicodeArrowChar(dir: Direction): string | undefined {
|
|
351
|
+
if (dirEquals(dir, Up)) return '▲'
|
|
352
|
+
if (dirEquals(dir, Down)) return '▼'
|
|
353
|
+
if (dirEquals(dir, Left)) return '◄'
|
|
354
|
+
if (dirEquals(dir, Right)) return '►'
|
|
355
|
+
if (dirEquals(dir, UpperRight)) return '↗'
|
|
356
|
+
if (dirEquals(dir, UpperLeft)) return '↖'
|
|
357
|
+
if (dirEquals(dir, LowerRight)) return '↘'
|
|
358
|
+
if (dirEquals(dir, LowerLeft)) return '↙'
|
|
359
|
+
return undefined
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** ASCII-mode counterpart of {@link unicodeArrowChar} — orthogonal only, no diagonals. */
|
|
363
|
+
export function asciiArrowChar(dir: Direction): string | undefined {
|
|
364
|
+
if (dirEquals(dir, Up)) return '^'
|
|
365
|
+
if (dirEquals(dir, Down)) return 'v'
|
|
366
|
+
if (dirEquals(dir, Left)) return '<'
|
|
367
|
+
if (dirEquals(dir, Right)) return '>'
|
|
368
|
+
return undefined
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Draw the arrowhead at the end of an edge path.
|
|
373
|
+
*
|
|
374
|
+
* `marker` overrides the directional glyph with a fixed circle/cross glyph
|
|
375
|
+
* for `--o`/`--x` (flowchart) terminators — those are direction-
|
|
376
|
+
* independent, so the direction computed below is only used to place it,
|
|
377
|
+
* never to pick which glyph to draw. See issue #330.
|
|
378
|
+
*/
|
|
379
|
+
function drawArrowHead(
|
|
380
|
+
graph: AsciiGraph,
|
|
381
|
+
lastLine: DrawingCoord[],
|
|
382
|
+
fallbackDir: Direction,
|
|
383
|
+
marker?: 'circle' | 'cross',
|
|
384
|
+
): Canvas {
|
|
385
|
+
const canvas = copyCanvas(graph.canvas)
|
|
386
|
+
if (lastLine.length === 0) return canvas
|
|
387
|
+
|
|
388
|
+
// Direction is derived from the *final step* into `lastPos` (its
|
|
389
|
+
// immediate predecessor in `lastLine`), not from `lastLine`'s first
|
|
390
|
+
// point. Those coincide for an ordinary straight segment, but
|
|
391
|
+
// `determinePath`'s Case-4 diagonal fallback (edge-routing.ts) can hand
|
|
392
|
+
// drawLine a single non-axis-aligned pair, which it then draws as an L
|
|
393
|
+
// (horizontal run, then vertical run — see draw-lines.ts) folded into
|
|
394
|
+
// one `lastLine` array. Using the first point there would span both
|
|
395
|
+
// legs and read as diagonal even when the actual approach into the
|
|
396
|
+
// arrowhead is a plain orthogonal step. See issue #1083.
|
|
397
|
+
const lastPos = lastLine[lastLine.length - 1]!
|
|
398
|
+
const from =
|
|
399
|
+
lastLine.length >= 2 ? lastLine[lastLine.length - 2]! : lastLine[0]!
|
|
400
|
+
let dir = determineDirection(from, lastPos)
|
|
401
|
+
if (lastLine.length === 1 || dirEquals(dir, Middle)) dir = fallbackDir
|
|
402
|
+
|
|
403
|
+
const markerChar = markerArrowChar(graph.config.useAscii, marker)
|
|
404
|
+
let char: string
|
|
405
|
+
if (markerChar !== undefined) {
|
|
406
|
+
char = markerChar
|
|
407
|
+
} else if (!graph.config.useAscii) {
|
|
408
|
+
const resolved = unicodeArrowChar(dir)
|
|
409
|
+
// `resolved` is only ever undefined when `dir` itself is `Middle` (or
|
|
410
|
+
// an unrecognized Direction) — which, given the reassignment above,
|
|
411
|
+
// only happens when `fallbackDir` is itself `Middle`. No current
|
|
412
|
+
// caller passes that; every drawArrow/draw-bundles.ts call site
|
|
413
|
+
// computes a real directional fallback. Kept as a safety net rather
|
|
414
|
+
// than a hard assumption.
|
|
415
|
+
/* v8 ignore else */
|
|
416
|
+
if (resolved !== undefined) {
|
|
417
|
+
char = resolved
|
|
418
|
+
} else {
|
|
419
|
+
char = unicodeArrowChar(fallbackDir) ?? '●'
|
|
420
|
+
}
|
|
421
|
+
} else {
|
|
422
|
+
const resolved = asciiArrowChar(dir)
|
|
423
|
+
// Same defensive-only fallback as the unicode-mode branch above,
|
|
424
|
+
// mirrored for ASCII mode.
|
|
425
|
+
/* v8 ignore else */
|
|
426
|
+
if (resolved !== undefined) {
|
|
427
|
+
char = resolved
|
|
428
|
+
} else {
|
|
429
|
+
char = asciiArrowChar(fallbackDir) ?? '*'
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
write(canvas, lastPos.x, lastPos.y, char)
|
|
434
|
+
return canvas
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Draw corner characters at path bends (where the direction changes).
|
|
439
|
+
* Uses ┌┐└┘ in Unicode mode, + in ASCII mode.
|
|
440
|
+
*/
|
|
441
|
+
function drawCorners(graph: AsciiGraph, path: GridCoord[]): Canvas {
|
|
442
|
+
const canvas = copyCanvas(graph.canvas)
|
|
443
|
+
|
|
444
|
+
for (let idx = 1; idx < path.length - 1; idx++) {
|
|
445
|
+
const coord = path[idx]!
|
|
446
|
+
const dc = gridToDrawingCoord(graph, coord)
|
|
447
|
+
const prevDir = determineDirection(path[idx - 1]!, coord)
|
|
448
|
+
const nextDir = determineDirection(coord, path[idx + 1]!)
|
|
449
|
+
|
|
450
|
+
let corner: string
|
|
451
|
+
if (!graph.config.useAscii) {
|
|
452
|
+
if (
|
|
453
|
+
(dirEquals(prevDir, Right) && dirEquals(nextDir, Down)) ||
|
|
454
|
+
(dirEquals(prevDir, Up) && dirEquals(nextDir, Left))
|
|
455
|
+
) {
|
|
456
|
+
corner = '┐'
|
|
457
|
+
} else if (
|
|
458
|
+
(dirEquals(prevDir, Right) && dirEquals(nextDir, Up)) ||
|
|
459
|
+
(dirEquals(prevDir, Down) && dirEquals(nextDir, Left))
|
|
460
|
+
) {
|
|
461
|
+
corner = '┘'
|
|
462
|
+
} else if (
|
|
463
|
+
(dirEquals(prevDir, Left) && dirEquals(nextDir, Down)) ||
|
|
464
|
+
(dirEquals(prevDir, Up) && dirEquals(nextDir, Right))
|
|
465
|
+
) {
|
|
466
|
+
corner = '┌'
|
|
467
|
+
} else if (
|
|
468
|
+
(dirEquals(prevDir, Left) && dirEquals(nextDir, Up)) ||
|
|
469
|
+
(dirEquals(prevDir, Down) && dirEquals(nextDir, Right))
|
|
470
|
+
) {
|
|
471
|
+
corner = '└'
|
|
472
|
+
} else {
|
|
473
|
+
corner = '+'
|
|
474
|
+
}
|
|
475
|
+
} else {
|
|
476
|
+
corner = '+'
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
write(canvas, dc.x, dc.y, corner)
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return canvas
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* True when `edge` has a sibling edge connecting the exact same two nodes in
|
|
487
|
+
* the opposite direction (`A --> B` alongside `B --> A`) — a "reciprocal
|
|
488
|
+
* pair". Node identity is compared by reference: `converter.ts` resolves
|
|
489
|
+
* every edge's `from`/`to` from the same shared node map, so the same
|
|
490
|
+
* logical node is always the same object across edges.
|
|
491
|
+
*/
|
|
492
|
+
function hasReciprocalPartner(graph: AsciiGraph, edge: AsciiEdge): boolean {
|
|
493
|
+
return graph.edges.some(
|
|
494
|
+
(other) =>
|
|
495
|
+
other !== edge && other.from === edge.to && other.to === edge.from,
|
|
496
|
+
)
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/** Draw edge label text centered on the widest path segment. */
|
|
500
|
+
function drawArrowLabel(graph: AsciiGraph, edge: AsciiEdge): Canvas {
|
|
501
|
+
const canvas = copyCanvas(graph.canvas)
|
|
502
|
+
if (edge.text.length === 0) return canvas
|
|
503
|
+
|
|
504
|
+
const drawingLine = lineToDrawing(graph, edge.labelLine)
|
|
505
|
+
|
|
506
|
+
// Determine if this is an upward edge (target is above source in the path)
|
|
507
|
+
// This is used to offset labels on bidirectional edges to prevent overlap
|
|
508
|
+
let isUpwardEdge: boolean | undefined
|
|
509
|
+
if (edge.path.length >= 2) {
|
|
510
|
+
const startY = edge.path[0]!.y
|
|
511
|
+
const endY = edge.path[edge.path.length - 1]!.y
|
|
512
|
+
// Edge goes up if end Y is less than start Y (smaller Y = higher on screen)
|
|
513
|
+
if (endY < startY) {
|
|
514
|
+
isUpwardEdge = true
|
|
515
|
+
} else if (endY > startY) {
|
|
516
|
+
isUpwardEdge = false
|
|
517
|
+
}
|
|
518
|
+
// If endY === startY, it's horizontal, leave isUpwardEdge undefined
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Only a genuine reciprocal pair (A-->B alongside B-->A) needs its label
|
|
522
|
+
// pulled toward its own target instead of its own source — see #530 and
|
|
523
|
+
// drawTextOnLine's doc comment below. A lone vertical edge keeps the
|
|
524
|
+
// original "precede the arrow, near the source" placement.
|
|
525
|
+
const pullTowardTarget = hasReciprocalPartner(graph, edge)
|
|
526
|
+
|
|
527
|
+
drawTextOnLine(canvas, drawingLine, edge.text, isUpwardEdge, pullTowardTarget)
|
|
528
|
+
return canvas
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Draw text centered on a line segment defined by two drawing coordinates.
|
|
533
|
+
* Supports multi-line labels.
|
|
534
|
+
*
|
|
535
|
+
* When isUpwardEdge is provided, offsets the label vertically to prevent
|
|
536
|
+
* overlapping with labels from edges going the opposite direction:
|
|
537
|
+
* - Upward edges: label placed in lower portion of segment (near its own
|
|
538
|
+
* source), unless `pullTowardTarget` is set — see below.
|
|
539
|
+
* - Downward edges (isUpwardEdge=false): label placed in upper portion
|
|
540
|
+
* (near its own source), unless `pullTowardTarget` is set.
|
|
541
|
+
* - No direction (isUpwardEdge=undefined): label centered (default)
|
|
542
|
+
*
|
|
543
|
+
* `pullTowardTarget` inverts both of the above, pulling the label toward
|
|
544
|
+
* its own arrowhead (the edge's target end) instead of its source. This
|
|
545
|
+
* only makes a visible difference for a genuine reciprocal pair sharing one
|
|
546
|
+
* vertical channel (`A --> B` alongside `B --> A`, both routed through the
|
|
547
|
+
* same column): pulling each label toward its own *source* there pulls it
|
|
548
|
+
* right next to the *other* edge's arrowhead instead, since in a two-node
|
|
549
|
+
* cycle one edge's source is the other edge's target. #530 is exactly that
|
|
550
|
+
* bug — a same-pair bidirectional edge's two labels rendered swapped
|
|
551
|
+
* relative to the arrowheads they sit beside. A lone edge (no reciprocal
|
|
552
|
+
* partner) keeps the original near-source placement so its label still
|
|
553
|
+
* reads as "preceding" its own arrow rather than crowding the arrowhead.
|
|
554
|
+
*/
|
|
555
|
+
function drawTextOnLine(
|
|
556
|
+
canvas: Canvas,
|
|
557
|
+
line: DrawingCoord[],
|
|
558
|
+
label: string,
|
|
559
|
+
isUpwardEdge?: boolean,
|
|
560
|
+
pullTowardTarget = false,
|
|
561
|
+
): void {
|
|
562
|
+
if (line.length < 2) return
|
|
563
|
+
const minX = Math.min(line[0]!.x, line[1]!.x)
|
|
564
|
+
const maxX = Math.max(line[0]!.x, line[1]!.x)
|
|
565
|
+
const minY = Math.min(line[0]!.y, line[1]!.y)
|
|
566
|
+
const maxY = Math.max(line[0]!.y, line[1]!.y)
|
|
567
|
+
const middleX = minX + Math.floor((maxX - minX) / 2)
|
|
568
|
+
let middleY = minY + Math.floor((maxY - minY) / 2)
|
|
569
|
+
|
|
570
|
+
// Offset label vertically to prevent overlap on bidirectional edges
|
|
571
|
+
// For vertical segments (same X), shift based on edge direction
|
|
572
|
+
if (isUpwardEdge !== undefined && minX === maxX) {
|
|
573
|
+
const segmentHeight = maxY - minY
|
|
574
|
+
const offset = Math.max(1, Math.floor(segmentHeight / 4))
|
|
575
|
+
// XOR: pullTowardTarget flips which portion each direction lands in.
|
|
576
|
+
const towardMinY = pullTowardTarget ? isUpwardEdge : !isUpwardEdge
|
|
577
|
+
if (towardMinY) {
|
|
578
|
+
middleY = middleY - offset
|
|
579
|
+
} else {
|
|
580
|
+
middleY = middleY + offset
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Support multi-line labels
|
|
585
|
+
const lines = splitLines(label)
|
|
586
|
+
const startY = middleY - Math.floor((lines.length - 1) / 2)
|
|
587
|
+
|
|
588
|
+
for (let i = 0; i < lines.length; i++) {
|
|
589
|
+
const lineText = lines[i]!
|
|
590
|
+
const startX = middleX - Math.floor(displayWidth(lineText) / 2)
|
|
591
|
+
drawText(canvas, { x: startX, y: startY + i }, lineText)
|
|
592
|
+
}
|
|
593
|
+
}
|