diff --git a/docs/gallery/anvil/index.html b/docs/gallery/anvil/index.html index 203dd7f5..138aaa6f 100644 --- a/docs/gallery/anvil/index.html +++ b/docs/gallery/anvil/index.html @@ -191,8 +191,13 @@ .md h1, .md h2, .md h3 { letter-spacing: -0.01em; margin: 1.4rem 0 0.6rem; line-height: 1.25; } .md h1 { font-size: 1.35rem; } .md h2 { font-size: 1.15rem; } .md h3 { font-size: 1rem; } .md p { margin: 0.7rem 0; } - .md ul { margin: 0.7rem 0 0.7rem 1.4rem; } + .md ul, .md ol { margin: 0.7rem 0 0.7rem 1.4rem; } .md li { margin: 0.3rem 0; } + .md .table-wrap { overflow-x: auto; margin: 0.9rem 0; } + .md table { border-collapse: collapse; font-size: 0.86rem; min-width: 100%; } + .md th, .md td { text-align: left; vertical-align: top; padding: 0.4rem 0.75rem 0.4rem 0; + border-bottom: 1px solid var(--border); } + .md th { color: var(--text-dim); font-weight: 600; white-space: nowrap; } .md code { background: var(--surface-2); border: 1px solid var(--border); padding: 0.08rem 0.35rem; border-radius: 4px; font-family: var(--font-mono); font-size: 0.84em; } .md pre { background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); @@ -265,17 +270,17 @@
Composes skills mesh-editing-and-bmesh, bake-high-to-low, depsgraph-and-evaluated-data, engine-export-presets, and snippets bake_normal_high_to_low.py, setup_bake_target_image.py, lod_chain.py / decimate_to_budget.py, convex_hull_collider.py, export_preset_unity.py (helpers copied, not imported as a package).
Declared as named constants; every gate recomputes from the mesh, materials, UVs, evaluated LOD, collider, or export file.
-| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) | | --- | --- | --- | | Base triangles | 2500–4500 | 2988 / 2988 / 2988 | | LOD1 ratio | 0.32–0.62 of base | 0.5000 / 0.5000 / 0.5000 | | LOD2 ratio | 0.10–0.35 of base | 0.2195 / 0.2195 / 0.2195 | | Materials | exactly 2 distinct; ≥400 wood, ≥400 metal faces | 2 slots; 1040 / 822 | | UVs | in 0..1, AABB overlap ≤ 1e-5 | in range, overlap 0 | | Outer AABB | (0.621, 0.414, 0.489) m ± 0.015 | (0.6210, 0.4137, 0.4890), zmin 0 | | Collider tris | ≤ 280 | 134 | | Export | written, size > 0 | 244052 / 244052 / 244036 bytes |
| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) |
|---|---|---|
| Base triangles | 2500–4500 | 2988 / 2988 / 2988 |
| LOD1 ratio | 0.32–0.62 of base | 0.5000 / 0.5000 / 0.5000 |
| LOD2 ratio | 0.10–0.35 of base | 0.2195 / 0.2195 / 0.2195 |
| Materials | exactly 2 distinct; ≥400 wood, ≥400 metal faces | 2 slots; 1040 / 822 |
| UVs | in 0..1, AABB overlap ≤ 1e-5 | in range, overlap 0 |
| Outer AABB | (0.621, 0.414, 0.489) m ± 0.015 | (0.6210, 0.4137, 0.4890), zmin 0 |
| Collider tris | ≤ 280 | 134 |
| Export | written, size > 0 | 244052 / 244052 / 244036 bytes |
Base triangles dropped from 6616 to 2988 in the quality pass: overlapping face boxes and a beveled superellipse horn became one slotted slab plus a circular oval loft. Hidden coincident faces were the waste.
DECIMATE COLLAPSE triangle counts are not identical across series — the gate is a ratio band, not an exact count. Bake pixels are stochastic; the gate is has_data plus operator FINISHED, not byte-identity. Stave-width jitter uses fixed seed 17. Export byte counts differ by 16 B on 5.2.1 (glTF serializer), not a gated axis.
Recomputed from the generated mesh, not asserted about the script.
-| Axis | Declared | Measured (all three) | | --- | --- | --- | | Non-manifold edges | 0 | 0 | | Loose verts / edges | 0 / 0 | 0 / 0 | | Doubles merged at 1e-5 | 0 | 0 | | Zero-area faces | 0 | 0 | | N-gons | 0 | 0 | | Coplanar disjoint face pairs | 0 | 0 | | Grounded: zmin | within 1e-4 of 0 | 0.0000 | | Named supports: 16 staves | each zmin ≤ 0.001 | 16, stave_z 0.00000 | | Body plan | 0.621 m long × 0.180 m high ± 0.05 | 0.6210 × 0.1800 |
| Axis | Declared | Measured (all three) |
|---|---|---|
| Non-manifold edges | 0 | 0 |
| Loose verts / edges | 0 / 0 | 0 / 0 |
| Doubles merged at 1e-5 | 0 | 0 |
| Zero-area faces | 0 | 0 |
| N-gons | 0 | 0 |
| Coplanar disjoint face pairs | 0 | 0 |
Grounded: zmin | within 1e-4 of 0 | 0.0000 |
| Named supports: 16 staves | each zmin ≤ 0.001 | 16, stave_z 0.00000 |
| Body plan | 0.621 m long × 0.180 m high ± 0.05 | 0.6210 × 0.1800 |
| Axis | Declared | Measured (all three) | | --- | --- | --- | | Foot-on-head BVH gap | ≤ 0.006 m | 0.00300 | | Hoop bite (host r − inner hoop r) | 0.0012–0.008 m | 0.00300 |
+| Axis | Declared | Measured (all three) |
|---|---|---|
| Foot-on-head BVH gap | ≤ 0.006 m | 0.00300 |
| Hoop bite (host r − inner hoop r) | 0.0012–0.008 m | 0.00300 |
Each violates one named budget. All six were run on 4.5.11, 5.1.2 and 5.2.1 and returned the same code on each.
-| Flag | Budget violated | Exit | | --- | --- | --- | | --skip-decimate | LOD1 ratio band | 9 | | --stray-vert | loose vertex count is 0 | 15 | | --lift-z | bounding box zmin is 0 | 16 | | --short-staves | named stave supports at Z=0 | 16 | | --float-anvil | foot-on-head gap | 17 | | --round-band | hoop bite band | 18 |
| Flag | Budget violated | Exit |
|---|---|---|
--skip-decimate | LOD1 ratio band | 9 |
--stray-vert | loose vertex count is 0 | 15 |
--lift-z | bounding box zmin is 0 | 16 |
--short-staves | named stave supports at Z=0 | 16 |
--float-anvil | foot-on-head gap | 17 |
--round-band | hoop bite band | 18 |
blender --background --python anvil.py --
blender --background --python anvil.py -- --skip-decimate
@@ -288,7 +293,7 @@ Run
Smoke passes no flags.
Exit codes
File-local. 9 is a valid check code. 10 is reserved for gallery_framing.check_framing on the --output path. 15–19 are the hygiene and joint-fit family.
-| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Mesh did not build / no UV layer | | 4 | Base triangle count outside range | | 5 | Material count ≠ 2 distinct slots, or a face-count floor missed | | 6 | UVs outside 0..1 | | 7 | UV AABB overlap above tolerance | | 8 | World AABB off declared outer size | | 9 | LOD ratio band (--skip-decimate lands here) | | 10 | Framing gate (render path only) | | 11 | Collider triangle count above ceiling | | 12 | Bake did not finish or image has no data | | 13 | Export file missing or empty | | 14 | --output produced no file | | 15 | Mesh hygiene: loose, non-manifold, zero-area, doubles, n-gons, z-fight | | 16 | Not grounded: bounding box zmin off 0, or a named stave floats | | 17 | Joint fit: foot-on-head gap (--float-anvil) | | 18 | Seat: hoop bite band (--round-band) | | 19 | Body length or height off the stated real-world size |
+Code Meaning 0 Success 1 Uncaught exception (FATAL wrapper) 2 argparse / usage 3 Mesh did not build / no UV layer 4 Base triangle count outside range 5 Material count ≠ 2 distinct slots, or a face-count floor missed 6 UVs outside 0..1 7 UV AABB overlap above tolerance 8 World AABB off declared outer size 9 LOD ratio band (--skip-decimate lands here) 10 Framing gate (render path only) 11 Collider triangle count above ceiling 12 Bake did not finish or image has no data 13 Export file missing or empty 14 --output produced no file15 Mesh hygiene: loose, non-manifold, zero-area, doubles, n-gons, z-fight 16 Not grounded: bounding box zmin off 0, or a named stave floats 17 Joint fit: foot-on-head gap (--float-anvil) 18 Seat: hoop bite band (--round-band) 19 Body length or height off the stated real-world size
Source
diff --git a/docs/gallery/armature-bend/index.html b/docs/gallery/armature-bend/index.html
index 97430f20..c62f3ae1 100644
--- a/docs/gallery/armature-bend/index.html
+++ b/docs/gallery/armature-bend/index.html
@@ -191,8 +191,13 @@
.md h1, .md h2, .md h3 { letter-spacing: -0.01em; margin: 1.4rem 0 0.6rem; line-height: 1.25; }
.md h1 { font-size: 1.35rem; } .md h2 { font-size: 1.15rem; } .md h3 { font-size: 1rem; }
.md p { margin: 0.7rem 0; }
- .md ul { margin: 0.7rem 0 0.7rem 1.4rem; }
+ .md ul, .md ol { margin: 0.7rem 0 0.7rem 1.4rem; }
.md li { margin: 0.3rem 0; }
+ .md .table-wrap { overflow-x: auto; margin: 0.9rem 0; }
+ .md table { border-collapse: collapse; font-size: 0.86rem; min-width: 100%; }
+ .md th, .md td { text-align: left; vertical-align: top; padding: 0.4rem 0.75rem 0.4rem 0;
+ border-bottom: 1px solid var(--border); }
+ .md th { color: var(--text-dim); font-weight: 600; white-space: nowrap; }
.md code { background: var(--surface-2); border: 1px solid var(--border); padding: 0.08rem 0.35rem;
border-radius: 4px; font-family: var(--font-mono); font-size: 0.84em; }
.md pre { background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius);
@@ -261,7 +266,7 @@ armature-bend
A runnable example that rigs a tapered, segmented tube with a four-bone armature chain — edit_bones construction, name-bound vertex groups with smoothstep blend zones, a posed curl — and checks the depsgraph-evaluated result against closed-form linear blend skinning, following mesh-editing-and-bmesh and depsgraph-and-evaluated-data.
What it witnesses: the three contracts AI-generated rigging code most often gets wrong.
-1. edit_bones only exists in edit mode — the collection is asserted empty in object mode and holding the full chain (heads/tails at closed-form positions) inside mode_set(mode='EDIT'). Outside edit mode, reads silently return nothing. 2. Vertex groups bind to bones by name; a typo deforms nothing without erroring. Every group here is named after its bone and the deform proves the binding. 3. The armature modifier is exactly linear blend skinning: for *every* vertex, evaluated == Σ wᵢ · (pose_bone.matrix @ bone.matrix_local.inverted()) @ rest. The check re-implements LBS from the pose matrices and compares the whole evaluated mesh (max error ~5e-7). The root ring must stay pinned and the tip must deflect — a straight tube is a failure.
+edit_bones only exists in edit mode — the collection is asserted empty in object mode and holding the full chain (heads/tails at closed-form positions) inside mode_set(mode='EDIT'). Outside edit mode, reads silently return nothing.- Vertex groups bind to bones by name; a typo deforms nothing without erroring. Every group here is named after its bone and the deform proves the binding.
- The armature modifier is exactly linear blend skinning: for every vertex,
evaluated == Σ wᵢ · (pose_bone.matrix @ bone.matrix_local.inverted()) @ rest. The check re-implements LBS from the pose matrices and compares the whole evaluated mesh (max error ~5e-7). The root ring must stay pinned and the tip must deflect — a straight tube is a failure.
The armature API is identical on Blender 4.5 LTS and 5.1 — no version gate needed. One portability hazard is baked in as a comment: interleaving color_attributes writes with VertexGroup.add() dangles the attribute reference when the deform layer is allocated (hard crash on 4.5, silent luck on 5.1), so the script finishes all weight writes before creating the color attribute.
The render shows rest → half curl → full curl left to right, with per-bone weight bands visualized through a BoneTint color attribute — the smooth color blends at the joints are the same weights the LBS check asserts.
Run
@@ -276,7 +281,7 @@ Run
blender --background --python armature_bend.py -- --output bend.png --engine cycles
Per-script sequential checks. 9 is a valid check code; there is no rule against it.
| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | edit_bones populated in object mode | | 4 | Edit-mode bone chain off closed form | | 5 | Evaluated vertex count changed | | 6 | Evaluated mesh off closed-form LBS | | 7 | Root ring moved | | 8 | Tip did not deflect (--zero-curl lands here) | | 9 | --output produced no file |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | edit_bones populated in object mode |
| 4 | Edit-mode bone chain off closed form |
| 5 | Evaluated vertex count changed |
| 6 | Evaluated mesh off closed-form LBS |
| 7 | Root ring moved |
| 8 | Tip did not deflect (--zero-curl lands here) |
| 9 | --output produced no file |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --zero-curl.
len(loops) == 3K; POINT attr == len(vertices) == K+1.What each check catches on failure: wedge 3 miscolored in the CORNER pass (exit 4); naive writes reversed, so the hub reads palette[0] (exit 5); a ring vert corrupted, breaking the overwrite-ordering witness (exit 6); a constant palette, collapsing the shear so the probe cannot distinguish naive from correct (exit 7). Sizes wrong for the declared domain (exit 3).
Version witness: output is byte-identical on Blender 4.5.11 LTS and 5.1.2 — the color_attributes domain API is stable across both.
Render as proof: dual pinwheel from the same closed-form palette the check asserts. CORNER (left) holds eight crisp petals to the hub; naive POINT (right) smears — petal colors bleed across the shared hub and ring verts into a swirl. The broken state is in-frame by design: the right fan *is* the falsification variant. Fully matte petal materials (Specular IOR Level = 0) so the flat color data carries no specular line, per docs/VISUAL-STYLE.md.
Render as proof: dual pinwheel from the same closed-form palette the check asserts. CORNER (left) holds eight crisp petals to the hub; naive POINT (right) smears — petal colors bleed across the shared hub and ring verts into a swirl. The broken state is in-frame by design: the right fan is the falsification variant. Fully matte petal materials (Specular IOR Level = 0) so the flat color data carries no specular line, per docs/VISUAL-STYLE.md.
blender --background --python attribute_domain_shear.py --
blender --background --python attribute_domain_shear.py -- --no-overwrite
@@ -273,7 +278,7 @@ Run
blender --background --python attribute_domain_shear.py -- --output shear.png --engine cycles
Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper.
| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | CORNER or POINT attribute size wrong | | 4 | CORNER hub corners off wedge color | | 5 | POINT hub is not last-write (--no-overwrite lands here) | | 6 | Outer ring verts off last-write order | | 7 | Measured shear off palette closed form, or ~0 | | 9 | --output produced no file | | 10 | Gallery framing violation |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | CORNER or POINT attribute size wrong |
| 4 | CORNER hub corners off wedge color |
| 5 | POINT hub is not last-write (--no-overwrite lands here) |
| 6 | Outer ring verts off last-write order |
| 7 | Measured shear off palette closed form, or ~0 |
| 9 | --output produced no file |
| 10 | Gallery framing violation |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --no-overwrite.
Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper.
| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Missing UV layer on the target | | 4 | Bake did not FINISHED or image has_data is false | | 5 | Detail deviant-pixel fraction below 0.40 (--flat-source lands here) | | 6 | Detail MAD below 0.05 | | 7 | Flat control above 0.05 frac / 0.03 MAD | | 8 | Monotonic gap below 0.30 | | 9 | --output produced no file | | 10 | Gallery framing violation |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Missing UV layer on the target |
| 4 | Bake did not FINISHED or image has_data is false |
| 5 | Detail deviant-pixel fraction below 0.40 (--flat-source lands here) |
| 6 | Detail MAD below 0.05 |
| 7 | Flat control above 0.05 frac / 0.03 MAD |
| 8 | Monotonic gap below 0.30 |
| 9 | --output produced no file |
| 10 | Gallery framing violation |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output.
Per-script sequential checks. 9 is a valid check code; there is no rule against it.
| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Topology ≠ closed form (--no-extrude lands here) | | 4 | Non-manifold edges | | 6 | --output produced no file |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Topology ≠ closed form (--no-extrude lands here) |
| 4 | Non-manifold edges |
| 6 | --output produced no file |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --no-extrude.
Composes skills mesh-editing-and-bmesh, bake-high-to-low, depsgraph-and-evaluated-data, engine-export-presets, and snippets bake_normal_high_to_low.py, setup_bake_target_image.py, lod_chain.py / decimate_to_budget.py, convex_hull_collider.py, export_preset_unity.py (helpers copied, not imported as a package).
Declared as named constants; every gate recomputes from the mesh, materials, UVs, evaluated LOD, collider, or export file.
-| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) | | --- | --- | --- | | Base triangles | 1400–3600 | 2200 / 2200 / 2200 | | LOD1 ratio | 0.32–0.62 of base | 0.5000 / 0.5000 / 0.5000 | | LOD2 ratio | 0.10–0.35 of base | 0.2200 / 0.2200 / 0.2200 | | Materials | exactly 3 distinct; ≥80 wood, ≥24 ash, ≥200 stone | 3 slots; 216 / 48 / 1304 | | UVs | in 0..1, AABB overlap ≤ 1e-5 | in range, overlap 0 | | Outer AABB | (0.807, 0.808, 0.404) m ± 0.015 | (0.8069, 0.8079, 0.4037), zmin 0 | | Collider tris | ≤ 360 | 198 | | Export | written, size > 0 | 206244 / 206244 / 206236 bytes |
| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) |
|---|---|---|
| Base triangles | 1400–3600 | 2200 / 2200 / 2200 |
| LOD1 ratio | 0.32–0.62 of base | 0.5000 / 0.5000 / 0.5000 |
| LOD2 ratio | 0.10–0.35 of base | 0.2200 / 0.2200 / 0.2200 |
| Materials | exactly 3 distinct; ≥80 wood, ≥24 ash, ≥200 stone | 3 slots; 216 / 48 / 1304 |
| UVs | in 0..1, AABB overlap ≤ 1e-5 | in range, overlap 0 |
| Outer AABB | (0.807, 0.808, 0.404) m ± 0.015 | (0.8069, 0.8079, 0.4037), zmin 0 |
| Collider tris | ≤ 360 | 198 |
| Export | written, size > 0 | 206244 / 206244 / 206236 bytes |
Base triangles dropped from 3616 to 2200 in the quality pass: the old piece stacked identical boxes with a mortar gap and a teepee that met at one point. The rebuild is two seated courses plus crossing sticks; hidden overlapping volume went away.
DECIMATE COLLAPSE triangle counts are not guaranteed identical across series — the gate is a ratio band, not an exact count. Bake pixels are stochastic; the gate is has_data plus operator FINISHED, not byte-identity. Stone-width jitter uses fixed seed 17. glTF byte size differs by 8 B on 5.2.1.
Recomputed from the generated mesh, not asserted about the script.
-| Axis | Declared | Measured (all three) | | --- | --- | --- | | Non-manifold edges | 0 | 0 | | Loose verts / edges | 0 / 0 | 0 / 0 | | Doubles merged at 1e-5 | 0 | 0 | | Zero-area faces | 0 | 0 | | N-gons | 0 | 0 | | Coplanar disjoint face pairs | 0 | 0 | | Grounded: zmin | within 1e-4 of 0 | 0.0000 | | Named supports: 14 bottom stones | each zmin ≤ 0.001 | 14, stone_z 0.00000 | | Ring plan | 0.807 m dia × 0.156 m high ± 0.04 | 0.8074 × 0.1560 |
| Axis | Declared | Measured (all three) |
|---|---|---|
| Non-manifold edges | 0 | 0 |
| Loose verts / edges | 0 / 0 | 0 / 0 |
| Doubles merged at 1e-5 | 0 | 0 |
| Zero-area faces | 0 | 0 |
| N-gons | 0 | 0 |
| Coplanar disjoint face pairs | 0 | 0 |
Grounded: zmin | within 1e-4 of 0 | 0.0000 |
| Named supports: 14 bottom stones | each zmin ≤ 0.001 | 14, stone_z 0.00000 |
| Ring plan | 0.807 m dia × 0.156 m high ± 0.04 | 0.8074 × 0.1560 |
| Axis | Declared | Measured (all three) | | --- | --- | --- | | Teepee kiss (BVH nearest at caps) | ≤ 0.008 m | 0.00001 | | Course seat (upper zmin − lower zmax) | abs ≤ 0.004 m | 0.00000 |
+| Axis | Declared | Measured (all three) |
|---|---|---|
| Teepee kiss (BVH nearest at caps) | ≤ 0.008 m | 0.00001 |
| Course seat (upper zmin − lower zmax) | abs ≤ 0.004 m | 0.00000 |
Each violates one named budget. All six were run on 4.5.11, 5.1.2 and 5.2.1 and returned the same code on each.
-| Flag | Budget violated | Exit | | --- | --- | --- | | --skip-decimate | LOD1 ratio band | 9 | | --stray-vert | loose vertex count is 0 | 15 | | --lift-z | bounding box zmin is 0 | 16 | | --short-stones | named bottom-course supports at Z=0 | 16 | | --float-logs | teepee kiss | 17 | | --gap-courses | course seat | 18 |
| Flag | Budget violated | Exit |
|---|---|---|
--skip-decimate | LOD1 ratio band | 9 |
--stray-vert | loose vertex count is 0 | 15 |
--lift-z | bounding box zmin is 0 | 16 |
--short-stones | named bottom-course supports at Z=0 | 16 |
--float-logs | teepee kiss | 17 |
--gap-courses | course seat | 18 |
blender --background --python campfire.py --
blender --background --python campfire.py -- --skip-decimate
@@ -288,7 +293,7 @@ Run
Smoke passes no flags.
Exit codes
File-local. 9 is a valid check code. 10 is reserved for gallery_framing.check_framing on the --output path. 15–19 are the hygiene and joint-fit family.
-| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Mesh did not build / no UV layer | | 4 | Base triangle count outside range | | 5 | Material count ≠ 3 distinct slots, or a face-count floor missed | | 6 | UVs outside 0..1 | | 7 | UV AABB overlap above tolerance | | 8 | World AABB off declared outer size | | 9 | LOD ratio band (--skip-decimate lands here) | | 10 | Framing gate (render path only) | | 11 | Collider triangle count above ceiling | | 12 | Bake did not finish or image has no data | | 13 | Export file missing or empty | | 14 | --output produced no file | | 15 | Mesh hygiene: loose, non-manifold, zero-area, doubles, n-gons, z-fight | | 16 | Not grounded: bounding box zmin off 0, or a named bottom stone floats | | 17 | Joint fit: teepee kiss (--float-logs) | | 18 | Seat: course gap (--gap-courses) | | 19 | Ring diameter or height off the stated real-world size |
+Code Meaning 0 Success 1 Uncaught exception (FATAL wrapper) 2 argparse / usage 3 Mesh did not build / no UV layer 4 Base triangle count outside range 5 Material count ≠ 3 distinct slots, or a face-count floor missed 6 UVs outside 0..1 7 UV AABB overlap above tolerance 8 World AABB off declared outer size 9 LOD ratio band (--skip-decimate lands here) 10 Framing gate (render path only) 11 Collider triangle count above ceiling 12 Bake did not finish or image has no data 13 Export file missing or empty 14 --output produced no file15 Mesh hygiene: loose, non-manifold, zero-area, doubles, n-gons, z-fight 16 Not grounded: bounding box zmin off 0, or a named bottom stone floats 17 Joint fit: teepee kiss (--float-logs) 18 Seat: course gap (--gap-courses) 19 Ring diameter or height off the stated real-world size
Per-script sequential checks. 9 is a valid check code; there is no rule against it.
| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Body datablock is not the authored half | | 4 | Authored centerline vert count ≠ 28 | | 5 | Evaluated verts ≠ 2n − c (--no-mirror lands here) | | 6 | Evaluated on-plane verts ≠ centerline; also --output produced no file | | 7 | Evaluated Euler characteristic ≠ 2 | | 8 | Non-manifold edges in the evaluated shell | | 9 | Evaluated verts lack a mirrored partner | | 10 | Mirror partner deviation above tolerance | | 11 | Evaluated bbox not symmetric about X | | 12 | Mirrored-part origin off the plane | | 13 | Mirrored-part datablock is not the authored half | | 14 | Mirrored-part evaluated counts did not double | | 15 | Mirrored-part partner check failed | | 16 | Mirrored-part evaluated mesh stayed on one side |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Body datablock is not the authored half |
| 4 | Authored centerline vert count ≠ 28 |
| 5 | Evaluated verts ≠ 2n − c (--no-mirror lands here) |
| 6 | Evaluated on-plane verts ≠ centerline; also --output produced no file |
| 7 | Evaluated Euler characteristic ≠ 2 |
| 8 | Non-manifold edges in the evaluated shell |
| 9 | Evaluated verts lack a mirrored partner |
| 10 | Mirror partner deviation above tolerance |
| 11 | Evaluated bbox not symmetric about X |
| 12 | Mirrored-part origin off the plane |
| 13 | Mirrored-part datablock is not the authored half |
| 14 | Mirrored-part evaluated counts did not double |
| 15 | Mirrored-part partner check failed |
| 16 | Mirrored-part evaluated mesh stayed on one side |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --no-mirror.
Composes skills mesh-editing-and-bmesh, bake-high-to-low, depsgraph-and-evaluated-data, engine-export-presets, and snippets bake_normal_high_to_low.py, setup_bake_target_image.py, lod_chain.py / decimate_to_budget.py, convex_hull_collider.py, export_preset_unity.py (helpers copied, not imported as a package).
Declared as named constants; every gate recomputes from the mesh, materials, UVs, evaluated LOD, collider, or export file.
-| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) | | --- | --- | --- | | Base triangles | 2760–2960 | 2856 / 2856 / 2856 | | LOD1 ratio | 0.32–0.62 of base | 0.5000 / 0.5000 / 0.5000 | | LOD2 ratio | 0.10–0.35 of base | 0.2199 / 0.2199 / 0.2157 | | Materials | exactly 2 distinct, metal ≥ 24, wood ≥ 800 faces | 2 slots, floors met | | UVs | in 0..1, AABB overlap ≤ 1e-5 | in range, overlap 0 | | Outer AABB | (1.539, 0.749, 0.640) m ± 0.01 | (1.5393, 0.7486, 0.6400) | | Grounded | bbox min Z within 1e-4 of 0 | 0.0000 / 0.0000 / 0.0000 | | Named supports | each tyre's own zmin within 1e-4 of the floor | +Y 0.000000, −Y 0.000000 | | Hygiene | loose V/E, non-manifold, zero-area, doubles @1e-5, n-gons: all 0 | 0 / 0 / 0 on every axis | | Z-fighting | coplanar cross-shell face pairs = 0 | 0 / 0 / 0 | | Tyre seat | 0.0030–0.0055 m interference, per angular station | [0.00400, 0.00400] over 48 stations | | Wheel mirror | the two wheels match in X, Z and extent within 5e-5 m | 0.00000 mm | | Material-island gap | metal↔wood min distance ≤ 0.008 m | 0.00000 / 0.00000 / 0.00000 | | Collider tris | ≤ 360 | 154 | | Export | written, size > 0, removed after measuring | 221960 bytes on 5.2.1 |
| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) |
|---|---|---|
| Base triangles | 2760–2960 | 2856 / 2856 / 2856 |
| LOD1 ratio | 0.32–0.62 of base | 0.5000 / 0.5000 / 0.5000 |
| LOD2 ratio | 0.10–0.35 of base | 0.2199 / 0.2199 / 0.2157 |
| Materials | exactly 2 distinct, metal ≥ 24, wood ≥ 800 faces | 2 slots, floors met |
| UVs | in 0..1, AABB overlap ≤ 1e-5 | in range, overlap 0 |
| Outer AABB | (1.539, 0.749, 0.640) m ± 0.01 | (1.5393, 0.7486, 0.6400) |
| Grounded | bbox min Z within 1e-4 of 0 | 0.0000 / 0.0000 / 0.0000 |
| Named supports | each tyre's own zmin within 1e-4 of the floor | +Y 0.000000, −Y 0.000000 |
| Hygiene | loose V/E, non-manifold, zero-area, doubles @1e-5, n-gons: all 0 | 0 / 0 / 0 on every axis |
| Z-fighting | coplanar cross-shell face pairs = 0 | 0 / 0 / 0 |
| Tyre seat | 0.0030–0.0055 m interference, per angular station | [0.00400, 0.00400] over 48 stations |
| Wheel mirror | the two wheels match in X, Z and extent within 5e-5 m | 0.00000 mm |
| Material-island gap | metal↔wood min distance ≤ 0.008 m | 0.00000 / 0.00000 / 0.00000 |
| Collider tris | ≤ 360 | 154 |
| Export | written, size > 0, removed after measuring | 221960 bytes on 5.2.1 |
DECIMATE COLLAPSE triangle counts are not identical across series — 5.2.1 is more aggressive on LOD2. The gate is a ratio band, not an exact count. Bake pixels are stochastic; the gate is has_data plus operator FINISHED, not byte-identity. Construction uses no RNG. Export byte counts differ by a few bytes across series (glTF serializer); the gate is "written and non-empty", not a byte count, and the file is removed once measured.
Each flag breaks one stage so a named budget fails and the piece exits *that* code. Magnitudes are sized so no earlier gate can steal the failure: the two wheel moves stay inside BBOX_TOL, so the AABB gate (exit 8) still passes.
| Flag | Target budget | Exit | | --- | --- | --- | | --skip-decimate | LOD1 ratio band (ratio becomes 1.0) | 9 | | --lift-z | AABB grounded zmin (whole mesh up 0.05 m) | 16 | | --flush-tyre | Coplanar cross-shell pairs — sets the tyre's inner radius equal to the felloe's outer radius, which is the construction bug this piece was rebuilt to remove (0 → 48 pairs) | 15 | | --sink-tyre | Tyre seat band — buries the hoop 9 mm into the felloe so it reads as one body | 18 | | --float-wheel | Named supports — lifts one wheel 3 mm while the other still grounds the AABB | 16 | | --skew-wheel | Wheel mirror — pushes one wheel 6 mm out along the track | 19 |
Each flag breaks one stage so a named budget fails and the piece exits that code. Magnitudes are sized so no earlier gate can steal the failure: the two wheel moves stay inside BBOX_TOL, so the AABB gate (exit 8) still passes.
| Flag | Target budget | Exit |
|---|---|---|
--skip-decimate | LOD1 ratio band (ratio becomes 1.0) | 9 |
--lift-z | AABB grounded zmin (whole mesh up 0.05 m) | 16 |
--flush-tyre | Coplanar cross-shell pairs — sets the tyre's inner radius equal to the felloe's outer radius, which is the construction bug this piece was rebuilt to remove (0 → 48 pairs) | 15 |
--sink-tyre | Tyre seat band — buries the hoop 9 mm into the felloe so it reads as one body | 18 |
--float-wheel | Named supports — lifts one wheel 3 mm while the other still grounds the AABB | 16 |
--skew-wheel | Wheel mirror — pushes one wheel 6 mm out along the track | 19 |
--lift-z and --float-wheel share exit 16 and fail different budgets: --lift-z fails the AABB gate, which fires first; --float-wheel leaves the AABB grounded, so only the per-tyre support check can catch it.
blender --background --python cart.py --
@@ -282,7 +287,7 @@ Run
Smoke passes none of the falsifier flags and no --output.
Exit codes
File-local. 9 is a valid check code. 10 is reserved for gallery_framing.check_framing on the --output path.
-| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Mesh did not build / no UV layer | | 4 | Base triangle count outside range | | 5 | Material count ≠ 2 distinct slots, or metal faces missing | | 6 | UVs outside 0..1 | | 7 | UV AABB overlap above tolerance | | 8 | World AABB off declared outer size | | 9 | LOD ratio band (--skip-decimate lands here) | | 10 | Framing gate (render path only) | | 11 | Collider triangle count above ceiling | | 12 | Bake did not finish or image has no data | | 13 | Export file missing or empty | | 14 | --output produced no file | | 15 | Hygiene: loose geometry, non-manifold, zero-area, doubles, n-gons, or coplanar cross-shell face pairs (--flush-tyre lands here) | | 16 | Bbox min Z not grounded (--lift-z), or a named support off the floor (--float-wheel) | | 17 | Material-island gap above tolerance (parts meant to touch) | | 18 | Tyre seat depth outside its band (--sink-tyre lands here) | | 19 | Wheel mirror deviation above epsilon (--skew-wheel lands here) |
+Code Meaning 0 Success 1 Uncaught exception (FATAL wrapper) 2 argparse / usage 3 Mesh did not build / no UV layer 4 Base triangle count outside range 5 Material count ≠ 2 distinct slots, or metal faces missing 6 UVs outside 0..1 7 UV AABB overlap above tolerance 8 World AABB off declared outer size 9 LOD ratio band (--skip-decimate lands here) 10 Framing gate (render path only) 11 Collider triangle count above ceiling 12 Bake did not finish or image has no data 13 Export file missing or empty 14 --output produced no file15 Hygiene: loose geometry, non-manifold, zero-area, doubles, n-gons, or coplanar cross-shell face pairs (--flush-tyre lands here) 16 Bbox min Z not grounded (--lift-z), or a named support off the floor (--float-wheel) 17 Material-island gap above tolerance (parts meant to touch) 18 Tyre seat depth outside its band (--sink-tyre lands here) 19 Wheel mirror deviation above epsilon (--skew-wheel lands here)
Composes skills mesh-editing-and-bmesh, bake-high-to-low, depsgraph-and-evaluated-data, engine-export-presets, and snippets bake_normal_high_to_low.py, setup_bake_target_image.py, lod_chain.py / decimate_to_budget.py, convex_hull_collider.py, export_preset_unity.py (helpers copied, not imported as a package).
Declared as named constants; every gate recomputes from the mesh, materials, UVs, evaluated LOD, collider, or export file.
-| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) | | --- | --- | --- | | Base triangles | 1730–1870 | 1800 / 1800 / 1800 | | LOD1 ratio | 0.32–0.62 of base | 0.5000 / 0.5000 / 0.5000 | | LOD2 ratio | 0.10–0.35 of base | 0.2200 / 0.2200 / 0.2200 | | Materials | exactly 4 distinct; ≥160 bark, ≥90 grain, ≥170 metal, ≥150 haft faces | 4 slots; 180 / 216 / 404 / 176 | | UVs | in 0..1, AABB overlap ≤ 1e-5 | in range, overlap 0 | | Outer AABB | (0.526, 0.526, 1.006) m ± 0.012 | (0.5264, 0.5264, 1.0064) | | Collider tris | ≤ 560 | 538 | | Export | written, size > 0 | 180588 / 180588 / 180572 bytes |
| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) |
|---|---|---|
| Base triangles | 1730–1870 | 1800 / 1800 / 1800 |
| LOD1 ratio | 0.32–0.62 of base | 0.5000 / 0.5000 / 0.5000 |
| LOD2 ratio | 0.10–0.35 of base | 0.2200 / 0.2200 / 0.2200 |
| Materials | exactly 4 distinct; ≥160 bark, ≥90 grain, ≥170 metal, ≥150 haft faces | 4 slots; 180 / 216 / 404 / 176 |
| UVs | in 0..1, AABB overlap ≤ 1e-5 | in range, overlap 0 |
| Outer AABB | (0.526, 0.526, 1.006) m ± 0.012 | (0.5264, 0.5264, 1.0064) |
| Collider tris | ≤ 560 | 538 |
| Export | written, size > 0 | 180588 / 180588 / 180572 bytes |
The triangle band, the outer AABB and the collider ceiling were re-fitted in the quality pass that added the lap, the rivets and the 16-segment oval haft (1472 → 1800 triangles, collider 434 → 538). The triangle band is centred on the new measurement, ± 4 %.
Recomputed from the generated mesh, not asserted about the script.
-| Axis | Declared | Measured (all three) | | --- | --- | --- | | Non-manifold edges | 0 | 0 | | Loose verts / edges | 0 / 0 | 0 / 0 | | Doubles merged at 1e-5 | 0 | 0 | | Zero-area faces | 0 | 0 | | N-gons | 0 | 0 | | Coplanar disjoint face pairs | 0 | 0 | | Grounded: zmin | within 1e-4 of 0 | 0.0000 | | Log axis out of plumb | ≤ 1e-5 | 0.0000000 | | Log size | 0.535 × 0.360 m ± 0.030 / ± 0.010 | 0.5464 × 0.3600 | | Haft section, wide over thick at the grip | 1.25–1.60 | 1.3500 |
| Axis | Declared | Measured (all three) |
|---|---|---|
| Non-manifold edges | 0 | 0 |
| Loose verts / edges | 0 / 0 | 0 / 0 |
| Doubles merged at 1e-5 | 0 | 0 |
| Zero-area faces | 0 | 0 |
| N-gons | 0 | 0 |
| Coplanar disjoint face pairs | 0 | 0 |
Grounded: zmin | within 1e-4 of 0 | 0.0000 |
| Log axis out of plumb | ≤ 1e-5 | 0.0000000 |
| Log size | 0.535 × 0.360 m ± 0.030 / ± 0.010 | 0.5464 × 0.3600 |
| Haft section, wide over thick at the grip | 1.25–1.60 | 1.3500 |
Six shells that have to meet correctly: log, hoop, head, haft, and the two rivets through the hoop's lap.
-| Axis | Declared | Measured (all three) | | --- | --- | --- | | Shell count | exactly 6 | 6 | | Haft clearance inside the eye | ≥ 0.006 m | 0.01800 | | Haft engagement through the eye | ≥ 0.020 m | 0.08225 | | Haft breakout margin below the head | ≥ 0.006 m | 0.02175 | | Bit bury below the sawn top | ≥ 0.030 m | 0.07871 | | Bit inset from the rim | ≥ 0.030 m | 0.14647 | | Hoop bite into the log, every segment | 0.002–0.007 m | 0.00371–0.00422 | | Haft clearance above the log | ≥ 0.015 m, 0 verts inside | 0.07531, 0 | | Rivet seat into the hoop, each rivet | 0.0008–0.0025 m | 0.00150, 0.00154 | | Rivet dome proud of the hoop, each rivet | ≥ 0.0025 m | 0.00419, 0.00432 |
+| Axis | Declared | Measured (all three) |
|---|---|---|
| Shell count | exactly 6 | 6 |
| Haft clearance inside the eye | ≥ 0.006 m | 0.01800 |
| Haft engagement through the eye | ≥ 0.020 m | 0.08225 |
| Haft breakout margin below the head | ≥ 0.006 m | 0.02175 |
| Bit bury below the sawn top | ≥ 0.030 m | 0.07871 |
| Bit inset from the rim | ≥ 0.030 m | 0.14647 |
| Hoop bite into the log, every segment | 0.002–0.007 m | 0.00371–0.00422 |
| Haft clearance above the log | ≥ 0.015 m, 0 verts inside | 0.07531, 0 |
| Rivet seat into the hoop, each rivet | 0.0008–0.0025 m | 0.00150, 0.00154 |
| Rivet dome proud of the hoop, each rivet | ≥ 0.0025 m | 0.00419, 0.00432 |
The hoop bite is binned by angular segment against the log's own radius function. A single global midpoint radius misclassifies outer chamfer vertices as inner ones on an out-of-round log, which is how a hoop that visibly floated on one side still passed.
The rivet seat is measured radially at each rivet vertex's own angle, against the hoop's outer face read off the mesh by a ray cast from the axis. Each rivet is aimed down the hoop's surface normal, not the radial: the log is out of round, so across one 12 mm head its surface falls by up to 1.4 mm, and radially aimed heads seated 1.87 and 2.82 mm against a 1.5 mm design bite, one edge sunk and the other lifted.
DECIMATE COLLAPSE triangle counts happen to agree across all three series here; the gate is still a ratio band, not an exact count. Bake pixels are stochastic; the gate is has_data plus operator FINISHED, not byte-identity. Construction uses no RNG: two runs on each binary print identical measurements, and the three binaries agree on every gated axis. Export byte counts differ by 16 B on 5.2.1 (glTF serializer), not a gated axis.
The haft has its own material slot. It used to share the bark's, which made the hickory handle the darkest wood on the piece.
Each violates one named budget. All seven were run on 4.5.11, 5.1.2 and 5.2.1 and returned the same code on each.
-| Flag | Budget violated | Exit | | --- | --- | --- | | --skip-decimate | LOD1 ratio band | 9 | | --stray-vert | loose vertex count is 0 | 15 | | --lift-z | bounding box zmin is 0 | 16 | | --fat-haft | haft clearance inside the eye | 17 | | --round-band | hoop bite into the log | 18 | | --float-rivets | rivet seat into the hoop | 18 | | --round-haft | haft section wide over thick | 19 |
| Flag | Budget violated | Exit |
|---|---|---|
--skip-decimate | LOD1 ratio band | 9 |
--stray-vert | loose vertex count is 0 | 15 |
--lift-z | bounding box zmin is 0 | 16 |
--fat-haft | haft clearance inside the eye | 17 |
--round-band | hoop bite into the log | 18 |
--float-rivets | rivet seat into the hoop | 18 |
--round-haft | haft section wide over thick | 19 |
--fat-haft thickens the haft across the cheeks only, which is the dimension the eye clearance measures. Scaling the whole oval section grew the knob 16 mm past the bounding box, so it exited 8 instead.
blender --background --python chopping_block.py --
@@ -296,7 +301,7 @@ Run
Smoke passes no flags.
Exit codes
File-local. 9 is a valid check code. 10 is reserved for gallery_framing.check_framing on the --output path. 15–19 are the hygiene and joint-fit family.
-| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Mesh did not build / no UV layer | | 4 | Base triangle count outside range | | 5 | Material count ≠ 4 distinct slots, or a face-count floor missed | | 6 | UVs outside 0..1 | | 7 | UV AABB overlap above tolerance | | 8 | World AABB off declared outer size | | 9 | LOD ratio band (--skip-decimate lands here) | | 10 | Framing gate (render path only) | | 11 | Collider triangle count above ceiling | | 12 | Bake did not finish or image has no data | | 13 | Export file missing or empty | | 14 | --output produced no file | | 15 | Mesh hygiene: loose, non-manifold, zero-area, doubles, n-gons, z-fight | | 16 | Not grounded: bounding box zmin off 0 | | 17 | Joint fit: shell count, eye clearance, engagement, breakout, bury, inset | | 18 | Contact fit: hoop bite band, haft fouling the log, or a rivet off its seat band | | 19 | Log out of plumb, off its stated real-world size, or a round haft section |
+Code Meaning 0 Success 1 Uncaught exception (FATAL wrapper) 2 argparse / usage 3 Mesh did not build / no UV layer 4 Base triangle count outside range 5 Material count ≠ 4 distinct slots, or a face-count floor missed 6 UVs outside 0..1 7 UV AABB overlap above tolerance 8 World AABB off declared outer size 9 LOD ratio band (--skip-decimate lands here) 10 Framing gate (render path only) 11 Collider triangle count above ceiling 12 Bake did not finish or image has no data 13 Export file missing or empty 14 --output produced no file15 Mesh hygiene: loose, non-manifold, zero-area, doubles, n-gons, z-fight 16 Not grounded: bounding box zmin off 0 17 Joint fit: shell count, eye clearance, engagement, breakout, bury, inset 18 Contact fit: hoop bite band, haft fouling the log, or a rivet off its seat band 19 Log out of plumb, off its stated real-world size, or a round haft section
A runnable example that builds a fire hydrant street prop — lathed barrel, bonnet dome, three outlet caps, operating nut — plus the collision a game prop pipeline ingests: a compound of four convex hull pieces, one per part group, each built with bmesh.ops.convex_hull from a coarse collision cage, following mesh-editing-and-bmesh.
Pipeline arc neighbor: LODs in lod-decimate-chain, tangent space in triangulate-tangents, export in gltf-export-roundtrip — collision bounds are the piece an engine's physics ingest checks first, and the piece whose failure rejects the whole prop.
Scope: this witnesses the bpy-level contract a prop pipeline relies on (watertight convex pieces that enclose the render mesh within budget). It is not an engine exporter and does not produce a shippable FiveM/engine asset — it proves the geometry properties such an asset must have.
-What it witnesses: engines (GTA/FiveM-style prop workflows included) ingest collision as a *compound of convex pieces*, each piece under a per-piece face limit (255 is the common cap). The example builds that structure and derives every property from closed forms, per piece:
-sec(π/n), so each n-gon cage ring *circumscribes* its render ring exactly. Proud details (a rim, a nut) must be paid for in cage rows; concave details (the grooves) are free — the hull bridges over them. That trade-off is the collision-authoring lesson.What it witnesses: engines (GTA/FiveM-style prop workflows included) ingest collision as a compound of convex pieces, each piece under a per-piece face limit (255 is the common cap). The example builds that structure and derives every property from closed forms, per piece:
+sec(π/n), so each n-gon cage ring circumscribes its render ring exactly. Proud details (a rim, a nut) must be paid for in cage rows; concave details (the grooves) are free — the hull bridges over them. That trade-off is the collision-authoring lesson.What each check catches on failure: an artist editing the render mesh after the hull was generated (probe: pumper lug stretched 2×, exit 3, measured escape 0.730000); inverted hull winding (probe: polygons flipped, exit 5, signed volume −1.022381, the exact negation); a non-watertight piece (probe: one face deleted, exit 4, three border edges).
Version witness: output is byte-identical on Blender 4.5.11 LTS and 5.1.2 — same piece counts, same volumes, same excursions. bmesh.ops.convex_hull and foreach_get are stable across the pair.
The render shows the prop inside its four faceted translucent hull pieces: if any piece failed to enclose its geometry, painted metal would poke through the shell in frame.
@@ -279,7 +284,7 @@Per-script sequential checks. 9 is a valid check code; there is no rule against it.
| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Render vertex escapes its hull (--shrink-hull lands here) | | 4 | Hull edge does not border exactly two faces | | 5 | Signed volume ≤ 0 (inverted winding) | | 6 | Hull vertex off its own face plane | | 7 | Euler characteristic ≠ 2 | | 8 | Piece over the 255-face collision budget | | 9 | --output produced no file |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Render vertex escapes its hull (--shrink-hull lands here) |
| 4 | Hull edge does not border exactly two faces |
| 5 | Signed volume ≤ 0 (inverted winding) |
| 6 | Hull vertex off its own face plane |
| 7 | Euler characteristic ≠ 2 |
| 8 | Piece over the 255-face collision budget |
| 9 | --output produced no file |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --shrink-hull.
A runnable example that builds an HSV color wheel disc entirely with bmesh and colors it with Mesh.color_attributes.new() — the modern attributes API, not the deprecated Mesh.vertex_colors alias AI code keeps reaching for. It witnesses the domain trap that comes with it: a CORNER-domain attribute is sized to len(mesh.loops), not len(mesh.vertices), so per-vertex data has to be expanded across face corners before it is written. The material wires the same attribute into a Shader Attribute node (attribute_type='GEOMETRY') feeding Base Color — the step AI code most often skips, leaving the mesh gray even when the attribute data is correct.
What it witnesses: a FLOAT_COLOR attribute on the CORNER domain, filled with one foreach_get (loop → vertex index) and one foreach_set (loop color), never a per-loop Python assignment. The check asserts the attribute is sized to the loop count and *not* the vertex count, that it is color_attributes.active_color (so a renderer or exporter actually picks it up), and that several probe loops match the closed-form HSV value for the vertex they reference. A separate check in the render path confirms the Attribute node is actually linked to Base Color, not just present in the node tree.
What it witnesses: a FLOAT_COLOR attribute on the CORNER domain, filled with one foreach_get (loop → vertex index) and one foreach_set (loop color), never a per-loop Python assignment. The check asserts the attribute is sized to the loop count and not the vertex count, that it is color_attributes.active_color (so a renderer or exporter actually picks it up), and that several probe loops match the closed-form HSV value for the vertex they reference. A separate check in the render path confirms the Attribute node is actually linked to Base Color, not just present in the node tree.
# Cheap correctness check (no render) — the CI check:
blender --background --python color_attribute_wheel.py --
@@ -273,7 +278,7 @@ Run
blender --background --python color_attribute_wheel.py -- --output wheel.png --engine cycles
Per-script sequential checks. 9 is a valid check code; there is no rule against it.
| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Topology ≠ closed form | | 4 | Color attribute missing | | 5 | Domain/type ≠ CORNER/FLOAT_COLOR (--point-domain lands here) | | 6 | Attribute sized to verts, not loops | | 7 | active_color not set | | 8 | Probe loop color off HSV closed form | | 9 | --output produced no file |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Topology ≠ closed form |
| 4 | Color attribute missing |
| 5 | Domain/type ≠ CORNER/FLOAT_COLOR (--point-domain lands here) |
| 6 | Attribute sized to verts, not loops |
| 7 | active_color not set |
| 8 | Probe loop color off HSV closed form |
| 9 | --output produced no file |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --point-domain.
Per-script sequential checks. 9 is a valid check code; there is no rule against it.
| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Compositor tree / Glare / Threshold / link-chain contract (--threshold-high lands here) | | 4 | Check render missing, tube dark, no halo, or halo does not fall off | | 5 | Halo present with compositing off | | 6 | --output produced no file |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Compositor tree / Glare / Threshold / link-chain contract (--threshold-high lands here) |
| 4 | Check render missing, tube dark, no halo, or halo does not fall off |
| 5 | Halo present with compositing off |
| 6 | --output produced no file |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --threshold-high.
!Three shipping crates stacked, each yawed and offset differently
A stack of three shipping crates. A showcase piece, not an example — it witnesses no API contract. It asserts that generated geometry meets declared asset budgets, recomputed from the finished mesh.
One build_crate generator, invoked three times with a per-instance seed, then the shipped pipeline:
| Shipped content | Used for | | --- | --- | | skills/mesh-editing-and-bmesh | box, bevel and UV construction in one bmesh | | skills/procedural-materials-and-shaders | two Principled materials with noise-driven wear | | skills/bake-high-to-low | Cycles tangent-space normal bake, high onto low | | skills/engine-export-presets | Unity glTF (export_yup=True) | | skills/depsgraph-and-evaluated-data | evaluated triangle counts for the LOD ratios | | snippets/decimate_to_budget.py | LOD1 / LOD2 COLLAPSE chain | | snippets/convex_hull_collider.py | convex collider | | snippets/lod_chain.py | LOD naming and ratio pattern | | examples/mesh-hygiene-audit | hygiene combinatorics (copied, not imported) |
| Shipped content | Used for |
|---|---|
skills/mesh-editing-and-bmesh | box, bevel and UV construction in one bmesh |
skills/procedural-materials-and-shaders | two Principled materials with noise-driven wear |
skills/bake-high-to-low | Cycles tangent-space normal bake, high onto low |
skills/engine-export-presets | Unity glTF (export_yup=True) |
skills/depsgraph-and-evaluated-data | evaluated triangle counts for the LOD ratios |
snippets/decimate_to_budget.py | LOD1 / LOD2 COLLAPSE chain |
snippets/convex_hull_collider.py | convex collider |
snippets/lod_chain.py | LOD naming and ratio pattern |
examples/mesh-hygiene-audit | hygiene combinatorics (copied, not imported) |
The point of the piece. Each crate comes out of the same generator; what differs is the seed. That seed drives the yaw, the plank widths, and the board heights, so the three read as three of the same design rather than one model pasted three times.
True instancing — three objects sharing one mesh datablock — and per-instance geometry variation are mutually exclusive. This piece takes the variation, and the shipped asset is the flattened single mesh an engine would receive. The generator is reused; the geometry is not.
Two independent random streams keep that honest. The design stream (yaw, plank widths) is what --same-seed collapses. The placement stream (lateral offsets) stays per-instance either way, so a falsified stack has the same footprint as a good one and fails on the variation budget rather than on the bounding box.
Declared in the script as named constants, recomputed from the generated mesh. Measured values below are from Blender 5.2.1; see the table at the end for the 4.5.11 / 5.1.2 figures.
-| Budget | Band | Measured | | --- | --- | --- | | Base triangles | 3200–7200 | 4380 | | LOD1 ratio | 0.32–0.62 | 0.5000 | | LOD2 ratio | 0.10–0.35 | 0.2005 (5.2) / 0.2199 (4.5, 5.1) | | Material slots | exactly 2, distinct | 2 | | Iron faces | ≥ 120 | 216 | | Timber faces | ≥ 600 | 2436 | | UV bounds | inside 0..1 | (0.0008, 0.0008)–(0.9992, 0.9992) | | UV AABB overlap | ≤ 1e-5 | 0.000000 | | Outer AABB | 0.658 × 0.524 × 0.704 m ± 0.020 | 0.6582 × 0.5235 × 0.7040 | | Crate body footprint | 0.585 × 0.425 m ± 0.015, in the crate's own frame | 0.5846 × 0.4246 (all three) | | Collider triangles | ≤ 260 | 202 | | Normal bake | {'FINISHED'} with image data | {'FINISHED'}, has_data=True | | glTF export | file written, non-empty | ~340 kB | | Hygiene | all zero | loose 0/0, non-manifold 0, zero-area 0, doubles 0, n-gons 0, coplanar disjoint pairs 0 | | Grounded AABB | \|zmin\| ≤ 1e-4 | 0.00000 | | Ground runners | 3 named supports, each zmin ≤ 1e-3 | 3 at 0.00000 | | Crate-to-crate seat | both seats, surface gap ≤ 0.0015 m | 0.00000 (overlapping) | | Yaw band | each \|yaw\| in 0.040–0.192 rad | 0.0681, 0.0943, 0.1093 | | Per-instance variation | yaw spread ≥ 0.020 rad, plank spread ≥ 0.0008 m | 0.0262 rad, 0.00205 m |
| Budget | Band | Measured | ||
|---|---|---|---|---|
| Base triangles | 3200–7200 | 4380 | ||
| LOD1 ratio | 0.32–0.62 | 0.5000 | ||
| LOD2 ratio | 0.10–0.35 | 0.2005 (5.2) / 0.2199 (4.5, 5.1) | ||
| Material slots | exactly 2, distinct | 2 | ||
| Iron faces | ≥ 120 | 216 | ||
| Timber faces | ≥ 600 | 2436 | ||
| UV bounds | inside 0..1 | (0.0008, 0.0008)–(0.9992, 0.9992) | ||
| UV AABB overlap | ≤ 1e-5 | 0.000000 | ||
| Outer AABB | 0.658 × 0.524 × 0.704 m ± 0.020 | 0.6582 × 0.5235 × 0.7040 | ||
| Crate body footprint | 0.585 × 0.425 m ± 0.015, in the crate's own frame | 0.5846 × 0.4246 (all three) | ||
| Collider triangles | ≤ 260 | 202 | ||
| Normal bake | {'FINISHED'} with image data | {'FINISHED'}, has_data=True | ||
| glTF export | file written, non-empty | ~340 kB | ||
| Hygiene | all zero | loose 0/0, non-manifold 0, zero-area 0, doubles 0, n-gons 0, coplanar disjoint pairs 0 | ||
| Grounded AABB | \ | zmin\ | ≤ 1e-4 | 0.00000 |
| Ground runners | 3 named supports, each zmin ≤ 1e-3 | 3 at 0.00000 | ||
| Crate-to-crate seat | both seats, surface gap ≤ 0.0015 m | 0.00000 (overlapping) | ||
| Yaw band | each \ | yaw\ | in 0.040–0.192 rad | 0.0681, 0.0943, 0.1093 |
| Per-instance variation | yaw spread ≥ 0.020 rad, plank spread ≥ 0.0008 m | 0.0262 rad, 0.00205 m |
Real-world size: each crate is 0.58 × 0.42 m at the timber and 0.585 × 0.425 m over the corner iron, 0.244 m tall including runners and lid. Three stacked come to 0.70 m — knee height, wider than tall.
The stack AABB is the union of three yawed boxes, so a crate could drift to any size underneath it and the outer budget would not notice. Each crate's footprint is therefore measured after un-rotating by the yaw that crate was *measured* to have, not the yaw it was built with. All three land on 0.5846 × 0.4246 m exactly, which is what makes the un-rotation trustworthy.
+The stack AABB is the union of three yawed boxes, so a crate could drift to any size underneath it and the outer budget would not notice. Each crate's footprint is therefore measured after un-rotating by the yaw that crate was measured to have, not the yaw it was built with. All three land on 0.5846 × 0.4246 m exactly, which is what makes the un-rotation trustworthy.
Every part is an axis-aligned box in its crate's frame and then rotated about Z. A world-AABB shape filter measures the rotated bounding box, not the part: a 0.554 m board yawed 0.09 rad reports 0.061 m of depth instead of its 0.013 m thickness, and every filter keyed to thickness silently matches nothing. xy_principal recovers each box's own axes, and hands back the yaw as a by-product — which is where the variation budget's yaw numbers come from.
Fixed seed 41; no unseeded randomness. Identical geometry across runs on one binary and across 4.5.11, 5.1.2 and 5.2.1 — every measured value above is byte-identical on the three except LOD2, where DECIMATE COLLAPSE produces 878 triangles on 5.2 and 934 on 4.5 and 5.1. That is why the LOD gate is a ratio band (0.10–0.35, measured 0.2005 and 0.2199) and not an exact count.
Each breaks one pipeline stage so a named budget fails. All six were run on 4.5.11, 5.1.2 and 5.2.1 and produced the same exit code on all three.
-| Flag | Breaks | Exit | | --- | --- | --- | | --skip-decimate | drops the DECIMATE modifiers, LOD1 ratio goes to 1.0000 | 9 | | --stray-vert | adds one loose vertex *inside* the silhouette, so hygiene catches it rather than the bounding box | 15 | | --lift-z | lifts the whole mesh 50 mm off the floor | 16 | | --short-skids | floats one of the three ground runners 12 mm; the other two still ground the AABB, so only the named-support budget sees it | 16 | | --float-stack | lifts the top crate 9 mm clear of the lid below, opening a 34 mm seat gap | 18 | | --same-seed | gives all three crates the same design seed; yaw spread and plank spread both go to 0 | 20 |
| Flag | Breaks | Exit |
|---|---|---|
--skip-decimate | drops the DECIMATE modifiers, LOD1 ratio goes to 1.0000 | 9 |
--stray-vert | adds one loose vertex inside the silhouette, so hygiene catches it rather than the bounding box | 15 |
--lift-z | lifts the whole mesh 50 mm off the floor | 16 |
--short-skids | floats one of the three ground runners 12 mm; the other two still ground the AABB, so only the named-support budget sees it | 16 |
--float-stack | lifts the top crate 9 mm clear of the lid below, opening a 34 mm seat gap | 18 |
--same-seed | gives all three crates the same design seed; yaw spread and plank spread both go to 0 | 20 |
File-local and sequential. 9 is a valid check code; there is no rule against it. 1 is the FATAL wrapper — a crash, never a named check.
| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Mesh did not build, or has no UV layer | | 4 | Base triangle count outside band | | 5 | Material slots, or a material's face floor | | 6 | UVs outside 0..1 | | 7 | UV AABB overlap above tolerance | | 8 | Outer AABB off declared size | | 9 | LOD1 or LOD2 ratio outside band (--skip-decimate) | | 10 | Framing gate (examples/gallery_framing.py, render path only) | | 11 | Collider triangles above ceiling | | 12 | Normal bake failed or produced no image data | | 13 | glTF export missing or empty | | 14 | --output produced no file | | 15 | Mesh hygiene (--stray-vert) | | 16 | Grounded zmin, or a named ground runner floating (--lift-z, --short-skids) | | 18 | Crate-to-crate seat gap (--float-stack) | | 19 | Crate body footprint off declared size | | 20 | Per-instance variation collapsed (--same-seed) |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Mesh did not build, or has no UV layer |
| 4 | Base triangle count outside band |
| 5 | Material slots, or a material's face floor |
| 6 | UVs outside 0..1 |
| 7 | UV AABB overlap above tolerance |
| 8 | Outer AABB off declared size |
| 9 | LOD1 or LOD2 ratio outside band (--skip-decimate) |
| 10 | Framing gate (examples/gallery_framing.py, render path only) |
| 11 | Collider triangles above ceiling |
| 12 | Normal bake failed or produced no image data |
| 13 | glTF export missing or empty |
| 14 | --output produced no file |
| 15 | Mesh hygiene (--stray-vert) |
| 16 | Grounded zmin, or a named ground runner floating (--lift-z, --short-skids) |
| 18 | Crate-to-crate seat gap (--float-stack) |
| 19 | Crate body footprint off declared size |
| 20 | Per-instance variation collapsed (--same-seed) |
17 is unused here: this piece has no diagonal member. 15–19 are reserved across showcase pieces for the hygiene family, so the numbering skips rather than reuses.
# Budget check, no render. ~1.0 s on 4.5/5.1, ~1.2 s on 5.2.
@@ -301,7 +305,7 @@ Run it
blender --background --python crate_stack.py -- --output stack.webp
Smoke runs the check-only path. It does not pass --output or any falsifier.
| Value | 4.5.11 | 5.1.2 | 5.2.1 | | --- | --- | --- | --- | | Base triangles | 4380 | 4380 | 4380 | | LOD1 tris / ratio | 2190 / 0.5000 | 2190 / 0.5000 | 2190 / 0.5000 | | LOD2 tris / ratio | 934 / 0.2199 | 934 / 0.2199 | 878 / 0.2005 | | Outer AABB | 0.6582 × 0.5235 × 0.7040 | same | same | | Collider tris | 202 | 202 | 202 | | Yaws (rad) | 0.0943, −0.1093, 0.0681 | same | same | | Seat gap | 0.00000 | 0.00000 | 0.00000 | | Check wall-clock | ~0.99 s | ~1.01 s | ~1.20 s |
+| Value | 4.5.11 | 5.1.2 | 5.2.1 |
|---|---|---|---|
| Base triangles | 4380 | 4380 | 4380 |
| LOD1 tris / ratio | 2190 / 0.5000 | 2190 / 0.5000 | 2190 / 0.5000 |
| LOD2 tris / ratio | 934 / 0.2199 | 934 / 0.2199 | 878 / 0.2005 |
| Outer AABB | 0.6582 × 0.5235 × 0.7040 | same | same |
| Collider tris | 202 | 202 | 202 |
| Yaws (rad) | 0.0943, −0.1093, 0.0681 | same | same |
| Seat gap | 0.00000 | 0.00000 | 0.00000 |
| Check wall-clock | ~0.99 s | ~1.01 s | ~1.20 s |
The --output render path measures framing against the Layer 1 band via examples/gallery_framing.py (exit 10 on violation) before writing the still.
Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper.
| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage; also an active object existed after the data-API build | | 3 | Custom ID property did not land | | 4 | property_unset on a custom ID key did not TypeError, or it deleted the key | | 5 | del did not report removal | | 6 | Keep plate lost the ID property | | 7 | Clear plate still has the ID property (--skip-delete / --unset-instead land here) | | 10 | Gallery framing violation | | 12 | --output produced no file |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage; also an active object existed after the data-API build |
| 3 | Custom ID property did not land |
| 4 | property_unset on a custom ID key did not TypeError, or it deleted the key |
| 5 | del did not report removal |
| 6 | Keep plate lost the ID property |
| 7 | Clear plate still has the ID property (--skip-delete / --unset-instead land here) |
| 10 | Gallery framing violation |
| 12 | --output produced no file |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output, --skip-delete, or --unset-instead.
Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is also the shared framing helper.
| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Spline type is not BEZIER | | 4 | Bezier point count ≠ 8 | | 5 | bevel_depth ≠ 0.15 | | 6 | use_fill_caps is False (--no-caps lands here) | | 7 | Evaluated vert/face count off measured tessellation | | 8 | Tube does not rest on the floor | | 9 | Tube height ≠ 2 × bevel | | 10 | X span off closed form; also gallery framing violation | | 11 | --output produced no file |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Spline type is not BEZIER |
| 4 | Bezier point count ≠ 8 |
| 5 | bevel_depth ≠ 0.15 |
| 6 | use_fill_caps is False (--no-caps lands here) |
| 7 | Evaluated vert/face count off measured tessellation |
| 8 | Tube does not rest on the floor |
| 9 | Tube height ≠ 2 × bevel |
| 10 | X span off closed form; also gallery framing violation |
| 11 | --output produced no file |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --no-caps.
Pipeline arc neighbor: collision in collision-hull-proxy (the pair partner — same prop-pipeline audience), tangent space in triangulate-tangents — normal maps are baked against exactly this shading, and engines harden or soften the same edges at ingest.
Scope: this witnesses the bpy-level contract a prop pipeline relies on. It is not an engine exporter and does not claim engine/FiveM compatibility — it proves the mesh-data properties such an asset's shading must have.
What it witnesses:
-use_auto_smooth, use_custom_normals, and calc_normals are AttributeError on 4.5 LTS *and* 5.1. AI-generated Blender code still emits mesh.use_auto_smooth = True constantly; any script carrying that habit dies immediately, and this check keeps it dead.mesh.set_sharp_from_angle(30°) (which also sets the face smooth flags — probed on both versions) marks sharp exactly the edges whose *independently recomputed* dihedral angle crosses the threshold: Shell 48 of 72 edges, rib 12 of 12, neck 128 of 304 — an exact set match, not a count approximation.normals_split_custom_set stores per-loop normals in int16, so a 144-loop round-trip reads back within 1.407e-04 (tol 2e-4), unit length within 1.1e-07. Asserting float-exact custom normals is a real bug this check catches.shade_auto_smooth OPERATOR is a version-split trap. It builds the Smooth-by-Angle node-group modifier from a bundled asset. Headless on 4.5 LTS the asset load never finishes: the op returns {'CANCELLED'} — no exception — and the mesh stays untouched (measured: 0 smooth faces, 0 modifiers). Any script that ignores the return set ships flat shading and never knows. On 5.1 it FINISHES and adds the Smooth by Angle NODES modifier. The portable path is the data API above, version-gated here explicitly.use_auto_smooth, use_custom_normals, and calc_normals are AttributeError on 4.5 LTS and 5.1. AI-generated Blender code still emits mesh.use_auto_smooth = True constantly; any script carrying that habit dies immediately, and this check keeps it dead.mesh.set_sharp_from_angle(30°) (which also sets the face smooth flags — probed on both versions) marks sharp exactly the edges whose independently recomputed dihedral angle crosses the threshold: Shell 48 of 72 edges, rib 12 of 12, neck 128 of 304 — an exact set match, not a count approximation.normals_split_custom_set stores per-loop normals in int16, so a 144-loop round-trip reads back within 1.407e-04 (tol 2e-4), unit length within 1.1e-07. Asserting float-exact custom normals is a real bug this check catches.shade_auto_smooth OPERATOR is a version-split trap. It builds the Smooth-by-Angle node-group modifier from a bundled asset. Headless on 4.5 LTS the asset load never finishes: the op returns {'CANCELLED'} — no exception — and the mesh stays untouched (measured: 0 smooth faces, 0 modifiers). Any script that ignores the return set ships flat shading and never knows. On 5.1 it FINISHES and adds the Smooth by Angle NODES modifier. The portable path is the data API above, version-gated here explicitly.What each check catches on failure: author/audit threshold drift (probe: sharp marks applied at 20° but audited at 30°, exit 5, 4 extra edges); the two halves of the contract out of sync (probe: sharp_edge set but face smooth flags lost, exit 6, smooth-edge loops split by 3.83e-01); float-exactness assumed of custom normals (probe: tolerance 1e-6, exit 7, measured 1.407e-04); and any future version that resurrects the legacy API or changes the operator's headless behavior (exit 3/8).
Version witness: every value above is identical on Blender 4.5.11 LTS and 5.1.2 except the shade_auto_smooth operator behavior, which is asserted per version (CANCELLED + untouched on 4.5, FINISHED + NODES modifier on 5.1).
The render shows the same can shaded three ways — flat (faceted corners), smooth-everywhere (the smeared AI bug: ribs melt, highlights warp at the rim), and by-angle (the contract: smooth walls, crisp edges) — under a strip light whose reflection exposes every normal discontinuity.
@@ -279,7 +284,7 @@Per-script sequential checks. 9 is a valid check code; there is no rule against it.
| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Legacy shading API present, or modern path missing | | 4 | Non-manifold edges (dihedral test undefined) | | 5 | Sharp set ≠ independent dihedral (--mismatch-angle lands here) | | 6 | Evaluated loop normals not welded/split as the sharp set promises | | 7 | Custom split normals lost or dequantized in evaluation | | 8 | shade_auto_smooth operator behavior drifted from the version split | | 9 | --output produced no file |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Legacy shading API present, or modern path missing |
| 4 | Non-manifold edges (dihedral test undefined) |
| 5 | Sharp set ≠ independent dihedral (--mismatch-angle lands here) |
| 6 | Evaluated loop normals not welded/split as the sharp set promises |
| 7 | Custom split normals lost or dequantized in evaluation |
| 8 | shade_auto_smooth operator behavior drifted from the version split |
| 9 | --output produced no file |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --mismatch-angle.
Per-script sequential checks. 9 is a valid check code; there is no rule against it.
| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Needle count ≠ 12 | | 4 | Needle does not carry exactly one DAMPED_TRACK | | 5 | Constraint target is not Core | | 6 | track_axis is not TRACK_Z | | 7 | Constraint muted or influence < 1 (--mute lands here) | | 8 | TRACK_TO still present | | 9 | Evaluated aim dot below 0.998 | | 10 | --output produced no file |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Needle count ≠ 12 |
| 4 | Needle does not carry exactly one DAMPED_TRACK |
| 5 | Constraint target is not Core |
| 6 | track_axis is not TRACK_Z |
| 7 | Constraint muted or influence < 1 (--mute lands here) |
| 8 | TRACK_TO still present |
| 9 | Evaluated aim dot below 0.998 |
| 10 | --output produced no file |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --mute.
Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper.
| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Safe bevel produced zero-area faces | | 4 | Degenerate bevel zero-area count ≠ closed form (--both-safe lands here) | | 5 | min_area collapse under 1e5× | | 6 | Coincident-position count off the closed form | | 7 | Safe GLB carries degenerate triangles | | 8 | Degenerate GLB triangle or position count drifted | | 9 | --output produced no file | | 10 | Gallery framing violation |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Safe bevel produced zero-area faces |
| 4 | Degenerate bevel zero-area count ≠ closed form (--both-safe lands here) |
| 5 | min_area collapse under 1e5× |
| 6 | Coincident-position count off the closed form |
| 7 | Safe GLB carries degenerate triangles |
| 8 | Degenerate GLB triangle or position count drifted |
| 9 | --output produced no file |
| 10 | Gallery framing violation |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --both-safe.
Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper.
| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Evaluated mesh did not apply the modifier | | 4 | No OBJ written | | 5 | Export vert count ≠ evaluated (--unevaluated lands here) | | 6 | --output produced no file | | 10 | Gallery framing violation |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Evaluated mesh did not apply the modifier |
| 4 | No OBJ written |
| 5 | Export vert count ≠ evaluated (--unevaluated lands here) |
| 6 | --output produced no file |
| 10 | Gallery framing violation |
--obj is a path selector, not a falsifier.
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output, --obj, or --unevaluated.
The --output render path additionally measures framing against the Layer 1 band via examples/gallery_framing.py (exit 10 on violation) before writing the still.
Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper.
| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Evaluated Z scale ≠ wave_scale (--flat-expr lands here) | | 4 | Original datablock was not flushed | | 6 | --output produced no file | | 10 | Gallery framing violation |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Evaluated Z scale ≠ wave_scale (--flat-expr lands here) |
| 4 | Original datablock was not flushed |
| 6 | --output produced no file |
| 10 | Gallery framing violation |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --flat-expr.
Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper.
| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage; also exporter RNA missing expected glTF kwargs | | 3 | Source mast is not Z-dominant, or tip drifted | | 4 | glTF reimport produced no mesh | | 5 | Unity disk POSITION is not (x, z, -y), or Unity node has rotation | | 6 | Godot disk POSITION is not raw Z-up; also --output produced no file | | 7 | Unity reimport is not standing | | 8 | Godot reimport is not lying along Y, or reimported tip mismatch | | 9 | Reimported orientations did not differ (--same-axis lands here) | | 10 | Gallery framing violation | | 11 | --same-axis did not collapse the axis difference |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage; also exporter RNA missing expected glTF kwargs |
| 3 | Source mast is not Z-dominant, or tip drifted |
| 4 | glTF reimport produced no mesh |
| 5 | Unity disk POSITION is not (x, z, -y), or Unity node has rotation |
| 6 | Godot disk POSITION is not raw Z-up; also --output produced no file |
| 7 | Unity reimport is not standing |
| 8 | Godot reimport is not lying along Y, or reimported tip mismatch |
| 9 | Reimported orientations did not differ (--same-axis lands here) |
| 10 | Gallery framing violation |
| 11 | --same-axis did not collapse the axis difference |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --same-axis.
Intended size: 1.60 m bay, 1.14 m posts, 0.11 m post stock; outer AABB 1.624 × 0.134 × 1.232 m.
Declared as named constants; every gate recomputes from the mesh, materials, UVs, evaluated LOD, collider, or export file.
-| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) | | --- | --- | --- | | Base triangles | 1000–1400 | 1092 / 1092 / 1092 | | LOD1 ratio | 0.32–0.62 of base | 0.5000 / 0.5000 / 0.5000 | | LOD2 ratio | 0.10–0.35 of base | 0.2198 / 0.2198 / 0.1832 | | Materials | exactly 2 distinct, ≥48 metal, ≥24 wood | 2 slots, 156 metal / 390 wood | | UVs | in 0..1, AABB overlap ≤ 1e-5 | in range, overlap 0 | | Outer AABB | (1.624, 0.134, 1.232) m ± 0.015 | (1.6240, 0.1340, 1.2320), zmin 0 | | Collider tris | ≤ 180 | 32 | | Export | written, size > 0 | 82216 / 82216 / 82208 bytes |
| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) |
|---|---|---|
| Base triangles | 1000–1400 | 1092 / 1092 / 1092 |
| LOD1 ratio | 0.32–0.62 of base | 0.5000 / 0.5000 / 0.5000 |
| LOD2 ratio | 0.10–0.35 of base | 0.2198 / 0.2198 / 0.1832 |
| Materials | exactly 2 distinct, ≥48 metal, ≥24 wood | 2 slots, 156 metal / 390 wood |
| UVs | in 0..1, AABB overlap ≤ 1e-5 | in range, overlap 0 |
| Outer AABB | (1.624, 0.134, 1.232) m ± 0.015 | (1.6240, 0.1340, 1.2320), zmin 0 |
| Collider tris | ≤ 180 | 32 |
| Export | written, size > 0 | 82216 / 82216 / 82208 bytes |
Base triangles dropped from 1212 to 1092 in the quality pass: rail collars became U-wraps (no span-facing plate). Outer AABB tightened from 1.642 × 0.157 × 1.235 m.
DECIMATE COLLAPSE triangle counts are not identical across series — 5.2.1 is more aggressive on LOD2. The gate is a ratio band, not an exact count. Bake pixels are stochastic; the gate is has_data plus operator FINISHED, not byte-identity. Construction uses no RNG. Export byte counts differ by 8 B on 5.2.1 (glTF serializer), not a gated axis.
Recomputed from the generated mesh, not asserted about the script.
-| Axis | Declared | Measured (all three) | | --- | --- | --- | | Non-manifold edges | 0 | 0 | | Loose verts / edges | 0 / 0 | 0 / 0 | | Doubles merged at 1e-5 | 0 | 0 | | Zero-area faces | 0 | 0 | | N-gons | 0 | 0 | | Coplanar disjoint face pairs | 0 | 0 | | Grounded: zmin | within 1e-4 of 0 | 0.0000 | | Named shoes | 2, each zmin ≤ 0.002 | 2, 0.00000 |
| Axis | Declared | Measured (all three) |
|---|---|---|
| Non-manifold edges | 0 | 0 |
| Loose verts / edges | 0 / 0 | 0 / 0 |
| Doubles merged at 1e-5 | 0 | 0 |
| Zero-area faces | 0 | 0 |
| N-gons | 0 | 0 |
| Coplanar disjoint face pairs | 0 | 0 |
Grounded: zmin | within 1e-4 of 0 | 0.0000 |
| Named shoes | 2, each zmin ≤ 0.002 | 2, 0.00000 |
| Axis | Declared | Measured (all three) | | --- | --- | --- | | Brace-to-post BVH gap (both posts) | ≤ 0.008 m | 0.00240 | | Rail-to-post BVH gap | ≤ 0.008 m | 0.00093 | | Rail span vs 2·RAIL_HALF | ± 0.04 m | 1.3900 vs 1.390 |
| Axis | Declared | Measured (all three) |
|---|---|---|
| Brace-to-post BVH gap (both posts) | ≤ 0.008 m | 0.00240 |
| Rail-to-post BVH gap | ≤ 0.008 m | 0.00093 |
Rail span vs 2·RAIL_HALF | ± 0.04 m | 1.3900 vs 1.390 |
Each violates one named budget. All were run on 4.5.11, 5.1.2 and 5.2.1 and returned the same code on each.
-| Flag | Budget violated | Exit | | --- | --- | --- | | --skip-decimate | LOD1 ratio band | 9 | | --stray-vert | loose vertex count is 0 | 15 | | --lift-z | bounding box zmin is 0 | 16 | | --short-shoes | named shoes plant at z=0 | 16 | | --short-brace | brace-to-post BVH gap (both posts) | 17 | | --gap-rails | rail-to-post BVH gap | 18 | | --long-rails | rail span vs 2·RAIL_HALF | 19 |
| Flag | Budget violated | Exit |
|---|---|---|
--skip-decimate | LOD1 ratio band | 9 |
--stray-vert | loose vertex count is 0 | 15 |
--lift-z | bounding box zmin is 0 | 16 |
--short-shoes | named shoes plant at z=0 | 16 |
--short-brace | brace-to-post BVH gap (both posts) | 17 |
--gap-rails | rail-to-post BVH gap | 18 |
--long-rails | rail span vs 2·RAIL_HALF | 19 |
--short-shoes lifts only the iron shoes. After recenter a post or kick is the AABB zmin, so the AABB gate would still pass; the named-shoe stations fail. --short-brace shortens the left station; brace_gap is the max of the per-post minima so a brace that still hits the right post cannot sneak through. --long-rails adds an extra spanning board; the tenoned rails stay put so the gap budget still passes.
blender --background --python fence_kit.py --
@@ -291,7 +296,7 @@ Run
Smoke passes no flags.
Exit codes
File-local. 9 is a valid check code. 10 is reserved for gallery_framing.check_framing on the --output path. 15–19 are the hygiene and joint-fit family.
-| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Mesh did not build / no UV layer | | 4 | Base triangle count outside range | | 5 | Material count ≠ 2 distinct slots, or wood/metal faces missing | | 6 | UVs outside 0..1 | | 7 | UV AABB overlap above tolerance | | 8 | World AABB off declared outer size | | 9 | LOD ratio band (--skip-decimate lands here) | | 10 | Framing gate (render path only) | | 11 | Collider triangle count above ceiling | | 12 | Bake did not finish or image has no data | | 13 | Export file missing or empty | | 14 | --output produced no file | | 15 | Hygiene (--stray-vert lands here) | | 16 | Grounded zmin / named shoes (--lift-z, --short-shoes) | | 17 | Brace-to-post gap (--short-brace) | | 18 | Rail-to-post gap (--gap-rails) | | 19 | Rail span (--long-rails) |
+Code Meaning 0 Success 1 Uncaught exception (FATAL wrapper) 2 argparse / usage 3 Mesh did not build / no UV layer 4 Base triangle count outside range 5 Material count ≠ 2 distinct slots, or wood/metal faces missing 6 UVs outside 0..1 7 UV AABB overlap above tolerance 8 World AABB off declared outer size 9 LOD ratio band (--skip-decimate lands here) 10 Framing gate (render path only) 11 Collider triangle count above ceiling 12 Bake did not finish or image has no data 13 Export file missing or empty 14 --output produced no file15 Hygiene (--stray-vert lands here) 16 Grounded zmin / named shoes (--lift-z, --short-shoes) 17 Brace-to-post gap (--short-brace) 18 Rail-to-post gap (--gap-rails) 19 Rail span (--long-rails)
Per-script sequential checks. 9 is a valid check code; there is no rule against it.
| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Exporter/importer RNA kwargs drifted | | 4 | Base cage drifted from its closed form | | 5 | Authored UVs drifted from the box-map closed form | | 6 | Bevel produced no evaluated geometry | | 7 | On-disk node/mesh/generator contract drifted | | 8 | On-disk primitive/material binding count drifted | | 9 | On-disk POSITION bounds ≠ axis-converted bbox (--no-yup lands here) | | 10 | On-disk POSITION count ≠ evaluated loop count | | 11 | On-disk UV V-flip failed | | 12 | Re-import did not produce exactly one mesh | | 13 | Re-imported object carries a transform | | 14 | Material names drifted on re-import | | 15 | Re-import vert count ≠ evaluated loop count | | 16 | Round-trip position drift | | 17 | Round-trip normal drift | | 18 | Round-trip UV drift | | 19 | Re-import triangle count drifted | | 20 | Per-triangle material bindings drifted | | 21 | --output produced no file |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Exporter/importer RNA kwargs drifted |
| 4 | Base cage drifted from its closed form |
| 5 | Authored UVs drifted from the box-map closed form |
| 6 | Bevel produced no evaluated geometry |
| 7 | On-disk node/mesh/generator contract drifted |
| 8 | On-disk primitive/material binding count drifted |
| 9 | On-disk POSITION bounds ≠ axis-converted bbox (--no-yup lands here) |
| 10 | On-disk POSITION count ≠ evaluated loop count |
| 11 | On-disk UV V-flip failed |
| 12 | Re-import did not produce exactly one mesh |
| 13 | Re-imported object carries a transform |
| 14 | Material names drifted on re-import |
| 15 | Re-import vert count ≠ evaluated loop count |
| 16 | Round-trip position drift |
| 17 | Round-trip normal drift |
| 18 | Round-trip UV drift |
| 19 | Re-import triangle count drifted |
| 20 | Per-triangle material bindings drifted |
| 21 | --output produced no file |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --no-yup.
Per-script sequential checks. 9 is a valid check code; there is no rule against it.
| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Exporter RNA missing expected kwargs | | 4 | Vertex group count ≠ bone count | | 5 | Disk skins ≠ 1 (--no-skins lands here) | | 6 | Skin joints ≠ bone names | | 7 | Missing JOINTS_0/WEIGHTS_0, or accessor length mismatch | | 8 | Disk weight sums off 1.0 | | 9 | Disk verts exceed evaluated loops | | 10 | Armature count after import ≠ 1 | | 11 | Bone count drifted | | 12 | Named bone lost | | 13 | Bone parent drifted | | 14 | Rest matrices drifted | | 15 | Skinned mesh count after import ≠ 1 | | 16 | Re-import vert count ≠ disk | | 17 | Vertex group names drifted | | 18 | Weight round-trip drifted | | 19 | Deformation round-trip drifted | | 20 | --output produced no file |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Exporter RNA missing expected kwargs |
| 4 | Vertex group count ≠ bone count |
| 5 | Disk skins ≠ 1 (--no-skins lands here) |
| 6 | Skin joints ≠ bone names |
| 7 | Missing JOINTS_0/WEIGHTS_0, or accessor length mismatch |
| 8 | Disk weight sums off 1.0 |
| 9 | Disk verts exceed evaluated loops |
| 10 | Armature count after import ≠ 1 |
| 11 | Bone count drifted |
| 12 | Named bone lost |
| 13 | Bone parent drifted |
| 14 | Rest matrices drifted |
| 15 | Skinned mesh count after import ≠ 1 |
| 16 | Re-import vert count ≠ disk |
| 17 | Vertex group names drifted |
| 18 | Weight round-trip drifted |
| 19 | Deformation round-trip drifted |
| 20 | --output produced no file |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --no-skins.
Per-script sequential checks. 9 is a valid check code; there is no rule against it.
| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Carrier vertex count ≠ 1 | | 4 | Corner instance vert count ≠ 8 (--one-cell lands here) | | 5 | Evaluated topology ≠ 72 verts / 54 faces | | 6 | Set Material did not carry Lime | | 7 | Corner instance center off the closed-form grid point | | 8 | --output produced no file |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Carrier vertex count ≠ 1 |
| 4 | Corner instance vert count ≠ 8 (--one-cell lands here) |
| 5 | Evaluated topology ≠ 72 verts / 54 faces |
| 6 | Set Material did not carry Lime |
| 7 | Corner instance center off the closed-form grid point |
| 8 | --output produced no file |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --one-cell.
Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper.
| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Scale input identifier missing on the tree interface | | 4 | Modifiers do not share one node_group | | 5 | Version-path write raised (--api dict on 5.2, --api rna on 4.5) | | 6 | Version-path read raised | | 7 | Readback ≠ intended scale (--same-scale lands here) | | 8 | Evaluated Z-extent ≠ intended scale | | 9 | Evaluated mesh not sitting on z=0 | | 10 | Gallery framing violation | | 11 | Evaluated extents not distinct | | 12 | --output produced no file |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Scale input identifier missing on the tree interface |
| 4 | Modifiers do not share one node_group |
| 5 | Version-path write raised (--api dict on 5.2, --api rna on 4.5) |
| 6 | Version-path read raised |
| 7 | Readback ≠ intended scale (--same-scale lands here) |
| 8 | Evaluated Z-extent ≠ intended scale |
| 9 | Evaluated mesh not sitting on z=0 |
| 10 | Gallery framing violation |
| 11 | Evaluated extents not distinct |
| 12 | --output produced no file |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output, --same-scale, or --api dict/rna.
| Probe | Binary | Result | | --- | --- | --- | | --api dict | 5.2.1 LTS | exit 5, TypeError: id properties not supported for this type | | --api rna | 4.5.11 LTS | exit 5, AttributeError: 'NodesModifier' object has no attribute 'properties' | | --api auto | 5.2.1, 5.1.2, 4.5.11 | exit 0, extents 1.000 / 2.000 / 3.000 |
| Probe | Binary | Result |
|---|---|---|
--api dict | 5.2.1 LTS | exit 5, TypeError: id properties not supported for this type |
--api rna | 4.5.11 LTS | exit 5, AttributeError: 'NodesModifier' object has no attribute 'properties' |
--api auto | 5.2.1, 5.1.2, 4.5.11 | exit 0, extents 1.000 / 2.000 / 3.000 |
Per-script sequential checks. 9 is a valid check code; there is no rule against it.
| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | SDF remesh produced no/unchanged geometry (--no-sdf lands here) | | 4 | --output produced no file | | 5 | Wrong-era EEVEE engine id was accepted | | 6 | Input material dropped by remesh |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | SDF remesh produced no/unchanged geometry (--no-sdf lands here) |
| 4 | --output produced no file |
| 5 | Wrong-era EEVEE engine id was accepted |
| 6 | Input material dropped by remesh |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --no-sdf.
A jo-block (steel plinth + copper column) whose Geometry Nodes tree witnesses the 5.2 socket-identifier collapse on FunctionNodeCompare and FunctionNodeRandomValue. Follows geometry-nodes-python.
On 4.5.11 and 5.1.2, Compare INT exposes A_INT / B_INT and Random Value FLOAT exposes Min_001 / Max_001 / Value_001. On 5.2.1 those identifiers are gone — the live sockets reuse A / B and Min / Max / Value. Looking up the unique *enabled* socket of a given name wires on all three. Hard-coding the pre-5.2 identifiers raises on 5.2.
On 4.5.11 and 5.1.2, Compare INT exposes A_INT / B_INT and Random Value FLOAT exposes Min_001 / Max_001 / Value_001. On 5.2.1 those identifiers are gone — the live sockets reuse A / B and Min / Max / Value. Looking up the unique enabled socket of a given name wires on all three. Hard-coding the pre-5.2 identifiers raises on 5.2.
What it witnesses: identifier-agnostic wiring produces a 16-vert evaluated mesh (plinth + column) with POINT gauge_h == 1.80 on exactly the eight column verts. Compare INT 7 > 2 switches the column in (vert count). Random Value FLOAT with min=max=HEIGHT is stored as a named attribute (Random is a field; using it as a constant Size source evaluates to 0 — hazard found while authoring). zmax 1.94 is the construction axis, not the Random axis.
Scaffolding matches cross-version-property-delete (check() returns, argparse naive-API flag, FATAL wrapper). That example does not version-branch, so the per-version inventory assert is the gn-modifier-inputs shape.
What failure each check would catch:
@@ -276,7 +281,7 @@The still witnesses both axes, not only the Compare one. The column's graduations — a tick every 0.1 m in from each vertical corner, a heavier one every 0.5 m — are drawn by the column shader from the evaluated POINT gauge_h attribute, and only up to its stored height. With Compare broken the column is missing; with Random Value unwired the Store default leaves gauge_h at 0 and the column renders blank copper. Both were rendered to confirm it. The shader is render-path only and nothing in it feeds back into check(); the column is also turned to 18° for the still so two faces show, which check() cannot see because it reads object-space vertices.
Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper.
| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 4 | Identifier inventory wrong for this Blender version | | 5 | Pre-5.2 identifier missing (--legacy-ids on 5.2) | | 6 | Enabled-name socket lookup failed | | 7 | Evaluated vert count off (Compare did not switch the column in) | | 8 | Evaluated zmax off closed form | | 9 | POINT gauge_h not 1.80 on eight column verts | | 10 | Gallery framing violation | | 12 | --output produced no file |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 4 | Identifier inventory wrong for this Blender version |
| 5 | Pre-5.2 identifier missing (--legacy-ids on 5.2) |
| 6 | Enabled-name socket lookup failed |
| 7 | Evaluated vert count off (Compare did not switch the column in) |
| 8 | Evaluated zmax off closed form |
| 9 | POINT gauge_h not 1.80 on eight column verts |
| 10 | Gallery framing violation |
| 12 | --output produced no file |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --legacy-ids.
The --output render path measures framing via examples/gallery_framing.py (exit 10 on violation).
Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper.
| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage; also carrier mesh was rewritten | | 3 | Repeat evaluated vert/face count off closed form | | 4 | Repeat X-centers off closed form | | 5 | For Each evaluated vert/face count off closed form | | 6 | For Each Z-centers off closed form | | 10 | Gallery framing violation | | 12 | --output produced no file |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage; also carrier mesh was rewritten |
| 3 | Repeat evaluated vert/face count off closed form |
| 4 | Repeat X-centers off closed form |
| 5 | For Each evaluated vert/face count off closed form |
| 6 | For Each Z-centers off closed form |
| 10 | Gallery framing violation |
| 12 | --output produced no file |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output, --no-offset, --unpair-foreach, or --unpair-repeat.
Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper.
| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage; also GPv3 collection address contract | | 3 | LINEART thickness / radius trap for this Blender | | 4 | LINEART type, source, or use_contour (--no-contour lands here) | | 5 | Evaluated contour too thin | | 6 | Cleared source_object still produced strokes | | 7 | Contour+crease off still produced strokes, or restore failed | | 8 | --output produced no file | | 10 | Gallery framing violation |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage; also GPv3 collection address contract |
| 3 | LINEART thickness / radius trap for this Blender |
| 4 | LINEART type, source, or use_contour (--no-contour lands here) |
| 5 | Evaluated contour too thin |
| 6 | Cleared source_object still produced strokes |
| 7 | Contour+crease off still produced strokes, or restore failed |
| 8 | --output produced no file |
| 10 | Gallery framing violation |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --no-contour.
A runnable example that draws five nested neon rose curves as Grease Pencil v3 strokes — layer → frames.new(1).drawing → add_strokes([counts]) → per-point position, radius, opacity, vertex_color — the attribute-based API that replaced legacy GPencil across the 4.x-to-5.x window, the largest bpy API break the gallery witnesses.
What it witnesses: GPv3 lives at *different addresses* on the two supported versions, and the check asserts each side's actual contract:
+What it witnesses: GPv3 lives at different addresses on the two supported versions, and the check asserts each side's actual contract:
bpy.data.grease_pencils_v3 (GreasePencilv3), while bpy.data.grease_pencils still holds legacy GPencil whose frames carry .strokes directly and have no .drawing. Same collection name, incompatible API — the trap is asserted, not just avoided.bpy.data.grease_pencils name, and grease_pencils_v3 / bpy.types.GPencilStroke no longer exist.On the shared GPv3 surface it then asserts that stroke points are *views over attribute layers*: writing pt.radius / pt.opacity / pt.vertex_color lazily materializes radius, opacity, vertex_color (POINT) and cyclic (CURVE) layers in drawing.attributes, and every closed-form rose position round-trips exactly through the raw position attribute buffer via foreach_get.
On the shared GPv3 surface it then asserts that stroke points are views over attribute layers: writing pt.radius / pt.opacity / pt.vertex_color lazily materializes radius, opacity, vertex_color (POINT) and cyclic (CURVE) layers in drawing.attributes, and every closed-form rose position round-trips exactly through the raw position attribute buffer via foreach_get.
# Cheap correctness check (no render) — the CI check:
blender --background --python grease_pencil_rosette.py --
@@ -275,7 +280,7 @@ Run
blender --background --python grease_pencil_rosette.py -- --output rosette.png --engine cycles
Per-script sequential checks. 9 is a valid check code; there is no rule against it.
| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | GPv3 address / legacy trap for this Blender | | 4 | Object type, layer count, or frame number | | 5 | Stroke topology or cyclic (--open-strokes lands here) | | 6 | Lazy attribute layers missing or wrong domain/type | | 7 | Position buffer length or closed-form round-trip | | 8 | Grease Pencil material missing gpencil settings | | 9 | --output produced no file |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | GPv3 address / legacy trap for this Blender |
| 4 | Object type, layer count, or frame number |
| 5 | Stroke topology or cyclic (--open-strokes lands here) |
| 6 | Lazy attribute layers missing or wrong domain/type |
| 7 | Position buffer length or closed-form round-trip |
| 8 | Grease Pencil material missing gpencil settings |
| 9 | --output produced no file |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --open-strokes.
Composes skills mesh-editing-and-bmesh, bake-high-to-low, depsgraph-and-evaluated-data, engine-export-presets, and snippets bake_normal_high_to_low.py, setup_bake_target_image.py, lod_chain.py / decimate_to_budget.py, convex_hull_collider.py, export_preset_unity.py (helpers copied, not imported as a package).
Declared as named constants; every gate recomputes from the mesh, materials, UVs, evaluated LOD, collider, or export file.
-| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) | | --- | --- | --- | | Base triangles | 700–1000 | 928 / 928 / 928 | | LOD1 ratio | 0.32–0.62 of base | 0.5000 / 0.5000 / 0.5000 | | LOD2 ratio | 0.10–0.35 of base | 0.2198 / 0.2198 / 0.1897 | | Materials | exactly 3 distinct; ≥80 wood, stone, metal faces | 3 slots; 216 / 160 / 180 | | UVs | in 0..1, AABB overlap ≤ 1e-5 | in range, overlap 0 | | Outer AABB | (0.710, 0.419, 0.610) m ± 0.015 | (0.7100, 0.4185, 0.6100), zmin 0 | | Collider tris | ≤ 240 | 198 | | Export | written, size > 0 | 89316 / 89316 / 89312 bytes |
| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) |
|---|---|---|
| Base triangles | 700–1000 | 928 / 928 / 928 |
| LOD1 ratio | 0.32–0.62 of base | 0.5000 / 0.5000 / 0.5000 |
| LOD2 ratio | 0.10–0.35 of base | 0.2198 / 0.2198 / 0.1897 |
| Materials | exactly 3 distinct; ≥80 wood, stone, metal faces | 3 slots; 216 / 160 / 180 |
| UVs | in 0..1, AABB overlap ≤ 1e-5 | in range, overlap 0 |
| Outer AABB | (0.710, 0.419, 0.610) m ± 0.015 | (0.7100, 0.4185, 0.6100), zmin 0 |
| Collider tris | ≤ 240 | 198 |
| Export | written, size > 0 | 89316 / 89316 / 89312 bytes |
Recomputed from the generated mesh, not asserted about the script.
-| Axis | Declared | Measured (all three) | | --- | --- | --- | | Non-manifold edges | 0 | 0 | | Loose verts / edges | 0 / 0 | 0 / 0 | | Doubles merged at 1e-5 | 0 | 0 | | Zero-area faces | 0 | 0 | | N-gons | 0 | 0 | | Coplanar disjoint face pairs | 0 | 0 | | Grounded: zmin | within 1e-4 of 0 | 0.0000 | | Named supports: 4 shoes | each zmin ≤ 1e-3 | 4, shoe_z 0.00000 | | Stone size | 0.500 m dia × 0.095 m thick ± 0.02 / ± 0.015 | 0.5000 × 0.0950 |
| Axis | Declared | Measured (all three) |
|---|---|---|
| Non-manifold edges | 0 | 0 |
| Loose verts / edges | 0 / 0 | 0 / 0 |
| Doubles merged at 1e-5 | 0 | 0 |
| Zero-area faces | 0 | 0 |
| N-gons | 0 | 0 |
| Coplanar disjoint face pairs | 0 | 0 |
Grounded: zmin | within 1e-4 of 0 | 0.0000 |
| Named supports: 4 shoes | each zmin ≤ 1e-3 | 4, shoe_z 0.00000 |
| Stone size | 0.500 m dia × 0.095 m thick ± 0.02 / ± 0.015 | 0.5000 × 0.0950 |
| Axis | Declared | Measured (all three) | | --- | --- | --- | | Crank-axle gap | ≤ 0.008 m | 0.00418 | | Shoe-wood BVH gap | ≤ 0.008 m (overlap is 0) | 0.00000 | | Trough-sill per-side gap | ≤ 0.008 m (overlap is 0) | 0.00000 | | Stone dip into trough | 0.015–0.045 m | 0.02570 | | Trough floor zmin | ≥ 0.040 m | 0.05800 |
+| Axis | Declared | Measured (all three) |
|---|---|---|
| Crank-axle gap | ≤ 0.008 m | 0.00418 |
| Shoe-wood BVH gap | ≤ 0.008 m (overlap is 0) | 0.00000 |
| Trough-sill per-side gap | ≤ 0.008 m (overlap is 0) | 0.00000 |
| Stone dip into trough | 0.015–0.045 m | 0.02570 |
| Trough floor zmin | ≥ 0.040 m | 0.05800 |
DECIMATE COLLAPSE triangle counts are not identical across series — 5.2.1 is leaner on LOD2. The gate is a ratio band, not an exact count. Bake pixels are stochastic; the gate is has_data plus operator FINISHED, not byte-identity. Construction uses no RNG. Export byte counts differ by 4 B on 5.2.1 (glTF serializer), not a gated axis.
Each violates one named budget. All nine were run on 4.5.11, 5.1.2 and 5.2.1 and returned the same code on each.
-| Flag | Budget violated | Exit | | --- | --- | --- | | --skip-decimate | LOD1 ratio band | 9 | | --stray-vert | loose vertex count is 0 | 15 | | --lift-z | bounding box zmin is 0 | 16 | | --short-legs | named shoe supports at Z=0 | 16 | | --float-crank | crank-axle gap | 17 | | --float-legs | shoe-wood join | 17 | | --no-dip | stone dip band | 18 | | --narrow-trough | trough-sill seat | 18 |
| Flag | Budget violated | Exit |
|---|---|---|
--skip-decimate | LOD1 ratio band | 9 |
--stray-vert | loose vertex count is 0 | 15 |
--lift-z | bounding box zmin is 0 | 16 |
--short-legs | named shoe supports at Z=0 | 16 |
--float-crank | crank-axle gap | 17 |
--float-legs | shoe-wood join | 17 |
--no-dip | stone dip band | 18 |
--narrow-trough | trough-sill seat | 18 |
blender --background --python grindstone.py --
blender --background --python grindstone.py -- --skip-decimate
@@ -289,7 +294,7 @@ Run
Smoke passes no flags.
Exit codes
File-local. 9 is a valid check code. 10 is reserved for gallery_framing.check_framing on the --output path. 15–19 are the hygiene and joint-fit family.
-| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Mesh did not build / no UV layer | | 4 | Base triangle count outside range | | 5 | Material count ≠ 3 distinct slots, or a face-count floor missed | | 6 | UVs outside 0..1 | | 7 | UV AABB overlap above tolerance | | 8 | World AABB off declared outer size | | 9 | LOD ratio band (--skip-decimate lands here) | | 10 | Framing gate (render path only) | | 11 | Collider triangle count above ceiling | | 12 | Bake did not finish or image has no data | | 13 | Export file missing or empty | | 14 | --output produced no file | | 15 | Mesh hygiene: loose, non-manifold, zero-area, doubles, n-gons, z-fight | | 16 | Not grounded: bounding box zmin off 0, or a named shoe floats | | 17 | Joint fit: crank-axle gap, or shoe-wood join | | 18 | Seat: trough-sill gap, stone dip band, or trough sitting on the dirt | | 19 | Stone diameter or thickness off the stated real-world size |
+Code Meaning 0 Success 1 Uncaught exception (FATAL wrapper) 2 argparse / usage 3 Mesh did not build / no UV layer 4 Base triangle count outside range 5 Material count ≠ 3 distinct slots, or a face-count floor missed 6 UVs outside 0..1 7 UV AABB overlap above tolerance 8 World AABB off declared outer size 9 LOD ratio band (--skip-decimate lands here) 10 Framing gate (render path only) 11 Collider triangle count above ceiling 12 Bake did not finish or image has no data 13 Export file missing or empty 14 --output produced no file15 Mesh hygiene: loose, non-manifold, zero-area, doubles, n-gons, z-fight 16 Not grounded: bounding box zmin off 0, or a named shoe floats 17 Joint fit: crank-axle gap, or shoe-wood join 18 Seat: trough-sill gap, stone dip band, or trough sitting on the dirt 19 Stone diameter or thickness off the stated real-world size
Intended size: ~1.05 m village pump, 0.34 m plinth, 0.76 m column; outer AABB 0.610 × 0.382 × 1.052 m.
Declared as named constants; every gate recomputes from the mesh, materials, UVs, evaluated LOD, collider, or export file.
-| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) | | --- | --- | --- | | Base triangles | 900–2200 | 1064 / 1064 / 1064 | | LOD1 ratio | 0.32–0.62 of base | 0.5000 / 0.5000 / 0.5000 | | LOD2 ratio | 0.10–0.35 of base | 0.2199 / 0.2199 / 0.2162 | | Materials | exactly 2 distinct, ≥48 metal, ≥24 wood | 2 slots, 564 metal / 144 wood | | UVs | in 0..1, AABB overlap ≤ 1e-5 | in range, overlap 0 | | Outer AABB | (0.610, 0.382, 1.052) m ± 0.015 | (0.6100, 0.3820, 1.0520), zmin 0 | | Collider tris | ≤ 220 | 114 | | Export | written, size > 0 | 97304 / 97304 / 97296 bytes |
| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) |
|---|---|---|
| Base triangles | 900–2200 | 1064 / 1064 / 1064 |
| LOD1 ratio | 0.32–0.62 of base | 0.5000 / 0.5000 / 0.5000 |
| LOD2 ratio | 0.10–0.35 of base | 0.2199 / 0.2199 / 0.2162 |
| Materials | exactly 2 distinct, ≥48 metal, ≥24 wood | 2 slots, 564 metal / 144 wood |
| UVs | in 0..1, AABB overlap ≤ 1e-5 | in range, overlap 0 |
| Outer AABB | (0.610, 0.382, 1.052) m ± 0.015 | (0.6100, 0.3820, 1.0520), zmin 0 |
| Collider tris | ≤ 220 | 114 |
| Export | written, size > 0 | 97304 / 97304 / 97296 bytes |
Base triangles dropped from 1164 to 1064 in the quality pass: seven sausage cylinders became one gooseneck tube, and the wood grip is no longer beveled with the plinth. Outer AABB is 0.610 × 0.382 × 1.052 m (was 0.610 × 0.364 × 1.049 m).
DECIMATE COLLAPSE triangle counts are not identical across series — 5.2.1 is slightly leaner on LOD2. The gate is a ratio band, not an exact count. Bake pixels are stochastic; the gate is has_data plus operator FINISHED, not byte-identity. Construction uses no RNG. Export byte counts differ by 8 B on 5.2.1 (glTF serializer), not a gated axis.
Recomputed from the generated mesh, not asserted about the script.
-| Axis | Declared | Measured (all three) | | --- | --- | --- | | Non-manifold edges | 0 | 0 | | Loose verts / edges | 0 / 0 | 0 / 0 | | Doubles merged at 1e-5 | 0 | 0 | | Zero-area faces | 0 | 0 | | N-gons | 0 | 0 | | Coplanar disjoint face pairs | 0 | 0 | | Grounded: zmin | within 1e-4 of 0 | 0.0000 | | Plinth zmin | within 1e-4 of 0 | 0.00000 |
| Axis | Declared | Measured (all three) |
|---|---|---|
| Non-manifold edges | 0 | 0 |
| Loose verts / edges | 0 / 0 | 0 / 0 |
| Doubles merged at 1e-5 | 0 | 0 |
| Zero-area faces | 0 | 0 |
| N-gons | 0 | 0 |
| Coplanar disjoint face pairs | 0 | 0 |
Grounded: zmin | within 1e-4 of 0 | 0.0000 |
Plinth zmin | within 1e-4 of 0 | 0.00000 |
| Axis | Declared | Measured (all three) | | --- | --- | --- | | Spout-to-column BVH gap | ≤ 0.008 m | 0.00272 | | Flange-to-plinth BVH gap | ≤ 0.008 m | 0.00600 | | Lower-column radius vs COL_R_LO | ± 0.008 m | 0.06200 |
| Axis | Declared | Measured (all three) |
|---|---|---|
| Spout-to-column BVH gap | ≤ 0.008 m | 0.00272 |
| Flange-to-plinth BVH gap | ≤ 0.008 m | 0.00600 |
Lower-column radius vs COL_R_LO | ± 0.008 m | 0.06200 |
Each violates one named budget. All were run on 4.5.11, 5.1.2 and 5.2.1 and returned the same code on each.
-| Flag | Budget violated | Exit | | --- | --- | --- | | --skip-decimate | LOD1 ratio band | 9 | | --stray-vert | loose vertex count is 0 | 15 | | --lift-z | bounding box zmin is 0 | 16 | | --float-spout | spout-to-column BVH gap | 17 | | --float-flange | flange-to-plinth BVH gap | 18 | | --skinny-col | lower-column radius | 19 |
| Flag | Budget violated | Exit |
|---|---|---|
--skip-decimate | LOD1 ratio band | 9 |
--stray-vert | loose vertex count is 0 | 15 |
--lift-z | bounding box zmin is 0 | 16 |
--float-spout | spout-to-column BVH gap | 17 |
--float-flange | flange-to-plinth BVH gap | 18 |
--skinny-col | lower-column radius | 19 |
blender --background --python hand_pump.py --
blender --background --python hand_pump.py -- --skip-decimate
@@ -289,7 +294,7 @@ Run
Smoke passes no flags.
Exit codes
File-local. 9 is a valid check code. 10 is reserved for gallery_framing.check_framing on the --output path. 15–19 are the hygiene and joint-fit family.
-| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Mesh did not build / no UV layer | | 4 | Base triangle count outside range | | 5 | Material count ≠ 2 distinct slots, or wood/metal faces missing | | 6 | UVs outside 0..1 | | 7 | UV AABB overlap above tolerance | | 8 | World AABB off declared outer size | | 9 | LOD ratio band (--skip-decimate lands here) | | 10 | Framing gate (render path only) | | 11 | Collider triangle count above ceiling | | 12 | Bake did not finish or image has no data | | 13 | Export file missing or empty | | 14 | --output produced no file | | 15 | Hygiene (--stray-vert lands here) | | 16 | Grounded zmin / plinth (--lift-z) | | 17 | Spout-to-column gap (--float-spout) | | 18 | Flange-to-plinth gap (--float-flange) | | 19 | Column radius (--skinny-col) |
+Code Meaning 0 Success 1 Uncaught exception (FATAL wrapper) 2 argparse / usage 3 Mesh did not build / no UV layer 4 Base triangle count outside range 5 Material count ≠ 2 distinct slots, or wood/metal faces missing 6 UVs outside 0..1 7 UV AABB overlap above tolerance 8 World AABB off declared outer size 9 LOD ratio band (--skip-decimate lands here) 10 Framing gate (render path only) 11 Collider triangle count above ceiling 12 Bake did not finish or image has no data 13 Export file missing or empty 14 --output produced no file15 Hygiene (--stray-vert lands here) 16 Grounded zmin / plinth (--lift-z) 17 Spout-to-column gap (--float-spout) 18 Flange-to-plinth gap (--float-flange) 19 Column radius (--skinny-col)
Every closed-form term in the loaf shaping is even in x. An odd term (sin(x*24)) put the two belt stations on different surface heights and left the wraps 5.4 mm out of step, which no budget could see.
Declared as named constants; every gate recomputes from the mesh, materials, UVs, evaluated LOD, collider, or export file. Measurements below are identical on 4.5.11, 5.1.2 and 5.2.1 — construction uses no RNG and no version-dependent operator.
-| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) | | --- | --- | --- | | Base triangles | 1700–2400 | 2012 on all three | | LOD1 ratio | 0.32–0.62 of base | 0.5000 | | LOD2 ratio | 0.10–0.35 of base | 0.2197 | | Materials | exactly 2 distinct, ≥24 hay, ≥24 twine | 2 slots, 712 hay, 304 twine | | UVs | in 0..1, AABB overlap ≤ 1e-5 | in range, overlap 0 | | Outer AABB | (0.922, 0.530, 0.409) m ± 0.01 | (0.9208, 0.5295, 0.4087) | | Grounded zmin | within 1e-4 of 0 | 0.000000 | | Hygiene | loose/nonman/zero-area/doubles/n-gons/z-fight = 0 | all 0 | | Shell count | exactly 5 (loaf, 2 belts, 2 loops) | 5 | | Belt wrap zmin | each belt shell ≤ 0.006 m | 0.002628 both | | Belt seat depth | every station in [0.002, 0.020] m | 0.00400..0.01676 over 20/20 stations, both belts | | Mirror symmetry | belts and loops matched within 5e-4 m | 0.000000 | | Cinch depth | mid-span minus waist in [0.006, 0.020] m | 0.01420 | | Loaf X × Z | (0.922, 0.409) m ± 0.02 | (0.9208, 0.4087) | | Hay–twine gap | BVH surface < 0.008 m | 0.00002 | | Collider tris | ≤ 400 | 156 | | Export | written, size > 0 | 156664 / 156664 / 156656 bytes |
| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) |
|---|---|---|
| Base triangles | 1700–2400 | 2012 on all three |
| LOD1 ratio | 0.32–0.62 of base | 0.5000 |
| LOD2 ratio | 0.10–0.35 of base | 0.2197 |
| Materials | exactly 2 distinct, ≥24 hay, ≥24 twine | 2 slots, 712 hay, 304 twine |
| UVs | in 0..1, AABB overlap ≤ 1e-5 | in range, overlap 0 |
| Outer AABB | (0.922, 0.530, 0.409) m ± 0.01 | (0.9208, 0.5295, 0.4087) |
| Grounded zmin | within 1e-4 of 0 | 0.000000 |
| Hygiene | loose/nonman/zero-area/doubles/n-gons/z-fight = 0 | all 0 |
| Shell count | exactly 5 (loaf, 2 belts, 2 loops) | 5 |
| Belt wrap zmin | each belt shell ≤ 0.006 m | 0.002628 both |
| Belt seat depth | every station in [0.002, 0.020] m | 0.00400..0.01676 over 20/20 stations, both belts |
| Mirror symmetry | belts and loops matched within 5e-4 m | 0.000000 |
| Cinch depth | mid-span minus waist in [0.006, 0.020] m | 0.01420 |
| Loaf X × Z | (0.922, 0.409) m ± 0.02 | (0.9208, 0.4087) |
| Hay–twine gap | BVH surface < 0.008 m | 0.00002 |
| Collider tris | ≤ 400 | 156 |
| Export | written, size > 0 | 156664 / 156664 / 156656 bytes |
DECIMATE COLLAPSE triangle counts are not guaranteed identical across series — the gate is a ratio band, not an exact count. Bake pixels are stochastic; the gate is has_data plus operator FINISHED, not byte-identity. Export byte counts differ by 8 B on 5.2.1 (glTF serializer), which is not a gated axis; every geometric figure above is identical on all three binaries.
The export is deleted once its size is measured. Blender sets TMPDIR from its own preference, which resolves to the working directory on a stock portable build, so tempfile.gettempdir() is the repo root under CI and every run used to leave a .glb behind.
The z-fight count is taken over cross-shell face pairs, not merely over faces that share no vertex: two quads two steps apart on one flat cap share no vertex and are coplanar by construction, and counting those reported 96 pairs, none of them a hazard.
Each breaks one stage so a named budget fails and the piece exits that code. All six proven on 4.5.11, 5.1.2 and 5.2.1.
-| Flag | Target budget | Exit | | --- | --- | --- | | --skip-decimate | LOD1 ratio band | 9 | | --stray-vert | hygiene: loose vertices | 15 | | --lift-z | grounded AABB zmin | 16 | | --float-belts | per-belt wrap zmin | 16 | | --slack-belt | banded belt seat depth | 18 | | --skew-belt | belt mirror symmetry | 19 |
| Flag | Target budget | Exit |
|---|---|---|
--skip-decimate | LOD1 ratio band | 9 |
--stray-vert | hygiene: loose vertices | 15 |
--lift-z | grounded AABB zmin | 16 |
--float-belts | per-belt wrap zmin | 16 |
--slack-belt | banded belt seat depth | 18 |
--skew-belt | belt mirror symmetry | 19 |
--stray-vert parks its loose vertex inside the bale: above it, the bounding-box budget (exit 8) fired first and the run proved nothing about hygiene. --skew-belt shifts one belt along X rather than Z for the same reason — a Z skew landed a belt face coplanar with a loaf face and tripped the z-fight budget (exit 15) instead of its target. --float-belts moves twine only, so the loaf still grounds the AABB and the per-belt gate has to be the thing that fires.
blender --background --python hay_bale.py --
@@ -296,7 +301,7 @@ Known gap
At hero scale the piece still reads as a wrapped parcel rather than as straw. The form, the binding and the hygiene are right; the surface is not. The next pass on this piece belongs in the material and bake — straw needs directional fibre and colour break-up that a 256 px tangent-space normal off a smooth high mesh cannot deliver — not in more geometry. Recorded here so the next run does not re-litigate the silhouette.
Exit codes
File-local. 9 is a valid check code. 10 is reserved for gallery_framing.check_framing on the --output path.
-| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Mesh did not build / no UV layer | | 4 | Base triangle count outside range | | 5 | Material count ≠ 2 distinct slots, or hay/twine faces missing | | 6 | UVs outside 0..1 | | 7 | UV AABB overlap above tolerance | | 8 | World AABB off declared outer size | | 9 | LOD ratio band (--skip-decimate lands here) | | 10 | Framing gate (render path only) | | 11 | Collider triangle count above ceiling | | 12 | Bake did not finish or image has no data | | 13 | Export file missing or empty | | 14 | --output produced no file | | 15 | Hygiene: loose, non-manifold, zero-area, doubles, n-gons, or cross-shell z-fight (--stray-vert) | | 16 | Grounded: AABB zmin (--lift-z), or a belt that does not wrap under (--float-belts) | | 17 | Shell count, or hay–twine BVH gap above 8 mm | | 18 | Banded belt seat depth (--slack-belt) | | 19 | Belt mirror symmetry (--skew-belt), cinch depth, or loaf real-world size |
+Code Meaning 0 Success 1 Uncaught exception (FATAL wrapper) 2 argparse / usage 3 Mesh did not build / no UV layer 4 Base triangle count outside range 5 Material count ≠ 2 distinct slots, or hay/twine faces missing 6 UVs outside 0..1 7 UV AABB overlap above tolerance 8 World AABB off declared outer size 9 LOD ratio band (--skip-decimate lands here) 10 Framing gate (render path only) 11 Collider triangle count above ceiling 12 Bake did not finish or image has no data 13 Export file missing or empty 14 --output produced no file15 Hygiene: loose, non-manifold, zero-area, doubles, n-gons, or cross-shell z-fight (--stray-vert) 16 Grounded: AABB zmin (--lift-z), or a belt that does not wrap under (--float-belts) 17 Shell count, or hay–twine BVH gap above 8 mm 18 Banded belt seat depth (--slack-belt) 19 Belt mirror symmetry (--skew-belt), cinch depth, or loaf real-world size
Composes skills mesh-editing-and-bmesh, bake-high-to-low, depsgraph-and-evaluated-data, engine-export-presets, and snippets bake_normal_high_to_low.py, setup_bake_target_image.py, lod_chain.py / decimate_to_budget.py, convex_hull_collider.py, export_preset_unity.py (helpers copied, not imported as a package). Hygiene combinatorics match examples/mesh-hygiene-audit (copied, not imported). Coplanar-pair counting matches showcase/signpost.
Declared as named constants; every gate recomputes from the mesh, materials, UVs, evaluated LOD, collider, or export file.
-| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) | | --- | --- | --- | | Base triangles | 1400–2400 | 1680 / 1680 / 1680 | | LOD1 ratio | 0.32–0.62 of base | 0.5000 / 0.5000 / 0.5000 | | LOD2 ratio | 0.10–0.35 of base | 0.2190 / 0.2190 / 0.2190 | | Materials | exactly 2 distinct, ≥70 wood, ≥560 metal | 2 slots, 114 wood, 742 metal | | UVs | in 0..1, AABB overlap ≤ 1e-5 | in range, overlap 0 | | Outer AABB | (0.500, 0.149, 1.246) m ± 0.01 | (0.5000, 0.1490, 1.2460) | | Grounded zmin | within 1e-4 of 0 | 0 / 0 / 0 | | Hygiene | loose/nonman/zero-area/doubles/n-gons/coplanar pairs = 0 | all 0 | | Post seat | world zmin in (0.004, 0.014), XY centroid within 0.010 of origin, arm axis cos ≥ cos(0.5°) | 0.01000, 0, 1.000 | | Hung ring | centerline error ≤ 0.008 m, ring–wood overlap 0, eye–wood overlap 0, shank–ring overlap 0, shank bites the eye | 0, 0, 0, 0, 40 | | Shoe | each band overlaps the post, shoe overlaps the sole | band gap 0, sole–shoe 8 | | Wood–metal gap | BVH surface < 0.008 m | 0.00075 | | Collider tris | ≤ 280 | 80 | | Export | written, size > 0 | 127428 / 127428 / 127412 bytes |
| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) |
|---|---|---|
| Base triangles | 1400–2400 | 1680 / 1680 / 1680 |
| LOD1 ratio | 0.32–0.62 of base | 0.5000 / 0.5000 / 0.5000 |
| LOD2 ratio | 0.10–0.35 of base | 0.2190 / 0.2190 / 0.2190 |
| Materials | exactly 2 distinct, ≥70 wood, ≥560 metal | 2 slots, 114 wood, 742 metal |
| UVs | in 0..1, AABB overlap ≤ 1e-5 | in range, overlap 0 |
| Outer AABB | (0.500, 0.149, 1.246) m ± 0.01 | (0.5000, 0.1490, 1.2460) |
| Grounded zmin | within 1e-4 of 0 | 0 / 0 / 0 |
| Hygiene | loose/nonman/zero-area/doubles/n-gons/coplanar pairs = 0 | all 0 |
| Post seat | world zmin in (0.004, 0.014), XY centroid within 0.010 of origin, arm axis cos ≥ cos(0.5°) | 0.01000, 0, 1.000 |
| Hung ring | centerline error ≤ 0.008 m, ring–wood overlap 0, eye–wood overlap 0, shank–ring overlap 0, shank bites the eye | 0, 0, 0, 0, 40 |
| Shoe | each band overlaps the post, shoe overlaps the sole | band gap 0, sole–shoe 8 |
| Wood–metal gap | BVH surface < 0.008 m | 0.00075 |
| Collider tris | ≤ 280 | 80 |
| Export | written, size > 0 | 127428 / 127428 / 127412 bytes |
Base triangles rose from 1598 to 1680. The old rings were faceted tori clipped into the arm; the new eyes, shanks, and 24-segment hung rings replace that mesh. Cylinder caps are triangulated so the n-gon budget stays at 0.
DECIMATE COLLAPSE triangle counts are not identical across series — the gate is a ratio band, not an exact count. On this mesh the three binaries agreed. Bake pixels are stochastic; the gate is has_data plus operator FINISHED, not byte-identity. Construction uses no RNG. Export byte counts differ by 16 B on 5.2.1 (glTF serializer), not a gated axis. Euler is 12 (six balls and seven tori) and is not gated to 2.
Each falsifier violates exactly one named budget. Proven on Blender 4.5.11 LTS, 5.1.2, and 5.2.1 LTS (the binaries' own --version):
| Falsifier | Budget violated | Exit | Measured failure | | --- | --- | --- | --- | | --skip-decimate | LOD1 ratio band | 9 | ratio 1.0000 | | --stray-vert | Mesh hygiene, loose verts | 15 | loose_v=1 | | --twin-sole | Coplanar face pairs | 15 | zfight=6 | | --lift-z | Grounded zmin | 16 | zmin 0.050000 | | --clip-ring | Hung-ring centerline | 18 | ring_err 0.024, ring–wood overlap 32 | | --short-post | Seated post | 19 | post zmin 0.030 |
| Falsifier | Budget violated | Exit | Measured failure |
|---|---|---|---|
--skip-decimate | LOD1 ratio band | 9 | ratio 1.0000 |
--stray-vert | Mesh hygiene, loose verts | 15 | loose_v=1 |
--twin-sole | Coplanar face pairs | 15 | zfight=6 |
--lift-z | Grounded zmin | 16 | zmin 0.050000 |
--clip-ring | Hung-ring centerline | 18 | ring_err 0.024, ring–wood overlap 32 |
--short-post | Seated post | 19 | post zmin 0.030 |
Default exit is 0 on all three. --clip-ring also overlaps the rail; the centerline gate is the one that fires.
blender --background --python hitching_post.py --
@@ -283,7 +288,7 @@ Run
Smoke does not pass --output or a falsifier.
Exit codes
File-local. 9 is a valid check code. 10 is reserved for gallery_framing.check_framing on the --output path.
-| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Mesh did not build / no UV layer | | 4 | Base triangle count outside range | | 5 | Material count ≠ 2 distinct slots, or wood/metal faces missing | | 6 | UVs outside 0..1 | | 7 | UV AABB overlap above tolerance | | 8 | World AABB off declared outer size | | 9 | LOD ratio band (--skip-decimate lands here) | | 10 | Framing gate (render path only) | | 11 | Collider triangle count above ceiling | | 12 | Bake did not finish or image has no data | | 13 | Export file missing or empty | | 14 | --output produced no file | | 15 | Hygiene, including coplanar pairs (--stray-vert, --twin-sole) | | 16 | Grounded zmin (--lift-z) | | 17 | Wood–metal BVH gap above 8 mm | | 18 | Hung ring or shoe seat (--clip-ring) | | 19 | Post plumb, origin, and cup seat (--short-post) |
+Code Meaning 0 Success 1 Uncaught exception (FATAL wrapper) 2 argparse / usage 3 Mesh did not build / no UV layer 4 Base triangle count outside range 5 Material count ≠ 2 distinct slots, or wood/metal faces missing 6 UVs outside 0..1 7 UV AABB overlap above tolerance 8 World AABB off declared outer size 9 LOD ratio band (--skip-decimate lands here) 10 Framing gate (render path only) 11 Collider triangle count above ceiling 12 Bake did not finish or image has no data 13 Export file missing or empty 14 --output produced no file15 Hygiene, including coplanar pairs (--stray-vert, --twin-sole) 16 Grounded zmin (--lift-z) 17 Wood–metal BVH gap above 8 mm 18 Hung ring or shoe seat (--clip-ring) 19 Post plumb, origin, and cup seat (--short-post)
A runnable example that writes a procedural broadcast test card — seven neon bars, a luminance ramp, a PLUGE row with a bottom-left origin marker, and the classic circle — into a bpy.data.images.new() datablock with one pixels.foreach_set() call (512 × 288 × 4 = 589,824 floats), then renders it on an emissive studio monitor.
What it witnesses: the bpy.types.Image pixel-buffer contract. Image.pixels is a flat, row-major, bottom-left-origin float buffer that is *always* RGBA: channels == 4 and len(pixels) == width × height × 4 even when the image is created with alpha=False, so an RGB-stride foreach_set raises TypeError instead of writing. A byte image (the default) stores 8 bits per channel — every written float round-trips with error ≤ 0.5/255 and strictly > 0 (an exact round-trip would mean storage is not 8-bit); float_buffer=True stores float32 and round-trips at ~1e-7. Image.scale() reallocates the buffer, so a foreach_get into a stale-size list raises TypeError rather than silently shearing rows.
The save() trap (found while authoring — identical on 4.5 LTS and 5.1): Image.save() on a GENERATED image silently flips source to 'FILE' and drops the in-memory buffer (has_data becomes False). Every later pixels read re-loads from whatever currently sits at filepath_raw. The check proves it by overwriting the file with a flat-gray imposter image *after* save() and reading the imposter's pixels back through the original datablock. save_render() writes the same PNG but is non-destructive: source stays 'GENERATED' and the buffer stays exact. Scripts that write pixels, save(), then keep computing on pixels are silently computing on a decoded PNG.
What it witnesses: the bpy.types.Image pixel-buffer contract. Image.pixels is a flat, row-major, bottom-left-origin float buffer that is always RGBA: channels == 4 and len(pixels) == width × height × 4 even when the image is created with alpha=False, so an RGB-stride foreach_set raises TypeError instead of writing. A byte image (the default) stores 8 bits per channel — every written float round-trips with error ≤ 0.5/255 and strictly > 0 (an exact round-trip would mean storage is not 8-bit); float_buffer=True stores float32 and round-trips at ~1e-7. Image.scale() reallocates the buffer, so a foreach_get into a stale-size list raises TypeError rather than silently shearing rows.
The save() trap (found while authoring — identical on 4.5 LTS and 5.1): Image.save() on a GENERATED image silently flips source to 'FILE' and drops the in-memory buffer (has_data becomes False). Every later pixels read re-loads from whatever currently sits at filepath_raw. The check proves it by overwriting the file with a flat-gray imposter image after save() and reading the imposter's pixels back through the original datablock. save_render() writes the same PNG but is non-destructive: source stays 'GENERATED' and the buffer stays exact. Scripts that write pixels, save(), then keep computing on pixels are silently computing on a decoded PNG.
What each check catches on failure:
-W*H*3 write raises and the check exits 3).byte_err > 0 proves 8-bit storage really quantizes; byte and float images swapping behavior cannot hide.scale() no longer reallocating (stale-size read succeeding).save_render() for save() exits 7 because source stays GENERATED).W*H*3 write raises and the check exits 3).byte_err > 0 proves 8-bit storage really quantizes; byte and float images swapping behavior cannot hide.scale() no longer reallocating (stale-size read succeeding).save_render() for save() exits 7 because source stays GENERATED).Version divergence: none — every contract above, including the save() trap, was probed and asserts identically on Blender 4.5.11 LTS and 5.1.2. The only gate in the file is the EEVEE engine id for the optional render (BLENDER_EEVEE_NEXT on 4.x, BLENDER_EEVEE on 5.x).
Render hazard worth knowing: a bmesh-built plane has no UV map, and bmesh.ops.create_grid(..., calc_uvs=True) silently creates none unless a UV layer already exists — without one, an Image Texture samples texel (0,0) for every fragment and the screen renders as one flat color. The render path creates the layer explicitly.
Per-script sequential checks. 9 is a valid check code; there is no rule against it.
| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Pixel buffer is not always RGBA | | 4 | Byte round-trip vs closed-form card (--wrong-origin lands here) | | 5 | Float-buffer round-trip failed | | 6 | scale() did not reallocate, or stale-size read succeeded | | 7 | save() source/buffer-drop contract drifted | | 8 | save_render() flipped source or disturbed the buffer | | 9 | Byte PNG save/reload error | | 10 | --output produced no file |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Pixel buffer is not always RGBA |
| 4 | Byte round-trip vs closed-form card (--wrong-origin lands here) |
| 5 | Float-buffer round-trip failed |
| 6 | scale() did not reallocate, or stale-size read succeeded |
| 7 | save() source/buffer-drop contract drifted |
| 8 | save_render() flipped source or disturbed the buffer |
| 9 | Byte PNG save/reload error |
| 10 | --output produced no file |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --wrong-origin.
In the render, Closest interpolation keeps the pixel grid honest — the jagged circle edge is the 512 × 288 buffer itself, and the white marker in the PLUGE row sits at the bottom-left because that is where pixel (0, 0) lives. The monitor is staged as a designed object — beveled dark-polymer case, machined metal stand, teal power LED — on the dark studio stage from docs/VISUAL-STYLE.md (Standard view transform; warm key, cool fill and rim; a warm pool raking the back wall). The screen stays emissive and matte — specular off, emission strength 1.0 — so the card's values read exactly.
Composes skills mesh-editing-and-bmesh, bake-high-to-low, depsgraph-and-evaluated-data, engine-export-presets, and snippets bake_normal_high_to_low.py, setup_bake_target_image.py, lod_chain.py / decimate_to_budget.py, convex_hull_collider.py, export_preset_unity.py (helpers copied, not imported as a package).
Declared as named constants; every gate recomputes from the mesh, materials, UVs, evaluated LOD, collider, or export file.
-| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) | | --- | --- | --- | | Base triangles | 2800–4200 | 3664 / 3664 / 3664 | | LOD1 ratio | 0.32–0.62 of base | 0.5000 / 0.5000 / 0.5000 | | LOD2 ratio | 0.10–0.35 of base | 0.2200 / 0.2200 / 0.2200 | | Materials | exactly 2 distinct; ≥80 wood, ≥200 metal faces | 2 slots; 224 / 1768 | | UVs | in 0..1, AABB overlap ≤ 1e-5 | in range, overlap 0 | | Outer AABB | (0.987, 0.961, 0.860) m ± 0.015 | (0.9866, 0.9605, 0.8600), zmin 0 | | Collider tris | ≤ 280 | 250 | | Export | written, size > 0 | 287916 / 287916 / 287904 bytes |
| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) |
|---|---|---|
| Base triangles | 2800–4200 | 3664 / 3664 / 3664 |
| LOD1 ratio | 0.32–0.62 of base | 0.5000 / 0.5000 / 0.5000 |
| LOD2 ratio | 0.10–0.35 of base | 0.2200 / 0.2200 / 0.2200 |
| Materials | exactly 2 distinct; ≥80 wood, ≥200 metal faces | 2 slots; 224 / 1768 |
| UVs | in 0..1, AABB overlap ≤ 1e-5 | in range, overlap 0 |
| Outer AABB | (0.987, 0.961, 0.860) m ± 0.015 | (0.9866, 0.9605, 0.8600), zmin 0 |
| Collider tris | ≤ 280 | 250 |
| Export | written, size > 0 | 287916 / 287916 / 287904 bytes |
Base triangles rose from 3388 to 3664 in the quality pass: coaxial ferrule cups and a dual-wall lathe with a rolled rim replaced the old cone-tripod / overlapping-torus construction.
Recomputed from the generated mesh, not asserted about the script.
-| Axis | Declared | Measured (all three) | | --- | --- | --- | | Non-manifold edges | 0 | 0 | | Loose verts / edges | 0 / 0 | 0 / 0 | | Doubles merged at 1e-5 | 0 | 0 | | Zero-area faces | 0 | 0 | | N-gons | 0 | 0 | | Coplanar disjoint face pairs | 0 | 0 | | Grounded: zmin | within 1e-4 of 0 | 0.0000 | | Named supports: 3 ferrules | each zmin ≤ 1e-3 | 3, cup_z 0.00000 | | Pot size | 0.355 m dia × 0.334 m high ± 0.04 | 0.3545 × 0.3340 | | Pot–leg clearance | ≥ 0.04 m | 0.0464 |
| Axis | Declared | Measured (all three) |
|---|---|---|
| Non-manifold edges | 0 | 0 |
| Loose verts / edges | 0 / 0 | 0 / 0 |
| Doubles merged at 1e-5 | 0 | 0 |
| Zero-area faces | 0 | 0 |
| N-gons | 0 | 0 |
| Coplanar disjoint face pairs | 0 | 0 |
Grounded: zmin | within 1e-4 of 0 | 0.0000 |
| Named supports: 3 ferrules | each zmin ≤ 1e-3 | 3, cup_z 0.00000 |
| Pot size | 0.355 m dia × 0.334 m high ± 0.04 | 0.3545 × 0.3340 |
| Pot–leg clearance | ≥ 0.04 m | 0.0464 |
| Axis | Declared | Measured (all three) | | --- | --- | --- | | Hook-bail BVH gap | ≤ 0.010 m | 0.00000 | | Ferrule bite (pole r − inner wall, pole frame) | −0.006–0.000 m | −0.00185 |
+| Axis | Declared | Measured (all three) |
|---|---|---|
| Hook-bail BVH gap | ≤ 0.010 m | 0.00000 |
| Ferrule bite (pole r − inner wall, pole frame) | −0.006–0.000 m | −0.00185 |
DECIMATE COLLAPSE triangle counts are not identical across series — the gate is a ratio band, not an exact count. Bake pixels are stochastic; the gate is has_data plus operator FINISHED, not byte-identity. Construction uses no RNG. Export byte counts differ by 12 B on 5.2.1 (glTF serializer), not a gated axis.
Each violates one named budget. All seven were run on 4.5.11, 5.1.2 and 5.2.1 and returned the same code on each.
-| Flag | Budget violated | Exit | | --- | --- | --- | | --skip-decimate | LOD1 ratio band | 9 | | --stray-vert | loose vertex count is 0 | 15 | | --lift-z | bounding box zmin is 0 | 16 | | --short-legs | named ferrule supports at Z=0 | 16 | | --float-hook | hook-bail gap | 17 | | --pipe-ferrule | ferrule bite (pole starts above the well) | 18 |
| Flag | Budget violated | Exit |
|---|---|---|
--skip-decimate | LOD1 ratio band | 9 |
--stray-vert | loose vertex count is 0 | 15 |
--lift-z | bounding box zmin is 0 | 16 |
--short-legs | named ferrule supports at Z=0 | 16 |
--float-hook | hook-bail gap | 17 |
--pipe-ferrule | ferrule bite (pole starts above the well) | 18 |
blender --background --python iron_cauldron.py --
blender --background --python iron_cauldron.py -- --skip-decimate
@@ -288,7 +293,7 @@ Run
Smoke passes no flags.
Exit codes
File-local. 9 is a valid check code. 10 is reserved for gallery_framing.check_framing on the --output path. 15–19 are the hygiene and joint-fit family.
-| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Mesh did not build / no UV layer / pot clips tripod | | 4 | Base triangle count outside range | | 5 | Material count ≠ 2 distinct slots, or a face-count floor missed | | 6 | UVs outside 0..1 | | 7 | UV AABB overlap above tolerance | | 8 | World AABB off declared outer size | | 9 | LOD ratio band (--skip-decimate lands here) | | 10 | Framing gate (render path only) | | 11 | Collider triangle count above ceiling | | 12 | Bake did not finish or image has no data | | 13 | Export file missing or empty | | 14 | --output produced no file | | 15 | Mesh hygiene: loose, non-manifold, zero-area, doubles, n-gons, z-fight | | 16 | Not grounded: bounding box zmin off 0, or a named ferrule floats | | 17 | Joint fit: hook-bail gap | | 18 | Seat: ferrule bite band | | 19 | Pot diameter or height off the stated real-world size |
+Code Meaning 0 Success 1 Uncaught exception (FATAL wrapper) 2 argparse / usage 3 Mesh did not build / no UV layer / pot clips tripod 4 Base triangle count outside range 5 Material count ≠ 2 distinct slots, or a face-count floor missed 6 UVs outside 0..1 7 UV AABB overlap above tolerance 8 World AABB off declared outer size 9 LOD ratio band (--skip-decimate lands here) 10 Framing gate (render path only) 11 Collider triangle count above ceiling 12 Bake did not finish or image has no data 13 Export file missing or empty 14 --output produced no file15 Mesh hygiene: loose, non-manifold, zero-area, doubles, n-gons, z-fight 16 Not grounded: bounding box zmin off 0, or a named ferrule floats 17 Joint fit: hook-bail gap 18 Seat: ferrule bite band 19 Pot diameter or height off the stated real-world size
Per-script sequential checks. 9 is a valid check code; there is no rule against it.
| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Light datablock gained light_linking | | 4 | Light objects lost light_linking | | 5 | receiver_collection assignment did not read back (--skip-link lands here) | | 6 | Linked state: hero dark or hero/decoy ratio below gate | | 7 | Decoy rise below gate when unlinked | | 8 | Hero drifted across the unlink | | 9 | --output produced no file |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Light datablock gained light_linking |
| 4 | Light objects lost light_linking |
| 5 | receiver_collection assignment did not read back (--skip-link lands here) |
| 6 | Linked state: hero dark or hero/decoy ratio below gate |
| 7 | Decoy rise below gate when unlinked |
| 8 | Hero drifted across the unlink |
| 9 | --output produced no file |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --skip-link.
Authoring hazards pinned while building this (probes in .scratch-style bisect scripts; the reasons the check code re-fetches layers by name and re-asserts flags):
MeshUVLoopLayer reference across edit-mode UV ops and then iterating its .data segfaults Blender 4.5.11 headless (EXCEPTION_ACCESS_VIOLATION, 5/5 repro); the same read survives on 5.1.2. Re-fetch mesh.uv_layers[name] after CustomData-reallocating calls. Second confirmation of the UV-handle lifetime hazard found authoring triangulate-tangents.bpy.ops.uv.lightmap_pack silently packs nothing (exit OK) when the active layer has no UVs yet — unwrap first, then pack.Pipeline arc neighbors: UV-layer authoring in uv-layer-grid, tangent-space from UVs in triangulate-tangents, export round-trip in gltf-export-roundtrip, pivot discipline in prop-origin-transform.
Version witness: check output is byte-identical on Blender 4.5.11 LTS and 5.1.2 (21 parts, 3680 islands, drift 0, overlap 0, min island distance 0.00401). The UV *layout* is packer-version-dependent by design; the contracts are layout-independent invariants.
+Version witness: check output is byte-identical on Blender 4.5.11 LTS and 5.1.2 (21 parts, 3680 islands, drift 0, overlap 0, min island distance 0.00401). The UV layout is packer-version-dependent by design; the contracts are layout-independent invariants.
Render as proof: the cart beside its enlarged UV1 atlas board — the Bed's packed lightmap built from live UV data, so a change in the atlas moves the board geometry. The falsification variant (--falsify) translates the second-largest island onto the largest: a big emissive-red island visibly stacked over the atlas (15 SAT hits in the check probe). The render path also gates the asset itself through examples/gallery_asset_quality.py (naming, material variation, edge treatment — exit 11): 21 named parts, 5 materials, right-angle share 0.069.
blender --background --python lightmap_uv_channel.py --
@@ -274,7 +279,7 @@ Run
blender --background --python lightmap_uv_channel.py -- --falsify overlap.png
Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper. 11 is the shared asset-quality helper.
| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | UV layer names ≠ UV0 / UV1 | | 4 | UVMap / UV1 render/clone flags wrong after re-assert | | 5 | Channel 0 (UV0) touched by the UV1 unwrap | | 6 | UV1 loops outside [0,1] | | 7 | SAT overlap self-test failed, or UV1 triangle pairs overlap | | 8 | Min island distance below margin | | 9 | Part is not watertight | | 10 | Gallery framing violation; also --output produced no file | | 11 | Gallery asset-quality violation |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | UV layer names ≠ UV0 / UV1 |
| 4 | UVMap / UV1 render/clone flags wrong after re-assert |
| 5 | Channel 0 (UV0) touched by the UV1 unwrap |
| 6 | UV1 loops outside [0,1] |
| 7 | SAT overlap self-test failed, or UV1 triangle pairs overlap |
| 8 | Min island distance below margin |
| 9 | Part is not watertight |
| 10 | Gallery framing violation; also --output produced no file |
| 11 | Gallery asset-quality violation |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --falsify.
Per-script sequential checks. 9 is a valid check code; there is no rule against it.
| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Base topology ≠ closed form | | 4 | Base bbox ≠ closed form | | 5 | LOD0 evaluated counts ≠ base | | 6 | Evaluated tris ≥ base (--no-decimate lands here) | | 7 | Original datablock mutated after evaluation | | 8 | LOD tris outside ratio bounds | | 9 | LOD bbox lost silhouette-critical dimensions | | 10 | --output produced no file |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Base topology ≠ closed form |
| 4 | Base bbox ≠ closed form |
| 5 | LOD0 evaluated counts ≠ base |
| 6 | Evaluated tris ≥ base (--no-decimate lands here) |
| 7 | Original datablock mutated after evaluation |
| 8 | LOD tris outside ratio bounds |
| 9 | LOD bbox lost silhouette-critical dimensions |
| 10 | --output produced no file |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --no-decimate.
Composes skills mesh-editing-and-bmesh, bake-high-to-low, depsgraph-and-evaluated-data, engine-export-presets, and snippets bake_normal_high_to_low.py, setup_bake_target_image.py, lod_chain.py / decimate_to_budget.py, convex_hull_collider.py, export_preset_unity.py (helpers copied, not imported as a package).
Declared as named constants; every gate recomputes from the mesh, materials, UVs, evaluated LOD, collider, or export file.
-| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) | | --- | --- | --- | | Base triangles | 3900–5200 | 4324 / 4324 / 4324 | | LOD1 ratio | 0.32–0.62 of base | 0.5000 / 0.5000 / 0.5000 | | LOD2 ratio | 0.10–0.35 of base | 0.2197 / 0.2197 / 0.2197 | | Materials | exactly 3 distinct; ≥16 faces per stripe slot | 3 slots; 34 / 34 stripe faces | | UVs | in 0..1, AABB overlap ≤ 1e-5 | in range, overlap 0 | | Outer AABB | (1.389, 1.011, 1.740) m ± 0.01 | (1.3892, 1.0107, 1.7402), zmin 0 | | Collider tris | ≤ 80 | 58 | | Export | written, size > 0 | 312752 / 312752 / 312728 bytes |
| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) |
|---|---|---|
| Base triangles | 3900–5200 | 4324 / 4324 / 4324 |
| LOD1 ratio | 0.32–0.62 of base | 0.5000 / 0.5000 / 0.5000 |
| LOD2 ratio | 0.10–0.35 of base | 0.2197 / 0.2197 / 0.2197 |
| Materials | exactly 3 distinct; ≥16 faces per stripe slot | 3 slots; 34 / 34 stripe faces |
| UVs | in 0..1, AABB overlap ≤ 1e-5 | in range, overlap 0 |
| Outer AABB | (1.389, 1.011, 1.740) m ± 0.01 | (1.3892, 1.0107, 1.7402), zmin 0 |
| Collider tris | ≤ 80 | 58 |
| Export | written, size > 0 | 312752 / 312752 / 312728 bytes |
Recomputed from the generated mesh, not asserted about the script.
-| Axis | Declared | Measured (all three) | | --- | --- | --- | | Non-manifold edges | 0 | 0 | | Loose verts / edges | 0 / 0 | 0 / 0 | | Doubles merged at 1e-5 | 0 | 0 | | Zero-area faces | 0 | 0 | | N-gons | 0 | 0 | | Coplanar disjoint face pairs | 0 | 0 | | Grounded: zmin | within 1e-4 of 0 | 0.0000 | | Named supports: 4 wrap feet | each zmin ≤ 1e-3 | 4, foot_z 0.00000 | | Frame plan | 1.36 × 0.92 m ± 0.04 | 1.3600 × 0.9200 |
| Axis | Declared | Measured (all three) |
|---|---|---|
| Non-manifold edges | 0 | 0 |
| Loose verts / edges | 0 / 0 | 0 / 0 |
| Doubles merged at 1e-5 | 0 | 0 |
| Zero-area faces | 0 | 0 |
| N-gons | 0 | 0 |
| Coplanar disjoint face pairs | 0 | 0 |
Grounded: zmin | within 1e-4 of 0 | 0.0000 |
| Named supports: 4 wrap feet | each zmin ≤ 1e-3 | 4, foot_z 0.00000 |
| Frame plan | 1.36 × 0.92 m ± 0.04 | 1.3600 × 0.9200 |
| Axis | Declared | Measured (all three) | | --- | --- | --- | | Brace-vs-counter overlap | ≤ 1e-6 m³ | 0.000000 | | Header-post tenon engage | ≥ 0.4 × tenon | 0.0595 | | Awning-on-header seat | −0.002–0.010 m | 0.00378 | | Post plumb (XY drift) | ≤ 0.008 m | 0.00000 |
+| Axis | Declared | Measured (all three) |
|---|---|---|
| Brace-vs-counter overlap | ≤ 1e-6 m³ | 0.000000 |
| Header-post tenon engage | ≥ 0.4 × tenon | 0.0595 |
| Awning-on-header seat | −0.002–0.010 m | 0.00378 |
| Post plumb (XY drift) | ≤ 0.008 m | 0.00000 |
DECIMATE COLLAPSE triangle counts are not identical across series — the gate is a ratio band, not an exact count. Bake pixels are stochastic; the gate is has_data plus operator FINISHED, not byte-identity. Construction uses no RNG. Export byte counts differ by 24 B on 5.2.1 (glTF serializer), not a gated axis.
Each violates one named budget. All eight were run on 4.5.11, 5.1.2 and 5.2.1 and returned the same code on each.
-| Flag | Budget violated | Exit | | --- | --- | --- | | --skip-decimate | LOD1 ratio band | 9 | | --stray-vert | loose vertex count is 0 | 15 | | --lift-z | bounding box zmin is 0 | 16 | | --short-feet | named wrap-foot supports at Z=0 | 16 | | --low-brace | brace-vs-counter overlap | 17 | | --float-awning | awning-on-header seat | 18 | | --rake-posts | post plumb | 19 |
| Flag | Budget violated | Exit |
|---|---|---|
--skip-decimate | LOD1 ratio band | 9 |
--stray-vert | loose vertex count is 0 | 15 |
--lift-z | bounding box zmin is 0 | 16 |
--short-feet | named wrap-foot supports at Z=0 | 16 |
--low-brace | brace-vs-counter overlap | 17 |
--float-awning | awning-on-header seat | 18 |
--rake-posts | post plumb | 19 |
blender --background --python market_stall.py --
blender --background --python market_stall.py -- --skip-decimate
@@ -288,7 +293,7 @@ Run
Smoke passes no flags.
Exit codes
File-local. 9 is a valid check code. 10 is reserved for gallery_framing.check_framing on the --output path. 15–19 are the hygiene and joint-fit family.
-| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Mesh did not build / no UV layer | | 4 | Base triangle count outside range | | 5 | Material count ≠ 3 distinct slots, or stripe faces missing | | 6 | UVs outside 0..1 | | 7 | UV AABB overlap above tolerance | | 8 | World AABB off declared outer size | | 9 | LOD ratio band (--skip-decimate lands here) | | 10 | Framing gate (render path only) | | 11 | Collider triangle count above ceiling | | 12 | Bake did not finish or image has no data | | 13 | Export file missing or empty | | 14 | --output produced no file | | 15 | Mesh hygiene: loose, non-manifold, zero-area, doubles, n-gons, z-fight | | 16 | Not grounded: bounding box zmin off 0, or a named wrap foot floats | | 17 | Joint fit: brace occupying the counter volume | | 18 | Seat: awning-on-header gap | | 19 | Post plumb or frame plan off the stated real-world size |
+Code Meaning 0 Success 1 Uncaught exception (FATAL wrapper) 2 argparse / usage 3 Mesh did not build / no UV layer 4 Base triangle count outside range 5 Material count ≠ 3 distinct slots, or stripe faces missing 6 UVs outside 0..1 7 UV AABB overlap above tolerance 8 World AABB off declared outer size 9 LOD ratio band (--skip-decimate lands here) 10 Framing gate (render path only) 11 Collider triangle count above ceiling 12 Bake did not finish or image has no data 13 Export file missing or empty 14 --output produced no file15 Mesh hygiene: loose, non-manifold, zero-area, doubles, n-gons, z-fight 16 Not grounded: bounding box zmin off 0, or a named wrap foot floats 17 Joint fit: brace occupying the counter volume 18 Seat: awning-on-header gap 19 Post plumb or frame plan off the stated real-world size
A runnable example that builds an octagonal street valve body and runs the engine-ingest mesh hygiene checklist as executable topology checks, following mesh-editing-and-bmesh.
Pipeline arc neighbors: watertight / Euler on collision *hulls* in collision-hull-proxy, parametric closed solids in bmesh-gear, LOD face budgets in lod-decimate-chain, and join context in temp-override-join. Hygiene is the gate a prop pipeline runs on the *render* mesh before hull / LOD / export.
Pipeline arc neighbors: watertight / Euler on collision hulls in collision-hull-proxy, parametric closed solids in bmesh-gear, LOD face budgets in lod-decimate-chain, and join context in temp-override-join. Hygiene is the gate a prop pipeline runs on the render mesh before hull / LOD / export.
Scope: this witnesses the bpy-level contract a prop pipeline relies on (tris+quads, no loose verts, manifold edges, no zero-area faces, outward winding, Euler 2 for a closed solid). It is not an engine exporter.
What it witnesses: every gate is derived from mesh combinatorics or the divergence-theorem volume — never from a prior-run capture:
len(poly.vertices) <= 4 for every face.min_area.V − E + F == 2 for the clean closed manifold (measured: verts=66 edges=136 faces=72, volume≈0.652001, min_area≈1.146e-02).Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper.
| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Ngon present (--inject-ngon lands here) | | 4 | Loose vertices | | 5 | Non-manifold or boundary edges | | 6 | Zero-area faces | | 7 | Signed volume ≤ 0 | | 8 | Euler characteristic ≠ 2 | | 9 | --output produced no file | | 10 | Gallery framing violation |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Ngon present (--inject-ngon lands here) |
| 4 | Loose vertices |
| 5 | Non-manifold or boundary edges |
| 6 | Zero-area faces |
| 7 | Signed volume ≤ 0 |
| 8 | Euler characteristic ≠ 2 |
| 9 | --output produced no file |
| 10 | Gallery framing violation |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --inject-ngon.
A runnable example building the asset a tiling modular kit lives or dies by: a corridor segment whose open-end boundary vertices sit exactly on the declared tile grid, so instances placed at 4 m multiples share boundary positions with no gap and no overlap. Interior geometry is free-form (beveled detail boxes); only the boundary is snapped — the snap is a deliberate authoring pass, and the check is what catches you skipping it.
-The asset, for reuse: a 4 × 3 × 3 m corridor segment (hollow-rectangle shell plus twenty detail parts — frame rib, walkway plate, wall panel assemblies (backing plate + inset panel + four bolt heads each), trim rails, emissive light strips). Origin at the connection pivot (floor-center of the start edge, so instance *n* places at x = n·4), identity transforms by construction, datablocks under Kit.CorridorSeg.*, manifold everywhere except the two intentional open ends, and every detail part contained strictly inside the tile so it can never break a joint. Drop the hierarchy into a scene and array it along X.
The asset, for reuse: a 4 × 3 × 3 m corridor segment (hollow-rectangle shell plus twenty detail parts — frame rib, walkway plate, wall panel assemblies (backing plate + inset panel + four bolt heads each), trim rails, emissive light strips). Origin at the connection pivot (floor-center of the start edge, so instance n places at x = n·4), identity transforms by construction, datablocks under Kit.CorridorSeg.*, manifold everywhere except the two intentional open ends, and every detail part contained strictly inside the tile so it can never break a joint. Drop the hierarchy into a scene and array it along X.
Pipeline arc neighbors: watertight parametric solids in bmesh-gear, topology gates in mesh-hygiene-audit, origin/pivot discipline in prop-origin-transform, collision packaging in collision-hull-proxy.
What it witnesses (all closed form or independently re-derived):
x ∈ {0, 4} within 1e-6 m; every boundary edge lies on an end plane.(4, 0, 0) places its start ring at world positions matching the original's end ring within 1e-6 — no gap, no overlap at the joint.0..4 × ±1.5 × 0..3, all float32-exact values, deviation 0.0), and the whole-asset bbox matches it because detail is contained.Kit.CorridorSeg.* names, all part origins at the pivot.Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is this example's framing-deviation cap (call site, not the shared Layer 1 helper). 11 is the shared asset-quality helper.
| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Boundary vert count or end-plane membership | | 4 | End rings do not partition evenly, or opposing loops differ | | 5 | Tiled instances do not share boundary positions | | 6 | Shell or whole-asset bbox off the declared tile | | 7 | Boundary edge count, non-manifold edges, or rim off the end planes | | 8 | Detail part not watertight | | 9 | Detail part reaches a tile boundary; also --output produced no file | | 10 | Framing deviation score exceeds cap (--close-camera) | | 11 | Kit part count, unapplied scale, namespace, or origin; also gallery asset-quality violation |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Boundary vert count or end-plane membership |
| 4 | End rings do not partition evenly, or opposing loops differ |
| 5 | Tiled instances do not share boundary positions |
| 6 | Shell or whole-asset bbox off the declared tile |
| 7 | Boundary edge count, non-manifold edges, or rim off the end planes |
| 8 | Detail part not watertight |
| 9 | Detail part reaches a tile boundary; also --output produced no file |
| 10 | Framing deviation score exceeds cap (--close-camera) |
| 11 | Kit part count, unapplied scale, namespace, or origin; also gallery asset-quality violation |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output, --falsify, or --close-camera.
A runnable example that assembles a brass orrery — sun, three planets on pivot arms, one moon — entirely through the data API, witnessing the parenting contract that generated Blender code gets wrong most often. child.parent = pivot alone re-interprets the child's local matrix in the pivot's space and the child visibly teleports; keeping the world transform takes the two-line idiom:
child.parent = pivot
child.matrix_parent_inverse = pivot.matrix_world.inverted()
-What it witnesses: the check first proves the trap is real (a bare-parented probe jumps by more than half a unit), then that the idiom restores the probe's world position to within 1e-5. It also asserts the second half of the contract — matrix_world is the *last-evaluated* matrix, stale after any transform edit until bpy.context.view_layer.update() — and finally that every planet and the two-level moon land exactly on their closed-form orbit positions after the pivots spin.
What it witnesses: the check first proves the trap is real (a bare-parented probe jumps by more than half a unit), then that the idiom restores the probe's world position to within 1e-5. It also asserts the second half of the contract — matrix_world is the last-evaluated matrix, stale after any transform edit until bpy.context.view_layer.update() — and finally that every planet and the two-level moon land exactly on their closed-form orbit positions after the pivots spin.
# Cheap correctness check (no render) — the CI check:
blender --background --python parent_inverse_orrery.py --
@@ -275,7 +280,7 @@ Run
blender --background --python parent_inverse_orrery.py -- --output orrery.png --engine cycles
Per-script sequential checks. 9 is a valid check code; there is no rule against it.
| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Bare parenting did not jump | | 4 | Keep-world idiom off | | 5 | Stale matrix_world contract broken | | 6 | Planet off closed-form orbit (--skip-mpi lands here) | | 7 | Moon off closed-form orbit | | 8 | --output produced no file |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Bare parenting did not jump |
| 4 | Keep-world idiom off |
| 5 | Stale matrix_world contract broken |
| 6 | Planet off closed-form orbit (--skip-mpi lands here) |
| 7 | Moon off closed-form orbit |
| 8 | --output produced no file |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --skip-mpi.
Intended size: 1.40 m sitting width, 0.50 m seat depth, 0.43 m seat height, 0.64 m arm height; outer AABB 1.295 × 0.632 × 0.884 m.
Declared as named constants; every gate recomputes from the mesh, materials, UVs, evaluated LOD, collider, or export file.
-| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) | | --- | --- | --- | | Base triangles | 2200–4200 | 2520 / 2520 / 2520 | | LOD1 ratio | 0.32–0.62 of base | 0.5000 / 0.5000 / 0.5000 | | LOD2 ratio | 0.10–0.35 of base | 0.2198 / 0.2198 / 0.2198 | | Materials | exactly 2 distinct, ≥48 metal, ≥24 wood | 2 slots, 540 metal / 756 wood | | UVs | in 0..1, AABB overlap ≤ 1e-5 | in range, overlap 0 | | Outer AABB | (1.295, 0.632, 0.884) m ± 0.015 | (1.2945, 0.6314, 0.8840), zmin 0 | | Collider tris | ≤ 180 | 82 | | Export | written, size > 0 | 188164 / 188164 / 188148 bytes |
| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) |
|---|---|---|
| Base triangles | 2200–4200 | 2520 / 2520 / 2520 |
| LOD1 ratio | 0.32–0.62 of base | 0.5000 / 0.5000 / 0.5000 |
| LOD2 ratio | 0.10–0.35 of base | 0.2198 / 0.2198 / 0.2198 |
| Materials | exactly 2 distinct, ≥48 metal, ≥24 wood | 2 slots, 540 metal / 756 wood |
| UVs | in 0..1, AABB overlap ≤ 1e-5 | in range, overlap 0 |
| Outer AABB | (1.295, 0.632, 0.884) m ± 0.015 | (1.2945, 0.6314, 0.8840), zmin 0 |
| Collider tris | ≤ 180 | 82 |
| Export | written, size > 0 | 188164 / 188164 / 188148 bytes |
Base triangles rose from 2100 to 2520 in the quality pass: box-segment scrolls became 6-gon tubes. Outer AABB widened from 1.080 × 0.566 × 0.853 m to a 1.40 m sitting-width bench.
DECIMATE COLLAPSE triangle counts are not guaranteed identical across series — the gate is a ratio band, not an exact count. This mesh matched on 4.5.11 / 5.1.2 / 5.2.1. Bake pixels are stochastic; the gate is has_data plus operator FINISHED, not byte-identity. Construction uses no RNG. Export byte counts differ by 16 B on 5.2.1 (glTF serializer), not a gated axis.
Recomputed from the generated mesh, not asserted about the script.
-| Axis | Declared | Measured (all three) | | --- | --- | --- | | Non-manifold edges | 0 | 0 | | Loose verts / edges | 0 / 0 | 0 / 0 | | Doubles merged at 1e-5 | 0 | 0 | | Zero-area faces | 0 | 0 | | N-gons | 0 | 0 | | Coplanar disjoint face pairs | 0 | 0 | | Grounded: zmin | within 1e-4 of 0 | 0.0000 | | Named toes | 4, each zmin ≤ 0.002 | 4, 0.00000 |
| Axis | Declared | Measured (all three) |
|---|---|---|
| Non-manifold edges | 0 | 0 |
| Loose verts / edges | 0 / 0 | 0 / 0 |
| Doubles merged at 1e-5 | 0 | 0 |
| Zero-area faces | 0 | 0 |
| N-gons | 0 | 0 |
| Coplanar disjoint face pairs | 0 | 0 |
Grounded: zmin | within 1e-4 of 0 | 0.0000 |
| Named toes | 4, each zmin ≤ 0.002 | 4, 0.00000 |
| Axis | Declared | Measured (all three) | | --- | --- | --- | | Stretcher-to-post BVH gap | ≤ 0.008 m | 0.00120 | | Slat-to-post BVH gap | ≤ 0.008 m | 0.00028 | | Seat span vs 2·hx | ± 0.08 m | 1.2720 vs 1.260 |
| Axis | Declared | Measured (all three) |
|---|---|---|
| Stretcher-to-post BVH gap | ≤ 0.008 m | 0.00120 |
| Slat-to-post BVH gap | ≤ 0.008 m | 0.00028 |
Seat span vs 2·hx | ± 0.08 m | 1.2720 vs 1.260 |
Each violates one named budget. All were run on 4.5.11, 5.1.2 and 5.2.1 and returned the same code on each.
-| Flag | Budget violated | Exit | | --- | --- | --- | | --skip-decimate | LOD1 ratio band | 9 | | --stray-vert | loose vertex count is 0 | 15 | | --lift-z | bounding box zmin is 0 | 16 | | --short-feet | named toes plant at z=0 | 16 | | --float-stretcher | stretcher-to-post BVH gap | 17 | | --gap-slats | slat-to-post BVH gap | 18 | | --narrow-seat | seat span vs 2·hx | 19 |
| Flag | Budget violated | Exit |
|---|---|---|
--skip-decimate | LOD1 ratio band | 9 |
--stray-vert | loose vertex count is 0 | 15 |
--lift-z | bounding box zmin is 0 | 16 |
--short-feet | named toes plant at z=0 | 16 |
--float-stretcher | stretcher-to-post BVH gap | 17 |
--gap-slats | slat-to-post BVH gap | 18 |
--narrow-seat | seat span vs 2·hx | 19 |
--short-feet lifts only the outward toe verts. After recenter the post join is the AABB zmin, so the AABB gate would still pass; the named-toe stations fail.
blender --background --python park_bench.py --
@@ -291,7 +296,7 @@ Run
Smoke passes no flags.
Exit codes
File-local. 9 is a valid check code. 10 is reserved for gallery_framing.check_framing on the --output path. 15–19 are the hygiene and joint-fit family.
-| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Mesh did not build / no UV layer | | 4 | Base triangle count outside range | | 5 | Material count ≠ 2 distinct slots, or wood/metal faces missing | | 6 | UVs outside 0..1 | | 7 | UV AABB overlap above tolerance | | 8 | World AABB off declared outer size | | 9 | LOD ratio band (--skip-decimate lands here) | | 10 | Framing gate (render path only) | | 11 | Collider triangle count above ceiling | | 12 | Bake did not finish or image has no data | | 13 | Export file missing or empty | | 14 | --output produced no file | | 15 | Hygiene (--stray-vert lands here) | | 16 | Grounded zmin / named toes (--lift-z, --short-feet) | | 17 | Stretcher-to-post gap (--float-stretcher) | | 18 | Slat-to-post gap (--gap-slats) | | 19 | Seat span (--narrow-seat) |
+Code Meaning 0 Success 1 Uncaught exception (FATAL wrapper) 2 argparse / usage 3 Mesh did not build / no UV layer 4 Base triangle count outside range 5 Material count ≠ 2 distinct slots, or wood/metal faces missing 6 UVs outside 0..1 7 UV AABB overlap above tolerance 8 World AABB off declared outer size 9 LOD ratio band (--skip-decimate lands here) 10 Framing gate (render path only) 11 Collider triangle count above ceiling 12 Bake did not finish or image has no data 13 Export file missing or empty 14 --output produced no file15 Hygiene (--stray-vert lands here) 16 Grounded zmin / named toes (--lift-z, --short-feet) 17 Stretcher-to-post gap (--float-stretcher) 18 Slat-to-post gap (--gap-slats) 19 Seat span (--narrow-seat)
A runnable example that witnesses the float-image PNG save trap: a float_buffer=True image saved via Image.save() to PNG is written as 16-bit RGBA and is unpremultiplied as if the buffer were associated-alpha before storage. The normal pixels API authors *straight* RGBA, so any channel where c > a blows past 1.0 and clamps to white — closed-form RGB error reaches 0.98 for authored (0.02, 0.02, 0.02) at alpha 1/255. The same buffer saved as OpenEXR round-trips at float precision. A byte image (float_buffer=False) writes 8-bit PNG with straight alpha and only pays ordinary quantization.
A runnable example that witnesses the float-image PNG save trap: a float_buffer=True image saved via Image.save() to PNG is written as 16-bit RGBA and is unpremultiplied as if the buffer were associated-alpha before storage. The normal pixels API authors straight RGBA, so any channel where c > a blows past 1.0 and clamps to white — closed-form RGB error reaches 0.98 for authored (0.02, 0.02, 0.02) at alpha 1/255. The same buffer saved as OpenEXR round-trips at float precision. A byte image (float_buffer=False) writes 8-bit PNG with straight alpha and only pays ordinary quantization.
Found while probing the ROADMAP "image save-format" candidate against live Blender 5.1 — the hazard is not classic 8-bit premul quantization (float PNG is 16-bit); it is the false associated-alpha unpremultiply on a straight buffer. Documented here so the contract cannot quietly drift.
What it witnesses:
Image.save() on a Non-Color float image writes RGBA16 (IHDR bit_depth=16, color_type=6). Reloaded pixels match the closed form q16(min(1, c / q16(a))) within 2/65535. Max RGB error vs authored on the probe palette is >= 0.90 (measured 0.98).1e-5 (measured ~3e-8).float_buffer=False writes RGBA8; pixels match independent per-channel q8 within 0.5/255. The stress cell at (0.02, a=1/255) stays near 0.02, not clamped white.color_mode='RGB' drops alpha — save_render with color_mode='RGB' reloads opaque alpha (≈ 1.0) even when authored alpha was 1/255.What each check catches on failure:
-color_mode='RGB' preserving authored alpha (exit 12).color_mode='RGB' preserving authored alpha (exit 12).Version divergence: none probed on the save/reload contracts above — they assert on Blender 5.1.1 locally. Blender 4.5 LTS is exercised by the blender-smoke CI job (4.5 is not installed on this authoring host; do not treat a 4.4 run as a 4.5 substitute). The only version gate in the file is the EEVEE engine id for the optional render (BLENDER_EEVEE_NEXT on 4.x, BLENDER_EEVEE on 5.x).
Render: two framed verification displays on a shared plinth — the left face bakes the closed-form PNG mangling (dark rows flash to white at low-alpha columns), the right face shows the authored straight buffer (EXR-clean); nameplates read FLOAT → PNG and FLOAT → EXR. If the contract failed, both panels would read the same.
Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is also the shared framing helper.
| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage; also closed-form palette no longer stresses unpremul | | 3 | float_buffer=True image reports is_float=False | | 4 | Float→PNG IHDR is not RGBA16 | | 5 | Float→PNG RGB error below floor (--opaque-alpha lands here) | | 6 | Float→PNG disagrees with closed-form false-unpremul model | | 7 | Float→EXR round-trip above tolerance | | 8 | float_buffer=False image reports is_float=True | | 9 | Byte→PNG IHDR is not RGBA8 | | 10 | Byte→PNG disagrees with straight-alpha 8-bit model; also gallery framing violation | | 11 | Byte→PNG stress cell looks false-unpremul-mangled | | 12 | EXR color_mode=RGB alpha-drop contract | | 13 | --output produced no file |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage; also closed-form palette no longer stresses unpremul |
| 3 | float_buffer=True image reports is_float=False |
| 4 | Float→PNG IHDR is not RGBA16 |
| 5 | Float→PNG RGB error below floor (--opaque-alpha lands here) |
| 6 | Float→PNG disagrees with closed-form false-unpremul model |
| 7 | Float→EXR round-trip above tolerance |
| 8 | float_buffer=False image reports is_float=True |
| 9 | Byte→PNG IHDR is not RGBA8 |
| 10 | Byte→PNG disagrees with straight-alpha 8-bit model; also gallery framing violation |
| 11 | Byte→PNG stress cell looks false-unpremul-mangled |
| 12 | EXR color_mode=RGB alpha-drop contract |
| 13 | --output produced no file |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --opaque-alpha.
Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper.
| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Stale matrix_world contract broken | | 4 | World bbox moved across bake | | 5 | Scale after bake is not (1,1,1) | | 6 | Origin not at local base center | | 7 | Bare parenting did not jump | | 8 | MPI did not restore world location (--skip-mpi lands here) | | 9 | --output produced no file | | 10 | Gallery framing violation |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Stale matrix_world contract broken |
| 4 | World bbox moved across bake |
| 5 | Scale after bake is not (1,1,1) |
| 6 | Origin not at local base center |
| 7 | Bare parenting did not jump |
| 8 | MPI did not restore world location (--skip-mpi lands here) |
| 9 | --output produced no file |
| 10 | Gallery framing violation |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --skip-mpi.
Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper.
| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Interface sockets missing Tint / Roughness / Shader | | 4 | Group datablock users ≠ 2 | | 5 | Instance points at a different node tree | | 6 | Instance Tint values identical (--same-tint lands here) | | 7 | --output produced no file | | 10 | Gallery framing violation |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Interface sockets missing Tint / Roughness / Shader |
| 4 | Group datablock users ≠ 2 |
| 5 | Instance points at a different node tree |
| 6 | Instance Tint values identical (--same-tint lands here) |
| 7 | --output produced no file |
| 10 | Gallery framing violation |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --same-tint.
Per-script sequential checks. 9 is a valid check code; there is no rule against it.
| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | No shape keys on mesh | | 4 | Key names ≠ Basis, Tall | | 5 | Tall.value ≠ 0.5 (--zero-blend lands here) | | 6 | Undeformed mesh.vertices not at Basis | | 7 | Evaluated vert off closed-form blend | | 8 | Evaluated Z span off closed form | | 9 | Top flare off closed form | | 10 | --output produced no file |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | No shape keys on mesh |
| 4 | Key names ≠ Basis, Tall |
| 5 | Tall.value ≠ 0.5 (--zero-blend lands here) |
| 6 | Undeformed mesh.vertices not at Basis |
| 7 | Evaluated vert off closed-form blend |
| 8 | Evaluated Z span off closed form |
| 9 | Top flare off closed form |
| 10 | --output produced no file |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --zero-blend.
Composes skills mesh-editing-and-bmesh, bake-high-to-low, depsgraph-and-evaluated-data, engine-export-presets, and snippets bake_normal_high_to_low.py, setup_bake_target_image.py, lod_chain.py / decimate_to_budget.py, convex_hull_collider.py, export_preset_unity.py (helpers copied, not imported as a package).
Declared as named constants; every gate recomputes from the mesh, materials, UVs, evaluated LOD, collider, or export file.
-| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) | | --- | --- | --- | | Base triangles | 1200–2800 | 2508 / 2508 / 2508 | | LOD1 ratio | 0.32–0.62 of base | 0.5000 / 0.5000 / 0.5000 | | LOD2 ratio | 0.10–0.35 of base | 0.2193 / 0.2193 / 0.2057 | | Materials | exactly 2 distinct; ≥200 wood, ≥48 metal faces | 2 slots; 1170 / 280 | | UVs | in 0..1, AABB overlap ≤ 1e-5 | in range, overlap 0 | | Outer AABB | (1.146, 0.726, 0.612) m ± 0.015 | (1.1460, 0.7260, 0.6120), zmin 0 | | Collider tris | ≤ 220 | 76 | | Export | written, size > 0 | 197408 / 197408 / 197392 bytes |
| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) |
|---|---|---|
| Base triangles | 1200–2800 | 2508 / 2508 / 2508 |
| LOD1 ratio | 0.32–0.62 of base | 0.5000 / 0.5000 / 0.5000 |
| LOD2 ratio | 0.10–0.35 of base | 0.2193 / 0.2193 / 0.2057 |
| Materials | exactly 2 distinct; ≥200 wood, ≥48 metal faces | 2 slots; 1170 / 280 |
| UVs | in 0..1, AABB overlap ≤ 1e-5 | in range, overlap 0 |
| Outer AABB | (1.146, 0.726, 0.612) m ± 0.015 | (1.1460, 0.7260, 0.6120), zmin 0 |
| Collider tris | ≤ 220 | 76 |
| Export | written, size > 0 | 197408 / 197408 / 197392 bytes |
Base triangles rose from 624 to 2508 in the quality pass: the old beveled cube with glued-on slats became a post-and-slat crate with tenons, sills, L-straps, and pipe handles.
DECIMATE COLLAPSE triangle counts are not identical across series — 5.2.1 is leaner on LOD2. The gate is a ratio band, not an exact count. Bake pixels are stochastic; the gate is has_data plus operator FINISHED, not byte-identity. Slat-width jitter uses fixed seed 17. Export byte counts differ by 16 B on 5.2.1 (glTF serializer), not a gated axis.
Recomputed from the generated mesh, not asserted about the script.
-| Axis | Declared | Measured (all three) | | --- | --- | --- | | Non-manifold edges | 0 | 0 | | Loose verts / edges | 0 / 0 | 0 / 0 | | Doubles merged at 1e-5 | 0 | 0 | | Zero-area faces | 0 | 0 | | N-gons | 0 | 0 | | Coplanar disjoint face pairs | 0 | 0 | | Grounded: zmin | within 1e-4 of 0 | 0.0000 | | Named supports: 3 skids | each zmin ≤ 1e-3 | 3, skid_z 0.00000 | | Body plan | 1.056 × 0.716 m ± 0.05 | 1.0560 × 0.7160 |
| Axis | Declared | Measured (all three) |
|---|---|---|
| Non-manifold edges | 0 | 0 |
| Loose verts / edges | 0 / 0 | 0 / 0 |
| Doubles merged at 1e-5 | 0 | 0 |
| Zero-area faces | 0 | 0 |
| N-gons | 0 | 0 |
| Coplanar disjoint face pairs | 0 | 0 |
Grounded: zmin | within 1e-4 of 0 | 0.0000 |
| Named supports: 3 skids | each zmin ≤ 1e-3 | 3, skid_z 0.00000 |
| Body plan | 1.056 × 0.716 m ± 0.05 | 1.0560 × 0.7160 |
| Axis | Declared | Measured (all three) | | --- | --- | --- | | Handle-end BVH gap | ≤ 0.008 m | 0.00000 | | Slat-post BVH gap | ≤ 0.006 m | 0.00000 |
+| Axis | Declared | Measured (all three) |
|---|---|---|
| Handle-end BVH gap | ≤ 0.008 m | 0.00000 |
| Slat-post BVH gap | ≤ 0.006 m | 0.00000 |
Each violates one named budget. All seven were run on 4.5.11, 5.1.2 and 5.2.1 and returned the same code on each.
-| Flag | Budget violated | Exit | | --- | --- | --- | | --skip-decimate | LOD1 ratio band | 9 | | --stray-vert | loose vertex count is 0 | 15 | | --lift-z | bounding box zmin is 0 | 16 | | --short-skids | named skid supports at Z=0 | 16 | | --float-handle | handle-end gap | 17 | | --omit-slats | slat-post seat | 18 |
| Flag | Budget violated | Exit |
|---|---|---|
--skip-decimate | LOD1 ratio band | 9 |
--stray-vert | loose vertex count is 0 | 15 |
--lift-z | bounding box zmin is 0 | 16 |
--short-skids | named skid supports at Z=0 | 16 |
--float-handle | handle-end gap | 17 |
--omit-slats | slat-post seat | 18 |
blender --background --python shipping_crate.py --
blender --background --python shipping_crate.py -- --skip-decimate
@@ -288,7 +293,7 @@ Run
Smoke passes no flags.
Exit codes
File-local. 9 is a valid check code. 10 is reserved for gallery_framing.check_framing on the --output path. 15–19 are the hygiene and joint-fit family.
-| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Mesh did not build / no UV layer | | 4 | Base triangle count outside range | | 5 | Material count ≠ 2 distinct slots, or a face-count floor missed | | 6 | UVs outside 0..1 | | 7 | UV AABB overlap above tolerance | | 8 | World AABB off declared outer size | | 9 | LOD ratio band (--skip-decimate lands here) | | 10 | Framing gate (render path only) | | 11 | Collider triangle count above ceiling | | 12 | Bake did not finish or image has no data | | 13 | Export file missing or empty | | 14 | --output produced no file | | 15 | Mesh hygiene: loose, non-manifold, zero-area, doubles, n-gons, z-fight | | 16 | Not grounded: bounding box zmin off 0, or a named skid floats | | 17 | Joint fit: handle-end gap | | 18 | Seat: slat-post gap | | 19 | Body plan off the stated real-world size |
+Code Meaning 0 Success 1 Uncaught exception (FATAL wrapper) 2 argparse / usage 3 Mesh did not build / no UV layer 4 Base triangle count outside range 5 Material count ≠ 2 distinct slots, or a face-count floor missed 6 UVs outside 0..1 7 UV AABB overlap above tolerance 8 World AABB off declared outer size 9 LOD ratio band (--skip-decimate lands here) 10 Framing gate (render path only) 11 Collider triangle count above ceiling 12 Bake did not finish or image has no data 13 Export file missing or empty 14 --output produced no file15 Mesh hygiene: loose, non-manifold, zero-area, doubles, n-gons, z-fight 16 Not grounded: bounding box zmin off 0, or a named skid floats 17 Joint fit: handle-end gap 18 Seat: slat-post gap 19 Body plan off the stated real-world size
Intended size: 1.48 m post, boards 0.62 m and 0.54 m, 0.118 m post stock; outer AABB 1.260 × 0.142 × 1.551 m.
Declared as named constants; every gate recomputes from the mesh, materials, UVs, evaluated LOD, collider, or export file.
-| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) | | --- | --- | --- | | Base triangles | 320–480 | 376 / 376 / 376 | | LOD1 ratio | 0.32–0.62 of base | 0.5000 / 0.5000 / 0.4681 | | LOD2 ratio | 0.10–0.35 of base | 0.2181 / 0.2181 / 0.1649 | | Materials | exactly 3 distinct, ≥24 metal, ≥12 board, ≥12 wood | 3 slots, 108 metal, 22 board, 60 wood | | UVs | in 0..1, AABB overlap ≤ 1e-5 | in range, overlap 0 | | Outer AABB | (1.260, 0.142, 1.551) m ± 0.015 | (1.2600, 0.1420, 1.5510), zmin 0 | | Collider tris | ≤ 180 | 52 | | Export | written, size > 0 | 35688 / 35688 / 35676 bytes |
| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) |
|---|---|---|
| Base triangles | 320–480 | 376 / 376 / 376 |
| LOD1 ratio | 0.32–0.62 of base | 0.5000 / 0.5000 / 0.4681 |
| LOD2 ratio | 0.10–0.35 of base | 0.2181 / 0.2181 / 0.1649 |
| Materials | exactly 3 distinct, ≥24 metal, ≥12 board, ≥12 wood | 3 slots, 108 metal, 22 board, 60 wood |
| UVs | in 0..1, AABB overlap ≤ 1e-5 | in range, overlap 0 |
| Outer AABB | (1.260, 0.142, 1.551) m ± 0.015 | (1.2600, 0.1420, 1.5510), zmin 0 |
| Collider tris | ≤ 180 | 52 |
| Export | written, size > 0 | 35688 / 35688 / 35676 bytes |
Base triangles dropped from 504 to 376 in the quality pass: glued diamond cubes and a solid shoe block became wedges and a four-plate collar. Outer AABB tightened from 1.368 × 0.201 × 1.566 m.
DECIMATE COLLAPSE triangle counts are not identical across series — 5.2.1 is leaner on LOD1 and LOD2. The gate is a ratio band, not an exact count. Bake pixels are stochastic; the gate is has_data plus operator FINISHED, not byte-identity. Construction uses no RNG. Export byte counts differ by 12 B on 5.2.1 (glTF serializer), not a gated axis.
Recomputed from the generated mesh, not asserted about the script.
-| Axis | Declared | Measured (all three) | | --- | --- | --- | | Non-manifold edges | 0 | 0 | | Loose verts / edges | 0 / 0 | 0 / 0 | | Doubles merged at 1e-5 | 0 | 0 | | Zero-area faces | 0 | 0 | | N-gons | 0 | 0 | | Coplanar disjoint face pairs | 0 | 0 | | Grounded: zmin | within 1e-4 of 0 | 0.0000 | | Named shoe plates | ≥1, each zmin ≤ 0.002 | 4, 0.00000 |
| Axis | Declared | Measured (all three) |
|---|---|---|
| Non-manifold edges | 0 | 0 |
| Loose verts / edges | 0 / 0 | 0 / 0 |
| Doubles merged at 1e-5 | 0 | 0 |
| Zero-area faces | 0 | 0 |
| N-gons | 0 | 0 |
| Coplanar disjoint face pairs | 0 | 0 |
Grounded: zmin | within 1e-4 of 0 | 0.0000 |
| Named shoe plates | ≥1, each zmin ≤ 0.002 | 4, 0.00000 |
| Axis | Declared | Measured (all three) | | --- | --- | --- | | Board-to-post BVH gap | ≤ 0.008 m | 0.00500 | | Strap-to-post BVH gap | ≤ 0.008 m | 0.00300 | | Board AABB Y (axis-aligned) | ≤ 0.055 m | 0.0280 |
+| Axis | Declared | Measured (all three) |
|---|---|---|
| Board-to-post BVH gap | ≤ 0.008 m | 0.00500 |
| Strap-to-post BVH gap | ≤ 0.008 m | 0.00300 |
| Board AABB Y (axis-aligned) | ≤ 0.055 m | 0.0280 |
Each violates one named budget. All were run on 4.5.11, 5.1.2 and 5.2.1 and returned the same code on each.
-| Flag | Budget violated | Exit | | --- | --- | --- | | --skip-decimate | LOD1 ratio band | 9 | | --stray-vert | loose vertex count is 0 | 15 | | --lift-z | bounding box zmin is 0 | 16 | | --short-shoe | named shoes plant at z=0 | 16 | | --gap-board | board-to-post BVH gap | 17 | | --float-strap | strap-to-post BVH gap | 18 | | --yaw-boards | board AABB Y (orthogonal) | 19 |
| Flag | Budget violated | Exit |
|---|---|---|
--skip-decimate | LOD1 ratio band | 9 |
--stray-vert | loose vertex count is 0 | 15 |
--lift-z | bounding box zmin is 0 | 16 |
--short-shoe | named shoes plant at z=0 | 16 |
--gap-board | board-to-post BVH gap | 17 |
--float-strap | strap-to-post BVH gap | 18 |
--yaw-boards | board AABB Y (orthogonal) | 19 |
--short-shoe lifts only the iron shoe plates. After zmin snap the post is the AABB zmin, so the AABB gate would still pass; the named shoe stations fail. --gap-board moves the board inner station outside the post. --float-strap adds Y offset to the U-wrap so it no longer bites. --yaw-boards restores a 0.12 rad yaw; the AABB secondary extent exceeds the thickness bound.
blender --background --python signpost.py --
@@ -291,7 +296,7 @@ Run
Smoke passes no flags.
Exit codes
File-local. 9 is a valid check code. 10 is reserved for gallery_framing.check_framing on the --output path. 15–19 are the hygiene and joint-fit family.
-| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Mesh did not build / no UV layer | | 4 | Base triangle count outside range | | 5 | Material count ≠ 3 distinct slots, or wood/board/metal faces missing | | 6 | UVs outside 0..1 | | 7 | UV AABB overlap above tolerance | | 8 | World AABB off declared outer size | | 9 | LOD ratio band (--skip-decimate lands here) | | 10 | Framing gate (render path only) | | 11 | Collider triangle count above ceiling | | 12 | Bake did not finish or image has no data | | 13 | Export file missing or empty | | 14 | --output produced no file | | 15 | Hygiene (--stray-vert lands here) | | 16 | Grounded zmin / named shoes (--lift-z, --short-shoe) | | 17 | Board-to-post gap (--gap-board) | | 18 | Strap-to-post gap (--float-strap) | | 19 | Board yaw / AABB Y (--yaw-boards) |
+Code Meaning 0 Success 1 Uncaught exception (FATAL wrapper) 2 argparse / usage 3 Mesh did not build / no UV layer 4 Base triangle count outside range 5 Material count ≠ 3 distinct slots, or wood/board/metal faces missing 6 UVs outside 0..1 7 UV AABB overlap above tolerance 8 World AABB off declared outer size 9 LOD ratio band (--skip-decimate lands here) 10 Framing gate (render path only) 11 Collider triangle count above ceiling 12 Bake did not finish or image has no data 13 Export file missing or empty 14 --output produced no file15 Hygiene (--stray-vert lands here) 16 Grounded zmin / named shoes (--lift-z, --short-shoe) 17 Board-to-post gap (--gap-board) 18 Strap-to-post gap (--float-strap) 19 Board yaw / AABB Y (--yaw-boards)
What each check catches on failure: broken Sky→Background link (exit 6), wrong sky_type for the running version (exit 3), lost sun_elevation round-trip (exit 4), dust_density / NISHITA present on the wrong side of 5.0 (exit 5), a non-working sky (zenith floor, exit 7), and an elevation that does not brighten zenith (exit 8 — rise below 1.25).
Version witness: sky_type and the dust/aerosol rename are the divergence; sun_elevation itself is stable. Zenith rise differs by model (Nishita vs multiple scattering) but clears the same gate on both binaries.
World carries a Nishita / multiple-scattering Sky Texture instead of the default near-black Background — the contract *is* the sky. Gallery still is a dual-elevation diptych (8° | 55°) so failure (identical panels) is visible at thumbnail scale. Layer 1 still holds: view_transform='Standard', designed terracotta materials, chosen camera, no helpers in frame.
World carries a Nishita / multiple-scattering Sky Texture instead of the default near-black Background — the contract is the sky. Gallery still is a dual-elevation diptych (8° | 55°) so failure (identical panels) is visible at thumbnail scale. Layer 1 still holds: view_transform='Standard', designed terracotta materials, chosen camera, no helpers in frame.
The hero is the sky itself — a world contract, not an object on the stage. The silhouette matte can only measure the cone reference props (0.369x / 0.489y), so the out-of-band number is a measurement artifact, not the composition; if ever wired to examples/gallery_framing.py, this example reports with deviation="world-contract subject; matte sees only the cone props" rather than enforcing.
Per-script sequential checks. 9 is a valid check code; there is no rule against it.
| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | sky_type is not the identifier for this Blender | | 4 | sun_elevation round-trip failed | | 5 | dust_density / aerosol_density / NISHITA / MULTIPLE_SCATTERING trap | | 6 | Sky → Background → World Output links broken (--unlink-sky lands here) | | 7 | High-elevation zenith luma below floor | | 8 | Zenith luma did not rise with elevation | | 9 | --output produced no file |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | sky_type is not the identifier for this Blender |
| 4 | sun_elevation round-trip failed |
| 5 | dust_density / aerosol_density / NISHITA / MULTIPLE_SCATTERING trap |
| 6 | Sky → Background → World Output links broken (--unlink-sky lands here) |
| 7 | High-elevation zenith luma below floor |
| 8 | Zenith luma did not rise with elevation |
| 9 | --output produced no file |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --unlink-sky.
Per-script sequential checks. 9 is a valid check code; there is no rule against it.
| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Topology ≠ 60/90/32 | | 4 | Euler characteristic ≠ 2 | | 5 | Face census ≠ 12 pentagons + 20 hexagons | | 6 | Vertex degree not uniform 3; also --output produced no file | | 7 | Non-manifold edges | | 8 | Edge lengths not uniform | | 9 | Face planarity off | | 10 | Centroid off origin | | 11 | Circumradius not uniform | | 12 | Panel material count ≠ 2 | | 13 | Panel binding not by vertex count (--invert-bind lands here) |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Topology ≠ 60/90/32 |
| 4 | Euler characteristic ≠ 2 |
| 5 | Face census ≠ 12 pentagons + 20 hexagons |
| 6 | Vertex degree not uniform 3; also --output produced no file |
| 7 | Non-manifold edges |
| 8 | Edge lengths not uniform |
| 9 | Face planarity off |
| 10 | Centroid off origin |
| 11 | Circumradius not uniform |
| 12 | Panel material count ≠ 2 |
| 13 | Panel binding not by vertex count (--invert-bind lands here) |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --invert-bind.
A runnable example building the thing a spawn system actually leans on: named socket empties parented into an asset, so a module dropped onto a socket with an *identity local transform* lands exactly where the artist put it, oriented the way the naming convention promises. Every clause in that sentence is a matrix identity, and every one of them drifts silently — a skipped matrix_parent_inverse, a basis built with the cross product the wrong way round, a transform frozen on the parent after the sockets were placed.
A runnable example building the thing a spawn system actually leans on: named socket empties parented into an asset, so a module dropped onto a socket with an identity local transform lands exactly where the artist put it, oriented the way the naming convention promises. Every clause in that sentence is a matrix identity, and every one of them drifts silently — a skipped matrix_parent_inverse, a basis built with the cross product the wrong way round, a transform frozen on the parent after the sockets were placed.
The asset, for reuse: Drone.Survey, an 0.86 m span survey quadcopter — a lofted fuselage with a vented deck and canopy face, four canted booms with finned motor cans and nav lights, and a two-rail landing gear. Seven mount pads carry seven sockets, and four interchangeable modules seat on them:
| Socket | Mount normal | Module | | --- | --- | --- | | SKT_Rotor.FL/FR/RL/RR | 8° canted outward | Mod.Rotor (twisted, tapered blades) | | SKT_Camera | belly, −Z | Mod.CamPod (gimbal ball + lens) | | SKT_Mast | dorsal, +Z | Mod.Mast (antenna, dish, beacon) | | SKT_Battery | rear, tilted down | Mod.Battery (cell block + gauge) |
| Socket | Mount normal | Module |
|---|---|---|
SKT_Rotor.FL/FR/RL/RR | 8° canted outward | Mod.Rotor (twisted, tapered blades) |
SKT_Camera | belly, −Z | Mod.CamPod (gimbal ball + lens) |
SKT_Mast | dorsal, +Z | Mod.Mast (antenna, dish, beacon) |
SKT_Battery | rear, tilted down | Mod.Battery (cell block + gauge) |
Origin is the skid contact plane centre (z == 0 is where it rests, +X is forward), transforms are identity by construction, datablocks live under Drone.Survey.* and SKT_*, and accents are per-face material slots rather than extra objects. To use it: instantiate a module, parent it to the socket, zero the local transform. That is the whole API — and it only works because the socket matrices hold.
Socket convention (encoded in the SKT_ prefix):
Y × Z, so the basis is right-handed (det == +1).Pipeline arc neighbours: pivot and origin discipline in prop-origin-transform, parent-inverse under animation in parent-inverse-orrery, tile-grid boundary contracts in modular-kit-snap, collision packaging in collision-hull-proxy.
What it witnesses (all closed form or independently re-derived):
-matrix_world values equal root.matrix_world @ authored_local within 1e-6 (measured 1.788e-07); every basis is orthonormal (Gram error 3.576e-07) and right-handed (det − 1 = 2.980e-07).dot − 1 = 1.735e-07), and carries a strictly identity local matrix — seated by the socket, not nudged into place.root.matrix_world @ authored_local (2.384e-07) and every module stays on its socket (0.0).Drone.Survey.* / SKT_* names, no default datablocks, min z == 0.00e+00.matrix_world values equal root.matrix_world @ authored_local within 1e-6 (measured 1.788e-07); every basis is orthonormal (Gram error 3.576e-07) and right-handed (det − 1 = 2.980e-07).dot − 1 = 1.735e-07), and carries a strictly identity local matrix — seated by the socket, not nudged into place.root.matrix_world @ authored_local (2.384e-07) and every module stays on its socket (0.0).Drone.Survey.* / SKT_* names, no default datablocks, min z == 0.00e+00.transform_apply on the root resets 7/7 children's matrix_parent_inverse to identity and hands each child the root's scale locally (measured 0.35 for an applied 1.35). World matrices survive to float32; *local* transforms do not. A spawn system that reads matrix_local, or an exporter that trusts "transforms are applied", reads different numbers after an artist freezes the rig. Both halves are asserted, so the behaviour cannot change under us silently.bm.normal_update() does not fix winding. It recomputes normals from the existing winding. The lofted fuselage wound its side quads inward and rendered as a flat unshaded white panel rather than an obvious hole; bmesh.ops.recalc_face_normals is what fixes it, and Part.finish now always calls it.degenerate-bevel-weld witnesses. 2.2 mm is clean. Note that examples/gallery_asset_quality.py raises ValueError: zero length vectors have no valid angle on such a mesh rather than reporting it.transform_apply on the root resets 7/7 children's matrix_parent_inverse to identity and hands each child the root's scale locally (measured 0.35 for an applied 1.35). World matrices survive to float32; local transforms do not. A spawn system that reads matrix_local, or an exporter that trusts "transforms are applied", reads different numbers after an artist freezes the rig. Both halves are asserted, so the behaviour cannot change under us silently.bm.normal_update() does not fix winding. It recomputes normals from the existing winding. The lofted fuselage wound its side quads inward and rendered as a flat unshaded white panel rather than an obvious hole; bmesh.ops.recalc_face_normals is what fixes it, and Part.finish now always calls it.degenerate-bevel-weld witnesses. 2.2 mm is clean. Note that examples/gallery_asset_quality.py raises ValueError: zero length vectors have no valid angle on such a mesh rather than reporting it.What each check catches on failure (every one probed, with the measured error): sockets parented without the parent-inverse — worst jump 0.690128 m (exit 3); a left-handed basis from z.cross(y) — det == −1.000000 (exit 3); a rotor pad built flat instead of canted — 1.395e-01 off the Newell normal (exit 4); an 11° roll on the basis — 1.917e-01 off the up rule (exit 4); a module nudged 4 mm off its socket (exit 5); a default Cube datablock name (exit 6); a socket left unparented so it ignores the re-pose — 1.417e+00 (exit 7); a child selected during the apply — 1.252e+00 to 2.244e+00 of socket drift (exit 8).
Version witness: check output is byte-identical on Blender 4.5.11 LTS and 5.1.2 — same counts, same measured deviations to every printed digit, and the same framing numbers on the render path.
Render as proof: the drone on the dark stage with every module seated, the orange mount pads reading as the socket signature. The falsification variant (--falsify) parents the sockets without matrix_parent_inverse: the pads are left bare, the rotors and mast vanish from the airframe, and the camera pod and battery hang in space near the top of frame. A broken matrix is not a subtle pixel difference here — the asset comes apart.
The --output render path additionally gates framing via examples/gallery_framing.py (fill 0.881x, margins 0.066/0.053/0.122/0.106, no edge touched) and the asset floors via examples/gallery_asset_quality.py (32 materials, edge90 0.027, no default names). --falsify is a diagnostic, not a gallery hero: it measures and prints framing but does not enforce the Layer 1 band — modules leave the frame on purpose. --falsify and --probe still exit 0.
Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper. 11 is the shared asset-quality helper.
| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Socket world matrix, orthonormal basis, or determinant | | 4 | Socket pad normal, origin, or up-axis rule | | 5 | Module seating / mount axis / leftover local transform | | 6 | Population, namespace, default datablock name, SKT_ prefix, or skid plane | | 7 | Re-pose: socket or module lost the root | | 8 | Parent-inverse clear, child scale pushdown, or post-apply module drift | | 9 | --output produced no file | | 10 | Gallery framing violation | | 11 | Gallery asset-quality violation |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Socket world matrix, orthonormal basis, or determinant |
| 4 | Socket pad normal, origin, or up-axis rule |
| 5 | Module seating / mount axis / leftover local transform |
| 6 | Population, namespace, default datablock name, SKT_ prefix, or skid plane |
| 7 | Re-pose: socket or module lost the root |
| 8 | Parent-inverse clear, child scale pushdown, or post-apply module drift |
| 9 | --output produced no file |
| 10 | Gallery framing violation |
| 11 | Gallery asset-quality violation |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output, --falsify, or --probe.
A freestanding masonry arch — two coursed piers, projecting imposts, nine voussoirs turning a semicircle, and a keystone standing proud at the crown. A showcase piece, not an example — it witnesses no API contract. It asserts that generated geometry meets declared asset budgets, recomputed from the finished mesh.
| Shipped content | Used for | | --- | --- | | skills/mesh-editing-and-bmesh | wedge and box construction, chamfer, UVs in one bmesh | | skills/procedural-materials-and-shaders | two Principled stone materials with noise-driven weathering | | skills/bake-high-to-low | Cycles tangent-space normal bake, high onto low | | skills/engine-export-presets | Unity glTF (export_yup=True) | | skills/depsgraph-and-evaluated-data | evaluated triangle counts for the LOD ratios | | snippets/decimate_to_budget.py | LOD1 / LOD2 COLLAPSE chain | | snippets/convex_hull_collider.py | convex collider | | snippets/lod_chain.py | LOD naming and ratio pattern | | examples/mesh-hygiene-audit | hygiene combinatorics (copied, not imported) |
| Shipped content | Used for |
|---|---|
skills/mesh-editing-and-bmesh | wedge and box construction, chamfer, UVs in one bmesh |
skills/procedural-materials-and-shaders | two Principled stone materials with noise-driven weathering |
skills/bake-high-to-low | Cycles tangent-space normal bake, high onto low |
skills/engine-export-presets | Unity glTF (export_yup=True) |
skills/depsgraph-and-evaluated-data | evaluated triangle counts for the LOD ratios |
snippets/decimate_to_budget.py | LOD1 / LOD2 COLLAPSE chain |
snippets/convex_hull_collider.py | convex collider |
snippets/lod_chain.py | LOD naming and ratio pattern |
examples/mesh-hygiene-audit | hygiene combinatorics (copied, not imported) |
An arch can fail invisibly. Nine wedge blocks laid in a row are still nine wedge blocks; the thing that makes them an arch is that their intrados vertices sit on a circle. So the piece recomputes that circle from vertex positions — not from the angles the generator used — and asserts every intrados vertex lands within 4 mm of the declared 0.60 m radius, over an arc spanning at least 168°.
--off-circle is the falsifier built for exactly this. It keeps the angles, the joints, the materials, the triangle count and the bounding box identical, and only wanders the intrados radius by ±18 mm. Every other budget in the piece still passes. Only the circle fit sees it.
The first attempt selected intrados vertices by radius. That swept in the chamfer vertices sitting one bevel-width out on each radial face and reported a 5.1 mm error on a true arch. The fit now runs over the vertices of faces that actually face the springing centre — normal inward in XZ *and* with a small Y component, which is what excludes the chamfer strips running along the intrados edges.
+The first attempt selected intrados vertices by radius. That swept in the chamfer vertices sitting one bevel-width out on each radial face and reported a 5.1 mm error on a true arch. The fit now runs over the vertices of faces that actually face the springing centre — normal inward in XZ and with a small Y component, which is what excludes the chamfer strips running along the intrados edges.
Declared in the script as named constants, recomputed from the generated mesh. Measured values are from Blender 5.2.1; the cross-version table is at the end.
-| Budget | Band | Measured | | --- | --- | --- | | Base triangles | 700–2600 | 924 | | LOD1 ratio | 0.32–0.62 | 0.5000 | | LOD2 ratio | 0.10–0.35 | 0.2121 (5.2) / 0.2186 (4.5, 5.1) | | Material slots | exactly 2, distinct | 2 | | Dressed faces (keystone, imposts, plinth and head courses) | ≥ 100 | 182 | | Ashlar faces | ≥ 280 | 364 | | UV bounds | inside 0..1 | (0.0017, 0.0017)–(0.9983, 0.9983) | | UV AABB overlap | ≤ 1e-5 | 0.000000 | | Outer AABB | 1.644 × 0.510 × 2.057 m ± 0.020 | 1.6440 × 0.5100 × 2.0568 | | Clear opening | 1.200 m ± 0.015, measured between the pier faces | 1.2033 | | Collider triangles | ≤ 260 | 168 | | Normal bake | {'FINISHED'} with image data | {'FINISHED'}, has_data=True | | glTF export | file written, non-empty | ~87 kB | | Hygiene | all zero | loose 0/0, non-manifold 0, zero-area 0, doubles 0, n-gons 0, coplanar disjoint pairs 0 | | Grounded AABB | \|zmin\| ≤ 1e-4 | 0.00000 | | Pier supports | 2 piers, each base course zmin ≤ 1e-3 | 2 at 0.00000 | | Keystone proud of the wall face | ≥ 0.020 m | 0.03500 | | Springing joint | overlap, surface gap ≤ 1e-4 | 0.00000 | | Mortar joints | every adjacent pair in 0.006–0.017 m | all eight at 0.00970 | | Intrados circle fit | every vertex within 0.004 m of R = 0.60 | 0.00097 | | Intrados arc span | ≥ 168° | 178.0° |
| Budget | Band | Measured | ||
|---|---|---|---|---|
| Base triangles | 700–2600 | 924 | ||
| LOD1 ratio | 0.32–0.62 | 0.5000 | ||
| LOD2 ratio | 0.10–0.35 | 0.2121 (5.2) / 0.2186 (4.5, 5.1) | ||
| Material slots | exactly 2, distinct | 2 | ||
| Dressed faces (keystone, imposts, plinth and head courses) | ≥ 100 | 182 | ||
| Ashlar faces | ≥ 280 | 364 | ||
| UV bounds | inside 0..1 | (0.0017, 0.0017)–(0.9983, 0.9983) | ||
| UV AABB overlap | ≤ 1e-5 | 0.000000 | ||
| Outer AABB | 1.644 × 0.510 × 2.057 m ± 0.020 | 1.6440 × 0.5100 × 2.0568 | ||
| Clear opening | 1.200 m ± 0.015, measured between the pier faces | 1.2033 | ||
| Collider triangles | ≤ 260 | 168 | ||
| Normal bake | {'FINISHED'} with image data | {'FINISHED'}, has_data=True | ||
| glTF export | file written, non-empty | ~87 kB | ||
| Hygiene | all zero | loose 0/0, non-manifold 0, zero-area 0, doubles 0, n-gons 0, coplanar disjoint pairs 0 | ||
| Grounded AABB | \ | zmin\ | ≤ 1e-4 | 0.00000 |
| Pier supports | 2 piers, each base course zmin ≤ 1e-3 | 2 at 0.00000 | ||
| Keystone proud of the wall face | ≥ 0.020 m | 0.03500 | ||
| Springing joint | overlap, surface gap ≤ 1e-4 | 0.00000 | ||
| Mortar joints | every adjacent pair in 0.006–0.017 m | all eight at 0.00970 | ||
| Intrados circle fit | every vertex within 0.004 m of R = 0.60 | 0.00097 | ||
| Intrados arc span | ≥ 168° | 178.0° |
Real-world size: a 1.20 m clear opening under a semicircular head, 1.64 m across the piers and 2.06 m to the top of the keystone — a garden gate arch.
Fixed seed 23; no unseeded randomness. Every measured value above is byte-identical on 4.5.11, 5.1.2 and 5.2.1 except LOD2, where DECIMATE COLLAPSE produces 196 triangles on 5.2 and 202 on 4.5 and 5.1. That is why the LOD gate is a ratio band (0.10–0.35, measured 0.2121 and 0.2186) and not an exact count.
Each breaks one pipeline stage so a named budget fails. All seven were run on 4.5.11, 5.1.2 and 5.2.1 and produced the same exit code on all three.
-| Flag | Breaks | Exit | | --- | --- | --- | | --skip-decimate | drops the DECIMATE modifiers, LOD1 ratio goes to 1.0000 | 9 | | --stray-vert | adds one loose vertex inside the opening, so hygiene catches it rather than the bounding box | 15 | | --lift-z | lifts the whole mesh 50 mm off the floor | 16 | | --float-pier | floats one pier 12 mm; the other still grounds the AABB, so only the named-support budget sees it | 16 | | --sink-keystone | sinks the keystone to a quarter of its projection (8.75 mm) — the imposts still set the Y envelope, so the bounding box is unchanged | 17 | | --wide-mortar | triples the joint angle, opening every joint to 29.1 mm | 18 | | --off-circle | wanders the intrados radius ±18 mm while keeping angles, joints and envelope | 19 |
| Flag | Breaks | Exit |
|---|---|---|
--skip-decimate | drops the DECIMATE modifiers, LOD1 ratio goes to 1.0000 | 9 |
--stray-vert | adds one loose vertex inside the opening, so hygiene catches it rather than the bounding box | 15 |
--lift-z | lifts the whole mesh 50 mm off the floor | 16 |
--float-pier | floats one pier 12 mm; the other still grounds the AABB, so only the named-support budget sees it | 16 |
--sink-keystone | sinks the keystone to a quarter of its projection (8.75 mm) — the imposts still set the Y envelope, so the bounding box is unchanged | 17 |
--wide-mortar | triples the joint angle, opening every joint to 29.1 mm | 18 |
--off-circle | wanders the intrados radius ±18 mm while keeping angles, joints and envelope | 19 |
Three of these needed the model changed, not the budget:
--sink-keystone originally removed the projection outright, which shrank the Y bounding box by 70 mm and tripped the AABB gate first. The imposts were added so the envelope no longer depends on the keystone — and an impost course is correct masonry the arch was missing anyway.--off-circle began as --flat-arch, laying the voussoirs as a lintel. That is 0.62 m shorter and fails on the bounding box, proving nothing about the circle fit. Wandering the radius inside the same envelope is the honest version.--float-pier tripped the z-fight budget rather than the support budget, because the impost was pinned to an absolute height while the courses under it rose into it. The impost now rides on its own pier.bmesh.ops.bevel gives every face it creates material_index 0, so chamfering nineteen blocks left 30 faces on the dressed slot and moved 464 to ashlar. The slot count and the distinct-material check both still passed. Only the per-material face floor caught it, which is exactly the class showcase/README.md warns about. Materials are now re-stamped after the bevel, per block, by nearest recorded centroid.N_COURSE beds rather than N_COURSE - 1.find_nearest is unsigned. A voussoir seated *inside* the pier head reported a 5.4 mm gap where there was none, because the distance to the host's skin is positive from inside too. Joint gaps now test BVH overlap first and return zero when two blocks interpenetrate.bmesh.ops.bevel gives every face it creates material_index 0, so chamfering nineteen blocks left 30 faces on the dressed slot and moved 464 to ashlar. The slot count and the distinct-material check both still passed. Only the per-material face floor caught it, which is exactly the class showcase/README.md warns about. Materials are now re-stamped after the bevel, per block, by nearest recorded centroid.N_COURSE beds rather than N_COURSE - 1.find_nearest is unsigned. A voussoir seated inside the pier head reported a 5.4 mm gap where there was none, because the distance to the host's skin is positive from inside too. Joint gaps now test BVH overlap first and return zero when two blocks interpenetrate.File-local and sequential. 9 is a valid check code; there is no rule against it. 1 is the FATAL wrapper — a crash, never a named check.
| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Mesh did not build, or has no UV layer | | 4 | Base triangle count outside band | | 5 | Material slots, or a material's face floor | | 6 | UVs outside 0..1 | | 7 | UV AABB overlap above tolerance | | 8 | Outer AABB off declared size | | 9 | LOD1 or LOD2 ratio outside band (--skip-decimate) | | 10 | Framing gate (examples/gallery_framing.py, render path only) | | 11 | Collider triangles above ceiling | | 12 | Normal bake failed or produced no image data | | 13 | glTF export missing or empty | | 14 | --output produced no file | | 15 | Mesh hygiene (--stray-vert) | | 16 | Grounded zmin, or a pier base floating (--lift-z, --float-pier) | | 17 | Keystone projection or springing joint (--sink-keystone) | | 18 | Mortar joint outside band (--wide-mortar) | | 19 | Intrados circle fit, or clear opening (--off-circle) |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Mesh did not build, or has no UV layer |
| 4 | Base triangle count outside band |
| 5 | Material slots, or a material's face floor |
| 6 | UVs outside 0..1 |
| 7 | UV AABB overlap above tolerance |
| 8 | Outer AABB off declared size |
| 9 | LOD1 or LOD2 ratio outside band (--skip-decimate) |
| 10 | Framing gate (examples/gallery_framing.py, render path only) |
| 11 | Collider triangles above ceiling |
| 12 | Normal bake failed or produced no image data |
| 13 | glTF export missing or empty |
| 14 | --output produced no file |
| 15 | Mesh hygiene (--stray-vert) |
| 16 | Grounded zmin, or a pier base floating (--lift-z, --float-pier) |
| 17 | Keystone projection or springing joint (--sink-keystone) |
| 18 | Mortar joint outside band (--wide-mortar) |
| 19 | Intrados circle fit, or clear opening (--off-circle) |
# Budget check, no render. ~0.82 s on 4.5, ~0.85 s on 5.1, ~0.98 s on 5.2.
blender --background --python stone_archway.py --
@@ -298,7 +302,7 @@ Run it
blender --background --python stone_archway.py -- --output arch.webp
Smoke runs the check-only path. It does not pass --output or any falsifier.
| Value | 4.5.11 | 5.1.2 | 5.2.1 | | --- | --- | --- | --- | | Base triangles | 924 | 924 | 924 | | LOD1 tris / ratio | 462 / 0.5000 | 462 / 0.5000 | 462 / 0.5000 | | LOD2 tris / ratio | 202 / 0.2186 | 202 / 0.2186 | 196 / 0.2121 | | Face counts (ashlar / dressed) | 364 / 182 | 364 / 182 | 364 / 182 | | Outer AABB | 1.6440 × 0.5100 × 2.0568 | same | same | | Collider tris | 168 | 168 | 168 | | Intrados deviation | 0.00097 | 0.00097 | 0.00097 | | Mortar joints | all 0.00970 | all 0.00970 | all 0.00970 | | Check wall-clock | ~0.82 s | ~0.85 s | ~0.98 s |
+| Value | 4.5.11 | 5.1.2 | 5.2.1 |
|---|---|---|---|
| Base triangles | 924 | 924 | 924 |
| LOD1 tris / ratio | 462 / 0.5000 | 462 / 0.5000 | 462 / 0.5000 |
| LOD2 tris / ratio | 202 / 0.2186 | 202 / 0.2186 | 196 / 0.2121 |
| Face counts (ashlar / dressed) | 364 / 182 | 364 / 182 | 364 / 182 |
| Outer AABB | 1.6440 × 0.5100 × 2.0568 | same | same |
| Collider tris | 168 | 168 | 168 |
| Intrados deviation | 0.00097 | 0.00097 | 0.00097 |
| Mortar joints | all 0.00970 | all 0.00970 | all 0.00970 |
| Check wall-clock | ~0.82 s | ~0.85 s | ~0.98 s |
Composes skills mesh-editing-and-bmesh, bake-high-to-low, depsgraph-and-evaluated-data, engine-export-presets, and snippets bake_normal_high_to_low.py, setup_bake_target_image.py, lod_chain.py / decimate_to_budget.py, convex_hull_collider.py, export_preset_unity.py (helpers copied, not imported as a package).
Declared as named constants; every gate recomputes from the mesh, materials, UVs, evaluated LOD, collider, or export file.
-| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) | | --- | --- | --- | | Base triangles | 8280–9500 | 9380 / 9380 / 9380 | | LOD1 ratio | 0.32–0.62 of base | 0.5000 / 0.5000 / 0.5000 | | LOD2 ratio | 0.10–0.35 of base | 0.2198 / 0.2198 / 0.2198 | | Materials | exactly 3 distinct | 3 | | Material faces | stone ≥ 3000, wood ≥ 600, metal ≥ 100 | all above | | UVs | in 0..1, AABB overlap ≤ 1e-5 | in range, overlap 0 | | Outer AABB | (1.312, 1.312, 1.761) m ± 0.01 | (1.3120, 1.3120, 1.7606) | | Grounded | bbox min Z within 1e-4 of 0 | 0.0000 / 0.0000 / 0.0000 | | Named supports | all 12 bottom-course stones on the floor within 1e-4 | 12/12, worst 0.000000 | | Hygiene | loose V/E, non-manifold, zero-area, doubles @1e-5, n-gons: all 0 | 0 / 0 / 0 on every axis | | Z-fighting | coplanar cross-shell face pairs = 0 | 0 / 0 / 0 | | Post tenon | each post seats 0.015–0.022 m into the measured coping top | 0.01800 / 0.01800 / 0.01800 | | Post placement | each post within 0.02 rad, in plan, of a roof hip corner recomputed from the mesh | 0.00000 | | Bucket clearance | hangs 0.085–0.115 m clear of the measured coping top | 0.10000 | | Material-island gap | stone↔wood and metal↔wood min distance ≤ 0.008 m | 0.00000 / 0.00000 / 0.00000 | | Collider tris | ≤ 380 | 346 | | Export | written, size > 0, removed after measuring | 690092 bytes on 5.2.1 |
| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) |
|---|---|---|
| Base triangles | 8280–9500 | 9380 / 9380 / 9380 |
| LOD1 ratio | 0.32–0.62 of base | 0.5000 / 0.5000 / 0.5000 |
| LOD2 ratio | 0.10–0.35 of base | 0.2198 / 0.2198 / 0.2198 |
| Materials | exactly 3 distinct | 3 |
| Material faces | stone ≥ 3000, wood ≥ 600, metal ≥ 100 | all above |
| UVs | in 0..1, AABB overlap ≤ 1e-5 | in range, overlap 0 |
| Outer AABB | (1.312, 1.312, 1.761) m ± 0.01 | (1.3120, 1.3120, 1.7606) |
| Grounded | bbox min Z within 1e-4 of 0 | 0.0000 / 0.0000 / 0.0000 |
| Named supports | all 12 bottom-course stones on the floor within 1e-4 | 12/12, worst 0.000000 |
| Hygiene | loose V/E, non-manifold, zero-area, doubles @1e-5, n-gons: all 0 | 0 / 0 / 0 on every axis |
| Z-fighting | coplanar cross-shell face pairs = 0 | 0 / 0 / 0 |
| Post tenon | each post seats 0.015–0.022 m into the measured coping top | 0.01800 / 0.01800 / 0.01800 |
| Post placement | each post within 0.02 rad, in plan, of a roof hip corner recomputed from the mesh | 0.00000 |
| Bucket clearance | hangs 0.085–0.115 m clear of the measured coping top | 0.10000 |
| Material-island gap | stone↔wood and metal↔wood min distance ≤ 0.008 m | 0.00000 / 0.00000 / 0.00000 |
| Collider tris | ≤ 380 | 346 |
| Export | written, size > 0, removed after measuring | 690092 bytes on 5.2.1 |
DECIMATE COLLAPSE triangle counts are not guaranteed identical across series — the gate is a ratio band, not an exact count. This mesh happened to match on 4.5.11 / 5.1.2 / 5.2.1. Bake pixels are stochastic; the gate is has_data plus operator FINISHED, not byte-identity. Construction uses no RNG. glTF byte size differs by a few hundred bytes across series; the gate is "written and non-empty", not a byte count, and the file is removed once measured.
Each flag breaks one stage so a named budget fails and the piece exits *that* code.
-| Flag | Target budget | Exit | | --- | --- | --- | | --skip-decimate | LOD1 ratio band (ratio becomes 1.0) | 9 | | --lift-z | AABB grounded zmin (whole mesh up 0.05 m) | 16 | | --stand-posts | Coplanar cross-shell pairs — puts the posts back on the axis *and* stands them on the coping, which is the pair of choices the shipped piece made (0 → 4 pairs) | 15 | | --float-stone | Named supports — lifts one bottom-course stone 4 mm while the other eleven still ground the AABB | 16 | | --shallow-tenon | Post tenon band — seats the posts only 4 mm, too shallow to be a joint but not flush, so no faces share a plane | 18 | | --drop-bucket | Bucket clearance — lowers the bucket 0.26 m back down the shaft | 18 | | --turn-posts | Post placement — rotates the post ring one curb facet off the roof's hip corners | 19 |
Each flag breaks one stage so a named budget fails and the piece exits that code.
+| Flag | Target budget | Exit |
|---|---|---|
--skip-decimate | LOD1 ratio band (ratio becomes 1.0) | 9 |
--lift-z | AABB grounded zmin (whole mesh up 0.05 m) | 16 |
--stand-posts | Coplanar cross-shell pairs — puts the posts back on the axis and stands them on the coping, which is the pair of choices the shipped piece made (0 → 4 pairs) | 15 |
--float-stone | Named supports — lifts one bottom-course stone 4 mm while the other eleven still ground the AABB | 16 |
--shallow-tenon | Post tenon band — seats the posts only 4 mm, too shallow to be a joint but not flush, so no faces share a plane | 18 |
--drop-bucket | Bucket clearance — lowers the bucket 0.26 m back down the shaft | 18 |
--turn-posts | Post placement — rotates the post ring one curb facet off the roof's hip corners | 19 |
Two of these had to be re-aimed, which is the point of declaring the target:
--stand-posts seats the posts flush and returns them to the axis. Flush alone, at the corrected 45°, leaves no coping top face within COPLANAR_CENTRE_MAX of a foot, so it fell through to the tenon band (18) and witnessed nothing about z-fighting.--turn-posts rotates by exactly one curb facet (30°). Any other angle sets the feet down on a different part of the 12-gon coping and the stone-wood gap gate (17) steals the failure; a full 45° also swings the crank grip past the eave and fails the AABB gate (8) — the same trap stone-archway's --flat-arch fell into. One facet is the only rotation whose local seat geometry is identical by symmetry.Smoke passes none of the falsifier flags and no --output.
File-local. 9 is a valid check code. 10 is reserved for gallery_framing.check_framing on the --output path.
| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Mesh did not build / no UV layer | | 4 | Base triangle count outside range | | 5 | Material count ≠ 3 distinct slots, or a material face floor missed | | 6 | UVs outside 0..1 | | 7 | UV AABB overlap above tolerance | | 8 | World AABB off declared outer size | | 9 | LOD ratio band (--skip-decimate lands here) | | 10 | Framing gate (render path only) | | 11 | Collider triangle count above ceiling | | 12 | Bake did not finish or image has no data | | 13 | Export file missing or empty | | 14 | --output produced no file | | 15 | Hygiene: loose geometry, non-manifold, zero-area, doubles, n-gons, or coplanar cross-shell face pairs (--stand-posts lands here) | | 16 | Bbox min Z not grounded (--lift-z), or a named support off the floor (--float-stone) | | 17 | Material-island gap above tolerance (parts meant to touch) | | 18 | Post tenon depth outside its band (--shallow-tenon), or bucket clearance outside its band (--drop-bucket) | | 19 | A post not under a roof hip corner in plan (--turn-posts lands here) |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Mesh did not build / no UV layer |
| 4 | Base triangle count outside range |
| 5 | Material count ≠ 3 distinct slots, or a material face floor missed |
| 6 | UVs outside 0..1 |
| 7 | UV AABB overlap above tolerance |
| 8 | World AABB off declared outer size |
| 9 | LOD ratio band (--skip-decimate lands here) |
| 10 | Framing gate (render path only) |
| 11 | Collider triangle count above ceiling |
| 12 | Bake did not finish or image has no data |
| 13 | Export file missing or empty |
| 14 | --output produced no file |
| 15 | Hygiene: loose geometry, non-manifold, zero-area, doubles, n-gons, or coplanar cross-shell face pairs (--stand-posts lands here) |
| 16 | Bbox min Z not grounded (--lift-z), or a named support off the floor (--float-stone) |
| 17 | Material-island gap above tolerance (parts meant to touch) |
| 18 | Post tenon depth outside its band (--shallow-tenon), or bucket clearance outside its band (--drop-bucket) |
| 19 | A post not under a roof hip corner in plan (--turn-posts lands here) |
Intended size: 0.30 m square foot, 1.12 m octagonal post (0.10 m across at the base), 0.50 m arm, 0.24 × 0.30 m cage; outer AABB 0.300 × 0.786 × 1.287 m.
Declared as named constants; every gate recomputes from the mesh, materials, UVs, evaluated LOD, collider, or export file.
-| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) | | --- | --- | --- | | Base triangles | 3000–3800 | 3338 / 3338 / 3338 | | LOD1 ratio | 0.32–0.62 of base | 0.4997 / 0.4997 / 0.4997 | | LOD2 ratio | 0.10–0.35 of base | 0.2199 / 0.2199 / 0.2193 | | Materials | exactly 3 distinct; ≥200 metal, ≥8 glass, ≥24 brass | 3 slots; 1530 / 24 / 209 | | UVs | in 0..1, AABB overlap ≤ 1e-5 | in range, overlap 0 | | Outer AABB | (0.300, 0.786, 1.287) m ± 0.01 | (0.3000, 0.7860, 1.2874), zmin 0 | | Collider tris | ≤ 120 | 78 | | Export | written, size > 0 | 250980 / 250980 / 250964 bytes |
| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) |
|---|---|---|
| Base triangles | 3000–3800 | 3338 / 3338 / 3338 |
| LOD1 ratio | 0.32–0.62 of base | 0.4997 / 0.4997 / 0.4997 |
| LOD2 ratio | 0.10–0.35 of base | 0.2199 / 0.2199 / 0.2193 |
| Materials | exactly 3 distinct; ≥200 metal, ≥8 glass, ≥24 brass | 3 slots; 1530 / 24 / 209 |
| UVs | in 0..1, AABB overlap ≤ 1e-5 | in range, overlap 0 |
| Outer AABB | (0.300, 0.786, 1.287) m ± 0.01 | (0.3000, 0.7860, 1.2874), zmin 0 |
| Collider tris | ≤ 120 | 78 |
| Export | written, size > 0 | 250980 / 250980 / 250964 bytes |
Base triangles dropped from 3520 to 3338 in the quality pass: the open 4-gon cone roof became a closed square pyramid, cage crossings stopped sharing vertices, and a pane rebate replaced coplanar glass.
DECIMATE COLLAPSE triangle counts are not identical across series — 5.2.1 is 2 tris leaner on LOD2. The gate is a ratio band, not an exact count. Bake pixels are stochastic; the gate is has_data plus operator FINISHED, not byte-identity. Construction uses no RNG. Export byte counts differ by 16 B on 5.2.1 (glTF serializer), not a gated axis.
Recomputed from the generated mesh, not asserted about the script.
-| Axis | Declared | Measured (all three) | | --- | --- | --- | | Non-manifold edges | 0 | 0 | | Loose verts / edges | 0 / 0 | 0 / 0 | | Doubles merged at 1e-5 | 0 | 0 | | Zero-area faces | 0 | 0 | | N-gons | 0 | 0 | | Coplanar disjoint face pairs | 0 | 0 | | Grounded: zmin | within 1e-4 of 0 | 0.0000 | | Post plan | 0.100 m across × 1.120 m high ± (0.02, 0.04) | 0.1000 × 1.1200 | | Post plumb (top vs bottom XY) | ≤ 0.008 m | 0.00000 |
| Axis | Declared | Measured (all three) |
|---|---|---|
| Non-manifold edges | 0 | 0 |
| Loose verts / edges | 0 / 0 | 0 / 0 |
| Doubles merged at 1e-5 | 0 | 0 |
| Zero-area faces | 0 | 0 |
| N-gons | 0 | 0 |
| Coplanar disjoint face pairs | 0 | 0 |
Grounded: zmin | within 1e-4 of 0 | 0.0000 |
| Post plan | 0.100 m across × 1.120 m high ± (0.02, 0.04) | 0.1000 × 1.1200 |
| Post plumb (top vs bottom XY) | ≤ 0.008 m | 0.00000 |
| Axis | Declared | Measured (all three) | | --- | --- | --- | | Brace-to-arm BVH gap | ≤ 0.006 m | 0.00000 | | Arm underside vs roof peak | ≥ 0.008 m clearance | 0.05300 | | Hanger-to-roof BVH gap | ≤ 0.004 m | −0.00100 |
+| Axis | Declared | Measured (all three) |
|---|---|---|
| Brace-to-arm BVH gap | ≤ 0.006 m | 0.00000 |
| Arm underside vs roof peak | ≥ 0.008 m clearance | 0.05300 |
| Hanger-to-roof BVH gap | ≤ 0.004 m | −0.00100 |
Each violates one named budget. All six were run on 4.5.11, 5.1.2 and 5.2.1 and returned the same code on each.
-| Flag | Budget violated | Exit | | --- | --- | --- | | --skip-decimate | LOD1 ratio band | 9 | | --stray-vert | loose vertex count is 0 | 15 | | --lift-z | bounding box zmin is 0 | 16 | | --float-brace | brace-to-arm BVH gap | 17 | | --sink-arm | arm-over-roof clearance | 18 | | --rake-post | post plumb | 19 |
| Flag | Budget violated | Exit |
|---|---|---|
--skip-decimate | LOD1 ratio band | 9 |
--stray-vert | loose vertex count is 0 | 15 |
--lift-z | bounding box zmin is 0 | 16 |
--float-brace | brace-to-arm BVH gap | 17 |
--sink-arm | arm-over-roof clearance | 18 |
--rake-post | post plumb | 19 |
--sink-arm lowers only the arm. Hang-station hang_z stays at post top so the cage does not follow, which is what lets the clearance budget fire.
blender --background --python street_lantern.py --
@@ -290,7 +295,7 @@ Run
Smoke passes no flags.
Exit codes
File-local. 9 is a valid check code. 10 is reserved for gallery_framing.check_framing on the --output path. 15–19 are the hygiene and joint-fit family.
-| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Mesh did not build / no UV layer | | 4 | Base triangle count outside range | | 5 | Material count ≠ 3 distinct slots, or a face-count floor missed | | 6 | UVs outside 0..1 | | 7 | UV AABB overlap above tolerance | | 8 | World AABB off declared outer size | | 9 | LOD ratio band (--skip-decimate lands here) | | 10 | Framing gate (render path only) | | 11 | Collider triangle count above ceiling | | 12 | Bake did not finish or image has no data | | 13 | Export file missing or empty | | 14 | --output produced no file | | 15 | Mesh hygiene: loose, non-manifold, zero-area, doubles, n-gons, z-fight | | 16 | Not grounded: bounding box zmin off 0 | | 17 | Joint fit: brace-to-arm gap (--float-brace) | | 18 | Seat: arm-roof clearance or hanger-roof gap (--sink-arm) | | 19 | Post plumb or post size off the stated real-world size (--rake-post) |
+Code Meaning 0 Success 1 Uncaught exception (FATAL wrapper) 2 argparse / usage 3 Mesh did not build / no UV layer 4 Base triangle count outside range 5 Material count ≠ 3 distinct slots, or a face-count floor missed 6 UVs outside 0..1 7 UV AABB overlap above tolerance 8 World AABB off declared outer size 9 LOD ratio band (--skip-decimate lands here) 10 Framing gate (render path only) 11 Collider triangle count above ceiling 12 Bake did not finish or image has no data 13 Export file missing or empty 14 --output produced no file15 Mesh hygiene: loose, non-manifold, zero-area, doubles, n-gons, z-fight 16 Not grounded: bounding box zmin off 0 17 Joint fit: brace-to-arm gap (--float-brace) 18 Seat: arm-roof clearance or hanger-roof gap (--sink-arm) 19 Post plumb or post size off the stated real-world size (--rake-post)
A runnable example that renders a 3×2 grid of spheres — one material per cell — to a single PNG. It demonstrates the procedural-materials-and-shaders patterns end to end:
set_specular shim (Specular → Specular IOR Level, renamed in Blender 4.0).It doubles as a live proof of the EEVEE engine-id behavior: the version-branch helper resolves BLENDER_EEVEE on Blender 5.x and BLENDER_EEVEE_NEXT on 4.2–4.5, and the check witnesses the inversion for real — the *other* era's id must be rejected by the running build (assignment raises TypeError) and the helper's id accepted — so a regression in that mapping fails the example, not just the docs.
It doubles as a live proof of the EEVEE engine-id behavior: the version-branch helper resolves BLENDER_EEVEE on Blender 5.x and BLENDER_EEVEE_NEXT on 4.2–4.5, and the check witnesses the inversion for real — the other era's id must be rejected by the running build (assignment raises TypeError) and the helper's id accepted — so a regression in that mapping fails the example, not just the docs.
# Cheap correctness check (materials + engine-id witness, no render):
blender --background --python swatch_grid.py --
@@ -277,7 +282,7 @@ Run
blender --background --python swatch_grid.py -- --output swatch.png --engine cycles --samples 16 --width 960
Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper.
| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Distinct swatch colors ≠ 6 (--same-base lands here); also render not six distinct regions | | 4 | --output produced no file | | 5 | Wrong-era EEVEE engine id was accepted | | 10 | Gallery framing violation |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Distinct swatch colors ≠ 6 (--same-base lands here); also render not six distinct regions |
| 4 | --output produced no file |
| 5 | Wrong-era EEVEE engine id was accepted |
| 10 | Gallery framing violation |
--no-verify was a skip-flag and has been removed. Pixel verification always runs when --output is passed.
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --same-base.
Composes skills mesh-editing-and-bmesh, bake-high-to-low, depsgraph-and-evaluated-data, engine-export-presets, and snippets bake_normal_high_to_low.py, setup_bake_target_image.py, lod_chain.py / decimate_to_budget.py, convex_hull_collider.py, export_preset_unity.py (helpers copied, not imported as a package).
Declared as named constants; every gate recomputes from the mesh, materials, UVs, evaluated LOD, collider, or export file.
-| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) | | --- | --- | --- | | Base triangles | 2300–3200 | 2528 / 2528 / 2528 | | LOD1 ratio | 0.32–0.62 of base | 0.5000 / 0.5000 / 0.5000 | | LOD2 ratio | 0.10–0.35 of base | 0.2199 / 0.2199 / 0.2199 | | Materials | exactly 2 distinct; ≥200 wood, ≥48 metal faces | 2 slots; 1168 / 256 | | UVs | in 0..1, AABB overlap ≤ 1e-5 | in range, overlap 0 | | Outer AABB | (0.341, 0.341, 0.470) m ± 0.015 | (0.3400, 0.3400, 0.4700), zmin 0 | | Collider tris | ≤ 640 | 396 | | Export | written, size > 0 | 215912 / 215912 / 215900 bytes |
| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) |
|---|---|---|
| Base triangles | 2300–3200 | 2528 / 2528 / 2528 |
| LOD1 ratio | 0.32–0.62 of base | 0.5000 / 0.5000 / 0.5000 |
| LOD2 ratio | 0.10–0.35 of base | 0.2199 / 0.2199 / 0.2199 |
| Materials | exactly 2 distinct; ≥200 wood, ≥48 metal faces | 2 slots; 1168 / 256 |
| UVs | in 0..1, AABB overlap ≤ 1e-5 | in range, overlap 0 |
| Outer AABB | (0.341, 0.341, 0.470) m ± 0.015 | (0.3400, 0.3400, 0.4700), zmin 0 |
| Collider tris | ≤ 640 | 396 |
| Export | written, size > 0 | 215912 / 215912 / 215900 bytes |
Recomputed from the generated mesh, not asserted about the script.
-| Axis | Declared | Measured (all three) | | --- | --- | --- | | Non-manifold edges | 0 | 0 | | Loose verts / edges | 0 / 0 | 0 / 0 | | Doubles merged at 1e-5 | 0 | 0 | | Zero-area faces | 0 | 0 | | N-gons | 0 | 0 | | Coplanar disjoint face pairs | 0 | 0 | | Grounded: zmin | within 1e-4 of 0 | 0.0000 | | Named supports: 4 ferrule cups | each zmin ≤ 1e-3 | 4, cup_z 0.00000 | | Seat | 0.340 m dia × 0.470 m high ± 0.02 | 0.3400 × 0.4700 |
| Axis | Declared | Measured (all three) |
|---|---|---|
| Non-manifold edges | 0 | 0 |
| Loose verts / edges | 0 / 0 | 0 / 0 |
| Doubles merged at 1e-5 | 0 | 0 |
| Zero-area faces | 0 | 0 |
| N-gons | 0 | 0 |
| Coplanar disjoint face pairs | 0 | 0 |
Grounded: zmin | within 1e-4 of 0 | 0.0000 |
| Named supports: 4 ferrule cups | each zmin ≤ 1e-3 | 4, cup_z 0.00000 |
| Seat | 0.340 m dia × 0.470 m high ± 0.02 | 0.3400 × 0.4700 |
| Axis | Declared | Measured (all three) | | --- | --- | --- | | Stretcher-leg BVH gap | ≤ 0.008 m | 0.00000 | | Ferrule inner clearance (r_wood − r_in) | −0.005 .. 0.000 m | −0.00125 |
| Axis | Declared | Measured (all three) |
|---|---|---|
| Stretcher-leg BVH gap | ≤ 0.008 m | 0.00000 |
Ferrule inner clearance (r_wood − r_in) | −0.005 .. 0.000 m | −0.00125 |
DECIMATE COLLAPSE triangle counts are not identical across series — the gate is a ratio band, not an exact count. Bake pixels are stochastic; the gate is has_data plus operator FINISHED, not byte-identity. Construction uses no RNG. Export byte counts may differ by a few bytes across series.
Each violates one named budget. All seven were run on 4.5.11, 5.1.2 and 5.2.1 and returned the same code on each.
-| Flag | Budget violated | Exit | | --- | --- | --- | | --skip-decimate | LOD1 ratio band | 9 | | --stray-vert | loose vertex count is 0 | 15 | | --lift-z | bounding box zmin is 0 | 16 | | --short-legs | named ferrule cups at Z=0 | 16 | | --float-stretchers | stretcher-leg join | 17 | | --pipe-ferrule | ferrule inner-clearance band | 18 |
| Flag | Budget violated | Exit |
|---|---|---|
--skip-decimate | LOD1 ratio band | 9 |
--stray-vert | loose vertex count is 0 | 15 |
--lift-z | bounding box zmin is 0 | 16 |
--short-legs | named ferrule cups at Z=0 | 16 |
--float-stretchers | stretcher-leg join | 17 |
--pipe-ferrule | ferrule inner-clearance band | 18 |
blender --background --python tavern_stool.py --
blender --background --python tavern_stool.py -- --skip-decimate
@@ -287,7 +292,7 @@ Run
Smoke passes no flags.
Exit codes
File-local. 9 is a valid check code. 10 is reserved for gallery_framing.check_framing on the --output path. 15–19 are the hygiene and joint-fit family.
-| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Mesh did not build / no UV layer | | 4 | Base triangle count outside range | | 5 | Material count ≠ 2 distinct slots, or a face-count floor missed | | 6 | UVs outside 0..1 | | 7 | UV AABB overlap above tolerance | | 8 | World AABB off declared outer size | | 9 | LOD ratio band (--skip-decimate lands here) | | 10 | Framing gate (render path only) | | 11 | Collider triangle count above ceiling | | 12 | Bake did not finish or image has no data | | 13 | Export file missing or empty | | 14 | --output produced no file | | 15 | Mesh hygiene: loose, non-manifold, zero-area, doubles, n-gons, z-fight | | 16 | Not grounded: bounding box zmin off 0, or a named ferrule cup floats | | 17 | Joint fit: stretcher-leg gap | | 18 | Seat: ferrule inner-clearance band | | 19 | Seat diameter or height off the stated real-world size |
+Code Meaning 0 Success 1 Uncaught exception (FATAL wrapper) 2 argparse / usage 3 Mesh did not build / no UV layer 4 Base triangle count outside range 5 Material count ≠ 2 distinct slots, or a face-count floor missed 6 UVs outside 0..1 7 UV AABB overlap above tolerance 8 World AABB off declared outer size 9 LOD ratio band (--skip-decimate lands here) 10 Framing gate (render path only) 11 Collider triangle count above ceiling 12 Bake did not finish or image has no data 13 Export file missing or empty 14 --output produced no file15 Mesh hygiene: loose, non-manifold, zero-area, doubles, n-gons, z-fight 16 Not grounded: bounding box zmin off 0, or a named ferrule cup floats 17 Joint fit: stretcher-leg gap 18 Seat: ferrule inner-clearance band 19 Seat diameter or height off the stated real-world size
Per-script sequential checks. 9 is a valid check code; there is no rule against it.
| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Mesh object count after join ≠ 1 (--no-override lands here) | | 4 | Joined target is not the sole remaining mesh | | 5 | Topology ≠ 24 verts / 18 faces | | 6 | Source objects still present | | 7 | Local Z span did not cover all steps | | 8 | --output produced no file |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Mesh object count after join ≠ 1 (--no-override lands here) |
| 4 | Joined target is not the sole remaining mesh |
| 5 | Topology ≠ 24 verts / 18 faces |
| 6 | Source objects still present |
| 7 | Local Z span did not cover all steps |
| 8 | --output produced no file |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --no-override.
Intended size: 1.80 m square hill tile, ~0.16 m sine amplitude, nine seated stones; outer AABB 1.800 × 1.800 × 0.552 m.
Declared as named constants; every gate recomputes from the mesh, materials, UVs, evaluated LOD, collider, or export file.
-| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) | | --- | --- | --- | | Base triangles | 1400–2800 | 1758 / 1758 / 1758 | | LOD1 ratio | 0.32–0.62 of base | 0.4994 / 0.4994 / 0.4994 | | LOD2 ratio | 0.10–0.35 of base | 0.2196 / 0.2196 / 0.2196 | | Materials | exactly 2 distinct, ≥24 stone faces | 2 slots, 720 stone | | UVs | in 0..1, AABB overlap ≤ 1e-5 | in range, overlap 0 | | Outer AABB | (1.800, 1.800, 0.552) m ± 0.015 | (1.8000, 1.8000, 0.5523), zmin 0 | | Collider tris | ≤ 120 | 81 | | Export | written, size > 0 | 158156 / 158156 / 158140 bytes |
| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) |
|---|---|---|
| Base triangles | 1400–2800 | 1758 / 1758 / 1758 |
| LOD1 ratio | 0.32–0.62 of base | 0.4994 / 0.4994 / 0.4994 |
| LOD2 ratio | 0.10–0.35 of base | 0.2196 / 0.2196 / 0.2196 |
| Materials | exactly 2 distinct, ≥24 stone faces | 2 slots, 720 stone |
| UVs | in 0..1, AABB overlap ≤ 1e-5 | in range, overlap 0 |
| Outer AABB | (1.800, 1.800, 0.552) m ± 0.015 | (1.8000, 1.8000, 0.5523), zmin 0 |
| Collider tris | ≤ 120 | 81 |
| Export | written, size > 0 | 158156 / 158156 / 158140 bytes |
Base triangles rose from 1194 to 1758 in the quality pass: 9-vert hill became a 21-vert grid, and bevelled cubes became subdiv-2 icospheres. Outer Z dropped from 0.655 m (crate corners swinging through the slab) to 0.552 m of seated stone on the hill.
DECIMATE COLLAPSE triangle counts are not guaranteed identical across series — the gate is a ratio band, not an exact count. This mesh matched on 4.5.11 / 5.1.2 / 5.2.1. Bake pixels are stochastic; the gate is has_data plus operator FINISHED, not byte-identity. Construction uses no RNG. Export byte counts differ by 16 B on 5.2.1 (glTF serializer), not a gated axis.
Recomputed from the generated mesh, not asserted about the script.
-| Axis | Declared | Measured (all three) | | --- | --- | --- | | Non-manifold edges | 0 | 0 | | Loose verts / edges | 0 / 0 | 0 / 0 | | Doubles merged at 1e-5 | 0 | 0 | | Zero-area faces | 0 | 0 | | N-gons | 0 | 0 | | Coplanar disjoint face pairs | 0 | 0 | | Grounded: zmin | within 1e-4 of 0 | 0.0000 | | Stone shells | 9 | 9 | | Per-stone faces | ≥ 40 | 80 | | Stone floor zmin | ≥ 0.012 m | 0.11334 | | Seat offset (zmin − dirt Z) | ≤ 0.02 m | −0.03500 |
| Axis | Declared | Measured (all three) |
|---|---|---|
| Non-manifold edges | 0 | 0 |
| Loose verts / edges | 0 / 0 | 0 / 0 |
| Doubles merged at 1e-5 | 0 | 0 |
| Zero-area faces | 0 | 0 |
| N-gons | 0 | 0 |
| Coplanar disjoint face pairs | 0 | 0 |
Grounded: zmin | within 1e-4 of 0 | 0.0000 |
| Stone shells | 9 | 9 |
| Per-stone faces | ≥ 40 | 80 |
Stone floor zmin | ≥ 0.012 m | 0.11334 |
Seat offset (zmin − dirt Z) | ≤ 0.02 m | −0.03500 |
Each violates one named budget. All were run on 4.5.11, 5.1.2 and 5.2.1 and returned the same code on each.
-| Flag | Budget violated | Exit | | --- | --- | --- | | --skip-decimate | LOD1 ratio band | 9 | | --stray-vert | loose vertex count is 0 | 15 | | --lift-z | bounding box zmin is 0 | 16 | | --poke-rock | stone floor zmin | 17 | | --float-rocks | seat offset vs sampled dirt Z | 18 | | --box-rocks | per-stone face floor | 19 |
| Flag | Budget violated | Exit |
|---|---|---|
--skip-decimate | LOD1 ratio band | 9 |
--stray-vert | loose vertex count is 0 | 15 |
--lift-z | bounding box zmin is 0 | 16 |
--poke-rock | stone floor zmin | 17 |
--float-rocks | seat offset vs sampled dirt Z | 18 |
--box-rocks | per-stone face floor | 19 |
blender --background --python terrain_scatter.py --
blender --background --python terrain_scatter.py -- --skip-decimate
@@ -287,7 +292,7 @@ Run
Smoke passes no flags.
Exit codes
File-local. 9 is a valid check code. 10 is reserved for gallery_framing.check_framing on the --output path. 15–19 are the hygiene and joint-fit family.
-| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Mesh did not build / no UV layer | | 4 | Base triangle count outside range | | 5 | Material count ≠ 2 distinct slots, or stone faces missing | | 6 | UVs outside 0..1 | | 7 | UV AABB overlap above tolerance | | 8 | World AABB off declared outer size | | 9 | LOD ratio band (--skip-decimate lands here) | | 10 | Framing gate (render path only) | | 11 | Collider triangle count above ceiling | | 12 | Bake did not finish or image has no data | | 13 | Export file missing or empty | | 14 | --output produced no file | | 15 | Hygiene (--stray-vert lands here) | | 16 | Grounded zmin (--lift-z) | | 17 | Stone floor poke (--poke-rock) | | 18 | Float above host (--float-rocks) | | 19 | Stone shell faces (--box-rocks) |
+Code Meaning 0 Success 1 Uncaught exception (FATAL wrapper) 2 argparse / usage 3 Mesh did not build / no UV layer 4 Base triangle count outside range 5 Material count ≠ 2 distinct slots, or stone faces missing 6 UVs outside 0..1 7 UV AABB overlap above tolerance 8 World AABB off declared outer size 9 LOD ratio band (--skip-decimate lands here) 10 Framing gate (render path only) 11 Collider triangle count above ceiling 12 Bake did not finish or image has no data 13 Export file missing or empty 14 --output produced no file15 Hygiene (--stray-vert lands here) 16 Grounded zmin (--lift-z) 17 Stone floor poke (--poke-rock) 18 Float above host (--float-rocks) 19 Stone shell faces (--box-rocks)
A runnable example that builds a beveled 3D stamp of the running Blender version — a TextCurve (bpy.data.curves.new(type='FONT')) whose body is the live bpy.app.version_string, so every render self-documents which Blender produced it. CI artifacts made this way are self-labeling.
What it witnesses: the TextCurve data-API contract. curves.new(type='FONT') returns a Curve subclass, the built-in font ("Bfont Regular") is always loaded even headless, body is plain assignable text that regenerates geometry on edit (more characters → strictly wider evaluated mesh), and extrude / bevel_depth produce exactly predictable solids: the evaluated mesh z-extent equals 2 × (extrude + bevel_depth) and the round bevel widens the outline in-plane by 2 × bevel_depth. Flat text (no extrude, no bevel) is already *filled* — faces exist — but strictly planar. The check also witnesses the depsgraph lifetime hazard: after to_mesh_clear() the returned Mesh reference is dead and any access raises ReferenceError.
What it witnesses: the TextCurve data-API contract. curves.new(type='FONT') returns a Curve subclass, the built-in font ("Bfont Regular") is always loaded even headless, body is plain assignable text that regenerates geometry on edit (more characters → strictly wider evaluated mesh), and extrude / bevel_depth produce exactly predictable solids: the evaluated mesh z-extent equals 2 × (extrude + bevel_depth) and the round bevel widens the outline in-plane by 2 × bevel_depth. Flat text (no extrude, no bevel) is already filled — faces exist — but strictly planar. The check also witnesses the depsgraph lifetime hazard: after to_mesh_clear() the returned Mesh reference is dead and any access raises ReferenceError.
Version divergence: the string format itself differs — "5.1.2" on 5.x but "4.5.11 LTS" on 4.5 (the suffix is part of version_string). The check therefore asserts the contract that holds on both: version_string starts with the dotted bpy.app.version tuple. Code that parses version_string as a bare semver breaks on every LTS build; branch on the bpy.app.version tuple instead.
# Cheap correctness check (no render) — the CI check:
@@ -275,7 +280,7 @@ Run
The render scales the stamp to a constant width, so the frame holds for any version-string length.
Exit codes
Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper.
-| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Not a FONT TextCurve with the built-in Bfont | | 4 | body is not the live version_string (--wrong-body lands here) | | 5 | Flat text is not a filled planar mesh | | 6 | Extrude / bevel closed form failed | | 7 | Appending characters did not widen the text | | 8 | Mesh survived to_mesh_clear() | | 9 | --output produced no file | | 10 | Gallery framing violation |
+Code Meaning 0 Success 1 Uncaught exception (FATAL wrapper) 2 argparse / usage 3 Not a FONT TextCurve with the built-in Bfont 4 body is not the live version_string (--wrong-body lands here)5 Flat text is not a filled planar mesh 6 Extrude / bevel closed form failed 7 Appending characters did not widen the text 8 Mesh survived to_mesh_clear() 9 --output produced no file10 Gallery framing violation
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --wrong-body.
Composes skills mesh-editing-and-bmesh, bake-high-to-low, depsgraph-and-evaluated-data, engine-export-presets, and snippets bake_normal_high_to_low.py, setup_bake_target_image.py, lod_chain.py / decimate_to_budget.py, convex_hull_collider.py, export_preset_unity.py (helpers copied, not imported as a package).
Declared as named constants; every gate recomputes from the mesh, materials, UVs, evaluated LOD, collider, or export file.
-| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) | | --- | --- | --- | | Base triangles | 3600–4500 | 3852 / 3852 / 3852 | | LOD1 ratio | 0.32–0.62 of base | 0.5000 / 0.5000 / 0.5000 | | LOD2 ratio | 0.10–0.35 of base | 0.2196 / 0.2196 / 0.2129 | | Materials | exactly 2 distinct; ≥200 wood, ≥80 metal faces | 2 slots; 1608 / 318 | | UVs | in 0..1, AABB overlap ≤ 1e-5 | in range, overlap 0 | | Outer AABB | (0.764, 0.542, 0.691) m ± 0.015 | (0.7640, 0.5423, 0.6907), zmin 0 | | Collider tris | ≤ 200 | 176 | | Export | written, size > 0 | 288196 / 288196 / 288184 bytes |
| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) |
|---|---|---|
| Base triangles | 3600–4500 | 3852 / 3852 / 3852 |
| LOD1 ratio | 0.32–0.62 of base | 0.5000 / 0.5000 / 0.5000 |
| LOD2 ratio | 0.10–0.35 of base | 0.2196 / 0.2196 / 0.2129 |
| Materials | exactly 2 distinct; ≥200 wood, ≥80 metal faces | 2 slots; 1608 / 318 |
| UVs | in 0..1, AABB overlap ≤ 1e-5 | in range, overlap 0 |
| Outer AABB | (0.764, 0.542, 0.691) m ± 0.015 | (0.7640, 0.5423, 0.6907), zmin 0 |
| Collider tris | ≤ 200 | 176 |
| Export | written, size > 0 | 288196 / 288196 / 288184 bytes |
Recomputed from the generated mesh, not asserted about the script.
-| Axis | Declared | Measured (all three) | | --- | --- | --- | | Non-manifold edges | 0 | 0 | | Loose verts / edges | 0 / 0 | 0 / 0 | | Doubles merged at 1e-5 | 0 | 0 | | Zero-area faces | 0 | 0 | | N-gons | 0 | 0 | | Coplanar disjoint face pairs | 0 | 0 | | Grounded: zmin | within 1e-4 of 0 | 0.0000 | | Named supports: 4 feet | each zmin ≤ 1e-3 | 4, foot_z 0.00000 | | Body plan | 0.74 × 0.46 m ± 0.04 | 0.7400 × 0.4600 |
| Axis | Declared | Measured (all three) |
|---|---|---|
| Non-manifold edges | 0 | 0 |
| Loose verts / edges | 0 / 0 | 0 / 0 |
| Doubles merged at 1e-5 | 0 | 0 |
| Zero-area faces | 0 | 0 |
| N-gons | 0 | 0 |
| Coplanar disjoint face pairs | 0 | 0 |
Grounded: zmin | within 1e-4 of 0 | 0.0000 |
| Named supports: 4 feet | each zmin ≤ 1e-3 | 4, foot_z 0.00000 |
| Body plan | 0.74 × 0.46 m ± 0.04 | 0.7400 × 0.4600 |
| Axis | Declared | Measured (all three) | | --- | --- | --- | | Lid-knuckle to hinge barrel | ≤ 0.020 m | 0.00700 | | Band-wall BVH gap | ≤ 0.008 m | 0.00000 |
+| Axis | Declared | Measured (all three) |
|---|---|---|
| Lid-knuckle to hinge barrel | ≤ 0.020 m | 0.00700 |
| Band-wall BVH gap | ≤ 0.008 m | 0.00000 |
DECIMATE COLLAPSE triangle counts are not identical across series — the gate is a ratio band, not an exact count. Bake pixels are stochastic; the gate is has_data plus operator FINISHED, not byte-identity. Construction uses no RNG. Export byte counts may differ by a few bytes across series.
Each violates one named budget. All seven were run on 4.5.11, 5.1.2 and 5.2.1 and returned the same code on each.
-| Flag | Budget violated | Exit | | --- | --- | --- | | --skip-decimate | LOD1 ratio band | 9 | | --stray-vert | loose vertex count is 0 | 15 | | --lift-z | bounding box zmin is 0 | 16 | | --short-legs | named foot supports at Z=0 | 16 | | --float-hinge | lid-knuckle to hinge barrel | 17 | | --narrow-bands | band-wall seat | 18 |
| Flag | Budget violated | Exit |
|---|---|---|
--skip-decimate | LOD1 ratio band | 9 |
--stray-vert | loose vertex count is 0 | 15 |
--lift-z | bounding box zmin is 0 | 16 |
--short-legs | named foot supports at Z=0 | 16 |
--float-hinge | lid-knuckle to hinge barrel | 17 |
--narrow-bands | band-wall seat | 18 |
blender --background --python treasure_chest.py --
blender --background --python treasure_chest.py -- --skip-decimate
@@ -287,7 +292,7 @@ Run
Smoke passes no flags.
Exit codes
File-local. 9 is a valid check code. 10 is reserved for gallery_framing.check_framing on the --output path. 15–19 are the hygiene and joint-fit family.
-| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Mesh did not build / no UV layer | | 4 | Base triangle count outside range | | 5 | Material count ≠ 2 distinct slots, or a face-count floor missed | | 6 | UVs outside 0..1 | | 7 | UV AABB overlap above tolerance | | 8 | World AABB off declared outer size | | 9 | LOD ratio band (--skip-decimate lands here) | | 10 | Framing gate (render path only) | | 11 | Collider triangle count above ceiling | | 12 | Bake did not finish or image has no data | | 13 | Export file missing or empty | | 14 | --output produced no file | | 15 | Mesh hygiene: loose, non-manifold, zero-area, doubles, n-gons, z-fight | | 16 | Not grounded: bounding box zmin off 0, or a named foot floats | | 17 | Joint fit: lid-knuckle to hinge barrel | | 18 | Seat: band-wall gap | | 19 | Body plan off the stated real-world size |
+Code Meaning 0 Success 1 Uncaught exception (FATAL wrapper) 2 argparse / usage 3 Mesh did not build / no UV layer 4 Base triangle count outside range 5 Material count ≠ 2 distinct slots, or a face-count floor missed 6 UVs outside 0..1 7 UV AABB overlap above tolerance 8 World AABB off declared outer size 9 LOD ratio band (--skip-decimate lands here) 10 Framing gate (render path only) 11 Collider triangle count above ceiling 12 Bake did not finish or image has no data 13 Export file missing or empty 14 --output produced no file15 Mesh hygiene: loose, non-manifold, zero-area, doubles, n-gons, z-fight 16 Not grounded: bounding box zmin off 0, or a named foot floats 17 Joint fit: lid-knuckle to hinge barrel 18 Seat: band-wall gap 19 Body plan off the stated real-world size
Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper.
| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Loop-triangle count ≠ closed form | | 4 | Re-fetched UV layer drifted from the polar field (--zero-uv lands here) | | 5 | Tangent basis not orthonormal | | 6 | Bitangent ≠ sign × (n × t) | | 7 | Tangents deviate from the edge/UV closed form | | 8 | Flipped tangent inside a clean triangle | | 10 | Gallery framing violation; also --output produced no file |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Loop-triangle count ≠ closed form |
| 4 | Re-fetched UV layer drifted from the polar field (--zero-uv lands here) |
| 5 | Tangent basis not orthonormal |
| 6 | Bitangent ≠ sign × (n × t) |
| 7 | Tangents deviate from the edge/UV closed form |
| 8 | Flipped tangent inside a clean triangle |
| 10 | Gallery framing violation; also --output produced no file |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --zero-uv.
By default it runs only the frame-independent correctness check: it inserts the rotation keys, samples the object's Z rotation at frame 1 vs a later frame, and asserts they differ (the keys drive playback). --output additionally renders a still; the full animated loop is a showcase extra, not part of the CI check.
Per-script sequential checks. 9 is a valid check code; there is no rule against it.
| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Rotation keys do not drive playback (--no-keys lands here) | | 4 | --output produced no file | | 5 | Wrong-era EEVEE engine id was accepted |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Rotation keys do not drive playback (--no-keys lands here) |
| 4 | --output produced no file |
| 5 | Wrong-era EEVEE engine id was accepted |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --no-keys.
Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper.
| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage; also base cube verts ≠ 8 | | 3 | VIEWPORT TESSELLATE point/face count off closed form | | 4 | RENDER TESSELLATE point/face count or scheme off closed form | | 5 | BEST_MATCH cage point count or scheme off | | 6 | RENDER and VIEWPORT USDA point counts are identical | | 10 | Gallery framing violation | | 12 | --output produced no file |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage; also base cube verts ≠ 8 |
| 3 | VIEWPORT TESSELLATE point/face count off closed form |
| 4 | RENDER TESSELLATE point/face count or scheme off closed form |
| 5 | BEST_MATCH cage point count or scheme off |
| 6 | RENDER and VIEWPORT USDA point counts are identical |
| 10 | Gallery framing violation |
| 12 | --output produced no file |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output, --evaluation-mode, or --subdivision.
What it witnesses: three related contracts on the same grid topology (SEG×SEG faces, (SEG+1)² verts, size 1.0 → coords in [-1, 1]):
create_grid(..., calc_uvs=True) with no prior bm.loops.layers.uv.new(...) leaves len(bm.loops.layers.uv) == 0 and mesh.uv_layers empty after to_mesh.calc_uvs=True fills every loop. UVs must match the closed form u = (x/size + 1)/2, v = (y/size + 1)/2 within 1e-6, both on the BMesh and after persisting to mesh.uv_layers.active.data.calc_uvs=False, then uv.new("UVMap") and a loop write of the same closed form. Does not depend on calc_uvs at all; same tolerance.What each check catches on failure:
-calc_uvs=True starts creating a layer on its own (falsified: expecting n_layers == 0 fails if a layer appears; temporarily pre-creating a layer before the hazard probe exits 3).create_grid segment/size contract drift (wrong face/vert counts).calc_uvs filling a different parameterization, or a one-texel shift (falsified: adding 0.1 to every U exited 6 with measured error 0.1).to_mesh.--output) — the saved PNG is read back and probed at each panel's projected center: the hazard panel must be one flat teal (per-channel spread ≤ 0.02, ordering b>g>r), the repaired panel a high-contrast checker (spread ≥ 0.25). Falsified twice: secretly giving the hazard panel UVs exited 11 with measured spread 0.7333; repainting texel (0,0) magenta exited 12 with measured rgb (0.927, 0.118, 0.536).calc_uvs=True starts creating a layer on its own (falsified: expecting n_layers == 0 fails if a layer appears; temporarily pre-creating a layer before the hazard probe exits 3).create_grid segment/size contract drift (wrong face/vert counts).calc_uvs filling a different parameterization, or a one-texel shift (falsified: adding 0.1 to every U exited 6 with measured error 0.1).to_mesh.--output) — the saved PNG is read back and probed at each panel's projected center: the hazard panel must be one flat teal (per-channel spread ≤ 0.02, ordering b>g>r), the repaired panel a high-contrast checker (spread ≥ 0.25). Falsified twice: secretly giving the hazard panel UVs exited 11 with measured spread 0.7333; repainting texel (0,0) magenta exited 12 with measured rgb (0.927, 0.118, 0.536).Version divergence: none probed — the silent no-op and closed-form UV fill assert identically on Blender 4.5 LTS and 5.1. The only gate in the file is the EEVEE engine id for the optional render (BLENDER_EEVEE_NEXT on 4.x, BLENDER_EEVEE on 5.x).
Render: two framed lightbox displays on floor trays (rear kick legs, status LED) sharing one neon checker image, shot at a slight 3/4. Left is the hazard (no UV layer) — flat teal of texel (0, 0). Right is the repair (pre-create + calc_uvs) — full magenta/cyan checker. If the UV contract failed, both panels would read the same — and the still is not just an illustration: the script re-reads its own render and exits non-zero unless the pixels prove the flat-vs-checker split (measured on 5.1.2: hazard spread 0.0078, repair spread 0.7412; identical on 4.5.11).
Per-script sequential checks. 9 is a valid check code; there is no rule against it. This example does not call the gallery framing helper; 10 is the missing-render-file check.
| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Silent-no-op hazard gone (--precreate-on-hazard lands here) | | 4 | Grid topology drifted | | 5 | Pre-create + calc_uvs did not persist one UV layer | | 6 | calc_uvs closed-form error | | 7 | Mesh UV round-trip error | | 8 | calc_uvs=False unexpectedly created a UV layer | | 9 | Explicit UV assignment error | | 10 | --output produced no file | | 11 | Broken panel is not flat | | 12 | Broken panel is not texel-(0,0) teal | | 13 | Repaired panel is not a checker | | 14 | Broken and repaired panels render identically |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Silent-no-op hazard gone (--precreate-on-hazard lands here) |
| 4 | Grid topology drifted |
| 5 | Pre-create + calc_uvs did not persist one UV layer |
| 6 | calc_uvs closed-form error |
| 7 | Mesh UV round-trip error |
| 8 | calc_uvs=False unexpectedly created a UV layer |
| 9 | Explicit UV assignment error |
| 10 | --output produced no file |
| 11 | Broken panel is not flat |
| 12 | Broken panel is not texel-(0,0) teal |
| 13 | Repaired panel is not a checker |
| 14 | Broken and repaired panels render identically |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --precreate-on-hazard.
A runnable example baking ambient occlusion into a colour attribute — the cheap contact shadows an engine gets for free if it reads vertex colour. Unlike most bakes this one can be checked against a *formula* rather than against a previous run's numbers: the hemisphere visibility integral has closed forms.
+A runnable example baking ambient occlusion into a colour attribute — the cheap contact shadows an engine gets for free if it reads vertex colour. Unlike most bakes this one can be checked against a formula rather than against a previous run's numbers: the hemisphere visibility integral has closed forms.
For a point on the floor a distance d from an infinitely wide wall of height H, integrating the cosine-weighted hemisphere over the directions the wall blocks gives (the φ integral collapses to π / √(1+k²)):
A(k) = ½ (1 − 1/√(1+k²)), k = H/d AO = 1 − A(k)
AO depends only on the ratio H/d and increases strictly with d — which is exactly "a concave corner darkens monotonically with depth", as an equation. For an unoccluded flat surface the integral is exactly 1.
The asset, for reuse: Well.Stone, a 2.5 m stone village well — three courses of 15 masonry blocks each on a 16-slab paved apron, a 15-slab coping ring, a lined bore, two braced timber posts with a crossbeam, an iron-banded windlass with a crank, and a hanging bucket. 11 parts, 11 materials, 6966 vertices. Origin at the ground contact centre (z == 0 is where it rests), identity transforms, datablocks under Well.Stone.*. Every part ships with the AO baked into two colour attributes, and default_color_name points at the linear one so an engine reads the right channel.
Pipeline arc neighbours: attribute domains in attribute-domain-shear and color-attribute-wheel, the second UV set for *texture*-baked lighting in lightmap-uv-channel, topology gates in mesh-hygiene-audit.
Pipeline arc neighbours: attribute domains in attribute-domain-shear and color-attribute-wheel, the second UV set for texture-baked lighting in lightmap-uv-channel, topology gates in mesh-hygiene-audit.
What it witnesses (all closed form or independently re-derived):
1 − A(H/d) at six probe distances spanning two decades of H/d (60.0 down to 0.60); worst error 6.760e-04 against a 2.5e-03 gate.FLOAT_COLOR round-trips the linear value exactly (0.000e+00). BYTE_COLOR does not — see below.data_type and domain.Well.Stone.* names, no default datablocks, min z == 0.00e+00, render colour attribute pinned to AO.BYTE_COLOR is sRGB-encoded 8-bit, not linear 8-bit. Writing linear 0.735 reads back 0.7379107. The round-trip error peaks at 3.782e-03, and the readback matches an independent encode → quantise → decode model to 3.189e-07 — so the encoding is confirmed, not guessed. Darks get more precision than a linear ramp would give and midtones get less. An exporter that hands the raw bytes to an engine expecting linear occlusion ships visibly wrong shadows. Store AO in FLOAT_COLOR unless the target genuinely wants sRGB.bmesh.ops.bevel offsets along *cached* face normals. Moving a vertex does not refresh them. While the stale normal is within 90° of the true one the bevel merely skews; past 90° it flips sign and grows the solid outward. Measured while authoring, on a ring of bevelled boxes placed around a circle: the boxes at 0/36/72° bevelled correctly and the one at 108° grew by exactly one offset (12 mm) in both z directions, putting geometry below the ground plane. t.normal_update() before every bevel is the fix, and every _box/_prism here calls it; the hygiene check (exit 8) is what caught it.color_attributes enumeration order is not portable — measured BYTE_COLOR first on 4.5.11 and FLOAT_COLOR first on 5.1.2 for the same mesh. Look attributes up by name, never by index.BYTE_COLOR is sRGB-encoded 8-bit, not linear 8-bit. Writing linear 0.735 reads back 0.7379107. The round-trip error peaks at 3.782e-03, and the readback matches an independent encode → quantise → decode model to 3.189e-07 — so the encoding is confirmed, not guessed. Darks get more precision than a linear ramp would give and midtones get less. An exporter that hands the raw bytes to an engine expecting linear occlusion ships visibly wrong shadows. Store AO in FLOAT_COLOR unless the target genuinely wants sRGB.bmesh.ops.bevel offsets along cached face normals. Moving a vertex does not refresh them. While the stale normal is within 90° of the true one the bevel merely skews; past 90° it flips sign and grows the solid outward. Measured while authoring, on a ring of bevelled boxes placed around a circle: the boxes at 0/36/72° bevelled correctly and the one at 108° grew by exactly one offset (12 mm) in both z directions, putting geometry below the ground plane. t.normal_update() before every bevel is the fix, and every _box/_prism here calls it; the hygiene check (exit 8) is what caught it.color_attributes enumeration order is not portable — measured BYTE_COLOR first on 4.5.11 and FLOAT_COLOR first on 5.1.2 for the same mesh. Look attributes up by name, never by index.What each check catches on failure (every one probed, with the measured error): an integrator sampling the full sphere rather than the hemisphere — AO 0.752686 against a closed form of 0.508332, error 2.444e-01 (exit 3); uniform-solid-angle weighting instead of cosine — error 1.202e-02 (exit 3); rays cast with no normal offset so they self-hit the surface they start on — unoccluded plate bakes to 0.160644531 instead of 1.0 (exit 4); a bake collapsed to a constant — spread 0.000000 (exit 5); the sRGB storage model swapped for naive linear 8-bit quantisation — 4.577e-03 disagreement with the real readback (exit 6); a Subdivision modifier resampling the attribute — 3270 evaluated elements against 840 authored (exit 7); a part sunk below the ground plane — -1.200e-02 (exit 8); a default Cube datablock name (exit 8).
The exit-7 probe earned its keep twice: the depsgraph check originally compared zip(src.data, eva.data), which stops at the shorter sequence, so a resampled attribute reported a clean 0.0 deviation and the probe exited 0. The length guard was added because the falsification pass failed to fail.
Version witness: check output is byte-identical on Blender 4.5.11 LTS and 5.1.2 — same 6966 vertices, same closed-form errors to every printed digit, same storage deviations.
@@ -280,7 +285,7 @@The --output render path additionally gates framing via examples/gallery_framing.py (fill 0.839y, margins 0.241/0.238/0.072/0.089, no edge touched) and the asset floors via examples/gallery_asset_quality.py (11 materials, edge90 0.152, no default names).
Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper. 11 is the shared asset-quality helper.
| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Calibration AO off closed form | | 4 | Unoccluded plate is not 1.0, or AO is not strictly increasing with distance | | 5 | Bake empty, out of range, or spread too small | | 6 | FLOAT_COLOR / BYTE_COLOR round-trip contract | | 7 | Colour attributes missing, retyped, or drifted under the depsgraph | | 8 | Asset part count, unapplied scale, namespace, render attribute, or ground plane | | 9 | --output produced no file | | 10 | Gallery framing violation | | 11 | Gallery asset-quality violation |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Calibration AO off closed form |
| 4 | Unoccluded plate is not 1.0, or AO is not strictly increasing with distance |
| 5 | Bake empty, out of range, or spread too small |
| 6 | FLOAT_COLOR / BYTE_COLOR round-trip contract |
| 7 | Colour attributes missing, retyped, or drifted under the depsgraph |
| 8 | Asset part count, unapplied scale, namespace, render attribute, or ground plane |
| 9 | --output produced no file |
| 10 | Gallery framing violation |
| 11 | Gallery asset-quality violation |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --falsify.
Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper; it is also the missing-render code.
| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Pre-limit max influences ≠ 5 | | 4 | Vertex over the 4-influence cap (--skip-limit lands here) | | 5 | Limit changed nothing | | 6 | Weight sums off 1.0 after renormalize | | 7 | Pose damaged by pruning, or evaluated vert count changed | | 8 | Evaluated mesh off LBS over limited weights | | 9 | Root-weighted mount moved | | 10 | Gallery framing violation; also --output produced no file |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Pre-limit max influences ≠ 5 |
| 4 | Vertex over the 4-influence cap (--skip-limit lands here) |
| 5 | Limit changed nothing |
| 6 | Weight sums off 1.0 after renormalize |
| 7 | Pose damaged by pruning, or evaluated vert count changed |
| 8 | Evaluated mesh off LBS over limited weights |
| 9 | Root-weighted mount moved |
| 10 | Gallery framing violation; also --output produced no file |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --skip-limit.
A runnable example that witnesses the sequencer API rename between Blender 4.5 LTS and 5.x — the single most common way AI-generated VSE code dies on a modern Blender. Every tutorial-era snippet does scene.sequence_editor.sequences.new_effect(..., frame_end=...); on 5.x that path is gone twice over (sequences removed, frame_end kwarg rejected), and on 4.5 the modern spelling (strips, length=) is already the only one the new collection accepts.
What it witnesses: a deterministic cut list — three color programs A/B/C, a GAMMA_CROSS fed by a dedicated source pair T1/T2, a scene strip, and a text strip — asserted against closed-form spans on each version's canonical accessors, then re-asserted after a save/reload round-trip:
-sequence_editor.sequences raises AttributeError; only .strips exists. 4.5: both accessors exist and see the same strips (the transition bridge).strips.new_effect(...) ends a strip with frame_end= on 4.5 but length= on 5.x; the wrong kwarg raises TypeError on each side (asserted, not assumed). 5.x also rejects the removed TRANSFORM effect type — per-strip strip.transform (StripTransform) places strips in the frame on both versions.frame_final_* on 4.5; left_handle/right_handle/duration on 5.x, where the frame_final_* names are deprecated aliases (removal announced for 6.0) that must still read equal — the bridge is asserted lossless, with the RNA is_deprecated flag checked on 5.x and its absence checked on 4.5.input_1/input_2 (not input1) on both versions, in authored order, and the cross clamped to exactly the T1/T2 overlap.new_scene(name, scene, channel, frame_start) on both versions (no length/frame_end kwarg); default span is the source scene's frame range; the strip sources a *separate* Stage scene.save_as_mainfile → open_mainfile.--check-pixels) — a 96×54 render asserts each mosaic cell center carries its strip color (tol 0.1), the cross cell is a true mid blend (strictly between the sources on every channel), and a margin point matches neither cross source — because a consumed input compositing independently would paint it full-frame.sequence_editor.sequences raises AttributeError; only .strips exists. 4.5: both accessors exist and see the same strips (the transition bridge).strips.new_effect(...) ends a strip with frame_end= on 4.5 but length= on 5.x; the wrong kwarg raises TypeError on each side (asserted, not assumed). 5.x also rejects the removed TRANSFORM effect type — per-strip strip.transform (StripTransform) places strips in the frame on both versions.frame_final_* on 4.5; left_handle/right_handle/duration on 5.x, where the frame_final_* names are deprecated aliases (removal announced for 6.0) that must still read equal — the bridge is asserted lossless, with the RNA is_deprecated flag checked on 5.x and its absence checked on 4.5.input_1/input_2 (not input1) on both versions, in authored order, and the cross clamped to exactly the T1/T2 overlap.new_scene(name, scene, channel, frame_start) on both versions (no length/frame_end kwarg); default span is the source scene's frame range; the strip sources a separate Stage scene.save_as_mainfile → open_mainfile.--check-pixels) — a 96×54 render asserts each mosaic cell center carries its strip color (tol 0.1), the cross cell is a true mid blend (strictly between the sources on every channel), and a margin point matches neither cross source — because a consumed input compositing independently would paint it full-frame.What each check catches on failure:
-.sequences returning on 5.x, or the 4.5 bridge seeing different contents (falsified: the legacy path on 5.1.1 raises AttributeError: 'SequenceEditor' object has no attribute 'sequences').new_effect(frame_end=...) on 5.1.1 raises TypeError: ... expected (name, type, channel, frame_start, length, input1, input2)).GC span (25, 33, 8) != closed form (25, 34, 9) — note the cross was *clamped* to the 8-frame overlap, itself a witnessed behavior).input1=t2, input2=t1 exited 7 with measured inputs=(T2, T1), expected T1 -> T2)..sequences returning on 5.x, or the 4.5 bridge seeing different contents (falsified: the legacy path on 5.1.1 raises AttributeError: 'SequenceEditor' object has no attribute 'sequences').new_effect(frame_end=...) on 5.1.1 raises TypeError: ... expected (name, type, channel, frame_start, length, input1, input2)).GC span (25, 33, 8) != closed form (25, 34, 9) — note the cross was clamped to the 8-frame overlap, itself a witnessed behavior).input1=t2, input2=t1 exited 7 with measured inputs=(T2, T1), expected T1 -> T2).Hazards discovered while authoring (all witnessed by the checks above):
bpy_prop_collection is falsy — se.strips or se.sequences silently falls through to the legacy accessor on an empty timeline. Always branch on hasattr.Version divergence: the whole example is the divergence — gated on the bpy.app.version tuple (>= (5, 0, 0)), never on version_string ("4.5.11 LTS" is not bare semver). Each side asserts its own canonical contract plus the other side's removal/bridge state. Measured values are identical on 4.5.11 and 5.1.2, including the pixel witness. 5.2 LTS COLOR strips bake readonly width/height from the scene render resolution at new_effect time (4.5/5.1 have no intrinsic size — scale is a fraction of the output frame). The pixel check sets 96×54 *before* building the cut list; creating at factory 1920×1080 then rendering 96×54 makes 0.36-scaled cells larger than the output, C covers the frame, and TL samples amber (0.949, 0.62, 0.102) instead of crimson.
Render: the program wall *is* the sequencer output at frame 29 (mid cross) — crimson A, teal B, amber long-runner C, and the 50/50 cross blend, over the Stage scene strip showing the dark studio. An off-by-one in end-exclusive span math drops its cell to the dark stage; the caption strip carries the closed form. The hero presents that authentic frame on a reference monitor in a dark-studio editing bay — the pixels on the screen are the genuine sequencer output (evidence); only the bay around them is staged (presentation). Rendered locally with EEVEE (GPU host); the checks and --check-pixels need no GPU (Cycles CPU).
Version divergence: the whole example is the divergence — gated on the bpy.app.version tuple (>= (5, 0, 0)), never on version_string ("4.5.11 LTS" is not bare semver). Each side asserts its own canonical contract plus the other side's removal/bridge state. Measured values are identical on 4.5.11 and 5.1.2, including the pixel witness. 5.2 LTS COLOR strips bake readonly width/height from the scene render resolution at new_effect time (4.5/5.1 have no intrinsic size — scale is a fraction of the output frame). The pixel check sets 96×54 before building the cut list; creating at factory 1920×1080 then rendering 96×54 makes 0.36-scaled cells larger than the output, C covers the frame, and TL samples amber (0.949, 0.62, 0.102) instead of crimson.
Render: the program wall is the sequencer output at frame 29 (mid cross) — crimson A, teal B, amber long-runner C, and the 50/50 cross blend, over the Stage scene strip showing the dark studio. An off-by-one in end-exclusive span math drops its cell to the dark stage; the caption strip carries the closed form. The hero presents that authentic frame on a reference monitor in a dark-studio editing bay — the pixels on the screen are the genuine sequencer output (evidence); only the bay around them is staged (presentation). Rendered locally with EEVEE (GPU host); the checks and --check-pixels need no GPU (Cycles CPU).
The gallery still renders from the dedicated Bay scene (camera BayCam), not bpy.context.scene — a framing measurement with examples/gallery_framing.py must be handed that (scene, camera) pair explicitly.
Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is also the shared framing helper.
| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | .sequences accessor contract for this Blender | | 4 | Wrong-era new_effect end kwarg or TRANSFORM type did not TypeError | | 5 | Strip span or channel off closed form | | 6 | frame_final_* deprecation bridge for this Blender | | 7 | GC wiring or strip .type enum (--swap-inputs lands here) | | 8 | Scene strip span or Stage source | | 9 | Mosaic transform, compositing defaults, text body, or strip color | | 10 | Save/reload round-trip re-assert failed; also gallery framing violation | | 11 | --output produced no file | | 12 | --check-pixels compositing contract |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | .sequences accessor contract for this Blender |
| 4 | Wrong-era new_effect end kwarg or TRANSFORM type did not TypeError |
| 5 | Strip span or channel off closed form |
| 6 | frame_final_* deprecation bridge for this Blender |
| 7 | GC wiring or strip .type enum (--swap-inputs lands here) |
| 8 | Scene strip span or Stage source |
| 9 | Mosaic transform, compositing defaults, text body, or strip color |
| 10 | Save/reload round-trip re-assert failed; also gallery framing violation |
| 11 | --output produced no file |
| 12 | --check-pixels compositing contract |
The blender-smoke workflow runs the check and the pixel witness on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --swap-inputs.
Per-script sequential checks. 9 is a valid check code; there is no rule against it.
| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | GC span off closed form | | 4 | GC inputs are not T1 → T2 (--swap-inputs lands here) | | 5 | Sample t convention drifted | | 6 | Cross sample off the gamma-0.5 closed form | | 7 | Mid-cross lerp deviation missing (naive mix) | | 8 | --output produced no file |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | GC span off closed form |
| 4 | GC inputs are not T1 → T2 (--swap-inputs lands here) |
| 5 | Sample t convention drifted |
| 6 | Cross sample off the gamma-0.5 closed form |
| 7 | Mid-cross lerp deviation missing (naive mix) |
| 8 | --output produced no file |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --swap-inputs.
Intended size: 0.28 m plaque width, 0.40 m plaque height, bowl projecting 0.24 m from the wall; outer AABB 0.304 × 0.293 × 0.424 m.
Declared as named constants; every gate recomputes from the mesh, materials, UVs, evaluated LOD, collider, or export file.
-| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) | | --- | --- | --- | | Base triangles | 1400–1700 | 1528 / 1528 / 1528 | | LOD1 ratio | 0.32–0.62 of base | 0.5000 / 0.5000 / 0.5000 | | LOD2 ratio | 0.10–0.35 of base | 0.2199 / 0.2199 / 0.2199 | | Materials | exactly 4 distinct, ≥24 stone, ≥24 metal, ≥8 flame | 4 slots, 540 stone, 228 metal, 78 flame | | UVs | in 0..1, AABB overlap ≤ 1e-5 | in range, overlap 0 | | Outer AABB | (0.304, 0.293, 0.424) m ± 0.015 | (0.3040, 0.2930, 0.4240), zmin 0 | | Collider tris | ≤ 180 | 60 | | Export | written, size > 0 | 124608 / 124608 / 124584 bytes |
| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) |
|---|---|---|
| Base triangles | 1400–1700 | 1528 / 1528 / 1528 |
| LOD1 ratio | 0.32–0.62 of base | 0.5000 / 0.5000 / 0.5000 |
| LOD2 ratio | 0.10–0.35 of base | 0.2199 / 0.2199 / 0.2199 |
| Materials | exactly 4 distinct, ≥24 stone, ≥24 metal, ≥8 flame | 4 slots, 540 stone, 228 metal, 78 flame |
| UVs | in 0..1, AABB overlap ≤ 1e-5 | in range, overlap 0 |
| Outer AABB | (0.304, 0.293, 0.424) m ± 0.015 | (0.3040, 0.2930, 0.4240), zmin 0 |
| Collider tris | ≤ 180 | 60 |
| Export | written, size > 0 | 124608 / 124608 / 124584 bytes |
Base triangles rose from 848 to 1528 in the quality pass: one mailbox slab became four courses of running-bond ashlar plus a round arm and a triangulated bowl. Outer AABB tightened from 0.384 × 0.360 × 0.670 m (and zmin 0.140) to 0.304 × 0.293 × 0.424 m at zmin 0.
DECIMATE COLLAPSE triangle counts are not identical across series. The gate is a ratio band, not an exact count. Bake pixels are stochastic; the gate is has_data plus operator FINISHED, not byte-identity. Construction uses no RNG. Export byte counts differ by 24 B on 5.2.1 (glTF serializer), not a gated axis.
Recomputed from the generated mesh, not asserted about the script.
-| Axis | Declared | Measured (all three) | | --- | --- | --- | | Non-manifold edges | 0 | 0 | | Loose verts / edges | 0 / 0 | 0 / 0 | | Doubles merged at 1e-5 | 0 | 0 | | Zero-area faces | 0 | 0 | | N-gons | 0 | 0 | | Coplanar disjoint face pairs | 0 | 0 | | Grounded: zmin | within 1e-4 of 0 | 0.0000 |
| Axis | Declared | Measured (all three) |
|---|---|---|
| Non-manifold edges | 0 | 0 |
| Loose verts / edges | 0 / 0 | 0 / 0 |
| Doubles merged at 1e-5 | 0 | 0 |
| Zero-area faces | 0 | 0 |
| N-gons | 0 | 0 |
| Coplanar disjoint face pairs | 0 | 0 |
Grounded: zmin | within 1e-4 of 0 | 0.0000 |
| Axis | Declared | Measured (all three) | | --- | --- | --- | | Arm-to-cup BVH gap | ≤ 0.008 m | 0.00087 | | Plate-to-plaque BVH gap | ≤ 0.008 m | 0.00400 | | Plaque width vs WALL_W | ± 0.04 m | 0.2800 vs 0.280 |
| Axis | Declared | Measured (all three) |
|---|---|---|
| Arm-to-cup BVH gap | ≤ 0.008 m | 0.00087 |
| Plate-to-plaque BVH gap | ≤ 0.008 m | 0.00400 |
Plaque width vs WALL_W | ± 0.04 m | 0.2800 vs 0.280 |
Each violates one named budget. All were run on 4.5.11, 5.1.2 and 5.2.1 and returned the same code on each.
-| Flag | Budget violated | Exit | | --- | --- | --- | | --skip-decimate | LOD1 ratio band | 9 | | --stray-vert | loose vertex count is 0 | 15 | | --lift-z | bounding box zmin is 0 | 16 | | --float-arm | arm-to-cup BVH gap | 17 | | --float-plate | plate-to-plaque BVH gap | 18 | | --skinny-plaque | plaque width vs WALL_W | 19 |
| Flag | Budget violated | Exit |
|---|---|---|
--skip-decimate | LOD1 ratio band | 9 |
--stray-vert | loose vertex count is 0 | 15 |
--lift-z | bounding box zmin is 0 | 16 |
--float-arm | arm-to-cup BVH gap | 17 |
--float-plate | plate-to-plaque BVH gap | 18 |
--skinny-plaque | plaque width vs WALL_W | 19 |
--float-arm shortens the tube to 55 % of the plate-to-cup station vector; the cup stays put. --float-plate moves only the hang plate off the plaque; the arm still reaches the cup so 17 still passes. --skinny-plaque scales the back plaque and courses to 0.55 · WALL_W.
blender --background --python wall_torch.py --
@@ -290,7 +295,7 @@ Run
Smoke passes no flags.
Exit codes
File-local. 9 is a valid check code. 10 is reserved for gallery_framing.check_framing on the --output path. 15–19 are the hygiene and joint-fit family.
-| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Mesh did not build / no UV layer | | 4 | Base triangle count outside range | | 5 | Material count ≠ 4 distinct slots, or stone/metal/flame faces missing | | 6 | UVs outside 0..1 | | 7 | UV AABB overlap above tolerance | | 8 | World AABB off declared outer size | | 9 | LOD ratio band (--skip-decimate lands here) | | 10 | Framing gate (render path only) | | 11 | Collider triangle count above ceiling | | 12 | Bake did not finish or image has no data | | 13 | Export file missing or empty | | 14 | --output produced no file | | 15 | Hygiene (--stray-vert lands here) | | 16 | Grounded zmin (--lift-z) | | 17 | Arm-to-cup gap (--float-arm) | | 18 | Plate-to-plaque gap (--float-plate) | | 19 | Plaque width (--skinny-plaque) |
+Code Meaning 0 Success 1 Uncaught exception (FATAL wrapper) 2 argparse / usage 3 Mesh did not build / no UV layer 4 Base triangle count outside range 5 Material count ≠ 4 distinct slots, or stone/metal/flame faces missing 6 UVs outside 0..1 7 UV AABB overlap above tolerance 8 World AABB off declared outer size 9 LOD ratio band (--skip-decimate lands here) 10 Framing gate (render path only) 11 Collider triangle count above ceiling 12 Bake did not finish or image has no data 13 Export file missing or empty 14 --output produced no file15 Hygiene (--stray-vert lands here) 16 Grounded zmin (--lift-z) 17 Arm-to-cup gap (--float-arm) 18 Plate-to-plaque gap (--float-plate) 19 Plaque width (--skinny-plaque)
Composes skills mesh-editing-and-bmesh, bake-high-to-low, depsgraph-and-evaluated-data, engine-export-presets, and snippets bake_normal_high_to_low.py, setup_bake_target_image.py, lod_chain.py / decimate_to_budget.py, convex_hull_collider.py, export_preset_unity.py (helpers copied, not imported as a package).
Declared as named constants; every gate recomputes from the mesh, materials, UVs, evaluated LOD, collider, or export file.
-| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) | | --- | --- | --- | | Base triangles | 6200–8200 | 6408 / 6408 / 6408 | | LOD1 ratio | 0.32–0.62 of base | 0.5000 / 0.5000 / 0.5000 | | LOD2 ratio | 0.10–0.35 of base | 0.2197 / 0.2197 / 0.2185 | | Materials | exactly 3 distinct; ≥80 metal, ≥40 roof faces | 3 slots; 300 metal, 126 roof | | UVs | in 0..1, AABB overlap ≤ 1e-5 | in range, overlap 0 | | Outer AABB | (1.594, 1.594, 2.973) m ± 0.01 | (1.5940, 1.5940, 2.9731), zmin 0 | | Collider tris | ≤ 180 | 150 | | Export | written, size > 0 | 464572 / 464572 / 464560 bytes |
| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) |
|---|---|---|
| Base triangles | 6200–8200 | 6408 / 6408 / 6408 |
| LOD1 ratio | 0.32–0.62 of base | 0.5000 / 0.5000 / 0.5000 |
| LOD2 ratio | 0.10–0.35 of base | 0.2197 / 0.2197 / 0.2185 |
| Materials | exactly 3 distinct; ≥80 metal, ≥40 roof faces | 3 slots; 300 metal, 126 roof |
| UVs | in 0..1, AABB overlap ≤ 1e-5 | in range, overlap 0 |
| Outer AABB | (1.594, 1.594, 2.973) m ± 0.01 | (1.5940, 1.5940, 2.9731), zmin 0 |
| Collider tris | ≤ 180 | 150 |
| Export | written, size > 0 | 464572 / 464572 / 464560 bytes |
Base triangles rose from 5220 to 6408 in the quality pass: a taller eave-true roof and coursed shakes, not hidden interior faces.
Recomputed from the generated mesh, not asserted about the script.
-| Axis | Declared | Measured (all three) | | --- | --- | --- | | Non-manifold edges | 0 | 0 | | Loose verts / edges | 0 / 0 | 0 / 0 | | Doubles merged at 1e-5 | 0 | 0 | | Zero-area faces | 0 | 0 | | N-gons | 0 | 0 | | Coplanar disjoint face pairs | 0 | 0 | | Grounded: zmin | within 1e-4 of 0 | 0.0000 | | Named supports: 4 iron shoes | each zmin ≤ 1e-3 | 4, shoe_z 0.00000 | | Plan | 1.56 m eave × 2.97 m height ± 0.08 / ± 0.28 | 1.5940 × 2.9731 |
| Axis | Declared | Measured (all three) |
|---|---|---|
| Non-manifold edges | 0 | 0 |
| Loose verts / edges | 0 / 0 | 0 / 0 |
| Doubles merged at 1e-5 | 0 | 0 |
| Zero-area faces | 0 | 0 |
| N-gons | 0 | 0 |
| Coplanar disjoint face pairs | 0 | 0 |
Grounded: zmin | within 1e-4 of 0 | 0.0000 |
| Named supports: 4 iron shoes | each zmin ≤ 1e-3 | 4, shoe_z 0.00000 |
| Plan | 1.56 m eave × 2.97 m height ± 0.08 / ± 0.28 | 1.5940 × 2.9731 |
| Axis | Declared | Measured (all three) | | --- | --- | --- | | Rail-to-post engage | ≥ 0.012 m | 0.0180 | | Girt-collar standoff | ≤ 0.010 m | 0.00200 | | Post plumb (XY drift) | ≤ 0.010 m | 0.00000 |
+| Axis | Declared | Measured (all three) |
|---|---|---|
| Rail-to-post engage | ≥ 0.012 m | 0.0180 |
| Girt-collar standoff | ≤ 0.010 m | 0.00200 |
| Post plumb (XY drift) | ≤ 0.010 m | 0.00000 |
DECIMATE COLLAPSE triangle counts are not identical across series — 5.2.1 is leaner on LOD2. The gate is a ratio band, not an exact count. Bake pixels are stochastic; the gate is has_data plus operator FINISHED, not byte-identity. Construction uses no RNG. Export byte counts differ by 12 B on 5.2.1 (glTF serializer), not a gated axis.
Each violates one named budget. All eight were run on 4.5.11, 5.1.2 and 5.2.1 and returned the same code on each.
-| Flag | Budget violated | Exit | | --- | --- | --- | | --skip-decimate | LOD1 ratio band | 9 | | --stray-vert | loose vertex count is 0 | 15 | | --lift-z | bounding box zmin is 0 | 16 | | --short-shoes | named shoe supports at Z=0 | 16 | | --short-rails | rail-to-post joint fit | 17 | | --float-band | iron-collar seat | 18 | | --rake-posts | post plumb | 19 |
| Flag | Budget violated | Exit |
|---|---|---|
--skip-decimate | LOD1 ratio band | 9 |
--stray-vert | loose vertex count is 0 | 15 |
--lift-z | bounding box zmin is 0 | 16 |
--short-shoes | named shoe supports at Z=0 | 16 |
--short-rails | rail-to-post joint fit | 17 |
--float-band | iron-collar seat | 18 |
--rake-posts | post plumb | 19 |
blender --background --python watchtower.py --
blender --background --python watchtower.py -- --skip-decimate
@@ -289,7 +294,7 @@ Run
Smoke passes no flags.
Exit codes
File-local. 9 is a valid check code. 10 is reserved for gallery_framing.check_framing on the --output path. 15–19 are the hygiene and joint-fit family.
-| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Mesh did not build / no UV layer | | 4 | Base triangle count outside range | | 5 | Material count ≠ 3 distinct slots, or metal/roof faces missing | | 6 | UVs outside 0..1 | | 7 | UV AABB overlap above tolerance | | 8 | World AABB off declared outer size | | 9 | LOD ratio band (--skip-decimate lands here) | | 10 | Framing gate (render path only) | | 11 | Collider triangle count above ceiling | | 12 | Bake did not finish or image has no data | | 13 | Export file missing or empty | | 14 | --output produced no file | | 15 | Mesh hygiene: loose, non-manifold, zero-area, doubles, n-gons, z-fight | | 16 | Not grounded: bounding box zmin off 0, or a named shoe floats | | 17 | Joint fit: rail-to-post engage | | 18 | Seat: girt-collar standoff | | 19 | Post plumb or plan off the stated real-world size |
+Code Meaning 0 Success 1 Uncaught exception (FATAL wrapper) 2 argparse / usage 3 Mesh did not build / no UV layer 4 Base triangle count outside range 5 Material count ≠ 3 distinct slots, or metal/roof faces missing 6 UVs outside 0..1 7 UV AABB overlap above tolerance 8 World AABB off declared outer size 9 LOD ratio band (--skip-decimate lands here) 10 Framing gate (render path only) 11 Collider triangle count above ceiling 12 Bake did not finish or image has no data 13 Export file missing or empty 14 --output produced no file15 Mesh hygiene: loose, non-manifold, zero-area, doubles, n-gons, z-fight 16 Not grounded: bounding box zmin off 0, or a named shoe floats 17 Joint fit: rail-to-post engage 18 Seat: girt-collar standoff 19 Post plumb or plan off the stated real-world size
Intended size: 1.08 m tray length, 0.44 m across the U, 0.50 m overall height; outer AABB 1.093 × 0.552 × 0.504 m.
Declared as named constants; every gate recomputes from the mesh, materials, UVs, evaluated LOD, collider, or export file.
-| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) | | --- | --- | --- | | Base triangles | 3300–4000 | 3700 / 3700 / 3700 | | LOD1 ratio | 0.32–0.62 of base | 0.5000 / 0.5000 / 0.5000 | | LOD2 ratio | 0.10–0.35 of base | 0.2200 / 0.2200 / 0.2200 | | Materials | exactly 3 distinct; ≥24 wood, ≥24 metal, ≥6 water | 3 slots; 1674 / 156 / 29 | | UVs | in 0..1, AABB overlap ≤ 1e-5 | in range, overlap 0 | | Outer AABB | (1.093, 0.552, 0.504) m ± 0.01 | (1.0932, 0.5520, 0.5040), zmin 0 | | Collider tris | ≤ 80 | 64 | | Export | written, size > 0 | 268840 / 268840 / 268824 bytes |
| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) |
|---|---|---|
| Base triangles | 3300–4000 | 3700 / 3700 / 3700 |
| LOD1 ratio | 0.32–0.62 of base | 0.5000 / 0.5000 / 0.5000 |
| LOD2 ratio | 0.10–0.35 of base | 0.2200 / 0.2200 / 0.2200 |
| Materials | exactly 3 distinct; ≥24 wood, ≥24 metal, ≥6 water | 3 slots; 1674 / 156 / 29 |
| UVs | in 0..1, AABB overlap ≤ 1e-5 | in range, overlap 0 |
| Outer AABB | (1.093, 0.552, 0.504) m ± 0.01 | (1.0932, 0.5520, 0.5040), zmin 0 |
| Collider tris | ≤ 80 | 64 |
| Export | written, size > 0 | 268840 / 268840 / 268824 bytes |
Base triangles rose from 2484 to 3700 in the quality pass: box end slabs and a single extruded U became ten jittered staves, U end-caps, continuous straps offset from the legs, and stretchers that meet the inner faces.
DECIMATE COLLAPSE triangle counts are not identical across series — the gate is a ratio band, not an exact count. Bake pixels are stochastic; the gate is has_data plus operator FINISHED, not byte-identity. Stave-width jitter uses closed-form sin(i), no RNG. Export byte counts differ by 16 B on 5.2.1 (glTF serializer), not a gated axis.
The hull-size budget (exit 19) runs before the outer AABB budget (exit 8) so --narrow-hull is not swallowed by a moved bounding box.
Recomputed from the generated mesh, not asserted about the script.
-| Axis | Declared | Measured (all three) | | --- | --- | --- | | Non-manifold edges | 0 | 0 | | Loose verts / edges | 0 / 0 | 0 / 0 | | Doubles merged at 1e-5 | 0 | 0 | | Zero-area faces | 0 | 0 | | N-gons | 0 | 0 | | Coplanar disjoint face pairs | 0 | 0 | | Grounded: zmin | within 1e-4 of 0 | 0.0000 | | Named supports: 4 shoes | each zmin ≤ 0.001 | 4, shoe_z 0.00000 | | Hull plan | 1.08 m × 0.44 m ± 0.08 | 1.0448 × 0.4661 |
| Axis | Declared | Measured (all three) |
|---|---|---|
| Non-manifold edges | 0 | 0 |
| Loose verts / edges | 0 / 0 | 0 / 0 |
| Doubles merged at 1e-5 | 0 | 0 |
| Zero-area faces | 0 | 0 |
| N-gons | 0 | 0 |
| Coplanar disjoint face pairs | 0 | 0 |
Grounded: zmin | within 1e-4 of 0 | 0.0000 |
| Named supports: 4 shoes | each zmin ≤ 0.001 | 4, shoe_z 0.00000 |
| Hull plan | 1.08 m × 0.44 m ± 0.08 | 1.0448 × 0.4661 |
| Axis | Declared | Measured (all three) | | --- | --- | --- | | End-cap rim Z-span | ≤ 0.080 m | 0.0050 | | Strap-to-hull BVH gap | ≤ 0.008 m | 0.00000 | | Water-to-hull BVH gap | ≤ 0.008 m | 0.00260 |
+| Axis | Declared | Measured (all three) |
|---|---|---|
| End-cap rim Z-span | ≤ 0.080 m | 0.0050 |
| Strap-to-hull BVH gap | ≤ 0.008 m | 0.00000 |
| Water-to-hull BVH gap | ≤ 0.008 m | 0.00260 |
Each violates one named budget. All seven were run on 4.5.11, 5.1.2 and 5.2.1 and returned the same code on each.
-| Flag | Budget violated | Exit | | --- | --- | --- | | --skip-decimate | LOD1 ratio band | 9 | | --stray-vert | loose vertex count is 0 | 15 | | --lift-z | bounding box zmin is 0 | 16 | | --short-legs | named shoe supports at Z=0 | 16 | | --box-ends | end-cap rim span | 17 | | --float-strap | strap-to-hull gap | 18 | | --narrow-hull | hull plan vs stated size | 19 |
| Flag | Budget violated | Exit |
|---|---|---|
--skip-decimate | LOD1 ratio band | 9 |
--stray-vert | loose vertex count is 0 | 15 |
--lift-z | bounding box zmin is 0 | 16 |
--short-legs | named shoe supports at Z=0 | 16 |
--box-ends | end-cap rim span | 17 |
--float-strap | strap-to-hull gap | 18 |
--narrow-hull | hull plan vs stated size | 19 |
--short-legs lifts the shoes only, so AABB zmin stays 0 (the legs still plant) and the named-support gate is what fires.
blender --background --python water_trough.py --
@@ -292,7 +297,7 @@ Run
Smoke passes no flags.
Exit codes
File-local. 9 is a valid check code. 10 is reserved for gallery_framing.check_framing on the --output path. 15–19 are the hygiene and joint-fit family.
-| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Mesh did not build / no UV layer | | 4 | Base triangle count outside range | | 5 | Material count ≠ 3 distinct slots, or a face-count floor missed | | 6 | UVs outside 0..1 | | 7 | UV AABB overlap above tolerance | | 8 | World AABB off declared outer size | | 9 | LOD ratio band (--skip-decimate lands here) | | 10 | Framing gate (render path only) | | 11 | Collider triangle count above ceiling | | 12 | Bake did not finish or image has no data | | 13 | Export file missing or empty | | 14 | --output produced no file | | 15 | Mesh hygiene: loose, non-manifold, zero-area, doubles, n-gons, z-fight | | 16 | Not grounded: bounding box zmin off 0, or a named shoe floats | | 17 | Joint fit: end-cap U-rim span (--box-ends) | | 18 | Seat: strap or water BVH gap (--float-strap) | | 19 | Hull length or width off the stated real-world size (--narrow-hull) |
+Code Meaning 0 Success 1 Uncaught exception (FATAL wrapper) 2 argparse / usage 3 Mesh did not build / no UV layer 4 Base triangle count outside range 5 Material count ≠ 3 distinct slots, or a face-count floor missed 6 UVs outside 0..1 7 UV AABB overlap above tolerance 8 World AABB off declared outer size 9 LOD ratio band (--skip-decimate lands here) 10 Framing gate (render path only) 11 Collider triangle count above ceiling 12 Bake did not finish or image has no data 13 Export file missing or empty 14 --output produced no file15 Mesh hygiene: loose, non-manifold, zero-area, doubles, n-gons, z-fight 16 Not grounded: bounding box zmin off 0, or a named shoe floats 17 Joint fit: end-cap U-rim span (--box-ends) 18 Seat: strap or water BVH gap (--float-strap) 19 Hull length or width off the stated real-world size (--narrow-hull)
A runnable example that displaces a 96×96 grid (9,409 vertices) into a standing wave using one foreach_get and one foreach_set — the bulk-IO pattern from use-foreach-set-for-bulk-data and the mesh-editing-and-bmesh skill — instead of 9,409 individual mesh.vertices[i].co accesses.
What it witnesses: the bulk path is not just faster, it is *correct* — the check asserts the flat grid gained the expected Z span (the write actually landed) and that every vertex matches the closed-form wave, so a stride or interleave bug in the flat buffer cannot hide behind a lucky probe.
+What it witnesses: the bulk path is not just faster, it is correct — the check asserts the flat grid gained the expected Z span (the write actually landed) and that every vertex matches the closed-form wave, so a stride or interleave bug in the flat buffer cannot hide behind a lucky probe.
Edge-to-edge field — the displaced sheet fills the frame and crops at the left, right, and bottom edges so the wave reads as a surface, not an object on a stage (measured fill 1.000x/0.817y with edge touch on three sides). If wired to examples/gallery_framing.py, call it with deviation="edge-to-edge wave field; the fill is the point".
Per-script sequential checks. 9 is a valid check code; there is no rule against it.
| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 4 | Z-span not in the closed-form band (--flat lands here) | | 5 | A vertex is off the closed-form wave | | 6 | --output produced no file |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 4 | Z-span not in the closed-form band (--flat lands here) |
| 5 | A vertex is off the closed-form wave |
| 6 | --output produced no file |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --flat.
Composes skills mesh-editing-and-bmesh, bake-high-to-low, depsgraph-and-evaluated-data, engine-export-presets, and snippets bake_normal_high_to_low.py, setup_bake_target_image.py, lod_chain.py / decimate_to_budget.py, convex_hull_collider.py, export_preset_unity.py (helpers copied, not imported as a package).
Declared as named constants; every gate recomputes from the mesh, materials, UVs, evaluated LOD, collider, or export file.
-| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) | | --- | --- | --- | | Base triangles | 2300–2800 | 2500 / 2500 / 2500 | | LOD1 ratio | 0.32–0.62 of base | 0.5000 / 0.5000 / 0.5000 | | LOD2 ratio | 0.10–0.35 of base | 0.2200 / 0.2200 / 0.2104 | | Materials | exactly 2 distinct; ≥700 wood, ≥180 metal faces | 2 slots; 1094 / 190 | | UVs | in 0..1, AABB overlap ≤ 1e-5 | in range, overlap 0 | | Outer AABB | (1.558, 0.630, 0.574) m ± 0.015 | (1.5556, 0.6300, 0.5720), zmin 0 | | Collider tris | ≤ 220 | 90 | | Export | written, size > 0 | 187576 / 187576 / 187560 bytes |
| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) |
|---|---|---|
| Base triangles | 2300–2800 | 2500 / 2500 / 2500 |
| LOD1 ratio | 0.32–0.62 of base | 0.5000 / 0.5000 / 0.5000 |
| LOD2 ratio | 0.10–0.35 of base | 0.2200 / 0.2200 / 0.2104 |
| Materials | exactly 2 distinct; ≥700 wood, ≥180 metal faces | 2 slots; 1094 / 190 |
| UVs | in 0..1, AABB overlap ≤ 1e-5 | in range, overlap 0 |
| Outer AABB | (1.558, 0.630, 0.574) m ± 0.015 | (1.5556, 0.6300, 0.5720), zmin 0 |
| Collider tris | ≤ 220 | 90 |
| Export | written, size > 0 | 187576 / 187576 / 187560 bytes |
Recomputed from the generated mesh, not asserted about the script.
-| Axis | Declared | Measured (all three) | | --- | --- | --- | | Non-manifold edges | 0 | 0 | | Loose verts / edges | 0 / 0 | 0 / 0 | | Doubles merged at 1e-5 | 0 | 0 | | Zero-area faces | 0 | 0 | | N-gons | 0 | 0 | | Coplanar disjoint face pairs | 0 | 0 | | Grounded: zmin | within 1e-4 of 0 | 0.0000 | | Named supports: 2 shoes + tyre | each zmin ≤ 1e-3 | shoe_z 0.00000, tyre_z 0.00000 | | Tray size | (0.720, 0.532, 0.220) m ± 0.02 | (0.7200, 0.5320, 0.2200) |
| Axis | Declared | Measured (all three) |
|---|---|---|
| Non-manifold edges | 0 | 0 |
| Loose verts / edges | 0 / 0 | 0 / 0 |
| Doubles merged at 1e-5 | 0 | 0 |
| Zero-area faces | 0 | 0 |
| N-gons | 0 | 0 |
| Coplanar disjoint face pairs | 0 | 0 |
Grounded: zmin | within 1e-4 of 0 | 0.0000 |
| Named supports: 2 shoes + tyre | each zmin ≤ 1e-3 | shoe_z 0.00000, tyre_z 0.00000 |
| Tray size | (0.720, 0.532, 0.220) m ± 0.02 | (0.7200, 0.5320, 0.2200) |
| Axis | Declared | Measured (all three) | | --- | --- | --- | | Spoke clearance (hub − spoke) | ≥ 0.010 m | 0.04000 | | Handle-tray overlap | gap ≤ 0.020 m | −0.07062 | | Wall-floor seat | ≥ 0.005 m | 0.01200 | | Metal-wood BVH gap | ≤ 0.010 m | 0.00000 | | Tread aspect (tyre width / radial) | ≥ 2.5 | 5.111 |
+| Axis | Declared | Measured (all three) |
|---|---|---|
| Spoke clearance (hub − spoke) | ≥ 0.010 m | 0.04000 |
| Handle-tray overlap | gap ≤ 0.020 m | −0.07062 |
| Wall-floor seat | ≥ 0.005 m | 0.01200 |
| Metal-wood BVH gap | ≤ 0.010 m | 0.00000 |
| Tread aspect (tyre width / radial) | ≥ 2.5 | 5.111 |
DECIMATE COLLAPSE triangle counts are not identical across series — 5.2.1 is leaner on LOD2. The gate is a ratio band, not an exact count. Bake pixels are stochastic; the gate is has_data plus operator FINISHED, not byte-identity. Construction uses no RNG. Export byte counts differ by 16 B on 5.2.1 (glTF serializer), not a gated axis.
Each violates one named budget. All eight were run on 4.5.11, 5.1.2 and 5.2.1 and returned the same code on each.
-| Flag | Budget violated | Exit | | --- | --- | --- | | --skip-decimate | LOD1 ratio band | 9 | | --stray-vert | loose vertex count is 0 | 15 | | --lift-z | bounding box zmin is 0 | 16 | | --short-legs | named shoe supports at Z=0 | 16 | | --fat-spokes | spoke clearance inside the hub | 17 | | --float-walls | wall-floor seat | 17 | | --pipe-rim | tread aspect (torus on a flat felloe) | 18 |
| Flag | Budget violated | Exit |
|---|---|---|
--skip-decimate | LOD1 ratio band | 9 |
--stray-vert | loose vertex count is 0 | 15 |
--lift-z | bounding box zmin is 0 | 16 |
--short-legs | named shoe supports at Z=0 | 16 |
--fat-spokes | spoke clearance inside the hub | 17 |
--float-walls | wall-floor seat | 17 |
--pipe-rim | tread aspect (torus on a flat felloe) | 18 |
blender --background --python wheelbarrow.py --
blender --background --python wheelbarrow.py -- --skip-decimate
@@ -288,7 +293,7 @@ Run
Smoke passes no flags.
Exit codes
File-local. 9 is a valid check code. 10 is reserved for gallery_framing.check_framing on the --output path. 15–19 are the hygiene and joint-fit family.
-| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Mesh did not build / no UV layer | | 4 | Base triangle count outside range | | 5 | Material count ≠ 2 distinct slots, or a face-count floor missed | | 6 | UVs outside 0..1 | | 7 | UV AABB overlap above tolerance | | 8 | World AABB off declared outer size | | 9 | LOD ratio band (--skip-decimate lands here) | | 10 | Framing gate (render path only) | | 11 | Collider triangle count above ceiling | | 12 | Bake did not finish or image has no data | | 13 | Export file missing or empty | | 14 | --output produced no file | | 15 | Mesh hygiene: loose, non-manifold, zero-area, doubles, n-gons, z-fight | | 16 | Not grounded: bounding box zmin off 0, or a named shoe/tyre floats | | 17 | Joint fit: spoke clearance, handle join, wall-floor seat, or metal-wood gap | | 18 | Seat: tread aspect of the tyre | | 19 | Tray size off the stated real-world dimensions |
+Code Meaning 0 Success 1 Uncaught exception (FATAL wrapper) 2 argparse / usage 3 Mesh did not build / no UV layer 4 Base triangle count outside range 5 Material count ≠ 2 distinct slots, or a face-count floor missed 6 UVs outside 0..1 7 UV AABB overlap above tolerance 8 World AABB off declared outer size 9 LOD ratio band (--skip-decimate lands here) 10 Framing gate (render path only) 11 Collider triangle count above ceiling 12 Bake did not finish or image has no data 13 Export file missing or empty 14 --output produced no file15 Mesh hygiene: loose, non-manifold, zero-area, doubles, n-gons, z-fight 16 Not grounded: bounding box zmin off 0, or a named shoe/tyre floats 17 Joint fit: spoke clearance, handle join, wall-floor seat, or metal-wood gap 18 Seat: tread aspect of the tyre 19 Tray size off the stated real-world dimensions
Composes skills mesh-editing-and-bmesh, bake-high-to-low, depsgraph-and-evaluated-data, engine-export-presets, and snippets bake_normal_high_to_low.py, setup_bake_target_image.py, lod_chain.py / decimate_to_budget.py, convex_hull_collider.py, export_preset_unity.py (helpers copied, not imported as a package).
Declared as named constants; every gate recomputes from the mesh, materials, UVs, evaluated LOD, collider, or export file.
-| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) | | --- | --- | --- | | Base triangles | 6000–8500 | 7216 / 7216 / 7216 | | LOD1 ratio | 0.32–0.62 of base | 0.5000 / 0.5000 / 0.5000 | | LOD2 ratio | 0.10–0.35 of base | 0.2198 / 0.2198 / 0.2198 | | Materials | exactly 2 distinct; ≥400 wood, ≥200 metal faces | 2 slots; 2512 / 1440 | | UVs | in 0..1, AABB overlap ≤ 1e-5 | in range, overlap 0 | | Outer AABB | (0.722, 0.714, 0.880) m ± 0.015 | (0.7220, 0.7142, 0.8800), zmin 0 | | Collider tris | ≤ 400 | 238 | | Export | written, size > 0 | 544140 / 544140 / 544124 bytes |
| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) |
|---|---|---|
| Base triangles | 6000–8500 | 7216 / 7216 / 7216 |
| LOD1 ratio | 0.32–0.62 of base | 0.5000 / 0.5000 / 0.5000 |
| LOD2 ratio | 0.10–0.35 of base | 0.2198 / 0.2198 / 0.2198 |
| Materials | exactly 2 distinct; ≥400 wood, ≥200 metal faces | 2 slots; 2512 / 1440 |
| UVs | in 0..1, AABB overlap ≤ 1e-5 | in range, overlap 0 |
| Outer AABB | (0.722, 0.714, 0.880) m ± 0.015 | (0.7220, 0.7142, 0.8800), zmin 0 |
| Collider tris | ≤ 400 | 238 |
| Export | written, size > 0 | 544140 / 544140 / 544124 bytes |
Base triangles rose from 4312 to 7216 in the quality pass: 12 identical staves with 5 mm daylight and circular hoops became 20 jittered staves, 8-ring bulge, chord-seated hoops, polygonal croze heads, and a bung.
DECIMATE COLLAPSE triangle counts are not identical across series — the gate is a ratio band, not an exact count. Bake pixels are stochastic; the gate is has_data plus operator FINISHED, not byte-identity. Stave-width jitter uses fixed seed 17. Export byte counts differ by 16 B on 5.2.1 (glTF serializer), not a gated axis.
Recomputed from the generated mesh, not asserted about the script.
-| Axis | Declared | Measured (all three) | | --- | --- | --- | | Non-manifold edges | 0 | 0 | | Loose verts / edges | 0 / 0 | 0 / 0 | | Doubles merged at 1e-5 | 0 | 0 | | Zero-area faces | 0 | 0 | | N-gons | 0 | 0 | | Coplanar disjoint face pairs | 0 | 0 | | Grounded: zmin | within 1e-4 of 0 | 0.0000 | | Named supports: 20 staves | each zmin ≤ 0.001 | 20, stave_z 0.00000 | | Body plan | 0.714 m dia × 0.880 m high ± 0.04 | 0.7142 × 0.8800 |
| Axis | Declared | Measured (all three) |
|---|---|---|
| Non-manifold edges | 0 | 0 |
| Loose verts / edges | 0 / 0 | 0 / 0 |
| Doubles merged at 1e-5 | 0 | 0 |
| Zero-area faces | 0 | 0 |
| N-gons | 0 | 0 |
| Coplanar disjoint face pairs | 0 | 0 |
Grounded: zmin | within 1e-4 of 0 | 0.0000 |
| Named supports: 20 staves | each zmin ≤ 0.001 | 20, stave_z 0.00000 |
| Body plan | 0.714 m dia × 0.880 m high ± 0.04 | 0.7142 × 0.8800 |
| Axis | Declared | Measured (all three) | | --- | --- | --- | | Head-croze rim BVH gap | ≤ 0.010 m | 0.00698 | | Hoop bite (host r − inner hoop r) | 0.0012–0.008 m | 0.00300 |
+| Axis | Declared | Measured (all three) |
|---|---|---|
| Head-croze rim BVH gap | ≤ 0.010 m | 0.00698 |
| Hoop bite (host r − inner hoop r) | 0.0012–0.008 m | 0.00300 |
Each violates one named budget. All six were run on 4.5.11, 5.1.2 and 5.2.1 and returned the same code on each.
-| Flag | Budget violated | Exit | | --- | --- | --- | | --skip-decimate | LOD1 ratio band | 9 | | --stray-vert | loose vertex count is 0 | 15 | | --lift-z | bounding box zmin is 0 | 16 | | --short-staves | named stave supports at Z=0 | 16 | | --float-head | head-croze gap | 17 | | --round-band | hoop bite band | 18 |
| Flag | Budget violated | Exit |
|---|---|---|
--skip-decimate | LOD1 ratio band | 9 |
--stray-vert | loose vertex count is 0 | 15 |
--lift-z | bounding box zmin is 0 | 16 |
--short-staves | named stave supports at Z=0 | 16 |
--float-head | head-croze gap | 17 |
--round-band | hoop bite band | 18 |
blender --background --python wooden_barrel.py --
blender --background --python wooden_barrel.py -- --skip-decimate
@@ -288,7 +293,7 @@ Run
Smoke passes no flags.
Exit codes
File-local. 9 is a valid check code. 10 is reserved for gallery_framing.check_framing on the --output path. 15–19 are the hygiene and joint-fit family.
-| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Mesh did not build / no UV layer | | 4 | Base triangle count outside range | | 5 | Material count ≠ 2 distinct slots, or a face-count floor missed | | 6 | UVs outside 0..1 | | 7 | UV AABB overlap above tolerance | | 8 | World AABB off declared outer size | | 9 | LOD ratio band (--skip-decimate lands here) | | 10 | Framing gate (render path only) | | 11 | Collider triangle count above ceiling | | 12 | Bake did not finish or image has no data | | 13 | Export file missing or empty | | 14 | --output produced no file | | 15 | Mesh hygiene: loose, non-manifold, zero-area, doubles, n-gons, z-fight | | 16 | Not grounded: bounding box zmin off 0, or a named stave floats | | 17 | Joint fit: head-croze gap (--float-head) | | 18 | Seat: hoop bite band (--round-band) | | 19 | Body diameter or height off the stated real-world size |
+Code Meaning 0 Success 1 Uncaught exception (FATAL wrapper) 2 argparse / usage 3 Mesh did not build / no UV layer 4 Base triangle count outside range 5 Material count ≠ 2 distinct slots, or a face-count floor missed 6 UVs outside 0..1 7 UV AABB overlap above tolerance 8 World AABB off declared outer size 9 LOD ratio band (--skip-decimate lands here) 10 Framing gate (render path only) 11 Collider triangle count above ceiling 12 Bake did not finish or image has no data 13 Export file missing or empty 14 --output produced no file15 Mesh hygiene: loose, non-manifold, zero-area, doubles, n-gons, z-fight 16 Not grounded: bounding box zmin off 0, or a named stave floats 17 Joint fit: head-croze gap (--float-head) 18 Seat: hoop bite band (--round-band) 19 Body diameter or height off the stated real-world size
Composes skills mesh-editing-and-bmesh, bake-high-to-low, depsgraph-and-evaluated-data, engine-export-presets, and snippets bake_normal_high_to_low.py, setup_bake_target_image.py, lod_chain.py / decimate_to_budget.py, convex_hull_collider.py, export_preset_unity.py (helpers copied, not imported as a package).
Declared as named constants; every gate recomputes from the mesh, materials, UVs, evaluated LOD, collider, or export file.
-| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) | | --- | --- | --- | | Base triangles | 4000–5800 | 4872 / 4872 / 4872 | | LOD1 ratio | 0.32–0.62 of base | 0.5000 / 0.5000 / 0.5000 | | LOD2 ratio | 0.10–0.35 of base | 0.2196 / 0.2196 / 0.2188 | | Materials | exactly 3 distinct; ≥350 wood, ≥180 metal, ≥40 rope | 3 slots; 1424 / 1020 / 208 | | UVs | in 0..1, AABB overlap ≤ 1e-5 | in range, overlap 0 | | Outer AABB | (0.383, 0.346, 0.527) m ± 0.015 | (0.3832, 0.3462, 0.5271), zmin 0 | | Collider tris | ≤ 420 | 302 | | Export | written, size > 0 | 378296 / 378296 / 378276 bytes |
| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) |
|---|---|---|
| Base triangles | 4000–5800 | 4872 / 4872 / 4872 |
| LOD1 ratio | 0.32–0.62 of base | 0.5000 / 0.5000 / 0.5000 |
| LOD2 ratio | 0.10–0.35 of base | 0.2196 / 0.2196 / 0.2188 |
| Materials | exactly 3 distinct; ≥350 wood, ≥180 metal, ≥40 rope | 3 slots; 1424 / 1020 / 208 |
| UVs | in 0..1, AABB overlap ≤ 1e-5 | in range, overlap 0 |
| Outer AABB | (0.383, 0.346, 0.527) m ± 0.015 | (0.3832, 0.3462, 0.5271), zmin 0 |
| Collider tris | ≤ 420 | 302 |
| Export | written, size > 0 | 378296 / 378296 / 378276 bytes |
Base triangles rose from 4272 to 4872 in the quality pass: overlapping identical staves, circular hoops, and a 12-box bail became 16 jittered staves, chord-seated hoops, a polygonal croze floor, and a round bail through the ear rings.
DECIMATE COLLAPSE triangle counts are not identical across series — 5.2.1 is 4 tris leaner on LOD2. The gate is a ratio band, not an exact count. Bake pixels are stochastic; the gate is has_data plus operator FINISHED, not byte-identity. Stave-width jitter uses fixed seed 17. Export byte counts differ by 20 B on 5.2.1 (glTF serializer), not a gated axis.
Recomputed from the generated mesh, not asserted about the script.
-| Axis | Declared | Measured (all three) | | --- | --- | --- | | Non-manifold edges | 0 | 0 | | Loose verts / edges | 0 / 0 | 0 / 0 | | Doubles merged at 1e-5 | 0 | 0 | | Zero-area faces | 0 | 0 | | N-gons | 0 | 0 | | Coplanar disjoint face pairs | 0 | 0 | | Grounded: zmin | within 1e-4 of 0 | 0.0000 | | Named supports: 16 staves | each zmin ≤ 0.001 | 16, stave_z 0.00000 | | Body plan | 0.335 m dia × 0.360 m high ± 0.04 | 0.3349 × 0.3600 |
| Axis | Declared | Measured (all three) |
|---|---|---|
| Non-manifold edges | 0 | 0 |
| Loose verts / edges | 0 / 0 | 0 / 0 |
| Doubles merged at 1e-5 | 0 | 0 |
| Zero-area faces | 0 | 0 |
| N-gons | 0 | 0 |
| Coplanar disjoint face pairs | 0 | 0 |
Grounded: zmin | within 1e-4 of 0 | 0.0000 |
| Named supports: 16 staves | each zmin ≤ 0.001 | 16, stave_z 0.00000 |
| Body plan | 0.335 m dia × 0.360 m high ± 0.04 | 0.3349 × 0.3600 |
| Axis | Declared | Measured (all three) | | --- | --- | --- | | Bail-ear BVH gap | ≤ 0.006 m | 0.00125 | | Floor-croze rim BVH gap | ≤ 0.008 m | 0.00458 | | Hoop bite (host r − inner hoop r) | 0.0010–0.007 m | 0.00250 |
+| Axis | Declared | Measured (all three) |
|---|---|---|
| Bail-ear BVH gap | ≤ 0.006 m | 0.00125 |
| Floor-croze rim BVH gap | ≤ 0.008 m | 0.00458 |
| Hoop bite (host r − inner hoop r) | 0.0010–0.007 m | 0.00250 |
Each violates one named budget. All seven were run on 4.5.11, 5.1.2 and 5.2.1 and returned the same code on each.
-| Flag | Budget violated | Exit | | --- | --- | --- | | --skip-decimate | LOD1 ratio band | 9 | | --stray-vert | loose vertex count is 0 | 15 | | --lift-z | bounding box zmin is 0 | 16 | | --short-staves | named stave supports at Z=0 | 16 | | --float-handle | bail-ear gap | 17 | | --float-bottom | floor-croze gap | 17 | | --round-band | hoop bite band | 18 |
| Flag | Budget violated | Exit |
|---|---|---|
--skip-decimate | LOD1 ratio band | 9 |
--stray-vert | loose vertex count is 0 | 15 |
--lift-z | bounding box zmin is 0 | 16 |
--short-staves | named stave supports at Z=0 | 16 |
--float-handle | bail-ear gap | 17 |
--float-bottom | floor-croze gap | 17 |
--round-band | hoop bite band | 18 |
blender --background --python wooden_bucket.py --
blender --background --python wooden_bucket.py -- --skip-decimate
@@ -289,7 +294,7 @@ Run
Smoke passes no flags.
Exit codes
File-local. 9 is a valid check code. 10 is reserved for gallery_framing.check_framing on the --output path. 15–19 are the hygiene and joint-fit family.
-| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Mesh did not build / no UV layer | | 4 | Base triangle count outside range | | 5 | Material count ≠ 3 distinct slots, or a face-count floor missed | | 6 | UVs outside 0..1 | | 7 | UV AABB overlap above tolerance | | 8 | World AABB off declared outer size | | 9 | LOD ratio band (--skip-decimate lands here) | | 10 | Framing gate (render path only) | | 11 | Collider triangle count above ceiling | | 12 | Bake did not finish or image has no data | | 13 | Export file missing or empty | | 14 | --output produced no file | | 15 | Mesh hygiene: loose, non-manifold, zero-area, doubles, n-gons, z-fight | | 16 | Not grounded: bounding box zmin off 0, or a named stave floats | | 17 | Joint fit: bail-ear gap or floor-croze gap | | 18 | Seat: hoop bite band (--round-band) | | 19 | Body diameter or height off the stated real-world size |
+Code Meaning 0 Success 1 Uncaught exception (FATAL wrapper) 2 argparse / usage 3 Mesh did not build / no UV layer 4 Base triangle count outside range 5 Material count ≠ 3 distinct slots, or a face-count floor missed 6 UVs outside 0..1 7 UV AABB overlap above tolerance 8 World AABB off declared outer size 9 LOD ratio band (--skip-decimate lands here) 10 Framing gate (render path only) 11 Collider triangle count above ceiling 12 Bake did not finish or image has no data 13 Export file missing or empty 14 --output produced no file15 Mesh hygiene: loose, non-manifold, zero-area, doubles, n-gons, z-fight 16 Not grounded: bounding box zmin off 0, or a named stave floats 17 Joint fit: bail-ear gap or floor-croze gap 18 Seat: hoop bite band (--round-band) 19 Body diameter or height off the stated real-world size
Composes skills mesh-editing-and-bmesh, bake-high-to-low, depsgraph-and-evaluated-data, engine-export-presets, and snippets bake_normal_high_to_low.py, setup_bake_target_image.py, lod_chain.py / decimate_to_budget.py, convex_hull_collider.py, export_preset_unity.py (helpers copied, not imported as a package). Hygiene combinatorics match examples/mesh-hygiene-audit (copied, not imported). Coplanar-pair counting matches showcase/signpost.
Declared as named constants; every gate recomputes from the mesh, materials, UVs, evaluated LOD, collider, or export file.
-| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) | | --- | --- | --- | | Base triangles | 1800–2800 | 2536 / 2536 / 2536 | | LOD1 ratio | 0.32–0.62 of base | 0.5000 / 0.5000 / 0.5000 | | LOD2 ratio | 0.10–0.35 of base | 0.2192 / 0.2192 / 0.2192 | | Materials | exactly 2 distinct, ≥280 wood, ≥180 metal | 2 slots, 612 wood, 736 metal | | UVs | in 0..1, AABB overlap ≤ 1e-5 | in range, overlap 0 | | Outer AABB | (0.490, 0.376, 1.487) m ± 0.01 | (0.4897, 0.3760, 1.4870) | | Grounded | abs(zmin) ≤ 1e-4, each sole at 0, rail end ≥ 0.012 m | zmin 0, sole 0, rail 0.02037 | | Mesh hygiene | non-manifold, loose v/e, doubles at 1e-5, zero-area, n-gons, coplanar pairs all 0 | all 0 | | Parts | 16 shells (2 stiles, 6 rungs, 2 sleeves, 2 cap sleeves, 2 plates, 2 soles) | 16 (2 / 6 / 2 / 2) | | Rung depth clearance | ≥ 0.003 m inside the stile faces | 0.00800 | | Tenon engagement | ≥ 0.008 m past the stile inner face | 0.01399 | | Tenon breakout margin | ≥ 0.008 m short of the stile outer face | 0.03780 | | Shoe bite / cover | bite ≥ 0.010 m, cover ≥ 0.016 m | 0.02000 / 0.03800 | | Collider tris | ≤ 180 | 24 | | Export | written, size > 0 | 187528 / 187528 / 187504 bytes |
| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) |
|---|---|---|
| Base triangles | 1800–2800 | 2536 / 2536 / 2536 |
| LOD1 ratio | 0.32–0.62 of base | 0.5000 / 0.5000 / 0.5000 |
| LOD2 ratio | 0.10–0.35 of base | 0.2192 / 0.2192 / 0.2192 |
| Materials | exactly 2 distinct, ≥280 wood, ≥180 metal | 2 slots, 612 wood, 736 metal |
| UVs | in 0..1, AABB overlap ≤ 1e-5 | in range, overlap 0 |
| Outer AABB | (0.490, 0.376, 1.487) m ± 0.01 | (0.4897, 0.3760, 1.4870) |
| Grounded | abs(zmin) ≤ 1e-4, each sole at 0, rail end ≥ 0.012 m | zmin 0, sole 0, rail 0.02037 |
| Mesh hygiene | non-manifold, loose v/e, doubles at 1e-5, zero-area, n-gons, coplanar pairs all 0 | all 0 |
| Parts | 16 shells (2 stiles, 6 rungs, 2 sleeves, 2 cap sleeves, 2 plates, 2 soles) | 16 (2 / 6 / 2 / 2) |
| Rung depth clearance | ≥ 0.003 m inside the stile faces | 0.00800 |
| Tenon engagement | ≥ 0.008 m past the stile inner face | 0.01399 |
| Tenon breakout margin | ≥ 0.008 m short of the stile outer face | 0.03780 |
| Shoe bite / cover | bite ≥ 0.010 m, cover ≥ 0.016 m | 0.02000 / 0.03800 |
| Collider tris | ≤ 180 | 24 |
| Export | written, size > 0 | 187528 / 187528 / 187504 bytes |
Base triangles rose from 936 to 2536. The previous mesh was two boxes, six cloned dowels, and four chrome cubes. Turned rungs (tenon and barrel in one shell) plus ferrule collars and chamfered soles added the triangles. Nothing was removed as a hidden face.
The joint budgets are measured per rung against each stile, from vertex positions in the un-raked construction frame, and the reported value is the worst case. Clearance is the tenon, not the barrel: the barrel is thicker only between the stiles.
DECIMATE COLLAPSE triangle counts are not identical across series — the gate is a ratio band, not an exact count. On this mesh the three binaries agreed. Bake pixels are stochastic; the gate is has_data plus operator FINISHED, not byte-identity. Construction uses no RNG. Export byte counts differ by 24 B on 5.2.1 (glTF serializer), not a gated axis. Euler is 24 (multi-body) and is not gated to 2.
Each falsifier violates exactly one named budget. Proven on Blender 4.5.11 LTS, 5.1.2, and 5.2.1 LTS (the binaries' own --version):
| Falsifier | Budget violated | Exit | Measured failure | | --- | --- | --- | --- | | --skip-decimate | LOD1 ratio band | 9 | ratio 1.0000 | | --stray-vert | Mesh hygiene, loose verts | 15 | loose_v=1 | | --twin-sole | Coplanar face pairs | 15 | zfight=6 | | --lift-z | Grounded zmin | 16 | zmin 0.050000 | | --fat-rungs | Rung depth clearance | 17 | clearance 0 | | --short-stile | Shoe bite / cover | 18 | bite 0.070, cover −0.012 |
| Falsifier | Budget violated | Exit | Measured failure |
|---|---|---|---|
--skip-decimate | LOD1 ratio band | 9 | ratio 1.0000 |
--stray-vert | Mesh hygiene, loose verts | 15 | loose_v=1 |
--twin-sole | Coplanar face pairs | 15 | zfight=6 |
--lift-z | Grounded zmin | 16 | zmin 0.050000 |
--fat-rungs | Rung depth clearance | 17 | clearance 0 |
--short-stile | Shoe bite / cover | 18 | bite 0.070, cover −0.012 |
Default exit is 0 on all three.
blender --background --python wooden_ladder.py --
@@ -284,7 +289,7 @@ Run
Smoke passes no flags: no --output, no falsifier.
Exit codes
File-local. 9 is a valid check code. 10 is reserved for gallery_framing.check_framing on the --output path.
-| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Mesh did not build / no UV layer | | 4 | Base triangle count outside range | | 5 | Material count ≠ 2 distinct slots, or wood/metal faces missing | | 6 | UVs outside 0..1 | | 7 | UV AABB overlap above tolerance | | 8 | World AABB off declared outer size | | 9 | LOD ratio band (--skip-decimate lands here) | | 10 | Framing gate (render path only) | | 11 | Collider triangle count above ceiling | | 12 | Bake did not finish or image has no data | | 13 | Export file missing or empty | | 14 | --output produced no file | | 15 | Mesh hygiene, including coplanar pairs (--stray-vert, --twin-sole) | | 16 | Grounded zmin, or a rail end sitting in the tread (--lift-z) | | 17 | Part count or rung-to-stile joint fit (--fat-rungs) | | 18 | Shoe bite / cover (--short-stile) |
+Code Meaning 0 Success 1 Uncaught exception (FATAL wrapper) 2 argparse / usage 3 Mesh did not build / no UV layer 4 Base triangle count outside range 5 Material count ≠ 2 distinct slots, or wood/metal faces missing 6 UVs outside 0..1 7 UV AABB overlap above tolerance 8 World AABB off declared outer size 9 LOD ratio band (--skip-decimate lands here) 10 Framing gate (render path only) 11 Collider triangle count above ceiling 12 Bake did not finish or image has no data 13 Export file missing or empty 14 --output produced no file15 Mesh hygiene, including coplanar pairs (--stray-vert, --twin-sole) 16 Grounded zmin, or a rail end sitting in the tread (--lift-z) 17 Part count or rung-to-stile joint fit (--fat-rungs) 18 Shoe bite / cover (--short-stile)
{html.escape(m.group(1))}")
elif m.group(2) is not None:
out.append(f"{render_inline(m.group(2), resolve)}")
+ elif m.group(3) is not None:
+ out.append(f"{render_inline(m.group(3), resolve)}")
else:
- href = html.escape(resolve(m.group(4)), quote=True)
- out.append(f'{render_inline(m.group(3), resolve)}')
+ # An inline image becomes a link carrying its alt text: the gallery
+ # only publishes the hero, so the image file itself lives on GitHub.
+ href = html.escape(resolve(m.group(6)), quote=True)
+ out.append(f'{render_inline(m.group(5), resolve)}')
pos = m.end()
out.append(html.escape(text[pos:]))
return "".join(out)
+def _split_row(row: str) -> list[str]:
+ """Split a pipe-table row on `|` outside backtick code spans."""
+ row = row.strip()
+ if row.startswith("|"):
+ row = row[1:]
+ if row.endswith("|") and not row.endswith("\\|"):
+ row = row[:-1]
+ cells, cur, in_code = [], [], False
+ for ch in row:
+ if ch == "`":
+ in_code = not in_code
+ if ch == "|" and not in_code:
+ cells.append("".join(cur).strip())
+ cur = []
+ else:
+ cur.append(ch)
+ cells.append("".join(cur).strip())
+ return cells
+
+
+def _starts_block(lines: list[str], i: int) -> bool:
+ s = lines[i].strip()
+ if s.startswith(("#", "- ", "```")) or _OL_ITEM.match(s):
+ return True
+ return (s.startswith("|") and i + 1 < len(lines)
+ and bool(_TABLE_SEP.match(lines[i + 1].strip())))
+
+
def md_to_html(text: str, resolve, skip_first_h1: bool = True) -> str:
lines = text.splitlines()
out: list[str] = []
@@ -704,28 +751,51 @@ def md_to_html(text: str, resolve, skip_first_h1: bool = True) -> str:
i += 1
continue
- if stripped.startswith("- "):
+ if _PREVIEW_IMAGE.match(stripped):
+ i += 1
+ continue
+
+ if (stripped.startswith("|") and i + 1 < len(lines)
+ and _TABLE_SEP.match(lines[i + 1].strip())):
+ head = _split_row(stripped)
+ i += 2 # header row + separator
+ rows: list[list[str]] = []
+ while i < len(lines) and lines[i].strip().startswith("|"):
+ rows.append(_split_row(lines[i]))
+ i += 1
+ ths = "".join(f'{render_inline(' '.join(para), resolve)}
") diff --git a/scripts/site/build_site.py b/scripts/site/build_site.py index bf5a7719..096375c8 100644 --- a/scripts/site/build_site.py +++ b/scripts/site/build_site.py @@ -46,21 +46,32 @@ def parse_frontmatter(text: str) -> tuple[dict[str, str], str]: return meta, body -def _extract_tools_from_frontmatter(lines: list[str]) -> list[str]: - """Parse a YAML list of tools from frontmatter lines (handles ``tools:`` key).""" - tools: list[str] = [] - in_tools = False +def _extract_frontmatter_list(lines: list[str], key: str) -> list[str]: + """Parse the YAML block list under ``key:`` from frontmatter lines. + + An inline scalar (``key: value``) comes back as a one-item list, so a + single-glob rule written without list syntax still reports its scope.""" + items: list[str] = [] + in_list = False for line in lines: stripped = line.strip() - if stripped.startswith("tools:"): - in_tools = True + if stripped.startswith(f"{key}:"): + inline = stripped[len(key) + 1:].strip().strip("\"'") + if inline: + return [inline] + in_list = True continue - if in_tools: + if in_list: if stripped.startswith("- "): - tools.append(stripped[2:].strip()) + items.append(stripped[2:].strip().strip("\"'")) else: break - return tools + return items + + +def _extract_tools_from_frontmatter(lines: list[str]) -> list[str]: + """Parse a YAML list of tools from frontmatter lines (handles ``tools:`` key).""" + return _extract_frontmatter_list(lines, "tools") def _extract_trigger_section(body: str) -> list[str]: @@ -148,26 +159,24 @@ def parse_rules(repo_root: Path) -> list[dict]: continue text = rule_file.read_text(encoding="utf-8", errors="replace") - lines = text.strip().splitlines() + meta, body = parse_frontmatter(text) + fm_lines = text[: len(text) - len(body)].splitlines() name = rule_file.stem.replace("-", " ").replace("_", " ").title() - description = "" - scope = "" + description = _truncate_words(meta.get("description", ""), 200) + if not description: + for line in body.splitlines(): + stripped = line.strip() + if stripped and not stripped.startswith("#"): + description = stripped[:200] + break - for line in lines: - stripped = line.strip() - if stripped.startswith("---"): - continue - if ":" in stripped and not description: - key, _, val = stripped.partition(":") - key_lower = key.strip().lower() - if key_lower == "description": - description = _truncate_words(val.strip(), 200) - elif key_lower in ("globs", "scope"): - scope = val.strip() - continue - if stripped and not description: - description = stripped[:200] + # `globs` is a YAML list in every shipped rule; the old line parser + # stopped at `description` (always the first key) and never saw it. + scope = ", ".join( + _extract_frontmatter_list(fm_lines, "globs") + or _extract_frontmatter_list(fm_lines, "scope") + ) results.append({ "name": name, @@ -400,6 +409,9 @@ def main(): "rule_count": len(rules), "examples": examples, "example_count": len(examples), + # gallery.json lists rendered examples only; plugin.json lists every + # shipped example, check-only ones included (the README's count). + "example_total": len(plugin.get("examples", [])) or len(examples), "featured_examples": featured, "featured_count": len(featured), "showcase": showcase, diff --git a/scripts/site/template.html.j2 b/scripts/site/template.html.j2 index 800dc76c..9e6f4b88 100644 --- a/scripts/site/template.html.j2 +++ b/scripts/site/template.html.j2 @@ -145,7 +145,7 @@ .render-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1rem; } .render-card { display: block; border: 1px solid var(--border); border-radius: 4px; overflow: hidden; background: var(--panel-2); transition: outline-color 0.12s, border-color 0.12s; outline: 2px solid transparent; outline-offset: -1px; } - .render-card:hover { border-color: var(--select); outline-color: var(--select); } + .render-card:hover, .render-card:focus-visible { border-color: var(--select); outline-color: var(--select); } .render-card img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } .render-meta { display: flex; align-items: baseline; gap: 0.75rem; padding: 0.6rem 0.9rem; } .render-meta .name { font-family: var(--font-mono); font-size: 0.8rem; color: var(--text); white-space: nowrap; } @@ -254,28 +254,28 @@Skills, rules, snippets, and starter templates that teach Cursor and Claude Code
the bpy that works —
- every pattern executed headless on Blender 4.5 LTS and 5.1 before it ships.
$ git clone {{ site.links.github | default(plugin.repository) }}
@@ -299,11 +299,11 @@
{% if featured_examples %}{{ featured_count }} of {% endif %}{{ example_count }} frames
These aren't mockups. Each example runs headless on Blender 4.5 LTS and 5.1 in the - smoke workflow, asserts its own correctness, and exits non-zero if the API drifted. The render is - what the code produced.
-Browse all {{ example_count }} examples and {{ showcase_count }} showcase pieces in the gallery →
+These aren't mockups. Each example runs headless on Blender 5.2 LTS and 4.5 LTS in the + smoke workflow, asserts its own correctness, and exits non-zero if the API drifted. Each still was + rendered by that same script.
+Showcase pieces compose shipped skills into a recognizable asset and assert declared budgets — triangle counts, materials, UVs, LODs, colliders, exports. They are not examples. A still that merely rendered is not an assertion.
-Open the gallery (tag: showcase) →
+Open the showcase in the gallery →
Each skill is the canonical pattern for one job — operators, panels, bmesh, geometry nodes, slotted actions — plus the mistakes AI assistants actually make there. Where - 4.5 LTS and 5.1 diverge, both code paths are shown.
+ 4.5 LTS and 5.x diverge, both code paths are shown.Always-on guardrails for the failure modes that make Blender Python look right - and run wrong: ops in loops, leaked bmesh, deprecated context dicts, per-vertex Python loops.
+Guardrails that load whenever a matching file is open, for the failure modes that make + Blender Python look right and run wrong: ops in loops, leaked bmesh, deprecated context dicts, per-vertex Python loops.
| Rule | Scope | Flags |
|---|