diff --git a/src/bonsai/bonsai/bim/module/aggregate/operator.py b/src/bonsai/bonsai/bim/module/aggregate/operator.py index 6bbee38d6f..a6b95f48f6 100644 --- a/src/bonsai/bonsai/bim/module/aggregate/operator.py +++ b/src/bonsai/bonsai/bim/module/aggregate/operator.py @@ -386,7 +386,7 @@ class BIM_OT_disable_aggregate_mode(bpy.types.Operator): def execute(self, context): bpy.ops.object.select_all(action="DESELECT") - bonsai.core.aggregate.disable_aggregate_mode(tool.Aggregate) + bonsai.core.aggregate.exit_aggregate_mode(tool.Aggregate) return {"FINISHED"} diff --git a/src/bonsai/bonsai/bim/module/aggregate/prop.py b/src/bonsai/bonsai/bim/module/aggregate/prop.py index 0fb63661b3..03a3d0f533 100644 --- a/src/bonsai/bonsai/bim/module/aggregate/prop.py +++ b/src/bonsai/bonsai/bim/module/aggregate/prop.py @@ -119,6 +119,7 @@ class Objects(bpy.types.PropertyGroup): class BIMAggregateProperties(PropertyGroup): in_aggregate_mode: BoolProperty(name="In Edit Mode", update=update_aggregate_mode_decorator) editing_aggregate: PointerProperty(name="Editing Aggregate", type=bpy.types.Object) + previous_editing_aggregate: PointerProperty(name="Editing Aggregate", type=bpy.types.Object) editing_objects: CollectionProperty(type=Objects) not_editing_objects: CollectionProperty(type=Objects) aggregate_decorator: BoolProperty( @@ -126,10 +127,16 @@ class BIMAggregateProperties(PropertyGroup): default=False, update=update_aggregate_decorator, ) + previous_state: BoolProperty( + name="True if it was previously in aggregate mode", + default=False, + ) if TYPE_CHECKING: in_aggregate_mode: bool editing_aggregate: Union[bpy.types.Object, None] + previous_editing_aggregate: Union[bpy.types.Object, None] editing_objects: bpy.types.bpy_prop_collection_idprop[Objects] not_editing_objects: bpy.types.bpy_prop_collection_idprop[Objects] aggregate_decorator: bool + previous_state: bool diff --git a/src/bonsai/bonsai/bim/module/geometry/operator.py b/src/bonsai/bonsai/bim/module/geometry/operator.py index d779e52765..6ce4b0ad11 100644 --- a/src/bonsai/bonsai/bim/module/geometry/operator.py +++ b/src/bonsai/bonsai/bim/module/geometry/operator.py @@ -1957,9 +1957,8 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator): nprops = tool.Nest.get_nest_props() if self.has_aggregates(selected_objs): - if not aprops.in_aggregate_mode: - bonsai.core.aggregate.enable_aggregate_mode(tool.Aggregate, context.active_object) - return {"FINISHED"} + bonsai.core.aggregate.enter_aggregate_mode(tool.Aggregate, context.active_object) + return {"FINISHED"} if self.has_nests(selected_objs): if not nprops.in_nest_mode: @@ -1970,7 +1969,11 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator): self.handle_single_object(context, context.active_object) elif len(selected_objs) == 0: if aprops.in_aggregate_mode: - bonsai.core.aggregate.disable_aggregate_mode(tool.Aggregate) + gprops = tool.Geometry.get_geometry_props() + if gprops.representation_obj: + tool.Geometry.disable_item_mode() + else: + bonsai.core.aggregate.exit_aggregate_mode(tool.Aggregate) return {"FINISHED"} if nprops.in_nest_mode: bonsai.core.nest.disable_nest_mode(tool.Nest) @@ -2105,6 +2108,10 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator): props = tool.Aggregate.get_aggregate_props() if (aggregate or parts) and not props.in_aggregate_mode: return True + elif element != tool.Ifc.get_entity(props.editing_aggregate) and aggregate != tool.Ifc.get_entity(props.editing_aggregate): + return True + elif parts and aggregate == tool.Ifc.get_entity(props.editing_aggregate): + return True else: return False @@ -3291,10 +3298,12 @@ class OverrideMove(bpy.types.Operator): parts = ifcopenshell.util.element.get_parts(element) if parts: aggregates_to_move.append(tool.Ifc.get_object(element)) - continue - if not parts and props.in_aggregate_mode: + aggregates_to_move.extend(list(tool.Aggregate.get_parts_recursively(element))) + continue aggregate = ifcopenshell.util.element.get_aggregate(element) + if not parts and props.in_aggregate_mode and aggregate == tool.Ifc.get_entity(props.editing_aggregate): + continue if aggregate: aggregates_to_move.append(tool.Ifc.get_object(aggregate)) obj.select_set(False) diff --git a/src/bonsai/bonsai/core/aggregate.py b/src/bonsai/bonsai/core/aggregate.py index 693063b602..b50f91fa6b 100644 --- a/src/bonsai/bonsai/core/aggregate.py +++ b/src/bonsai/bonsai/core/aggregate.py @@ -87,18 +87,24 @@ def add_part_to_object( blender.set_active_object(obj) -def enable_aggregate_mode( +def enter_aggregate_mode( aggregator: tool.Aggregate, obj: bpy.types.Object, ): - if aggregator.get_aggregate_mode(): - disable_aggregate_mode(aggregator) + aggregator.update_previous_aggregate_mode_state() + if aggregator.get_higher_aggregate(): + aggregator.disable_aggregate_mode() aggregator.enable_aggregate_mode(obj) -def disable_aggregate_mode(aggregator: tool.Aggregate): - aggregator.disable_aggregate_mode() +def exit_aggregate_mode(aggregator: tool.Aggregate): + aggregator.update_previous_aggregate_mode_state() + if new_obj := aggregator.get_higher_aggregate(): + aggregator.disable_aggregate_mode() + aggregator.enable_aggregate_mode(new_obj) + else: + aggregator.disable_aggregate_mode() class IncompatibleAggregateError(Exception): diff --git a/src/bonsai/bonsai/tool/aggregate.py b/src/bonsai/bonsai/tool/aggregate.py index e2fb4df5f9..df644a825b 100644 --- a/src/bonsai/bonsai/tool/aggregate.py +++ b/src/bonsai/bonsai/tool/aggregate.py @@ -88,6 +88,20 @@ class Aggregate(bonsai.core.tool.Aggregate): if rel.is_a("IfcRelAggregates"): return rel.RelatingObject + @classmethod + def get_aggregates_recursively(cls, element: ifcopenshell.entity_instance) -> set[ifcopenshell.entity_instance]: + """Get elements aggregates recursively, resulting set includes `element`.""" + aggregates = list() + queue = {element} + while queue: + element = queue.pop() + aggregate = ifcopenshell.util.element.get_aggregate(element) + if aggregate: + queue.update({aggregate}) + if ifcopenshell.util.element.get_parts(element): + aggregates.append(element) + return aggregates + @classmethod def get_parts_recursively(cls, element: ifcopenshell.entity_instance) -> set[ifcopenshell.entity_instance]: """Get elements parts recursively, resulting set includes `element`.""" @@ -100,9 +114,17 @@ class Aggregate(bonsai.core.tool.Aggregate): return parts @classmethod - def get_aggregate_mode(cls) -> bool: + def get_higher_aggregate(cls) -> ifcopenshell.entity_instance: props = cls.get_aggregate_props() - return props.in_aggregate_mode + editing_aggregate = tool.Ifc.get_entity(props.editing_aggregate) + higher_aggregate = ifcopenshell.util.element.get_aggregate(editing_aggregate) + return tool.Ifc.get_object(higher_aggregate) if higher_aggregate else None + + @classmethod + def update_previous_aggregate_mode_state(cls): + props = cls.get_aggregate_props() + props.previous_state = props.in_aggregate_mode + props.previous_editing_aggregate = props.editing_aggregate @classmethod def enable_aggregate_mode(cls, active_object: bpy.types.Object) -> set[Literal["FINISHED"]]: @@ -112,12 +134,24 @@ class Aggregate(bonsai.core.tool.Aggregate): element = tool.Ifc.get_entity(active_object) if not element: return {"FINISHED"} - aggregate = ifcopenshell.util.element.get_aggregate(element) - parts = ifcopenshell.util.element.get_parts(element) + # Defines the aggregate based on previous state + # Controls whether the user is entering a deeper level or + # exiting to a higher level. See core/aggregate.py + aggregates = tool.Aggregate.get_aggregates_recursively(element) + aggregate = aggregates[-1] + if props.previous_state: + previous_aggregate = tool.Ifc.get_entity(props.previous_editing_aggregate) + if previous_aggregate in aggregates: + reference_index = aggregates.index(previous_aggregate) + aggregate = aggregates[reference_index - 1] + else: + aggregate = aggregates[0] + + parts = tool.Aggregate.get_parts_recursively(element) if not aggregate and not parts: return {"FINISHED"} if not parts: - parts = ifcopenshell.util.element.get_parts(aggregate) + parts = tool.Aggregate.get_parts_recursively(aggregate) if parts: props.editing_aggregate = tool.Ifc.get_object(aggregate) if aggregate else tool.Ifc.get_object(element) parts_objs = [tool.Ifc.get_object(part) for part in parts] @@ -157,7 +191,6 @@ class Aggregate(bonsai.core.tool.Aggregate): if not element: continue - parts = ifcopenshell.util.element.get_parts(tool.Ifc.get_entity(props.editing_aggregate)) if context.space_data.local_view: bpy.ops.view3d.localview()