agentic-mermaid 0.1.1 → 0.1.2

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.
Files changed (336) hide show
  1. package/CHANGELOG.md +144 -4
  2. package/LICENSES/Apache-2.0.txt +202 -0
  3. package/README.md +25 -32
  4. package/THIRD_PARTY_NOTICES.md +29 -0
  5. package/dist/agent-core.d.ts +463 -0
  6. package/dist/agent-core.js +397 -0
  7. package/dist/agent.d.ts +73 -1868
  8. package/dist/agent.js +336 -37325
  9. package/dist/agentic-mermaid-mcp.js +7 -38399
  10. package/dist/am.js +33632 -40445
  11. package/dist/chunk-4QECACIA.js +4702 -0
  12. package/dist/chunk-CDPKJ2BQ.js +37 -0
  13. package/dist/chunk-QKDLMOJJ.js +3487 -0
  14. package/dist/chunk-WI35L7IR.js +615 -0
  15. package/dist/chunk-WN3YDOBG.js +149 -0
  16. package/dist/chunk-ZYMMKQ3I.js +62637 -0
  17. package/dist/index-D54dqSdC.d.ts +6777 -0
  18. package/dist/index.d.ts +1 -1
  19. package/dist/index.js +310 -33014
  20. package/dist/mcp-cli-SBQSTJFC.js +17 -0
  21. package/docs/README.md +14 -6
  22. package/docs/schemas/style-spec.schema.json +1221 -30
  23. package/examples/styles/catalog.json +54 -0
  24. package/examples/styles/cloudflare-workers-inspired-prototype.mmd +9 -0
  25. package/examples/styles/cloudflare-workers-inspired-prototype.style.json +110 -0
  26. package/examples/styles/cupertino-prototype.mmd +10 -0
  27. package/examples/styles/cupertino-prototype.style.json +63 -0
  28. package/examples/styles/mid-century-report.style.json +2 -2
  29. package/examples/styles/star-chart-atlas.style.json +2 -1
  30. package/examples/styles/transit-route-map.style.json +2 -2
  31. package/examples/styles/vercel-inspired-prototype.mmd +6 -0
  32. package/examples/styles/vercel-inspired-prototype.style.json +87 -0
  33. package/package.json +68 -43
  34. package/server.json +2 -2
  35. package/skills/agentic-mermaid-diagram-workflow/SKILL.md +17 -36
  36. package/skills/agentic-mermaid-diagram-workflow/references/cli.md +4 -5
  37. package/skills/agentic-mermaid-diagram-workflow/references/code-mode.md +15 -31
  38. package/skills/agentic-mermaid-diagram-workflow/references/sequence.md +10 -14
  39. package/skills/agentic-mermaid-live-editor/SKILL.md +100 -206
  40. package/AGENT_NATIVE.md +0 -607
  41. package/Instructions_for_agents.md +0 -89
  42. package/TODO.md +0 -684
  43. package/assets/hero.png +0 -0
  44. package/dist/index-wjRjbsP-.d.ts +0 -1102
  45. package/docs/agent-api-cookbook.md +0 -308
  46. package/docs/agent-mutation-policy.md +0 -104
  47. package/docs/agent-workflow-examples.md +0 -46
  48. package/docs/api.md +0 -211
  49. package/docs/ascii.md +0 -103
  50. package/docs/assets/improvements/box-start-connector.png +0 -0
  51. package/docs/assets/improvements/comment-dropped-lint.png +0 -0
  52. package/docs/assets/improvements/er-content-loss.png +0 -0
  53. package/docs/assets/improvements/fan-in-trunks.png +0 -0
  54. package/docs/assets/improvements/gantt-family.png +0 -0
  55. package/docs/assets/improvements/gantt-milestone-clamp.png +0 -0
  56. package/docs/assets/improvements/gantt-verify-seam.png +0 -0
  57. package/docs/assets/improvements/subgraph-container-edges.png +0 -0
  58. package/docs/assets/improvements/wrapper-frontmatter.png +0 -0
  59. package/docs/assets/improvements/wrapper-init-directive.png +0 -0
  60. package/docs/assets/improvements/wrapper-leading-comment.png +0 -0
  61. package/docs/assets/style-cookbook/mid-century-report.png +0 -0
  62. package/docs/assets/style-cookbook/star-chart-atlas.png +0 -0
  63. package/docs/assets/style-cookbook/transit-route-map.png +0 -0
  64. package/docs/comparison.md +0 -94
  65. package/docs/config.md +0 -105
  66. package/docs/contributing/adding-diagram-types.md +0 -144
  67. package/docs/contributing/diagram-family-citizenship.matrix.json +0 -2127
  68. package/docs/contributing/diagram-family-citizenship.md +0 -82
  69. package/docs/contributing/harvesting-upstream-tests.md +0 -126
  70. package/docs/contributing/lessons-learned.md +0 -276
  71. package/docs/contributing/releasing.md +0 -82
  72. package/docs/contributing/visual-review-evidence.md +0 -53
  73. package/docs/custom-style-cookbook.md +0 -154
  74. package/docs/design/families/README.md +0 -26
  75. package/docs/design/families/architecture-beta.md +0 -48
  76. package/docs/design/families/flowchart-parser-conformance.md +0 -22
  77. package/docs/design/families/gantt-research.md +0 -137
  78. package/docs/design/families/gantt.md +0 -329
  79. package/docs/design/families/journey-agentic-cards-before.png +0 -0
  80. package/docs/design/families/journey-experience-curve-implementation.png +0 -0
  81. package/docs/design/families/journey-mermaid-classic-implementation.png +0 -0
  82. package/docs/design/families/journey-mermaid-classic-prototype.png +0 -0
  83. package/docs/design/families/journey-mermaid-classic-prototype.svg +0 -163
  84. package/docs/design/families/journey-mermaid-classic-prototype.svg.png +0 -0
  85. package/docs/design/families/journey-migration-parity.md +0 -439
  86. package/docs/design/families/journey-section-overlap-after.png +0 -0
  87. package/docs/design/families/journey-section-overlap-before.png +0 -0
  88. package/docs/design/families/journey-usage-research.md +0 -423
  89. package/docs/design/families/journey.md +0 -153
  90. package/docs/design/families/xychart.md +0 -85
  91. package/docs/design/system/README.md +0 -45
  92. package/docs/design/system/abstraction-audit.md +0 -224
  93. package/docs/design/system/abstraction-recommendations.md +0 -655
  94. package/docs/design/system/architecture.mmd +0 -29
  95. package/docs/design/system/architecture.svg +0 -137
  96. package/docs/design/system/layout-guarantees-and-robustness.md +0 -351
  97. package/docs/design/system/layout-rubric.md +0 -138
  98. package/docs/design/system/route-contracts.md +0 -848
  99. package/docs/design/system/source-preservation-ladder.md +0 -52
  100. package/docs/design/system/styles-rollout.md +0 -216
  101. package/docs/design/system/ugly-layouts.md +0 -119
  102. package/docs/diagram-families.md +0 -193
  103. package/docs/features.md +0 -147
  104. package/docs/fork-differences.md +0 -130
  105. package/docs/getting-started.md +0 -175
  106. package/docs/issue-derived-test-cases.md +0 -156
  107. package/docs/layout-characterization/README.md +0 -245
  108. package/docs/layout-characterization/contact-sheet-families.md +0 -498
  109. package/docs/layout-characterization/contact-sheet.md +0 -638
  110. package/docs/layout-characterization/properties.md +0 -281
  111. package/docs/layout-characterization/visual-quality.md +0 -160
  112. package/docs/layout-characterization/visual-snapshots/architecture.svg +0 -79
  113. package/docs/layout-characterization/visual-snapshots/class.svg +0 -60
  114. package/docs/layout-characterization/visual-snapshots/er.svg +0 -61
  115. package/docs/layout-characterization/visual-snapshots/flowchart.svg +0 -58
  116. package/docs/layout-characterization/visual-snapshots/gantt.svg +0 -52
  117. package/docs/layout-characterization/visual-snapshots/journey.svg +0 -98
  118. package/docs/layout-characterization/visual-snapshots/pie.svg +0 -36
  119. package/docs/layout-characterization/visual-snapshots/quadrant.svg +0 -47
  120. package/docs/layout-characterization/visual-snapshots/sequence.svg +0 -59
  121. package/docs/layout-characterization/visual-snapshots/state.svg +0 -52
  122. package/docs/layout-characterization/visual-snapshots/timeline.svg +0 -64
  123. package/docs/layout-characterization/visual-snapshots/xychart.svg +0 -97
  124. package/docs/mcp-code-mode-rationale.md +0 -101
  125. package/docs/mcp-http-transport.md +0 -223
  126. package/docs/mermaid-layout-complaints.md +0 -837
  127. package/docs/mutation-testing.md +0 -206
  128. package/docs/project/divergences.md +0 -98
  129. package/docs/project/dogfooding-docs-strategy.md +0 -66
  130. package/docs/project/hosted-mcp-cloudflare-plan.md +0 -338
  131. package/docs/project/lessons-learned.md +0 -1019
  132. package/docs/project/remove-role-styling-plan.md +0 -176
  133. package/docs/quality.md +0 -215
  134. package/docs/react.md +0 -97
  135. package/docs/style-authoring.md +0 -133
  136. package/docs/testing-strategy.md +0 -335
  137. package/docs/theming.md +0 -162
  138. package/examples/agent-improve-auth-flow.ts +0 -206
  139. package/examples/agent-loop.ts +0 -53
  140. package/examples/mcp-vs-cli-complex-diagrams.ts +0 -133
  141. package/llms.txt +0 -104
  142. package/skill-evals/README.md +0 -77
  143. package/skill-evals/fixtures/architecture-cache/input.mmd +0 -5
  144. package/skill-evals/fixtures/class-payment/input.mmd +0 -4
  145. package/skill-evals/fixtures/er-orders/input.mmd +0 -10
  146. package/skill-evals/fixtures/flowchart-cache/input.mmd +0 -3
  147. package/skill-evals/fixtures/gantt-release/input.mmd +0 -7
  148. package/skill-evals/fixtures/journey-checkout/input.mmd +0 -6
  149. package/skill-evals/fixtures/pie-exports/input.mmd +0 -5
  150. package/skill-evals/fixtures/quadrant-priorities/input.mmd +0 -11
  151. package/skill-evals/fixtures/sequence-retry/input.mmd +0 -8
  152. package/skill-evals/fixtures/state-lifecycle/input.mmd +0 -5
  153. package/skill-evals/fixtures/timeline-release/input.mmd +0 -6
  154. package/skill-evals/fixtures/xychart-growth/input.mmd +0 -5
  155. package/skill-evals/shared-benchmark.json +0 -510
  156. package/src/agent/analyze.ts +0 -157
  157. package/src/agent/apply.ts +0 -146
  158. package/src/agent/architecture-body.ts +0 -458
  159. package/src/agent/certificates.ts +0 -110
  160. package/src/agent/class-body.ts +0 -319
  161. package/src/agent/core.ts +0 -207
  162. package/src/agent/create.ts +0 -106
  163. package/src/agent/describe.ts +0 -368
  164. package/src/agent/er-body.ts +0 -224
  165. package/src/agent/facts.ts +0 -360
  166. package/src/agent/families-builtin.ts +0 -920
  167. package/src/agent/families.ts +0 -232
  168. package/src/agent/family-layouts.ts +0 -832
  169. package/src/agent/flowchart-body.ts +0 -372
  170. package/src/agent/flowchart-unsupported.ts +0 -243
  171. package/src/agent/font-coverage.ts +0 -288
  172. package/src/agent/gantt-body.ts +0 -495
  173. package/src/agent/index.ts +0 -10
  174. package/src/agent/journey-body.ts +0 -374
  175. package/src/agent/label-metrics.ts +0 -28
  176. package/src/agent/layout-to-rendered.ts +0 -63
  177. package/src/agent/mutate.ts +0 -97
  178. package/src/agent/mutation-ops.ts +0 -51
  179. package/src/agent/op-schema.ts +0 -443
  180. package/src/agent/parse.ts +0 -223
  181. package/src/agent/pie-body.ts +0 -270
  182. package/src/agent/png.ts +0 -171
  183. package/src/agent/quadrant-body.ts +0 -336
  184. package/src/agent/quality.ts +0 -363
  185. package/src/agent/readability-audit.ts +0 -92
  186. package/src/agent/rendered-route-audit.ts +0 -61
  187. package/src/agent/sequence-body.ts +0 -345
  188. package/src/agent/serialize.ts +0 -214
  189. package/src/agent/state-body.ts +0 -589
  190. package/src/agent/structural-count.ts +0 -150
  191. package/src/agent/timeline-body.ts +0 -280
  192. package/src/agent/trace-log.ts +0 -43
  193. package/src/agent/types.ts +0 -995
  194. package/src/agent/verify.ts +0 -603
  195. package/src/agent/xychart-body.ts +0 -472
  196. package/src/architecture/config.ts +0 -282
  197. package/src/architecture/layout.ts +0 -496
  198. package/src/architecture/parser.ts +0 -375
  199. package/src/architecture/renderer.ts +0 -611
  200. package/src/architecture/types.ts +0 -113
  201. package/src/ascii/ansi.ts +0 -423
  202. package/src/ascii/architecture.ts +0 -210
  203. package/src/ascii/canvas.ts +0 -443
  204. package/src/ascii/class-diagram.ts +0 -697
  205. package/src/ascii/converter.ts +0 -475
  206. package/src/ascii/draw.ts +0 -1693
  207. package/src/ascii/edge-bundling.ts +0 -328
  208. package/src/ascii/edge-routing.ts +0 -330
  209. package/src/ascii/er-diagram.ts +0 -439
  210. package/src/ascii/gantt.ts +0 -276
  211. package/src/ascii/grid.ts +0 -747
  212. package/src/ascii/index.ts +0 -196
  213. package/src/ascii/journey.ts +0 -169
  214. package/src/ascii/meta.ts +0 -244
  215. package/src/ascii/multiline-utils.ts +0 -80
  216. package/src/ascii/pathfinder.ts +0 -301
  217. package/src/ascii/pie.ts +0 -67
  218. package/src/ascii/quadrant.ts +0 -158
  219. package/src/ascii/reverse.ts +0 -216
  220. package/src/ascii/sequence.ts +0 -499
  221. package/src/ascii/shapes/circle.ts +0 -27
  222. package/src/ascii/shapes/corners.ts +0 -143
  223. package/src/ascii/shapes/diamond.ts +0 -27
  224. package/src/ascii/shapes/hexagon.ts +0 -27
  225. package/src/ascii/shapes/index.ts +0 -106
  226. package/src/ascii/shapes/rectangle.ts +0 -168
  227. package/src/ascii/shapes/rounded.ts +0 -27
  228. package/src/ascii/shapes/special.ts +0 -332
  229. package/src/ascii/shapes/stadium.ts +0 -110
  230. package/src/ascii/shapes/state.ts +0 -192
  231. package/src/ascii/shapes/types.ts +0 -73
  232. package/src/ascii/timeline.ts +0 -141
  233. package/src/ascii/types.ts +0 -312
  234. package/src/ascii/validate.ts +0 -120
  235. package/src/ascii/width.ts +0 -81
  236. package/src/ascii/wrap.ts +0 -131
  237. package/src/ascii/xychart.ts +0 -919
  238. package/src/browser.ts +0 -29
  239. package/src/class/layout.ts +0 -240
  240. package/src/class/parser.ts +0 -332
  241. package/src/class/renderer.ts +0 -638
  242. package/src/class/types.ts +0 -129
  243. package/src/cli/agent-instructions.ts +0 -1
  244. package/src/cli/am-bin.ts +0 -6
  245. package/src/cli/exit-codes.ts +0 -18
  246. package/src/cli/index.ts +0 -1132
  247. package/src/cli/init-agent.ts +0 -147
  248. package/src/cli/run-entrypoint.ts +0 -10
  249. package/src/color-resolver.ts +0 -152
  250. package/src/elk-instance.ts +0 -114
  251. package/src/er/layout.ts +0 -187
  252. package/src/er/parser.ts +0 -226
  253. package/src/er/renderer.ts +0 -792
  254. package/src/er/types.ts +0 -99
  255. package/src/family-rubric.ts +0 -425
  256. package/src/font-manifest.ts +0 -31
  257. package/src/gantt/layout.ts +0 -365
  258. package/src/gantt/parser.ts +0 -282
  259. package/src/gantt/pipeline.ts +0 -27
  260. package/src/gantt/renderer.ts +0 -414
  261. package/src/gantt/schedule.ts +0 -513
  262. package/src/gantt/types.ts +0 -255
  263. package/src/index.ts +0 -368
  264. package/src/journey/layout.ts +0 -554
  265. package/src/journey/parse-core.ts +0 -251
  266. package/src/journey/parser.ts +0 -101
  267. package/src/journey/renderer.ts +0 -917
  268. package/src/journey/types.ts +0 -144
  269. package/src/layout/geometry.ts +0 -182
  270. package/src/layout/pass.ts +0 -142
  271. package/src/layout/passes/index.ts +0 -3916
  272. package/src/layout-engine.ts +0 -1565
  273. package/src/layout-rubric.ts +0 -659
  274. package/src/mcp/artifacts.ts +0 -137
  275. package/src/mcp/dynamic-harness.ts +0 -31
  276. package/src/mcp/dynamic-modules.d.ts +0 -7
  277. package/src/mcp/facade.ts +0 -608
  278. package/src/mcp/harness-runtime.ts +0 -203
  279. package/src/mcp/hosted-server.ts +0 -468
  280. package/src/mcp/mcp-bin.ts +0 -3
  281. package/src/mcp/mcp-cli.ts +0 -82
  282. package/src/mcp/protocol.ts +0 -13
  283. package/src/mcp/sandbox.ts +0 -195
  284. package/src/mcp/sdk-decl.ts +0 -391
  285. package/src/mcp/server.ts +0 -362
  286. package/src/mcp/tool-surface.ts +0 -146
  287. package/src/mermaid-source.ts +0 -655
  288. package/src/multiline-utils.ts +0 -228
  289. package/src/parser.ts +0 -1031
  290. package/src/pie/layout.ts +0 -168
  291. package/src/pie/parser.ts +0 -136
  292. package/src/pie/renderer.ts +0 -234
  293. package/src/pie/types.ts +0 -82
  294. package/src/quadrant/layout.ts +0 -191
  295. package/src/quadrant/parser.ts +0 -183
  296. package/src/quadrant/renderer.ts +0 -322
  297. package/src/quadrant/types.ts +0 -113
  298. package/src/render-family-hooks.ts +0 -244
  299. package/src/renderer.ts +0 -1099
  300. package/src/route-contracts.ts +0 -2723
  301. package/src/scene/backend.ts +0 -76
  302. package/src/scene/fidelity.ts +0 -183
  303. package/src/scene/hybrid-backend.ts +0 -174
  304. package/src/scene/ir.ts +0 -181
  305. package/src/scene/marks.ts +0 -127
  306. package/src/scene/rough-backend.ts +0 -378
  307. package/src/scene/seed.ts +0 -46
  308. package/src/scene/style-registry.ts +0 -556
  309. package/src/sequence/layout.ts +0 -456
  310. package/src/sequence/parser.ts +0 -255
  311. package/src/sequence/renderer.ts +0 -604
  312. package/src/sequence/types.ts +0 -154
  313. package/src/shape-clipping.ts +0 -351
  314. package/src/shared/accessibility-directives.ts +0 -22
  315. package/src/shared/batched.ts +0 -61
  316. package/src/shared/color-math.ts +0 -93
  317. package/src/shared/svg-a11y.ts +0 -29
  318. package/src/shared/syntax-error.ts +0 -17
  319. package/src/shared/unicode-ranges.ts +0 -135
  320. package/src/styles.ts +0 -318
  321. package/src/svg-paths.ts +0 -28
  322. package/src/text-metrics.ts +0 -248
  323. package/src/theme.ts +0 -710
  324. package/src/timeline/layout.ts +0 -381
  325. package/src/timeline/parser.ts +0 -215
  326. package/src/timeline/renderer.ts +0 -538
  327. package/src/timeline/types.ts +0 -105
  328. package/src/types.ts +0 -462
  329. package/src/website-worker-modules.d.ts +0 -18
  330. package/src/xychart/axis-utils.ts +0 -109
  331. package/src/xychart/colors.ts +0 -124
  332. package/src/xychart/config.ts +0 -88
  333. package/src/xychart/layout.ts +0 -656
  334. package/src/xychart/parser.ts +0 -504
  335. package/src/xychart/renderer.ts +0 -640
  336. package/src/xychart/types.ts +0 -282
