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 5248627a41.

* 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
This commit is contained in:
Gorgious56
2022-02-03 07:16:43 +01:00
committed by GitHub
parent 94ff8349d3
commit 098ab2c237
9 changed files with 200 additions and 31 deletions
@@ -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,
)
@@ -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()
@@ -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,
)
@@ -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)
@@ -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
@@ -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)
+21 -1
View File
@@ -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
+1 -1
View File
@@ -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):
@@ -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):