@@ -190,7 +190,7 @@ <h1>mesh-hygiene-audit</h1>
190190< ul > < li > < strong > No ngons.</ strong > < code > len(poly.vertices) <= 4</ code > for every face.</ li > < li > < strong > No loose vertices.</ strong > Every vert has degree ≥ 1.</ li > < li > < strong > Manifold edges.</ strong > Every edge borders exactly 2 faces (closed solid).</ li > < li > < strong > No zero-area faces.</ strong > Face area > 1e-10; prints measured < code > min_area</ code > .</ li > < li > < strong > Outward winding.</ strong > Positive signed volume (divergence theorem).</ li > < li > < strong > Euler sphere.</ strong > < code > V − E + F == 2</ code > for the clean closed manifold (measured: verts=66 edges=136 faces=72, volume≈0.652001, min_area≈1.146e-02).</ li > </ ul >
191191< p > < strong > What each check catches on failure:</ strong > loose vert injected (exit 4); face deleted → boundary edges (exit 5); edge dissolved → ngon (exit 3); all faces flipped → signed volume negated (exit 7); face collapsed → zero-area (exit 6).</ p >
192192< p > < strong > Version witness:</ strong > output is byte-identical on Blender 4.5.11 LTS and 5.1.2 — same counts, same volume, same < code > min_area</ code > .</ p >
193- < p > < strong > Render as proof:</ strong > dual panel, same brass on both. DIRTY stages a camera-facing through-hole (boundary edges) plus a loose vert; emissive beads/edge tubes are built from the live mesh incidence the check uses — not decorative paint. CLEAN is the intact manifold. A warm card behind the hole makes the void readable against the backdrop .</ p >
193+ < p > < strong > Render as proof:</ strong > dual panel, same brass on both. DIRTY stages a camera-facing through-hole (boundary edges) plus a loose vert; emissive beads/edge tubes are built from the live mesh incidence the check uses — not decorative paint. CLEAN is the intact manifold. An out-of-frame AREA light rakes through the hole from behind so the aperture reads warm without a visible light panel in frame .</ p >
194194< p > < strong > Not depicted in the still (check-proven only):</ strong > ngon dissolve, zero-area collapse, and full-mesh winding invert — they do not read as geometry at thumbnail scale without faking annotation.</ p >
195195< h2 > Run</ h2 >
196196< pre > < code > blender --background --python mesh_hygiene_audit.py --
@@ -605,8 +605,8 @@ <h2>Source</h2>
605605 ob.data.materials.append(brass)
606606
607607 bead_mat = make_material(
608- < span class ="s "> "LooseBead"</ span > , (< span class ="n "> 1.0 </ span > , < span class ="n "> 0.85 </ span > , < span class ="n "> 0.15 </ span > ), rough=< span class ="n "> 0.3 </ span > , metallic=< span class ="n "> 0.0 </ span > ,
609- emit=(< span class ="n "> 1.0</ span > , < span class ="n "> 0.9 </ span > , < span class ="n "> 0.2 </ span > ), estr=< span class ="n "> 3.0 </ span > ,
608+ < span class ="s "> "LooseBead"</ span > , (< span class ="n "> 0.95 </ span > , < span class ="n "> 0.75 </ span > , < span class ="n "> 0.2 </ span > ), rough=< span class ="n "> 0.35 </ span > , metallic=< span class ="n "> 0.1 </ span > ,
609+ emit=(< span class ="n "> 1.0</ span > , < span class ="n "> 0.85 </ span > , < span class ="n "> 0.25 </ span > ), estr=< span class ="n "> 0.85 </ span > ,
610610 )
611611 < span class ="k "> for</ span > i, co < span class ="k "> in</ span > enumerate(loose):
612612 sme = bpy.data.meshes.new(< span class ="s "> f"</ span > < span class ="s "> LooseBead</ span > {i}< span class ="s "> "</ span > )
@@ -622,8 +622,8 @@ <h2>Source</h2>
622622 sc.collection.objects.link(sob)
623623
624624 edge_mat = make_material(
625- < span class ="s "> "BoundaryEdge"</ span > , (< span class ="n "> 1.0 </ span > , < span class ="n "> 0.25 </ span > , < span class ="n "> 0.05 </ span > ), rough=< span class ="n "> 0.35 </ span > , metallic=< span class ="n "> 0.0 </ span > ,
626- emit=(< span class ="n "> 1.0</ span > , < span class ="n "> 0.35 </ span > , < span class ="n "> 0.05 </ span > ), estr=< span class ="n "> 2.8 </ span > ,
625+ < span class ="s "> "BoundaryEdge"</ span > , (< span class ="n "> 0.95 </ span > , < span class ="n "> 0.35 </ span > , < span class ="n "> 0.1 </ span > ), rough=< span class ="n "> 0.4 </ span > , metallic=< span class ="n "> 0.15 </ span > ,
626+ emit=(< span class ="n "> 1.0</ span > , < span class ="n "> 0.4 </ span > , < span class ="n "> 0.1 </ span > ), estr=< span class ="n "> 0.7 </ span > ,
627627 )
628628 < span class ="k "> for</ span > i, (a, b) < span class ="k "> in</ span > enumerate(boundary):
629629 mid = (a + b) * < span class ="n "> 0.5</ span >
@@ -678,55 +678,52 @@ <h2>Source</h2>
678678 clean_ob.hide_render = < span class ="k "> True</ span >
679679 clean_ob.hide_viewport = < span class ="k "> True</ span >
680680
681- left = _duplicate_mesh_obj(sc, clean_ob, < span class ="s "> "Dirty"</ span > , (-< span class ="n "> 1.15 </ span > , < span class ="n "> 0.0</ span > , < span class ="n "> 0.0</ span > ))
682- left.rotation_euler.z = math.radians(< span class ="n "> 22.5 </ span > )
681+ left = _duplicate_mesh_obj(sc, clean_ob, < span class ="s "> "Dirty"</ span > , (-< span class ="n "> 0.88 </ span > , < span class ="n "> 0.0</ span > , < span class ="n "> 0.0</ span > ))
682+ left.rotation_euler.z = math.radians(< span class ="n "> 20 </ span > )
683683 bpy.context.view_layer.update()
684684 _inject_through_hole(left.data)
685685 inject_defect(left.data, < span class ="s "> "loose"</ span > )
686686 mark_defects_from_audit(left)
687687
688- right = _duplicate_mesh_obj(sc, clean_ob, < span class ="s "> "Clean"</ span > , (< span class ="n "> 1.15 </ span > , < span class ="n "> 0.0</ span > , < span class ="n "> 0.0</ span > ))
689- right.rotation_euler.z = math.radians(-< span class ="n "> 12 </ span > )
688+ right = _duplicate_mesh_obj(sc, clean_ob, < span class ="s "> "Clean"</ span > , (< span class ="n "> 0.88 </ span > , < span class ="n "> 0.0</ span > , < span class ="n "> 0.0</ span > ))
689+ right.rotation_euler.z = math.radians(-< span class ="n "> 10 </ span > )
690690 right.data.materials.clear()
691691 right.data.materials.append(
692692 make_material(< span class ="s "> "CleanBrass"</ span > , (< span class ="n "> 0.55</ span > , < span class ="n "> 0.38</ span > , < span class ="n "> 0.14</ span > ), rough=< span class ="n "> 0.38</ span > , metallic=< span class ="n "> 0.85</ span > )
693693 )
694694 < span class ="k "> for</ span > p < span class ="k "> in</ span > right.data.polygons:
695695 p.use_smooth = abs(p.normal.z) < < span class ="n "> 0.85</ span >
696696
697- placard(sc, < span class ="s "> "DIRTY"</ span > , (-< span class ="n "> 1.15 </ span > , -< span class ="n "> 1.0 </ span > , < span class ="n "> 0.02</ span > ), size=< span class ="n "> 0.15 </ span > )
698- placard(sc, < span class ="s "> "CLEAN"</ span > , (< span class ="n "> 1.15 </ span > , -< span class ="n "> 1.0 </ span > , < span class ="n "> 0.02</ span > ), size=< span class ="n "> 0.15 </ span > )
697+ placard(sc, < span class ="s "> "DIRTY"</ span > , (-< span class ="n "> 0.88 </ span > , -< span class ="n "> 1.15 </ span > , < span class ="n "> 0.02</ span > ), size=< span class ="n "> 0.11 </ span > )
698+ placard(sc, < span class ="s "> "CLEAN"</ span > , (< span class ="n "> 0.88 </ span > , -< span class ="n "> 1.15 </ span > , < span class ="n "> 0.02</ span > ), size=< span class ="n "> 0.11 </ span > )
699699
700700 build_studio(sc)
701- < span class ="c "> # Lit card behind DIRTY so the through-hole voids read against warm light</ span >
702- card_me = bpy.data.meshes.new(< span class ="s "> "HoleCard"</ span > )
703- cbm = bmesh.new()
701+ < span class ="c "> # Out-of-frame POINT behind DIRTY — aperture glow without a rectangular AREA</ span >
702+ < span class ="c "> # footprint on the floor (Layer 1: no visible light shapes).</ span >
703+ hole_ld = bpy.data.lights.new(< span class ="s "> "HoleBack"</ span > , < span class ="s "> "POINT"</ span > )
704+ hole_ld.energy = < span class ="n "> 95.0</ span >
705+ hole_ld.color = (< span class ="n "> 1.0</ span > , < span class ="n "> 0.70</ span > , < span class ="n "> 0.40</ span > )
704706 < span class ="k "> try</ span > :
705- bmesh.ops.create_grid(cbm, x_segments=< span class ="n "> 1</ span > , y_segments=< span class ="n "> 1</ span > , size=< span class ="n "> 1.2</ span > )
706- cbm.to_mesh(card_me)
707- < span class ="k "> finally</ span > :
708- cbm.free()
709- card_mat = make_material(
710- < span class ="s "> "HoleCard"</ span > , (< span class ="n "> 1.0</ span > , < span class ="n "> 0.55</ span > , < span class ="n "> 0.25</ span > ), rough=< span class ="n "> 0.9</ span > , metallic=< span class ="n "> 0.0</ span > ,
711- emit=(< span class ="n "> 1.0</ span > , < span class ="n "> 0.55</ span > , < span class ="n "> 0.2</ span > ), estr=< span class ="n "> 1.8</ span > ,
712- )
713- card_me.materials.append(card_mat)
714- card = bpy.data.objects.new(< span class ="s "> "HoleCard"</ span > , card_me)
715- card.location = (-< span class ="n "> 1.15</ span > , < span class ="n "> 1.4</ span > , < span class ="n "> 0.65</ span > )
716- card.rotation_euler = (math.radians(< span class ="n "> 90</ span > ), < span class ="n "> 0.0</ span > , < span class ="n "> 0.0</ span > )
717- sc.collection.objects.link(card)
707+ hole_ld.shadow_soft_size = < span class ="n "> 1.6</ span >
708+ < span class ="k "> except</ span > AttributeError:
709+ < span class ="k "> pass</ span >
710+ hole_back = bpy.data.objects.new(< span class ="s "> "HoleBack"</ span > , hole_ld)
711+ hole_back.location = (-< span class ="n "> 0.88</ span > , < span class ="n "> 4.6</ span > , < span class ="n "> 0.75</ span > )
712+ sc.collection.objects.link(hole_back)
713+ < span class ="c "> # Pull key slightly so brass highlights don't clip with the hole glow</ span >
718714 < span class ="k "> for</ span > ob < span class ="k "> in</ span > sc.objects:
719- < span class ="k "> if</ span > ob.type == < span class ="s "> "LIGHT"</ span > < span class ="k "> and</ span > ob.name == < span class ="s "> "Wedge"</ span > :
720- ob.data.energy = < span class ="n "> 580.0</ span >
721- ob.location = (< span class ="n "> 0.0</ span > , < span class ="n "> 6.5</ span > , < span class ="n "> 3.5</ span > )
715+ < span class ="k "> if</ span > ob.type == < span class ="s "> "LIGHT"</ span > < span class ="k "> and</ span > ob.name == < span class ="s "> "Key"</ span > :
716+ ob.data.energy = < span class ="n "> 380.0</ span >
717+ < span class ="k "> if</ span > ob.type == < span class ="s "> "LIGHT"</ span > < span class ="k "> and</ span > ob.name == < span class ="s "> "Glint"</ span > :
718+ ob.data.energy = < span class ="n "> 140.0</ span >
722719
723720 cam_data = bpy.data.cameras.new(< span class ="s "> "Cam"</ span > )
724- cam_data.lens = < span class ="n "> 50 .0</ span >
721+ cam_data.lens = < span class ="n "> 38 .0</ span >
725722 cam = bpy.data.objects.new(< span class ="s "> "Cam"</ span > , cam_data)
726- cam.location = (< span class ="n "> 0.1 </ span > , -< span class ="n "> 5.6 </ span > , < span class ="n "> 1.5 </ span > )
723+ cam.location = (< span class ="n "> 0.0 </ span > , -< span class ="n "> 7.8 </ span > , < span class ="n "> 1.9 </ span > )
727724 sc.collection.objects.link(cam)
728725 aim = bpy.data.objects.new(< span class ="s "> "Aim"</ span > , < span class ="k "> None</ span > )
729- aim.location = (< span class ="n "> 0.0</ span > , < span class ="n "> 0.0</ span > , < span class ="n "> 0.62 </ span > )
726+ aim.location = (< span class ="n "> 0.0</ span > , < span class ="n "> 0.0</ span > , < span class ="n "> 0.48 </ span > )
730727 sc.collection.objects.link(aim)
731728 tr = cam.constraints.new(< span class ="s "> "TRACK_TO"</ span > )
732729 tr.target = aim
0 commit comments