Skip to content

Commit 480a159

Browse files
TMHSDigitalclaude
andauthored
fix(showcase): rebuild cart and stone-well joints, with eight new hygiene budgets (#206)
* fix(showcase): hoop the cart's tyre onto the felloe and round the rim The tyre's inner radius was set equal to the felloe's outer radius (`r_mid = RIM_OUTER + TYRE_T/2, radial_t = TYRE_T/2`), which put both surfaces on one plane for every segment: 32 coplanar cross-shell face pairs, 16 per wheel. It was visible as speckle on the committed hero and in a clay pass, and no budget could see it. Derive the tyre's inner radius from the felloe's outer radius minus a named interference (TYRE_SEAT) and its tread from the felloe plus its own thickness, so the band bites into the rim and still owns the ground contact. Wheel segments 16 -> 24: at 16 the rim read as a polygon and the facet facing the key light rendered as a hard white plate. Iron raised from (0.13, rough 0.32) to (0.20, rough 0.50), inside the range its sibling pieces use, because the tyre was reading as black plastic. Four new budgets, each recomputed from the generated mesh: - coplanar cross-shell face pairs = 0 (hay-bale's combinatorics, copied) - each tyre's own zmin on the floor, not just the AABB's - tyre seat depth banded per angular station, 48 stations - the two wheels mirrored in Y within 5e-5 m Four new falsifiers, each landing on its own code and sized to stay inside BBOX_TOL so the AABB gate cannot steal the failure: --flush-tyre (15), --sink-tyre (18), --float-wheel (16), --skew-wheel (19). Also drops the temp .glb once measured — the leak PR #205 found in hay-bale is in this piece too. Base triangles re-fitted to 2760-2960: narrower than the 2470-2900 it replaces and centred on the measured 2856. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> * fix(showcase): put the well's posts under the roof corners and tenon them in Three structural defects, all found by looking at an inspection sheet: 1. The four roof posts stood at 0/90/180/270 degrees while the roof's 4-gon cone is rotated pi/4, so its corners sit on the diagonals. The posts were under the midpoint of each eave: the roof's corners cantilevered 0.80 m unsupported, and one post stood dead centre in the well mouth in every orthographic view. Posts moved to the diagonals, and the windlass, crank and grip turn with them. 2. Each post's bottom face landed exactly on the coping's top face — 4 coplanar cross-shell face pairs, one per post. Posts are now tenoned POST_SEAT into the coping, with POST_TOP held fixed so nothing above them moves. 3. EAVE_HALF was derived from the post ring plus an overhang, giving a 1.64 m roof over a 1.08 m drum: an umbrella, not a well house. It is now derived from what the roof must cover — the curb's own radius plus a named clearance — which brings the AABB to 1.312 m. The bucket hung at z=0.68 with only its rim level with the coping: invisible in the hero and in all six orthographic views. It now hangs BUCKET_CLEAR above the measured coping top and reads in the still. Five new budgets, each recomputed from the generated mesh — the tenon and the bucket clearance measure against the coping top read off the stone material, not against the constant the builder used: - coplanar cross-shell face pairs = 0 - all 12 bottom-course stones on the floor, not just the lowest - post tenon depth banded, per post - each post within 0.02 rad in plan of a roof hip corner - bucket clearance banded above the coping Five new falsifiers. Two needed re-aiming to hit their own budget: --stand-posts also returns the posts to the axis, because flush alone at 45 degrees leaves no coping face within COPLANAR_CENTRE_MAX of a foot; --turn-posts rotates by exactly one curb facet, because any other angle sets the feet on different coping geometry and the gap gate (17) steals the failure, and a full 45 degrees swings the crank past the eave and fails the AABB gate (8). Collider ceiling 320 -> 380: the convex hull of the resized roof measures 346. Also drops the temp .glb once measured. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> * fix(examples): make ngon-triangulate's falsifier actually falsify `--skip-triangulate` printed an error and returned 4 from its own branch. The flag announced a failure rather than causing one, and the handling assertions it exists to falsify never ran. It now skips the repair and lets those assertions catch the unrepaired mesh, which reports the real measurement (`ngons=1 tris=0 quads=4`). The contract, the assertions and the exit codes are unchanged. Proven identical before and after on 4.5.11 LTS, 5.1.2 and 5.2.1 LTS: default 0, --no-dissolve 3, --skip-triangulate 4. README: adds the verified `calc_tangents` abort text, which is byte-identical on all three binaries, a falsifier table naming what each flag breaks, and 4.5/5.2 API links for calc_tangents, dissolve_edges and triangulate. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> * docs(showcase): fold this pass's defects into the shared conventions Seven conventions every future piece inherits, each from a defect in this pass that no existing budget could see: a band is hooped onto its host and never set flush against it; a member is tenoned into its seat and never stood on it, measured against the host read off the mesh; posts go under a roof's corners, with the corners taken from the pooled eave vertices rather than one fascia board; a roof is sized from what it covers, not from what carries it; the subject hangs where it can be seen; mirrored assemblies pair and compare, with the falsifier sized inside BBOX_TOL; and segment counts are a silhouette budget. Also registers this pass's falsifier flags in the shared list and regenerates the gallery for the two rebuilt pieces. 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 4e58049 commit 480a159

