agentic-mermaid 0.1.0 → 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.
- package/CHANGELOG.md +149 -4
- package/LICENSES/Apache-2.0.txt +202 -0
- package/README.md +25 -32
- package/THIRD_PARTY_NOTICES.md +29 -0
- package/assets/fonts/FONT-LICENSES.md +9 -5
- package/assets/fonts/Inter-Bold.ttf +0 -0
- package/assets/fonts/Inter-Medium.ttf +0 -0
- package/assets/fonts/Inter-Regular.ttf +0 -0
- package/assets/fonts/Inter-SemiBold.ttf +0 -0
- package/dist/agent-core.d.ts +463 -0
- package/dist/agent-core.js +397 -0
- package/dist/agent.d.ts +81 -1822
- package/dist/agent.js +336 -36839
- package/dist/agentic-mermaid-mcp.js +7 -37908
- package/dist/am.js +33638 -36261
- package/dist/chunk-4QECACIA.js +4702 -0
- package/dist/chunk-CDPKJ2BQ.js +37 -0
- package/dist/chunk-QKDLMOJJ.js +3487 -0
- package/dist/chunk-WI35L7IR.js +615 -0
- package/dist/chunk-WN3YDOBG.js +149 -0
- package/dist/chunk-ZYMMKQ3I.js +62637 -0
- package/dist/index-D54dqSdC.d.ts +6777 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +310 -32836
- package/dist/mcp-cli-SBQSTJFC.js +17 -0
- package/docs/README.md +14 -6
- package/docs/schemas/style-spec.schema.json +1221 -30
- package/examples/styles/catalog.json +54 -0
- package/examples/styles/cloudflare-workers-inspired-prototype.mmd +9 -0
- package/examples/styles/cloudflare-workers-inspired-prototype.style.json +110 -0
- package/examples/styles/cupertino-prototype.mmd +10 -0
- package/examples/styles/cupertino-prototype.style.json +63 -0
- package/examples/styles/mid-century-report.style.json +2 -2
- package/examples/styles/star-chart-atlas.style.json +2 -1
- package/examples/styles/transit-route-map.style.json +2 -2
- package/examples/styles/vercel-inspired-prototype.mmd +6 -0
- package/examples/styles/vercel-inspired-prototype.style.json +87 -0
- package/package.json +70 -43
- package/server.json +41 -0
- package/skills/agentic-mermaid-diagram-workflow/SKILL.md +17 -36
- package/skills/agentic-mermaid-diagram-workflow/references/cli.md +4 -5
- package/skills/agentic-mermaid-diagram-workflow/references/code-mode.md +15 -31
- package/skills/agentic-mermaid-diagram-workflow/references/sequence.md +10 -14
- package/skills/agentic-mermaid-live-editor/SKILL.md +100 -206
- package/AGENT_NATIVE.md +0 -602
- package/Instructions_for_agents.md +0 -83
- package/TODO.md +0 -662
- package/assets/hero.png +0 -0
- package/dist/index-x2Cj1dVv.d.ts +0 -1089
- package/docs/agent-api-cookbook.md +0 -308
- package/docs/agent-mutation-policy.md +0 -104
- package/docs/agent-workflow-examples.md +0 -46
- package/docs/api.md +0 -203
- package/docs/ascii.md +0 -103
- package/docs/assets/improvements/box-start-connector.png +0 -0
- package/docs/assets/improvements/comment-dropped-lint.png +0 -0
- package/docs/assets/improvements/er-content-loss.png +0 -0
- package/docs/assets/improvements/fan-in-trunks.png +0 -0
- package/docs/assets/improvements/gantt-family.png +0 -0
- package/docs/assets/improvements/gantt-milestone-clamp.png +0 -0
- package/docs/assets/improvements/gantt-verify-seam.png +0 -0
- package/docs/assets/improvements/subgraph-container-edges.png +0 -0
- package/docs/assets/improvements/wrapper-frontmatter.png +0 -0
- package/docs/assets/improvements/wrapper-init-directive.png +0 -0
- package/docs/assets/improvements/wrapper-leading-comment.png +0 -0
- package/docs/assets/style-cookbook/mid-century-report.png +0 -0
- package/docs/assets/style-cookbook/star-chart-atlas.png +0 -0
- package/docs/assets/style-cookbook/transit-route-map.png +0 -0
- package/docs/comparison.md +0 -94
- package/docs/config.md +0 -105
- package/docs/contributing/adding-diagram-types.md +0 -144
- package/docs/contributing/diagram-family-citizenship.matrix.json +0 -2127
- package/docs/contributing/diagram-family-citizenship.md +0 -82
- package/docs/contributing/harvesting-upstream-tests.md +0 -126
- package/docs/contributing/lessons-learned.md +0 -276
- package/docs/contributing/releasing.md +0 -71
- package/docs/contributing/visual-review-evidence.md +0 -53
- package/docs/custom-style-cookbook.md +0 -154
- package/docs/design/families/README.md +0 -26
- package/docs/design/families/architecture-beta.md +0 -48
- package/docs/design/families/flowchart-parser-conformance.md +0 -22
- package/docs/design/families/gantt-research.md +0 -137
- package/docs/design/families/gantt.md +0 -329
- package/docs/design/families/journey-agentic-cards-before.png +0 -0
- package/docs/design/families/journey-mermaid-classic-implementation.png +0 -0
- package/docs/design/families/journey-mermaid-classic-prototype.png +0 -0
- package/docs/design/families/journey-mermaid-classic-prototype.svg +0 -163
- package/docs/design/families/journey-mermaid-classic-prototype.svg.png +0 -0
- package/docs/design/families/journey-migration-parity.md +0 -433
- package/docs/design/families/journey-usage-research.md +0 -423
- package/docs/design/families/journey.md +0 -125
- package/docs/design/families/xychart.md +0 -85
- package/docs/design/system/README.md +0 -45
- package/docs/design/system/abstraction-audit.md +0 -224
- package/docs/design/system/abstraction-recommendations.md +0 -655
- package/docs/design/system/architecture.mmd +0 -29
- package/docs/design/system/architecture.svg +0 -137
- package/docs/design/system/layout-guarantees-and-robustness.md +0 -351
- package/docs/design/system/layout-rubric.md +0 -138
- package/docs/design/system/route-contracts.md +0 -848
- package/docs/design/system/source-preservation-ladder.md +0 -52
- package/docs/design/system/styles-rollout.md +0 -216
- package/docs/design/system/ugly-layouts.md +0 -119
- package/docs/diagram-families.md +0 -193
- package/docs/features.md +0 -147
- package/docs/fork-differences.md +0 -130
- package/docs/getting-started.md +0 -175
- package/docs/issue-derived-test-cases.md +0 -156
- package/docs/layout-characterization/README.md +0 -245
- package/docs/layout-characterization/contact-sheet-families.md +0 -496
- package/docs/layout-characterization/contact-sheet.md +0 -638
- package/docs/layout-characterization/properties.md +0 -281
- package/docs/layout-characterization/visual-quality.md +0 -160
- package/docs/layout-characterization/visual-snapshots/architecture.svg +0 -79
- package/docs/layout-characterization/visual-snapshots/class.svg +0 -60
- package/docs/layout-characterization/visual-snapshots/er.svg +0 -61
- package/docs/layout-characterization/visual-snapshots/flowchart.svg +0 -58
- package/docs/layout-characterization/visual-snapshots/gantt.svg +0 -52
- package/docs/layout-characterization/visual-snapshots/journey.svg +0 -118
- package/docs/layout-characterization/visual-snapshots/pie.svg +0 -36
- package/docs/layout-characterization/visual-snapshots/quadrant.svg +0 -47
- package/docs/layout-characterization/visual-snapshots/sequence.svg +0 -59
- package/docs/layout-characterization/visual-snapshots/state.svg +0 -52
- package/docs/layout-characterization/visual-snapshots/timeline.svg +0 -64
- package/docs/layout-characterization/visual-snapshots/xychart.svg +0 -97
- package/docs/mcp-code-mode-rationale.md +0 -101
- package/docs/mcp-http-transport.md +0 -223
- package/docs/mermaid-layout-complaints.md +0 -837
- package/docs/mutation-testing.md +0 -206
- package/docs/project/divergences.md +0 -98
- package/docs/project/dogfooding-docs-strategy.md +0 -66
- package/docs/project/hosted-mcp-cloudflare-plan.md +0 -338
- package/docs/project/lessons-learned.md +0 -1019
- package/docs/project/remove-role-styling-plan.md +0 -176
- package/docs/quality.md +0 -212
- package/docs/react.md +0 -97
- package/docs/style-authoring.md +0 -133
- package/docs/testing-strategy.md +0 -335
- package/docs/theming.md +0 -162
- package/examples/agent-improve-auth-flow.ts +0 -206
- package/examples/agent-loop.ts +0 -53
- package/examples/mcp-vs-cli-complex-diagrams.ts +0 -133
- package/llms.txt +0 -104
- package/skill-evals/README.md +0 -77
- package/skill-evals/fixtures/architecture-cache/input.mmd +0 -5
- package/skill-evals/fixtures/class-payment/input.mmd +0 -4
- package/skill-evals/fixtures/er-orders/input.mmd +0 -10
- package/skill-evals/fixtures/flowchart-cache/input.mmd +0 -3
- package/skill-evals/fixtures/gantt-release/input.mmd +0 -7
- package/skill-evals/fixtures/journey-checkout/input.mmd +0 -6
- package/skill-evals/fixtures/pie-exports/input.mmd +0 -5
- package/skill-evals/fixtures/quadrant-priorities/input.mmd +0 -11
- package/skill-evals/fixtures/sequence-retry/input.mmd +0 -8
- package/skill-evals/fixtures/state-lifecycle/input.mmd +0 -5
- package/skill-evals/fixtures/timeline-release/input.mmd +0 -6
- package/skill-evals/fixtures/xychart-growth/input.mmd +0 -5
- package/skill-evals/shared-benchmark.json +0 -510
- package/src/agent/analyze.ts +0 -157
- package/src/agent/apply.ts +0 -146
- package/src/agent/architecture-body.ts +0 -458
- package/src/agent/certificates.ts +0 -110
- package/src/agent/class-body.ts +0 -319
- package/src/agent/core.ts +0 -207
- package/src/agent/create.ts +0 -106
- package/src/agent/describe.ts +0 -364
- package/src/agent/er-body.ts +0 -224
- package/src/agent/facts.ts +0 -337
- package/src/agent/families-builtin.ts +0 -934
- package/src/agent/families.ts +0 -232
- package/src/agent/family-layouts.ts +0 -826
- package/src/agent/flowchart-body.ts +0 -372
- package/src/agent/flowchart-unsupported.ts +0 -243
- package/src/agent/gantt-body.ts +0 -495
- package/src/agent/index.ts +0 -10
- package/src/agent/journey-body.ts +0 -414
- package/src/agent/label-metrics.ts +0 -28
- package/src/agent/layout-to-rendered.ts +0 -63
- package/src/agent/mutate.ts +0 -97
- package/src/agent/mutation-ops.ts +0 -51
- package/src/agent/op-schema.ts +0 -439
- package/src/agent/parse.ts +0 -223
- package/src/agent/pie-body.ts +0 -270
- package/src/agent/png.ts +0 -117
- package/src/agent/quadrant-body.ts +0 -336
- package/src/agent/quality.ts +0 -363
- package/src/agent/readability-audit.ts +0 -92
- package/src/agent/rendered-route-audit.ts +0 -61
- package/src/agent/sequence-body.ts +0 -345
- package/src/agent/serialize.ts +0 -214
- package/src/agent/state-body.ts +0 -589
- package/src/agent/structural-count.ts +0 -150
- package/src/agent/timeline-body.ts +0 -280
- package/src/agent/trace-log.ts +0 -43
- package/src/agent/types.ts +0 -979
- package/src/agent/verify.ts +0 -571
- package/src/agent/xychart-body.ts +0 -472
- package/src/architecture/config.ts +0 -282
- package/src/architecture/layout.ts +0 -496
- package/src/architecture/parser.ts +0 -375
- package/src/architecture/renderer.ts +0 -611
- package/src/architecture/types.ts +0 -113
- package/src/ascii/ansi.ts +0 -423
- package/src/ascii/architecture.ts +0 -210
- package/src/ascii/canvas.ts +0 -443
- package/src/ascii/class-diagram.ts +0 -697
- package/src/ascii/converter.ts +0 -475
- package/src/ascii/draw.ts +0 -1693
- package/src/ascii/edge-bundling.ts +0 -328
- package/src/ascii/edge-routing.ts +0 -330
- package/src/ascii/er-diagram.ts +0 -439
- package/src/ascii/gantt.ts +0 -276
- package/src/ascii/grid.ts +0 -747
- package/src/ascii/index.ts +0 -196
- package/src/ascii/journey.ts +0 -189
- package/src/ascii/meta.ts +0 -244
- package/src/ascii/multiline-utils.ts +0 -80
- package/src/ascii/pathfinder.ts +0 -301
- package/src/ascii/pie.ts +0 -67
- package/src/ascii/quadrant.ts +0 -158
- package/src/ascii/reverse.ts +0 -216
- package/src/ascii/sequence.ts +0 -499
- package/src/ascii/shapes/circle.ts +0 -27
- package/src/ascii/shapes/corners.ts +0 -143
- package/src/ascii/shapes/diamond.ts +0 -27
- package/src/ascii/shapes/hexagon.ts +0 -27
- package/src/ascii/shapes/index.ts +0 -106
- package/src/ascii/shapes/rectangle.ts +0 -168
- package/src/ascii/shapes/rounded.ts +0 -27
- package/src/ascii/shapes/special.ts +0 -332
- package/src/ascii/shapes/stadium.ts +0 -110
- package/src/ascii/shapes/state.ts +0 -192
- package/src/ascii/shapes/types.ts +0 -73
- package/src/ascii/timeline.ts +0 -128
- package/src/ascii/types.ts +0 -312
- package/src/ascii/validate.ts +0 -120
- package/src/ascii/width.ts +0 -81
- package/src/ascii/xychart.ts +0 -919
- package/src/browser.ts +0 -29
- package/src/class/layout.ts +0 -240
- package/src/class/parser.ts +0 -332
- package/src/class/renderer.ts +0 -638
- package/src/class/types.ts +0 -129
- package/src/cli/agent-instructions.ts +0 -1
- package/src/cli/am-bin.ts +0 -6
- package/src/cli/exit-codes.ts +0 -18
- package/src/cli/index.ts +0 -1117
- package/src/cli/init-agent.ts +0 -147
- package/src/color-resolver.ts +0 -152
- package/src/elk-instance.ts +0 -114
- package/src/er/layout.ts +0 -187
- package/src/er/parser.ts +0 -226
- package/src/er/renderer.ts +0 -792
- package/src/er/types.ts +0 -99
- package/src/font-manifest.ts +0 -27
- package/src/gantt/layout.ts +0 -365
- package/src/gantt/parser.ts +0 -282
- package/src/gantt/pipeline.ts +0 -27
- package/src/gantt/renderer.ts +0 -414
- package/src/gantt/schedule.ts +0 -513
- package/src/gantt/types.ts +0 -255
- package/src/index.ts +0 -368
- package/src/journey/layout.ts +0 -520
- package/src/journey/parser.ts +0 -218
- package/src/journey/renderer.ts +0 -811
- package/src/journey/types.ts +0 -144
- package/src/layout/geometry.ts +0 -182
- package/src/layout/pass.ts +0 -142
- package/src/layout/passes/index.ts +0 -3916
- package/src/layout-engine.ts +0 -1565
- package/src/layout-rubric.ts +0 -659
- package/src/mcp/artifacts.ts +0 -137
- package/src/mcp/dynamic-harness.ts +0 -31
- package/src/mcp/dynamic-modules.d.ts +0 -7
- package/src/mcp/facade.ts +0 -608
- package/src/mcp/harness-runtime.ts +0 -203
- package/src/mcp/hosted-server.ts +0 -468
- package/src/mcp/mcp-bin.ts +0 -3
- package/src/mcp/mcp-cli.ts +0 -82
- package/src/mcp/protocol.ts +0 -13
- package/src/mcp/sandbox.ts +0 -195
- package/src/mcp/sdk-decl.ts +0 -387
- package/src/mcp/server.ts +0 -362
- package/src/mcp/tool-surface.ts +0 -146
- package/src/mermaid-source.ts +0 -655
- package/src/multiline-utils.ts +0 -228
- package/src/parser.ts +0 -1031
- package/src/pie/layout.ts +0 -168
- package/src/pie/parser.ts +0 -136
- package/src/pie/renderer.ts +0 -234
- package/src/pie/types.ts +0 -82
- package/src/quadrant/layout.ts +0 -191
- package/src/quadrant/parser.ts +0 -183
- package/src/quadrant/renderer.ts +0 -322
- package/src/quadrant/types.ts +0 -113
- package/src/render-family-hooks.ts +0 -244
- package/src/renderer.ts +0 -1099
- package/src/route-contracts.ts +0 -2723
- package/src/scene/backend.ts +0 -76
- package/src/scene/fidelity.ts +0 -183
- package/src/scene/hybrid-backend.ts +0 -174
- package/src/scene/ir.ts +0 -181
- package/src/scene/marks.ts +0 -127
- package/src/scene/rough-backend.ts +0 -378
- package/src/scene/seed.ts +0 -46
- package/src/scene/style-registry.ts +0 -556
- package/src/sequence/layout.ts +0 -456
- package/src/sequence/parser.ts +0 -255
- package/src/sequence/renderer.ts +0 -604
- package/src/sequence/types.ts +0 -154
- package/src/shape-clipping.ts +0 -351
- package/src/shared/accessibility-directives.ts +0 -22
- package/src/shared/batched.ts +0 -61
- package/src/shared/color-math.ts +0 -72
- package/src/shared/svg-a11y.ts +0 -29
- package/src/shared/syntax-error.ts +0 -17
- package/src/shared/unicode-ranges.ts +0 -135
- package/src/styles.ts +0 -318
- package/src/svg-paths.ts +0 -28
- package/src/text-metrics.ts +0 -248
- package/src/theme.ts +0 -639
- package/src/timeline/layout.ts +0 -381
- package/src/timeline/parser.ts +0 -215
- package/src/timeline/renderer.ts +0 -538
- package/src/timeline/types.ts +0 -105
- package/src/types.ts +0 -454
- package/src/website-worker-modules.d.ts +0 -18
- package/src/xychart/axis-utils.ts +0 -109
- package/src/xychart/colors.ts +0 -124
- package/src/xychart/config.ts +0 -88
- package/src/xychart/layout.ts +0 -656
- package/src/xychart/parser.ts +0 -504
- package/src/xychart/renderer.ts +0 -640
- package/src/xychart/types.ts +0 -282
package/src/ascii/sequence.ts
DELETED
|
@@ -1,499 +0,0 @@
|
|
|
1
|
-
// ============================================================================
|
|
2
|
-
// ASCII renderer — sequence diagrams
|
|
3
|
-
//
|
|
4
|
-
// Renders sequenceDiagram text to ASCII/Unicode art using a column-based layout.
|
|
5
|
-
// Each actor occupies a column with a vertical lifeline; messages are horizontal
|
|
6
|
-
// arrows between lifelines. Blocks (loop/alt/opt/par) wrap around message groups.
|
|
7
|
-
//
|
|
8
|
-
// Layout is fundamentally different from flowcharts — no grid or A* pathfinding.
|
|
9
|
-
// Instead: actors → columns, messages → rows, all positioned linearly.
|
|
10
|
-
// ============================================================================
|
|
11
|
-
|
|
12
|
-
import { parseSequenceDiagram } from '../sequence/parser.ts'
|
|
13
|
-
import type { SequenceDiagram, Block } from '../sequence/types.ts'
|
|
14
|
-
import type { Canvas, AsciiConfig, RoleCanvas, CharRole, AsciiTheme, ColorMode } from './types.ts'
|
|
15
|
-
import { mkCanvas, mkRoleCanvas, canvasToString, increaseSize, increaseRoleCanvasSize, setRole } from './canvas.ts'
|
|
16
|
-
import { splitLines, maxLineWidth, lineCount } from './multiline-utils.ts'
|
|
17
|
-
import { visualWidth, charVisualWidth } from './width.ts'
|
|
18
|
-
|
|
19
|
-
/** Classify a box-drawing character as 'border' or 'text'. */
|
|
20
|
-
function classifyBoxChar(ch: string): CharRole {
|
|
21
|
-
if (/^[┌┐└┘├┤┬┴┼│─╭╮╰╯+\-|]$/.test(ch)) return 'border'
|
|
22
|
-
return 'text'
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Render a Mermaid sequence diagram to ASCII/Unicode text.
|
|
27
|
-
*
|
|
28
|
-
* Pipeline: parse → layout (columns + rows) → draw onto canvas → string.
|
|
29
|
-
*/
|
|
30
|
-
export function renderSequenceAscii(text: string, config: AsciiConfig, colorMode?: ColorMode, theme?: AsciiTheme): string {
|
|
31
|
-
const lines = text.split('\n').map(l => l.trim()).filter(l => l.length > 0 && !l.startsWith('%%'))
|
|
32
|
-
const diagram = parseSequenceDiagram(lines)
|
|
33
|
-
|
|
34
|
-
if (diagram.actors.length === 0) return ''
|
|
35
|
-
|
|
36
|
-
const useAscii = config.useAscii
|
|
37
|
-
|
|
38
|
-
// Box-drawing characters
|
|
39
|
-
const H = useAscii ? '-' : '─'
|
|
40
|
-
const V = useAscii ? '|' : '│'
|
|
41
|
-
const TL = useAscii ? '+' : '┌'
|
|
42
|
-
const TR = useAscii ? '+' : '┐'
|
|
43
|
-
const BL = useAscii ? '+' : '└'
|
|
44
|
-
const BR = useAscii ? '+' : '┘'
|
|
45
|
-
const JT = useAscii ? '+' : '┬' // top junction on lifeline
|
|
46
|
-
const JB = useAscii ? '+' : '┴' // bottom junction on lifeline
|
|
47
|
-
const JL = useAscii ? '+' : '├' // left junction
|
|
48
|
-
const JR = useAscii ? '+' : '┤' // right junction
|
|
49
|
-
|
|
50
|
-
// ---- LAYOUT: compute lifeline X positions ----
|
|
51
|
-
|
|
52
|
-
const actorIdx = new Map<string, number>()
|
|
53
|
-
diagram.actors.forEach((a, i) => actorIdx.set(a.id, i))
|
|
54
|
-
|
|
55
|
-
const boxPad = 1
|
|
56
|
-
// Use max line width for multi-line actor labels
|
|
57
|
-
const actorBoxWidths = diagram.actors.map(a => maxLineWidth(a.label) + 2 * boxPad + 2)
|
|
58
|
-
const halfBox = actorBoxWidths.map(w => Math.ceil(w / 2))
|
|
59
|
-
// Calculate actor box heights based on number of lines in label
|
|
60
|
-
const actorBoxHeights = diagram.actors.map(a => lineCount(a.label) + 2) // lines + top/bottom border
|
|
61
|
-
const actorBoxH = Math.max(...actorBoxHeights, 3) // Use max height for consistent lifeline positioning
|
|
62
|
-
|
|
63
|
-
// Compute minimum gap between adjacent lifelines based on message labels.
|
|
64
|
-
// For messages spanning multiple actors, distribute the required width across gaps.
|
|
65
|
-
const adjMaxWidth: number[] = new Array(Math.max(diagram.actors.length - 1, 0)).fill(0)
|
|
66
|
-
|
|
67
|
-
for (const msg of diagram.messages) {
|
|
68
|
-
const fi = actorIdx.get(msg.from)!
|
|
69
|
-
const ti = actorIdx.get(msg.to)!
|
|
70
|
-
if (fi === ti) continue // self-messages don't affect spacing
|
|
71
|
-
const lo = Math.min(fi, ti)
|
|
72
|
-
const hi = Math.max(fi, ti)
|
|
73
|
-
// Required gap per span = (max line width + arrow decorations) / number of gaps
|
|
74
|
-
const needed = maxLineWidth(msg.label) + 4
|
|
75
|
-
const numGaps = hi - lo
|
|
76
|
-
const perGap = Math.ceil(needed / numGaps)
|
|
77
|
-
for (let g = lo; g < hi; g++) {
|
|
78
|
-
adjMaxWidth[g] = Math.max(adjMaxWidth[g]!, perGap)
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Compute lifeline x-positions (greedy left-to-right)
|
|
83
|
-
const llX: number[] = [halfBox[0]!]
|
|
84
|
-
for (let i = 1; i < diagram.actors.length; i++) {
|
|
85
|
-
const gap = Math.max(
|
|
86
|
-
halfBox[i - 1]! + halfBox[i]! + 2,
|
|
87
|
-
adjMaxWidth[i - 1]! + 2,
|
|
88
|
-
10,
|
|
89
|
-
)
|
|
90
|
-
llX[i] = llX[i - 1]! + gap
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// ---- LAYOUT: compute vertical positions for messages ----
|
|
94
|
-
|
|
95
|
-
// For each message index, track the y where its arrow is drawn.
|
|
96
|
-
// Also track block start/end y positions and divider y positions.
|
|
97
|
-
const msgArrowY: number[] = []
|
|
98
|
-
const msgLabelY: number[] = []
|
|
99
|
-
const blockStartY = new Map<number, number>()
|
|
100
|
-
const blockEndY = new Map<number, number>()
|
|
101
|
-
const divYMap = new Map<string, number>() // "blockIdx:divIdx" → y
|
|
102
|
-
const notePositions: Array<{ x: number; y: number; width: number; height: number; lines: string[] }> = []
|
|
103
|
-
|
|
104
|
-
let curY = actorBoxH // start right below header boxes
|
|
105
|
-
|
|
106
|
-
const positionNote = (note: SequenceDiagram['notes'][number], y: number) => {
|
|
107
|
-
const nLines = splitLines(note.text)
|
|
108
|
-
const nWidth = Math.max(...nLines.map(line => maxLineWidth(line)), 0) + 4
|
|
109
|
-
const nHeight = nLines.length + 2
|
|
110
|
-
const aIdx = actorIdx.get(note.actorIds[0]!) ?? 0
|
|
111
|
-
let nx: number
|
|
112
|
-
if (note.position === 'left') {
|
|
113
|
-
nx = llX[aIdx]! - nWidth - 1
|
|
114
|
-
} else if (note.position === 'right') {
|
|
115
|
-
nx = llX[aIdx]! + 2
|
|
116
|
-
} else if (note.actorIds.length >= 2) {
|
|
117
|
-
const aIdx2 = actorIdx.get(note.actorIds[note.actorIds.length - 1]!) ?? aIdx
|
|
118
|
-
nx = Math.floor((llX[aIdx]! + llX[aIdx2]!) / 2) - Math.floor(nWidth / 2)
|
|
119
|
-
} else {
|
|
120
|
-
nx = llX[aIdx]! - Math.floor(nWidth / 2)
|
|
121
|
-
}
|
|
122
|
-
return { x: Math.max(0, nx), y, width: nWidth, height: nHeight, lines: nLines }
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
for (const note of diagram.notes) {
|
|
126
|
-
if (note.afterIndex !== -1) continue
|
|
127
|
-
curY += 1
|
|
128
|
-
const positioned = positionNote(note, curY)
|
|
129
|
-
notePositions.push(positioned)
|
|
130
|
-
curY += positioned.height
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
for (let m = 0; m < diagram.messages.length; m++) {
|
|
134
|
-
// Block openings at this message
|
|
135
|
-
for (let b = 0; b < diagram.blocks.length; b++) {
|
|
136
|
-
if (diagram.blocks[b]!.startIndex === m) {
|
|
137
|
-
curY += 2 // 1 blank + 1 header row
|
|
138
|
-
blockStartY.set(b, curY - 1)
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Dividers at this message index
|
|
143
|
-
for (let b = 0; b < diagram.blocks.length; b++) {
|
|
144
|
-
for (let d = 0; d < diagram.blocks[b]!.dividers.length; d++) {
|
|
145
|
-
if (diagram.blocks[b]!.dividers[d]!.index === m) {
|
|
146
|
-
curY += 1
|
|
147
|
-
divYMap.set(`${b}:${d}`, curY)
|
|
148
|
-
curY += 1
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
curY += 1 // blank row before message
|
|
154
|
-
|
|
155
|
-
const msg = diagram.messages[m]!
|
|
156
|
-
const isSelf = msg.from === msg.to
|
|
157
|
-
|
|
158
|
-
// Calculate height needed for multi-line message labels
|
|
159
|
-
const msgLineCount = lineCount(msg.label)
|
|
160
|
-
|
|
161
|
-
if (isSelf) {
|
|
162
|
-
// Self-message occupies 3+ rows: top-arm, label-col(s), bottom-arm
|
|
163
|
-
msgLabelY[m] = curY + 1
|
|
164
|
-
msgArrowY[m] = curY
|
|
165
|
-
curY += 2 + msgLineCount // top-arm + label lines + bottom-arm
|
|
166
|
-
} else {
|
|
167
|
-
// Normal message: label row(s) then arrow row
|
|
168
|
-
msgLabelY[m] = curY
|
|
169
|
-
msgArrowY[m] = curY + msgLineCount // arrow goes after all label lines
|
|
170
|
-
curY += msgLineCount + 1 // label lines + arrow row
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Notes after this message
|
|
174
|
-
for (let n = 0; n < diagram.notes.length; n++) {
|
|
175
|
-
if (diagram.notes[n]!.afterIndex === m) {
|
|
176
|
-
curY += 1
|
|
177
|
-
const note = diagram.notes[n]!
|
|
178
|
-
const positioned = positionNote(note, curY)
|
|
179
|
-
notePositions.push(positioned)
|
|
180
|
-
curY += positioned.height
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Block closings after this message
|
|
185
|
-
for (let b = 0; b < diagram.blocks.length; b++) {
|
|
186
|
-
if (diagram.blocks[b]!.endIndex === m) {
|
|
187
|
-
curY += 1
|
|
188
|
-
blockEndY.set(b, curY)
|
|
189
|
-
curY += 1
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
curY += 1 // gap before footer
|
|
195
|
-
const footerY = curY
|
|
196
|
-
const totalH = footerY + actorBoxH
|
|
197
|
-
|
|
198
|
-
// Total canvas width
|
|
199
|
-
const lastLL = llX[llX.length - 1] ?? 0
|
|
200
|
-
const lastHalf = halfBox[halfBox.length - 1] ?? 0
|
|
201
|
-
let totalW = lastLL + lastHalf + 2
|
|
202
|
-
|
|
203
|
-
// Ensure canvas is wide enough for self-message labels and notes.
|
|
204
|
-
// Use maxLineWidth so multi-line self labels (e.g. "hello<br/>world") get
|
|
205
|
-
// sized by the longest line rather than the raw concatenated string length.
|
|
206
|
-
for (let m = 0; m < diagram.messages.length; m++) {
|
|
207
|
-
const msg = diagram.messages[m]!
|
|
208
|
-
if (msg.from === msg.to) {
|
|
209
|
-
const fi = actorIdx.get(msg.from)!
|
|
210
|
-
const selfRight = llX[fi]! + 6 + 2 + maxLineWidth(msg.label)
|
|
211
|
-
totalW = Math.max(totalW, selfRight + 1)
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
for (const np of notePositions) {
|
|
215
|
-
totalW = Math.max(totalW, np.x + np.width + 1)
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const canvas = mkCanvas(totalW, totalH - 1)
|
|
219
|
-
const rc = mkRoleCanvas(totalW, totalH - 1)
|
|
220
|
-
|
|
221
|
-
/** Set a character on the canvas and track its role. */
|
|
222
|
-
function setC(x: number, y: number, ch: string, role: CharRole): void {
|
|
223
|
-
if (x >= 0 && x < canvas.length && y >= 0 && y < (canvas[0]?.length ?? 0)) {
|
|
224
|
-
canvas[x]![y] = ch
|
|
225
|
-
setRole(rc, x, y, role)
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// ---- DRAW: helper to place a bordered actor box (supports multi-line labels) ----
|
|
230
|
-
|
|
231
|
-
function drawActorBox(cx: number, topY: number, label: string): void {
|
|
232
|
-
const lines = splitLines(label)
|
|
233
|
-
const maxW = maxLineWidth(label)
|
|
234
|
-
const w = maxW + 2 * boxPad + 2
|
|
235
|
-
const h = lines.length + 2 // lines + top/bottom border
|
|
236
|
-
const left = cx - Math.floor(w / 2)
|
|
237
|
-
|
|
238
|
-
// Top border
|
|
239
|
-
setC(left, topY, TL, 'border')
|
|
240
|
-
for (let x = 1; x < w - 1; x++) setC(left + x, topY, H, 'border')
|
|
241
|
-
setC(left + w - 1, topY, TR, 'border')
|
|
242
|
-
|
|
243
|
-
// Content lines (centered horizontally within the box).
|
|
244
|
-
// Use terminal-column width so CJK / emoji labels sit symmetric in the
|
|
245
|
-
// box, and iterate codepoints (not UTF-16 units) so a single emoji
|
|
246
|
-
// surrogate pair counts as one glyph.
|
|
247
|
-
for (let i = 0; i < lines.length; i++) {
|
|
248
|
-
const row = topY + 1 + i
|
|
249
|
-
setC(left, row, V, 'border')
|
|
250
|
-
setC(left + w - 1, row, V, 'border')
|
|
251
|
-
const line = lines[i]!
|
|
252
|
-
const lineW = visualWidth(line)
|
|
253
|
-
const ls = left + 1 + boxPad + Math.floor((maxW - lineW) / 2)
|
|
254
|
-
let cursor = ls
|
|
255
|
-
for (const ch of line) {
|
|
256
|
-
setC(cursor, row, ch, 'text')
|
|
257
|
-
cursor += charVisualWidth(ch)
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// Bottom border
|
|
262
|
-
const bottomY = topY + h - 1
|
|
263
|
-
setC(left, bottomY, BL, 'border')
|
|
264
|
-
for (let x = 1; x < w - 1; x++) setC(left + x, bottomY, H, 'border')
|
|
265
|
-
setC(left + w - 1, bottomY, BR, 'border')
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// ---- DRAW: lifelines ----
|
|
269
|
-
|
|
270
|
-
for (let i = 0; i < diagram.actors.length; i++) {
|
|
271
|
-
const x = llX[i]!
|
|
272
|
-
for (let y = actorBoxH; y <= footerY; y++) {
|
|
273
|
-
setC(x, y, V, 'line')
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// ---- DRAW: actor header + footer boxes (drawn over lifelines) ----
|
|
278
|
-
|
|
279
|
-
for (let i = 0; i < diagram.actors.length; i++) {
|
|
280
|
-
const actor = diagram.actors[i]!
|
|
281
|
-
drawActorBox(llX[i]!, 0, actor.label)
|
|
282
|
-
drawActorBox(llX[i]!, footerY, actor.label)
|
|
283
|
-
|
|
284
|
-
// Lifeline junctions on box borders (Unicode only)
|
|
285
|
-
if (!useAscii) {
|
|
286
|
-
setC(llX[i]!, actorBoxH - 1, JT, 'junction')
|
|
287
|
-
setC(llX[i]!, footerY, JB, 'junction')
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// ---- DRAW: messages ----
|
|
292
|
-
|
|
293
|
-
for (let m = 0; m < diagram.messages.length; m++) {
|
|
294
|
-
const msg = diagram.messages[m]!
|
|
295
|
-
const fi = actorIdx.get(msg.from)!
|
|
296
|
-
const ti = actorIdx.get(msg.to)!
|
|
297
|
-
const fromX = llX[fi]!
|
|
298
|
-
const toX = llX[ti]!
|
|
299
|
-
const isSelf = fi === ti
|
|
300
|
-
const isDashed = msg.lineStyle === 'dashed'
|
|
301
|
-
const isFilled = msg.arrowHead === 'filled'
|
|
302
|
-
|
|
303
|
-
// Arrow line character (solid vs dashed)
|
|
304
|
-
const lineChar = isDashed ? (useAscii ? '.' : '╌') : H
|
|
305
|
-
|
|
306
|
-
if (isSelf) {
|
|
307
|
-
// Self-message: an N+2 row loop to the right of the lifeline. For a
|
|
308
|
-
// single-line label that's the classic 3-row form:
|
|
309
|
-
// ├──┐ (row 0 = msgArrowY)
|
|
310
|
-
// │ │ Label (row 1)
|
|
311
|
-
// │◄─┘ (row 2)
|
|
312
|
-
// For an N-line label the middle row repeats N times so each line gets
|
|
313
|
-
// its own row with a continuing right-side wall.
|
|
314
|
-
const y0 = msgArrowY[m]!
|
|
315
|
-
const loopW = 4
|
|
316
|
-
const selfLines = splitLines(msg.label)
|
|
317
|
-
|
|
318
|
-
// Row 0: start junction + horizontal + top-right corner
|
|
319
|
-
setC(fromX, y0, JL, 'junction')
|
|
320
|
-
for (let x = fromX + 1; x < fromX + loopW; x++) setC(x, y0, lineChar, 'line')
|
|
321
|
-
setC(fromX + loopW, y0, useAscii ? '+' : '┐', 'corner')
|
|
322
|
-
|
|
323
|
-
// Middle rows: vertical wall on the right + the label line for each row.
|
|
324
|
-
const labelX = fromX + loopW + 2
|
|
325
|
-
for (let lineIdx = 0; lineIdx < selfLines.length; lineIdx++) {
|
|
326
|
-
const row = y0 + 1 + lineIdx
|
|
327
|
-
// Lifeline wall on the source side (kept by lifeline pass, but make
|
|
328
|
-
// sure the right-hand wall is continuous so multi-line labels look
|
|
329
|
-
// contained).
|
|
330
|
-
setC(fromX + loopW, row, V, 'line')
|
|
331
|
-
const line = selfLines[lineIdx]!
|
|
332
|
-
// Use codepoint+stride pattern (same as actor box at lines 256-257) so
|
|
333
|
-
// CJK / emoji / surrogate-pair labels render with correct column width.
|
|
334
|
-
let cursor = labelX
|
|
335
|
-
for (const ch of line) {
|
|
336
|
-
if (cursor < totalW) setC(cursor, row, ch, 'text')
|
|
337
|
-
cursor += charVisualWidth(ch)
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
// Row N+1: arrow-back + horizontal + bottom-right corner.
|
|
342
|
-
const arrowChar = isFilled ? (useAscii ? '<' : '◀') : (useAscii ? '<' : '◁')
|
|
343
|
-
const bottomRow = y0 + 1 + selfLines.length
|
|
344
|
-
setC(fromX, bottomRow, arrowChar, 'arrow')
|
|
345
|
-
for (let x = fromX + 1; x < fromX + loopW; x++) setC(x, bottomRow, lineChar, 'line')
|
|
346
|
-
setC(fromX + loopW, bottomRow, useAscii ? '+' : '┘', 'corner')
|
|
347
|
-
} else {
|
|
348
|
-
// Normal message: label on row above, arrow on row below
|
|
349
|
-
const labelY = msgLabelY[m]!
|
|
350
|
-
const arrowY = msgArrowY[m]!
|
|
351
|
-
const leftToRight = fromX < toX
|
|
352
|
-
|
|
353
|
-
// Draw label centered between the two lifelines (supports multi-line).
|
|
354
|
-
// Center by terminal column width so CJK / emoji labels sit symmetric
|
|
355
|
-
// on the arrow and so left-pad rounding follows box parity, not line
|
|
356
|
-
// parity. Iterate codepoints (not UTF-16 code units) so emoji surrogate
|
|
357
|
-
// pairs render as one glyph.
|
|
358
|
-
const boxWidth = Math.abs(toX - fromX)
|
|
359
|
-
const msgLines = splitLines(msg.label)
|
|
360
|
-
|
|
361
|
-
for (let lineIdx = 0; lineIdx < msgLines.length; lineIdx++) {
|
|
362
|
-
const line = msgLines[lineIdx]!
|
|
363
|
-
const lineCells = visualWidth(line)
|
|
364
|
-
const labelStart = Math.min(fromX, toX) + Math.floor((boxWidth - lineCells) / 2)
|
|
365
|
-
const y = labelY + lineIdx
|
|
366
|
-
let cursor = labelStart
|
|
367
|
-
for (const ch of line) {
|
|
368
|
-
if (cursor >= 0 && cursor < totalW) setC(cursor, y, ch, 'text')
|
|
369
|
-
cursor += charVisualWidth(ch)
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// Draw arrow line
|
|
374
|
-
if (leftToRight) {
|
|
375
|
-
for (let x = fromX + 1; x < toX; x++) setC(x, arrowY, lineChar, 'line')
|
|
376
|
-
// Arrowhead at destination
|
|
377
|
-
const ah = isFilled ? (useAscii ? '>' : '▶') : (useAscii ? '>' : '▷')
|
|
378
|
-
setC(toX, arrowY, ah, 'arrow')
|
|
379
|
-
} else {
|
|
380
|
-
for (let x = toX + 1; x < fromX; x++) setC(x, arrowY, lineChar, 'line')
|
|
381
|
-
const ah = isFilled ? (useAscii ? '<' : '◀') : (useAscii ? '<' : '◁')
|
|
382
|
-
setC(toX, arrowY, ah, 'arrow')
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
// ---- DRAW: blocks (loop, alt, opt, par, etc.) ----
|
|
388
|
-
|
|
389
|
-
for (let b = 0; b < diagram.blocks.length; b++) {
|
|
390
|
-
const block = diagram.blocks[b]!
|
|
391
|
-
const topY = blockStartY.get(b)
|
|
392
|
-
const botY = blockEndY.get(b)
|
|
393
|
-
if (topY === undefined || botY === undefined) continue
|
|
394
|
-
|
|
395
|
-
// Find the leftmost/rightmost lifelines involved in this block's messages.
|
|
396
|
-
// For self-messages the visible extent runs PAST the lifeline column —
|
|
397
|
-
// it's `llX[f] + loopW + 2 + max-label-line-width`. Treat that as the
|
|
398
|
-
// right edge for self-arrows so the block frame doesn't clip the label.
|
|
399
|
-
let minLX = totalW
|
|
400
|
-
let maxLX = 0
|
|
401
|
-
for (let m = block.startIndex; m <= block.endIndex; m++) {
|
|
402
|
-
if (m >= diagram.messages.length) break
|
|
403
|
-
const msg = diagram.messages[m]!
|
|
404
|
-
const f = actorIdx.get(msg.from) ?? 0
|
|
405
|
-
const t = actorIdx.get(msg.to) ?? 0
|
|
406
|
-
minLX = Math.min(minLX, llX[Math.min(f, t)]!)
|
|
407
|
-
if (f === t) {
|
|
408
|
-
// Self-arrow: the label sits at llX[f] + 4 + 2 = llX[f] + 6 with the
|
|
409
|
-
// longest line determining the extra width. Match the totalW math
|
|
410
|
-
// (llX[f] + 6 + maxLineWidth(label)) and add a 2-cell breathing room
|
|
411
|
-
// so the right border doesn't sit immediately on the last glyph.
|
|
412
|
-
const selfRight = llX[f]! + 6 + maxLineWidth(msg.label) + 2
|
|
413
|
-
maxLX = Math.max(maxLX, selfRight)
|
|
414
|
-
} else {
|
|
415
|
-
maxLX = Math.max(maxLX, llX[Math.max(f, t)]!)
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
const bLeft = Math.max(0, minLX - 4)
|
|
420
|
-
const bRight = Math.min(totalW - 1, maxLX + 4)
|
|
421
|
-
|
|
422
|
-
// Top border with block type label
|
|
423
|
-
setC(bLeft, topY, TL, 'border')
|
|
424
|
-
for (let x = bLeft + 1; x < bRight; x++) setC(x, topY, H, 'border')
|
|
425
|
-
setC(bRight, topY, TR, 'border')
|
|
426
|
-
// Write block header label over the top border (supports multi-line)
|
|
427
|
-
const hdrLabel = block.label ? `${block.type} [${block.label}]` : block.type
|
|
428
|
-
const hdrLines = splitLines(hdrLabel)
|
|
429
|
-
|
|
430
|
-
for (let lineIdx = 0; lineIdx < hdrLines.length && topY + lineIdx < botY; lineIdx++) {
|
|
431
|
-
const line = hdrLines[lineIdx]!
|
|
432
|
-
for (let i = 0; i < line.length && bLeft + 1 + i < bRight; i++) {
|
|
433
|
-
setC(bLeft + 1 + i, topY + lineIdx, line[i]!, 'text')
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
// Bottom border
|
|
438
|
-
setC(bLeft, botY, BL, 'border')
|
|
439
|
-
for (let x = bLeft + 1; x < bRight; x++) setC(x, botY, H, 'border')
|
|
440
|
-
setC(bRight, botY, BR, 'border')
|
|
441
|
-
|
|
442
|
-
// Side borders
|
|
443
|
-
for (let y = topY + 1; y < botY; y++) {
|
|
444
|
-
setC(bLeft, y, V, 'border')
|
|
445
|
-
setC(bRight, y, V, 'border')
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
// Dividers
|
|
449
|
-
for (let d = 0; d < block.dividers.length; d++) {
|
|
450
|
-
const dY = divYMap.get(`${b}:${d}`)
|
|
451
|
-
if (dY === undefined) continue
|
|
452
|
-
const dashChar = isDashedH()
|
|
453
|
-
setC(bLeft, dY, JL, 'junction')
|
|
454
|
-
for (let x = bLeft + 1; x < bRight; x++) setC(x, dY, dashChar, 'line')
|
|
455
|
-
setC(bRight, dY, JR, 'junction')
|
|
456
|
-
// Divider label
|
|
457
|
-
const dLabel = block.dividers[d]!.label
|
|
458
|
-
if (dLabel) {
|
|
459
|
-
const dStr = `[${dLabel}]`
|
|
460
|
-
for (let i = 0; i < dStr.length && bLeft + 1 + i < bRight; i++) {
|
|
461
|
-
setC(bLeft + 1 + i, dY, dStr[i]!, 'text')
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
// ---- DRAW: notes ----
|
|
468
|
-
|
|
469
|
-
for (const np of notePositions) {
|
|
470
|
-
// Ensure canvas is big enough
|
|
471
|
-
increaseSize(canvas, np.x + np.width, np.y + np.height)
|
|
472
|
-
increaseRoleCanvasSize(rc, np.x + np.width, np.y + np.height)
|
|
473
|
-
// Top border
|
|
474
|
-
setC(np.x, np.y, TL, 'border')
|
|
475
|
-
for (let x = 1; x < np.width - 1; x++) setC(np.x + x, np.y, H, 'border')
|
|
476
|
-
setC(np.x + np.width - 1, np.y, TR, 'border')
|
|
477
|
-
// Content rows
|
|
478
|
-
for (let l = 0; l < np.lines.length; l++) {
|
|
479
|
-
const ly = np.y + 1 + l
|
|
480
|
-
setC(np.x, ly, V, 'border')
|
|
481
|
-
setC(np.x + np.width - 1, ly, V, 'border')
|
|
482
|
-
for (let i = 0; i < np.lines[l]!.length; i++) {
|
|
483
|
-
setC(np.x + 2 + i, ly, np.lines[l]![i]!, 'text')
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
// Bottom border
|
|
487
|
-
const by = np.y + np.height - 1
|
|
488
|
-
setC(np.x, by, BL, 'border')
|
|
489
|
-
for (let x = 1; x < np.width - 1; x++) setC(np.x + x, by, H, 'border')
|
|
490
|
-
setC(np.x + np.width - 1, by, BR, 'border')
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
return canvasToString(canvas, { roleCanvas: rc, colorMode, theme })
|
|
494
|
-
|
|
495
|
-
// ---- Helper: dashed horizontal character ----
|
|
496
|
-
function isDashedH(): string {
|
|
497
|
-
return useAscii ? '-' : '╌'
|
|
498
|
-
}
|
|
499
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
// ============================================================================
|
|
2
|
-
// Circle shape renderer — uses corner decorators instead of curves
|
|
3
|
-
// ============================================================================
|
|
4
|
-
|
|
5
|
-
import type { ShapeRenderer } from './types.ts'
|
|
6
|
-
import { getBoxDimensions, renderBox, getBoxAttachmentPoint } from './rectangle.ts'
|
|
7
|
-
import { getCorners } from './corners.ts'
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Circle shape renderer.
|
|
11
|
-
* Uses circle markers (◯) at corners to indicate circular shape semantics.
|
|
12
|
-
*
|
|
13
|
-
* Renders as:
|
|
14
|
-
* ◯─────────◯
|
|
15
|
-
* │ Label │
|
|
16
|
-
* ◯─────────◯
|
|
17
|
-
*/
|
|
18
|
-
export const circleRenderer: ShapeRenderer = {
|
|
19
|
-
getDimensions: getBoxDimensions,
|
|
20
|
-
|
|
21
|
-
render(label, dimensions, options) {
|
|
22
|
-
const corners = getCorners('circle', options.useAscii)
|
|
23
|
-
return renderBox(label, dimensions, corners, options.useAscii)
|
|
24
|
-
},
|
|
25
|
-
|
|
26
|
-
getAttachmentPoint: getBoxAttachmentPoint,
|
|
27
|
-
}
|
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
// ============================================================================
|
|
2
|
-
// Corner character lookup table for shape rendering
|
|
3
|
-
// ============================================================================
|
|
4
|
-
//
|
|
5
|
-
// All shapes are rendered as rectangles with distinctive corner characters
|
|
6
|
-
// to indicate shape type. This eliminates diagonal characters while keeping
|
|
7
|
-
// shapes visually distinguishable.
|
|
8
|
-
|
|
9
|
-
import type { AsciiNodeShape } from '../types.ts'
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Corner characters for a shape in both Unicode and ASCII modes.
|
|
13
|
-
*/
|
|
14
|
-
export interface CornerChars {
|
|
15
|
-
/** Top-left corner */
|
|
16
|
-
tl: string
|
|
17
|
-
/** Top-right corner */
|
|
18
|
-
tr: string
|
|
19
|
-
/** Bottom-left corner */
|
|
20
|
-
bl: string
|
|
21
|
-
/** Bottom-right corner */
|
|
22
|
-
br: string
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Shape corner configuration with both Unicode and ASCII variants.
|
|
27
|
-
*/
|
|
28
|
-
export interface ShapeCorners {
|
|
29
|
-
unicode: CornerChars
|
|
30
|
-
ascii: CornerChars
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Corner character lookup table for all shape types.
|
|
35
|
-
*
|
|
36
|
-
* Design principles:
|
|
37
|
-
* - All shapes use orthogonal box structure (no diagonals)
|
|
38
|
-
* - Corner characters indicate shape semantics
|
|
39
|
-
* - ASCII fallbacks use available punctuation
|
|
40
|
-
*/
|
|
41
|
-
export const SHAPE_CORNERS: Record<AsciiNodeShape, ShapeCorners> = {
|
|
42
|
-
// Standard rectangular shapes
|
|
43
|
-
rectangle: {
|
|
44
|
-
unicode: { tl: '┌', tr: '┐', bl: '└', br: '┘' },
|
|
45
|
-
ascii: { tl: '+', tr: '+', bl: '+', br: '+' },
|
|
46
|
-
},
|
|
47
|
-
service: {
|
|
48
|
-
unicode: { tl: '┌', tr: '┐', bl: '└', br: '┘' },
|
|
49
|
-
ascii: { tl: '+', tr: '+', bl: '+', br: '+' },
|
|
50
|
-
},
|
|
51
|
-
rounded: {
|
|
52
|
-
unicode: { tl: '╭', tr: '╮', bl: '╰', br: '╯' },
|
|
53
|
-
ascii: { tl: '.', tr: '.', bl: "'", br: "'" },
|
|
54
|
-
},
|
|
55
|
-
|
|
56
|
-
// Circular shapes - use circle markers at corners
|
|
57
|
-
circle: {
|
|
58
|
-
unicode: { tl: '◯', tr: '◯', bl: '◯', br: '◯' },
|
|
59
|
-
ascii: { tl: 'o', tr: 'o', bl: 'o', br: 'o' },
|
|
60
|
-
},
|
|
61
|
-
doublecircle: {
|
|
62
|
-
unicode: { tl: '◎', tr: '◎', bl: '◎', br: '◎' },
|
|
63
|
-
ascii: { tl: '@', tr: '@', bl: '@', br: '@' },
|
|
64
|
-
},
|
|
65
|
-
|
|
66
|
-
// Diamond - decision nodes
|
|
67
|
-
diamond: {
|
|
68
|
-
unicode: { tl: '◇', tr: '◇', bl: '◇', br: '◇' },
|
|
69
|
-
ascii: { tl: '<', tr: '>', bl: '<', br: '>' },
|
|
70
|
-
},
|
|
71
|
-
|
|
72
|
-
// Hexagon - process nodes (crop corners — monospace-safe, distinct from rectangle)
|
|
73
|
-
hexagon: {
|
|
74
|
-
unicode: { tl: '⌜', tr: '⌝', bl: '⌞', br: '⌟' },
|
|
75
|
-
ascii: { tl: '*', tr: '*', bl: '*', br: '*' },
|
|
76
|
-
},
|
|
77
|
-
|
|
78
|
-
// Stadium/pill shape
|
|
79
|
-
stadium: {
|
|
80
|
-
unicode: { tl: '(', tr: ')', bl: '(', br: ')' },
|
|
81
|
-
ascii: { tl: '(', tr: ')', bl: '(', br: ')' },
|
|
82
|
-
},
|
|
83
|
-
|
|
84
|
-
// Subroutine - double vertical bars
|
|
85
|
-
subroutine: {
|
|
86
|
-
unicode: { tl: '╟', tr: '╢', bl: '╟', br: '╢' },
|
|
87
|
-
ascii: { tl: '|', tr: '|', bl: '|', br: '|' },
|
|
88
|
-
},
|
|
89
|
-
|
|
90
|
-
// Cylinder/database
|
|
91
|
-
cylinder: {
|
|
92
|
-
unicode: { tl: '╭', tr: '╮', bl: '╰', br: '╯' },
|
|
93
|
-
ascii: { tl: '.', tr: '.', bl: "'", br: "'" },
|
|
94
|
-
},
|
|
95
|
-
|
|
96
|
-
// Asymmetric/flag - pointer on left side
|
|
97
|
-
asymmetric: {
|
|
98
|
-
unicode: { tl: '▷', tr: '┐', bl: '▷', br: '┘' },
|
|
99
|
-
ascii: { tl: '>', tr: '+', bl: '>', br: '+' },
|
|
100
|
-
},
|
|
101
|
-
|
|
102
|
-
// Trapezoid - wider at bottom (top corners slope inward)
|
|
103
|
-
trapezoid: {
|
|
104
|
-
unicode: { tl: '/', tr: '\\', bl: '└', br: '┘' },
|
|
105
|
-
ascii: { tl: '/', tr: '\\', bl: '+', br: '+' },
|
|
106
|
-
},
|
|
107
|
-
|
|
108
|
-
// Trapezoid-alt - wider at top (bottom corners slope inward)
|
|
109
|
-
'trapezoid-alt': {
|
|
110
|
-
unicode: { tl: '┌', tr: '┐', bl: '\\', br: '/' },
|
|
111
|
-
ascii: { tl: '+', tr: '+', bl: '\\', br: '/' },
|
|
112
|
-
},
|
|
113
|
-
|
|
114
|
-
// Parallelogram leaning right - all four corners sit on '/' slants
|
|
115
|
-
'lean-r': {
|
|
116
|
-
unicode: { tl: '/', tr: '/', bl: '/', br: '/' },
|
|
117
|
-
ascii: { tl: '/', tr: '/', bl: '/', br: '/' },
|
|
118
|
-
},
|
|
119
|
-
|
|
120
|
-
// Parallelogram leaning left - all four corners sit on '\' slants
|
|
121
|
-
'lean-l': {
|
|
122
|
-
unicode: { tl: '\\', tr: '\\', bl: '\\', br: '\\' },
|
|
123
|
-
ascii: { tl: '\\', tr: '\\', bl: '\\', br: '\\' },
|
|
124
|
-
},
|
|
125
|
-
|
|
126
|
-
// State diagram pseudostates (special handling, not corner-based)
|
|
127
|
-
'state-start': {
|
|
128
|
-
unicode: { tl: '●', tr: '●', bl: '●', br: '●' },
|
|
129
|
-
ascii: { tl: '*', tr: '*', bl: '*', br: '*' },
|
|
130
|
-
},
|
|
131
|
-
'state-end': {
|
|
132
|
-
unicode: { tl: '◉', tr: '◉', bl: '◉', br: '◉' },
|
|
133
|
-
ascii: { tl: '@', tr: '@', bl: '@', br: '@' },
|
|
134
|
-
},
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Get corner characters for a shape type.
|
|
139
|
-
*/
|
|
140
|
-
export function getCorners(shape: AsciiNodeShape, useAscii: boolean): CornerChars {
|
|
141
|
-
const corners = SHAPE_CORNERS[shape] ?? SHAPE_CORNERS.rectangle
|
|
142
|
-
return useAscii ? corners.ascii : corners.unicode
|
|
143
|
-
}
|