mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 09:21:46 +00:00
fix styles update on material assignment/unassignment #4843
This commit is contained in:
@@ -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)
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"]]
|
||||||
|
|||||||
Reference in New Issue
Block a user