mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-20 23:36:20 +00:00
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:
@@ -20,11 +20,14 @@ import bpy
|
|||||||
from . import ui, prop, operator
|
from . import ui, prop, operator
|
||||||
|
|
||||||
classes = (
|
classes = (
|
||||||
operator.AssignObject,
|
operator.BIM_OT_assign_object,
|
||||||
operator.UnassignObject,
|
operator.BIM_OT_unassign_object,
|
||||||
operator.EnableEditingAggregate,
|
operator.BIM_OT_enable_editing_aggregate,
|
||||||
operator.DisableEditingAggregate,
|
operator.BIM_OT_disable_editing_aggregate,
|
||||||
operator.AddAggregate,
|
operator.BIM_OT_add_aggregate,
|
||||||
|
operator.BIM_OT_select_parts,
|
||||||
|
operator.BIM_OT_select_aggregate,
|
||||||
|
operator.BIM_OT_add_part_to_object,
|
||||||
prop.BIMObjectAggregateProperties,
|
prop.BIMObjectAggregateProperties,
|
||||||
ui.BIM_PT_aggregate,
|
ui.BIM_PT_aggregate,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -32,24 +32,50 @@ class AggregateData:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def load(cls):
|
def load(cls):
|
||||||
cls.data = {
|
cls.data = {
|
||||||
"has_aggregate": cls.has_aggregate(),
|
"has_relating_object": cls.has_relating_object(),
|
||||||
"label": cls.get_label(),
|
"relating_object_label": cls.get_relating_object_label(),
|
||||||
"relating_object_id": cls.get_relating_object_id(),
|
"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
|
cls.is_loaded = True
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def has_aggregate(cls):
|
def get_relating_object(cls):
|
||||||
return ifcopenshell.util.element.get_aggregate(tool.Ifc.get_entity(bpy.context.active_object))
|
return ifcopenshell.util.element.get_aggregate(tool.Ifc.get_entity(bpy.context.active_object))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_label(cls):
|
def has_relating_object(cls) -> bool:
|
||||||
aggregate = ifcopenshell.util.element.get_aggregate(tool.Ifc.get_entity(bpy.context.active_object))
|
return cls.get_relating_object() is not None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_relating_object_label(cls) -> str:
|
||||||
|
aggregate = cls.get_relating_object()
|
||||||
if aggregate:
|
if aggregate:
|
||||||
return f"{aggregate.is_a()}/{aggregate.Name or ''}"
|
return f"{aggregate.is_a()}/{aggregate.Name or ''}"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_relating_object_id(cls):
|
def get_relating_object_id(cls) -> int:
|
||||||
aggregate = ifcopenshell.util.element.get_aggregate(tool.Ifc.get_entity(bpy.context.active_object))
|
aggregate = cls.get_relating_object()
|
||||||
if aggregate:
|
if aggregate:
|
||||||
return aggregate.id()
|
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"}
|
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_idname = "bim.assign_object"
|
||||||
bl_label = "Assign Object"
|
bl_label = "Assign Object"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
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_idname = "bim.unassign_object"
|
||||||
bl_label = "Unassign Object"
|
bl_label = "Unassign Object"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
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_idname = "bim.enable_editing_aggregate"
|
||||||
bl_label = "Enable Editing Aggregate"
|
bl_label = "Enable Editing Aggregate"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
@@ -75,7 +81,9 @@ class EnableEditingAggregate(bpy.types.Operator, Operator):
|
|||||||
core.enable_editing_aggregate(tool.Aggregate, obj=context.active_object)
|
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_idname = "bim.disable_editing_aggregate"
|
||||||
bl_label = "Disable Editing Aggregate"
|
bl_label = "Disable Editing Aggregate"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
@@ -84,7 +92,9 @@ class DisableEditingAggregate(bpy.types.Operator, Operator):
|
|||||||
core.disable_editing_aggregate(tool.Aggregate, obj=context.active_object)
|
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_idname = "bim.add_aggregate"
|
||||||
bl_label = "Add Aggregate"
|
bl_label = "Add Aggregate"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
@@ -99,12 +109,7 @@ class AddAggregate(bpy.types.Operator):
|
|||||||
if not element:
|
if not element:
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
aggregate_collection = bpy.data.collections.new("IfcElementAssembly/Assembly")
|
aggregate = self.create_aggregate(context)
|
||||||
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")
|
|
||||||
|
|
||||||
tool.Collector.sync(obj)
|
tool.Collector.sync(obj)
|
||||||
current_aggregate = ifcopenshell.util.element.get_aggregate(element)
|
current_aggregate = ifcopenshell.util.element.get_aggregate(element)
|
||||||
current_container = ifcopenshell.util.element.get_container(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)
|
core.assign_object(tool.Ifc, tool.Aggregate, tool.Collector, relating_obj=aggregate, related_obj=obj)
|
||||||
return {"FINISHED"}
|
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):
|
class BIMObjectAggregateProperties(PropertyGroup):
|
||||||
is_editing: BoolProperty(name="Is Editing")
|
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
|
return True
|
||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
|
layout = self.layout
|
||||||
if not AggregateData.is_loaded:
|
if not AggregateData.is_loaded:
|
||||||
AggregateData.load()
|
AggregateData.load()
|
||||||
|
|
||||||
props = context.active_object.BIMObjectAggregateProperties
|
props = context.active_object.BIMObjectAggregateProperties
|
||||||
|
|
||||||
if props.is_editing:
|
if props.is_editing:
|
||||||
row = self.layout.row(align=True)
|
row = layout.row(align=True)
|
||||||
row.prop(props, "relating_object", text="")
|
row.prop(props, "relating_object", text="")
|
||||||
if props.relating_object:
|
if props.relating_object:
|
||||||
op = row.operator("bim.assign_object", icon="CHECKMARK", text="")
|
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
|
op.related_object = context.active_object.BIMObjectProperties.ifc_definition_id
|
||||||
row.operator("bim.disable_editing_aggregate", icon="CANCEL", text="")
|
row.operator("bim.disable_editing_aggregate", icon="CANCEL", text="")
|
||||||
else:
|
else:
|
||||||
row = self.layout.row(align=True)
|
row = layout.row(align=True)
|
||||||
if AggregateData.data["has_aggregate"]:
|
if AggregateData.data["has_relating_object"]:
|
||||||
row.label(text=AggregateData.data["label"])
|
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.enable_editing_aggregate", icon="GREASEPENCIL", text="")
|
||||||
row.operator("bim.add_aggregate", icon="ADD", text="")
|
row.operator("bim.add_aggregate", icon="ADD", text="")
|
||||||
op = row.operator("bim.unassign_object", icon="X", text="")
|
op = row.operator("bim.unassign_object", icon="X", text="")
|
||||||
op.relating_object = AggregateData.data["relating_object_id"]
|
op.relating_object = AggregateData.data["relating_object_id"]
|
||||||
op.related_object = context.active_object.BIMObjectProperties.ifc_definition_id
|
op.related_object = context.active_object.BIMObjectProperties.ifc_definition_id
|
||||||
else:
|
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.enable_editing_aggregate", icon="GREASEPENCIL", text="")
|
||||||
row.operator("bim.add_aggregate", icon="ADD", 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(relating_obj)
|
||||||
collector.assign(related_obj)
|
collector.assign(related_obj)
|
||||||
return rel
|
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)
|
||||||
|
|||||||
@@ -23,4 +23,24 @@ from blenderbim.bim.ifc import IfcStore
|
|||||||
|
|
||||||
|
|
||||||
class Blender:
|
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
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ class Spatial(blenderbim.core.tool.Spatial):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def set_active_object(cls, obj):
|
def set_active_object(cls, obj):
|
||||||
bpy.context.view_layer.objects.active = obj
|
bpy.context.view_layer.objects.active = obj
|
||||||
obj.select_set(True)
|
cls.select_object(obj)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def set_relative_object_matrix(cls, target_obj, relative_to_obj, matrix):
|
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
|
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):
|
def replace_attribute(element, old, new):
|
||||||
for i, attribute in enumerate(element):
|
for i, attribute in enumerate(element):
|
||||||
if has_element_reference(attribute, old):
|
if has_element_reference(attribute, old):
|
||||||
|
|||||||
Reference in New Issue
Block a user