diff --git a/src/ifcopenshell-python/ifcopenshell/api/style/assign_material_style.py b/src/ifcopenshell-python/ifcopenshell/api/style/assign_material_style.py index 614f3f30ba..25dc506cc4 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/style/assign_material_style.py +++ b/src/ifcopenshell-python/ifcopenshell/api/style/assign_material_style.py @@ -114,6 +114,7 @@ class Usecase: else: self.create_new_definition_representation() + # handle material constituents and shape aspects material_constituents_names = [] for inverse in self.file.get_inverse(self.settings["material"]): if inverse.is_a("IfcMaterialConstituent") and inverse.Name: @@ -136,25 +137,28 @@ class Usecase: ) def modify_existing_definition_representation(self): + # NOTE: while it's theoritically possible to have multiple styles per 1 material + # (either with multiple styled items or multiple styles in 1 item) + # we use an implicit convention that definition_representation = self.settings["material"].HasRepresentation[0] representation = self.get_styled_representation(definition_representation) if representation: - potential_orphans = [] items = list(representation.Items) new_items = [] - removed_items = [] + same_style_items = [] for item in items: if not item.is_a("IfcStyledItem"): continue if self.has_proposed_style(item): return if self.has_same_style_type(item): - removed_items.append(item) + same_style_items.append(item) else: new_items.append(item) - new_items.append(self.create_styled_item()) + item_to_reuse = same_style_items.pop(0) if same_style_items else None + new_items.append(self.create_styled_item(item_to_reuse)) representation.Items = new_items - for item in removed_items: + for item in same_style_items: if len(self.file.get_inverse(item)) == 0: self.file.remove(item) else: @@ -172,7 +176,7 @@ class Usecase: representation = self.create_styled_representation() definition_representation = self.file.create_entity( "IfcMaterialDefinitionRepresentation", - **{"Representations": [representation], "RepresentedMaterial": self.settings["material"]} + **{"Representations": [representation], "RepresentedMaterial": self.settings["material"]}, ) def get_styled_representation(self, definition_representation): @@ -191,8 +195,14 @@ class Usecase: "ContextOfItems": self.settings["context"], "RepresentationIdentifier": self.settings["context"].ContextIdentifier, "Items": [self.create_styled_item()], - } + }, ) - def create_styled_item(self): - return self.file.create_entity("IfcStyledItem", **{"Styles": [self.style], "Name": self.settings["style"].Name}) + def create_styled_item(self, reuse_item=None): + if reuse_item is None: + return self.file.create_entity( + "IfcStyledItem", **{"Styles": [self.style], "Name": self.settings["style"].Name} + ) + reuse_item.Styles = (self.style,) + reuse_item.Name = self.settings["style"].Name + return reuse_item diff --git a/src/ifcopenshell-python/ifcopenshell/api/style/unassign_material_style.py b/src/ifcopenshell-python/ifcopenshell/api/style/unassign_material_style.py index f88030ad9b..51270a863d 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/style/unassign_material_style.py +++ b/src/ifcopenshell-python/ifcopenshell/api/style/unassign_material_style.py @@ -49,7 +49,6 @@ class Usecase: } def execute(self): - to_delete = set() for definition in self.settings["material"].HasRepresentation: for representation in definition.Representations: if not representation.is_a("IfcStyledRepresentation"): diff --git a/src/ifcopenshell-python/test/api/style/test_assign_material_style.py b/src/ifcopenshell-python/test/api/style/test_assign_material_style.py index eab6dd73b4..7c25ba578f 100644 --- a/src/ifcopenshell-python/test/api/style/test_assign_material_style.py +++ b/src/ifcopenshell-python/test/api/style/test_assign_material_style.py @@ -22,9 +22,35 @@ import ifcopenshell import ifcopenshell.api -# TODO: test assigning styles without material constituents class TestAssignMaterialStyle(test.bootstrap.IFC4): - def test_update_shape_aspect_representaitons_items_styles_if_material_is_part_of_matching_material_constituents( + def test_run(self): + material = ifcopenshell.api.run("material.add_material", self.file) + context = self.file.createIfcGeometricRepresentationContext() + style = self.file.createIfcSurfaceStyle() + ifcopenshell.api.run("style.assign_material_style", self.file, material=material, style=style, context=context) + + assert len(material.HasRepresentation) == 1 + definition = material.HasRepresentation[0] + assert len(definition.Representations) == 1 + representation = definition.Representations[0] + assert representation.is_a("IfcStyledRepresentation") + assert representation.ContextOfItems == context + assert len(representation.Items) == 1 + item = representation.Items[0] + assert item.is_a("IfcStyledItem") + assert item.Styles == (style,) + + style2 = self.file.createIfcSurfaceStyle() + ifcopenshell.api.run("style.assign_material_style", self.file, material=material, style=style2, context=context) + + # reuse existing elements and reassign the style + assert material.HasRepresentation == (definition,) + assert definition.Representations == (representation,) + assert len(representation.Items) == 1 + assert representation.Items[0] == item + assert representation.Items[0].Styles == (style2,) + + def test_update_shape_aspect_representations_items_styles_if_material_is_part_of_matching_material_constituents( self, ): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") diff --git a/src/ifcopenshell-python/test/api/style/test_unassign_material_style.py b/src/ifcopenshell-python/test/api/style/test_unassign_material_style.py new file mode 100644 index 0000000000..e2d2cc1964 --- /dev/null +++ b/src/ifcopenshell-python/test/api/style/test_unassign_material_style.py @@ -0,0 +1,47 @@ +# 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 +import ifcopenshell.api + + +class TestAssignMaterialStyle(test.bootstrap.IFC4): + def test_run(self): + material = ifcopenshell.api.run("material.add_material", self.file) + context = self.file.createIfcGeometricRepresentationContext() + style = self.file.createIfcSurfaceStyle() + ifcopenshell.api.run("style.assign_material_style", self.file, material=material, style=style, context=context) + + # unassign 1 style + style2 = self.file.createIfcSurfaceStyle() + item = self.file.by_type("IfcStyledItem")[0] + item.Styles = item.Styles + (style2,) + ifcopenshell.api.run( + "style.unassign_material_style", self.file, material=material, style=style2, context=context + ) + assert item.Styles == (style,) + + # unassign last style + ifcopenshell.api.run( + "style.unassign_material_style", self.file, material=material, style=style, context=context + ) + assert len(self.file.by_type("IfcMaterialDefinitionRepresentation")) == 0 + assert len(self.file.by_type("IfcStyledRepresentation")) == 0 + assert len(self.file.by_type("IfcStyledItem")) == 0