diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index 02ae75792b..e5bae47acc 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -637,10 +637,12 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator): elif element.is_a("IfcProduct"): # NOTE: Non-types are not exposed in UI directly # but the code is still used when appending products by query. - self.import_product_from_ifc(element, context) - element_type = ifcopenshell.util.element.get_type(element) - if element_type is not None and tool.Ifc.get_object(element_type) is None: - self.import_type_from_ifc(element_type, context) + elements = self.get_appended_elements(element) + self.import_product_from_ifc(elements, context) + for appended_element in elements: + element_type = ifcopenshell.util.element.get_type(appended_element) + if element_type is not None and tool.Ifc.get_object(element_type) is None: + self.import_type_from_ifc(element_type, context) elif element.is_a("IfcMaterial"): self.import_material_from_ifc(element, context) elif element.is_a("IfcSurfaceStyle"): @@ -676,17 +678,32 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator): ifc_importer.file = self.file ifc_importer.create_style(style) - def import_product_from_ifc(self, element: ifcopenshell.entity_instance, context: bpy.types.Context) -> None: + def get_appended_elements(self, element: ifcopenshell.entity_instance) -> set[ifcopenshell.entity_instance]: + """``element`` plus any parts brought in with it (e.g. the parts of an appended assembly).""" + elements = {element} + for part in ifcopenshell.util.element.get_decomposition(element): + if not part.is_a("IfcFeatureElement") or part.is_a("IfcSurfaceFeature"): + elements.add(part) + return elements + + def import_product_from_ifc( + self, + elements: Union[ifcopenshell.entity_instance, set[ifcopenshell.entity_instance]], + context: bpy.types.Context, + ) -> None: self.file = tool.Ifc.get() + if isinstance(elements, ifcopenshell.entity_instance): + elements = {elements} logger = logging.getLogger("ImportIFC") ifc_import_settings = import_ifc.IfcImportSettings.factory(context, IfcStore.path, logger) ifc_importer = import_ifc.IfcImporter(ifc_import_settings) ifc_importer.file = self.file ifc_importer.process_context_filter() ifc_importer.material_creator.load_existing_materials() - self.import_materials(element, ifc_importer) - self.import_styles(element, ifc_importer) - ifc_importer.create_generic_elements({element}) + for element in elements: + self.import_materials(element, ifc_importer) + self.import_styles(element, ifc_importer) + ifc_importer.create_generic_elements(elements) ifc_importer.place_objects_in_collections() def import_type_from_ifc(self, element: ifcopenshell.entity_instance, context: bpy.types.Context) -> None: @@ -2772,10 +2789,12 @@ class AppendInspectedLinkedElement(AppendLibraryElement): library=linked_ifc_file, element=element_to_append, ) - self.import_product_from_ifc(element, context) - element_type = ifcopenshell.util.element.get_type(element) - if element_type and tool.Ifc.get_object(element_type) is None: - self.import_type_from_ifc(element_type, context) + elements = self.get_appended_elements(element) + self.import_product_from_ifc(elements, context) + for appended_element in elements: + element_type = ifcopenshell.util.element.get_type(appended_element) + if element_type and tool.Ifc.get_object(element_type) is None: + self.import_type_from_ifc(element_type, context) return {"FINISHED"} diff --git a/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py b/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py index 8a8f2307a4..d2cc391cce 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py +++ b/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py @@ -397,7 +397,7 @@ class Usecase: self.whitelisted_inverse_attributes = { "IfcObjectDefinition": ["HasAssociations"], "IfcObject": ["IsDefinedBy.IfcRelDefinesByProperties"], - "IfcElement": ["HasOpenings"], + "IfcElement": ["HasOpenings", "IsDecomposedBy"], "IfcDistributionElement": ["IsNestedBy"], self.base_material_class: ["HasExternalReferences", "HasProperties", "HasRepresentation"], "IfcRepresentationItem": [ @@ -529,24 +529,18 @@ class Usecase: new = self.file.create_entity(element.is_a()) self.reuse_identities[element_identity] = new + # Void, projection, and aggregation relationships are "dependent" and always considered. + is_dependent_rel = element.is_a() in ("IfcRelVoidsElement", "IfcRelProjectsElement", "IfcRelAggregates") + for i, attribute in enumerate(element): new_attribute = None if isinstance(attribute, ifcopenshell.entity_instance): - # Void and projection relationships are special because they - # are "dependent" relationships, so we always consider them. - # We do _not_ whitelist (i.e. in is_another_asset) - # IfcFeatureElement because you can have things like - # IfcRelAssociatesClassification to openings! We only ever want - # to consider IfcFeatureElements in IfcRelVoidsElements and - # IfcRelProjectsElements. - if element.is_a() in ("IfcRelVoidsElement", "IfcRelProjectsElement") or not self.is_another_asset( - attribute - ): + if is_dependent_rel or not self.is_another_asset(attribute): new_attribute = self.add_element(attribute) elif isinstance(attribute, tuple) and attribute and isinstance(attribute[0], ifcopenshell.entity_instance): new_attribute = [] for item in attribute: - if self.is_another_asset(item): + if not is_dependent_rel and self.is_another_asset(item): continue if skip_not_reused_entities_attr_i is not None and i == skip_not_reused_entities_attr_i: identity = item.wrapped_data.identity() diff --git a/src/ifcopenshell-python/test/api/project/test_append_asset.py b/src/ifcopenshell-python/test/api/project/test_append_asset.py index 949fc2fa5f..0ad383e4b2 100644 --- a/src/ifcopenshell-python/test/api/project/test_append_asset.py +++ b/src/ifcopenshell-python/test/api/project/test_append_asset.py @@ -18,6 +18,7 @@ import numpy as np +import ifcopenshell.api.aggregate import ifcopenshell.api.classification import ifcopenshell.api.context import ifcopenshell.api.cost @@ -409,6 +410,29 @@ class TestAppendAssetIFC2X3(test.bootstrap.IFC2X3): ifcopenshell.api.project.append_asset(self.file, library=library, element=element) assert self.file.by_type("IfcWall")[0].HasOpenings[0].RelatedOpeningElement.is_a("IfcOpeningElement") + def test_append_an_aggregate_with_its_parts_and_sub_aggregates_and_openings(self): + library = ifcopenshell.api.project.create_file(version=self.file.schema) + top = ifcopenshell.api.root.create_entity(library, ifc_class="IfcElementAssembly", name="House_Module") + sub_assembly = ifcopenshell.api.root.create_entity( + library, ifc_class="IfcElementAssembly", name="Wall_Panel_SubAssembly" + ) + part = ifcopenshell.api.root.create_entity(library, ifc_class="IfcWall", name="Panel Wall") + opening = ifcopenshell.api.root.create_entity(library, ifc_class="IfcOpeningElement") + ifcopenshell.api.feature.add_feature(library, feature=opening, element=part) + ifcopenshell.api.aggregate.assign_object(library, relating_object=sub_assembly, products=[part]) + ifcopenshell.api.aggregate.assign_object(library, relating_object=top, products=[sub_assembly]) + + appended = ifcopenshell.api.project.append_asset(self.file, library=library, element=top) + + assert len(self.file.by_type("IfcElementAssembly")) == 2 + assert len(self.file.by_type("IfcWall")) == 1 + assert len(self.file.by_type("IfcOpeningElement")) == 1 + appended_sub = appended.IsDecomposedBy[0].RelatedObjects[0] + assert appended_sub.is_a("IfcElementAssembly") + appended_part = appended_sub.IsDecomposedBy[0].RelatedObjects[0] + assert appended_part.is_a("IfcWall") + assert appended_part.HasOpenings[0].RelatedOpeningElement.is_a("IfcOpeningElement") + def test_append_a_product_with_unrelated_relationships_to_openings(self): library = ifcopenshell.api.project.create_file(version=self.file.schema) ifcopenshell.api.root.create_entity(library, ifc_class="IfcProject")