| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192 |
- """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)
|