From 341d423f8daefa0b3ab723ef5ff102f35777e1d8 Mon Sep 17 00:00:00 2001 From: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> Date: Tue, 22 Sep 2026 19:18:52 -0400 Subject: [PATCH] fix(site): correct landing-page claims and render README tables on gallery pages A review of the live Pages site found the landing page naming Blender 5.1 as the primary target and calling every render a CI artifact, when CI checks 5.2 LTS and 4.5 LTS and renders only swatch-grid. The Rules table's Scope column was empty because the parser stopped at `description`, and render cards showed no keyboard focus ring. On the gallery, the README converter had no table, emphasis, ordered-list or image support, so all 182 budget tables printed as raw pipe text. - template: 5.2 LTS / 4.5 LTS wording, accurate render claim, visible focus on render cards, showcase CTA links to #tag=showcase, rules and templates copy match the repo, stats rendered server-side - build_site: parse `globs` YAML lists for the rule scope column; HUD example count includes check-only examples (plugin.json) - build_gallery: pipe tables, *emphasis*, ordered lists, inline images; drop the README's own preview image, which duplicates the hero Co-Authored-By: Claude Opus 5.5 (1M context) Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> --- docs/gallery/anvil/index.html | 17 ++-- docs/gallery/armature-bend/index.html | 11 ++- .../gallery/attribute-domain-shear/index.html | 11 ++- .../bake-normal-high-to-low/index.html | 9 +- docs/gallery/bmesh-gear/index.html | 9 +- docs/gallery/campfire/index.html | 17 ++-- docs/gallery/car-mirror-symmetry/index.html | 9 +- docs/gallery/cart/index.html | 15 ++- docs/gallery/chopping-block/index.html | 17 ++-- docs/gallery/collision-hull-proxy/index.html | 13 ++- docs/gallery/color-attribute-wheel/index.html | 11 ++- docs/gallery/compositor-glare/index.html | 9 +- docs/gallery/crate-stack/index.html | 20 ++-- .../cross-version-property-delete/index.html | 9 +- docs/gallery/curve-bevel-arc/index.html | 9 +- docs/gallery/custom-normals-shade/index.html | 11 ++- docs/gallery/damped-track-aim/index.html | 9 +- docs/gallery/degenerate-bevel-weld/index.html | 9 +- docs/gallery/depsgraph-export/index.html | 9 +- docs/gallery/driver-wave/index.html | 9 +- docs/gallery/export-preset-axis/index.html | 9 +- docs/gallery/fence-kit/index.html | 17 ++-- docs/gallery/gltf-export-roundtrip/index.html | 9 +- docs/gallery/gltf-skin-roundtrip/index.html | 9 +- docs/gallery/gn-instance-grid/index.html | 9 +- docs/gallery/gn-modifier-inputs/index.html | 11 ++- docs/gallery/gn-sdf-remesh/index.html | 9 +- docs/gallery/gn-socket-rename/index.html | 11 ++- docs/gallery/gn-zone-iterate/index.html | 9 +- docs/gallery/gp-lineart-contour/index.html | 9 +- docs/gallery/grease-pencil-rosette/index.html | 13 ++- docs/gallery/grindstone/index.html | 17 ++-- docs/gallery/hand-pump/index.html | 17 ++-- docs/gallery/hay-bale/index.html | 13 ++- docs/gallery/hitching-post/index.html | 13 ++- docs/gallery/image-pixels-testcard/index.html | 15 ++- docs/gallery/index.html | 7 +- docs/gallery/iron-cauldron/index.html | 17 ++-- docs/gallery/light-link-studio/index.html | 9 +- docs/gallery/lightmap-uv-channel/index.html | 11 ++- docs/gallery/lod-decimate-chain/index.html | 9 +- docs/gallery/market-stall/index.html | 17 ++-- docs/gallery/mesh-hygiene-audit/index.html | 11 ++- docs/gallery/modular-kit-snap/index.html | 11 ++- docs/gallery/parent-inverse-orrery/index.html | 11 ++- docs/gallery/park-bench/index.html | 17 ++-- docs/gallery/png-exr-alpha/index.html | 13 ++- docs/gallery/prop-origin-transform/index.html | 9 +- docs/gallery/shader-node-group/index.html | 9 +- docs/gallery/shape-key-blend/index.html | 9 +- docs/gallery/shipping-crate/index.html | 17 ++-- docs/gallery/signpost/index.html | 17 ++-- .../sky-texture-sun-elevation/index.html | 11 ++- docs/gallery/soccer-ball-goldberg/index.html | 9 +- docs/gallery/socket-attach-points/index.html | 17 ++-- docs/gallery/stone-archway/index.html | 22 +++-- docs/gallery/stone-well/index.html | 15 ++- docs/gallery/street-lantern/index.html | 17 ++-- docs/gallery/swatch-grid/index.html | 11 ++- docs/gallery/tavern-stool/index.html | 17 ++-- docs/gallery/temp-override-join/index.html | 9 +- docs/gallery/terrain-scatter/index.html | 15 ++- docs/gallery/text-version-stamp/index.html | 11 ++- docs/gallery/treasure-chest/index.html | 17 ++-- docs/gallery/triangulate-tangents/index.html | 9 +- docs/gallery/turntable/index.html | 9 +- .../usd-export-evaluation-mode/index.html | 9 +- docs/gallery/uv-layer-grid/index.html | 11 ++- docs/gallery/vertex-color-ao/index.html | 15 ++- docs/gallery/vertex-weight-limit/index.html | 9 +- docs/gallery/vse-cut-list/index.html | 17 ++-- docs/gallery/vse-gamma-cross/index.html | 9 +- docs/gallery/wall-torch/index.html | 17 ++-- docs/gallery/watchtower/index.html | 17 ++-- docs/gallery/water-trough/index.html | 17 ++-- docs/gallery/wave-displace/index.html | 11 ++- docs/gallery/wheelbarrow/index.html | 17 ++-- docs/gallery/wooden-barrel/index.html | 17 ++-- docs/gallery/wooden-bucket/index.html | 17 ++-- docs/gallery/wooden-ladder/index.html | 13 ++- scripts/build_gallery.py | 96 ++++++++++++++++--- scripts/site/build_site.py | 64 ++++++++----- scripts/site/template.html.j2 | 41 ++++---- 83 files changed, 841 insertions(+), 362 deletions(-) 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 @@

anvil

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).

Budgets

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 |

+
AxisDeclaredMeasured (4.5.11 / 5.1.2 / 5.2.1)
Base triangles2500–45002988 / 2988 / 2988
LOD1 ratio0.32–0.62 of base0.5000 / 0.5000 / 0.5000
LOD2 ratio0.10–0.35 of base0.2195 / 0.2195 / 0.2195
Materialsexactly 2 distinct; ≥400 wood, ≥400 metal faces2 slots; 1040 / 822
UVsin 0..1, AABB overlap ≤ 1e-5in range, overlap 0
Outer AABB(0.621, 0.414, 0.489) m ± 0.015(0.6210, 0.4137, 0.4890), zmin 0
Collider tris≤ 280134
Exportwritten, size > 0244052 / 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.

Hygiene

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 |

+
AxisDeclaredMeasured (all three)
Non-manifold edges00
Loose verts / edges0 / 00 / 0
Doubles merged at 1e-500
Zero-area faces00
N-gons00
Coplanar disjoint face pairs00
Grounded: zminwithin 1e-4 of 00.0000
Named supports: 16 staveseach zmin ≤ 0.00116, stave_z 0.00000
Body plan0.621 m long × 0.180 m high ± 0.050.6210 × 0.1800

Joint fit and seat

-

| 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 |

+
AxisDeclaredMeasured (all three)
Foot-on-head BVH gap≤ 0.006 m0.00300
Hoop bite (host r − inner hoop r)0.0012–0.008 m0.00300

Falsifiers

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 |

+
FlagBudget violatedExit
--skip-decimateLOD1 ratio band9
--stray-vertloose vertex count is 015
--lift-zbounding box zmin is 016
--short-stavesnamed stave supports at Z=016
--float-anvilfoot-on-head gap17
--round-bandhoop bite band18

Run

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 |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build / no UV layer
4Base triangle count outside range
5Material count ≠ 2 distinct slots, or a face-count floor missed
6UVs outside 0..1
7UV AABB overlap above tolerance
8World AABB off declared outer size
9LOD ratio band (--skip-decimate lands here)
10Framing gate (render path only)
11Collider triangle count above ceiling
12Bake did not finish or image has no data
13Export file missing or empty
14--output produced no file
15Mesh hygiene: loose, non-manifold, zero-area, doubles, n-gons, z-fight
16Not grounded: bounding box zmin off 0, or a named stave floats
17Joint fit: foot-on-head gap (--float-anvil)
18Seat: hoop bite band (--round-band)
19Body 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.

+
  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.

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

Exit codes

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 |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3edit_bones populated in object mode
4Edit-mode bone chain off closed form
5Evaluated vertex count changed
6Evaluated mesh off closed-form LBS
7Root ring moved
8Tip 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.

diff --git a/docs/gallery/attribute-domain-shear/index.html b/docs/gallery/attribute-domain-shear/index.html index 0e86c596..1c4ca6a8 100644 --- a/docs/gallery/attribute-domain-shear/index.html +++ b/docs/gallery/attribute-domain-shear/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,7 +270,7 @@

attribute-domain-shear

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.

Run

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

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 | 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 |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3CORNER or POINT attribute size wrong
4CORNER hub corners off wedge color
5POINT hub is not last-write (--no-overwrite lands here)
6Outer ring verts off last-write order
7Measured shear off palette closed form, or ~0
9--output produced no file
10Gallery 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.

diff --git a/docs/gallery/bake-normal-high-to-low/index.html b/docs/gallery/bake-normal-high-to-low/index.html index 64d99fe8..08c3a996 100644 --- a/docs/gallery/bake-normal-high-to-low/index.html +++ b/docs/gallery/bake-normal-high-to-low/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); @@ -278,7 +283,7 @@

Run

blender --background --python bake_normal_high_to_low.py -- --output hatch.png --engine cycles

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 | 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 |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Missing UV layer on the target
4Bake did not FINISHED or image has_data is false
5Detail deviant-pixel fraction below 0.40 (--flat-source lands here)
6Detail MAD below 0.05
7Flat control above 0.05 frac / 0.03 MAD
8Monotonic gap below 0.30
9--output produced no file
10Gallery 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.

diff --git a/docs/gallery/bmesh-gear/index.html b/docs/gallery/bmesh-gear/index.html index 099f8964..51f25143 100644 --- a/docs/gallery/bmesh-gear/index.html +++ b/docs/gallery/bmesh-gear/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); @@ -273,7 +278,7 @@

Run

blender --background --python bmesh_gear.py -- --output gear.png --engine cycles

Exit codes

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 |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Topology ≠ closed form (--no-extrude lands here)
4Non-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.

diff --git a/docs/gallery/campfire/index.html b/docs/gallery/campfire/index.html index e6902a0d..97a29401 100644 --- a/docs/gallery/campfire/index.html +++ b/docs/gallery/campfire/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 @@

campfire

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).

Budgets

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 |

+
AxisDeclaredMeasured (4.5.11 / 5.1.2 / 5.2.1)
Base triangles1400–36002200 / 2200 / 2200
LOD1 ratio0.32–0.62 of base0.5000 / 0.5000 / 0.5000
LOD2 ratio0.10–0.35 of base0.2200 / 0.2200 / 0.2200
Materialsexactly 3 distinct; ≥80 wood, ≥24 ash, ≥200 stone3 slots; 216 / 48 / 1304
UVsin 0..1, AABB overlap ≤ 1e-5in range, overlap 0
Outer AABB(0.807, 0.808, 0.404) m ± 0.015(0.8069, 0.8079, 0.4037), zmin 0
Collider tris≤ 360198
Exportwritten, size > 0206244 / 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.

Hygiene

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 |

+
AxisDeclaredMeasured (all three)
Non-manifold edges00
Loose verts / edges0 / 00 / 0
Doubles merged at 1e-500
Zero-area faces00
N-gons00
Coplanar disjoint face pairs00
Grounded: zminwithin 1e-4 of 00.0000
Named supports: 14 bottom stoneseach zmin ≤ 0.00114, stone_z 0.00000
Ring plan0.807 m dia × 0.156 m high ± 0.040.8074 × 0.1560

Joint fit and seat

-

| 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 |

+
AxisDeclaredMeasured (all three)
Teepee kiss (BVH nearest at caps)≤ 0.008 m0.00001
Course seat (upper zmin − lower zmax)abs ≤ 0.004 m0.00000

Falsifiers

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 |

+
FlagBudget violatedExit
--skip-decimateLOD1 ratio band9
--stray-vertloose vertex count is 015
--lift-zbounding box zmin is 016
--short-stonesnamed bottom-course supports at Z=016
--float-logsteepee kiss17
--gap-coursescourse seat18

Run

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 |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build / no UV layer
4Base triangle count outside range
5Material count ≠ 3 distinct slots, or a face-count floor missed
6UVs outside 0..1
7UV AABB overlap above tolerance
8World AABB off declared outer size
9LOD ratio band (--skip-decimate lands here)
10Framing gate (render path only)
11Collider triangle count above ceiling
12Bake did not finish or image has no data
13Export file missing or empty
14--output produced no file
15Mesh hygiene: loose, non-manifold, zero-area, doubles, n-gons, z-fight
16Not grounded: bounding box zmin off 0, or a named bottom stone floats
17Joint fit: teepee kiss (--float-logs)
18Seat: course gap (--gap-courses)
19Ring diameter or height off the stated real-world size

Source

diff --git a/docs/gallery/car-mirror-symmetry/index.html b/docs/gallery/car-mirror-symmetry/index.html index c5fb4659..428ef85a 100644 --- a/docs/gallery/car-mirror-symmetry/index.html +++ b/docs/gallery/car-mirror-symmetry/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); @@ -277,7 +282,7 @@

Run

blender --background --python car_mirror_symmetry.py -- --output car.png --engine cycles

Exit codes

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 |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Body datablock is not the authored half
4Authored centerline vert count ≠ 28
5Evaluated verts ≠ 2n − c (--no-mirror lands here)
6Evaluated on-plane verts ≠ centerline; also --output produced no file
7Evaluated Euler characteristic ≠ 2
8Non-manifold edges in the evaluated shell
9Evaluated verts lack a mirrored partner
10Mirror partner deviation above tolerance
11Evaluated bbox not symmetric about X
12Mirrored-part origin off the plane
13Mirrored-part datablock is not the authored half
14Mirrored-part evaluated counts did not double
15Mirrored-part partner check failed
16Mirrored-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.

diff --git a/docs/gallery/cart/index.html b/docs/gallery/cart/index.html index 3a93f29d..12a6b136 100644 --- a/docs/gallery/cart/index.html +++ b/docs/gallery/cart/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); @@ -264,11 +269,11 @@

cart

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).

Budgets

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 |

+
AxisDeclaredMeasured (4.5.11 / 5.1.2 / 5.2.1)
Base triangles2760–29602856 / 2856 / 2856
LOD1 ratio0.32–0.62 of base0.5000 / 0.5000 / 0.5000
LOD2 ratio0.10–0.35 of base0.2199 / 0.2199 / 0.2157
Materialsexactly 2 distinct, metal ≥ 24, wood ≥ 800 faces2 slots, floors met
UVsin 0..1, AABB overlap ≤ 1e-5in range, overlap 0
Outer AABB(1.539, 0.749, 0.640) m ± 0.01(1.5393, 0.7486, 0.6400)
Groundedbbox min Z within 1e-4 of 00.0000 / 0.0000 / 0.0000
Named supportseach tyre's own zmin within 1e-4 of the floor+Y 0.000000, −Y 0.000000
Hygieneloose V/E, non-manifold, zero-area, doubles @1e-5, n-gons: all 00 / 0 / 0 on every axis
Z-fightingcoplanar cross-shell face pairs = 00 / 0 / 0
Tyre seat0.0030–0.0055 m interference, per angular station[0.00400, 0.00400] over 48 stations
Wheel mirrorthe two wheels match in X, Z and extent within 5e-5 m0.00000 mm
Material-island gapmetal↔wood min distance ≤ 0.008 m0.00000 / 0.00000 / 0.00000
Collider tris≤ 360154
Exportwritten, size > 0, removed after measuring221960 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.

Falsifiers

-

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.

+
FlagTarget budgetExit
--skip-decimateLOD1 ratio band (ratio becomes 1.0)9
--lift-zAABB grounded zmin (whole mesh up 0.05 m)16
--flush-tyreCoplanar 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-tyreTyre seat band — buries the hoop 9 mm into the felloe so it reads as one body18
--float-wheelNamed supports — lifts one wheel 3 mm while the other still grounds the AABB16
--skew-wheelWheel mirror — pushes one wheel 6 mm out along the track19

--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.

Run

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) |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build / no UV layer
4Base triangle count outside range
5Material count ≠ 2 distinct slots, or metal faces missing
6UVs outside 0..1
7UV AABB overlap above tolerance
8World AABB off declared outer size
9LOD ratio band (--skip-decimate lands here)
10Framing gate (render path only)
11Collider triangle count above ceiling
12Bake did not finish or image has no data
13Export file missing or empty
14--output produced no file
15Hygiene: loose geometry, non-manifold, zero-area, doubles, n-gons, or coplanar cross-shell face pairs (--flush-tyre lands here)
16Bbox min Z not grounded (--lift-z), or a named support off the floor (--float-wheel)
17Material-island gap above tolerance (parts meant to touch)
18Tyre seat depth outside its band (--sink-tyre lands here)
19Wheel mirror deviation above epsilon (--skew-wheel lands here)

Source

diff --git a/docs/gallery/chopping-block/index.html b/docs/gallery/chopping-block/index.html index ed708a66..f66bc77f 100644 --- a/docs/gallery/chopping-block/index.html +++ b/docs/gallery/chopping-block/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,14 +270,14 @@

chopping-block

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).

Budgets

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 |

+
AxisDeclaredMeasured (4.5.11 / 5.1.2 / 5.2.1)
Base triangles1730–18701800 / 1800 / 1800
LOD1 ratio0.32–0.62 of base0.5000 / 0.5000 / 0.5000
LOD2 ratio0.10–0.35 of base0.2200 / 0.2200 / 0.2200
Materialsexactly 4 distinct; ≥160 bark, ≥90 grain, ≥170 metal, ≥150 haft faces4 slots; 180 / 216 / 404 / 176
UVsin 0..1, AABB overlap ≤ 1e-5in range, overlap 0
Outer AABB(0.526, 0.526, 1.006) m ± 0.012(0.5264, 0.5264, 1.0064)
Collider tris≤ 560538
Exportwritten, size > 0180588 / 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 %.

