From 3d18aca2f2ff27261d32df0e03ce7d3aeef28909 Mon Sep 17 00:00:00 2001 From: falken10vdl Date: Fri, 21 Nov 2025 17:24:10 +0100 Subject: [PATCH] Add operators for saving and loading blend metadata files --- src/bonsai/bonsai/bim/__init__.py | 2 + src/bonsai/bonsai/bim/module/project/ui.py | 13 ++++ src/bonsai/bonsai/bim/operator.py | 86 ++++++++++++++++++++++ 3 files changed, 101 insertions(+) diff --git a/src/bonsai/bonsai/bim/__init__.py b/src/bonsai/bonsai/bim/__init__.py index 6b1bc27457..63116e7d6f 100644 --- a/src/bonsai/bonsai/bim/__init__.py +++ b/src/bonsai/bonsai/bim/__init__.py @@ -115,11 +115,13 @@ classes = [ operator.EditBlenderCollection, operator.FileAssociate, operator.FileUnassociate, + operator.LoadBlendMetadataAndIFC, operator.OpenPath, operator.OpenUpstream, operator.OpenUri, operator.ReloadIfcFile, operator.RevertClippingPlaneCut, + operator.SaveBlendMetadataFile, operator.SelectDir, operator.SelectIfcFile, operator.SelectURIAttribute, diff --git a/src/bonsai/bonsai/bim/module/project/ui.py b/src/bonsai/bonsai/bim/module/project/ui.py index 6fe5b55604..c728908ed2 100644 --- a/src/bonsai/bonsai/bim/module/project/ui.py +++ b/src/bonsai/bonsai/bim/module/project/ui.py @@ -331,6 +331,19 @@ class BIM_PT_project(Panel): col.prop(props, "ifc_file", text="") row.operator("bim.select_ifc_file", icon="FILE_FOLDER", text="") + # Blend metadata file UI + row = self.layout.row(align=True) + col = row.column() + col.enabled = False + # Show blend metadata file as a label, not editable + blendmetadata_path = "" + if props.ifc_file: + blendmetadata_path = props.ifc_file + ".blendmetadata" + col.label(text=blendmetadata_path) + row.operator("bim.save_blend_metadata_file", icon="FILE_TICK", text="") + row = self.layout.row(align=True) + row.operator("bim.load_blend_metadata_and_ifc", icon="FILE_REFRESH", text="Load Metadata + IFC") + class BIM_PT_new_project_wizard(Panel): bl_label = "New Project Wizard" diff --git a/src/bonsai/bonsai/bim/operator.py b/src/bonsai/bonsai/bim/operator.py index 2d6f7b9340..3d74f20596 100644 --- a/src/bonsai/bonsai/bim/operator.py +++ b/src/bonsai/bonsai/bim/operator.py @@ -236,6 +236,92 @@ class SelectIfcFile(bpy.types.Operator, IFCFileSelector, ImportHelper): return res return ImportHelper.invoke(self, context, event) +class SaveBlendMetadataFile(bpy.types.Operator): + bl_idname = "bim.save_blend_metadata_file" + bl_label = "Save Blend Metadata File" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + """ + Save the current blend file as a metadata-only file (no geometry), preserving settings, window arrangement, geometry nodes, etc. + """ + props = tool.Blender.get_bim_props() + ifc_file = getattr(props, "ifc_file", None) + if not ifc_file: + self.report({"WARNING"}, "No IFC file path set.") + return {"CANCELLED"} + + blendmetadata_path = ifc_file + ".metadata.blend" + + # Save a temporary copy of the current blend file + temp_path = bpy.path.abspath('//__temp_blendmetadata.blend') + bpy.ops.wm.save_as_mainfile(filepath=temp_path, copy=True) + + # Prepare Python script to run in background Blender to remove geometry + cleanup_script = f""" +import bpy +# Remove all mesh objects and mesh collections +for obj in bpy.data.objects: + if obj.type == 'MESH': + bpy.data.objects.remove(obj, do_unlink=True) +for collection in bpy.data.collections: + # Remove collections that only contain mesh objects + if all(o.type == 'MESH' for o in collection.objects): + bpy.data.collections.remove(collection) +bpy.ops.wm.save_as_mainfile(filepath=r'{blendmetadata_path}') +""" + + # Save cleanup script to temp file + import tempfile + with tempfile.NamedTemporaryFile('w', suffix='.py', delete=False) as script_file: + script_file.write(cleanup_script) + script_path = script_file.name + + # Run Blender in background to clean and save metadata file + blender_exe = bpy.app.binary_path + import subprocess + result = subprocess.run([ + blender_exe, + temp_path, + '--background', + '--python', script_path + ], capture_output=True) + + # Clean up temp files + try: + os.remove(temp_path) + os.remove(script_path) + except Exception: + pass + + if result.returncode == 0: + self.report({"INFO"}, f"Blend metadata file saved to: {blendmetadata_path}") + else: + self.report({"ERROR"}, f"Failed to save blend metadata file: {result.stderr.decode()}") + return {"FINISHED"} + +class LoadBlendMetadataAndIFC(bpy.types.Operator): + bl_idname = "bim.load_blend_metadata_and_ifc" + bl_label = "Load Blend Metadata and IFC" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + props = tool.Blender.get_bim_props() + ifc_file = getattr(props, "ifc_file", None) + if not ifc_file: + self.report({"WARNING"}, "No IFC file path set.") + return {"CANCELLED"} + + metadata_path = ifc_file + ".metadata.blend" + # Open the metadata blend file + bpy.ops.wm.open_mainfile(filepath=metadata_path) + # After loading metadata, clear blend warning (no geometry loaded yet) + props = tool.Blender.get_bim_props() + props.has_blend_warning = False + # Load the IFC file into the current session (preserve layout) + bpy.ops.bim.load_project(filepath=ifc_file, should_start_fresh_session=False) + self.report({"INFO"}, f"Loaded metadata and IFC: {metadata_path}, {ifc_file}") + return {"FINISHED"} # TODO: Unused operator. # Is there a need for this or 'DIR_PATH' propety subtype does almost the same,