17 files changed

Lines changed: 1764 additions & 119 deletions

File tree

‎docs/gallery/assets/cart-hero.webp‎

-692 Bytes
Loading
-1.58 KB
Loading

‎docs/gallery/cart/index.html‎

Lines changed: 350 additions & 14 deletions
Large diffs are not rendered by default.
50.6 KB
Loading
51.6 KB
Loading

‎docs/gallery/index.html‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -903,7 +903,7 @@ <h2><a href="shipping-crate/">shipping-crate</a></h2>
903903
<div class="card-body">
904904
<h2><a href="stone-well/">stone-well</a></h2>
905905
<p class="teaches">A procedural stone well through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.</p>
906-
<p class="witnesses"><span class="tag">witnesses</span> Recomputed: 9380 tris, three materials with face floors, UVs in 0..1 with zero AABB overlap, outer AABB 1.640×1.640×1.761 m, LOD ratios in band, convex collider 306 tris, hygiene zero (loose, non-manifold, doubles, n-gons), grounded zmin, non-empty glTF. --skip-decimate exits 9 on the LOD1 ratio budget; --lift-z exits 16 on the grounded budget.</p>
906+
<p class="witnesses"><span class="tag">witnesses</span> Recomputed: 9380 tris, three materials with face floors, UVs in 0..1 with zero AABB overlap, outer AABB 1.312×1.312×1.761 m, LOD ratios in band, convex collider 346 tris, hygiene zero (loose, non-manifold, doubles, n-gons), zero coplanar cross-shell face pairs, all 12 bottom-course stones on the floor, each post tenoned 18 mm into the measured coping and standing under a roof hip corner, the bucket hanging 0.10 m clear of the coping, grounded zmin, non-empty glTF. --stand-posts exits 15 on the z-fight budget; --float-stone exits 16 on the named-support budget; --shallow-tenon and --drop-bucket exit 18; --turn-posts exits 19 on post placement; --skip-decimate exits 9 on the LOD1 ratio budget; --lift-z exits 16 on the grounded budget.</p>
907907
<a class="card-link" href="stone-well/">View showcase piece <span aria-hidden="true">&rarr;</span></a>
908908
</div>
909909
</article>
@@ -1002,7 +1002,7 @@ <h2><a href="watchtower/">watchtower</a></h2>
10021002
<div class="card-body">
10031003
<h2><a href="cart/">cart</a></h2>
10041004
<p class="teaches">A procedural two-wheel wooden cart through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.</p>
1005-
<p class="witnesses"><span class="tag">witnesses</span> Recomputed: 2600 tris, two materials with metal and wood face floors, UVs in 0..1 with zero AABB overlap, outer AABB 1.539×0.749×0.640 m, LOD ratios in band, convex collider 122 tris, hygiene zero (loose, non-manifold, doubles, n-gons), grounded zmin, non-empty glTF. --skip-decimate exits 9 on the LOD1 ratio budget; --lift-z exits 16 on the grounded budget.</p>
1005+
<p class="witnesses"><span class="tag">witnesses</span> Recomputed: 2856 tris, two materials with metal and wood face floors, UVs in 0..1 with zero AABB overlap, outer AABB 1.539×0.749×0.640 m, LOD ratios in band, convex collider 154 tris, hygiene zero (loose, non-manifold, doubles, n-gons), zero coplanar cross-shell face pairs, both tyres grounded, tyre seat depth in band over 48 angular stations, the two wheels mirrored within 5e-5 m, grounded zmin, non-empty glTF. --flush-tyre exits 15 on the z-fight budget; --sink-tyre exits 18 on the tyre seat band; --float-wheel exits 16 on the named-support budget; --skew-wheel exits 19 on the wheel mirror; --skip-decimate exits 9 on the LOD1 ratio budget; --lift-z exits 16 on the grounded budget.</p>
10061006
<a class="card-link" href="cart/">View showcase piece <span aria-hidden="true">&rarr;</span></a>
10071007
</div>
10081008
</article>