Hygiene

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 |

+
AxisDeclaredMeasured (all three)
Non-manifold edges00
Loose verts / edges0 / 00 / 0
Doubles merged at 1e-500
Zero-area faces00
N-gons00
Coplanar disjoint face pairs00
Grounded: zminwithin 1e-4 of 00.0000
Log axis out of plumb≤ 1e-50.0000000
Log size0.535 × 0.360 m ± 0.030 / ± 0.0100.5464 × 0.3600
Haft section, wide over thick at the grip1.25–1.601.3500

Joint fit

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 |

+
AxisDeclaredMeasured (all three)
Shell countexactly 66
Haft clearance inside the eye≥ 0.006 m0.01800
Haft engagement through the eye≥ 0.020 m0.08225
Haft breakout margin below the head≥ 0.006 m0.02175
Bit bury below the sawn top≥ 0.030 m0.07871
Bit inset from the rim≥ 0.030 m0.14647
Hoop bite into the log, every segment0.002–0.007 m0.00371–0.00422
Haft clearance above the log≥ 0.015 m, 0 verts inside0.07531, 0
Rivet seat into the hoop, each rivet0.0008–0.0025 m0.00150, 0.00154
Rivet dome proud of the hoop, each rivet≥ 0.0025 m0.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.

@@ -281,7 +286,7 @@

Shading and materials

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.

Falsifiers

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 |

+
FlagBudget violatedExit
--skip-decimateLOD1 ratio band9
--stray-vertloose vertex count is 015
--lift-zbounding box zmin is 016
--fat-hafthaft clearance inside the eye17
--round-bandhoop bite into the log18
--float-rivetsrivet seat into the hoop18
--round-hafthaft section wide over thick19

--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.

Run

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 |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build / no UV layer
4Base triangle count outside range
5Material count ≠ 4 distinct slots, or a face-count floor missed
6UVs outside 0..1
7UV AABB overlap above tolerance
8World AABB off declared outer size
9LOD ratio band (--skip-decimate lands here)
10Framing gate (render path only)
11Collider triangle count above ceiling
12Bake did not finish or image has no data
13Export file missing or empty
14--output produced no file
15Mesh hygiene: loose, non-manifold, zero-area, doubles, n-gons, z-fight
16Not grounded: bounding box zmin off 0
17Joint fit: shell count, eye clearance, engagement, breakout, bury, inset
18Contact fit: hoop bite band, haft fouling the log, or a rivet off its seat band
19Log out of plumb, off its stated real-world size, or a round haft section

Source

diff --git a/docs/gallery/collision-hull-proxy/index.html b/docs/gallery/collision-hull-proxy/index.html index a8db299c..a0854696 100644 --- a/docs/gallery/collision-hull-proxy/index.html +++ b/docs/gallery/collision-hull-proxy/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); @@ -262,8 +267,8 @@

collision-hull-proxy

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:

-
  • Containment. Every render-mesh vertex lies on the inner side of every face plane of its piece's hull — max excursion 4.4e-08 (tol 2e-4). This holds by construction, not luck: the cage is a coarse lathe whose rings are inflated by 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.
  • Convexity. The same plane test restricted to the hull's own vertices (4.8e-08) — what a convex solver assumes when it uploads the piece.
  • Watertight + manifold. Every hull edge borders exactly two faces, and V − E + F = 2: a convex hull is a topological sphere.
  • Outward winding. Positive signed volume by the divergence theorem (body piece 1.022381) — inverted collision is a physics no-op.
  • Budget. Every piece ≤ 255 faces: body 70, pumper 60, side caps 60+60; the compound totals 250. The naive approach fails this — a hull of the *dense render mesh* measures 380 faces, over budget, which is why real pipelines hull a cage, never the render mesh.
+

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:

+
  • Containment. Every render-mesh vertex lies on the inner side of every face plane of its piece's hull — max excursion 4.4e-08 (tol 2e-4). This holds by construction, not luck: the cage is a coarse lathe whose rings are inflated by 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.
  • Convexity. The same plane test restricted to the hull's own vertices (4.8e-08) — what a convex solver assumes when it uploads the piece.
  • Watertight + manifold. Every hull edge borders exactly two faces, and V − E + F = 2: a convex hull is a topological sphere.
  • Outward winding. Positive signed volume by the divergence theorem (body piece 1.022381) — inverted collision is a physics no-op.
  • Budget. Every piece ≤ 255 faces: body 70, pumper 60, side caps 60+60; the compound totals 250. The naive approach fails this — a hull of the dense render mesh measures 380 faces, over budget, which is why real pipelines hull a cage, never the render mesh.

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 @@

Run

blender --background --python collision_hull_proxy.py -- --output hydrant.png --engine cycles

Exit codes

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 |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Render vertex escapes its hull (--shrink-hull lands here)
4Hull edge does not border exactly two faces
5Signed volume ≤ 0 (inverted winding)
6Hull vertex off its own face plane
7Euler characteristic ≠ 2
8Piece 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.

diff --git a/docs/gallery/color-attribute-wheel/index.html b/docs/gallery/color-attribute-wheel/index.html index 3899b06b..65793aab 100644 --- a/docs/gallery/color-attribute-wheel/index.html +++ b/docs/gallery/color-attribute-wheel/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); @@ -260,7 +265,7 @@

color-attribute-wheel

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.

Run

# 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

Exit codes

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 |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Topology ≠ closed form
4Color attribute missing
5Domain/type ≠ CORNER/FLOAT_COLOR (--point-domain lands here)
6Attribute sized to verts, not loops
7active_color not set
8Probe 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.

diff --git a/docs/gallery/compositor-glare/index.html b/docs/gallery/compositor-glare/index.html index 6f1cd8d8..0e1378c8 100644 --- a/docs/gallery/compositor-glare/index.html +++ b/docs/gallery/compositor-glare/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); @@ -274,7 +279,7 @@

Run

blender --background --python compositor_glare.py -- --output rings.png --engine cycles

Exit codes

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 |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Compositor tree / Glare / Threshold / link-chain contract (--threshold-high lands here)
4Check render missing, tube dark, no halo, or halo does not fall off
5Halo 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.

diff --git a/docs/gallery/crate-stack/index.html b/docs/gallery/crate-stack/index.html index fbe4d3ce..0502f579 100644 --- a/docs/gallery/crate-stack/index.html +++ b/docs/gallery/crate-stack/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); @@ -259,21 +264,20 @@

crate-stack

-

!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.

What it composes

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 contentUsed for
skills/mesh-editing-and-bmeshbox, bevel and UV construction in one bmesh
skills/procedural-materials-and-shaderstwo Principled materials with noise-driven wear
skills/bake-high-to-lowCycles tangent-space normal bake, high onto low
skills/engine-export-presetsUnity glTF (export_yup=True)
skills/depsgraph-and-evaluated-dataevaluated triangle counts for the LOD ratios
snippets/decimate_to_budget.pyLOD1 / LOD2 COLLAPSE chain
snippets/convex_hull_collider.pyconvex collider
snippets/lod_chain.pyLOD naming and ratio pattern
examples/mesh-hygiene-audithygiene combinatorics (copied, not imported)

Reuse with variation

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.

Budgets

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 |

+
BudgetBandMeasured
Base triangles3200–72004380
LOD1 ratio0.32–0.620.5000
LOD2 ratio0.10–0.350.2005 (5.2) / 0.2199 (4.5, 5.1)
Material slotsexactly 2, distinct2
Iron faces≥ 120216
Timber faces≥ 6002436
UV boundsinside 0..1(0.0008, 0.0008)–(0.9992, 0.9992)
UV AABB overlap≤ 1e-50.000000
Outer AABB0.658 × 0.524 × 0.704 m ± 0.0200.6582 × 0.5235 × 0.7040
Crate body footprint0.585 × 0.425 m ± 0.015, in the crate's own frame0.5846 × 0.4246 (all three)
Collider triangles≤ 260202
Normal bake{'FINISHED'} with image data{'FINISHED'}, has_data=True
glTF exportfile written, non-empty~340 kB
Hygieneall zeroloose 0/0, non-manifold 0, zero-area 0, doubles 0, n-gons 0, coplanar disjoint pairs 0
Grounded AABB\zmin\≤ 1e-40.00000
Ground runners3 named supports, each zmin ≤ 1e-33 at 0.00000
Crate-to-crate seatboth seats, surface gap ≤ 0.0015 m0.00000 (overlapping)
Yaw bandeach \yaw\in 0.040–0.192 rad0.0681, 0.0943, 0.1093
Per-instance variationyaw spread ≥ 0.020 rad, plank spread ≥ 0.0008 m0.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.

Why the footprint is measured in the crate's own frame

-

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.

Why parts are classified by principal axes

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.

Why stack levels come from the runners

@@ -282,10 +286,10 @@

Determinism

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.

Falsifiers

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 |

+
FlagBreaksExit
--skip-decimatedrops the DECIMATE modifiers, LOD1 ratio goes to 1.00009
--stray-vertadds one loose vertex inside the silhouette, so hygiene catches it rather than the bounding box15
--lift-zlifts the whole mesh 50 mm off the floor16
--short-skidsfloats one of the three ground runners 12 mm; the other two still ground the AABB, so only the named-support budget sees it16
--float-stacklifts the top crate 9 mm clear of the lid below, opening a 34 mm seat gap18
--same-seedgives all three crates the same design seed; yaw spread and plank spread both go to 020

Exit codes

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) |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build, or has no UV layer
4Base triangle count outside band
5Material slots, or a material's face floor
6UVs outside 0..1
7UV AABB overlap above tolerance
8Outer AABB off declared size
9LOD1 or LOD2 ratio outside band (--skip-decimate)
10Framing gate (examples/gallery_framing.py, render path only)
11Collider triangles above ceiling
12Normal bake failed or produced no image data
13glTF export missing or empty
14--output produced no file
15Mesh hygiene (--stray-vert)
16Grounded zmin, or a named ground runner floating (--lift-z, --short-skids)
18Crate-to-crate seat gap (--float-stack)
19Crate body footprint off declared size
20Per-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.

Run it

# 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.

Cross-version measurements

-

| 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 |

+
Value4.5.115.1.25.2.1
Base triangles438043804380
LOD1 tris / ratio2190 / 0.50002190 / 0.50002190 / 0.5000
LOD2 tris / ratio934 / 0.2199934 / 0.2199878 / 0.2005
Outer AABB0.6582 × 0.5235 × 0.7040samesame
Collider tris202202202
Yaws (rad)0.0943, −0.1093, 0.0681samesame
Seat gap0.000000.000000.00000
Check wall-clock~0.99 s~1.01 s~1.20 s

Source

diff --git a/docs/gallery/cross-version-property-delete/index.html b/docs/gallery/cross-version-property-delete/index.html index eb69bab3..c245a70d 100644 --- a/docs/gallery/cross-version-property-delete/index.html +++ b/docs/gallery/cross-version-property-delete/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); @@ -271,7 +276,7 @@

Run

The --output render path measures framing against the Layer 1 band via examples/gallery_framing.py (exit 10 on violation) before writing the still.

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; 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 |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage; also an active object existed after the data-API build
3Custom ID property did not land
4property_unset on a custom ID key did not TypeError, or it deleted the key
5del did not report removal
6Keep plate lost the ID property
7Clear plate still has the ID property (--skip-delete / --unset-instead land here)
10Gallery 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.

diff --git a/docs/gallery/curve-bevel-arc/index.html b/docs/gallery/curve-bevel-arc/index.html index f383c480..7f11fcd5 100644 --- a/docs/gallery/curve-bevel-arc/index.html +++ b/docs/gallery/curve-bevel-arc/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); @@ -273,7 +278,7 @@

Run

blender --background --python curve_bevel_arc.py -- --output arc.png --engine cycles

Exit codes

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 |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Spline type is not BEZIER
4Bezier point count ≠ 8
5bevel_depth ≠ 0.15
6use_fill_caps is False (--no-caps lands here)
7Evaluated vert/face count off measured tessellation
8Tube does not rest on the floor
9Tube height ≠ 2 × bevel
10X 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.

diff --git a/docs/gallery/custom-normals-shade/index.html b/docs/gallery/custom-normals-shade/index.html index 9d2d63c0..a1c1698c 100644 --- a/docs/gallery/custom-normals-shade/index.html +++ b/docs/gallery/custom-normals-shade/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); @@ -263,7 +268,7 @@

custom-normals-shade

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:

-
  • The legacy shading API is gone — on both supported versions. 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.
  • Shade-by-angle is exact. 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.
  • The evaluated shading matches the attribute's promise. Through depsgraph evaluation, loop normals across a smooth edge are welded (deviation 0.0, tol 1e-3) and loop normals across a sharp edge carry their face normals split by exactly the dihedral (angle error 0.0 rad, tol 5e-3), all unit length (err 5.6e-08).
  • Custom split normals survive depsgraph evaluation — with a quantization budget, not float precision: 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.
  • The divergence: the legacy 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.
+
  • The legacy shading API is gone — on both supported versions. 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.
  • Shade-by-angle is exact. 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.
  • The evaluated shading matches the attribute's promise. Through depsgraph evaluation, loop normals across a smooth edge are welded (deviation 0.0, tol 1e-3) and loop normals across a sharp edge carry their face normals split by exactly the dihedral (angle error 0.0 rad, tol 5e-3), all unit length (err 5.6e-08).
  • Custom split normals survive depsgraph evaluation — with a quantization budget, not float precision: 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.
  • The divergence: the legacy 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 @@

Run

blender --background --python custom_normals_shade.py -- --output cans.png --engine cycles

Exit codes

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 |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Legacy shading API present, or modern path missing
4Non-manifold edges (dihedral test undefined)
5Sharp set ≠ independent dihedral (--mismatch-angle lands here)
6Evaluated loop normals not welded/split as the sharp set promises
7Custom split normals lost or dequantized in evaluation
8shade_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.

diff --git a/docs/gallery/damped-track-aim/index.html b/docs/gallery/damped-track-aim/index.html index 2d232f8d..2a34c345 100644 --- a/docs/gallery/damped-track-aim/index.html +++ b/docs/gallery/damped-track-aim/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); @@ -275,7 +280,7 @@

Run

blender --background --python damped_track_aim.py -- --output aim.png --engine cycles

Exit codes

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 |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Needle count ≠ 12
4Needle does not carry exactly one DAMPED_TRACK
5Constraint target is not Core
6track_axis is not TRACK_Z
7Constraint muted or influence < 1 (--mute lands here)
8TRACK_TO still present
9Evaluated 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.

diff --git a/docs/gallery/degenerate-bevel-weld/index.html b/docs/gallery/degenerate-bevel-weld/index.html index fe5f8d65..59f33028 100644 --- a/docs/gallery/degenerate-bevel-weld/index.html +++ b/docs/gallery/degenerate-bevel-weld/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); @@ -273,7 +278,7 @@

Run

blender --background --python degenerate_bevel_weld.py -- --output bevel.png --engine cycles

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 | 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 |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Safe bevel produced zero-area faces
4Degenerate bevel zero-area count ≠ closed form (--both-safe lands here)
5min_area collapse under 1e5×
6Coincident-position count off the closed form
7Safe GLB carries degenerate triangles
8Degenerate GLB triangle or position count drifted
9--output produced no file
10Gallery 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.

diff --git a/docs/gallery/depsgraph-export/index.html b/docs/gallery/depsgraph-export/index.html index 2cc6e638..a9102d14 100644 --- a/docs/gallery/depsgraph-export/index.html +++ b/docs/gallery/depsgraph-export/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); @@ -276,7 +281,7 @@

Run

blender --background --python depsgraph_export.py -- --obj exported.obj

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 | 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 |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Evaluated mesh did not apply the modifier
4No OBJ written
5Export vert count ≠ evaluated (--unevaluated lands here)
6--output produced no file
10Gallery 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.

diff --git a/docs/gallery/driver-wave/index.html b/docs/gallery/driver-wave/index.html index 659bcc23..3a354ef6 100644 --- a/docs/gallery/driver-wave/index.html +++ b/docs/gallery/driver-wave/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); @@ -274,7 +279,7 @@

Run

blender --background --python driver_wave.py -- --output driver.png --engine cycles

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 | Evaluated Z scale ≠ wave_scale (--flat-expr lands here) | | 4 | Original datablock was not flushed | | 6 | --output produced no file | | 10 | Gallery framing violation |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Evaluated Z scale ≠ wave_scale (--flat-expr lands here)
4Original datablock was not flushed
6--output produced no file
10Gallery 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.

diff --git a/docs/gallery/export-preset-axis/index.html b/docs/gallery/export-preset-axis/index.html index ffbdc000..73c586c8 100644 --- a/docs/gallery/export-preset-axis/index.html +++ b/docs/gallery/export-preset-axis/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); @@ -276,7 +281,7 @@

Run

blender --background --python export_preset_axis.py -- --output beacon.png --engine cycles

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; 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 |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage; also exporter RNA missing expected glTF kwargs
3Source mast is not Z-dominant, or tip drifted
4glTF reimport produced no mesh
5Unity disk POSITION is not (x, z, -y), or Unity node has rotation
6Godot disk POSITION is not raw Z-up; also --output produced no file
7Unity reimport is not standing
8Godot reimport is not lying along Y, or reimported tip mismatch
9Reimported orientations did not differ (--same-axis lands here)
10Gallery 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.

diff --git a/docs/gallery/fence-kit/index.html b/docs/gallery/fence-kit/index.html index fd1f292b..d5cb7831 100644 --- a/docs/gallery/fence-kit/index.html +++ b/docs/gallery/fence-kit/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); @@ -266,17 +271,17 @@

fence-kit

Intended size: 1.60 m bay, 1.14 m posts, 0.11 m post stock; outer AABB 1.624 × 0.134 × 1.232 m.

Budgets

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 |

+
AxisDeclaredMeasured (4.5.11 / 5.1.2 / 5.2.1)
Base triangles1000–14001092 / 1092 / 1092
LOD1 ratio0.32–0.62 of base0.5000 / 0.5000 / 0.5000
LOD2 ratio0.10–0.35 of base0.2198 / 0.2198 / 0.1832
Materialsexactly 2 distinct, ≥48 metal, ≥24 wood2 slots, 156 metal / 390 wood
UVsin 0..1, AABB overlap ≤ 1e-5in range, overlap 0
Outer AABB(1.624, 0.134, 1.232) m ± 0.015(1.6240, 0.1340, 1.2320), zmin 0
Collider tris≤ 18032
Exportwritten, size > 082216 / 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.

Hygiene

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 |

+
AxisDeclaredMeasured (all three)
Non-manifold edges00
Loose verts / edges0 / 00 / 0
Doubles merged at 1e-500
Zero-area faces00
N-gons00
Coplanar disjoint face pairs00
Grounded: zminwithin 1e-4 of 00.0000
Named shoes2, each zmin ≤ 0.0022, 0.00000

