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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified docs/gallery/assets/hay-bale-hero.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion docs/gallery/gn-sdf-remesh/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,7 @@ <h2>Source</h2>
<span class="c"># EEVEE-id inversion witnessed for real: the OTHER era&#x27;s id must be</span>
<span class="c"># rejected by this build, the helper&#x27;s accepted</span>
eid = get_eevee_engine_id()
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>
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>
<span class="k">try</span>:
bpy.context.scene.render.engine = wrong
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>
Expand Down
773 changes: 660 additions & 113 deletions docs/gallery/hay-bale/index.html

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions docs/gallery/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -1162,12 +1162,12 @@ <h2><a href="wooden-ladder/">wooden-ladder</a></h2>
</article>
<article class="card" data-tags="mesh export showcase">
<a class="card-media" href="hay-bale/" aria-label="hay-bale showcase piece detail page">
<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" />
<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" />
</a>
<div class="card-body">
<h2><a href="hay-bale/">hay-bale</a></h2>
<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>
<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>
<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>
<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>
<a class="card-link" href="hay-bale/">View showcase piece <span aria-hidden="true">&rarr;</span></a>
</div>
</article>
Expand Down
9 changes: 8 additions & 1 deletion docs/gallery/iron-cauldron/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,14 @@ <h2>Source</h2>


<span class="k">def</span> eevee_engine_id():
<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>
<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.

Mapping matches examples/swatch-grid.get_eevee_engine_id, which asserts it
against the running build (copied, not imported). Inherited the inverted
form from showcase/shipping-crate; it returned &#x27;BLENDER_EEVEE_NEXT&#x27; on 5.x,
where that id does not exist.
&quot;&quot;&quot;</span>
<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>


<span class="k">def</span> fail(msg, code):
Expand Down
9 changes: 8 additions & 1 deletion docs/gallery/shipping-crate/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,14 @@ <h2>Source</h2>


<span class="k">def</span> eevee_engine_id():
<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>
<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.

Mapping matches examples/swatch-grid.get_eevee_engine_id, which asserts it
against the running build (copied, not imported). The old form here keyed
on &gt;= (4, 2, 0) and so returned &#x27;BLENDER_EEVEE_NEXT&#x27; on 5.x, where that id
does not exist — the render path raised TypeError on 5.1 and 5.2.
&quot;&quot;&quot;</span>
<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>


