Skip to content

Commit 56b7825

Browse files
TMHSDigitalclaude
andauthored
fix(showcase): hay-bale quality pass with five new hygiene budgets, and exit-pre-sidecar doc accuracy (#205)
* fix(showcase): hay-bale visual quality pass with five new hygiene budgets The inspection sheet found nine defects that every shipped budget passed. The belts were constant rectangles hung on a rounded, bulged loaf, so they stood proud at the middle of each face, were swallowed at the corners, and stopped 2.9-8.3 mm above the floor with a visible notch -- while the AABB zmin gate stayed green because the loaf grounded the box. The two belts were 5.4 mm out of step because the loaf's ridge term was odd in x. Nothing cinched: the waist measured 0.1 mm. All 426 faces were smooth-shaded, which erased every ridge and nap and left a bar of soap. Fixed structurally, not by nudging: - Build, bevel and ground the loaf FIRST, then raycast its finished cross-section per belt station and follow that profile at a named bite. Moving a belt now moves its seat with it. - Every closed-form shaping term is even in x (cos, not sin), so the two stations sample identical surface heights. - The cinch is derived from BELT_XS, and the ridge fades out under each belt because twine compresses straw flat where it bites. - Shading decided per part: flat loaf, smooth cord. - Straw detail moved to the high mesh, where the bake stage already exists to carry it onto the low mesh. Five budgets so none of it can come back, all recomputed from the generated mesh: cross-shell coplanar z-fight pairs (15), per-belt wrap zmin (16), shell count (17), banded per-station belt seat depth (18), belt mirror symmetry and cinch depth (19). Four new falsifiers, each landing on the budget it targets, proven on 4.5.11, 5.1.2 and 5.2.1: --stray-vert 15, --float-belts 16, --slack-belt 18, --skew-belt 19. --stray-vert parks its vertex inside the bale because above it the bbox budget fired first; --skew-belt shifts along X because a Z skew tripped the z-fight budget instead. Also deletes the temp glTF once its size is measured: Blender's TMPDIR resolves to the working directory on a portable build, so every run left a .glb in the repo root. Geometry is identical on all three binaries. The piece still reads as a wrapped parcel rather than straw at hero scale; that is recorded in the README as a material and bake problem, not a geometry one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> * docs(exit-pre-sidecar): correct a false catalog claim and document the falsifiers The module docstring told readers the catalog's min_version was 5.0 and wrong. It has been 5.1 since the example's introducing commit, so the note was never true and sent anyone who read it to "fix" a correct catalog row. Re-verified the version claim against all three binaries: hasattr(bpy.app.handlers, "exit_pre") is False on 4.5.11 LTS and True on 5.1.2 and 5.2.1 LTS, and exit_pre is the only exit- or quit-related name in bpy.app.handlers on the versions that have it -- so there is no older spelling to fall back to. The documented behaviour is correct. README also now: - states the contract and why it matters (atexit is the bug class this catches), with the 4.5 and 5.2 bpy.app.handlers API links - explains that exiting 77 on 4.5 is correct by design, and what stops a skip from spreading: min_version 5.1, the runner failing a skip above the floor, and summarize.py reddening an all-skip job - enumerates the falsifiers in a table with what each breaks, its script exit and how the harness fails, and names --write-in-main and --atexit-instead as the two an existence-only check would pass, which is why the catalog row carries sidecar_contains Contract, assertions and exit codes unchanged. Verified before and after on 4.5.11, 5.1.2 and 5.2.1: default SKIP/PASS/PASS, all five harness falsifiers FAIL, --force-run exits 2 on 4.5, plain default exits 77. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> * docs(showcase): record the mirror-symmetry and wrapper-seating conventions Three conventions every future piece inherits, each from a defect the hay-bale pass found that no existing budget could see. Even shaping terms and mirror symmetry (19): where a prop has mirrored members, every closed-form term in the host's shaping function must be even in the mirrored axis. hay-bale used sin(x*24), which is odd, so its two belt stations sampled different surface heights and the wraps shipped 5.4 mm out of step. Pair the mirrored shells and compare. Wrappers follow the host's profile, and the host is finished first (16/18): build, bevel and ground the host, then hang the wrapper on the finished surface by raycasting the host's own cross-section. A constant rectangle on a rounded host stands proud at the middle of each face and is swallowed at the corners. Assert per wrapper shell that the loop passes under the host, because the AABB zmin gate stays green when the host grounds the box. Seat depth is measured station-locally, along the vertex's own direction from the station axis, never by nearest-surface distance: a wrapper in a concave waist has its nearest host face on the bulge shoulder at a neighbouring station. Bin by rounding to the nearest station, never by flooring, or a station on a bin boundary splits its inner and outer rings and the inner-only bin reports the outer offset as the seat depth. Also sharpens the z-fight budget: count coplanar pairs cross-shell, not merely over faces sharing no vertex. Two quads two steps apart on one flat cap share no vertex and are coplanar by construction -- that read 96 pairs on hay-bale, none of them a hazard, which is the same unsatisfiable-budget trap the surrounding paragraph already warns about. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> --------- Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent e2fb2fc commit 56b7825

16 files changed

Lines changed: 1563 additions & 284 deletions

File tree

6.18 KB
Loading
48.2 KB
Loading

‎docs/gallery/gn-sdf-remesh/index.html‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -401,7 +401,7 @@ <h2>Source</h2>
401401
<span class="c"># EEVEE-id inversion witnessed for real: the OTHER era&#x27;s id must be</span>
402402
<span class="c"># rejected by this build, the helper&#x27;s accepted</span>
403403
eid = get_eevee_engine_id()
404-
wrong = <span class="s">&#x27;BLENDER_EEVEE_NEXT&#x27;</span> <span class="k">if</span> bpy.app.version &gt;= (<span class="n">5</span>, <span class="n">0</span>, <span class="n">0</span>) <span class="k">else</span> <span class="s">&#x27;BLENDER_EEVEE&#x27;</span>
404+
wrong = <span class="s">&#x27;BLENDER_EEVEE_NEXT&#x27;</span> <span class="k">if</span> bpy.app.version &gt;= (<span class="n">5</span>, <span class="n">0</span>, <span class="n">0</span>) <span class="k">else</span> <span class="s">&#x27;BLENDER_EEVEE&#x27;</span> <span class="c"># engine-id-exempt: the wrong-era id this example asserts is rejected</span>
405405
<span class="k">try</span>:
406406
bpy.context.scene.render.engine = wrong
407407
print(<span class="s">f&quot;</span><span class="s">ERROR: wrong-era EEVEE id &#x27;</span>{wrong}<span class="s">&#x27; was accepted</span><span class="s">&quot;</span>, file=sys.stderr); <span class="k">return</span> <span class="n">5</span>

‎docs/gallery/hay-bale/index.html‎

Lines changed: 660 additions & 113 deletions
Large diffs are not rendered by default.

‎docs/gallery/index.html‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1162,12 +1162,12 @@ <h2><a href="wooden-ladder/">wooden-ladder</a></h2>
11621162
</article>
11631163
<article class="card" data-tags="mesh export showcase">
11641164
<a class="card-media" href="hay-bale/" aria-label="hay-bale showcase piece detail page">
1165-
<img src="assets/hay-bale-hero.webp" alt="hay-bale — A procedural bound hay bale with a cinched pillow loaf, end nap, and two sisal twine belts through UVs, bake, LOD, collider, and Unity glTF, asserting…" loading="lazy" decoding="async" />
1165+
<img src="assets/hay-bale-hero.webp" alt="hay-bale — A bound hay bale whose twine belts follow the loaf&#x27;s own raycast cross-section, asserting recomputed geometry budgets, not an API contract." loading="lazy" decoding="async" />
11661166
</a>
11671167
<div class="card-body">
11681168
<h2><a href="hay-bale/">hay-bale</a></h2>
1169-
<p class="teaches">A procedural bound hay bale with a cinched pillow loaf, end nap, and two sisal twine belts through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.</p>
1170-
<p class="witnesses"><span class="tag">witnesses</span> Recomputed: 852 tris, two materials with 294 hay and 132 twine faces, UVs in 0..1 with zero AABB overlap, outer AABB 0.920×0.517×0.426 m, zmin 0, hygiene 0, hay-twine BVH gap 0.07 mm, LOD ratios in band, convex collider 78 tris, non-empty glTF. --skip-decimate exits 9 on LOD1; --lift-z exits 16 on grounded zmin.</p>
1169+
<p class="teaches">A bound hay bale whose twine belts follow the loaf&#x27;s own raycast cross-section, asserting recomputed geometry budgets, not an API contract.</p>
1170+
<p class="witnesses"><span class="tag">witnesses</span> Recomputed: 2012 tris, two materials with 712 hay and 304 twine faces, UVs in 0..1 with zero AABB overlap, outer AABB 0.921x0.530x0.409 m, zmin 0, hygiene and cross-shell z-fight 0, 5 shells, each belt wrapping under the loaf at zmin 2.6 mm, banded belt seat depth 4.0-16.8 mm over 20/20 stations on both belts, belt mirror error 0, cinch depth 14.2 mm, LOD ratios in band, convex collider 156 tris, non-empty glTF. --skip-decimate exits 9; --stray-vert 15; --lift-z and --float-belts 16; --slack-belt 18; --skew-belt 19.</p>
11711171
<a class="card-link" href="hay-bale/">View showcase piece <span aria-hidden="true">&rarr;</span></a>
11721172
</div>
11731173
</article>

‎docs/gallery/iron-cauldron/index.html‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -413,7 +413,14 @@ <h2>Source</h2>
413413

414414

415415
<span class="k">def</span> eevee_engine_id():
416-
<span class="k">return</span> <span class="s">&quot;BLENDER_EEVEE_NEXT&quot;</span> <span class="k">if</span> bpy.app.version &gt;= (<span class="n">4</span>, <span class="n">2</span>, <span class="n">0</span>) <span class="k">else</span> <span class="s">&quot;BLENDER_EEVEE&quot;</span>
416+
<span class="s">&quot;&quot;&quot;EEVEE id: &#x27;BLENDER_EEVEE&#x27; on 5.0+, &#x27;BLENDER_EEVEE_NEXT&#x27; on 4.2-4.5.
417+
418+
Mapping matches examples/swatch-grid.get_eevee_engine_id, which asserts it
419+
against the running build (copied, not imported). Inherited the inverted
420+
form from showcase/shipping-crate; it returned &#x27;BLENDER_EEVEE_NEXT&#x27; on 5.x,
421+
where that id does not exist.
422+
&quot;&quot;&quot;</span>
423+
<span class="k">return</span> <span class="s">&quot;BLENDER_EEVEE&quot;</span> <span class="k">if</span> bpy.app.version &gt;= (<span class="n">5</span>, <span class="n">0</span>, <span class="n">0</span>) <span class="k">else</span> <span class="s">&quot;BLENDER_EEVEE_NEXT&quot;</span>
417424

418425

419426
<span class="k">def</span> fail(msg, code):

‎docs/gallery/shipping-crate/index.html‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -399,7 +399,14 @@ <h2>Source</h2>
399399

400400

401401
<span class="k">def</span> eevee_engine_id():
402-
<span class="k">return</span> <span class="s">&quot;BLENDER_EEVEE_NEXT&quot;</span> <span class="k">if</span> bpy.app.version &gt;= (<span class="n">4</span>, <span class="n">2</span>, <span class="n">0</span>) <span class="k">else</span> <span class="s">&quot;BLENDER_EEVEE&quot;</span>
402+
<span class="s">&quot;&quot;&quot;EEVEE id: &#x27;BLENDER_EEVEE&#x27; on 5.0+, &#x27;BLENDER_EEVEE_NEXT&#x27; on 4.2-4.5.
403+
404+
Mapping matches examples/swatch-grid.get_eevee_engine_id, which asserts it
405+
against the running build (copied, not imported). The old form here keyed
406+
on &gt;= (4, 2, 0) and so returned &#x27;BLENDER_EEVEE_NEXT&#x27; on 5.x, where that id
407+
does not exist — the render path raised TypeError on 5.1 and 5.2.
408+
&quot;&quot;&quot;</span>
409+
<span class="k">return</span> <span class="s">&quot;BLENDER_EEVEE&quot;</span> <span class="k">if</span> bpy.app.version &gt;= (<span class="n">5</span>, <span class="n">0</span>, <span class="n">0</span>) <span class="k">else</span> <span class="s">&quot;BLENDER_EEVEE_NEXT&quot;</span>
403410

404411

405412
<span class="k">def</span> fail(msg, code):

‎docs/gallery/swatch-grid/index.html‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -556,7 +556,7 @@ <h2>Source</h2>
556556
<span class="c"># Cycles. Witness the inversion for real: the OTHER era&#x27;s id must be rejected</span>
557557
<span class="c"># by this build, and the helper&#x27;s id must be accepted.</span>
558558
eid = get_eevee_engine_id()
559-
wrong = <span class="s">&#x27;BLENDER_EEVEE_NEXT&#x27;</span> <span class="k">if</span> bpy.app.version &gt;= (<span class="n">5</span>, <span class="n">0</span>, <span class="n">0</span>) <span class="k">else</span> <span class="s">&#x27;BLENDER_EEVEE&#x27;</span>
559+
wrong = <span class="s">&#x27;BLENDER_EEVEE_NEXT&#x27;</span> <span class="k">if</span> bpy.app.version &gt;= (<span class="n">5</span>, <span class="n">0</span>, <span class="n">0</span>) <span class="k">else</span> <span class="s">&#x27;BLENDER_EEVEE&#x27;</span> <span class="c"># engine-id-exempt: the wrong-era id this example asserts is rejected</span>
560560
<span class="k">try</span>:
561561
sc.render.engine = wrong
562562
print(<span class="s">f&quot;</span><span class="s">ERROR: wrong-era EEVEE id &#x27;</span>{wrong}<span class="s">&#x27; was accepted by this build — </span><span class="s">&quot;</span>

‎docs/gallery/turntable/index.html‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -420,7 +420,7 @@ <h2>Source</h2>
420420
<span class="c"># the EEVEE-id mapping is asserted regardless of whether we render: the</span>
421421
<span class="c"># OTHER era&#x27;s id must be rejected by this build, the helper&#x27;s accepted</span>
422422
eid = get_eevee_engine_id()
423-
wrong = <span class="s">&#x27;BLENDER_EEVEE_NEXT&#x27;</span> <span class="k">if</span> bpy.app.version &gt;= (<span class="n">5</span>, <span class="n">0</span>, <span class="n">0</span>) <span class="k">else</span> <span class="s">&#x27;BLENDER_EEVEE&#x27;</span>
423+
wrong = <span class="s">&#x27;BLENDER_EEVEE_NEXT&#x27;</span> <span class="k">if</span> bpy.app.version &gt;= (<span class="n">5</span>, <span class="n">0</span>, <span class="n">0</span>) <span class="k">else</span> <span class="s">&#x27;BLENDER_EEVEE&#x27;</span> <span class="c"># engine-id-exempt: the wrong-era id this example asserts is rejected</span>
424424
<span class="k">try</span>:
425425
bpy.context.scene.render.engine = wrong
426426
print(<span class="s">f&quot;</span><span class="s">ERROR: wrong-era EEVEE id &#x27;</span>{wrong}<span class="s">&#x27; was accepted</span><span class="s">&quot;</span>, file=sys.stderr); <span class="k">return</span> <span class="n">5</span>

‎examples/exit-pre-sidecar/README.md‎

Lines changed: 63 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,69 @@ Witnesses [`drivers-and-app-handlers`](../../skills/drivers-and-app-handlers/SKI
55
`main` does not write the file. The host runner asserts it after the
66
process exits (`tests/smoke/run_example.py --expect-sidecar`).
77

8-
5.1+. 4.5 LTS: `AttributeError` — no `exit_pre`. Skip
9-
(`SMOKE_SKIP: exit_pre requires Blender 5.1+`, exit 77, catalog
10-
`min_version` 5.1). `--force-run` bypasses the skip so 4.5 fails
11-
accessing the handler list.
8+
No gallery still. There is no geometry: the witness is a file that does
9+
not exist until the process is already gone, and a render would look
10+
identical whether the handler fired or not.
1211

13-
No gallery still. There is no geometry.
12+
## The contract
1413

15-
The harness checks **contents** (`sidecar_contains=exit_pre-ok`), not
16-
existence only. A file written from `main` or `atexit` is red. Several
17-
falsifiers exit 0 from the script so the **harness** can fail after Blender
18-
dies.
14+
`exit_pre` is the only handler in `bpy.app.handlers` that runs during
15+
interpreter teardown, and it is the difference between a callback that
16+
runs and one that silently never does. Code that relies on `atexit` for
17+
this — saving a session log, flushing a render manifest, releasing a
18+
licence token — is the class of bug this example exists to catch.
19+
20+
| Blender | `hasattr(bpy.app.handlers, "exit_pre")` |
21+
| --- | --- |
22+
| 4.5.11 LTS | `False` — `AttributeError` on access |
23+
| 5.1.2 | `True` |
24+
| 5.2.1 LTS | `True` |
25+
26+
Re-verified 2026-09-22 against all three binaries. `exit_pre` is also
27+
the only exit- or quit-related name in `bpy.app.handlers` on the
28+
versions that have it, so there is no older spelling to fall back to.
29+
30+
API reference:
31+
[`bpy.app.handlers` (5.2 LTS)](https://docs.blender.org/api/current/bpy.app.handlers.html)
32+
·
33+
[`bpy.app.handlers` (4.5 LTS)](https://docs.blender.org/api/4.5/bpy.app.handlers.html)
34+
— the 4.5 page lists no `exit_pre`, which is the absence this example
35+
turns into a skip.
36+
37+
## Why 4.5 skips rather than fails
38+
39+
Exiting 77 on 4.5 is **correct by design**, not a gap in coverage. The
40+
handler does not exist there, so there is no contract to witness and no
41+
assertion that could meaningfully run. The catalog carries
42+
`min_version` 5.1 and the runner treats a skip above that floor as a
43+
FAIL, so the skip cannot quietly spread to a version that should run.
44+
`tests/smoke/summarize.py` makes a job red if *every* example skipped,
45+
so a repo-wide skip cannot pass either.
46+
47+
`--force-run` bypasses the skip so 4.5 proves the absence rather than
48+
assuming it: the run exits 2 on `AttributeError`, and also exits 2 if
49+
`exit_pre` turns out to exist on a version that should not have it.
50+
51+
## Falsifiers
52+
53+
The harness checks the sidecar's **contents**
54+
(`sidecar_contains=exit_pre-ok`), not merely that a file appeared.
55+
Most falsifiers therefore exit **0 from the script** — Blender reports
56+
success — and the **harness** fails afterwards, which is the only place
57+
a post-exit contract can be falsified.
58+
59+
| Flag | What it breaks | Script exit | Harness |
60+
| --- | --- | --- | --- |
61+
| `--no-handler` | nothing registered, nothing written | 0 | FAIL, no sidecar |
62+
| `--silent-handler` | `exit_pre` registered but writes nothing | 0 | FAIL, no sidecar |
63+
| `--wrong-text` | handler writes `nope` | 0 | FAIL, contents mismatch |
64+
| `--write-in-main` | `from-main` written in `main`, no handler | 0 | FAIL, contents mismatch |
65+
| `--atexit-instead` | `atexit` writes `atexit-ok` instead of `exit_pre` | 0 | FAIL, contents mismatch |
66+
| `--force-run` on 4.5 | asserts `exit_pre` is genuinely absent | 2 | n/a |
67+
68+
`--write-in-main` and `--atexit-instead` are the two that matter: both
69+
produce a file, so an existence-only check would pass them. They are why
70+
the catalog row carries `sidecar_contains`.
1971

2072
## Run
2173

@@ -28,6 +80,8 @@ python tests/smoke/run_example.py --name exit-pre-sidecar \
2880
--expect-sidecar /tmp/exit-pre.sidecar --sidecar-contains exit_pre-ok
2981
```
3082

83+
Append `-- --wrong-text` (or any flag above) to falsify.
84+
3185
## Exit codes
3286

3387
Per-script sequential checks. `9` is a valid check code; there is no rule

0 commit comments

Comments
 (0)