feat(diagram-engine): open shape vocabulary — catalog + pass-through, structured style merge

The expressiveness gap traced to vocabulary: draw.io has hundreds of shapes,
the declarative layer allowed six. Opening it, with the failure modes from
design review handled:

- shapes.ts: a ~20-entry curated catalog (full style fragment incl. the
  matching perimeter — required or edges connect to the bounding box; a
  text-scale factor verified in the real editor — the same sentence overflows
  a 1.0x rhombus and fits a 1.5x one; labelOutside+glyph for umlActor-style
  figures). Any other token passes through verbatim: draw.io degrades unknown
  shapes to rectangles safely (verified). Injection-capable tokens (;/=) are
  rejected outright.
- Pass-through emits a WARNING with a nearest-catalog hint (bounded edit
  distance), so a typo'd 'cyclinder' is a one-turn fix instead of a silently
  rectangular node forever. set_shape/set_role/set_group operations make the
  fix possible without remove+re-add (which would drop links).
- mergeStyle(): style fragments merge per-key (later wins, bare shape classes
  displace each other) instead of string concatenation. This is what makes
  shape and theme composable by rule — shape owns geometry keys, theme owns
  colour/type keys, and an overlap resolves by order instead of emitting
  contradictory duplicates.
- dai_shape marker carries the declared token through the round trip:
  appearance-based reverse mapping cannot distinguish aliases (diamond vs
  decision) or a rotated queue from a cylinder.
- dai_auto marker separates engine-measured size from user-fixed size: parsed
  boxes no longer freeze the first layout's numbers, so changing a label
  re-measures. Pinned nodes keep everything, as before.
- draw_graph's closed shape enum opened to match (first instance of the
  schema-drift problem the review predicted).

14 new tests: merge ownership, injection rejection, near-match hints,
alias-preserving round trip, re-measure on label change, mxgraph.* tokens
staying boxes with role/group intact. 556 unit tests green; acceptance
diagram (person/hexagon/cylinder/queue/cloud/decision/callout) verified in
the real editor.
This commit is contained in:
dayuan.jiang
2026-08-09 21:56:09 +09:00
parent db9db1ff4e
commit 50826c0ac8
11 changed files with 644 additions and 75 deletions

View File

@@ -32,19 +32,14 @@ export interface PoolCell {
export type Align = "start" | "center" | "end" | "stretch"
/**
* The outline a flowchart box is drawn with.
* What a box IS, drawn as its conventional outline.
*
* Flowchart notation is conventional, not decorative: a reader takes a diamond to mean a
* branch and a stadium to mean an entry or exit point. Rendering every step as the same
* rectangle throws that away.
* Open vocabulary: catalog names ("cylinder", "decision", "person"…) get full engine
* support — correct perimeter, text sized to fit the outline. Any other draw.io shape
* token passes through verbatim and degrades to a rectangle if the editor does not
* know it. See shapes.ts.
*/
export type BoxShape =
| "box"
| "decision"
| "terminator"
| "round"
| "data"
| "document"
export type BoxShape = string
/** A catalog icon: a real stencil, drawn at a fixed glyph size with a label below. */
export interface IconNode {