<span class="k">def</span> fail(msg, code):
Expand Down
2 changes: 1 addition & 1 deletion docs/gallery/swatch-grid/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -556,7 +556,7 @@ <h2>Source</h2>
<span class="c"># Cycles. Witness the inversion for real: the OTHER era&#x27;s id must be rejected</span>
<span class="c"># by this build, and the helper&#x27;s id must be accepted.</span>
eid = get_eevee_engine_id()
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>
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>
<span class="k">try</span>:
sc.render.engine = wrong
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>
Expand Down
2 changes: 1 addition & 1 deletion docs/gallery/turntable/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,7 @@ <h2>Source</h2>
<span class="c"># the EEVEE-id mapping is asserted regardless of whether we render: the</span>
<span class="c"># OTHER era&#x27;s id must be rejected by this build, the helper&#x27;s accepted</span>
eid = get_eevee_engine_id()
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>
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>
<span class="k">try</span>:
bpy.context.scene.render.engine = wrong
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>
Expand Down
72 changes: 63 additions & 9 deletions examples/exit-pre-sidecar/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,69 @@ Witnesses [`drivers-and-app-handlers`](../../skills/drivers-and-app-handlers/SKI
`main` does not write the file. The host runner asserts it after the
process exits (`tests/smoke/run_example.py --expect-sidecar`).

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

No gallery still. There is no geometry.
## The contract

The harness checks **contents** (`sidecar_contains=exit_pre-ok`), not
existence only. A file written from `main` or `atexit` is red. Several
falsifiers exit 0 from the script so the **harness** can fail after Blender
dies.
`exit_pre` is the only handler in `bpy.app.handlers` that runs during
interpreter teardown, and it is the difference between a callback that
runs and one that silently never does. Code that relies on `atexit` for
this — saving a session log, flushing a render manifest, releasing a
licence token — is the class of bug this example exists to catch.

| Blender | `hasattr(bpy.app.handlers, "exit_pre")` |
| --- | --- |
| 4.5.11 LTS | `False` — `AttributeError` on access |
| 5.1.2 | `True` |
| 5.2.1 LTS | `True` |

Re-verified 2026-09-22 against all three binaries. `exit_pre` is also
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.

API reference:
[`bpy.app.handlers` (5.2 LTS)](https://docs.blender.org/api/current/bpy.app.handlers.html)
·
[`bpy.app.handlers` (4.5 LTS)](https://docs.blender.org/api/4.5/bpy.app.handlers.html)
— the 4.5 page lists no `exit_pre`, which is the absence this example
turns into a skip.

## Why 4.5 skips rather than fails

Exiting 77 on 4.5 is **correct by design**, not a gap in coverage. The
handler does not exist there, so there is no contract to witness and no
assertion that could meaningfully run. The catalog carries
`min_version` 5.1 and the runner treats a skip above that floor as a
FAIL, so the skip cannot quietly spread to a version that should run.
`tests/smoke/summarize.py` makes a job red if *every* example skipped,
so a repo-wide skip cannot pass either.

`--force-run` bypasses the skip so 4.5 proves the absence rather than
assuming it: the run exits 2 on `AttributeError`, and also exits 2 if
`exit_pre` turns out to exist on a version that should not have it.

## Falsifiers

The harness checks the sidecar's **contents**
(`sidecar_contains=exit_pre-ok`), not merely that a file appeared.
Most falsifiers therefore exit **0 from the script** — Blender reports
success — and the **harness** fails afterwards, which is the only place
a post-exit contract can be falsified.

| Flag | What it breaks | Script exit | Harness |
| --- | --- | --- | --- |
| `--no-handler` | nothing registered, nothing written | 0 | FAIL, no sidecar |
| `--silent-handler` | `exit_pre` registered but writes nothing | 0 | FAIL, no sidecar |
| `--wrong-text` | handler writes `nope` | 0 | FAIL, contents mismatch |
| `--write-in-main` | `from-main` written in `main`, no handler | 0 | FAIL, contents mismatch |
| `--atexit-instead` | `atexit` writes `atexit-ok` instead of `exit_pre` | 0 | FAIL, contents mismatch |
| `--force-run` on 4.5 | asserts `exit_pre` is genuinely absent | 2 | n/a |

`--write-in-main` and `--atexit-instead` are the two that matter: both
produce a file, so an existence-only check would pass them. They are why
the catalog row carries `sidecar_contains`.

## Run

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

Append `-- --wrong-text` (or any flag above) to falsify.

## Exit codes

Per-script sequential checks. `9` is a valid check code; there is no rule
Expand Down
9 changes: 7 additions & 2 deletions examples/exit-pre-sidecar/exit_pre_sidecar.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,15 @@
handler writes it. The host runner (``tests/smoke/run_example.py``)
asserts the file after the process exits.

5.1+. 4.5 LTS has no ``exit_pre`` (AttributeError). Catalog
``min_version`` 5.0 is wrong — the floor is 5.1. Skip: ``SMOKE_SKIP``.
5.1+. 4.5 LTS has no ``exit_pre`` (AttributeError), so the catalog
``min_version`` is 5.1 and the script skips below it (``SMOKE_SKIP``).
``--force-run`` bypasses the skip so 4.5 fails accessing ``exit_pre``.

Re-verified 2026-09-22 on 4.5.11 LTS, 5.1.2 and 5.2.1 LTS:
``hasattr(bpy.app.handlers, "exit_pre")`` is False / True / True, and
``exit_pre`` is the only exit-related name in ``bpy.app.handlers`` on
the versions that have it.

No gallery still. There is no geometry.

blender --background --python exit_pre_sidecar.py --
Expand Down
35 changes: 35 additions & 0 deletions showcase/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,13 @@ entry in `showcase/gallery.json`, and a rendered still.
the last one over faces that share **no** vertex: the triangles of one
flat fan cap are coplanar and close-centred by construction, and
counting those makes the budget unsatisfiable rather than meaningful.
Count the coplanar pairs **cross-shell**, not merely over faces that
share no vertex: two quads two steps apart on one flat cap share no
vertex and are coplanar and close-centred by construction. Counting
those reported 96 pairs on `hay-bale`, none of them a hazard, which is
the same unsatisfiable-budget trap in a second disguise. Z-fighting is
two separate bodies landing on one plane, and that is a cross-shell
pair by definition.
Multi-body props do **not** require Euler characteristic 2 — that is a
single-shell contract. Two boxes that share a coplanar face z-fight;
two that share a vertex weld into one shell. Seat a joint with a named
Expand Down Expand Up @@ -100,6 +107,26 @@ entry in `showcase/gallery.json`, and a rendered still.
hypot-length box rotated about its centroid. The centroid form is
short of one host and punches through the other. `--short-brace` /
`--float-spout` (or the piece's equivalent) is the falsifier.
- **Even shaping terms and mirror symmetry (exit 19).** Where a prop has
mirrored members — two belts, two brackets, a pair of feet — 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; `cos` fixed it exactly. Assert it: pair the
mirrored shells and compare their axis positions and extents within a
named epsilon. A bounding box cannot see this, and neither can a
per-member budget that only ever looks at one member.
- **Wrappers follow the host's profile, and the host is finished first
(exit 16/18).** Build, bevel and **ground the host, then** hang the
wrapper on the finished surface by raycasting the host's own
cross-section at the wrapper's station. Placing a wrapper against
half-built geometry and shifting everything afterwards leaves each one
a different distance off the floor. A constant-section rectangle on a
rounded host stands proud at the middle of each face and is swallowed
at the corners — `hay-bale`'s belts 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. Assert **per wrapper shell** that the loop
passes under the host, not just that something touches Z=0.
- **Seat conformance (exit 18).** A band, hoop, strap or collar wrapped
around a host asserts a **banded** seat depth — a minimum so it cannot
float and a maximum so it cannot sink — sampled per angular segment
Expand All @@ -110,6 +137,14 @@ entry in `showcase/gallery.json`, and a rendered still.
vertices as inner ones the moment the host is out of round, which is
how a hoop that visibly gapped on one side still passed. The paired
falsifier makes the wrapper a true circle on an out-of-round host.
Measure the depth **station-locally**, along the vertex's own direction
from the station axis — not by nearest-surface distance. A wrapper
seated in a concave waist has its nearest host face on the bulge
shoulder at a neighbouring station, and the wrap then reads as sitting
10 mm outside a host it is in fact hugging. Bin the samples by
**rounding** to the nearest station, never by flooring: a station on a
bin boundary puts its inner and outer rings in different bins, and the
inner-only bin reports the wrapper's outer offset as its seat depth.
- **Level sole on a raked leg (exit 18).** A raked stile does not sit
in a world-axis cube. Build the sleeve in the member frame so it
follows the rake; add the tread after the rake as its own
Expand Down
Loading
Loading