diff --git a/cadquery/assembly.py b/cadquery/assembly.py index 4083a271c..0e76d9d81 100644 --- a/cadquery/assembly.py +++ b/cadquery/assembly.py @@ -281,26 +281,40 @@ def remove(self, name: str) -> "Assembly": """ Remove a part/subassembly from the current assembly. - :param name: Name of the part/subassembly to be removed + :param name: Name of the part/subassembly to be removed. The entire + assembly tree is searched, so the object does not need to be a + direct child. :return: The modified assembly *NOTE* This method can cause problems with deeply nested assemblies and does not remove constraints associated with the removed part/subassembly. """ - # Make sure the part/subassembly is actually part of the assembly - if name not in self.objects: - raise ValueError(f"No object with name '{name}' found in the assembly") + # Try exact key match first (supports full paths like "sub/part1"), + # then fall back to searching by simple name across the tree + if name in self.objects: + key = name + else: + key = None + for k, v in self.objects.items(): + if v.name == name: + key = k + break + + if key is None: + raise ValueError( + f"No object with name '{name}' found in the assembly" + ) # Get the part/assembly to be removed - to_remove = self.objects[name] + to_remove = self.objects[key] # Remove the part/assembly from the parent's children list if to_remove.parent: to_remove.parent.children.remove(to_remove) # Remove the part/assembly from the assembly's object dictionary - del self.objects[name] + del self.objects[key] # Remove all descendants from the objects dictionary for descendant_name in to_remove._flatten().keys(): diff --git a/tests/test_assembly.py b/tests/test_assembly.py index b8201731e..83b31b24a 100644 --- a/tests/test_assembly.py +++ b/tests/test_assembly.py @@ -2385,6 +2385,31 @@ def test_remove_without_parent(): assert len(assy.objects) == 1 +def test_assembly_remove_nested(): + """ + Tests the ability to remove a nested part from an assembly by name, + without knowing the full path (issue #2013). + """ + + outer = cq.Assembly(name="outer") + inner = cq.Assembly(name="inner") + inner.add(cq.Workplane("XY").box(1, 1, 1), name="box_a") + inner.add(cq.Workplane("XY").box(2, 2, 2), name="box_b") + outer.add(inner) + + # Should be: outer, inner, box_a, box_b + assert len(outer.objects) == 4 + + # Remove a nested part by simple name + outer.remove("box_a") + + assert len(outer.objects) == 3 + assert "box_a" not in [v.name for v in outer.objects.values()] + + # The other nested part should still be there + assert "box_b" in [v.name for v in outer.objects.values()] + + def test_step_color(tmpdir): """ Checks color handling for STEP export.