mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-26 18:21:59 +00:00
Delete aggregate if all its parts are deleted. Prevents having orphaned aggregates, if you do a bulk delete with empties turned off.
This commit is contained in:
@@ -94,6 +94,9 @@ class BIM_OT_aggregate_unassign_object(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
def _execute(self, context):
|
def _execute(self, context):
|
||||||
|
aggregates_to_check = set()
|
||||||
|
|
||||||
|
# First pass: unassign all parts and track their aggregates
|
||||||
for obj in tool.Blender.get_selected_objects():
|
for obj in tool.Blender.get_selected_objects():
|
||||||
element = tool.Ifc.get_entity(obj)
|
element = tool.Ifc.get_entity(obj)
|
||||||
if not element:
|
if not element:
|
||||||
@@ -101,6 +104,10 @@ class BIM_OT_aggregate_unassign_object(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
aggregate = ifcopenshell.util.element.get_aggregate(element)
|
aggregate = ifcopenshell.util.element.get_aggregate(element)
|
||||||
if not aggregate:
|
if not aggregate:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Track this aggregate for later checking
|
||||||
|
aggregates_to_check.add(aggregate)
|
||||||
|
|
||||||
core.unassign_object(
|
core.unassign_object(
|
||||||
tool.Ifc,
|
tool.Ifc,
|
||||||
tool.Aggregate,
|
tool.Aggregate,
|
||||||
@@ -115,7 +122,27 @@ class BIM_OT_aggregate_unassign_object(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
if pset:
|
if pset:
|
||||||
pset = tool.Ifc.get().by_id(pset["id"])
|
pset = tool.Ifc.get().by_id(pset["id"])
|
||||||
ifcopenshell.api.pset.remove_pset(tool.Ifc.get(), product=element, pset=pset)
|
ifcopenshell.api.pset.remove_pset(tool.Ifc.get(), product=element, pset=pset)
|
||||||
|
|
||||||
|
# Second pass: delete aggregates that now have no parts
|
||||||
|
deleted_aggregates = []
|
||||||
|
for aggregate in aggregates_to_check:
|
||||||
|
related_objects = ifcopenshell.util.element.get_parts(aggregate)
|
||||||
|
if len(related_objects) == 0:
|
||||||
|
aggregate_name = aggregate.Name or f"{aggregate.is_a()} #{aggregate.id()}"
|
||||||
|
deleted_aggregates.append(aggregate_name)
|
||||||
|
|
||||||
|
aggregate_obj = tool.Ifc.get_object(aggregate)
|
||||||
|
if aggregate_obj:
|
||||||
|
ifcopenshell.api.root.remove_product(tool.Ifc.get(), product=aggregate)
|
||||||
|
bpy.data.objects.remove(aggregate_obj, do_unlink=True)
|
||||||
|
|
||||||
|
# Show info message if aggregates were deleted
|
||||||
|
if deleted_aggregates:
|
||||||
|
if len(deleted_aggregates) == 1:
|
||||||
|
self.report({'INFO'}, f"Aggregate '{deleted_aggregates[0]}' was deleted because it had no remaining parts")
|
||||||
|
else:
|
||||||
|
aggregate_list = ", ".join(f"'{name}'" for name in deleted_aggregates)
|
||||||
|
self.report({'INFO'}, f"Aggregates {aggregate_list} were deleted because they had no remaining parts")
|
||||||
|
|
||||||
class BIM_OT_enable_editing_aggregate(bpy.types.Operator):
|
class BIM_OT_enable_editing_aggregate(bpy.types.Operator):
|
||||||
"""Enable editing aggregation relationship"""
|
"""Enable editing aggregation relationship"""
|
||||||
|
|||||||
@@ -787,7 +787,6 @@ def calc_delete_is_batch(ifc_file: ifcopenshell.file, context: bpy.types.Context
|
|||||||
is_batch = total_elements > 500000 and total_polygons > 2000
|
is_batch = total_elements > 500000 and total_polygons > 2000
|
||||||
return is_batch
|
return is_batch
|
||||||
|
|
||||||
|
|
||||||
class OverrideDelete(bpy.types.Operator):
|
class OverrideDelete(bpy.types.Operator):
|
||||||
bl_idname = "bim.override_object_delete"
|
bl_idname = "bim.override_object_delete"
|
||||||
bl_label = "IFC Delete"
|
bl_label = "IFC Delete"
|
||||||
@@ -858,6 +857,10 @@ class OverrideDelete(bpy.types.Operator):
|
|||||||
objects_to_remove = context.selected_objects
|
objects_to_remove = context.selected_objects
|
||||||
|
|
||||||
self.process_arrays(context)
|
self.process_arrays(context)
|
||||||
|
|
||||||
|
# Track aggregates before deleting their parts
|
||||||
|
aggregates_to_check = self.track_aggregates(objects_to_remove)
|
||||||
|
|
||||||
clear_active_object = True
|
clear_active_object = True
|
||||||
|
|
||||||
for i, obj in enumerate(objects_to_remove, 1):
|
for i, obj in enumerate(objects_to_remove, 1):
|
||||||
@@ -898,6 +901,9 @@ class OverrideDelete(bpy.types.Operator):
|
|||||||
else:
|
else:
|
||||||
bpy.data.objects.remove(obj)
|
bpy.data.objects.remove(obj)
|
||||||
|
|
||||||
|
# Delete empty aggregates after deleting their parts
|
||||||
|
self.delete_empty_aggregates(aggregates_to_check)
|
||||||
|
|
||||||
for opening in tool.Model.get_model_props().openings:
|
for opening in tool.Model.get_model_props().openings:
|
||||||
if opening.obj is not None and not tool.Ifc.get_entity(opening.obj):
|
if opening.obj is not None and not tool.Ifc.get_entity(opening.obj):
|
||||||
bpy.data.objects.remove(opening.obj)
|
bpy.data.objects.remove(opening.obj)
|
||||||
@@ -930,6 +936,47 @@ class OverrideDelete(bpy.types.Operator):
|
|||||||
data["old_file"].redo()
|
data["old_file"].redo()
|
||||||
tool.Ifc.set(data["new_file"])
|
tool.Ifc.set(data["new_file"])
|
||||||
|
|
||||||
|
def track_aggregates(self, objects_to_remove):
|
||||||
|
"""Track aggregates that contain objects being deleted"""
|
||||||
|
aggregates_to_check = set()
|
||||||
|
for obj in objects_to_remove:
|
||||||
|
if not tool.Blender.is_valid_data_block(obj):
|
||||||
|
continue
|
||||||
|
element = tool.Ifc.get_entity(obj)
|
||||||
|
if not element:
|
||||||
|
continue
|
||||||
|
aggregate = ifcopenshell.util.element.get_aggregate(element)
|
||||||
|
if aggregate:
|
||||||
|
aggregates_to_check.add(aggregate)
|
||||||
|
return aggregates_to_check
|
||||||
|
|
||||||
|
def delete_empty_aggregates(self, aggregates_to_check):
|
||||||
|
"""Delete aggregates that now have no parts"""
|
||||||
|
deleted_aggregates = []
|
||||||
|
for aggregate in aggregates_to_check:
|
||||||
|
# Check if aggregate still exists (might have been deleted already)
|
||||||
|
try:
|
||||||
|
aggregate.id()
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
|
||||||
|
related_objects = ifcopenshell.util.element.get_parts(aggregate)
|
||||||
|
if len(related_objects) == 0:
|
||||||
|
aggregate_name = aggregate.Name or f"{aggregate.is_a()} #{aggregate.id()}"
|
||||||
|
deleted_aggregates.append(aggregate_name)
|
||||||
|
|
||||||
|
aggregate_obj = tool.Ifc.get_object(aggregate)
|
||||||
|
if aggregate_obj and tool.Blender.is_valid_data_block(aggregate_obj):
|
||||||
|
tool.Geometry.delete_ifc_object(aggregate_obj)
|
||||||
|
|
||||||
|
# Show info message if aggregates were deleted
|
||||||
|
if deleted_aggregates:
|
||||||
|
if len(deleted_aggregates) == 1:
|
||||||
|
self.report({'INFO'}, f"Aggregate '{deleted_aggregates[0]}' was deleted because it had no remaining parts")
|
||||||
|
else:
|
||||||
|
aggregate_list = ", ".join(f"'{name}'" for name in deleted_aggregates)
|
||||||
|
self.report({'INFO'}, f"Aggregates {aggregate_list} were deleted because they had no remaining parts")
|
||||||
|
|
||||||
def process_arrays(self, context: bpy.types.Context) -> None:
|
def process_arrays(self, context: bpy.types.Context) -> None:
|
||||||
ifc_file = tool.Ifc.get()
|
ifc_file = tool.Ifc.get()
|
||||||
selected_objects = set(context.selected_objects)
|
selected_objects = set(context.selected_objects)
|
||||||
@@ -955,7 +1002,6 @@ class OverrideDelete(bpy.types.Operator):
|
|||||||
else:
|
else:
|
||||||
break # allows to remove only n last layers of an array
|
break # allows to remove only n last layers of an array
|
||||||
|
|
||||||
|
|
||||||
class SelectedIdsData(NamedTuple):
|
class SelectedIdsData(NamedTuple):
|
||||||
objects: set[bpy.types.Object]
|
objects: set[bpy.types.Object]
|
||||||
collections: set[bpy.types.Collection]
|
collections: set[bpy.types.Collection]
|
||||||
|
|||||||
Reference in New Issue
Block a user