Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/blender-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -576,14 +576,14 @@ jobs:
# 1e-6, shell bbox == declared tile (dev 0), manifold shell except
# the 16 open-end edges, watertight detail parts contained inside
# the tile, and reuse hygiene (identity scales, kit names, pivot
# origins). Exits non-zero on failure.
# origins) across 21 parts. Exits non-zero on failure.
xvfb-run -a "$BLENDER" --background \
--python examples/modular-kit-snap/modular_kit_snap.py --

- name: Shipped example - lightmap UV channel (two-layer bake contract)
run: |
set -euo pipefail
# Check only (no render): a 9-part market cart; asserts per-part
# Check only (no render): a 21-part market cart; asserts per-part
# UVMap/UVLight layers with active == UVLight and
# active_render == UVMap re-asserted after the ops (they clear
# both flags), UV0 loops untouched by the UV1 unwrap (drift 0),
Expand Down
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,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. Documented framing deviations (VISUAL-STYLE Layer 1) call `check_framing(..., deviation="reason")` — the helper reports instead of enforcing, and the reason string is required.
- **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.
- **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 `<img alt>` text and witnesses callouts in `docs/gallery/index.html` and `docs/gallery/<name>/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).
- **Playwright gallery captures:** gallery `<img>` tags lazy-load, and Chromium scroll-anchoring defeats `scrollTo(0, 0)` after images load — take full-page captures and crop locally, not scroll-then-shoot.
Expand Down
21 changes: 19 additions & 2 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,8 @@ Not committed; target list for the next content version. (v0.3.0 shipped the smo
- UV-handle lifetime snippet: re-fetch attribute/UV layers by name after any CustomData-reallocating call (`calc_tangents`, `VertexGroup.add`, modifier edits) — held handles dangle silently on 4.5, survive by luck on 5.1 (found authoring `triangulate-tangents`; **confirmed harder authoring `lightmap-uv-channel`: iterating a held `MeshUVLoopLayer.data` after edit-mode UV ops segfaults 4.5.11 headless, EXCEPTION_ACCESS_VIOLATION 5/5, survives on 5.1.2**)
- ~~glTF skinned-mesh export witness: follow-up to `gltf-export-roundtrip` + `vertex-weight-limit`~~ **SHIPPED** as `examples/gltf-skin-roundtrip/` — `skins[0].joints` names every bone, JOINTS_0/WEIGHTS_0 with unit sums (3e-8), weights bit-exact, rest matrices to 2.4e-07, deformation to 4.8e-07; exporter welds duplicate loops (sorted-multiset comparisons mispair — compare by rest-key); unparented skinned meshes make the exporter bind an armature by name
- ~~Degenerate-bevel weld hazard~~ **SHIPPED** as `examples/degenerate-bevel-weld/` — at `offset == min_dim/2` the bevel band collapses to exactly 12 zero-area faces (4 min-axis edges × 3 segments) with 16 coincident verts (4 × (segments+1)) and min_area down 3.2e5×; a stdlib GLB re-parse counts **32 degenerate triangles crossing the export boundary** (MEASURED regression constant) versus 0 for the safe mesh — the loops an engine merge-by-distance welds; byte-identical on 4.5.11 and 5.1.2
- ~~Modular kit snap witness~~ **SHIPPED** as `examples/modular-kit-snap/` — tiling corridor segment: 16 boundary verts on `x ∈ {0, TILE}` within 1e-6 m, end rings coincident under the tile offset (nearest-key match), linked-duplicate joint deviation 0.0, shell bbox == declared tile (dev 0.0), manifold except the 16 open-end edges, 12 detail parts contained inside the tile; unsnapped probe (3 mm skew, 2 mm nudge) fails at exits 3/4 with measured errors; sorted-zip ring matching mispairs displaced verts — match by nearest key (authoring hazard, fixed); byte-identical on 4.5.11 and 5.1.2
- ~~Lightmap UV channel witness~~ **SHIPPED** as `examples/lightmap-uv-channel/` — two-channel UV contract on a market cart: per-part `UVMap`/`UVLight` with `active`/`active_render` re-asserted after the ops (edit-mode UV ops clear both flags; `uv_layers.new()` never moves them), UV0 drift 0.0 (clobber trap measured 2.068), every UV1 loop in [0,1], 0 overlapping islands via independent binned strict-SAT (15 hits when falsified), min island distance 0.00401 vs nominal margin 0.002 (`MARGIN_DIV × 0.01` semantics measured live); held `MeshUVLoopLayer.data` after edit-mode UV ops segfaults 4.5.11 (5/5, EXCEPTION_ACCESS_VIOLATION), silent no-op pack without prior unwrap; byte-identical on 4.5.11 and 5.1.2 (1114 islands)
- ~~Modular kit snap witness~~ **SHIPPED** as `examples/modular-kit-snap/` — tiling corridor segment: 16 boundary verts on `x ∈ {0, TILE}` within 1e-6 m, end rings coincident under the tile offset (nearest-key match), linked-duplicate joint deviation 0.0, shell bbox == declared tile (dev 0.0), manifold except the 16 open-end edges, 20 detail parts contained inside the tile; unsnapped probe (3 mm skew, 2 mm nudge) fails at exits 3/4 with measured errors; sorted-zip ring matching mispairs displaced verts — match by nearest key (authoring hazard, fixed); byte-identical on 4.5.11 and 5.1.2
- ~~Lightmap UV channel witness~~ **SHIPPED** as `examples/lightmap-uv-channel/` — two-channel UV contract on a market cart: per-part `UVMap`/`UVLight` with `active`/`active_render` re-asserted after the ops (edit-mode UV ops clear both flags; `uv_layers.new()` never moves them), UV0 drift 0.0 (clobber trap measured 2.068), every UV1 loop in [0,1], 0 overlapping islands via independent binned strict-SAT (15 hits when falsified), min island distance 0.00401 vs nominal margin 0.002 (`MARGIN_DIV × 0.01` semantics measured live); held `MeshUVLoopLayer.data` after edit-mode UV ops segfaults 4.5.11 (5/5, EXCEPTION_ACCESS_VIOLATION), silent no-op pack without prior unwrap; byte-identical on 4.5.11 and 5.1.2 (3680 islands)
- UV atlas **utilization** witness — coverage/wasted-texel closed forms for a packed lightmap atlas (the non-overlap, unit-square, margin, and active/active_render contracts shipped in `lightmap-uv-channel`; utilization is the remaining unbuilt slice of the old "UV atlas pack" candidate)
- ~~GAMMA_CROSS blend-curve witness~~ **SHIPPED** as `examples/vse-gamma-cross/` — the cross blends in a gamma-0.5 space: `((1-t)·√A + t·√B)²` with `t = (frame − start)/duration`, never 1 inside the effect; mid-cross dips 0.115 below the sRGB lerp from crimson/teal (closed form (0.341, 0.349, 0.463) confirmed per frame); AgX-default sampling poisons the fit (0.146 red-channel error, `view_transform='Standard'` mandatory); deleting a consumed input orphans-and-deletes the effect — follow-up to `vse-cut-list`
- Falsy `bpy_prop_collection` trap snippet: an empty collection is falsy, so `editor.strips or editor.sequences` silently falls through to the legacy accessor on an empty timeline — always branch on `hasattr`; likely generalizes across the API (found authoring `vse-cut-list`)
Expand All @@ -131,6 +131,23 @@ Not committed; target list for the next content version. (v0.3.0 shipped the smo

- ~~Gallery framing recomposition worklist~~ **DRAINED**: all four recomposition cases restaged and gated with `check_framing` — depsgraph-export (0.984x/touch→0.812x; cube+dome recentered, aim-based camera), vertex-weight-limit (touch B→0.867y; more frontal camera, lower aim, base in frame), triangulate-tangents (0.334x→0.833y; 2x-class move-in plus Key/Glint trim that also cleared a pre-existing dome blowout), text-version-stamp (0.663x→0.719x; moved in with aim on the stamp center, dead third gone). The seven marginal cases were camera-nudged in the preceding sweep; deviations documented in #108 (damped-track-aim, wave-displace); sky-texture-sun-elevation is a measurement artifact (world subject — report-only if wired). Remaining: nothing on the framing worklist — future examples inherit the gate at authoring time.

## Asset-quality survey worklist (measured 2026-07-24, `gallery_asset_quality` floors)

Full-gallery floor survey after the gate landed (`feat/asset-quality-gate`).
26 of 42 examples pass all floors. The 14 below-floor entries, with
measured numbers — every one is a **contract-vehicle or display subject**
(testcard TV, VSE monitors, text bars, swatch/display rigs, single honest
primitives), not a game asset meant for reuse, so none is a remodeling
mandate; the floors exist to catch future asset-type examples at authoring
time. Two non-mesh subjects (curve-bevel-arc, grease-pencil-rosette) are
not measurable by the floors (curve/GP data, no mesh hero). Re-survey after
any example remodel.

- naming (default datablock names): gltf-skin-roundtrip (3 parts), gn-sdf-remesh (`Torus`), turntable (`Suzanne`)
- materials (multi-part, single material): armature-bend (3 parts/1 mat — vertex-color weight bands are the honest display), degenerate-bevel-weld (2/1), driver-wave (16/1, also edge90 1.0), shape-key-blend (3/1, also edge90 1.0)
- edge90 (raw right angles, 1.0): depsgraph-export (also naming + materials), image-pixels-testcard, png-exr-alpha, temp-override-join, text-version-stamp, vse-cut-list; vse-gamma-cross at 0.92
- reference: gallery best-modeled measure collision-hull-proxy 0.044, custom-normals-shade 0.288, vertex-weight-limit 0.150, lod-decimate-chain 0.019; calibration details in `docs/VISUAL-STYLE.md` § Asset quality

## Future (uncommitted)

- Asset library and asset browser scripting skill
Expand Down
79 changes: 79 additions & 0 deletions docs/VISUAL-STYLE.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,85 @@ the default stage.
(`vse-cut-list` on a monitor, `image-pixels-testcard` on a TV) rather than
recreating them.

## Asset quality (measured gate)

"Modeled with intent" is measured, not a vibe. Three parts: measurable
floors, a named reference bar, and the asset sheet. The floors are the
cheap filter that catches the obvious failures; the asset sheet is what
actually decides. Applies to asset-type examples (game props, kits) —
not to world, field, art, or media subjects.

### Floors

Computed by `examples/gallery_asset_quality.py` (render path only, same
call pattern as `gallery_framing`; prints measured values; exit 11 on
violation; never imported by a check-only path):

- **Naming** — no default datablock names on hero parts (Cube, Sphere,
Torus, Suzanne, …). A designed asset names its parts.
- **Material variation** — a hero assembled from ≥ 2 parts carries ≥ 2
distinct materials. A single flat Principled slot across an entire prop
is this gallery's most reliable placeholder predictor. Single-part
subjects are exempt: one honest material is their truth (a brass gear).
- **Edge treatment** — at most **75 %** of a hero's manifold edges may be
right angles (90° ± 2°). Unbroken 90° corners across the board means no
bevel, no chamfer, no shading break. Honest machined teeth pass
(`bmesh-gear` measures 0.667); raw boxes (1.0) fail.

Calibrated empirically against the gallery's own assets, exactly as the
0.02 margin floor was:

| asset | parts | materials | edge90 | compactness (info only) |
| --- | --- | --- | --- | --- |
| collision-hull-proxy (reference) | 16 | 6 | 0.044 | 65.9 |
| custom-normals-shade (reference) | 39 | 2 | 0.288 | 31.0 |
| vertex-weight-limit (reference) | 1 | 4 | 0.150 | 34.8 |
| lod-decimate-chain (reference) | 3 | 4 | 0.019 | 79.1 |
| depsgraph-export (weak) | 2 | 1 | 1.000 | 23.9 |
| text-version-stamp (weak) | 1 | 1 | 1.000 | 405.3 |
| bmesh-gear (simple, honest) | 1 | 1 | 0.667 | 18.3 |
| soccer-ball-goldberg (simple, honest) | 1 | 2 | 0.000 | 10.0 |

Dropped floors, with the evidence that killed them — a floor that cannot
be set without failing a genuinely good asset is not a floor, and a floor
hand-tuned to pass everything is not one either:

- **Part count** — `vertex-weight-limit`'s mech arm is a single skinned
mesh and is reference-quality. Skinned deform meshes are one mesh by
design. The naming half survives.
- **Silhouette compactness** — `soccer-ball-goldberg` scores 10.0,
`bmesh-gear` 18.3, `turntable` 19.1: genuinely good simple subjects
with low-complexity silhouettes. Measured and printed as information.
- **Dominant-material share** — `custom-normals-shade`'s jerry can and
`lod-decimate-chain`'s rocket carry their accents in per-face
assignment, invisible to a per-part share (both measure 1.0 dominant).
Printed as information only.

### The Goodhart warning

These floors are necessary, not sufficient. Bolting meaningless greebles
onto a box to raise silhouette complexity satisfies every number above
and still fails. Detail must serve the object's plausibility: fixtures
where fixtures belong, wear where contact happens, structure where
structure carries load. The asset sheet exists because metrics can be
gamed; a lineup of peers cannot.

### Reference bar

The asset-quality reference set is pinned in `CLAUDE.md` § Quality Gates
for Example Runs — canonical membership lives there, not here.

### Asset sheet gate

For every new asset-type example: render the hero alone — neutral
three-quarter view, plain studio lighting, no staging tricks, no labels,
no comparison props — composite it beside the reference assets rendered
the same way, commit under `docs/gallery/asset-sheets/`, and link it in
the PR body with a verdict. The asset ships only if it is not
identifiable as the least-designed object in that lineup. Isolating the
asset from its staging is the point: a strong scene can carry a weak
model, and this gate removes the scene.

## Output

- Render 1280×720 PNG (`taa_render_samples`/`cycles.samples` 32–64).
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/gallery/assets/lightmap-uv-channel-hero.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/gallery/assets/modular-kit-snap-hero.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/gallery/contact-sheets/lightmap-uv-channel.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/gallery/contact-sheets/modular-kit-snap.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion docs/gallery/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -780,7 +780,7 @@ <h2><a href="modular-kit-snap/">modular-kit-snap</a></h2>
<div class="card-body">
<h2><a href="lightmap-uv-channel/">lightmap-uv-channel</a></h2>
<p class="teaches">A market cart carrying the two-channel UV contract for baked lighting: UV0 untouched, UVLight packed with no overlaps and a respected margin.</p>
<p class="witnesses"><span class="tag">witnesses</span> Per-part UVMap/UVLight with active vs active_render pinned (ops clear both flags), UV0 drift 0.0, 1114 islands with 0 SAT overlaps, min island distance 0.00401.</p>
<p class="witnesses"><span class="tag">witnesses</span> Per-part UVMap/UVLight with active vs active_render pinned (ops clear both flags), UV0 drift 0.0, 3680 islands with 0 SAT overlaps, min island distance 0.00401.</p>
<a class="card-link" href="lightmap-uv-channel/">View example <span aria-hidden="true">&rarr;</span></a>
</div>
</article>
Expand Down
Loading
Loading