@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,611 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// ASCII renderer — bundled edge drawing (for parallel links, A & B --> C)
|
|
3
|
+
//
|
|
4
|
+
// Split out of draw.ts. Renders the fan-in/fan-out bundles computed by
|
|
5
|
+
// ./edge-bundling.ts (junction points, shared trunks) onto the canvas.
|
|
6
|
+
// ============================================================================
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
Canvas,
|
|
10
|
+
DrawingCoord,
|
|
11
|
+
Direction,
|
|
12
|
+
AsciiGraph,
|
|
13
|
+
AsciiNode,
|
|
14
|
+
AsciiEdge,
|
|
15
|
+
EdgeBundle,
|
|
16
|
+
} from './types.ts'
|
|
17
|
+
import {
|
|
18
|
+
Up,
|
|
19
|
+
Down,
|
|
20
|
+
Left,
|
|
21
|
+
Right,
|
|
22
|
+
drawingCoordEquals,
|
|
23
|
+
requireGridCoord,
|
|
24
|
+
} from './types.ts'
|
|
25
|
+
import { copyCanvas, write } from './canvas.ts'
|
|
26
|
+
import { determineDirection, dirEquals } from './edge-routing.ts'
|
|
27
|
+
import { gridToDrawingCoord } from './grid.ts'
|
|
28
|
+
import { getShapeAttachmentPoint } from './shapes/index.ts'
|
|
29
|
+
import type { ShapeDimensions } from './shapes/index.ts'
|
|
30
|
+
import { drawLine } from './draw-lines.ts'
|
|
31
|
+
import { markerArrowChar } from './draw-arrows.ts'
|
|
32
|
+
|
|
33
|
+
// ============================================================================
|
|
34
|
+
// Node attachment point helper
|
|
35
|
+
// ============================================================================
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Get the drawing coordinate where an edge attaches to a node's border.
|
|
39
|
+
* Uses grid-allocated dimensions so attachment points align with the actual
|
|
40
|
+
* drawn box (which may be wider/taller than the intrinsic shape dimensions
|
|
41
|
+
* when sharing a column/row with a larger node).
|
|
42
|
+
*/
|
|
43
|
+
function getNodeAttachmentPoint(
|
|
44
|
+
graph: AsciiGraph,
|
|
45
|
+
node: AsciiNode,
|
|
46
|
+
dir: Direction,
|
|
47
|
+
): DrawingCoord {
|
|
48
|
+
// Every node reaching bundled-edge drawing has already been placed by the
|
|
49
|
+
// grid layout pass (see grid.ts's createMapping) — requireGridCoord
|
|
50
|
+
// validates that rather than trusting it silently across the module
|
|
51
|
+
// boundary, since the type checker can't see it.
|
|
52
|
+
const gc = requireGridCoord(node)
|
|
53
|
+
|
|
54
|
+
// Calculate actual drawn dimensions from grid (matching drawBoxWithGridDimensions)
|
|
55
|
+
let w = 0
|
|
56
|
+
for (let i = 0; i < 2; i++) {
|
|
57
|
+
w += graph.columnWidth.get(gc.x + i) ?? 0
|
|
58
|
+
}
|
|
59
|
+
let h = 0
|
|
60
|
+
for (let i = 0; i < 2; i++) {
|
|
61
|
+
h += graph.rowHeight.get(gc.y + i) ?? 0
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Build dimensions matching the actual drawn box size
|
|
65
|
+
const gridDimensions: ShapeDimensions = {
|
|
66
|
+
width: w + 1,
|
|
67
|
+
height: h + 1,
|
|
68
|
+
labelArea: { x: 0, y: 0, width: 0, height: 0 },
|
|
69
|
+
gridColumns: [0, 0, 0],
|
|
70
|
+
gridRows: [0, 0, 0],
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const baseCoord = node.drawingCoord
|
|
74
|
+
if (baseCoord === null) {
|
|
75
|
+
// Same invariant as gridCoord above: drawingCoord is derived from
|
|
76
|
+
// gridCoord during layout, so it's set for every placed node.
|
|
77
|
+
/* v8 ignore next */
|
|
78
|
+
throw new Error(
|
|
79
|
+
`Node "${node.name}" has no drawingCoord; grid layout must run before bundled-edge drawing`,
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
return getShapeAttachmentPoint(node.shape, dir, gridDimensions, baseCoord)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Draw a single edge's segment in a bundle (source → junction for fan-in,
|
|
87
|
+
* junction → target for fan-out).
|
|
88
|
+
*
|
|
89
|
+
* Returns the same tuple format as drawArrow for consistency.
|
|
90
|
+
*/
|
|
91
|
+
export function drawBundledEdgeSegment(
|
|
92
|
+
graph: AsciiGraph,
|
|
93
|
+
edge: AsciiEdge,
|
|
94
|
+
bundle: EdgeBundle,
|
|
95
|
+
): [Canvas, Canvas, Canvas, Canvas, Canvas, Canvas] {
|
|
96
|
+
const empty = copyCanvas(graph.canvas)
|
|
97
|
+
|
|
98
|
+
// Captured as a local so its non-null, non-empty narrowing survives into
|
|
99
|
+
// the closures below — narrowing a property access like
|
|
100
|
+
// `edge.pathToJunction` does not persist inside a callback (the compiler
|
|
101
|
+
// can't rule out the callback mutating it), but narrowing a local const
|
|
102
|
+
// does.
|
|
103
|
+
const pathToJunction = edge.pathToJunction
|
|
104
|
+
if (!pathToJunction || pathToJunction.length === 0) {
|
|
105
|
+
return [empty, empty, empty, empty, empty, empty]
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Draw the path segment (pathToJunction)
|
|
109
|
+
const pathCanvas = copyCanvas(graph.canvas)
|
|
110
|
+
const useAscii = graph.config.useAscii
|
|
111
|
+
|
|
112
|
+
// Convert grid coords to drawing coords
|
|
113
|
+
// For fan-in: first point is at source node border (use attachment point)
|
|
114
|
+
// For fan-out: last point is at target node border (use attachment point)
|
|
115
|
+
const drawingPath = pathToJunction.map((gc, idx) => {
|
|
116
|
+
if (bundle.type === 'fan-in' && idx === 0) {
|
|
117
|
+
// First point: use source node's actual border position
|
|
118
|
+
return getNodeAttachmentPoint(graph, edge.from, edge.startDir)
|
|
119
|
+
}
|
|
120
|
+
if (bundle.type === 'fan-out' && idx === pathToJunction.length - 1) {
|
|
121
|
+
// Last point: use target node's actual border position
|
|
122
|
+
return getNodeAttachmentPoint(graph, edge.to, edge.endDir)
|
|
123
|
+
}
|
|
124
|
+
return gridToDrawingCoord(graph, gc)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
// Draw line segments
|
|
128
|
+
for (let i = 1; i < drawingPath.length; i++) {
|
|
129
|
+
const from = drawingPath[i - 1]!
|
|
130
|
+
const to = drawingPath[i]!
|
|
131
|
+
if (!drawingCoordEquals(from, to)) {
|
|
132
|
+
// Always skip both endpoints of every segment (offset 1, -1),
|
|
133
|
+
// matching non-bundled drawPath behavior. This leaves endpoint
|
|
134
|
+
// characters to corner/junction/boxStart canvases, preventing
|
|
135
|
+
// line characters from corrupting them via mergeJunctions.
|
|
136
|
+
drawLine(pathCanvas, from, to, 1, -1, useAscii, edge.style)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Draw corners at path bends
|
|
141
|
+
const cornersCanvas = copyCanvas(graph.canvas)
|
|
142
|
+
for (let idx = 1; idx < pathToJunction.length - 1; idx++) {
|
|
143
|
+
const coord = pathToJunction[idx]!
|
|
144
|
+
const dc = gridToDrawingCoord(graph, coord)
|
|
145
|
+
const prevDir = determineDirection(pathToJunction[idx - 1]!, coord)
|
|
146
|
+
const nextDir = determineDirection(coord, pathToJunction[idx + 1]!)
|
|
147
|
+
|
|
148
|
+
let corner: string
|
|
149
|
+
if (!useAscii) {
|
|
150
|
+
if (
|
|
151
|
+
(dirEquals(prevDir, Right) && dirEquals(nextDir, Down)) ||
|
|
152
|
+
(dirEquals(prevDir, Up) && dirEquals(nextDir, Left))
|
|
153
|
+
) {
|
|
154
|
+
corner = '┐'
|
|
155
|
+
} else if (
|
|
156
|
+
(dirEquals(prevDir, Right) && dirEquals(nextDir, Up)) ||
|
|
157
|
+
(dirEquals(prevDir, Down) && dirEquals(nextDir, Left))
|
|
158
|
+
) {
|
|
159
|
+
corner = '┘'
|
|
160
|
+
} else if (
|
|
161
|
+
(dirEquals(prevDir, Left) && dirEquals(nextDir, Down)) ||
|
|
162
|
+
(dirEquals(prevDir, Up) && dirEquals(nextDir, Right))
|
|
163
|
+
) {
|
|
164
|
+
corner = '┌'
|
|
165
|
+
} else if (
|
|
166
|
+
(dirEquals(prevDir, Left) && dirEquals(nextDir, Up)) ||
|
|
167
|
+
(dirEquals(prevDir, Down) && dirEquals(nextDir, Right))
|
|
168
|
+
) {
|
|
169
|
+
corner = '└'
|
|
170
|
+
} else {
|
|
171
|
+
corner = '+'
|
|
172
|
+
}
|
|
173
|
+
} else {
|
|
174
|
+
corner = '+'
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
write(cornersCanvas, dc.x, dc.y, corner)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Draw box start connector (for fan-in, from source node)
|
|
181
|
+
// The connector is placed at the first point coordinate (box border position)
|
|
182
|
+
// since we use offsets 1,-1 for drawLine, the line starts one step past this point
|
|
183
|
+
const boxStartCanvas = copyCanvas(graph.canvas)
|
|
184
|
+
if (bundle.type === 'fan-in' && pathToJunction.length >= 2) {
|
|
185
|
+
const firstPoint = drawingPath[0]!
|
|
186
|
+
const dir = determineDirection(pathToJunction[0]!, pathToJunction[1]!)
|
|
187
|
+
|
|
188
|
+
const junction = useAscii ? '+' : null
|
|
189
|
+
if (dirEquals(dir, Up))
|
|
190
|
+
write(boxStartCanvas, firstPoint.x, firstPoint.y, junction ?? '┴')
|
|
191
|
+
else if (dirEquals(dir, Down))
|
|
192
|
+
write(boxStartCanvas, firstPoint.x, firstPoint.y, junction ?? '┬')
|
|
193
|
+
else if (dirEquals(dir, Left))
|
|
194
|
+
write(boxStartCanvas, firstPoint.x, firstPoint.y, junction ?? '┤')
|
|
195
|
+
else if (dirEquals(dir, Right))
|
|
196
|
+
write(boxStartCanvas, firstPoint.x, firstPoint.y, junction ?? '├')
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Label canvas (bundled edges typically don't have labels, but handle it)
|
|
200
|
+
const labelCanvas = copyCanvas(graph.canvas)
|
|
201
|
+
|
|
202
|
+
return [pathCanvas, boxStartCanvas, empty, empty, cornersCanvas, labelCanvas]
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Draw the shared path segment of a bundle (junction → target for fan-in,
|
|
207
|
+
* source → junction for fan-out).
|
|
208
|
+
*/
|
|
209
|
+
export function drawBundleSharedPath(
|
|
210
|
+
graph: AsciiGraph,
|
|
211
|
+
bundle: EdgeBundle,
|
|
212
|
+
): [Canvas, Canvas] {
|
|
213
|
+
const pathCanvas = copyCanvas(graph.canvas)
|
|
214
|
+
const cornersCanvas = copyCanvas(graph.canvas)
|
|
215
|
+
|
|
216
|
+
if (bundle.sharedPath.length < 2) {
|
|
217
|
+
return [pathCanvas, cornersCanvas]
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const useAscii = graph.config.useAscii
|
|
221
|
+
const style = bundle.edges[0]?.style ?? 'solid'
|
|
222
|
+
const graphDir = graph.config.graphDirection
|
|
223
|
+
|
|
224
|
+
// Convert grid coords to drawing coords
|
|
225
|
+
// For fan-in: last point is at target node border
|
|
226
|
+
// For fan-out: first point is at source node border
|
|
227
|
+
const drawingPath = bundle.sharedPath.map((gc, idx) => {
|
|
228
|
+
if (bundle.type === 'fan-in' && idx === bundle.sharedPath.length - 1) {
|
|
229
|
+
// Last point: use target node's actual border position (entry from above/left)
|
|
230
|
+
const entryDir = graphDir === 'TD' ? Up : Left
|
|
231
|
+
return getNodeAttachmentPoint(graph, bundle.sharedNode, entryDir)
|
|
232
|
+
}
|
|
233
|
+
if (bundle.type === 'fan-out' && idx === 0) {
|
|
234
|
+
// First point: use source node's actual border position (exit going down/right)
|
|
235
|
+
const exitDir = graphDir === 'TD' ? Down : Right
|
|
236
|
+
return getNodeAttachmentPoint(graph, bundle.sharedNode, exitDir)
|
|
237
|
+
}
|
|
238
|
+
return gridToDrawingCoord(graph, gc)
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
// Draw line segments with appropriate offsets
|
|
242
|
+
for (let i = 1; i < drawingPath.length; i++) {
|
|
243
|
+
const from = drawingPath[i - 1]!
|
|
244
|
+
const to = drawingPath[i]!
|
|
245
|
+
if (!drawingCoordEquals(from, to)) {
|
|
246
|
+
// Always skip both endpoints (offset 1, -1), matching non-bundled drawPath.
|
|
247
|
+
drawLine(pathCanvas, from, to, 1, -1, useAscii, style)
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Draw corners at path bends
|
|
252
|
+
for (let idx = 1; idx < bundle.sharedPath.length - 1; idx++) {
|
|
253
|
+
const coord = bundle.sharedPath[idx]!
|
|
254
|
+
const dc = gridToDrawingCoord(graph, coord)
|
|
255
|
+
const prevDir = determineDirection(bundle.sharedPath[idx - 1]!, coord)
|
|
256
|
+
const nextDir = determineDirection(coord, bundle.sharedPath[idx + 1]!)
|
|
257
|
+
|
|
258
|
+
let corner: string
|
|
259
|
+
if (!useAscii) {
|
|
260
|
+
if (
|
|
261
|
+
(dirEquals(prevDir, Right) && dirEquals(nextDir, Down)) ||
|
|
262
|
+
(dirEquals(prevDir, Up) && dirEquals(nextDir, Left))
|
|
263
|
+
) {
|
|
264
|
+
corner = '┐'
|
|
265
|
+
} else if (
|
|
266
|
+
(dirEquals(prevDir, Right) && dirEquals(nextDir, Up)) ||
|
|
267
|
+
(dirEquals(prevDir, Down) && dirEquals(nextDir, Left))
|
|
268
|
+
) {
|
|
269
|
+
corner = '┘'
|
|
270
|
+
} else if (
|
|
271
|
+
(dirEquals(prevDir, Left) && dirEquals(nextDir, Down)) ||
|
|
272
|
+
(dirEquals(prevDir, Up) && dirEquals(nextDir, Right))
|
|
273
|
+
) {
|
|
274
|
+
corner = '┌'
|
|
275
|
+
} else if (
|
|
276
|
+
(dirEquals(prevDir, Left) && dirEquals(nextDir, Up)) ||
|
|
277
|
+
(dirEquals(prevDir, Down) && dirEquals(nextDir, Right))
|
|
278
|
+
) {
|
|
279
|
+
corner = '└'
|
|
280
|
+
} else {
|
|
281
|
+
corner = '+'
|
|
282
|
+
}
|
|
283
|
+
} else {
|
|
284
|
+
corner = '+'
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
write(cornersCanvas, dc.x, dc.y, corner)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return [pathCanvas, cornersCanvas]
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Draw the arrowhead for a fan-in bundle (single arrowhead at the shared target).
|
|
295
|
+
*/
|
|
296
|
+
export function drawBundleArrowhead(
|
|
297
|
+
graph: AsciiGraph,
|
|
298
|
+
bundle: EdgeBundle,
|
|
299
|
+
): Canvas {
|
|
300
|
+
const canvas = copyCanvas(graph.canvas)
|
|
301
|
+
|
|
302
|
+
if (bundle.sharedPath.length < 2) return canvas
|
|
303
|
+
|
|
304
|
+
// Get the last segment direction
|
|
305
|
+
const lastIdx = bundle.sharedPath.length - 1
|
|
306
|
+
const secondLast = bundle.sharedPath[lastIdx - 1]!
|
|
307
|
+
const last = bundle.sharedPath[lastIdx]!
|
|
308
|
+
const dir = determineDirection(secondLast, last)
|
|
309
|
+
|
|
310
|
+
// Get drawing coord 1 char outside the target node's border (not on the border itself).
|
|
311
|
+
// This matches non-bundled edges where drawPath uses offsetTo=-1 and the arrowhead
|
|
312
|
+
// sits at the last drawn point (1 char before the border).
|
|
313
|
+
const graphDir = graph.config.graphDirection
|
|
314
|
+
const entryDir = graphDir === 'TD' ? Up : Left
|
|
315
|
+
const dc = getNodeAttachmentPoint(graph, bundle.sharedNode, entryDir)
|
|
316
|
+
// Offset 1 char away from the box border so arrowhead sits outside the box
|
|
317
|
+
if (graphDir === 'TD') dc.y -= 1
|
|
318
|
+
else dc.x -= 1
|
|
319
|
+
|
|
320
|
+
// Draw arrowhead. A fan-in bundle merges several edges into one shared
|
|
321
|
+
// trunk/junction, so there's only one arrowhead to draw for all of them —
|
|
322
|
+
// it only gets a circle/cross glyph when every bundled edge agrees on the
|
|
323
|
+
// same marker; a mix (e.g. one `-->`, one `--o`) falls back to the plain
|
|
324
|
+
// directional arrowhead rather than guessing which edge's marker "wins".
|
|
325
|
+
// See issue #330.
|
|
326
|
+
const firstMarker = bundle.edges[0]?.endMarker
|
|
327
|
+
const sharedMarker = bundle.edges.every((e) => e.endMarker === firstMarker)
|
|
328
|
+
? firstMarker
|
|
329
|
+
: undefined
|
|
330
|
+
const markerChar = markerArrowChar(graph.config.useAscii, sharedMarker)
|
|
331
|
+
|
|
332
|
+
let char: string
|
|
333
|
+
if (markerChar !== undefined) {
|
|
334
|
+
char = markerChar
|
|
335
|
+
} else if (!graph.config.useAscii) {
|
|
336
|
+
if (dirEquals(dir, Up)) char = '▲'
|
|
337
|
+
else if (dirEquals(dir, Down)) char = '▼'
|
|
338
|
+
else if (dirEquals(dir, Left)) char = '◄'
|
|
339
|
+
else if (dirEquals(dir, Right)) char = '►'
|
|
340
|
+
else char = '▼' // default
|
|
341
|
+
} else {
|
|
342
|
+
if (dirEquals(dir, Up)) char = '^'
|
|
343
|
+
else if (dirEquals(dir, Down)) char = 'v'
|
|
344
|
+
else if (dirEquals(dir, Left)) char = '<'
|
|
345
|
+
else if (dirEquals(dir, Right)) char = '>'
|
|
346
|
+
else char = 'v' // default
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
write(canvas, dc.x, dc.y, char)
|
|
350
|
+
return canvas
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Draw the arrowhead for a single edge in a fan-out bundle.
|
|
355
|
+
*/
|
|
356
|
+
export function drawBundledEdgeArrowhead(
|
|
357
|
+
graph: AsciiGraph,
|
|
358
|
+
edge: AsciiEdge,
|
|
359
|
+
): Canvas {
|
|
360
|
+
const canvas = copyCanvas(graph.canvas)
|
|
361
|
+
|
|
362
|
+
if (!edge.pathToJunction || edge.pathToJunction.length < 2) return canvas
|
|
363
|
+
|
|
364
|
+
// Get the last segment direction
|
|
365
|
+
const lastIdx = edge.pathToJunction.length - 1
|
|
366
|
+
const secondLast = edge.pathToJunction[lastIdx - 1]!
|
|
367
|
+
const last = edge.pathToJunction[lastIdx]!
|
|
368
|
+
const dir = determineDirection(secondLast, last)
|
|
369
|
+
|
|
370
|
+
// Get drawing coord 1 char outside the target node's border
|
|
371
|
+
const graphDir = graph.config.graphDirection
|
|
372
|
+
const entryDir = graphDir === 'TD' ? Up : Left
|
|
373
|
+
const dc = getNodeAttachmentPoint(graph, edge.to, entryDir)
|
|
374
|
+
// Offset 1 char away from the box border so arrowhead sits outside the box
|
|
375
|
+
if (graphDir === 'TD') dc.y -= 1
|
|
376
|
+
else dc.x -= 1
|
|
377
|
+
|
|
378
|
+
// Draw arrowhead — this is a single edge (unlike the fan-in case above),
|
|
379
|
+
// so its own endMarker unambiguously picks the glyph. See issue #330.
|
|
380
|
+
const markerChar = markerArrowChar(graph.config.useAscii, edge.endMarker)
|
|
381
|
+
|
|
382
|
+
let char: string
|
|
383
|
+
if (markerChar !== undefined) {
|
|
384
|
+
char = markerChar
|
|
385
|
+
} else if (!graph.config.useAscii) {
|
|
386
|
+
if (dirEquals(dir, Up)) char = '▲'
|
|
387
|
+
else if (dirEquals(dir, Down)) char = '▼'
|
|
388
|
+
else if (dirEquals(dir, Left)) char = '◄'
|
|
389
|
+
else if (dirEquals(dir, Right)) char = '►'
|
|
390
|
+
else char = '▼' // default
|
|
391
|
+
} else {
|
|
392
|
+
if (dirEquals(dir, Up)) char = '^'
|
|
393
|
+
else if (dirEquals(dir, Down)) char = 'v'
|
|
394
|
+
else if (dirEquals(dir, Left)) char = '<'
|
|
395
|
+
else if (dirEquals(dir, Right)) char = '>'
|
|
396
|
+
else char = 'v' // default
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
write(canvas, dc.x, dc.y, char)
|
|
400
|
+
return canvas
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/** Reverse a cardinal direction (start arrowheads point back at the node they exit). */
|
|
404
|
+
function reverseDirection(dir: Direction): Direction {
|
|
405
|
+
if (dirEquals(dir, Up)) return Down
|
|
406
|
+
if (dirEquals(dir, Down)) return Up
|
|
407
|
+
if (dirEquals(dir, Left)) return Right
|
|
408
|
+
if (dirEquals(dir, Right)) return Left
|
|
409
|
+
return dir
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Draw the start-side arrowhead for a fan-out bundle (single arrowhead at
|
|
414
|
+
* the shared source, mirroring drawBundleArrowhead's shared-target end).
|
|
415
|
+
*/
|
|
416
|
+
export function drawBundleArrowheadStart(
|
|
417
|
+
graph: AsciiGraph,
|
|
418
|
+
bundle: EdgeBundle,
|
|
419
|
+
): Canvas {
|
|
420
|
+
const canvas = copyCanvas(graph.canvas)
|
|
421
|
+
|
|
422
|
+
if (bundle.sharedPath.length < 2) return canvas
|
|
423
|
+
|
|
424
|
+
// Get the first segment direction (source -> next point) and reverse it:
|
|
425
|
+
// the start arrowhead points back into the source, opposite the trunk's
|
|
426
|
+
// direction of travel.
|
|
427
|
+
const first = bundle.sharedPath[0]!
|
|
428
|
+
const second = bundle.sharedPath[1]!
|
|
429
|
+
const dir = reverseDirection(determineDirection(first, second))
|
|
430
|
+
|
|
431
|
+
// Get drawing coord 1 char outside the source node's border (not on the
|
|
432
|
+
// border itself), on the side the trunk exits from.
|
|
433
|
+
const graphDir = graph.config.graphDirection
|
|
434
|
+
const exitDir = graphDir === 'TD' ? Down : Right
|
|
435
|
+
const dc = getNodeAttachmentPoint(graph, bundle.sharedNode, exitDir)
|
|
436
|
+
// Offset 1 char away from the box border so arrowhead sits outside the box
|
|
437
|
+
if (graphDir === 'TD') dc.y += 1
|
|
438
|
+
else dc.x += 1
|
|
439
|
+
|
|
440
|
+
// Draw arrowhead
|
|
441
|
+
let char: string
|
|
442
|
+
if (!graph.config.useAscii) {
|
|
443
|
+
if (dirEquals(dir, Up)) char = '▲'
|
|
444
|
+
else if (dirEquals(dir, Down)) char = '▼'
|
|
445
|
+
else if (dirEquals(dir, Left)) char = '◄'
|
|
446
|
+
else if (dirEquals(dir, Right)) char = '►'
|
|
447
|
+
else char = '▲' // default
|
|
448
|
+
} else {
|
|
449
|
+
if (dirEquals(dir, Up)) char = '^'
|
|
450
|
+
else if (dirEquals(dir, Down)) char = 'v'
|
|
451
|
+
else if (dirEquals(dir, Left)) char = '<'
|
|
452
|
+
else if (dirEquals(dir, Right)) char = '>'
|
|
453
|
+
else char = '^' // default
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
write(canvas, dc.x, dc.y, char)
|
|
457
|
+
return canvas
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Draw the start-side arrowhead for a single edge in a fan-in bundle
|
|
462
|
+
* (mirroring drawBundledEdgeArrowhead's per-edge target end).
|
|
463
|
+
*/
|
|
464
|
+
export function drawBundledEdgeArrowheadStart(
|
|
465
|
+
graph: AsciiGraph,
|
|
466
|
+
edge: AsciiEdge,
|
|
467
|
+
): Canvas {
|
|
468
|
+
const canvas = copyCanvas(graph.canvas)
|
|
469
|
+
|
|
470
|
+
if (!edge.pathToJunction || edge.pathToJunction.length < 2) return canvas
|
|
471
|
+
|
|
472
|
+
// Get the first segment direction and reverse it: the start arrowhead
|
|
473
|
+
// points back into the edge's own source, opposite its direction of travel.
|
|
474
|
+
const first = edge.pathToJunction[0]!
|
|
475
|
+
const second = edge.pathToJunction[1]!
|
|
476
|
+
const dir = reverseDirection(determineDirection(first, second))
|
|
477
|
+
|
|
478
|
+
// Get drawing coord 1 char outside the source node's border, on the side
|
|
479
|
+
// this edge actually exits from.
|
|
480
|
+
const dc = getNodeAttachmentPoint(graph, edge.from, edge.startDir)
|
|
481
|
+
// Offset 1 char away from the box border so arrowhead sits outside the box
|
|
482
|
+
if (dirEquals(edge.startDir, Up)) dc.y -= 1
|
|
483
|
+
else if (dirEquals(edge.startDir, Down)) dc.y += 1
|
|
484
|
+
else if (dirEquals(edge.startDir, Left)) dc.x -= 1
|
|
485
|
+
else if (dirEquals(edge.startDir, Right)) dc.x += 1
|
|
486
|
+
|
|
487
|
+
// Draw arrowhead
|
|
488
|
+
let char: string
|
|
489
|
+
if (!graph.config.useAscii) {
|
|
490
|
+
if (dirEquals(dir, Up)) char = '▲'
|
|
491
|
+
else if (dirEquals(dir, Down)) char = '▼'
|
|
492
|
+
else if (dirEquals(dir, Left)) char = '◄'
|
|
493
|
+
else if (dirEquals(dir, Right)) char = '►'
|
|
494
|
+
else char = '▼' // default
|
|
495
|
+
} else {
|
|
496
|
+
if (dirEquals(dir, Up)) char = '^'
|
|
497
|
+
else if (dirEquals(dir, Down)) char = 'v'
|
|
498
|
+
else if (dirEquals(dir, Left)) char = '<'
|
|
499
|
+
else if (dirEquals(dir, Right)) char = '>'
|
|
500
|
+
else char = 'v' // default
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
write(canvas, dc.x, dc.y, char)
|
|
504
|
+
return canvas
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Draw the junction character where bundled edges merge/split.
|
|
509
|
+
*
|
|
510
|
+
* Analyzes actual connecting directions to choose the correct character:
|
|
511
|
+
* - ┼ (cross): lines from all 4 directions
|
|
512
|
+
* - ┬ (T down): lines from left, right, and down
|
|
513
|
+
* - ┴ (T up): lines from left, right, and up
|
|
514
|
+
* - ├ (T right): lines from up, down, and right
|
|
515
|
+
* - ┤ (T left): lines from up, down, and left
|
|
516
|
+
*/
|
|
517
|
+
export function drawJunctionCharacter(
|
|
518
|
+
graph: AsciiGraph,
|
|
519
|
+
bundle: EdgeBundle,
|
|
520
|
+
): Canvas {
|
|
521
|
+
const canvas = copyCanvas(graph.canvas)
|
|
522
|
+
|
|
523
|
+
if (!bundle.junctionPoint) return canvas
|
|
524
|
+
|
|
525
|
+
const dc = gridToDrawingCoord(graph, bundle.junctionPoint)
|
|
526
|
+
const useAscii = graph.config.useAscii
|
|
527
|
+
|
|
528
|
+
// Analyze what directions actually connect to the junction
|
|
529
|
+
let hasUp = false
|
|
530
|
+
let hasDown = false
|
|
531
|
+
let hasLeft = false
|
|
532
|
+
let hasRight = false
|
|
533
|
+
|
|
534
|
+
// Check shared path direction (where the line continues to/from the shared node)
|
|
535
|
+
if (bundle.sharedPath.length >= 2) {
|
|
536
|
+
// For fan-in: shared path goes FROM junction TO target (index 0 is junction)
|
|
537
|
+
// For fan-out: shared path goes FROM source TO junction (last index is junction)
|
|
538
|
+
const junctionIdx =
|
|
539
|
+
bundle.type === 'fan-in' ? 0 : bundle.sharedPath.length - 1
|
|
540
|
+
const adjacentIdx =
|
|
541
|
+
bundle.type === 'fan-in' ? 1 : bundle.sharedPath.length - 2
|
|
542
|
+
const sharedDir = determineDirection(
|
|
543
|
+
bundle.sharedPath[junctionIdx]!,
|
|
544
|
+
bundle.sharedPath[adjacentIdx]!,
|
|
545
|
+
)
|
|
546
|
+
// This is the direction the shared path GOES from junction
|
|
547
|
+
if (dirEquals(sharedDir, Down)) hasDown = true
|
|
548
|
+
else if (dirEquals(sharedDir, Up)) hasUp = true
|
|
549
|
+
else if (dirEquals(sharedDir, Right)) hasRight = true
|
|
550
|
+
else if (dirEquals(sharedDir, Left)) hasLeft = true
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Check each edge's path direction at the junction
|
|
554
|
+
for (const edge of bundle.edges) {
|
|
555
|
+
if (edge.pathToJunction && edge.pathToJunction.length >= 2) {
|
|
556
|
+
// For fan-in: pathToJunction goes FROM source TO junction (last is junction)
|
|
557
|
+
// For fan-out: pathToJunction goes FROM junction TO target (first is junction)
|
|
558
|
+
const junctionIdx =
|
|
559
|
+
bundle.type === 'fan-in' ? edge.pathToJunction.length - 1 : 0
|
|
560
|
+
const adjacentIdx =
|
|
561
|
+
bundle.type === 'fan-in' ? edge.pathToJunction.length - 2 : 1
|
|
562
|
+
|
|
563
|
+
const arrivalDir = determineDirection(
|
|
564
|
+
edge.pathToJunction[adjacentIdx]!,
|
|
565
|
+
edge.pathToJunction[junctionIdx]!,
|
|
566
|
+
)
|
|
567
|
+
// This is the direction the edge ARRIVES at junction from
|
|
568
|
+
// e.g., if arrivalDir is Right, the line comes FROM the left
|
|
569
|
+
if (dirEquals(arrivalDir, Down))
|
|
570
|
+
hasUp = true // arrived going down = came from up
|
|
571
|
+
else if (dirEquals(arrivalDir, Up)) hasDown = true
|
|
572
|
+
else if (dirEquals(arrivalDir, Right)) hasLeft = true
|
|
573
|
+
else if (dirEquals(arrivalDir, Left)) hasRight = true
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Select character based on connected directions
|
|
578
|
+
let char: string
|
|
579
|
+
if (!useAscii) {
|
|
580
|
+
if (hasUp && hasDown && hasLeft && hasRight) {
|
|
581
|
+
char = '┼' // cross - all 4 directions
|
|
582
|
+
} else if (hasDown && hasLeft && hasRight && !hasUp) {
|
|
583
|
+
char = '┬' // T pointing down
|
|
584
|
+
} else if (hasUp && hasLeft && hasRight && !hasDown) {
|
|
585
|
+
char = '┴' // T pointing up
|
|
586
|
+
} else if (hasUp && hasDown && hasRight && !hasLeft) {
|
|
587
|
+
char = '├' // T pointing right
|
|
588
|
+
} else if (hasUp && hasDown && hasLeft && !hasRight) {
|
|
589
|
+
char = '┤' // T pointing left
|
|
590
|
+
} else if (hasLeft && hasRight) {
|
|
591
|
+
char = '─' // horizontal only
|
|
592
|
+
} else if (hasUp && hasDown) {
|
|
593
|
+
char = '│' // vertical only
|
|
594
|
+
} else if (hasDown && hasRight) {
|
|
595
|
+
char = '┌' // corner
|
|
596
|
+
} else if (hasDown && hasLeft) {
|
|
597
|
+
char = '┐'
|
|
598
|
+
} else if (hasUp && hasRight) {
|
|
599
|
+
char = '└'
|
|
600
|
+
} else if (hasUp && hasLeft) {
|
|
601
|
+
char = '┘'
|
|
602
|
+
} else {
|
|
603
|
+
char = '┼' // fallback
|
|
604
|
+
}
|
|
605
|
+
} else {
|
|
606
|
+
char = '+'
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
write(canvas, dc.x, dc.y, char)
|
|
610
|
+
return canvas
|
|
611
|
+
}
|