Joint fit and span

-

| 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 |

+
AxisDeclaredMeasured (all three)
Brace-to-post BVH gap (both posts)≤ 0.008 m0.00240
Rail-to-post BVH gap≤ 0.008 m0.00093
Rail span vs 2·RAIL_HALF± 0.04 m1.3900 vs 1.390

Falsifiers

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 |

+
FlagBudget violatedExit
--skip-decimateLOD1 ratio band9
--stray-vertloose vertex count is 015
--lift-zbounding box zmin is 016
--short-shoesnamed shoes plant at z=016
--short-bracebrace-to-post BVH gap (both posts)17
--gap-railsrail-to-post BVH gap18
--long-railsrail span vs 2·RAIL_HALF19

--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.

Run

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) |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build / no UV layer
4Base triangle count outside range
5Material count ≠ 2 distinct slots, or wood/metal faces missing
6UVs outside 0..1
7UV AABB overlap above tolerance
8World AABB off declared outer size
9LOD ratio band (--skip-decimate lands here)
10Framing gate (render path only)
11Collider triangle count above ceiling
12Bake did not finish or image has no data
13Export file missing or empty
14--output produced no file
15Hygiene (--stray-vert lands here)
16Grounded zmin / named shoes (--lift-z, --short-shoes)
17Brace-to-post gap (--short-brace)
18Rail-to-post gap (--gap-rails)
19Rail span (--long-rails)

Source

diff --git a/docs/gallery/gltf-export-roundtrip/index.html b/docs/gallery/gltf-export-roundtrip/index.html index ac25f2a0..01842965 100644 --- a/docs/gallery/gltf-export-roundtrip/index.html +++ b/docs/gallery/gltf-export-roundtrip/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); @@ -278,7 +283,7 @@

Run

blender --background --python gltf_export_roundtrip.py -- --output crate.png --engine cycles

Exit codes

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 |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Exporter/importer RNA kwargs drifted
4Base cage drifted from its closed form
5Authored UVs drifted from the box-map closed form
6Bevel produced no evaluated geometry
7On-disk node/mesh/generator contract drifted
8On-disk primitive/material binding count drifted
9On-disk POSITION bounds ≠ axis-converted bbox (--no-yup lands here)
10On-disk POSITION count ≠ evaluated loop count
11On-disk UV V-flip failed
12Re-import did not produce exactly one mesh
13Re-imported object carries a transform
14Material names drifted on re-import
15Re-import vert count ≠ evaluated loop count
16Round-trip position drift
17Round-trip normal drift
18Round-trip UV drift
19Re-import triangle count drifted
20Per-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.

diff --git a/docs/gallery/gltf-skin-roundtrip/index.html b/docs/gallery/gltf-skin-roundtrip/index.html index 0abf3cc0..b360c4e0 100644 --- a/docs/gallery/gltf-skin-roundtrip/index.html +++ b/docs/gallery/gltf-skin-roundtrip/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); @@ -278,7 +283,7 @@

Run

blender --background --python gltf_skin_roundtrip.py -- --output scorp.png --engine cycles

Exit codes

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 |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Exporter RNA missing expected kwargs
4Vertex group count ≠ bone count
5Disk skins ≠ 1 (--no-skins lands here)
6Skin joints ≠ bone names
7Missing JOINTS_0/WEIGHTS_0, or accessor length mismatch
8Disk weight sums off 1.0
9Disk verts exceed evaluated loops
10Armature count after import ≠ 1
11Bone count drifted
12Named bone lost
13Bone parent drifted
14Rest matrices drifted
15Skinned mesh count after import ≠ 1
16Re-import vert count ≠ disk
17Vertex group names drifted
18Weight round-trip drifted
19Deformation 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.

diff --git a/docs/gallery/gn-instance-grid/index.html b/docs/gallery/gn-instance-grid/index.html index d9a88873..60fe46a2 100644 --- a/docs/gallery/gn-instance-grid/index.html +++ b/docs/gallery/gn-instance-grid/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); @@ -273,7 +278,7 @@

Run

blender --background --python gn_instance_grid.py -- --output grid.png --engine cycles

Exit codes

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 |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Carrier vertex count ≠ 1
4Corner instance vert count ≠ 8 (--one-cell lands here)
5Evaluated topology ≠ 72 verts / 54 faces
6Set Material did not carry Lime
7Corner 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.

diff --git a/docs/gallery/gn-modifier-inputs/index.html b/docs/gallery/gn-modifier-inputs/index.html index 8d2983ab..e65729e2 100644 --- a/docs/gallery/gn-modifier-inputs/index.html +++ b/docs/gallery/gn-modifier-inputs/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); @@ -278,10 +283,10 @@

Run

blender --background --python gn_modifier_inputs.py -- --output stairs.png --engine cycles

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 | 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 |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Scale input identifier missing on the tree interface
4Modifiers do not share one node_group
5Version-path write raised (--api dict on 5.2, --api rna on 4.5)
6Version-path read raised
7Readback ≠ intended scale (--same-scale lands here)
8Evaluated Z-extent ≠ intended scale
9Evaluated mesh not sitting on z=0
10Gallery framing violation
11Evaluated 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.

Falsification

-

| 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 |

+
ProbeBinaryResult
--api dict5.2.1 LTSexit 5, TypeError: id properties not supported for this type
--api rna4.5.11 LTSexit 5, AttributeError: 'NodesModifier' object has no attribute 'properties'
--api auto5.2.1, 5.1.2, 4.5.11exit 0, extents 1.000 / 2.000 / 3.000

Source

diff --git a/docs/gallery/gn-sdf-remesh/index.html b/docs/gallery/gn-sdf-remesh/index.html index e37bc85f..7321fc7b 100644 --- a/docs/gallery/gn-sdf-remesh/index.html +++ b/docs/gallery/gn-sdf-remesh/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); @@ -274,7 +279,7 @@

Run

blender --background --python gn_sdf_remesh.py -- --output remesh.png --engine cycles

Exit codes

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 |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3SDF remesh produced no/unchanged geometry (--no-sdf lands here)
4--output produced no file
5Wrong-era EEVEE engine id was accepted
6Input 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.

diff --git a/docs/gallery/gn-socket-rename/index.html b/docs/gallery/gn-socket-rename/index.html index 16b84a92..426b230e 100644 --- a/docs/gallery/gn-socket-rename/index.html +++ b/docs/gallery/gn-socket-rename/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); @@ -260,7 +265,7 @@

gn-socket-rename

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 @@

Render

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.

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 | | 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 |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
4Identifier inventory wrong for this Blender version
5Pre-5.2 identifier missing (--legacy-ids on 5.2)
6Enabled-name socket lookup failed
7Evaluated vert count off (Compare did not switch the column in)
8Evaluated zmax off closed form
9POINT gauge_h not 1.80 on eight column verts
10Gallery 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.

diff --git a/docs/gallery/gn-zone-iterate/index.html b/docs/gallery/gn-zone-iterate/index.html index f5a13e08..fa70feea 100644 --- a/docs/gallery/gn-zone-iterate/index.html +++ b/docs/gallery/gn-zone-iterate/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); @@ -273,7 +278,7 @@

Run

The --output render path measures framing via examples/gallery_framing.py (exit 10 on violation).

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; 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 |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage; also carrier mesh was rewritten
3Repeat evaluated vert/face count off closed form
4Repeat X-centers off closed form
5For Each evaluated vert/face count off closed form
6For Each Z-centers off closed form
10Gallery 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.

diff --git a/docs/gallery/gp-lineart-contour/index.html b/docs/gallery/gp-lineart-contour/index.html index 91eb2d58..5d5b7b75 100644 --- a/docs/gallery/gp-lineart-contour/index.html +++ b/docs/gallery/gp-lineart-contour/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); @@ -275,7 +280,7 @@

Run

blender --background --python gp_lineart_contour.py -- --output lineart.png

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; 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 |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage; also GPv3 collection address contract
3LINEART thickness / radius trap for this Blender
4LINEART type, source, or use_contour (--no-contour lands here)
5Evaluated contour too thin
6Cleared source_object still produced strokes
7Contour+crease off still produced strokes, or restore failed
8--output produced no file
10Gallery 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.

diff --git a/docs/gallery/grease-pencil-rosette/index.html b/docs/gallery/grease-pencil-rosette/index.html index 9f41b2c3..efaeca17 100644 --- a/docs/gallery/grease-pencil-rosette/index.html +++ b/docs/gallery/grease-pencil-rosette/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); @@ -260,9 +265,9 @@

grease-pencil-rosette

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:

  • Blender 4.5 LTS — GPv3 is 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.
  • Blender 5.x — legacy is gone: GPv3 took over the 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.

Run

# 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

Exit codes

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 |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3GPv3 address / legacy trap for this Blender
4Object type, layer count, or frame number
5Stroke topology or cyclic (--open-strokes lands here)
6Lazy attribute layers missing or wrong domain/type
7Position buffer length or closed-form round-trip
8Grease 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.

diff --git a/docs/gallery/grindstone/index.html b/docs/gallery/grindstone/index.html index 789b4428..263f5ace 100644 --- a/docs/gallery/grindstone/index.html +++ b/docs/gallery/grindstone/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,16 +270,16 @@

grindstone

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).

Budgets

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 |

+
AxisDeclaredMeasured (4.5.11 / 5.1.2 / 5.2.1)
Base triangles700–1000928 / 928 / 928
LOD1 ratio0.32–0.62 of base0.5000 / 0.5000 / 0.5000
LOD2 ratio0.10–0.35 of base0.2198 / 0.2198 / 0.1897
Materialsexactly 3 distinct; ≥80 wood, stone, metal faces3 slots; 216 / 160 / 180
UVsin 0..1, AABB overlap ≤ 1e-5in range, overlap 0
Outer AABB(0.710, 0.419, 0.610) m ± 0.015(0.7100, 0.4185, 0.6100), zmin 0
Collider tris≤ 240198
Exportwritten, size > 089316 / 89316 / 89312 bytes

Hygiene

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 |

+
AxisDeclaredMeasured (all three)
Non-manifold edges00
Loose verts / edges0 / 00 / 0
Doubles merged at 1e-500
Zero-area faces00
N-gons00
Coplanar disjoint face pairs00
Grounded: zminwithin 1e-4 of 00.0000
Named supports: 4 shoeseach zmin ≤ 1e-34, shoe_z 0.00000
Stone size0.500 m dia × 0.095 m thick ± 0.02 / ± 0.0150.5000 × 0.0950

Joint fit and seat

-

| 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 |

+
AxisDeclaredMeasured (all three)
Crank-axle gap≤ 0.008 m0.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 trough0.015–0.045 m0.02570
Trough floor zmin≥ 0.040 m0.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.

Falsifiers

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 |

+
FlagBudget violatedExit
--skip-decimateLOD1 ratio band9
--stray-vertloose vertex count is 015
--lift-zbounding box zmin is 016
--short-legsnamed shoe supports at Z=016
--float-crankcrank-axle gap17
--float-legsshoe-wood join17
--no-dipstone dip band18
--narrow-troughtrough-sill seat18

Run

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 |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build / no UV layer
4Base triangle count outside range
5Material count ≠ 3 distinct slots, or a face-count floor missed
6UVs outside 0..1
7UV AABB overlap above tolerance
8World AABB off declared outer size
9LOD ratio band (--skip-decimate lands here)
10Framing gate (render path only)
11Collider triangle count above ceiling
12Bake did not finish or image has no data
13Export file missing or empty
14--output produced no file
15Mesh hygiene: loose, non-manifold, zero-area, doubles, n-gons, z-fight
16Not grounded: bounding box zmin off 0, or a named shoe floats
17Joint fit: crank-axle gap, or shoe-wood join
18Seat: trough-sill gap, stone dip band, or trough sitting on the dirt
19Stone diameter or thickness off the stated real-world size

Source

diff --git a/docs/gallery/hand-pump/index.html b/docs/gallery/hand-pump/index.html index ed6fe16b..4e428bbb 100644 --- a/docs/gallery/hand-pump/index.html +++ b/docs/gallery/hand-pump/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); @@ -266,17 +271,17 @@

hand-pump

Intended size: ~1.05 m village pump, 0.34 m plinth, 0.76 m column; outer AABB 0.610 × 0.382 × 1.052 m.

Budgets

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 |

+
AxisDeclaredMeasured (4.5.11 / 5.1.2 / 5.2.1)
Base triangles900–22001064 / 1064 / 1064
LOD1 ratio0.32–0.62 of base0.5000 / 0.5000 / 0.5000
LOD2 ratio0.10–0.35 of base0.2199 / 0.2199 / 0.2162
Materialsexactly 2 distinct, ≥48 metal, ≥24 wood2 slots, 564 metal / 144 wood
UVsin 0..1, AABB overlap ≤ 1e-5in range, overlap 0
Outer AABB(0.610, 0.382, 1.052) m ± 0.015(0.6100, 0.3820, 1.0520), zmin 0
Collider tris≤ 220114
Exportwritten, size > 097304 / 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.

Hygiene

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 |

+
AxisDeclaredMeasured (all three)
Non-manifold edges00
Loose verts / edges0 / 00 / 0
Doubles merged at 1e-500
Zero-area faces00
N-gons00
Coplanar disjoint face pairs00
Grounded: zminwithin 1e-4 of 00.0000
Plinth zminwithin 1e-4 of 00.00000

Joint fit and column

-

| 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 |

+
AxisDeclaredMeasured (all three)
Spout-to-column BVH gap≤ 0.008 m0.00272
Flange-to-plinth BVH gap≤ 0.008 m0.00600
Lower-column radius vs COL_R_LO± 0.008 m0.06200

Falsifiers

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 |

+
FlagBudget violatedExit
--skip-decimateLOD1 ratio band9
--stray-vertloose vertex count is 015
--lift-zbounding box zmin is 016
--float-spoutspout-to-column BVH gap17
--float-flangeflange-to-plinth BVH gap18
--skinny-collower-column radius19

Run

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) |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build / no UV layer
4Base triangle count outside range
5Material count ≠ 2 distinct slots, or wood/metal faces missing
6UVs outside 0..1
7UV AABB overlap above tolerance
8World AABB off declared outer size
9LOD ratio band (--skip-decimate lands here)
10Framing gate (render path only)
11Collider triangle count above ceiling
12Bake did not finish or image has no data
13Export file missing or empty
14--output produced no file
15Hygiene (--stray-vert lands here)
16Grounded zmin / plinth (--lift-z)
17Spout-to-column gap (--float-spout)
18Flange-to-plinth gap (--float-flange)
19Column radius (--skinny-col)

Source

diff --git a/docs/gallery/hay-bale/index.html b/docs/gallery/hay-bale/index.html index 532e35a1..84f88339 100644 --- a/docs/gallery/hay-bale/index.html +++ b/docs/gallery/hay-bale/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); @@ -269,7 +274,7 @@

How the belts are built

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.

Budgets

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 |

+
AxisDeclaredMeasured (4.5.11 / 5.1.2 / 5.2.1)
Base triangles1700–24002012 on all three
LOD1 ratio0.32–0.62 of base0.5000
LOD2 ratio0.10–0.35 of base0.2197
Materialsexactly 2 distinct, ≥24 hay, ≥24 twine2 slots, 712 hay, 304 twine
UVsin 0..1, AABB overlap ≤ 1e-5in range, overlap 0
Outer AABB(0.922, 0.530, 0.409) m ± 0.01(0.9208, 0.5295, 0.4087)
Grounded zminwithin 1e-4 of 00.000000
Hygieneloose/nonman/zero-area/doubles/n-gons/z-fight = 0all 0
Shell countexactly 5 (loaf, 2 belts, 2 loops)5
Belt wrap zmineach belt shell ≤ 0.006 m0.002628 both
Belt seat depthevery station in [0.002, 0.020] m0.00400..0.01676 over 20/20 stations, both belts
Mirror symmetrybelts and loops matched within 5e-4 m0.000000
Cinch depthmid-span minus waist in [0.006, 0.020] m0.01420
Loaf X × Z(0.922, 0.409) m ± 0.02(0.9208, 0.4087)
Hay–twine gapBVH surface < 0.008 m0.00002
Collider tris≤ 400156
Exportwritten, size > 0156664 / 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.

Why these budgets and not an AABB

@@ -277,7 +282,7 @@

Why these budgets and not an AABB

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.

Falsifiers

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 |

+
FlagTarget budgetExit
--skip-decimateLOD1 ratio band9
--stray-verthygiene: loose vertices15
--lift-zgrounded AABB zmin16
--float-beltsper-belt wrap zmin16
--slack-beltbanded belt seat depth18
--skew-beltbelt mirror symmetry19

--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.

Run

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 |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build / no UV layer
4Base triangle count outside range
5Material count ≠ 2 distinct slots, or hay/twine faces missing
6UVs outside 0..1
7UV AABB overlap above tolerance
8World AABB off declared outer size
9LOD ratio band (--skip-decimate lands here)
10Framing gate (render path only)
11Collider triangle count above ceiling
12Bake did not finish or image has no data
13Export file missing or empty
14--output produced no file
15Hygiene: loose, non-manifold, zero-area, doubles, n-gons, or cross-shell z-fight (--stray-vert)
16Grounded: AABB zmin (--lift-z), or a belt that does not wrap under (--float-belts)
17Shell count, or hay–twine BVH gap above 8 mm
18Banded belt seat depth (--slack-belt)
19Belt mirror symmetry (--skew-belt), cinch depth, or loaf real-world size

Source

diff --git a/docs/gallery/hitching-post/index.html b/docs/gallery/hitching-post/index.html index ca0e4394..fb537f50 100644 --- a/docs/gallery/hitching-post/index.html +++ b/docs/gallery/hitching-post/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,11 +270,11 @@

hitching-post

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.

Budgets

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 |

+
AxisDeclaredMeasured (4.5.11 / 5.1.2 / 5.2.1)
Base triangles1400–24001680 / 1680 / 1680
LOD1 ratio0.32–0.62 of base0.5000 / 0.5000 / 0.5000
LOD2 ratio0.10–0.35 of base0.2190 / 0.2190 / 0.2190
Materialsexactly 2 distinct, ≥70 wood, ≥560 metal2 slots, 114 wood, 742 metal
UVsin 0..1, AABB overlap ≤ 1e-5in range, overlap 0
Outer AABB(0.500, 0.149, 1.246) m ± 0.01(0.5000, 0.1490, 1.2460)
Grounded zminwithin 1e-4 of 00 / 0 / 0
Hygieneloose/nonman/zero-area/doubles/n-gons/coplanar pairs = 0all 0
Post seatworld 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 ringcenterline error ≤ 0.008 m, ring–wood overlap 0, eye–wood overlap 0, shank–ring overlap 0, shank bites the eye0, 0, 0, 0, 40
Shoeeach band overlaps the post, shoe overlaps the soleband gap 0, sole–shoe 8
Wood–metal gapBVH surface < 0.008 m0.00075
Collider tris≤ 28080
Exportwritten, size > 0127428 / 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 |

