Skip to content

fix: arch diagram box auto-height under-estimates text (CJK up to -26% even on 16:9; -51% on 4:3) #285

Description

@ShotaroKataoka

Summary

The architecture-diagram layout engine under-estimates auto-sized box heights, causing
text to overflow the rounded rectangle. Two independent defects compound:

  1. CJK text (affects all templates, including 16:9): the width estimate assumes
    halfwidth characters (~5pt average). Fullwidth characters are 11pt (1em), so Japanese
    description text gets roughly half the lines it needs.
  2. Non-16:9 templates: the estimation constants are burned in for the 16:9 physical
    density (1px = 0.5pt). On 4:3 (1px = 0.375pt) the same 11pt text occupies 33% more px.

Measured shortfall of the estimated height vs. required height (box width 240px,
title + description, build_layout direct invocation):

Case 16:9 4:3
Halfwidth title only -4% -15%
Halfwidth + description -5% -40%
Fullwidth (Japanese) + description -26% -51%

This was listed as a known limitation in #283 (workaround: specify box.height
explicitly). The "~25%" figure there under-stated the impact — line wrapping is
discrete, so the two defects multiply.

Root cause

sdpm/sdpm/engine/layout/placement.py:43-49:

char_per_line = max(1, bw // 10)   # assumes 1 char = 10px = 5pt at 16:9, halfwidth only
...
bh = lines * 24 + 40               # assumes 1 line = 24px = 12pt at 16:9

The true calibration in pt (verified against the builder): box text is always 11pt
(layout/model.py), rendered line height 13.2pt (1.2em), padding 20pt, average char
width 5pt halfwidth / 11pt fullwidth. The px constants are these pt values converted
at the 16:9 rate only. Icon-label metrics (len(line) * 8, label heights) have the
same burn-in.

The same 16:9 rate is also hardcoded in the knowledge layer: the width guide in
sdpm/references/workflows/slide-json-spec.md teaches fullwidth = pt × 2 × char count
— the «× 2» is 1pt = 2px, true only for 16:9. Agents budget text 33% too optimistically
on 4:3 for every textbox, not just arch diagrams.

Design

ptPerPx — declare the deck's unit relationship in slideSize

Slide JSON mixes two unit systems: layout in px (virtual, width normalized to 1920),
fonts in pt (physical). The exchange rate is ptPerPx = physical width in pt / 1920,
which cannot be derived from the canvas size (4:3 and 16:10 presets share
0.375 despite different aspect ratios — it depends on physical width).

deck.json's slideSize is best understood as the spec's coordinate-system
declaration (like an SVG viewBox) — px coordinates are meaningless without it, and
generate already validates the declaration against the template. The pt↔px rate is
part of that declaration:

"slideSize": {"width": 1920, "height": 1440, "ptPerPx": 0.375}

Information flow (same transcription pattern as #208 — the value originates from
tools, not hand-maintained constants):

analyze_template reports slide_size {width, height, ptPerPx}
  → agent transcribes verbatim into deck.json (no arithmetic)
  → consumers: agent text budgeting (width guide), arch_diagram(pt_per_px=...)
  → generate validates declaration vs. template
(exception: pptx import writes the same shape directly)

Layout engine: explicit pt_per_px argument (default 0.5)

render_architecturebuild_layout_layout_scale gain a keyword argument.
Estimates are rewritten as pt-true values converted by pt_per_px, with per-character
CJK-aware width (fullwidth = 11pt, halfwidth = 5pt). Default 0.5 is the exact 16:9
value, so all existing callers are unchanged.

Rejected alternatives (recorded for the future):

  • Passing slide_width_emu: EMU is an infrastructure concept; layout/ currently
    imports nothing but stdlib and stays a template-agnostic pure px space
  • Reusing the converter's conversion_scale ContextVar: layout → converter would
    invert the dependency DAG, and implicit global state breaks the pure-function tests
  • Deriving from canvas height (assume 7.5in): swaps a 16:9 burn-in for a
    7.5in burn-in (16:10 presets are 6.25in → 17% error)
  • Scaling the font instead of the estimate: the engine-wide convention is
    "pt is physical, px is normalized layout"; arch text would become inconsistent
    with surrounding slide text
  • Adding text-overflow checks to lint: heuristic width checks are false-positive
    prone (fonts, autofit, mixed scripts); the accurate detector already exists at
    generate/preview time (LibreOffice renders the real pptx — correct for any
    aspect ratio since fix: 4:3 など 16:9 以外のカスタムテンプレートで生成すると、レイアウト崩れ・プレビュー見切れが発生する #208's isotropic measure fix). Budgeting belongs to the
    knowledge layer, verification to generate — lint stays context-free

Note: 16:9 output changes too (intentional)

This is a bug fix: 16:9 boxes with halfwidth text grow ~5-10%; Japanese descriptions
gain their missing lines. Existing decks re-generate with slightly different arch
layouts.

Also fixed in this change

  1. fix: 4:3 など 16:9 以外のカスタムテンプレートで生成すると、レイアウト崩れ・プレビュー見切れが発生する #208 gap (found during impact analysis): the remote template-analysis cache
    (analysisJson in DynamoDB) stores only {theme_colors, layouts}slide_size
    is dropped. Cache hits on analyze_template therefore return no slide_size,
    silently defeating fix: 4:3 など 16:9 以外のカスタムテンプレートで生成すると、レイアウト崩れ・プレビュー見切れが発生する #208's transcription flow for uploaded templates. The cache
    will store the full analyzer output.
  2. Dead code: api.init(template=...) has had zero production callers since
    PR 🐛 fix(mcp-local-analyze-template-search): use get_templates_dirs() fo… #152 removed the tool-side passthrough (the MCP init_presentation tool takes
    no template; remote init explicitly documents "Template is NOT set at init time").
    PR fix: support non-16:9 custom templates (4:3 etc.) and fix two pre-existing live preview issues #283 unknowingly extended this dead branch (slideSize write at init) and added
    tests for it. The branch, its tests, and stale doc references will be removed;
    slideSize writers reduce to: agent transcription + pptx import.

Out of scope

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions