Skip to content

Commit cc1ccb8

Browse files
authored
feat: asset-quality gate + remodel modular-kit-snap and lightmap-uv-channel assets (#117)
* feat: add asset-quality gate for gallery examples 'Designed, not placeholder' becomes a measured gate: three floors in examples/gallery_asset_quality.py (naming, material variation scoped to assembled heroes, right-angle edge fraction <= 0.75), a pinned reference bar (CLAUDE.md canonical membership: collision-hull-proxy, custom-normals-shade, vertex-weight-limit, lod-decimate-chain), and the asset sheet (hero alone, neutral 3/4 studio, composited beside the reference set, committed under docs/gallery/asset-sheets/). Floors calibrated empirically against the gallery's best and weakest; three candidates dropped with evidence: part count (vertex-weight-limit's skinned mech arm is one mesh and reference-quality), silhouette compactness (soccer-ball-goldberg 10.0, bmesh-gear 18.3 are good simple subjects), dominant-material share (jerry can and rocket carry accents in per-face assignment, both measure 1.0). Floor falsification: raw cube and single-material assembly exit 11, designed restore exits 0. Goodhart warning written into VISUAL-STYLE: greebles for the metric's sake do not count. Signed-off-by: fOuttaMyPaint <TMhospitalitystrategies@gmail.com> * feat: remodel lightmap-uv-channel cart under the asset-quality gate The shipped cart read as placeholder: uniform tan (its DarkWood wheel material was silently never applied - a prefix lookup bug, replaced with exact per-part mapping), wheels as lumpy polygons with a black dot for a hub, no fittings. Rebuilt as an assembly of 21 named watertight parts: plank-grooved bed with side rails, corner brackets, and fasteners, real wheel assemblies (disc + hub + iron band + bolt ring) on a capped axle, and a ribbed canvas canopy. Atlas board enlarged for card-scale legibility. Render path now gates through gallery_asset_quality (exit 11): 5 materials, edge90 0.069, naming clean. Closed forms moved deliberately, contracts unchanged: parts 9 -> 21, islands 1114 -> 3680 (byte-identical on 5.1.2 and 4.5.11; the MeshUVLoopLayer re-fetch ordering that avoids the documented 4.5.11 segfault is preserved and verified). Re-falsified: clobber drift 2.068, overlap 15 SAT hits, margin collapse 2e-5. Signed-off-by: fOuttaMyPaint <TMhospitalitystrategies@gmail.com> * feat: remodel modular-kit-snap panels and palette under the gate Three defects fixed: flat grey wall panels are now assemblies (backing plate + inset panel + four bolt heads each), the palette shifted teal-slate so the card no longer twins with lightmap-uv-channel's warm cart, and mean luminance was brought into the calibration range (82.7 -> 66.1, ceiling 77.7) by dimming fixtures/spill/rim - the interior run no longer reads as a fourth over-bright card, so only the documented framing deviation remains. Detail parts 12 -> 20 (hygiene 13 -> 21), boundary contract untouched (16 verts/edges, probes still exit 3/4 with measured errors), materials now cached per design key instead of duplicated per part per segment. Signed-off-by: fOuttaMyPaint <TMhospitalitystrategies@gmail.com> * chore: integrate asset-quality gate - survey worklist, sheets, regenerated pages ROADMAP carries the full-gallery floor survey: 26 of 42 pass; the 14 below-floor entries are all contract-vehicle/display subjects with measured numbers, none a game asset, none a remodeling mandate. Asset sheets for both remodeled examples committed under docs/gallery/asset-sheets/ (before/after beside the reference set). Staging contact sheets refreshed for both heroes. gallery.json, blender-smoke comments, and generated gallery pages updated for the moved closed forms (21 parts/3680 islands; 20 detail/21 hygiene parts). Signed-off-by: fOuttaMyPaint <TMhospitalitystrategies@gmail.com> --------- Signed-off-by: fOuttaMyPaint <TMhospitalitystrategies@gmail.com>
1 parent b7080aa commit cc1ccb8

