Skip to content

Commit

Permalink
Add Bézier curve support to Workplane and Sketch (#1529)
Browse files Browse the repository at this point in the history
  • Loading branch information
dov committed Mar 23, 2024
1 parent 00fdd71 commit 76fbff7
Show file tree
Hide file tree
Showing 5 changed files with 134 additions and 1 deletion.
33 changes: 33 additions & 0 deletions cadquery/cq.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
"""
Expand Down
23 changes: 22 additions & 1 deletion cadquery/occ_impl/shapes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -146,6 +146,7 @@
)

from OCP.Geom import (
Geom_BezierCurve,
Geom_ConicalSurface,
Geom_CylindricalSurface,
Geom_Surface,
Expand Down Expand Up @@ -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):
"""
Expand Down
17 changes: 17 additions & 0 deletions cadquery/sketch.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
13 changes: 13 additions & 0 deletions tests/test_sketch.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
49 changes: 49 additions & 0 deletions tests/test_workplanes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

0 comments on commit 76fbff7

Please sign in to comment.