Files
IfcOpenShell/src/bonsai/bonsai/bim/module/material/operator.py
T
Ryan Schultz 6ba5f5af3d Fix CardinalPoint not applied to all selected objects
EditAssignedMaterial propagated layer set usage attributes
to all selected objects but skipped this loop for profile
set usage. Add the same loop so CardinalPoint and
ReferenceExtent are copied to each selected object's
IfcMaterialProfileSetUsage on save.

Generated with the assistance of an AI coding tool.
2026-05-28 21:21:16 -05:00

1024 lines
40 KiB
Python

# Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of Bonsai.
#
# Bonsai is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Bonsai 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import json
from typing import TYPE_CHECKING, Any, Literal, Union
import bpy
import ifcopenshell.api.material
import ifcopenshell.api.profile
import ifcopenshell.api.style
import ifcopenshell.util.element
import ifcopenshell.util.representation
import bonsai.bim.helper
import bonsai.bim.module.model.profile as model_profile
import bonsai.core.material as core
import bonsai.tool as tool
from bonsai.bim.module.model import slab, wall
if TYPE_CHECKING:
from bonsai.bim.prop import Attribute
class LoadMaterials(bpy.types.Operator):
bl_idname = "bim.load_materials"
bl_label = "Load Materials"
bl_description = "Display list of named materials"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
props = tool.Material.get_material_props()
core.load_materials(tool.Material, props.material_type)
return {"FINISHED"}
class DisableEditingMaterials(bpy.types.Operator):
bl_idname = "bim.disable_editing_materials"
bl_label = "Disable Editing Materials"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
core.disable_editing_materials(tool.Material)
return {"FINISHED"}
class SelectByMaterial(bpy.types.Operator):
bl_idname = "bim.select_by_material"
bl_label = "Select By Material"
bl_description = "Select objects using the provided material"
bl_options = {"REGISTER", "UNDO"}
material: bpy.props.IntProperty()
def execute(self, context):
material = tool.Ifc.get().by_id(self.material)
core.select_by_material(tool.Material, tool.Spatial, material=material)
# copy selection query to clipboard
if material.is_a("IfcMaterialLayerSet"):
material_name = material.LayerSetName
else:
material_name = material.Name
result = f'material="{material_name}"'
bpy.context.window_manager.clipboard = result
self.report({"INFO"}, f"({result}) was copied to the clipboard.")
return {"FINISHED"}
class EnableEditingMaterial(bpy.types.Operator):
bl_idname = "bim.enable_editing_material"
bl_label = "Enable Editing Material"
bl_options = {"REGISTER", "UNDO"}
material: bpy.props.IntProperty()
def execute(self, context):
core.enable_editing_material(tool.Material, material=tool.Ifc.get().by_id(self.material))
return {"FINISHED"}
class EditMaterial(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.edit_material"
bl_label = "Edit Material"
bl_options = {"REGISTER", "UNDO"}
material: bpy.props.IntProperty()
def _execute(self, context):
core.edit_material(tool.Ifc, tool.Material, material=tool.Ifc.get().by_id(self.material))
class DisableEditingMaterial(bpy.types.Operator):
bl_idname = "bim.disable_editing_material"
bl_label = "Disable Editing Material"
bl_options = {"REGISTER", "UNDO"}
material: bpy.props.IntProperty()
def execute(self, context):
core.disable_editing_material(tool.Material)
return {"FINISHED"}
class AssignParameterizedProfile(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.assign_parameterized_profile"
bl_label = "Assign Parameterized Profile"
bl_options = {"REGISTER", "UNDO"}
ifc_class: bpy.props.StringProperty()
material_profile: bpy.props.IntProperty()
obj: bpy.props.StringProperty()
def _execute(self, context):
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
self.file = tool.Ifc.get()
profile = ifcopenshell.api.profile.add_parameterized_profile(
self.file,
ifc_class=self.ifc_class,
)
ifcopenshell.api.material.assign_profile(
self.file,
material_profile=self.file.by_id(self.material_profile),
profile=profile,
)
bpy.ops.bim.enable_editing_material_set_item(obj=obj.name, material_set_item=self.material_profile)
class AddMaterial(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_material"
bl_label = "Add Material"
bl_description = "Create a new IfcMaterial"
bl_options = {"REGISTER", "UNDO"}
name: bpy.props.StringProperty(default="Default")
category: bpy.props.StringProperty(default="")
description: bpy.props.StringProperty(default="")
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
def draw(self, context):
row = self.layout
row.prop(self, "name", text="Name")
row = self.layout
row.prop(self, "description", text="Description")
row = self.layout
row.prop(self, "category", text="Category")
def _execute(self, context):
core.add_material(tool.Ifc, tool.Material, name=self.name, category=self.category, description=self.description)
class DuplicateMaterial(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.duplicate_material"
bl_label = "Duplicate Material"
bl_options = {"REGISTER", "UNDO"}
material: bpy.props.IntProperty(name="Material ID")
def _execute(self, context):
material = tool.Ifc.get().by_id(self.material)
tool.Material.duplicate_material(material)
bpy.ops.bim.load_materials()
class AddMaterialSet(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_material_set"
bl_label = "Add Material Set"
bl_description = "Create a new material set, will use a default material/profile as a placeholder"
bl_options = {"REGISTER", "UNDO"}
set_type: bpy.props.StringProperty()
def _execute(self, context):
core.add_material_set(tool.Ifc, tool.Material, set_type=self.set_type)
class RemoveMaterial(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_material"
bl_label = "Remove Material"
bl_options = {"REGISTER", "UNDO"}
material: bpy.props.IntProperty()
def _execute(self, context):
res = core.remove_material(tool.Ifc, tool.Material, material=tool.Ifc.get().by_id(self.material))
if not res:
self.report({"ERROR"}, "Material is used in material sets and cannot be removed.")
return {"CANCELLED"}
class RemoveMaterialSet(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_material_set"
bl_label = "Remove Material Set"
bl_options = {"REGISTER", "UNDO"}
material: bpy.props.IntProperty()
def _execute(self, context):
core.remove_material_set(tool.Ifc, tool.Material, material=tool.Ifc.get().by_id(self.material))
class AssignMaterialToSelected(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.assign_material_to_selected"
bl_label = "Assign Material To Selected"
bl_description = (
"Assign currently selected material in Materials UI to the selected objects.\n"
"Occurrences automatically get usages for layer/profile sets.\n\n"
"ALT+CLICK to assign without a usage."
)
bl_options = {"REGISTER", "UNDO"}
material: bpy.props.IntProperty(name="Material IFC ID")
should_auto_assign_usage: bpy.props.BoolProperty(
name="Auto Assign Usage",
default=True,
options={"SKIP_SAVE"},
)
@classmethod
def poll(cls, context):
if not context.selected_objects:
cls.poll_message_set("No objects selected")
return False
return True
def invoke(self, context, event):
if event.type == "LEFTMOUSE" and event.alt:
self.should_auto_assign_usage = False
return self.execute(context)
def _execute(self, context):
material = tool.Ifc.get().by_id(self.material)
objects = tool.Blender.get_selected_objects()
core.assign_material(
tool.Ifc,
tool.Material,
material_type=material.is_a(),
objects=objects,
material=material,
should_auto_assign_usage=self.should_auto_assign_usage,
)
class AssignMaterial(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.assign_material"
bl_label = "Assign Material"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
material_type: bpy.props.StringProperty()
@classmethod
def description(cls, context, properties):
if not (material_type := properties.material_type):
if not (obj := context.active_object):
return ""
omprops = tool.Material.get_object_material_props(obj)
material_type = omprops.material_type
description = "Assign current IfcMaterial to the selected objects"
if material_type != "IfcMaterial":
description += f" as part of a new {material_type}"
return description
def _execute(self, context):
objects = [bpy.data.objects.get(self.obj)] if self.obj else tool.Blender.get_selected_objects()
core.assign_material(tool.Ifc, tool.Material, material_type=self.material_type, objects=objects)
class UnassignMaterial(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.unassign_material"
bl_label = "Unassign Material"
bl_description = (
"Unassign material from the selected objects.\n\n"
"If object inherits material from a type, material will be unassigned from the type.\n"
"If object has a material usage, related material set will be unassigned from the type."
)
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
if TYPE_CHECKING:
obj: str
def _execute(self, context):
objects = [bpy.data.objects[self.obj]] if self.obj else tool.Blender.get_selected_objects()
core.unassign_material(tool.Ifc, tool.Material, objects=list(objects))
class AddConstituent(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_constituent"
bl_label = "Add Constituent"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
constituent_set: bpy.props.IntProperty()
def _execute(self, context):
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
omprops = tool.Material.get_object_material_props(obj)
self.file = tool.Ifc.get()
ifcopenshell.api.material.add_constituent(
self.file,
constituent_set=self.file.by_id(self.constituent_set),
material=self.file.by_id(int(omprops.material)),
)
class RemoveConstituent(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_constituent"
bl_label = "Remove Constituent"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
constituent: bpy.props.IntProperty()
def _execute(self, context):
constituent = tool.Ifc.get().by_id(self.constituent)
for material_set in constituent.ToMaterialConstituentSet:
if len(material_set.MaterialConstituents) == 1:
self.report({"ERROR"}, "At least one constituent must exist")
return {"CANCELLED"}
ifcopenshell.api.material.remove_constituent(tool.Ifc.get(), constituent=constituent)
class AddProfile(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_profile"
bl_label = "Add Profile"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
profile_set: bpy.props.IntProperty()
def _execute(self, context):
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
assert obj
self.file = tool.Ifc.get()
props = tool.Material.get_material_props()
omprops = tool.Material.get_object_material_props(obj)
ifcopenshell.api.material.add_profile(
self.file,
profile_set=self.file.by_id(self.profile_set),
material=self.file.by_id(int(omprops.material)),
profile=self.file.by_id(int(props.profiles)),
)
class RemoveProfile(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_profile"
bl_label = "Remove Profile"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
profile: bpy.props.IntProperty()
def _execute(self, context):
profile = tool.Ifc.get().by_id(self.profile)
for material_set in profile.ToMaterialProfileSet:
if len(material_set.MaterialProfiles) == 1:
self.report({"ERROR"}, "At least one profile must exist")
return {"CANCELLED"}
ifcopenshell.api.material.remove_profile(tool.Ifc.get(), profile=profile)
class AddLayer(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_layer"
bl_label = "Add Layer"
bl_description = "The List is Ordered From Origin Point"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
layer_set: bpy.props.IntProperty()
def _execute(self, context):
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
assert obj
omprops = tool.Material.get_object_material_props(obj)
layer_set = tool.Ifc.get().by_id(self.layer_set)
ifcopenshell.api.material.add_layer(
tool.Ifc.get(),
layer_set=layer_set,
material=tool.Ifc.get().by_id(int(omprops.material)),
)
slab.DumbSlabPlaner().regenerate_from_layer_set(layer_set)
wall.DumbWallPlaner().regenerate_from_layer_set(layer_set)
class ReorderMaterialSetItem(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.reorder_material_set_item"
bl_label = "Reorder Material Set Item"
bl_description = "Change the order of materials"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
old_index: bpy.props.IntProperty()
new_index: bpy.props.IntProperty()
material_set: bpy.props.IntProperty()
def _execute(self, context):
self.file = tool.Ifc.get()
ifcopenshell.api.material.reorder_set_item(
self.file,
material_set=self.file.by_id(self.material_set),
old_index=self.old_index,
new_index=self.new_index,
)
class RemoveLayer(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_layer"
bl_label = "Remove Layer"
bl_options = {"REGISTER", "UNDO"}
layer: bpy.props.IntProperty()
def _execute(self, context):
layer = tool.Ifc.get().by_id(self.layer)
material_sets = set(layer.ToMaterialLayerSet)
for material_set in layer.ToMaterialLayerSet:
if len(material_set.MaterialLayers) == 1:
self.report({"ERROR"}, "At least one layer must exist")
return {"CANCELLED"}
ifcopenshell.api.material.remove_layer(tool.Ifc.get(), layer=layer)
for material_set in material_sets:
slab.DumbSlabPlaner().regenerate_from_layer_set(material_set)
wall.DumbWallPlaner().regenerate_from_layer_set(material_set)
class DuplicateLayer(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.duplicate_layer"
bl_label = "Duplicate Layer"
bl_options = {"REGISTER", "UNDO"}
layer: bpy.props.IntProperty()
def _execute(self, context):
layer = tool.Ifc.get().by_id(self.layer)
new_layer = ifcopenshell.util.element.copy(tool.Ifc.get(), layer)
for material_set in layer.ToMaterialLayerSet:
layers = list(material_set.MaterialLayers)
layers.insert(layers.index(layer), new_layer)
material_set.MaterialLayers = layers
class AddListItem(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_list_item"
bl_label = "Add List Item"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
list_item_set: bpy.props.IntProperty()
def _execute(self, context):
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
assert obj
omprops = tool.Material.get_object_material_props(obj)
self.file = tool.Ifc.get()
ifcopenshell.api.material.add_list_item(
self.file,
material_list=self.file.by_id(self.list_item_set),
material=self.file.by_id(int(omprops.material)),
)
class RemoveListItem(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_list_item"
bl_label = "Remove List Item"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
list_item_set: bpy.props.IntProperty()
list_item: bpy.props.IntProperty()
list_item_index: bpy.props.IntProperty()
def _execute(self, context):
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
self.file = tool.Ifc.get()
ifcopenshell.api.material.remove_list_item(
self.file,
material_list=self.file.by_id(self.list_item_set),
material_index=self.list_item_index,
)
class EnableEditingAssignedMaterial(bpy.types.Operator):
bl_idname = "bim.enable_editing_assigned_material"
bl_label = "Enable Editing Assigned Material"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
def execute(self, context):
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
assert obj
props = tool.Material.get_object_material_props(obj)
props.is_editing = True
element = tool.Ifc.get_entity(obj)
material = ifcopenshell.util.element.get_material(element)
assert material
if material.is_a("IfcMaterial"):
props.material = str(material.id())
return {"FINISHED"}
props.material_set_usage_attributes.clear()
props.material_set_attributes.clear()
if "Usage" in material.is_a():
bonsai.bim.helper.import_attributes(
material, props.material_set_usage_attributes, callback=self.import_attributes_callback
)
bonsai.bim.helper.import_attributes(material[0], props.material_set_attributes)
else:
bonsai.bim.helper.import_attributes(material, props.material_set_attributes)
# Load custom offset from BBIM_MaterialLayer pset
tool.Model.load_custom_offset_from_pset(element, obj)
return {"FINISHED"}
def import_attributes_callback(
self, name: str, prop: Union["Attribute", None], data: dict[str, Any]
) -> None | Literal[True]:
if name == "CardinalPoint":
# TODO: complain to buildingSMART
cardinal_point_map = {
1: "bottom left",
2: "bottom centre",
3: "bottom right",
4: "mid-depth left",
5: "mid-depth centre",
6: "mid-depth right",
7: "top left",
8: "top centre",
9: "top right",
10: "geometric centroid",
11: "bottom in line with the geometric centroid",
12: "left in line with the geometric centroid",
13: "right in line with the geometric centroid",
14: "top in line with the geometric centroid",
15: "shear centre",
16: "bottom in line with the shear centre",
17: "left in line with the shear centre",
18: "right in line with the shear centre",
19: "top in line with the shear centre",
}
assert prop
prop.data_type = "enum"
prop.enum_items = json.dumps(cardinal_point_map)
if data[name]:
prop.enum_value = str(data[name])
return True
class DisableEditingAssignedMaterial(bpy.types.Operator):
bl_idname = "bim.disable_editing_assigned_material"
bl_label = "Disable Editing Assigned Material"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
def execute(self, context):
bpy.ops.bim.disable_editing_material_set_item()
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
assert obj
props = tool.Material.get_object_material_props(obj)
props.is_editing = False
return {"FINISHED"}
class EditAssignedMaterial(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.edit_assigned_material"
bl_label = "Edit Assigned Material"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
material_set: bpy.props.IntProperty()
material_set_usage: bpy.props.IntProperty()
def _execute(self, context):
self.file = tool.Ifc.get()
active_obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
assert active_obj
props = tool.Material.get_object_material_props(active_obj)
element = tool.Ifc.get_entity(active_obj)
assert element
material = ifcopenshell.util.element.get_material(element)
assert material
objects = tool.Blender.get_selected_objects()
if props.active_material_set_item_id != 0: # We were editing a material layer set item
bpy.ops.bim.edit_material_set_item(material_set_item=props.active_material_set_item_id)
if material.is_a("IfcMaterial"):
for obj in objects:
bpy.ops.bim.unassign_material(obj=obj.name)
bpy.ops.bim.assign_material(obj=obj.name, material_type="IfcMaterial")
bpy.ops.bim.disable_editing_assigned_material(obj=active_obj.name)
return {"FINISHED"}
material_set = self.file.by_id(self.material_set)
attributes = bonsai.bim.helper.export_attributes(props.material_set_attributes)
ifcopenshell.api.material.edit_assigned_material(
self.file,
element=material_set,
attributes=attributes,
)
if self.material_set_usage:
material_set_usage = self.file.by_id(self.material_set_usage)
attributes = bonsai.bim.helper.export_attributes(props.material_set_usage_attributes)
if material_set_usage.is_a("IfcMaterialLayerSetUsage"):
ifcopenshell.api.material.edit_layer_usage(
self.file,
usage=material_set_usage,
attributes=attributes,
)
layer_sets_to_regenerate = set()
for obj in objects:
obj_element = tool.Ifc.get_entity(obj)
obj_material_usage = ifcopenshell.util.element.get_material(obj_element)
if obj_material_usage and obj_material_usage.is_a("IfcMaterialLayerSetUsage"):
obj_material_usage.OffsetFromReferenceLine = material.OffsetFromReferenceLine
obj_material_usage.DirectionSense = material.DirectionSense
obj_material_usage.ReferenceExtent = material.ReferenceExtent
layer_sets_to_regenerate.add(obj_material_usage.ForLayerSet)
# Save custom offset to BBIM_MaterialLayer pset
tool.Model.save_custom_offset_to_pset(obj_element, obj)
for layer_set in layer_sets_to_regenerate:
wall.DumbWallPlaner().regenerate_from_layer_set(layer_set)
slab.DumbSlabPlaner().regenerate_from_layer_set(layer_set)
if material_set_usage.is_a("IfcMaterialProfileSetUsage"):
if "CardinalPoint" in attributes and attributes["CardinalPoint"] is not None:
attributes["CardinalPoint"] = int(attributes["CardinalPoint"])
ifcopenshell.api.material.edit_profile_usage(
self.file,
usage=material_set_usage,
attributes=attributes,
)
for obj in objects:
obj_element = tool.Ifc.get_entity(obj)
if not obj_element:
continue
obj_material_usage = ifcopenshell.util.element.get_material(obj_element)
if obj_material_usage and obj_material_usage.is_a("IfcMaterialProfileSetUsage"):
obj_material_usage.CardinalPoint = material_set_usage.CardinalPoint
obj_material_usage.ReferenceExtent = material_set_usage.ReferenceExtent
model_profile.DumbProfileRecalculator().recalculate(objects)
bpy.ops.bim.disable_editing_assigned_material(obj=active_obj.name)
class EnableEditingMaterialSetItemProfile(bpy.types.Operator):
bl_idname = "bim.enable_editing_material_set_item_profile"
bl_label = "Enable Editing Material Set Item Profile"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
material_set_item: bpy.props.IntProperty()
def execute(self, context):
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
assert obj
self.props = tool.Material.get_object_material_props(obj)
self.props.active_material_set_item_id = self.material_set_item
self.props.material_set_item_profile_attributes.clear()
profile = tool.Ifc.get().by_id(self.material_set_item).Profile
bonsai.bim.helper.import_attributes(profile, self.props.material_set_item_profile_attributes)
return {"FINISHED"}
class DisableEditingMaterialSetItemProfile(bpy.types.Operator):
bl_idname = "bim.disable_editing_material_set_item_profile"
bl_label = "Disable Editing Material Set Item Profile"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
def execute(self, context):
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
assert obj
self.props = tool.Material.get_object_material_props(obj)
self.props.active_material_set_item_id = 0
self.props.material_set_item_profile_attributes.clear()
return {"FINISHED"}
class EditMaterialSetItemProfile(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.edit_material_set_item_profile"
bl_label = "Edit Material Set Item Profile"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
material_set_item: bpy.props.IntProperty()
def _execute(self, context):
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
assert obj
self.props = tool.Material.get_object_material_props(obj)
attributes = bonsai.bim.helper.export_attributes(self.props.material_set_item_profile_attributes)
profile = tool.Ifc.get().by_id(self.material_set_item).Profile
ifcopenshell.api.profile.edit_profile(tool.Ifc.get(), profile=profile, attributes=attributes)
self.props.active_material_set_item_id = 0
self.props.material_set_item_profile_attributes.clear()
model_profile.DumbProfileRegenerator().regenerate_from_profile_def(profile)
class EnableEditingMaterialSetItem(bpy.types.Operator):
bl_idname = "bim.enable_editing_material_set_item"
bl_label = "Enable Editing Material Set Item"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
material_set_item: bpy.props.IntProperty()
def execute(self, context):
self.file = tool.Ifc.get()
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
assert obj
self.mprops = tool.Material.get_material_props()
self.props = tool.Material.get_object_material_props(obj)
self.props.active_material_set_item_id = self.material_set_item
element = tool.Ifc.get_entity(obj)
assert element
material = ifcopenshell.util.element.get_material(element, should_skip_usage=True)
material_set_item = self.file.by_id(self.material_set_item)
self.props.material_set_item_material = str(material_set_item.Material.id())
self.props.material_set_item_attributes.clear()
bonsai.bim.helper.import_attributes(
material_set_item,
self.props.material_set_item_attributes,
callback=self.import_attributes_callback,
)
if material_set_item.is_a("IfcMaterialProfile"):
if material_set_item.Profile and material_set_item.Profile.ProfileName:
self.mprops.profiles = str(material_set_item.Profile.id())
return {"FINISHED"}
def import_attributes_callback(
self, name: str, prop: Union["Attribute", None], data: dict[str, Any]
) -> None | Literal[True]:
if data["type"] != "IfcMaterialLayer" or name != "IsVentilated" or not prop:
return None
# Keep null semantics unchanged on export, but avoid an empty UI selection.
prop.data_type = "enum"
prop.special_type = "LOGICAL"
prop.enum_items = json.dumps(("TRUE", "FALSE", "UNKNOWN"))
value = data[name]
if value == "UNKNOWN":
prop.enum_value = "UNKNOWN"
elif value is None:
# Keep visible default as FALSE, but preserve null semantics on save.
prop.enum_value = "FALSE"
prop.is_null = True
else:
prop.enum_value = "TRUE" if value else "FALSE"
return True
class DisableEditingMaterialSetItem(bpy.types.Operator):
bl_idname = "bim.disable_editing_material_set_item"
bl_label = "Disable Editing Material Set Item"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
def execute(self, context):
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
assert obj
props = tool.Material.get_object_material_props(obj)
props.active_material_set_item_id = 0
return {"FINISHED"}
class EditMaterialSetItem(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.edit_material_set_item"
bl_label = "Edit Material Set Item"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
material_set_item: bpy.props.IntProperty()
def _execute(self, context):
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
self.file = tool.Ifc.get()
assert obj
props = tool.Material.get_object_material_props(obj)
mprops = tool.Material.get_material_props()
element = tool.Ifc.get_entity(obj)
assert element
material = ifcopenshell.util.element.get_material(element, should_skip_usage=True)
assert material
attributes = bonsai.bim.helper.export_attributes(props.material_set_item_attributes)
if material.is_a("IfcMaterialConstituentSet"):
ifcopenshell.api.material.edit_constituent(
self.file,
constituent=self.file.by_id(self.material_set_item),
attributes=attributes,
material=self.file.by_id(int(props.material_set_item_material)),
)
elif material.is_a("IfcMaterialLayerSet"):
layer = self.file.by_id(self.material_set_item)
ifcopenshell.api.material.edit_layer(
self.file,
layer=layer,
attributes=attributes,
material=self.file.by_id(int(props.material_set_item_material)),
)
slab.DumbSlabPlaner().regenerate_from_layer(layer)
wall.DumbWallPlaner().regenerate_from_layer(layer)
elif material.is_a("IfcMaterialProfileSet"):
profile_def = None
if mprops.profiles:
profile_def = tool.Ifc.get().by_id(int(mprops.profiles))
ifcopenshell.api.material.edit_profile(
self.file,
profile=self.file.by_id(self.material_set_item),
attributes=attributes,
profile_def=profile_def,
material=self.file.by_id(int(props.material_set_item_material)),
)
else:
pass
bpy.ops.bim.disable_editing_material_set_item(obj=obj.name)
class ExpandMaterialCategory(bpy.types.Operator):
bl_idname = "bim.expand_material_category"
bl_label = "Expand Material Category"
bl_description = "SHIFT+CLICK to expand all material categories"
bl_options = {"REGISTER", "UNDO"}
category: bpy.props.StringProperty()
expand_all: bpy.props.BoolProperty(name="Expand All", default=False, options={"SKIP_SAVE"})
def invoke(self, context, event):
# Expanding all categories on shift+click.
# Make sure to use SKIP_SAVE on property, otherwise it might get stuck.
if event.type == "LEFTMOUSE" and event.shift:
self.expand_all = True
return self.execute(context)
def execute(self, context):
props = tool.Material.get_material_props()
for index, category in (
(i, c)
for i, c in enumerate(props.materials)
if c.is_category and (self.expand_all or c.name == self.category)
):
category.is_expanded = True
if category.name == self.category:
props.active_material_index = index
core.load_materials(tool.Material, props.material_type)
return {"FINISHED"}
class ContractMaterialCategory(bpy.types.Operator):
bl_idname = "bim.contract_material_category"
bl_label = "Contract Material Category"
bl_description = "SHIFT+CLICK to contract all material categories"
bl_options = {"REGISTER", "UNDO"}
category: bpy.props.StringProperty()
contract_all: bpy.props.BoolProperty(name="Contract All", default=False, options={"SKIP_SAVE"})
def invoke(self, context, event):
# Contracting all categories on shift+click.
# Make sure to use SKIP_SAVE on property, otherwise it might get stuck.
if event.type == "LEFTMOUSE" and event.shift:
self.contract_all = True
return self.execute(context)
def execute(self, context):
props = tool.Material.get_material_props()
for index, category in (
(i, c)
for i, c in enumerate(props.materials)
if c.is_category and (self.contract_all or c.name == self.category)
):
category.is_expanded = False
if category.name == self.category:
props.active_material_index = index
core.load_materials(tool.Material, props.material_type)
return {"FINISHED"}
class EnableEditingMaterialStyle(bpy.types.Operator):
bl_idname = "bim.enable_editing_material_style"
bl_label = "Enable Editing Material Style"
bl_options = {"REGISTER", "UNDO"}
material: bpy.props.IntProperty()
def execute(self, context):
props = tool.Material.get_material_props()
props.active_material_id = self.material
props.editing_material_type = "STYLE"
# set already applied style and context if possible
if not 0 <= props.active_material_index < len(props.materials):
return {"FINISHED"}
ifc_file = tool.Ifc.get()
material = ifc_file.by_id(props.materials[props.active_material_index].ifc_definition_id)
if not material.HasRepresentation:
# MODEL_VIEW is probably the one that's used with the styles most frequently.
context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
if context:
props.contexts = str(context.id())
return {"FINISHED"}
rep = material.HasRepresentation[0].Representations[0] # IfcStyledRepresentation
props.contexts = str(rep.ContextOfItems.id())
style = rep.Items[0].Styles[0]
if style.Name: # props.styles only has named styles
props.styles = str(rep.Items[0].Styles[0].id())
return {"FINISHED"}
class EditMaterialStyle(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.edit_material_style"
bl_label = "Edit Material Style"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
if not tool.Ifc.get():
cls.poll_message_set("No IFC project loaded")
return False
props = tool.Material.get_material_props()
active_style = tool.Blender.get_enum_safe(props, "styles")
if not active_style:
cls.poll_message_set("No style selected to assign.")
return False
return True
def _execute(self, context):
props = tool.Material.get_material_props()
ifc_file = tool.Ifc.get()
material = ifc_file.by_id(props.active_material_id)
style = ifc_file.by_id(int(props.styles))
context = ifc_file.by_id(int(props.contexts))
ifcopenshell.api.style.assign_material_style(ifc_file, material=material, style=style, context=context)
tool.Material.disable_editing_material()
core.load_materials(tool.Material, props.material_type)
# NOTE: Update all elements that has this material and
# not just the ones that are using it in IfcMaterialConstituent,
# to handle cases where Style is connected to geometry implicitly:
# e.g. RepItem->IfcElement->IfcMaterial->Style
# instead of RepItem->ShapeAspect->Constituent->Style.
tool.Material.update_elements_using_material(material)
class UnassignMaterialStyle(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.unassign_material_style"
bl_label = "Unassign Material Style"
bl_options = {"REGISTER", "UNDO"}
style: bpy.props.IntProperty()
context: bpy.props.IntProperty()
def _execute(self, context):
props = tool.Material.get_material_props()
material = tool.Ifc.get().by_id(props.materials[props.active_material_index].ifc_definition_id)
style = tool.Ifc.get().by_id(self.style)
context = tool.Ifc.get().by_id(self.context)
ifcopenshell.api.style.unassign_material_style(tool.Ifc.get(), material=material, style=style, context=context)
core.load_materials(tool.Material, props.material_type)
tool.Material.update_elements_using_material(material)
class SelectMaterialInMaterialsUI(bpy.types.Operator):
bl_idname = "bim.material_ui_select"
bl_label = "Select Material In Materials UI"
bl_options = {"REGISTER", "UNDO"}
material_id: bpy.props.IntProperty()
if TYPE_CHECKING:
material_id: int
def execute(self, context):
props = tool.Material.get_material_props()
ifc_file = tool.Ifc.get()
material_id = self.material_id
material = ifc_file.by_id(material_id)
material_class = material.is_a()
# Current we do not support editing usaged not on instance,
# so fallback to the material set it's referring to.
if material_class.endswith("Usage"):
material_class = material_class.removesuffix("Usage")
if material_class == "IfcMaterialProfileSet":
material = material.ForProfileSet
else: # IfcMaterialLayerSet
material = material.ForLayerSet
material_id = material.id()
core.load_materials(tool.Material, material_class)
def get_material_item() -> Union[tuple[int, bpy.types.PropertyGroup], None]:
return next(((i, m) for i, m in enumerate(props.materials) if m.ifc_definition_id == material_id), None)
material_item = get_material_item()
# Material category is contracted, won't occur for non-IfcMaterials.
if material_item is None:
material_category = tool.Material.get_material_category(material)
bpy.ops.bim.expand_material_category(category=material_category)
material_item = get_material_item()
assert material_item
item_index, _ = material_item
props.active_material_index = item_index
self.report(
{"INFO"},
f"Material '{tool.Material.get_material_name(material) or 'Unnamed'}' is selected in Materials UI.",
)
return {"FINISHED"}