Add Bézier curve support to Workplane and Sketch (#1529)

This commit is contained in:
Dov Grobgeld
2024-03-23 17:15:54 +02:00
committed by GitHub
parent 00fdd71d7d
commit 76fbff7158
5 changed files with 134 additions and 1 deletions

View File

@ -1629,6 +1629,39 @@ class Workplane(object):
return self.newObject([p]) 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 # line a specified incremental amount from current point
def line(self: T, xDist: float, yDist: float, forConstruction: bool = False) -> T: def line(self: T, xDist: float, yDist: float, forConstruction: bool = False) -> T:
""" """

View File

@ -56,7 +56,7 @@ from OCP.gp import (
) )
# Array of points (used for B-spline construction): # 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): # Array of vectors (used for B-spline interpolation):
from OCP.TColgp import TColgp_Array1OfVec from OCP.TColgp import TColgp_Array1OfVec
@ -146,6 +146,7 @@ from OCP.BRepAlgoAPI import (
) )
from OCP.Geom import ( from OCP.Geom import (
Geom_BezierCurve,
Geom_ConicalSurface, Geom_ConicalSurface,
Geom_CylindricalSurface, Geom_CylindricalSurface,
Geom_Surface, Geom_Surface,
@ -2091,6 +2092,26 @@ class Edge(Shape, Mixin1D):
BRepBuilderAPI_MakeEdge(Vector(v1).toPnt(), Vector(v2).toPnt()).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): class Wire(Shape, Mixin1D):
""" """

View File

@ -859,6 +859,23 @@ class Sketch(object):
return self.spline(pts, None, False, tag, forConstruction) 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: def close(self: T, tag: Optional[str] = None) -> T:
""" """
Connect last edge to the first one. Connect last edge to the first one.

View File

@ -470,6 +470,19 @@ def test_edge_interface():
assert len(s6.vertices()._selection) == 1 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(): def test_assemble():
s1 = Sketch() s1 = Sketch()

View File

@ -250,3 +250,52 @@ class TestWorkplanes(BaseTest):
(bbBox.xlen, bbBox.ylen, bbBox.zlen), (1.0, 1.0, 1.0), 4 (bbBox.xlen, bbBox.ylen, bbBox.zlen), (1.0, 1.0, 1.0), 4
) )
self.assertAlmostEqual(r.findSolid().Volume(), 1.0, 5) 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)