Skip to content

Commit 7546866

Browse files
authored
Merge pull request #694 from CadQuery/splineApprox
Implement makeSplineApprox for edges and faces
2 parents df550f0 + 7a259be commit 7546866

File tree

5 files changed

+536
-38
lines changed

5 files changed

+536
-38
lines changed

cadquery/cq.py

Lines changed: 193 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
Dict,
3636
)
3737
from typing_extensions import Literal
38-
38+
from inspect import Parameter, Signature
3939

4040
from .occ_impl.geom import Vector, Plane, Location
4141
from .occ_impl.shapes import (
@@ -264,7 +264,15 @@ def _collectProperty(self, propName: str) -> List[CQObject]:
264264

265265
return list(all.values())
266266

267+
@overload
267268
def split(self: T, keepTop: bool = False, keepBottom: bool = False) -> T:
269+
...
270+
271+
@overload
272+
def split(self: T, splitter: Union[T, Shape]) -> T:
273+
...
274+
275+
def split(self: T, *args, **kwargs) -> T:
268276
"""
269277
Splits a solid on the stack into two parts, optionally keeping the separate parts.
270278
@@ -284,28 +292,64 @@ def split(self: T, keepTop: bool = False, keepBottom: bool = False) -> T:
284292
c = c.faces(">Y").workplane(-0.5).split(keepTop=True)
285293
"""
286294

287-
if (not keepTop) and (not keepBottom):
288-
raise ValueError("You have to keep at least one half")
289-
290-
solid = self.findSolid()
295+
# split using an object
296+
if len(args) == 1 and isinstance(args[0], (Workplane, Shape)):
291297

292-
maxDim = solid.BoundingBox().DiagonalLength * 10.0
293-
topCutBox = self.rect(maxDim, maxDim)._extrude(maxDim)
294-
bottomCutBox = self.rect(maxDim, maxDim)._extrude(-maxDim)
298+
arg = args[0]
295299

296-
top = solid.cut(bottomCutBox)
297-
bottom = solid.cut(topCutBox)
300+
solid = self.findSolid()
301+
tools = (
302+
(arg,)
303+
if isinstance(arg, Shape)
304+
else [v for v in arg.vals() if isinstance(v, Shape)]
305+
)
306+
rv = [solid.split(*tools)]
298307

299-
if keepTop and keepBottom:
300-
# Put both on the stack, leave original unchanged.
301-
return self.newObject([top, bottom])
308+
# split using the current wokrplane
302309
else:
303-
# Put the one we are keeping on the stack, and also update the
304-
# context solidto the one we kept.
305-
if keepTop:
306-
return self.newObject([top])
310+
311+
# boilerplate for arg/kwarg parsing
312+
sig = Signature(
313+
(
314+
Parameter(
315+
"keepTop", Parameter.POSITIONAL_OR_KEYWORD, default=False
316+
),
317+
Parameter(
318+
"keepBottom", Parameter.POSITIONAL_OR_KEYWORD, default=False
319+
),
320+
)
321+
)
322+
323+
bound_args = sig.bind(*args, **kwargs)
324+
bound_args.apply_defaults()
325+
326+
keepTop = bound_args.arguments["keepTop"]
327+
keepBottom = bound_args.arguments["keepBottom"]
328+
329+
if (not keepTop) and (not keepBottom):
330+
raise ValueError("You have to keep at least one half")
331+
332+
solid = self.findSolid()
333+
334+
maxDim = solid.BoundingBox().DiagonalLength * 10.0
335+
topCutBox = self.rect(maxDim, maxDim)._extrude(maxDim)
336+
bottomCutBox = self.rect(maxDim, maxDim)._extrude(-maxDim)
337+
338+
top = solid.cut(bottomCutBox)
339+
bottom = solid.cut(topCutBox)
340+
341+
if keepTop and keepBottom:
342+
# Put both on the stack, leave original unchanged.
343+
rv = [top, bottom]
307344
else:
308-
return self.newObject([bottom])
345+
# Put the one we are keeping on the stack, and also update the
346+
# context solid to the one we kept.
347+
if keepTop:
348+
rv = [top]
349+
else:
350+
rv = [bottom]
351+
352+
return self.newObject(rv)
309353

310354
@deprecate()
311355
def combineSolids(
@@ -1729,6 +1773,20 @@ def slot2D(self: T, length: float, diameter: float, angle: float = 0) -> T:
17291773

17301774
return self.eachpoint(lambda loc: slot.moved(loc), True)
17311775

1776+
def _toVectors(
1777+
self, pts: Iterable[VectorLike], includeCurrent: bool
1778+
) -> List[Vector]:
1779+
1780+
vecs = [self.plane.toWorldCoords(p) for p in pts]
1781+
1782+
if includeCurrent:
1783+
gstartPoint = self._findFromPoint(False)
1784+
allPoints = [gstartPoint] + vecs
1785+
else:
1786+
allPoints = vecs
1787+
1788+
return allPoints
1789+
17321790
def spline(
17331791
self: T,
17341792
listOfXYTuple: Iterable[VectorLike],
@@ -1742,10 +1800,9 @@ def spline(
17421800
makeWire: bool = False,
17431801
) -> T:
17441802
"""
1745-
Create a spline interpolated through the provided points.
1803+
Create a spline interpolated through the provided points (2D or 3D).
17461804
17471805
:param listOfXYTuple: points to interpolate through
1748-
:type listOfXYTuple: list of 2-tuple
17491806
:param tangents: vectors specifying the direction of the tangent to the
17501807
curve at each of the specified interpolation points.
17511808
@@ -1807,13 +1864,7 @@ def spline(
18071864
that cannot be correctly interpreted as a spline.
18081865
"""
18091866

1810-
vecs = [self.plane.toWorldCoords(p) for p in listOfXYTuple]
1811-
1812-
if includeCurrent:
1813-
gstartPoint = self._findFromPoint(False)
1814-
allPoints = [gstartPoint] + vecs
1815-
else:
1816-
allPoints = vecs
1867+
allPoints = self._toVectors(listOfXYTuple, includeCurrent)
18171868

18181869
if tangents:
18191870
tangents_g: Optional[Sequence[Vector]] = [
@@ -1844,31 +1895,141 @@ def spline(
18441895

18451896
return self.newObject([rv_w if makeWire else e])
18461897

1898+
def splineApprox(
1899+
self: T,
1900+
points: Iterable[VectorLike],
1901+
tol: Optional[float] = 1e-6,
1902+
minDeg: int = 1,
1903+
maxDeg: int = 6,
1904+
smoothing: Optional[Tuple[float, float, float]] = (1, 1, 1),
1905+
forConstruction: bool = False,
1906+
includeCurrent: bool = False,
1907+
makeWire: bool = False,
1908+
) -> T:
1909+
"""
1910+
Create a spline interpolated through the provided points (2D or 3D).
1911+
1912+
:param points: points to interpolate through
1913+
:param tol: tolerance of the algorithm (default: 1e-6)
1914+
:param minDeg: minimum spline degree (default: 1)
1915+
:param maxDeg: maximum spline degree (default: 6)
1916+
:param smoothing: optional parameters for the variational smoothing algorithm (default: (1,1,1))
1917+
:param includeCurrent: use current point as a starting point of the curve
1918+
:param makeWire: convert the resulting spline edge to a wire
1919+
:return: a Workplane object with the current point at the end of the spline
1920+
1921+
*WARNING* for advanced users.
1922+
"""
1923+
1924+
allPoints = self._toVectors(points, includeCurrent)
1925+
1926+
e = Edge.makeSplineApprox(
1927+
allPoints,
1928+
minDeg=minDeg,
1929+
maxDeg=maxDeg,
1930+
smoothing=smoothing,
1931+
**({"tol": tol} if tol else {}),
1932+
)
1933+
1934+
if makeWire:
1935+
rv_w = Wire.assembleEdges([e])
1936+
if not forConstruction:
1937+
self._addPendingWire(rv_w)
1938+
else:
1939+
if not forConstruction:
1940+
self._addPendingEdge(e)
1941+
1942+
return self.newObject([rv_w if makeWire else e])
1943+
18471944
def parametricCurve(
18481945
self: T,
18491946
func: Callable[[float], VectorLike],
18501947
N: int = 400,
18511948
start: float = 0,
18521949
stop: float = 1,
1950+
tol: float = 1e-6,
1951+
minDeg: int = 1,
1952+
maxDeg: int = 6,
1953+
smoothing: Optional[Tuple[float, float, float]] = (1, 1, 1),
18531954
makeWire: bool = True,
18541955
) -> T:
18551956
"""
1856-
Create a spline interpolated through the provided points.
1957+
Create a spline curve approximating the provided function.
18571958
1858-
:param func: function f(t) that will generate (x,y) pairs
1859-
:type func: float --> (float,float)
1959+
:param func: function f(t) that will generate (x,y,z) pairs
1960+
:type func: float --> (float,float,float)
18601961
:param N: number of points for discretization
18611962
:param start: starting value of the parameter t
18621963
:param stop: final value of the parameter t
1964+
:param tol: tolerance of the algorithm (default: 1e-3)
1965+
:param minDeg: minimum spline degree (default: 1)
1966+
:param maxDeg: maximum spline degree (default: 6)
1967+
:param smoothing: optional parameters for the variational smoothing algorithm (default: (1,1,1))
18631968
:param makeWire: convert the resulting spline edge to a wire
18641969
:return: a Workplane object with the current point unchanged
18651970
18661971
"""
18671972

18681973
diff = stop - start
1869-
allPoints = [func(start + diff * t / N) for t in range(N + 1)]
1974+
allPoints = self._toVectors(
1975+
(func(start + diff * t / N) for t in range(N + 1)), False
1976+
)
1977+
1978+
e = Edge.makeSplineApprox(
1979+
allPoints, tol=tol, smoothing=smoothing, minDeg=minDeg, maxDeg=maxDeg
1980+
)
1981+
1982+
if makeWire:
1983+
rv_w = Wire.assembleEdges([e])
1984+
self._addPendingWire(rv_w)
1985+
else:
1986+
self._addPendingEdge(e)
1987+
1988+
return self.newObject([rv_w if makeWire else e])
1989+
1990+
def parametricSurface(
1991+
self: T,
1992+
func: Callable[[float, float], VectorLike],
1993+
N: int = 20,
1994+
start: float = 0,
1995+
stop: float = 1,
1996+
tol: float = 1e-2,
1997+
minDeg: int = 1,
1998+
maxDeg: int = 6,
1999+
smoothing: Optional[Tuple[float, float, float]] = (1, 1, 1),
2000+
) -> T:
2001+
"""
2002+
Create a spline surface approximating the provided function.
2003+
2004+
:param func: function f(u,v) that will generate (x,y,z) pairs
2005+
:type func: (float,float) --> (float,float,float)
2006+
:param N: number of points for discretization in one direction
2007+
:param start: starting value of the parameters u,v
2008+
:param stop: final value of the parameters u,v
2009+
:param tol: tolerance used by the approximation algorithm (default: 1e-3)
2010+
:param minDeg: minimum spline degree (default: 1)
2011+
:param maxDeg: maximum spline degree (default: 3)
2012+
:param smoothing: optional parameters for the variational smoothing algorithm (default: (1,1,1))
2013+
:return: a Workplane object with the current point unchanged
2014+
2015+
This method might be unstable and may require tuning of the tol parameter.
2016+
2017+
"""
2018+
2019+
diff = stop - start
2020+
allPoints = []
2021+
2022+
for i in range(N + 1):
2023+
generator = (
2024+
func(start + diff * i / N, start + diff * j / N) for j in range(N + 1)
2025+
)
2026+
allPoints.append(self._toVectors(generator, False))
2027+
2028+
f = Face.makeSplineApprox(
2029+
allPoints, tol=tol, smoothing=smoothing, minDeg=minDeg, maxDeg=maxDeg
2030+
)
18702031

1871-
return self.spline(allPoints, includeCurrent=False, makeWire=makeWire)
2032+
return self.newObject([f])
18722033

18732034
def ellipseArc(
18742035
self: T,

0 commit comments

Comments
 (0)