From 098ab2c237f92bb7e510e3aded622200e6d5eec7 Mon Sep 17 00:00:00 2001 From: Gorgious56 Date: Thu, 3 Feb 2022 07:16:43 +0100 Subject: [PATCH] Spatial Elements Utilities and QOL (#2008) * Tool to add storey to specific building * Operator to add new storey to building * Add new storey to building button in aggregates panel * Minor fix * add method to data class to retrieve ifc class name * Extract aggregate creation logic * Prevent aggregating non-ifc elements + display popup info * Add tooltips + rename operators * Rename function to get relating object * Rename relating object data variable to be able to have access to related objects * Add utilities to get related objects * Add utility function to get related objects from element * display related object in Aggregates panel in object properties * Revert "Rename function to get relating object" This reverts commit 5248627a410eed232781e8835342870c27778119. * Update method names * Add Operator to select parts of an element * Ditch box layout and only display number of parts * Add operator to select aggregate of an element * Display operator to select aggregate of element * Move logic to aggregate module * Add utility methods to Blender tool * Add buttons to create Ifcsite and IfcBuilding from object panel --- .../bim/module/aggregate/__init__.py | 13 ++- .../blenderbim/bim/module/aggregate/data.py | 40 ++++++-- .../bim/module/aggregate/operator.py | 97 ++++++++++++++++--- .../blenderbim/bim/module/aggregate/prop.py | 13 ++- .../blenderbim/bim/module/aggregate/ui.py | 33 ++++++- src/blenderbim/blenderbim/core/aggregate.py | 6 ++ src/blenderbim/blenderbim/tool/blender.py | 22 ++++- src/blenderbim/blenderbim/tool/spatial.py | 2 +- .../ifcopenshell/util/element.py | 5 + 9 files changed, 200 insertions(+), 31 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/aggregate/__init__.py b/src/blenderbim/blenderbim/bim/module/aggregate/__init__.py index 9f347356d0..b7b1d6d827 100644 --- a/src/blenderbim/blenderbim/bim/module/aggregate/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/aggregate/__init__.py @@ -20,11 +20,14 @@ import bpy from . import ui, prop, operator classes = ( - operator.AssignObject, - operator.UnassignObject, - operator.EnableEditingAggregate, - operator.DisableEditingAggregate, - operator.AddAggregate, + operator.BIM_OT_assign_object, + operator.BIM_OT_unassign_object, + operator.BIM_OT_enable_editing_aggregate, + operator.BIM_OT_disable_editing_aggregate, + operator.BIM_OT_add_aggregate, + operator.BIM_OT_select_parts, + operator.BIM_OT_select_aggregate, + operator.BIM_OT_add_part_to_object, prop.BIMObjectAggregateProperties, ui.BIM_PT_aggregate, ) diff --git a/src/blenderbim/blenderbim/bim/module/aggregate/data.py b/src/blenderbim/blenderbim/bim/module/aggregate/data.py index 095bfff16e..ee717cccec 100644 --- a/src/blenderbim/blenderbim/bim/module/aggregate/data.py +++ b/src/blenderbim/blenderbim/bim/module/aggregate/data.py @@ -32,24 +32,50 @@ class AggregateData: @classmethod def load(cls): cls.data = { - "has_aggregate": cls.has_aggregate(), - "label": cls.get_label(), + "has_relating_object": cls.has_relating_object(), + "relating_object_label": cls.get_relating_object_label(), "relating_object_id": cls.get_relating_object_id(), + "has_related_objects": cls.has_related_objects(), + "related_objects_amount": cls.get_related_objects_amount(), + "ifc_class": cls.ifc_class(), } cls.is_loaded = True @classmethod - def has_aggregate(cls): + def get_relating_object(cls): return ifcopenshell.util.element.get_aggregate(tool.Ifc.get_entity(bpy.context.active_object)) @classmethod - def get_label(cls): - aggregate = ifcopenshell.util.element.get_aggregate(tool.Ifc.get_entity(bpy.context.active_object)) + def has_relating_object(cls) -> bool: + return cls.get_relating_object() is not None + + @classmethod + def get_relating_object_label(cls) -> str: + aggregate = cls.get_relating_object() if aggregate: return f"{aggregate.is_a()}/{aggregate.Name or ''}" @classmethod - def get_relating_object_id(cls): - aggregate = ifcopenshell.util.element.get_aggregate(tool.Ifc.get_entity(bpy.context.active_object)) + def get_relating_object_id(cls) -> int: + aggregate = cls.get_relating_object() if aggregate: return aggregate.id() + + @classmethod + def get_related_objects(cls): + return ifcopenshell.util.element.get_parts(tool.Ifc.get_entity(bpy.context.active_object)) + + @classmethod + def get_related_objects_amount(cls): + parts = cls.get_related_objects() + return len(parts) if parts else 0 + + @classmethod + def has_related_objects(cls) -> bool: + return bool(cls.get_related_objects()) + + @classmethod + def ifc_class(cls) -> str: + element = tool.Ifc.get_entity(bpy.context.active_object) + if element: + return element.is_a() diff --git a/src/blenderbim/blenderbim/bim/module/aggregate/operator.py b/src/blenderbim/blenderbim/bim/module/aggregate/operator.py index 1d1d57bed0..5a095fe1b2 100644 --- a/src/blenderbim/blenderbim/bim/module/aggregate/operator.py +++ b/src/blenderbim/blenderbim/bim/module/aggregate/operator.py @@ -33,7 +33,9 @@ class Operator: return {"FINISHED"} -class AssignObject(bpy.types.Operator, Operator): +class BIM_OT_assign_object(bpy.types.Operator, Operator): + """Create aggregation relationship between two ifc elements""" + bl_idname = "bim.assign_object" bl_label = "Assign Object" bl_options = {"REGISTER", "UNDO"} @@ -50,7 +52,9 @@ class AssignObject(bpy.types.Operator, Operator): ) -class UnassignObject(bpy.types.Operator, Operator): +class BIM_OT_unassign_object(bpy.types.Operator, Operator): + """Remove aggregation relationship between two ifc elements""" + bl_idname = "bim.unassign_object" bl_label = "Unassign Object" bl_options = {"REGISTER", "UNDO"} @@ -66,7 +70,9 @@ class UnassignObject(bpy.types.Operator, Operator): ) -class EnableEditingAggregate(bpy.types.Operator, Operator): +class BIM_OT_enable_editing_aggregate(bpy.types.Operator, Operator): + """Enable editing aggregation relationship""" + bl_idname = "bim.enable_editing_aggregate" bl_label = "Enable Editing Aggregate" bl_options = {"REGISTER", "UNDO"} @@ -75,7 +81,9 @@ class EnableEditingAggregate(bpy.types.Operator, Operator): core.enable_editing_aggregate(tool.Aggregate, obj=context.active_object) -class DisableEditingAggregate(bpy.types.Operator, Operator): +class BIM_OT_disable_editing_aggregate(bpy.types.Operator, Operator): + """Disable editing aggregation relationship""" + bl_idname = "bim.disable_editing_aggregate" bl_label = "Disable Editing Aggregate" bl_options = {"REGISTER", "UNDO"} @@ -84,7 +92,9 @@ class DisableEditingAggregate(bpy.types.Operator, Operator): core.disable_editing_aggregate(tool.Aggregate, obj=context.active_object) -class AddAggregate(bpy.types.Operator): +class BIM_OT_add_aggregate(bpy.types.Operator): + """Add aggregate to IFC element""" + bl_idname = "bim.add_aggregate" bl_label = "Add Aggregate" bl_options = {"REGISTER", "UNDO"} @@ -99,12 +109,7 @@ class AddAggregate(bpy.types.Operator): if not element: return {"FINISHED"} - aggregate_collection = bpy.data.collections.new("IfcElementAssembly/Assembly") - context.scene.collection.children.link(aggregate_collection) - aggregate = bpy.data.objects.new("Assembly", None) - aggregate_collection.objects.link(aggregate) - bpy.ops.bim.assign_class(obj=aggregate.name, ifc_class="IfcElementAssembly") - + aggregate = self.create_aggregate(context) tool.Collector.sync(obj) current_aggregate = ifcopenshell.util.element.get_aggregate(element) current_container = ifcopenshell.util.element.get_container(element) @@ -126,3 +131,73 @@ class AddAggregate(bpy.types.Operator): ) core.assign_object(tool.Ifc, tool.Aggregate, tool.Collector, relating_obj=aggregate, related_obj=obj) return {"FINISHED"} + + def create_aggregate(self, context): + aggregate_collection = bpy.data.collections.new("IfcElementAssembly/Assembly") + context.scene.collection.children.link(aggregate_collection) + aggregate = bpy.data.objects.new("Assembly", None) + aggregate_collection.objects.link(aggregate) + bpy.ops.bim.assign_class(obj=aggregate.name, ifc_class="IfcElementAssembly") + return aggregate + + +class BIM_OT_select_parts(bpy.types.Operator): + """Select Parts""" + + bl_idname = "bim.select_parts" + bl_label = "Select Parts" + bl_options = {"REGISTER", "UNDO"} + obj: bpy.props.StringProperty() + + def execute(self, context): + self.file = IfcStore.get_file() + obj = bpy.data.objects.get(self.obj) or context.active_object + parts = ifcopenshell.util.element.get_parts(tool.Ifc.get_entity(obj)) + parts_objs = set(tool.Ifc.get_object(part) for part in parts) + selectable_parts_objs = set(context.selectable_objects).intersection(parts_objs) + for selectable_part_obj in selectable_parts_objs: + selectable_part_obj.select_set(True) + return {"FINISHED"} + + +class BIM_OT_select_aggregate(bpy.types.Operator): + """Select Aggregate""" + + bl_idname = "bim.select_aggregate" + bl_label = "Select Aggregate" + bl_options = {"REGISTER", "UNDO"} + obj: bpy.props.StringProperty() + + def execute(self, context): + self.file = IfcStore.get_file() + obj = bpy.data.objects.get(self.obj) or context.active_object + aggregate = ifcopenshell.util.element.get_aggregate(tool.Ifc.get_entity(obj)) + aggregate_obj = tool.Ifc.get_object(aggregate) + if aggregate_obj in context.selectable_objects: + aggregate_obj.select_set(True) + return {"FINISHED"} + + +class BIM_OT_add_part_to_object(bpy.types.Operator, Operator): + bl_idname = "bim.add_part_to_object" + bl_label = "Add Part to Object" + bl_options = {"REGISTER", "UNDO"} + part_class: bpy.props.StringProperty(name="Class", options={"HIDDEN"}) + part_name: bpy.props.StringProperty(name="Name") + obj: bpy.props.StringProperty(options={"HIDDEN"}) + + def invoke(self, context, event): + self.part_name = "My " + self.part_class.lstrip("Ifc") + return context.window_manager.invoke_props_dialog(self) + + def _execute(self, context): + obj = bpy.data.objects.get(self.obj) or context.active_object + core.add_part_to_object( + tool.Ifc, + tool.Aggregate, + tool.Collector, + tool.Blender, + obj=obj, + part_class=self.part_class, + part_name=self.part_name, + ) diff --git a/src/blenderbim/blenderbim/bim/module/aggregate/prop.py b/src/blenderbim/blenderbim/bim/module/aggregate/prop.py index f7f0afa1cc..5faa2760eb 100644 --- a/src/blenderbim/blenderbim/bim/module/aggregate/prop.py +++ b/src/blenderbim/blenderbim/bim/module/aggregate/prop.py @@ -32,6 +32,17 @@ from bpy.props import ( ) +def update_relating_object(self, context): + def message(self, context): + self.layout.label(text="Please select a valid Ifc Element") + + if self.relating_object is None: + return + if not self.relating_object.BIMObjectProperties.ifc_definition_id: + context.window_manager.popup_menu(message, title="Invalid Element Selected", icon="INFO") + self.relating_object = None + + class BIMObjectAggregateProperties(PropertyGroup): is_editing: BoolProperty(name="Is Editing") - relating_object: PointerProperty(name="Aggregate", type=bpy.types.Object) + relating_object: PointerProperty(name="Aggregate", type=bpy.types.Object, update=update_relating_object) diff --git a/src/blenderbim/blenderbim/bim/module/aggregate/ui.py b/src/blenderbim/blenderbim/bim/module/aggregate/ui.py index 370a02e32e..c092e27adb 100644 --- a/src/blenderbim/blenderbim/bim/module/aggregate/ui.py +++ b/src/blenderbim/blenderbim/bim/module/aggregate/ui.py @@ -44,13 +44,14 @@ class BIM_PT_aggregate(Panel): return True def draw(self, context): + layout = self.layout if not AggregateData.is_loaded: AggregateData.load() props = context.active_object.BIMObjectAggregateProperties if props.is_editing: - row = self.layout.row(align=True) + row = layout.row(align=True) row.prop(props, "relating_object", text="") if props.relating_object: op = row.operator("bim.assign_object", icon="CHECKMARK", text="") @@ -58,15 +59,37 @@ class BIM_PT_aggregate(Panel): op.related_object = context.active_object.BIMObjectProperties.ifc_definition_id row.operator("bim.disable_editing_aggregate", icon="CANCEL", text="") else: - row = self.layout.row(align=True) - if AggregateData.data["has_aggregate"]: - row.label(text=AggregateData.data["label"]) + row = layout.row(align=True) + if AggregateData.data["has_relating_object"]: + row.label(text=AggregateData.data["relating_object_label"], icon="TRIA_UP") + op = row.operator("bim.select_aggregate", icon="RESTRICT_SELECT_OFF", text="") + op.obj = context.active_object.name row.operator("bim.enable_editing_aggregate", icon="GREASEPENCIL", text="") row.operator("bim.add_aggregate", icon="ADD", text="") op = row.operator("bim.unassign_object", icon="X", text="") op.relating_object = AggregateData.data["relating_object_id"] op.related_object = context.active_object.BIMObjectProperties.ifc_definition_id else: - row.label(text="No Aggregate Found") + row.label(text="No Aggregate", icon="TRIA_UP") row.operator("bim.enable_editing_aggregate", icon="GREASEPENCIL", text="") row.operator("bim.add_aggregate", icon="ADD", text="") + + row = layout.row(align=True) + parts = AggregateData.data['related_objects_amount'] + row.label(text=f"{parts or 'No'} Part{'s' if parts > 1 else ''}", icon="TRIA_DOWN") + if AggregateData.data["has_related_objects"]: + op = row.operator("bim.select_parts", icon="RESTRICT_SELECT_OFF", text="") + op.obj = context.active_object.name + + ifc_class = AggregateData.data["ifc_class"] + part_class = "" + if ifc_class == "IfcBuilding": + part_class = "IfcBuildingStorey" + elif ifc_class == "IfcSite": + part_class = "IfcBuilding" + elif ifc_class == "IfcProject": + part_class = "IfcSite" + if part_class != "": + op = layout.operator("bim.add_part_to_object", text="Add " + part_class.lstrip("Ifc")) + op.part_class = part_class + op.obj = context.active_object.name diff --git a/src/blenderbim/blenderbim/core/aggregate.py b/src/blenderbim/blenderbim/core/aggregate.py index b57aa52335..02366e54e2 100644 --- a/src/blenderbim/blenderbim/core/aggregate.py +++ b/src/blenderbim/blenderbim/core/aggregate.py @@ -42,3 +42,9 @@ def unassign_object(ifc, collector, relating_obj=None, related_obj=None): collector.assign(relating_obj) collector.assign(related_obj) return rel + + +def add_part_to_object(ifc, aggregator, collector, blender, obj, part_class, part_name=None): + part_obj = blender.create_ifc_object(ifc_class=part_class, name=part_name) + assign_object(ifc, aggregator, collector, relating_obj=obj, related_obj=part_obj) + blender.set_active_object(obj) diff --git a/src/blenderbim/blenderbim/tool/blender.py b/src/blenderbim/blenderbim/tool/blender.py index 7b3bcffddf..7441957b02 100644 --- a/src/blenderbim/blenderbim/tool/blender.py +++ b/src/blenderbim/blenderbim/tool/blender.py @@ -23,4 +23,24 @@ from blenderbim.bim.ifc import IfcStore class Blender: - pass + @classmethod + def set_active_object(cls, obj): + bpy.context.view_layer.objects.active = obj + obj.select_set(True) + + @classmethod + def get_name(cls, ifc_class, name): + if not bpy.data.objects.get(f"{ifc_class}/{name}"): + return name + i = 2 + while bpy.data.objects.get(f"{ifc_class}/{name} {i}"): + i += 1 + return f"{name} {i}" + + @classmethod + def create_ifc_object(cls, ifc_class: str, name: str = None, data=None) -> bpy.types.Object: + name = name or "My " + ifc_class + name = cls.get_name(ifc_class, name) + obj = bpy.data.objects.new(name, data) + bpy.ops.bim.assign_class(obj=obj.name, ifc_class=ifc_class) + return obj diff --git a/src/blenderbim/blenderbim/tool/spatial.py b/src/blenderbim/blenderbim/tool/spatial.py index f521463861..18e41ec01e 100644 --- a/src/blenderbim/blenderbim/tool/spatial.py +++ b/src/blenderbim/blenderbim/tool/spatial.py @@ -112,7 +112,7 @@ class Spatial(blenderbim.core.tool.Spatial): @classmethod def set_active_object(cls, obj): bpy.context.view_layer.objects.active = obj - obj.select_set(True) + cls.select_object(obj) @classmethod def set_relative_object_matrix(cls, target_obj, relative_to_obj, matrix): diff --git a/src/ifcopenshell-python/ifcopenshell/util/element.py b/src/ifcopenshell-python/ifcopenshell/util/element.py index 603f0af989..0f7b1b1c66 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/element.py +++ b/src/ifcopenshell-python/ifcopenshell/util/element.py @@ -183,6 +183,11 @@ def get_aggregate(element): return element.Decomposes[0].RelatingObject +def get_parts(element): + if hasattr(element, "IsDecomposedBy") and element.IsDecomposedBy: + return element.IsDecomposedBy[0].RelatedObjects + + def replace_attribute(element, old, new): for i, attribute in enumerate(element): if has_element_reference(attribute, old):