diff --git a/CLAUDE.md b/CLAUDE.md index 1f3c81c2..8948b09e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -158,7 +158,7 @@ Stage with **explicit paths only** — never `git add -A` or `git add .`. Cursor - `docs/VISUAL-STYLE.md` is the **binding** render standard; deviations are defects. - **Framing gate:** Layer 1 framing is measured, not eyeballed — the example's `--output` render path calls the shared helper `examples/gallery_framing.py` (`check_framing`, exit 10 on violation) before writing the still: hero fill 0.70–0.90 of frame in at least one axis, every element that matters clearing all four edges by ≥ 0.02. The check-only path never invokes it, so smoke runtimes are unaffected. The helper measures; call sites enforce. Bleed compositions call `measure_framing_deviation` and assert their own cap at the call site — `check_framing` has no `deviation=` flag. -- **Contact-sheet gate:** composite the candidate hero beside the pinned calibration set — currently `armature-bend`, `damped-track-aim`, `bmesh-gear` — commit the composite under `docs/gallery/contact-sheets/`, link it in the PR body, and report per-criterion verdicts (stage darkness, wedge warmth, subject fill, saturation, thumbnail legibility) including mean luminance versus the calibration images. A claim without the committed composite is not acceptable evidence. **This list is the canonical home of the pinned set** — update it here when a new example outclasses a member; `docs/new-example-prompt.md` points here rather than naming members. The longer "calibration references" list in `docs/VISUAL-STYLE.md` is a style reference, not this contact-sheet set. +- **Contact-sheet gate:** composite the candidate hero beside the pinned calibration set — currently `armature-bend`, `damped-track-aim`, `bmesh-gear` — commit the composite under `docs/gallery/contact-sheets/`, link it in the PR body, and report per-criterion verdicts (stage darkness, wedge warmth, subject fill, saturation, thumbnail legibility) including mean luminance versus the calibration images. A claim without the committed composite is not acceptable evidence. **This list is the canonical home of the pinned set** — update it here when a new example outclasses a member; `docs/new-example-prompt.md` points here rather than naming members. The longer "calibration references" list in `docs/VISUAL-STYLE.md` is a style reference, not this contact-sheet set. Tooling: `python scripts/render_hero.py --blender PATH --only NAME` renders the README `--output` command and writes the hero and preview webps (the one committed PNG-to-webp path); `python scripts/contact_sheet.py NAME` builds the composite in the committed format and prints each tile's luma, stage, wedge warmth and saturation; `python scripts/measure_hero_drift.py` checks committed heroes against what the code renders now. - **Asset-sheet gate (asset-type examples — game props/kits):** composite the hero asset rendered alone (neutral three-quarter view, plain studio lighting, no staging tricks, no labels, no comparison props) beside the pinned asset-quality reference set — currently `collision-hull-proxy`, `custom-normals-shade`, `vertex-weight-limit`, `lod-decimate-chain` — rendered the same way; commit under `docs/gallery/asset-sheets/`, link it in the PR body, and report a verdict. The asset ships only if it is not identifiable as the least-designed object in that lineup — a strong scene can carry a weak model; this gate removes the scene. **This list is the canonical home of the reference set** — update it here when a new asset outclasses a member. The measurable floors behind the gate (naming, material variation, edge treatment) live in `examples/gallery_asset_quality.py` — render path only, same call pattern as `gallery_framing`, exit 11 on violation — with the calibration table and dropped-floor evidence in `docs/VISUAL-STYLE.md` § Asset quality. - **Falsification:** every check must be proven to fail once — break the contract, observe the non-zero exit, restore — with the probe and the measured error reported in the PR body. An assertion that cannot fail witnesses nothing. - **After gallery regeneration** (`python scripts/build_gallery.py`), read the **generated HTML** character by character — the `` text and witnesses callouts in `docs/gallery/index.html` and `docs/gallery//index.html` — not just `examples/gallery.json`. Precedent: the `teaches.split(".")[0]` bug truncated 14/21 card alts at dotted API paths like `bmesh.ops` while the source JSON looked fine (fixed in PR #68). diff --git a/assets/og-card.jpg b/assets/og-card.jpg index ae24d1b7..9a612f7e 100644 Binary files a/assets/og-card.jpg and b/assets/og-card.jpg differ diff --git a/docs/gallery/assets/anvil-hero.webp b/docs/gallery/assets/anvil-hero.webp index 39b42162..52c8c3c6 100644 Binary files a/docs/gallery/assets/anvil-hero.webp and b/docs/gallery/assets/anvil-hero.webp differ diff --git a/docs/gallery/assets/bake-normal-high-to-low-hero.webp b/docs/gallery/assets/bake-normal-high-to-low-hero.webp index 69e6c21a..bda5faac 100644 Binary files a/docs/gallery/assets/bake-normal-high-to-low-hero.webp and b/docs/gallery/assets/bake-normal-high-to-low-hero.webp differ diff --git a/docs/gallery/assets/campfire-hero.webp b/docs/gallery/assets/campfire-hero.webp index 9f1d0ac4..15745d09 100644 Binary files a/docs/gallery/assets/campfire-hero.webp and b/docs/gallery/assets/campfire-hero.webp differ diff --git a/docs/gallery/assets/collision-hull-proxy-hero.webp b/docs/gallery/assets/collision-hull-proxy-hero.webp index 82a49b21..2e3e1093 100644 Binary files a/docs/gallery/assets/collision-hull-proxy-hero.webp and b/docs/gallery/assets/collision-hull-proxy-hero.webp differ diff --git a/docs/gallery/assets/compositor-glare-hero.webp b/docs/gallery/assets/compositor-glare-hero.webp index 0bb46b15..5824a1c5 100644 Binary files a/docs/gallery/assets/compositor-glare-hero.webp and b/docs/gallery/assets/compositor-glare-hero.webp differ diff --git a/docs/gallery/assets/custom-normals-shade-hero.webp b/docs/gallery/assets/custom-normals-shade-hero.webp index 81ffa591..00dbfe1d 100644 Binary files a/docs/gallery/assets/custom-normals-shade-hero.webp and b/docs/gallery/assets/custom-normals-shade-hero.webp differ diff --git a/docs/gallery/assets/damped-track-aim-hero.webp b/docs/gallery/assets/damped-track-aim-hero.webp index e0b81346..dfef6de3 100644 Binary files a/docs/gallery/assets/damped-track-aim-hero.webp and b/docs/gallery/assets/damped-track-aim-hero.webp differ diff --git a/docs/gallery/assets/fence-kit-hero.webp b/docs/gallery/assets/fence-kit-hero.webp index 836bf9d7..ad6fd732 100644 Binary files a/docs/gallery/assets/fence-kit-hero.webp and b/docs/gallery/assets/fence-kit-hero.webp differ diff --git a/docs/gallery/assets/gn-modifier-inputs-hero.webp b/docs/gallery/assets/gn-modifier-inputs-hero.webp index 4da5f22b..0e2494f2 100644 Binary files a/docs/gallery/assets/gn-modifier-inputs-hero.webp and b/docs/gallery/assets/gn-modifier-inputs-hero.webp differ diff --git a/docs/gallery/assets/grindstone-hero.webp b/docs/gallery/assets/grindstone-hero.webp index 5e8245df..1b323765 100644 Binary files a/docs/gallery/assets/grindstone-hero.webp and b/docs/gallery/assets/grindstone-hero.webp differ diff --git a/docs/gallery/assets/hand-pump-hero.webp b/docs/gallery/assets/hand-pump-hero.webp index d52e9080..9760145e 100644 Binary files a/docs/gallery/assets/hand-pump-hero.webp and b/docs/gallery/assets/hand-pump-hero.webp differ diff --git a/docs/gallery/assets/iron-cauldron-hero.webp b/docs/gallery/assets/iron-cauldron-hero.webp index f981b302..9c348e6b 100644 Binary files a/docs/gallery/assets/iron-cauldron-hero.webp and b/docs/gallery/assets/iron-cauldron-hero.webp differ diff --git a/docs/gallery/assets/light-link-studio-hero.webp b/docs/gallery/assets/light-link-studio-hero.webp index ca53cca4..28de01ec 100644 Binary files a/docs/gallery/assets/light-link-studio-hero.webp and b/docs/gallery/assets/light-link-studio-hero.webp differ diff --git a/docs/gallery/assets/park-bench-hero.webp b/docs/gallery/assets/park-bench-hero.webp index 3305f65e..a443e1ea 100644 Binary files a/docs/gallery/assets/park-bench-hero.webp and b/docs/gallery/assets/park-bench-hero.webp differ diff --git a/docs/gallery/assets/shipping-crate-hero.webp b/docs/gallery/assets/shipping-crate-hero.webp index abcf26fc..48c80d4f 100644 Binary files a/docs/gallery/assets/shipping-crate-hero.webp and b/docs/gallery/assets/shipping-crate-hero.webp differ diff --git a/docs/gallery/assets/signpost-hero.webp b/docs/gallery/assets/signpost-hero.webp index c8c1ef4e..2bcd5523 100644 Binary files a/docs/gallery/assets/signpost-hero.webp and b/docs/gallery/assets/signpost-hero.webp differ diff --git a/docs/gallery/assets/tavern-stool-hero.webp b/docs/gallery/assets/tavern-stool-hero.webp index d59703a2..deaa34b4 100644 Binary files a/docs/gallery/assets/tavern-stool-hero.webp and b/docs/gallery/assets/tavern-stool-hero.webp differ diff --git a/docs/gallery/assets/temp-override-join-hero.webp b/docs/gallery/assets/temp-override-join-hero.webp index 32071492..c1031726 100644 Binary files a/docs/gallery/assets/temp-override-join-hero.webp and b/docs/gallery/assets/temp-override-join-hero.webp differ diff --git a/docs/gallery/assets/terrain-scatter-hero.webp b/docs/gallery/assets/terrain-scatter-hero.webp index 8325bc55..31dc7538 100644 Binary files a/docs/gallery/assets/terrain-scatter-hero.webp and b/docs/gallery/assets/terrain-scatter-hero.webp differ diff --git a/docs/gallery/assets/text-version-stamp-hero.webp b/docs/gallery/assets/text-version-stamp-hero.webp index 654dff5d..68d6087f 100644 Binary files a/docs/gallery/assets/text-version-stamp-hero.webp and b/docs/gallery/assets/text-version-stamp-hero.webp differ diff --git a/docs/gallery/assets/treasure-chest-hero.webp b/docs/gallery/assets/treasure-chest-hero.webp index a83ab3c0..a1a55cf1 100644 Binary files a/docs/gallery/assets/treasure-chest-hero.webp and b/docs/gallery/assets/treasure-chest-hero.webp differ diff --git a/docs/gallery/assets/vse-cut-list-hero.webp b/docs/gallery/assets/vse-cut-list-hero.webp index a49bbdcf..4ede31ff 100644 Binary files a/docs/gallery/assets/vse-cut-list-hero.webp and b/docs/gallery/assets/vse-cut-list-hero.webp differ diff --git a/docs/gallery/assets/wall-torch-hero.webp b/docs/gallery/assets/wall-torch-hero.webp index a790e58b..63ce96ac 100644 Binary files a/docs/gallery/assets/wall-torch-hero.webp and b/docs/gallery/assets/wall-torch-hero.webp differ diff --git a/docs/gallery/assets/wave-displace-hero.webp b/docs/gallery/assets/wave-displace-hero.webp index ffa6409a..a15156a1 100644 Binary files a/docs/gallery/assets/wave-displace-hero.webp and b/docs/gallery/assets/wave-displace-hero.webp differ diff --git a/docs/gallery/assets/wheelbarrow-hero.webp b/docs/gallery/assets/wheelbarrow-hero.webp index 44cfcb7b..509778c6 100644 Binary files a/docs/gallery/assets/wheelbarrow-hero.webp and b/docs/gallery/assets/wheelbarrow-hero.webp differ diff --git a/docs/gallery/contact-sheets/anvil-contact-sheet.webp b/docs/gallery/contact-sheets/anvil-contact-sheet.webp new file mode 100644 index 00000000..99487a66 Binary files /dev/null and b/docs/gallery/contact-sheets/anvil-contact-sheet.webp differ diff --git a/docs/gallery/contact-sheets/bake-normal-high-to-low-contact-sheet.webp b/docs/gallery/contact-sheets/bake-normal-high-to-low-contact-sheet.webp index 60419763..65a127be 100644 Binary files a/docs/gallery/contact-sheets/bake-normal-high-to-low-contact-sheet.webp and b/docs/gallery/contact-sheets/bake-normal-high-to-low-contact-sheet.webp differ diff --git a/docs/gallery/contact-sheets/campfire-contact-sheet.webp b/docs/gallery/contact-sheets/campfire-contact-sheet.webp new file mode 100644 index 00000000..6decf7fc Binary files /dev/null and b/docs/gallery/contact-sheets/campfire-contact-sheet.webp differ diff --git a/docs/gallery/contact-sheets/collision-hull-proxy-contact-sheet.webp b/docs/gallery/contact-sheets/collision-hull-proxy-contact-sheet.webp index d90a0ab0..36e6de9a 100644 Binary files a/docs/gallery/contact-sheets/collision-hull-proxy-contact-sheet.webp and b/docs/gallery/contact-sheets/collision-hull-proxy-contact-sheet.webp differ diff --git a/docs/gallery/contact-sheets/compositor-glare-contact-sheet.webp b/docs/gallery/contact-sheets/compositor-glare-contact-sheet.webp new file mode 100644 index 00000000..bd1eb689 Binary files /dev/null and b/docs/gallery/contact-sheets/compositor-glare-contact-sheet.webp differ diff --git a/docs/gallery/contact-sheets/custom-normals-shade-contact-sheet.webp b/docs/gallery/contact-sheets/custom-normals-shade-contact-sheet.webp index 29b627f8..b5ad0a09 100644 Binary files a/docs/gallery/contact-sheets/custom-normals-shade-contact-sheet.webp and b/docs/gallery/contact-sheets/custom-normals-shade-contact-sheet.webp differ diff --git a/docs/gallery/contact-sheets/damped-track-aim-contact-sheet.webp b/docs/gallery/contact-sheets/damped-track-aim-contact-sheet.webp new file mode 100644 index 00000000..e6cc3343 Binary files /dev/null and b/docs/gallery/contact-sheets/damped-track-aim-contact-sheet.webp differ diff --git a/docs/gallery/contact-sheets/fence-kit-contact-sheet.webp b/docs/gallery/contact-sheets/fence-kit-contact-sheet.webp new file mode 100644 index 00000000..346afba7 Binary files /dev/null and b/docs/gallery/contact-sheets/fence-kit-contact-sheet.webp differ diff --git a/docs/gallery/contact-sheets/gn-modifier-inputs-contact-sheet.webp b/docs/gallery/contact-sheets/gn-modifier-inputs-contact-sheet.webp index 7b075203..7bc87258 100644 Binary files a/docs/gallery/contact-sheets/gn-modifier-inputs-contact-sheet.webp and b/docs/gallery/contact-sheets/gn-modifier-inputs-contact-sheet.webp differ diff --git a/docs/gallery/contact-sheets/grindstone-contact-sheet.webp b/docs/gallery/contact-sheets/grindstone-contact-sheet.webp new file mode 100644 index 00000000..9e4c8dfc Binary files /dev/null and b/docs/gallery/contact-sheets/grindstone-contact-sheet.webp differ diff --git a/docs/gallery/contact-sheets/hand-pump-contact-sheet.webp b/docs/gallery/contact-sheets/hand-pump-contact-sheet.webp new file mode 100644 index 00000000..b5fedebd Binary files /dev/null and b/docs/gallery/contact-sheets/hand-pump-contact-sheet.webp differ diff --git a/docs/gallery/contact-sheets/hitching-post-contact-sheet.webp b/docs/gallery/contact-sheets/hitching-post-contact-sheet.webp new file mode 100644 index 00000000..720885c9 Binary files /dev/null and b/docs/gallery/contact-sheets/hitching-post-contact-sheet.webp differ diff --git a/docs/gallery/contact-sheets/iron-cauldron-contact-sheet.webp b/docs/gallery/contact-sheets/iron-cauldron-contact-sheet.webp new file mode 100644 index 00000000..15e3766e Binary files /dev/null and b/docs/gallery/contact-sheets/iron-cauldron-contact-sheet.webp differ diff --git a/docs/gallery/contact-sheets/light-link-studio-contact-sheet.webp b/docs/gallery/contact-sheets/light-link-studio-contact-sheet.webp index a80ebe45..e3ac6081 100644 Binary files a/docs/gallery/contact-sheets/light-link-studio-contact-sheet.webp and b/docs/gallery/contact-sheets/light-link-studio-contact-sheet.webp differ diff --git a/docs/gallery/contact-sheets/market-stall-contact-sheet.webp b/docs/gallery/contact-sheets/market-stall-contact-sheet.webp new file mode 100644 index 00000000..b6377487 Binary files /dev/null and b/docs/gallery/contact-sheets/market-stall-contact-sheet.webp differ diff --git a/docs/gallery/contact-sheets/park-bench-contact-sheet.webp b/docs/gallery/contact-sheets/park-bench-contact-sheet.webp new file mode 100644 index 00000000..c71c1830 Binary files /dev/null and b/docs/gallery/contact-sheets/park-bench-contact-sheet.webp differ diff --git a/docs/gallery/contact-sheets/shipping-crate-contact-sheet.webp b/docs/gallery/contact-sheets/shipping-crate-contact-sheet.webp new file mode 100644 index 00000000..4350dd54 Binary files /dev/null and b/docs/gallery/contact-sheets/shipping-crate-contact-sheet.webp differ diff --git a/docs/gallery/contact-sheets/signpost-contact-sheet.webp b/docs/gallery/contact-sheets/signpost-contact-sheet.webp new file mode 100644 index 00000000..f51a2c98 Binary files /dev/null and b/docs/gallery/contact-sheets/signpost-contact-sheet.webp differ diff --git a/docs/gallery/contact-sheets/street-lantern-contact-sheet.webp b/docs/gallery/contact-sheets/street-lantern-contact-sheet.webp new file mode 100644 index 00000000..a202917b Binary files /dev/null and b/docs/gallery/contact-sheets/street-lantern-contact-sheet.webp differ diff --git a/docs/gallery/contact-sheets/tavern-stool-contact-sheet.webp b/docs/gallery/contact-sheets/tavern-stool-contact-sheet.webp new file mode 100644 index 00000000..40deb61d Binary files /dev/null and b/docs/gallery/contact-sheets/tavern-stool-contact-sheet.webp differ diff --git a/docs/gallery/contact-sheets/temp-override-join-contact-sheet.webp b/docs/gallery/contact-sheets/temp-override-join-contact-sheet.webp new file mode 100644 index 00000000..34ca79bb Binary files /dev/null and b/docs/gallery/contact-sheets/temp-override-join-contact-sheet.webp differ diff --git a/docs/gallery/contact-sheets/terrain-scatter-contact-sheet.webp b/docs/gallery/contact-sheets/terrain-scatter-contact-sheet.webp new file mode 100644 index 00000000..099cdccf Binary files /dev/null and b/docs/gallery/contact-sheets/terrain-scatter-contact-sheet.webp differ diff --git a/docs/gallery/contact-sheets/text-version-stamp-contact-sheet.webp b/docs/gallery/contact-sheets/text-version-stamp-contact-sheet.webp index a84de1c3..b48f2a81 100644 Binary files a/docs/gallery/contact-sheets/text-version-stamp-contact-sheet.webp and b/docs/gallery/contact-sheets/text-version-stamp-contact-sheet.webp differ diff --git a/docs/gallery/contact-sheets/treasure-chest-contact-sheet.webp b/docs/gallery/contact-sheets/treasure-chest-contact-sheet.webp new file mode 100644 index 00000000..6ef410bc Binary files /dev/null and b/docs/gallery/contact-sheets/treasure-chest-contact-sheet.webp differ diff --git a/docs/gallery/contact-sheets/vse-cut-list-contact-sheet.webp b/docs/gallery/contact-sheets/vse-cut-list-contact-sheet.webp index 55d8d11b..bb190f12 100644 Binary files a/docs/gallery/contact-sheets/vse-cut-list-contact-sheet.webp and b/docs/gallery/contact-sheets/vse-cut-list-contact-sheet.webp differ diff --git a/docs/gallery/contact-sheets/wall-torch-contact-sheet.webp b/docs/gallery/contact-sheets/wall-torch-contact-sheet.webp new file mode 100644 index 00000000..e8adf93e Binary files /dev/null and b/docs/gallery/contact-sheets/wall-torch-contact-sheet.webp differ diff --git a/docs/gallery/contact-sheets/watchtower-contact-sheet.webp b/docs/gallery/contact-sheets/watchtower-contact-sheet.webp new file mode 100644 index 00000000..8a2ddd29 Binary files /dev/null and b/docs/gallery/contact-sheets/watchtower-contact-sheet.webp differ diff --git a/docs/gallery/contact-sheets/water-trough-contact-sheet.webp b/docs/gallery/contact-sheets/water-trough-contact-sheet.webp new file mode 100644 index 00000000..783523f5 Binary files /dev/null and b/docs/gallery/contact-sheets/water-trough-contact-sheet.webp differ diff --git a/docs/gallery/contact-sheets/wave-displace-contact-sheet.webp b/docs/gallery/contact-sheets/wave-displace-contact-sheet.webp new file mode 100644 index 00000000..d81131e6 Binary files /dev/null and b/docs/gallery/contact-sheets/wave-displace-contact-sheet.webp differ diff --git a/docs/gallery/contact-sheets/wheelbarrow-contact-sheet.webp b/docs/gallery/contact-sheets/wheelbarrow-contact-sheet.webp new file mode 100644 index 00000000..8081dd44 Binary files /dev/null and b/docs/gallery/contact-sheets/wheelbarrow-contact-sheet.webp differ diff --git a/docs/gallery/contact-sheets/wooden-barrel-contact-sheet.webp b/docs/gallery/contact-sheets/wooden-barrel-contact-sheet.webp new file mode 100644 index 00000000..92f6964c Binary files /dev/null and b/docs/gallery/contact-sheets/wooden-barrel-contact-sheet.webp differ diff --git a/docs/gallery/contact-sheets/wooden-bucket-contact-sheet.webp b/docs/gallery/contact-sheets/wooden-bucket-contact-sheet.webp new file mode 100644 index 00000000..2e9b519f Binary files /dev/null and b/docs/gallery/contact-sheets/wooden-bucket-contact-sheet.webp differ diff --git a/docs/gallery/contact-sheets/wooden-ladder-contact-sheet.webp b/docs/gallery/contact-sheets/wooden-ladder-contact-sheet.webp new file mode 100644 index 00000000..d1701705 Binary files /dev/null and b/docs/gallery/contact-sheets/wooden-ladder-contact-sheet.webp differ diff --git a/examples/bake-normal-high-to-low/preview.webp b/examples/bake-normal-high-to-low/preview.webp index 54c1dc88..be76ca54 100644 Binary files a/examples/bake-normal-high-to-low/preview.webp and b/examples/bake-normal-high-to-low/preview.webp differ diff --git a/examples/collision-hull-proxy/preview.webp b/examples/collision-hull-proxy/preview.webp index 8e781f60..b8564e8d 100644 Binary files a/examples/collision-hull-proxy/preview.webp and b/examples/collision-hull-proxy/preview.webp differ diff --git a/examples/compositor-glare/preview.webp b/examples/compositor-glare/preview.webp index 26a1ff4b..f5eae363 100644 Binary files a/examples/compositor-glare/preview.webp and b/examples/compositor-glare/preview.webp differ diff --git a/examples/custom-normals-shade/preview.webp b/examples/custom-normals-shade/preview.webp index 96977767..8953e5f5 100644 Binary files a/examples/custom-normals-shade/preview.webp and b/examples/custom-normals-shade/preview.webp differ diff --git a/examples/damped-track-aim/preview.webp b/examples/damped-track-aim/preview.webp index 4a9b3145..bf8ee317 100644 Binary files a/examples/damped-track-aim/preview.webp and b/examples/damped-track-aim/preview.webp differ diff --git a/examples/gn-modifier-inputs/preview.webp b/examples/gn-modifier-inputs/preview.webp index dfc19c7c..ddf547f8 100644 Binary files a/examples/gn-modifier-inputs/preview.webp and b/examples/gn-modifier-inputs/preview.webp differ diff --git a/examples/light-link-studio/preview.webp b/examples/light-link-studio/preview.webp index 9b8d8dbd..49ad31b9 100644 Binary files a/examples/light-link-studio/preview.webp and b/examples/light-link-studio/preview.webp differ diff --git a/examples/temp-override-join/preview.webp b/examples/temp-override-join/preview.webp index 0ba9b3a2..b5234a56 100644 Binary files a/examples/temp-override-join/preview.webp and b/examples/temp-override-join/preview.webp differ diff --git a/examples/text-version-stamp/preview.webp b/examples/text-version-stamp/preview.webp index 315e3608..6b65c980 100644 Binary files a/examples/text-version-stamp/preview.webp and b/examples/text-version-stamp/preview.webp differ diff --git a/examples/vse-cut-list/preview.webp b/examples/vse-cut-list/preview.webp index 3458671e..e0d96577 100644 Binary files a/examples/vse-cut-list/preview.webp and b/examples/vse-cut-list/preview.webp differ diff --git a/examples/wave-displace/preview.webp b/examples/wave-displace/preview.webp index fa11060e..60750a36 100644 Binary files a/examples/wave-displace/preview.webp and b/examples/wave-displace/preview.webp differ diff --git a/scripts/contact_sheet.py b/scripts/contact_sheet.py new file mode 100644 index 00000000..7622b7d5 --- /dev/null +++ b/scripts/contact_sheet.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +"""Build a gallery contact sheet and print the numbers its verdict reports. + +Composites a candidate hero beside the pinned calibration set (canonical +membership: CLAUDE.md, Quality Gates) in the committed format: four 640x360 +tiles, candidate first, captioned, on the dark sheet background, written to +``docs/gallery/contact-sheets/-contact-sheet.webp``. + +It also prints what the gate's verdict needs measured: mean luminance +(Rec. 709) of each tile, the stage (mean luminance of the frame border, where +the backdrop is), the wedge warmth (red minus blue over the brightest tenth +of the lower half, where the warm floor pool sits), and mean saturation. The +verdict itself - "is this sortable as the odd one out?" - stays a judgment +made by looking at the sheet; this only makes the numbers repeatable. + +Authoring tool: host Python with Pillow and numpy. + +Usage: + python scripts/contact_sheet.py NAME [NAME ...] + +Exit codes: 0 written, 2 usage. +""" +from __future__ import annotations + +import sys + +import numpy as np +from PIL import Image, ImageDraw + +from measure_hero_drift import REPO, entries + +CALIBRATION = ("armature-bend", "damped-track-aim", "bmesh-gear") +TILE = (640, 360) +GAP, CAPTION = 10, 32 +BG = (22, 23, 26) +TEXT = (150, 152, 160) +OUT = REPO / "docs" / "gallery" / "contact-sheets" + + +def metrics(im: Image.Image) -> dict: + a = np.asarray(im.convert("RGB"), dtype=np.float64) / 255.0 + luma = a @ np.array([0.2126, 0.7152, 0.0722]) + h, w = luma.shape + b = max(1, h // 12) + border = np.concatenate([luma[:b].ravel(), luma[-b:].ravel(), + luma[:, :b].ravel(), luma[:, -b:].ravel()]) + lower = a[h // 2:].reshape(-1, 3) + lower_l = luma[h // 2:].ravel() + hot = lower[lower_l >= np.quantile(lower_l, 0.9)] + mx, mn = a.max(axis=2), a.min(axis=2) + sat = np.where(mx > 0, (mx - mn) / np.maximum(mx, 1e-9), 0.0) + return { + "luma": float(luma.mean()), + "stage": float(border.mean()), + "warmth": float((hot[:, 0] - hot[:, 2]).mean()), + "sat": float(sat.mean()), + } + + +def build(name: str, heroes: dict[str, str]) -> list[tuple[str, dict]]: + names = [name] + [c for c in CALIBRATION if c != name] + tw, th = TILE + sheet = Image.new("RGB", (GAP + len(names) * (tw + GAP), GAP + th + CAPTION), BG) + draw = ImageDraw.Draw(sheet) + rows = [] + for i, n in enumerate(names): + im = Image.open(REPO / heroes[n]).convert("RGB") + x = GAP + i * (tw + GAP) + sheet.paste(im.resize(TILE, Image.LANCZOS), (x, GAP)) + label = f"{n} <- candidate" if i == 0 else f"{n} (calibration)" + draw.text((x + 4, GAP + th + 10), label, fill=TEXT) + rows.append((label, metrics(im))) + OUT.mkdir(parents=True, exist_ok=True) + sheet.save(OUT / f"{name}-contact-sheet.webp", "WEBP", quality=90) + return rows + + +def main(argv=None) -> int: + argv = sys.argv[1:] if argv is None else argv + if not argv: + print(__doc__.split("Usage:")[1].strip(), file=sys.stderr) + return 2 + heroes = {e["name"]: e["hero"] for e in entries()} + unknown = [n for n in argv if n not in heroes] + if unknown: + print(f"unknown entries: {', '.join(unknown)}", file=sys.stderr) + return 2 + for name in argv: + rows = build(name, heroes) + print(f"\n{name} -> docs/gallery/contact-sheets/{name}-contact-sheet.webp") + print(f" {'tile':34} {'luma':>6} {'stage':>6} {'warmth':>7} {'sat':>6}") + for label, m in rows: + print(f" {label:34} {m['luma']:6.4f} {m['stage']:6.4f} " + f"{m['warmth']:+7.4f} {m['sat']:6.4f}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/render_hero.py b/scripts/render_hero.py new file mode 100644 index 00000000..a3b42997 --- /dev/null +++ b/scripts/render_hero.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +"""Regenerate gallery hero and preview webps from the code, one committed path. + +For each named entry in ``examples/gallery.json`` / ``showcase/gallery.json``: + +1. render the README's documented ``--output`` command to a lossless PNG, + keeping every other documented flag (``--engine cycles`` ...); +2. write the hero, ``docs/gallery/assets/-hero.webp`` at 1280x720; +3. write the preview at the entry's ``preview`` path, 1200x675 (Lanczos). + +Both are webp quality 90, the setting every showcase script uses when it +writes webp itself. Before this file the PNG -> webp step lived in uncommitted +scratch scripts, so nobody could regenerate a still and get the committed +result (#200). The script's own ``--output`` render path runs the framing +(exit 10) and asset-quality (exit 11) gates before it writes, so a +non-zero exit here leaves the committed files untouched. + +Authoring tool, not CI: host Python with Pillow; one full render per entry. + +Usage: + python scripts/render_hero.py --blender PATH --only NAME [--only NAME ...] + +Exit codes: 0 all written, 2 usage, 3 at least one render failed. +""" +from __future__ import annotations + +import argparse +import subprocess +import sys +import tempfile +from pathlib import Path + +from PIL import Image + +from measure_hero_drift import REPO, entries, render_args + +HERO_SIZE = (1280, 720) +PREVIEW_SIZE = (1200, 675) +QUALITY = 90 + + +def main(argv=None) -> int: + p = argparse.ArgumentParser(description=__doc__.split("\n")[0]) + p.add_argument("--blender", required=True, help="Blender binary to render with") + p.add_argument("--only", action="append", required=True, + help="entry to regenerate (repeatable)") + args = p.parse_args(argv) + + blender = str(Path(args.blender).resolve()) + version = subprocess.run([blender, "--version"], capture_output=True, text=True, + encoding="utf-8", errors="replace").stdout.splitlines()[0] + print(f"# renderer: {blender}\n# version: {version}", flush=True) + + by_name = {e["name"]: e for e in entries()} + unknown = [n for n in args.only if n not in by_name] + if unknown: + print(f"unknown entries: {', '.join(unknown)}", file=sys.stderr) + return 2 + + failed = [] + with tempfile.TemporaryDirectory() as tmp: + for name in args.only: + e = by_name[name] + png = Path(tmp) / f"{name}.png" + cmd = render_args(e, png) + if cmd is None: + print(f"{name}: no --output command in README", flush=True) + failed.append(name) + continue + script = next((REPO / e["dir"]).glob("*.py")) + proc = subprocess.run( + [blender, "--background", "--factory-startup", "--python", str(script), + "--", *cmd], + capture_output=True, text=True, encoding="utf-8", errors="replace", cwd=REPO, + ) + if proc.returncode != 0 or not png.is_file(): + tail = (proc.stdout + proc.stderr).strip().splitlines()[-3:] + print(f"{name}: render failed, exit {proc.returncode}: {' | '.join(tail)}", + flush=True) + failed.append(name) + continue + im = Image.open(png).convert("RGB") + if im.size != HERO_SIZE: + print(f"{name}: render is {im.size[0]}x{im.size[1]}, expected " + f"{HERO_SIZE[0]}x{HERO_SIZE[1]}", flush=True) + failed.append(name) + continue + hero, preview = REPO / e["hero"], REPO / e["preview"] + im.save(hero, "WEBP", quality=QUALITY) + im.resize(PREVIEW_SIZE, Image.LANCZOS).save(preview, "WEBP", quality=QUALITY) + print(f"{name}: wrote {e['hero']} ({hero.stat().st_size} B) and " + f"{e['preview']} ({preview.stat().st_size} B)", flush=True) + + print(f"\n# {len(args.only) - len(failed)} regenerated, {len(failed)} failed") + return 3 if failed else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/showcase/anvil/preview.webp b/showcase/anvil/preview.webp index 61f49b02..c5fb1520 100644 Binary files a/showcase/anvil/preview.webp and b/showcase/anvil/preview.webp differ diff --git a/showcase/campfire/preview.webp b/showcase/campfire/preview.webp index 106ae2f3..307046ab 100644 Binary files a/showcase/campfire/preview.webp and b/showcase/campfire/preview.webp differ diff --git a/showcase/fence-kit/preview.webp b/showcase/fence-kit/preview.webp index 66ae37f5..7a710a26 100644 Binary files a/showcase/fence-kit/preview.webp and b/showcase/fence-kit/preview.webp differ diff --git a/showcase/grindstone/preview.webp b/showcase/grindstone/preview.webp index 90a7ba37..789cbc88 100644 Binary files a/showcase/grindstone/preview.webp and b/showcase/grindstone/preview.webp differ diff --git a/showcase/hand-pump/preview.webp b/showcase/hand-pump/preview.webp index 06e69f45..433b95b0 100644 Binary files a/showcase/hand-pump/preview.webp and b/showcase/hand-pump/preview.webp differ diff --git a/showcase/iron-cauldron/preview.webp b/showcase/iron-cauldron/preview.webp index 1ce8a2ae..e10ba3f0 100644 Binary files a/showcase/iron-cauldron/preview.webp and b/showcase/iron-cauldron/preview.webp differ diff --git a/showcase/park-bench/preview.webp b/showcase/park-bench/preview.webp index c17c89b4..443a36fc 100644 Binary files a/showcase/park-bench/preview.webp and b/showcase/park-bench/preview.webp differ diff --git a/showcase/shipping-crate/preview.webp b/showcase/shipping-crate/preview.webp index 379ecee8..658045f5 100644 Binary files a/showcase/shipping-crate/preview.webp and b/showcase/shipping-crate/preview.webp differ diff --git a/showcase/signpost/preview.webp b/showcase/signpost/preview.webp index 3048328b..c325d82e 100644 Binary files a/showcase/signpost/preview.webp and b/showcase/signpost/preview.webp differ diff --git a/showcase/tavern-stool/preview.webp b/showcase/tavern-stool/preview.webp index e466bed7..ecfa8df0 100644 Binary files a/showcase/tavern-stool/preview.webp and b/showcase/tavern-stool/preview.webp differ diff --git a/showcase/terrain-scatter/preview.webp b/showcase/terrain-scatter/preview.webp index 8399ff29..8e728664 100644 Binary files a/showcase/terrain-scatter/preview.webp and b/showcase/terrain-scatter/preview.webp differ diff --git a/showcase/treasure-chest/preview.webp b/showcase/treasure-chest/preview.webp index 449a1c53..51be3b28 100644 Binary files a/showcase/treasure-chest/preview.webp and b/showcase/treasure-chest/preview.webp differ diff --git a/showcase/wall-torch/preview.webp b/showcase/wall-torch/preview.webp index 438de785..c9559de6 100644 Binary files a/showcase/wall-torch/preview.webp and b/showcase/wall-torch/preview.webp differ diff --git a/showcase/wheelbarrow/preview.webp b/showcase/wheelbarrow/preview.webp index 89e4a3df..09c62574 100644 Binary files a/showcase/wheelbarrow/preview.webp and b/showcase/wheelbarrow/preview.webp differ