diff --git a/src/blenderbim/blenderbim/bim/module/model/product.py b/src/blenderbim/blenderbim/bim/module/model/product.py index 486d03f68f..5bb6fa9796 100644 --- a/src/blenderbim/blenderbim/bim/module/model/product.py +++ b/src/blenderbim/blenderbim/bim/module/model/product.py @@ -18,6 +18,7 @@ import bpy import mathutils +import numpy as np from functools import reduce import ifcopenshell import ifcopenshell.api @@ -39,7 +40,7 @@ from mathutils import Vector, Matrix from bpy_extras.object_utils import AddObjectHelper from . import prop import json -from typing import Any, Union +from typing import Any, Union, Optional class EnableAddType(bpy.types.Operator, tool.Ifc.Operator): @@ -513,88 +514,19 @@ def regenerate_profile_usage(usecase_path, ifc_file, settings): def ensure_material_assigned(usecase_path: str, ifc_file: ifcopenshell.file, settings: dict[str, Any]) -> None: - if usecase_path == "material.assign_material": - if not settings.get("material", None): - return - elements = settings["products"] - else: - elements = [] - for rel in ifc_file.by_type("IfcRelAssociatesMaterial"): - if rel.RelatingMaterial == settings["material"] or [ - e for e in ifc_file.traverse(rel.RelatingMaterial) if e == settings["material"] - ]: - elements.extend(rel.RelatedObjects) + elements = settings["products"] - update_blender_ifc_materials(elements) + for element in elements[:]: + if element.is_a("IfcElementType"): + elements.extend(tool.Model.get_occurrences_without_material_override(element)) + + tool.Model.apply_ifc_material_changes(elements, assigned_material=settings["material"]) def ensure_material_unassigned(usecase_path: str, ifc_file: ifcopenshell.file, settings: dict[str, Any]) -> None: elements = settings["products"] - if elements[0].is_a("IfcElementType"): - elements.extend(ifcopenshell.util.element.get_types(elements[0])) - update_blender_ifc_materials(elements) - -def update_blender_ifc_materials(elements: list[ifcopenshell.entity_instance]) -> None: - """update mesh blender materials that have ifc material connected to them - by replacing them with `blender_material`""" - # since different elements can share meshes (e.g. occurrecnes without openings) - # we need to make sure not to affect them accidentally - meshes_users: dict[bpy.types.Mesh, set[bpy.types.Object]] = dict() - for obj in bpy.data.objects: - if not obj.data: - continue - meshes_users.setdefault(obj.data, set()).add(obj) - - objects: set[bpy.types.Object] = set() - for element in elements: - obj: bpy.types.Object = tool.Ifc.get_object(element) - if not obj or not obj.data: - continue - objects.add(obj) - - meshes: set[bpy.types.Mesh] = {obj.data for obj in objects} - - for mesh in meshes: - mesh_users = meshes_users[mesh] - if not mesh_users.issubset(objects): - continue - - # NOTE: we need `obj` as removing materials and appending them to `mesh.materials` - # will mess up mesh faces material indices - - # NOTE: we make an assumption here that all mesh users - # have the same material - they either inherit it from the type - # or type doesn't have a material. - # - # If we add option to UI to add materials overriding type materials - # then this assumption won't be safe anymore - - obj = next(iter(mesh_users)) - element = tool.Ifc.get_entity(obj) - current_material = ifcopenshell.util.element.get_material(element) - if current_material: - current_material = tool.Ifc.get_object(current_material) - - material_replaced = False - - for material_slot in obj.material_slots: - material = material_slot.material - if material is None: - continue - ifc_material = tool.Ifc.get_entity(material) - # it's blender material for style, so ignore it - if not ifc_material: - continue - if ifc_material == current_material: - continue - material_slot.material = current_material - material_replaced = True - - if not material_replaced and current_material: - mesh.materials.append(current_material) - - # clear empty slots - for i, material in reversed(list(enumerate(mesh.materials[:]))): - if material is None: - mesh.materials.pop(index=i) + for element in elements[:]: + if element.is_a("IfcElementType"): + elements.extend(tool.Model.get_occurrences_without_material_override(element)) + tool.Model.apply_ifc_material_changes(elements) diff --git a/src/blenderbim/blenderbim/core/geometry.py b/src/blenderbim/blenderbim/core/geometry.py index 70898b7b49..f4af512a26 100644 --- a/src/blenderbim/blenderbim/core/geometry.py +++ b/src/blenderbim/blenderbim/core/geometry.py @@ -128,8 +128,9 @@ def switch_representation( if not current_obj_data and geometry.is_text_literal(representation): return - has_openings = apply_openings and getattr(entity, "HasOpenings", None) - if has_openings: + use_immediate_repr = apply_openings and getattr(entity, "HasOpenings", None) + use_immediate_repr = use_immediate_repr or geometry.has_material_style_override(entity) + if use_immediate_repr: # if it has openings make sure to switch to element's mapped representation representation = geometry.unresolve_type_representation(representation, entity) else: @@ -145,7 +146,7 @@ def switch_representation( else: new_repr_data = old_repr_data - geometry.change_object_data(obj, new_repr_data, is_global=is_global and not has_openings) + geometry.change_object_data(obj, new_repr_data, is_global=is_global and not use_immediate_repr) geometry.record_object_materials(obj) # we assume that all the occurences and the type have the same representation context active diff --git a/src/blenderbim/blenderbim/tool/geometry.py b/src/blenderbim/blenderbim/tool/geometry.py index 60e5076790..7210b08f21 100644 --- a/src/blenderbim/blenderbim/tool/geometry.py +++ b/src/blenderbim/blenderbim/tool/geometry.py @@ -508,6 +508,18 @@ class Geometry(blenderbim.core.tool.Geometry): return bool(obj.data.splines) return False + @classmethod + def has_material_style_override(cls, element: ifcopenshell.entity_instance) -> bool: + if element.is_a("IfcTypeProduct"): + return False + own_material = ifcopenshell.util.element.get_material(element, should_inherit=False) + if own_material: + inherited_style = cls.get_inherited_material_style(element) + style = tool.Material.get_style(own_material) if own_material else None + if inherited_style != style: + return True + return False + @classmethod def import_representation(cls, obj, representation, apply_openings=True): logger = logging.getLogger("ImportIFC") @@ -802,17 +814,29 @@ class Geometry(blenderbim.core.tool.Geometry): meshes_to_objects = {(obj := tool.Ifc.get_object(element)).data: obj for element in elements} for obj in meshes_to_objects.values(): - representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id) - blenderbim.core.geometry.switch_representation( - tool.Ifc, - tool.Geometry, - obj=obj, - representation=representation, - should_reload=True, - is_global=True, - should_sync_changes_first=False, - apply_openings=True, - ) + cls._reload_representation(obj) + + @classmethod + def _reload_representation(cls, obj: bpy.types.Object) -> None: + """Reload representation only for this object. + + Be careful as this method won't reload representation for related objects + that use the same representation but have different meshes + (e.g. because of the openings). + In the most cases just use reload_representation + as it will handle those complications by itself. + """ + representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id) + blenderbim.core.geometry.switch_representation( + tool.Ifc, + tool.Geometry, + obj=obj, + representation=representation, + should_reload=True, + is_global=True, + should_sync_changes_first=False, + apply_openings=True, + ) @classmethod def remove_representation_item(cls, representation_item): @@ -1010,3 +1034,64 @@ class Geometry(blenderbim.core.tool.Geometry): if (result := obj.BIMObjectProperties.blender_offset_type) == "NONE": result = obj.BIMObjectProperties.blender_offset_type = "OBJECT_PLACEMENT" return result + + @classmethod + def has_geometry_without_styles(cls, mesh: bpy.types.Mesh) -> bool: + """Check if mesh has geometry without styles. + + Detects geometry without styles based on how + MaterialCreator works - will check if either + mesh has no material slots or has an empty material slot. + """ + return not mesh.materials or any(m is None for m in mesh.materials) + + @classmethod + def get_representation_styles( + cls, representation: ifcopenshell.entity_instance + ) -> set[ifcopenshell.entity_instance]: + """Return a set of styles assigned to the representation directly.""" + + styles = set() + + # Get all stylable representation items. + items = [] + for item in representation.Items: + if item.is_a("IfcMappedItem"): + items.extend(item.MappingSource.MappedRepresentation.Items) + if item.is_a("IfcBooleanResult"): + operand = item.FirstOperand + while True: + items.append(operand) + if operand.is_a("IfcBooleanResult"): + operand = operand.FirstOperand + else: + break + items.append(item) + + for item in items: + if not item.StyledByItem: + continue + current_styles = list(item.StyledByItem[0].Styles) + while current_styles: + style = current_styles.pop() + if style.is_a("IfcPresentationStyle"): + styles.add(style) + elif style.is_a("IfcPresentationStyleAssignment"): + current_styles.extend(style.Styles) + + return styles + + @classmethod + def get_inherited_material_style( + cls, element: ifcopenshell.entity_instance + ) -> Union[ifcopenshell.entity_instance, None]: + if element.is_a("IfcTypeProduct"): + return + element_type = ifcopenshell.util.element.get_type(element) + if not element_type: + return + materials = ifcopenshell.util.element.get_materials(element_type) + if not materials: + return + material_style = tool.Material.get_style(materials[0]) + return material_style diff --git a/src/blenderbim/blenderbim/tool/model.py b/src/blenderbim/blenderbim/tool/model.py index 9f2490bec9..3ef1321909 100644 --- a/src/blenderbim/blenderbim/tool/model.py +++ b/src/blenderbim/blenderbim/tool/model.py @@ -1255,3 +1255,144 @@ class Model(blenderbim.core.tool.Model): if fillings: with bpy.context.temp_override(selected_objects=list(fillings.values())): bpy.ops.bim.recalculate_fill() + + @classmethod + def apply_ifc_material_changes( + cls, + elements: list[ifcopenshell.entity_instance], + assigned_material: Optional[ifcopenshell.entity_instance] = None, + ) -> None: + """Update mesh blender materials for provided elements after material assignment/unassignment. + + `assigned_material` argument is there just to indicate whether we apply material changes + after material assignment or material unassignment. + """ + + style_object = None + if assigned_material: + # NOTE: currently only IfcMaterials are supported + # for anyone else we just switch representation. + if not assigned_material.is_a("IfcMaterial"): + tool.Geometry.reload_representation([tool.Ifc.get_object(e) for e in elements]) + return + + # Since different elements can share meshes (e.g. occurrences without openings) + # we need to make sure not to process them multiple times. + meshes_users: dict[bpy.types.Mesh, set[bpy.types.Object]] = dict() + for obj in bpy.data.objects: + if not obj.data: + continue + meshes_users.setdefault(obj.data, set()).add(obj) + + objects: set[bpy.types.Object] = set() + for element in elements: + obj: bpy.types.Object = tool.Ifc.get_object(element) + if not obj or not obj.data: + continue + objects.add(obj) + + meshes: set[bpy.types.Mesh] = {obj.data for obj in objects} + + for mesh in meshes: + mesh_users = meshes_users[mesh] + + if not mesh_users.issubset(objects): + # It's unsafe to make changes to the mesh + # as it's used by objects unrelated to the current change. + # E.g. material with a style was assigned to a particular occurrence + # and this change shouldn't be applied to other occurrences and type. + objs_to_reload = mesh_users.intersection(objects) + for obj in objs_to_reload: + tool.Geometry._reload_representation(obj) + continue + + obj = next(iter(mesh_users)) + element = tool.Ifc.get_entity(obj) + own_material = ifcopenshell.util.element.get_material(element, should_inherit=False) + inherited_mstyle = tool.Geometry.get_inherited_material_style(element) + + if assigned_material: + if own_material: + ms2 = tool.Material.get_style(own_material) + ms1 = inherited_mstyle + else: + ms1 = None + ms2 = inherited_mstyle + + cls.replace_material_style(mesh, obj, ms1, ms2) + + else: # Material unnassignment. + if not tool.Geometry.has_geometry_without_styles(mesh): + tool.Geometry._reload_representation(obj) + continue + + if not inherited_mstyle: + continue + + cls.replace_material_style(mesh, obj, None, inherited_mstyle) + + @classmethod + def get_occurrences_without_material_override( + cls, element_type: ifcopenshell.entity_instance + ) -> list[ifcopenshell.entity_instance]: + occurrences = [ + e + for e in ifcopenshell.util.element.get_types(element_type) + if not ifcopenshell.util.element.get_material(e, should_inherit=False) + ] + return occurrences + + @classmethod + def replace_material_style( + cls, + mesh: bpy.types.Mesh, + obj: bpy.types.Object, + mstyle1: Union[ifcopenshell.entity_instance, None], + mstyle2: Union[ifcopenshell.entity_instance, None], + ) -> None: + if mstyle1 == mstyle2: + return + + # Get Blender materials. + mbstyle1, mbstyle2 = None, None + if mstyle1: + mbstyle1 = tool.Ifc.get_object(mstyle1) + assert isinstance(mbstyle1, bpy.types.Material) + if mstyle2: + mbstyle2 = tool.Ifc.get_object(mstyle2) + assert isinstance(mbstyle2, bpy.types.Material) + + # Copy data to the list as mesh.materials doesn't allow to search for None. + materials: list[Union[bpy.types.Material, None]] = mesh.materials[:] + # Material style is overridden by representation item, nothing to change. + if mesh.materials and mbstyle1 not in materials: + return + + i1 = None + if mbstyle1 is None: + if not materials: + mesh.materials.append(None) + i1 = 0 + else: + i1 = materials.index(None) + else: + rep = tool.Geometry.get_active_representation(obj) + assert rep # Type checker. + rep_styles = tool.Geometry.get_representation_styles(rep) + if mbstyle1 in rep_styles: + tool.Geometry._reload_representation(obj) + return + else: + i1 = materials.index(mbstyle1) + + if mbstyle2 in materials: + i2 = materials.index(mbstyle2) + # Reassign faces. + buffer = np.empty(len(mesh.polygons), dtype=np.int32) + mesh.polygons.foreach_get("material_index", buffer) + buffer[buffer == i1] = i2 + mesh.polygons.foreach_set("material_index", buffer) + + mesh.materials.pop(index=i1) + else: + mesh.materials[i1] = mbstyle2 diff --git a/src/blenderbim/test/tool/test_model.py b/src/blenderbim/test/tool/test_model.py index eb0417390f..48fd4c459c 100644 --- a/src/blenderbim/test/tool/test_model.py +++ b/src/blenderbim/test/tool/test_model.py @@ -18,13 +18,18 @@ import bpy import ifcopenshell +import ifcopenshell.api.material +import ifcopenshell.api.style +import ifcopenshell.util.element +import ifcopenshell.util.representation +import ifcopenshell.util.unit import blenderbim.core.tool import blenderbim.tool as tool import numpy as np import json from test.bim.bootstrap import NewFile from blenderbim.tool.model import Model as subject -from ifcopenshell.util.shape_builder import V +from ifcopenshell.util.shape_builder import V, ShapeBuilder class TestImplementsTool(NewFile): @@ -438,3 +443,212 @@ class TestUsingArrays(NewFile): element = tool.Ifc.get_entity(obj) pset = ifcopenshell.util.element.get_pset(element, "BBIM_Array") assert pset is None, (obj, pset) + + +class TestApplyIfcMaterialChanges(NewFile): + def get_used_styles(self, obj: bpy.types.Object) -> set[ifcopenshell.entity_instance]: + ifc_file = tool.Ifc.get() + return {ifc_file.by_id(s.material.BIMMaterialProperties.ifc_style_id) for s in obj.material_slots if s.material} + + def get_mesh(self, obj: bpy.types.Object) -> bpy.types.Mesh: + mesh = obj.data + assert isinstance(mesh, bpy.types.Mesh) + return mesh + + def setup_test(self, and_elements: bool = True) -> None: + bpy.context.scene.BIMProjectProperties.template_file = "0" + bpy.context.scene.unit_settings.length_unit = "MILLIMETERS" + bpy.ops.bim.create_project() + ifc_file = tool.Ifc.get() + + # Setup materials and styles. + context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW") + assert context # Type checker. + + red_material = ifcopenshell.api.material.add_material(ifc_file, "Red Material") + bpy.ops.bim.load_styles(style_type="IfcSurfaceStyle") + bpy.ops.bim.enable_adding_presentation_style() + bpy.data.scenes["Scene"].BIMStylesProperties.style_name = "Red" + bpy.ops.bim.add_presentation_style() + red_style = next((i for i in ifc_file.by_type("IfcSurfaceStyle") if i.Name == "Red")) + ifcopenshell.api.style.assign_material_style(ifc_file, red_material, red_style, context) + + blue_material = ifcopenshell.api.material.add_material(ifc_file, "Blue Material") + bpy.ops.bim.enable_adding_presentation_style() + bpy.data.scenes["Scene"].BIMStylesProperties.style_name = "Blue" + bpy.ops.bim.add_presentation_style() + blue_style = next((i for i in ifc_file.by_type("IfcSurfaceStyle") if i.Name == "Blue")) + ifcopenshell.api.style.assign_material_style(ifc_file, blue_material, blue_style, context) + + bpy.ops.bim.enable_adding_presentation_style() + bpy.data.scenes["Scene"].BIMStylesProperties.style_name = "Green" + bpy.ops.bim.add_presentation_style() + + if and_elements: + self.setup_elements() + + def setup_elements(self) -> None: + ifc_file = tool.Ifc.get() + blue_material = next((i for i in ifc_file.by_type("IfcMaterial") if i.Name == "Blue Material")) + blue_style = tool.Material.get_style(blue_material) + + # Element type. + bpy.ops.mesh.primitive_cube_add(size=10, location=(0, 0, 4)) + element_type_obj = bpy.data.objects["Cube"] + bpy.ops.bim.assign_class(ifc_class="IfcActuatorType", predefined_type="ELECTRICACTUATOR", userdefined_type="") + element_type = tool.Ifc.get_entity(element_type_obj) + + # Setup occurrences. + relating_type_id = element_type.id() + bpy.ops.bim.add_constr_type_instance(relating_type_id=relating_type_id) + simple = bpy.context.active_object + simple.name = "Simple" + + # Occurrence with an opening. + bpy.ops.bim.add_constr_type_instance(relating_type_id=relating_type_id) + with_opening = bpy.context.active_object + with_opening.name = "With Opening" + bpy.ops.bim.add_potential_opening() + tool.Blender.set_objects_selection( + bpy.context, active_object=with_opening, selected_objects=[with_opening, bpy.data.objects["Opening"]] + ) + bpy.ops.bim.add_opening() + + # Occurrence with a material override. + bpy.ops.bim.add_constr_type_instance(relating_type_id=relating_type_id) + with_material = bpy.context.active_object + with_material.name = "With Material" + tool.Blender.set_objects_selection(bpy.context, active_object=with_material, selected_objects=[with_material]) + + ifcopenshell.api.material.assign_material( + ifc_file, products=[tool.Ifc.get_entity(with_material)], material=blue_material + ) + + assert self.get_used_styles(element_type_obj) == set() + for element in ifc_file.by_type("IfcActuator"): + obj = tool.Ifc.get_object(element) + expected = {blue_style} if obj.name == "With Material" else set() + assert self.get_used_styles(obj) == expected + + def test_element_type_and_occurrences(self): + self.setup_test() + ifc_file = tool.Ifc.get() + element_type = next(ifc_file.by_type("IfcActuatorType").__iter__()) + red_material = next((i for i in ifc_file.by_type("IfcMaterial") if i.Name == "Red Material")) + red_style = tool.Material.get_style(red_material) + blue_style = next((i for i in ifc_file.by_type("IfcSurfaceStyle") if i.Name == "Blue")) + + ifcopenshell.api.material.assign_material(ifc_file, material=red_material, products=[element_type]) + assert self.get_used_styles(tool.Ifc.get_object(element_type)) == {red_style} + for element in ifc_file.by_type("IfcActuator"): + obj = tool.Ifc.get_object(element) + expected = {blue_style} if obj.name == "With Material" else {red_style} + assert self.get_used_styles(obj) == expected + + ifcopenshell.api.material.unassign_material(ifc_file, products=[element_type]) + assert self.get_used_styles(tool.Ifc.get_object(element_type)) == set() + for element in ifc_file.by_type("IfcActuator"): + obj = tool.Ifc.get_object(element) + expected = {blue_style} if obj.name == "With Material" else set() + assert self.get_used_styles(obj) == expected + + def test_dont_override_exisiting_styles(self): + self.setup_test() + ifc_file = tool.Ifc.get() + element_type = next(ifc_file.by_type("IfcActuatorType").__iter__()) + red_material = next((i for i in ifc_file.by_type("IfcMaterial") if i.Name == "Red Material")) + green_style = next((i for i in ifc_file.by_type("IfcSurfaceStyle") if i.Name == "Green")) + + # Occurrence with a style. + element_type_obj = tool.Ifc.get_object(element_type) + tool.Blender.set_objects_selection( + bpy.context, active_object=element_type_obj, selected_objects=[element_type_obj] + ) + bpy.ops.bim.assign_style_to_selected(style_id=green_style.id()) + + ifcopenshell.api.material.assign_material(ifc_file, material=red_material, products=[element_type]) + assert self.get_used_styles(tool.Ifc.get_object(element_type)) == {green_style} + for element in ifc_file.by_type("IfcActuator"): + obj = tool.Ifc.get_object(element) + assert self.get_used_styles(obj) == {green_style} + + ifcopenshell.api.material.unassign_material(ifc_file, products=[element_type]) + assert self.get_used_styles(tool.Ifc.get_object(element_type)) == {green_style} + for element in ifc_file.by_type("IfcActuator"): + obj = tool.Ifc.get_object(element) + assert self.get_used_styles(obj) == {green_style} + + def test_assign_material_to_representation_that_has_2_items_and_1_item_has_a_style(self): + self.setup_test(and_elements=False) + ifc_file = tool.Ifc.get() + red_material = next((i for i in ifc_file.by_type("IfcMaterial") if i.Name == "Red Material")) + red_style = tool.Material.get_style(red_material) + green_style = next((i for i in ifc_file.by_type("IfcSurfaceStyle") if i.Name == "Green")) + + bpy.ops.mesh.primitive_cube_add(size=10, location=(0, 0, 4)) + obj = bpy.data.objects["Cube"] + bpy.ops.bim.assign_class(ifc_class="IfcActuator", predefined_type="ELECTRICACTUATOR", userdefined_type="") + element = tool.Ifc.get_entity(obj) + builder = ShapeBuilder(ifc_file) + + # Change representation that consists of 2 rep items: + # 1 with style and other without. + rep = tool.Geometry.get_active_representation(obj) + assert rep + cube = rep.Items[0] + cube2 = builder.deep_copy(cube) + rep.Items = [cube, cube2] + tool.Style.assign_style_to_representation_item(cube, green_style) + tool.Geometry._reload_representation(obj) + + def get_material_indices(mesh: bpy.types.Mesh) -> np.ndarray: + buffer = np.empty(len(mesh.polygons), dtype=np.int32) + mesh.polygons.foreach_get("material_index", buffer) + return buffer + + mesh = self.get_mesh(obj) + assert len(mesh.materials) == 2 + assert set(mesh.materials) == {bpy.data.materials["Green"], None} + + ifcopenshell.api.material.assign_material(ifc_file, products=[element], material=red_material) + assert self.get_used_styles(obj) == {green_style, red_style} + ifcopenshell.api.material.unassign_material(ifc_file, products=[element]) + mesh = self.get_mesh(obj) + assert len(mesh.materials) == 2 + assert set(mesh.materials) == {bpy.data.materials["Green"], None} + + # Test that if style is the same it would just reuse it. + tool.Style.assign_style_to_representation_item(cube, red_style) + tool.Geometry._reload_representation(obj) + mesh = self.get_mesh(obj) + assert len(mesh.materials) == 2 + assert set(mesh.materials) == {bpy.data.materials["Red"], None} + + ifcopenshell.api.material.assign_material(ifc_file, products=[element], material=red_material) + assert mesh.materials[:] == [bpy.data.materials["Red"]] + # All polygons are just reassigned to the existing material. + assert set(get_material_indices(mesh)) == {mesh.materials.find("Red")} + + ifcopenshell.api.material.unassign_material(ifc_file, products=[element]) + mesh = self.get_mesh(obj) + assert len(mesh.materials) == 2 + assert set(mesh.materials) == {bpy.data.materials["Red"], None} + assert set(get_material_indices(mesh)) == {0, 1} + + def test_assign_unassign_overriding_occurrence_material(self): + self.setup_test(and_elements=True) + ifc_file = tool.Ifc.get() + element_type = next(ifc_file.by_type("IfcActuatorType").__iter__()) + red_material = next((i for i in ifc_file.by_type("IfcMaterial") if i.Name == "Red Material")) + no_style_material = ifcopenshell.api.material.add_material(ifc_file, "No Style") + obj = bpy.data.objects["Simple"] + element = tool.Ifc.get_entity(obj) + + ifcopenshell.api.material.assign_material(ifc_file, material=red_material, products=[element_type]) + + # Override type material. + ifcopenshell.api.material.assign_material(ifc_file, material=no_style_material, products=[element]) + assert self.get_mesh(obj).materials[:] == [] + + ifcopenshell.api.material.unassign_material(ifc_file, products=[element]) + assert self.get_mesh(obj).materials[:] == [bpy.data.materials["Red"]]