From 646f2f2e3289e1913ccee47d2324e1ebefc7e221 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 15 Sep 2025 10:13:59 +0500 Subject: [PATCH] Add tool for 'group' module --- .../bonsai/bim/module/group/operator.py | 66 +++---------- src/bonsai/bonsai/bim/module/group/ui.py | 4 +- src/bonsai/bonsai/bim/module/pset/data.py | 4 +- src/bonsai/bonsai/bim/module/pset/ui.py | 4 +- src/bonsai/bonsai/tool/__init__.py | 1 + src/bonsai/bonsai/tool/blender.py | 8 +- src/bonsai/bonsai/tool/group.py | 94 +++++++++++++++++++ 7 files changed, 116 insertions(+), 65 deletions(-) create mode 100644 src/bonsai/bonsai/tool/group.py diff --git a/src/bonsai/bonsai/bim/module/group/operator.py b/src/bonsai/bonsai/bim/module/group/operator.py index b4bfb44822..ec006196ab 100644 --- a/src/bonsai/bonsai/bim/module/group/operator.py +++ b/src/bonsai/bonsai/bim/module/group/operator.py @@ -23,7 +23,6 @@ import ifcopenshell.util.element import bonsai.bim.helper import bonsai.tool as tool import json -from natsort import natsorted class LoadGroups(bpy.types.Operator, tool.Ifc.Operator): @@ -31,48 +30,12 @@ class LoadGroups(bpy.types.Operator, tool.Ifc.Operator): bl_label = "Load Groups" bl_options = {"REGISTER", "UNDO"} - expanded_groups: list[int] - def _execute(self, context): - self.props = tool.Blender.get_group_props() - self.expanded_groups = json.loads(self.props.expanded_groups_json) - self.props.groups.clear() - - groups = [ - group for group in tool.Ifc.get().by_type("IfcGroup", include_subtypes=False) if not group.HasAssignments - ] - sorted_groups = natsorted(groups, key=lambda group: group.Name or "Unnamed") - - for group in sorted_groups: - self.load_group(group) - - self.props.is_editing = True - bpy.ops.bim.disable_editing_group() + tool.Group.import_groups() + tool.Group.enable_group_editing_ui() + tool.Group.disable_editing_group() return {"FINISHED"} - def load_group(self, group: ifcopenshell.entity_instance, tree_depth: int = 0) -> None: - new = self.props.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 self.expanded_groups - - 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") - ] - sorted_related_groups = natsorted(related_groups, key=lambda group: group.Name or "Unnamed") - - if sorted_related_groups: - new.has_children = True - if new.is_expanded: - for related_group in sorted_related_groups: - self.load_group(related_group, tree_depth=tree_depth + 1) - class ToggleGroup(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.toggle_group" @@ -83,7 +46,7 @@ class ToggleGroup(bpy.types.Operator, tool.Ifc.Operator): option: bpy.props.StringProperty(name="Expand or Collapse") def _execute(self, context): - props = tool.Blender.get_group_props() + props = tool.Group.get_group_props() expanded_groups: set[int] expanded_groups = set(json.loads(props.expanded_groups_json)) if self.option == "Expand": @@ -101,9 +64,8 @@ class DisableGroupEditingUI(bpy.types.Operator, tool.Ifc.Operator): bl_options = {"REGISTER", "UNDO"} def _execute(self, context): - props = tool.Blender.get_group_props() - props.is_editing = False - props.active_group_id = 0 + tool.Group.disable_group_editing_ui() + tool.Group.disable_editing_group() return {"FINISHED"} @@ -125,7 +87,7 @@ class AddGroup(bpy.types.Operator, tool.Ifc.Operator): ifcopenshell.api.group.assign_group( tool.Ifc.get(), products=[result], group=tool.Ifc.get().by_id(self.group) ) - bpy.ops.bim.load_groups() + tool.Group.import_groups("IfcGroup") return {"FINISHED"} @@ -135,11 +97,11 @@ class EditGroup(bpy.types.Operator, tool.Ifc.Operator): bl_options = {"REGISTER", "UNDO"} def _execute(self, context): - props = tool.Blender.get_group_props() + props = tool.Group.get_group_props() attributes = bonsai.bim.helper.export_attributes(props.group_attributes) ifc_file = tool.Ifc.get() ifcopenshell.api.group.edit_group(ifc_file, group=ifc_file.by_id(props.active_group_id), attributes=attributes) - bpy.ops.bim.load_groups() + tool.Group.import_groups("IfcGroup") return {"FINISHED"} @@ -152,7 +114,7 @@ class RemoveGroup(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): self.file = tool.Ifc.get() ifcopenshell.api.group.remove_group(self.file, group=self.file.by_id(self.group)) - bpy.ops.bim.load_groups() + tool.Group.import_groups("IfcGroup") return {"FINISHED"} @@ -163,10 +125,11 @@ class EnableEditingGroup(bpy.types.Operator, tool.Ifc.Operator): group: bpy.props.IntProperty() def _execute(self, context): - props = tool.Blender.get_group_props() + props = tool.Group.get_group_props() props.group_attributes.clear() bonsai.bim.helper.import_attributes(tool.Ifc.get().by_id(self.group), props.group_attributes) - props.active_group_id = self.group + ifc_file = tool.Ifc.get() + tool.Group.set_active_group_to_edit(ifc_file.by_id(self.group)) return {"FINISHED"} @@ -176,8 +139,7 @@ class DisableEditingGroup(bpy.types.Operator, tool.Ifc.Operator): bl_options = {"REGISTER", "UNDO"} def _execute(self, context): - props = tool.Blender.get_group_props() - props.active_group_id = 0 + tool.Group.disable_editing_group() return {"FINISHED"} diff --git a/src/bonsai/bonsai/bim/module/group/ui.py b/src/bonsai/bonsai/bim/module/group/ui.py index 926fe76da5..097b581582 100644 --- a/src/bonsai/bonsai/bim/module/group/ui.py +++ b/src/bonsai/bonsai/bim/module/group/ui.py @@ -44,7 +44,7 @@ class BIM_PT_groups(Panel): def draw(self, context): if not GroupsData.is_loaded: GroupsData.load() - self.props = tool.Blender.get_group_props() + self.props = tool.Group.get_group_props() assert self.layout row = self.layout.row(align=True) @@ -106,7 +106,7 @@ class BIM_PT_object_groups(Panel): if not ObjectGroupsData.is_loaded: ObjectGroupsData.load() assert self.layout - self.props = tool.Blender.get_group_props() + self.props = tool.Group.get_group_props() for group in ObjectGroupsData.data["groups"]: row = self.layout.row(align=True) diff --git a/src/bonsai/bonsai/bim/module/pset/data.py b/src/bonsai/bonsai/bim/module/pset/data.py index 2a12d0951b..12b38efc98 100644 --- a/src/bonsai/bonsai/bim/module/pset/data.py +++ b/src/bonsai/bonsai/bim/module/pset/data.py @@ -269,7 +269,7 @@ class GroupQtosData(Data): @classmethod def load(cls): - props = tool.Blender.get_group_props() + props = tool.Group.get_group_props() assert (active_group := props.active_group) ifc_definition_id = active_group.ifc_definition_id cls.data = {"qtos": cls.psetqtos(tool.Ifc.get_entity_by_id(ifc_definition_id), qtos_only=True)} @@ -282,7 +282,7 @@ class GroupPsetData(Data): @classmethod def load(cls): - props = tool.Blender.get_group_props() + props = tool.Group.get_group_props() assert (active_group := props.active_group) ifc_definition_id = active_group.ifc_definition_id cls.data = {"psets": cls.psetqtos(tool.Ifc.get_entity_by_id(ifc_definition_id), psets_only=True)} diff --git a/src/bonsai/bonsai/bim/module/pset/ui.py b/src/bonsai/bonsai/bim/module/pset/ui.py index 843991e517..ab11c5f1ce 100644 --- a/src/bonsai/bonsai/bim/module/pset/ui.py +++ b/src/bonsai/bonsai/bim/module/pset/ui.py @@ -667,7 +667,7 @@ class BIM_PT_group_qtos(Panel): @classmethod def poll(cls, context): - props = tool.Blender.get_group_props() + props = tool.Group.get_group_props() return bool(props.active_group) def draw(self, context): @@ -699,7 +699,7 @@ class BIM_PT_group_psets(Panel): @classmethod def poll(cls, context): - props = tool.Blender.get_group_props() + props = tool.Group.get_group_props() return bool(props.active_group) def draw(self, context): diff --git a/src/bonsai/bonsai/tool/__init__.py b/src/bonsai/bonsai/tool/__init__.py index 8da603f655..8dae934981 100644 --- a/src/bonsai/bonsai/tool/__init__.py +++ b/src/bonsai/bonsai/tool/__init__.py @@ -37,6 +37,7 @@ from bonsai.tool.drawing import Drawing from bonsai.tool.feature import Feature from bonsai.tool.geometry import Geometry from bonsai.tool.georeference import Georeference +from bonsai.tool.group import Group from bonsai.tool.ifc import Ifc from bonsai.tool.ifcgit import IfcGit from bonsai.tool.ifcgit import IfcGitRepo diff --git a/src/bonsai/bonsai/tool/blender.py b/src/bonsai/bonsai/tool/blender.py index 2833467a0a..48cbe60cb7 100644 --- a/src/bonsai/bonsai/tool/blender.py +++ b/src/bonsai/bonsai/tool/blender.py @@ -63,7 +63,6 @@ if TYPE_CHECKING: from bonsai.bim.module.csv.prop import CsvProperties from bonsai.bim.module.diff.prop import DiffProperties from bonsai.bim.module.fm.prop import BIMFMProperties - from bonsai.bim.module.group.prop import BIMGroupProperties from bonsai.bim.module.light.prop import BIMSolarProperties, RadianceExporterProperties T = TypeVar("T") @@ -260,7 +259,7 @@ class Blender(bonsai.core.tool.Blender): wsprops = tool.Sequence.get_work_schedule_props() return wsprops.active_work_schedule_id elif obj_type == "Group": - props = tool.Blender.get_group_props() + props = tool.Group.get_group_props() assert (active_group := props.active_group) return active_group.ifc_definition_id elif obj_type == "Zone": @@ -1778,11 +1777,6 @@ class Blender(bonsai.core.tool.Blender): assert (scene := bpy.context.scene) return scene.DiffProperties # pyright: ignore[reportAttributeAccessIssue] - @classmethod - def get_group_props(cls) -> BIMGroupProperties: - assert (scene := bpy.context.scene) - return scene.BIMGroupProperties # pyright: ignore[reportAttributeAccessIssue] - @classmethod def get_bim_props(cls, scene: Optional[bpy.types.Scene] = None) -> BIMProperties: if scene is None: diff --git a/src/bonsai/bonsai/tool/group.py b/src/bonsai/bonsai/tool/group.py new file mode 100644 index 0000000000..22bc589b62 --- /dev/null +++ b/src/bonsai/bonsai/tool/group.py @@ -0,0 +1,94 @@ +# Bonsai - OpenBIM Blender Add-on +# Copyright (C) 2022 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 json +import bpy +import ifcopenshell +import bonsai.bim.helper +import bonsai.core.tool +import bonsai.tool as tool +from typing import TYPE_CHECKING +from natsort import natsorted + +if TYPE_CHECKING: + from bonsai.bim.module.group.prop import BIMGroupProperties + + +class Group(bonsai.core.tool.System): + @classmethod + def get_group_props(cls) -> BIMGroupProperties: + 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() + + groups = [ + group for group in tool.Ifc.get().by_type("IfcGroup", include_subtypes=False) if not group.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.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 + + 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") + ] + sorted_related_groups = natsorted(related_groups, key=lambda group: group.Name or "Unnamed") + + if sorted_related_groups: + new.has_children = True + if new.is_expanded: + for related_group in sorted_related_groups: + load_group(related_group, tree_depth=tree_depth + 1) + + for group in sorted_groups: + load_group(group) + + @classmethod + def enable_group_editing_ui(cls) -> None: + props = cls.get_group_props() + props.is_editing = True + + @classmethod + def disable_group_editing_ui(cls) -> None: + props = cls.get_group_props() + props.is_editing = False + + @classmethod + def disable_editing_group(cls) -> None: + props = cls.get_group_props() + props.active_group_id = 0 + + @classmethod + def set_active_group_to_edit(cls, group: ifcopenshell.entity_instance) -> None: + props = cls.get_group_props() + props.active_group_id = group.id()