Quality gates for adaptive layout:
- fill_metrics()/check_scale_up() in the safety harness: overflow catches
content too big for a panel, but nothing caught content that stays tiny
on panels >= 2x the plugin's declared design size. The check measures
lit-content extents and warns (or fails, when a plugin opts into
"fill_check": "strict" in test/harness.json) below 50% coverage on the
doubled axis. Warn-only by default so no existing plugin breaks.
- harness.json "variants": extra runs with config overlays and their own
golden dirs, so an opt-in mode (e.g. layout_mode: adaptive) is golden-
tested beside the classic default. check_plugin.py loops base + variants
and labels variant results mode@name.
- Dev preview server: GET /api/sizes (harness size sample), POST
/api/render-matrix (render at up to 12 sizes in one call), size-preset
dropdown, and an "All Sizes" side-by-side gallery in the preview UI.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add src/adaptive_images.py — the image counterpart to fit_text:
- fit_image(img, box, mode=contain|cover|fill_height|stretch,
crop_to_ink, anchor, resample, upscale) promoting the proven plugin
patterns (football's crop-to-ink fill-height logos, masters' cover
crop + NEAREST flags, static-image's letterbox). Upscales by default —
thumbnail()'s downscale-only behavior is why imagery stays tiny on
big panels.
- draw_fitted_image() pastes aligned within a Region with alpha mask.
- One central Pillow>=9.1 RESAMPLE shim replacing ~15 plugin copies.
LayoutContext.fit_image() caches results per (identity, box size,
options) with a 64-entry LRU; id()-keyed entries pin the source image.
BasePlugin.draw_image() is the one-liner adoption path beside draw_fit.
Composites in adaptive_layout.py: Region.offset() (user x/y-offset
passthrough), scoreboard_regions() (the two-logos-plus-score card math
duplicated across six sports plugins, logo_slot = min(H, W//2)), and
media_row() (art-left/text-right).
Fix LogoHelper's size-blind cache key (stale sizes on panel change);
deprecation note on dead image_utils.py.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add src/adaptive_layout.py — opt-in core helpers so plugins render
legibly on any panel size without hand-tuned per-display layouts:
- Region: integer rect algebra (bands/columns/weighted splits/centering)
that partitions space so text bands can't overlap by construction
- Font ladders: ordered (family, size) steps known to render crisply
(LADDER_GRID: X11 BDFs at native sizes; LADDER_ARCADE: PressStart2P at
8px multiples) — fitting walks the ladder instead of scaling pixel
fonts fractionally
- LayoutContext: breakpoint tiers, geometry scale vs. a declared design
size, and cached fit_text/fit_lines/font_for_rows queries
Generalizes the three patterns proven in the field: f1-scoreboard's
scale factor, masters-tournament's tiers, baseball-scoreboard's font
fallback ladder.
Wiring: BasePlugin gains a lazy .layout property and draw_fit();
FontManager gains get_native_bdf_size() and a cache_generation counter;
manifest schema gains display.design_size and requires.display_size
max_width/max_height; 96x48 joins DEFAULT_TEST_SIZES; the bounds-check
harness records negative-coordinate draws; TextHelper's broken
measurement helpers are fixed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>