diff --git a/src/bonsai/bonsai/bim/export_ifc.py b/src/bonsai/bonsai/bim/export_ifc.py index 7a2968472b..778db184d9 100644 --- a/src/bonsai/bonsai/bim/export_ifc.py +++ b/src/bonsai/bonsai/bim/export_ifc.py @@ -49,6 +49,7 @@ class IfcExporter: IfcStore.update_cache() self.sync_all_objects() self.sync_edited_objects() + tool.Project.save_linked_models_to_ifc() extension = self.ifc_export_settings.output_file.split(".")[-1].lower() if extension == "ifczip": with tempfile.TemporaryDirectory() as unzipped_path: diff --git a/src/bonsai/bonsai/bim/import_ifc.py b/src/bonsai/bonsai/bim/import_ifc.py index 53e71a47b3..c130fc2d7b 100644 --- a/src/bonsai/bonsai/bim/import_ifc.py +++ b/src/bonsai/bonsai/bim/import_ifc.py @@ -258,6 +258,8 @@ class IfcImporter: self.profile_code("Place objects in collections") self.setup_arrays() self.profile_code("Setup arrays") + tool.Project.load_linked_models_from_ifc() + self.profile_code("Load linked models") self.lock_scales() self.profile_code("Lock objects scales") self.add_project_to_scene() diff --git a/src/bonsai/bonsai/tool/document.py b/src/bonsai/bonsai/tool/document.py index aed65a4986..25799948fc 100644 --- a/src/bonsai/bonsai/tool/document.py +++ b/src/bonsai/bonsai/tool/document.py @@ -21,7 +21,7 @@ import ifcopenshell.util.system import bonsai.bim.helper import bonsai.core.tool import bonsai.tool as tool -from typing import Any, Union +from typing import Any, Union, Sequence class Document(bonsai.core.tool.Document): @@ -102,7 +102,7 @@ class Document(bonsai.core.tool.Document): def import_references(cls, document: ifcopenshell.entity_instance) -> None: props = bpy.context.scene.BIMDocumentProperties is_ifc2x3 = tool.Ifc.get_schema() == "IFC2X3" - references = (document.DocumentReferences or []) if is_ifc2x3 else document.HasDocumentReferences + references = cls.get_document_references(document) for element in references: new = props.documents.add() new.ifc_definition_id = element.id() @@ -157,3 +157,12 @@ class Document(bonsai.core.tool.Document): def set_external_reference_id(cls, reference: ifcopenshell.entity_instance, value: Union[str, None]) -> None: """Set IfcExternalReference.ItemReference/Identification, compatible with IFC2X3.""" reference[1] = value + + @classmethod + def get_document_references( + cls, document: ifcopenshell.entity_instance + ) -> tuple[ifcopenshell.entity_instance, ...]: + """Get IfcDocumentReference.ReferencedDocuments, compatible with IFC2X3.""" + if document.file.schema == "IFC2X3": + return document.DocumentReferences or () + return document.HasDocumentReferences diff --git a/src/bonsai/bonsai/tool/project.py b/src/bonsai/bonsai/tool/project.py index c309f88346..3bdfcab417 100644 --- a/src/bonsai/bonsai/tool/project.py +++ b/src/bonsai/bonsai/tool/project.py @@ -19,6 +19,7 @@ import os import bpy import ifcopenshell +import ifcopenshell.api.document import ifcopenshell.util.representation import ifcopenshell.util.unit import bonsai.core.aggregate @@ -32,7 +33,7 @@ import bonsai.tool as tool from bonsai.bim.ifc import IfcStore from ifcopenshell.api.project.append_asset import APPENDABLE_ASSET_TYPES from pathlib import Path -from typing import Optional +from typing import Optional, Union class Project(bonsai.core.tool.Project): @@ -214,3 +215,74 @@ class Project(bonsai.core.tool.Project): @classmethod def run_root_reload_grid_decorator(cls) -> None: tool.Root.reload_grid_decorator() + + @classmethod + def get_linked_models_document(cls) -> Union[ifcopenshell.entity_instance, None]: + for document in tool.Ifc.get().by_type("IfcDocumentInformation"): + if document.Name == "BBIM_Linked_Models": + return document + + @classmethod + def load_linked_models_from_ifc(cls) -> None: + links = bpy.context.scene.BIMProjectProperties.links + links.clear() + links_document = cls.get_linked_models_document() + if not links_document: + return + + references = tool.Document.get_document_references(links_document) + if not references: + return + + for reference in references: + link = links.add() + link.name = reference.Location + + @classmethod + def save_linked_models_to_ifc(cls) -> None: + ifc_file = tool.Ifc.get() + links = bpy.context.scene.BIMProjectProperties.links + filepaths: set[Path] = set() + for link in links: + filepaths.add(Path(link.name)) + + links_document = next( + ( + document + for document in ifc_file.by_type("IfcDocumentInformation") + if document.Name == "BBIM_Linked_Models" + ), + None, + ) + + if not filepaths and links_document is None: + return + + paths_to_add = filepaths.copy() + references_to_remove: list[ifcopenshell.entity_instance] = [] + if links_document: + references = tool.Document.get_document_references(links_document) + for reference in references: + # I guess got corrupted by the user. + if not (location := reference.Location): + references_to_remove.remove(reference) + continue + path = Path(location) + if path in paths_to_add: + paths_to_add.remove(path) + else: + references_to_remove.append(reference) + + if paths_to_add: + if links_document is None: + links_document = ifcopenshell.api.document.add_information(ifc_file) + links_document.Name = "BBIM_Linked_Models" + links_document.Description = "Bonsai internal document containing references to currently linked models" + + for path in paths_to_add: + reference = ifcopenshell.api.document.add_reference(ifc_file, links_document) + reference.Location = path.as_posix() + + if references_to_remove: + for reference in references_to_remove: + ifcopenshell.api.document.remove_reference(ifc_file, reference) diff --git a/src/bonsai/test/tool/test_project.py b/src/bonsai/test/tool/test_project.py index 17aaf8b3ac..4b68a97343 100644 --- a/src/bonsai/test/tool/test_project.py +++ b/src/bonsai/test/tool/test_project.py @@ -18,6 +18,8 @@ import bpy import ifcopenshell +import ifcopenshell.api.document +import ifcopenshell.api.root import bonsai.core.tool import bonsai.tool as tool import tempfile @@ -238,3 +240,108 @@ class TestLoadProject(NewFile): assert not tool.Ifc.get() assert bpy.data.objects["IfcWall/Wall"] assert tool.Blender.is_valid_data_block(monkey) + + +class TestLoadLinkedModels(NewFile): + def test_load_linked_models_no_document(self): + links = bpy.context.scene.BIMProjectProperties.links + ifc = ifcopenshell.file() + tool.Ifc.set(ifc) + subject.load_linked_models_from_ifc() + assert len(links) == 0 + + def test_load_linked_models_document_no_references(self): + ifc = ifcopenshell.file() + links = bpy.context.scene.BIMProjectProperties.links + ifcopenshell.api.root.create_entity(ifc, "IfcProject") + document = ifcopenshell.api.document.add_information(ifc) + document.Name = "BBIM_Linked_Models" + tool.Ifc.set(ifc) + subject.load_linked_models_from_ifc() + assert len(links) == 0 + + def test_load_linked_models_document_with_references(self): + ifc = ifcopenshell.file() + links = bpy.context.scene.BIMProjectProperties.links + ifcopenshell.api.root.create_entity(ifc, "IfcProject") + document = ifcopenshell.api.document.add_information(ifc) + document.Name = "BBIM_Linked_Models" + reference = ifcopenshell.api.document.add_reference(ifc, document) + linked_model_path = "test.ifc" + reference.Location = linked_model_path + tool.Ifc.set(ifc) + subject.load_linked_models_from_ifc() + assert len(links) == 1 + assert links[0].name == linked_model_path + + +class TestSaveLinkedModelsToIfc(NewFile): + def test_save_linked_models_to_ifc_no_links(self): + ifc = ifcopenshell.file() + tool.Ifc.set(ifc) + subject.save_linked_models_to_ifc() + assert len(ifc.by_type("IfcDocumentInformation")) == 0 + assert len(ifc.by_type("IfcDocumentReference")) == 0 + + def test_save_linked_models_to_ifc_paths_to_add(self): + ifc = ifcopenshell.file() + ifcopenshell.api.root.create_entity(ifc, "IfcProject") + links = bpy.context.scene.BIMProjectProperties.links + link = links.add() + linked_model_path = "test.ifc" + link.name = linked_model_path + tool.Ifc.set(ifc) + subject.save_linked_models_to_ifc() + assert len(documents := ifc.by_type("IfcDocumentInformation")) == 1 + assert documents[0].Name == "BBIM_Linked_Models" + assert len(references := ifc.by_type("IfcDocumentReference")) == 1 + assert references[0].Location == linked_model_path + + def test_save_linked_models_to_ifc_already_created_references(self): + ifc = ifcopenshell.file() + links = bpy.context.scene.BIMProjectProperties.links + ifcopenshell.api.root.create_entity(ifc, "IfcProject") + + document = ifcopenshell.api.document.add_information(ifc) + document.Name = "BBIM_Linked_Models" + document_id = document.id() + reference = ifcopenshell.api.document.add_reference(ifc, document) + linked_model_path = "test.ifc" + reference.Location = linked_model_path + reference_id = reference.id() + + link = links.add() + linked_model_path = "test.ifc" + link.name = linked_model_path + tool.Ifc.set(ifc) + subject.save_linked_models_to_ifc() + + # Information and references to stay intact. + assert len(documents := ifc.by_type("IfcDocumentInformation")) == 1 + assert documents[0].id() == document_id + assert documents[0].Name == "BBIM_Linked_Models" + assert len(references := ifc.by_type("IfcDocumentReference")) == 1 + assert references[0].id() == reference_id + assert references[0].Location == linked_model_path + + def test_save_linked_models_to_ifc_references_to_remove(self): + ifc = ifcopenshell.file() + links = bpy.context.scene.BIMProjectProperties.links + ifcopenshell.api.root.create_entity(ifc, "IfcProject") + + document = ifcopenshell.api.document.add_information(ifc) + document.Name = "BBIM_Linked_Models" + document_id = document.id() + reference = ifcopenshell.api.document.add_reference(ifc, document) + linked_model_path = "test.ifc" + reference.Location = linked_model_path + + tool.Ifc.set(ifc) + subject.save_linked_models_to_ifc() + links.clear() + + # Remove reference for removed link. + assert len(documents := ifc.by_type("IfcDocumentInformation")) == 1 + assert documents[0].id() == document_id + assert documents[0].Name == "BBIM_Linked_Models" + assert len(ifc.by_type("IfcDocumentReference")) == 0