From 4110dd0b8313bc65b7b1b53cbda5e17910fe2f58 Mon Sep 17 00:00:00 2001 From: Martin Budden Date: Wed, 25 Aug 2021 16:42:07 +0100 Subject: [PATCH 1/6] Added cylinder 3D primitive. --- cadquery/cq.py | 58 ++++++++++++++++++++++++++++++++++++++++++ doc/apireference.rst | 1 + tests/test_cadquery.py | 30 ++++++++++++++++++++++ 3 files changed, 89 insertions(+) diff --git a/cadquery/cq.py b/cadquery/cq.py index 6ab87ff4..c75db259 100644 --- a/cadquery/cq.py +++ b/cadquery/cq.py @@ -3800,6 +3800,64 @@ class Workplane(object): else: return self.union(spheres, clean=clean) + def cylinder( + self: T, + radius: float, + height: float, + centered: Union[bool, Tuple[bool, bool, bool]] = True, + combine: bool = True, + clean: bool = True, + ) -> T: + + """ + Returns a cylinder with the specified radius and height for each point on the stack + + :param radius: The radius of the cylinder + :type radius: float > 0 + :param height: The height of the cylinder + :type height: float > 0 + :param centered: If True, the cylinder will be centered around the reference point. If False, + the corner of a bounding box around the cylinder will be on the reference point and it + will extend in the positive x, y and z directions. Can also use a 3-tuple to specify + centering along each axis. + :param combine: Whether the results should be combined with other solids on the stack + (and each other) + :type combine: true to combine shapes, false otherwise + :param clean: call :py:meth:`clean` afterwards to have a clean shape + :return: A cylinder object for each point on the stack + + One cylinder is created for each item on the current stack. If no items are on the stack, one + box using the current workplane center is created. + + If combine is true, the result will be a single object on the stack. If a solid was found + in the chain, the result is that solid with all cylinders produced fused onto it otherwise, + the result is the combination of all the produced cylinders. + + If combine is false, the result will be a list of the cylinders produced. + """ + + if isinstance(centered, bool): + centered = (centered, centered, centered) + + offset = Vector() + if not centered[0]: + offset += Vector(radius, 0, 0) + if not centered[1]: + offset += Vector(0, radius, 0) + if centered[2]: + offset += Vector(0, 0, -height/2) + + c = self.circle(radius)._extrude(height).move(Location(offset)) + + # We want a cylinder for each point on the workplane + cylinders = self.eachpoint(lambda loc: c.moved(loc), True) + + # If we don't need to combine everything, just return the created cylinders + if not combine: + return cylinders + else: + return self.union(cylinders, clean=clean) + def wedge( self: T, dx: float, diff --git a/doc/apireference.rst b/doc/apireference.rst index 0f68375e..45a66441 100644 --- a/doc/apireference.rst +++ b/doc/apireference.rst @@ -90,6 +90,7 @@ Some 3-d operations also require an active 2-d workplane, but some do not. Workplane.cutThruAll Workplane.box Workplane.sphere + Workplane.cylinder Workplane.union Workplane.combine Workplane.intersect diff --git a/tests/test_cadquery.py b/tests/test_cadquery.py index d3b2c72d..19a407e7 100644 --- a/tests/test_cadquery.py +++ b/tests/test_cadquery.py @@ -2443,6 +2443,36 @@ class TestCadQuery(BaseTest): self.assertEqual(1, s.solids().size()) self.assertEqual(4, s.faces().size()) + def testCylinderDefaults(self): + s = Workplane("XY").cylinder(10, 20) + self.saveModel(s) + self.assertEqual(1, s.size()) + self.assertEqual(1, s.solids().size()) + self.assertEqual(3, s.faces().size()) + self.assertEqual(2, s.vertices().size()) + self.assertTupleAlmostEquals(s.val().Center().toTuple(), (0, 0, 0), 3) + + def testCylinderCentering(self): + radius = 10 + height = 40 + b = (True, False) + expected_x = (0, radius) + expected_y = (0, radius) + expected_z = (0, height/2) + for (xopt, xval), (yopt, yval), (zopt, zval) in product( + zip(b, expected_x), zip(b, expected_y), zip(b, expected_z) + ): + s = Workplane("XY").cylinder(radius, height, centered=(xopt, yopt, zopt)) + self.assertEqual(1, s.size()) + self.assertTupleAlmostEquals(s.val().Center().toTuple(), (xval, yval, zval), 3) + # check centered=True produces the same result as centered=(True, True, True) + for val in b: + s0 = Workplane("XY").cylinder(radius, height, centered=val) + self.assertEqual(s0.size(), 1) + s1 = Workplane("XY").cylinder(radius, height, centered=(val, val, val)) + self.assertEqual(s1.size(), 1) + self.assertTupleAlmostEquals(s0.val().Center().toTuple(), s1.val().Center().toTuple(), 3) + def testWedgeDefaults(self): s = Workplane("XY").wedge(10, 10, 10, 5, 5, 5, 5) self.saveModel(s) From 2efa1f0b0030c0ecdd8e020f5ba5f6d0c522856b Mon Sep 17 00:00:00 2001 From: Martin Budden Date: Wed, 25 Aug 2021 17:30:24 +0100 Subject: [PATCH 2/6] Code reformat. --- cadquery/cq.py | 14 +++++++------- tests/test_cadquery.py | 10 +++++++--- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/cadquery/cq.py b/cadquery/cq.py index c75db259..939448ef 100644 --- a/cadquery/cq.py +++ b/cadquery/cq.py @@ -3801,13 +3801,13 @@ class Workplane(object): return self.union(spheres, clean=clean) def cylinder( - self: T, - radius: float, - height: float, - centered: Union[bool, Tuple[bool, bool, bool]] = True, - combine: bool = True, - clean: bool = True, - ) -> T: + self: T, + radius: float, + height: float, + centered: Union[bool, Tuple[bool, bool, bool]] = True, + combine: bool = True, + clean: bool = True, + ) -> T: """ Returns a cylinder with the specified radius and height for each point on the stack diff --git a/tests/test_cadquery.py b/tests/test_cadquery.py index 19a407e7..ca8fc139 100644 --- a/tests/test_cadquery.py +++ b/tests/test_cadquery.py @@ -2458,20 +2458,24 @@ class TestCadQuery(BaseTest): b = (True, False) expected_x = (0, radius) expected_y = (0, radius) - expected_z = (0, height/2) + expected_z = (0, height / 2) for (xopt, xval), (yopt, yval), (zopt, zval) in product( zip(b, expected_x), zip(b, expected_y), zip(b, expected_z) ): s = Workplane("XY").cylinder(radius, height, centered=(xopt, yopt, zopt)) self.assertEqual(1, s.size()) - self.assertTupleAlmostEquals(s.val().Center().toTuple(), (xval, yval, zval), 3) + self.assertTupleAlmostEquals( + s.val().Center().toTuple(), (xval, yval, zval), 3 + ) # check centered=True produces the same result as centered=(True, True, True) for val in b: s0 = Workplane("XY").cylinder(radius, height, centered=val) self.assertEqual(s0.size(), 1) s1 = Workplane("XY").cylinder(radius, height, centered=(val, val, val)) self.assertEqual(s1.size(), 1) - self.assertTupleAlmostEquals(s0.val().Center().toTuple(), s1.val().Center().toTuple(), 3) + self.assertTupleAlmostEquals( + s0.val().Center().toTuple(), s1.val().Center().toTuple(), 3 + ) def testWedgeDefaults(self): s = Workplane("XY").wedge(10, 10, 10, 5, 5, 5, 5) From 498f5d78d610c53ab210a7c4d1ec7474bd44a862 Mon Sep 17 00:00:00 2001 From: Martin Budden Date: Wed, 25 Aug 2021 17:52:47 +0100 Subject: [PATCH 3/6] Changed to use Solid.makeCylinder. --- cadquery/cq.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/cadquery/cq.py b/cadquery/cq.py index 939448ef..48998e9e 100644 --- a/cadquery/cq.py +++ b/cadquery/cq.py @@ -3804,11 +3804,12 @@ class Workplane(object): self: T, radius: float, height: float, + direct: Vector = Vector(0, 0, 1), + angle: float = 360, centered: Union[bool, Tuple[bool, bool, bool]] = True, combine: bool = True, clean: bool = True, ) -> T: - """ Returns a cylinder with the specified radius and height for each point on the stack @@ -3816,6 +3817,10 @@ class Workplane(object): :type radius: float > 0 :param height: The height of the cylinder :type height: float > 0 + :param direct: The direction axis for the creation of the cylinder + :type direct: A three-tuple + :param angle: The angle to sweep the cylinder arc through + :type angle: float > 0 :param centered: If True, the cylinder will be centered around the reference point. If False, the corner of a bounding box around the cylinder will be on the reference point and it will extend in the positive x, y and z directions. Can also use a 3-tuple to specify @@ -3845,12 +3850,12 @@ class Workplane(object): if not centered[1]: offset += Vector(0, radius, 0) if centered[2]: - offset += Vector(0, 0, -height/2) + offset += Vector(0, 0, -height / 2) - c = self.circle(radius)._extrude(height).move(Location(offset)) + s = Solid.makeCylinder(radius, height, offset, direct, angle) # We want a cylinder for each point on the workplane - cylinders = self.eachpoint(lambda loc: c.moved(loc), True) + cylinders = self.eachpoint(lambda loc: s.moved(loc), True) # If we don't need to combine everything, just return the created cylinders if not combine: From 727ab27c7256bc830c6a80fd4c91d3a400d9e833 Mon Sep 17 00:00:00 2001 From: Martin Budden Date: Wed, 25 Aug 2021 20:24:50 +0100 Subject: [PATCH 4/6] Swapped cylinder height and radius parameters to match cone. --- cadquery/cq.py | 6 +++--- doc/roadmap.rst | 3 +-- tests/test_cadquery.py | 8 ++++---- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/cadquery/cq.py b/cadquery/cq.py index 48998e9e..ea0df3b5 100644 --- a/cadquery/cq.py +++ b/cadquery/cq.py @@ -3802,8 +3802,8 @@ class Workplane(object): def cylinder( self: T, - radius: float, height: float, + radius: float, direct: Vector = Vector(0, 0, 1), angle: float = 360, centered: Union[bool, Tuple[bool, bool, bool]] = True, @@ -3813,10 +3813,10 @@ class Workplane(object): """ Returns a cylinder with the specified radius and height for each point on the stack - :param radius: The radius of the cylinder - :type radius: float > 0 :param height: The height of the cylinder :type height: float > 0 + :param radius: The radius of the cylinder + :type radius: float > 0 :param direct: The direction axis for the creation of the cylinder :type direct: A three-tuple :param angle: The angle to sweep the cylinder arc through diff --git a/doc/roadmap.rst b/doc/roadmap.rst index c81f73ec..2d52d7c4 100644 --- a/doc/roadmap.rst +++ b/doc/roadmap.rst @@ -81,9 +81,8 @@ rotation/transform that return a copy primitive creation Need primitive creation for: * cone - * cylinder * torus * wedge extrude/cut up to surface - allow a cut or extrude to terminate at another surface, rather than either through all or a fixed distance \ No newline at end of file + allow a cut or extrude to terminate at another surface, rather than either through all or a fixed distance diff --git a/tests/test_cadquery.py b/tests/test_cadquery.py index ca8fc139..d8394875 100644 --- a/tests/test_cadquery.py +++ b/tests/test_cadquery.py @@ -2444,7 +2444,7 @@ class TestCadQuery(BaseTest): self.assertEqual(4, s.faces().size()) def testCylinderDefaults(self): - s = Workplane("XY").cylinder(10, 20) + s = Workplane("XY").cylinder(20, 10) self.saveModel(s) self.assertEqual(1, s.size()) self.assertEqual(1, s.solids().size()) @@ -2462,16 +2462,16 @@ class TestCadQuery(BaseTest): for (xopt, xval), (yopt, yval), (zopt, zval) in product( zip(b, expected_x), zip(b, expected_y), zip(b, expected_z) ): - s = Workplane("XY").cylinder(radius, height, centered=(xopt, yopt, zopt)) + s = Workplane("XY").cylinder(height, radius, centered=(xopt, yopt, zopt)) self.assertEqual(1, s.size()) self.assertTupleAlmostEquals( s.val().Center().toTuple(), (xval, yval, zval), 3 ) # check centered=True produces the same result as centered=(True, True, True) for val in b: - s0 = Workplane("XY").cylinder(radius, height, centered=val) + s0 = Workplane("XY").cylinder(height, radius, centered=val) self.assertEqual(s0.size(), 1) - s1 = Workplane("XY").cylinder(radius, height, centered=(val, val, val)) + s1 = Workplane("XY").cylinder(height, radius, centered=(val, val, val)) self.assertEqual(s1.size(), 1) self.assertTupleAlmostEquals( s0.val().Center().toTuple(), s1.val().Center().toTuple(), 3 From 71e3e407938e112068eb688656bd320ac6061aff Mon Sep 17 00:00:00 2001 From: Martin Budden Date: Tue, 31 Aug 2021 17:52:00 +0100 Subject: [PATCH 5/6] Update as per suggestions in review. --- cadquery/cq.py | 2 +- tests/test_cadquery.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/cadquery/cq.py b/cadquery/cq.py index ea0df3b5..f7d3e87d 100644 --- a/cadquery/cq.py +++ b/cadquery/cq.py @@ -3832,7 +3832,7 @@ class Workplane(object): :return: A cylinder object for each point on the stack One cylinder is created for each item on the current stack. If no items are on the stack, one - box using the current workplane center is created. + cylinder using the current workplane center is created. If combine is true, the result will be a single object on the stack. If a solid was found in the chain, the result is that solid with all cylinders produced fused onto it otherwise, diff --git a/tests/test_cadquery.py b/tests/test_cadquery.py index d8394875..9eb06681 100644 --- a/tests/test_cadquery.py +++ b/tests/test_cadquery.py @@ -2445,7 +2445,6 @@ class TestCadQuery(BaseTest): def testCylinderDefaults(self): s = Workplane("XY").cylinder(20, 10) - self.saveModel(s) self.assertEqual(1, s.size()) self.assertEqual(1, s.solids().size()) self.assertEqual(3, s.faces().size()) From 00083ed3343b20c18d17a67a81acd3303db4c72e Mon Sep 17 00:00:00 2001 From: Martin Budden Date: Tue, 31 Aug 2021 17:59:05 +0100 Subject: [PATCH 6/6] Improved cylinder docstring. --- cadquery/cq.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cadquery/cq.py b/cadquery/cq.py index f7d3e87d..5afd1d51 100644 --- a/cadquery/cq.py +++ b/cadquery/cq.py @@ -3832,7 +3832,7 @@ class Workplane(object): :return: A cylinder object for each point on the stack One cylinder is created for each item on the current stack. If no items are on the stack, one - cylinder using the current workplane center is created. + cylinder is created using the current workplane center. If combine is true, the result will be a single object on the stack. If a solid was found in the chain, the result is that solid with all cylinders produced fused onto it otherwise,