From 8c28f52e6a3d3d2589d321372b5db14ffa7ebbdd Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 5 Mar 2024 12:13:22 +0500 Subject: [PATCH] fix duplicated ifcstyleditems using append_asset since we don't use `file.add` to add inverse items and we use `file.create_entity` it was duplicating new elements, added optional parameter that makes it possible to track already added relationships between `append_asset`s. --- .../ifcopenshell/api/project/append_asset.py | 52 +++++++++++++++++-- .../test/api/project/test_append_asset.py | 17 ++++++ .../ifcpatch/recipes/ExtractElements.py | 5 +- 3 files changed, 68 insertions(+), 6 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py b/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py index 462c4a9181..7ac21e07f3 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py +++ b/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py @@ -22,7 +22,7 @@ import ifcopenshell.api.owner.settings class Usecase: - def __init__(self, file, library=None, element=None): + def __init__(self, file, library=None, element=None, reuse_identities=None): """Appends an asset from a library into the active project A BIM library asset may be a type product (e.g. wall type), product @@ -43,7 +43,12 @@ class Usecase: :param element: An element in the library file of the asset. It may be an IfcTypeProduct, IfcProduct, IfcMaterial, IfcCostSchedule, or IfcProfileDef. - :type library: ifcopenshell.entity_instance.entity_instance + :type element: ifcopenshell.entity_instance.entity_instance + :param reuse_identities: Optional dictionary of mapped entities' identities to the + already created elements. It will be used to avoid creating + duplicated inverse elements during multiple `project.append_asset` calls. If you want + to add just 1 asset or if added assets won't have any shared elements, then it can be left empty. + :type reuse_identities: dict[int, ifcopenshell.entity_instance.entity_instance] :return: The appended element :rtype: ifcopenshell.entity_instance.entity_instance @@ -83,13 +88,37 @@ class Usecase: # Now we can easily append our wall type from our libary wall_type = ifcopenshell.api.run("project.append_asset", model, library=library, element=wall_type) + + Example of adding multiple assets and avoiding duplicated inverses: + + .. code:: python + + # since occurrences of IfcWindow of the same type + # might have shared inverses (e.g. IfcStyledItem) + # we provide a dictionary that will be populated with newly created items + # and reused to avoid duplicated elements + reuse_identities = dict() + + for element in ifcopenshell.util.selector.filter_elements(model, "IfcWindow"): + ifcopenshell.api.run( + "project.append_asset", + model, library=library, + element=wall_type + reuse_identities=reuse_identities + ) + """ self.file: ifcopenshell.file = file - self.settings = {"library": library, "element": element} + self.settings = { + "library": library, + "element": element, + "reuse_identities": {} if reuse_identities is None else reuse_identities, + } def execute(self): # mapping of old element ids to new elements self.added_elements: dict[int, ifcopenshell.entity_instance] = {} + self.reuse_identities: dict[int, ifcopenshell.entity_instance] = self.settings["reuse_identities"] self.whitelisted_inverse_attributes = {} if self.settings["element"].is_a("IfcTypeProduct"): self.target_class = "IfcTypeProduct" @@ -165,7 +194,11 @@ class Usecase: if element_type: ifcopenshell.api.owner.settings.factory_reset() new_type = ifcopenshell.api.run( - "project.append_asset", self.file, library=self.settings["library"], element=element_type + "project.append_asset", + self.file, + library=self.settings["library"], + element=element_type, + reuse_identities=self.reuse_identities, ) ifcopenshell.api.run( "type.assign_type", @@ -232,11 +265,20 @@ class Usecase: elif not attribute_class: self.add_inverse_element(inverse) - def add_inverse_element(self, element): + def add_inverse_element(self, element: ifcopenshell.entity_instance) -> None: # Inverse attributes are added manually because they are basically # relationships that can reference many other assets that we are not # interested in. + + element_identity = element.wrapped_data.identity() + + # check if inverse element were created before + if self.reuse_identities.get(element_identity) is not None: + return + new = self.file.create_entity(element.is_a()) + self.reuse_identities[element_identity] = new + for i, attribute in enumerate(element): new_attribute = None if isinstance(attribute, ifcopenshell.entity_instance): 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 020c2bcf37..3e03be43d5 100644 --- a/src/ifcopenshell-python/test/api/project/test_append_asset.py +++ b/src/ifcopenshell-python/test/api/project/test_append_asset.py @@ -173,6 +173,23 @@ class TestAppendAsset(test.bootstrap.IFC4): assert len(new.RepresentationMaps[0].MappedRepresentation.Items[0].StyledByItem) == 1 assert self.file.by_type("IfcStyledItem")[0].Item == self.file.by_type("IfcBoundingBox")[0] + def test_append_product_with_styles_to_reuse_styleditems(self): + library = ifcopenshell.api.run("project.create_file") + element_type = ifcopenshell.api.run("root.create_entity", library, ifc_class="IfcWallType") + history = library.createIfcOwnerHistory() + element_type.OwnerHistory = history + item = library.createIfcBoundingBox() + library.createIfcStyledItem(Item=item) + context = self.file.createIfcGeometricRepresentationContext() + mapped_rep = library.createIfcShapeRepresentation(Items=[item], ContextOfItems=context) + element_type.RepresentationMaps = [library.createIfcRepresentationMap(MappedRepresentation=mapped_rep)] + + element = ifcopenshell.api.run("root.create_entity", library, ifc_class="IfcWall") + ifcopenshell.api.run("type.assign_type", library, related_object=element, relating_type=element_type) + + ifcopenshell.api.run("project.append_asset", self.file, library=library, element=element) + assert len(self.file.by_type("IfcStyledItem")) == 1 + def test_append_a_type_product_with_a_styled_materials(self): ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject") local_context = ifcopenshell.api.run("context.add_context", self.file, context_type="Model") diff --git a/src/ifcpatch/ifcpatch/recipes/ExtractElements.py b/src/ifcpatch/ifcpatch/recipes/ExtractElements.py index d41bb8fa34..35d3e2bc08 100644 --- a/src/ifcpatch/ifcpatch/recipes/ExtractElements.py +++ b/src/ifcpatch/ifcpatch/recipes/ExtractElements.py @@ -56,6 +56,7 @@ class Patcher: self.aggregates = {} self.new = ifcopenshell.file(schema=self.file.wrapped_data.schema) self.owner_history = None + self.reuse_identities: dict[int, ifcopenshell.entity_instance] = {} for owner_history in self.file.by_type("IfcOwnerHistory"): self.owner_history = self.new.add(owner_history) break @@ -83,7 +84,9 @@ class Patcher: pass if element.is_a("IfcProject"): return self.new.add(element) - return ifcopenshell.api.run("project.append_asset", self.new, library=self.file, element=element) + return ifcopenshell.api.run( + "project.append_asset", self.new, library=self.file, element=element, reuse_identities=self.reuse_identities + ) def add_decomposition_parents(self, element, new_element) -> None: for rel in element.Decomposes: