From 4c1af9e2f5af7b78dff08b371cf0193fee0fbf81 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 15 Sep 2025 10:02:32 +0500 Subject: [PATCH] Systems UI - display hierarchy of systems #7062 --- .../bonsai/bim/module/group/operator.py | 31 +++++---- src/bonsai/bonsai/bim/module/group/ui.py | 4 +- src/bonsai/bonsai/bim/module/system/prop.py | 9 +++ src/bonsai/bonsai/bim/module/system/ui.py | 14 +++- src/bonsai/bonsai/tool/group.py | 69 +++++++++++++++---- src/bonsai/bonsai/tool/system.py | 11 +-- 6 files changed, 100 insertions(+), 38 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/group/operator.py b/src/bonsai/bonsai/bim/module/group/operator.py index ec006196ab..a146cb24ea 100644 --- a/src/bonsai/bonsai/bim/module/group/operator.py +++ b/src/bonsai/bonsai/bim/module/group/operator.py @@ -22,7 +22,7 @@ import ifcopenshell.api.group import ifcopenshell.util.element import bonsai.bim.helper import bonsai.tool as tool -import json +from typing import TYPE_CHECKING, Literal, get_args class LoadGroups(bpy.types.Operator, tool.Ifc.Operator): @@ -31,7 +31,7 @@ class LoadGroups(bpy.types.Operator, tool.Ifc.Operator): bl_options = {"REGISTER", "UNDO"} def _execute(self, context): - tool.Group.import_groups() + tool.Group.import_groups("IfcGroup") tool.Group.enable_group_editing_ui() tool.Group.disable_editing_group() return {"FINISHED"} @@ -41,20 +41,23 @@ class ToggleGroup(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.toggle_group" bl_label = "Toggle Group" bl_options = {"REGISTER", "UNDO"} - ifc_definition_id: bpy.props.IntProperty() - index: bpy.props.IntProperty() - option: bpy.props.StringProperty(name="Expand or Collapse") + + ifc_definition_id: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration] + group_type: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration] + items=[(i, i, "") for i in get_args(tool.Group.GroupType)], + ) + option: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration] + items=[(i, i, "") for i in get_args(tool.Group.ToggleOption)], + ) + + if TYPE_CHECKING: + ifc_definition_id: int + group_type: tool.Group.GroupType + option: tool.Group.ToggleOption def _execute(self, context): - props = tool.Group.get_group_props() - expanded_groups: set[int] - expanded_groups = set(json.loads(props.expanded_groups_json)) - if self.option == "Expand": - expanded_groups.add(self.ifc_definition_id) - elif self.ifc_definition_id in expanded_groups: - expanded_groups.remove(self.ifc_definition_id) - props.expanded_groups_json = json.dumps(list(expanded_groups)) - bpy.ops.bim.load_groups() + group = tool.Ifc.get().by_id(self.ifc_definition_id) + tool.Group.toggle_group(group, self.group_type, self.option) return {"FINISHED"} diff --git a/src/bonsai/bonsai/bim/module/group/ui.py b/src/bonsai/bonsai/bim/module/group/ui.py index 097b581582..ac437e9c42 100644 --- a/src/bonsai/bonsai/bim/module/group/ui.py +++ b/src/bonsai/bonsai/bim/module/group/ui.py @@ -139,7 +139,7 @@ class BIM_UL_groups(UIList): op = row.operator( "bim.toggle_group", icon="TRIA_DOWN" if item.is_expanded else "TRIA_RIGHT", text="", emboss=False ) + op.group_type = "IfcGroup" op.ifc_definition_id = item.ifc_definition_id - op.index = index - op.option = "Collapse" if item.is_expanded else "Expand" + op.option = "COLLAPSE" if item.is_expanded else "EXPAND" row.prop(item, "name", text="", emboss=False) diff --git a/src/bonsai/bonsai/bim/module/system/prop.py b/src/bonsai/bonsai/bim/module/system/prop.py index e51b98fa96..796bc932cd 100644 --- a/src/bonsai/bonsai/bim/module/system/prop.py +++ b/src/bonsai/bonsai/bim/module/system/prop.py @@ -54,11 +54,17 @@ class System(PropertyGroup): name: StringProperty(name="Name", update=update_system_name) ifc_class: StringProperty(name="IFC Class") ifc_definition_id: IntProperty(name="IFC Definition ID") + has_children: BoolProperty(name="Has Children") + tree_depth: IntProperty(name="Tree Depth") + is_expanded: BoolProperty(name="Is Expanded") if TYPE_CHECKING: name: str ifc_class: str ifc_definition_id: int + has_children: bool + tree_depth: int + is_expanded: bool def update_zone_name(self: "Zone", context: bpy.types.Context) -> None: @@ -91,6 +97,8 @@ class BIMSystemProperties(PropertyGroup): is_editing: BoolProperty(name="Is Editing", default=False) is_adding: BoolProperty(name="Is Adding", default=False) systems: CollectionProperty(name="Systems", type=System) + expanded_groups_json: StringProperty(name="Expanded Systems JSON", default="[]") + """See `expanded_groups_json`. We name it "groups" for compatibility with Group UI code.""" active_system_index: IntProperty(name="Active System Index") active_system_id: IntProperty(name="Active System Id") edited_system_id: IntProperty(name="Edited System Id") @@ -104,6 +112,7 @@ class BIMSystemProperties(PropertyGroup): is_editing: bool is_adding: bool systems: bpy.types.bpy_prop_collection_idprop[System] + expanded_groups_json: str active_system_index: int active_system_id: int edited_system_id: int diff --git a/src/bonsai/bonsai/bim/module/system/ui.py b/src/bonsai/bonsai/bim/module/system/ui.py index 03b4be2af5..fc5d1cf3ea 100644 --- a/src/bonsai/bonsai/bim/module/system/ui.py +++ b/src/bonsai/bonsai/bim/module/system/ui.py @@ -428,10 +428,22 @@ class BIM_UL_systems(UIList): icon, active_data, active_propname, - ): + ) -> None: if item: row = layout.row(align=True) + for i in range(0, item.tree_depth): + row.label(text="", icon="BLANK1") system_id = item.ifc_definition_id + if item.has_children: + op = row.operator( + "bim.toggle_group", + text="", + icon="TRIA_DOWN" if item.is_expanded else "TRIA_RIGHT", + emboss=False, + ) + op.group_type = "IfcSystem" + op.ifc_definition_id = item.ifc_definition_id + op.option = "COLLAPSE" if item.is_expanded else "EXPAND" if data.edited_system_id == system_id: row.label(text="", icon="GREASEPENCIL") row.prop(item, "name", text="", icon=SYSTEM_ICONS[item.ifc_class], emboss=False) diff --git a/src/bonsai/bonsai/tool/group.py b/src/bonsai/bonsai/tool/group.py index 22bc589b62..46b3a5fb57 100644 --- a/src/bonsai/bonsai/tool/group.py +++ b/src/bonsai/bonsai/tool/group.py @@ -23,11 +23,12 @@ import ifcopenshell import bonsai.bim.helper import bonsai.core.tool import bonsai.tool as tool -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Literal, assert_never, Union from natsort import natsorted if TYPE_CHECKING: - from bonsai.bim.module.group.prop import BIMGroupProperties + from bonsai.bim.module.group.prop import BIMGroupProperties, Group as GroupProp + from bonsai.bim.module.system.prop import BIMSystemProperties, System class Group(bonsai.core.tool.System): @@ -36,31 +37,60 @@ class Group(bonsai.core.tool.System): assert (scene := bpy.context.scene) return scene.BIMGroupProperties # pyright: ignore[reportAttributeAccessIssue] - @classmethod - def import_groups(cls) -> None: - props = tool.Group.get_group_props() - expanded_groups: list[int] = json.loads(props.expanded_groups_json) - props.groups.clear() + GroupType = Literal["IfcGroup", "IfcSystem"] - groups = [ - group for group in tool.Ifc.get().by_type("IfcGroup", include_subtypes=False) if not group.HasAssignments - ] + @classmethod + def get_groups_data(cls, group_type: GroupType) -> Union[ + tuple[BIMGroupProperties, bpy.types.bpy_prop_collection_idprop[GroupProp]], + tuple[BIMSystemProperties, bpy.types.bpy_prop_collection_idprop[System]], + ]: + if group_type == "IfcGroup": + props = tool.Group.get_group_props() + blender_groups = props.groups + return props, blender_groups + elif group_type == "IfcSystem": + props = tool.System.get_system_props() + blender_groups = props.systems + return props, blender_groups + else: + assert_never(group_type) + + @classmethod + def import_groups(cls, group_type: GroupType) -> None: + from bonsai.bim.module.system.prop import System + + ifc_file = tool.Ifc.get() + props, blender_groups = cls.get_groups_data(group_type) + if group_type == "IfcGroup": + base_groups = ifc_file.by_type("IfcGroup", include_subtypes=False) + elif group_type == "IfcSystem": + base_groups = [g for g in tool.System.get_systems() if not g.is_a("IfcStructuralAnalysisModel")] + else: + assert_never(group_type) + + expanded_groups_json = props.expanded_groups_json + expanded_groups: list[int] = json.loads(expanded_groups_json) + blender_groups.clear() + + groups = [g for g in base_groups if not g.HasAssignments] sorted_groups = natsorted(groups, key=lambda group: group.Name or "Unnamed") def load_group(group: ifcopenshell.entity_instance, tree_depth: int = 0) -> None: - new = props.groups.add() + new = blender_groups.add() new.ifc_definition_id = group.id() new["name"] = group.Name or "Unnamed" new.tree_depth = tree_depth new.has_children = False new.is_expanded = group.id() in expanded_groups + if isinstance(new, System): + new.ifc_class = group.is_a() related_groups: list[ifcopenshell.entity_instance] related_groups = [ related_object for rel in group.IsGroupedBy or [] for related_object in rel.RelatedObjects - if related_object.is_a("IfcGroup") + if related_object.is_a(group_type) ] sorted_related_groups = natsorted(related_groups, key=lambda group: group.Name or "Unnamed") @@ -92,3 +122,18 @@ class Group(bonsai.core.tool.System): def set_active_group_to_edit(cls, group: ifcopenshell.entity_instance) -> None: props = cls.get_group_props() props.active_group_id = group.id() + + ToggleOption = Literal["EXPAND", "COLLAPSE"] + + @classmethod + def toggle_group(cls, group: ifcopenshell.entity_instance, group_type: GroupType, option: ToggleOption) -> None: + props, _ = cls.get_groups_data(group_type) + expanded_groups: set[int] + expanded_groups = set(json.loads(props.expanded_groups_json)) + ifc_definition_id = group.id() + if option == "EXPAND": + expanded_groups.add(ifc_definition_id) + elif ifc_definition_id in expanded_groups: + expanded_groups.remove(ifc_definition_id) + props.expanded_groups_json = json.dumps(list(expanded_groups)) + cls.import_groups(group_type) diff --git a/src/bonsai/bonsai/tool/system.py b/src/bonsai/bonsai/tool/system.py index 41ad127663..154d030384 100644 --- a/src/bonsai/bonsai/tool/system.py +++ b/src/bonsai/bonsai/tool/system.py @@ -33,6 +33,7 @@ from mathutils import Matrix, Vector from bonsai.bim.module.system.data import ObjectSystemData, SystemDecorationData from enum import Enum from typing import TYPE_CHECKING, Optional, Any, Union +from natsort import natsorted if TYPE_CHECKING: from bonsai.bim.module.system.prop import BIMSystemProperties, BIMZoneProperties, BIMZoneProperties @@ -160,15 +161,7 @@ class System(bonsai.core.tool.System): @classmethod def import_systems(cls) -> None: - props = cls.get_system_props() - props.systems.clear() - for system in cls.get_systems(): - if system.is_a() in ["IfcStructuralAnalysisModel"]: - continue - new = props.systems.add() - new.ifc_definition_id = system.id() - new["name"] = system.Name or "Unnamed" - new.ifc_class = system.is_a() + tool.Group.import_groups("IfcSystem") @classmethod def load_ports(cls, element: ifcopenshell.entity_instance, ports: list[ifcopenshell.entity_instance]) -> None: