diff --git a/cadquery/cq.py b/cadquery/cq.py index c3a843d84..c8c3853c0 100644 --- a/cadquery/cq.py +++ b/cadquery/cq.py @@ -1629,6 +1629,39 @@ def lineTo(self: T, x: float, y: float, forConstruction: bool = False) -> T: return self.newObject([p]) + def bezier( + self: T, + listOfXYTuple: Iterable[VectorLike], + forConstruction: bool = False, + includeCurrent: bool = False, + makeWire: bool = False, + ) -> T: + """ + Make a cubic Bézier curve by the provided points (2D or 3D). + + :param listOfXYTuple: Bezier control points and end point. + All points except the last point are Bezier control points, + and the last point is the end point + :param includeCurrent: Use the current point as a starting point of the curve + :param makeWire: convert the resulting bezier edge to a wire + :return: a Workplane object with the current point at the end of the bezier + + The Bézier Will begin at either current point or the first point + of listOfXYTuple, and end with the last point of listOfXYTuple + """ + allPoints = self._toVectors(listOfXYTuple, includeCurrent) + + e = Edge.makeBezier(allPoints) + + if makeWire: + rv_w = Wire.assembleEdges([e]) + if not forConstruction: + self._addPendingWire(rv_w) + elif not forConstruction: + self._addPendingEdge(e) + + return self.newObject([rv_w if makeWire else e]) + # line a specified incremental amount from current point def line(self: T, xDist: float, yDist: float, forConstruction: bool = False) -> T: """ diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 9e312259b..56e0cf70b 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -56,7 +56,7 @@ ) # Array of points (used for B-spline construction): -from OCP.TColgp import TColgp_HArray1OfPnt, TColgp_HArray2OfPnt +from OCP.TColgp import TColgp_HArray1OfPnt, TColgp_HArray2OfPnt, TColgp_Array1OfPnt # Array of vectors (used for B-spline interpolation): from OCP.TColgp import TColgp_Array1OfVec @@ -146,6 +146,7 @@ ) from OCP.Geom import ( + Geom_BezierCurve, Geom_ConicalSurface, Geom_CylindricalSurface, Geom_Surface, @@ -2091,6 +2092,26 @@ def makeLine(cls, v1: VectorLike, v2: VectorLike) -> "Edge": BRepBuilderAPI_MakeEdge(Vector(v1).toPnt(), Vector(v2).toPnt()).Edge() ) + @classmethod + def makeBezier(cls, points: List[Vector]) -> "Edge": + """ + Create a cubic Bézier Curve from the points. + + :param points: a list of Vectors that represent the points. + The edge will pass through the first and the last point, + and the inner points are Bézier control points. + :return: An edge + """ + + # Convert to a TColgp_Array1OfPnt + arr = TColgp_Array1OfPnt(1, len(points)) + for i, v in enumerate(points): + arr.SetValue(i + 1, Vector(v).toPnt()) + + bez = Geom_BezierCurve(arr) + + return cls(BRepBuilderAPI_MakeEdge(bez).Edge()) + class Wire(Shape, Mixin1D): """ diff --git a/cadquery/sketch.py b/cadquery/sketch.py index 1b0bd2888..fce851fcb 100644 --- a/cadquery/sketch.py +++ b/cadquery/sketch.py @@ -859,6 +859,23 @@ def spline( return self.spline(pts, None, False, tag, forConstruction) + def bezier( + self: T, + pts: Iterable[Point], + tag: Optional[str] = None, + forConstruction: bool = False, + ) -> T: + """ + Construct an bezier curve. + + The edge will pass through the last points, and the inner points + are bezier control points. + """ + p1 = self._endPoint() + val = Edge.makeBezier([Vector(*p) for p in pts]) + + return self.edge(val, tag, forConstruction) + def close(self: T, tag: Optional[str] = None) -> T: """ Connect last edge to the first one. diff --git a/tests/test_sketch.py b/tests/test_sketch.py index bdd104452..dc2a2788e 100644 --- a/tests/test_sketch.py +++ b/tests/test_sketch.py @@ -470,6 +470,19 @@ def test_edge_interface(): assert len(s6.vertices()._selection) == 1 +def test_bezier(): + s1 = ( + Sketch() + .segment((0, 0), (0, 0.5)) + .bezier(((0, 0.5), (-1, 2), (1, 0.5), (5, 0))) + .bezier(((5, 0), (1, -0.5), (-1, -2), (0, -0.5))) + .close() + .assemble() + ) + assert s1._faces.Area() == approx(5.35) + # What other kind of tests can we do? + + def test_assemble(): s1 = Sketch() diff --git a/tests/test_workplanes.py b/tests/test_workplanes.py index 0468601d6..535113001 100644 --- a/tests/test_workplanes.py +++ b/tests/test_workplanes.py @@ -250,3 +250,52 @@ def test_mirror_face(self): (bbBox.xlen, bbBox.ylen, bbBox.zlen), (1.0, 1.0, 1.0), 4 ) self.assertAlmostEqual(r.findSolid().Volume(), 1.0, 5) + + def test_bezier_curve(self): + # Quadratic bezier + r = ( + Workplane("XZ") + .bezier([(0, 0), (1, 2), (5, 0)]) + .bezier([(1, -2), (0, 0)], includeCurrent=True) + .close() + .extrude(1) + ) + + bbBox = r.findSolid().BoundingBox() + # Why is the bounding box larger than expected? + self.assertTupleAlmostEquals((bbBox.xlen, bbBox.ylen, bbBox.zlen), (5, 1, 2), 1) + self.assertAlmostEqual(r.findSolid().Volume(), 6.6666667, 4) + + r = Workplane("XY").bezier([(0, 0), (1, 2), (2, -1), (5, 0)]) + self.assertTrue(len(r.ctx.pendingEdges) == 1) + r = ( + r.lineTo(5, -0.1) + .bezier([(2, -3), (1, 0), (0, 0)], includeCurrent=True) + .close() + .extrude(1) + ) + + bbBox = r.findSolid().BoundingBox() + self.assertTupleAlmostEquals( + (bbBox.xlen, bbBox.ylen, bbBox.zlen), (5, 2.06767, 1), 1 + ) + self.assertAlmostEqual(r.findSolid().Volume(), 4.975, 4) + + # Test makewire by translate and loft example like in + # the documentation + r = Workplane("XY").bezier([(0, 0), (1, 2), (1, -1), (0, 0)], makeWire=True) + + self.assertTrue(len(r.ctx.pendingWires) == 1) + r = r.translate((0, 0, 0.2)).toPending().loft() + self.assertAlmostEqual(r.findSolid().Volume(), 0.09, 4) + + # Finally test forConstruction + r = Workplane("XY").bezier( + [(0, 0), (1, 2), (1, -1), (0, 0)], makeWire=True, forConstruction=True + ) + self.assertTrue(len(r.ctx.pendingWires) == 0) + + r = Workplane("XY").bezier( + [(0, 0), (1, 2), (2, -1), (5, 0)], forConstruction=True + ) + self.assertTrue(len(r.ctx.pendingEdges) == 0)