agentic-mermaid 0.1.1 → 0.2.0
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/AGENT_NATIVE.md +155 -91
- package/CHANGELOG.md +163 -4
- package/Instructions_for_agents.md +10 -10
- package/LICENSES/Apache-2.0.txt +202 -0
- package/README.md +31 -38
- package/THIRD_PARTY_NOTICES.md +29 -0
- package/assets/hero.png +0 -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 +33626 -40446
- package/dist/chunk-2P5FKUQC.js +3485 -0
- package/dist/chunk-2S3VSS6I.js +149 -0
- package/dist/chunk-2SILJDW2.js +37 -0
- package/dist/chunk-BJYMBT4I.js +61963 -0
- package/dist/chunk-FDM43XU7.js +615 -0
- package/dist/chunk-MYPDVZCO.js +4702 -0
- package/dist/index-DWPa2MJs.d.ts +6780 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +310 -33014
- package/dist/mcp-cli-5J47WEY3.js +17 -0
- package/docs/README.md +39 -31
- package/docs/agent-api-cookbook.md +44 -35
- package/docs/agent-mutation-policy.md +1 -1
- package/docs/agent-workflow-examples.md +2 -2
- package/docs/api.md +284 -77
- package/docs/ascii.md +26 -16
- package/docs/code-mode-impact.md +214 -0
- package/docs/comparison.md +23 -11
- package/docs/computational-aesthetics-prototype-plan.md +252 -0
- package/docs/config.md +26 -10
- package/docs/custom-fonts.md +265 -0
- package/docs/custom-style-cookbook.md +130 -9
- package/docs/diagram-families.md +48 -23
- package/docs/features.md +59 -52
- package/docs/fork-differences.md +6 -3
- package/docs/getting-started.md +23 -16
- package/docs/issue-derived-test-cases.md +29 -22
- package/docs/mcp-code-mode-rationale.md +13 -6
- package/docs/mcp-http-transport.md +20 -13
- package/docs/mermaid-layout-complaints.md +66 -52
- package/docs/mutation-testing.md +145 -66
- package/docs/quality.md +23 -14
- package/docs/react.md +2 -2
- package/docs/schemas/style-spec.schema.json +1221 -30
- package/docs/style-authoring.md +241 -38
- package/docs/svg-semantic-contract.md +49 -0
- package/docs/testing-strategy.md +166 -58
- package/docs/theming.md +19 -32
- 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 +85 -45
- 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/TODO.md +0 -684
- package/dist/index-wjRjbsP-.d.ts +0 -1102
- 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/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/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/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/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/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/scene/backend.ts
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
// ============================================================================
|
|
2
|
-
// StyleBackend interface + DefaultBackend + backend registry (SPEC §3.2).
|
|
3
|
-
//
|
|
4
|
-
// A backend consumes a SceneDoc and produces the SVG document string. The
|
|
5
|
-
// DefaultBackend is the Agentic Mermaid crisp renderer: it emits each mark's
|
|
6
|
-
// construction-time crisp serialization verbatim, so its output is
|
|
7
|
-
// byte-identical to the pre-IR string renderers (svg-equivalence.test.ts is
|
|
8
|
-
// the corpus-wide gate). Styled backends (rough/hybrid) redraw shape and
|
|
9
|
-
// connector marks from their semantic fields and re-derive the document shell
|
|
10
|
-
// from PreludeMark parameters; they never dispatch on diagram family.
|
|
11
|
-
// ============================================================================
|
|
12
|
-
|
|
13
|
-
import type { SceneDoc, SceneNode } from './ir.ts'
|
|
14
|
-
import { indentLines } from './marks.ts'
|
|
15
|
-
import type { StyleSpec } from './style-registry.ts'
|
|
16
|
-
|
|
17
|
-
export interface StyleBackendContext {
|
|
18
|
-
/** User-supplied deterministic re-roll seed (RenderOptions.seed). */
|
|
19
|
-
seed: number
|
|
20
|
-
/** The selected style (undefined on the crisp default path). */
|
|
21
|
-
style?: StyleSpec
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface StyleBackend {
|
|
25
|
-
id: string
|
|
26
|
-
/** Serialize one top-level mark (recursing through groups). */
|
|
27
|
-
drawNode(node: SceneNode, ctx: StyleBackendContext): string
|
|
28
|
-
/** Serialize the whole document. */
|
|
29
|
-
render(doc: SceneDoc, ctx: StyleBackendContext): string
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** Recompose a group from (possibly restyled) child serializations using the
|
|
33
|
-
* group's own indent/join rules. Shared by all backends so wrapper semantics
|
|
34
|
-
* (classes, data-*, ARIA) stay identical across styles. */
|
|
35
|
-
export function composeGroup(
|
|
36
|
-
open: string,
|
|
37
|
-
close: string,
|
|
38
|
-
join: string,
|
|
39
|
-
children: Array<{ serialized: string; indent: number }>,
|
|
40
|
-
): string {
|
|
41
|
-
return [open, ...children.map(c => indentLines(c.serialized, c.indent)), close].join(join)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** Styled documents carry an explicit page rect after the prelude — resvg
|
|
45
|
-
* does not paint the root style="background:…" CSS (SPEC §10). Emitted by
|
|
46
|
-
* every backend when a style is active and the document isn't transparent;
|
|
47
|
-
* the crisp path (no style) is byte-identical to previous releases. */
|
|
48
|
-
export function pageRectFor(doc: SceneDoc, ctx: StyleBackendContext): string {
|
|
49
|
-
const prelude = doc.parts[0]
|
|
50
|
-
if (!ctx.style || prelude?.kind !== 'prelude' || prelude.prelude.transparent) return ''
|
|
51
|
-
return `<rect width="${doc.width}" height="${doc.height}" fill="var(--bg)" data-backdrop="page" />`
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export const DefaultBackend: StyleBackend = {
|
|
55
|
-
id: 'default',
|
|
56
|
-
drawNode(node: SceneNode): string {
|
|
57
|
-
return node.crisp
|
|
58
|
-
},
|
|
59
|
-
render(doc: SceneDoc, ctx: StyleBackendContext): string {
|
|
60
|
-
const pageRect = pageRectFor(doc, ctx)
|
|
61
|
-
if (!pageRect) return doc.parts.map(part => part.crisp).join('\n')
|
|
62
|
-
return doc.parts
|
|
63
|
-
.map((part, i) => (i === 0 ? `${part.crisp}\n${pageRect}` : part.crisp))
|
|
64
|
-
.join('\n')
|
|
65
|
-
},
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const BACKENDS = new Map<string, StyleBackend>([[DefaultBackend.id, DefaultBackend]])
|
|
69
|
-
|
|
70
|
-
export function registerBackend(backend: StyleBackend): void {
|
|
71
|
-
BACKENDS.set(backend.id, backend)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export function getBackend(id: string): StyleBackend | undefined {
|
|
75
|
-
return BACKENDS.get(id)
|
|
76
|
-
}
|
package/src/scene/fidelity.ts
DELETED
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
// ============================================================================
|
|
2
|
-
// Scene fidelity checker — asserts a mark's semantic fields agree with its
|
|
3
|
-
// crisp serialization, so styled backends can trust the geometry they redraw.
|
|
4
|
-
//
|
|
5
|
-
// Test-only oracle (driven by scene-fidelity.test.ts across the corpus): it
|
|
6
|
-
// parses the crisp strings we ourselves emit — a stable, owned format — and
|
|
7
|
-
// returns human-readable problems instead of throwing, so one run reports
|
|
8
|
-
// every divergence in a lowering.
|
|
9
|
-
// ============================================================================
|
|
10
|
-
|
|
11
|
-
import type { Geometry, SceneDoc, SceneNode } from './ir.ts'
|
|
12
|
-
|
|
13
|
-
/** Parse the top-level SVG elements out of a crisp chunk (self-closed or
|
|
14
|
-
* paired), ignoring nested content. Good enough for our own emitters. */
|
|
15
|
-
export function topLevelElements(crisp: string): Array<{ tag: string; attrs: Map<string, string> }> {
|
|
16
|
-
const out: Array<{ tag: string; attrs: Map<string, string> }> = []
|
|
17
|
-
const re = /<([a-zA-Z][a-zA-Z0-9-]*)((?:\s+[a-zA-Z_:][\w:.-]*="[^"]*")*)\s*\/?>/g
|
|
18
|
-
let depth = 0
|
|
19
|
-
let m: RegExpExecArray | null
|
|
20
|
-
const closeRe = /<\/([a-zA-Z][a-zA-Z0-9-]*)>/g
|
|
21
|
-
// Walk open/self-close/close tags in order, tracking depth.
|
|
22
|
-
const tokens: Array<{ index: number; kind: 'open' | 'self' | 'close'; tag: string; attrs?: Map<string, string> }> = []
|
|
23
|
-
while ((m = re.exec(crisp)) !== null) {
|
|
24
|
-
const attrs = new Map<string, string>()
|
|
25
|
-
for (const am of m[2]!.matchAll(/([a-zA-Z_:][\w:.-]*)="([^"]*)"/g)) attrs.set(am[1]!, am[2]!)
|
|
26
|
-
tokens.push({ index: m.index, kind: m[0]!.endsWith('/>') ? 'self' : 'open', tag: m[1]!, attrs })
|
|
27
|
-
}
|
|
28
|
-
while ((m = closeRe.exec(crisp)) !== null) tokens.push({ index: m.index, kind: 'close', tag: m[1]! })
|
|
29
|
-
tokens.sort((a, b) => a.index - b.index)
|
|
30
|
-
for (const t of tokens) {
|
|
31
|
-
if (t.kind === 'self') {
|
|
32
|
-
if (depth === 0) out.push({ tag: t.tag, attrs: t.attrs! })
|
|
33
|
-
} else if (t.kind === 'open') {
|
|
34
|
-
if (depth === 0) out.push({ tag: t.tag, attrs: t.attrs! })
|
|
35
|
-
depth++
|
|
36
|
-
} else {
|
|
37
|
-
depth--
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
return out
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function num(s: string | undefined): number | undefined {
|
|
44
|
-
if (s === undefined) return undefined
|
|
45
|
-
const n = Number(s)
|
|
46
|
-
return Number.isFinite(n) ? n : undefined
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function geometryProblems(geom: Geometry, els: Array<{ tag: string; attrs: Map<string, string> }>, path: string, problems: string[]): number {
|
|
50
|
-
// Returns how many elements this geometry consumed.
|
|
51
|
-
const el = els[0]
|
|
52
|
-
const want = (tag: string) => {
|
|
53
|
-
if (!el) { problems.push(`${path}: expected <${tag}>, crisp has no element`); return undefined }
|
|
54
|
-
if (el.tag !== tag) { problems.push(`${path}: expected <${tag}>, crisp has <${el.tag}>`); return undefined }
|
|
55
|
-
return el
|
|
56
|
-
}
|
|
57
|
-
const eq = (field: string, semantic: number | string | undefined, attr: string | undefined) => {
|
|
58
|
-
if (semantic === undefined) return
|
|
59
|
-
if (String(semantic) !== attr && num(attr) !== semantic) {
|
|
60
|
-
problems.push(`${path}.${field}: semantic ${semantic} != crisp ${attr}`)
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
switch (geom.kind) {
|
|
64
|
-
case 'rect': {
|
|
65
|
-
const e = want('rect'); if (!e) return 1
|
|
66
|
-
eq('x', geom.x, e.attrs.get('x')); eq('y', geom.y, e.attrs.get('y'))
|
|
67
|
-
eq('width', geom.width, e.attrs.get('width')); eq('height', geom.height, e.attrs.get('height'))
|
|
68
|
-
if (geom.rx !== undefined) eq('rx', geom.rx, e.attrs.get('rx'))
|
|
69
|
-
return 1
|
|
70
|
-
}
|
|
71
|
-
case 'circle': {
|
|
72
|
-
const e = want('circle'); if (!e) return 1
|
|
73
|
-
eq('cx', geom.cx, e.attrs.get('cx')); eq('cy', geom.cy, e.attrs.get('cy')); eq('r', geom.r, e.attrs.get('r'))
|
|
74
|
-
return 1
|
|
75
|
-
}
|
|
76
|
-
case 'ellipse': {
|
|
77
|
-
const e = want('ellipse'); if (!e) return 1
|
|
78
|
-
eq('cx', geom.cx, e.attrs.get('cx')); eq('cy', geom.cy, e.attrs.get('cy'))
|
|
79
|
-
eq('rx', geom.rx, e.attrs.get('rx')); eq('ry', geom.ry, e.attrs.get('ry'))
|
|
80
|
-
return 1
|
|
81
|
-
}
|
|
82
|
-
case 'line': {
|
|
83
|
-
const e = want('line'); if (!e) return 1
|
|
84
|
-
eq('x1', geom.x1, e.attrs.get('x1')); eq('y1', geom.y1, e.attrs.get('y1'))
|
|
85
|
-
eq('x2', geom.x2, e.attrs.get('x2')); eq('y2', geom.y2, e.attrs.get('y2'))
|
|
86
|
-
return 1
|
|
87
|
-
}
|
|
88
|
-
case 'polygon':
|
|
89
|
-
case 'polyline': {
|
|
90
|
-
const e = want(geom.kind); if (!e) return 1
|
|
91
|
-
const expected = geom.points.map(p => `${p.x},${p.y}`).join(' ')
|
|
92
|
-
if (e.attrs.get('points') !== expected) {
|
|
93
|
-
problems.push(`${path}.points: semantic "${expected.slice(0, 60)}" != crisp "${(e.attrs.get('points') ?? '').slice(0, 60)}"`)
|
|
94
|
-
}
|
|
95
|
-
return 1
|
|
96
|
-
}
|
|
97
|
-
case 'path': {
|
|
98
|
-
const e = want('path'); if (!e) return 1
|
|
99
|
-
if (e.attrs.get('d') !== geom.d) {
|
|
100
|
-
problems.push(`${path}.d: semantic "${geom.d.slice(0, 60)}" != crisp "${(e.attrs.get('d') ?? '').slice(0, 60)}"`)
|
|
101
|
-
}
|
|
102
|
-
return 1
|
|
103
|
-
}
|
|
104
|
-
case 'compound': {
|
|
105
|
-
let consumed = 0
|
|
106
|
-
for (let i = 0; i < geom.children.length; i++) {
|
|
107
|
-
consumed += geometryProblems(geom.children[i]!, els.slice(consumed), `${path}.children[${i}]`, problems)
|
|
108
|
-
}
|
|
109
|
-
return consumed
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function unescapeXml(s: string): string {
|
|
115
|
-
return s.replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, '&')
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
export function nodeProblems(node: SceneNode, path: string, problems: string[]): void {
|
|
119
|
-
switch (node.kind) {
|
|
120
|
-
case 'shape': {
|
|
121
|
-
const els = topLevelElements(node.crisp)
|
|
122
|
-
geometryProblems(node.geometry, els, `${path}(shape:${node.id})`, problems)
|
|
123
|
-
return
|
|
124
|
-
}
|
|
125
|
-
case 'connector': {
|
|
126
|
-
if (node.crisp === '') {
|
|
127
|
-
if (node.lineStyle !== 'invisible') problems.push(`${path}(connector:${node.id}): empty crisp but lineStyle=${node.lineStyle}`)
|
|
128
|
-
return
|
|
129
|
-
}
|
|
130
|
-
const els = topLevelElements(node.crisp)
|
|
131
|
-
geometryProblems(node.geometry as Geometry, els, `${path}(connector:${node.id})`, problems)
|
|
132
|
-
const el = els[0]
|
|
133
|
-
if (el && node.endMarker && !(el.attrs.get('marker-end') ?? '').includes(`#${node.endMarker.id}`)) {
|
|
134
|
-
problems.push(`${path}(connector:${node.id}): endMarker ${node.endMarker.id} not in crisp marker-end`)
|
|
135
|
-
}
|
|
136
|
-
if (el && node.startMarker && !(el.attrs.get('marker-start') ?? '').includes(`#${node.startMarker.id}`)) {
|
|
137
|
-
problems.push(`${path}(connector:${node.id}): startMarker ${node.startMarker.id} not in crisp marker-start`)
|
|
138
|
-
}
|
|
139
|
-
return
|
|
140
|
-
}
|
|
141
|
-
case 'text': {
|
|
142
|
-
if (node.crisp === '') { problems.push(`${path}(text:${node.id}): empty crisp`); return }
|
|
143
|
-
const els = topLevelElements(node.crisp)
|
|
144
|
-
const el = els.find(e => e.tag === 'text')
|
|
145
|
-
if (!el) { problems.push(`${path}(text:${node.id}): crisp has no <text>`); return }
|
|
146
|
-
const fs = el.attrs.get('font-size')
|
|
147
|
-
if (fs !== undefined && num(fs) !== node.fontSize) {
|
|
148
|
-
problems.push(`${path}(text:${node.id}).fontSize: semantic ${node.fontSize} != crisp ${fs}`)
|
|
149
|
-
}
|
|
150
|
-
// The semantic text must appear in the crisp chunk. Both sides are
|
|
151
|
-
// normalized the way the text emitter normalizes labels (markdown
|
|
152
|
-
// backticks, <b>/<i>/<u>/<s> emphasis tags, whitespace), so formatted
|
|
153
|
-
// labels don't false-positive.
|
|
154
|
-
const normalize = (s: string) => unescapeXml(s)
|
|
155
|
-
.replace(/<br\s*\/?>/gi, ' ')
|
|
156
|
-
.replace(/<[^>]+>/g, '')
|
|
157
|
-
.replace(/[`*_]/g, '')
|
|
158
|
-
.replace(/\s+/g, ' ')
|
|
159
|
-
.trim()
|
|
160
|
-
const wantText = normalize(node.text)
|
|
161
|
-
if (wantText && !normalize(node.crisp).includes(wantText.split(' ')[0]!)) {
|
|
162
|
-
problems.push(`${path}(text:${node.id}): text "${wantText.slice(0, 40)}" not found in crisp`)
|
|
163
|
-
}
|
|
164
|
-
return
|
|
165
|
-
}
|
|
166
|
-
case 'group': {
|
|
167
|
-
if (!node.crisp.startsWith(node.open)) problems.push(`${path}(group:${node.id}): crisp does not start with open tag`)
|
|
168
|
-
if (!node.crisp.endsWith(node.close)) problems.push(`${path}(group:${node.id}): crisp does not end with close tag`)
|
|
169
|
-
node.children.forEach((child, i) => nodeProblems(child.node, `${path}/${i}`, problems))
|
|
170
|
-
return
|
|
171
|
-
}
|
|
172
|
-
case 'raw':
|
|
173
|
-
case 'prelude':
|
|
174
|
-
return
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/** Validate a whole SceneDoc. Returns [] when every mark is faithful. */
|
|
179
|
-
export function sceneFidelityProblems(doc: SceneDoc): string[] {
|
|
180
|
-
const problems: string[] = []
|
|
181
|
-
doc.parts.forEach((part, i) => nodeProblems(part, `parts[${i}]`, problems))
|
|
182
|
-
return problems
|
|
183
|
-
}
|
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
// ============================================================================
|
|
2
|
-
// HybridBackend (SPEC §3.2/§11 phase 4) — rough.js composition plus the
|
|
3
|
-
// native marks rough.js does not provide: pressure-sensitive perfect-freehand
|
|
4
|
-
// ribbons (variable-width filled outlines from a jittered centerline) and
|
|
5
|
-
// watercolor washes (translucent glaze + edge darkening, Curtis '97 in cheap
|
|
6
|
-
// form). Everything else — role policy, halos, marker carriers, defs — is the
|
|
7
|
-
// shared sketch walker from rough-backend.ts; geometries the native marks
|
|
8
|
-
// can't express (arbitrary paths) fall back to rough.js.
|
|
9
|
-
//
|
|
10
|
-
// Deterministic per the §8 substream contract: the centerline sampler and
|
|
11
|
-
// wash jitter draw only from nodeSeed-derived mulberry32 streams.
|
|
12
|
-
// ============================================================================
|
|
13
|
-
|
|
14
|
-
import { getStroke } from 'perfect-freehand'
|
|
15
|
-
import type { Geometry } from './ir.ts'
|
|
16
|
-
import { registerBackend } from './backend.ts'
|
|
17
|
-
import { createSketchBackend, sketchGeometryRough } from './rough-backend.ts'
|
|
18
|
-
import type { GeometrySketcher } from './rough-backend.ts'
|
|
19
|
-
import { makeRng } from './seed.ts'
|
|
20
|
-
|
|
21
|
-
const r3 = (n: number) => Math.round(n * 1000) / 1000
|
|
22
|
-
const lerp = (a: number, b: number, t: number) => a + (b - a) * t
|
|
23
|
-
const clamp01 = (t: number) => Math.max(0, Math.min(1, t))
|
|
24
|
-
|
|
25
|
-
type Pt = { x: number; y: number }
|
|
26
|
-
|
|
27
|
-
/** Convert closed geometries to a polygon for freehand/wash rendering.
|
|
28
|
-
* Returns null when the geometry can't be polygonized cheaply (paths). */
|
|
29
|
-
function polygonize(geom: Geometry): Pt[] | null {
|
|
30
|
-
switch (geom.kind) {
|
|
31
|
-
case 'rect':
|
|
32
|
-
return [
|
|
33
|
-
{ x: geom.x, y: geom.y },
|
|
34
|
-
{ x: geom.x + geom.width, y: geom.y },
|
|
35
|
-
{ x: geom.x + geom.width, y: geom.y + geom.height },
|
|
36
|
-
{ x: geom.x, y: geom.y + geom.height },
|
|
37
|
-
]
|
|
38
|
-
case 'polygon':
|
|
39
|
-
return geom.points
|
|
40
|
-
case 'circle': {
|
|
41
|
-
const out: Pt[] = []
|
|
42
|
-
for (let i = 0; i < 24; i++) {
|
|
43
|
-
const a = (i / 24) * Math.PI * 2
|
|
44
|
-
out.push({ x: geom.cx + Math.cos(a) * geom.r, y: geom.cy + Math.sin(a) * geom.r })
|
|
45
|
-
}
|
|
46
|
-
return out
|
|
47
|
-
}
|
|
48
|
-
case 'ellipse': {
|
|
49
|
-
const out: Pt[] = []
|
|
50
|
-
for (let i = 0; i < 28; i++) {
|
|
51
|
-
const a = (i / 28) * Math.PI * 2
|
|
52
|
-
out.push({ x: geom.cx + Math.cos(a) * geom.rx, y: geom.cy + Math.sin(a) * geom.ry })
|
|
53
|
-
}
|
|
54
|
-
return out
|
|
55
|
-
}
|
|
56
|
-
default:
|
|
57
|
-
return null
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function strokePathFromOutline(outline: number[][]): string {
|
|
62
|
-
if (outline.length < 2) return ''
|
|
63
|
-
const d = [`M${r3(outline[0]![0]!)},${r3(outline[0]![1]!)}`]
|
|
64
|
-
for (let i = 1; i < outline.length; i++) {
|
|
65
|
-
const [x0, y0] = outline[i]!
|
|
66
|
-
const [x1, y1] = outline[(i + 1) % outline.length]!
|
|
67
|
-
d.push(`Q${r3(x0!)},${r3(y0!)} ${r3((x0! + x1!) / 2)},${r3((y0! + y1!) / 2)}`)
|
|
68
|
-
}
|
|
69
|
-
d.push('Z')
|
|
70
|
-
return d.join(' ')
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/** Resample a polyline/polygon into a jittered, pressure-annotated centerline
|
|
74
|
-
* (ported from the prototype's engine.ts, behavior unchanged). */
|
|
75
|
-
function sampledCenterline(pts: Pt[], rng: () => number, closed: boolean, wobble: number): [number, number, number][] {
|
|
76
|
-
const src = closed ? [...pts, pts[0]!] : pts
|
|
77
|
-
const out: [number, number, number][] = []
|
|
78
|
-
for (let i = 0; i < src.length - 1; i++) {
|
|
79
|
-
const a = src[i]!, b = src[i + 1]!
|
|
80
|
-
const len = Math.hypot(b.x - a.x, b.y - a.y) || 1
|
|
81
|
-
const steps = Math.max(1, Math.ceil(len / 14))
|
|
82
|
-
const nx = -(b.y - a.y) / len, ny = (b.x - a.x) / len
|
|
83
|
-
for (let j = 0; j < steps; j++) {
|
|
84
|
-
if (i > 0 && j === 0) continue
|
|
85
|
-
const t = j / steps
|
|
86
|
-
const atEnd = !closed && ((i === 0 && j === 0) || (i === src.length - 2 && j === steps))
|
|
87
|
-
const jitter = atEnd ? 0 : (rng() - 0.5) * 2 * wobble
|
|
88
|
-
const p = { x: lerp(a.x, b.x, t) + nx * jitter, y: lerp(a.y, b.y, t) + ny * jitter }
|
|
89
|
-
const phase = (i + t) / Math.max(1, src.length - 1)
|
|
90
|
-
const pressure = clamp01(0.52 + Math.sin(phase * Math.PI * 2) * 0.12 + (rng() - 0.5) * 0.16)
|
|
91
|
-
out.push([p.x, p.y, pressure])
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
const last = src[src.length - 1]!
|
|
95
|
-
out.push([last.x, last.y, closed ? out[0]?.[2] ?? 0.55 : 0.46])
|
|
96
|
-
return out
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function freehandStroke(pts: Pt[], rng: () => number, o: { width: number; closed: boolean }): string {
|
|
100
|
-
if (pts.length < 2) return ''
|
|
101
|
-
const input = sampledCenterline(pts, rng, o.closed, 1)
|
|
102
|
-
const outline = getStroke(input, {
|
|
103
|
-
size: o.width,
|
|
104
|
-
thinning: 0.58,
|
|
105
|
-
smoothing: 0.62,
|
|
106
|
-
streamline: 0.35,
|
|
107
|
-
simulatePressure: false,
|
|
108
|
-
start: { cap: true, taper: o.closed ? 0 : 10 },
|
|
109
|
-
end: { cap: true, taper: o.closed ? 0 : 14 },
|
|
110
|
-
last: true,
|
|
111
|
-
}) as number[][]
|
|
112
|
-
return strokePathFromOutline(outline)
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/** Watercolor wash: wobbled translucent fill + edge-darkening stroke. */
|
|
116
|
-
function watercolorWash(poly: Pt[], rng: () => number, fill: string, opacity: number, edge: number): string {
|
|
117
|
-
const wob = poly.map(p => ({ x: p.x + (rng() - 0.5) * 3, y: p.y + (rng() - 0.5) * 3 }))
|
|
118
|
-
const d = wob.map((p, i) => `${i ? 'L' : 'M'}${r3(p.x)},${r3(p.y)}`).join(' ') + ' Z'
|
|
119
|
-
return (
|
|
120
|
-
`<path d="${d}" fill="${fill}" fill-opacity="${opacity}" stroke="none" />` +
|
|
121
|
-
`\n<path d="${d}" fill="none" stroke="${fill}" stroke-width="1.4" stroke-opacity="${edge}" />`
|
|
122
|
-
)
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/** The hybrid sketcher: freehand ribbons when the style asks for them, wash
|
|
126
|
-
* fills under the outline; null falls back to rough.js in the walker. */
|
|
127
|
-
const hybridSketcher: GeometrySketcher = (geom, opts) => {
|
|
128
|
-
const strokeKind = opts.style?.stroke ?? 'jittered'
|
|
129
|
-
const fillKind = opts.style?.fill ?? 'none'
|
|
130
|
-
const wantsWash = fillKind === 'wash' && opts.fill !== undefined
|
|
131
|
-
const wantsFreehand = strokeKind === 'freehand'
|
|
132
|
-
if (!wantsWash && !wantsFreehand) return null
|
|
133
|
-
|
|
134
|
-
const parts: string[] = []
|
|
135
|
-
if (wantsWash) {
|
|
136
|
-
const rng = makeRng((opts.seed ^ 0x9e3779b9) >>> 0)
|
|
137
|
-
const poly = geom.kind === 'path' ? null : polygonize(geom)
|
|
138
|
-
if (poly) {
|
|
139
|
-
parts.push(watercolorWash(poly, rng, opts.fill!, opts.style?.washOpacity ?? 0.3, opts.style?.washEdge ?? 0.34))
|
|
140
|
-
} else if (geom.kind === 'path') {
|
|
141
|
-
// Arbitrary paths (pie wedges): translucent glaze on the exact path.
|
|
142
|
-
parts.push(`<path d="${geom.d}" fill="${opts.fill}" fill-opacity="${opts.style?.washOpacity ?? 0.3}" stroke="${opts.fill}" stroke-width="1.4" stroke-opacity="${opts.style?.washEdge ?? 0.34}" />`)
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// Dashed/dotted connectors keep their dash semantics: a filled ribbon
|
|
147
|
-
// cannot carry stroke-dasharray, so they fall through to the rough stroke.
|
|
148
|
-
if (wantsFreehand && !opts.dash) {
|
|
149
|
-
const rng = makeRng(opts.seed || 1)
|
|
150
|
-
const width = Math.max(2.5, opts.width * 2.2)
|
|
151
|
-
let ribbon = ''
|
|
152
|
-
if (geom.kind === 'polyline') {
|
|
153
|
-
ribbon = freehandStroke(geom.points, rng, { width, closed: false })
|
|
154
|
-
} else if (geom.kind === 'line') {
|
|
155
|
-
ribbon = freehandStroke([{ x: geom.x1, y: geom.y1 }, { x: geom.x2, y: geom.y2 }], rng, { width, closed: false })
|
|
156
|
-
} else {
|
|
157
|
-
const poly = polygonize(geom)
|
|
158
|
-
if (poly) ribbon = freehandStroke(poly, rng, { width, closed: true })
|
|
159
|
-
}
|
|
160
|
-
if (ribbon) {
|
|
161
|
-
parts.push(`<path d="${ribbon}" fill="${opts.stroke}" stroke="none" />`)
|
|
162
|
-
return parts.join('\n')
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// Outline falls back to rough (jittered); wash already drew the fill.
|
|
167
|
-
const outline = sketchGeometryRough(geom, opts.seed, opts.stroke, opts.width, wantsWash ? undefined : opts.fill, opts.p, opts.dash)
|
|
168
|
-
if (outline) parts.push(outline)
|
|
169
|
-
return parts.length > 0 ? parts.join('\n') : null
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
export const HybridBackend = createSketchBackend('hybrid', hybridSketcher)
|
|
173
|
-
|
|
174
|
-
registerBackend(HybridBackend)
|
package/src/scene/ir.ts
DELETED
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
// ============================================================================
|
|
2
|
-
// SceneGraph IR — the post-layout semantic render-mark tree (SPEC §3.1).
|
|
3
|
-
//
|
|
4
|
-
// Family renderers lower their positioned results to a SceneDoc instead of
|
|
5
|
-
// concatenating SVG strings directly. Each mark carries BOTH:
|
|
6
|
-
// - semantic fields (role, geometry, paints, channels, stable id) that
|
|
7
|
-
// styled backends (rough/hybrid) consume, and
|
|
8
|
-
// - the exact crisp serialization, built at construction time from the same
|
|
9
|
-
// inputs by the mark constructors in marks.ts, which the DefaultBackend
|
|
10
|
-
// emits verbatim — so the default path stays byte-identical to the
|
|
11
|
-
// pre-IR string renderers (guarded by svg-equivalence.test.ts).
|
|
12
|
-
//
|
|
13
|
-
// The scene-fidelity test (scene-fidelity.test.ts) parses each mark's crisp
|
|
14
|
-
// element and asserts the semantic fields agree with it, so a styled backend
|
|
15
|
-
// can never silently draw different geometry than crisp output shows.
|
|
16
|
-
//
|
|
17
|
-
// Determinism: constructing a SceneDoc is pure — no RNG, no clock. Stochastic
|
|
18
|
-
// styling happens inside styled backends, seeded per §8's substream contract
|
|
19
|
-
// (see seed.ts).
|
|
20
|
-
// ============================================================================
|
|
21
|
-
|
|
22
|
-
import type { DiagramColors } from '../theme.ts'
|
|
23
|
-
|
|
24
|
-
/** Semantic channels preserved across the renderer boundary (SPEC §5).
|
|
25
|
-
* `tone`/hue are style-derived outputs, not stored here. */
|
|
26
|
-
export interface SemanticChannels {
|
|
27
|
-
/** Hierarchy / callout weight / star magnitude. */
|
|
28
|
-
importance?: number
|
|
29
|
-
/** Chart value or quantitative measure (normalized where known). */
|
|
30
|
-
value?: number
|
|
31
|
-
/** Chart series, entity type, swimlane, section identity. */
|
|
32
|
-
category?: string
|
|
33
|
-
/** e.g. gantt 'done' | 'active' | 'crit'; state 'start' | 'end'. */
|
|
34
|
-
status?: string
|
|
35
|
-
/** Task completion in [0,1]. */
|
|
36
|
-
progress?: number
|
|
37
|
-
/** Transit route / PCB net identity. */
|
|
38
|
-
route?: string
|
|
39
|
-
/** Author-flagged emphasis. */
|
|
40
|
-
emphasis?: boolean
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/** Mark roles. A closed set per family keeps backends role-aware ("don't
|
|
44
|
-
* hachure a lifeline; do hatch a node") without importing family modules. */
|
|
45
|
-
export type SceneRole =
|
|
46
|
-
// shared / flowchart
|
|
47
|
-
| 'node' | 'edge' | 'edge-label' | 'group' | 'group-header' | 'label'
|
|
48
|
-
// sequence
|
|
49
|
-
| 'actor' | 'lifeline' | 'activation' | 'message' | 'block' | 'note'
|
|
50
|
-
// class / er
|
|
51
|
-
| 'class-box' | 'member' | 'entity' | 'attribute' | 'relationship' | 'cardinality'
|
|
52
|
-
// charts
|
|
53
|
-
| 'pie-slice' | 'legend' | 'bar' | 'series' | 'point' | 'axis' | 'grid'
|
|
54
|
-
| 'plate' | 'section' | 'task' | 'milestone' | 'marker-line'
|
|
55
|
-
// timeline / journey / architecture
|
|
56
|
-
| 'rail' | 'period' | 'event' | 'score' | 'actor-pill' | 'service' | 'junction' | 'icon'
|
|
57
|
-
// document furniture
|
|
58
|
-
| 'title' | 'defs' | 'prelude' | 'chrome'
|
|
59
|
-
|
|
60
|
-
/** Geometry a styled backend can redraw. Numbers are in final user units.
|
|
61
|
-
* Paint strings may be CSS var()/color-mix() refs — they resolve later via
|
|
62
|
-
* inlineResolvedColors, exactly like the crisp path. */
|
|
63
|
-
export type Geometry =
|
|
64
|
-
| { kind: 'rect'; x: number; y: number; width: number; height: number; rx?: number; ry?: number }
|
|
65
|
-
| { kind: 'circle'; cx: number; cy: number; r: number }
|
|
66
|
-
| { kind: 'ellipse'; cx: number; cy: number; rx: number; ry: number }
|
|
67
|
-
| { kind: 'line'; x1: number; y1: number; x2: number; y2: number }
|
|
68
|
-
| { kind: 'polygon'; points: Array<{ x: number; y: number }> }
|
|
69
|
-
| { kind: 'polyline'; points: Array<{ x: number; y: number }> }
|
|
70
|
-
| { kind: 'path'; d: string }
|
|
71
|
-
| { kind: 'compound'; children: Geometry[] }
|
|
72
|
-
|
|
73
|
-
/** The resolved paint attached to a mark — already cascaded (theme tokens,
|
|
74
|
-
* classDef, inline style, role defaults) by the family lowering. */
|
|
75
|
-
export interface MarkPaint {
|
|
76
|
-
fill?: string
|
|
77
|
-
stroke?: string
|
|
78
|
-
strokeWidth?: string
|
|
79
|
-
strokeDasharray?: string
|
|
80
|
-
opacity?: string
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export interface SceneNodeBase {
|
|
84
|
-
/** Stable identifier for seeding and cross-references. Derived from source
|
|
85
|
-
* semantics (node id, edge endpoints, slice label...), never list position
|
|
86
|
-
* (§8 seed contract). */
|
|
87
|
-
id: string
|
|
88
|
-
role: SceneRole
|
|
89
|
-
channels?: SemanticChannels
|
|
90
|
-
/** The exact crisp SVG serialization of this mark (possibly multi-line,
|
|
91
|
-
* possibly '' for marks that draw nothing, e.g. invisible edges). */
|
|
92
|
-
crisp: string
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export interface ShapeMark extends SceneNodeBase {
|
|
96
|
-
kind: 'shape'
|
|
97
|
-
geometry: Geometry
|
|
98
|
-
paint: MarkPaint
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export interface MarkerRef {
|
|
102
|
-
/** Def id referenced via url(#...) — 'arrowhead', 'cls-inherit', ... */
|
|
103
|
-
id: string
|
|
104
|
-
/** Marker archetype so styled backends can redraw without parsing defs. */
|
|
105
|
-
shape: 'arrow' | 'open-arrow' | 'circle' | 'cross' | 'triangle' | 'diamond' | 'diamond-open'
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export interface ConnectorMark extends SceneNodeBase {
|
|
109
|
-
kind: 'connector'
|
|
110
|
-
geometry:
|
|
111
|
-
| { kind: 'polyline'; points: Array<{ x: number; y: number }> }
|
|
112
|
-
| { kind: 'path'; d: string; points?: Array<{ x: number; y: number }> }
|
|
113
|
-
| { kind: 'line'; x1: number; y1: number; x2: number; y2: number }
|
|
114
|
-
lineStyle: 'solid' | 'dotted' | 'dashed' | 'thick' | 'invisible'
|
|
115
|
-
paint: MarkPaint
|
|
116
|
-
startMarker?: MarkerRef
|
|
117
|
-
endMarker?: MarkerRef
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export interface TextMark extends SceneNodeBase {
|
|
121
|
-
kind: 'text'
|
|
122
|
-
/** Unescaped source text (may contain <br> separators for multiline). */
|
|
123
|
-
text: string
|
|
124
|
-
/** Anchor position the crisp emitter used. */
|
|
125
|
-
x: number
|
|
126
|
-
y: number
|
|
127
|
-
fontSize: number
|
|
128
|
-
anchor: 'start' | 'middle' | 'end'
|
|
129
|
-
paint: MarkPaint
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/** A semantic wrapper (<g ...>...</g>) whose children are scene nodes.
|
|
133
|
-
* Serialization is reconstructed from open/close/children + per-child indent,
|
|
134
|
-
* so styled backends can restyle children while keeping wrapper semantics
|
|
135
|
-
* (classes, data-* attributes, ARIA) byte-compatible. */
|
|
136
|
-
export interface GroupMark extends SceneNodeBase {
|
|
137
|
-
kind: 'group'
|
|
138
|
-
open: string
|
|
139
|
-
close: string
|
|
140
|
-
children: Array<{ node: SceneNode; indent: number }>
|
|
141
|
-
/** Joiner between open/children/close segments; '\n' everywhere today. */
|
|
142
|
-
join: string
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/** Escape hatch for chunks not yet lowered semantically (style blocks,
|
|
146
|
-
* defs bodies, icon glyph stacks...). Styled backends pass these through or
|
|
147
|
-
* replace them wholesale via their own prelude/defs policy — they must not
|
|
148
|
-
* parse them. Aim to shrink raw usage over time. */
|
|
149
|
-
export interface RawMark extends SceneNodeBase {
|
|
150
|
-
kind: 'raw'
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/** Document prelude parameters — everything svgOpenTag/buildStyleBlock were
|
|
154
|
-
* called with, so a styled backend can re-derive its own document shell
|
|
155
|
-
* (different palette, fonts, backdrop) without string-parsing the crisp one. */
|
|
156
|
-
export interface PreludeMark extends SceneNodeBase {
|
|
157
|
-
kind: 'prelude'
|
|
158
|
-
prelude: {
|
|
159
|
-
width: number
|
|
160
|
-
height: number
|
|
161
|
-
colors: DiagramColors
|
|
162
|
-
transparent: boolean
|
|
163
|
-
font: string
|
|
164
|
-
hasMonoFont: boolean
|
|
165
|
-
/** Family-specific extra CSS appended after the shared style block ('' if none). */
|
|
166
|
-
extraCss: string
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
export type SceneNode = ShapeMark | ConnectorMark | TextMark | GroupMark | RawMark | PreludeMark
|
|
171
|
-
|
|
172
|
-
/** The lowered document: an ordered flat list of top-level marks. The crisp
|
|
173
|
-
* serialization is parts.map(crisp).join('\n') — exactly what the string
|
|
174
|
-
* renderers produced. */
|
|
175
|
-
export interface SceneDoc {
|
|
176
|
-
family: string
|
|
177
|
-
width: number
|
|
178
|
-
height: number
|
|
179
|
-
colors: DiagramColors
|
|
180
|
-
parts: SceneNode[]
|
|
181
|
-
}
|