mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 09:21:46 +00:00
feat: auto-assign containers to root aggregates and organize parts in outliner
When assigning a container to an aggregated element, automatically promote the operation to the root aggregate and move all nested parts to the container's collection in the Blender outliner. Changes: - AssignContainer now traverses the aggregate hierarchy to find the root aggregate when a user selects any nested part - All parts and sub-aggregates are moved to the container's collection in the outliner while preserving IFC aggregate relationships - Parts remain aggregated in IFC (not directly contained), only their Blender collection membership changes - RefreshLinkedAggregate now also moves all parts to the correct container collection when restoring original data This provides a more intuitive UX - users can select any part and the entire assembly moves together, properly organized under the spatial container. Fixes the previous behavior where: - Aggregated elements were skipped with a warning - Parts weren't organized under the container in the outliner - Aggregate nesting was broken after container assignment
This commit is contained in:
@@ -1715,19 +1715,40 @@ class RefreshLinkedAggregate(bpy.types.Operator, tool.Ifc.Operator):
|
||||
# Only assign container if element is not already aggregated under another element
|
||||
# Aggregated elements should not be in the spatial structure
|
||||
if not ifcopenshell.util.element.get_aggregate(element):
|
||||
container = original_data[matching_group_id][index]["Container"]
|
||||
bonsai.core.spatial.assign_container(
|
||||
tool.Ifc,
|
||||
tool.Collector,
|
||||
tool.Spatial,
|
||||
container=original_data[matching_group_id][index]["Container"],
|
||||
container=container,
|
||||
element_obj=obj,
|
||||
)
|
||||
for part in ifcopenshell.util.element.get_parts(tool.Ifc.get_entity(obj)):
|
||||
tool.Collector.assign(tool.Ifc.get_object(part))
|
||||
|
||||
assignments = original_data[matching_group_id][index]["Assignment"]
|
||||
if assignments:
|
||||
assign_to_annotations(obj, assignments)
|
||||
|
||||
# Get the container's collection for moving parts in the outliner
|
||||
container_obj = tool.Ifc.get_object(container)
|
||||
container_collection = container_obj.BIMObjectProperties.collection if container_obj else None
|
||||
|
||||
# Move all parts to the container's collection in the outliner
|
||||
if container_collection:
|
||||
for part in ifcopenshell.util.element.get_parts(element):
|
||||
part_obj = tool.Ifc.get_object(part)
|
||||
if part_obj:
|
||||
# Remove from all previous collections
|
||||
for col in part_obj.users_collection[:]:
|
||||
col.objects.unlink(part_obj)
|
||||
|
||||
# Link to container collection
|
||||
if part_obj.name not in container_collection.objects:
|
||||
container_collection.objects.link(part_obj)
|
||||
|
||||
# Recursively handle nested parts
|
||||
for nested_part in ifcopenshell.util.element.get_parts(part):
|
||||
nested_part_obj = tool.Ifc.get_object(nested_part)
|
||||
if nested_part_obj:
|
||||
for col in nested_part_obj.users_collection[:]:
|
||||
col.objects.unlink(nested_part_obj)
|
||||
if nested_part_obj.name not in container_collection.objects:
|
||||
container_collection.objects.link(nested_part_obj)
|
||||
else:
|
||||
try:
|
||||
obj.name = original_data[matching_group_id][index]["Name"]
|
||||
|
||||
@@ -175,31 +175,84 @@ class AssignContainer(bpy.types.Operator, tool.Ifc.Operator):
|
||||
else:
|
||||
return
|
||||
|
||||
def get_root_aggregate(element):
|
||||
"""Traverse up the aggregate hierarchy to find the top-most aggregate"""
|
||||
current = element
|
||||
root = None
|
||||
while aggregate := ifcopenshell.util.element.get_aggregate(current):
|
||||
root = aggregate
|
||||
current = aggregate
|
||||
return root
|
||||
|
||||
def get_all_parts_recursive(element):
|
||||
"""Recursively get all parts of an aggregate"""
|
||||
parts = []
|
||||
for part in ifcopenshell.util.element.get_parts(element):
|
||||
parts.append(part)
|
||||
# Recursively get nested parts
|
||||
parts.extend(get_all_parts_recursive(part))
|
||||
return parts
|
||||
|
||||
objs: list[bpy.types.Object] = []
|
||||
# In IFC element can be either contained of aggregated,
|
||||
# tehrefore we skip aggregated elements here to prevent confusion.
|
||||
# Can't handle it in `poll` since user might just select bunch of elements
|
||||
# and try to assign a container to them
|
||||
# and excluding aggregates because of the `poll` failing might get awkward.
|
||||
skipped_aggregates = 0
|
||||
processed_elements = set() # Track elements we've already handled (by IFC ID)
|
||||
promoted_parts = 0 # Count how many parts were promoted to their root aggregate
|
||||
|
||||
for obj in tool.Blender.get_selected_objects():
|
||||
if not (element := tool.Ifc.get_entity(obj)):
|
||||
continue
|
||||
if ifcopenshell.util.element.get_aggregate(element):
|
||||
skipped_aggregates += 1
|
||||
continue
|
||||
objs.append(obj)
|
||||
|
||||
# Check if element is part of an aggregate (at any level)
|
||||
if root_aggregate := get_root_aggregate(element):
|
||||
# Skip if we've already processed this root aggregate
|
||||
if root_aggregate.id() in processed_elements:
|
||||
continue
|
||||
|
||||
# Get the root aggregate object and add it instead
|
||||
if root_aggregate_obj := tool.Ifc.get_object(root_aggregate):
|
||||
objs.append(root_aggregate_obj)
|
||||
processed_elements.add(root_aggregate.id())
|
||||
if root_aggregate != element: # Only count as promoted if different from selected
|
||||
promoted_parts += 1
|
||||
else:
|
||||
# Element is not part of any aggregate
|
||||
if element.id() not in processed_elements:
|
||||
objs.append(obj)
|
||||
processed_elements.add(element.id())
|
||||
|
||||
# Get the container's collection
|
||||
container_obj = tool.Ifc.get_object(container)
|
||||
container_collection = container_obj.BIMObjectProperties.collection if container_obj else None
|
||||
|
||||
for element_obj in objs:
|
||||
element = tool.Ifc.get_entity(element_obj)
|
||||
|
||||
# Only assign container to the ROOT aggregate (this updates IFC relationships)
|
||||
if self.remove_from_other_containers:
|
||||
for col in element_obj.users_collection[:]:
|
||||
col.objects.unlink(element_obj)
|
||||
core.assign_container(tool.Ifc, tool.Collector, tool.Spatial, container=container, element_obj=element_obj)
|
||||
|
||||
# For parts, only move them in Blender collections (don't change IFC relationships)
|
||||
if container_collection:
|
||||
all_parts = get_all_parts_recursive(element)
|
||||
for part in all_parts:
|
||||
if part_obj := tool.Ifc.get_object(part):
|
||||
# Always remove from ALL previous collections when moving to new container
|
||||
for col in part_obj.users_collection[:]:
|
||||
col.objects.unlink(part_obj)
|
||||
|
||||
# Link to new container collection (Blender-only, no IFC change)
|
||||
if part_obj.name not in container_collection.objects:
|
||||
container_collection.objects.link(part_obj)
|
||||
|
||||
aggregates_msg = ""
|
||||
if skipped_aggregates:
|
||||
aggregates_msg = f" {skipped_aggregates} aggregated elements skipped."
|
||||
self.report({"INFO"}, f"{len(objs)} elements assigned.{aggregates_msg}")
|
||||
# Disable editing mode for all selected objects
|
||||
for obj in tool.Blender.get_selected_objects():
|
||||
core.disable_editing_container(tool.Spatial, obj=obj)
|
||||
|
||||
promoted_msg = ""
|
||||
if promoted_parts:
|
||||
promoted_msg = f" {promoted_parts} nested parts promoted to their root aggregates."
|
||||
self.report({"INFO"}, f"{len(objs)} elements assigned.{promoted_msg}")
|
||||
|
||||
|
||||
class EnableEditingContainer(bpy.types.Operator):
|
||||
|
||||
Reference in New Issue
Block a user