Skip to content

Commit 76fbff7

Browse files
authored
Add Bézier curve support to Workplane and Sketch (#1529)
1 parent 00fdd71 commit 76fbff7

File tree

5 files changed

+134
-1
lines changed

5 files changed

+134
-1
lines changed

cadquery/cq.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1629,6 +1629,39 @@ def lineTo(self: T, x: float, y: float, forConstruction: bool = False) -> T:
16291629

16301630
return self.newObject([p])
16311631

1632+
def bezier(
1633+
self: T,
1634+
listOfXYTuple: Iterable[VectorLike],
1635+
forConstruction: bool = False,
1636+
includeCurrent: bool = False,
1637+
makeWire: bool = False,
1638+
) -> T:
1639+
"""
1640+
Make a cubic Bézier curve by the provided points (2D or 3D).
1641+
1642+
:param listOfXYTuple: Bezier control points and end point.
1643+
All points except the last point are Bezier control points,
1644+
and the last point is the end point
1645+
:param includeCurrent: Use the current point as a starting point of the curve
1646+
:param makeWire: convert the resulting bezier edge to a wire
1647+
:return: a Workplane object with the current point at the end of the bezier
1648+
1649+
The Bézier Will begin at either current point or the first point
1650+
of listOfXYTuple, and end with the last point of listOfXYTuple
1651+
"""
1652+
allPoints = self._toVectors(listOfXYTuple, includeCurrent)
1653+
1654+
e = Edge.makeBezier(allPoints)
1655+
1656+
if makeWire:
1657+
rv_w = Wire.assembleEdges([e])
1658+
if not forConstruction:
1659+
self._addPendingWire(rv_w)
1660+
elif not forConstruction:
1661+
self._addPendingEdge(e)
1662+
1663+
return self.newObject([rv_w if makeWire else e])
1664+
16321665
# line a specified incremental amount from current point
16331666
def line(self: T, xDist: float, yDist: float, forConstruction: bool = False) -> T:
16341667
"""

cadquery/occ_impl/shapes.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
)
5757

5858
# Array of points (used for B-spline construction):
59-
from OCP.TColgp import TColgp_HArray1OfPnt, TColgp_HArray2OfPnt
59+
from OCP.TColgp import TColgp_HArray1OfPnt, TColgp_HArray2OfPnt, TColgp_Array1OfPnt
6060

6161
# Array of vectors (used for B-spline interpolation):
6262
from OCP.TColgp import TColgp_Array1OfVec
@@ -146,6 +146,7 @@
146146
)
147147

148148
from OCP.Geom import (
149+
Geom_BezierCurve,
149150
Geom_ConicalSurface,
150151
Geom_CylindricalSurface,
151152
Geom_Surface,
@@ -2091,6 +2092,26 @@ def makeLine(cls, v1: VectorLike, v2: VectorLike) -> "Edge":
20912092
BRepBuilderAPI_MakeEdge(Vector(v1).toPnt(), Vector(v2).toPnt()).Edge()
20922093
)
20932094

2095+
@classmethod
2096+
def makeBezier(cls, points: List[Vector]) -> "Edge":
2097+
"""
2098+
Create a cubic Bézier Curve from the points.
2099+
2100+
:param points: a list of Vectors that represent the points.
2101+
The edge will pass through the first and the last point,
2102+
and the inner points are Bézier control points.
2103+
:return: An edge
2104+
"""
2105+
2106+
# Convert to a TColgp_Array1OfPnt
2107+
arr = TColgp_Array1OfPnt(1, len(points))
2108+
for i, v in enumerate(points):
2109+
arr.SetValue(i + 1, Vector(v).toPnt())
2110+
2111+
bez = Geom_BezierCurve(arr)
2112+
2113+
return cls(BRepBuilderAPI_MakeEdge(bez).Edge())
2114+
20942115

20952116
class Wire(Shape, Mixin1D):
20962117
"""

cadquery/sketch.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -859,6 +859,23 @@ def spline(
859859

860860
return self.spline(pts, None, False, tag, forConstruction)
861861

862+
def bezier(
863+
self: T,
864+
pts: Iterable[Point],
865+
tag: Optional[str] = None,
866+
forConstruction: bool = False,
867+
) -> T:
868+
"""
869+
Construct an bezier curve.
870+
871+
The edge will pass through the last points, and the inner points
872+
are bezier control points.
873+
"""
874+
p1 = self._endPoint()
875+
val = Edge.makeBezier([Vector(*p) for p in pts])
876+
877+
return self.edge(val, tag, forConstruction)
878+
862879
def close(self: T, tag: Optional[str] = None) -> T:
863880
"""
864881
Connect last edge to the first one.

tests/test_sketch.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,19 @@ def test_edge_interface():
470470
assert len(s6.vertices()._selection) == 1
471471

472472

473+
def test_bezier():
474+
s1 = (
475+
Sketch()
476+
.segment((0, 0), (0, 0.5))
477+
.bezier(((0, 0.5), (-1, 2), (1, 0.5), (5, 0)))
478+
.bezier(((5, 0), (1, -0.5), (-1, -2), (0, -0.5)))
479+
.close()
480+
.assemble()
481+
)
482+
assert s1._faces.Area() == approx(5.35)
483+
# What other kind of tests can we do?
484+
485+
473486
def test_assemble():
474487

475488
s1 = Sketch()

tests/test_workplanes.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,3 +250,52 @@ def test_mirror_face(self):
250250
(bbBox.xlen, bbBox.ylen, bbBox.zlen), (1.0, 1.0, 1.0), 4
251251
)
252252
self.assertAlmostEqual(r.findSolid().Volume(), 1.0, 5)
253+
254+
def test_bezier_curve(self):
255+
# Quadratic bezier
256+
r = (
257+
Workplane("XZ")
258+
.bezier([(0, 0), (1, 2), (5, 0)])
259+
.bezier([(1, -2), (0, 0)], includeCurrent=True)
260+
.close()
261+
.extrude(1)
262+
)
263+
264+
bbBox = r.findSolid().BoundingBox()
265+
# Why is the bounding box larger than expected?
266+
self.assertTupleAlmostEquals((bbBox.xlen, bbBox.ylen, bbBox.zlen), (5, 1, 2), 1)
267+
self.assertAlmostEqual(r.findSolid().Volume(), 6.6666667, 4)
268+
269+
r = Workplane("XY").bezier([(0, 0), (1, 2), (2, -1), (5, 0)])
270+
self.assertTrue(len(r.ctx.pendingEdges) == 1)
271+
r = (
272+
r.lineTo(5, -0.1)
273+
.bezier([(2, -3), (1, 0), (0, 0)], includeCurrent=True)
274+
.close()
275+
.extrude(1)
276+
)
277+
278+
bbBox = r.findSolid().BoundingBox()
279+
self.assertTupleAlmostEquals(
280+
(bbBox.xlen, bbBox.ylen, bbBox.zlen), (5, 2.06767, 1), 1
281+
)
282+
self.assertAlmostEqual(r.findSolid().Volume(), 4.975, 4)
283+
284+
# Test makewire by translate and loft example like in
285+
# the documentation
286+
r = Workplane("XY").bezier([(0, 0), (1, 2), (1, -1), (0, 0)], makeWire=True)
287+
288+
self.assertTrue(len(r.ctx.pendingWires) == 1)
289+
r = r.translate((0, 0, 0.2)).toPending().loft()
290+
self.assertAlmostEqual(r.findSolid().Volume(), 0.09, 4)
291+
292+
# Finally test forConstruction
293+
r = Workplane("XY").bezier(
294+
[(0, 0), (1, 2), (1, -1), (0, 0)], makeWire=True, forConstruction=True
295+
)
296+
self.assertTrue(len(r.ctx.pendingWires) == 0)
297+
298+
r = Workplane("XY").bezier(
299+
[(0, 0), (1, 2), (2, -1), (5, 0)], forConstruction=True
300+
)
301+
self.assertTrue(len(r.ctx.pendingEdges) == 0)

0 commit comments

Comments
 (0)