+
FalsifierBudget violatedExitMeasured failure
--skip-decimateLOD1 ratio band9ratio 1.0000
--stray-vertMesh hygiene, loose verts15loose_v=1
--twin-soleCoplanar face pairs15zfight=6
--lift-zGrounded zmin16zmin 0.050000
--clip-ringHung-ring centerline18ring_err 0.024, ring–wood overlap 32
--short-postSeated post19post zmin 0.030

Default exit is 0 on all three. --clip-ring also overlaps the rail; the centerline gate is the one that fires.

Run

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) |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build / no UV layer
4Base triangle count outside range
5Material count ≠ 2 distinct slots, or wood/metal faces missing
6UVs outside 0..1
7UV AABB overlap above tolerance
8World AABB off declared outer size
9LOD ratio band (--skip-decimate lands here)
10Framing gate (render path only)
11Collider triangle count above ceiling
12Bake did not finish or image has no data
13Export file missing or empty
14--output produced no file
15Hygiene, including coplanar pairs (--stray-vert, --twin-sole)
16Grounded zmin (--lift-z)
17Wood–metal BVH gap above 8 mm
18Hung ring or shoe seat (--clip-ring)
19Post plumb, origin, and cup seat (--short-post)

Source

diff --git a/docs/gallery/image-pixels-testcard/index.html b/docs/gallery/image-pixels-testcard/index.html index 1638d1be..f6d95481 100644 --- a/docs/gallery/image-pixels-testcard/index.html +++ b/docs/gallery/image-pixels-testcard/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); @@ -260,10 +265,10 @@

image-pixels-testcard

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:

-
  • *Buffer geometry* — an API change to per-image channel counts, or code assuming an RGB stride (falsified: a W*H*3 write raises and the check exits 3).
  • *Byte/float round-trip vs the closed-form card* — any stride, orientation, or packing bug in the bulk path; a one-pixel shift was deliberately introduced once and the check exited 4 with measured error 0.97 against tolerance 0.00196.
  • *Quantization floor* — byte_err > 0 proves 8-bit storage really quantizes; byte and float images swapping behavior cannot hide.
  • *Reallocation* — scale() no longer reallocating (stale-size read succeeding).
  • *save() lifecycle* — the source-flip/buffer-drop behavior changing (falsified: substituting save_render() for save() exits 7 because source stays GENERATED).
  • *Disk round-trip* — a byte sRGB image saved to PNG and reloaded must match at quantization tolerance.
+
  • Buffer geometry — an API change to per-image channel counts, or code assuming an RGB stride (falsified: a W*H*3 write raises and the check exits 3).
  • Byte/float round-trip vs the closed-form card — any stride, orientation, or packing bug in the bulk path; a one-pixel shift was deliberately introduced once and the check exited 4 with measured error 0.97 against tolerance 0.00196.
  • Quantization floor — byte_err > 0 proves 8-bit storage really quantizes; byte and float images swapping behavior cannot hide.
  • Reallocation — scale() no longer reallocating (stale-size read succeeding).
  • save() lifecycle — the source-flip/buffer-drop behavior changing (falsified: substituting save_render() for save() exits 7 because source stays GENERATED).
  • Disk round-trip — a byte sRGB image saved to PNG and reloaded must match at quantization tolerance.

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.

Run

@@ -278,7 +283,7 @@

Run

blender --background --python image_pixels_testcard.py -- --output card.png --engine cycles

Exit codes

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 |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Pixel buffer is not always RGBA
4Byte round-trip vs closed-form card (--wrong-origin lands here)
5Float-buffer round-trip failed
6scale() did not reallocate, or stale-size read succeeded
7save() source/buffer-drop contract drifted
8save_render() flipped source or disturbed the buffer
9Byte 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.

diff --git a/docs/gallery/index.html b/docs/gallery/index.html index d2fbc1a3..61e0ec12 100644 --- a/docs/gallery/index.html +++ b/docs/gallery/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); diff --git a/docs/gallery/iron-cauldron/index.html b/docs/gallery/iron-cauldron/index.html index 00773bb2..8ebf5548 100644 --- a/docs/gallery/iron-cauldron/index.html +++ b/docs/gallery/iron-cauldron/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 @@

iron-cauldron

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).

Budgets

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 |

+
AxisDeclaredMeasured (4.5.11 / 5.1.2 / 5.2.1)
Base triangles2800–42003664 / 3664 / 3664
LOD1 ratio0.32–0.62 of base0.5000 / 0.5000 / 0.5000
LOD2 ratio0.10–0.35 of base0.2200 / 0.2200 / 0.2200
Materialsexactly 2 distinct; ≥80 wood, ≥200 metal faces2 slots; 224 / 1768
UVsin 0..1, AABB overlap ≤ 1e-5in range, overlap 0
Outer AABB(0.987, 0.961, 0.860) m ± 0.015(0.9866, 0.9605, 0.8600), zmin 0
Collider tris≤ 280250
Exportwritten, size > 0287916 / 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.

Hygiene

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 |

+
AxisDeclaredMeasured (all three)
Non-manifold edges00
Loose verts / edges0 / 00 / 0
Doubles merged at 1e-500
Zero-area faces00
N-gons00
Coplanar disjoint face pairs00
Grounded: zminwithin 1e-4 of 00.0000
Named supports: 3 ferruleseach zmin ≤ 1e-33, cup_z 0.00000
Pot size0.355 m dia × 0.334 m high ± 0.040.3545 × 0.3340
Pot–leg clearance≥ 0.04 m0.0464

Joint fit and seat

-

| 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 |

+
AxisDeclaredMeasured (all three)
Hook-bail BVH gap≤ 0.010 m0.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.

Falsifiers

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 |

+
FlagBudget violatedExit
--skip-decimateLOD1 ratio band9
--stray-vertloose vertex count is 015
--lift-zbounding box zmin is 016
--short-legsnamed ferrule supports at Z=016
--float-hookhook-bail gap17
--pipe-ferruleferrule bite (pole starts above the well)18

Run

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 |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build / no UV layer / pot clips tripod
4Base triangle count outside range
5Material count ≠ 2 distinct slots, or a face-count floor missed
6UVs outside 0..1
7UV AABB overlap above tolerance
8World AABB off declared outer size
9LOD ratio band (--skip-decimate lands here)
10Framing gate (render path only)
11Collider triangle count above ceiling
12Bake did not finish or image has no data
13Export file missing or empty
14--output produced no file
15Mesh hygiene: loose, non-manifold, zero-area, doubles, n-gons, z-fight
16Not grounded: bounding box zmin off 0, or a named ferrule floats
17Joint fit: hook-bail gap
18Seat: ferrule bite band
19Pot diameter or height off the stated real-world size

Source

diff --git a/docs/gallery/light-link-studio/index.html b/docs/gallery/light-link-studio/index.html index b1ceecbc..7cb6ea61 100644 --- a/docs/gallery/light-link-studio/index.html +++ b/docs/gallery/light-link-studio/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); @@ -276,7 +281,7 @@

Run

blender --background --python light_link_studio.py -- --output linked.png

Exit codes

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 |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Light datablock gained light_linking
4Light objects lost light_linking
5receiver_collection assignment did not read back (--skip-link lands here)
6Linked state: hero dark or hero/decoy ratio below gate
7Decoy rise below gate when unlinked
8Hero 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.

diff --git a/docs/gallery/lightmap-uv-channel/index.html b/docs/gallery/lightmap-uv-channel/index.html index 22172dd0..9b385151 100644 --- a/docs/gallery/lightmap-uv-channel/index.html +++ b/docs/gallery/lightmap-uv-channel/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); @@ -266,7 +271,7 @@

lightmap-uv-channel

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):

  • Holding a 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.

Run

blender --background --python lightmap_uv_channel.py --
@@ -274,7 +279,7 @@ 

Run

blender --background --python lightmap_uv_channel.py -- --falsify overlap.png

Exit codes

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 |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3UV layer names ≠ UV0 / UV1
4UVMap / UV1 render/clone flags wrong after re-assert
5Channel 0 (UV0) touched by the UV1 unwrap
6UV1 loops outside [0,1]
7SAT overlap self-test failed, or UV1 triangle pairs overlap
8Min island distance below margin
9Part is not watertight
10Gallery framing violation; also --output produced no file
11Gallery 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.

diff --git a/docs/gallery/lod-decimate-chain/index.html b/docs/gallery/lod-decimate-chain/index.html index 053977d7..7b9bea40 100644 --- a/docs/gallery/lod-decimate-chain/index.html +++ b/docs/gallery/lod-decimate-chain/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); @@ -277,7 +282,7 @@

Run

blender --background --python lod_decimate_chain.py -- --output rocket.png --engine cycles

Exit codes

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 |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Base topology ≠ closed form
4Base bbox ≠ closed form
5LOD0 evaluated counts ≠ base
6Evaluated tris ≥ base (--no-decimate lands here)
7Original datablock mutated after evaluation
8LOD tris outside ratio bounds
9LOD 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.

diff --git a/docs/gallery/market-stall/index.html b/docs/gallery/market-stall/index.html index 5d8f417c..c6178d4f 100644 --- a/docs/gallery/market-stall/index.html +++ b/docs/gallery/market-stall/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,16 +270,16 @@

market-stall

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).

Budgets

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 |

+
AxisDeclaredMeasured (4.5.11 / 5.1.2 / 5.2.1)
Base triangles3900–52004324 / 4324 / 4324
LOD1 ratio0.32–0.62 of base0.5000 / 0.5000 / 0.5000
LOD2 ratio0.10–0.35 of base0.2197 / 0.2197 / 0.2197
Materialsexactly 3 distinct; ≥16 faces per stripe slot3 slots; 34 / 34 stripe faces
UVsin 0..1, AABB overlap ≤ 1e-5in range, overlap 0
Outer AABB(1.389, 1.011, 1.740) m ± 0.01(1.3892, 1.0107, 1.7402), zmin 0
Collider tris≤ 8058
Exportwritten, size > 0312752 / 312752 / 312728 bytes

Hygiene

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 |

+
AxisDeclaredMeasured (all three)
Non-manifold edges00
Loose verts / edges0 / 00 / 0
Doubles merged at 1e-500
Zero-area faces00
N-gons00
Coplanar disjoint face pairs00
Grounded: zminwithin 1e-4 of 00.0000
Named supports: 4 wrap feeteach zmin ≤ 1e-34, foot_z 0.00000
Frame plan1.36 × 0.92 m ± 0.041.3600 × 0.9200

Joint fit and seat

-

| 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 |

+
AxisDeclaredMeasured (all three)
Brace-vs-counter overlap≤ 1e-6 m³0.000000
Header-post tenon engage≥ 0.4 × tenon0.0595
Awning-on-header seat−0.002–0.010 m0.00378
Post plumb (XY drift)≤ 0.008 m0.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.

Falsifiers

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 |

+
FlagBudget violatedExit
--skip-decimateLOD1 ratio band9
--stray-vertloose vertex count is 015
--lift-zbounding box zmin is 016
--short-feetnamed wrap-foot supports at Z=016
--low-bracebrace-vs-counter overlap17
--float-awningawning-on-header seat18
--rake-postspost plumb19

Run

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 |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build / no UV layer
4Base triangle count outside range
5Material count ≠ 3 distinct slots, or stripe faces missing
6UVs outside 0..1
7UV AABB overlap above tolerance
8World AABB off declared outer size
9LOD ratio band (--skip-decimate lands here)
10Framing gate (render path only)
11Collider triangle count above ceiling
12Bake did not finish or image has no data
13Export file missing or empty
14--output produced no file
15Mesh hygiene: loose, non-manifold, zero-area, doubles, n-gons, z-fight
16Not grounded: bounding box zmin off 0, or a named wrap foot floats
17Joint fit: brace occupying the counter volume
18Seat: awning-on-header gap
19Post plumb or frame plan off the stated real-world size

Source

diff --git a/docs/gallery/mesh-hygiene-audit/index.html b/docs/gallery/mesh-hygiene-audit/index.html index d76b7b7f..78ff362f 100644 --- a/docs/gallery/mesh-hygiene-audit/index.html +++ b/docs/gallery/mesh-hygiene-audit/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); @@ -260,7 +265,7 @@

mesh-hygiene-audit

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:

  • No ngons. len(poly.vertices) <= 4 for every face.
  • No loose vertices. Every vert has degree ≥ 1.
  • Manifold edges. Every edge borders exactly 2 faces (closed solid).
  • No zero-area faces. Face area > 1e-10; prints measured min_area.
  • Outward winding. Positive signed volume (divergence theorem).
  • Euler sphere. V − E + F == 2 for the clean closed manifold (measured: verts=66 edges=136 faces=72, volume≈0.652001, min_area≈1.146e-02).
@@ -275,7 +280,7 @@

Run

blender --background --python mesh_hygiene_audit.py -- --output hygiene.png --engine cycles

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 | 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 |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Ngon present (--inject-ngon lands here)
4Loose vertices
5Non-manifold or boundary edges
6Zero-area faces
7Signed volume ≤ 0
8Euler characteristic ≠ 2
9--output produced no file
10Gallery 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.

diff --git a/docs/gallery/modular-kit-snap/index.html b/docs/gallery/modular-kit-snap/index.html index 5d25f452..3352aca3 100644 --- a/docs/gallery/modular-kit-snap/index.html +++ b/docs/gallery/modular-kit-snap/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); @@ -260,7 +265,7 @@

modular-kit-snap

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):

  • Snap. Exactly 16 boundary verts (8 per open end, from the hollow-rectangle profile), every one on its end plane x ∈ {0, 4} within 1e-6 m; every boundary edge lies on an end plane.
  • Loop coincidence. The two end rings, matched by nearest (y, z) key, agree within 1e-6 — opposing loops are coincident under the tile offset.
  • Tiling. A linked duplicate offset by (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.
  • Bounding box. The shell's local bbox equals the declared tile exactly (0..4 × ±1.5 × 0..3, all float32-exact values, deviation 0.0), and the whole-asset bbox matches it because detail is contained.
  • Manifold. Every edge has exactly 2 link faces except the 16 boundary ring edges (1 face each) — the open ends are the only boundaries.
  • Reuse hygiene. Identity scales, Kit.CorridorSeg.* names, all part origins at the pivot.
@@ -275,7 +280,7 @@

Run

blender --background --python modular_kit_snap.py -- --output close.png --close-camera

Exit codes

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 |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Boundary vert count or end-plane membership
4End rings do not partition evenly, or opposing loops differ
5Tiled instances do not share boundary positions
6Shell or whole-asset bbox off the declared tile
7Boundary edge count, non-manifold edges, or rim off the end planes
8Detail part not watertight
9Detail part reaches a tile boundary; also --output produced no file
10Framing deviation score exceeds cap (--close-camera)
11Kit 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.

diff --git a/docs/gallery/parent-inverse-orrery/index.html b/docs/gallery/parent-inverse-orrery/index.html index f3b54654..bfbf1d3b 100644 --- a/docs/gallery/parent-inverse-orrery/index.html +++ b/docs/gallery/parent-inverse-orrery/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); @@ -262,7 +267,7 @@

parent-inverse-orrery

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.

Run

# 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

Exit codes

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 |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Bare parenting did not jump
4Keep-world idiom off
5Stale matrix_world contract broken
6Planet off closed-form orbit (--skip-mpi lands here)
7Moon 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.

diff --git a/docs/gallery/park-bench/index.html b/docs/gallery/park-bench/index.html index 2967c7cc..dfedceb7 100644 --- a/docs/gallery/park-bench/index.html +++ b/docs/gallery/park-bench/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); @@ -266,17 +271,17 @@

park-bench

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.

Budgets

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 |

+
AxisDeclaredMeasured (4.5.11 / 5.1.2 / 5.2.1)
Base triangles2200–42002520 / 2520 / 2520
LOD1 ratio0.32–0.62 of base0.5000 / 0.5000 / 0.5000
LOD2 ratio0.10–0.35 of base0.2198 / 0.2198 / 0.2198
Materialsexactly 2 distinct, ≥48 metal, ≥24 wood2 slots, 540 metal / 756 wood
UVsin 0..1, AABB overlap ≤ 1e-5in range, overlap 0
Outer AABB(1.295, 0.632, 0.884) m ± 0.015(1.2945, 0.6314, 0.8840), zmin 0
Collider tris≤ 18082
Exportwritten, size > 0188164 / 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.

Hygiene

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 |

+
AxisDeclaredMeasured (all three)
Non-manifold edges00
Loose verts / edges0 / 00 / 0
Doubles merged at 1e-500
Zero-area faces00
N-gons00
Coplanar disjoint face pairs00
Grounded: zminwithin 1e-4 of 00.0000
Named toes4, each zmin ≤ 0.0024, 0.00000

Joint fit and seat

-

| 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 |

+
AxisDeclaredMeasured (all three)
Stretcher-to-post BVH gap≤ 0.008 m0.00120
Slat-to-post BVH gap≤ 0.008 m0.00028
Seat span vs 2·hx± 0.08 m1.2720 vs 1.260

Falsifiers

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 |

+
FlagBudget violatedExit
--skip-decimateLOD1 ratio band9
--stray-vertloose vertex count is 015
--lift-zbounding box zmin is 016
--short-feetnamed toes plant at z=016
--float-stretcherstretcher-to-post BVH gap17
--gap-slatsslat-to-post BVH gap18
--narrow-seatseat span vs 2·hx19

--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.

Run

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) |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build / no UV layer
4Base triangle count outside range
5Material count ≠ 2 distinct slots, or wood/metal faces missing
6UVs outside 0..1
7UV AABB overlap above tolerance
8World AABB off declared outer size
9LOD ratio band (--skip-decimate lands here)
10Framing gate (render path only)
11Collider triangle count above ceiling
12Bake did not finish or image has no data
13Export file missing or empty
14--output produced no file
15Hygiene (--stray-vert lands here)
16Grounded zmin / named toes (--lift-z, --short-feet)
17Stretcher-to-post gap (--float-stretcher)
18Slat-to-post gap (--gap-slats)
19Seat span (--narrow-seat)

Source

diff --git a/docs/gallery/png-exr-alpha/index.html b/docs/gallery/png-exr-alpha/index.html index 83e49e42..33c887d9 100644 --- a/docs/gallery/png-exr-alpha/index.html +++ b/docs/gallery/png-exr-alpha/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); @@ -259,12 +264,12 @@

