See #1848. Refactor group data class. Add support for multiple subgroup relationships.

This commit is contained in:
Dion Moult
2023-01-31 15:35:14 +11:00
parent 95e924a5bc
commit 7e0b1b9cbd
4 changed files with 136 additions and 114 deletions
@@ -0,0 +1,67 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2023 Dion Moult <dion@thinkmoult.com>
#
# 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 <http://www.gnu.org/licenses/>.
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
@@ -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"}
@@ -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):
@@ -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")