From 8687e8f04b257df9e29d6bed437eb6a799fe0055 Mon Sep 17 00:00:00 2001 From: "dayuan.jiang" Date: Sun, 9 Aug 2026 22:22:08 +0900 Subject: [PATCH] fix(diagram-engine): slack packs to column top; paragraphs set flush-left MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The poster's two ugliest defects were engine policy, not model declarations: - A column stretched by its siblings distributed the slack into gaps (and, via grow, into boxes), producing huge panels with three lines floating in the middle. Slack policy now differs by axis: a ROW still spreads and centres (a flowchart layer reads as a pyramid), a COLUMN packs to the top and leaves leftover space at the bottom — where a reader expects it. - Multi-line body text rendered dead-centre (FALLBACK_BOX's verticalAlign=middle). Typography's basic rule, applied by content: a paragraph (explicit breaks or wrap-length text) sets align=left, verticalAlign=top with padding; a short label stays centred. Rectangles only — inside a rhombus or cloud the safe text area IS the middle. - Prompts corrected: stretch is about width, content keeps natural height, and columns are balanced by moving content — not by inflating boxes. 565 tests green; the same poster declaration re-rendered without the giant hollow panels. --- app/api/chat/route.ts | 2 +- lib/diagram-engine/layout.ts | 11 +++++++++-- lib/diagram-engine/render.ts | 20 ++++++++++++++++++++ lib/system-prompts.ts | 7 +++++-- 4 files changed, 35 insertions(+), 5 deletions(-) diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index 2c83634..f8fefda 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -704,7 +704,7 @@ Example: If previous output ended with ', , ,
— so one box carries a bold keyword, a second paragraph, a coloured verdict line. Emoji in headings (💡 Core Idea) read instantly. +The layout model is FLEXBOX. row/col containers nest freely; a box with internal structure is an invisible col container (pad 10-14) holding smaller boxes. Three knobs: grow (columns split leftover WIDTH by weight — grow 3 / grow 2 gives a 3:2 page; for containers in a row, not for leaf boxes), align "stretch" (child fills its column's width; content keeps natural height and packs to the top — the engine leaves leftover vertical space at the bottom, never inflates boxes to fill it, so balance columns by moving content between them), pad (8-14 tight card, default 24 roomy section). Labels take inline HTML — , , ,
— so one box carries a bold keyword, a second paragraph, a coloured verdict line. Paragraphs set themselves flush-left automatically; short labels centre. Emoji in headings (💡 Core Idea) read instantly. For a POSTER (paper summary, cheat sheet): one col container as the page; a banner box as the masthead with align stretch (do NOT also use set_title — the banner IS the title); a muted box for the byline; a row container holding 2-4 col containers with grow weights as columns; each section a heading-role box + content boxes, all align stretch. Give each section a distinct group name — sections sharing a group share a hue, so groups are how the poster gets its colour. Use roles on boxes: callout for the core idea, good/bad for verdict pairs, metric for the headline number, muted for fine print. A comparison card: add_container dir=col gap=8 pad=12 grow=1 role=bad, then a bold title box, the body text, a role=bad answer bar (all align stretch), and a coloured "✗ Often Wrong" verdict with align start. diff --git a/lib/diagram-engine/layout.ts b/lib/diagram-engine/layout.ts index 3e584d3..1b3c793 100644 --- a/lib/diagram-engine/layout.ts +++ b/lib/diagram-engine/layout.ts @@ -722,11 +722,18 @@ function place(p: Placed, x: number, y: number, links: LayoutLinks): void { slack = 0 } - const gap = k > 1 ? n.gap + Math.min(n.gap, slack / (k - 1)) : n.gap + // Slack policy differs by axis. A ROW spreads and centres — a flowchart layer + // reads as a pyramid, and dead space at the right edge of a row looks like a + // mistake. A COLUMN packs to the top and leaves the slack at the bottom: a column + // is usually tall because a SIBLING made it tall, and stretching its gaps (or its + // boxes, via grow) turns every panel into a huge frame with three lines floating + // in the middle — the single ugliest thing in the poster this replaced. + const gap = + alongRow && k > 1 ? n.gap + Math.min(n.gap, slack / (k - 1)) : n.gap const span = kids.reduce((s, kid) => s + (alongRow ? kid.rect.w : kid.rect.h), 0) + gap * Math.max(0, k - 1) - let cur = (alongRow ? innerX : innerTop) + Math.max(0, (extent - span) / 2) + let cur = alongRow ? innerX + Math.max(0, (extent - span) / 2) : innerTop for (const kid of kids) { // A stretching role fills the cross axis: a masthead spans its page, a section diff --git a/lib/diagram-engine/render.ts b/lib/diagram-engine/render.ts index 871601a..1bf52dc 100644 --- a/lib/diagram-engine/render.ts +++ b/lib/diagram-engine/render.ts @@ -69,6 +69,18 @@ export function esc(s: string): string { // tags DO need handling is the measure pass (layout.ts autoBoxSize), which must not // count markup as text. +/** + * Is this label a paragraph rather than a short caption? + * + * Typography's basic rule: labels centre, paragraphs set flush left. The split is by + * content — explicit line breaks, or enough text that draw.io will wrap it — because + * the model declares WHAT the text is, never how to align it. + */ +function isParagraph(label: string): boolean { + const plain = label.replace(/<[^<>]+>/g, "") + return /\n/.test(label) || /
60 +} + /** Resolve a catalog name to a style. Injected so the engine does not own the catalog. */ export type StyleResolver = ( name: string, @@ -168,6 +180,14 @@ function styleFor( n.role || n.group ? themedStyle(n.role ?? "body", hue(n.group), "leaf") : undefined, + // Typography, decided by the content: a PARAGRAPH sets left and top — + // a three-line body floating dead-centre in its box was the poster's + // second-ugliest defect — while a short label stays centred. Only for + // rectangles: inside a rhombus or a cloud the safe text area is the + // middle, which is exactly where centring puts it. + isParagraph(n.label) && !n.shape + ? "align=left;verticalAlign=top;spacingLeft=10;spacingRight=10;spacingTop=8;" + : undefined, n.fill ? `fillColor=${n.fill};` : undefined, n.stroke ? `strokeColor=${n.stroke};` : undefined, n.bold ? "fontStyle=1;" : undefined, diff --git a/lib/system-prompts.ts b/lib/system-prompts.ts index d4f1cbd..a3a502f 100644 --- a/lib/system-prompts.ts +++ b/lib/system-prompts.ts @@ -114,8 +114,11 @@ Use restructure_diagram ALSO for poster-style layouts — paper summaries, cheat freely, and a box with INTERNAL structure is just an invisible col container (pad 10-14) holding smaller boxes. Three knobs, use them everywhere: - grow: columns split leftover width by weight (grow 3 / grow 2 makes a 3:2 page). - - align "stretch": a child fills its parent's cross axis — headings, highlight bars and - body boxes should almost always stretch, or the column looks ragged. + - align "stretch": a child fills its parent's cross axis (width, in a column). Use it on + headings, highlight bars and body boxes so a column's edges line up. Note stretch is + about WIDTH — content keeps its natural height and packs to the top of its column; the + engine leaves leftover vertical space at the bottom, never inflates boxes to fill it. + So do NOT give leaf boxes grow to "fill" a column — balance columns by moving content. - pad: small (8-14) for tight cards, default 24 for roomy sections. Labels take inline HTML — , , ,
— so one box can hold a bold keyword, a second paragraph, a coloured verdict line. Emoji in headings (💡 Core Idea)