feat(diagram-engine): flex knobs (grow/align/pad) + inline rich-text labels

The expressiveness gap between engine output and hand-written XML came down
to two missing capabilities, both generic:

- Block layout inside a box: nested containers already existed, but there was
  no way to split space by weight, pin a child to an edge, or tighten padding.
  Added grow (flex-grow over the parent's leftover flow-axis space, TeX's
  glue), align (start/center/end/stretch on the cross axis) and pad
  (per-group interior padding). All three round-trip via dai_grow/dai_align/
  dai_pad markers.

- Inline rich text: labels already render HTML (html=1 on every style, esc()
  entities decode back), but the measure pass counted markup as text. The
  visibleText() strip makes autoBoxSize measure what draw.io draws: <br> is a
  line, other inline tags are invisible.

Graphviz (HTML-like table labels), D2 (grid containers + markdown-in-shape)
and TeX (box+glue) converged on exactly this design: nested boxes for block
structure, proportional glue, a small inline set for text — never full HTML.

Prompts teach the composition with a comparison-card recipe; verified by
rebuilding the CoT poster end to end in the real editor.
This commit is contained in:
dayuan.jiang
2026-08-09 20:55:12 +09:00
parent e78322ca52
commit db9db1ff4e
9 changed files with 432 additions and 23 deletions

View File

@@ -25,6 +25,7 @@ import {
isInvisible,
stampCell,
stampContainer,
stampFlex,
stampGroup,
stampLane,
stampLeaf,
@@ -35,7 +36,7 @@ import {
stampSequence,
} from "./markers"
import { type RoutedEdge, routeEdges } from "./route"
import { hueOf, NEUTRAL, type Role, themedStyle } from "./theme"
import { hueOf, NEUTRAL, themedStyle } from "./theme"
import {
type BoxShape,
type DiagramNode,
@@ -58,6 +59,14 @@ export function esc(s: string): string {
.replace(/'/g, "&#39;")
}
// NOTE ON RICH TEXT: a label may carry inline HTML — <b>, <i>, <font color>, <br>,
// <span> — and needs no special handling here. `esc()` writes it into the value
// attribute as entities, the XML parser decodes them back, and because every style
// carries `html=1` draw.io renders the tags. That is exactly how hand-written rich
// labels have always worked; the editor sanitises HTML labels itself. The one place
// tags DO need handling is the measure pass (layout.ts autoBoxSize), which must not
// count markup as text.
/** Resolve a catalog name to a style. Injected so the engine does not own the catalog. */
export type StyleResolver = (
name: string,
@@ -177,6 +186,7 @@ function styleFor(
let stamped = stampLeaf(base, "box")
if (n.role && n.role !== "body") stamped = stampRole(stamped, n.role)
if (n.group) stamped = stampGroup(stamped, n.group)
stamped = stampFlex(stamped, { grow: n.grow, align: n.align })
return n.cell ? stampCell(stamped, n.cell) : stamped
}
@@ -218,6 +228,8 @@ function styleFor(
}
if (groupRole && groupRole !== "body") base = stampRole(base, groupRole)
if (zone) base = stampGroup(base, zone)
if (n.kind === "group")
base = stampFlex(base, { grow: n.grow, align: n.align, pad: n.pad })
return stampContainer(base, {
kind: n.kind,
dir: n.kind === "grid" ? "grid" : n.dir,