fix styles update on material assignment/unassignment #4843

This commit is contained in:
Andrej730
2024-06-14 18:41:37 +05:00
parent a834e5b4cd
commit c4ae9578e9
5 changed files with 468 additions and 95 deletions
@@ -18,6 +18,7 @@
import bpy import bpy
import mathutils import mathutils
import numpy as np
from functools import reduce from functools import reduce
import ifcopenshell import ifcopenshell
import ifcopenshell.api import ifcopenshell.api
@@ -39,7 +40,7 @@ from mathutils import Vector, Matrix
from bpy_extras.object_utils import AddObjectHelper from bpy_extras.object_utils import AddObjectHelper
from . import prop from . import prop
import json import json
from typing import Any, Union from typing import Any, Union, Optional
class EnableAddType(bpy.types.Operator, tool.Ifc.Operator): 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: def ensure_material_assigned(usecase_path: str, ifc_file: ifcopenshell.file, settings: dict[str, Any]) -> None:
if usecase_path == "material.assign_material": elements = settings["products"]
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)
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: def ensure_material_unassigned(usecase_path: str, ifc_file: ifcopenshell.file, settings: dict[str, Any]) -> None:
elements = settings["products"] elements = settings["products"]
if elements[0].is_a("IfcElementType"):
elements.extend(ifcopenshell.util.element.get_types(elements[0]))
update_blender_ifc_materials(elements)
for element in elements[:]:
def update_blender_ifc_materials(elements: list[ifcopenshell.entity_instance]) -> None: if element.is_a("IfcElementType"):
"""update mesh blender materials that have ifc material connected to them elements.extend(tool.Model.get_occurrences_without_material_override(element))
by replacing them with `blender_material`""" tool.Model.apply_ifc_material_changes(elements)
# 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)
+4 -3
View File
@@ -128,8 +128,9 @@ def switch_representation(
if not current_obj_data and geometry.is_text_literal(representation): if not current_obj_data and geometry.is_text_literal(representation):
return return
has_openings = apply_openings and getattr(entity, "HasOpenings", None) use_immediate_repr = apply_openings and getattr(entity, "HasOpenings", None)
if has_openings: 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 # if it has openings make sure to switch to element's mapped representation
representation = geometry.unresolve_type_representation(representation, entity) representation = geometry.unresolve_type_representation(representation, entity)
else: else:
@@ -145,7 +146,7 @@ def switch_representation(
else: else:
new_repr_data = old_repr_data 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) geometry.record_object_materials(obj)
# we assume that all the occurences and the type have the same representation context active # we assume that all the occurences and the type have the same representation context active
+96 -11
View File
@@ -508,6 +508,18 @@ class Geometry(blenderbim.core.tool.Geometry):
return bool(obj.data.splines) return bool(obj.data.splines)
return False 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 @classmethod
def import_representation(cls, obj, representation, apply_openings=True): def import_representation(cls, obj, representation, apply_openings=True):
logger = logging.getLogger("ImportIFC") 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} meshes_to_objects = {(obj := tool.Ifc.get_object(element)).data: obj for element in elements}
for obj in meshes_to_objects.values(): for obj in meshes_to_objects.values():
representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id) cls._reload_representation(obj)
blenderbim.core.geometry.switch_representation(
tool.Ifc, @classmethod
tool.Geometry, def _reload_representation(cls, obj: bpy.types.Object) -> None:
obj=obj, """Reload representation only for this object.
representation=representation,
should_reload=True, Be careful as this method won't reload representation for related objects
is_global=True, that use the same representation but have different meshes
should_sync_changes_first=False, (e.g. because of the openings).
apply_openings=True, 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 @classmethod
def remove_representation_item(cls, representation_item): 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": if (result := obj.BIMObjectProperties.blender_offset_type) == "NONE":
result = obj.BIMObjectProperties.blender_offset_type = "OBJECT_PLACEMENT" result = obj.BIMObjectProperties.blender_offset_type = "OBJECT_PLACEMENT"
return result 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
+141
View File
@@ -1255,3 +1255,144 @@ class Model(blenderbim.core.tool.Model):
if fillings: if fillings:
with bpy.context.temp_override(selected_objects=list(fillings.values())): with bpy.context.temp_override(selected_objects=list(fillings.values())):
bpy.ops.bim.recalculate_fill() 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
+215 -1
View File
@@ -18,13 +18,18 @@
import bpy import bpy
import ifcopenshell 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.core.tool
import blenderbim.tool as tool import blenderbim.tool as tool
import numpy as np import numpy as np
import json import json
from test.bim.bootstrap import NewFile from test.bim.bootstrap import NewFile
from blenderbim.tool.model import Model as subject 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): class TestImplementsTool(NewFile):
@@ -438,3 +443,212 @@ class TestUsingArrays(NewFile):
element = tool.Ifc.get_entity(obj) element = tool.Ifc.get_entity(obj)
pset = ifcopenshell.util.element.get_pset(element, "BBIM_Array") pset = ifcopenshell.util.element.get_pset(element, "BBIM_Array")
assert pset is None, (obj, pset) 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"]]