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
|
@@ -1,832 +0,0 @@
|
|
|
1
|
-
// ============================================================================
|
|
2
|
-
// QUAL-1 — RenderedLayout adapters for the non-graph diagram families.
|
|
3
|
-
//
|
|
4
|
-
// The perceptual-quality metrics (measureQuality / checkQuality) operate on a
|
|
5
|
-
// RenderedLayout. Flowchart/state reuse the ELK geometric path; sequence and
|
|
6
|
-
// timeline have bespoke adapters in index.ts. Everything else previously fell
|
|
7
|
-
// through to emptyRenderedLayout, so those families showed nodeCount 0 — the
|
|
8
|
-
// metrics were blind to them (TODO QUAL-1, docs/quality.md honest-gap).
|
|
9
|
-
//
|
|
10
|
-
// This module closes that gap by projecting each family's REAL positioned
|
|
11
|
-
// layout (the same geometry the SVG renderer draws) into a RenderedLayout:
|
|
12
|
-
//
|
|
13
|
-
// family nodes edges groups
|
|
14
|
-
// ------------ -------------------------- -------------------- ------------------
|
|
15
|
-
// class class boxes relations —
|
|
16
|
-
// er entity boxes relations —
|
|
17
|
-
// journey task boxes + section labels — section frames
|
|
18
|
-
// architecture services + junctions edges groups (flattened)
|
|
19
|
-
// xychart bars + line points + axis — plot area
|
|
20
|
-
// ticks
|
|
21
|
-
// pie slice label boxes (legend — —
|
|
22
|
-
// anchor + approx bbox)
|
|
23
|
-
// quadrant points — quadrant regions
|
|
24
|
-
// gantt tasks + milestones — section bands
|
|
25
|
-
//
|
|
26
|
-
// Opaque-safe: every adapter parses d.canonicalSource via the legacy parser +
|
|
27
|
-
// layouter (the exact path renderMermaidSVG uses) wrapped in try/catch. An
|
|
28
|
-
// invalid opaque diagram of a renderable family must NOT make layoutMermaid
|
|
29
|
-
// throw — it degrades to emptyRenderedLayout. All coordinates pass through
|
|
30
|
-
// toFinite (throws on NaN/Infinity), so a garbage coordinate fails loudly
|
|
31
|
-
// rather than silently poisoning the metrics.
|
|
32
|
-
//
|
|
33
|
-
// Determinism: every layouter here is deterministic (no RNG / clock), so
|
|
34
|
-
// layoutMermaid(d) called twice is deep-equal.
|
|
35
|
-
// ============================================================================
|
|
36
|
-
|
|
37
|
-
import type {
|
|
38
|
-
ValidDiagram,
|
|
39
|
-
RenderedLayout,
|
|
40
|
-
RenderedLayoutNode,
|
|
41
|
-
RenderedLayoutEdge,
|
|
42
|
-
RenderedLayoutGroup,
|
|
43
|
-
LayoutWarning,
|
|
44
|
-
Finite,
|
|
45
|
-
XyChartBody,
|
|
46
|
-
PieBody,
|
|
47
|
-
QuadrantBody,
|
|
48
|
-
GanttBody,
|
|
49
|
-
} from './types.ts'
|
|
50
|
-
import type { FamilyEdgeRouteCertificate, Point, RegionContainmentCertificate } from '../types.ts'
|
|
51
|
-
import { toFinite } from './types.ts'
|
|
52
|
-
import { emptyRenderedLayout } from './layout-to-rendered.ts'
|
|
53
|
-
|
|
54
|
-
import { toMermaidLines, normalizeMermaidSource } from '../mermaid-source.ts'
|
|
55
|
-
import { parseClassDiagram } from '../class/parser.ts'
|
|
56
|
-
import { layoutClassDiagram } from '../class/layout.ts'
|
|
57
|
-
import { parseErDiagram } from '../er/parser.ts'
|
|
58
|
-
import { layoutErDiagram } from '../er/layout.ts'
|
|
59
|
-
import { layoutSequenceDiagram } from '../sequence/layout.ts'
|
|
60
|
-
import { parseSequenceDiagram } from '../sequence/parser.ts'
|
|
61
|
-
import { parseTimelineDiagram } from '../timeline/parser.ts'
|
|
62
|
-
import { layoutTimelineDiagram } from '../timeline/layout.ts'
|
|
63
|
-
import { parseJourneyDiagram } from '../journey/parser.ts'
|
|
64
|
-
import { layoutJourneyDiagram } from '../journey/layout.ts'
|
|
65
|
-
import { parseArchitectureDiagram } from '../architecture/parser.ts'
|
|
66
|
-
import { layoutArchitectureDiagram } from '../architecture/layout.ts'
|
|
67
|
-
import { applyXYChartFrontmatterConfig, parseXYChart, resolveXYChartConfig, resolveXYChartTheme } from '../xychart/parser.ts'
|
|
68
|
-
import { layoutXYChart } from '../xychart/layout.ts'
|
|
69
|
-
import type { XYAxis, XYChart } from '../xychart/types.ts'
|
|
70
|
-
import type { PositionedArchitectureEdge, PositionedArchitectureGroup, PositionedArchitectureJunction, PositionedArchitectureService } from '../architecture/types.ts'
|
|
71
|
-
import { parsePieChart } from '../pie/parser.ts'
|
|
72
|
-
import { layoutPieChart } from '../pie/layout.ts'
|
|
73
|
-
import type { PieChart } from '../pie/types.ts'
|
|
74
|
-
import { parseQuadrantChart } from '../quadrant/parser.ts'
|
|
75
|
-
import { layoutQuadrantChart } from '../quadrant/layout.ts'
|
|
76
|
-
import type { QuadrantChart } from '../quadrant/types.ts'
|
|
77
|
-
import { parseGanttModel, applyGanttFrontmatterConfig, GANTT_DURATION_RE } from '../gantt/parser.ts'
|
|
78
|
-
import { resolveGanttSchedule } from '../gantt/schedule.ts'
|
|
79
|
-
import { buildGanttRenderPipeline } from '../gantt/pipeline.ts'
|
|
80
|
-
import { layoutGantt } from '../gantt/layout.ts'
|
|
81
|
-
import type { GanttEndExpr, GanttLayoutResult, GanttModel, GanttModelSection, GanttModelTask, GanttStartExpr, GanttTaskTag } from '../gantt/types.ts'
|
|
82
|
-
|
|
83
|
-
function f(n: number): Finite { return toFinite(Math.round(n)) }
|
|
84
|
-
|
|
85
|
-
/** Round a span against its rounded start. Rounding x and width
|
|
86
|
-
* independently can shift the far edge ±1px away from rounded edge
|
|
87
|
-
* endpoints, which fires the class/ER anchor tripwires (TOL 0.5) on
|
|
88
|
-
* geometry that sits exactly on-boundary pre-rounding — observed live on
|
|
89
|
-
* an onboarding-probe ER diagram. Used where edgeAnchors checks run. */
|
|
90
|
-
function fSpan(start: number, length: number): Finite { return toFinite(Math.round(start + length) - Math.round(start)) }
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Family-aware RenderedLayout adapter. Dispatches on body kind; for the
|
|
94
|
-
* renderable non-graph families it layouts from d.canonicalSource (which is
|
|
95
|
-
* always populated for both structured and opaque bodies). Returns null for
|
|
96
|
-
* families this module doesn't own (flowchart/state graph layouts and opaque
|
|
97
|
-
* flowchart compatibility are handled in index.ts) so the caller can keep its
|
|
98
|
-
* existing dispatch.
|
|
99
|
-
*/
|
|
100
|
-
export function layoutFamilyToRendered(d: ValidDiagram, opts: { debug?: boolean } = {}): RenderedLayout | null {
|
|
101
|
-
// Each adapter is self-contained: it wraps the legacy parse+layout in
|
|
102
|
-
// try/catch and degrades to emptyRenderedLayout(d.kind) on render-error, so
|
|
103
|
-
// an invalid opaque body of a renderable family never makes this throw.
|
|
104
|
-
switch (d.kind) {
|
|
105
|
-
case 'class': return classToRendered(d, opts)
|
|
106
|
-
case 'er': return erToRendered(d, opts)
|
|
107
|
-
case 'sequence': return sequenceToRendered(d, opts)
|
|
108
|
-
case 'timeline': return timelineToRendered(d, opts)
|
|
109
|
-
case 'journey': return journeyToRendered(d)
|
|
110
|
-
case 'architecture': return architectureToRendered(d, opts)
|
|
111
|
-
case 'xychart': return xychartToRendered(d, opts)
|
|
112
|
-
case 'pie': return pieToRendered(d, opts)
|
|
113
|
-
case 'quadrant': return quadrantToRendered(d, opts)
|
|
114
|
-
case 'gantt': return ganttToRendered(d, opts)
|
|
115
|
-
default: return null
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// ---- class ----------------------------------------------------------------
|
|
120
|
-
|
|
121
|
-
function classToRendered(d: ValidDiagram, opts: { debug?: boolean } = {}): RenderedLayout {
|
|
122
|
-
try {
|
|
123
|
-
const positioned = layoutClassDiagram(parseClassDiagram(toMermaidLines(d.canonicalSource)))
|
|
124
|
-
const nodes: RenderedLayoutNode[] = positioned.classes.map(c => ({
|
|
125
|
-
id: c.id, x: f(c.x), y: f(c.y), w: fSpan(c.x, c.width), h: fSpan(c.y, c.height), shape: 'rectangle', label: c.label,
|
|
126
|
-
}))
|
|
127
|
-
const boxById = new Map(positioned.classes.map(c => [c.id, { x: c.x, y: c.y, width: c.width, height: c.height }]))
|
|
128
|
-
const edges: RenderedLayoutEdge[] = positioned.relationships.map((r, i) => ({
|
|
129
|
-
id: `rel#${i}:${r.from}->${r.to}`, from: r.from, to: r.to,
|
|
130
|
-
path: r.points.map(p => [f(p.x), f(p.y)] as [Finite, Finite]),
|
|
131
|
-
label: r.label && r.labelPosition ? { x: f(r.labelPosition.x), y: f(r.labelPosition.y), text: r.label } : undefined,
|
|
132
|
-
route: opts.debug ? boxRouteCertificate('class', i, r.points, boxById.get(r.from), boxById.get(r.to)) : undefined,
|
|
133
|
-
}))
|
|
134
|
-
return { version: 1, kind: d.kind, nodes, edges, groups: [], bounds: { w: f(positioned.width), h: f(positioned.height) } }
|
|
135
|
-
} catch { return emptyRenderedLayout(d.kind) }
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// ---- er -------------------------------------------------------------------
|
|
139
|
-
|
|
140
|
-
function erToRendered(d: ValidDiagram, opts: { debug?: boolean } = {}): RenderedLayout {
|
|
141
|
-
try {
|
|
142
|
-
const positioned = layoutErDiagram(parseErDiagram(toMermaidLines(d.canonicalSource)))
|
|
143
|
-
const nodes: RenderedLayoutNode[] = positioned.entities.map(e => ({
|
|
144
|
-
id: e.id, x: f(e.x), y: f(e.y), w: fSpan(e.x, e.width), h: fSpan(e.y, e.height), shape: 'rectangle', label: e.label,
|
|
145
|
-
}))
|
|
146
|
-
const boxById = new Map(positioned.entities.map(e => [e.id, { x: e.x, y: e.y, width: e.width, height: e.height }]))
|
|
147
|
-
const edges: RenderedLayoutEdge[] = positioned.relationships.map((r, i) => ({
|
|
148
|
-
id: `rel#${i}:${r.entity1}->${r.entity2}`, from: r.entity1, to: r.entity2,
|
|
149
|
-
path: r.points.map(p => [f(p.x), f(p.y)] as [Finite, Finite]),
|
|
150
|
-
label: r.label ? labelMidpoint(r.points, r.label) : undefined,
|
|
151
|
-
route: opts.debug ? boxRouteCertificate('er', i, r.points, boxById.get(r.entity1), boxById.get(r.entity2)) : undefined,
|
|
152
|
-
}))
|
|
153
|
-
return { version: 1, kind: d.kind, nodes, edges, groups: [], bounds: { w: f(positioned.width), h: f(positioned.height) } }
|
|
154
|
-
} catch { return emptyRenderedLayout(d.kind) }
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function boxRouteCertificate(
|
|
158
|
-
family: 'class' | 'er',
|
|
159
|
-
edgeIndex: number,
|
|
160
|
-
points: Point[],
|
|
161
|
-
source: { x: number; y: number; width: number; height: number } | undefined,
|
|
162
|
-
target: { x: number; y: number; width: number; height: number } | undefined,
|
|
163
|
-
): FamilyEdgeRouteCertificate {
|
|
164
|
-
const sourceBoundary = !!(source && points[0] && pointOnRawRectBoundary(points[0].x, points[0].y, source, 1))
|
|
165
|
-
const targetBoundary = !!(target && points[points.length - 1] && pointOnRawRectBoundary(points[points.length - 1]!.x, points[points.length - 1]!.y, target, 1))
|
|
166
|
-
const orthogonal = routeOrthogonal(points)
|
|
167
|
-
return {
|
|
168
|
-
family,
|
|
169
|
-
edgeIndex,
|
|
170
|
-
routeClass: 'family-layout',
|
|
171
|
-
invariant: sourceBoundary && targetBoundary && orthogonal ? 'orthogonal-box' : 'unverified-family-route',
|
|
172
|
-
bendCount: bendCount(points),
|
|
173
|
-
orthogonal,
|
|
174
|
-
sourceBoundary,
|
|
175
|
-
targetBoundary,
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function routeOrthogonal(points: Point[]): boolean {
|
|
180
|
-
for (let i = 1; i < points.length; i++) {
|
|
181
|
-
const a = points[i - 1]!, b = points[i]!
|
|
182
|
-
if (Math.abs(a.x - b.x) > 0.5 && Math.abs(a.y - b.y) > 0.5) return false
|
|
183
|
-
}
|
|
184
|
-
return true
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function bendCount(points: Point[]): number {
|
|
188
|
-
if (points.length < 3) return 0
|
|
189
|
-
let bends = 0
|
|
190
|
-
for (let i = 1; i < points.length - 1; i++) {
|
|
191
|
-
const a = points[i - 1]!, b = points[i]!, c = points[i + 1]!
|
|
192
|
-
const sameX = Math.abs(a.x - b.x) <= 0.5 && Math.abs(b.x - c.x) <= 0.5
|
|
193
|
-
const sameY = Math.abs(a.y - b.y) <= 0.5 && Math.abs(b.y - c.y) <= 0.5
|
|
194
|
-
if (!sameX && !sameY) bends++
|
|
195
|
-
}
|
|
196
|
-
return bends
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function pointOnRawRectBoundary(x: number, y: number, n: { x: number; y: number; width: number; height: number }, tol: number): boolean {
|
|
200
|
-
const onVertical = (Math.abs(x - n.x) <= tol || Math.abs(x - (n.x + n.width)) <= tol) && y >= n.y - tol && y <= n.y + n.height + tol
|
|
201
|
-
const onHorizontal = (Math.abs(y - n.y) <= tol || Math.abs(y - (n.y + n.height)) <= tol) && x >= n.x - tol && x <= n.x + n.width + tol
|
|
202
|
-
return onVertical || onHorizontal
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function labelMidpoint(points: Array<{ x: number; y: number }>, text: string): { x: Finite; y: Finite; text: string } | undefined {
|
|
206
|
-
if (points.length === 0) return undefined
|
|
207
|
-
const mid = points[Math.floor(points.length / 2)]!
|
|
208
|
-
return { x: f(mid.x), y: f(mid.y), text }
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
type ElementCertificateFamily = 'timeline' | 'xychart' | 'pie' | 'quadrant' | 'gantt'
|
|
212
|
-
type ElementCertificateInvariant = RegionContainmentCertificate['invariant']
|
|
213
|
-
|
|
214
|
-
function elementCertificates(
|
|
215
|
-
family: ElementCertificateFamily,
|
|
216
|
-
layout: RenderedLayout,
|
|
217
|
-
invariant: ElementCertificateInvariant,
|
|
218
|
-
referenceGroup?: RenderedLayoutGroup,
|
|
219
|
-
containment: 'bounds' | 'center' = 'bounds',
|
|
220
|
-
): RegionContainmentCertificate[] {
|
|
221
|
-
const groupsByMember = new Map<string, RenderedLayoutGroup>()
|
|
222
|
-
for (const group of layout.groups) {
|
|
223
|
-
for (const member of group.members) groupsByMember.set(member, group)
|
|
224
|
-
}
|
|
225
|
-
return layout.nodes.map(n => {
|
|
226
|
-
const withinBounds = nodeWithinBounds(n, layout.bounds)
|
|
227
|
-
const group = groupsByMember.get(n.id) ?? referenceGroup
|
|
228
|
-
const withinGroup = group ? (containment === 'center' ? nodeCenterWithinGroup(n, group) : nodeWithinGroup(n, group)) : undefined
|
|
229
|
-
return {
|
|
230
|
-
family,
|
|
231
|
-
elementId: n.id,
|
|
232
|
-
routeClass: 'family-layout',
|
|
233
|
-
invariant: withinBounds && (withinGroup ?? true) ? invariant : 'unverified-family-layout',
|
|
234
|
-
bounds: { x: n.x, y: n.y, w: n.w, h: n.h },
|
|
235
|
-
center: { x: f(n.x + n.w / 2), y: f(n.y + n.h / 2) },
|
|
236
|
-
containment,
|
|
237
|
-
withinBounds,
|
|
238
|
-
...(group ? { groupId: group.id, withinGroup } : {}),
|
|
239
|
-
}
|
|
240
|
-
})
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function nodeWithinBounds(n: RenderedLayoutNode, bounds: RenderedLayout['bounds'], tol = 0.5): boolean {
|
|
244
|
-
return n.x >= -tol && n.y >= -tol && n.x + n.w <= bounds.w + tol && n.y + n.h <= bounds.h + tol
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function nodeWithinGroup(n: RenderedLayoutNode, g: RenderedLayoutGroup, tol = 0.5): boolean {
|
|
248
|
-
return n.x >= g.x - tol && n.y >= g.y - tol && n.x + n.w <= g.x + g.w + tol && n.y + n.h <= g.y + g.h + tol
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
function nodeCenterWithinGroup(n: RenderedLayoutNode, g: RenderedLayoutGroup, tol = 0.5): boolean {
|
|
252
|
-
const cx = n.x + n.w / 2
|
|
253
|
-
const cy = n.y + n.h / 2
|
|
254
|
-
return cx >= g.x - tol && cy >= g.y - tol && cx <= g.x + g.w + tol && cy <= g.y + g.h + tol
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// ---- sequence -------------------------------------------------------------
|
|
258
|
-
|
|
259
|
-
function sequenceToRendered(d: ValidDiagram, opts: { debug?: boolean } = {}): RenderedLayout {
|
|
260
|
-
try {
|
|
261
|
-
if (d.kind !== 'sequence') return emptyRenderedLayout(d.kind)
|
|
262
|
-
const positioned = layoutSequenceDiagram(parseSequenceDiagram(toMermaidLines(d.canonicalSource)))
|
|
263
|
-
const lifelineByActor = new Map(positioned.lifelines.map(l => [l.actorId, l.x]))
|
|
264
|
-
const nodes: RenderedLayoutNode[] = [
|
|
265
|
-
...positioned.actors.map(a => ({
|
|
266
|
-
id: a.id, x: f(a.x - a.width / 2), y: f(a.y), w: f(a.width), h: f(a.height),
|
|
267
|
-
shape: 'rectangle', label: a.label,
|
|
268
|
-
})),
|
|
269
|
-
...positioned.notes.map((n, i) => ({
|
|
270
|
-
id: `note#${i}`, x: f(n.x), y: f(n.y), w: f(n.width), h: f(n.height),
|
|
271
|
-
shape: 'note', label: n.text,
|
|
272
|
-
})),
|
|
273
|
-
]
|
|
274
|
-
return {
|
|
275
|
-
version: 1, kind: d.kind,
|
|
276
|
-
nodes,
|
|
277
|
-
edges: positioned.messages.map((m, i) => {
|
|
278
|
-
const path = sequenceMessagePath(m)
|
|
279
|
-
return {
|
|
280
|
-
id: `msg#${i}:${m.from}->${m.to}`, from: m.from, to: m.to,
|
|
281
|
-
path: path.map(p => [f(p.x), f(p.y)] as [Finite, Finite]),
|
|
282
|
-
label: m.label ? sequenceMessageLabel(m) : undefined,
|
|
283
|
-
route: opts.debug ? sequenceRouteCertificate(i, m, path, lifelineByActor) : undefined,
|
|
284
|
-
}
|
|
285
|
-
}),
|
|
286
|
-
groups: positioned.blocks.map((b, i) => ({
|
|
287
|
-
id: `block#${i}:${b.type}`, x: f(b.x), y: f(b.y), w: f(b.width), h: f(b.height), members: [], label: b.label,
|
|
288
|
-
})),
|
|
289
|
-
bounds: { w: f(positioned.width), h: f(positioned.height) },
|
|
290
|
-
}
|
|
291
|
-
} catch { return emptyRenderedLayout(d.kind) }
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
function sequenceMessagePath(message: { x1: number; x2: number; y: number; isSelf: boolean }): Point[] {
|
|
295
|
-
if (!message.isSelf) return [{ x: message.x1, y: message.y }, { x: message.x2, y: message.y }]
|
|
296
|
-
const loopW = 30
|
|
297
|
-
const loopH = 20
|
|
298
|
-
return [
|
|
299
|
-
{ x: message.x1, y: message.y },
|
|
300
|
-
{ x: message.x1 + loopW, y: message.y },
|
|
301
|
-
{ x: message.x1 + loopW, y: message.y + loopH },
|
|
302
|
-
{ x: message.x2, y: message.y + loopH },
|
|
303
|
-
]
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
function sequenceMessageLabel(message: { x1: number; x2: number; y: number; isSelf: boolean; label: string }): { x: Finite; y: Finite; text: string } {
|
|
307
|
-
if (message.isSelf) return { x: f(message.x1 + 38), y: f(message.y + 10), text: message.label }
|
|
308
|
-
return { x: f((message.x1 + message.x2) / 2), y: f(message.y - 10), text: message.label }
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
function sequenceRouteCertificate(
|
|
312
|
-
edgeIndex: number,
|
|
313
|
-
message: { from: string; to: string; x1: number; x2: number; y: number; isSelf: boolean },
|
|
314
|
-
points: Point[],
|
|
315
|
-
lifelineByActor: Map<string, number>,
|
|
316
|
-
): FamilyEdgeRouteCertificate {
|
|
317
|
-
const sourceX = lifelineByActor.get(message.from)
|
|
318
|
-
const targetX = lifelineByActor.get(message.to)
|
|
319
|
-
const first = points[0]
|
|
320
|
-
const last = points[points.length - 1]
|
|
321
|
-
const sourceLifeline = sourceX !== undefined && first !== undefined && Math.abs(first.x - sourceX) <= 1
|
|
322
|
-
const targetLifeline = targetX !== undefined && last !== undefined && Math.abs(last.x - targetX) <= 1
|
|
323
|
-
const horizontal = !message.isSelf && points.every(p => Math.abs(p.y - message.y) <= 1)
|
|
324
|
-
const orthogonal = routeOrthogonal(points)
|
|
325
|
-
return {
|
|
326
|
-
family: 'sequence',
|
|
327
|
-
edgeIndex,
|
|
328
|
-
routeClass: 'family-layout',
|
|
329
|
-
invariant: sourceLifeline && targetLifeline && orthogonal ? (message.isSelf ? 'self-message' : 'lifeline-message') : 'unverified-family-route',
|
|
330
|
-
bendCount: bendCount(points),
|
|
331
|
-
horizontal,
|
|
332
|
-
sourceLifeline,
|
|
333
|
-
targetLifeline,
|
|
334
|
-
selfMessage: message.isSelf,
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// ---- timeline -------------------------------------------------------------
|
|
339
|
-
|
|
340
|
-
function timelineToRendered(d: ValidDiagram, opts: { debug?: boolean } = {}): RenderedLayout {
|
|
341
|
-
try {
|
|
342
|
-
const positioned = layoutTimelineDiagram(parseTimelineDiagram(toMermaidLines(d.canonicalSource)))
|
|
343
|
-
const nodes: RenderedLayoutNode[] = []
|
|
344
|
-
const groups: RenderedLayoutGroup[] = []
|
|
345
|
-
for (const section of positioned.sections) {
|
|
346
|
-
const members: string[] = []
|
|
347
|
-
for (const period of section.periods) {
|
|
348
|
-
const periodId = `${period.id}:period`
|
|
349
|
-
nodes.push({
|
|
350
|
-
id: periodId, x: f(period.pillX), y: f(period.pillY), w: f(period.pillWidth), h: f(period.pillHeight),
|
|
351
|
-
shape: 'rounded', label: period.label,
|
|
352
|
-
})
|
|
353
|
-
members.push(periodId)
|
|
354
|
-
for (const event of period.events) {
|
|
355
|
-
nodes.push({
|
|
356
|
-
id: event.id, x: f(event.x), y: f(event.y), w: f(event.width), h: f(event.height),
|
|
357
|
-
shape: 'rectangle', label: event.text,
|
|
358
|
-
})
|
|
359
|
-
members.push(event.id)
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
if (section.framed) {
|
|
363
|
-
groups.push({ id: section.id, x: f(section.x), y: f(section.y), w: f(section.width), h: f(section.height), members, label: section.label })
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
const layout: RenderedLayout = { version: 1, kind: d.kind, nodes, edges: [], groups, bounds: { w: f(positioned.width), h: f(positioned.height) } }
|
|
367
|
-
if (opts.debug) layout.certificates = elementCertificates('timeline', layout, 'timeline-interval')
|
|
368
|
-
return layout
|
|
369
|
-
} catch { return emptyRenderedLayout(d.kind) }
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
// ---- journey --------------------------------------------------------------
|
|
373
|
-
|
|
374
|
-
function journeyToRendered(d: ValidDiagram): RenderedLayout {
|
|
375
|
-
try {
|
|
376
|
-
const positioned = layoutJourneyDiagram(parseJourneyDiagram(toMermaidLines(d.canonicalSource)))
|
|
377
|
-
const nodes: RenderedLayoutNode[] = []
|
|
378
|
-
const groups: RenderedLayoutGroup[] = []
|
|
379
|
-
for (const s of positioned.sections) {
|
|
380
|
-
const memberIds: string[] = []
|
|
381
|
-
for (const t of s.tasks) {
|
|
382
|
-
nodes.push({ id: t.id, x: f(t.x), y: f(t.y), w: f(t.width), h: f(t.height), shape: 'rectangle', label: t.text })
|
|
383
|
-
memberIds.push(t.id)
|
|
384
|
-
}
|
|
385
|
-
// The group is the section COLUMN (header band down through its task
|
|
386
|
-
// boxes), not just the header rect — so groupContainment verifies the
|
|
387
|
-
// real invariant: every task sits inside its own section span.
|
|
388
|
-
const bottom = s.tasks.length > 0
|
|
389
|
-
? Math.max(s.y + s.height, ...s.tasks.map(t => t.y + t.height))
|
|
390
|
-
: s.y + s.height
|
|
391
|
-
groups.push({ id: s.id, x: f(s.x), y: f(s.y), w: f(s.width), h: f(bottom - s.y), members: memberIds, label: s.label })
|
|
392
|
-
}
|
|
393
|
-
return { version: 1, kind: d.kind, nodes, edges: [], groups, bounds: { w: f(positioned.width), h: f(positioned.height) } }
|
|
394
|
-
} catch { return emptyRenderedLayout(d.kind) }
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
// ---- architecture ---------------------------------------------------------
|
|
398
|
-
|
|
399
|
-
function architectureToRendered(d: ValidDiagram, opts: { debug?: boolean } = {}): RenderedLayout {
|
|
400
|
-
try {
|
|
401
|
-
const positioned = layoutArchitectureDiagram(parseArchitectureDiagram(toMermaidLines(d.canonicalSource)))
|
|
402
|
-
const nodes: RenderedLayoutNode[] = [
|
|
403
|
-
...positioned.services.map(s => ({
|
|
404
|
-
id: s.id, x: f(s.x), y: f(s.y), w: f(s.width), h: f(s.height), shape: 'service', label: s.label,
|
|
405
|
-
})),
|
|
406
|
-
...positioned.junctions.map(j => ({
|
|
407
|
-
id: j.id, x: f(j.x), y: f(j.y), w: f(j.width), h: f(j.height), shape: 'circle' as const, label: undefined,
|
|
408
|
-
})),
|
|
409
|
-
]
|
|
410
|
-
const flatGroups = new Map<string, PositionedArchitectureGroup>()
|
|
411
|
-
const groups: RenderedLayoutGroup[] = []
|
|
412
|
-
const flatten = (g: PositionedArchitectureGroup): void => {
|
|
413
|
-
flatGroups.set(g.id, g)
|
|
414
|
-
groups.push({ id: g.id, x: f(g.x), y: f(g.y), w: f(g.width), h: f(g.height), members: [], label: g.label, parentId: g.parentId })
|
|
415
|
-
for (const c of g.children) flatten(c)
|
|
416
|
-
}
|
|
417
|
-
for (const g of positioned.groups) flatten(g)
|
|
418
|
-
const serviceById = new Map(positioned.services.map(s => [s.id, s]))
|
|
419
|
-
const junctionById = new Map(positioned.junctions.map(j => [j.id, j]))
|
|
420
|
-
const edges: RenderedLayoutEdge[] = positioned.edges.map((e, i) => ({
|
|
421
|
-
id: `edge#${i}:${e.source.id}->${e.target.id}`, from: e.source.id, to: e.target.id,
|
|
422
|
-
path: e.points.map(p => [f(p.x), f(p.y)] as [Finite, Finite]),
|
|
423
|
-
label: e.label && e.labelPosition ? { x: f(e.labelPosition.x), y: f(e.labelPosition.y), text: e.label } : undefined,
|
|
424
|
-
route: opts.debug ? architectureRouteCertificate(i, e, serviceById, junctionById, flatGroups) : undefined,
|
|
425
|
-
}))
|
|
426
|
-
return { version: 1, kind: d.kind, nodes, edges, groups, bounds: { w: f(positioned.width), h: f(positioned.height) } }
|
|
427
|
-
} catch { return emptyRenderedLayout(d.kind) }
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
function architectureRouteCertificate(
|
|
431
|
-
edgeIndex: number,
|
|
432
|
-
edge: PositionedArchitectureEdge,
|
|
433
|
-
services: Map<string, PositionedArchitectureService>,
|
|
434
|
-
junctions: Map<string, PositionedArchitectureJunction>,
|
|
435
|
-
groups: Map<string, PositionedArchitectureGroup>,
|
|
436
|
-
): FamilyEdgeRouteCertificate {
|
|
437
|
-
const boundsFor = (endpoint: PositionedArchitectureEdge['source']) => {
|
|
438
|
-
if (endpoint.boundary === 'group') {
|
|
439
|
-
const service = services.get(endpoint.id)
|
|
440
|
-
return service?.parentId ? groups.get(service.parentId) : undefined
|
|
441
|
-
}
|
|
442
|
-
return services.get(endpoint.id) ?? junctions.get(endpoint.id)
|
|
443
|
-
}
|
|
444
|
-
const sourceBounds = boundsFor(edge.source)
|
|
445
|
-
const targetBounds = boundsFor(edge.target)
|
|
446
|
-
const first = edge.points[0]
|
|
447
|
-
const last = edge.points[edge.points.length - 1]
|
|
448
|
-
const sourceAnchored = !!(first && sourceBounds && pointOnSide(first, sourceBounds, edge.source.side, 1))
|
|
449
|
-
const targetAnchored = !!(last && targetBounds && pointOnSide(last, targetBounds, edge.target.side, 1))
|
|
450
|
-
const orthogonal = routeOrthogonal(edge.points)
|
|
451
|
-
return {
|
|
452
|
-
family: 'architecture',
|
|
453
|
-
edgeIndex,
|
|
454
|
-
routeClass: 'family-layout',
|
|
455
|
-
invariant: sourceAnchored && targetAnchored && orthogonal ? 'side-anchored' : 'unverified-family-route',
|
|
456
|
-
bendCount: bendCount(edge.points),
|
|
457
|
-
orthogonal,
|
|
458
|
-
sourceSide: edge.source.side,
|
|
459
|
-
targetSide: edge.target.side,
|
|
460
|
-
sourceBoundary: edge.source.boundary,
|
|
461
|
-
targetBoundary: edge.target.boundary,
|
|
462
|
-
sourceAnchored,
|
|
463
|
-
targetAnchored,
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
function pointOnSide(
|
|
468
|
-
point: Point,
|
|
469
|
-
bounds: { x: number; y: number; width: number; height: number },
|
|
470
|
-
side: 'L' | 'R' | 'T' | 'B',
|
|
471
|
-
tol: number,
|
|
472
|
-
): boolean {
|
|
473
|
-
switch (side) {
|
|
474
|
-
case 'L': return Math.abs(point.x - bounds.x) <= tol && point.y >= bounds.y - tol && point.y <= bounds.y + bounds.height + tol
|
|
475
|
-
case 'R': return Math.abs(point.x - (bounds.x + bounds.width)) <= tol && point.y >= bounds.y - tol && point.y <= bounds.y + bounds.height + tol
|
|
476
|
-
case 'T': return Math.abs(point.y - bounds.y) <= tol && point.x >= bounds.x - tol && point.x <= bounds.x + bounds.width + tol
|
|
477
|
-
case 'B': return Math.abs(point.y - (bounds.y + bounds.height)) <= tol && point.x >= bounds.x - tol && point.x <= bounds.x + bounds.width + tol
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
// ---- xychart --------------------------------------------------------------
|
|
482
|
-
|
|
483
|
-
function xychartToRendered(d: ValidDiagram, opts: { debug?: boolean } = {}): RenderedLayout {
|
|
484
|
-
try {
|
|
485
|
-
const positioned = layoutXYChart(xychartForRenderedLayout(d))
|
|
486
|
-
const nodes: RenderedLayoutNode[] = []
|
|
487
|
-
// Bars are the primary boxes.
|
|
488
|
-
positioned.bars.forEach((b, i) => {
|
|
489
|
-
nodes.push({ id: `bar#${i}`, x: f(b.x), y: f(b.y), w: f(b.width), h: f(b.height), shape: 'rectangle', label: b.label })
|
|
490
|
-
})
|
|
491
|
-
// Line series points become small marker boxes so line-only charts are
|
|
492
|
-
// still measured (whitespace/legibility care about node area).
|
|
493
|
-
positioned.lines.forEach((ln, li) => {
|
|
494
|
-
ln.points.forEach((p, pi) => {
|
|
495
|
-
nodes.push({ id: `line#${li}:pt#${pi}`, x: f(p.x - 3), y: f(p.y - 3), w: f(6), h: f(6), shape: 'circle', label: p.label })
|
|
496
|
-
})
|
|
497
|
-
})
|
|
498
|
-
// Plot area is the single group (the chart's content frame).
|
|
499
|
-
const groups: RenderedLayoutGroup[] = [{
|
|
500
|
-
id: 'plot', x: f(positioned.plotArea.x), y: f(positioned.plotArea.y),
|
|
501
|
-
w: f(positioned.plotArea.width), h: f(positioned.plotArea.height), members: [],
|
|
502
|
-
}]
|
|
503
|
-
const layout: RenderedLayout = { version: 1, kind: d.kind, nodes, edges: [], groups, bounds: { w: f(positioned.width), h: f(positioned.height) } }
|
|
504
|
-
if (opts.debug) layout.certificates = elementCertificates('xychart', layout, 'plot-contained', groups[0], 'center')
|
|
505
|
-
return layout
|
|
506
|
-
} catch { return emptyRenderedLayout(d.kind) }
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
function xychartForRenderedLayout(d: ValidDiagram): XYChart {
|
|
510
|
-
const normalized = normalizeMermaidSource(d.canonicalSource)
|
|
511
|
-
if (d.body.kind === 'xychart') return xychartFromBody(d.body, normalized.frontmatter)
|
|
512
|
-
return applyXYChartFrontmatterConfig(parseXYChart(normalized.lines), normalized.frontmatter)
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
function xychartFromBody(body: XyChartBody, frontmatter: ReturnType<typeof normalizeMermaidSource>['frontmatter']): XYChart {
|
|
516
|
-
const series = body.series.map((s): XYChart['series'][number] => ({
|
|
517
|
-
type: s.kind,
|
|
518
|
-
label: s.name,
|
|
519
|
-
data: [...s.values],
|
|
520
|
-
}))
|
|
521
|
-
const yAxis = xyAxisFromBody(body.yAxis)
|
|
522
|
-
if (!yAxis.range && series.length > 0) {
|
|
523
|
-
const allValues = series.flatMap(s => s.data)
|
|
524
|
-
let min = Math.min(...allValues)
|
|
525
|
-
let max = Math.max(...allValues)
|
|
526
|
-
const span = max - min || 1
|
|
527
|
-
min = min - span * 0.1
|
|
528
|
-
max = max + span * 0.1
|
|
529
|
-
if (min > 0 && min < span * 0.5) min = 0
|
|
530
|
-
yAxis.range = { min, max }
|
|
531
|
-
}
|
|
532
|
-
if (!yAxis.range) yAxis.range = { min: 0, max: 100 }
|
|
533
|
-
|
|
534
|
-
const chart: XYChart = {
|
|
535
|
-
horizontal: body.horizontal ?? false,
|
|
536
|
-
xAxis: xyAxisFromBody(body.xAxis),
|
|
537
|
-
yAxis,
|
|
538
|
-
series,
|
|
539
|
-
config: resolveXYChartConfig({}),
|
|
540
|
-
theme: resolveXYChartTheme({}),
|
|
541
|
-
}
|
|
542
|
-
if (body.title !== undefined) chart.title = body.title
|
|
543
|
-
if (body.horizontal !== undefined) chart.headerOrientation = body.horizontal ? 'horizontal' : 'vertical'
|
|
544
|
-
return applyXYChartFrontmatterConfig(chart, frontmatter)
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
function xyAxisFromBody(axis: XyChartBody['xAxis']): XYAxis {
|
|
548
|
-
const out: XYAxis = {}
|
|
549
|
-
if (!axis) return out
|
|
550
|
-
if (axis.name !== undefined) out.title = axis.name
|
|
551
|
-
if (axis.categories) out.categories = [...axis.categories]
|
|
552
|
-
if (axis.range) out.range = { ...axis.range }
|
|
553
|
-
return out
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
// ---- pie ------------------------------------------------------------------
|
|
557
|
-
|
|
558
|
-
function pieToRendered(d: ValidDiagram, opts: { debug?: boolean } = {}): RenderedLayout {
|
|
559
|
-
try {
|
|
560
|
-
const positioned = layoutPieChart(pieChartForRenderedLayout(d))
|
|
561
|
-
// Pie has no structural nodes/edges — the slices are angular wedges. Use
|
|
562
|
-
// each slice's legend row as a label-anchored box (legend swatch top-left,
|
|
563
|
-
// approximate width from label length at the legend font baseline). This
|
|
564
|
-
// gives the metrics a positive node area + legible labels to measure.
|
|
565
|
-
const CHAR_PX = 7
|
|
566
|
-
const nodes: RenderedLayoutNode[] = positioned.legend.map((l, i) => {
|
|
567
|
-
const labelText = `${l.label} (${(l.fraction * 100).toFixed(1)}%)`
|
|
568
|
-
const w = Math.max(l.swatchSize, labelText.length * CHAR_PX + l.swatchSize)
|
|
569
|
-
return { id: `slice#${i}:${l.label}`, x: f(l.x), y: f(l.y), w: f(w), h: f(l.swatchSize), shape: 'rectangle', label: labelText }
|
|
570
|
-
})
|
|
571
|
-
const layout: RenderedLayout = { version: 1, kind: d.kind, nodes, edges: [], groups: [], bounds: { w: f(positioned.width), h: f(positioned.height) } }
|
|
572
|
-
if (opts.debug) layout.certificates = elementCertificates('pie', layout, 'legend-contained')
|
|
573
|
-
return layout
|
|
574
|
-
} catch { return emptyRenderedLayout(d.kind) }
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
function pieChartForRenderedLayout(d: ValidDiagram): PieChart {
|
|
578
|
-
if (d.body.kind === 'pie') return pieChartFromBody(d.body)
|
|
579
|
-
return parsePieChart(toMermaidLines(d.canonicalSource))
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
function pieChartFromBody(body: PieBody): PieChart {
|
|
583
|
-
const chart: PieChart = {
|
|
584
|
-
showData: body.showData,
|
|
585
|
-
entries: body.slices.map(s => ({ label: s.label, value: s.value })),
|
|
586
|
-
}
|
|
587
|
-
if (body.title !== undefined) chart.title = body.title
|
|
588
|
-
return chart
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
// ---- gantt ------------------------------------------------------------------
|
|
592
|
-
|
|
593
|
-
function ganttToRendered(d: ValidDiagram, opts: { debug?: boolean } = {}): RenderedLayout {
|
|
594
|
-
try {
|
|
595
|
-
const positioned = ganttPositionedForRenderedLayout(d)
|
|
596
|
-
// Bars and milestones are the nodes; section bands are the groups. Verts
|
|
597
|
-
// and ticks are markers, not boxes — they carry no node area. Milestones
|
|
598
|
-
// report the diamond's true bounding box; the zero-width floor on bars is
|
|
599
|
-
// pulled back inside the plot so a range-edge task never fakes a breach.
|
|
600
|
-
const plotRight = positioned.plot.x + positioned.plot.w
|
|
601
|
-
const nodes: RenderedLayoutNode[] = positioned.bars.map(b => {
|
|
602
|
-
const id = b.id ?? `task#${b.taskIndex}`
|
|
603
|
-
if (b.milestoneX !== undefined) {
|
|
604
|
-
const r = b.h / 2
|
|
605
|
-
return { id, x: f(b.milestoneX - r), y: f(b.y), w: f(b.h), h: f(b.h), shape: 'diamond', label: b.label }
|
|
606
|
-
}
|
|
607
|
-
const w = Math.max(2, b.w)
|
|
608
|
-
const x = Math.min(b.x, plotRight - w)
|
|
609
|
-
return { id, x: f(x), y: f(b.y), w: f(w), h: f(b.h), shape: 'rectangle', label: b.label }
|
|
610
|
-
})
|
|
611
|
-
const groups: RenderedLayoutGroup[] = positioned.sections.map((s, i) => ({
|
|
612
|
-
id: `section#${i}`, x: f(positioned.plot.x), y: f(s.y), w: f(positioned.plot.w), h: f(s.h),
|
|
613
|
-
members: positioned.bars.filter(b => b.sectionIndex === i).map(b => b.id ?? `task#${b.taskIndex}`),
|
|
614
|
-
label: s.label,
|
|
615
|
-
}))
|
|
616
|
-
const layout: RenderedLayout = { version: 1, kind: d.kind, nodes, edges: [], groups, bounds: { w: f(positioned.width), h: f(positioned.height) } }
|
|
617
|
-
if (opts.debug) layout.certificates = elementCertificates('gantt', layout, 'section-contained')
|
|
618
|
-
return layout
|
|
619
|
-
} catch { return emptyRenderedLayout(d.kind) }
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
function ganttPositionedForRenderedLayout(d: ValidDiagram): GanttLayoutResult {
|
|
623
|
-
const normalized = normalizeMermaidSource(d.canonicalSource)
|
|
624
|
-
if (d.body.kind === 'gantt') {
|
|
625
|
-
const model = ganttModelFromBody(d.body)
|
|
626
|
-
if (model) {
|
|
627
|
-
const configured = applyGanttFrontmatterConfig(model, normalized.frontmatter)
|
|
628
|
-
const schedule = resolveGanttSchedule(configured)
|
|
629
|
-
return layoutGantt(configured, schedule, { today: schedule.today })
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
return buildGanttRenderPipeline(normalized.lines, normalized.frontmatter).positioned
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
function ganttModelFromBody(body: GanttBody): GanttModel | null {
|
|
636
|
-
if (body.statements?.some(st => st.kind === 'opaque-block')) return null
|
|
637
|
-
const sections: GanttModelSection[] = body.sections.map((s, index) => {
|
|
638
|
-
const section: GanttModelSection = { taskIndexes: [], line: index + 1 }
|
|
639
|
-
if (s.label !== undefined) section.label = s.label
|
|
640
|
-
return section
|
|
641
|
-
})
|
|
642
|
-
const model: GanttModel = {
|
|
643
|
-
dateFormat: 'YYYY-MM-DD',
|
|
644
|
-
inclusiveEndDates: false,
|
|
645
|
-
topAxis: false,
|
|
646
|
-
excludes: [],
|
|
647
|
-
includes: [],
|
|
648
|
-
weekendStart: 'saturday',
|
|
649
|
-
weekStart: 'sunday',
|
|
650
|
-
sections: sections.length > 0 ? sections : [{ taskIndexes: [] }],
|
|
651
|
-
tasks: [],
|
|
652
|
-
clicks: [],
|
|
653
|
-
}
|
|
654
|
-
if (body.title !== undefined) model.title = body.title
|
|
655
|
-
|
|
656
|
-
for (let sectionIndex = 0; sectionIndex < body.sections.length; sectionIndex++) {
|
|
657
|
-
const section = body.sections[sectionIndex]!
|
|
658
|
-
const modelSection = model.sections[sectionIndex]!
|
|
659
|
-
for (const task of section.tasks) {
|
|
660
|
-
const taskIndex = model.tasks.length
|
|
661
|
-
const modelTask: GanttModelTask = {
|
|
662
|
-
index: taskIndex,
|
|
663
|
-
label: task.label,
|
|
664
|
-
tags: [...task.tags] as GanttTaskTag[],
|
|
665
|
-
end: ganttEndExpr(task.end),
|
|
666
|
-
sectionIndex,
|
|
667
|
-
line: taskIndex + 1,
|
|
668
|
-
}
|
|
669
|
-
if (task.taskId !== undefined) modelTask.id = task.taskId
|
|
670
|
-
if (task.start !== undefined) modelTask.start = ganttStartExpr(task.start)
|
|
671
|
-
model.tasks.push(modelTask)
|
|
672
|
-
modelSection.taskIndexes.push(taskIndex)
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
return model
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
function ganttStartExpr(raw: string): GanttStartExpr {
|
|
679
|
-
const after = raw.match(/^after\s+(.+)$/)
|
|
680
|
-
if (after) return { kind: 'after', refs: after[1]!.split(/\s+/).filter(Boolean) }
|
|
681
|
-
return { kind: 'date', raw }
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
function ganttEndExpr(raw: string): GanttEndExpr {
|
|
685
|
-
const until = raw.match(/^until\s+(.+)$/)
|
|
686
|
-
if (until) return { kind: 'until', refs: until[1]!.split(/\s+/).filter(Boolean) }
|
|
687
|
-
if (GANTT_DURATION_RE.test(raw)) return { kind: 'duration', raw }
|
|
688
|
-
return { kind: 'date', raw }
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
/**
|
|
692
|
-
* Closes the "verify ok but render throws" seam for structured gantt bodies
|
|
693
|
-
* (found by harvesting upstream's `excludes weekdays …` parser case): when the
|
|
694
|
-
* schedule cannot resolve, verify surfaces UNRESOLVABLE_SCHEDULE (error
|
|
695
|
-
* severity) carrying the named GANTT_* reason, instead of silently degrading
|
|
696
|
-
* to an empty layout. GANTT_EMPTY maps to the existing EMPTY_DIAGRAM code.
|
|
697
|
-
*/
|
|
698
|
-
export function ganttScheduleWarning(d: ValidDiagram): LayoutWarning | null {
|
|
699
|
-
try {
|
|
700
|
-
const normalized = normalizeMermaidSource(d.canonicalSource)
|
|
701
|
-
const model = applyGanttFrontmatterConfig(parseGanttModel(normalized.lines), normalized.frontmatter)
|
|
702
|
-
resolveGanttSchedule(model)
|
|
703
|
-
return null
|
|
704
|
-
} catch (e) {
|
|
705
|
-
const message = e instanceof Error ? e.message : String(e)
|
|
706
|
-
if (message.startsWith('GANTT_EMPTY')) return { code: 'EMPTY_DIAGRAM' }
|
|
707
|
-
return { code: 'UNRESOLVABLE_SCHEDULE', reason: message }
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
/**
|
|
712
|
-
* Geometric tripwires for the gantt layout (docs/design/families/gantt.md
|
|
713
|
-
* §Verification; issue #26 WS11): OFF_CANVAS when a resolved bar/milestone
|
|
714
|
-
* leaves the canvas, GROUP_BREACH when a section-owned bar leaves its section
|
|
715
|
-
* band. The layout produces contained geometry by construction (property-
|
|
716
|
-
* tested), so these fire only if a later pass mutates geometry — the same
|
|
717
|
-
* zero-noise contract as the route-contract tripwires.
|
|
718
|
-
*/
|
|
719
|
-
export function layoutGeometryWarnings(
|
|
720
|
-
layout: RenderedLayout,
|
|
721
|
-
opts: { edgeAnchors?: boolean; nodeOverlaps?: boolean; groupContainment?: boolean } = {},
|
|
722
|
-
): LayoutWarning[] {
|
|
723
|
-
const warnings: LayoutWarning[] = []
|
|
724
|
-
const TOL = 0.5
|
|
725
|
-
for (const n of layout.nodes) {
|
|
726
|
-
if (n.x < -TOL || n.x + n.w > layout.bounds.w + TOL) warnings.push({ code: 'OFF_CANVAS', target: n.id, axis: 'x' })
|
|
727
|
-
if (n.y < -TOL || n.y + n.h > layout.bounds.h + TOL) warnings.push({ code: 'OFF_CANVAS', target: n.id, axis: 'y' })
|
|
728
|
-
}
|
|
729
|
-
for (const e of layout.edges) {
|
|
730
|
-
for (const [x, y] of e.path) {
|
|
731
|
-
if (x < -TOL || x > layout.bounds.w + TOL) { warnings.push({ code: 'OFF_CANVAS', target: e.id, axis: 'x' }); break }
|
|
732
|
-
}
|
|
733
|
-
for (const [x, y] of e.path) {
|
|
734
|
-
if (y < -TOL || y > layout.bounds.h + TOL) { warnings.push({ code: 'OFF_CANVAS', target: e.id, axis: 'y' }); break }
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
if (opts.nodeOverlaps) {
|
|
738
|
-
for (let i = 0; i < layout.nodes.length; i++) {
|
|
739
|
-
for (let j = i + 1; j < layout.nodes.length; j++) {
|
|
740
|
-
const a = layout.nodes[i]!, b = layout.nodes[j]!
|
|
741
|
-
const ox = Math.max(0, Math.min(a.x + a.w, b.x + b.w) - Math.max(a.x, b.x))
|
|
742
|
-
const oy = Math.max(0, Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y))
|
|
743
|
-
const area = ox * oy
|
|
744
|
-
if (area > TOL) warnings.push({ code: 'NODE_OVERLAP', a: a.id, b: b.id, areaPx: Math.round(area) })
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
if (opts.groupContainment) {
|
|
749
|
-
const nodeById = new Map(layout.nodes.map(n => [n.id, n]))
|
|
750
|
-
for (const g of layout.groups) {
|
|
751
|
-
for (const memberId of g.members) {
|
|
752
|
-
const n = nodeById.get(memberId)
|
|
753
|
-
if (!n) continue
|
|
754
|
-
const inside = n.x >= g.x - TOL && n.y >= g.y - TOL &&
|
|
755
|
-
n.x + n.w <= g.x + g.w + TOL && n.y + n.h <= g.y + g.h + TOL
|
|
756
|
-
if (!inside) warnings.push({ code: 'GROUP_BREACH', group: g.id, member: memberId })
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
if (opts.edgeAnchors) {
|
|
761
|
-
const nodeById = new Map(layout.nodes.map(n => [n.id, n]))
|
|
762
|
-
for (const e of layout.edges) {
|
|
763
|
-
const first = e.path[0]
|
|
764
|
-
const last = e.path[e.path.length - 1]
|
|
765
|
-
const source = nodeById.get(e.from)
|
|
766
|
-
const target = nodeById.get(e.to)
|
|
767
|
-
if (first && source && !pointOnRectBoundary(first[0], first[1], source, TOL)) warnings.push({ code: 'ROUTE_SHAPE_MISANCHOR', edge: e.id, node: source.id })
|
|
768
|
-
if (last && target && !pointOnRectBoundary(last[0], last[1], target, TOL)) warnings.push({ code: 'ROUTE_SHAPE_MISANCHOR', edge: e.id, node: target.id })
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
return warnings
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
function pointOnRectBoundary(x: number, y: number, n: RenderedLayoutNode, tol: number): boolean {
|
|
775
|
-
const onVertical = (Math.abs(x - n.x) <= tol || Math.abs(x - (n.x + n.w)) <= tol) && y >= n.y - tol && y <= n.y + n.h + tol
|
|
776
|
-
const onHorizontal = (Math.abs(y - n.y) <= tol || Math.abs(y - (n.y + n.h)) <= tol) && x >= n.x - tol && x <= n.x + n.w + tol
|
|
777
|
-
return onVertical || onHorizontal
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
export function ganttGeometryWarnings(layout: RenderedLayout): LayoutWarning[] {
|
|
781
|
-
const warnings: LayoutWarning[] = []
|
|
782
|
-
const TOL = 0.5 // coordinates round through f(); allow rounding slack
|
|
783
|
-
for (const n of layout.nodes) {
|
|
784
|
-
if (n.x < -TOL || n.x + n.w > layout.bounds.w + TOL) warnings.push({ code: 'OFF_CANVAS', target: n.id, axis: 'x' })
|
|
785
|
-
if (n.y < -TOL || n.y + n.h > layout.bounds.h + TOL) warnings.push({ code: 'OFF_CANVAS', target: n.id, axis: 'y' })
|
|
786
|
-
}
|
|
787
|
-
const nodeById = new Map(layout.nodes.map(n => [n.id, n]))
|
|
788
|
-
for (const g of layout.groups) {
|
|
789
|
-
for (const memberId of g.members) {
|
|
790
|
-
const n = nodeById.get(memberId)
|
|
791
|
-
if (!n) continue
|
|
792
|
-
const inside = n.x >= g.x - TOL && n.y >= g.y - TOL &&
|
|
793
|
-
n.x + n.w <= g.x + g.w + TOL && n.y + n.h <= g.y + g.h + TOL
|
|
794
|
-
if (!inside) warnings.push({ code: 'GROUP_BREACH', group: g.id, member: memberId })
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
return warnings
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
// ---- quadrant -------------------------------------------------------------
|
|
801
|
-
|
|
802
|
-
function quadrantToRendered(d: ValidDiagram, opts: { debug?: boolean } = {}): RenderedLayout {
|
|
803
|
-
try {
|
|
804
|
-
const positioned = layoutQuadrantChart(quadrantChartForRenderedLayout(d))
|
|
805
|
-
const nodes: RenderedLayoutNode[] = positioned.points.map((p, i) => ({
|
|
806
|
-
id: `point#${i}:${p.label}`, x: f(p.cx - p.radius), y: f(p.cy - p.radius),
|
|
807
|
-
w: f(p.radius * 2), h: f(p.radius * 2), shape: 'circle', label: p.label,
|
|
808
|
-
}))
|
|
809
|
-
const groups: RenderedLayoutGroup[] = positioned.regions.map(r => ({
|
|
810
|
-
id: `quadrant#${r.number}`, x: f(r.x), y: f(r.y), w: f(r.width), h: f(r.height), members: [], label: r.label,
|
|
811
|
-
}))
|
|
812
|
-
const layout: RenderedLayout = { version: 1, kind: d.kind, nodes, edges: [], groups, bounds: { w: f(positioned.width), h: f(positioned.height) } }
|
|
813
|
-
if (opts.debug) layout.certificates = elementCertificates('quadrant', layout, 'plot-contained')
|
|
814
|
-
return layout
|
|
815
|
-
} catch { return emptyRenderedLayout(d.kind) }
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
function quadrantChartForRenderedLayout(d: ValidDiagram): QuadrantChart {
|
|
819
|
-
if (d.body.kind === 'quadrant') return quadrantChartFromBody(d.body)
|
|
820
|
-
return parseQuadrantChart(toMermaidLines(d.canonicalSource))
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
function quadrantChartFromBody(body: QuadrantBody): QuadrantChart {
|
|
824
|
-
const chart: QuadrantChart = {
|
|
825
|
-
quadrants: [...body.quadrants] as QuadrantChart['quadrants'],
|
|
826
|
-
points: body.points.map(p => ({ label: p.label, x: p.x, y: p.y })),
|
|
827
|
-
}
|
|
828
|
-
if (body.title !== undefined) chart.title = body.title
|
|
829
|
-
if (body.xAxis) chart.xAxis = { ...body.xAxis }
|
|
830
|
-
if (body.yAxis) chart.yAxis = { ...body.yAxis }
|
|
831
|
-
return chart
|
|
832
|
-
}
|