From 99b2aef1bd24f0d6c070842b5e716df5f6ce8f7b Mon Sep 17 00:00:00 2001 From: Petru Conduraru Date: Thu, 16 Jul 2026 14:50:41 +0300 Subject: [PATCH] Fix #6663: keep a duplicated nested aggregate inside its parent aggregate Duplicating a linked aggregate (Ctrl+Shift+D) that is itself nested inside another aggregate dropped the copy outside its parent instead of keeping it nested. Root cause: DuplicateMoveLinkedAggregate.execute_ifc_duplicate_linked_aggregate_operator called tool.Root.recreate_aggregate(old_to_new) a second time, after execute_ifc_duplicate_operator had already called it once (via duplicate_ifc_objects) and, for the case where the parent aggregate itself was not part of the duplication set, had already correctly restored the new nested aggregate's assignment to that original parent. On the second, redundant call, recreate_aggregate looks up the parent aggregate in old_to_new again, still does not find it there (since the parent was never duplicated), and its "parent not found" branch actively unassigns the just-restored link, stranding the duplicate outside its parent. Fix: drop the redundant second call. The plain duplicate path already handles both cases correctly (parent duplicated and parent not duplicated). Verified live in headless Blender 5.2 / Bonsai on the repo's own test/files/linked-aggregates.ifc: nested Wall_01/Wall_02 wrapped into a new aggregate "Nested" inside the existing "Assembly" aggregate, then duplicated via bim.object_duplicate_move_linked_aggregate. Before the fix, the new "Nested_01" aggregate had no parent aggregate; after the fix it correctly remains aggregated by "Assembly". Also added a regression scenario to test/bim/feature/geometry.feature covering this exact case, manually traced against the existing step definitions (the full pytest-bdd harness needs a network-provisioned Blender python to run locally, so it hasn't been executed through pytest itself; CI will run it). black + ruff clean. Generated with the assistance of an AI coding tool. --- src/bonsai/bonsai/bim/module/geometry/operator.py | 9 +++++++-- src/bonsai/test/bim/feature/geometry.feature | 15 +++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/geometry/operator.py b/src/bonsai/bonsai/bim/module/geometry/operator.py index 9bc0566532..6d62d6cccf 100644 --- a/src/bonsai/bonsai/bim/module/geometry/operator.py +++ b/src/bonsai/bonsai/bim/module/geometry/operator.py @@ -1531,8 +1531,13 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator): old_to_new = OverrideDuplicateMove.execute_ifc_duplicate_operator(self, context, linked=True) all_old_to_new.update(old_to_new) # Collect all duplicates - # Recreate aggregate structure - tool.Root.recreate_aggregate(old_to_new) + # Note: execute_ifc_duplicate_operator() above already recreates the aggregate + # structure for us (including reassigning to an unduplicated parent aggregate, see + # its "Restore parent aggregate relationships" step). Calling + # tool.Root.recreate_aggregate() again here is not just redundant: since the parent + # aggregate is never part of `old_to_new` when it wasn't itself duplicated, + # recreate_aggregate()'s "parent not found" branch actively *unassigns* the just + # restored parent link, stranding a duplicated nested aggregate outside its parent (#6663). # Copy linked aggregate data copy_linked_aggregate_data(old_to_new) diff --git a/src/bonsai/test/bim/feature/geometry.feature b/src/bonsai/test/bim/feature/geometry.feature index 1236f416c8..fa2a1dbafd 100644 --- a/src/bonsai/test/bim/feature/geometry.feature +++ b/src/bonsai/test/bim/feature/geometry.feature @@ -610,6 +610,21 @@ Scenario: Duplicate linked aggregate And the object "Assembly_01" exists Then the object "IfcElementAssembly/Assembly" and "Assembly_01" belong to the same Linked Aggregate Group +Scenario: Duplicate linked aggregate - a nested aggregate stays inside its parent aggregate + Given I load the IFC test file "/test/files/linked-aggregates.ifc" + And the object "IfcWall/Wall_01" is selected + And additionally the object "IfcWall/Wall_02" is selected + When I press "bim.add_aggregate(aggregate_name='Nested')" + Then the object "IfcElementAssembly/Nested" is aggregated by object "IfcElementAssembly/Assembly" + When the object "IfcWall/Wall_01" is selected + And I duplicate linked aggregate the selected objects + Then the object "IfcWall/Wall_01.001" exists + And the object "IfcWall/Wall_02.001" exists + # msgbus updates don't happen in background mode, so obj is not renamed to "IfcElementAssembly/Nested_01" + And the object "Nested_01" exists + And the object "IfcWall/Wall_01.001" is aggregated by object "Nested_01" + And the object "Nested_01" is aggregated by object "IfcElementAssembly/Assembly" + Scenario: Refresh linked aggregate Given I load the IFC test file "/test/files/linked-aggregates.ifc" And the object "IfcWall/Wall_01" is selected