classification.remove_reference - support batching #4474

This commit is contained in:
Andrej730
2024-04-15 12:20:19 +05:00
parent d6e69bd6e9
commit 144831f9ee
5 changed files with 155 additions and 64 deletions
@@ -20,6 +20,7 @@ import bpy
import json import json
import ifcopenshell import ifcopenshell
import ifcopenshell.api import ifcopenshell.api
import ifcopenshell.util.classification
import ifcopenshell.util.element import ifcopenshell.util.element
import blenderbim.tool as tool import blenderbim.tool as tool
import blenderbim.bim.helper 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) active_reference = tool.Ifc.get().by_id(self.reference)
identification = active_reference[1] identification = active_reference[1]
elements_by_references: dict[ifcopenshell.entity_instance, list[ifcopenshell.entity_instance]] = []
for obj in objects: for obj in objects:
ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(obj, self.obj_type, context) ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(obj, self.obj_type, context)
element = tool.Ifc.get().by_id(ifc_definition_id) 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 ( if (identification and reference[1] == identification) or (
not identification and reference == active_reference not identification and reference == active_reference
): ):
ifcopenshell.api.run( elements_by_references.setdefault(reference, []).append(element)
"classification.remove_reference",
tool.Ifc.get(), if elements_by_references:
reference=reference, for reference, products in elements_by_references.items():
product=element, ifcopenshell.api.run(
) "classification.remove_reference",
tool.Ifc.get(),
reference=reference,
products=products,
)
class EditClassificationReference(bpy.types.Operator, tool.Ifc.Operator): class EditClassificationReference(bpy.types.Operator, tool.Ifc.Operator):
@@ -90,6 +90,9 @@ ARGUMENTS_DEPRECATION = {
"classification.add_reference": partial( "classification.add_reference": partial(
batching_argument_deprecation, prev_argument="product", new_argument="products" batching_argument_deprecation, prev_argument="product", new_argument="products"
), ),
"classification.remove_reference": partial(
batching_argument_deprecation, prev_argument="product", new_argument="products"
),
} }
@@ -17,12 +17,18 @@
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>. # along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.element import ifcopenshell.util.element
class Usecase: class Usecase:
def __init__(self, file, reference=None, product=None): def __init__(
"""Removes a classification reference from a product 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, If the classification reference is no longer associated to any products,
the classification reference itself is also removed. the classification reference itself is also removed.
@@ -30,9 +36,12 @@ class Usecase:
:param reference: The IfcClassificationReference entity of the :param reference: The IfcClassificationReference entity of the
relationship you want to remove. relationship you want to remove.
:type reference: ifcopenshell.entity_instance.entity_instance :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. 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 :return: None
:rtype: None :rtype: None
@@ -47,39 +56,72 @@ class Usecase:
products=[wall_type], classification=classification, products=[wall_type], classification=classification,
identification="W_01", name="Interior Walls") identification="W_01", name="Interior Walls")
ifcopenshell.api.run("classification.remove_reference", model, ifcopenshell.api.run("classification.remove_reference", model,
reference=reference, product=wall_type) reference=reference, products=[wall_type])
""" """
self.file = file self.file = file
self.settings = {"reference": reference, "product": product} self.settings = {"reference": reference, "products": products}
def execute(self): def execute(self) -> None:
if self.settings["product"].is_a("IfcRoot"): is_ifc2x3 = self.file.schema == "IFC2X3"
for rel in self.file.by_type("IfcRelAssociatesClassification"): products = set(self.settings["products"])
if rel.RelatingClassification == self.settings["reference"] and rel.RelatedObjects: referenced = ifcopenshell.util.element.get_referenced_elements(self.settings["reference"])
if self.settings["product"] in rel.RelatedObjects: products -= products.difference(referenced)
related_objects = list(rel.RelatedObjects)
related_objects.remove(self.settings["product"]) # all products are already unassigned from a reference
if len(related_objects): if not products:
rel.RelatedObjects = related_objects return
else:
history = rel.OwnerHistory rooted_products: set[ifcopenshell.entity_instance] = set()
self.file.remove(rel) non_rooted_products: set[ifcopenshell.entity_instance] = set()
if history: for product in self.settings["products"]:
ifcopenshell.util.element.remove_deep2(self.file, history) if product.is_a("IfcRoot"):
else: rooted_products.add(product)
for rel in self.file.by_type("IfcExternalReferenceRelationship"): else:
if rel.RelatingReference == self.settings["reference"] and rel.RelatedResourceObjects: non_rooted_products.add(product)
if self.settings["product"] in rel.RelatedResourceObjects:
related_objects = list(rel.RelatedResourceObjects) if non_rooted_products and is_ifc2x3:
related_objects.remove(self.settings["product"]) raise TypeError(f"Cannot add reference to non-IfcRoot element in IFC2X3: {non_rooted_products}.")
if len(related_objects):
rel.RelatedResourceObjects = related_objects if rooted_products:
else: reference_rels: set[ifcopenshell.entity_instance] = set()
self.file.remove(rel) 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 # TODO: we only handle lightweight classifications here
if ( referenced_elements = ifcopenshell.util.element.get_referenced_elements(self.settings["reference"])
not self.settings["reference"].ClassificationRefForObjects if not referenced_elements:
and not self.settings["reference"].ExternalReferenceForResources
):
self.file.remove(self.settings["reference"]) self.file.remove(self.settings["reference"])
@@ -16,6 +16,7 @@
# You should have received a copy of the GNU Lesser General Public License # You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>. # along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import pytest
import test.bootstrap import test.bootstrap
import ifcopenshell.api import ifcopenshell.api
import ifcopenshell.util.classification import ifcopenshell.util.classification
@@ -25,58 +26,80 @@ class TestRemoveReference(test.bootstrap.IFC4):
def test_removing_a_reference(self): def test_removing_a_reference(self):
ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject") ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject")
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") 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") result = ifcopenshell.api.run("classification.add_classification", self.file, classification="Name")
reference = ifcopenshell.api.run( reference = ifcopenshell.api.run(
"classification.add_reference", "classification.add_reference",
self.file, self.file,
products=[element], products=[element, element2],
identification="X", identification="X",
name="Foobar", name="Foobar",
classification=result, 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(element)) == 0
assert len(ifcopenshell.util.classification.get_references(element2)) == 0
assert len(self.file.by_type("IfcClassificationReference")) == 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") ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject")
element = self.file.createIfcMaterial() 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") result = ifcopenshell.api.run("classification.add_classification", self.file, classification="Name")
reference = ifcopenshell.api.run( if self.file.schema == "IFC2X3":
"classification.add_reference", with pytest.raises(TypeError):
self.file, reference = ifcopenshell.api.run(
products=[element], "classification.add_reference",
identification="X", self.file,
name="Foobar", products=[element, element2, element3],
classification=result, 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(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 assert len(self.file.by_type("IfcClassificationReference")) == 0
def test_retaining_the_reference_if_still_in_use(self): def test_retaining_the_reference_if_still_in_use(self):
ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject") ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject")
element = self.file.createIfcMaterial() element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
element2 = self.file.createIfcMaterial() 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") result = ifcopenshell.api.run("classification.add_classification", self.file, classification="Name")
reference = ifcopenshell.api.run( reference = ifcopenshell.api.run(
"classification.add_reference", "classification.add_reference",
self.file, self.file,
products=[element], products=[element, element2, element3],
identification="X",
name="Foobar",
classification=result,
)
reference2 = ifcopenshell.api.run(
"classification.add_reference",
self.file,
products=[element2],
identification="X", identification="X",
name="Foobar", name="Foobar",
classification=result, classification=result,
) )
assert len(self.file.by_type("IfcClassificationReference")) == 1 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 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 assert len(self.file.by_type("IfcClassificationReference")) == 0
class TestRemoveReferenceIFC2X3(test.bootstrap.IFC2X3, TestRemoveReference):
pass
@@ -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].Identification == "X"
assert list(ifcopenshell.util.classification.get_references(element2))[0].Name == "Foobar" assert list(ifcopenshell.util.classification.get_references(element2))[0].Name == "Foobar"
assert list(ifcopenshell.util.classification.get_references(element2))[0] == references[0] 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