From 0bc0f3ea0a82af801b5e0f610e1a128ff09dd0f4 Mon Sep 17 00:00:00 2001 From: Aman Jain Date: Sat, 18 Jul 2026 17:48:09 -0700 Subject: [PATCH 1/6] fix: make box() parameter names consistent with Workplane.box and Solid.makeBox The standalone `box(w, l, h)` function used `w` (width) for the X axis and `l` (length) for the Y axis, which is the opposite convention from `Workplane.box(length, width, height)` and `Solid.makeBox(length, width, height)` where length=X and width=Y. Rename parameters to `box(length, width, height)` with docstring specifying axis mapping, matching the rest of the API. This is backward-compatible since all existing callers use positional arguments. Addresses #2011 --- cadquery/occ_impl/shapes.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 7d3c665a9..72c5cf5db 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -6553,14 +6553,21 @@ def plane() -> Face: return _shape(BRepBuilderAPI_MakeFace(pln_geom, -INF, INF, -INF, INF).Face(), Face) -def box(w: float, l: float, h: float) -> Solid: +def box(length: float, width: float, height: float) -> Solid: """ - Construct a solid box. + Construct a solid box centered on the XY plane. + + :param length: box size along the X axis + :param width: box size along the Y axis + :param height: box size along the Z axis """ return _shape( BRepPrimAPI_MakeBox( - gp_Ax2(Vector(-w / 2, -l / 2, 0).toPnt(), Vector(0, 0, 1).toDir()), w, l, h + gp_Ax2(Vector(-length / 2, -width / 2, 0).toPnt(), Vector(0, 0, 1).toDir()), + length, + width, + height, ).Shape(), Solid, ) From 7de0785db8ae052665976d225d79288ba2ec966c Mon Sep 17 00:00:00 2001 From: Aman Jain Date: Sat, 18 Jul 2026 17:49:33 -0700 Subject: [PATCH 2/6] feat: add toBOM() method to Assembly for bill of materials export Adds a toBOM() method to the Assembly class that generates a flat list of BOM line items from the assembly tree. Each entry includes the component name, nesting level, and whether it has geometry attached. This provides a structured way to extract a bill of materials from a CadQuery assembly, enabling integration with inventory/PLM systems like InvenTree or other downstream manufacturing tools. --- cadquery/assembly.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/cadquery/assembly.py b/cadquery/assembly.py index fdf98dd73..0e79f3f55 100644 --- a/cadquery/assembly.py +++ b/cadquery/assembly.py @@ -627,6 +627,37 @@ def export( return self + def toBOM( + self, + indent: int = 0, + _result: Optional[List[Dict[str, Any]]] = None, + ) -> List[Dict[str, Any]]: + """ + Generate a Bill of Materials (BOM) for this assembly. + + Returns a flat list of dictionaries, one per component, with fields: + - name: component name + - level: nesting depth (0 = root) + - has_shape: whether the component has geometry + + :param indent: current nesting level (used internally for recursion) + :return: list of BOM line items + """ + + if _result is None: + _result = [] + + _result.append({ + "name": self.name, + "level": indent, + "has_shape": self.obj is not None, + }) + + for child in self.children: + child.toBOM(indent=indent + 1, _result=_result) + + return _result + @classmethod def importStep(cls, path: str, unit: UnitLiterals = "MM") -> Self: """ From 86722f852034588b437e187b1ff5f2bcfcffed7d Mon Sep 17 00:00:00 2001 From: Aman Jain Date: Sun, 2 Aug 2026 08:04:06 -0700 Subject: [PATCH 3/6] style: fix black formatting in toBOM method --- cadquery/assembly.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/cadquery/assembly.py b/cadquery/assembly.py index 0e79f3f55..3d72846e3 100644 --- a/cadquery/assembly.py +++ b/cadquery/assembly.py @@ -647,11 +647,13 @@ def toBOM( if _result is None: _result = [] - _result.append({ - "name": self.name, - "level": indent, - "has_shape": self.obj is not None, - }) + _result.append( + { + "name": self.name, + "level": indent, + "has_shape": self.obj is not None, + } + ) for child in self.children: child.toBOM(indent=indent + 1, _result=_result) From 21f676f1d7566cb5ea771ebcb471e5177c365bd8 Mon Sep 17 00:00:00 2001 From: Aman Jain Date: Sun, 2 Aug 2026 21:51:33 -0700 Subject: [PATCH 4/6] style: fix line length to pass black check --- cadquery/occ_impl/shapes.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 72c5cf5db..8ed1e9b9d 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -6564,7 +6564,10 @@ def box(length: float, width: float, height: float) -> Solid: return _shape( BRepPrimAPI_MakeBox( - gp_Ax2(Vector(-length / 2, -width / 2, 0).toPnt(), Vector(0, 0, 1).toDir()), + gp_Ax2( + Vector(-length / 2, -width / 2, 0).toPnt(), + Vector(0, 0, 1).toDir(), + ), length, width, height, From f0c6694dbf887d9b42cb351796f102a6fbce6236 Mon Sep 17 00:00:00 2001 From: Aman Jain Date: Sun, 2 Aug 2026 22:09:37 -0700 Subject: [PATCH 5/6] style: match project's custom black formatting exactly --- cadquery/assembly.py | 10 ++-------- cadquery/occ_impl/shapes.py | 3 +-- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/cadquery/assembly.py b/cadquery/assembly.py index 3d72846e3..630bac69c 100644 --- a/cadquery/assembly.py +++ b/cadquery/assembly.py @@ -628,9 +628,7 @@ def export( return self def toBOM( - self, - indent: int = 0, - _result: Optional[List[Dict[str, Any]]] = None, + self, indent: int = 0, _result: Optional[List[Dict[str, Any]]] = None, ) -> List[Dict[str, Any]]: """ Generate a Bill of Materials (BOM) for this assembly. @@ -648,11 +646,7 @@ def toBOM( _result = [] _result.append( - { - "name": self.name, - "level": indent, - "has_shape": self.obj is not None, - } + {"name": self.name, "level": indent, "has_shape": self.obj is not None,} ) for child in self.children: diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 8ed1e9b9d..a0dbd74f7 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -6565,8 +6565,7 @@ def box(length: float, width: float, height: float) -> Solid: return _shape( BRepPrimAPI_MakeBox( gp_Ax2( - Vector(-length / 2, -width / 2, 0).toPnt(), - Vector(0, 0, 1).toDir(), + Vector(-length / 2, -width / 2, 0).toPnt(), Vector(0, 0, 1).toDir(), ), length, width, From 8848bf496e11231fdbf5bbd4c766df86ebd33f2f Mon Sep 17 00:00:00 2001 From: Aman Jain Date: Sun, 2 Aug 2026 22:38:05 -0700 Subject: [PATCH 6/6] test: add unit test for toBOM() method --- tests/test_assembly.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/test_assembly.py b/tests/test_assembly.py index 08d4a5f21..72fd524d9 100644 --- a/tests/test_assembly.py +++ b/tests/test_assembly.py @@ -2603,3 +2603,21 @@ def test_name_geometries(tmpdir): assert len([l for l in lines if "top_face" in l]) == 2 assert len([l for l in lines if "plane_" in l]) == 2 assert len([l for l in lines if "seg_" in l]) == 3 + + +def test_toBOM(): + + assy = cq.Assembly(name="root") + assy.add(box(1, 1, 1), name="part1") + + subassy = cq.Assembly(name="sub") + subassy.add(box(2, 2, 2), name="part2") + assy.add(subassy, name="sub") + + bom = assy.toBOM() + + assert len(bom) == 4 + assert bom[0] == {"name": "root", "level": 0, "has_shape": False} + assert bom[1] == {"name": "part1", "level": 1, "has_shape": True} + assert bom[2] == {"name": "sub", "level": 1, "has_shape": False} + assert bom[3] == {"name": "part2", "level": 2, "has_shape": True}