diff --git a/src/bonsai/bonsai/bim/module/pset/__init__.py b/src/bonsai/bonsai/bim/module/pset/__init__.py index 0ab35183e4..45221737c5 100644 --- a/src/bonsai/bonsai/bim/module/pset/__init__.py +++ b/src/bonsai/bonsai/bim/module/pset/__init__.py @@ -33,16 +33,16 @@ classes = ( operator.UnsharePset, operator.BIM_OT_add_property_to_edit, operator.BIM_OT_remove_property_to_edit, - operator.BIM_OT_clear_list, - operator.BIM_OT_rename_parameters, + operator.BIM_OT_bulk_edit_clear_list, + operator.BIM_OT_pset_bulk_rename_parameters, operator.BIM_OT_add_edit_custom_property, operator.BIM_OT_bulk_remove_psets, prop.IfcPropertyEnumeratedValue, prop.IfcProperty, prop.PsetProperties, - prop.RenameProperties, - prop.AddEditProperties, - prop.DeletePsets, + prop.RenamePropertyEntry, + prop.AddEditPropertyEntry, + prop.DeletePsetEntry, prop.GlobalPsetProperties, ui.BIM_PT_object_psets, ui.BIM_PT_object_qtos, @@ -71,9 +71,6 @@ def register(): bpy.types.Scene.GroupPsetProperties = bpy.props.PointerProperty(type=prop.PsetProperties) bpy.types.Scene.ProfilePsetProperties = bpy.props.PointerProperty(type=prop.PsetProperties) bpy.types.Scene.WorkSchedulePsetProperties = bpy.props.PointerProperty(type=prop.PsetProperties) - bpy.types.Scene.RenameProperties = bpy.props.CollectionProperty(type=prop.RenameProperties) - bpy.types.Scene.AddEditProperties = bpy.props.CollectionProperty(type=prop.AddEditProperties) - bpy.types.Scene.DeletePsets = bpy.props.CollectionProperty(type=prop.DeletePsets) bpy.types.Scene.GlobalPsetProperties = bpy.props.PointerProperty(type=prop.GlobalPsetProperties) @@ -86,7 +83,4 @@ def unregister(): del bpy.types.Scene.GroupPsetProperties del bpy.types.Scene.ProfilePsetProperties del bpy.types.Scene.WorkSchedulePsetProperties - del bpy.types.Scene.RenameProperties - del bpy.types.Scene.AddEditProperties - del bpy.types.Scene.DeletePsets del bpy.types.Scene.GlobalPsetProperties diff --git a/src/bonsai/bonsai/bim/module/pset/data.py b/src/bonsai/bonsai/bim/module/pset/data.py index f139de5f9e..1dbf533ae7 100644 --- a/src/bonsai/bonsai/bim/module/pset/data.py +++ b/src/bonsai/bonsai/bim/module/pset/data.py @@ -26,7 +26,7 @@ import bonsai.bim.schema # TODO: Should this cache belong here? Dunno. Maybe. -is_expanded = {} +is_expanded: dict[int, bool] = {} def refresh(): diff --git a/src/bonsai/bonsai/bim/module/pset/operator.py b/src/bonsai/bonsai/bim/module/pset/operator.py index ebd3e8009e..8988b7557d 100644 --- a/src/bonsai/bonsai/bim/module/pset/operator.py +++ b/src/bonsai/bonsai/bim/module/pset/operator.py @@ -18,6 +18,7 @@ import bpy import json +from bpy.types import Context, OperatorProperties import ifcopenshell.api import ifcopenshell.api.pset import ifcopenshell.util.attribute @@ -29,6 +30,10 @@ import bonsai.tool as tool import bonsai.core.pset as core import bonsai.bim.module.pset.data from bonsai.bim.ifc import IfcStore +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from bonsai.bim.module.pset.prop import RenamePropertyEntry, AddEditPropertyEntry class TogglePsetExpansion(bpy.types.Operator, tool.Ifc.Operator): @@ -263,55 +268,85 @@ class CopyPropertyToSelection(bpy.types.Operator, tool.Ifc.Operator): class BIM_OT_add_property_to_edit(bpy.types.Operator): - bl_label = "Add Edit Rule" + bl_label = "Add Property to Edit" bl_idname = "bim.add_property_to_edit" bl_options = {"REGISTER", "UNDO"} - option: bpy.props.StringProperty() - index: bpy.props.IntProperty(default=-1) + option: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration] + items=[(t, t, "") for t in tool.Pset.BULK_OPERATION_TYPES], + ) + index: bpy.props.IntProperty(default=-1) # pyright: ignore[reportRedeclaration] + + if TYPE_CHECKING: + option: tool.Pset.BulkOperationType + index: int + + @classmethod + def description(cls, context: bpy.types.Context, properties: bpy.types.OperatorProperties) -> str: + return f"Add property entry to for bulk operation '{properties.option}'." def execute(self, context): if self.index == -1: - getattr(context.scene, self.option).add() + tool.Pset.get_bulk_operation_collection(self.option).add() else: - getattr(context.scene, self.option)[self.index].enum_values.add() + assert self.option == "ADD_EDIT" + props = tool.Pset.get_global_pset_props() + props.psets_to_add_edit[self.index].enum_values.add() return {"FINISHED"} class BIM_OT_remove_property_to_edit(bpy.types.Operator): - bl_label = "Remove Property to Be Renamed" + bl_label = "Remove Property from Editing" bl_idname = "bim.remove_property_to_edit" bl_options = {"REGISTER", "UNDO"} - index: bpy.props.IntProperty() - index2: bpy.props.IntProperty(default=-1) - option: bpy.props.StringProperty() + index: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration] + index2: bpy.props.IntProperty(default=-1) # pyright: ignore[reportRedeclaration] + option: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration] + items=[(t, t, "") for t in tool.Pset.BULK_OPERATION_TYPES], + ) + + if TYPE_CHECKING: + index: int + index2: int + option: tool.Pset.BulkOperationType + + @classmethod + def description(cls, context: bpy.types.Context, properties: bpy.types.OperatorProperties) -> str: + return f"Remove property entry from bulk operation '{properties.option}'." def execute(self, context): if self.index2 == -1: - getattr(context.scene, self.option).remove(self.index) + tool.Pset.get_bulk_operation_collection(self.option).remove(self.index) else: - getattr(context.scene, self.option)[self.index].enum_values.remove(self.index2) + assert self.option == "ADD_EDIT" + props = tool.Pset.get_global_pset_props() + props.psets_to_add_edit[self.index].enum_values.remove(self.index2) return {"FINISHED"} -class BIM_OT_clear_list(bpy.types.Operator): +class BIM_OT_bulk_edit_clear_list(bpy.types.Operator): bl_label = "Clear List of Properties" - bl_idname = "bim.clear_list" + bl_idname = "bim.pset_bulk_edit_clear_list" bl_options = {"REGISTER", "UNDO"} - option: bpy.props.StringProperty() + option: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration] + items=[(t, t, "") for t in tool.Pset.BULK_OPERATION_TYPES], + ) + + if TYPE_CHECKING: + option: tool.Pset.BulkOperationType def execute(self, context): - getattr(context.scene, self.option).clear() + tool.Pset.get_bulk_operation_collection(self.option).clear() return {"FINISHED"} -class BIM_OT_rename_parameters(bpy.types.Operator, tool.Ifc.Operator): +class BIM_OT_pset_bulk_rename_parameters(bpy.types.Operator, tool.Ifc.Operator): bl_label = "Rename Parameters" - bl_idname = "bim.rename_parameters" + bl_idname = "bim.pset_bulk_rename_parameters" bl_options = {"REGISTER", "UNDO"} bl_description = "Rename parameters that are subclasses of IfcElement" def _execute(self, context): - props_to_map = context.scene.RenameProperties + props_to_map = tool.Pset.get_global_pset_props().psets_to_rename ifc_file = tool.Ifc.get() all_ifc_elements = ifc_file.by_type("IfcElement") @@ -319,20 +354,27 @@ class BIM_OT_rename_parameters(bpy.types.Operator, tool.Ifc.Operator): for definition in ifc_element.IsDefinedBy: if definition.is_a("IfcRelDefinesByProperties"): prop_set = definition.RelatingPropertyDefinition - self.rename_property(prop_set, props_to_map, ifc_element) + self.rename_property(prop_set, props_to_map) self.report({"INFO"}, "Finished applying changes") return {"FINISHED"} - def rename_property(self, property_set, properties_to_map, ifc_element): + def rename_property( + self, + property_set: ifcopenshell.entity_instance, + properties_to_map: "bpy.types.bpy_prop_collection_idprop[RenamePropertyEntry]", + ) -> None: + property_container: tuple[ifcopenshell.entity_instance, ...] if property_set.is_a() == "IfcPropertySet": property_container = property_set.HasProperties elif property_set.is_a() == "IfcElementQuantity": property_container = property_set.Quantities + else: + assert False for obj_prop in property_container: for prop2map in properties_to_map: - if prop2map.pset_name != property_set.Name: + if prop2map.name != property_set.Name: continue if prop2map.existing_property_name == obj_prop.Name: obj_prop.Name = prop2map.new_property_name @@ -341,46 +383,51 @@ class BIM_OT_rename_parameters(bpy.types.Operator, tool.Ifc.Operator): class BIM_OT_add_edit_custom_property(bpy.types.Operator, tool.Ifc.Operator): bl_label = "Add or Edit a Custom Property" bl_idname = "bim.add_edit_custom_property" + bl_description = "Edit pset properties for selected objects." bl_options = {"REGISTER", "UNDO"} index: bpy.props.IntProperty() def _execute(self, context): self.file = tool.Ifc.get() - props = context.scene.AddEditProperties + entries = tool.Pset.get_global_pset_props().psets_to_add_edit + elements_changed = 0 for obj in tool.Blender.get_selected_objects(): ifc_element = tool.Ifc.get_entity(obj) if not ifc_element: continue - for prop in props: + for prop in entries: value = getattr(prop, prop.get_value_name()) primary_measure_type = prop.primary_measure_type if prop.template_type == "IfcPropertyEnumeratedValue": value_ifc_entity = self.generate_enum_entity(prop) elif prop.template_type == "IfcPropertySingleValue": - value_ifc_entity = getattr(self.file, f"create{primary_measure_type}")(value) + value_ifc_entity = self.file.create_entity(primary_measure_type, value) + else: + assert False new_pset = ifcopenshell.api.pset.add_pset(self.file, product=ifc_element, name=prop.pset_name) - ifcopenshell.api.pset.edit_pset( - self.file, pset=new_pset, properties={prop.property_name: value_ifc_entity} - ) - self.report({"INFO"}, "Finished applying changes") + ifcopenshell.api.pset.edit_pset(self.file, pset=new_pset, properties={prop.name: value_ifc_entity}) + + if entries: + elements_changed += 1 + self.report({"INFO"}, f"Finished applying changes, {elements_changed} elements changed.") return {"FINISHED"} - def generate_enum_entity(self, prop): + def generate_enum_entity(self, prop: "AddEditPropertyEntry") -> ifcopenshell.entity_instance: prop_type = prop.get_value_name() prop_enum = self.file.create_entity( "IFCPROPERTYENUMERATION", - Name=prop.property_name, + Name=prop.name, EnumerationValues=tuple( self.file.create_entity(prop.primary_measure_type, ev[prop_type]) for ev in prop.enum_values ), ) prop_enum_value = self.file.create_entity( "IFCPROPERTYENUMERATEDVALUE", - Name=prop.property_name, + Name=prop.name, EnumerationValues=tuple( self.file.create_entity(prop.primary_measure_type, ev[prop_type]) for ev in prop.enum_values @@ -400,7 +447,7 @@ class BIM_OT_bulk_remove_psets(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): self.file = tool.Ifc.get() - props = context.scene.DeletePsets + props = tool.Pset.get_global_pset_props() for obj in tool.Blender.get_selected_objects(): ifc_element = tool.Ifc.get_entity(obj) @@ -408,8 +455,8 @@ class BIM_OT_bulk_remove_psets(bpy.types.Operator, tool.Ifc.Operator): continue psets = ifcopenshell.util.element.get_psets(ifc_element) - for prop in props: - pset = prop.pset_name + for prop in props.psets_to_delete: + pset = prop.name if pset in psets: try: ifcopenshell.api.pset.remove_pset( diff --git a/src/bonsai/bonsai/bim/module/pset/prop.py b/src/bonsai/bonsai/bim/module/pset/prop.py index d3a0c78d6e..1ecdd8c378 100644 --- a/src/bonsai/bonsai/bim/module/pset/prop.py +++ b/src/bonsai/bonsai/bim/module/pset/prop.py @@ -233,14 +233,14 @@ def get_object_qto_name(self: "PsetProperties", context: object) -> tool.Blender # TODO: unsafe? -def get_template_type(self: "AddEditProperties", context: object) -> tool.Blender.BLENDER_ENUM_ITEMS: +def get_template_type(self: "AddEditPropertyEntry", context: object) -> tool.Blender.BLENDER_ENUM_ITEMS: version = tool.Ifc.get_schema() for t in ("IfcPropertySingleValue", "IfcPropertyEnumeratedValue"): yield (t, t, ifcopenshell.util.doc.get_entity_doc(version, t).get("description", "")) # TODO: unsafe? -def get_primary_measure_type(self: "AddEditProperties", context: object) -> tool.Blender.BLENDER_ENUM_ITEMS: +def get_primary_measure_type(self: "AddEditPropertyEntry", context: object) -> tool.Blender.BLENDER_ENUM_ITEMS: if not AddEditCustomPropertiesData.is_loaded: AddEditCustomPropertiesData.load() return AddEditCustomPropertiesData.data["primary_measure_type"] @@ -295,20 +295,20 @@ class PsetProperties(PropertyGroup): prop_value: str -class RenameProperties(PropertyGroup): - pset_name: StringProperty(name="Pset") +class RenamePropertyEntry(PropertyGroup): + name: StringProperty(name="Pset") existing_property_name: StringProperty(name="Existing Property Name") new_property_name: StringProperty(name="New Property Name") if TYPE_CHECKING: - pset_name: str + name: str existing_property_name: str new_property_name: str -class AddEditProperties(PropertyGroup): +class AddEditPropertyEntry(PropertyGroup): pset_name: StringProperty(name="Pset") - property_name: StringProperty(name="Property") + name: StringProperty(name="Property") string_value: StringProperty(name="Value") bool_value: BoolProperty(name="Value") int_value: IntProperty(name="Value") @@ -319,7 +319,7 @@ class AddEditProperties(PropertyGroup): if TYPE_CHECKING: pset_name: str - property_name: str + name: str string_value: str bool_value: bool int_value: int @@ -342,17 +342,26 @@ class AddEditProperties(PropertyGroup): return "float_value" -class DeletePsets(PropertyGroup): - pset_name: StringProperty(name="Pset") +# This class is needed just to make tooltip more descriptive. +class DeletePsetEntry(PropertyGroup): + name: StringProperty(name="Pset to Remove") if TYPE_CHECKING: - pset_name: str + name: str class GlobalPsetProperties(PropertyGroup): pset_filter: StringProperty(name="Pset Filter", options={"TEXTEDIT_UPDATE"}) qto_filter: StringProperty(name="Qto Filter", options={"TEXTEDIT_UPDATE"}) + # Bulk operations. + psets_to_delete: CollectionProperty(type=DeletePsetEntry) # pyright: ignore[reportRedeclaration] + psets_to_rename: CollectionProperty(type=RenamePropertyEntry) # pyright: ignore[reportRedeclaration] + psets_to_add_edit: CollectionProperty(type=AddEditPropertyEntry) # pyright: ignore[reportRedeclaration] + if TYPE_CHECKING: pset_filter: str qto_filter: str + psets_to_delete: bpy.types.bpy_prop_collection_idprop[DeletePsetEntry] + psets_to_rename: bpy.types.bpy_prop_collection_idprop[RenamePropertyEntry] + psets_to_add_edit: bpy.types.bpy_prop_collection_idprop[AddEditPropertyEntry] diff --git a/src/bonsai/bonsai/bim/module/pset/ui.py b/src/bonsai/bonsai/bim/module/pset/ui.py index f63f9e3189..c1215d48a0 100644 --- a/src/bonsai/bonsai/bim/module/pset/ui.py +++ b/src/bonsai/bonsai/bim/module/pset/ui.py @@ -815,28 +815,28 @@ class BIM_PT_rename_parameters(Panel): bl_options = {"DEFAULT_CLOSED"} def draw(self, context): + assert self.layout layout = self.layout - props = context.scene.RenameProperties + entries = tool.Pset.get_global_pset_props().psets_to_rename row = layout.row() op = row.operator("bim.add_property_to_edit", icon="ADD") - op.option = "RenameProperties" + op.option = "RENAME" - if props: - for index, prop in enumerate(props): + if entries: + for index, prop in enumerate(entries): row = layout.row(align=True) - prop_with_search(row, prop, "pset_name", text="") + row.prop(prop, "name", text="") row.prop(prop, "existing_property_name", text="") row.prop(prop, "new_property_name", text="") op = row.operator("bim.remove_property_to_edit", icon="X", text="") op.index = index - op.option = "RenameProperties" + op.option = "RENAME" - if props: row = layout.row(align=True) - row.operator("bim.rename_parameters", icon="CHECKMARK") - clear = row.operator("bim.clear_list", icon="CANCEL", text="") - clear.option = "RenameProperties" + row.operator("bim.pset_bulk_rename_parameters", icon="CHECKMARK") + clear = row.operator("bim.pset_bulk_edit_clear_list", icon="CANCEL", text="") + clear.option = "RENAME" class BIM_PT_add_edit_custom_properties(Panel): @@ -849,30 +849,31 @@ class BIM_PT_add_edit_custom_properties(Panel): bl_options = {"DEFAULT_CLOSED"} def draw(self, context): + assert self.layout layout = self.layout - props = context.scene.AddEditProperties + entries = tool.Pset.get_global_pset_props().psets_to_add_edit row = layout.row() op = row.operator("bim.add_property_to_edit", icon="ADD") - op.option = "AddEditProperties" + op.option = "ADD_EDIT" op.index = -1 - if props: - for index, prop in enumerate(props): + if entries: + for index, prop in enumerate(entries): row = layout.row(align=True) prop_with_search(row, prop, "pset_name", text="") - row.prop(prop, "property_name", text="") + row.prop(prop, "name", text="") if prop.template_type == "IfcPropertySingleValue": row.prop(prop, prop.get_value_name(), text="") prop_with_search(row, prop, "primary_measure_type", text="") row.prop(prop, "template_type", text="") op = row.operator("bim.remove_property_to_edit", icon="X", text="") op.index = index - op.option = "AddEditProperties" + op.option = "ADD_EDIT" if prop.template_type == "IfcPropertyEnumeratedValue": op = row.operator("bim.add_property_to_edit", icon="ADD", text="Add Enum") - op.option = "AddEditProperties" + op.option = "ADD_EDIT" op.index = index for index2, prop2 in enumerate(prop.enum_values): row = layout.row() @@ -883,13 +884,12 @@ class BIM_PT_add_edit_custom_properties(Panel): op = row.operator("bim.remove_property_to_edit", icon="X", text="") op.index = index op.index2 = index2 - op.option = "AddEditProperties" + op.option = "ADD_EDIT" - if props: row = layout.row(align=True) op = row.operator("bim.add_edit_custom_property", icon="CHECKMARK", text="Apply Changes") - clear = row.operator("bim.clear_list", icon="CANCEL", text="") - clear.option = "AddEditProperties" + clear = row.operator("bim.pset_bulk_edit_clear_list", icon="CANCEL", text="") + clear.option = "ADD_EDIT" class BIM_PT_delete_psets(Panel): @@ -902,23 +902,23 @@ class BIM_PT_delete_psets(Panel): bl_options = {"DEFAULT_CLOSED"} def draw(self, context): + assert self.layout layout = self.layout - props = context.scene.DeletePsets + entries = tool.Pset.get_global_pset_props().psets_to_delete row = layout.row() op = row.operator("bim.add_property_to_edit", icon="ADD") - op.option = "DeletePsets" + op.option = "DELETE" - if props: - for index, prop in enumerate(props): + if entries: + for index, prop in enumerate(entries): row = layout.row(align=True) - prop_with_search(row, prop, "pset_name", text="") + row.prop(prop, "name", text="") op = row.operator("bim.remove_property_to_edit", icon="X", text="") op.index = index - op.option = "DeletePsets" + op.option = "DELETE" - if props: row = layout.row(align=True) op = row.operator("bim.bulk_remove_psets", icon="CHECKMARK", text="Apply Changes") - clear = row.operator("bim.clear_list", icon="CANCEL", text="") - clear.option = "DeletePsets" + clear = row.operator("bim.pset_bulk_edit_clear_list", icon="CANCEL", text="") + clear.option = "DELETE" diff --git a/src/bonsai/bonsai/tool/pset.py b/src/bonsai/bonsai/tool/pset.py index cb7d7216eb..3390c02c4c 100644 --- a/src/bonsai/bonsai/tool/pset.py +++ b/src/bonsai/bonsai/tool/pset.py @@ -30,16 +30,40 @@ from typing import Union, Literal, Any, TYPE_CHECKING, assert_never if TYPE_CHECKING: - from bonsai.bim.module.pset.prop import PsetProperties, GlobalPsetProperties + from bonsai.bim.module.pset.prop import ( + PsetProperties, + GlobalPsetProperties, + AddEditPropertyEntry, + RenamePropertyEntry, + DeletePsetEntry, + ) class Pset(bonsai.core.tool.Pset): PSET_TYPE = Literal["PSET", "QTO"] + BulkOperationType = Literal["ADD_EDIT", "RENAME", "DELETE"] + BULK_OPERATION_TYPES = ("ADD_EDIT", "RENAME", "DELETE") @classmethod def get_global_pset_props(cls) -> GlobalPsetProperties: return bpy.context.scene.GlobalPsetProperties + @classmethod + def get_bulk_operation_collection(cls, operation_type: BulkOperationType) -> Union[ + bpy.types.bpy_prop_collection_idprop[AddEditPropertyEntry], + bpy.types.bpy_prop_collection_idprop[RenamePropertyEntry], + bpy.types.bpy_prop_collection_idprop[DeletePsetEntry], + ]: + props = cls.get_global_pset_props() + if operation_type == "ADD_EDIT": + return props.psets_to_add_edit + elif operation_type == "RENAME": + return props.psets_to_rename + elif operation_type == "DELETE": + return props.psets_to_delete + else: + assert False + @classmethod def get_element_pset( cls, element: ifcopenshell.entity_instance, pset_name: str