21 files changed

Lines changed: 945 additions & 139 deletions

.github/workflows/blender-smoke.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -576,14 +576,14 @@ jobs:
576576
# 1e-6, shell bbox == declared tile (dev 0), manifold shell except
577577
# the 16 open-end edges, watertight detail parts contained inside
578578
# the tile, and reuse hygiene (identity scales, kit names, pivot
579-
# origins). Exits non-zero on failure.
579+
# origins) across 21 parts. Exits non-zero on failure.
580580
xvfb-run -a "$BLENDER" --background \
581581
--python examples/modular-kit-snap/modular_kit_snap.py --
582582
583583
- name: Shipped example - lightmap UV channel (two-layer bake contract)
584584
run: |
585585
set -euo pipefail
586-
# Check only (no render): a 9-part market cart; asserts per-part
586+
# Check only (no render): a 21-part market cart; asserts per-part
587587
# UVMap/UVLight layers with active == UVLight and
588588
# active_render == UVMap re-asserted after the ops (they clear
589589
# both flags), UV0 loops untouched by the UV1 unwrap (drift 0),

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ Stage with **explicit paths only** — never `git add -A` or `git add .`. Cursor
124124
- `docs/VISUAL-STYLE.md` is the **binding** render standard; deviations are defects.
125125
- **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.
126126
- **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.
127+
- **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.
127128
- **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.
128129
- **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).
129130
- **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.

ROADMAP.md

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,8 @@ Not committed; target list for the next content version. (v0.3.0 shipped the smo
109109
- 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**)
110110
- ~~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
111111
- ~~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
112-
- ~~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
113-
- ~~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)
112+
- ~~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
113+
- ~~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)
114114
- 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)
115115
- ~~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`
116116
- 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`)
@@ -131,6 +131,23 @@ Not committed; target list for the next content version. (v0.3.0 shipped the smo
131131

132132
- ~~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.
133133

134+
## Asset-quality survey worklist (measured 2026-07-24, `gallery_asset_quality` floors)
135+
136+
Full-gallery floor survey after the gate landed (`feat/asset-quality-gate`).
137+
26 of 42 examples pass all floors. The 14 below-floor entries, with
138+
measured numbers — every one is a **contract-vehicle or display subject**
139+
(testcard TV, VSE monitors, text bars, swatch/display rigs, single honest
140+
primitives), not a game asset meant for reuse, so none is a remodeling
141+
mandate; the floors exist to catch future asset-type examples at authoring
142+
time. Two non-mesh subjects (curve-bevel-arc, grease-pencil-rosette) are
143+
not measurable by the floors (curve/GP data, no mesh hero). Re-survey after
144+
any example remodel.
145+
146+
- naming (default datablock names): gltf-skin-roundtrip (3 parts), gn-sdf-remesh (`Torus`), turntable (`Suzanne`)
147+
- 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)
148+
- 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
149+
- 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
150+
134151
## Future (uncommitted)
135152

136153
- Asset library and asset browser scripting skill

docs/VISUAL-STYLE.md

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,85 @@ the default stage.
103103
(`vse-cut-list` on a monitor, `image-pixels-testcard` on a TV) rather than
104104
recreating them.
105105

