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