Skip to content

Commit 5056dfc

Browse files
authored
Assembly.remove implementation (#1776)
* Initial Assembly.remove implementation * Lint pass * Added test for removing a name that does not exist and renamed the tests for clarity * Probably got overzealous since a child should never be without a parent assembly * Nevermind, mypy fails if the parent is not checked * Added simple, top-down handling of constraint removal * Took constraint removal back out * Added a note to the docstring about the caveats of this new remove method
1 parent ea5068b commit 5056dfc

File tree

2 files changed

+133
-0
lines changed

2 files changed

+133
-0
lines changed

cadquery/assembly.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,41 @@ def add(self, arg, **kwargs):
232232

233233
return self
234234

235+
def remove(self, name: str) -> "Assembly":
236+
"""
237+
Remove a part/subassembly from the current assembly.
238+
239+
:param name: Name of the part/subassembly to be removed
240+
:return: The modified assembly
241+
242+
*NOTE* This method can cause problems with deeply nested assemblies and does not remove
243+
constraints associated with the removed part/subassembly.
244+
"""
245+
246+
# Make sure the part/subassembly is actually part of the assembly
247+
if name not in self.objects:
248+
raise ValueError(f"No object with name '{name}' found in the assembly")
249+
250+
# Get the part/assembly to be removed
251+
to_remove = self.objects[name]
252+
253+
# Remove the part/assembly from the parent's children list
254+
if to_remove.parent:
255+
to_remove.parent.children.remove(to_remove)
256+
257+
# Remove the part/assembly from the assembly's object dictionary
258+
del self.objects[name]
259+
260+
# Remove all descendants from the objects dictionary
261+
for descendant_name in to_remove._flatten().keys():
262+
if descendant_name in self.objects:
263+
del self.objects[descendant_name]
264+
265+
# Update the parent reference
266+
to_remove.parent = None
267+
268+
return self
269+
235270
def _query(self, q: str) -> Tuple[str, Optional[Shape]]:
236271
"""
237272
Execute a selector query on the assembly.

tests/test_assembly.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1705,3 +1705,101 @@ def test_step_export_filesize(tmpdir):
17051705
filesize[i] = stepfile.stat().st_size
17061706

17071707
assert filesize[1] < 1.2 * filesize[0]
1708+
1709+
1710+
def test_assembly_remove_no_name_match():
1711+
"""
1712+
Tests to make sure that removing a part/subassembly with a name that does not exist fails.
1713+
"""
1714+
1715+
assy = cq.Assembly()
1716+
assy.add(box(1, 1, 1), name="part1")
1717+
assy.add(box(2, 2, 2), name="part2")
1718+
1719+
with pytest.raises(ValueError):
1720+
assy.remove("part3")
1721+
1722+
1723+
def test_assembly_remove_part():
1724+
"""
1725+
Tests the ability to remove a part from an assembly.
1726+
"""
1727+
assy = cq.Assembly()
1728+
assy.add(box(1, 1, 1), name="part1")
1729+
assy.add(box(2, 2, 2), name="part2", loc=cq.Location(5.0, 5.0, 5.0))
1730+
1731+
# Make sure we have the correct number of children (2 parts)
1732+
assert len(assy.children) == 2
1733+
assert len(assy.objects) == 3
1734+
1735+
# Remove the first part
1736+
assy.remove("part1")
1737+
1738+
# Make sure we have the correct number of children (1 part)
1739+
assert len(assy.children) == 1
1740+
assert len(assy.objects) == 2
1741+
1742+
1743+
def test_assembly_remove_subassy():
1744+
"""
1745+
Tests the ability to remove a subassembly from an assembly.
1746+
"""
1747+
1748+
# Create the top-level assembly
1749+
assy = cq.Assembly()
1750+
assy.add(box(1, 1, 1), name="loplevel_part1")
1751+
1752+
# Create the subassembly
1753+
subassy = cq.Assembly()
1754+
subassy.add(box(1, 1, 1), name="part1")
1755+
subassy.add(box(2, 2, 2), name="part2", loc=cq.Location(5.0, 5.0, 5.0))
1756+
1757+
# Add the subassembly to the top-level assembly
1758+
assy.add(subassy, name="subassy")
1759+
1760+
# Make sure we have the 1 top-level part and the subassembly
1761+
assert len(assy.children) == 2
1762+
assert len(assy.objects) == 5
1763+
1764+
# Remove the subassembly
1765+
assy.remove("subassy")
1766+
1767+
# Make sure we have the correct number of children (1 part)
1768+
assert len(assy.children) == 1
1769+
assert len(assy.objects) == 2
1770+
1771+
# Recreate the assembly with a nested subassembly
1772+
assy = cq.Assembly()
1773+
assy.add(box(1, 1, 1), name="loplevel_part1")
1774+
subassy = cq.Assembly()
1775+
subassy.add(box(1, 1, 1), name="part1")
1776+
subassy.add(box(2, 2, 2), name="part2", loc=cq.Location(2.0, 2.0, 2.0))
1777+
assy.add(subassy, name="subassy")
1778+
1779+
# Try to remove a part from a subassembly by using the path string
1780+
assert len(assy.children[1].children) == 2
1781+
assy.remove("subassy/part2")
1782+
assert len(assy.children[1].children) == 1
1783+
1784+
1785+
def test_remove_without_parent():
1786+
"""
1787+
Tests the ability to remove a part from an assembly when the part has no parent.
1788+
This may never happen in practice, but the case has to be covered for mypy to pass.
1789+
"""
1790+
1791+
# Create a root assembly
1792+
assy = cq.Assembly(name="root")
1793+
1794+
# Create a part and add it to the assembly
1795+
part = cq.Workplane().box(1, 1, 1)
1796+
assy.add(part, name="part")
1797+
1798+
# Artificially remove the parent to cover a branching test case
1799+
assy.children[0].parent = None
1800+
1801+
# Remove the part
1802+
assy.remove("part")
1803+
1804+
assert len(assy.children) == 1
1805+
assert len(assy.objects) == 1

0 commit comments

Comments
 (0)