Skip to content

Commit fe0fcef

Browse files
TMHSDigitalclaude
andcommitted
fix(showcase): one canonical convex_hull_collider and export_unity; report helper drift
Showcase helpers are copied inline by design, but nothing tied the copies together. convex_hull_collider existed in 10 variants: the is_valid guard on geom_interior/geom_unused (a hull can list an element in both; deleting it twice raises ReferenceError) had reached 14 copies while 39 kept the unguarded body. export_unity had 3 equivalent variants. - Every plain collider copy (54 pieces) now carries one canonical guarded body with a comment saying why. Three variants stay, each now guarded and recorded as justified: dissolve + triangulate for round props (anvil, campfire, wooden-barrel, wooden-bucket), hull from bare vertices (farm-tractor), per-group hulls (sea-stack-arch). - export_unity: one body in all 76 pieces. - tests/check_helper_drift.py hashes each copy of the named pipeline helpers (docstrings ignored) and compares the variant count with the allowance and reason recorded in its CANONICAL table; --show NAME prints each variant, --strict fails. Validate runs it warn-only. Current: collider 4/4, export_unity 1/1, hygiene_audit 7/7, pack_uvs 13/13, assign_slots 20/20. - showcase/README.md: "Canonical helpers" says which body to copy and how to rerun the report. Proof (local, .scratch/blender-5.2.1-windows-x64, 5.2.1 LTS): run_catalog over all 76 showcase rows after the change: 76 happy paths PASS and 76 catalog falsifiers PASS at their declared exits (152/152), so every collider budget, triangle ceiling and export still holds. Every script py_compiles. Closes #344 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 2335989 commit fe0fcef

79 files changed

Lines changed: 687 additions & 804 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/validate.yml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ jobs:
7373
- name: Every gallery still is gated by gallery_framing
7474
run: python3 tests/check_render_gates.py
7575

76+
- name: Showcase helper drift (warn-only report)
77+
run: python3 tests/check_helper_drift.py
78+
7679
- name: Check falsifiers declare the budget they target
7780
run: python3 tests/check_falsifier_targets.py
7881

‎showcase/README.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -743,6 +743,18 @@ entry in `showcase/gallery.json`, and a rendered still.
743743
piece composes. Duplicated helpers stay inlined or copied; showcase
744744
scripts do not import snippets as a package.
745745

746+
- **Canonical helpers.** Copy a pipeline helper from a piece that carries
747+
the canonical body, not from whichever piece is nearest.
748+
`tests/check_helper_drift.py` (Validate, warn-only) hashes each copy and
749+
lists outliers; its `CANONICAL` table is the record of how many variants
750+
each helper may have and why. `convex_hull_collider` has one guarded
751+
body (filter `geom_interior` / `geom_unused` on `is_valid` before each
752+
delete; the hull can list an element twice) plus three justified
753+
variants: dissolve + triangulate for round props, hull from bare
754+
vertices (`farm-tractor`), per-group hulls (`sea-stack-arch`).
755+
`export_unity` has exactly one body. Fix a helper bug in every copy, then
756+
rerun the report: `python tests/check_helper_drift.py --show NAME`.
757+
746758
## Layout
747759

