Skip to content

Commit bb29acc

Browse files
fix(showcase): rebuild chopping-block as a log round with a felling axe (#173)
* docs(showcase): add seat conformance, plumb, and shading conventions The chopping-block pass found three defect classes the existing conventions did not cover, and each one had already shipped past a green check on at least one piece. A hoop generated as a circle on an out-of-round host gaps on one side and sinks on the other, but a seat check that compares against a single global midpoint radius classifies the outer chamfer vertices as inner ones and reports a clean fit. Seat depth has to be banded and sampled per angular segment against the host's own radius function. A prop with an appendage has an outer AABB that is the appendage, so the body underneath can drift to any size while the AABB budget stays green. Plumb measured off the exact zmin and zmax rings is meaningless once a rim is notched, because those rings are a handful of vertices. Coplanar face pairs belong in the hygiene family, counted over faces that share no vertex; counting fan-cap triangles makes the budget unsatisfiable rather than meaningful. Shading is not measurable, which is why it needs saying: smoothing a wobbled 36-gon erases every bit of surface modelling in it. Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com> * fix(showcase): rebuild chopping-block as a log round with a felling axe The piece passed every budget it declared and still read as a tapered pail with a hammer balanced on the lid. Eleven defects came out of the inspection sheet; the budgets could not see any of them. The stump was a truncated cone wider at the base, so the silhouette was a bucket. It is now an out-of-round loft whose radius is a closed-form function of angle and height, with real log-round proportions: 0.535 m across by 0.360 m tall, where it was 1:1 and read as a canister no amount of surface detail could rescue. The axe head was four flat plates with visible daylight between them, built from stacked boxes and resting on the top face without entering it. It is one lofted shell from poll to bit, buried 78.7 mm below the sawn face. Its chamfers are modelled into the section profile because bevelling a four-sided loft left sixteen boundary edges and two doubles at the bit -- the chamfer is now structural rather than an operator applied to geometry that cannot take it. The hoop was a 14-gon torus that alternately poked through the wood and floated off it. It is generated from the log's own radius function, so it seats uniformly by construction, and it moved from under the rim, where it read as a lid clamp, down to stump height. The log is flat-shaded. Smoothed, its wobble and the drying checks in the end grain were invisible and the whole point of the surface modelling was lost. Every defect that can be measured is now an assertion recomputed from the generated mesh: hygiene at exit 15, grounded at 16, four-shell axe joint fit at 17, hoop seat band and haft clearance at 18, plumb and stated size at 19. Five falsifiers prove them, each against a named budget. Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com> --------- Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 594a476 commit bb29acc

8 files changed

Lines changed: 2007 additions & 604 deletions

File tree

5.01 KB
Loading

docs/gallery/chopping-block/index.html

Lines changed: 953 additions & 292 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
@@ -1096,12 +1096,12 @@ <h2><a href="signpost/">signpost</a></h2>
10961096
</article>
10971097
<article class="card" data-tags="mesh export showcase">
10981098
<a class="card-media" href="chopping-block/" aria-label="chopping-block example detail page">
1099-
<img src="assets/chopping-block-hero.webp" alt="chopping-block — A procedural chopping block with an embedded axe through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract." loading="lazy" decoding="async" />
1099+
<img src="assets/chopping-block-hero.webp" alt="chopping-block — A procedural log-round chopping block with an iron hoop and a felling axe buried in the sawn face, through UVs, bake, LOD, collider, and Unity glTF, asserting" loading="lazy" decoding="async" />
11001100
</a>
11011101
<div class="card-body">
11021102
<h2><a href="chopping-block/">chopping-block</a></h2>
1103-
<p class="teaches">A procedural chopping block with an embedded axe through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.</p>
1104-
<p class="witnesses"><span class="tag">witnesses</span> Recomputed: 2016 tris, two materials with 122 metal faces, UVs in 0..1 with zero AABB overlap, outer AABB 0.471×0.463×0.841 m, LOD ratios in band (5.2 COLLAPSE 2 tris leaner on LOD2), convex collider 188 tris, non-empty glTF. --skip-decimate exits 9 on the LOD1 ratio budget.</p>
1103+
<p class="teaches">A procedural log-round chopping block with an iron hoop and a felling axe buried in the sawn face, through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.</p>
1104+
<p class="witnesses"><span class="tag">witnesses</span> Recomputed: 1472 tris, three materials with 252 bark / 252 grain / 288 metal faces, UVs in 0..1 with zero AABB overlap, outer AABB 0.526 x 0.526 x 1.012 m, LOD ratios in band, convex collider 434 tris, non-empty glTF. Hygiene: zero non-manifold edges, loose geometry, doubles, zero-area faces, n-gons and coplanar disjoint face pairs, grounded at zmin 0, log plumb to 1e-7. Joint fit: four shells, 18.0 mm haft clearance in the eye, 78.7 mm bit bury, hoop biting 3.7-4.0 mm into the log on every segment. --fat-haft exits 17 on eye clearance; --round-band exits 18 on the hoop bite band.</p>
11051105
<a class="card-link" href="chopping-block/">View example <span aria-hidden="true">&rarr;</span></a>
11061106
</div>
11071107
</article>

showcase/README.md

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,16 +37,22 @@ entry in `showcase/gallery.json`, and a rendered still.
3737
falsifier (exit 9), `--stray-vert` the mesh-hygiene falsifier (exit
3838
15), `--lift-z` the grounded-zmin falsifier (exit 16), and
3939
`--fat-rungs` (or the piece's equivalent) the joint-fit falsifier
40-
(exit 17). A budget with no falsifier witnesses nothing: prove each
41-
one fails once, and check the exit code, not just non-zero.
40+
(exit 17), and `--round-band` (or equivalent) the seat-conformance
41+
falsifier (exit 18). A budget with no falsifier witnesses nothing:
42+
prove each one fails once, and check the exit code, not just
43+
non-zero.
4244
- **Hygiene budgets.** Copied combinatorics from
4345
`examples/mesh-hygiene-audit` (do not import the example). Every piece
4446
asserts on the generated mesh: non-manifold edges 0, loose verts 0,
4547
loose edges 0, doubles at 1e-5 0, zero-area faces 0, n-gons 0, world
46-
AABB min Z within 1e-4 of 0. Multi-body props do **not** require Euler
47-
characteristic 2 — that is a single-shell contract. Pairs of parts
48-
meant to touch assert a BVH surface gap below a named epsilon
49-
(vert-vert is the wrong metric for thin straps and collars).
48+
AABB min Z within 1e-4 of 0, and coplanar disjoint face pairs 0. Count
49+
the last one over faces that share **no** vertex: the triangles of one
50+
flat fan cap are coplanar and close-centred by construction, and
51+
counting those makes the budget unsatisfiable rather than meaningful.
52+
Multi-body props do **not** require Euler characteristic 2 — that is a
53+
single-shell contract. Pairs of parts meant to touch assert a BVH
54+
surface gap below a named epsilon (vert-vert is the wrong metric for
55+
thin straps and collars).
5056
- **Joint-fit budgets.** Parts that interpenetrate on purpose — a tenon
5157
in a mortise, a peg in a hub — assert how deep the overlap goes, not
5258
just that it exists. Split the mesh into shells by edge connectivity,
@@ -58,6 +64,25 @@ entry in `showcase/gallery.json`, and a rendered still.
5864
the host's chamfer as a spike — that is the class this catches.
5965
Measure in the construction frame (un-rotate by any rake) so a tilted
6066
host does not inflate its own AABB.
67+
- **Seat conformance (exit 18).** A band, hoop, strap or collar wrapped
68+
around a host asserts a **banded** seat depth — a minimum so it cannot
69+
float and a maximum so it cannot sink — sampled per angular segment
70+
rather than as a single global figure. Derive the wrapper's profile
71+
from the host's own radius function instead of a circle, and classify
72+
inner versus outer vertices against the host surface at each vertex's
73+
own angle. A global midpoint radius misclassifies outer chamfer
74+
vertices as inner ones the moment the host is out of round, which is
75+
how a hoop that visibly gapped on one side still passed. The paired
76+
falsifier makes the wrapper a true circle on an out-of-round host.
77+
- **Plumb and real-world size (exit 19).** Assert that the axis of a
78+
turned or lofted body is vertical, by comparing the XY centroid of a
79+
bottom slab against a top slab — not the exact `zmin` and `zmax`
80+
rings, which may be a handful of vertices once a rim is notched or
81+
chipped. Separately assert the body's own diameter and height against
82+
the dimensions the README states in metres, with a named tolerance.
83+
The outer AABB does not cover this: on a prop with an appendage the
84+
AABB is the appendage, and the body can drift to any size underneath
85+
it.
6186
- **Material face floors.** Every declared material asserts a named
6287
minimum face count on the finished mesh, recomputed from
6388
`polygon.material_index`. This catches the slot-assignment wipe class:
@@ -67,7 +92,14 @@ entry in `showcase/gallery.json`, and a rendered still.
6792
append/replace, never clear-and-rebuild.
6893
- **Exit codes** are file-local: `0` success, argparse `2`, `3` and above
6994
in check order. `9` is legal. FATAL `sys.exit(1)` is a crash, never a
70-
named check.
95+
named check. `15``19` are reserved across pieces for the hygiene
96+
family above: `15` hygiene, `16` grounded, `17` joint fit, `18` seat
97+
conformance and contact, `19` plumb and real-world size.
98+
- **Shading is part of the model.** A flat-shaded low-poly body and a
99+
smooth-shaded one are different objects to a viewer. Smoothing a
100+
wobbled 36-gon erases every bit of surface modelling in it and leaves
101+
a featureless drum, so decide per part and say why. Nothing here is
102+
measurable, which is exactly why it has to be looked at.
71103
- **Rendered still and gallery entry.** Showcase pieces are visual by
72104
definition. The pathology / sidecar exemption does not apply. Call
73105
`examples/gallery_framing.check_framing` on the `--output` path only.

showcase/chopping-block/README.md

Lines changed: 83 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
# Chopping block
22

3-
A showcase piece, not an example. Procedural chopping block (hooped
4-
timber stump with an embedded axe) then the shipped pipeline:
5-
unique-cell UVs, Cycles high-to-low normal bake, LOD chain, convex
6-
collider, Unity glTF export.
3+
A showcase piece, not an example. Procedural chopping block (a hooped
4+
log round with a felling axe buried in the sawn face) then the shipped
5+
pipeline: unique-cell UVs, Cycles high-to-low normal bake, LOD chain,
6+
convex collider, Unity glTF export.
7+
8+
The log is an out-of-round loft whose radius is a closed-form function
9+
of angle and height; the hoop, the top rim and the drying checks are all
10+
generated from that same function, so they stay seated when a dimension
11+
changes. The axe head is one lofted shell from poll to bit with its
12+
chamfers modelled into the section profile, and the haft is swept
13+
through the eye rather than pushed into it.
714

815
It asserts **budget conformance** of the generated result. It does not
916
witness an API contract. "It rendered without error" is not a check.
@@ -21,38 +28,89 @@ materials, UVs, evaluated LOD, collider, or export file.
2128

2229
| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) |
2330
| --- | --- | --- |
24-
| Base triangles | 1880–2160 | 2016 / 2016 / 2016 |
31+
| Base triangles | 1410–1530 | 1472 / 1472 / 1472 |
2532
| LOD1 ratio | 0.32–0.62 of base | 0.5000 / 0.5000 / 0.5000 |
26-
| LOD2 ratio | 0.10–0.35 of base | 0.2192 / 0.2192 / 0.2183 |
27-
| Materials | exactly 2 distinct, ≥24 metal faces | 2 slots, 122 metal |
33+
| LOD2 ratio | 0.10–0.35 of base | 0.2188 / 0.2188 / 0.2188 |
34+
| Materials | exactly 3 distinct; ≥160 bark, ≥90 grain, ≥24 metal faces | 3 slots; 252 / 252 / 288 |
2835
| UVs | in `0..1`, AABB overlap ≤ 1e-5 | in range, overlap 0 |
29-
| Outer AABB | (0.471, 0.463, 0.841) m ± 0.01 | (0.4713, 0.4626, 0.8413), zmin 0 |
30-
| Collider tris | ≤ 220 | 188 |
31-
| Export | written, size > 0 | 157000 / 157000 / 156992 bytes |
36+
| Outer AABB | (0.526, 0.526, 1.012) m ± 0.01 | (0.5264, 0.5264, 1.0117) |
37+
| Collider tris | ≤ 470 | 434 |
38+
| Export | written, size > 0 | 150784 / 150784 / 150776 bytes |
39+
40+
### Hygiene
3241

33-
DECIMATE COLLAPSE triangle counts are **not** identical across series —
34-
5.2.1 is 2 tris leaner on LOD2. The gate is a ratio band, not an exact
35-
count. Bake pixels are stochastic; the gate is `has_data` plus operator
36-
`FINISHED`, not byte-identity. Construction uses no RNG. Export byte
37-
counts differ by 8 B on 5.2.1 (glTF serializer), not a gated axis.
42+
Recomputed from the generated mesh, not asserted about the script.
43+
44+
| Axis | Declared | Measured (all three) |
45+
| --- | --- | --- |
46+
| Non-manifold edges | 0 | 0 |
47+
| Loose verts / edges | 0 / 0 | 0 / 0 |
48+
| Doubles merged at 1e-5 | 0 | 0 |
49+
| Zero-area faces | 0 | 0 |
50+
| N-gons | 0 | 0 |
51+
| Coplanar disjoint face pairs | 0 | 0 |
52+
| Grounded: `zmin` | within 1e-5 of 0 | 0.0000 |
53+
| Log axis out of plumb | ≤ 2e-4 | 0.0000000 |
54+
| Log size | 0.535 × 0.360 m ± 0.03 / ± 0.02 | 0.5464 × 0.3600 |
3855

39-
`--skip-decimate` skips the LOD DECIMATE stage so LOD1 ratio is 1.0 and
40-
exit 9 fires. That is the named budget the falsifier violates.
56+
### Joint fit
57+
58+
Four shells that have to meet correctly: log, hoop, head, haft.
59+
60+
| Axis | Declared | Measured (all three) |
61+
| --- | --- | --- |
62+
| Shell count | exactly 4 | 4 |
63+
| Haft clearance inside the eye | ≥ 0.006 m | 0.01800 |
64+
| Haft engagement through the eye | ≥ 0.020 m | 0.08049 |
65+
| Haft breakout margin below the head | ≥ 0.006 m | 0.02351 |
66+
| Bit bury below the sawn top | ≥ 0.030 m | 0.07871 |
67+
| Bit inset from the rim | ≥ 0.030 m | 0.14647 |
68+
| Hoop bite into the log, every segment | 0.002–0.007 m | 0.00371–0.00400 |
69+
| Haft clearance above the log | ≥ 0.015 m, 0 verts inside | 0.07858, 0 |
70+
71+
The hoop bite is binned by angular segment against the log's own radius
72+
function. A single global midpoint radius misclassifies outer chamfer
73+
vertices as inner ones on an out-of-round log, which is how a hoop that
74+
visibly floated on one side still passed.
75+
76+
DECIMATE COLLAPSE triangle counts happen to agree across all three
77+
series here; the gate is still a ratio band, not an exact count. Bake
78+
pixels are stochastic; the gate is `has_data` plus operator `FINISHED`,
79+
not byte-identity. Construction uses no RNG. Export byte counts differ
80+
by 8 B on 5.2.1 (glTF serializer), not a gated axis.
81+
82+
### Falsifiers
83+
84+
Each violates one named budget. All five were run on 4.5.11, 5.1.2 and
85+
5.2.1 and returned the same code on each.
86+
87+
| Flag | Budget violated | Exit |
88+
| --- | --- | --- |
89+
| `--skip-decimate` | LOD1 ratio band | 9 |
90+
| `--stray-vert` | loose vertex count is 0 | 15 |
91+
| `--lift-z` | bounding box `zmin` is 0 | 16 |
92+
| `--fat-haft` | haft clearance inside the eye | 17 |
93+
| `--round-band` | hoop bite into the log | 18 |
4194

4295
## Run
4396

4497
```bash
4598
blender --background --python chopping_block.py --
4699
blender --background --python chopping_block.py -- --skip-decimate
100+
blender --background --python chopping_block.py -- --stray-vert
101+
blender --background --python chopping_block.py -- --lift-z
102+
blender --background --python chopping_block.py -- --fat-haft
103+
blender --background --python chopping_block.py -- --round-band
47104
blender --background --python chopping_block.py -- --output chopping-block.png
48105
```
49106

50-
Smoke does not pass `--output` or `--skip-decimate`.
107+
Smoke passes no flags.
51108

52109
## Exit codes
53110

54111
File-local. `9` is a valid check code. `10` is reserved for
55-
`gallery_framing.check_framing` on the `--output` path.
112+
`gallery_framing.check_framing` on the `--output` path. `15``19` are the
113+
hygiene and joint-fit family.
56114

57115
| Code | Meaning |
58116
| --- | --- |
@@ -61,7 +119,7 @@ File-local. `9` is a valid check code. `10` is reserved for
61119
| 2 | argparse / usage |
62120
| 3 | Mesh did not build / no UV layer |
63121
| 4 | Base triangle count outside range |
64-
| 5 | Material count ≠ 2 distinct slots, or wood/metal faces missing |
122+
| 5 | Material count ≠ 3 distinct slots, or a face-count floor missed |
65123
| 6 | UVs outside 0..1 |
66124
| 7 | UV AABB overlap above tolerance |
67125
| 8 | World AABB off declared outer size |
@@ -71,3 +129,8 @@ File-local. `9` is a valid check code. `10` is reserved for
71129
| 12 | Bake did not finish or image has no data |
72130
| 13 | Export file missing or empty |
73131
| 14 | `--output` produced no file |
132+
| 15 | Mesh hygiene: loose, non-manifold, zero-area, doubles, n-gons, z-fight |
133+
| 16 | Not grounded: bounding box `zmin` off 0 |
134+
| 17 | Axe joint fit: shell count, eye clearance, engagement, breakout, bury, inset |
135+
| 18 | Contact fit: hoop bite band, or haft fouling the log |
136+
| 19 | Log out of plumb, or off its stated real-world size |

0 commit comments

Comments
 (0)