From 144831f9ee0a2e3b53d060e43ec9bf92c6a321c2 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 15 Apr 2024 12:20:19 +0500 Subject: [PATCH] classification.remove_reference - support batching #4474 --- .../bim/module/classification/operator.py | 18 ++- .../ifcopenshell/api/__init__.py | 3 + .../api/classification/remove_reference.py | 110 ++++++++++++------ .../classification/test_remove_reference.py | 71 +++++++---- src/ifcopenshell-python/test/api/test_api.py | 17 +++ 5 files changed, 155 insertions(+), 64 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/classification/operator.py b/src/blenderbim/blenderbim/bim/module/classification/operator.py index fa35886176..74c84eae21 100644 --- a/src/blenderbim/blenderbim/bim/module/classification/operator.py +++ b/src/blenderbim/blenderbim/bim/module/classification/operator.py @@ -20,6 +20,7 @@ import bpy import json import ifcopenshell import ifcopenshell.api +import ifcopenshell.util.classification import ifcopenshell.util.element import blenderbim.tool as tool import blenderbim.bim.helper @@ -293,6 +294,7 @@ class RemoveClassificationReference(bpy.types.Operator, tool.Ifc.Operator): active_reference = tool.Ifc.get().by_id(self.reference) identification = active_reference[1] + elements_by_references: dict[ifcopenshell.entity_instance, list[ifcopenshell.entity_instance]] = [] for obj in objects: ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(obj, self.obj_type, context) element = tool.Ifc.get().by_id(ifc_definition_id) @@ -301,12 +303,16 @@ class RemoveClassificationReference(bpy.types.Operator, tool.Ifc.Operator): if (identification and reference[1] == identification) or ( not identification and reference == active_reference ): - ifcopenshell.api.run( - "classification.remove_reference", - tool.Ifc.get(), - reference=reference, - product=element, - ) + elements_by_references.setdefault(reference, []).append(element) + + if elements_by_references: + for reference, products in elements_by_references.items(): + ifcopenshell.api.run( + "classification.remove_reference", + tool.Ifc.get(), + reference=reference, + products=products, + ) class EditClassificationReference(bpy.types.Operator, tool.Ifc.Operator): diff --git a/src/ifcopenshell-python/ifcopenshell/api/__init__.py b/src/ifcopenshell-python/ifcopenshell/api/__init__.py index 7c739cf71b..d2027e06e1 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/__init__.py +++ b/src/ifcopenshell-python/ifcopenshell/api/__init__.py @@ -90,6 +90,9 @@ ARGUMENTS_DEPRECATION = { "classification.add_reference": partial( batching_argument_deprecation, prev_argument="product", new_argument="products" ), + "classification.remove_reference": partial( + batching_argument_deprecation, prev_argument="product", new_argument="products" + ), } diff --git a/src/ifcopenshell-python/ifcopenshell/api/classification/remove_reference.py b/src/ifcopenshell-python/ifcopenshell/api/classification/remove_reference.py index b91989a1ae..d4e8d802a6 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/classification/remove_reference.py +++ b/src/ifcopenshell-python/ifcopenshell/api/classification/remove_reference.py @@ -17,12 +17,18 @@ # along with IfcOpenShell. If not, see . import ifcopenshell +import ifcopenshell.api import ifcopenshell.util.element class Usecase: - def __init__(self, file, reference=None, product=None): - """Removes a classification reference from a product + def __init__( + self, + file: ifcopenshell.file, + reference: ifcopenshell.entity_instance, + products: list[ifcopenshell.entity_instance], + ): + """Removes a classification reference from the list of products If the classification reference is no longer associated to any products, the classification reference itself is also removed. @@ -30,9 +36,12 @@ class Usecase: :param reference: The IfcClassificationReference entity of the relationship you want to remove. :type reference: ifcopenshell.entity_instance.entity_instance - :param product: The object entity of the relationship you want to + :param product: The list fo object entities of the relationship you want to remove. - :type reference: ifcopenshell.entity_instance.entity_instance + :type product: list[ifcopenshell.entity_instance.entity_instance] + + :raises TypeError: If file is IFC2X3 and `products` has non-IfcRoot elements. + :return: None :rtype: None @@ -47,39 +56,72 @@ class Usecase: products=[wall_type], classification=classification, identification="W_01", name="Interior Walls") ifcopenshell.api.run("classification.remove_reference", model, - reference=reference, product=wall_type) + reference=reference, products=[wall_type]) """ self.file = file - self.settings = {"reference": reference, "product": product} + self.settings = {"reference": reference, "products": products} - def execute(self): - if self.settings["product"].is_a("IfcRoot"): - for rel in self.file.by_type("IfcRelAssociatesClassification"): - if rel.RelatingClassification == self.settings["reference"] and rel.RelatedObjects: - if self.settings["product"] in rel.RelatedObjects: - related_objects = list(rel.RelatedObjects) - related_objects.remove(self.settings["product"]) - if len(related_objects): - rel.RelatedObjects = related_objects - else: - history = rel.OwnerHistory - self.file.remove(rel) - if history: - ifcopenshell.util.element.remove_deep2(self.file, history) - else: - for rel in self.file.by_type("IfcExternalReferenceRelationship"): - if rel.RelatingReference == self.settings["reference"] and rel.RelatedResourceObjects: - if self.settings["product"] in rel.RelatedResourceObjects: - related_objects = list(rel.RelatedResourceObjects) - related_objects.remove(self.settings["product"]) - if len(related_objects): - rel.RelatedResourceObjects = related_objects - else: - self.file.remove(rel) + def execute(self) -> None: + is_ifc2x3 = self.file.schema == "IFC2X3" + products = set(self.settings["products"]) + referenced = ifcopenshell.util.element.get_referenced_elements(self.settings["reference"]) + products -= products.difference(referenced) + + # all products are already unassigned from a reference + if not products: + return + + rooted_products: set[ifcopenshell.entity_instance] = set() + non_rooted_products: set[ifcopenshell.entity_instance] = set() + for product in self.settings["products"]: + if product.is_a("IfcRoot"): + rooted_products.add(product) + else: + non_rooted_products.add(product) + + if non_rooted_products and is_ifc2x3: + raise TypeError(f"Cannot add reference to non-IfcRoot element in IFC2X3: {non_rooted_products}.") + + if rooted_products: + reference_rels: set[ifcopenshell.entity_instance] = set() + for product in rooted_products: + reference_rels.update(product.HasAssociations) + + reference_rels = { + rel + for rel in reference_rels + if rel.is_a("IfcRelAssociatesClassification") + and rel.RelatingClassification == self.settings["reference"] + } + + for rel in reference_rels: + related_objects = set(rel.RelatedObjects) - rooted_products + if related_objects: + rel.RelatedObjects = list(related_objects) + ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": rel}) + else: + history = rel.OwnerHistory + self.file.remove(rel) + if history: + ifcopenshell.util.element.remove_deep2(self.file, history) + + if non_rooted_products: + reference_rels: set[ifcopenshell.entity_instance] = set() + for product in non_rooted_products: + rels = getattr(product, "HasExternalReferences", None) + if rels is None: + rels = getattr(product, "HasExternalReference", []) + reference_rels.update(rels) + + reference_rels = {rel for rel in reference_rels if rel.RelatingReference == self.settings["reference"]} + for rel in reference_rels: + related_objects = set(rel.RelatedResourceObjects) - non_rooted_products + if related_objects: + rel.RelatedResourceObjects = list(related_objects) + else: + self.file.remove(rel) # TODO: we only handle lightweight classifications here - if ( - not self.settings["reference"].ClassificationRefForObjects - and not self.settings["reference"].ExternalReferenceForResources - ): + referenced_elements = ifcopenshell.util.element.get_referenced_elements(self.settings["reference"]) + if not referenced_elements: self.file.remove(self.settings["reference"]) diff --git a/src/ifcopenshell-python/test/api/classification/test_remove_reference.py b/src/ifcopenshell-python/test/api/classification/test_remove_reference.py index 63ae287aa3..8e03fdef4f 100644 --- a/src/ifcopenshell-python/test/api/classification/test_remove_reference.py +++ b/src/ifcopenshell-python/test/api/classification/test_remove_reference.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . +import pytest import test.bootstrap import ifcopenshell.api import ifcopenshell.util.classification @@ -25,58 +26,80 @@ class TestRemoveReference(test.bootstrap.IFC4): def test_removing_a_reference(self): ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject") element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") result = ifcopenshell.api.run("classification.add_classification", self.file, classification="Name") reference = ifcopenshell.api.run( "classification.add_reference", self.file, - products=[element], + products=[element, element2], identification="X", name="Foobar", classification=result, ) - ifcopenshell.api.run("classification.remove_reference", self.file, product=element, reference=reference) + ifcopenshell.api.run( + "classification.remove_reference", self.file, products=[element, element2], reference=reference + ) assert len(ifcopenshell.util.classification.get_references(element)) == 0 + assert len(ifcopenshell.util.classification.get_references(element2)) == 0 assert len(self.file.by_type("IfcClassificationReference")) == 0 - def test_removing_a_reference_from_a_resource(self): + def test_removing_a_reference_from_a_resource_and_from_a_root(self): ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject") element = self.file.createIfcMaterial() + element2 = self.file.createIfcCostValue() + element3 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") result = ifcopenshell.api.run("classification.add_classification", self.file, classification="Name") - reference = ifcopenshell.api.run( - "classification.add_reference", - self.file, - products=[element], - identification="X", - name="Foobar", - classification=result, + if self.file.schema == "IFC2X3": + with pytest.raises(TypeError): + reference = ifcopenshell.api.run( + "classification.add_reference", + self.file, + products=[element, element2, element3], + identification="X", + name="Foobar", + classification=result, + ) + return + else: + reference = ifcopenshell.api.run( + "classification.add_reference", + self.file, + products=[element, element2, element3], + identification="X", + name="Foobar", + classification=result, + ) + + ifcopenshell.api.run( + "classification.remove_reference", self.file, products=[element, element2, element3], reference=reference ) - ifcopenshell.api.run("classification.remove_reference", self.file, product=element, reference=reference) assert len(ifcopenshell.util.classification.get_references(element)) == 0 + assert len(ifcopenshell.util.classification.get_references(element2)) == 0 + assert len(ifcopenshell.util.classification.get_references(element3)) == 0 assert len(self.file.by_type("IfcClassificationReference")) == 0 def test_retaining_the_reference_if_still_in_use(self): ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject") - element = self.file.createIfcMaterial() - element2 = self.file.createIfcMaterial() + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + element3 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") result = ifcopenshell.api.run("classification.add_classification", self.file, classification="Name") reference = ifcopenshell.api.run( "classification.add_reference", self.file, - products=[element], - identification="X", - name="Foobar", - classification=result, - ) - reference2 = ifcopenshell.api.run( - "classification.add_reference", - self.file, - products=[element2], + products=[element, element2, element3], identification="X", name="Foobar", classification=result, ) assert len(self.file.by_type("IfcClassificationReference")) == 1 - ifcopenshell.api.run("classification.remove_reference", self.file, product=element, reference=reference) + ifcopenshell.api.run( + "classification.remove_reference", self.file, products=[element, element2], reference=reference + ) assert len(self.file.by_type("IfcClassificationReference")) == 1 - ifcopenshell.api.run("classification.remove_reference", self.file, product=element2, reference=reference2) + ifcopenshell.api.run("classification.remove_reference", self.file, products=[element3], reference=reference) assert len(self.file.by_type("IfcClassificationReference")) == 0 + + +class TestRemoveReferenceIFC2X3(test.bootstrap.IFC2X3, TestRemoveReference): + pass diff --git a/src/ifcopenshell-python/test/api/test_api.py b/src/ifcopenshell-python/test/api/test_api.py index 7910cf9739..0156bfa6ad 100644 --- a/src/ifcopenshell-python/test/api/test_api.py +++ b/src/ifcopenshell-python/test/api/test_api.py @@ -219,3 +219,20 @@ class TestTemporarySupportForDeprecatedAPIArguments(test.bootstrap.IFC4): assert list(ifcopenshell.util.classification.get_references(element2))[0].Identification == "X" assert list(ifcopenshell.util.classification.get_references(element2))[0].Name == "Foobar" assert list(ifcopenshell.util.classification.get_references(element2))[0] == references[0] + + @deprecation_check + def test_removing_a_reference(self): + ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject") + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + result = ifcopenshell.api.run("classification.add_classification", self.file, classification="Name") + reference = ifcopenshell.api.run( + "classification.add_reference", + self.file, + products=[element], + identification="X", + name="Foobar", + classification=result, + ) + ifcopenshell.api.run("classification.remove_reference", self.file, product=element, reference=reference) + assert len(ifcopenshell.util.classification.get_references(element)) == 0 + assert len(self.file.by_type("IfcClassificationReference")) == 0