‎docs/gallery/stone-well/index.html‎

Lines changed: 433 additions & 30 deletions
Large diffs are not rendered by default.

‎examples/ngon-triangulate/README.md‎

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,38 @@ glTF tris = 12 for a cube *or* this mesh. `--skip-triangulate` exits 4.
2222
No gallery still. A hexagon on a cube does not read at thumbnail
2323
without fake annotation.
2424

25+
## Verified behaviour
26+
27+
`Mesh.calc_tangents` raises `RuntimeError` on any face with more than
28+
four loops. The message is byte-identical on 4.5.11 LTS, 5.1.2, and
29+
5.2.1 LTS:
30+
31+
```
32+
Error: Tangent space can only be computed for tris/quads, aborting
33+
```
34+
35+
`TANGENT_ABORT` matches on the `tris/quads` substring, so the check
36+
survives a reword of the surrounding sentence but still fails if the
37+
abort stops happening at all.
38+
39+
## Falsifiers
40+
41+
Each flag breaks one stage and lands on the assertion that stage feeds.
42+
Neither announces a failure; both let a real check catch the mesh.
43+
44+
| Flag | What it breaks | Exit |
45+
| --- | --- | --- |
46+
| `--no-dissolve` | never makes the n-gon, so the pre-assertion finds no pathology (`ngon count 0 != 1`) | 3 |
47+
| `--skip-triangulate` | skips `bmesh.ops.triangulate`, so the handling assertion sees the n-gon survive (`ngons=1 tris=0 quads=4`) | 4 |
48+
49+
## API reference
50+
51+
| Name | 4.5 LTS | 5.2 LTS |
52+
| --- | --- | --- |
53+
| `Mesh.calc_tangents` | [4.5](https://docs.blender.org/api/4.5/bpy.types.Mesh.html#bpy.types.Mesh.calc_tangents) | [5.2](https://docs.blender.org/api/current/bpy.types.Mesh.html#bpy.types.Mesh.calc_tangents) |
54+
| `bmesh.ops.dissolve_edges` | [4.5](https://docs.blender.org/api/4.5/bmesh.ops.html#bmesh.ops.dissolve_edges) | [5.2](https://docs.blender.org/api/current/bmesh.ops.html#bmesh.ops.dissolve_edges) |
55+
| `bmesh.ops.triangulate` | [4.5](https://docs.blender.org/api/4.5/bmesh.ops.html#bmesh.ops.triangulate) | [5.2](https://docs.blender.org/api/current/bmesh.ops.html#bmesh.ops.triangulate) |
56+
2557
## Run
2658

2759
```bash
@@ -41,7 +73,7 @@ against it.
4173
| 1 | Uncaught exception (FATAL wrapper) |
4274
| 2 | argparse / usage |
4375
| 3 | Pathology missing: n-gon count, loops, or face count (`--no-dissolve` lands here) |
44-
| 4 | `calc_tangents` / triangulate handling (`--skip-triangulate` lands here) |
76+
| 4 | `calc_tangents` / triangulate handling (`--skip-triangulate` lands here, via the real handling assertion) |
4577

4678
The `blender-smoke` workflow runs the check on Blender 5.2 LTS and 4.5 LTS
4779
(5.1 on the weekly cron, the `needs-5.1` PR label, or manual dispatch).

‎examples/ngon-triangulate/ngon_triangulate.py‎

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
1010
* 5 faces, 1 n-gon, that face has 6 loops
1111
* ``Mesh.calc_tangents`` aborts until triangulated
12-
(same abort ``triangulate-tangents`` documents)
12+
(same abort ``triangulate-tangents`` documents). The message is
13+
byte-identical on 4.5.11 LTS, 5.1.2 and 5.2.1 LTS:
14+
"Error: Tangent space can only be computed for tris/quads, aborting"
1315
* triangulate the n-gon → 4 tris + 4 quads, 28 loops, tangents succeed
1416
1517
glTF tri count is 12 either way (hexagon+quads or a cube) — not a
@@ -117,13 +119,12 @@ def check(ob, skip_triangulate):
117119
file=sys.stderr,
118120
)
119121
return 4
120-
if skip_triangulate:
121-
print(
122-
"ERROR: skip-triangulate left the n-gon; handling unrepaired",
123-
file=sys.stderr,
124-
)
125-
return 4
126-
triangulate_ngons(me)
122+
# --skip-triangulate skips the repair and lets the handling assertions
123+
# below catch the unrepaired mesh. Returning 4 from here instead would
124+
# make the flag announce a failure rather than cause one, and the
125+
# assertions it is supposed to falsify would never run.
126+
if not skip_triangulate:
127+
triangulate_ngons(me)
127128
leftover = ngons(me)
128129
tris = sum(1 for p in me.polygons if len(p.vertices) == 3)
129130
quads = sum(1 for p in me.polygons if len(p.vertices) == 4)

‎showcase/README.md‎

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,13 @@ entry in `showcase/gallery.json`, and a rendered still.
4242
coplanar-face budget fails (exit 15). `--short-stile` /
4343
`--short-post` lift a member out of its cup (exit 18 or 19).
4444
`--clip-ring` pulls a hung ring off the eye centerline (exit 18).
45+
`--flush-tyre` / `--stand-posts` restore a flush surface so the
46+
coplanar budget fails (exit 15). `--float-wheel` / `--float-stone`
47+
lift one named support while the rest still ground the AABB (exit 16).
48+
`--sink-tyre` / `--shallow-tenon` put a seat outside its band (exit
49+
18). `--drop-bucket` hides the hung subject (exit 18).
50+
`--skew-wheel` / `--turn-posts` break a mirror or placement budget
51+
(exit 19).
4552
A budget with no falsifier witnesses nothing:
4653
prove each one fails once, and check the exit code, not just
4754
non-zero.
@@ -187,6 +194,63 @@ entry in `showcase/gallery.json`, and a rendered still.
187194
The outer AABB does not cover this: on a prop with an appendage the
188195
AABB is the appendage, and the body can drift to any size underneath
189196
it.
197+
- **A band is hooped onto its host, never set flush against it (exit
198+
15/18).** A tyre, ferrule, hoop or collar derives its **inner** radius
199+
from the host's **outer** radius minus a named interference, and its
200+
outer radius from the host plus its own thickness. Writing
201+
`r_mid = host_out + t/2, radial_t = t/2` — the obvious spelling —
202+
makes the band's inner cylinder and the host's tread the *same
203+
surface*, so every segment is a coplanar cross-shell pair around the
204+
whole circumference. `cart` shipped that way and measured 32 pairs
205+
(16 per wheel); the speckle was visible on the committed hero and in
206+
a clay pass, and no budget could see it. `--flush-tyre` restores the
207+
equality and is the falsifier.
208+
- **A member is tenoned into its seat, never stood on it (exit
209+
15/18).** A post, leg or stile whose bottom face lands exactly on its
210+
host's top face puts both on one plane. Derive the member's bottom
211+
from the host's top minus a named seat depth, and hold the member's
212+
*top* fixed so nothing above it moves. `stone-well` stood its four
213+
roof posts on the coping and measured 4 pairs, one per post.
214+
Assert the seat as a **band** recomputed against the host surface read
215+
off the generated mesh — `max z` over the host's material — not
216+
against the constant the builder used, which witnesses nothing.
217+
- **Posts go under the roof's corners, not the middle of its eaves
218+
(exit 19).** Where a hip or pyramid roof is carried on four posts,
219+
assert each post's plan bearing against a hip corner **recomputed from
220+
the generated mesh**, as a wrapped angular difference. Take the
221+
corners from the *pooled* vertices at the eave line: pulling them from
222+
a single shell picks one fascia board, whose own extremes sit 90° off
223+
the corners it is nailed to, and the budget then fails on a correct
224+
model. `stone-well` placed its posts on the axes, which cantilevered
225+
the roof's corners 0.80 m and stood one post dead centre in the well
226+
mouth in every orthographic view.
227+
- **A roof is sized from what it must cover, not from what carries it.**
228+
Deriving the eave reach from the post ring plus an overhang gave
229+
`stone-well` a 1.64 m roof over a 1.08 m drum — an umbrella. Derive it
230+
from the covered body's own radius plus a named clearance
231+
(`EAVE_HALF = R_OUTER + CURB_OUT + EAVE_CLEAR`), and let the overhang
232+
fall out of that. Expect the convex-hull collider budget to move when
233+
the roof does; re-fit it and say so.
234+
- **The subject hangs where it can be seen (exit 18).** A prop whose
235+
story is one small part — a bucket on a rope, a lantern on a hook —
236+
asserts that part's clearance above the body it hangs over, as a band,
237+
measured against the body read off the mesh. `stone-well`'s bucket sat
238+
down the shaft with only its rim level with the coping: invisible in
239+
the hero and in all six orthographic views, and no budget noticed.
240+
`--drop-bucket` is the falsifier.
241+
- **Mirrored assemblies (exit 19).** Where a prop has a left and a
242+
right of the same part — two wheels, two brackets — pair the shells
243+
and assert they match in the two axes they share and in their extents,
244+
within a named epsilon, and are opposite in the mirrored axis. Size
245+
the falsifier's displacement to stay **inside** `BBOX_TOL` so the AABB
246+
gate cannot steal the failure: `cart`'s `--skew-wheel` moves one wheel
247+
6 mm along a track whose tolerance is 10 mm.
248+
- **Segment counts are a silhouette budget, not a triangle budget.** A
249+
16-gon felloe reads as a polygon at hero size, and the flat facet
250+
facing the key light renders as a hard white plate. `cart` went to 24
251+
and the chords disappeared. Re-fit the triangle band around the new
252+
measured count rather than leaving the old one — and prefer a band
253+
*narrower* than the one it replaces, centred on the measurement.
190254
- **Material face floors.** Every declared material asserts a named
191255
minimum face count on the finished mesh, recomputed from
192256
`polygon.material_index`. This catches the slot-assignment wipe class:

0 commit comments

Comments
 (0)