png-exr-alpha

-

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:

  • Float → PNG false unpremul — 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).
  • Float → OpenEXR fidelity — same authored buffer round-trips within 1e-5 (measured ~3e-8).
  • Byte → PNG straight alpha — 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.
  • EXR 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:

-
  • *RGBA16 IHDR* — float PNG bit-depth or color-type change (falsified expectation: 8-bit would exit 4).
  • *Error floor* — float PNG path stops destroying straight mid-tones / dark values at low alpha (exit 5).
  • *Closed-form residual* — encoding no longer matches false-unpremul+clamp (falsified once by swapping in the byte straight-alpha model; exited 6 with residual 0.9803922).
  • *EXR fidelity* — OpenEXR float RGBA round-trip regressing (exit 7).
  • *Byte straight path* — byte images starting to false-unpremul, or bit-depth flipping to 16 (exits 9–11).
  • *RGB EXR alpha drop* — color_mode='RGB' preserving authored alpha (exit 12).
+
  • RGBA16 IHDR — float PNG bit-depth or color-type change (falsified expectation: 8-bit would exit 4).
  • Error floor — float PNG path stops destroying straight mid-tones / dark values at low alpha (exit 5).
  • Closed-form residual — encoding no longer matches false-unpremul+clamp (falsified once by swapping in the byte straight-alpha model; exited 6 with residual 0.9803922).
  • EXR fidelity — OpenEXR float RGBA round-trip regressing (exit 7).
  • Byte straight path — byte images starting to false-unpremul, or bit-depth flipping to 16 (exits 9–11).
  • RGB EXR alpha drop — 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.

Run

@@ -279,7 +284,7 @@

Run

blender --background --python png_exr_alpha.py -- --output alpha.png --engine cycles

Exit codes

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 |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage; also closed-form palette no longer stresses unpremul
3float_buffer=True image reports is_float=False
4Float→PNG IHDR is not RGBA16
5Float→PNG RGB error below floor (--opaque-alpha lands here)
6Float→PNG disagrees with closed-form false-unpremul model
7Float→EXR round-trip above tolerance
8float_buffer=False image reports is_float=True
9Byte→PNG IHDR is not RGBA8
10Byte→PNG disagrees with straight-alpha 8-bit model; also gallery framing violation
11Byte→PNG stress cell looks false-unpremul-mangled
12EXR 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.

diff --git a/docs/gallery/prop-origin-transform/index.html b/docs/gallery/prop-origin-transform/index.html index 01740959..a0bf3769 100644 --- a/docs/gallery/prop-origin-transform/index.html +++ b/docs/gallery/prop-origin-transform/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); @@ -274,7 +279,7 @@

Run

blender --background --python prop_origin_transform.py -- --output origin.png --engine cycles

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 | 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 |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Stale matrix_world contract broken
4World bbox moved across bake
5Scale after bake is not (1,1,1)
6Origin not at local base center
7Bare parenting did not jump
8MPI did not restore world location (--skip-mpi lands here)
9--output produced no file
10Gallery 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.

diff --git a/docs/gallery/shader-node-group/index.html b/docs/gallery/shader-node-group/index.html index 5df3d4fe..0d455fba 100644 --- a/docs/gallery/shader-node-group/index.html +++ b/docs/gallery/shader-node-group/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); @@ -273,7 +278,7 @@

Run

blender --background --python shader_node_group.py -- --output spheres.png --engine cycles

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 | 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 |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Interface sockets missing Tint / Roughness / Shader
4Group datablock users ≠ 2
5Instance points at a different node tree
6Instance Tint values identical (--same-tint lands here)
7--output produced no file
10Gallery 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.

diff --git a/docs/gallery/shape-key-blend/index.html b/docs/gallery/shape-key-blend/index.html index babcfa99..e611078c 100644 --- a/docs/gallery/shape-key-blend/index.html +++ b/docs/gallery/shape-key-blend/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); @@ -273,7 +278,7 @@

Run

blender --background --python shape_key_blend.py -- --output blend.png --engine cycles

Exit codes

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 |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3No shape keys on mesh
4Key names ≠ Basis, Tall
5Tall.value ≠ 0.5 (--zero-blend lands here)
6Undeformed mesh.vertices not at Basis
7Evaluated vert off closed-form blend
8Evaluated Z span off closed form
9Top 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.

diff --git a/docs/gallery/shipping-crate/index.html b/docs/gallery/shipping-crate/index.html index 53c6cf80..a7308f34 100644 --- a/docs/gallery/shipping-crate/index.html +++ b/docs/gallery/shipping-crate/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 @@

shipping-crate

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).

Budgets

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 |

+
AxisDeclaredMeasured (4.5.11 / 5.1.2 / 5.2.1)
Base triangles1200–28002508 / 2508 / 2508
LOD1 ratio0.32–0.62 of base0.5000 / 0.5000 / 0.5000
LOD2 ratio0.10–0.35 of base0.2193 / 0.2193 / 0.2057
Materialsexactly 2 distinct; ≥200 wood, ≥48 metal faces2 slots; 1170 / 280
UVsin 0..1, AABB overlap ≤ 1e-5in range, overlap 0
Outer AABB(1.146, 0.726, 0.612) m ± 0.015(1.1460, 0.7260, 0.6120), zmin 0
Collider tris≤ 22076
Exportwritten, size > 0197408 / 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.

Hygiene

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 |

+
AxisDeclaredMeasured (all three)
Non-manifold edges00
Loose verts / edges0 / 00 / 0
Doubles merged at 1e-500
Zero-area faces00
N-gons00
Coplanar disjoint face pairs00
Grounded: zminwithin 1e-4 of 00.0000
Named supports: 3 skidseach zmin ≤ 1e-33, skid_z 0.00000
Body plan1.056 × 0.716 m ± 0.051.0560 × 0.7160

Joint fit and seat

-

| Axis | Declared | Measured (all three) | | --- | --- | --- | | Handle-end BVH gap | ≤ 0.008 m | 0.00000 | | Slat-post BVH gap | ≤ 0.006 m | 0.00000 |

+
AxisDeclaredMeasured (all three)
Handle-end BVH gap≤ 0.008 m0.00000
Slat-post BVH gap≤ 0.006 m0.00000

Falsifiers

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 |

+
FlagBudget violatedExit
--skip-decimateLOD1 ratio band9
--stray-vertloose vertex count is 015
--lift-zbounding box zmin is 016
--short-skidsnamed skid supports at Z=016
--float-handlehandle-end gap17
--omit-slatsslat-post seat18

Run

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 |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build / no UV layer
4Base triangle count outside range
5Material count ≠ 2 distinct slots, or a face-count floor missed
6UVs outside 0..1
7UV AABB overlap above tolerance
8World AABB off declared outer size
9LOD ratio band (--skip-decimate lands here)
10Framing gate (render path only)
11Collider triangle count above ceiling
12Bake did not finish or image has no data
13Export file missing or empty
14--output produced no file
15Mesh hygiene: loose, non-manifold, zero-area, doubles, n-gons, z-fight
16Not grounded: bounding box zmin off 0, or a named skid floats
17Joint fit: handle-end gap
18Seat: slat-post gap
19Body plan off the stated real-world size

Source

diff --git a/docs/gallery/signpost/index.html b/docs/gallery/signpost/index.html index fc8b0c35..68f927ca 100644 --- a/docs/gallery/signpost/index.html +++ b/docs/gallery/signpost/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); @@ -266,17 +271,17 @@

signpost

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.

Budgets

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 |

+
AxisDeclaredMeasured (4.5.11 / 5.1.2 / 5.2.1)
Base triangles320–480376 / 376 / 376
LOD1 ratio0.32–0.62 of base0.5000 / 0.5000 / 0.4681
LOD2 ratio0.10–0.35 of base0.2181 / 0.2181 / 0.1649
Materialsexactly 3 distinct, ≥24 metal, ≥12 board, ≥12 wood3 slots, 108 metal, 22 board, 60 wood
UVsin 0..1, AABB overlap ≤ 1e-5in range, overlap 0
Outer AABB(1.260, 0.142, 1.551) m ± 0.015(1.2600, 0.1420, 1.5510), zmin 0
Collider tris≤ 18052
Exportwritten, size > 035688 / 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.

Hygiene

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 |

+
AxisDeclaredMeasured (all three)
Non-manifold edges00
Loose verts / edges0 / 00 / 0
Doubles merged at 1e-500
Zero-area faces00
N-gons00
Coplanar disjoint face pairs00
Grounded: zminwithin 1e-4 of 00.0000
Named shoe plates≥1, each zmin ≤ 0.0024, 0.00000

Joint fit and orthogonal boards

-

| 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 |

+
AxisDeclaredMeasured (all three)
Board-to-post BVH gap≤ 0.008 m0.00500
Strap-to-post BVH gap≤ 0.008 m0.00300
Board AABB Y (axis-aligned)≤ 0.055 m0.0280

Falsifiers

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 |

+
FlagBudget violatedExit
--skip-decimateLOD1 ratio band9
--stray-vertloose vertex count is 015
--lift-zbounding box zmin is 016
--short-shoenamed shoes plant at z=016
--gap-boardboard-to-post BVH gap17
--float-strapstrap-to-post BVH gap18
--yaw-boardsboard 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.

Run

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) |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build / no UV layer
4Base triangle count outside range
5Material count ≠ 3 distinct slots, or wood/board/metal faces missing
6UVs outside 0..1
7UV AABB overlap above tolerance
8World AABB off declared outer size
9LOD ratio band (--skip-decimate lands here)
10Framing gate (render path only)
11Collider triangle count above ceiling
12Bake did not finish or image has no data
13Export file missing or empty
14--output produced no file
15Hygiene (--stray-vert lands here)
16Grounded zmin / named shoes (--lift-z, --short-shoe)
17Board-to-post gap (--gap-board)
18Strap-to-post gap (--float-strap)
19Board yaw / AABB Y (--yaw-boards)

Source

diff --git a/docs/gallery/sky-texture-sun-elevation/index.html b/docs/gallery/sky-texture-sun-elevation/index.html index 2291986a..879272ef 100644 --- a/docs/gallery/sky-texture-sun-elevation/index.html +++ b/docs/gallery/sky-texture-sun-elevation/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,7 +270,7 @@

sky-texture-sun-elevation

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.

Stage deviation

-

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.

Framing deviation

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.

Run

@@ -279,7 +284,7 @@

Run

blender --background --python sky_texture_sun_elevation.py -- --output sky.png

Exit codes

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 |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3sky_type is not the identifier for this Blender
4sun_elevation round-trip failed
5dust_density / aerosol_density / NISHITA / MULTIPLE_SCATTERING trap
6Sky → Background → World Output links broken (--unlink-sky lands here)
7High-elevation zenith luma below floor
8Zenith 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.

diff --git a/docs/gallery/soccer-ball-goldberg/index.html b/docs/gallery/soccer-ball-goldberg/index.html index 6626c79d..9b4d9394 100644 --- a/docs/gallery/soccer-ball-goldberg/index.html +++ b/docs/gallery/soccer-ball-goldberg/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); @@ -278,7 +283,7 @@

Run

blender --background --python soccer_ball_goldberg.py -- --output ball.png --engine cycles

Exit codes

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) |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Topology ≠ 60/90/32
4Euler characteristic ≠ 2
5Face census ≠ 12 pentagons + 20 hexagons
6Vertex degree not uniform 3; also --output produced no file
7Non-manifold edges
8Edge lengths not uniform
9Face planarity off
10Centroid off origin
11Circumradius not uniform
12Panel material count ≠ 2
13Panel 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.

diff --git a/docs/gallery/socket-attach-points/index.html b/docs/gallery/socket-attach-points/index.html index 53e21e59..b98a15aa 100644 --- a/docs/gallery/socket-attach-points/index.html +++ b/docs/gallery/socket-attach-points/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); @@ -259,17 +264,17 @@

socket-attach-points

-

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) |

+
SocketMount normalModule
SKT_Rotor.FL/FR/RL/RR8° canted outwardMod.Rotor (twisted, tapered blades)
SKT_Camerabelly, −ZMod.CamPod (gimbal ball + lens)
SKT_Mastdorsal, +ZMod.Mast (antenna, dish, beacon)
SKT_Batteryrear, tilted downMod.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):

  • +Z — the outward mount normal, matching the mount pad's own face normal.
  • +Y — the module's up reference: chassis +Z orthogonalised against the socket normal, falling back to chassis +X when the normal is parallel to chassis up (the belly and dorsal mounts take that branch).
  • +X — 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):

-
  • Socket matrices. All 7 evaluated 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).
  • Orientation vs geometry. Each socket's world +Z equals the Newell normal of its mount pad's mount face — computed from raw vertex coordinates, and the pad mesh is built by a *quaternion swing from +Z* while the socket basis is built by *explicit Gram-Schmidt*, so the two derivations are independent (normal deviation 1.794e-07). The socket origin sits on that face's centroid (6.687e-08), and the up axis matches the documented fallback rule (1.943e-07).
  • Seating. Every module's mount origin coincides with its socket origin (offset exactly 0.0) with its mount axis on the socket +Z (dot − 1 = 1.735e-07), and carries a strictly identity local matrix — seated by the socket, not nudged into place.
  • Rigid invariance. Re-posing the chassis root to a second arbitrary transform moves every socket to root.matrix_world @ authored_local (2.384e-07) and every module stays on its socket (0.0).
  • Transform apply. Freezing loc/rot/scale on the root leaves every socket's *world* matrix put (2.235e-07) — see the hazard below for what it does to the *local* ones.
  • Reuse hygiene. 14 chassis parts at identity scale, Drone.Survey.* / SKT_* names, no default datablocks, min z == 0.00e+00.
+
  • Socket matrices. All 7 evaluated 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).
  • Orientation vs geometry. Each socket's world +Z equals the Newell normal of its mount pad's mount face — computed from raw vertex coordinates, and the pad mesh is built by a quaternion swing from +Z while the socket basis is built by explicit Gram-Schmidt, so the two derivations are independent (normal deviation 1.794e-07). The socket origin sits on that face's centroid (6.687e-08), and the up axis matches the documented fallback rule (1.943e-07).
  • Seating. Every module's mount origin coincides with its socket origin (offset exactly 0.0) with its mount axis on the socket +Z (dot − 1 = 1.735e-07), and carries a strictly identity local matrix — seated by the socket, not nudged into place.
  • Rigid invariance. Re-posing the chassis root to a second arbitrary transform moves every socket to root.matrix_world @ authored_local (2.384e-07) and every module stays on its socket (0.0).
  • Transform apply. Freezing loc/rot/scale on the root leaves every socket's world matrix put (2.235e-07) — see the hazard below for what it does to the local ones.
  • Reuse hygiene. 14 chassis parts at identity scale, Drone.Survey.* / SKT_* names, no default datablocks, min z == 0.00e+00.

Hazards found while authoring

-
  • Applying a transform on an Empty root pushes it down into the children. An Empty has no object data to bake into, so 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.
  • Only the root may be selected during the apply. A child left selected gets the parent transform applied twice — measured 2.335 m of socket drift on this asset.
  • 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.
  • Bevel offsets near half the local edge length collapse. The skid rails at a 5 mm offset on a 9.9 mm octagon edge produced 3 zero-area faces each — the collapse 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.
+
  • Applying a transform on an Empty root pushes it down into the children. An Empty has no object data to bake into, so 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.
  • Only the root may be selected during the apply. A child left selected gets the parent transform applied twice — measured 2.335 m of socket drift on this asset.
  • 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.
  • Bevel offsets near half the local edge length collapse. The skid rails at a 5 mm offset on a 9.9 mm octagon edge produced 3 zero-area faces each — the collapse 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.

@@ -281,7 +286,7 @@

Run

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.

Exit codes

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 |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Socket world matrix, orthonormal basis, or determinant
4Socket pad normal, origin, or up-axis rule
5Module seating / mount axis / leftover local transform
6Population, namespace, default datablock name, SKT_ prefix, or skid plane
7Re-pose: socket or module lost the root
8Parent-inverse clear, child scale pushdown, or post-apply module drift
9--output produced no file
10Gallery framing violation
11Gallery 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.

diff --git a/docs/gallery/stone-archway/index.html b/docs/gallery/stone-archway/index.html index 69a10167..dd27d9ab 100644 --- a/docs/gallery/stone-archway/index.html +++ b/docs/gallery/stone-archway/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); @@ -259,31 +264,30 @@

stone-archway

-

!A semicircular masonry archway: coursed piers, projecting imposts, nine voussoirs and a proud keystone

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.

What it composes

-

| 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 contentUsed for
skills/mesh-editing-and-bmeshwedge and box construction, chamfer, UVs in one bmesh
skills/procedural-materials-and-shaderstwo Principled stone materials with noise-driven weathering
skills/bake-high-to-lowCycles tangent-space normal bake, high onto low
skills/engine-export-presetsUnity glTF (export_yup=True)
skills/depsgraph-and-evaluated-dataevaluated triangle counts for the LOD ratios
snippets/decimate_to_budget.pyLOD1 / LOD2 COLLAPSE chain
snippets/convex_hull_collider.pyconvex collider
snippets/lod_chain.pyLOD naming and ratio pattern
examples/mesh-hygiene-audithygiene combinatorics (copied, not imported)

The budget that matters

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.

Measuring it took two corrections

-

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.

Budgets

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° |

+
BudgetBandMeasured
Base triangles700–2600924
LOD1 ratio0.32–0.620.5000
LOD2 ratio0.10–0.350.2121 (5.2) / 0.2186 (4.5, 5.1)
Material slotsexactly 2, distinct2
Dressed faces (keystone, imposts, plinth and head courses)≥ 100182
Ashlar faces≥ 280364
UV boundsinside 0..1(0.0017, 0.0017)–(0.9983, 0.9983)
UV AABB overlap≤ 1e-50.000000
Outer AABB1.644 × 0.510 × 2.057 m ± 0.0201.6440 × 0.5100 × 2.0568
Clear opening1.200 m ± 0.015, measured between the pier faces1.2033
Collider triangles≤ 260168
Normal bake{'FINISHED'} with image data{'FINISHED'}, has_data=True
glTF exportfile written, non-empty~87 kB
Hygieneall zeroloose 0/0, non-manifold 0, zero-area 0, doubles 0, n-gons 0, coplanar disjoint pairs 0
Grounded AABB\zmin\≤ 1e-40.00000
Pier supports2 piers, each base course zmin ≤ 1e-32 at 0.00000
Keystone proud of the wall face≥ 0.020 m0.03500
Springing jointoverlap, surface gap ≤ 1e-40.00000
Mortar jointsevery adjacent pair in 0.006–0.017 mall eight at 0.00970
Intrados circle fitevery vertex within 0.004 m of R = 0.600.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.

