Compare commits

...

1 Commits

Author SHA1 Message Date
Petru Conduraru 99b2aef1bd 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.
2026-07-16 14:50:41 +03:00
2 changed files with 22 additions and 2 deletions
@@ -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)
@@ -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