diff --git a/src/bonsai/bonsai/bim/module/geometry/__init__.py b/src/bonsai/bonsai/bim/module/geometry/__init__.py index ebccfaf93b..ba3143233b 100644 --- a/src/bonsai/bonsai/bim/module/geometry/__init__.py +++ b/src/bonsai/bonsai/bim/module/geometry/__init__.py @@ -34,6 +34,7 @@ classes = ( operator.DuplicateMoveLinkedAggregate, operator.DuplicateMoveLinkedAggregateMacro, operator.EditObjectPlacement, + operator.EditRepresentationItemLayer, operator.EditRepresentationItemShapeAspect, operator.EditRepresentationItemStyle, operator.EnableEditingRepresentationItemShapeAspect, @@ -67,6 +68,7 @@ classes = ( operator.SelectConnection, operator.SelectRepresentationItem, operator.SwitchRepresentation, + operator.UnassignRepresentationItemLayer, operator.UnassignRepresentationItemStyle, operator.UpdateItemAttributes, operator.UpdateParametricRepresentation, diff --git a/src/bonsai/bonsai/bim/module/geometry/operator.py b/src/bonsai/bonsai/bim/module/geometry/operator.py index 394439839c..31511562e7 100644 --- a/src/bonsai/bonsai/bim/module/geometry/operator.py +++ b/src/bonsai/bonsai/bim/module/geometry/operator.py @@ -23,6 +23,7 @@ import logging import numpy as np import numpy.typing as npt import ifcopenshell +import ifcopenshell.api.layer import ifcopenshell.util.element import ifcopenshell.util.placement import ifcopenshell.util.representation @@ -2325,7 +2326,7 @@ class EnableEditingRepresentationItems(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = tool.Geometry.get_active_or_representation_obj() - props = obj.BIMGeometryProperties + props = tool.Geometry.get_object_geometry_props(obj) props.is_editing = True props.items.clear() @@ -3209,3 +3210,51 @@ class OverrideMove(bpy.types.Operator): return {"FINISHED"} return {"FINISHED"} + + +class EditRepresentationItemLayer(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.edit_representation_item_layer" + bl_label = "Edit Representation Item Layer" + bl_description = "Edit presentation layer for the active representation item." + bl_options = {"REGISTER", "UNDO"} + + def _execute(self, context): + ifc_file = tool.Ifc.get() + obj = context.active_object + assert obj + props = tool.Geometry.get_object_geometry_props(obj) + new_layer = ifc_file.by_id(int(props.representation_item_layer)) + + item = ifc_file.by_id(int(props.active_item.ifc_definition_id)) + item_layer = next(iter(item.LayerAssignment), None) + + # We assume just 1 layer can be assigned to representation item. + if item_layer: + # Same layer is already assigned. + if item_layer == new_layer: + props.is_editing_item_layer = False + return {"FINISHED"} + ifcopenshell.api.layer.unassign_layer(ifc_file, [item], item_layer) + + ifcopenshell.api.layer.assign_layer(ifc_file, [item], new_layer) + props.is_editing_item_layer = False + bpy.ops.bim.enable_editing_representation_items() + return {"FINISHED"} + + +class UnassignRepresentationItemLayer(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.unassign_representation_item_layer" + bl_label = "Unassign Representation Item Layer" + bl_description = "Unassign presentation layer from the active representation item." + bl_options = {"REGISTER", "UNDO"} + + def _execute(self, context): + ifc_file = tool.Ifc.get() + obj = context.active_object + assert obj + props = tool.Geometry.get_object_geometry_props(obj) + item = ifc_file.by_id(props.active_item.ifc_definition_id) + layer = item.LayerAssignment[0] # If there is no layer, then button is not visible in UI. + ifcopenshell.api.layer.unassign_layer(ifc_file, [item], layer) + bpy.ops.bim.enable_editing_representation_items() + return {"FINISHED"} diff --git a/src/bonsai/bonsai/bim/module/geometry/prop.py b/src/bonsai/bonsai/bim/module/geometry/prop.py index 8792585822..50e2b19d04 100644 --- a/src/bonsai/bonsai/bim/module/geometry/prop.py +++ b/src/bonsai/bonsai/bim/module/geometry/prop.py @@ -32,7 +32,7 @@ from bpy.props import ( FloatVectorProperty, CollectionProperty, ) -from typing import Optional +from typing import Optional, TYPE_CHECKING def get_contexts(self, context): @@ -105,6 +105,14 @@ def get_material_constituents(self, context, edit_text): return ObjectMaterialData.data["active_material_constituents"] +def get_layers(self, context): + from bonsai.bim.module.layer.data import LayersData + + if not LayersData.is_loaded: + LayersData.load() + return LayersData.data["layers_enum"] + + def update_shape_aspect(self, context): shape_aspect_id = self.representation_item_shape_aspect attrs = self.shape_aspect_attrs @@ -128,6 +136,16 @@ class RepresentationItem(PropertyGroup): shape_aspect_id: IntProperty(name="Shape Aspect IFC ID") tags: StringProperty(name="Tags") + if TYPE_CHECKING: + name: str + surface_style: str + surface_style_id: int + layer: str + ifc_definition_id: int + shape_aspect: str + shape_aspect_id: int + tags: str + class RepresentationItemObject(PropertyGroup): name: StringProperty(name="Name") @@ -149,6 +167,20 @@ class ShapeAspect(PropertyGroup): ) +def update_is_editing_item_layer(self: "BIMObjectGeometryProperties", context: bpy.types.Context) -> None: + if self.is_editing_item_layer: + ifc_file = tool.Ifc.get() + active_ui_item = self.active_item + assert active_ui_item + item = ifc_file.by_id(active_ui_item.ifc_definition_id) + if layer := next(iter(item.LayerAssignment), None): + self.representation_item_layer = str(layer.id()) + return + + if "representation_item_layer" in self: + del self["representation_item_layer"] + + class BIMObjectGeometryProperties(PropertyGroup): contexts: EnumProperty(items=get_contexts, name="Contexts") is_editing: BoolProperty(name="Is Editing", default=False) @@ -161,12 +193,32 @@ class BIMObjectGeometryProperties(PropertyGroup): items=get_shape_aspects, name="Representation Item Shape Aspect", update=update_shape_aspect ) shape_aspect_attrs: PointerProperty(type=ShapeAspect) + is_editing_item_layer: BoolProperty( + name="Is Editing Item's Presentation Layer", + description="Toggle editing presentation layer for the item.", + default=False, + update=update_is_editing_item_layer, + ) + representation_item_layer: EnumProperty(items=get_layers, name="Representation Item's Layer") @property def active_item(self): - if self.active_item_index < len(self.items): + if 0 <= self.active_item_index < len(self.items): return self.items[self.active_item_index] + if TYPE_CHECKING: + contexts: str + is_editing: bool + items: bpy.types.bpy_prop_collection_idprop[RepresentationItem] + active_item_index: int + is_editing_item_style: bool + representation_item_style: str + is_editing_item_shape_aspect: bool + representation_item_shape_aspect: str + shape_aspect_attrs: ShapeAspect + is_editing_item_layer: bool + representation_item_layer: str + class BIMGeometryProperties(PropertyGroup): # Revit workaround diff --git a/src/bonsai/bonsai/bim/module/geometry/ui.py b/src/bonsai/bonsai/bim/module/geometry/ui.py index 86ade6b6f4..c7f8133a79 100644 --- a/src/bonsai/bonsai/bim/module/geometry/ui.py +++ b/src/bonsai/bonsai/bim/module/geometry/ui.py @@ -179,7 +179,7 @@ class BIM_PT_representation_items(Panel): RepresentationItemsData.load() obj = context.scene.BIMGeometryProperties.representation_obj or tool.Blender.get_active_object() - props = obj.BIMGeometryProperties + props = tool.Geometry.get_object_geometry_props(obj) row = self.layout.row(align=True) row.label(text=f"{RepresentationItemsData.data['total_items']} Items Found") @@ -207,7 +207,9 @@ class BIM_PT_representation_items(Panel): surface_style = active_item.surface_style surface_style_id = active_item.surface_style_id shape_aspect = active_item.shape_aspect + layer = active_item.layer + # Style. row = self.layout.row(align=True) if props.is_editing_item_style: # NOTE: we currently support 1 item having just 1 style @@ -226,9 +228,19 @@ class BIM_PT_representation_items(Panel): if surface_style: row.operator("bim.unassign_representation_item_style", icon="X", text="") - row = self.layout.row() - row.label(text=active_item.layer or "No Presentation Layer", icon="STICKY_UVS_LOC") + # Presentation layer. + row = self.layout.row(align=True) + if props.is_editing_item_layer: + prop_with_search(row, props, "representation_item_layer", icon="STICKY_UVS_LOC", text="") + row.operator("bim.edit_representation_item_layer", icon="CHECKMARK", text="") + row.prop(props, "is_editing_item_layer", icon="CANCEL", text="") + else: + row.label(text=layer or "No Presentation Layer", icon="STICKY_UVS_LOC") + row.prop(props, "is_editing_item_layer", icon="GREASEPENCIL", text="") + if layer: + row.operator("bim.unassign_representation_item_layer", icon="X", text="") + # Mappings. if active_item.name.endswith("FaceSet"): if "UV" in active_item.tags: text = "Has UV mapping" @@ -242,6 +254,7 @@ class BIM_PT_representation_items(Panel): text = "Has no colour mapping" self.layout.label(text=text, icon="COLOR") + # Shape aspect. row = self.layout.row(align=True) if props.is_editing_item_shape_aspect: row.prop(props, "representation_item_shape_aspect", icon="SHAPEKEY_DATA", text="") diff --git a/src/bonsai/bonsai/bim/module/layer/data.py b/src/bonsai/bonsai/bim/module/layer/data.py index 8aece515bb..4de8a9687f 100644 --- a/src/bonsai/bonsai/bim/module/layer/data.py +++ b/src/bonsai/bonsai/bim/module/layer/data.py @@ -19,6 +19,7 @@ import bpy import ifcopenshell import bonsai.tool as tool +from typing import Any def refresh(): @@ -31,12 +32,31 @@ class LayersData: @classmethod def load(cls): - cls.data = {"total_layers": cls.total_layers(), "active_layers": cls.active_layers()} + cls.data = { + "active_layers": cls.active_layers(), + "layers": cls.layers(), + } + # After .layers(). + cls.data["total_layers"] = cls.total_layers() + cls.data["layers_enum"] = cls.layers_enum() + cls.is_loaded = True @classmethod def total_layers(cls) -> int: - return len(tool.Ifc.get().by_type("IfcPresentationLayerAssignment")) + return len(cls.data["layers"]) + + @classmethod + def layers(cls) -> dict[int, dict[str, Any]]: + results = dict() + KEEP_ATTRS = set(("id", "Name", "Description")) + for layer in tool.Ifc.get().by_type("IfcPresentationLayerAssignment"): + results[layer.id()] = {k: v for k, v in layer.get_info().items() if k in KEEP_ATTRS} + return results + + @classmethod + def layers_enum(cls) -> list[tuple[str, str, str]]: + return list((str(data["id"]), data["Name"], data["Description"] or "") for data in cls.data["layers"].values()) @classmethod def active_layers(cls) -> dict[int, str]: