"""Import and verify exact Spatial Lab base geometry in Blender. From Blender Python: import_bundle(path, scene=dedicated_scene). CLI: blender -b --factory-startup --python bridge.py -- --bundle X --save Y --report Z Only tagged objects of the same project in the specified scene are owned by this adapter. """ import argparse import json import math import sys from pathlib import Path LAB=Path(__file__).resolve().parents[1] if str(LAB) not in sys.path:sys.path.insert(0,str(LAB)) from spatial_lab.kernel import validate_bundle from spatial_lab.project import read_json,write_json from spatial_lab.math3d import point def enum_set(owner,property_name,value): valid=[item.identifier for item in owner.bl_rna.properties[property_name].enum_items] if value not in valid:raise ValueError(f'{value} is not a supported {property_name}: {valid}') setattr(owner,property_name,value) def create_scene(name='Spatial Lab'): import bpy scene=bpy.data.scenes.new(name);scene.unit_settings.scale_length=1;enum_set(scene.unit_settings,'system','METRIC') return scene def _owned(scene,project_id): result={} for o in scene.objects: if o.get('spatial_lab_project')==project_id: key=o.get('spatial_lab_id') if key in result:raise ValueError('Duplicate imported object ID: '+str(key)) result[key]=o return result def import_bundle(source,scene=None,prune=True): import bpy from mathutils import Matrix bundle=read_json(source) if isinstance(source,(str,Path)) else source validate_bundle(bundle) scene=scene or bpy.context.scene if abs(scene.unit_settings.scale_length-1)>1e-9:raise ValueError('Import into a scene with unit scale 1 (metres)') pid=bundle['project_id'];old=_owned(scene,pid) for spec in bundle['objects']: if spec['id'] in old and old[spec['id']].get('spatial_lab_kind')!=spec['kind']:raise ValueError('An existing stable ID changed object kind: '+spec['id']) collection=next((c for c in scene.collection.children if c.get('spatial_lab_project')==pid),None) if collection is None: collection=bpy.data.collections.new('Spatial Lab / '+bundle['name']);collection['spatial_lab_project']=pid;scene.collection.children.link(collection) cache={};created=0;updated=0 for gid,g in bundle['geometries'].items(): me=bpy.data.meshes.new('SL geometry '+gid[:12]);me.from_pydata(g['vertices'],[],g['faces']);me.update() me['spatial_lab_project']=pid;me['spatial_lab_geometry']=gid;cache[gid]=me def material(spec): key=spec['node_id'];mat=next((m for m in bpy.data.materials if m.get('spatial_lab_project')==pid and m.get('spatial_lab_node')==key),None) if mat is None: mat=bpy.data.materials.new('SL / '+key);mat['spatial_lab_project']=pid;mat['spatial_lab_node']=key;mat.use_nodes=True shader=next(n for n in mat.node_tree.nodes if n.type=='BSDF_PRINCIPLED');shader.inputs['Base Color'].default_value=(*spec['color'],1);shader.inputs['Roughness'].default_value=.76 mat.diffuse_color=(*spec['color'],1) return mat seen=set() for spec in bundle['objects']: oid=spec['id'];seen.add(oid);kind=spec['kind'] data=cache.get(spec.get('geometry')) if kind=='curve': pts=spec['points'];edges=[(i,i+1) for i in range(len(pts)-1)] if spec.get('closed'):edges.append((len(pts)-1,0)) data=bpy.data.meshes.new('SL guide '+oid);data.from_pydata(pts,edges,[]);data.update();data['spatial_lab_project']=pid obj=old.get(oid);old_materials=[] if obj is None: obj=bpy.data.objects.new('SL / '+oid,data);collection.objects.link(obj);created+=1 else: old_materials=[s.material for s in obj.material_slots];obj.data=data;updated+=1 if obj.name not in collection.objects:collection.objects.link(obj) obj['spatial_lab_project']=pid;obj['spatial_lab_id']=oid;obj['spatial_lab_kind']=kind;obj['spatial_lab_node']=spec['node_id'];obj['spatial_lab_role']=spec.get('role','geometry') obj['spatial_lab_source_hash']=bundle['bundle_hash'];obj.matrix_world=Matrix(spec['matrix']);obj.color=(*spec['color'],1) if kind=='mesh': assigned=old_materials or [material(spec)] while len(data.materials)tolerance:errors.append('Local-coordinate error exceeds tolerance') if world_error>tolerance:errors.append('World-coordinate error exceeds tolerance') if matrix_error>tolerance:errors.append('Transform error exceeds tolerance') return {'passed':not errors,'project_id':bundle['project_id'],'bundle_hash':bundle['bundle_hash'],'objects_checked':len(expected),'vertices_checked_including_instances':vertices,'triangles_checked_including_instances':faces,'tolerance_m':tolerance,'max_local_vertex_error_m':local_error,'max_world_vertex_error_m':world_error,'max_matrix_component_error':matrix_error,'errors':errors,'scope':'Base mesh coordinates, connectivity, orientation and transforms. Artist modifiers are deliberately excluded.'} def main(): import bpy parser=argparse.ArgumentParser();parser.add_argument('--bundle',required=True);parser.add_argument('--save');parser.add_argument('--report');parser.add_argument('--exercise-update',action='store_true') args=parser.parse_args(sys.argv[sys.argv.index('--')+1:] if '--' in sys.argv else []) bundle=read_json(args.bundle);scene=create_scene('Spatial Lab / '+bundle['name']);bpy.context.window.scene=scene imported=import_bundle(bundle,scene);verified=verify_bundle(bundle,scene) if args.exercise_update: marker=bpy.data.objects.new('Unrelated object must survive',None);scene.collection.objects.link(marker) mesh_obj=next(o for o in scene.objects if o.get('spatial_lab_kind')=='mesh');pointer=mesh_obj.as_pointer() modifier=mesh_obj.modifiers.new('Artist bevel survives reimport','BEVEL');modifier.width=.015;modifier.segments=2 mat=bpy.data.materials.new('Artist material survives reimport');mesh_obj.material_slots[0].link='OBJECT';mesh_obj.material_slots[0].material=mat second=import_bundle(bundle,scene) assert mesh_obj.as_pointer()==pointer and mesh_obj.modifiers.get(modifier.name) is not None and mesh_obj.material_slots[0].material==mat assert marker.name in scene.objects and second['created']==0 # Restore the original base appearance for the delivered demo, after testing preservation. mesh_obj.modifiers.remove(modifier);mesh_obj.material_slots[0].material=next(m for m in bpy.data.materials if m.get('spatial_lab_project')==bundle['project_id'] and m.get('spatial_lab_node')==mesh_obj['spatial_lab_node']) bpy.data.objects.remove(marker,do_unlink=True) verified=verify_bundle(bundle,scene);verified['reimport_preserves_object_identity_modifiers_materials_and_unrelated_objects']=True if args.save: Path(args.save).parent.mkdir(parents=True,exist_ok=True);bpy.ops.wm.save_as_mainfile(filepath=str(Path(args.save).resolve())) result={'import':imported,'verification':verified} if args.report:write_json(args.report,result) print(json.dumps(result,indent=2)) if not verified['passed']:raise RuntimeError('Blender transfer verification failed') if __name__=='__main__':main()