106+
## Asset quality (measured gate)
107+
108+
"Modeled with intent" is measured, not a vibe. Three parts: measurable
109+
floors, a named reference bar, and the asset sheet. The floors are the
110+
cheap filter that catches the obvious failures; the asset sheet is what
111+
actually decides. Applies to asset-type examples (game props, kits) —
112+
not to world, field, art, or media subjects.
113+
114+
### Floors
115+
116+
Computed by `examples/gallery_asset_quality.py` (render path only, same
117+
call pattern as `gallery_framing`; prints measured values; exit 11 on
118+
violation; never imported by a check-only path):
119+
120+
- **Naming** — no default datablock names on hero parts (Cube, Sphere,
121+
Torus, Suzanne, …). A designed asset names its parts.
122+
- **Material variation** — a hero assembled from ≥ 2 parts carries ≥ 2
123+
distinct materials. A single flat Principled slot across an entire prop
124+
is this gallery's most reliable placeholder predictor. Single-part
125+
subjects are exempt: one honest material is their truth (a brass gear).
126+
- **Edge treatment** — at most **75 %** of a hero's manifold edges may be
127+
right angles (90° ± 2°). Unbroken 90° corners across the board means no
128+
bevel, no chamfer, no shading break. Honest machined teeth pass
129+
(`bmesh-gear` measures 0.667); raw boxes (1.0) fail.
130+
131+
Calibrated empirically against the gallery's own assets, exactly as the
132+
0.02 margin floor was:
133+
134+
| asset | parts | materials | edge90 | compactness (info only) |
135+
| --- | --- | --- | --- | --- |
136+
| collision-hull-proxy (reference) | 16 | 6 | 0.044 | 65.9 |
137+
| custom-normals-shade (reference) | 39 | 2 | 0.288 | 31.0 |
138+
| vertex-weight-limit (reference) | 1 | 4 | 0.150 | 34.8 |
139+
| lod-decimate-chain (reference) | 3 | 4 | 0.019 | 79.1 |
140+
| depsgraph-export (weak) | 2 | 1 | 1.000 | 23.9 |
141+
| text-version-stamp (weak) | 1 | 1 | 1.000 | 405.3 |
142+
| bmesh-gear (simple, honest) | 1 | 1 | 0.667 | 18.3 |
143+
| soccer-ball-goldberg (simple, honest) | 1 | 2 | 0.000 | 10.0 |
144+
145+
Dropped floors, with the evidence that killed them — a floor that cannot
146+
be set without failing a genuinely good asset is not a floor, and a floor
147+
hand-tuned to pass everything is not one either:
148+
149+
- **Part count**`vertex-weight-limit`'s mech arm is a single skinned
150+
mesh and is reference-quality. Skinned deform meshes are one mesh by
151+
design. The naming half survives.
152+
- **Silhouette compactness**`soccer-ball-goldberg` scores 10.0,
153+
`bmesh-gear` 18.3, `turntable` 19.1: genuinely good simple subjects
154+
with low-complexity silhouettes. Measured and printed as information.
155+
- **Dominant-material share**`custom-normals-shade`'s jerry can and
156+
`lod-decimate-chain`'s rocket carry their accents in per-face
157+
assignment, invisible to a per-part share (both measure 1.0 dominant).
158+
Printed as information only.
159+
160+
### The Goodhart warning
161+
162+
These floors are necessary, not sufficient. Bolting meaningless greebles
163+
onto a box to raise silhouette complexity satisfies every number above
164+
and still fails. Detail must serve the object's plausibility: fixtures
165+
where fixtures belong, wear where contact happens, structure where
166+
structure carries load. The asset sheet exists because metrics can be
167+
gamed; a lineup of peers cannot.
168+
169+
### Reference bar
170+
171+
The asset-quality reference set is pinned in `CLAUDE.md` § Quality Gates
172+
for Example Runs — canonical membership lives there, not here.
173+
174+
### Asset sheet gate
175+
176+
For every new asset-type example: render the hero alone — neutral
177+
three-quarter view, plain studio lighting, no staging tricks, no labels,
178+
no comparison props — composite it beside the reference assets rendered
179+
the same way, commit under `docs/gallery/asset-sheets/`, and link it in
180+
the PR body with a verdict. The asset ships only if it is not
181+
identifiable as the least-designed object in that lineup. Isolating the
182+
asset from its staging is the point: a strong scene can carry a weak
183+
model, and this gate removes the scene.
184+
106185
## Output
107186

108187
- Render 1280×720 PNG (`taa_render_samples`/`cycles.samples` 32–64).
47.7 KB
Loading
52 KB
Loading
14.1 KB
Loading
3.23 KB
Loading
1.34 KB
Loading
-574 Bytes
Loading

0 commit comments

Comments
 (0)