Determinism

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.

Falsifiers

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 |

+
FlagBreaksExit
--skip-decimatedrops the DECIMATE modifiers, LOD1 ratio goes to 1.00009
--stray-vertadds one loose vertex inside the opening, so hygiene catches it rather than the bounding box15
--lift-zlifts the whole mesh 50 mm off the floor16
--float-pierfloats one pier 12 mm; the other still grounds the AABB, so only the named-support budget sees it16
--sink-keystonesinks the keystone to a quarter of its projection (8.75 mm) — the imposts still set the Y envelope, so the bounding box is unchanged17
--wide-mortartriples the joint angle, opening every joint to 29.1 mm18
--off-circlewanders the intrados radius ±18 mm while keeping angles, joints and envelope19

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.

Findings the budgets forced

-
  • The chamfer pass silently repainted the mesh. 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.
  • The impost landed exactly on the top course. Same footprint, same plane, two coincident face centres — a z-fight. It now sits on its own mortar bed, which is why the pier lays 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.
  • Joint gaps have to be measured both ways. The keystone's radial faces are wider in Y than its neighbours', so every keystone vertex on the joint lies outside the neighbour and read 33 mm instead of the 9.7 mm joint. The metric is the closest approach of the two surfaces, min of both directions.
+
  • The chamfer pass silently repainted the mesh. 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.
  • The impost landed exactly on the top course. Same footprint, same plane, two coincident face centres — a z-fight. It now sits on its own mortar bed, which is why the pier lays 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.
  • Joint gaps have to be measured both ways. The keystone's radial faces are wider in Y than its neighbours', so every keystone vertex on the joint lies outside the neighbour and read 33 mm instead of the 9.7 mm joint. The metric is the closest approach of the two surfaces, min of both directions.

Exit codes

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) |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build, or has no UV layer
4Base triangle count outside band
5Material slots, or a material's face floor
6UVs outside 0..1
7UV AABB overlap above tolerance
8Outer AABB off declared size
9LOD1 or LOD2 ratio outside band (--skip-decimate)
10Framing gate (examples/gallery_framing.py, render path only)
11Collider triangles above ceiling
12Normal bake failed or produced no image data
13glTF export missing or empty
14--output produced no file
15Mesh hygiene (--stray-vert)
16Grounded zmin, or a pier base floating (--lift-z, --float-pier)
17Keystone projection or springing joint (--sink-keystone)
18Mortar joint outside band (--wide-mortar)
19Intrados circle fit, or clear opening (--off-circle)

Run it

# 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.

Cross-version measurements

-

| 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 |

+
Value4.5.115.1.25.2.1
Base triangles924924924
LOD1 tris / ratio462 / 0.5000462 / 0.5000462 / 0.5000
LOD2 tris / ratio202 / 0.2186202 / 0.2186196 / 0.2121
Face counts (ashlar / dressed)364 / 182364 / 182364 / 182
Outer AABB1.6440 × 0.5100 × 2.0568samesame
Collider tris168168168
Intrados deviation0.000970.000970.00097
Mortar jointsall 0.00970all 0.00970all 0.00970
Check wall-clock~0.82 s~0.85 s~0.98 s

Source

diff --git a/docs/gallery/stone-well/index.html b/docs/gallery/stone-well/index.html index 33f8b0c3..d8d0cad1 100644 --- a/docs/gallery/stone-well/index.html +++ b/docs/gallery/stone-well/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); @@ -264,11 +269,11 @@

stone-well

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).

Budgets

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 |

+
AxisDeclaredMeasured (4.5.11 / 5.1.2 / 5.2.1)
Base triangles8280–95009380 / 9380 / 9380
LOD1 ratio0.32–0.62 of base0.5000 / 0.5000 / 0.5000
LOD2 ratio0.10–0.35 of base0.2198 / 0.2198 / 0.2198
Materialsexactly 3 distinct3
Material facesstone ≥ 3000, wood ≥ 600, metal ≥ 100all above
UVsin 0..1, AABB overlap ≤ 1e-5in range, overlap 0
Outer AABB(1.312, 1.312, 1.761) m ± 0.01(1.3120, 1.3120, 1.7606)
Groundedbbox min Z within 1e-4 of 00.0000 / 0.0000 / 0.0000
Named supportsall 12 bottom-course stones on the floor within 1e-412/12, worst 0.000000
Hygieneloose V/E, non-manifold, zero-area, doubles @1e-5, n-gons: all 00 / 0 / 0 on every axis
Z-fightingcoplanar cross-shell face pairs = 00 / 0 / 0
Post tenoneach post seats 0.015–0.022 m into the measured coping top0.01800 / 0.01800 / 0.01800
Post placementeach post within 0.02 rad, in plan, of a roof hip corner recomputed from the mesh0.00000
Bucket clearancehangs 0.085–0.115 m clear of the measured coping top0.10000
Material-island gapstone↔wood and metal↔wood min distance ≤ 0.008 m0.00000 / 0.00000 / 0.00000
Collider tris≤ 380346
Exportwritten, size > 0, removed after measuring690092 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.

Falsifiers

-

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.

+
FlagTarget budgetExit
--skip-decimateLOD1 ratio band (ratio becomes 1.0)9
--lift-zAABB grounded zmin (whole mesh up 0.05 m)16
--stand-postsCoplanar 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-stoneNamed supports — lifts one bottom-course stone 4 mm while the other eleven still ground the AABB16
--shallow-tenonPost tenon band — seats the posts only 4 mm, too shallow to be a joint but not flush, so no faces share a plane18
--drop-bucketBucket clearance — lowers the bucket 0.26 m back down the shaft18
--turn-postsPost placement — rotates the post ring one curb facet off the roof's hip corners19

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.

Run

@@ -284,7 +289,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 ≠ 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) |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build / no UV layer
4Base triangle count outside range
5Material count ≠ 3 distinct slots, or a material face floor missed
6UVs outside 0..1
7UV AABB overlap above tolerance
8World AABB off declared outer size
9LOD ratio band (--skip-decimate lands here)
10Framing gate (render path only)
11Collider triangle count above ceiling
12Bake did not finish or image has no data
13Export file missing or empty
14--output produced no file
15Hygiene: loose geometry, non-manifold, zero-area, doubles, n-gons, or coplanar cross-shell face pairs (--stand-posts lands here)
16Bbox min Z not grounded (--lift-z), or a named support off the floor (--float-stone)
17Material-island gap above tolerance (parts meant to touch)
18Post tenon depth outside its band (--shallow-tenon), or bucket clearance outside its band (--drop-bucket)
19A post not under a roof hip corner in plan (--turn-posts lands here)

Source

diff --git a/docs/gallery/street-lantern/index.html b/docs/gallery/street-lantern/index.html index 467aaee7..5315d12b 100644 --- a/docs/gallery/street-lantern/index.html +++ b/docs/gallery/street-lantern/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); @@ -266,17 +271,17 @@

street-lantern

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.

Budgets

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 |

+
AxisDeclaredMeasured (4.5.11 / 5.1.2 / 5.2.1)
Base triangles3000–38003338 / 3338 / 3338
LOD1 ratio0.32–0.62 of base0.4997 / 0.4997 / 0.4997
LOD2 ratio0.10–0.35 of base0.2199 / 0.2199 / 0.2193
Materialsexactly 3 distinct; ≥200 metal, ≥8 glass, ≥24 brass3 slots; 1530 / 24 / 209
UVsin 0..1, AABB overlap ≤ 1e-5in range, overlap 0
Outer AABB(0.300, 0.786, 1.287) m ± 0.01(0.3000, 0.7860, 1.2874), zmin 0
Collider tris≤ 12078
Exportwritten, size > 0250980 / 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.

Hygiene

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 |

+
AxisDeclaredMeasured (all three)
Non-manifold edges00
Loose verts / edges0 / 00 / 0
Doubles merged at 1e-500
Zero-area faces00
N-gons00
Coplanar disjoint face pairs00
Grounded: zminwithin 1e-4 of 00.0000
Post plan0.100 m across × 1.120 m high ± (0.02, 0.04)0.1000 × 1.1200
Post plumb (top vs bottom XY)≤ 0.008 m0.00000

Joint fit and seat

-

| 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 |

+
AxisDeclaredMeasured (all three)
Brace-to-arm BVH gap≤ 0.006 m0.00000
Arm underside vs roof peak≥ 0.008 m clearance0.05300
Hanger-to-roof BVH gap≤ 0.004 m−0.00100

Falsifiers

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 |

+
FlagBudget violatedExit
--skip-decimateLOD1 ratio band9
--stray-vertloose vertex count is 015
--lift-zbounding box zmin is 016
--float-bracebrace-to-arm BVH gap17
--sink-armarm-over-roof clearance18
--rake-postpost plumb19

--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.

Run

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) |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build / no UV layer
4Base triangle count outside range
5Material count ≠ 3 distinct slots, or a face-count floor missed
6UVs outside 0..1
7UV AABB overlap above tolerance
8World AABB off declared outer size
9LOD ratio band (--skip-decimate lands here)
10Framing gate (render path only)
11Collider triangle count above ceiling
12Bake did not finish or image has no data
13Export file missing or empty
14--output produced no file
15Mesh hygiene: loose, non-manifold, zero-area, doubles, n-gons, z-fight
16Not grounded: bounding box zmin off 0
17Joint fit: brace-to-arm gap (--float-brace)
18Seat: arm-roof clearance or hanger-roof gap (--sink-arm)
19Post plumb or post size off the stated real-world size (--rake-post)

Source

diff --git a/docs/gallery/swatch-grid/index.html b/docs/gallery/swatch-grid/index.html index 3eb5e21b..f814ea41 100644 --- a/docs/gallery/swatch-grid/index.html +++ b/docs/gallery/swatch-grid/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 @@

swatch-grid

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:

  • Principled BSDF metals (gold, copper: high metallic, low roughness) and dielectrics (red/blue plastic, white rough), configured with string socket lookups and 4-tuple colors.
  • The emission pattern (an emissive orange swatch).
  • The cross-version 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.

Run

# 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

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 | 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 |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Distinct swatch colors ≠ 6 (--same-base lands here); also render not six distinct regions
4--output produced no file
5Wrong-era EEVEE engine id was accepted
10Gallery 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.

Verified

diff --git a/docs/gallery/tavern-stool/index.html b/docs/gallery/tavern-stool/index.html index 587d9f7a..5edcf79d 100644 --- a/docs/gallery/tavern-stool/index.html +++ b/docs/gallery/tavern-stool/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,16 +270,16 @@

tavern-stool

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).

Budgets

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 |

+
AxisDeclaredMeasured (4.5.11 / 5.1.2 / 5.2.1)
Base triangles2300–32002528 / 2528 / 2528
LOD1 ratio0.32–0.62 of base0.5000 / 0.5000 / 0.5000
LOD2 ratio0.10–0.35 of base0.2199 / 0.2199 / 0.2199
Materialsexactly 2 distinct; ≥200 wood, ≥48 metal faces2 slots; 1168 / 256
UVsin 0..1, AABB overlap ≤ 1e-5in range, overlap 0
Outer AABB(0.341, 0.341, 0.470) m ± 0.015(0.3400, 0.3400, 0.4700), zmin 0
Collider tris≤ 640396
Exportwritten, size > 0215912 / 215912 / 215900 bytes

Hygiene

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 |

+
AxisDeclaredMeasured (all three)
Non-manifold edges00
Loose verts / edges0 / 00 / 0
Doubles merged at 1e-500
Zero-area faces00
N-gons00
Coplanar disjoint face pairs00
Grounded: zminwithin 1e-4 of 00.0000
Named supports: 4 ferrule cupseach zmin ≤ 1e-34, cup_z 0.00000
Seat0.340 m dia × 0.470 m high ± 0.020.3400 × 0.4700

Joint fit and seat

-

| 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 |

+
AxisDeclaredMeasured (all three)
Stretcher-leg BVH gap≤ 0.008 m0.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.

Falsifiers

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 |

+
FlagBudget violatedExit
--skip-decimateLOD1 ratio band9
--stray-vertloose vertex count is 015
--lift-zbounding box zmin is 016
--short-legsnamed ferrule cups at Z=016
--float-stretchersstretcher-leg join17
--pipe-ferruleferrule inner-clearance band18

Run

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 |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build / no UV layer
4Base triangle count outside range
5Material count ≠ 2 distinct slots, or a face-count floor missed
6UVs outside 0..1
7UV AABB overlap above tolerance
8World AABB off declared outer size
9LOD ratio band (--skip-decimate lands here)
10Framing gate (render path only)
11Collider triangle count above ceiling
12Bake did not finish or image has no data
13Export file missing or empty
14--output produced no file
15Mesh hygiene: loose, non-manifold, zero-area, doubles, n-gons, z-fight
16Not grounded: bounding box zmin off 0, or a named ferrule cup floats
17Joint fit: stretcher-leg gap
18Seat: ferrule inner-clearance band
19Seat diameter or height off the stated real-world size

Source

diff --git a/docs/gallery/temp-override-join/index.html b/docs/gallery/temp-override-join/index.html index 623b975b..df35978d 100644 --- a/docs/gallery/temp-override-join/index.html +++ b/docs/gallery/temp-override-join/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); @@ -273,7 +278,7 @@

Run

blender --background --python temp_override_join.py -- --output join.png --engine cycles

Exit codes

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 |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh object count after join ≠ 1 (--no-override lands here)
4Joined target is not the sole remaining mesh
5Topology ≠ 24 verts / 18 faces
6Source objects still present
7Local 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.

diff --git a/docs/gallery/terrain-scatter/index.html b/docs/gallery/terrain-scatter/index.html index de89305c..0056ec49 100644 --- a/docs/gallery/terrain-scatter/index.html +++ b/docs/gallery/terrain-scatter/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); @@ -266,15 +271,15 @@

terrain-scatter

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.

Budgets

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 |

+
AxisDeclaredMeasured (4.5.11 / 5.1.2 / 5.2.1)
Base triangles1400–28001758 / 1758 / 1758
LOD1 ratio0.32–0.62 of base0.4994 / 0.4994 / 0.4994
LOD2 ratio0.10–0.35 of base0.2196 / 0.2196 / 0.2196
Materialsexactly 2 distinct, ≥24 stone faces2 slots, 720 stone
UVsin 0..1, AABB overlap ≤ 1e-5in range, overlap 0
Outer AABB(1.800, 1.800, 0.552) m ± 0.015(1.8000, 1.8000, 0.5523), zmin 0
Collider tris≤ 12081
Exportwritten, size > 0158156 / 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.

Hygiene

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 |

+
AxisDeclaredMeasured (all three)
Non-manifold edges00
Loose verts / edges0 / 00 / 0
Doubles merged at 1e-500
Zero-area faces00
N-gons00
Coplanar disjoint face pairs00
Grounded: zminwithin 1e-4 of 00.0000
Stone shells99
Per-stone faces≥ 4080
Stone floor zmin≥ 0.012 m0.11334
Seat offset (zmin − dirt Z)≤ 0.02 m−0.03500

Falsifiers

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 |

+
FlagBudget violatedExit
--skip-decimateLOD1 ratio band9
--stray-vertloose vertex count is 015
--lift-zbounding box zmin is 016
--poke-rockstone floor zmin17
--float-rocksseat offset vs sampled dirt Z18
--box-rocksper-stone face floor19

Run

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) |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build / no UV layer
4Base triangle count outside range
5Material count ≠ 2 distinct slots, or stone faces missing
6UVs outside 0..1
7UV AABB overlap above tolerance
8World AABB off declared outer size
9LOD ratio band (--skip-decimate lands here)
10Framing gate (render path only)
11Collider triangle count above ceiling
12Bake did not finish or image has no data
13Export file missing or empty
14--output produced no file
15Hygiene (--stray-vert lands here)
16Grounded zmin (--lift-z)
17Stone floor poke (--poke-rock)
18Float above host (--float-rocks)
19Stone shell faces (--box-rocks)

Source

diff --git a/docs/gallery/text-version-stamp/index.html b/docs/gallery/text-version-stamp/index.html index ba6bbae8..49580076 100644 --- a/docs/gallery/text-version-stamp/index.html +++ b/docs/gallery/text-version-stamp/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); @@ -260,7 +265,7 @@

text-version-stamp

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.

Run

# 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 |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Not a FONT TextCurve with the built-in Bfont
4body is not the live version_string (--wrong-body lands here)
5Flat text is not a filled planar mesh
6Extrude / bevel closed form failed
7Appending characters did not widen the text
8Mesh survived to_mesh_clear()
9--output produced no file
10Gallery 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.

diff --git a/docs/gallery/treasure-chest/index.html b/docs/gallery/treasure-chest/index.html index 9c9fa6f1..7b5be2e8 100644 --- a/docs/gallery/treasure-chest/index.html +++ b/docs/gallery/treasure-chest/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,16 +270,16 @@

treasure-chest

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).

Budgets

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 |

+
AxisDeclaredMeasured (4.5.11 / 5.1.2 / 5.2.1)
Base triangles3600–45003852 / 3852 / 3852
LOD1 ratio0.32–0.62 of base0.5000 / 0.5000 / 0.5000
LOD2 ratio0.10–0.35 of base0.2196 / 0.2196 / 0.2129
Materialsexactly 2 distinct; ≥200 wood, ≥80 metal faces2 slots; 1608 / 318
UVsin 0..1, AABB overlap ≤ 1e-5in range, overlap 0
Outer AABB(0.764, 0.542, 0.691) m ± 0.015(0.7640, 0.5423, 0.6907), zmin 0
Collider tris≤ 200176
Exportwritten, size > 0288196 / 288196 / 288184 bytes

Hygiene

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 |

+
AxisDeclaredMeasured (all three)
Non-manifold edges00
Loose verts / edges0 / 00 / 0
Doubles merged at 1e-500
Zero-area faces00
N-gons00
Coplanar disjoint face pairs00
Grounded: zminwithin 1e-4 of 00.0000
Named supports: 4 feeteach zmin ≤ 1e-34, foot_z 0.00000
Body plan0.74 × 0.46 m ± 0.040.7400 × 0.4600

Joint fit and seat

-

| Axis | Declared | Measured (all three) | | --- | --- | --- | | Lid-knuckle to hinge barrel | ≤ 0.020 m | 0.00700 | | Band-wall BVH gap | ≤ 0.008 m | 0.00000 |

+
AxisDeclaredMeasured (all three)
Lid-knuckle to hinge barrel≤ 0.020 m0.00700
Band-wall BVH gap≤ 0.008 m0.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.

Falsifiers

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 |

