From 3ffc0bc39c4a6bbc0865722215d3eecb1a686fde Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 20 Jan 2025 16:05:13 +0500 Subject: [PATCH] append_asset to use custom file.add to prevent duplicating profiles and materials #5925 --- .../ifcopenshell/api/project/append_asset.py | 58 ++++++++++++++++++- .../test/api/project/test_append_asset.py | 34 +++++++++++ 2 files changed, 90 insertions(+), 2 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py b/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py index 0274521deb..ade4dbc41b 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py +++ b/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py @@ -143,6 +143,7 @@ class Usecase: file: ifcopenshell.file settings: dict[str, Any] assume_asset_uniqueness_by_name: bool + whitelisted_inverse_attributes: dict[str, list[str]] def execute(self): # mapping of old element ids to new elements @@ -268,7 +269,7 @@ class Usecase: existing_element = self.get_existing_element(element) if existing_element: return existing_element - new = self.file.add(element) + new = self.file_add(element) self.added_elements[element.id()] = new self.check_inverses(element) subelement_queue = self.settings["library"].traverse(element, max_levels=1)[1:] @@ -280,7 +281,7 @@ class Usecase: if not self.has_whitelisted_inverses(existing_element): self.check_inverses(subelement) else: - self.added_elements[subelement.id()] = self.file.add(subelement) + self.added_elements[subelement.id()] = self.file_add(subelement) self.check_inverses(subelement) subelement_queue.extend(self.settings["library"].traverse(subelement, max_levels=1)[1:]) return new @@ -421,3 +422,56 @@ class Usecase: context_type=added_context.ContextType, context_identifier=added_context.ContextIdentifier, ) + + def file_add(self, element: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance: + """Reimplementation of `file.add` but taking into account that some elements (profiles, materials) + are already existing (checking by their name) and shouldn't be duplicated. + + The problem with `file.add` it's recursively adding element and all it's attributes + and there is no control to prevent it from adding certain type of elements. + """ + ifc_file = self.file + if not self.assume_asset_uniqueness_by_name: + return ifc_file.add(element) + + reuse_identities = self.reuse_identities + element_identity = element.wrapped_data.identity() + if added_element := reuse_identities.get(element_identity): + return added_element + + # Maybe element already exists. + if element.is_a("IfcProfileDef"): + profile_name = element.ProfileName + existing_profile = next( + (e for e in ifc_file.by_type("IfcProfileDef") if e.ProfileName == profile_name), None + ) + if existing_profile is not None: + reuse_identities[element_identity] = existing_profile + return existing_profile + elif element.is_a("IfcMaterial"): + material_name = element.Name + existing_material = next((e for e in ifc_file.by_type("IfcMaterial") if e.Name == material_name), None) + if existing_material is not None: + reuse_identities[element_identity] = existing_material + return existing_material + + attrs = {} + for attr_index, attr_value in enumerate(element): + # `None` is set by default already. + if attr_value is None: + continue + elif isinstance(attr_value, ifcopenshell.entity_instance): + attr_value = self.file_add(attr_value) + elif isinstance(attr_value, tuple): + # Assume type is consistent across the tuple. + if isinstance(attr_value[0], ifcopenshell.entity_instance): + attr_value = tuple(self.file_add(e) for e in attr_value) + attrs[attr_index] = attr_value + + # Adding entity at the end just to keep it consistent with `file.add`. + new = ifc_file.create_entity(element.is_a()) + reuse_identities[element_identity] = new + for attr_index, attr_value in attrs.items(): + new[attr_index] = attr_value + + return new 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 224d2f9d1b..e0402f75e5 100644 --- a/src/ifcopenshell-python/test/api/project/test_append_asset.py +++ b/src/ifcopenshell-python/test/api/project/test_append_asset.py @@ -28,6 +28,7 @@ import ifcopenshell.api.style import ifcopenshell.api.context import ifcopenshell.api.project import ifcopenshell.api.material +import ifcopenshell.api.profile import ifcopenshell.util.element import ifcopenshell.util.placement import numpy as np @@ -499,3 +500,36 @@ class TestAppendAssetIFC4(test.bootstrap.IFC4, TestAppendAssetIFC2X3): appended_item = self.file.by_type("IfcCostSchedule")[0].Controls[0].RelatedObjects[0] assert appended_item.is_a("IfcCostItem") assert appended_item.IsNestedBy[0].RelatedObjects[0].is_a("IfcCostItem") + + def test_not_duplicate_profiles_and_materials_based_on_name(self): + library = ifcopenshell.api.project.create_file(version=self.file.schema) + column_type = ifcopenshell.api.root.create_entity(library, "IfcColumnType") + library_profile = ifcopenshell.api.profile.add_parameterized_profile(library, "IfcCircleProfileDef") + library_profile.ProfileName = "TestProfile" + material_set = ifcopenshell.api.material.add_material_set(library, set_type="IfcMaterialProfileSet") + material = ifcopenshell.api.material.add_material(library, "TestMaterial") + ifcopenshell.api.material.add_profile(library, material_set, material, library_profile) + ifcopenshell.api.material.assign_material( + library, [column_type], material=material_set, type="IfcMaterialProfileSet" + ) + + # Test adding a profile with existing name. + profile = ifcopenshell.api.profile.add_parameterized_profile(self.file, "IfcCircleProfileDef") + profile.ProfileName = "TestProfile" + ifcopenshell.api.project.append_asset(self.file, library, library_profile) + profiles = self.file.by_type("IfcProfileDef") + assert len(profiles) == 1 and profiles[0].ProfileName == "TestProfile" + + # Test adding a material with existing name. + material = ifcopenshell.api.material.add_material(self.file, "TestMaterial") + ifcopenshell.api.project.append_asset(self.file, library, library_profile) + materials = self.file.by_type("IfcMaterial") + assert len(materials) == 1 and materials[0].Name == "TestMaterial" + + # Test implicitly adding profile+material with existing names. + ifcopenshell.api.project.append_asset(self.file, library, column_type) + assert len(self.file.by_type("IfcColumnType")) == 1 + profiles = self.file.by_type("IfcProfileDef") + assert len(profiles) == 1 and profiles[0].ProfileName == "TestProfile" + materials = self.file.by_type("IfcMaterial") + assert len(materials) == 1 and materials[0].Name == "TestMaterial"