mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-09-01 17:10:24 +08:00
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:
@@ -25,6 +25,7 @@ import { extractDiagramXML } from "@/lib/utils"
|
||||
import {
|
||||
type Direction,
|
||||
hasMarkers,
|
||||
isAutoSized,
|
||||
isLaneChrome,
|
||||
isPinned,
|
||||
MARKER,
|
||||
@@ -361,6 +362,12 @@ function zoneOf(style: string): string | undefined {
|
||||
return v ? decodeURIComponent(v) : undefined
|
||||
}
|
||||
|
||||
/** The declared shape token stamped on a cell, if any. */
|
||||
function shapeOf(style: string): string | undefined {
|
||||
const v = readMarker(style, MARKER.shape)
|
||||
return v ? decodeURIComponent(v) : undefined
|
||||
}
|
||||
|
||||
/** The flex fields stamped on a cell, as sparse properties ready to spread. */
|
||||
function flexOf(style: string): {
|
||||
grow?: number
|
||||
@@ -1069,19 +1076,29 @@ export function parseDiagram(xml: string, pageIndex = 0): ParseResult {
|
||||
const headH = lifeline
|
||||
? Number(styleValue(c.style, "size") ?? "44")
|
||||
: undefined
|
||||
// An engine-measured box must NOT read its size back as a fixed size:
|
||||
// that would freeze the first layout's measurement, so a later label or
|
||||
// shape change would keep the stale box instead of re-measuring. A pinned
|
||||
// node is the exception — the user froze it, geometry and all.
|
||||
const auto = isAutoSized(c.style) && !pinned
|
||||
const node: BoxNode = {
|
||||
kind: "box",
|
||||
id: c.id,
|
||||
label: c.value,
|
||||
w: c.geo ? Math.round(c.geo.w) : undefined,
|
||||
w: auto ? undefined : c.geo ? Math.round(c.geo.w) : undefined,
|
||||
h: lifeline
|
||||
? Math.round(headH && headH > 0 ? headH : 44)
|
||||
: c.geo
|
||||
? Math.round(c.geo.h)
|
||||
: undefined,
|
||||
: auto
|
||||
? undefined
|
||||
: c.geo
|
||||
? Math.round(c.geo.h)
|
||||
: undefined,
|
||||
fill: styleValue(c.style, "fillColor"),
|
||||
stroke: styleValue(c.style, "strokeColor"),
|
||||
shape: boxShape(c.style),
|
||||
// The declared token wins: appearance-based reverse mapping cannot
|
||||
// distinguish aliases (decision vs diamond) or identify a passed-through
|
||||
// token whose style is just `shape=<name>`.
|
||||
shape: shapeOf(c.style) ?? boxShape(c.style),
|
||||
role: roleOf(c.style),
|
||||
group: zoneOf(c.style),
|
||||
...flexOf(c.style),
|
||||
|
||||
Reference in New Issue
Block a user