+
FlagBudget violatedExit
--skip-decimateLOD1 ratio band9
--stray-vertloose vertex count is 015
--lift-zbounding box zmin is 016
--short-legsnamed foot supports at Z=016
--float-hingelid-knuckle to hinge barrel17
--narrow-bandsband-wall seat18

Run

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 |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build / no UV layer
4Base triangle count outside range
5Material count ≠ 2 distinct slots, or a face-count floor missed
6UVs outside 0..1
7UV AABB overlap above tolerance
8World AABB off declared outer size
9LOD ratio band (--skip-decimate lands here)
10Framing gate (render path only)
11Collider triangle count above ceiling
12Bake did not finish or image has no data
13Export file missing or empty
14--output produced no file
15Mesh hygiene: loose, non-manifold, zero-area, doubles, n-gons, z-fight
16Not grounded: bounding box zmin off 0, or a named foot floats
17Joint fit: lid-knuckle to hinge barrel
18Seat: band-wall gap
19Body plan off the stated real-world size

Source

diff --git a/docs/gallery/triangulate-tangents/index.html b/docs/gallery/triangulate-tangents/index.html index db87604f..bac9f1ab 100644 --- a/docs/gallery/triangulate-tangents/index.html +++ b/docs/gallery/triangulate-tangents/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); @@ -278,7 +283,7 @@

Run

blender --background --python triangulate_tangents.py -- --output buckler.png --engine cycles

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 | 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 |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Loop-triangle count ≠ closed form
4Re-fetched UV layer drifted from the polar field (--zero-uv lands here)
5Tangent basis not orthonormal
6Bitangent ≠ sign × (n × t)
7Tangents deviate from the edge/UV closed form
8Flipped tangent inside a clean triangle
10Gallery 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.

diff --git a/docs/gallery/turntable/index.html b/docs/gallery/turntable/index.html index 4f61ab76..01674264 100644 --- a/docs/gallery/turntable/index.html +++ b/docs/gallery/turntable/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); @@ -274,7 +279,7 @@

Run

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.

Exit codes

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 |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Rotation keys do not drive playback (--no-keys lands here)
4--output produced no file
5Wrong-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.

diff --git a/docs/gallery/usd-export-evaluation-mode/index.html b/docs/gallery/usd-export-evaluation-mode/index.html index e5e69ce3..49d5ad92 100644 --- a/docs/gallery/usd-export-evaluation-mode/index.html +++ b/docs/gallery/usd-export-evaluation-mode/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); @@ -271,7 +276,7 @@

Run

blender --background --python usd_export_evaluation_mode.py -- --output u.png

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; 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 |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage; also base cube verts ≠ 8
3VIEWPORT TESSELLATE point/face count off closed form
4RENDER TESSELLATE point/face count or scheme off closed form
5BEST_MATCH cage point count or scheme off
6RENDER and VIEWPORT USDA point counts are identical
10Gallery 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.

diff --git a/docs/gallery/uv-layer-grid/index.html b/docs/gallery/uv-layer-grid/index.html index 9a396906..2b2ae921 100644 --- a/docs/gallery/uv-layer-grid/index.html +++ b/docs/gallery/uv-layer-grid/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); @@ -264,7 +269,7 @@

uv-layer-grid

What it witnesses: three related contracts on the same grid topology (SEG×SEG faces, (SEG+1)² verts, size 1.0 → coords in [-1, 1]):

  • Silent no-op — 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.
  • Pre-create repair — create the UV layer first, then 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.
  • Explicit assignment fallback — 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:

-
  • *Silent no-op* — 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).
  • *Topology* — create_grid segment/size contract drift (wrong face/vert counts).
  • *Closed-form UVs* — 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).
  • *Mesh persistence* — UV layer or loop data lost across to_mesh.
  • *Explicit path* — loop assignment or the closed form itself regressing.
  • *Pixel witness* (with --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).
+
  • Silent no-op — 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).
  • Topology — create_grid segment/size contract drift (wrong face/vert counts).
  • Closed-form UVs — 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).
  • Mesh persistence — UV layer or loop data lost across to_mesh.
  • Explicit path — loop assignment or the closed form itself regressing.
  • Pixel witness (with --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).

Run

@@ -279,7 +284,7 @@

Run

blender --background --python uv_layer_grid.py -- --output uv.png --engine cycles

Exit codes

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 |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Silent-no-op hazard gone (--precreate-on-hazard lands here)
4Grid topology drifted
5Pre-create + calc_uvs did not persist one UV layer
6calc_uvs closed-form error
7Mesh UV round-trip error
8calc_uvs=False unexpectedly created a UV layer
9Explicit UV assignment error
10--output produced no file
11Broken panel is not flat
12Broken panel is not texel-(0,0) teal
13Repaired panel is not a checker
14Broken 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.

diff --git a/docs/gallery/vertex-color-ao/index.html b/docs/gallery/vertex-color-ao/index.html index 464904f1..1d9f96ef 100644 --- a/docs/gallery/vertex-color-ao/index.html +++ b/docs/gallery/vertex-color-ao/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); @@ -259,16 +264,16 @@

vertex-color-ao

-

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):

  • Analytic AO. The integrator matches 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.
  • Unoccluded is exactly 1. An isolated flat plate bakes to 1.000000000 — not approximately, exactly, because no ray can self-hit.
  • Monotone with depth. AO rises strictly across the calibration ruler, 0.508301 → 0.928467.
  • Range and spread. Every baked value on the asset lies in [0, 1] and the bake actually uses the range (measured spread 1.000000); a silently constant bake would pass a bare range check and fails this one.
  • Storage. FLOAT_COLOR round-trips the linear value exactly (0.000e+00). BYTE_COLOR does not — see below.
  • Depsgraph survival. Both attributes read back off the evaluated mesh at deviation exactly 0.0, keeping data_type and domain.
  • Reuse hygiene. Identity scales, Well.Stone.* names, no default datablocks, min z == 0.00e+00, render colour attribute pinned to AO.

Hazards found while authoring

-
  • 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.
  • Point-domain AO needs vertices to vary across. An 8-vertex slab bakes to one nearly-constant value per face, so the crevice gradient never reaches the attribute. The first draft rendered as flat pale stone with a correct bake underneath. Parts are subdivided before the bake for this reason.
  • 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.
  • Point-domain AO needs vertices to vary across. An 8-vertex slab bakes to one nearly-constant value per face, so the crevice gradient never reaches the attribute. The first draft rendered as flat pale stone with a correct bake underneath. Parts are subdivided before the bake for this reason.
  • 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 @@

Run

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).

Exit codes

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 |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Calibration AO off closed form
4Unoccluded plate is not 1.0, or AO is not strictly increasing with distance
5Bake empty, out of range, or spread too small
6FLOAT_COLOR / BYTE_COLOR round-trip contract
7Colour attributes missing, retyped, or drifted under the depsgraph
8Asset part count, unapplied scale, namespace, render attribute, or ground plane
9--output produced no file
10Gallery framing violation
11Gallery 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.

diff --git a/docs/gallery/vertex-weight-limit/index.html b/docs/gallery/vertex-weight-limit/index.html index 13de2d4a..1fb77289 100644 --- a/docs/gallery/vertex-weight-limit/index.html +++ b/docs/gallery/vertex-weight-limit/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); @@ -277,7 +282,7 @@

Run

blender --background --python vertex_weight_limit.py -- --output arm.png --engine cycles

Exit codes

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 |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Pre-limit max influences ≠ 5
4Vertex over the 4-influence cap (--skip-limit lands here)
5Limit changed nothing
6Weight sums off 1.0 after renormalize
7Pose damaged by pruning, or evaluated vert count changed
8Evaluated mesh off LBS over limited weights
9Root-weighted mount moved
10Gallery 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.

diff --git a/docs/gallery/vse-cut-list/index.html b/docs/gallery/vse-cut-list/index.html index 1a7a08d9..4f6ad0c0 100644 --- a/docs/gallery/vse-cut-list/index.html +++ b/docs/gallery/vse-cut-list/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,13 +266,13 @@

vse-cut-list

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:

-
  • Accessor rename — 5.x: sequence_editor.sequences raises AttributeError; only .strips exists. 4.5: both accessors exist and see the same strips (the transition bridge).
  • Creation signature — 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-range closed form — every strip's (start, end, duration) matches the authored end-exclusive span: 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.
  • Effect wiring — input_1/input_2 (not input1) on both versions, in authored order, and the cross clamped to exactly the T1/T2 overlap.
  • Scene strip — 4-arg 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.
  • Round-trip — spans, channels, colors, GC wiring, text, and mosaic transforms (pixel-unit offsets, tol 1e-3) all survive save_as_mainfile → open_mainfile.
  • Compositing (--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.
+
  • Accessor rename — 5.x: sequence_editor.sequences raises AttributeError; only .strips exists. 4.5: both accessors exist and see the same strips (the transition bridge).
  • Creation signature — 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-range closed form — every strip's (start, end, duration) matches the authored end-exclusive span: 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.
  • Effect wiring — input_1/input_2 (not input1) on both versions, in authored order, and the cross clamped to exactly the T1/T2 overlap.
  • Scene strip — 4-arg 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.
  • Round-trip — spans, channels, colors, GC wiring, text, and mosaic transforms (pixel-unit offsets, tol 1e-3) all survive save_as_mainfile → open_mainfile.
  • Compositing (--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:

-
  • *Accessor* — .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').
  • *Creation* — Blender re-accepting a removed kwarg silently, or the TRANSFORM enum returning (falsified: calling new_effect(frame_end=...) on 5.1.1 raises TypeError: ... expected (name, type, channel, frame_start, length, input1, input2)).
  • *Spans* — end-inclusive vs end-exclusive off-by-one, retiming drift (falsified: expecting GC span (25, 34) exited 5 with measured GC span (25, 33, 8) != closed form (25, 34, 9) — note the cross was *clamped* to the 8-frame overlap, itself a witnessed behavior).
  • *Wiring* — swapped or dropped cross inputs (falsified: input1=t2, input2=t1 exited 7 with measured inputs=(T2, T1), expected T1 -> T2).
  • *Round-trip* — serialization dropping strip data (falsified: corrupting the text strip before save exited 10 via the reloaded re-assert).
  • *Pixels* — a broken span or cell transform dropping a cell, a frozen cross, or a consumed input leaking back into the composite (found by authoring: GC below T1/T2 let T2 paint the whole wall teal).
+
  • Accessor — .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').
  • Creation — Blender re-accepting a removed kwarg silently, or the TRANSFORM enum returning (falsified: calling new_effect(frame_end=...) on 5.1.1 raises TypeError: ... expected (name, type, channel, frame_start, length, input1, input2)).
  • Spans — end-inclusive vs end-exclusive off-by-one, retiming drift (falsified: expecting GC span (25, 34) exited 5 with measured GC span (25, 33, 8) != closed form (25, 34, 9) — note the cross was clamped to the 8-frame overlap, itself a witnessed behavior).
  • Wiring — swapped or dropped cross inputs (falsified: input1=t2, input2=t1 exited 7 with measured inputs=(T2, T1), expected T1 -> T2).
  • Round-trip — serialization dropping strip data (falsified: corrupting the text strip before save exited 10 via the reloaded re-assert).
  • Pixels — a broken span or cell transform dropping a cell, a frozen cross, or a consumed input leaking back into the composite (found by authoring: GC below T1/T2 let T2 paint the whole wall teal).

Hazards discovered while authoring (all witnessed by the checks above):

  • A GAMMA_CROSS asked to outlast its inputs' overlap is silently clamped to the overlap — request length 9 over a (25, 33) overlap, get (25, 33).
  • A scene strip pointing at its own scene is a feedback loop and renders transparent (alpha 0) — the "stage" is silently absent. Source a separate scene.
  • Effect strips consume their inputs: input strips never composite on their own channel, and the effect's transform applies on top of the inputs' transforms. Consumption requires the effect on a channel above its inputs; below, they keep painting independently.
  • An empty 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).

Framing measurement

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.

Run

@@ -285,7 +290,7 @@

Run

blender --background --python vse_cut_list.py -- --output vse.png

Exit codes

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 |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3.sequences accessor contract for this Blender
4Wrong-era new_effect end kwarg or TRANSFORM type did not TypeError
5Strip span or channel off closed form
6frame_final_* deprecation bridge for this Blender
7GC wiring or strip .type enum (--swap-inputs lands here)
8Scene strip span or Stage source
9Mosaic transform, compositing defaults, text body, or strip color
10Save/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.

diff --git a/docs/gallery/vse-gamma-cross/index.html b/docs/gallery/vse-gamma-cross/index.html index fc0cd9ea..8c50ec08 100644 --- a/docs/gallery/vse-gamma-cross/index.html +++ b/docs/gallery/vse-gamma-cross/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); @@ -277,7 +282,7 @@

Run

blender --background --python vse_gamma_cross.py -- --output bench.png --engine cycles

Exit codes

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 |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3GC span off closed form
4GC inputs are not T1 → T2 (--swap-inputs lands here)
5Sample t convention drifted
6Cross sample off the gamma-0.5 closed form
7Mid-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.

diff --git a/docs/gallery/wall-torch/index.html b/docs/gallery/wall-torch/index.html index d560fbd7..12cbe307 100644 --- a/docs/gallery/wall-torch/index.html +++ b/docs/gallery/wall-torch/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); @@ -266,17 +271,17 @@

wall-torch

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.

Budgets

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 |

+
AxisDeclaredMeasured (4.5.11 / 5.1.2 / 5.2.1)
Base triangles1400–17001528 / 1528 / 1528
LOD1 ratio0.32–0.62 of base0.5000 / 0.5000 / 0.5000
LOD2 ratio0.10–0.35 of base0.2199 / 0.2199 / 0.2199
Materialsexactly 4 distinct, ≥24 stone, ≥24 metal, ≥8 flame4 slots, 540 stone, 228 metal, 78 flame
UVsin 0..1, AABB overlap ≤ 1e-5in range, overlap 0
Outer AABB(0.304, 0.293, 0.424) m ± 0.015(0.3040, 0.2930, 0.4240), zmin 0
Collider tris≤ 18060
Exportwritten, size > 0124608 / 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.

Hygiene

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 |

+
AxisDeclaredMeasured (all three)
Non-manifold edges00
Loose verts / edges0 / 00 / 0
Doubles merged at 1e-500
Zero-area faces00
N-gons00
Coplanar disjoint face pairs00
Grounded: zminwithin 1e-4 of 00.0000

Joint fit and plaque size

-

| 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 |

+
AxisDeclaredMeasured (all three)
Arm-to-cup BVH gap≤ 0.008 m0.00087
Plate-to-plaque BVH gap≤ 0.008 m0.00400
Plaque width vs WALL_W± 0.04 m0.2800 vs 0.280

Falsifiers

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 |

+
FlagBudget violatedExit
--skip-decimateLOD1 ratio band9
--stray-vertloose vertex count is 015
--lift-zbounding box zmin is 016
--float-armarm-to-cup BVH gap17
--float-plateplate-to-plaque BVH gap18
--skinny-plaqueplaque width vs WALL_W19

--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.

Run

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) |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build / no UV layer
4Base triangle count outside range
5Material count ≠ 4 distinct slots, or stone/metal/flame faces missing
6UVs outside 0..1
7UV AABB overlap above tolerance
8World AABB off declared outer size
9LOD ratio band (--skip-decimate lands here)
10Framing gate (render path only)
11Collider triangle count above ceiling
12Bake did not finish or image has no data
13Export file missing or empty
14--output produced no file
15Hygiene (--stray-vert lands here)
16Grounded zmin (--lift-z)
17Arm-to-cup gap (--float-arm)
18Plate-to-plaque gap (--float-plate)
19Plaque width (--skinny-plaque)

Source

diff --git a/docs/gallery/watchtower/index.html b/docs/gallery/watchtower/index.html index ac1c5b0b..4d21a982 100644 --- a/docs/gallery/watchtower/index.html +++ b/docs/gallery/watchtower/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 @@

watchtower

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).

Budgets

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 |

+
AxisDeclaredMeasured (4.5.11 / 5.1.2 / 5.2.1)
Base triangles6200–82006408 / 6408 / 6408
LOD1 ratio0.32–0.62 of base0.5000 / 0.5000 / 0.5000
LOD2 ratio0.10–0.35 of base0.2197 / 0.2197 / 0.2185
Materialsexactly 3 distinct; ≥80 metal, ≥40 roof faces3 slots; 300 metal, 126 roof
UVsin 0..1, AABB overlap ≤ 1e-5in range, overlap 0
Outer AABB(1.594, 1.594, 2.973) m ± 0.01(1.5940, 1.5940, 2.9731), zmin 0
Collider tris≤ 180150
Exportwritten, size > 0464572 / 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.

Hygiene

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 |

+
AxisDeclaredMeasured (all three)
Non-manifold edges00
Loose verts / edges0 / 00 / 0
Doubles merged at 1e-500
Zero-area faces00
N-gons00
Coplanar disjoint face pairs00
Grounded: zminwithin 1e-4 of 00.0000
Named supports: 4 iron shoeseach zmin ≤ 1e-34, shoe_z 0.00000
Plan1.56 m eave × 2.97 m height ± 0.08 / ± 0.281.5940 × 2.9731

Joint fit and seat

-

| 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 |

+
AxisDeclaredMeasured (all three)
Rail-to-post engage≥ 0.012 m0.0180
Girt-collar standoff≤ 0.010 m0.00200
Post plumb (XY drift)≤ 0.010 m0.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.

Falsifiers

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 |

+
FlagBudget violatedExit
--skip-decimateLOD1 ratio band9
--stray-vertloose vertex count is 015
--lift-zbounding box zmin is 016
--short-shoesnamed shoe supports at Z=016
--short-railsrail-to-post joint fit17
--float-bandiron-collar seat18
--rake-postspost plumb19

Run

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 |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build / no UV layer
4Base triangle count outside range
5Material count ≠ 3 distinct slots, or metal/roof faces missing
6UVs outside 0..1
7UV AABB overlap above tolerance
8World AABB off declared outer size
9LOD ratio band (--skip-decimate lands here)
10Framing gate (render path only)
11Collider triangle count above ceiling
12Bake did not finish or image has no data
13Export file missing or empty
14--output produced no file
15Mesh hygiene: loose, non-manifold, zero-area, doubles, n-gons, z-fight
16Not grounded: bounding box zmin off 0, or a named shoe floats
17Joint fit: rail-to-post engage
18Seat: girt-collar standoff
19Post plumb or plan off the stated real-world size

Source

diff --git a/docs/gallery/water-trough/index.html b/docs/gallery/water-trough/index.html index 814ccf54..a34d720c 100644 --- a/docs/gallery/water-trough/index.html +++ b/docs/gallery/water-trough/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); @@ -266,18 +271,18 @@

water-trough

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.