package/src/ascii/draw.ts DELETED
@@ -1,1693 +0,0 @@
1
- // ============================================================================
2
- // ASCII renderer — drawing operations
3
- //
4
- // Ported from AlexanderGrooff/mermaid-ascii cmd/draw.go + cmd/arrow.go.
5
- // Contains all visual rendering: boxes, lines, arrows, corners,
6
- // subgraphs, labels, and the top-level draw orchestrator.
7
- // ============================================================================
8
-
9
- import type {
10
- Canvas, DrawingCoord, GridCoord, Direction,
11
- AsciiGraph, AsciiNode, AsciiEdge, AsciiSubgraph, AsciiEdgeStyle, EdgeBundle, EdgeMarker,
12
- } from './types.ts'
13
- import {
14
- Up, Down, Left, Right, UpperLeft, UpperRight, LowerLeft, LowerRight, Middle,
15
- drawingCoordEquals,
16
- } from './types.ts'
17
- import { mkCanvas, copyCanvas, getCanvasSize, mergeCanvases, drawText, mkRoleCanvas, setRole, mergeRoleCanvases } from './canvas.ts'
18
- import type { RoleCanvas, CharRole } from './types.ts'
19
- import { determineDirection, dirEquals } from './edge-routing.ts'
20
- import { gridToDrawingCoord, lineToDrawing } from './grid.ts'
21
- import { splitLines } from './multiline-utils.ts'
22
- import { visualWidth } from './width.ts'
23
- import { getCorners } from './shapes/corners.ts'
24
- import { getShapeAttachmentPoint } from './shapes/index.ts'
25
-
26
- // ============================================================================
27
- // Node drawing — renders a node using shape-aware rendering
28
- // ============================================================================
29
-
30
- /**
31
- * Draw a node using its shape type.
32
- * Returns a standalone canvas containing the rendered shape.
33
- *
34
- * For basic shapes (rectangle, rounded), uses grid-determined dimensions
35
- * to ensure consistent sizing across nodes in the same column.
36
- * For special shapes (diamond, circle, state pseudo-states, etc.),
37
- * uses shape-specific dimension calculation but centers the content
38
- * within the grid cell dimensions to ensure proper vertical alignment.
39
- */
40
- export function drawNode(node: AsciiNode, graph: AsciiGraph): Canvas {
41
- // All shapes use grid-determined dimensions to fill their allocated space.
42
- // This ensures consistent sizing across nodes and eliminates gaps between
43
- // nodes and subgraph borders. All shapes are rectangles with distinctive
44
- // corner characters (defined in corners.ts) to indicate shape type.
45
- return drawBoxWithGridDimensions(node, graph)
46
- }
47
-
48
- /**
49
- * Draw a box shape using grid-determined dimensions.
50
- * This ensures consistent sizing when multiple nodes share a column,
51
- * and eliminates gaps between nodes and subgraph borders by filling
52
- * the entire allocated grid space.
53
- *
54
- * All shapes are rendered as rectangles with distinctive corner characters
55
- * (defined in corners.ts) to indicate shape type.
56
- */
57
- function drawBoxWithGridDimensions(node: AsciiNode, graph: AsciiGraph): Canvas {
58
- const gc = node.gridCoord!
59
- const useAscii = graph.config.useAscii
60
-
61
- // Width spans 2 columns (border + content) - matching original behavior
62
- let w = 0
63
- for (let i = 0; i < 2; i++) {
64
- w += graph.columnWidth.get(gc.x + i) ?? 0
65
- }
66
- // Height spans 2 rows (border + content)
67
- let h = 0
68
- for (let i = 0; i < 2; i++) {
69
- h += graph.rowHeight.get(gc.y + i) ?? 0
70
- }
71
-
72
- const from: DrawingCoord = { x: 0, y: 0 }
73
- const to: DrawingCoord = { x: w, y: h }
74
- const box = mkCanvas(Math.max(from.x, to.x), Math.max(from.y, to.y))
75
-
76
- // Get corner characters for this shape type
77
- const corners = getCorners(node.shape, useAscii)
78
-
79
- // State-end uses double border to differentiate from state-start
80
- const isDoubleBox = node.shape === 'state-end'
81
- const hChar = useAscii ? (isDoubleBox ? '=' : '-') : (isDoubleBox ? '═' : '─')
82
- const vChar = useAscii ? (isDoubleBox ? '‖' : '|') : (isDoubleBox ? '║' : '│')
83
-
84
- // Double-box corners (for state-end)
85
- const doubleCorners = useAscii
86
- ? { tl: '#', tr: '#', bl: '#', br: '#' }
87
- : { tl: '╔', tr: '╗', bl: '╚', br: '╝' }
88
- const effectiveCorners = isDoubleBox ? doubleCorners : corners
89
-
90
- // Draw box border with shape-specific corners
91
- for (let x = from.x + 1; x < to.x; x++) box[x]![from.y] = hChar
92
- for (let x = from.x + 1; x < to.x; x++) box[x]![to.y] = hChar
93
- for (let y = from.y + 1; y < to.y; y++) box[from.x]![y] = vChar
94
- for (let y = from.y + 1; y < to.y; y++) box[to.x]![y] = vChar
95
- box[from.x]![from.y] = effectiveCorners.tl
96
- box[to.x]![from.y] = effectiveCorners.tr
97
- box[from.x]![to.y] = effectiveCorners.bl
98
- box[to.x]![to.y] = effectiveCorners.br
99
-
100
- // Center the multi-line display label inside the box
101
- const label = node.displayLabel
102
- const lines = splitLines(label)
103
- const textCenterY = from.y + Math.floor(h / 2)
104
- const startY = textCenterY - Math.floor((lines.length - 1) / 2)
105
-
106
- for (let i = 0; i < lines.length; i++) {
107
- const line = lines[i]!
108
- const textX = from.x + Math.floor(w / 2) - Math.ceil(visualWidth(line) / 2) + 1
109
- drawText(box, { x: textX, y: startY + i }, line, true)
110
- }
111
-
112
- return box
113
- }
114
-
115
- /**
116
- * Draw a node box with centered label text.
117
- * Returns a standalone canvas containing just the box.
118
- * Box size is determined by the grid column/row sizes for the node's position.
119
- */
120
- export function drawBox(node: AsciiNode, graph: AsciiGraph): Canvas {
121
- return drawNode(node, graph)
122
- }
123
-
124
- // ============================================================================
125
- // Multi-section box drawing — for class and ER diagram nodes
126
- // ============================================================================
127
-
128
- /**
129
- * Draw a multi-section box with horizontal dividers between sections.
130
- * Used by class diagrams (header | attributes | methods) and ER diagrams (header | attributes).
131
- * Each section is an array of text lines to render left-aligned with padding.
132
- *
133
- * @param sections - Array of sections, each section is an array of text lines
134
- * @param useAscii - true for ASCII chars, false for Unicode box-drawing
135
- * @param padding - horizontal padding inside the box (default 1)
136
- * @returns A standalone Canvas containing the multi-section box
137
- */
138
- export function drawMultiBox(
139
- sections: string[][],
140
- useAscii: boolean,
141
- padding: number = 1,
142
- ): Canvas {
143
- // Compute width: widest line across all sections + 2*padding + 2 border chars
144
- let maxTextWidth = 0
145
- for (const section of sections) {
146
- for (const line of section) {
147
- maxTextWidth = Math.max(maxTextWidth, visualWidth(line))
148
- }
149
- }
150
- const innerWidth = maxTextWidth + 2 * padding
151
- const boxWidth = innerWidth + 2 // +2 for left/right border
152
-
153
- // Compute height: sum of all section line counts + dividers + 2 border rows
154
- let totalLines = 0
155
- for (const section of sections) {
156
- totalLines += Math.max(section.length, 1) // at least 1 row per section
157
- }
158
- const numDividers = sections.length - 1
159
- const boxHeight = totalLines + numDividers + 2 // +2 for top/bottom border
160
-
161
- // Box-drawing characters
162
- const hLine = useAscii ? '-' : '─'
163
- const vLine = useAscii ? '|' : '│'
164
- const tl = useAscii ? '+' : '┌'
165
- const tr = useAscii ? '+' : '┐'
166
- const bl = useAscii ? '+' : '└'
167
- const br = useAscii ? '+' : '┘'
168
- const divL = useAscii ? '+' : '├'
169
- const divR = useAscii ? '+' : '┤'
170
-
171
- const canvas = mkCanvas(boxWidth - 1, boxHeight - 1)
172
-
173
- // Top border
174
- canvas[0]![0] = tl
175
- for (let x = 1; x < boxWidth - 1; x++) canvas[x]![0] = hLine
176
- canvas[boxWidth - 1]![0] = tr
177
-
178
- // Bottom border
179
- canvas[0]![boxHeight - 1] = bl
180
- for (let x = 1; x < boxWidth - 1; x++) canvas[x]![boxHeight - 1] = hLine
181
- canvas[boxWidth - 1]![boxHeight - 1] = br
182
-
183
- // Left and right borders (full height)
184
- for (let y = 1; y < boxHeight - 1; y++) {
185
- canvas[0]![y] = vLine
186
- canvas[boxWidth - 1]![y] = vLine
187
- }
188
-
189
- // Render sections with dividers
190
- let row = 1 // current y position (starts after top border)
191
- for (let s = 0; s < sections.length; s++) {
192
- const section = sections[s]!
193
- const lines = section.length > 0 ? section : ['']
194
-
195
- // Draw section text lines
196
- for (const line of lines) {
197
- const startX = 1 + padding
198
- drawText(canvas, { x: startX, y: row }, line, true)
199
- row++
200
- }
201
-
202
- // Draw divider after each section except the last
203
- if (s < sections.length - 1) {
204
- canvas[0]![row] = divL
205
- for (let x = 1; x < boxWidth - 1; x++) canvas[x]![row] = hLine
206
- canvas[boxWidth - 1]![row] = divR
207
- row++
208
- }
209
- }
210
-
211
- return canvas
212
- }
213
-
214
- // ============================================================================
215
- // Line drawing — 8-directional lines on the canvas
216
- // ============================================================================
217
-
218
- /**
219
- * Line character sets for different edge styles.
220
- * Each style has horizontal, vertical, and diagonal characters for both
221
- * Unicode (box-drawing) and ASCII (basic punctuation) modes.
222
- *
223
- * Unicode dotted: ┄ (horizontal), ┆ (vertical) — U+2504, U+2506
224
- * Unicode thick: ━ (horizontal), ┃ (vertical) — U+2501, U+2503
225
- */
226
- /**
227
- * Line character sets for different edge styles.
228
- * Only horizontal and vertical characters - no diagonals.
229
- * All edges use orthogonal Manhattan routing (90° bends only).
230
- */
231
- const LINE_CHARS = {
232
- solid: {
233
- h: { unicode: '─', ascii: '-' },
234
- v: { unicode: '│', ascii: '|' },
235
- },
236
- dotted: {
237
- h: { unicode: '┄', ascii: '.' },
238
- v: { unicode: '┆', ascii: ':' },
239
- },
240
- thick: {
241
- h: { unicode: '━', ascii: '=' },
242
- v: { unicode: '┃', ascii: '‖' },
243
- },
244
- } as const
245
-
246
- /**
247
- * Draw a line between two drawing coordinates using orthogonal Manhattan routing.
248
- * Returns the list of coordinates that were drawn on.
249
- * offsetFrom/offsetTo control how many cells to skip at the start/end.
250
- *
251
- * All lines use 90° bends only - no diagonal lines are produced.
252
- * For diagonal directions, uses horizontal-first routing (draws horizontal
253
- * segment, then vertical segment).
254
- */
255
- export function drawLine(
256
- canvas: Canvas,
257
- from: DrawingCoord,
258
- to: DrawingCoord,
259
- offsetFrom: number,
260
- offsetTo: number,
261
- useAscii: boolean,
262
- style: AsciiEdgeStyle = 'solid',
263
- ): DrawingCoord[] {
264
- const dir = determineDirection(from, to)
265
- const drawnCoords: DrawingCoord[] = []
266
-
267
- // Select character set based on style (horizontal and vertical only).
268
- // Invisible links never reach here (callers short-circuit), but fall back
269
- // to solid glyphs defensively so the index is total.
270
- const chars = LINE_CHARS[style === 'invisible' ? 'solid' : style]
271
- const hChar = useAscii ? chars.h.ascii : chars.h.unicode
272
- const vChar = useAscii ? chars.v.ascii : chars.v.unicode
273
-
274
- // Pure vertical directions
275
- if (dirEquals(dir, Up)) {
276
- for (let y = from.y - offsetFrom; y >= to.y - offsetTo; y--) {
277
- drawnCoords.push({ x: from.x, y })
278
- canvas[from.x]![y] = vChar
279
- }
280
- } else if (dirEquals(dir, Down)) {
281
- for (let y = from.y + offsetFrom; y <= to.y + offsetTo; y++) {
282
- drawnCoords.push({ x: from.x, y })
283
- canvas[from.x]![y] = vChar
284
- }
285
- }
286
- // Pure horizontal directions
287
- else if (dirEquals(dir, Left)) {
288
- for (let x = from.x - offsetFrom; x >= to.x - offsetTo; x--) {
289
- drawnCoords.push({ x, y: from.y })
290
- canvas[x]![from.y] = hChar
291
- }
292
- } else if (dirEquals(dir, Right)) {
293
- for (let x = from.x + offsetFrom; x <= to.x + offsetTo; x++) {
294
- drawnCoords.push({ x, y: from.y })
295
- canvas[x]![from.y] = hChar
296
- }
297
- }
298
- // Diagonal directions: use Manhattan routing (horizontal-first, then vertical)
299
- // UpperLeft: go left first, then up
300
- else if (dirEquals(dir, UpperLeft)) {
301
- // Horizontal segment: from.x -> to.x (going left)
302
- for (let x = from.x - offsetFrom; x >= to.x; x--) {
303
- drawnCoords.push({ x, y: from.y })
304
- canvas[x]![from.y] = hChar
305
- }
306
- // Vertical segment: from.y -> to.y (going up)
307
- for (let y = from.y - 1; y >= to.y - offsetTo; y--) {
308
- drawnCoords.push({ x: to.x, y })
309
- canvas[to.x]![y] = vChar
310
- }
311
- }
312
- // UpperRight: go right first, then up
313
- else if (dirEquals(dir, UpperRight)) {
314
- // Horizontal segment: from.x -> to.x (going right)
315
- for (let x = from.x + offsetFrom; x <= to.x; x++) {
316
- drawnCoords.push({ x, y: from.y })
317
- canvas[x]![from.y] = hChar
318
- }
319
- // Vertical segment: from.y -> to.y (going up)
320
- for (let y = from.y - 1; y >= to.y - offsetTo; y--) {
321
- drawnCoords.push({ x: to.x, y })
322
- canvas[to.x]![y] = vChar
323
- }
324
- }
325
- // LowerLeft: go left first, then down
326
- else if (dirEquals(dir, LowerLeft)) {
327
- // Horizontal segment: from.x -> to.x (going left)
328
- for (let x = from.x - offsetFrom; x >= to.x; x--) {
329
- drawnCoords.push({ x, y: from.y })
330
- canvas[x]![from.y] = hChar
331
- }
332
- // Vertical segment: from.y -> to.y (going down)
333
- for (let y = from.y + 1; y <= to.y + offsetTo; y++) {
334
- drawnCoords.push({ x: to.x, y })
335
- canvas[to.x]![y] = vChar
336
- }
337
- }
338
- // LowerRight: go right first, then down
339
- // Special case: if x difference is small (1), draw straight vertical at from.x
340
- // This keeps edges visually aligned with the source node
341
- else if (dirEquals(dir, LowerRight)) {
342
- const dx = to.x - from.x
343
- if (dx <= 1) {
344
- // Draw vertical line at from.x (source's x-coordinate)
345
- for (let y = from.y + offsetFrom; y <= to.y + offsetTo; y++) {
346
- drawnCoords.push({ x: from.x, y })
347
- canvas[from.x]![y] = vChar
348
- }
349
- } else {
350
- // Horizontal segment: from.x -> to.x (going right)
351
- for (let x = from.x + offsetFrom; x <= to.x; x++) {
352
- drawnCoords.push({ x, y: from.y })
353
- canvas[x]![from.y] = hChar
354
- }
355
- // Vertical segment: from.y -> to.y (going down)
356
- for (let y = from.y + 1; y <= to.y + offsetTo; y++) {
357
- drawnCoords.push({ x: to.x, y })
358
- canvas[to.x]![y] = vChar
359
- }
360
- }
361
- }
362
-
363
- return drawnCoords
364
- }
365
-
366
- // ============================================================================
367
- // Arrow drawing — path, corners, arrowheads, box-start junctions, labels
368
- // ============================================================================
369
-
370
- /**
371
- * Draw a complete arrow (edge) between two nodes.
372
- * Returns 6 separate canvases for layered compositing:
373
- * [path, boxStart, arrowHeadEnd, arrowHeadStart, corners, label]
374
- *
375
- * Supports bidirectional arrows via edge.hasArrowStart and edge.hasArrowEnd.
376
- */
377
- export function drawArrow(
378
- graph: AsciiGraph,
379
- edge: AsciiEdge,
380
- ): [Canvas, Canvas, Canvas, Canvas, Canvas, Canvas] {
381
- // Invisible links (~~~) shape the layout but draw no glyphs.
382
- if (edge.path.length === 0 || edge.style === 'invisible') {
383
- const empty = copyCanvas(graph.canvas)
384
- return [empty, empty, empty, empty, empty, empty]
385
- }
386
-
387
- // BUILD-14: edges whose endpoint is a subgraph container attach to the
388
- // container border, not the inner anchor node we routed through.
389
- if (edge.attachToSubgraph || edge.attachFromSubgraph) {
390
- return drawContainerEdge(graph, edge)
391
- }
392
-
393
- const labelCanvas = drawArrowLabel(graph, edge)
394
- const startPoint = edge.fromSubgraph
395
- ? getSubgraphAttachmentPoint(graph, edge.fromSubgraph, edge.from, edge.startDir)
396
- : getNodeAttachmentPoint(graph, edge.from, edge.startDir)
397
- const endPoint = edge.toSubgraph
398
- ? getSubgraphAttachmentPoint(graph, edge.toSubgraph, edge.to, edge.endDir)
399
- : undefined
400
- const [pathCanvas, linesDrawn, lineDirs] = drawPath(graph, edge.path, edge.style, startPoint, endPoint)
401
- const isStatePseudo = !edge.fromSubgraph && (edge.from.shape === 'state-start' || edge.from.shape === 'state-end')
402
- const boxStartCanvas = isStatePseudo
403
- ? copyCanvas(graph.canvas)
404
- : drawBoxStart(graph, edge.startDir, startPoint)
405
-
406
- // Draw end marker only if hasArrowEnd is true (default behavior)
407
- let arrowHeadEndCanvas: Canvas
408
- if (edge.hasArrowEnd) {
409
- arrowHeadEndCanvas = drawEndpointMarker(
410
- graph,
411
- linesDrawn[linesDrawn.length - 1]!,
412
- lineDirs[lineDirs.length - 1]!,
413
- edge.endMarker,
414
- )
415
- } else {
416
- arrowHeadEndCanvas = copyCanvas(graph.canvas)
417
- }
418
-
419
- // Draw start arrowhead for bidirectional edges
420
- // The start arrowhead needs to be at the box connector position (one step back
421
- // from the first line point), pointing into the source node.
422
- let arrowHeadStartCanvas: Canvas
423
- if (edge.hasArrowStart && linesDrawn.length > 0) {
424
- const firstLine = linesDrawn[0]!
425
- const firstPoint = firstLine[0]!
426
- const startDir = reverseDirection(lineDirs[0]!)
427
-
428
- // Calculate the box connector position (one step back from first point)
429
- const arrowPos: DrawingCoord = { x: firstPoint.x, y: firstPoint.y }
430
- if (dirEquals(lineDirs[0]!, Right)) arrowPos.x = firstPoint.x - 1
431
- else if (dirEquals(lineDirs[0]!, Left)) arrowPos.x = firstPoint.x + 1
432
- else if (dirEquals(lineDirs[0]!, Down)) arrowPos.y = firstPoint.y - 1
433
- else if (dirEquals(lineDirs[0]!, Up)) arrowPos.y = firstPoint.y + 1
434
-
435
- // Create a synthetic line ending at the marker position.
436
- const syntheticLine: DrawingCoord[] = [firstPoint, arrowPos]
437
- arrowHeadStartCanvas = drawEndpointMarker(graph, syntheticLine, startDir, edge.startMarker)
438
- } else {
439
- arrowHeadStartCanvas = copyCanvas(graph.canvas)
440
- }
441
-
442
- const cornersCanvas = drawCorners(graph, edge.path)
443
-
444
- return [pathCanvas, boxStartCanvas, arrowHeadEndCanvas, arrowHeadStartCanvas, cornersCanvas, labelCanvas]
445
- }
446
-
447
- /**
448
- * BUILD-14: Draw an edge that terminates at (or originates from) a subgraph
449
- * container. The edge was routed to a representative inner anchor node; here we
450
- * clip the visible polyline to the container's border rectangle and place the
451
- * arrowhead on the border, so the edge attaches to the container instead of the
452
- * phantom box or the inner node.
453
- *
454
- * Returns the same 6-canvas tuple as drawArrow.
455
- */
456
- function drawContainerEdge(
457
- graph: AsciiGraph,
458
- edge: AsciiEdge,
459
- ): [Canvas, Canvas, Canvas, Canvas, Canvas, Canvas] {
460
- const empty = copyCanvas(graph.canvas)
461
-
462
- // Build the full drawing-coordinate polyline from the grid path.
463
- let poly = edge.path.map(c => gridToDrawingCoord(graph, c))
464
- poly = dedupeCollinear(poly)
465
- if (poly.length < 2) return [empty, empty, empty, empty, empty, empty]
466
-
467
- // Clip the start at the source container border (if any).
468
- if (edge.attachFromSubgraph) {
469
- const sg = edge.attachFromSubgraph
470
- poly = clipPolylineAtBorder(poly, sg, 'start')
471
- }
472
- // Clip the end at the target container border (if any).
473
- if (edge.attachToSubgraph) {
474
- poly = clipPolylineAtBorder(poly, edge.attachToSubgraph, 'end')
475
- }
476
- if (poly.length < 2) return [empty, empty, empty, empty, empty, empty]
477
-
478
- // Draw the polyline segments.
479
- const pathCanvas = copyCanvas(graph.canvas)
480
- const useAscii = graph.config.useAscii
481
- // Mirror standard drawPath: every segment skips its first and last cell so
482
- // bend cells are owned solely by the corner canvas (avoids junction glyph
483
- // corruption when two segments meet). Exceptions: the very first cell is the
484
- // container-border exit (draw it, offset 0) and the very last cell is either
485
- // the container-border entry (draw it, offset 0) or one cell before a real
486
- // target node (offset -1, arrowhead drawn separately).
487
- const lastSegmentEndsAtRealNode = !edge.attachToSubgraph
488
- for (let i = 1; i < poly.length; i++) {
489
- const from = poly[i - 1]!
490
- const to = poly[i]!
491
- if (drawingCoordEquals(from, to)) continue
492
- const isFirst = i === 1
493
- const isLast = i === poly.length - 1
494
- const offsetFrom = isFirst && edge.attachFromSubgraph ? 0 : 1
495
- let offsetTo: number
496
- if (isLast) {
497
- offsetTo = lastSegmentEndsAtRealNode ? -1 : 0
498
- } else {
499
- offsetTo = -1
500
- }
501
- drawLine(pathCanvas, from, to, offsetFrom, offsetTo, useAscii, edge.style)
502
- }
503
-
504
- // Corner characters at bends.
505
- const cornersCanvas = copyCanvas(graph.canvas)
506
- for (let i = 1; i < poly.length - 1; i++) {
507
- const prev = poly[i - 1]!
508
- const coord = poly[i]!
509
- const next = poly[i + 1]!
510
- const prevDir = determineDirection(prev, coord)
511
- const nextDir = determineDirection(coord, next)
512
- cornersCanvas[coord.x]![coord.y] = cornerChar(prevDir, nextDir, useAscii)
513
- }
514
-
515
- // Arrowhead at the END of the edge.
516
- const arrowHeadEndCanvas = copyCanvas(graph.canvas)
517
- if (edge.hasArrowEnd && poly.length >= 2) {
518
- const last = poly[poly.length - 1]!
519
- const beforeLast = poly[poly.length - 2]!
520
- const arrowDir = determineDirection(beforeLast, last)
521
- if (edge.attachToSubgraph) {
522
- // Terminal is the container border — draw the arrowhead on it.
523
- arrowHeadEndCanvas[last.x]![last.y] = arrowHeadChar(arrowDir, useAscii)
524
- } else {
525
- // Terminal is a real node — draw the arrowhead one cell before its border
526
- // (matching standard edges, which use offsetTo=-1).
527
- const tip = stepBack(last, arrowDir)
528
- arrowHeadEndCanvas[tip.x]![tip.y] = arrowHeadChar(arrowDir, useAscii)
529
- }
530
- }
531
-
532
- // Marker at the START of the edge (container exit, or reversed real source).
533
- const arrowHeadStartCanvas = copyCanvas(graph.canvas)
534
- if (edge.hasArrowStart && poly.length >= 2) {
535
- const first = poly[0]!
536
- const second = poly[1]!
537
- const startDir = determineDirection(second, first)
538
- arrowHeadStartCanvas[first.x]![first.y] = arrowHeadChar(startDir, useAscii)
539
- }
540
-
541
- const labelCanvas = drawArrowLabel(graph, edge)
542
-
543
- return [pathCanvas, empty, arrowHeadEndCanvas, arrowHeadStartCanvas, cornersCanvas, labelCanvas]
544
- }
545
-
546
- /** Move one cell backward against a direction (toward the segment origin). */
547
- function stepBack(p: DrawingCoord, dir: Direction): DrawingCoord {
548
- if (dirEquals(dir, Up)) return { x: p.x, y: p.y + 1 }
549
- if (dirEquals(dir, Down)) return { x: p.x, y: p.y - 1 }
550
- if (dirEquals(dir, Left)) return { x: p.x + 1, y: p.y }
551
- if (dirEquals(dir, Right)) return { x: p.x - 1, y: p.y }
552
- return { x: p.x, y: p.y }
553
- }
554
-
555
- /** Remove redundant collinear midpoints from a drawing polyline. */
556
- function dedupeCollinear(poly: DrawingCoord[]): DrawingCoord[] {
557
- const out: DrawingCoord[] = []
558
- for (const p of poly) {
559
- const last = out[out.length - 1]
560
- if (last && drawingCoordEquals(last, p)) continue
561
- out.push({ x: p.x, y: p.y })
562
- }
563
- // Collapse three collinear points into two.
564
- const result: DrawingCoord[] = []
565
- for (let i = 0; i < out.length; i++) {
566
- if (i > 0 && i < out.length - 1) {
567
- const a = out[i - 1]!
568
- const b = out[i]!
569
- const c = out[i + 1]!
570
- const collinearH = a.y === b.y && b.y === c.y
571
- const collinearV = a.x === b.x && b.x === c.x
572
- if (collinearH || collinearV) continue
573
- }
574
- result.push(out[i]!)
575
- }
576
- return result
577
- }
578
-
579
- /**
580
- * Clip a drawing polyline so the chosen end sits exactly on a subgraph's border.
581
- *
582
- * - which = 'end': the polyline ends inside the container; truncate it at the
583
- * first point where it crosses the border (walking from the start) and snap
584
- * the terminal point onto the border line.
585
- * - which = 'start': the polyline starts inside the container; drop the inside
586
- * portion and start at the border crossing (walking from the end backwards).
587
- */
588
- function clipPolylineAtBorder(
589
- poly: DrawingCoord[],
590
- sg: AsciiSubgraph,
591
- which: 'start' | 'end',
592
- ): DrawingCoord[] {
593
- const inside = (p: DrawingCoord): boolean =>
594
- p.x > sg.minX && p.x < sg.maxX && p.y > sg.minY && p.y < sg.maxY
595
-
596
- if (which === 'end') {
597
- // Walk from start; find first segment entering the container.
598
- for (let i = 1; i < poly.length; i++) {
599
- const prev = poly[i - 1]!
600
- const curr = poly[i]!
601
- if (!inside(prev) && inside(curr)) {
602
- const border = borderCrossing(prev, curr, sg)
603
- return [...poly.slice(0, i), border]
604
- }
605
- if (inside(prev)) {
606
- // Already inside at the previous point — clip there.
607
- const border = borderCrossing(poly[Math.max(0, i - 2)] ?? prev, prev, sg)
608
- return [...poly.slice(0, i - 1), border]
609
- }
610
- }
611
- // Never detected a crossing (e.g. anchor outside bbox); snap last point to
612
- // the nearest border edge from the penultimate point.
613
- const last = poly[poly.length - 1]!
614
- const prev = poly[poly.length - 2]!
615
- return [...poly.slice(0, poly.length - 1), borderCrossing(prev, last, sg)]
616
- }
617
-
618
- // which === 'start': walk from the end backwards.
619
- for (let i = poly.length - 2; i >= 0; i--) {
620
- const next = poly[i + 1]!
621
- const curr = poly[i]!
622
- if (!inside(next) && inside(curr)) {
623
- const border = borderCrossing(next, curr, sg)
624
- return [border, ...poly.slice(i + 1)]
625
- }
626
- if (inside(next)) continue
627
- }
628
- const first = poly[0]!
629
- const next = poly[1]!
630
- return [borderCrossing(next, first, sg), ...poly.slice(1)]
631
- }
632
-
633
- /**
634
- * Given a segment from an outside point to an inside point, return the point on
635
- * the container border where the orthogonal segment crosses it. Edge paths are
636
- * orthogonal, so the segment is either horizontal or vertical.
637
- */
638
- function borderCrossing(
639
- outside: DrawingCoord,
640
- insidePt: DrawingCoord,
641
- sg: AsciiSubgraph,
642
- ): DrawingCoord {
643
- if (outside.y === insidePt.y) {
644
- // Horizontal segment crosses a vertical border (left or right).
645
- const x = outside.x < insidePt.x ? sg.minX : sg.maxX
646
- return { x, y: insidePt.y }
647
- }
648
- if (outside.x === insidePt.x) {
649
- // Vertical segment crosses a horizontal border (top or bottom).
650
- const y = outside.y < insidePt.y ? sg.minY : sg.maxY
651
- return { x: insidePt.x, y }
652
- }
653
- // Diagonal fallback (shouldn't happen with orthogonal routing): clamp inside
654
- // point to the nearest border.
655
- return {
656
- x: Math.max(sg.minX, Math.min(sg.maxX, insidePt.x)),
657
- y: Math.max(sg.minY, Math.min(sg.maxY, insidePt.y)),
658
- }
659
- }
660
-
661
- /** Pick the corner glyph for a bend given incoming/outgoing directions. */
662
- function cornerChar(prevDir: Direction, nextDir: Direction, useAscii: boolean): string {
663
- if (useAscii) return '+'
664
- if ((dirEquals(prevDir, Right) && dirEquals(nextDir, Down)) ||
665
- (dirEquals(prevDir, Up) && dirEquals(nextDir, Left))) return '┐'
666
- if ((dirEquals(prevDir, Right) && dirEquals(nextDir, Up)) ||
667
- (dirEquals(prevDir, Down) && dirEquals(nextDir, Left))) return '┘'
668
- if ((dirEquals(prevDir, Left) && dirEquals(nextDir, Down)) ||
669
- (dirEquals(prevDir, Up) && dirEquals(nextDir, Right))) return '┌'
670
- if ((dirEquals(prevDir, Left) && dirEquals(nextDir, Up)) ||
671
- (dirEquals(prevDir, Down) && dirEquals(nextDir, Right))) return '└'
672
- return '+'
673
- }
674
-
675
- /** Pick the arrowhead glyph for a direction. */
676
- function arrowHeadChar(dir: Direction, useAscii: boolean): string {
677
- if (!useAscii) {
678
- if (dirEquals(dir, Up)) return '▲'
679
- if (dirEquals(dir, Down)) return '▼'
680
- if (dirEquals(dir, Left)) return '◄'
681
- if (dirEquals(dir, Right)) return '►'
682
- return '●'
683
- }
684
- if (dirEquals(dir, Up)) return '^'
685
- if (dirEquals(dir, Down)) return 'v'
686
- if (dirEquals(dir, Left)) return '<'
687
- if (dirEquals(dir, Right)) return '>'
688
- return '*'
689
- }
690
-
691
- /**
692
- * Reverse a direction (for bidirectional arrow start heads).
693
- */
694
- function reverseDirection(dir: Direction): Direction {
695
- if (dirEquals(dir, Up)) return Down
696
- if (dirEquals(dir, Down)) return Up
697
- if (dirEquals(dir, Left)) return Right
698
- if (dirEquals(dir, Right)) return Left
699
- if (dirEquals(dir, UpperLeft)) return LowerRight
700
- if (dirEquals(dir, UpperRight)) return LowerLeft
701
- if (dirEquals(dir, LowerLeft)) return UpperRight
702
- if (dirEquals(dir, LowerRight)) return UpperLeft
703
- return Middle
704
- }
705
-
706
- /**
707
- * Draw the path lines for an edge.
708
- * Returns the canvas, the coordinates drawn for each segment, and the direction of each segment.
709
- */
710
- function drawPath(
711
- graph: AsciiGraph,
712
- path: GridCoord[],
713
- style: AsciiEdgeStyle = 'solid',
714
- startPointOverride?: DrawingCoord,
715
- endPointOverride?: DrawingCoord,
716
- ): [Canvas, DrawingCoord[][], Direction[]] {
717
- const canvas = copyCanvas(graph.canvas)
718
- let previousCoord = path[0]!
719
- const linesDrawn: DrawingCoord[][] = []
720
- const lineDirs: Direction[] = []
721
-
722
- for (let i = 1; i < path.length; i++) {
723
- const nextCoord = path[i]!
724
- const prevDC = (i === 1 && startPointOverride)
725
- ? startPointOverride
726
- : gridToDrawingCoord(graph, previousCoord)
727
- const nextDC = (i === path.length - 1 && endPointOverride)
728
- ? endPointOverride
729
- : gridToDrawingCoord(graph, nextCoord)
730
-
731
- if (drawingCoordEquals(prevDC, nextDC)) {
732
- previousCoord = nextCoord
733
- continue
734
- }
735
-
736
- const dir = determineDirection(previousCoord, nextCoord)
737
- const segment = drawLine(canvas, prevDC, nextDC, 1, -1, graph.config.useAscii, style)
738
- if (segment.length === 0) segment.push(prevDC)
739
- linesDrawn.push(segment)
740
- lineDirs.push(dir)
741
- previousCoord = nextCoord
742
- }
743
-
744
- return [canvas, linesDrawn, lineDirs]
745
- }
746
-
747
- /**
748
- * Draw the junction character where an edge exits the source node's box.
749
- * Only applies to Unicode mode (ASCII mode just uses the line characters).
750
- * Skips drawing for state pseudo-states which have their own visual borders.
751
- */
752
- function drawBoxStart(
753
- graph: AsciiGraph,
754
- startDir: Direction,
755
- startPoint: DrawingCoord,
756
- ): Canvas {
757
- const canvas = copyCanvas(graph.canvas)
758
- if (graph.config.useAscii) return canvas
759
-
760
- if (dirEquals(startDir, Up)) canvas[startPoint.x]![startPoint.y] = '┴'
761
- else if (dirEquals(startDir, Down)) canvas[startPoint.x]![startPoint.y] = '┬'
762
- else if (dirEquals(startDir, Left)) canvas[startPoint.x]![startPoint.y] = '┤'
763
- else if (dirEquals(startDir, Right)) canvas[startPoint.x]![startPoint.y] = '├'
764
-
765
- return canvas
766
- }
767
-
768
- function endpointMarkerChar(marker: Exclude<EdgeMarker, 'arrow'>, useAscii: boolean): string {
769
- if (useAscii) return marker === 'circle' ? 'o' : 'x'
770
- return marker === 'circle' ? '◯' : '✕'
771
- }
772
-
773
- function drawEndpointMarker(
774
- graph: AsciiGraph,
775
- lastLine: DrawingCoord[],
776
- fallbackDir: Direction,
777
- marker: EdgeMarker | undefined,
778
- ): Canvas {
779
- if (marker === 'circle' || marker === 'cross') {
780
- const canvas = copyCanvas(graph.canvas)
781
- if (lastLine.length === 0) return canvas
782
- const lastPos = lastLine[lastLine.length - 1]!
783
- canvas[lastPos.x]![lastPos.y] = endpointMarkerChar(marker, graph.config.useAscii)
784
- return canvas
785
- }
786
-
787
- return drawArrowHead(graph, lastLine, fallbackDir)
788
- }
789
-
790
- /**
791
- * Draw the arrowhead at the end of an edge path.
792
- * Uses triangular Unicode symbols (▲▼◄►) or ASCII symbols (^v<>).
793
- */
794
- function drawArrowHead(
795
- graph: AsciiGraph,
796
- lastLine: DrawingCoord[],
797
- fallbackDir: Direction,
798
- ): Canvas {
799
- const canvas = copyCanvas(graph.canvas)
800
- if (lastLine.length === 0) return canvas
801
-
802
- const from = lastLine[0]!
803
- const lastPos = lastLine[lastLine.length - 1]!
804
- let dir = determineDirection(from, lastPos)
805
- if (lastLine.length === 1 || dirEquals(dir, Middle)) dir = fallbackDir
806
-
807
- let char: string
808
-
809
- if (!graph.config.useAscii) {
810
- if (dirEquals(dir, Up)) char = '▲'
811
- else if (dirEquals(dir, Down)) char = '▼'
812
- else if (dirEquals(dir, Left)) char = '◄'
813
- else if (dirEquals(dir, Right)) char = '►'
814
- else if (dirEquals(dir, UpperRight)) char = '◥'
815
- else if (dirEquals(dir, UpperLeft)) char = '◤'
816
- else if (dirEquals(dir, LowerRight)) char = '◢'
817
- else if (dirEquals(dir, LowerLeft)) char = '◣'
818
- else {
819
- // Fallback
820
- if (dirEquals(fallbackDir, Up)) char = '▲'
821
- else if (dirEquals(fallbackDir, Down)) char = '▼'
822
- else if (dirEquals(fallbackDir, Left)) char = '◄'
823
- else if (dirEquals(fallbackDir, Right)) char = '►'
824
- else if (dirEquals(fallbackDir, UpperRight)) char = '◥'
825
- else if (dirEquals(fallbackDir, UpperLeft)) char = '◤'
826
- else if (dirEquals(fallbackDir, LowerRight)) char = '◢'
827
- else if (dirEquals(fallbackDir, LowerLeft)) char = '◣'
828
- else char = '●'
829
- }
830
- } else {
831
- if (dirEquals(dir, Up)) char = '^'
832
- else if (dirEquals(dir, Down)) char = 'v'
833
- else if (dirEquals(dir, Left)) char = '<'
834
- else if (dirEquals(dir, Right)) char = '>'
835
- else {
836
- if (dirEquals(fallbackDir, Up)) char = '^'
837
- else if (dirEquals(fallbackDir, Down)) char = 'v'
838
- else if (dirEquals(fallbackDir, Left)) char = '<'
839
- else if (dirEquals(fallbackDir, Right)) char = '>'
840
- else char = '*'
841
- }
842
- }
843
-
844
- canvas[lastPos.x]![lastPos.y] = char
845
- return canvas
846
- }
847
-
848
- /**
849
- * Draw corner characters at path bends (where the direction changes).
850
- * Uses ┌┐└┘ in Unicode mode, + in ASCII mode.
851
- */
852
- function drawCorners(graph: AsciiGraph, path: GridCoord[]): Canvas {
853
- const canvas = copyCanvas(graph.canvas)
854
-
855
- for (let idx = 1; idx < path.length - 1; idx++) {
856
- const coord = path[idx]!
857
- const dc = gridToDrawingCoord(graph, coord)
858
- const prevDir = determineDirection(path[idx - 1]!, coord)
859
- const nextDir = determineDirection(coord, path[idx + 1]!)
860
- if (dirEquals(prevDir, nextDir)) continue
861
-
862
- // Skip collinear intermediate points: a non-bend (prevDir === nextDir) is a
863
- // straight run, and painting a corner glyph there would punch a hole in the
864
- // trunk line (upstream lukilabs#113). mergePath normally removes these, so
865
- // this is a defensive guard against any path that still carries one.
866
- if (dirEquals(prevDir, nextDir)) continue
867
-
868
- let corner: string
869
- if (!graph.config.useAscii) {
870
- if ((dirEquals(prevDir, Right) && dirEquals(nextDir, Down)) ||
871
- (dirEquals(prevDir, Up) && dirEquals(nextDir, Left))) {
872
- corner = '┐'
873
- } else if ((dirEquals(prevDir, Right) && dirEquals(nextDir, Up)) ||
874
- (dirEquals(prevDir, Down) && dirEquals(nextDir, Left))) {
875
- corner = '┘'
876
- } else if ((dirEquals(prevDir, Left) && dirEquals(nextDir, Down)) ||
877
- (dirEquals(prevDir, Up) && dirEquals(nextDir, Right))) {
878
- corner = '┌'
879
- } else if ((dirEquals(prevDir, Left) && dirEquals(nextDir, Up)) ||
880
- (dirEquals(prevDir, Down) && dirEquals(nextDir, Right))) {
881
- corner = '└'
882
- } else {
883
- corner = '+'
884
- }
885
- } else {
886
- corner = '+'
887
- }
888
-
889
- canvas[dc.x]![dc.y] = corner
890
- }
891
-
892
- return canvas
893
- }
894
-
895
- /** Draw edge label text centered on the widest path segment. */
896
- function drawArrowLabel(graph: AsciiGraph, edge: AsciiEdge): Canvas {
897
- const canvas = copyCanvas(graph.canvas)
898
- if (edge.text.length === 0) return canvas
899
-
900
- const drawingLine = lineToDrawing(graph, edge.labelLine)
901
-
902
- // Offset labels only for true bidirectional pairs. Ordinary downward
903
- // fan-out branches should keep labels centered on the branch, not shifted
904
- // upward into the shared trunk.
905
- const hasOppositeEdge = graph.edges.some(other =>
906
- other !== edge && other.from === edge.to && other.to === edge.from,
907
- )
908
- let isUpwardEdge: boolean | undefined
909
- if (hasOppositeEdge && edge.path.length >= 2) {
910
- const startY = edge.path[0]!.y
911
- const endY = edge.path[edge.path.length - 1]!.y
912
- if (endY < startY) isUpwardEdge = true
913
- else if (endY > startY) isUpwardEdge = false
914
- }
915
-
916
- drawTextOnLine(canvas, drawingLine, edge.text, isUpwardEdge)
917
- return canvas
918
- }
919
-
920
- /**
921
- * Draw text centered on a line segment defined by two drawing coordinates.
922
- * Supports multi-line labels.
923
- *
924
- * When isUpwardEdge is provided, offsets the label vertically to prevent
925
- * overlapping with labels from edges going the opposite direction:
926
- * - Upward edges: label placed in lower portion of segment
927
- * - Downward edges (isUpwardEdge=false): label placed in upper portion
928
- * - No direction (isUpwardEdge=undefined): label centered (default)
929
- */
930
- function drawTextOnLine(canvas: Canvas, line: DrawingCoord[], label: string, isUpwardEdge?: boolean): void {
931
- if (line.length < 2) return
932
- const minX = Math.min(line[0]!.x, line[1]!.x)
933
- const maxX = Math.max(line[0]!.x, line[1]!.x)
934
- const minY = Math.min(line[0]!.y, line[1]!.y)
935
- const maxY = Math.max(line[0]!.y, line[1]!.y)
936
- const middleX = minX + Math.floor((maxX - minX) / 2)
937
- let middleY = minY + Math.floor((maxY - minY) / 2)
938
-
939
- // Offset label vertically to prevent overlap on bidirectional edges
940
- // For vertical segments (same X), shift based on edge direction
941
- if (isUpwardEdge !== undefined && minX === maxX) {
942
- const segmentHeight = maxY - minY
943
- const offset = Math.max(1, Math.floor(segmentHeight / 4))
944
- if (isUpwardEdge) {
945
- // Upward edge: place label in lower portion
946
- middleY = middleY + offset
947
- } else {
948
- // Downward edge: place label in upper portion
949
- middleY = middleY - offset
950
- }
951
- }
952
-
953
- // Support multi-line labels
954
- const lines = splitLines(label)
955
- const startY = middleY - Math.floor((lines.length - 1) / 2)
956
-
957
- for (let i = 0; i < lines.length; i++) {
958
- const lineText = lines[i]!
959
- const startX = middleX - Math.floor(lineText.length / 2)
960
- drawText(canvas, { x: startX, y: startY + i }, lineText)
961
- }
962
- }
963
-
964
- // ============================================================================
965
- // Node attachment point helper
966
- // ============================================================================
967
-
968
- /**
969
- * Get the drawing coordinate where an edge attaches to a node's border.
970
- * Uses grid-allocated dimensions so attachment points align with the actual
971
- * drawn box (which may be wider/taller than the intrinsic shape dimensions
972
- * when sharing a column/row with a larger node).
973
- */
974
- function getNodeAttachmentPoint(
975
- graph: AsciiGraph,
976
- node: AsciiNode,
977
- dir: Direction,
978
- ): DrawingCoord {
979
- const gc = node.gridCoord!
980
-
981
- // Calculate actual drawn dimensions from grid (matching drawBoxWithGridDimensions)
982
- let w = 0
983
- for (let i = 0; i < 2; i++) {
984
- w += graph.columnWidth.get(gc.x + i) ?? 0
985
- }
986
- let h = 0
987
- for (let i = 0; i < 2; i++) {
988
- h += graph.rowHeight.get(gc.y + i) ?? 0
989
- }
990
-
991
- // Build dimensions matching the actual drawn box size
992
- const gridDimensions = {
993
- width: w + 1,
994
- height: h + 1,
995
- labelArea: { x: 0, y: 0, width: 0, height: 0 },
996
- gridColumns: [0, 0, 0] as [number, number, number],
997
- gridRows: [0, 0, 0] as [number, number, number],
998
- }
999
-
1000
- const baseCoord = node.drawingCoord!
1001
- return getShapeAttachmentPoint(node.shape, dir, gridDimensions, baseCoord)
1002
- }
1003
-
1004
- function clamp(n: number, min: number, max: number): number {
1005
- return Math.max(min, Math.min(max, n))
1006
- }
1007
-
1008
- /** Attach a container edge to the subgraph border nearest its inner anchor. */
1009
- function getSubgraphAttachmentPoint(
1010
- graph: AsciiGraph,
1011
- sg: AsciiSubgraph,
1012
- anchor: AsciiNode,
1013
- dir: Direction,
1014
- ): DrawingCoord {
1015
- const anchorPoint = getNodeAttachmentPoint(graph, anchor, dir)
1016
- if (dirEquals(dir, Up)) {
1017
- return { x: clamp(anchorPoint.x, sg.minX + 1, sg.maxX - 1), y: sg.minY }
1018
- }
1019
- if (dirEquals(dir, Down)) {
1020
- return { x: clamp(anchorPoint.x, sg.minX + 1, sg.maxX - 1), y: sg.maxY }
1021
- }
1022
- if (dirEquals(dir, Left)) {
1023
- return { x: sg.minX, y: clamp(anchorPoint.y, sg.minY + 1, sg.maxY - 1) }
1024
- }
1025
- if (dirEquals(dir, Right)) {
1026
- return { x: sg.maxX, y: clamp(anchorPoint.y, sg.minY + 1, sg.maxY - 1) }
1027
- }
1028
- return anchorPoint
1029
- }
1030
-
1031
- // ============================================================================
1032
- // Bundled edge drawing — for parallel links (A & B --> C)
1033
- // ============================================================================
1034
-
1035
- /**
1036
- * Draw a single edge's segment in a bundle (source → junction for fan-in,
1037
- * junction → target for fan-out).
1038
- *
1039
- * Returns the same tuple format as drawArrow for consistency.
1040
- */
1041
- function drawBundledEdgeSegment(
1042
- graph: AsciiGraph,
1043
- edge: AsciiEdge,
1044
- bundle: EdgeBundle,
1045
- ): [Canvas, Canvas, Canvas, Canvas, Canvas, Canvas] {
1046
- const empty = copyCanvas(graph.canvas)
1047
-
1048
- if (!edge.pathToJunction || edge.pathToJunction.length === 0) {
1049
- return [empty, empty, empty, empty, empty, empty]
1050
- }
1051
-
1052
- // Draw the path segment (pathToJunction)
1053
- const pathCanvas = copyCanvas(graph.canvas)
1054
- const useAscii = graph.config.useAscii
1055
-
1056
- // Convert grid coords to drawing coords
1057
- // For fan-in: first point is at source node border (use attachment point)
1058
- // For fan-out: last point is at target node border (use attachment point)
1059
- const drawingPath = edge.pathToJunction.map((gc, idx) => {
1060
- if (bundle.type === 'fan-in' && idx === 0) {
1061
- // First point: use source node's actual border position
1062
- return getNodeAttachmentPoint(graph, edge.from, edge.startDir)
1063
- }
1064
- if (bundle.type === 'fan-out' && idx === edge.pathToJunction!.length - 1) {
1065
- // Last point: use target node's actual border position
1066
- return getNodeAttachmentPoint(graph, edge.to, edge.endDir)
1067
- }
1068
- return gridToDrawingCoord(graph, gc)
1069
- })
1070
-
1071
- // Draw line segments
1072
- for (let i = 1; i < drawingPath.length; i++) {
1073
- const from = drawingPath[i - 1]!
1074
- const to = drawingPath[i]!
1075
- if (!drawingCoordEquals(from, to)) {
1076
- // Always skip both endpoints of every segment (offset 1, -1),
1077
- // matching non-bundled drawPath behavior. This leaves endpoint
1078
- // characters to corner/junction/boxStart canvases, preventing
1079
- // line characters from corrupting them via mergeJunctions.
1080
- drawLine(pathCanvas, from, to, 1, -1, useAscii, edge.style)
1081
- }
1082
- }
1083
-
1084
- // Draw corners at path bends
1085
- const cornersCanvas = copyCanvas(graph.canvas)
1086
- for (let idx = 1; idx < edge.pathToJunction.length - 1; idx++) {
1087
- const coord = edge.pathToJunction[idx]!
1088
- const dc = gridToDrawingCoord(graph, coord)
1089
- const prevDir = determineDirection(edge.pathToJunction[idx - 1]!, coord)
1090
- const nextDir = determineDirection(coord, edge.pathToJunction[idx + 1]!)
1091
- if (dirEquals(prevDir, nextDir)) continue
1092
-
1093
- let corner: string
1094
- if (!useAscii) {
1095
- if ((dirEquals(prevDir, Right) && dirEquals(nextDir, Down)) ||
1096
- (dirEquals(prevDir, Up) && dirEquals(nextDir, Left))) {
1097
- corner = '┐'
1098
- } else if ((dirEquals(prevDir, Right) && dirEquals(nextDir, Up)) ||
1099
- (dirEquals(prevDir, Down) && dirEquals(nextDir, Left))) {
1100
- corner = '┘'
1101
- } else if ((dirEquals(prevDir, Left) && dirEquals(nextDir, Down)) ||
1102
- (dirEquals(prevDir, Up) && dirEquals(nextDir, Right))) {
1103
- corner = '┌'
1104
- } else if ((dirEquals(prevDir, Left) && dirEquals(nextDir, Up)) ||
1105
- (dirEquals(prevDir, Down) && dirEquals(nextDir, Right))) {
1106
- corner = '└'
1107
- } else {
1108
- corner = '+'
1109
- }
1110
- } else {
1111
- corner = '+'
1112
- }
1113
-
1114
- cornersCanvas[dc.x]![dc.y] = corner
1115
- }
1116
-
1117
- // Draw box start connector (for fan-in, from source node)
1118
- // The connector is placed at the first point coordinate (box border position)
1119
- // since we use offsets 1,-1 for drawLine, the line starts one step past this point
1120
- const boxStartCanvas = copyCanvas(graph.canvas)
1121
- if (bundle.type === 'fan-in' && edge.pathToJunction.length >= 2) {
1122
- const firstPoint = drawingPath[0]!
1123
- const dir = determineDirection(edge.pathToJunction[0]!, edge.pathToJunction[1]!)
1124
-
1125
- if (!useAscii) {
1126
- if (dirEquals(dir, Up)) boxStartCanvas[firstPoint.x]![firstPoint.y] = '┴'
1127
- else if (dirEquals(dir, Down)) boxStartCanvas[firstPoint.x]![firstPoint.y] = '┬'
1128
- else if (dirEquals(dir, Left)) boxStartCanvas[firstPoint.x]![firstPoint.y] = '┤'
1129
- else if (dirEquals(dir, Right)) boxStartCanvas[firstPoint.x]![firstPoint.y] = '├'
1130
- }
1131
- }
1132
-
1133
- // Label canvas (bundled edges typically don't have labels, but handle it)
1134
- const labelCanvas = copyCanvas(graph.canvas)
1135
-
1136
- return [pathCanvas, boxStartCanvas, empty, empty, cornersCanvas, labelCanvas]
1137
- }
1138
-
1139
- /**
1140
- * Draw the shared path segment of a bundle (junction → target for fan-in,
1141
- * source → junction for fan-out).
1142
- */
1143
- function drawBundleSharedPath(graph: AsciiGraph, bundle: EdgeBundle): [Canvas, Canvas] {
1144
- const pathCanvas = copyCanvas(graph.canvas)
1145
- const cornersCanvas = copyCanvas(graph.canvas)
1146
-
1147
- if (bundle.sharedPath.length < 2) {
1148
- return [pathCanvas, cornersCanvas]
1149
- }
1150
-
1151
- const useAscii = graph.config.useAscii
1152
- const style = bundle.edges[0]?.style ?? 'solid'
1153
- const graphDir = graph.config.graphDirection
1154
-
1155
- // Convert grid coords to drawing coords
1156
- // For fan-in: last point is at target node border
1157
- // For fan-out: first point is at source node border
1158
- const drawingPath = bundle.sharedPath.map((gc, idx) => {
1159
- if (bundle.type === 'fan-in' && idx === bundle.sharedPath.length - 1) {
1160
- // Last point: use target node's actual border position (entry from above/left)
1161
- const entryDir = graphDir === 'TD' ? Up : Left
1162
- return getNodeAttachmentPoint(graph, bundle.sharedNode, entryDir)
1163
- }
1164
- if (bundle.type === 'fan-out' && idx === 0) {
1165
- // First point: use source node's actual border position (exit going down/right)
1166
- const exitDir = graphDir === 'TD' ? Down : Right
1167
- return getNodeAttachmentPoint(graph, bundle.sharedNode, exitDir)
1168
- }
1169
- return gridToDrawingCoord(graph, gc)
1170
- })
1171
-
1172
- // Draw line segments with appropriate offsets
1173
- for (let i = 1; i < drawingPath.length; i++) {
1174
- const from = drawingPath[i - 1]!
1175
- const to = drawingPath[i]!
1176
- if (!drawingCoordEquals(from, to)) {
1177
- // Always skip both endpoints (offset 1, -1), matching non-bundled drawPath.
1178
- drawLine(pathCanvas, from, to, 1, -1, useAscii, style)
1179
- }
1180
- }
1181
-
1182
- // Draw corners at path bends
1183
- for (let idx = 1; idx < bundle.sharedPath.length - 1; idx++) {
1184
- const coord = bundle.sharedPath[idx]!
1185
- const dc = gridToDrawingCoord(graph, coord)
1186
- const prevDir = determineDirection(bundle.sharedPath[idx - 1]!, coord)
1187
- const nextDir = determineDirection(coord, bundle.sharedPath[idx + 1]!)
1188
- if (dirEquals(prevDir, nextDir)) continue
1189
-
1190
- let corner: string
1191
- if (!useAscii) {
1192
- if ((dirEquals(prevDir, Right) && dirEquals(nextDir, Down)) ||
1193
- (dirEquals(prevDir, Up) && dirEquals(nextDir, Left))) {
1194
- corner = '┐'
1195
- } else if ((dirEquals(prevDir, Right) && dirEquals(nextDir, Up)) ||
1196
- (dirEquals(prevDir, Down) && dirEquals(nextDir, Left))) {
1197
- corner = '┘'
1198
- } else if ((dirEquals(prevDir, Left) && dirEquals(nextDir, Down)) ||
1199
- (dirEquals(prevDir, Up) && dirEquals(nextDir, Right))) {
1200
- corner = '┌'
1201
- } else if ((dirEquals(prevDir, Left) && dirEquals(nextDir, Up)) ||
1202
- (dirEquals(prevDir, Down) && dirEquals(nextDir, Right))) {
1203
- corner = '└'
1204
- } else {
1205
- corner = '+'
1206
- }
1207
- } else {
1208
- corner = '+'
1209
- }
1210
-
1211
- cornersCanvas[dc.x]![dc.y] = corner
1212
- }
1213
-
1214
- return [pathCanvas, cornersCanvas]
1215
- }
1216
-
1217
- /**
1218
- * Draw the arrowhead for a fan-in bundle (single arrowhead at the shared target).
1219
- */
1220
- function drawBundleArrowhead(graph: AsciiGraph, bundle: EdgeBundle): Canvas {
1221
- const canvas = copyCanvas(graph.canvas)
1222
-
1223
- if (bundle.sharedPath.length < 2) return canvas
1224
-
1225
- // Get the last segment direction
1226
- const lastIdx = bundle.sharedPath.length - 1
1227
- const secondLast = bundle.sharedPath[lastIdx - 1]!
1228
- const last = bundle.sharedPath[lastIdx]!
1229
- const dir = determineDirection(secondLast, last)
1230
-
1231
- // Get drawing coord 1 char outside the target node's border (not on the border itself).
1232
- // This matches non-bundled edges where drawPath uses offsetTo=-1 and the arrowhead
1233
- // sits at the last drawn point (1 char before the border).
1234
- const graphDir = graph.config.graphDirection
1235
- const entryDir = graphDir === 'TD' ? Up : Left
1236
- const dc = getNodeAttachmentPoint(graph, bundle.sharedNode, entryDir)
1237
- // Offset 1 char away from the box border so arrowhead sits outside the box
1238
- if (graphDir === 'TD') dc.y -= 1
1239
- else dc.x -= 1
1240
-
1241
- // Draw arrowhead
1242
- let char: string
1243
- if (!graph.config.useAscii) {
1244
- if (dirEquals(dir, Up)) char = '▲'
1245
- else if (dirEquals(dir, Down)) char = '▼'
1246
- else if (dirEquals(dir, Left)) char = '◄'
1247
- else if (dirEquals(dir, Right)) char = '►'
1248
- else char = '▼' // default
1249
- } else {
1250
- if (dirEquals(dir, Up)) char = '^'
1251
- else if (dirEquals(dir, Down)) char = 'v'
1252
- else if (dirEquals(dir, Left)) char = '<'
1253
- else if (dirEquals(dir, Right)) char = '>'
1254
- else char = 'v' // default
1255
- }
1256
-
1257
- canvas[dc.x]![dc.y] = char
1258
- return canvas
1259
- }
1260
-
1261
- /**
1262
- * Draw the arrowhead for a single edge in a fan-out bundle.
1263
- */
1264
- function drawBundledEdgeArrowhead(graph: AsciiGraph, edge: AsciiEdge): Canvas {
1265
- const canvas = copyCanvas(graph.canvas)
1266
-
1267
- if (!edge.pathToJunction || edge.pathToJunction.length < 2) return canvas
1268
-
1269
- // Get the last segment direction
1270
- const lastIdx = edge.pathToJunction.length - 1
1271
- const secondLast = edge.pathToJunction[lastIdx - 1]!
1272
- const last = edge.pathToJunction[lastIdx]!
1273
- const dir = determineDirection(secondLast, last)
1274
-
1275
- // Get drawing coord 1 char outside the target node's border
1276
- const graphDir = graph.config.graphDirection
1277
- const entryDir = graphDir === 'TD' ? Up : Left
1278
- const dc = getNodeAttachmentPoint(graph, edge.to, entryDir)
1279
- // Offset 1 char away from the box border so arrowhead sits outside the box
1280
- if (graphDir === 'TD') dc.y -= 1
1281
- else dc.x -= 1
1282
-
1283
- // Draw arrowhead
1284
- let char: string
1285
- if (!graph.config.useAscii) {
1286
- if (dirEquals(dir, Up)) char = '▲'
1287
- else if (dirEquals(dir, Down)) char = '▼'
1288
- else if (dirEquals(dir, Left)) char = '◄'
1289
- else if (dirEquals(dir, Right)) char = '►'
1290
- else char = '▼' // default
1291
- } else {
1292
- if (dirEquals(dir, Up)) char = '^'
1293
- else if (dirEquals(dir, Down)) char = 'v'
1294
- else if (dirEquals(dir, Left)) char = '<'
1295
- else if (dirEquals(dir, Right)) char = '>'
1296
- else char = 'v' // default
1297
- }
1298
-
1299
- canvas[dc.x]![dc.y] = char
1300
- return canvas
1301
- }
1302
-
1303
- /**
1304
- * Draw the junction character where bundled edges merge/split.
1305
- *
1306
- * Analyzes actual connecting directions to choose the correct character:
1307
- * - ┼ (cross): lines from all 4 directions
1308
- * - ┬ (T down): lines from left, right, and down
1309
- * - ┴ (T up): lines from left, right, and up
1310
- * - ├ (T right): lines from up, down, and right
1311
- * - ┤ (T left): lines from up, down, and left
1312
- */
1313
- function drawJunctionCharacter(graph: AsciiGraph, bundle: EdgeBundle): Canvas {
1314
- const canvas = copyCanvas(graph.canvas)
1315
-
1316
- if (!bundle.junctionPoint) return canvas
1317
-
1318
- const dc = gridToDrawingCoord(graph, bundle.junctionPoint)
1319
- const useAscii = graph.config.useAscii
1320
-
1321
- // Analyze what directions actually connect to the junction
1322
- let hasUp = false
1323
- let hasDown = false
1324
- let hasLeft = false
1325
- let hasRight = false
1326
-
1327
- // Check shared path direction (where the line continues to/from the shared node)
1328
- if (bundle.sharedPath.length >= 2) {
1329
- // For fan-in: shared path goes FROM junction TO target (index 0 is junction)
1330
- // For fan-out: shared path goes FROM source TO junction (last index is junction)
1331
- const junctionIdx = bundle.type === 'fan-in' ? 0 : bundle.sharedPath.length - 1
1332
- const adjacentIdx = bundle.type === 'fan-in' ? 1 : bundle.sharedPath.length - 2
1333
- const sharedDir = determineDirection(
1334
- bundle.sharedPath[junctionIdx]!,
1335
- bundle.sharedPath[adjacentIdx]!
1336
- )
1337
- // This is the direction the shared path GOES from junction
1338
- if (dirEquals(sharedDir, Down)) hasDown = true
1339
- else if (dirEquals(sharedDir, Up)) hasUp = true
1340
- else if (dirEquals(sharedDir, Right)) hasRight = true
1341
- else if (dirEquals(sharedDir, Left)) hasLeft = true
1342
- }
1343
-
1344
- // Check each edge's path direction at the junction
1345
- for (const edge of bundle.edges) {
1346
- if (edge.pathToJunction && edge.pathToJunction.length >= 2) {
1347
- // For fan-in: pathToJunction goes FROM source TO junction (last is junction)
1348
- // For fan-out: pathToJunction goes FROM junction TO target (first is junction)
1349
- const junctionIdx = bundle.type === 'fan-in'
1350
- ? edge.pathToJunction.length - 1
1351
- : 0
1352
- const adjacentIdx = bundle.type === 'fan-in'
1353
- ? edge.pathToJunction.length - 2
1354
- : 1
1355
-
1356
- const arrivalDir = determineDirection(
1357
- edge.pathToJunction[adjacentIdx]!,
1358
- edge.pathToJunction[junctionIdx]!
1359
- )
1360
- // This is the direction the edge ARRIVES at junction from
1361
- // e.g., if arrivalDir is Right, the line comes FROM the left
1362
- if (dirEquals(arrivalDir, Down)) hasUp = true // arrived going down = came from up
1363
- else if (dirEquals(arrivalDir, Up)) hasDown = true
1364
- else if (dirEquals(arrivalDir, Right)) hasLeft = true
1365
- else if (dirEquals(arrivalDir, Left)) hasRight = true
1366
- }
1367
- }
1368
-
1369
- // Select character based on connected directions
1370
- let char: string
1371
- if (!useAscii) {
1372
- if (hasUp && hasDown && hasLeft && hasRight) {
1373
- char = '┼' // cross - all 4 directions
1374
- } else if (hasDown && hasLeft && hasRight && !hasUp) {
1375
- char = '┬' // T pointing down
1376
- } else if (hasUp && hasLeft && hasRight && !hasDown) {
1377
- char = '┴' // T pointing up
1378
- } else if (hasUp && hasDown && hasRight && !hasLeft) {
1379
- char = '├' // T pointing right
1380
- } else if (hasUp && hasDown && hasLeft && !hasRight) {
1381
- char = '┤' // T pointing left
1382
- } else if (hasLeft && hasRight) {
1383
- char = '─' // horizontal only
1384
- } else if (hasUp && hasDown) {
1385
- char = '│' // vertical only
1386
- } else if (hasDown && hasRight) {
1387
- char = '┌' // corner
1388
- } else if (hasDown && hasLeft) {
1389
- char = '┐'
1390
- } else if (hasUp && hasRight) {
1391
- char = '└'
1392
- } else if (hasUp && hasLeft) {
1393
- char = '┘'
1394
- } else {
1395
- char = '┼' // fallback
1396
- }
1397
- } else {
1398
- char = '+'
1399
- }
1400
-
1401
- canvas[dc.x]![dc.y] = char
1402
- return canvas
1403
- }
1404
-
1405
- // ============================================================================
1406
- // Subgraph drawing
1407
- // ============================================================================
1408
-
1409
- /** Draw a subgraph border rectangle. */
1410
- export function drawSubgraphBox(sg: AsciiSubgraph, graph: AsciiGraph): Canvas {
1411
- const width = sg.maxX - sg.minX
1412
- const height = sg.maxY - sg.minY
1413
- if (width <= 0 || height <= 0) return mkCanvas(0, 0)
1414
-
1415
- const from: DrawingCoord = { x: 0, y: 0 }
1416
- const to: DrawingCoord = { x: width, y: height }
1417
- const canvas = mkCanvas(width, height)
1418
-
1419
- if (!graph.config.useAscii) {
1420
- for (let x = from.x + 1; x < to.x; x++) canvas[x]![from.y] = '─'
1421
- for (let x = from.x + 1; x < to.x; x++) canvas[x]![to.y] = '─'
1422
- for (let y = from.y + 1; y < to.y; y++) canvas[from.x]![y] = '│'
1423
- for (let y = from.y + 1; y < to.y; y++) canvas[to.x]![y] = '│'
1424
- canvas[from.x]![from.y] = '┌'
1425
- canvas[to.x]![from.y] = '┐'
1426
- canvas[from.x]![to.y] = '└'
1427
- canvas[to.x]![to.y] = '┘'
1428
- } else {
1429
- for (let x = from.x + 1; x < to.x; x++) canvas[x]![from.y] = '-'
1430
- for (let x = from.x + 1; x < to.x; x++) canvas[x]![to.y] = '-'
1431
- for (let y = from.y + 1; y < to.y; y++) canvas[from.x]![y] = '|'
1432
- for (let y = from.y + 1; y < to.y; y++) canvas[to.x]![y] = '|'
1433
- canvas[from.x]![from.y] = '+'
1434
- canvas[to.x]![from.y] = '+'
1435
- canvas[from.x]![to.y] = '+'
1436
- canvas[to.x]![to.y] = '+'
1437
- }
1438
-
1439
- return canvas
1440
- }
1441
-
1442
- /** Draw a subgraph label centered in its header area. Supports multi-line labels. */
1443
- export function drawSubgraphLabel(sg: AsciiSubgraph, graph: AsciiGraph): [Canvas, DrawingCoord] {
1444
- const width = sg.maxX - sg.minX
1445
- const height = sg.maxY - sg.minY
1446
- if (width <= 0 || height <= 0) return [mkCanvas(0, 0), { x: 0, y: 0 }]
1447
-
1448
- const canvas = mkCanvas(width, height)
1449
-
1450
- // Support multi-line subgraph labels
1451
- const lines = splitLines(sg.name)
1452
-
1453
- // Start at row 1 inside subgraph, expand downward for multiple lines
1454
- for (let i = 0; i < lines.length; i++) {
1455
- const line = lines[i]!
1456
- const labelY = 1 + i
1457
- let labelX = Math.floor(width / 2) - Math.floor(line.length / 2)
1458
- if (labelX < 1) labelX = 1
1459
-
1460
- for (let j = 0; j < line.length; j++) {
1461
- if (labelX + j < width && labelY < height) {
1462
- canvas[labelX + j]![labelY] = line[j]!
1463
- }
1464
- }
1465
- }
1466
-
1467
- return [canvas, { x: sg.minX, y: sg.minY }]
1468
- }
1469
-
1470
- // ============================================================================
1471
- // Top-level draw orchestrator
1472
- // ============================================================================
1473
-
1474
- /** Sort subgraphs by nesting depth (shallowest first) for correct layered rendering. */
1475
- function sortSubgraphsByDepth(subgraphs: AsciiSubgraph[]): AsciiSubgraph[] {
1476
- function getDepth(sg: AsciiSubgraph): number {
1477
- return sg.parent === null ? 0 : 1 + getDepth(sg.parent)
1478
- }
1479
- const sorted = [...subgraphs]
1480
- sorted.sort((a, b) => getDepth(a) - getDepth(b))
1481
- return sorted
1482
- }
1483
-
1484
- // ============================================================================
1485
- // Role tracking helpers for colored output
1486
- // ============================================================================
1487
-
1488
- /**
1489
- * Fill roles for all non-space characters in a canvas region.
1490
- * Used after drawing a layer to record what role those characters have.
1491
- */
1492
- function fillRolesFromCanvas(
1493
- roleCanvas: RoleCanvas,
1494
- canvas: Canvas,
1495
- offset: DrawingCoord,
1496
- role: CharRole,
1497
- ): void {
1498
- for (let x = 0; x < canvas.length; x++) {
1499
- for (let y = 0; y < (canvas[0]?.length ?? 0); y++) {
1500
- const char = canvas[x]?.[y]
1501
- if (char && char !== ' ') {
1502
- const rx = x + offset.x
1503
- const ry = y + offset.y
1504
- // Use setRole which auto-expands the role canvas if needed
1505
- if (rx >= 0 && ry >= 0) {
1506
- setRole(roleCanvas, rx, ry, role)
1507
- }
1508
- }
1509
- }
1510
- }
1511
- }
1512
-
1513
- /**
1514
- * Fill roles for multiple canvases with the same role.
1515
- */
1516
- function fillRolesFromCanvases(
1517
- roleCanvas: RoleCanvas,
1518
- canvases: Canvas[],
1519
- offset: DrawingCoord,
1520
- role: CharRole,
1521
- ): void {
1522
- for (const canvas of canvases) {
1523
- fillRolesFromCanvas(roleCanvas, canvas, offset, role)
1524
- }
1525
- }
1526
-
1527
- /**
1528
- * Special handling for node boxes: border chars get 'border' role, text gets 'text' role.
1529
- * Detects text by checking if character is alphanumeric or common punctuation.
1530
- */
1531
- function fillRolesForNodeBox(
1532
- roleCanvas: RoleCanvas,
1533
- canvas: Canvas,
1534
- offset: DrawingCoord,
1535
- ): void {
1536
- const isBorderChar = (c: string) => /^[┌┐└┘├┤┬┴┼│─╭╮╰╯+\-|.':]$/.test(c)
1537
-
1538
- for (let x = 0; x < canvas.length; x++) {
1539
- for (let y = 0; y < (canvas[0]?.length ?? 0); y++) {
1540
- const char = canvas[x]?.[y]
1541
- if (char && char !== ' ') {
1542
- const rx = x + offset.x
1543
- const ry = y + offset.y
1544
- // Use setRole which auto-expands the role canvas if needed
1545
- if (rx >= 0 && ry >= 0) {
1546
- setRole(roleCanvas, rx, ry, isBorderChar(char) ? 'border' : 'text')
1547
- }
1548
- }
1549
- }
1550
- }
1551
- }
1552
-
1553
- /**
1554
- * Main draw function — renders the entire graph onto the canvas.
1555
- * Drawing order matters for correct layering:
1556
- * 1. Subgraph borders (bottom layer)
1557
- * 2. Node boxes
1558
- * 3. Edge paths (lines)
1559
- * 4. Edge corners
1560
- * 5. Arrowheads
1561
- * 6. Box-start junctions
1562
- * 7. Edge labels
1563
- * 8. Subgraph labels (top layer)
1564
- *
1565
- * Also fills the roleCanvas with character roles for colored output.
1566
- */
1567
- export function drawGraph(graph: AsciiGraph): Canvas {
1568
- const useAscii = graph.config.useAscii
1569
- const zero: DrawingCoord = { x: 0, y: 0 }
1570
-
1571
- // Draw subgraph borders
1572
- const sortedSgs = sortSubgraphsByDepth(graph.subgraphs)
1573
- for (const sg of sortedSgs) {
1574
- const sgCanvas = drawSubgraphBox(sg, graph)
1575
- const offset: DrawingCoord = { x: sg.minX, y: sg.minY }
1576
- graph.canvas = mergeCanvases(graph.canvas, offset, useAscii, sgCanvas)
1577
- // Subgraph borders get 'border' role
1578
- fillRolesFromCanvas(graph.roleCanvas, sgCanvas, offset, 'border')
1579
- }
1580
-
1581
- // Draw node boxes
1582
- for (const node of graph.nodes) {
1583
- if (!node.drawn && node.drawingCoord && node.drawing) {
1584
- graph.canvas = mergeCanvases(graph.canvas, node.drawingCoord, useAscii, node.drawing)
1585
- // Node boxes: detect border vs text characters
1586
- fillRolesForNodeBox(graph.roleCanvas, node.drawing, node.drawingCoord)
1587
- node.drawn = true
1588
- }
1589
- }
1590
-
1591
- // Collect all edge drawing layers
1592
- const lineCanvases: Canvas[] = []
1593
- const cornerCanvases: Canvas[] = []
1594
- const arrowHeadEndCanvases: Canvas[] = []
1595
- const arrowHeadStartCanvases: Canvas[] = []
1596
- const boxStartCanvases: Canvas[] = []
1597
- const labelCanvases: Canvas[] = []
1598
- const junctionCanvases: Canvas[] = []
1599
-
1600
- // Track which bundles have been processed (to draw shared paths only once)
1601
- const processedBundles = new Set<EdgeBundle>()
1602
-
1603
- for (const edge of graph.edges) {
1604
- // Handle bundled edges specially
1605
- if (edge.bundle && edge.pathToJunction) {
1606
- const bundle = edge.bundle
1607
-
1608
- // Draw this edge's individual path (source → junction for fan-in, junction → target for fan-out)
1609
- const [pathC, boxStartC, , , cornersC, labelC] = drawBundledEdgeSegment(graph, edge, bundle)
1610
- lineCanvases.push(pathC)
1611
- cornerCanvases.push(cornersC)
1612
- boxStartCanvases.push(boxStartC)
1613
- labelCanvases.push(labelC)
1614
-
1615
- // Draw the bundle's shared path and arrowhead only once
1616
- if (!processedBundles.has(bundle)) {
1617
- processedBundles.add(bundle)
1618
-
1619
- // Draw shared path (junction → target for fan-in, source → junction for fan-out)
1620
- const [sharedPathC, sharedCornersC] = drawBundleSharedPath(graph, bundle)
1621
- lineCanvases.push(sharedPathC)
1622
- cornerCanvases.push(sharedCornersC)
1623
-
1624
- // Draw arrowhead at target for fan-in (once for all edges in bundle)
1625
- if (bundle.type === 'fan-in') {
1626
- const arrowHeadC = drawBundleArrowhead(graph, bundle)
1627
- arrowHeadEndCanvases.push(arrowHeadC)
1628
- }
1629
-
1630
- // Draw junction character
1631
- const junctionC = drawJunctionCharacter(graph, bundle)
1632
- junctionCanvases.push(junctionC)
1633
- }
1634
-
1635
- // For fan-out bundles, draw arrowhead at each target
1636
- if (bundle.type === 'fan-out' && edge.hasArrowEnd) {
1637
- const arrowHeadC = drawBundledEdgeArrowhead(graph, edge)
1638
- arrowHeadEndCanvases.push(arrowHeadC)
1639
- }
1640
- } else {
1641
- // Non-bundled edge: use standard drawing
1642
- const [pathC, boxStartC, arrowHeadEndC, arrowHeadStartC, cornersC, labelC] = drawArrow(graph, edge)
1643
- lineCanvases.push(pathC)
1644
- cornerCanvases.push(cornersC)
1645
- arrowHeadEndCanvases.push(arrowHeadEndC)
1646
- arrowHeadStartCanvases.push(arrowHeadStartC)
1647
- boxStartCanvases.push(boxStartC)
1648
- labelCanvases.push(labelC)
1649
- }
1650
- }
1651
-
1652
- // Merge edge layers in order and track roles
1653
- // Note: arrowHeadStart is merged AFTER boxStart so bidirectional arrows
1654
- // properly overwrite the box connector at the source end
1655
- graph.canvas = mergeCanvases(graph.canvas, zero, useAscii, ...lineCanvases)
1656
- fillRolesFromCanvases(graph.roleCanvas, lineCanvases, zero, 'line')
1657
-
1658
- graph.canvas = mergeCanvases(graph.canvas, zero, useAscii, ...cornerCanvases)
1659
- fillRolesFromCanvases(graph.roleCanvas, cornerCanvases, zero, 'corner')
1660
-
1661
- graph.canvas = mergeCanvases(graph.canvas, zero, useAscii, ...junctionCanvases)
1662
- fillRolesFromCanvases(graph.roleCanvas, junctionCanvases, zero, 'junction')
1663
-
1664
- graph.canvas = mergeCanvases(graph.canvas, zero, useAscii, ...arrowHeadEndCanvases)
1665
- fillRolesFromCanvases(graph.roleCanvas, arrowHeadEndCanvases, zero, 'arrow')
1666
-
1667
- graph.canvas = mergeCanvases(graph.canvas, zero, useAscii, ...boxStartCanvases)
1668
- fillRolesFromCanvases(graph.roleCanvas, boxStartCanvases, zero, 'junction')
1669
-
1670
- graph.canvas = mergeCanvases(graph.canvas, zero, useAscii, ...arrowHeadStartCanvases)
1671
- fillRolesFromCanvases(graph.roleCanvas, arrowHeadStartCanvases, zero, 'arrow')
1672
-
1673
- const trunkJunctionCanvas = copyCanvas(graph.canvas)
1674
- for (const jc of graph.trunkJunctions) {
1675
- const dc = gridToDrawingCoord(graph, jc)
1676
- trunkJunctionCanvas[dc.x]![dc.y] = useAscii ? '+' : (graph.config.graphDirection === 'LR' ? '├' : '┬')
1677
- }
1678
- graph.canvas = mergeCanvases(graph.canvas, zero, useAscii, trunkJunctionCanvas)
1679
- fillRolesFromCanvas(graph.roleCanvas, trunkJunctionCanvas, zero, 'junction')
1680
-
1681
- graph.canvas = mergeCanvases(graph.canvas, zero, useAscii, ...labelCanvases)
1682
- fillRolesFromCanvases(graph.roleCanvas, labelCanvases, zero, 'text')
1683
-
1684
- // Draw subgraph labels last (on top)
1685
- for (const sg of graph.subgraphs) {
1686
- if (sg.nodes.length === 0) continue
1687
- const [labelCanvas, offset] = drawSubgraphLabel(sg, graph)
1688
- graph.canvas = mergeCanvases(graph.canvas, offset, useAscii, labelCanvas)
1689
- fillRolesFromCanvas(graph.roleCanvas, labelCanvas, offset, 'text')
1690
- }
1691
-
1692
- return graph.canvas
1693
- }