748760
```text

‎showcase/anvil/anvil.py‎

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1162,24 +1162,24 @@ def make_lod(obj, name, ratio, skip_decimate):
11621162

11631163

11641164
def convex_hull_collider(obj, name):
1165+
# bmesh.ops.convex_hull can list the same element in geom_interior and
1166+
# geom_unused; once deleted it is invalid and a second delete raises
1167+
# ReferenceError. Filter on is_valid before each delete.
1168+
# Variant for round props: limited dissolve merges the hull's near-coplanar
1169+
# facets, then triangulate, to stay under the collider triangle ceiling.
11651170
mesh = bpy.data.meshes.new(name)
11661171
bm = bmesh.new()
11671172
try:
11681173
bm.from_mesh(obj.data)
11691174
result = bmesh.ops.convex_hull(bm, input=list(bm.verts))
1170-
interior = result.get("geom_interior") or []
1171-
unused = result.get("geom_unused") or []
1175+
interior = [g for g in result.get("geom_interior") or [] if g.is_valid]
11721176
if interior:
11731177
bmesh.ops.delete(bm, geom=interior, context="VERTS")
1178+
unused = [g for g in result.get("geom_unused") or [] if g.is_valid]
11741179
if unused:
11751180
bmesh.ops.delete(bm, geom=unused, context="VERTS")
1176-
bmesh.ops.dissolve_limit(
1177-
bm,
1178-
angle_limit=math.radians(10.0),
1179-
verts=list(bm.verts),
1180-
edges=list(bm.edges),
1181-
delimit={"NORMAL"},
1182-
)
1181+
bmesh.ops.dissolve_limit(bm, angle_limit=math.radians(10.0), verts=list(bm.verts),
1182+
edges=list(bm.edges), delimit={"NORMAL"})
11831183
bmesh.ops.triangulate(bm, faces=list(bm.faces))
11841184
bm.to_mesh(mesh)
11851185
mesh.update()
@@ -1235,14 +1235,9 @@ def export_unity(path, objects):
12351235
for ob in objects:
12361236
ob.select_set(True)
12371237
bpy.context.view_layer.objects.active = objects[0]
1238-
bpy.ops.export_scene.gltf(
1239-
filepath=path,
1240-
use_selection=True,
1241-
export_yup=True,
1242-
export_apply=True,
1243-
export_draco_mesh_compression_enable=False,
1244-
export_animations=False,
1245-
)
1238+
bpy.ops.export_scene.gltf(filepath=path, use_selection=True, export_yup=True,
1239+
export_apply=True, export_draco_mesh_compression_enable=False,
1240+
export_animations=False)
12461241

12471242

12481243
def check(

‎showcase/apothecary-shelf/apothecary_shelf.py‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1305,10 +1305,9 @@ def export_unity(path, objects):
13051305
for ob in objects:
13061306
ob.select_set(True)
13071307
bpy.context.view_layer.objects.active = objects[0]
1308-
bpy.ops.export_scene.gltf(
1309-
filepath=path, use_selection=True, export_yup=True, export_apply=True,
1310-
export_draco_mesh_compression_enable=False, export_animations=False,
1311-
)
1308+
bpy.ops.export_scene.gltf(filepath=path, use_selection=True, export_yup=True,
1309+
export_apply=True, export_draco_mesh_compression_enable=False,
1310+
export_animations=False)
13121311

13131312

13141313
def check(skip_decimate, lift_z=False, **flags):

‎showcase/archery-target/archery_target.py‎

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2124,16 +2124,18 @@ def make_lod(obj, name, ratio, skip_decimate):
21242124

21252125

21262126
def convex_hull_collider(obj, name):
2127-
# Duplicated from snippets/convex_hull_collider.py (not a package).
2127+
# bmesh.ops.convex_hull can list the same element in geom_interior and
2128+
# geom_unused; once deleted it is invalid and a second delete raises
2129+
# ReferenceError. Filter on is_valid before each delete.
21282130
mesh = bpy.data.meshes.new(name)
21292131
bm = bmesh.new()
21302132
try:
21312133
bm.from_mesh(obj.data)
21322134
result = bmesh.ops.convex_hull(bm, input=list(bm.verts))
2133-
interior = result.get("geom_interior") or []
2134-
unused = result.get("geom_unused") or []
2135+
interior = [g for g in result.get("geom_interior") or [] if g.is_valid]
21352136
if interior:
21362137
bmesh.ops.delete(bm, geom=interior, context="VERTS")
2138+
unused = [g for g in result.get("geom_unused") or [] if g.is_valid]
21372139
if unused:
21382140
bmesh.ops.delete(bm, geom=unused, context="VERTS")
21392141
bm.to_mesh(mesh)
@@ -2187,20 +2189,14 @@ def bake_normal(high, low):
21872189

21882190

21892191
def export_unity(path, objects):
2190-
# Duplicated from snippets/export_preset_unity.py (not a package).
21912192
for ob in bpy.context.view_layer.objects:
21922193
ob.select_set(False)
21932194
for ob in objects:
21942195
ob.select_set(True)
21952196
bpy.context.view_layer.objects.active = objects[0]
2196-
bpy.ops.export_scene.gltf(
2197-
filepath=path,
2198-
use_selection=True,
2199-
export_yup=True,
2200-
export_apply=True,
2201-
export_draco_mesh_compression_enable=False,
2202-
export_animations=False,
2203-
)
2197+
bpy.ops.export_scene.gltf(filepath=path, use_selection=True, export_yup=True,
2198+
export_apply=True, export_draco_mesh_compression_enable=False,
2199+
export_animations=False)
22042200

22052201

22062202
FLOOR_NAMES = ("straw", "twine", "timber", "steel", "white", "black", "blue", "red", "gold",

‎showcase/bamboo-clump/bamboo_clump.py‎

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1578,17 +1578,18 @@ def make_lod(obj, name, ratio, skip_decimate):
15781578

15791579

15801580
def convex_hull_collider(obj, name):
1581-
# Duplicated from snippets/convex_hull_collider.py (not a package).
1581+
# bmesh.ops.convex_hull can list the same element in geom_interior and
1582+
# geom_unused; once deleted it is invalid and a second delete raises
1583+
# ReferenceError. Filter on is_valid before each delete.
15821584
mesh = bpy.data.meshes.new(name)
15831585
bm = bmesh.new()
15841586
try:
15851587
bm.from_mesh(obj.data)
15861588
result = bmesh.ops.convex_hull(bm, input=list(bm.verts))
1587-
interior = result.get("geom_interior") or []
1588-
unused = result.get("geom_unused") or []
1589+
interior = [g for g in result.get("geom_interior") or [] if g.is_valid]
15891590
if interior:
15901591
bmesh.ops.delete(bm, geom=interior, context="VERTS")
1591-
unused = [v for v in unused if v.is_valid]
1592+
unused = [g for g in result.get("geom_unused") or [] if g.is_valid]
15921593
if unused:
15931594
bmesh.ops.delete(bm, geom=unused, context="VERTS")
15941595
bm.to_mesh(mesh)
@@ -1642,20 +1643,14 @@ def bake_normal(high, low):
16421643

16431644

16441645
def export_unity(path, objects):
1645-
# Duplicated from snippets/export_preset_unity.py (not a package).
16461646
for ob in bpy.context.view_layer.objects:
16471647
ob.select_set(False)
16481648
for ob in objects:
16491649
ob.select_set(True)
16501650
bpy.context.view_layer.objects.active = objects[0]
1651-
bpy.ops.export_scene.gltf(
1652-
filepath=path,
1653-
use_selection=True,
1654-
export_yup=True,
1655-
export_apply=True,
1656-
export_draco_mesh_compression_enable=False,
1657-
export_animations=False,
1658-
)
1651+
bpy.ops.export_scene.gltf(filepath=path, use_selection=True, export_yup=True,
1652+
export_apply=True, export_draco_mesh_compression_enable=False,
1653+
export_animations=False)
16591654

16601655

16611656

‎showcase/basketball-hoop/basketball_hoop.py‎

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1637,16 +1637,18 @@ def make_lod(obj, name, ratio, skip_decimate):
16371637

16381638

16391639
def convex_hull_collider(obj, name):
1640-
# Duplicated from snippets/convex_hull_collider.py (not a package).
1640+
# bmesh.ops.convex_hull can list the same element in geom_interior and
1641+
# geom_unused; once deleted it is invalid and a second delete raises
1642+
# ReferenceError. Filter on is_valid before each delete.
16411643
mesh = bpy.data.meshes.new(name)
16421644
bm = bmesh.new()
16431645
try:
16441646
bm.from_mesh(obj.data)
16451647
result = bmesh.ops.convex_hull(bm, input=list(bm.verts))
1646-
interior = result.get("geom_interior") or []
1647-
unused = result.get("geom_unused") or []
1648+
interior = [g for g in result.get("geom_interior") or [] if g.is_valid]
16481649
if interior:
16491650
bmesh.ops.delete(bm, geom=interior, context="VERTS")
1651+
unused = [g for g in result.get("geom_unused") or [] if g.is_valid]
16501652
if unused:
16511653
bmesh.ops.delete(bm, geom=unused, context="VERTS")
16521654
bm.to_mesh(mesh)
@@ -1700,20 +1702,14 @@ def bake_normal(high, low):
17001702

17011703

17021704
def export_unity(path, objects):
1703-
# Duplicated from snippets/export_preset_unity.py (not a package).
17041705
for ob in bpy.context.view_layer.objects:
17051706
ob.select_set(False)
17061707
for ob in objects:
17071708
ob.select_set(True)
17081709
bpy.context.view_layer.objects.active = objects[0]
1709-
bpy.ops.export_scene.gltf(
1710-
filepath=path,
1711-
use_selection=True,
1712-
export_yup=True,
1713-
export_apply=True,
1714-
export_draco_mesh_compression_enable=False,
1715-
export_animations=False,
1716-
)
1710+
bpy.ops.export_scene.gltf(filepath=path, use_selection=True, export_yup=True,
1711+
export_apply=True, export_draco_mesh_compression_enable=False,
1712+
export_animations=False)
17171713

17181714

17191715
def check(skip_decimate, lift_z=False, stray_vert=False, short_brace=False,

‎showcase/book-trolley/book_trolley.py‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1474,10 +1474,9 @@ def export_unity(path, objects):
14741474
for ob in objects:
14751475
ob.select_set(True)
14761476
bpy.context.view_layer.objects.active = objects[0]
1477-
bpy.ops.export_scene.gltf(
1478-
filepath=path, use_selection=True, export_yup=True, export_apply=True,
1479-
export_draco_mesh_compression_enable=False, export_animations=False,
1480-
)
1477+
bpy.ops.export_scene.gltf(filepath=path, use_selection=True, export_yup=True,
1478+
export_apply=True, export_draco_mesh_compression_enable=False,
1479+
export_animations=False)
14811480

14821481

14831482
def check(skip_decimate, lift_z=False, **flags):

‎showcase/bookshelf/bookshelf.py‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1376,10 +1376,9 @@ def export_unity(path, objects):
13761376
for ob in objects:
13771377
ob.select_set(True)
13781378
bpy.context.view_layer.objects.active = objects[0]
1379-
bpy.ops.export_scene.gltf(
1380-
filepath=path, use_selection=True, export_yup=True, export_apply=True,
1381-
export_draco_mesh_compression_enable=False, export_animations=False,
1382-
)
1379+
bpy.ops.export_scene.gltf(filepath=path, use_selection=True, export_yup=True,
1380+
export_apply=True, export_draco_mesh_compression_enable=False,
1381+
export_animations=False)
13831382

13841383

13851384
def check(skip_decimate, lift_z=False, **flags):

‎showcase/boulder-cluster/boulder_cluster.py‎

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1915,18 +1915,18 @@ def make_lod(obj, name, ratio, skip_decimate):
19151915

19161916

19171917
def convex_hull_collider(obj, name):
1918-
# Duplicated from snippets/convex_hull_collider.py (not a package).
1918+
# bmesh.ops.convex_hull can list the same element in geom_interior and
1919+
# geom_unused; once deleted it is invalid and a second delete raises
1920+
# ReferenceError. Filter on is_valid before each delete.
19191921
mesh = bpy.data.meshes.new(name)
19201922
bm = bmesh.new()
19211923
try:
19221924
bm.from_mesh(obj.data)
19231925
result = bmesh.ops.convex_hull(bm, input=list(bm.verts))
1924-
interior = result.get("geom_interior") or []
1925-
unused = result.get("geom_unused") or []
1926+
interior = [g for g in result.get("geom_interior") or [] if g.is_valid]
19261927
if interior:
19271928
bmesh.ops.delete(bm, geom=interior, context="VERTS")
1928-
# the source is a point cloud: a vertex can be both interior and unused
1929-
unused = [v for v in unused if v.is_valid]
1929+
unused = [g for g in result.get("geom_unused") or [] if g.is_valid]
19301930
if unused:
19311931
bmesh.ops.delete(bm, geom=unused, context="VERTS")
19321932
bm.to_mesh(mesh)
@@ -1980,20 +1980,14 @@ def bake_normal(high, low):
19801980

19811981

19821982
def export_unity(path, objects):
1983-
# Duplicated from snippets/export_preset_unity.py (not a package).
19841983
for ob in bpy.context.view_layer.objects:
19851984
ob.select_set(False)
19861985
for ob in objects:
19871986
ob.select_set(True)
19881987
bpy.context.view_layer.objects.active = objects[0]
1989-
bpy.ops.export_scene.gltf(
1990-
filepath=path,
1991-
use_selection=True,
1992-
export_yup=True,
1993-
export_apply=True,
1994-
export_draco_mesh_compression_enable=False,
1995-
export_animations=False,
1996-
)
1988+
bpy.ops.export_scene.gltf(filepath=path, use_selection=True, export_yup=True,
1989+
export_apply=True, export_draco_mesh_compression_enable=False,
1990+
export_animations=False)
19971991

19981992

19991993
def check(skip_decimate, lift_z=False, stray_vert=False, perch_boulder=False, float_slab=False,

0 commit comments

Comments
 (0)