diff --git a/cadquery/cq.py b/cadquery/cq.py index b2af0678..8b636917 100644 --- a/cadquery/cq.py +++ b/cadquery/cq.py @@ -3800,6 +3800,69 @@ class Workplane(object): else: return self.union(spheres, clean=clean) + def cylinder( + self: T, + height: float, + radius: 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 + + :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 + :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 + 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 + 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, + 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) + + s = Solid.makeCylinder(radius, height, offset, direct, angle) + + # We want a cylinder for each point on the workplane + 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: + 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 21734e06..8f61fdc6 100644 --- a/doc/apireference.rst +++ b/doc/apireference.rst @@ -90,6 +90,7 @@ Some 3D operations also require an active 2D workplane, but some do not. Workplane.cutThruAll Workplane.box Workplane.sphere + Workplane.cylinder Workplane.union Workplane.combine Workplane.intersect diff --git a/doc/roadmap.rst b/doc/roadmap.rst index f1ff49b9..a1bc6cc2 100644 --- a/doc/roadmap.rst +++ b/doc/roadmap.rst @@ -81,7 +81,6 @@ rotation/transform that return a copy primitive creation Need primitive creation for: * cone - * cylinder * torus * wedge diff --git a/tests/test_cadquery.py b/tests/test_cadquery.py index 6396825e..f717ad7f 100644 --- a/tests/test_cadquery.py +++ b/tests/test_cadquery.py @@ -2443,6 +2443,39 @@ class TestCadQuery(BaseTest): self.assertEqual(1, s.solids().size()) self.assertEqual(4, s.faces().size()) + def testCylinderDefaults(self): + s = Workplane("XY").cylinder(20, 10) + 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(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(height, radius, centered=val) + self.assertEqual(s0.size(), 1) + 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 + ) + def testWedgeDefaults(self): s = Workplane("XY").wedge(10, 10, 10, 5, 5, 5, 5) self.saveModel(s)