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:
@@ -28,6 +28,7 @@
|
||||
* `pool()` primitive; sequence and radial are original to this repository.
|
||||
*/
|
||||
|
||||
import { resolveShape } from "./shapes"
|
||||
import { type Role, roleMetrics } from "./theme"
|
||||
import type {
|
||||
ContainerNode,
|
||||
@@ -212,7 +213,18 @@ function visibleText(label: string): string {
|
||||
export function autoBoxSize(
|
||||
label: string,
|
||||
role?: Role,
|
||||
shape?: string,
|
||||
): { w: number; h: number } {
|
||||
const spec = shape ? resolveShape(shape)?.spec : undefined
|
||||
// A glyph shape (umlActor…) has a fixed figure with the label below it: the slot is
|
||||
// the figure plus a line of text, and the text length does not scale the figure.
|
||||
if (spec?.labelOutside && spec.glyph) {
|
||||
const text = visibleText(label)
|
||||
return {
|
||||
w: Math.max(spec.glyph.w + 20, Math.min(160, text.length * 7 + 16)),
|
||||
h: spec.glyph.h + 22,
|
||||
}
|
||||
}
|
||||
const r = roleMetrics(role)
|
||||
const maxW = Math.round(260 * Math.max(1, r.charScale))
|
||||
const explicit = visibleText(label).split("\n")
|
||||
@@ -233,7 +245,13 @@ export function autoBoxSize(
|
||||
0,
|
||||
)
|
||||
const lineH = Math.round(r.fontSize * 1.6)
|
||||
return { w, h: Math.max(r.minH, lines * lineH + 26) }
|
||||
const h = Math.max(r.minH, lines * lineH + 26)
|
||||
// A non-rectangular outline inscribes a smaller text area than its bounding box —
|
||||
// a rhombus exactly half — so the box grows by the shape's measured factor.
|
||||
// Verified in the real editor: the same sentence overflows a 1.0× rhombus and fits
|
||||
// a 1.5× one.
|
||||
const s = spec?.textScale ?? 1
|
||||
return { w: Math.round(w * s), h: Math.round(h * s) }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -432,7 +450,7 @@ function measure(
|
||||
return { node: n, rect: { x: 0, y: 0, ...s }, children: [] }
|
||||
}
|
||||
if (n.kind === "box") {
|
||||
const auto = autoBoxSize(n.label, n.role)
|
||||
const auto = autoBoxSize(n.label, n.role, n.shape)
|
||||
return {
|
||||
node: n,
|
||||
rect: { x: 0, y: 0, w: n.w ?? auto.w, h: n.h ?? auto.h },
|
||||
|
||||
Reference in New Issue
Block a user