diff --git a/src/bonsai/bonsai/bim/module/attribute/__init__.py b/src/bonsai/bonsai/bim/module/attribute/__init__.py index b6c7a846dc..754901042f 100644 --- a/src/bonsai/bonsai/bim/module/attribute/__init__.py +++ b/src/bonsai/bonsai/bim/module/attribute/__init__.py @@ -25,13 +25,22 @@ classes = ( operator.EditAttributes, operator.GenerateGlobalId, operator.CopyAttributeToSelection, + operator.ExplorerAddEntity, + operator.ExplorerEnableEditingEntity, + operator.ExplorerDisableEditingEntity, + operator.ExplorerEditEntity, prop.BIMAttributeProperties, + prop.ExplorerEntity, + prop.BIMExplorerProperties, ui.BIM_PT_object_attributes, + ui.BIM_PT_explorer, + ui.BIM_UL_explorer, ) def register(): bpy.types.Object.BIMAttributeProperties = bpy.props.PointerProperty(type=prop.BIMAttributeProperties) + bpy.types.Scene.BIMExplorerProperties = bpy.props.PointerProperty(type=prop.BIMExplorerProperties) def unregister(): diff --git a/src/bonsai/bonsai/bim/module/attribute/operator.py b/src/bonsai/bonsai/bim/module/attribute/operator.py index 5f5bf9f8c2..2aefafd3b6 100644 --- a/src/bonsai/bonsai/bim/module/attribute/operator.py +++ b/src/bonsai/bonsai/bim/module/attribute/operator.py @@ -28,6 +28,9 @@ import bonsai.core.attribute as core import bonsai.core.spatial from typing import TYPE_CHECKING +if TYPE_CHECKING: + import bpy.stub_internal.rna_enums as rna_enums + def get_objs_for_operation( operator_properties: "AttributesOperator", context: bpy.types.Context @@ -204,3 +207,62 @@ class CopyAttributeToSelection(bpy.types.Operator, tool.Ifc.Operator): tool.Ifc, tool.Blender, tool.Root, tool.Spatial, name=self.name, value=value ) self.report({"INFO"}, f"Attribute was successfully copied to {total} elements.") + + +class ExplorerAddEntity(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.explorer_add_entity" + bl_label = "Add Entity" + bl_options = {"REGISTER", "UNDO"} + + def _execute(self, context) -> None: + props = tool.Attribute.get_explorer_props() + ifc_file = tool.Ifc.get() + + entity = ifc_file.create_entity(props.ifc_class) + tool.Attribute.refresh_uilist_entities() + tool.Attribute.enable_editing_entity(entity) + tool.Attribute.import_entity_attributes(entity) + + +class ExplorerEnableEditingEntity(bpy.types.Operator): + bl_idname = "bim.explorer_enable_editing_entity" + bl_label = "Enable Editing Entity" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context) -> "set[rna_enums.OperatorReturnItems]": + props = tool.Attribute.get_explorer_props() + ifc_file = tool.Ifc.get() + assert (active_entity := props.active_entity) + entity = ifc_file.by_id(active_entity.ifc_definition_id) + + tool.Attribute.disable_editing_entity() + tool.Attribute.enable_editing_entity(entity) + tool.Attribute.import_entity_attributes(entity) + return {"FINISHED"} + + +class ExplorerDisableEditingEntity(bpy.types.Operator): + bl_idname = "bim.explorer_disable_editing_entity" + bl_label = "Disable Editing Entity" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context) -> set["rna_enums.OperatorReturnItems"]: + tool.Attribute.disable_editing_entity() + return {"FINISHED"} + + +class ExplorerEditEntity(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.explorer_edit_entity" + bl_label = "Edit Entity" + bl_options = {"REGISTER", "UNDO"} + + def _execute(self, context) -> None: + ifc_file = tool.Ifc.get() + props = tool.Attribute.get_explorer_props() + entity = ifc_file.by_id(props.editing_entity_id) + + attrs = tool.Attribute.export_entity_attributes() + for attr, value in attrs.items(): + setattr(entity, attr, value) + tool.Attribute.refresh_uilist_entities() + tool.Attribute.disable_editing_entity() diff --git a/src/bonsai/bonsai/bim/module/attribute/prop.py b/src/bonsai/bonsai/bim/module/attribute/prop.py index b42846a3d6..d8de906b9e 100644 --- a/src/bonsai/bonsai/bim/module/attribute/prop.py +++ b/src/bonsai/bonsai/bim/module/attribute/prop.py @@ -17,6 +17,7 @@ # along with Bonsai. If not, see . import bpy +import bonsai.tool as tool from bonsai.bim.prop import StrProperty, Attribute from bpy.types import PropertyGroup from bpy.props import ( @@ -29,7 +30,7 @@ from bpy.props import ( FloatVectorProperty, CollectionProperty, ) -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Union class BIMAttributeProperties(PropertyGroup): @@ -39,3 +40,44 @@ class BIMAttributeProperties(PropertyGroup): if TYPE_CHECKING: attributes: bpy.types.bpy_prop_collection_idprop[Attribute] is_editing_attributes: bool + + +class ExplorerEntity(PropertyGroup): + ifc_definition_id: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration] + + if TYPE_CHECKING: + ifc_definition_id: int + + +class BIMExplorerProperties(PropertyGroup): + def get_ifc_class(self, context: object) -> tool.Blender.BLENDER_ENUM_ITEMS: + # TODO: Add more entities. + classes = [ + "IfcPostalAddress", + "IfcTelecomAddress", + ] + return [(c, c, "") for c in classes] + + def update_ifc_class(self, context: object) -> None: + tool.Attribute.refresh_uilist_entities() + + ifc_class: EnumProperty( # pyright: ignore[reportRedeclaration] + name="IFC Class To Search", + items=get_ifc_class, + update=update_ifc_class, + ) + entities: CollectionProperty(type=ExplorerEntity) # pyright: ignore[reportRedeclaration] + active_entity_index: IntProperty() # pyright: ignore[reportRedeclaration] + editing_entity_id: IntProperty() # pyright: ignore[reportRedeclaration] + entity_attributes: CollectionProperty(type=Attribute) # pyright: ignore[reportRedeclaration] + + if TYPE_CHECKING: + ifc_class: str + entities: bpy.types.bpy_prop_collection_idprop[ExplorerEntity] + active_entity_index: int + editing_entity_id: int + entity_attributes: bpy.types.bpy_prop_collection_idprop[Attribute] + + @property + def active_entity(self) -> Union[ExplorerEntity, None]: + return tool.Blender.get_active_uilist_element(self.entities, self.active_entity_index) diff --git a/src/bonsai/bonsai/bim/module/attribute/ui.py b/src/bonsai/bonsai/bim/module/attribute/ui.py index 065aaab880..2cde12da6d 100644 --- a/src/bonsai/bonsai/bim/module/attribute/ui.py +++ b/src/bonsai/bonsai/bim/module/attribute/ui.py @@ -16,11 +16,16 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . +from __future__ import annotations import bonsai.bim.helper import bpy.types from bpy.types import Panel from bonsai.bim.module.attribute.data import AttributesData import bonsai.tool as tool +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from bonsai.bim.module.attribute.prop import BIMExplorerProperties, ExplorerEntity def draw_ui(context: bpy.types.Context, layout: bpy.types.UILayout, attributes) -> None: @@ -66,3 +71,57 @@ class BIM_PT_object_attributes(Panel): if not AttributesData.is_loaded: AttributesData.load() draw_ui(context, self.layout, AttributesData.data["attributes"]) + + +class BIM_PT_explorer(Panel): + bl_label = "Explorer" + bl_idname = "BIM_PT_explorer" + bl_options = {"DEFAULT_CLOSED"} + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + bl_parent_id = "BIM_PT_tab_project_setup" + + def draw(self, context): + assert (layout := self.layout) + props = tool.Attribute.get_explorer_props() + active_entity = props.active_entity + + layout.prop(props, "ifc_class", text="") + row = layout.row(align=True) + row.label(text=f"{len(props.entities)} entities found") + + if active_entity: + if active_entity.ifc_definition_id == props.editing_entity_id: + row.operator("bim.explorer_disable_editing_entity", icon="CANCEL", text="") + else: + row.operator("bim.explorer_enable_editing_entity", icon="GREASEPENCIL", text="") + # TODO: 'Remove' button? + + layout.template_list("BIM_UL_explorer", "", props, "entities", props, "active_entity_index") + + if props.editing_entity_id: + box = self.layout.box() + row = box.row(align=True) + row.operator("bim.explorer_edit_entity", icon="CHECKMARK") + row.operator("bim.explorer_disable_editing_entity", icon="CANCEL", text="") + bonsai.bim.helper.draw_attributes(props.entity_attributes, box) + + +class BIM_UL_explorer(bpy.types.UIList): + def draw_item( + self, + context: bpy.types.Context, + layout: bpy.types.UILayout, + data: BIMExplorerProperties, + item: ExplorerEntity, + icon, + active_data, + active_propname, + ) -> None: + row = layout.row(align=True) + + if item.ifc_definition_id == data.editing_entity_id: + row.label(text=item.name, icon="GREASEPENCIL") + else: + row.label(text=item.name) diff --git a/src/bonsai/bonsai/core/tool.py b/src/bonsai/bonsai/core/tool.py index a58a7cd8ad..f71183910c 100644 --- a/src/bonsai/bonsai/core/tool.py +++ b/src/bonsai/bonsai/core/tool.py @@ -75,6 +75,11 @@ class Aggregate: def get_relating_object(cls, related_element): pass +@interface +class Attribute: + pass + + @interface class Bcf: pass diff --git a/src/bonsai/bonsai/tool/__init__.py b/src/bonsai/bonsai/tool/__init__.py index 6e3fc32944..8da603f655 100644 --- a/src/bonsai/bonsai/tool/__init__.py +++ b/src/bonsai/bonsai/tool/__init__.py @@ -17,6 +17,7 @@ # along with Bonsai. If not, see . from bonsai.tool.aggregate import Aggregate +from bonsai.tool.attribute import Attribute from bonsai.tool.bcf import Bcf from bonsai.tool.blender import Blender from bonsai.tool.boundary import Boundary diff --git a/src/bonsai/bonsai/tool/attribute.py b/src/bonsai/bonsai/tool/attribute.py new file mode 100644 index 0000000000..26590055b0 --- /dev/null +++ b/src/bonsai/bonsai/tool/attribute.py @@ -0,0 +1,77 @@ +# Bonsai - OpenBIM Blender Add-on +# Copyright (C) 2021 Dion Moult +# +# 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 . + +from __future__ import annotations +import bpy +import bonsai.core.tool +import bonsai.bim.helper as helper +import bonsai.tool as tool +import ifcopenshell +from typing import ( + Any, + Optional, + Union, + Literal, + TypeVar, + TYPE_CHECKING, + assert_never, +) + +if TYPE_CHECKING: + from bonsai.bim.module.attribute.prop import BIMAttributeProperties, BIMExplorerProperties + + T = TypeVar("T") + + +class Attribute(bonsai.core.tool.Attribute): + @classmethod + def get_explorer_props(cls) -> BIMExplorerProperties: + assert (scene := bpy.context.scene) + return scene.BIMExplorerProperties # pyright: ignore[reportAttributeAccessIssue] + + @classmethod + def refresh_uilist_entities(cls) -> None: + props = cls.get_explorer_props() + props.entities.clear() + ifc_file = tool.Ifc.get() + for element in ifc_file.by_type(props.ifc_class): + entity = props.entities.add() + entity.name = str(element) + entity.ifc_definition_id = element.id() + + @classmethod + def enable_editing_entity(cls, entity: ifcopenshell.entity_instance) -> None: + props = cls.get_explorer_props() + props.editing_entity_id = entity.id() + + @classmethod + def disable_editing_entity(cls) -> None: + props = cls.get_explorer_props() + props.entity_attributes.clear() + props.property_unset("editing_entity_id") + + @classmethod + def import_entity_attributes(cls, entity: ifcopenshell.entity_instance) -> None: + props = cls.get_explorer_props() + helper.import_attributes(entity.is_a(), props.entity_attributes, entity.get_info()) + + @classmethod + def export_entity_attributes(cls) -> dict[str, Any]: + props = cls.get_explorer_props() + attributes = helper.export_attributes(props.entity_attributes) + return attributes