diff --git a/src/blenderbim/blenderbim/bim/module/group/data.py b/src/blenderbim/blenderbim/bim/module/group/data.py new file mode 100644 index 0000000000..5ad1387e92 --- /dev/null +++ b/src/blenderbim/blenderbim/bim/module/group/data.py @@ -0,0 +1,67 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2023 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on 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. +# +# BlenderBIM Add-on 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 BlenderBIM Add-on. If not, see . + +import bpy +import ifcopenshell +import ifcopenshell.util.cost +import ifcopenshell.util.element +import blenderbim.tool as tool + + +def refresh(): + GroupsData.is_loaded = False + ObjectGroupsData.is_loaded = False + + +class GroupsData: + data = {} + is_loaded = False + + @classmethod + def load(cls): + cls.data = {"total_groups": cls.total_groups()} + cls.is_loaded = True + + @classmethod + def total_groups(cls): + return len(tool.Ifc.get().by_type("IfcGroup", include_subtypes=False)) + + +class ObjectGroupsData: + data = {} + is_loaded = False + + @classmethod + def load(cls): + cls.data = {"total_groups": cls.total_groups(), "groups": cls.groups()} + cls.is_loaded = True + + @classmethod + def total_groups(cls): + return len(tool.Ifc.get().by_type("IfcGroup", include_subtypes=False)) + + @classmethod + def groups(cls): + element = tool.Ifc.get_entity(bpy.context.active_object) + if not element: + return [] + results = [] + for rel in getattr(element, "HasAssignments", []) or []: + if rel.is_a("IfcRelAssignsToGroup"): + results.append({"id": rel.RelatingGroup.id(), "name": rel.RelatingGroup.Name or "Unnamed"}) + return results diff --git a/src/blenderbim/blenderbim/bim/module/group/operator.py b/src/blenderbim/blenderbim/bim/module/group/operator.py index 70a5eb927e..2e04c960fa 100644 --- a/src/blenderbim/blenderbim/bim/module/group/operator.py +++ b/src/blenderbim/blenderbim/bim/module/group/operator.py @@ -22,71 +22,48 @@ import ifcopenshell.api import blenderbim.bim.helper import blenderbim.tool as tool from blenderbim.bim.ifc import IfcStore -from ifcopenshell.api.group.data import Data from ifcopenshell.util.selector import Selector import json -class LoadGroups(bpy.types.Operator): +class LoadGroups(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.load_groups" bl_label = "Load Groups" bl_options = {"REGISTER", "UNDO"} - is_refresh: bpy.props.BoolProperty(default=False) - def execute(self, context): + def _execute(self, context): self.props = context.scene.BIMGroupProperties self.expanded_groups = json.loads(context.scene.ExpandedGroups.json_string) self.props.groups.clear() - self.ifc = IfcStore.get_file() - if not self.is_refresh: - context.scene.ExpandedGroups.json_string = "{}" - for ifc_definition_id, group in Data.groups.items(): - if not group["HasAssignments"]: - new = self.props.groups.add() - new.ifc_definition_id = ifc_definition_id - new.name = group["Name"] - new.selection_query = group["Description"].split("*selector*")[1] if group["Description"] else "" - new.tree_depth = 0 - - if group["IsGroupedBy"]: - # assumes 1:1 cardinality, will need to be updated to reflect IFC4 changes - # where the cardinality is 0:? - vulevukusej - sub_groups = [g for g in group["IsGroupedBy"][0].RelatedObjects if g.is_a("IfcGroup")] - new.has_children = True if len(sub_groups) != 0 else False - - if self.is_refresh: - self.recursively_load_sub_groups(ifc_definition_id, new) + for group in tool.Ifc.get().by_type("IfcGroup", include_subtypes=False): + if not group.HasAssignments: + self.load_group(group) self.props.is_editing = True bpy.ops.bim.disable_editing_group() - Data.load(IfcStore.get_file()) return {"FINISHED"} - def recursively_load_sub_groups(self, ifc_definition_id, parent): - sub_groups = [ - k for k, v in Data.products.items() if self.ifc.by_id(k).is_a("IfcGroup") and ifc_definition_id in v - ] - if str(ifc_definition_id) not in self.expanded_groups or sub_groups == []: - return - else: - parent.is_expanded = True - parent.has_children = True - for group in sub_groups: - new = self.props.groups.add() - new.ifc_definition_id = group - new.name = Data.groups[group]["Name"] - new.selection_query = ( - Data.groups[group]["Description"].split("*selector*")[1] - if Data.groups[group]["Description"] - else "" - ) - new.has_children = True if len(Data.groups[group]["IsGroupedBy"]) != 0 else False - new.tree_depth = parent.tree_depth + 1 - self.recursively_load_sub_groups(new.ifc_definition_id, new) + def load_group(self, group, tree_depth=0): + new = self.props.groups.add() + new.ifc_definition_id = group.id() + new.name = group.Name or "Unnamed" + new.selection_query = group.Description.split("*selector*")[1] if group.Description else "" + new.tree_depth = tree_depth + new.has_children = False + new.is_expanded = group.id() in self.expanded_groups + + for rel in group.IsGroupedBy or []: + for related_object in rel.RelatedObjects: + if not related_object.is_a("IfcGroup"): + continue + new.has_children = True + if not new.is_expanded: + return + self.load_group(related_object, tree_depth=tree_depth + 1) -class ToggleGroup(bpy.types.Operator): +class ToggleGroup(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.toggle_group" bl_label = "Toggle Group" bl_options = {"REGISTER", "UNDO"} @@ -94,58 +71,50 @@ class ToggleGroup(bpy.types.Operator): index: bpy.props.IntProperty() option: bpy.props.StringProperty(name="Expand or Collapse") - def execute(self, context): - context.scene.BIMGroupProperties.groups[self.index].is_expanded = True if self.option == "Expand" else False - json_string = json.loads(context.scene.ExpandedGroups.json_string) + def _execute(self, context): + expanded_groups = set(json.loads(context.scene.ExpandedGroups.json_string)) if self.option == "Expand": - json_string.setdefault(str(self.ifc_definition_id), None) - else: - json_string.pop(str(self.ifc_definition_id), None) - context.scene.ExpandedGroups.json_string = json.dumps(json_string) - bpy.ops.bim.load_groups(is_refresh=True) + expanded_groups.add(self.ifc_definition_id) + elif self.ifc_definition_id in expanded_groups: + expanded_groups.remove(self.ifc_definition_id) + context.scene.ExpandedGroups.json_string = json.dumps(list(expanded_groups)) + bpy.ops.bim.load_groups() return {"FINISHED"} -class DisableGroupEditingUI(bpy.types.Operator): +class DisableGroupEditingUI(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.disable_group_editing_ui" bl_label = "Disable Group Editing UI" bl_options = {"REGISTER", "UNDO"} - def execute(self, context): + def _execute(self, context): context.scene.BIMGroupProperties.is_editing = False context.scene.BIMGroupProperties.active_group_id = 0 return {"FINISHED"} -class AddGroup(bpy.types.Operator): +class AddGroup(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.add_group" bl_label = "Add New Group" bl_options = {"REGISTER", "UNDO"} group: bpy.props.IntProperty() - def execute(self, context): - return IfcStore.execute_ifc_operator(self, context) - def _execute(self, context): result = ifcopenshell.api.run("group.add_group", tool.Ifc.get()) if self.group: ifcopenshell.api.run( "group.assign_group", tool.Ifc.get(), products=[result], group=tool.Ifc.get().by_id(self.group) ) - Data.load(tool.Ifc.get()) - bpy.ops.bim.load_groups(is_refresh=True) + bpy.ops.bim.load_groups() return {"FINISHED"} -class EditGroup(bpy.types.Operator): +class EditGroup(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.edit_group" bl_label = "Edit Group" bl_options = {"REGISTER", "UNDO"} copy_from_selector: bpy.props.BoolProperty(name="Copy from Selector", default=False) - def execute(self, context): - return IfcStore.execute_ifc_operator(self, context) - def _execute(self, context): props = context.scene.BIMGroupProperties attributes = {} @@ -162,73 +131,65 @@ class EditGroup(bpy.types.Operator): ifcopenshell.api.run( "group.edit_group", self.file, **{"group": self.file.by_id(props.active_group_id), "attributes": attributes} ) - Data.load(IfcStore.get_file()) - bpy.ops.bim.load_groups(is_refresh=True) + bpy.ops.bim.load_groups() return {"FINISHED"} -class RemoveGroup(bpy.types.Operator): +class RemoveGroup(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.remove_group" bl_label = "Remove Group" bl_options = {"REGISTER", "UNDO"} group: bpy.props.IntProperty() - def execute(self, context): - return IfcStore.execute_ifc_operator(self, context) - def _execute(self, context): props = context.scene.BIMGroupProperties self.file = IfcStore.get_file() ifcopenshell.api.run("group.remove_group", self.file, **{"group": self.file.by_id(self.group)}) - Data.load(IfcStore.get_file()) - bpy.ops.bim.load_groups(is_refresh=True) + bpy.ops.bim.load_groups() return {"FINISHED"} -class EnableEditingGroup(bpy.types.Operator): +class EnableEditingGroup(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.enable_editing_group" bl_label = "Enable Editing Group" bl_options = {"REGISTER", "UNDO"} group: bpy.props.IntProperty() - def execute(self, context): + def _execute(self, context): props = context.scene.BIMGroupProperties props.group_attributes.clear() - blenderbim.bim.helper.import_attributes("IfcGroup", props.group_attributes, Data.groups[self.group]) + blenderbim.bim.helper.import_attributes2(tool.Ifc.get().by_id(self.group), props.group_attributes) props.active_group_id = self.group return {"FINISHED"} -class DisableEditingGroup(bpy.types.Operator): +class DisableEditingGroup(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.disable_editing_group" bl_label = "Disable Editing Group" bl_options = {"REGISTER", "UNDO"} - def execute(self, context): + def _execute(self, context): context.scene.BIMGroupProperties.active_group_id = 0 return {"FINISHED"} -class ToggleAssigningGroup(bpy.types.Operator): +class ToggleAssigningGroup(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.toggle_assigning_group" bl_label = "Toggle Assigning Group" bl_options = {"REGISTER", "UNDO"} - def execute(self, context): + def _execute(self, context): context.scene.BIMGroupProperties.is_adding = not context.scene.BIMGroupProperties.is_adding return {"FINISHED"} -class AssignGroup(bpy.types.Operator): +class AssignGroup(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.assign_group" bl_label = "Assign Group" bl_options = {"REGISTER", "UNDO"} product: bpy.props.StringProperty() group: bpy.props.IntProperty() - def execute(self, context): - return IfcStore.execute_ifc_operator(self, context) - def _execute(self, context): self.file = IfcStore.get_file() products = [bpy.data.objects.get(self.product)] if self.product else context.selected_objects @@ -241,20 +202,16 @@ class AssignGroup(bpy.types.Operator): products=[self.file.by_id(product.BIMObjectProperties.ifc_definition_id)], group=self.file.by_id(self.group), ) - Data.load(self.file) return {"FINISHED"} -class UnassignGroup(bpy.types.Operator): +class UnassignGroup(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.unassign_group" bl_label = "Unassign Group" bl_options = {"REGISTER", "UNDO"} product: bpy.props.StringProperty() group: bpy.props.IntProperty() - def execute(self, context): - return IfcStore.execute_ifc_operator(self, context) - def _execute(self, context): self.file = IfcStore.get_file() products = [bpy.data.objects.get(self.product)] if self.product else context.selected_objects @@ -269,38 +226,39 @@ class UnassignGroup(bpy.types.Operator): "group": self.file.by_id(self.group), } ) - Data.load(IfcStore.get_file()) return {"FINISHED"} -class SelectGroupProducts(bpy.types.Operator): +class SelectGroupProducts(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.select_group_products" bl_label = "Select Group Products" bl_options = {"REGISTER", "UNDO"} group: bpy.props.IntProperty() - def execute(self, context): + def _execute(self, context): self.file = IfcStore.get_file() for obj in context.visible_objects: obj.select_set(False) - if not obj.BIMObjectProperties.ifc_definition_id: + element = tool.Ifc.get_entity(obj) + if not element: continue - product_groups = Data.products.get(obj.BIMObjectProperties.ifc_definition_id, []) + product_groups = [ + r.RelatingGroup.id() + for r in getattr(element, "HasAssignments", []) or [] + if r.is_a("IfcRelAssignsToGroup") + ] if self.group in product_groups: obj.select_set(True) return {"FINISHED"} -class UpdateGroup(bpy.types.Operator): +class UpdateGroup(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.update_group" bl_label = "Update Group" bl_options = {"REGISTER", "UNDO"} query: bpy.props.StringProperty() group_id: bpy.props.IntProperty() - def execute(self, context): - return IfcStore.execute_ifc_operator(self, context) - def _execute(self, context): self.file = IfcStore.get_file() group = self.file.by_id(self.group_id) @@ -315,6 +273,5 @@ class UpdateGroup(bpy.types.Operator): "products": new_products, } ) - Data.load(IfcStore.get_file()) - bpy.ops.bim.load_groups(is_refresh=True) + bpy.ops.bim.load_groups() return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/group/prop.py b/src/blenderbim/blenderbim/bim/module/group/prop.py index 9a6ef31958..1ac4d85738 100644 --- a/src/blenderbim/blenderbim/bim/module/group/prop.py +++ b/src/blenderbim/blenderbim/bim/module/group/prop.py @@ -19,7 +19,6 @@ import bpy from blenderbim.bim.prop import StrProperty, Attribute from blenderbim.bim.helper import import_attributes -from ifcopenshell.api.group.data import Data from bpy.types import PropertyGroup import json from bpy.props import ( @@ -35,7 +34,7 @@ from bpy.props import ( class ExpandedGroups(StrProperty): - json_string: StringProperty(name="JSON String", default="{}") + json_string: StringProperty(name="JSON String", default="[]") class Group(PropertyGroup): diff --git a/src/blenderbim/blenderbim/bim/module/group/ui.py b/src/blenderbim/blenderbim/bim/module/group/ui.py index 0f293e54b8..5dd18553ee 100644 --- a/src/blenderbim/blenderbim/bim/module/group/ui.py +++ b/src/blenderbim/blenderbim/bim/module/group/ui.py @@ -18,7 +18,7 @@ from bpy.types import Panel, UIList from blenderbim.bim.ifc import IfcStore -from ifcopenshell.api.group.data import Data +from blenderbim.bim.module.group.data import GroupsData, ObjectGroupsData class BIM_PT_groups(Panel): @@ -35,12 +35,12 @@ class BIM_PT_groups(Panel): return IfcStore.get_file() def draw(self, context): - if not Data.is_loaded: - Data.load(IfcStore.get_file()) + if not GroupsData.is_loaded: + GroupsData.load() self.props = context.scene.BIMGroupProperties row = self.layout.row(align=True) - row.label(text=f"{len(Data.groups)} Groups Found", icon="OUTLINER") + row.label(text=f"{GroupsData.data['total_groups']} Groups Found", icon="OUTLINER") if self.props.is_editing: row.operator("bim.add_group", text="", icon="ADD").group = 0 row.operator("bim.disable_group_editing_ui", text="", icon="CANCEL") @@ -84,8 +84,8 @@ class BIM_PT_object_groups(Panel): return IfcStore.get_file() and context.active_object.BIMObjectProperties.ifc_definition_id def draw(self, context): - if not Data.is_loaded: - Data.load(IfcStore.get_file()) + if not ObjectGroupsData.is_loaded: + ObjectGroupsData.load() self.props = context.scene.BIMGroupProperties row = self.layout.row(align=True) if self.props.is_adding: @@ -100,15 +100,14 @@ class BIM_PT_object_groups(Panel): "active_group_index", ) else: - row.label(text=f"{len(Data.groups)} Groups in IFC Project", icon="OUTLINER") + row.label(text=f"{ObjectGroupsData.data['total_groups']} Groups in IFC Project", icon="OUTLINER") row.operator("bim.toggle_assigning_group", text="", icon="ADD") - groups_object = Data.products.get(context.active_object.BIMObjectProperties.ifc_definition_id, []) - for group_id in groups_object: + for group in ObjectGroupsData.data['groups']: row = self.layout.row(align=True) - row.label(text=Data.groups[group_id].get("Name", "Unnamed")) + row.label(text=group["name"]) op = row.operator("bim.unassign_group", text="", icon="X") - op.group = group_id + op.group = group["id"] if not groups_object: self.layout.label(text="No Group associated with Active Object")