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.
Files changed (340) hide show
  1. package/AGENT_NATIVE.md +155 -91
  2. package/CHANGELOG.md +163 -4
  3. package/Instructions_for_agents.md +10 -10
  4. package/LICENSES/Apache-2.0.txt +202 -0
  5. package/README.md +31 -38
  6. package/THIRD_PARTY_NOTICES.md +29 -0
  7. package/assets/hero.png +0 -0
  8. package/dist/agent-core.d.ts +463 -0
  9. package/dist/agent-core.js +397 -0
  10. package/dist/agent.d.ts +73 -1868
  11. package/dist/agent.js +336 -37325
  12. package/dist/agentic-mermaid-mcp.js +7 -38399
  13. package/dist/am.js +33626 -40446
  14. package/dist/chunk-2P5FKUQC.js +3485 -0
  15. package/dist/chunk-2S3VSS6I.js +149 -0
  16. package/dist/chunk-2SILJDW2.js +37 -0
  17. package/dist/chunk-BJYMBT4I.js +61963 -0
  18. package/dist/chunk-FDM43XU7.js +615 -0
  19. package/dist/chunk-MYPDVZCO.js +4702 -0
  20. package/dist/index-DWPa2MJs.d.ts +6780 -0
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.js +310 -33014
  23. package/dist/mcp-cli-5J47WEY3.js +17 -0
  24. package/docs/README.md +39 -31
  25. package/docs/agent-api-cookbook.md +44 -35
  26. package/docs/agent-mutation-policy.md +1 -1
  27. package/docs/agent-workflow-examples.md +2 -2
  28. package/docs/api.md +284 -77
  29. package/docs/ascii.md +26 -16
  30. package/docs/code-mode-impact.md +214 -0
  31. package/docs/comparison.md +23 -11
  32. package/docs/computational-aesthetics-prototype-plan.md +252 -0
  33. package/docs/config.md +26 -10
  34. package/docs/custom-fonts.md +265 -0
  35. package/docs/custom-style-cookbook.md +130 -9
  36. package/docs/diagram-families.md +48 -23
  37. package/docs/features.md +59 -52
  38. package/docs/fork-differences.md +6 -3
  39. package/docs/getting-started.md +23 -16
  40. package/docs/issue-derived-test-cases.md +29 -22
  41. package/docs/mcp-code-mode-rationale.md +13 -6
  42. package/docs/mcp-http-transport.md +20 -13
  43. package/docs/mermaid-layout-complaints.md +66 -52
  44. package/docs/mutation-testing.md +145 -66
  45. package/docs/quality.md +23 -14
  46. package/docs/react.md +2 -2
  47. package/docs/schemas/style-spec.schema.json +1221 -30
  48. package/docs/style-authoring.md +241 -38
  49. package/docs/svg-semantic-contract.md +49 -0
  50. package/docs/testing-strategy.md +166 -58
  51. package/docs/theming.md +19 -32
  52. package/examples/styles/catalog.json +54 -0
  53. package/examples/styles/cloudflare-workers-inspired-prototype.mmd +9 -0
  54. package/examples/styles/cloudflare-workers-inspired-prototype.style.json +110 -0
  55. package/examples/styles/cupertino-prototype.mmd +10 -0
  56. package/examples/styles/cupertino-prototype.style.json +63 -0
  57. package/examples/styles/mid-century-report.style.json +2 -2
  58. package/examples/styles/star-chart-atlas.style.json +2 -1
  59. package/examples/styles/transit-route-map.style.json +2 -2
  60. package/examples/styles/vercel-inspired-prototype.mmd +6 -0
  61. package/examples/styles/vercel-inspired-prototype.style.json +87 -0
  62. package/package.json +85 -45
  63. package/server.json +2 -2
  64. package/skills/agentic-mermaid-diagram-workflow/SKILL.md +17 -36
  65. package/skills/agentic-mermaid-diagram-workflow/references/cli.md +4 -5
  66. package/skills/agentic-mermaid-diagram-workflow/references/code-mode.md +15 -31
  67. package/skills/agentic-mermaid-diagram-workflow/references/sequence.md +10 -14
  68. package/skills/agentic-mermaid-live-editor/SKILL.md +100 -206
  69. package/TODO.md +0 -684
  70. package/dist/index-wjRjbsP-.d.ts +0 -1102
  71. package/docs/assets/improvements/box-start-connector.png +0 -0
  72. package/docs/assets/improvements/comment-dropped-lint.png +0 -0
  73. package/docs/assets/improvements/er-content-loss.png +0 -0
  74. package/docs/assets/improvements/fan-in-trunks.png +0 -0
  75. package/docs/assets/improvements/gantt-family.png +0 -0
  76. package/docs/assets/improvements/gantt-milestone-clamp.png +0 -0
  77. package/docs/assets/improvements/gantt-verify-seam.png +0 -0
  78. package/docs/assets/improvements/subgraph-container-edges.png +0 -0
  79. package/docs/assets/improvements/wrapper-frontmatter.png +0 -0
  80. package/docs/assets/improvements/wrapper-init-directive.png +0 -0
  81. package/docs/assets/improvements/wrapper-leading-comment.png +0 -0
  82. package/docs/assets/style-cookbook/mid-century-report.png +0 -0
  83. package/docs/assets/style-cookbook/star-chart-atlas.png +0 -0
  84. package/docs/assets/style-cookbook/transit-route-map.png +0 -0
  85. package/docs/contributing/adding-diagram-types.md +0 -144
  86. package/docs/contributing/diagram-family-citizenship.matrix.json +0 -2127
  87. package/docs/contributing/diagram-family-citizenship.md +0 -82
  88. package/docs/contributing/harvesting-upstream-tests.md +0 -126
  89. package/docs/contributing/lessons-learned.md +0 -276
  90. package/docs/contributing/releasing.md +0 -82
  91. package/docs/contributing/visual-review-evidence.md +0 -53
  92. package/docs/design/families/README.md +0 -26
  93. package/docs/design/families/architecture-beta.md +0 -48
  94. package/docs/design/families/flowchart-parser-conformance.md +0 -22
  95. package/docs/design/families/gantt-research.md +0 -137
  96. package/docs/design/families/gantt.md +0 -329
  97. package/docs/design/families/journey-agentic-cards-before.png +0 -0
  98. package/docs/design/families/journey-experience-curve-implementation.png +0 -0
  99. package/docs/design/families/journey-mermaid-classic-implementation.png +0 -0
  100. package/docs/design/families/journey-mermaid-classic-prototype.png +0 -0
  101. package/docs/design/families/journey-mermaid-classic-prototype.svg +0 -163
  102. package/docs/design/families/journey-mermaid-classic-prototype.svg.png +0 -0
  103. package/docs/design/families/journey-migration-parity.md +0 -439
  104. package/docs/design/families/journey-section-overlap-after.png +0 -0
  105. package/docs/design/families/journey-section-overlap-before.png +0 -0
  106. package/docs/design/families/journey-usage-research.md +0 -423
  107. package/docs/design/families/journey.md +0 -153
  108. package/docs/design/families/xychart.md +0 -85
  109. package/docs/design/system/README.md +0 -45
  110. package/docs/design/system/abstraction-audit.md +0 -224
  111. package/docs/design/system/abstraction-recommendations.md +0 -655
  112. package/docs/design/system/architecture.mmd +0 -29
  113. package/docs/design/system/architecture.svg +0 -137
  114. package/docs/design/system/layout-guarantees-and-robustness.md +0 -351
  115. package/docs/design/system/layout-rubric.md +0 -138
  116. package/docs/design/system/route-contracts.md +0 -848
  117. package/docs/design/system/source-preservation-ladder.md +0 -52
  118. package/docs/design/system/styles-rollout.md +0 -216
  119. package/docs/design/system/ugly-layouts.md +0 -119
  120. package/docs/layout-characterization/README.md +0 -245
  121. package/docs/layout-characterization/contact-sheet-families.md +0 -498
  122. package/docs/layout-characterization/contact-sheet.md +0 -638
  123. package/docs/layout-characterization/properties.md +0 -281
  124. package/docs/layout-characterization/visual-quality.md +0 -160
  125. package/docs/layout-characterization/visual-snapshots/architecture.svg +0 -79
  126. package/docs/layout-characterization/visual-snapshots/class.svg +0 -60
  127. package/docs/layout-characterization/visual-snapshots/er.svg +0 -61
  128. package/docs/layout-characterization/visual-snapshots/flowchart.svg +0 -58
  129. package/docs/layout-characterization/visual-snapshots/gantt.svg +0 -52
  130. package/docs/layout-characterization/visual-snapshots/journey.svg +0 -98
  131. package/docs/layout-characterization/visual-snapshots/pie.svg +0 -36
  132. package/docs/layout-characterization/visual-snapshots/quadrant.svg +0 -47
  133. package/docs/layout-characterization/visual-snapshots/sequence.svg +0 -59
  134. package/docs/layout-characterization/visual-snapshots/state.svg +0 -52
  135. package/docs/layout-characterization/visual-snapshots/timeline.svg +0 -64
  136. package/docs/layout-characterization/visual-snapshots/xychart.svg +0 -97
  137. package/docs/project/divergences.md +0 -98
  138. package/docs/project/dogfooding-docs-strategy.md +0 -66
  139. package/docs/project/hosted-mcp-cloudflare-plan.md +0 -338
  140. package/docs/project/lessons-learned.md +0 -1019
  141. package/docs/project/remove-role-styling-plan.md +0 -176
  142. package/examples/agent-improve-auth-flow.ts +0 -206
  143. package/examples/agent-loop.ts +0 -53
  144. package/examples/mcp-vs-cli-complex-diagrams.ts +0 -133
  145. package/llms.txt +0 -104
  146. package/skill-evals/README.md +0 -77
  147. package/skill-evals/fixtures/architecture-cache/input.mmd +0 -5
  148. package/skill-evals/fixtures/class-payment/input.mmd +0 -4
  149. package/skill-evals/fixtures/er-orders/input.mmd +0 -10
  150. package/skill-evals/fixtures/flowchart-cache/input.mmd +0 -3
  151. package/skill-evals/fixtures/gantt-release/input.mmd +0 -7
  152. package/skill-evals/fixtures/journey-checkout/input.mmd +0 -6
  153. package/skill-evals/fixtures/pie-exports/input.mmd +0 -5
  154. package/skill-evals/fixtures/quadrant-priorities/input.mmd +0 -11
  155. package/skill-evals/fixtures/sequence-retry/input.mmd +0 -8
  156. package/skill-evals/fixtures/state-lifecycle/input.mmd +0 -5
  157. package/skill-evals/fixtures/timeline-release/input.mmd +0 -6
  158. package/skill-evals/fixtures/xychart-growth/input.mmd +0 -5
  159. package/skill-evals/shared-benchmark.json +0 -510
  160. package/src/agent/analyze.ts +0 -157
  161. package/src/agent/apply.ts +0 -146
  162. package/src/agent/architecture-body.ts +0 -458
  163. package/src/agent/certificates.ts +0 -110
  164. package/src/agent/class-body.ts +0 -319
  165. package/src/agent/core.ts +0 -207
  166. package/src/agent/create.ts +0 -106
  167. package/src/agent/describe.ts +0 -368
  168. package/src/agent/er-body.ts +0 -224
  169. package/src/agent/facts.ts +0 -360
  170. package/src/agent/families-builtin.ts +0 -920
  171. package/src/agent/families.ts +0 -232
  172. package/src/agent/family-layouts.ts +0 -832
  173. package/src/agent/flowchart-body.ts +0 -372
  174. package/src/agent/flowchart-unsupported.ts +0 -243
  175. package/src/agent/font-coverage.ts +0 -288
  176. package/src/agent/gantt-body.ts +0 -495
  177. package/src/agent/index.ts +0 -10
  178. package/src/agent/journey-body.ts +0 -374
  179. package/src/agent/label-metrics.ts +0 -28
  180. package/src/agent/layout-to-rendered.ts +0 -63
  181. package/src/agent/mutate.ts +0 -97
  182. package/src/agent/mutation-ops.ts +0 -51
  183. package/src/agent/op-schema.ts +0 -443
  184. package/src/agent/parse.ts +0 -223
  185. package/src/agent/pie-body.ts +0 -270
  186. package/src/agent/png.ts +0 -171
  187. package/src/agent/quadrant-body.ts +0 -336
  188. package/src/agent/quality.ts +0 -363
  189. package/src/agent/readability-audit.ts +0 -92
  190. package/src/agent/rendered-route-audit.ts +0 -61
  191. package/src/agent/sequence-body.ts +0 -345
  192. package/src/agent/serialize.ts +0 -214
  193. package/src/agent/state-body.ts +0 -589
  194. package/src/agent/structural-count.ts +0 -150
  195. package/src/agent/timeline-body.ts +0 -280
  196. package/src/agent/trace-log.ts +0 -43
  197. package/src/agent/types.ts +0 -995
  198. package/src/agent/verify.ts +0 -603
  199. package/src/agent/xychart-body.ts +0 -472
  200. package/src/architecture/config.ts +0 -282
  201. package/src/architecture/layout.ts +0 -496
  202. package/src/architecture/parser.ts +0 -375
  203. package/src/architecture/renderer.ts +0 -611
  204. package/src/architecture/types.ts +0 -113
  205. package/src/ascii/ansi.ts +0 -423
  206. package/src/ascii/architecture.ts +0 -210
  207. package/src/ascii/canvas.ts +0 -443
  208. package/src/ascii/class-diagram.ts +0 -697
  209. package/src/ascii/converter.ts +0 -475
  210. package/src/ascii/draw.ts +0 -1693
  211. package/src/ascii/edge-bundling.ts +0 -328
  212. package/src/ascii/edge-routing.ts +0 -330
  213. package/src/ascii/er-diagram.ts +0 -439
  214. package/src/ascii/gantt.ts +0 -276
  215. package/src/ascii/grid.ts +0 -747
  216. package/src/ascii/index.ts +0 -196
  217. package/src/ascii/journey.ts +0 -169
  218. package/src/ascii/meta.ts +0 -244
  219. package/src/ascii/multiline-utils.ts +0 -80
  220. package/src/ascii/pathfinder.ts +0 -301
  221. package/src/ascii/pie.ts +0 -67
  222. package/src/ascii/quadrant.ts +0 -158
  223. package/src/ascii/reverse.ts +0 -216
  224. package/src/ascii/sequence.ts +0 -499
  225. package/src/ascii/shapes/circle.ts +0 -27
  226. package/src/ascii/shapes/corners.ts +0 -143
  227. package/src/ascii/shapes/diamond.ts +0 -27
  228. package/src/ascii/shapes/hexagon.ts +0 -27
  229. package/src/ascii/shapes/index.ts +0 -106
  230. package/src/ascii/shapes/rectangle.ts +0 -168
  231. package/src/ascii/shapes/rounded.ts +0 -27
  232. package/src/ascii/shapes/special.ts +0 -332
  233. package/src/ascii/shapes/stadium.ts +0 -110
  234. package/src/ascii/shapes/state.ts +0 -192
  235. package/src/ascii/shapes/types.ts +0 -73
  236. package/src/ascii/timeline.ts +0 -141
  237. package/src/ascii/types.ts +0 -312
  238. package/src/ascii/validate.ts +0 -120
  239. package/src/ascii/width.ts +0 -81
  240. package/src/ascii/wrap.ts +0 -131
  241. package/src/ascii/xychart.ts +0 -919
  242. package/src/browser.ts +0 -29
  243. package/src/class/layout.ts +0 -240
  244. package/src/class/parser.ts +0 -332
  245. package/src/class/renderer.ts +0 -638
  246. package/src/class/types.ts +0 -129
  247. package/src/cli/agent-instructions.ts +0 -1
  248. package/src/cli/am-bin.ts +0 -6
  249. package/src/cli/exit-codes.ts +0 -18
  250. package/src/cli/index.ts +0 -1132
  251. package/src/cli/init-agent.ts +0 -147
  252. package/src/cli/run-entrypoint.ts +0 -10
  253. package/src/color-resolver.ts +0 -152
  254. package/src/elk-instance.ts +0 -114
  255. package/src/er/layout.ts +0 -187
  256. package/src/er/parser.ts +0 -226
  257. package/src/er/renderer.ts +0 -792
  258. package/src/er/types.ts +0 -99
  259. package/src/family-rubric.ts +0 -425
  260. package/src/font-manifest.ts +0 -31
  261. package/src/gantt/layout.ts +0 -365
  262. package/src/gantt/parser.ts +0 -282
  263. package/src/gantt/pipeline.ts +0 -27
  264. package/src/gantt/renderer.ts +0 -414
  265. package/src/gantt/schedule.ts +0 -513
  266. package/src/gantt/types.ts +0 -255
  267. package/src/index.ts +0 -368
  268. package/src/journey/layout.ts +0 -554
  269. package/src/journey/parse-core.ts +0 -251
  270. package/src/journey/parser.ts +0 -101
  271. package/src/journey/renderer.ts +0 -917
  272. package/src/journey/types.ts +0 -144
  273. package/src/layout/geometry.ts +0 -182
  274. package/src/layout/pass.ts +0 -142
  275. package/src/layout/passes/index.ts +0 -3916
  276. package/src/layout-engine.ts +0 -1565
  277. package/src/layout-rubric.ts +0 -659
  278. package/src/mcp/artifacts.ts +0 -137
  279. package/src/mcp/dynamic-harness.ts +0 -31
  280. package/src/mcp/dynamic-modules.d.ts +0 -7
  281. package/src/mcp/facade.ts +0 -608
  282. package/src/mcp/harness-runtime.ts +0 -203
  283. package/src/mcp/hosted-server.ts +0 -468
  284. package/src/mcp/mcp-bin.ts +0 -3
  285. package/src/mcp/mcp-cli.ts +0 -82
  286. package/src/mcp/protocol.ts +0 -13
  287. package/src/mcp/sandbox.ts +0 -195
  288. package/src/mcp/sdk-decl.ts +0 -391
  289. package/src/mcp/server.ts +0 -362
  290. package/src/mcp/tool-surface.ts +0 -146
  291. package/src/mermaid-source.ts +0 -655
  292. package/src/multiline-utils.ts +0 -228
  293. package/src/parser.ts +0 -1031
  294. package/src/pie/layout.ts +0 -168
  295. package/src/pie/parser.ts +0 -136
  296. package/src/pie/renderer.ts +0 -234
  297. package/src/pie/types.ts +0 -82
  298. package/src/quadrant/layout.ts +0 -191
  299. package/src/quadrant/parser.ts +0 -183
  300. package/src/quadrant/renderer.ts +0 -322
  301. package/src/quadrant/types.ts +0 -113
  302. package/src/render-family-hooks.ts +0 -244
  303. package/src/renderer.ts +0 -1099
  304. package/src/route-contracts.ts +0 -2723
  305. package/src/scene/backend.ts +0 -76
  306. package/src/scene/fidelity.ts +0 -183
  307. package/src/scene/hybrid-backend.ts +0 -174
  308. package/src/scene/ir.ts +0 -181
  309. package/src/scene/marks.ts +0 -127
  310. package/src/scene/rough-backend.ts +0 -378
  311. package/src/scene/seed.ts +0 -46
  312. package/src/scene/style-registry.ts +0 -556
  313. package/src/sequence/layout.ts +0 -456
  314. package/src/sequence/parser.ts +0 -255
  315. package/src/sequence/renderer.ts +0 -604
  316. package/src/sequence/types.ts +0 -154
  317. package/src/shape-clipping.ts +0 -351
  318. package/src/shared/accessibility-directives.ts +0 -22
  319. package/src/shared/batched.ts +0 -61
  320. package/src/shared/color-math.ts +0 -93
  321. package/src/shared/svg-a11y.ts +0 -29
  322. package/src/shared/syntax-error.ts +0 -17
  323. package/src/shared/unicode-ranges.ts +0 -135
  324. package/src/styles.ts +0 -318
  325. package/src/svg-paths.ts +0 -28
  326. package/src/text-metrics.ts +0 -248
  327. package/src/theme.ts +0 -710
  328. package/src/timeline/layout.ts +0 -381
  329. package/src/timeline/parser.ts +0 -215
  330. package/src/timeline/renderer.ts +0 -538
  331. package/src/timeline/types.ts +0 -105
  332. package/src/types.ts +0 -462
  333. package/src/website-worker-modules.d.ts +0 -18
  334. package/src/xychart/axis-utils.ts +0 -109
  335. package/src/xychart/colors.ts +0 -124
  336. package/src/xychart/config.ts +0 -88
  337. package/src/xychart/layout.ts +0 -656
  338. package/src/xychart/parser.ts +0 -504
  339. package/src/xychart/renderer.ts +0 -640
  340. package/src/xychart/types.ts +0 -282
@@ -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
- }
@@ -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(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&amp;/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
- }