Budgets

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 |

+
AxisDeclaredMeasured (4.5.11 / 5.1.2 / 5.2.1)
Base triangles3300–40003700 / 3700 / 3700
LOD1 ratio0.32–0.62 of base0.5000 / 0.5000 / 0.5000
LOD2 ratio0.10–0.35 of base0.2200 / 0.2200 / 0.2200
Materialsexactly 3 distinct; ≥24 wood, ≥24 metal, ≥6 water3 slots; 1674 / 156 / 29
UVsin 0..1, AABB overlap ≤ 1e-5in range, overlap 0
Outer AABB(1.093, 0.552, 0.504) m ± 0.01(1.0932, 0.5520, 0.5040), zmin 0
Collider tris≤ 8064
Exportwritten, size > 0268840 / 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.

Hygiene

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 |

+
AxisDeclaredMeasured (all three)
Non-manifold edges00
Loose verts / edges0 / 00 / 0
Doubles merged at 1e-500
Zero-area faces00
N-gons00
Coplanar disjoint face pairs00
Grounded: zminwithin 1e-4 of 00.0000
Named supports: 4 shoeseach zmin ≤ 0.0014, shoe_z 0.00000
Hull plan1.08 m × 0.44 m ± 0.081.0448 × 0.4661

Joint fit and seat

-

| 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 |

+
AxisDeclaredMeasured (all three)
End-cap rim Z-span≤ 0.080 m0.0050
Strap-to-hull BVH gap≤ 0.008 m0.00000
Water-to-hull BVH gap≤ 0.008 m0.00260

Falsifiers

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 |

+
FlagBudget violatedExit
--skip-decimateLOD1 ratio band9
--stray-vertloose vertex count is 015
--lift-zbounding box zmin is 016
--short-legsnamed shoe supports at Z=016
--box-endsend-cap rim span17
--float-strapstrap-to-hull gap18
--narrow-hullhull plan vs stated size19

--short-legs lifts the shoes only, so AABB zmin stays 0 (the legs still plant) and the named-support gate is what fires.

Run

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) |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build / no UV layer
4Base triangle count outside range
5Material count ≠ 3 distinct slots, or a face-count floor missed
6UVs outside 0..1
7UV AABB overlap above tolerance
8World AABB off declared outer size
9LOD ratio band (--skip-decimate lands here)
10Framing gate (render path only)
11Collider triangle count above ceiling
12Bake did not finish or image has no data
13Export file missing or empty
14--output produced no file
15Mesh hygiene: loose, non-manifold, zero-area, doubles, n-gons, z-fight
16Not grounded: bounding box zmin off 0, or a named shoe floats
17Joint fit: end-cap U-rim span (--box-ends)
18Seat: strap or water BVH gap (--float-strap)
19Hull length or width off the stated real-world size (--narrow-hull)

Source

diff --git a/docs/gallery/wave-displace/index.html b/docs/gallery/wave-displace/index.html index daf0cef9..f2b5c7da 100644 --- a/docs/gallery/wave-displace/index.html +++ b/docs/gallery/wave-displace/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); @@ -260,7 +265,7 @@

wave-displace

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.

Framing deviation

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".

Run

@@ -275,7 +280,7 @@

Run

blender --background --python wave_displace.py -- --output wave.png --engine cycles

Exit codes

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 |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
4Z-span not in the closed-form band (--flat lands here)
5A 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.

diff --git a/docs/gallery/wheelbarrow/index.html b/docs/gallery/wheelbarrow/index.html index 19e48bbd..68a05e2d 100644 --- a/docs/gallery/wheelbarrow/index.html +++ b/docs/gallery/wheelbarrow/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,16 +270,16 @@

wheelbarrow

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).

Budgets

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 |

+
AxisDeclaredMeasured (4.5.11 / 5.1.2 / 5.2.1)
Base triangles2300–28002500 / 2500 / 2500
LOD1 ratio0.32–0.62 of base0.5000 / 0.5000 / 0.5000
LOD2 ratio0.10–0.35 of base0.2200 / 0.2200 / 0.2104
Materialsexactly 2 distinct; ≥700 wood, ≥180 metal faces2 slots; 1094 / 190
UVsin 0..1, AABB overlap ≤ 1e-5in range, overlap 0
Outer AABB(1.558, 0.630, 0.574) m ± 0.015(1.5556, 0.6300, 0.5720), zmin 0
Collider tris≤ 22090
Exportwritten, size > 0187576 / 187576 / 187560 bytes

Hygiene

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) |

+
AxisDeclaredMeasured (all three)
Non-manifold edges00
Loose verts / edges0 / 00 / 0
Doubles merged at 1e-500
Zero-area faces00
N-gons00
Coplanar disjoint face pairs00
Grounded: zminwithin 1e-4 of 00.0000
Named supports: 2 shoes + tyreeach zmin ≤ 1e-3shoe_z 0.00000, tyre_z 0.00000
Tray size(0.720, 0.532, 0.220) m ± 0.02(0.7200, 0.5320, 0.2200)

Joint fit and seat

-

| 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 |

+
AxisDeclaredMeasured (all three)
Spoke clearance (hub − spoke)≥ 0.010 m0.04000
Handle-tray overlapgap ≤ 0.020 m−0.07062
Wall-floor seat≥ 0.005 m0.01200
Metal-wood BVH gap≤ 0.010 m0.00000
Tread aspect (tyre width / radial)≥ 2.55.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.

Falsifiers

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 |

+
FlagBudget violatedExit
--skip-decimateLOD1 ratio band9
--stray-vertloose vertex count is 015
--lift-zbounding box zmin is 016
--short-legsnamed shoe supports at Z=016
--fat-spokesspoke clearance inside the hub17
--float-wallswall-floor seat17
--pipe-rimtread aspect (torus on a flat felloe)18

Run

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 |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build / no UV layer
4Base triangle count outside range
5Material count ≠ 2 distinct slots, or a face-count floor missed
6UVs outside 0..1
7UV AABB overlap above tolerance
8World AABB off declared outer size
9LOD ratio band (--skip-decimate lands here)
10Framing gate (render path only)
11Collider triangle count above ceiling
12Bake did not finish or image has no data
13Export file missing or empty
14--output produced no file
15Mesh hygiene: loose, non-manifold, zero-area, doubles, n-gons, z-fight
16Not grounded: bounding box zmin off 0, or a named shoe/tyre floats
17Joint fit: spoke clearance, handle join, wall-floor seat, or metal-wood gap
18Seat: tread aspect of the tyre
19Tray size off the stated real-world dimensions

Source

diff --git a/docs/gallery/wooden-barrel/index.html b/docs/gallery/wooden-barrel/index.html index 2a54c990..5dfa7542 100644 --- a/docs/gallery/wooden-barrel/index.html +++ b/docs/gallery/wooden-barrel/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 @@

wooden-barrel

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).

Budgets

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 |

+
AxisDeclaredMeasured (4.5.11 / 5.1.2 / 5.2.1)
Base triangles6000–85007216 / 7216 / 7216
LOD1 ratio0.32–0.62 of base0.5000 / 0.5000 / 0.5000
LOD2 ratio0.10–0.35 of base0.2198 / 0.2198 / 0.2198
Materialsexactly 2 distinct; ≥400 wood, ≥200 metal faces2 slots; 2512 / 1440
UVsin 0..1, AABB overlap ≤ 1e-5in range, overlap 0
Outer AABB(0.722, 0.714, 0.880) m ± 0.015(0.7220, 0.7142, 0.8800), zmin 0
Collider tris≤ 400238
Exportwritten, size > 0544140 / 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.

Hygiene

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 |

+
AxisDeclaredMeasured (all three)
Non-manifold edges00
Loose verts / edges0 / 00 / 0
Doubles merged at 1e-500
Zero-area faces00
N-gons00
Coplanar disjoint face pairs00
Grounded: zminwithin 1e-4 of 00.0000
Named supports: 20 staveseach zmin ≤ 0.00120, stave_z 0.00000
Body plan0.714 m dia × 0.880 m high ± 0.040.7142 × 0.8800

Joint fit and seat

-

| 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 |

+
AxisDeclaredMeasured (all three)
Head-croze rim BVH gap≤ 0.010 m0.00698
Hoop bite (host r − inner hoop r)0.0012–0.008 m0.00300

Falsifiers

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 |

+
FlagBudget violatedExit
--skip-decimateLOD1 ratio band9
--stray-vertloose vertex count is 015
--lift-zbounding box zmin is 016
--short-stavesnamed stave supports at Z=016
--float-headhead-croze gap17
--round-bandhoop bite band18

Run

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 |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build / no UV layer
4Base triangle count outside range
5Material count ≠ 2 distinct slots, or a face-count floor missed
6UVs outside 0..1
7UV AABB overlap above tolerance
8World AABB off declared outer size
9LOD ratio band (--skip-decimate lands here)
10Framing gate (render path only)
11Collider triangle count above ceiling
12Bake did not finish or image has no data
13Export file missing or empty
14--output produced no file
15Mesh hygiene: loose, non-manifold, zero-area, doubles, n-gons, z-fight
16Not grounded: bounding box zmin off 0, or a named stave floats
17Joint fit: head-croze gap (--float-head)
18Seat: hoop bite band (--round-band)
19Body diameter or height off the stated real-world size

Source

diff --git a/docs/gallery/wooden-bucket/index.html b/docs/gallery/wooden-bucket/index.html index d32eb80a..8082e3cc 100644 --- a/docs/gallery/wooden-bucket/index.html +++ b/docs/gallery/wooden-bucket/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 @@

wooden-bucket

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).

Budgets

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 |

+
AxisDeclaredMeasured (4.5.11 / 5.1.2 / 5.2.1)
Base triangles4000–58004872 / 4872 / 4872
LOD1 ratio0.32–0.62 of base0.5000 / 0.5000 / 0.5000
LOD2 ratio0.10–0.35 of base0.2196 / 0.2196 / 0.2188
Materialsexactly 3 distinct; ≥350 wood, ≥180 metal, ≥40 rope3 slots; 1424 / 1020 / 208
UVsin 0..1, AABB overlap ≤ 1e-5in range, overlap 0
Outer AABB(0.383, 0.346, 0.527) m ± 0.015(0.3832, 0.3462, 0.5271), zmin 0
Collider tris≤ 420302
Exportwritten, size > 0378296 / 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.

Hygiene

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 |

+
AxisDeclaredMeasured (all three)
Non-manifold edges00
Loose verts / edges0 / 00 / 0
Doubles merged at 1e-500
Zero-area faces00
N-gons00
Coplanar disjoint face pairs00
Grounded: zminwithin 1e-4 of 00.0000
Named supports: 16 staveseach zmin ≤ 0.00116, stave_z 0.00000
Body plan0.335 m dia × 0.360 m high ± 0.040.3349 × 0.3600

Joint fit and seat

-

| 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 |

+
AxisDeclaredMeasured (all three)
Bail-ear BVH gap≤ 0.006 m0.00125
Floor-croze rim BVH gap≤ 0.008 m0.00458
Hoop bite (host r − inner hoop r)0.0010–0.007 m0.00250

Falsifiers

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 |

+
FlagBudget violatedExit
--skip-decimateLOD1 ratio band9
--stray-vertloose vertex count is 015
--lift-zbounding box zmin is 016
--short-stavesnamed stave supports at Z=016
--float-handlebail-ear gap17
--float-bottomfloor-croze gap17
--round-bandhoop bite band18

Run

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 |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build / no UV layer
4Base triangle count outside range
5Material count ≠ 3 distinct slots, or a face-count floor missed
6UVs outside 0..1
7UV AABB overlap above tolerance
8World AABB off declared outer size
9LOD ratio band (--skip-decimate lands here)
10Framing gate (render path only)
11Collider triangle count above ceiling
12Bake did not finish or image has no data
13Export file missing or empty
14--output produced no file
15Mesh hygiene: loose, non-manifold, zero-area, doubles, n-gons, z-fight
16Not grounded: bounding box zmin off 0, or a named stave floats
17Joint fit: bail-ear gap or floor-croze gap
18Seat: hoop bite band (--round-band)
19Body diameter or height off the stated real-world size

Source

diff --git a/docs/gallery/wooden-ladder/index.html b/docs/gallery/wooden-ladder/index.html index 65b0b480..ec0ecf77 100644 --- a/docs/gallery/wooden-ladder/index.html +++ b/docs/gallery/wooden-ladder/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,12 +270,12 @@

wooden-ladder

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.

Budgets

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 |

+
AxisDeclaredMeasured (4.5.11 / 5.1.2 / 5.2.1)
Base triangles1800–28002536 / 2536 / 2536
LOD1 ratio0.32–0.62 of base0.5000 / 0.5000 / 0.5000
LOD2 ratio0.10–0.35 of base0.2192 / 0.2192 / 0.2192
Materialsexactly 2 distinct, ≥280 wood, ≥180 metal2 slots, 612 wood, 736 metal
UVsin 0..1, AABB overlap ≤ 1e-5in range, overlap 0
Outer AABB(0.490, 0.376, 1.487) m ± 0.01(0.4897, 0.3760, 1.4870)
Groundedabs(zmin) ≤ 1e-4, each sole at 0, rail end ≥ 0.012 mzmin 0, sole 0, rail 0.02037
Mesh hygienenon-manifold, loose v/e, doubles at 1e-5, zero-area, n-gons, coplanar pairs all 0all 0
Parts16 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 faces0.00800
Tenon engagement≥ 0.008 m past the stile inner face0.01399
Tenon breakout margin≥ 0.008 m short of the stile outer face0.03780
Shoe bite / coverbite ≥ 0.010 m, cover ≥ 0.016 m0.02000 / 0.03800
Collider tris≤ 18024
Exportwritten, size > 0187528 / 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 |

+
FalsifierBudget violatedExitMeasured failure
--skip-decimateLOD1 ratio band9ratio 1.0000
--stray-vertMesh hygiene, loose verts15loose_v=1
--twin-soleCoplanar face pairs15zfight=6
--lift-zGrounded zmin16zmin 0.050000
--fat-rungsRung depth clearance17clearance 0
--short-stileShoe bite / cover18bite 0.070, cover −0.012

Default exit is 0 on all three.

Run

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) |

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build / no UV layer
4Base triangle count outside range
5Material count ≠ 2 distinct slots, or wood/metal faces missing
6UVs outside 0..1
7UV AABB overlap above tolerance
8World AABB off declared outer size
9LOD ratio band (--skip-decimate lands here)
10Framing gate (render path only)
11Collider triangle count above ceiling
12Bake did not finish or image has no data
13Export file missing or empty
14--output produced no file
15Mesh hygiene, including coplanar pairs (--stray-vert, --twin-sole)
16Grounded zmin, or a rail end sitting in the tread (--lift-z)
17Part count or rung-to-stile joint fit (--fat-rungs)
18Shoe bite / cover (--short-stile)

Source

diff --git a/scripts/build_gallery.py b/scripts/build_gallery.py index acda7b70..d0f6f9d2 100644 --- a/scripts/build_gallery.py +++ b/scripts/build_gallery.py @@ -322,8 +322,13 @@ def assert_alts_survive_dotted_paths(examples: list) -> None: .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); @@ -646,11 +651,21 @@ def offset(row: int, col: int) -> int: # --------------------------------------------------------------------------- # Minimal Markdown renderer for the example READMEs (headings, paragraphs, -# unordered lists, fenced code, inline code/bold/links). Relative links are -# resolved against the example's directory on GitHub. +# ordered and unordered lists, pipe tables, fenced code, inline +# code/bold/emphasis/links/images). Relative links are resolved against the +# example's directory on GitHub. # --------------------------------------------------------------------------- -_INLINE = re.compile(r"`([^`]+)`|\*\*(.+?)\*\*|\[([^\]]+)\]\(([^)]+)\)") +_INLINE = re.compile( + r"`([^`]+)`" + r"|\*\*(.+?)\*\*" + r"|(? str: @@ -662,14 +677,46 @@ def render_inline(text: str, resolve) -> str: out.append(f"{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(c, resolve)}' for c in head) + trs = "".join( + "" + "".join(f"{render_inline(c, resolve)}" for c in r) + "" + for r in rows) + out.append(f'
{ths}' + f"{trs}
") + continue + + ordered = bool(_OL_ITEM.match(stripped)) + if stripped.startswith("- ") or ordered: + marker = _OL_ITEM if ordered else re.compile(r"^- ") items: list[str] = [] while i < len(lines): cur = lines[i].strip() - if cur.startswith("- "): - items.append(cur[2:]) + mm = marker.match(cur) + if mm: + items.append(cur[mm.end():]) elif cur and lines[i].startswith(" ") and items: items[-1] += " " + cur # wrapped continuation line else: break i += 1 lis = "".join(f"
  • {render_inline(it, resolve)}
  • " for it in items) - out.append(f"
      {lis}
    ") + tag = "ol" if ordered else "ul" + out.append(f"<{tag}>{lis}") continue para: list[str] = [stripped] i += 1 while i < len(lines): - nxt = lines[i].strip() - if not nxt or nxt.startswith(("#", "- ", "```")): + if not lines[i].strip() or _starts_block(lines, i): break - para.append(nxt) + para.append(lines[i].strip()) i += 1 out.append(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 @@
    - {% if skill_count %}Skills0{% endif %} - {% if rule_count %}Rules0{% endif %} - {% if snippet_count %}Snippets0{% endif %} - {% if template_count %}Templates0{% endif %} - {% if example_count %}Examples0{% endif %} - {% if showcase_count %}Showcase0{% endif %} + {% if skill_count %}Skills{{ skill_count }}{% endif %} + {% if rule_count %}Rules{{ rule_count }}{% endif %} + {% if snippet_count %}Snippets{{ snippet_count }}{% endif %} + {% if template_count %}Templates{{ template_count }}{% endif %} + {% if example_count %}Examples{{ example_total }}{% endif %} + {% if showcase_count %}Showcase{{ showcase_count }}{% endif %}
    - smoke-gated on 4.5 LTS + 5.1 · exit 0 + smoke-gated on 5.2 LTS + 4.5 LTS · exit 0

    Blender Python
    that actually runs.

    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.

    + backed by runnable examples asserted headless on Blender 5.2 LTS and 4.5 LTS before they ship.

    $ git clone {{ site.links.github | default(plugin.repository) }}
    @@ -299,11 +299,11 @@ {% if featured_examples %}{{ featured_count }} of {% endif %}{{ example_count }} frames
    -

    Every render here is a CI artifact.

    -

    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 →

    +

    Every render here is code that runs.

    +

    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.

    +

    Browse all {{ example_count }} rendered examples and {{ showcase_count }} showcase pieces in the gallery →

    {% for ex in shown %} @@ -332,7 +332,7 @@

    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 →