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.
- package/CHANGELOG.md +144 -4
- package/LICENSES/Apache-2.0.txt +202 -0
- package/README.md +25 -32
- package/THIRD_PARTY_NOTICES.md +29 -0
- package/dist/agent-core.d.ts +463 -0
- package/dist/agent-core.js +397 -0
- package/dist/agent.d.ts +73 -1868
- package/dist/agent.js +336 -37325
- package/dist/agentic-mermaid-mcp.js +7 -38399
- package/dist/am.js +33632 -40445
- 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 -33014
- 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 +68 -43
- package/server.json +2 -2
- 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 -607
- package/Instructions_for_agents.md +0 -89
- package/TODO.md +0 -684
- package/assets/hero.png +0 -0
- package/dist/index-wjRjbsP-.d.ts +0 -1102
- 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 -211
- 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 -82
- 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-experience-curve-implementation.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 -439
- package/docs/design/families/journey-section-overlap-after.png +0 -0
- package/docs/design/families/journey-section-overlap-before.png +0 -0
- package/docs/design/families/journey-usage-research.md +0 -423
- package/docs/design/families/journey.md +0 -153
- 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 -498
- 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 -98
- 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 -215
- 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 -368
- package/src/agent/er-body.ts +0 -224
- package/src/agent/facts.ts +0 -360
- package/src/agent/families-builtin.ts +0 -920
- package/src/agent/families.ts +0 -232
- package/src/agent/family-layouts.ts +0 -832
- package/src/agent/flowchart-body.ts +0 -372
- package/src/agent/flowchart-unsupported.ts +0 -243
- package/src/agent/font-coverage.ts +0 -288
- package/src/agent/gantt-body.ts +0 -495
- package/src/agent/index.ts +0 -10
- package/src/agent/journey-body.ts +0 -374
- 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 -443
- package/src/agent/parse.ts +0 -223
- package/src/agent/pie-body.ts +0 -270
- package/src/agent/png.ts +0 -171
- 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 -995
- package/src/agent/verify.ts +0 -603
- 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 -169
- 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 -141
- package/src/ascii/types.ts +0 -312
- package/src/ascii/validate.ts +0 -120
- package/src/ascii/width.ts +0 -81
- package/src/ascii/wrap.ts +0 -131
- 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 -1132
- package/src/cli/init-agent.ts +0 -147
- package/src/cli/run-entrypoint.ts +0 -10
- 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/family-rubric.ts +0 -425
- package/src/font-manifest.ts +0 -31
- 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 -554
- package/src/journey/parse-core.ts +0 -251
- package/src/journey/parser.ts +0 -101
- package/src/journey/renderer.ts +0 -917
- 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 -391
- 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 -93
- 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 -710
- 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 -462
- 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/sequence/renderer.ts
DELETED
|
@@ -1,604 +0,0 @@
|
|
|
1
|
-
import type { PositionedSequenceDiagram, PositionedActor, Lifeline, PositionedMessage, Activation, PositionedBlock, PositionedNote } from './types.ts'
|
|
2
|
-
import type { RenderContext } from '../types.ts'
|
|
3
|
-
import { svgOpenTag, buildStyleBlock, buildShadowDefs } from '../theme.ts'
|
|
4
|
-
import { FONT_SIZES, FONT_WEIGHTS, STROKE_WIDTHS, ARROW_HEAD, estimateTextWidth, TEXT_BASELINE_SHIFT, applyTextTransform, resolveRenderStyle } from '../styles.ts'
|
|
5
|
-
import type { RenderStyleDefaults, ResolvedRenderStyle } from '../styles.ts'
|
|
6
|
-
import { SEQUENCE_STYLE_DEFAULTS } from './layout.ts'
|
|
7
|
-
import { buildAccessibilityAttrs } from '../shared/svg-a11y.ts'
|
|
8
|
-
import { renderMultilineText, escapeAttr, escapeXml as escapeXmlUtil } from '../multiline-utils.ts'
|
|
9
|
-
import type { MarkerRef, SceneDoc, SceneNode } from '../scene/ir.ts'
|
|
10
|
-
import { hashId } from '../scene/seed.ts'
|
|
11
|
-
import * as marks from '../scene/marks.ts'
|
|
12
|
-
import { DefaultBackend } from '../scene/backend.ts'
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
// ============================================================================
|
|
16
|
-
// Sequence diagram SVG renderer
|
|
17
|
-
//
|
|
18
|
-
// Renders a positioned sequence diagram to SVG string.
|
|
19
|
-
// The diagram is first lowered to a SceneGraph (SPEC §3.1): every visual mark
|
|
20
|
-
// becomes a scene node carrying semantic fields (role, geometry, paint,
|
|
21
|
-
// channels, stable id) plus its exact crisp serialization, built here from
|
|
22
|
-
// the same inputs. renderSequenceSvg() is DefaultBackend serialization of
|
|
23
|
-
// that scene, so the default path stays byte-identical to the historical
|
|
24
|
-
// string renderer (corpus-gated by svg-equivalence.test.ts).
|
|
25
|
-
//
|
|
26
|
-
// All colors use CSS custom properties (var(--_xxx)) from the theme system.
|
|
27
|
-
//
|
|
28
|
-
// Render order (back to front):
|
|
29
|
-
// 1. Block backgrounds (loop/alt/opt)
|
|
30
|
-
// 2. Lifelines (dashed vertical lines)
|
|
31
|
-
// 3. Activation boxes
|
|
32
|
-
// 4. Messages (arrows with labels)
|
|
33
|
-
// 5. Notes
|
|
34
|
-
// 6. Actor boxes (at top)
|
|
35
|
-
// ============================================================================
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Render a positioned sequence diagram as an SVG string.
|
|
39
|
-
*
|
|
40
|
-
* @param colors - DiagramColors with bg/fg and optional enrichment variables.
|
|
41
|
-
* @param transparent - If true, renders with transparent background.
|
|
42
|
-
*/
|
|
43
|
-
export function renderSequenceSvg(
|
|
44
|
-
ctx: RenderContext<PositionedSequenceDiagram>,
|
|
45
|
-
): string {
|
|
46
|
-
return DefaultBackend.render(lowerSequenceScene(ctx), { seed: 0 })
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Lower a positioned sequence diagram to the SceneGraph IR. Mark order
|
|
51
|
-
* matches the historical parts[] order exactly; DefaultBackend joins crisps
|
|
52
|
-
* with '\n'.
|
|
53
|
-
*/
|
|
54
|
-
export function lowerSequenceScene(
|
|
55
|
-
ctx: RenderContext<PositionedSequenceDiagram>,
|
|
56
|
-
): SceneDoc {
|
|
57
|
-
const { positioned: diagram, colors, options } = ctx
|
|
58
|
-
const font = colors.font ?? 'Inter'
|
|
59
|
-
const transparent = options.transparent ?? false
|
|
60
|
-
const parts: SceneNode[] = []
|
|
61
|
-
const style = resolveRenderStyle(options, SEQUENCE_STYLE_DEFAULTS)
|
|
62
|
-
const uid = `seq-${hashId(diagram.width, diagram.height, diagram.actors.length, diagram.messages.length)}`
|
|
63
|
-
const titleId = `${uid}-title`
|
|
64
|
-
const descId = `${uid}-desc`
|
|
65
|
-
const rootAttrs = buildAccessibilityAttrs(diagram.accessibilityTitle, diagram.accessibilityDescription, titleId, descId)
|
|
66
|
-
|
|
67
|
-
// SVG root with CSS variables + style block + defs
|
|
68
|
-
parts.push(marks.prelude(
|
|
69
|
-
{
|
|
70
|
-
id: 'prelude',
|
|
71
|
-
width: diagram.width,
|
|
72
|
-
height: diagram.height,
|
|
73
|
-
colors,
|
|
74
|
-
transparent,
|
|
75
|
-
font,
|
|
76
|
-
hasMonoFont: false,
|
|
77
|
-
},
|
|
78
|
-
svgOpenTag(diagram.width, diagram.height, colors, transparent, rootAttrs) + '\n' +
|
|
79
|
-
buildStyleBlock(font, false, colors.shadow, colors.embedFontImport),
|
|
80
|
-
))
|
|
81
|
-
const defsParts: string[] = []
|
|
82
|
-
defsParts.push('<defs>')
|
|
83
|
-
|
|
84
|
-
// Arrow marker definitions
|
|
85
|
-
defsParts.push(arrowMarkerDefs(style))
|
|
86
|
-
const shadowDefs = buildShadowDefs(colors)
|
|
87
|
-
if (shadowDefs) defsParts.push(shadowDefs)
|
|
88
|
-
defsParts.push('</defs>')
|
|
89
|
-
parts.push(marks.raw({ id: 'defs', role: 'defs' }, defsParts.join('\n')))
|
|
90
|
-
|
|
91
|
-
if (diagram.accessibilityTitle) {
|
|
92
|
-
parts.push(marks.raw({ id: 'a11y-title', role: 'chrome' },
|
|
93
|
-
`<title id="${titleId}">${escapeXml(diagram.accessibilityTitle)}</title>`))
|
|
94
|
-
}
|
|
95
|
-
if (diagram.accessibilityDescription) {
|
|
96
|
-
parts.push(marks.raw({ id: 'a11y-desc', role: 'chrome' },
|
|
97
|
-
`<desc id="${descId}">${escapeXml(diagram.accessibilityDescription)}</desc>`))
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// 1. Block backgrounds (loop/alt/opt rectangles)
|
|
101
|
-
const blockOccurrence = new Map<string, number>()
|
|
102
|
-
for (const block of diagram.blocks) {
|
|
103
|
-
const k = blockOccurrence.get(block.type) ?? 0
|
|
104
|
-
blockOccurrence.set(block.type, k + 1)
|
|
105
|
-
parts.push(renderBlock(block, style, `block:${block.type}#${k}`))
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// 2. Lifelines (dashed vertical lines from actor to bottom)
|
|
109
|
-
for (const lifeline of diagram.lifelines) {
|
|
110
|
-
parts.push(renderLifeline(lifeline, style))
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// 3. Activation boxes
|
|
114
|
-
const activationOccurrence = new Map<string, number>()
|
|
115
|
-
for (const activation of diagram.activations) {
|
|
116
|
-
const k = activationOccurrence.get(activation.actorId) ?? 0
|
|
117
|
-
activationOccurrence.set(activation.actorId, k + 1)
|
|
118
|
-
parts.push(renderActivation(activation, style, `activation:${activation.actorId}#${k}`))
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// 4. Messages (horizontal arrows with labels)
|
|
122
|
-
const messageOccurrence = new Map<string, number>()
|
|
123
|
-
for (const message of diagram.messages) {
|
|
124
|
-
const pairKey = `${message.from}->${message.to}`
|
|
125
|
-
const k = messageOccurrence.get(pairKey) ?? 0
|
|
126
|
-
messageOccurrence.set(pairKey, k + 1)
|
|
127
|
-
parts.push(renderMessage(message, style, `message:${pairKey}#${k}`))
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// 5. Notes
|
|
131
|
-
const noteOccurrence = new Map<string, number>()
|
|
132
|
-
for (const note of diagram.notes) {
|
|
133
|
-
const noteKey = `${(note.actors ?? []).join(',')}@${note.position ?? 'over'}`
|
|
134
|
-
const k = noteOccurrence.get(noteKey) ?? 0
|
|
135
|
-
noteOccurrence.set(noteKey, k + 1)
|
|
136
|
-
parts.push(renderNote(note, style, `note:${noteKey}#${k}`))
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// 6. Actor boxes at top (rendered last so they're on top)
|
|
140
|
-
for (const actor of diagram.actors) {
|
|
141
|
-
parts.push(renderActor(actor, style))
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
parts.push(marks.raw({ id: 'svg-close', role: 'chrome' }, '</svg>'))
|
|
145
|
-
|
|
146
|
-
return { family: 'sequence', width: diagram.width, height: diagram.height, colors, parts }
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// ============================================================================
|
|
150
|
-
// Arrow marker definitions
|
|
151
|
-
// ============================================================================
|
|
152
|
-
|
|
153
|
-
function arrowMarkerDefs(style: ResolvedRenderStyle): string {
|
|
154
|
-
const w = ARROW_HEAD.width
|
|
155
|
-
const h = ARROW_HEAD.height
|
|
156
|
-
const edgeColor = escapeAttr(style.edgeStrokeColor ?? 'var(--_arrow)')
|
|
157
|
-
return (
|
|
158
|
-
` <marker id="seq-arrow" markerWidth="${w}" markerHeight="${h}" refX="${w}" refY="${h / 2}" orient="auto-start-reverse">` +
|
|
159
|
-
`\n <polygon points="0 0, ${w} ${h / 2}, 0 ${h}" fill="${edgeColor}" />` +
|
|
160
|
-
`\n </marker>` +
|
|
161
|
-
// Open arrow head (just lines, no fill)
|
|
162
|
-
`\n <marker id="seq-arrow-open" markerWidth="${w}" markerHeight="${h}" refX="${w}" refY="${h / 2}" orient="auto-start-reverse">` +
|
|
163
|
-
`\n <polyline points="0 0, ${w} ${h / 2}, 0 ${h}" fill="none" stroke="${edgeColor}" stroke-width="1" />` +
|
|
164
|
-
`\n </marker>`
|
|
165
|
-
)
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// ============================================================================
|
|
169
|
-
// Component renderers
|
|
170
|
-
// ============================================================================
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Render an actor box (participant = rectangle, actor = stick figure).
|
|
174
|
-
* Wrapped in <g class="actor"> with semantic data attributes.
|
|
175
|
-
*/
|
|
176
|
-
function renderActor(actor: PositionedActor, style: ResolvedRenderStyle): SceneNode {
|
|
177
|
-
const { id, x, y, width, height, label, type } = actor
|
|
178
|
-
const children: Array<{ node: SceneNode; indent: number }> = []
|
|
179
|
-
|
|
180
|
-
// Semantic wrapper with actor metadata
|
|
181
|
-
const open =
|
|
182
|
-
`<g class="actor" data-id="${escapeAttr(id)}" data-label="${escapeAttr(label)}" data-type="${type}">`
|
|
183
|
-
|
|
184
|
-
const rawTextColor = style.nodeTextColor ?? 'var(--_text)'
|
|
185
|
-
const displayLabel = applyTextTransform(label, style.nodeTextTransform)
|
|
186
|
-
const labelAttrs =
|
|
187
|
-
`font-size="${style.nodeLabelFontSize}" text-anchor="middle" font-weight="${style.nodeLabelFontWeight}"${letterAttr(style.nodeLetterSpacing)} fill="${escapeAttr(rawTextColor)}"`
|
|
188
|
-
|
|
189
|
-
if (type === 'actor') {
|
|
190
|
-
// Circle-person icon: outer circle + head circle + shoulders arc.
|
|
191
|
-
// Defined in a 24×24 coordinate space, scaled to 90% of the actor box height
|
|
192
|
-
// and centered both horizontally and vertically within the box.
|
|
193
|
-
// Stroke width is inverse-scaled so the visual thickness matches STROKE_WIDTHS.outerBox.
|
|
194
|
-
const s = (height / 24) * 0.9
|
|
195
|
-
const tx = x - 12 * s // center icon horizontally on actor.x
|
|
196
|
-
const ty = y + (height - 24 * s) / 2 // center icon vertically in actor box
|
|
197
|
-
const sw = style.nodeLineWidth / s // compensate for scale transform
|
|
198
|
-
const iconStroke = escapeAttr(style.edgeStrokeColor ?? 'var(--_line)')
|
|
199
|
-
|
|
200
|
-
// The figure lives inside a <g transform=...> wrapper, which the shape
|
|
201
|
-
// geometry contract can't express — kept as a raw icon mark.
|
|
202
|
-
children.push({
|
|
203
|
-
indent: 2,
|
|
204
|
-
node: marks.raw({ id: `actor:${id}:icon`, role: 'icon', channels: { category: id } },
|
|
205
|
-
`<g transform="translate(${tx},${ty}) scale(${s})">` +
|
|
206
|
-
// Outer circle
|
|
207
|
-
`\n <path d="M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z" fill="none" stroke="${iconStroke}" stroke-width="${sw}" />` +
|
|
208
|
-
// Head
|
|
209
|
-
`\n <path d="M15 10C15 11.6569 13.6569 13 12 13C10.3431 13 9 11.6569 9 10C9 8.34315 10.3431 7 12 7C13.6569 7 15 8.34315 15 10Z" fill="none" stroke="${iconStroke}" stroke-width="${sw}" />` +
|
|
210
|
-
// Shoulders
|
|
211
|
-
`\n <path d="M5.62842 18.3563C7.08963 17.0398 9.39997 16 12 16C14.6 16 16.9104 17.0398 18.3716 18.3563" fill="none" stroke="${iconStroke}" stroke-width="${sw}" />` +
|
|
212
|
-
`\n</g>`),
|
|
213
|
-
})
|
|
214
|
-
// Label below the icon (supports multi-line)
|
|
215
|
-
children.push({
|
|
216
|
-
indent: 2,
|
|
217
|
-
node: marks.text({
|
|
218
|
-
id: `actor:${id}:label`,
|
|
219
|
-
role: 'label',
|
|
220
|
-
text: displayLabel,
|
|
221
|
-
x,
|
|
222
|
-
y: y + height + 14,
|
|
223
|
-
fontSize: style.nodeLabelFontSize,
|
|
224
|
-
anchor: 'middle',
|
|
225
|
-
paint: { fill: rawTextColor },
|
|
226
|
-
channels: { category: id },
|
|
227
|
-
}, renderMultilineText(displayLabel, x, y + height + 14, style.nodeLabelFontSize, labelAttrs)),
|
|
228
|
-
})
|
|
229
|
-
} else {
|
|
230
|
-
// Participant: rectangle box with label (supports multi-line)
|
|
231
|
-
const boxX = x - width / 2
|
|
232
|
-
const rawFill = style.nodeFillColor ?? 'var(--_node-fill)'
|
|
233
|
-
const rawStroke = style.nodeBorderColor ?? 'var(--_node-stroke)'
|
|
234
|
-
const radius = style.cornerRadius ?? 4
|
|
235
|
-
children.push({
|
|
236
|
-
indent: 2,
|
|
237
|
-
node: marks.shape({
|
|
238
|
-
id: `actor:${id}:box`,
|
|
239
|
-
role: 'actor',
|
|
240
|
-
geometry: { kind: 'rect', x: boxX, y, width, height, rx: radius, ry: radius },
|
|
241
|
-
paint: { fill: rawFill, stroke: rawStroke, strokeWidth: String(style.nodeLineWidth) },
|
|
242
|
-
channels: { category: id },
|
|
243
|
-
},
|
|
244
|
-
`<rect x="${boxX}" y="${y}" width="${width}" height="${height}" rx="${radius}" ry="${radius}" ` +
|
|
245
|
-
`fill="${escapeAttr(rawFill)}" stroke="${escapeAttr(rawStroke)}" stroke-width="${style.nodeLineWidth}" />`),
|
|
246
|
-
})
|
|
247
|
-
children.push({
|
|
248
|
-
indent: 2,
|
|
249
|
-
node: marks.text({
|
|
250
|
-
id: `actor:${id}:label`,
|
|
251
|
-
role: 'label',
|
|
252
|
-
text: displayLabel,
|
|
253
|
-
x,
|
|
254
|
-
y: y + height / 2,
|
|
255
|
-
fontSize: style.nodeLabelFontSize,
|
|
256
|
-
anchor: 'middle',
|
|
257
|
-
paint: { fill: rawTextColor },
|
|
258
|
-
channels: { category: id },
|
|
259
|
-
}, renderMultilineText(displayLabel, x, y + height / 2, style.nodeLabelFontSize, labelAttrs)),
|
|
260
|
-
})
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
return marks.group({
|
|
264
|
-
id: `actor:${id}`,
|
|
265
|
-
role: 'actor',
|
|
266
|
-
open,
|
|
267
|
-
close: '</g>',
|
|
268
|
-
children,
|
|
269
|
-
channels: { category: id },
|
|
270
|
-
})
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
/**
|
|
274
|
-
* Render a lifeline (dashed vertical line from actor to bottom).
|
|
275
|
-
* Includes data-actor to link to its actor.
|
|
276
|
-
*/
|
|
277
|
-
function renderLifeline(lifeline: Lifeline, style: ResolvedRenderStyle): SceneNode {
|
|
278
|
-
const rawStroke = style.edgeStrokeColor ?? 'var(--_line)'
|
|
279
|
-
const strokeWidth = Math.max(0.75, style.lineWidth * 0.75)
|
|
280
|
-
return marks.connector({
|
|
281
|
-
id: `lifeline:${lifeline.actorId}`,
|
|
282
|
-
role: 'lifeline',
|
|
283
|
-
geometry: { kind: 'line', x1: lifeline.x, y1: lifeline.topY, x2: lifeline.x, y2: lifeline.bottomY },
|
|
284
|
-
lineStyle: 'dashed',
|
|
285
|
-
paint: { stroke: rawStroke, strokeWidth: String(strokeWidth), strokeDasharray: '6 4' },
|
|
286
|
-
},
|
|
287
|
-
`<line class="lifeline" data-actor="${escapeAttr(lifeline.actorId)}" ` +
|
|
288
|
-
`x1="${lifeline.x}" y1="${lifeline.topY}" x2="${lifeline.x}" y2="${lifeline.bottomY}" ` +
|
|
289
|
-
`stroke="${escapeAttr(rawStroke)}" stroke-width="${strokeWidth}" stroke-dasharray="6 4" />`)
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
/**
|
|
293
|
-
* Render an activation box (narrow filled rectangle on lifeline).
|
|
294
|
-
* Includes data-actor to link to its actor.
|
|
295
|
-
*/
|
|
296
|
-
function renderActivation(activation: Activation, style: ResolvedRenderStyle, sceneId: string): SceneNode {
|
|
297
|
-
const rawFill = style.nodeFillColor ?? 'var(--_node-fill)'
|
|
298
|
-
const rawStroke = style.nodeBorderColor ?? 'var(--_node-stroke)'
|
|
299
|
-
return marks.shape({
|
|
300
|
-
id: sceneId,
|
|
301
|
-
role: 'activation',
|
|
302
|
-
geometry: { kind: 'rect', x: activation.x, y: activation.topY, width: activation.width, height: activation.bottomY - activation.topY },
|
|
303
|
-
paint: { fill: rawFill, stroke: rawStroke, strokeWidth: String(STROKE_WIDTHS.innerBox) },
|
|
304
|
-
},
|
|
305
|
-
`<rect class="activation" data-actor="${escapeAttr(activation.actorId)}" ` +
|
|
306
|
-
`x="${activation.x}" y="${activation.topY}" width="${activation.width}" height="${activation.bottomY - activation.topY}" ` +
|
|
307
|
-
`fill="${escapeAttr(rawFill)}" stroke="${escapeAttr(rawStroke)}" stroke-width="${STROKE_WIDTHS.innerBox}" />`)
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
/**
|
|
311
|
-
* Render a message arrow with label.
|
|
312
|
-
* Wrapped in <g class="message"> with semantic data attributes.
|
|
313
|
-
*/
|
|
314
|
-
function renderMessage(msg: PositionedMessage, style: ResolvedRenderStyle, sceneId: string): SceneNode {
|
|
315
|
-
const children: Array<{ node: SceneNode; indent: number }> = []
|
|
316
|
-
const dashArray = msg.lineStyle === 'dashed' ? ' stroke-dasharray="6 4"' : ''
|
|
317
|
-
const markerId = msg.arrowHead === 'filled' ? 'seq-arrow' : 'seq-arrow-open'
|
|
318
|
-
const endMarker: MarkerRef = {
|
|
319
|
-
id: markerId,
|
|
320
|
-
shape: msg.arrowHead === 'filled' ? 'arrow' : 'open-arrow',
|
|
321
|
-
}
|
|
322
|
-
const rawStroke = style.edgeStrokeColor ?? 'var(--_line)'
|
|
323
|
-
const rawTextColor = style.edgeTextColor ?? 'var(--_text-muted)'
|
|
324
|
-
const displayLabel = applyTextTransform(msg.label, style.edgeTextTransform)
|
|
325
|
-
const linePaint = {
|
|
326
|
-
stroke: rawStroke,
|
|
327
|
-
strokeWidth: String(style.lineWidth),
|
|
328
|
-
...(msg.lineStyle === 'dashed' ? { strokeDasharray: '6 4' } : {}),
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
// Semantic wrapper with message metadata
|
|
332
|
-
const open =
|
|
333
|
-
`<g class="message" data-from="${escapeAttr(msg.from)}" data-to="${escapeAttr(msg.to)}" ` +
|
|
334
|
-
`data-label="${escapeAttr(msg.label)}" data-line-style="${msg.lineStyle}" ` +
|
|
335
|
-
`data-arrow-head="${msg.arrowHead}" data-self="${msg.isSelf}">`
|
|
336
|
-
|
|
337
|
-
if (msg.isSelf) {
|
|
338
|
-
// Self-message: curved loop going right and back
|
|
339
|
-
// Loop dimensions - loopH is fixed, loopW provides minimum clearance
|
|
340
|
-
const loopW = 30
|
|
341
|
-
const loopH = 20
|
|
342
|
-
const labelPadding = 8 // Space between loop and label
|
|
343
|
-
children.push({
|
|
344
|
-
indent: 2,
|
|
345
|
-
node: marks.connector({
|
|
346
|
-
id: `${sceneId}:line`,
|
|
347
|
-
role: 'message',
|
|
348
|
-
geometry: {
|
|
349
|
-
kind: 'polyline',
|
|
350
|
-
points: [
|
|
351
|
-
{ x: msg.x1, y: msg.y },
|
|
352
|
-
{ x: msg.x1 + loopW, y: msg.y },
|
|
353
|
-
{ x: msg.x1 + loopW, y: msg.y + loopH },
|
|
354
|
-
{ x: msg.x2, y: msg.y + loopH },
|
|
355
|
-
],
|
|
356
|
-
},
|
|
357
|
-
lineStyle: msg.lineStyle,
|
|
358
|
-
paint: linePaint,
|
|
359
|
-
endMarker,
|
|
360
|
-
},
|
|
361
|
-
`<polyline points="${msg.x1},${msg.y} ${msg.x1 + loopW},${msg.y} ${msg.x1 + loopW},${msg.y + loopH} ${msg.x2},${msg.y + loopH}" ` +
|
|
362
|
-
`fill="none" stroke="${escapeAttr(rawStroke)}" stroke-width="${style.lineWidth}"${dashArray} marker-end="url(#${markerId})" />`),
|
|
363
|
-
})
|
|
364
|
-
// Label to the right of the loop (supports multi-line)
|
|
365
|
-
children.push({
|
|
366
|
-
indent: 2,
|
|
367
|
-
node: marks.text({
|
|
368
|
-
id: `${sceneId}:label`,
|
|
369
|
-
role: 'label',
|
|
370
|
-
text: displayLabel,
|
|
371
|
-
x: msg.x1 + loopW + labelPadding,
|
|
372
|
-
y: msg.y + loopH / 2,
|
|
373
|
-
fontSize: style.edgeLabelFontSize,
|
|
374
|
-
anchor: 'start',
|
|
375
|
-
paint: { fill: rawTextColor },
|
|
376
|
-
}, renderMultilineText(displayLabel, msg.x1 + loopW + labelPadding, msg.y + loopH / 2, style.edgeLabelFontSize,
|
|
377
|
-
`font-size="${style.edgeLabelFontSize}" text-anchor="start" font-weight="${style.edgeLabelFontWeight}"${letterAttr(style.edgeLetterSpacing)} fill="${escapeAttr(rawTextColor)}"`)),
|
|
378
|
-
})
|
|
379
|
-
} else {
|
|
380
|
-
// Normal message: horizontal arrow
|
|
381
|
-
children.push({
|
|
382
|
-
indent: 2,
|
|
383
|
-
node: marks.connector({
|
|
384
|
-
id: `${sceneId}:line`,
|
|
385
|
-
role: 'message',
|
|
386
|
-
geometry: { kind: 'line', x1: msg.x1, y1: msg.y, x2: msg.x2, y2: msg.y },
|
|
387
|
-
lineStyle: msg.lineStyle,
|
|
388
|
-
paint: linePaint,
|
|
389
|
-
endMarker,
|
|
390
|
-
},
|
|
391
|
-
`<line x1="${msg.x1}" y1="${msg.y}" x2="${msg.x2}" y2="${msg.y}" ` +
|
|
392
|
-
`stroke="${escapeAttr(rawStroke)}" stroke-width="${style.lineWidth}"${dashArray} marker-end="url(#${markerId})" />`),
|
|
393
|
-
})
|
|
394
|
-
// Label above the arrow, centered (supports multi-line)
|
|
395
|
-
const midX = (msg.x1 + msg.x2) / 2
|
|
396
|
-
children.push({
|
|
397
|
-
indent: 2,
|
|
398
|
-
node: marks.text({
|
|
399
|
-
id: `${sceneId}:label`,
|
|
400
|
-
role: 'label',
|
|
401
|
-
text: displayLabel,
|
|
402
|
-
x: midX,
|
|
403
|
-
y: msg.y - 10,
|
|
404
|
-
fontSize: style.edgeLabelFontSize,
|
|
405
|
-
anchor: 'middle',
|
|
406
|
-
paint: { fill: rawTextColor },
|
|
407
|
-
}, renderMultilineText(displayLabel, midX, msg.y - 10, style.edgeLabelFontSize,
|
|
408
|
-
`font-size="${style.edgeLabelFontSize}" text-anchor="middle" font-weight="${style.edgeLabelFontWeight}"${letterAttr(style.edgeLetterSpacing)} fill="${escapeAttr(rawTextColor)}"`)),
|
|
409
|
-
})
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
return marks.group({
|
|
413
|
-
id: sceneId,
|
|
414
|
-
role: 'message',
|
|
415
|
-
open,
|
|
416
|
-
close: '</g>',
|
|
417
|
-
children,
|
|
418
|
-
})
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
/**
|
|
422
|
-
* Render a block background (loop/alt/opt).
|
|
423
|
-
* Wrapped in <g class="block"> with semantic data attributes.
|
|
424
|
-
*/
|
|
425
|
-
function renderBlock(block: PositionedBlock, style: ResolvedRenderStyle, sceneId: string): SceneNode {
|
|
426
|
-
const children: Array<{ node: SceneNode; indent: number }> = []
|
|
427
|
-
|
|
428
|
-
// Semantic wrapper with block metadata
|
|
429
|
-
const labelAttr = block.label ? ` data-label="${escapeAttr(block.label)}"` : ''
|
|
430
|
-
const open =
|
|
431
|
-
`<g class="block" data-type="${escapeAttr(block.type)}"${labelAttr}>`
|
|
432
|
-
|
|
433
|
-
// Outer rectangle
|
|
434
|
-
const rawFill = style.groupFillColor ?? 'none'
|
|
435
|
-
const rawStroke = style.groupBorderColor ?? 'var(--_node-stroke)'
|
|
436
|
-
children.push({
|
|
437
|
-
indent: 2,
|
|
438
|
-
node: marks.shape({
|
|
439
|
-
id: `${sceneId}:rect`,
|
|
440
|
-
role: 'block',
|
|
441
|
-
geometry: { kind: 'rect', x: block.x, y: block.y, width: block.width, height: block.height, rx: style.groupCornerRadius, ry: style.groupCornerRadius },
|
|
442
|
-
paint: { fill: rawFill, stroke: rawStroke, strokeWidth: String(style.groupLineWidth) },
|
|
443
|
-
},
|
|
444
|
-
`<rect x="${block.x}" y="${block.y}" width="${block.width}" height="${block.height}" ` +
|
|
445
|
-
`rx="${style.groupCornerRadius}" ry="${style.groupCornerRadius}" fill="${escapeAttr(rawFill)}" stroke="${escapeAttr(rawStroke)}" stroke-width="${style.groupLineWidth}" />`),
|
|
446
|
-
})
|
|
447
|
-
|
|
448
|
-
// Type label tab (top-left corner)
|
|
449
|
-
// For multi-line block labels, we use the first line for the tab but show full label
|
|
450
|
-
const labelText = applyTextTransform(`${block.type}${block.label ? ` [${block.label}]` : ''}`, style.groupTextTransform)
|
|
451
|
-
const firstLine = labelText.split('\n')[0]!
|
|
452
|
-
const tabWidth = estimateTextWidth(firstLine, style.groupHeaderFontSize, style.groupHeaderFontWeight) + style.groupPaddingX * 2
|
|
453
|
-
const tabHeight = Math.max(18, style.groupHeaderFontSize + style.groupPaddingY)
|
|
454
|
-
|
|
455
|
-
const rawHeaderFill = style.groupHeaderFillColor ?? 'var(--_group-hdr)'
|
|
456
|
-
children.push({
|
|
457
|
-
indent: 2,
|
|
458
|
-
node: marks.shape({
|
|
459
|
-
id: `${sceneId}:tab`,
|
|
460
|
-
role: 'block',
|
|
461
|
-
geometry: { kind: 'rect', x: block.x, y: block.y, width: tabWidth, height: tabHeight },
|
|
462
|
-
paint: { fill: rawHeaderFill, stroke: rawStroke, strokeWidth: String(style.groupLineWidth) },
|
|
463
|
-
},
|
|
464
|
-
`<rect x="${block.x}" y="${block.y}" width="${tabWidth}" height="${tabHeight}" ` +
|
|
465
|
-
`fill="${escapeAttr(rawHeaderFill)}" stroke="${escapeAttr(rawStroke)}" stroke-width="${style.groupLineWidth}" />`),
|
|
466
|
-
})
|
|
467
|
-
// Block type label (supports multi-line via <br> tags)
|
|
468
|
-
const rawHeaderText = style.groupTextColor ?? 'var(--_text-sec)'
|
|
469
|
-
children.push({
|
|
470
|
-
indent: 2,
|
|
471
|
-
node: marks.text({
|
|
472
|
-
id: `${sceneId}:label`,
|
|
473
|
-
role: 'label',
|
|
474
|
-
text: labelText,
|
|
475
|
-
x: block.x + style.groupLabelPaddingX,
|
|
476
|
-
y: block.y + tabHeight / 2,
|
|
477
|
-
fontSize: style.groupHeaderFontSize,
|
|
478
|
-
anchor: 'start',
|
|
479
|
-
paint: { fill: rawHeaderText },
|
|
480
|
-
}, renderMultilineText(
|
|
481
|
-
labelText,
|
|
482
|
-
block.x + style.groupLabelPaddingX,
|
|
483
|
-
block.y + tabHeight / 2,
|
|
484
|
-
style.groupHeaderFontSize,
|
|
485
|
-
`font-size="${style.groupHeaderFontSize}" font-weight="${style.groupHeaderFontWeight}"${style.groupFont ? ` font-family="${escapeAttr(style.groupFont)}"` : ''}${letterAttr(style.groupLetterSpacing)} fill="${escapeAttr(rawHeaderText)}"`
|
|
486
|
-
)),
|
|
487
|
-
})
|
|
488
|
-
|
|
489
|
-
// Divider lines (for alt/else, par/and)
|
|
490
|
-
const rawDividerStroke = style.edgeStrokeColor ?? 'var(--_line)'
|
|
491
|
-
const rawDividerText = style.edgeTextColor ?? 'var(--_text-muted)'
|
|
492
|
-
let dividerIndex = 0
|
|
493
|
-
for (const divider of block.dividers) {
|
|
494
|
-
const dividerId = `${sceneId}:divider#${dividerIndex}`
|
|
495
|
-
dividerIndex++
|
|
496
|
-
const dividerStrokeWidth = Math.max(0.75, style.lineWidth * 0.75)
|
|
497
|
-
children.push({
|
|
498
|
-
indent: 2,
|
|
499
|
-
node: marks.connector({
|
|
500
|
-
id: dividerId,
|
|
501
|
-
role: 'block',
|
|
502
|
-
geometry: { kind: 'line', x1: block.x, y1: divider.y, x2: block.x + block.width, y2: divider.y },
|
|
503
|
-
lineStyle: 'dashed',
|
|
504
|
-
paint: { stroke: rawDividerStroke, strokeWidth: String(dividerStrokeWidth), strokeDasharray: '6 4' },
|
|
505
|
-
},
|
|
506
|
-
`<line x1="${block.x}" y1="${divider.y}" x2="${block.x + block.width}" y2="${divider.y}" ` +
|
|
507
|
-
`stroke="${escapeAttr(rawDividerStroke)}" stroke-width="${dividerStrokeWidth}" stroke-dasharray="6 4" />`),
|
|
508
|
-
})
|
|
509
|
-
if (divider.label) {
|
|
510
|
-
const label = applyTextTransform(`[${divider.label}]`, style.edgeTextTransform)
|
|
511
|
-
// Divider label supports multi-line
|
|
512
|
-
children.push({
|
|
513
|
-
indent: 2,
|
|
514
|
-
node: marks.text({
|
|
515
|
-
id: `${dividerId}:label`,
|
|
516
|
-
role: 'label',
|
|
517
|
-
text: label,
|
|
518
|
-
x: block.x + 8,
|
|
519
|
-
y: divider.y + 14,
|
|
520
|
-
fontSize: style.edgeLabelFontSize,
|
|
521
|
-
anchor: 'start',
|
|
522
|
-
paint: { fill: rawDividerText },
|
|
523
|
-
}, renderMultilineText(label, block.x + 8, divider.y + 14, style.edgeLabelFontSize,
|
|
524
|
-
`font-size="${style.edgeLabelFontSize}" text-anchor="start" font-weight="${style.edgeLabelFontWeight}"${letterAttr(style.edgeLetterSpacing)} fill="${escapeAttr(rawDividerText)}"`)),
|
|
525
|
-
})
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
return marks.group({
|
|
530
|
-
id: sceneId,
|
|
531
|
-
role: 'block',
|
|
532
|
-
open,
|
|
533
|
-
close: '</g>',
|
|
534
|
-
children,
|
|
535
|
-
})
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
/**
|
|
539
|
-
* Render a note box.
|
|
540
|
-
* Wrapped in <g class="note"> with semantic data attributes.
|
|
541
|
-
*/
|
|
542
|
-
function renderNote(note: PositionedNote, style: ResolvedRenderStyle, sceneId: string): SceneNode {
|
|
543
|
-
const { x, y, width: w, height: h } = note
|
|
544
|
-
|
|
545
|
-
const actorsAttr = note.actors && note.actors.length > 0
|
|
546
|
-
? ` data-actors="${note.actors.map(escapeAttr).join(',')}"`
|
|
547
|
-
: ''
|
|
548
|
-
const positionAttr = note.position ? ` data-position="${escapeAttr(note.position)}"` : ''
|
|
549
|
-
|
|
550
|
-
const rawFill = style.nodeFillColor ?? 'var(--bg)'
|
|
551
|
-
const rawStroke = style.nodeBorderColor ?? 'var(--_node-stroke)'
|
|
552
|
-
const rawTextColor = style.nodeTextColor ?? 'var(--_text-muted)'
|
|
553
|
-
const radius = style.cornerRadius ?? 0
|
|
554
|
-
const displayText = applyTextTransform(note.text, style.nodeTextTransform)
|
|
555
|
-
|
|
556
|
-
return marks.group({
|
|
557
|
-
id: sceneId,
|
|
558
|
-
role: 'note',
|
|
559
|
-
open: `<g class="note"${positionAttr}${actorsAttr}>`,
|
|
560
|
-
close: '</g>',
|
|
561
|
-
children: [
|
|
562
|
-
{
|
|
563
|
-
indent: 2,
|
|
564
|
-
node: marks.shape({
|
|
565
|
-
id: `${sceneId}:rect`,
|
|
566
|
-
role: 'note',
|
|
567
|
-
geometry: { kind: 'rect', x, y, width: w, height: h, rx: radius, ry: radius },
|
|
568
|
-
paint: { fill: rawFill, stroke: rawStroke, strokeWidth: String(STROKE_WIDTHS.innerBox) },
|
|
569
|
-
},
|
|
570
|
-
`<rect x="${x}" y="${y}" width="${w}" height="${h}" rx="${radius}" ry="${radius}" ` +
|
|
571
|
-
`fill="${escapeAttr(rawFill)}" stroke="${escapeAttr(rawStroke)}" stroke-width="${STROKE_WIDTHS.innerBox}" />`),
|
|
572
|
-
},
|
|
573
|
-
{
|
|
574
|
-
indent: 2,
|
|
575
|
-
node: marks.text({
|
|
576
|
-
id: `${sceneId}:text`,
|
|
577
|
-
role: 'label',
|
|
578
|
-
text: displayText,
|
|
579
|
-
x: x + w / 2,
|
|
580
|
-
y: y + h / 2,
|
|
581
|
-
fontSize: style.nodeLabelFontSize,
|
|
582
|
-
anchor: 'middle',
|
|
583
|
-
paint: { fill: rawTextColor },
|
|
584
|
-
}, renderMultilineText(displayText, x + w / 2, y + h / 2, style.nodeLabelFontSize,
|
|
585
|
-
`font-size="${style.nodeLabelFontSize}" text-anchor="middle" font-weight="${style.nodeLabelFontWeight}"${letterAttr(style.nodeLetterSpacing)} fill="${escapeAttr(rawTextColor)}"`)),
|
|
586
|
-
},
|
|
587
|
-
],
|
|
588
|
-
})
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
// ============================================================================
|
|
592
|
-
// Utilities
|
|
593
|
-
// ============================================================================
|
|
594
|
-
|
|
595
|
-
function letterAttr(value: number): string {
|
|
596
|
-
return value !== 0 ? ` letter-spacing="${value}"` : ''
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
// Use shared escapeXml from multiline-utils
|
|
600
|
-
const escapeXml = escapeXmlUtil
|
|
601
|
-
|
|
602
|
-
/**
|
|
603
|
-
* Escape a string for use as an XML/HTML attribute value.
|
|
604
|
-
*/
|