fix: support non-16:9 custom templates (4:3 etc.) and fix two pre-existing live preview issues - #283
Merged
Merged
Conversation
16:9 以外(4:3 等)のカスタムテンプレートでレイアウトが崩れる問題 (#208) を修正。 座標系の正は builder/converter に既にある「幅1920px固定・高さ可変」とし、 それに従っていなかったレイヤーを整合させる: - engine に slide_size_px() / emu_per_px() を新設し、builder と converter の インライン計算を置き換え(挙動不変) - analyzer: emu=6350 固定をやめ幅1920正規化に統一(4:3 で 1440x1080 と 誤報告していたものが 1920x1440 になる) - lint: 高さ OOB チェックを撤去しスライドサイズ非依存に純化。 4:3 の正しい配置を誤警告で押し戻すフィードバックループを解消。 幅チェックは定数として常に正しいので維持 - generate: テンプレート実寸ベースの高さ境界警告と slideSize 食い違い警告を追加 - deck.json の slideSize は新規生成時のみ engine が書く(api.init / pptx import)。 既存 deck.json は更新しない - measure: viewBox 幅基準の等方スケールに変更(4:3 で y が圧縮される問題) - grid overlay / imbalance / detect_layout の基準を統一 16:9 の挙動は完全に不変(全変更が16:9では同値)。 analyzer は既存テストが皆無だったため 16:9 pin テストを先に追加した。 SPEC: 20260805-0100_aspect-ratio-agnostic-slide-canvas Refs: #208
references が「キャンバスは1920×1080、描画領域 y=173–950」と固定値で教えて いたため、4:3 テンプレート(実キャンバス 1920×1440px)でも AI が y≤1080 で 設計し下部が空白になっていた (#208 のメイン症状)。 - キャンバス寸法は「幅1920固定・高さは slideSize 参照」に書き換え - 派生値(描画領域など)は導出式を教え、16:9 の具体値は例示として残す。 比率スケールにしないのは y=173 が title.y2+margin 由来の絶対値、 y=950 が「下端から130px」由来の絶対マージンであり、 4:3 でタイトル帯を1.33倍に太らせるのは誤りだから - art-direction / vibe に deck.json への slideSize 記録手順を追加 (vibe は analyze_template を呼ばないため deck.json が唯一の伝達経路) - composer に「幅1920固定・高さ可変、slideSize を必ず参照」の規則を追加 - スタイルデモは 16:9 固定キャンバスであることを明記 - arch 図の box 自動高さが非16:9で過小になる既知の制約と回避策 (box.height の明示指定)を arch-layout-engine.md に記載 SPEC: 20260805-0100_aspect-ratio-agnostic-slide-canvas Refs: #208
…課題を修正 #208 のプレビュー見切れ修正に加え、実装中に発覚した2つの既存問題も解消。 3つとも「異なる意味の状態を1つの変数で兼用していた」ことが原因。 1. プレビューのアスペクト比追従(#208 の症状5) aspect-[16/9] 固定枠 + object-cover で 4:3 が上下25%クロップされていた。 スライドプレビュー(SlideThumbnail / WorkspaceView / SlideCarousel / AnimatedSlidePreview)を実比率追従にする。デッキ一覧カードは グリッド行高が 16:9/4:3 混在で揃わなくなるため意図的に現状維持。 幅上限は「高さ制約を幅に変換する」既存の構造を保ち、比率のみ子から 持ち上げて可変化(w-fit だと親幅←子幅←親幅の循環で幅0に崩壊する) 2. 最初の数枚がアニメーションされない(既存問題) slides タブ表示から3秒間の抑制タイマーがあり、新規デッキでは スライド出現でタブが自動切替されるため最初の数枚が必ず即時表示に なっていた。時間ベースの近似をやめ、マウント時の事実 (hadSlidesOnMount) で判定する。これは 2026-04-13 の SPEC で既に 到達していた設計で、settled タイマーは設計意図が文書化されていない 後付けだった。新規デッキでは knownUrl を渡さない 3. PNG フォールバックが機能しない(既存問題) - リトライごとに setError(false) していたためフォールバックが毎秒 アンマウントされ実質表示されなかった → fetch 成功確定時のみ false に - container null 時に無言 return して URL を処理済み扱いしていたため 恒久的に黒箱になっていた → lastComposeUrlRef をリセットしリトライ可能に - bgSvg も components も無い場合を失敗扱いに(背景のみのスライドは正当) - SlideThumbnail に onError と previewUrl 不在時の明示プレースホルダを追加 なお skipThisUpdate は「アニメーションなしで即時描画」の意味であり 「描画しない」ではない(early return にすると既存デッキが永久に 描画されなくなる)。 SPEC: 20260805-0100_aspect-ratio-agnostic-slide-canvas Refs: #208
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #208
Summary
Fixes layout breakage with custom templates whose slide size is not 16:9 (e.g. 4:3).
Also resolves two pre-existing live-preview problems that surfaced while working on it.
Background: this is not a new coordinate system, it is an alignment fix
The engine's px coordinate system already had a correct basis — 1920 px fixed
width, height following the aspect ratio — implemented in
builderandconverter(EMU_PER_PX = slide_width / 1920). A 4:3 template's real canvas istherefore 1920×1440 px.
The problem was that every other layer hardcoded a height of 1080. So this PR
does not introduce a new coordinate system; it aligns the remaining layers with
the basis that already existed.
references/teaches "canvas is 1920×1080, drawing area y=173–950", so the model never designs below y=1080emu=6350(reporting 1440×1080 for 4:3)aspect-[16/9]frame +object-covercrops ~25% verticallyNote that lint is not involved in the main symptom — the model never attempts
y>1080 in the first place, so it is never pushed back by lint. The false
out-of-bounds warning is both an independent symptom and a landmine that only
detonates once the knowledge layer is fixed. That is why the implementation
order is fixed as engine layer → knowledge layer.
Changes
Engine (
8243368f)slide_size_px()/emu_per_px()tosdpm.engineand replaced theinline computations in
builderandconverter(behaviour unchanged)emu=6350in favour of the 1920-width basisslide size
into breaking the layout, while a false negative is caught by the
generate-time check and by preview inspection
lint(data, slide_size=...)API extension and thecache-staleness problem on the remote save path (where the template binary is
unreachable) disappeared from the design entirely
dimensions (zero false positives) and a
slideSizemismatch warningslideSizeonly when creating a new deck(
api.init/ pptx import); it never updates an existingdeck.json.deck.jsonis the spec the agent writes — if the engine rewrote existingvalues, the single-writer principle would break and
generatewould gain aside effect of mutating its own input
detect_layoutKnowledge layer (
30b33f0c)slideSize"16:9 numbers as an example
y=173istitle.y2 + margin(a placeholder-derived absolute) andy=950is"130 px above the bottom" (an absolute margin). Fattening the title band by
1.33× on 4:3 would be incorrect
slideSizeintodeck.json(vibe never calls
analyze_template, sodeck.jsonis the only path thatreaches the composer)
slideSize"Web UI and live preview (
d4d6faa3)All three problems came from one variable carrying two different meanings.
Preview follows the real aspect ratio (symptom 5 in the issue)
SlideThumbnail/WorkspaceView/SlideCarousel/AnimatedSlidePreview) now follow the actual ratiolimit" structure and only the ratio is lifted from the child. Using
w-fitinstead collapses the width to 0 (parent width ← child width ← parent width)
to the tallest cell, so mixing 16:9 and 4:3 decks would leave ~33% blank
under the 16:9 cards. These are identification thumbnails, so cropping is
acceptable
The first few slides never animated (pre-existing)
new deck switches to that tab as soon as slides arrive — so the first
slides were always shown instantly
design that was already reached on 2026-04-13; the 3-second timer was a
later addition whose rationale was never documented
The PNG fallback never appeared (pre-existing)
setError(false)ran on every 1-second poll, so the fallback was unmountedbefore it could be seen
leaving a permanently black box
bgSvgnorcomponentsis now treated as a failure(a background-only slide is still legitimate)
SlideThumbnailgainedonErrorand an explicit placeholder whenpreviewUrlis missingDocumentation (
392f833d)docs/en/custom-template.mdCHANGELOG.md[Unreleased]Testing
make all— 804 passed, 6 skippedcd web-ui && npx eslint src/ --quiet && npx tsc --noEmit && npm test—229 passed
behaviour were added before changing it — without them, "existing tests pass
unchanged" would not have been valid evidence of 16:9 invariance
tests/test_converter_scale.pyandtest_converter_elements.pypassunchanged, which is the evidence for 16:9 invariance
Verified on a live AWS deployment
Security scan
ash scan --mode local --fail-on-findingsreports 20 findings and fails, butall of them come from
npm-audit(hono / ip-address / postcss / next /sharp / undici) and are unrelated to this PR. There are no findings from the
code itself — bandit, semgrep, detect-secrets, and checkov all pass. Dependency
updates are handled by Dependabot and are out of scope here.
Out of scope (known limitations)
layout/placement.py)box.heightexplicitly — is documented:rootdesign tokens, so documenting "style demos use a fixed 16:9 canvas" is sufficientIntersectionObserver); a separate improvement from this PRNote
No 4:3-specific branching was introduced, so other ratios such as 16:10 and A4
work automatically. Verification was concentrated on 4:3.