"""Read-only audit of the SAVED blend and GLBs; writes verification.json only. blender --background house-library.blend --python verify-library.py The caller must open this directory's house-library.blend before running. """ import importlib.util import json import sys from pathlib import Path import bpy import bmesh sys.dont_write_bytecode = True from mathutils import Matrix root = Path(__file__).resolve().parent spec = importlib.util.spec_from_file_location('house_generator', root.parent / 'blender-house-library.py') generator = importlib.util.module_from_spec(spec) spec.loader.exec_module(generator) manifest = json.loads((root / 'library.json').read_text(encoding='utf-8')) assert Path(bpy.data.filepath).resolve() == (root / manifest['blend_file']).resolve() assert all(scene.unit_settings.system == 'METRIC' and scene.unit_settings.scale_length == 1 for scene in bpy.data.scenes) assert len({m.name for m in bpy.data.materials if m.name in {entry['name'] for entry in manifest['materials']}}) == len(manifest['materials']) checked = [] for entry in manifest['archetypes']: assert entry['source_collection'] in bpy.data.collections for name, expected in entry['source_validation'].items(): assert generator.validate_mesh(bpy.data.objects[name]) == expected, name variants = [(entry['joined_object'], entry['validation'], entry['glb']), (entry['lod1']['joined_object'], entry['lod1']['validation'], entry['lod1']['glb']), (entry['foundation']['object'], entry['foundation']['validation'], entry['foundation']['glb'])] for name, expected, exported in variants: obj = bpy.data.objects[name] assert obj.type == 'MESH' and obj.matrix_world == Matrix.Identity(4), name assert not obj.modifiers, name assert generator.validate_mesh(obj) == expected, name assert generator.glb_summary(root / exported['file']) == exported, name assert all(m.use_backface_culling for m in obj.data.materials), name assert all(m is bpy.data.materials[m.name] for m in obj.data.materials), name checked.append(name) assert entry['validation']['bounds_m'][0][2] == 0 assert entry['validation']['triangles'] <= 35000 assert entry['foundation']['validation']['bounds_m'][1][2] < 0 for filename in manifest['preview']['individual_files'] + [manifest['preview']['file']]: assert (root / filename).is_file() and (root / filename).stat().st_size > 10000 # Negative controls mutate disposable copies in memory, never the saved library. negative_controls = {} for mutation in ('deleted_roof_face', 'inverted_roof_shell'): original = bpy.data.objects['SRC_house__Roof'] mutant = bpy.data.objects.new('NEGATIVE_' + mutation, original.data.copy()) bm = bmesh.new() bm.from_mesh(mutant.data) bm.faces.ensure_lookup_table() if mutation == 'deleted_roof_face': bmesh.ops.delete(bm, geom=[bm.faces[0]], context='FACES_ONLY') else: first = bm.faces[0] shell, stack = {first}, [first] while stack: for edge in stack.pop().edges: for neighbor in edge.link_faces: if neighbor not in shell: shell.add(neighbor) stack.append(neighbor) bmesh.ops.reverse_faces(bm, faces=list(shell)) bm.to_mesh(mutant.data) bm.free() try: generator.validate_mesh(mutant) except RuntimeError as error: message = str(error) assert ('nonmanifold_edges' if mutation == 'deleted_roof_face' else 'negative_volume_shells') in message negative_controls[mutation] = {'status': 'correctly_rejected', 'reason': message} else: raise AssertionError('Validator accepted negative control: ' + mutation) finally: mesh = mutant.data bpy.data.objects.remove(mutant, do_unlink=True) bpy.data.meshes.remove(mesh) result = {'status': 'passed', 'negative_controls': negative_controls, 'blender': bpy.app.version_string, 'archetypes': len(manifest['archetypes']), 'source_meshes': sum(len(a['source_objects']) for a in manifest['archetypes']), 'runtime_and_foundation_meshes_checked': len(checked), 'objects': checked, 'checks': ['Saved source collections and named objects resolve', 'Metric unit scale and identity object transforms', 'Shell edge/vertex manifoldness, positive signed volumes and contiguous winding', 'No zero-area faces or degenerate Blender triangles', 'One mesh per GLB and triangle counts match', 'Decoded GLB indices/positions/normals checked', 'Shared Blender materials and backface culling', 'Ground Z=0; foundations strictly below zero; <=35000 triangles', 'Preview files exist']} (root / 'verification.json').write_text(json.dumps(result, indent=2) + '\n', encoding='utf-8') print(json.dumps(result, indent=2), flush=True)