From c3107e37c00862622bdc4534a9384fa18a03c663 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 26 Mar 2025 13:54:49 +0500 Subject: [PATCH] material.remove_material_set to remove usages To prevent them from being invalidated. Noticed this as a possible issue investigating https://community.osarch.org/discussion/2820/error-message-in-bonsai-bim --- .../api/material/remove_material_set.py | 19 +++++++++++++++++-- .../api/material/unassign_material.py | 5 +++++ .../api/material/test_remove_material_set.py | 16 +++++++++++++++- 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/remove_material_set.py b/src/ifcopenshell-python/ifcopenshell/api/material/remove_material_set.py index 18477c6038..b11927e382 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/remove_material_set.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/remove_material_set.py @@ -17,6 +17,7 @@ # along with IfcOpenShell. If not, see . import ifcopenshell +import ifcopenshell.api.material import ifcopenshell.util.element @@ -24,7 +25,9 @@ def remove_material_set(file: ifcopenshell.file, material: ifcopenshell.entity_i """Removes a material set All set items, such as layers, profiles, or constituents will also be - removed. However, the materials and profile curves used by the layers, + removed. All set usages are also removed. + + However, the materials and profile curves used by the layers, profiles and constituents will not be removed. :param material: The IfcMaterialLayerSet, IfcMaterialConstituentSet, @@ -53,7 +56,15 @@ def remove_material_set(file: ifcopenshell.file, material: ifcopenshell.entity_i ifcopenshell.api.material.remove_material_set(model, material=material_set) """ - inverse_elements = file.get_inverse(material) + # Remove all usages for sets. + has_usages = material.is_a("IfcMaterialLayerSet") or material.is_a("IfcMaterialProfileSet") + if has_usages: + # Usage is invalid if it is not associated with some element, + # so we can remove usages through unassignment. + elements = ifcopenshell.util.element.get_elements_by_material(file, material) + if elements: + ifcopenshell.api.material.unassign_material(file, products=list(elements)) + if material.is_a("IfcMaterialLayerSet"): set_items = material.MaterialLayers or [] elif material.is_a("IfcMaterialProfileSet"): @@ -66,9 +77,13 @@ def remove_material_set(file: ifcopenshell.file, material: ifcopenshell.entity_i raise ValueError(f"Unknown material set type: {material.is_a()}") for set_item in set_items: file.remove(set_item) + + inverse_elements = file.get_inverse(material) file.remove(material) + for inverse in inverse_elements: if inverse.is_a("IfcRelAssociatesMaterial"): + # NOTE: for has_usages already handled by unassign_material. history = inverse.OwnerHistory file.remove(inverse) if history: diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/unassign_material.py b/src/ifcopenshell-python/ifcopenshell/api/material/unassign_material.py index 2ed3fd9e14..455fc02bc4 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/unassign_material.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/unassign_material.py @@ -31,6 +31,9 @@ def unassign_material(file: ifcopenshell.file, products: list[ifcopenshell.entit If the product does not have a material, nothing happens. + Unassigning a LayerSet or ProfileSet from the product type will also + remove all Usages of the set. + :param products: The list IfcProducts that may or may not have a material :return: None @@ -76,6 +79,8 @@ class Usecase: continue if material.is_a() in ["IfcMaterialLayerSet", "IfcMaterialProfileSet"]: # Remove set usages + # TODO: be more considerate and remove only usages + # associated with the set + product type, not all usages? for inverse in self.file.get_inverse(material): if self.file.schema == "IFC2X3": if not inverse.is_a("IfcMaterialLayerSetUsage"): diff --git a/src/ifcopenshell-python/test/api/material/test_remove_material_set.py b/src/ifcopenshell-python/test/api/material/test_remove_material_set.py index b7f0b47dd5..3c6c70b44a 100644 --- a/src/ifcopenshell-python/test/api/material/test_remove_material_set.py +++ b/src/ifcopenshell-python/test/api/material/test_remove_material_set.py @@ -17,9 +17,10 @@ # along with IfcOpenShell. If not, see . import test.bootstrap +import ifcopenshell.api.material import ifcopenshell.api.pset import ifcopenshell.api.root -import ifcopenshell.api.material +import ifcopenshell.api.type class TestRemoveMaterialSetIFC2X3(test.bootstrap.IFC2X3): @@ -45,6 +46,19 @@ class TestRemoveMaterialSetIFC2X3(test.bootstrap.IFC2X3): assert len(self.file.by_type("IfcMaterialLayer")) == 0 assert len(self.file.by_type("IfcMaterial")) == 1 + def test_removing_a_material_set_with_usages(self): + material = ifcopenshell.api.material.add_material_set(self.file, set_type="IfcMaterialLayerSet") + element_type = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWallType") + element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") + ifcopenshell.api.type.assign_type(self.file, related_objects=[element], relating_type=element_type) + ifcopenshell.api.material.assign_material(self.file, products=[element_type], material=material) + ifcopenshell.api.material.assign_material( + self.file, products=[element], material=material, type="IfcMaterialLayerSetUsage" + ) + ifcopenshell.api.material.remove_material_set(self.file, material=material) + assert len(self.file.by_type("IfcMaterialLayerSet")) == 0 + assert len(self.file.by_type("IfcMaterialLayerSetUsage")) == 0 + class TestRemoveMaterialSetIFC4(test.bootstrap.IFC4, TestRemoveMaterialSetIFC2X3): # IFC2X3 doesn't support adding a pset to IfcMaterialLayerSet