diff --git a/src/blenderbim/blenderbim/bim/module/constraint/operator.py b/src/blenderbim/blenderbim/bim/module/constraint/operator.py
index 91315fedae..f461554c49 100644
--- a/src/blenderbim/blenderbim/bim/module/constraint/operator.py
+++ b/src/blenderbim/blenderbim/bim/module/constraint/operator.py
@@ -205,18 +205,16 @@ class UnassignConstraint(bpy.types.Operator):
return IfcStore.execute_ifc_operator(self, context)
def _execute(self, context):
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
objs = [bpy.data.objects.get(self.obj)] if self.obj else context.selected_objects
- for obj in objs:
- obj_id = obj.BIMObjectProperties.ifc_definition_id
- if not obj_id:
- continue
+ products = [self.file.by_id(obj_id) for obj in objs if (obj_id := obj.BIMObjectProperties.ifc_definition_id)]
+ if products:
ifcopenshell.api.run(
"constraint.unassign_constraint",
self.file,
**{
- "product": self.file.by_id(obj_id),
+ "products": products,
"constraint": self.file.by_id(self.constraint),
- }
+ },
)
return {"FINISHED"}
diff --git a/src/blenderbim/blenderbim/core/resource.py b/src/blenderbim/blenderbim/core/resource.py
index 2d529c466a..63524cd03c 100644
--- a/src/blenderbim/blenderbim/core/resource.py
+++ b/src/blenderbim/blenderbim/core/resource.py
@@ -206,7 +206,7 @@ def remove_usage_constraint(ifc, resource_tool, resource, reference_path):
reference = resource_tool.get_metric_reference(metric, is_deep=True)
if reference == reference_path:
ifc.run("constraint.remove_metric", metric=metric)
- ifc.run("constraint.unassign_constraint", product=resource, constraint=constraint)
+ ifc.run("constraint.unassign_constraint", products=[resource], constraint=constraint)
ifc.run("constraint.remove_constraint", constraint=constraint)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/__init__.py b/src/ifcopenshell-python/ifcopenshell/api/__init__.py
index 07eec78914..f5a56279c0 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/__init__.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/__init__.py
@@ -114,6 +114,9 @@ ARGUMENTS_DEPRECATION = {
"constraint.assign_constraint": partial(
batching_argument_deprecation, prev_argument="product", new_argument="products"
),
+ "constraint.unassign_constraint": partial(
+ batching_argument_deprecation, prev_argument="product", new_argument="products"
+ ),
}
diff --git a/src/ifcopenshell-python/ifcopenshell/api/constraint/unassign_constraint.py b/src/ifcopenshell-python/ifcopenshell/api/constraint/unassign_constraint.py
index 66b3bc94b9..dbc1e1b7fb 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/constraint/unassign_constraint.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/constraint/unassign_constraint.py
@@ -17,18 +17,24 @@
# along with IfcOpenShell. If not, see .
import ifcopenshell
+import ifcopenshell.api
import ifcopenshell.util.element
class Usecase:
- def __init__(self, file, product=None, constraint=None):
- """Unassigns a constraint to a product
+ def __init__(
+ self,
+ file: ifcopenshell.file,
+ products: list[ifcopenshell.entity_instance],
+ constraint: ifcopenshell.entity_instance,
+ ):
+ """Unassigns a constraint from a list of products
The constraint will not be deleted and is available to be assigned to
other products.
- :param product: The product the constraint applies to.
- :type product: ifcopenshell.entity_instance.entity_instance
+ :param products: The list of products the constraint applies to.
+ :type products: list[ifcopenshell.entity_instance.entity_instance]
:param constraint: The IfcObjective constraint
:type constraint: ifcopenshell.entity_instance.entity_instance
:return: None
@@ -36,14 +42,42 @@ class Usecase:
"""
self.file = file
self.settings = {
- "product": product,
+ "products": products,
"constraint": constraint,
}
def execute(self):
- for rel in self.settings["product"].HasAssociations:
- if rel.is_a("IfcRelAssociatesConstraint") and rel.RelatingConstraint == self.settings["constraint"]:
- history = rel.OwnerHistory
- self.file.remove(rel)
- if history:
- ifcopenshell.util.element.remove_deep2(self.file, history)
+ products = set(self.settings["products"])
+ if not products:
+ return
+
+ self.constraint = self.settings["constraint"]
+ rels = self.get_constraint_rels()
+ related_objects = set()
+ for rel in rels:
+ related_objects.update(rel.RelatedObjects)
+
+ if not related_objects.intersection(products):
+ return
+
+ for rel in rels:
+ related_objects = set(rel.RelatedObjects)
+ if not related_objects.intersection(products):
+ continue
+ related_objects -= products
+ if related_objects:
+ rel.RelatedObjects = list(related_objects)
+ ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": rel})
+ continue
+
+ history = rel.OwnerHistory
+ self.file.remove(rel)
+ if history:
+ ifcopenshell.util.element.remove_deep2(self.file, history)
+
+ def get_constraint_rels(self) -> list[ifcopenshell.entity_instance]:
+ rels = []
+ for rel in self.file.get_inverse(self.constraint):
+ if rel.is_a("IfcRelAssociatesConstraint"):
+ rels.append(rel)
+ return rels
diff --git a/src/ifcopenshell-python/test/api/constraint/test_unassign_constraint.py b/src/ifcopenshell-python/test/api/constraint/test_unassign_constraint.py
new file mode 100644
index 0000000000..0d8a21b8bf
--- /dev/null
+++ b/src/ifcopenshell-python/test/api/constraint/test_unassign_constraint.py
@@ -0,0 +1,67 @@
+# IfcOpenShell - IFC toolkit and geometry engine
+# Copyright (C) 2022 Dion Moult
+#
+# This file is part of IfcOpenShell.
+#
+# IfcOpenShell is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# IfcOpenShell is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# 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.constraint
+
+
+class TestUnassignConstraint(test.bootstrap.IFC4):
+ def test_unassigning_a_constraint(self):
+ constraint = ifcopenshell.api.run("constraint.add_objective", self.file)
+ element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
+ element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
+ ifcopenshell.api.run(
+ "constraint.assign_constraint", self.file, products=[element, element2], constraint=constraint
+ )
+ ifcopenshell.api.run(
+ "constraint.unassign_constraint", self.file, products=[element, element2], constraint=constraint
+ )
+ assert ifcopenshell.util.constraint.get_constrained_elements(element) == set()
+ assert len(self.file.by_type("IfcRelAssociatesConstraint")) == 0
+
+ def test_doing_nothing_if_no_constraint(self):
+ constraint = ifcopenshell.api.run("constraint.add_objective", self.file)
+ element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
+ element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
+ ifcopenshell.api.run(
+ "constraint.unassign_constraint", self.file, products=[element, element2], constraint=constraint
+ )
+ assert ifcopenshell.util.constraint.get_constrained_elements(element) == set()
+ assert ifcopenshell.util.constraint.get_constrained_elements(element2) == set()
+
+ def test_updating_the_rel_when_a_reference_is_removed_with_multipled_elements(self):
+ constraint = ifcopenshell.api.run("constraint.add_objective", self.file)
+ element1 = 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")
+ ifcopenshell.api.run("constraint.assign_constraint", self.file, products=[element1], constraint=constraint)
+ rel = self.file.by_type("IfcRelAssociatesConstraint")[0]
+
+ ifcopenshell.api.run(
+ "constraint.assign_constraint", self.file, products=[element2, element3], constraint=constraint
+ )
+ ifcopenshell.api.run(
+ "constraint.unassign_constraint", self.file, products=[element1, element2], constraint=constraint
+ )
+ assert rel.RelatedObjects == (element3,)
+
+
+class TestUnassignConstraintIFC2X3(test.bootstrap.IFC2X3, TestUnassignConstraint):
+ pass
diff --git a/src/ifcopenshell-python/test/api/test_api.py b/src/ifcopenshell-python/test/api/test_api.py
index 5d99932ba8..72d20392f3 100644
--- a/src/ifcopenshell-python/test/api/test_api.py
+++ b/src/ifcopenshell-python/test/api/test_api.py
@@ -298,3 +298,16 @@ class TestTemporarySupportForDeprecatedAPIArguments(test.bootstrap.IFC4):
constraint = ifcopenshell.api.run("constraint.add_objective", self.file)
ifcopenshell.api.run("constraint.assign_constraint", self.file, product=element, constraint=constraint)
assert ifcopenshell.util.constraint.get_constrained_elements(constraint) == {element}
+
+ @deprecation_check
+ def test_unassigning_a_constraint(self):
+ constraint = ifcopenshell.api.run("constraint.add_objective", self.file)
+ element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
+ ifcopenshell.api.run(
+ "constraint.assign_constraint", self.file, product=element, constraint=constraint
+ )
+ ifcopenshell.api.run(
+ "constraint.unassign_constraint", self.file, product=element, constraint=constraint
+ )
+ assert ifcopenshell.util.constraint.get_constrained_elements(element) == set()
+ assert len(self.file.by_type("IfcRelAssociatesConstraint")) == 0