Skip to content

Commit 561f920

Browse files
fix: hang the iron cauldron inside the tripod
Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent abf516e commit 561f920

7 files changed

Lines changed: 127 additions & 20 deletions

File tree

194 Bytes
Loading

‎docs/gallery/index.html‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1145,7 +1145,7 @@ <h2><a href="tavern-stool/">tavern-stool</a></h2>
11451145
<div class="card-body">
11461146
<h2><a href="iron-cauldron/">iron-cauldron</a></h2>
11471147
<p class="teaches">A procedural hanging iron cauldron with a bulged open pot, bail, and timber tripod through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.</p>
1148-
<p class="witnesses"><span class="tag">witnesses</span> Recomputed: 3388 tris, two materials with 450 wood and 1198 metal faces, UVs in 0..1 with zero AABB overlap, outer AABB 0.606×0.591×0.826 m, LOD ratios in band (5.2 COLLAPSE 4 tris leaner on LOD2), convex collider 180 tris, non-empty glTF. --skip-decimate exits 9 on the LOD1 ratio budget.</p>
1148+
<p class="witnesses"><span class="tag">witnesses</span> Recomputed: 3388 tris, two materials with 450 wood and 1198 metal faces, UVs in 0..1 with zero AABB overlap, outer AABB 1.013×0.986×0.827 m, pot–leg clearance 0.051 m, LOD ratios in band (5.2 COLLAPSE 8 tris leaner on LOD2), convex collider 98 tris, non-empty glTF. --skip-decimate exits 9 on the LOD1 ratio budget.</p>
11491149
<a class="card-link" href="iron-cauldron/">View example <span aria-hidden="true">&rarr;</span></a>
11501150
</div>
11511151
</article>

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

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ <h1>iron-cauldron</h1>
253253
<img src="../assets/iron-cauldron-hero.webp" alt="iron-cauldron render" width="1280" height="720" />
254254
</button>
255255
<p class="zoom-hint">Rendered headless by the example itself — click to zoom.</p>
256-
<div class="callout"><span class="tag">witnesses</span> Recomputed: 3388 tris, two materials with 450 wood and 1198 metal faces, UVs in 0..1 with zero AABB overlap, outer AABB 0.606×0.591×0.826 m, LOD ratios in band (5.2 COLLAPSE 4 tris leaner on LOD2), convex collider 180 tris, non-empty glTF. --skip-decimate exits 9 on the LOD1 ratio budget.</div>
256+
<div class="callout"><span class="tag">witnesses</span> Recomputed: 3388 tris, two materials with 450 wood and 1198 metal faces, UVs in 0..1 with zero AABB overlap, outer AABB 1.013×0.986×0.827 m, pot–leg clearance 0.051 m, LOD ratios in band (5.2 COLLAPSE 8 tris leaner on LOD2), convex collider 98 tris, non-empty glTF. --skip-decimate exits 9 on the LOD1 ratio budget.</div>
257257
<div class="runline">
258258
<pre id="runCmd">blender --background --python showcase/iron-cauldron/iron_cauldron.py --</pre>
259259
<button class="copy-btn" id="copyRun" type="button">Copy</button>
@@ -264,7 +264,7 @@ <h1>iron-cauldron</h1>
264264
<p><strong>Composes</strong> skills <code>mesh-editing-and-bmesh</code>, <code>bake-high-to-low</code>, <code>depsgraph-and-evaluated-data</code>, <code>engine-export-presets</code>, and snippets <code>bake_normal_high_to_low.py</code>, <code>setup_bake_target_image.py</code>, <code>lod_chain.py</code> / <code>decimate_to_budget.py</code>, <code>convex_hull_collider.py</code>, <code>export_preset_unity.py</code> (helpers copied, not imported as a package).</p>
265265
<h2>Budgets</h2>
266266
<p>Declared as named constants; every gate <strong>recomputes</strong> from the mesh, materials, UVs, evaluated LOD, collider, or export file.</p>
267-
<p>| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) | | --- | --- | --- | | Base triangles | 3280–3520 | 3388 / 3388 / 3388 | | 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.2184 | | Materials | exactly 2 distinct, ≥12 wood, ≥24 metal | 2 slots, 450 wood, 1198 metal | | UVs | in <code>0..1</code>, AABB overlap ≤ 1e-5 | in range, overlap 0 | | Outer AABB | (0.606, 0.591, 0.826) m ± 0.01 | (0.6061, 0.5911, 0.8255), zmin 0.002 | | Collider tris | ≤ 200 | 180 | | Export | written, size &gt; 0 | 250748 / 250748 / 250736 bytes |</p>
267+
<p>| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) | | --- | --- | --- | | Base triangles | 3280–3520 | 3388 / 3388 / 3388 | | 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.2172 | | Materials | exactly 2 distinct, ≥12 wood, ≥24 metal | 2 slots, 450 wood, 1198 metal | | UVs | in <code>0..1</code>, AABB overlap ≤ 1e-5 | in range, overlap 0 | | Outer AABB | (1.013, 0.986, 0.827) m ± 0.01 | (1.0128, 0.9865, 0.8271), zmin 0.002 | | Pot–leg clearance | ≥ 0.04 m (recomputed vs pole axes) | 0.0513 / 0.0513 / 0.0513 | | Collider tris | ≤ 200 | 98 | | Export | written, size &gt; 0 | 245060 / 245060 / 245044 bytes |</p>
268268
<p>DECIMATE COLLAPSE triangle counts are <strong>not</strong> 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 <code>has_data</code> plus operator <code>FINISHED</code>, not byte-identity. Construction uses no RNG. Export byte counts differ by 12 B on 5.2.1 (glTF serializer), not a gated axis.</p>
269269
<p><code>--skip-decimate</code> skips the LOD DECIMATE stage so LOD1 ratio is 1.0 and exit 9 fires. That is the named budget the falsifier violates.</p>
270270
<h2>Run</h2>
@@ -274,7 +274,7 @@ <h2>Run</h2>
274274
<p>Smoke does not pass <code>--output</code> or <code>--skip-decimate</code>.</p>
275275
<h2>Exit codes</h2>
276276
<p>File-local. <code>9</code> is a valid check code. <code>10</code> is reserved for <code>gallery_framing.check_framing</code> on the <code>--output</code> path.</p>
277-
<p>| 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 (<code>--skip-decimate</code> 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 | <code>--output</code> produced no file |</p>
277+
<p>| 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 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 (<code>--skip-decimate</code> 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 | <code>--output</code> produced no file |</p>
278278
</section>
279279
<section class="detail-section src">
280280
<h2>Source</h2>
@@ -330,12 +330,13 @@ <h2>Source</h2>
330330
R_MID = <span class="n">0.205</span>
331331
R_TOP = <span class="n">0.148</span>
332332
APEX_Z = <span class="n">0.82</span>
333-
TRIPOD_R = <span class="n">0.32</span>
333+
TRIPOD_R = <span class="n">0.56</span>
334334
BAIL_SEGS = <span class="n">12</span>
335335
BAIL_R = <span class="n">0.009</span>
336336
BBOX_TOL = <span class="n">0.01</span>
337+
POT_LEG_CLEARANCE_MIN = <span class="n">0.04</span>
337338
<span class="c"># Fitted after locking geometry. Recomputed from bound_box.</span>
338-
OUTER_SIZE = (<span class="n">0.606</span>, <span class="n">0.591</span>, <span class="n">0.826</span>)
339+
OUTER_SIZE = (<span class="n">1.013</span>, <span class="n">0.986</span>, <span class="n">0.827</span>)
339340

340341
BASE_TRIS_MIN = <span class="n">3280</span>
341342
BASE_TRIS_MAX = <span class="n">3520</span>
@@ -726,6 +727,49 @@ <h2>Source</h2>
726727
<span class="k">return</span> (min(xs), min(ys), min(zs), max(xs), max(ys), max(zs))
727728

728729

730+
<span class="k">def</span> pot_tripod_clearance(mesh):
731+
<span class="c"># Recomputed from verts vs the three pole axes. Leg verts sit on the</span>
732+
<span class="c"># axes and are skipped; remaining shell verts in the pot band must</span>
733+
<span class="c"># clear the pole radius.</span>
734+
feet = []
735+
<span class="k">for</span> i <span class="k">in</span> range(<span class="n">3</span>):
736+
ang = i * (<span class="n">2.0</span> * math.pi / <span class="n">3.0</span>) + math.radians(<span class="n">18.0</span>)
737+
feet.append(
738+
Vector((TRIPOD_R * math.cos(ang), TRIPOD_R * math.sin(ang), <span class="n">0.016</span>))
739+
)
740+
apex = Vector((<span class="n">0.0</span>, <span class="n">0.0</span>, APEX_Z))
741+
z0 = POT_Z0 - <span class="n">0.02</span>
742+
z1 = POT_Z0 + POT_H + <span class="n">0.03</span>
743+
min_c = <span class="k">None</span>
744+
<span class="k">for</span> v <span class="k">in</span> mesh.vertices:
745+
p = Vector(v.co)
746+
<span class="k">if</span> p.z &lt; z0 <span class="k">or</span> p.z &gt; z1:
747+
<span class="k">continue</span>
748+
<span class="k">if</span> math.hypot(p.x, p.y) &lt; <span class="n">0.06</span>:
749+
<span class="k">continue</span>
750+
nearest = <span class="k">None</span>
751+
on_pole = <span class="k">False</span>
752+
<span class="k">for</span> foot <span class="k">in</span> feet:
753+
ab = apex - foot
754+
denom = ab.length_squared
755+
<span class="k">if</span> denom &lt; <span class="n">1e-12</span>:
756+
<span class="k">continue</span>
757+
t = max(<span class="n">0.0</span>, min(<span class="n">1.0</span>, (p - foot).dot(ab) / denom))
758+
axis = (p - (foot + t * ab)).length
759+
rad = <span class="n">0.028</span> * (<span class="n">1.0</span> - t) + <span class="n">0.018</span> * t
760+
gap = axis - rad
761+
<span class="k">if</span> axis &lt; rad + <span class="n">0.006</span>:
762+
on_pole = <span class="k">True</span>
763+
<span class="k">break</span>
764+
<span class="k">if</span> nearest <span class="k">is</span> <span class="k">None</span> <span class="k">or</span> gap &lt; nearest:
765+
nearest = gap
766+
<span class="k">if</span> on_pole <span class="k">or</span> nearest <span class="k">is</span> <span class="k">None</span>:
767+
<span class="k">continue</span>
768+
<span class="k">if</span> min_c <span class="k">is</span> <span class="k">None</span> <span class="k">or</span> nearest &lt; min_c:
769+
min_c = nearest
770+
<span class="k">return</span> min_c <span class="k">if</span> min_c <span class="k">is</span> <span class="k">not</span> <span class="k">None</span> <span class="k">else</span> -<span class="n">1.0</span>
771+
772+
729773
<span class="k">def</span> uv_stats(mesh):
730774
uv = mesh.uv_layers.active
731775
<span class="k">if</span> uv <span class="k">is</span> <span class="k">None</span>:
@@ -868,6 +912,14 @@ <h2>Source</h2>
868912
size_x = bb[<span class="n">3</span>] - bb[<span class="n">0</span>]
869913
size_y = bb[<span class="n">4</span>] - bb[<span class="n">1</span>]
870914
size_z = bb[<span class="n">5</span>] - bb[<span class="n">2</span>]
915+
clearance = pot_tripod_clearance(low.data)
916+
print(<span class="s">f&quot;</span><span class="s">measured pot_tripod_clearance=</span>{clearance:<span class="s">.4f</span>}<span class="s">&quot;</span>)
917+
<span class="k">if</span> clearance &lt; POT_LEG_CLEARANCE_MIN:
918+
<span class="k">return</span> fail(
919+
<span class="s">f&quot;</span><span class="s">pot clips tripod clearance </span>{clearance:<span class="s">.4f</span>}<span class="s"> </span><span class="s">&quot;</span>
920+
<span class="s">f&quot;</span><span class="s">&lt; </span>{POT_LEG_CLEARANCE_MIN}<span class="s">&quot;</span>,
921+
<span class="n">3</span>,
922+
), <span class="k">None</span>, <span class="k">None</span>, <span class="k">None</span>, <span class="k">None</span>, <span class="k">None</span>
871923

872924
img, tex = setup_bake_image(low, wood)
873925
<span class="k">if</span> img <span class="k">is</span> <span class="k">None</span>:
@@ -1046,10 +1098,10 @@ <h2>Source</h2>
10461098
cam_data = bpy.data.cameras.new(<span class="s">&quot;Cam&quot;</span>)
10471099
cam_data.lens = <span class="n">50.0</span>
10481100
cam = bpy.data.objects.new(<span class="s">&quot;Cam&quot;</span>, cam_data)
1049-
cam.location = (<span class="n">1.58</span>, -<span class="n">2.18</span>, <span class="n">1.22</span>)
1101+
cam.location = (<span class="n">1.82</span>, -<span class="n">2.50</span>, <span class="n">1.40</span>)
10501102
scene.collection.objects.link(cam)
10511103
aim = bpy.data.objects.new(<span class="s">&quot;Aim&quot;</span>, <span class="k">None</span>)
1052-
aim.location = (<span class="n">0.0</span>, <span class="n">0.0</span>, <span class="n">0.46</span>)
1104+
aim.location = (<span class="n">0.0</span>, <span class="n">0.0</span>, <span class="n">0.40</span>)
10531105
scene.collection.objects.link(aim)
10541106
con = cam.constraints.new(<span class="s">&quot;TRACK_TO&quot;</span>)
10551107
con.target = aim

‎showcase/gallery.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@
285285
"name": "iron-cauldron",
286286
"dir": "showcase/iron-cauldron",
287287
"teaches": "A procedural hanging iron cauldron with a bulged open pot, bail, and timber tripod through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.",
288-
"witnessesFix": "Recomputed: 3388 tris, two materials with 450 wood and 1198 metal faces, UVs in 0..1 with zero AABB overlap, outer AABB 0.606×0.591×0.826 m, LOD ratios in band (5.2 COLLAPSE 4 tris leaner on LOD2), convex collider 180 tris, non-empty glTF. --skip-decimate exits 9 on the LOD1 ratio budget.",
288+
"witnessesFix": "Recomputed: 3388 tris, two materials with 450 wood and 1198 metal faces, UVs in 0..1 with zero AABB overlap, outer AABB 1.013×0.986×0.827 m, pot–leg clearance 0.051 m, LOD ratios in band (5.2 COLLAPSE 8 tris leaner on LOD2), convex collider 98 tris, non-empty glTF. --skip-decimate exits 9 on the LOD1 ratio budget.",
289289
"hero": "docs/gallery/assets/iron-cauldron-hero.webp",
290290
"preview": "showcase/iron-cauldron/preview.webp",
291291
"tags": [

‎showcase/iron-cauldron/README.md‎

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,21 @@ materials, UVs, evaluated LOD, collider, or export file.
2323
| --- | --- | --- |
2424
| Base triangles | 3280–3520 | 3388 / 3388 / 3388 |
2525
| LOD1 ratio | 0.32–0.62 of base | 0.5000 / 0.5000 / 0.5000 |
26-
| LOD2 ratio | 0.10–0.35 of base | 0.2196 / 0.2196 / 0.2184 |
26+
| LOD2 ratio | 0.10–0.35 of base | 0.2196 / 0.2196 / 0.2172 |
2727
| Materials | exactly 2 distinct, ≥12 wood, ≥24 metal | 2 slots, 450 wood, 1198 metal |
2828
| UVs | in `0..1`, AABB overlap ≤ 1e-5 | in range, overlap 0 |
29-
| Outer AABB | (0.606, 0.591, 0.826) m ± 0.01 | (0.6061, 0.5911, 0.8255), zmin 0.002 |
30-
| Collider tris | ≤ 200 | 180 |
31-
| Export | written, size > 0 | 250748 / 250748 / 250736 bytes |
29+
| Outer AABB | (1.013, 0.986, 0.827) m ± 0.01 | (1.0128, 0.9865, 0.8271), zmin 0.002 |
30+
| Pot–leg clearance | ≥ 0.04 m (recomputed vs pole axes) | 0.0513 / 0.0513 / 0.0513 |
31+
| Collider tris | ≤ 200 | 98 |
32+
| Export | written, size > 0 | 245060 / 245060 / 245044 bytes |
3233

3334
DECIMATE COLLAPSE triangle counts are **not** identical across series —
34-
5.2.1 is 4 tris leaner on LOD2. The gate is a ratio band, not an exact
35+
5.2.1 is 8 tris leaner on LOD2. The gate is a ratio band, not an exact
3536
count. Bake pixels are stochastic; the gate is `has_data` plus operator
3637
`FINISHED`, not byte-identity. Construction uses no RNG. Export byte
37-
counts differ by 12 B on 5.2.1 (glTF serializer), not a gated axis.
38+
counts differ by 16 B on 5.2.1 (glTF serializer), not a gated axis.
39+
The pot–leg clearance is recomputed from shell verts vs the three pole
40+
axes; clipping exits 3.
3841

3942
`--skip-decimate` skips the LOD DECIMATE stage so LOD1 ratio is 1.0 and
4043
exit 9 fires. That is the named budget the falsifier violates.
@@ -59,7 +62,7 @@ File-local. `9` is a valid check code. `10` is reserved for
5962
| 0 | Success |
6063
| 1 | Uncaught exception (FATAL wrapper) |
6164
| 2 | argparse / usage |
62-
| 3 | Mesh did not build / no UV layer |
65+
| 3 | Mesh did not build / no UV layer / pot clips tripod |
6366
| 4 | Base triangle count outside range |
6467
| 5 | Material count ≠ 2 distinct slots, or wood/metal faces missing |
6568
| 6 | UVs outside 0..1 |

‎showcase/iron-cauldron/iron_cauldron.py‎

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,13 @@
4646
R_MID = 0.205
4747
R_TOP = 0.148
4848
APEX_Z = 0.82
49-
TRIPOD_R = 0.32
49+
TRIPOD_R = 0.56
5050
BAIL_SEGS = 12
5151
BAIL_R = 0.009
5252
BBOX_TOL = 0.01
53+
POT_LEG_CLEARANCE_MIN = 0.04
5354
# Fitted after locking geometry. Recomputed from bound_box.
54-
OUTER_SIZE = (0.606, 0.591, 0.826)
55+
OUTER_SIZE = (1.013, 0.986, 0.827)
5556

5657
BASE_TRIS_MIN = 3280
5758
BASE_TRIS_MAX = 3520
@@ -442,6 +443,49 @@ def world_bbox(obj):
442443
return (min(xs), min(ys), min(zs), max(xs), max(ys), max(zs))
443444

444445

446+
def pot_tripod_clearance(mesh):
447+
# Recomputed from verts vs the three pole axes. Leg verts sit on the
448+
# axes and are skipped; remaining shell verts in the pot band must
449+
# clear the pole radius.
450+
feet = []
451+
for i in range(3):
452+
ang = i * (2.0 * math.pi / 3.0) + math.radians(18.0)
453+
feet.append(
454+
Vector((TRIPOD_R * math.cos(ang), TRIPOD_R * math.sin(ang), 0.016))
455+
)
456+
apex = Vector((0.0, 0.0, APEX_Z))
457+
z0 = POT_Z0 - 0.02
458+
z1 = POT_Z0 + POT_H + 0.03
459+
min_c = None
460+
for v in mesh.vertices:
461+
p = Vector(v.co)
462+
if p.z < z0 or p.z > z1:
463+
continue
464+
if math.hypot(p.x, p.y) < 0.06:
465+
continue
466+
nearest = None
467+
on_pole = False
468+
for foot in feet:
469+
ab = apex - foot
470+
denom = ab.length_squared
471+
if denom < 1e-12:
472+
continue
473+
t = max(0.0, min(1.0, (p - foot).dot(ab) / denom))
474+
axis = (p - (foot + t * ab)).length
475+
rad = 0.028 * (1.0 - t) + 0.018 * t
476+
gap = axis - rad
477+
if axis < rad + 0.006:
478+
on_pole = True
479+
break
480+
if nearest is None or gap < nearest:
481+
nearest = gap
482+
if on_pole or nearest is None:
483+
continue
484+
if min_c is None or nearest < min_c:
485+
min_c = nearest
486+
return min_c if min_c is not None else -1.0
487+
488+
445489
def uv_stats(mesh):
446490
uv = mesh.uv_layers.active
447491
if uv is None:
@@ -584,6 +628,14 @@ def check(skip_decimate):
584628
size_x = bb[3] - bb[0]
585629
size_y = bb[4] - bb[1]
586630
size_z = bb[5] - bb[2]
631+
clearance = pot_tripod_clearance(low.data)
632+
print(f"measured pot_tripod_clearance={clearance:.4f}")
633+
if clearance < POT_LEG_CLEARANCE_MIN:
634+
return fail(
635+
f"pot clips tripod clearance {clearance:.4f} "
636+
f"< {POT_LEG_CLEARANCE_MIN}",
637+
3,
638+
), None, None, None, None, None
587639

588640
img, tex = setup_bake_image(low, wood)
589641
if img is None:
@@ -762,10 +814,10 @@ def light(name, loc, energy, size, col, rot):
762814
cam_data = bpy.data.cameras.new("Cam")
763815
cam_data.lens = 50.0
764816
cam = bpy.data.objects.new("Cam", cam_data)
765-
cam.location = (1.58, -2.18, 1.22)
817+
cam.location = (1.82, -2.50, 1.40)
766818
scene.collection.objects.link(cam)
767819
aim = bpy.data.objects.new("Aim", None)
768-
aim.location = (0.0, 0.0, 0.46)
820+
aim.location = (0.0, 0.0, 0.40)
769821
scene.collection.objects.link(aim)
770822
con = cam.constraints.new("TRACK_TO")
771823
con.target = aim
70 Bytes
Loading

0 commit comments

Comments
 (0)