mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-28 15:53:00 +00:00
6ba5f5af3d
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.
1024 lines
40 KiB
Python
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"}
|