bspy 4.3__py3-none-any.whl → 4.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bspy/_spline_domain.py +2 -1
- bspy/_spline_evaluation.py +1 -1
- bspy/_spline_fitting.py +29 -8
- bspy/_spline_intersection.py +9 -6
- bspy/_spline_milling.py +233 -0
- bspy/_spline_operations.py +55 -42
- bspy/solid.py +1 -1
- bspy/spline.py +107 -16
- {bspy-4.3.dist-info → bspy-4.4.dist-info}/METADATA +13 -12
- bspy-4.4.dist-info/RECORD +19 -0
- {bspy-4.3.dist-info → bspy-4.4.dist-info}/WHEEL +1 -1
- bspy-4.3.dist-info/RECORD +0 -18
- {bspy-4.3.dist-info → bspy-4.4.dist-info/licenses}/LICENSE +0 -0
- {bspy-4.3.dist-info → bspy-4.4.dist-info}/top_level.txt +0 -0
bspy/_spline_domain.py
CHANGED
|
@@ -330,6 +330,7 @@ def insert_knots(self, newKnotList):
|
|
|
330
330
|
continue
|
|
331
331
|
|
|
332
332
|
# Check if knot and its total multiplicity is valid.
|
|
333
|
+
knot = knots.dtype.type(knot) # Cast to correct type
|
|
333
334
|
if knot < knots[degree] or knot > knots[-order]:
|
|
334
335
|
raise ValueError(f"Knot insertion outside domain: {knot}")
|
|
335
336
|
position = np.searchsorted(knots, knot, 'right')
|
|
@@ -499,7 +500,7 @@ def remove_knots(self, tolerance, nLeft = 0, nRight = 0):
|
|
|
499
500
|
foldedIndices = list(filter(lambda x: x != id, indIndex))
|
|
500
501
|
currentFold, foldedBasis = currentSpline.fold(foldedIndices)
|
|
501
502
|
while True:
|
|
502
|
-
bestError = np.finfo(
|
|
503
|
+
bestError = np.finfo(self.coefs.dtype).max
|
|
503
504
|
bestSpline = currentFold
|
|
504
505
|
ix = currentFold.order[0]
|
|
505
506
|
while ix < currentFold.nCoef[0]:
|
bspy/_spline_evaluation.py
CHANGED
|
@@ -5,7 +5,7 @@ def bspline_values(knot, knots, splineOrder, u, derivativeOrder = 0, taylorCoefs
|
|
|
5
5
|
basis = np.zeros(splineOrder, knots.dtype)
|
|
6
6
|
if knot is None:
|
|
7
7
|
knot = np.searchsorted(knots, u, side = 'right')
|
|
8
|
-
knot = min(knot, len(knots) - splineOrder)
|
|
8
|
+
knot = min(max(knot, splineOrder), len(knots) - splineOrder)
|
|
9
9
|
if derivativeOrder >= splineOrder:
|
|
10
10
|
return knot, basis
|
|
11
11
|
basis[-1] = 1.0
|
bspy/_spline_fitting.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import numpy as np
|
|
2
2
|
import scipy as sp
|
|
3
3
|
import bspy.spline
|
|
4
|
+
import bspy.spline_block
|
|
4
5
|
import math
|
|
5
6
|
|
|
6
7
|
def circular_arc(radius, angle, tolerance = None):
|
|
@@ -12,14 +13,33 @@ def circular_arc(radius, angle, tolerance = None):
|
|
|
12
13
|
return bspy.Spline.section([(radius * np.cos(u * angle * np.pi / 180), radius * np.sin(u * angle * np.pi / 180), 90 + u * angle, 1.0 / radius) for u in np.linspace(0.0, 1.0, samples)])
|
|
13
14
|
|
|
14
15
|
def composition(splines, tolerance):
|
|
16
|
+
# Collect domains and check range bounds
|
|
17
|
+
domains = [None]
|
|
18
|
+
domain = None
|
|
19
|
+
for i, spline in enumerate(splines):
|
|
20
|
+
if domain is not None:
|
|
21
|
+
if len(domain) != spline.nDep:
|
|
22
|
+
raise ValueError(f"Domain dimension of spline {i-1} does not match range dimension of spline {i}")
|
|
23
|
+
rangeBounds = spline.range_bounds()
|
|
24
|
+
for ix in range(spline.nDep):
|
|
25
|
+
if rangeBounds[ix][0] < domain[ix][0] or rangeBounds[ix][1] > domain[ix][1]:
|
|
26
|
+
raise ValueError(f"Range of spline {i} exceeds domain of spline {i-1}")
|
|
27
|
+
domains.append(domain)
|
|
28
|
+
domain = spline.domain()
|
|
29
|
+
|
|
15
30
|
# Define the callback function
|
|
16
31
|
def composition_of_splines(u):
|
|
17
|
-
for
|
|
18
|
-
u =
|
|
32
|
+
for spline, domain in zip(splines[::-1], domains[::-1]):
|
|
33
|
+
u = spline(u)
|
|
34
|
+
if domain is not None:
|
|
35
|
+
# We've already checked that the range of spline is within the domain
|
|
36
|
+
# of its successor, but numerics may cause the spline value to slightly
|
|
37
|
+
# exceed its range, so we clip the spline value accordingly.
|
|
38
|
+
u = np.clip(u, domain[:, 0], domain[:, 1])
|
|
19
39
|
return u
|
|
20
40
|
|
|
21
41
|
# Approximate this composition
|
|
22
|
-
return bspy.Spline.fit(
|
|
42
|
+
return bspy.Spline.fit(domain, composition_of_splines, tolerance = tolerance)
|
|
23
43
|
|
|
24
44
|
def cone(radius1, radius2, height, tolerance = None):
|
|
25
45
|
if tolerance is None:
|
|
@@ -366,9 +386,10 @@ def fit(domain, f, order = None, knots = None, tolerance = 1.0e-4):
|
|
|
366
386
|
indices = nInd * [0]
|
|
367
387
|
iLast = nInd
|
|
368
388
|
while iLast >= 0:
|
|
389
|
+
# Create a tuple for the u value (must be a tuple to use it as a dictionary key)
|
|
369
390
|
uValue = tuple([uvw[i][indices[i]] for i in range(nInd)])
|
|
370
391
|
if not uValue in fDictionary:
|
|
371
|
-
fDictionary[uValue] = f(uValue)
|
|
392
|
+
fDictionary[uValue] = f(np.array(uValue))
|
|
372
393
|
fValues.append(fDictionary[uValue])
|
|
373
394
|
iLast = nInd - 1
|
|
374
395
|
while iLast >= 0:
|
|
@@ -518,7 +539,7 @@ def four_sided_patch(bottom, right, top, left, surfParam = 0.5):
|
|
|
518
539
|
|
|
519
540
|
return (1.0 - surfParam) * coons + surfParam * laplace
|
|
520
541
|
|
|
521
|
-
def geodesic(self, uvStart, uvEnd, tolerance = 1.0e-
|
|
542
|
+
def geodesic(self, uvStart, uvEnd, tolerance = 1.0e-5):
|
|
522
543
|
# Check validity of input
|
|
523
544
|
if self.nInd != 2: raise ValueError("Surface must have two independent variables")
|
|
524
545
|
if len(uvStart) != 2: raise ValueError("uvStart must have two components")
|
|
@@ -616,7 +637,7 @@ def geodesic(self, uvStart, uvEnd, tolerance = 1.0e-6):
|
|
|
616
637
|
initialGuess = line(uvStart, uvEnd).elevate([2])
|
|
617
638
|
|
|
618
639
|
# Solve the ODE and return the geodesic
|
|
619
|
-
solution = initialGuess.solve_ode(1, 1, geodesicCallback,
|
|
640
|
+
solution = initialGuess.solve_ode(1, 1, geodesicCallback, tolerance, (self, uvDomain))
|
|
620
641
|
return solution
|
|
621
642
|
|
|
622
643
|
def least_squares(uValues, dataPoints, order = None, knots = None, compression = 0.0,
|
|
@@ -878,7 +899,7 @@ def section(xytk):
|
|
|
878
899
|
# Join the pieces together and return
|
|
879
900
|
return bspy.Spline.join(mySections)
|
|
880
901
|
|
|
881
|
-
def solve_ode(self, nLeft, nRight, FAndF_u, tolerance = 1.0e-6, args = ()):
|
|
902
|
+
def solve_ode(self, nLeft, nRight, FAndF_u, tolerance = 1.0e-6, args = (), includeEstimate = False):
|
|
882
903
|
# Ensure that the ODE is properly formulated
|
|
883
904
|
|
|
884
905
|
if nLeft < 0: raise ValueError("Invalid number of left hand boundary conditions")
|
|
@@ -970,7 +991,7 @@ def solve_ode(self, nLeft, nRight, FAndF_u, tolerance = 1.0e-6, args = ()):
|
|
|
970
991
|
residuals = np.append(residuals, np.zeros((nLeft * nDep,)))
|
|
971
992
|
collocationMatrix[bandWidth, 0 : nLeft * nDep] = 1.0
|
|
972
993
|
for iPoint, t in enumerate(collocationPoints[iFirstPoint : iNextPoint]):
|
|
973
|
-
uData = np.array([workingSpline.derivative([i], t) for i in range(nOrder)]).T
|
|
994
|
+
uData = np.array([workingSpline.derivative([i], t) for i in range(nOrder + 1 if includeEstimate else nOrder)]).T
|
|
974
995
|
F, F_u = FAndF_u(t, uData, *args)
|
|
975
996
|
residuals = np.append(residuals, workingSpline.derivative([nOrder], t) - continuation * F)
|
|
976
997
|
ix = None
|
bspy/_spline_intersection.py
CHANGED
|
@@ -958,6 +958,8 @@ def intersect(self, other):
|
|
|
958
958
|
zeros = spline.zeros()
|
|
959
959
|
# Convert each intersection point into a Manifold.Crossing and each intersection interval into a Manifold.Coincidence.
|
|
960
960
|
for zero in zeros:
|
|
961
|
+
if isinstance(zero, tuple) and zero[1] - zero[0] < Manifold.minSeparation:
|
|
962
|
+
zero = 0.5 * (zero[0] + zero[1])
|
|
961
963
|
if isinstance(zero, tuple):
|
|
962
964
|
# Intersection is an interval, so create a Manifold.Coincidence.
|
|
963
965
|
planeBounds = (projection @ (self((zero[0],)) - other._point), projection @ (self((zero[1],)) - other._point))
|
|
@@ -1019,6 +1021,8 @@ def intersect(self, other):
|
|
|
1019
1021
|
zeros = block.zeros()
|
|
1020
1022
|
# Convert each intersection point into a Manifold.Crossing and each intersection interval into a Manifold.Coincidence.
|
|
1021
1023
|
for zero in zeros:
|
|
1024
|
+
if isinstance(zero, tuple) and zero[1] - zero[0] < Manifold.minSeparation:
|
|
1025
|
+
zero = 0.5 * (zero[0] + zero[1])
|
|
1022
1026
|
if isinstance(zero, tuple):
|
|
1023
1027
|
# Intersection is an interval, so create a Manifold.Coincidence.
|
|
1024
1028
|
|
|
@@ -1135,14 +1139,14 @@ def complete_slice(self, slice, solid):
|
|
|
1135
1139
|
newBoundary.touched = False
|
|
1136
1140
|
|
|
1137
1141
|
# Define function for adding slice points to full domain boundaries.
|
|
1138
|
-
def process_domain_point(boundary, domainPoint):
|
|
1142
|
+
def process_domain_point(boundary, domainPoint, adjustment):
|
|
1139
1143
|
point = boundary.manifold.evaluate(domainPoint)
|
|
1140
1144
|
# See if and where point touches full domain.
|
|
1141
1145
|
for newBoundary in fullDomain.boundaries:
|
|
1142
1146
|
vector = point - newBoundary.manifold._point
|
|
1143
1147
|
if abs(np.dot(newBoundary.manifold._normal, vector)) < Manifold.minSeparation:
|
|
1144
|
-
# Add the point onto the new boundary.
|
|
1145
|
-
normal = np.sign(newBoundary.manifold._tangentSpace.T @ boundary.manifold.normal(domainPoint))
|
|
1148
|
+
# Add the point onto the new boundary (adjust normal evaluation point to move away from boundary).
|
|
1149
|
+
normal = np.sign(newBoundary.manifold._tangentSpace.T @ boundary.manifold.normal(domainPoint + adjustment))
|
|
1146
1150
|
newBoundary.domain.add_boundary(Boundary(Hyperplane(normal, newBoundary.manifold._tangentSpace.T @ vector, 0.0), Solid(0, True)))
|
|
1147
1151
|
newBoundary.touched = True
|
|
1148
1152
|
break
|
|
@@ -1151,9 +1155,9 @@ def complete_slice(self, slice, solid):
|
|
|
1151
1155
|
for boundary in slice.boundaries:
|
|
1152
1156
|
domainBoundaries = boundary.domain.boundaries
|
|
1153
1157
|
domainBoundaries.sort(key=lambda boundary: (boundary.manifold.evaluate(0.0), boundary.manifold.normal(0.0)))
|
|
1154
|
-
process_domain_point(boundary, domainBoundaries[0].manifold._point)
|
|
1158
|
+
process_domain_point(boundary, domainBoundaries[0].manifold._point, Manifold.minSeparation)
|
|
1155
1159
|
if len(domainBoundaries) > 1:
|
|
1156
|
-
process_domain_point(boundary, domainBoundaries[-1].manifold._point)
|
|
1160
|
+
process_domain_point(boundary, domainBoundaries[-1].manifold._point, -Manifold.minSeparation)
|
|
1157
1161
|
|
|
1158
1162
|
# For touched boundaries, remove domain bounds that aren't needed and then add boundary to slice.
|
|
1159
1163
|
boundaryWasTouched = False
|
|
@@ -1161,7 +1165,6 @@ def complete_slice(self, slice, solid):
|
|
|
1161
1165
|
if newBoundary.touched:
|
|
1162
1166
|
boundaryWasTouched = True
|
|
1163
1167
|
domainBoundaries = newBoundary.domain.boundaries
|
|
1164
|
-
assert len(domainBoundaries) > 2
|
|
1165
1168
|
domainBoundaries.sort(key=lambda boundary: (boundary.manifold.evaluate(0.0), boundary.manifold.normal(0.0)))
|
|
1166
1169
|
# Ensure domain endpoints don't overlap and their normals are consistent.
|
|
1167
1170
|
if abs(domainBoundaries[0].manifold._point - domainBoundaries[1].manifold._point) < Manifold.minSeparation or \
|
bspy/_spline_milling.py
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import bspy.spline
|
|
3
|
+
import bspy.spline_block
|
|
4
|
+
|
|
5
|
+
def line_of_curvature(self, uvStart, is_max, tolerance = 1.0e-3):
|
|
6
|
+
if self.nInd != 2: raise ValueError("Surface must have two independent variables")
|
|
7
|
+
if len(uvStart) != 2: raise ValueError("uvStart must have two components")
|
|
8
|
+
uvDomain = self.domain()
|
|
9
|
+
if uvStart[0] < uvDomain[0, 0] or uvStart[0] > uvDomain[0, 1] or \
|
|
10
|
+
uvStart[1] < uvDomain[1, 0] or uvStart[1] > uvDomain[1, 1]:
|
|
11
|
+
raise ValueError("uvStart is outside domain of the surface")
|
|
12
|
+
is_max = bool(is_max) # Ensure is_max is a boolean for XNOR operation
|
|
13
|
+
|
|
14
|
+
# Define the callback function for the ODE solver
|
|
15
|
+
def curvatureLineCallback(t, u):
|
|
16
|
+
# Evaluate the surface information needed.
|
|
17
|
+
uv = np.maximum(uvDomain[:, 0], np.minimum(uvDomain[:, 1], u[:, 0]))
|
|
18
|
+
su = self.derivative((1, 0), uv)
|
|
19
|
+
sv = self.derivative((0, 1), uv)
|
|
20
|
+
suu = self.derivative((2, 0), uv)
|
|
21
|
+
suv = self.derivative((1, 1), uv)
|
|
22
|
+
svv = self.derivative((0, 2), uv)
|
|
23
|
+
suuu = self.derivative((3, 0), uv)
|
|
24
|
+
suuv = self.derivative((2, 1), uv)
|
|
25
|
+
suvv = self.derivative((1, 2), uv)
|
|
26
|
+
svvv = self.derivative((0, 3), uv)
|
|
27
|
+
normal = self.normal(uv)
|
|
28
|
+
|
|
29
|
+
# Calculate curvature matrix and its derivatives.
|
|
30
|
+
sU = np.concatenate((su, sv)).reshape(2, -1)
|
|
31
|
+
sUu = np.concatenate((suu, suv)).reshape(2, -1)
|
|
32
|
+
sUv = np.concatenate((suv, svv)).reshape(2, -1)
|
|
33
|
+
sUU = np.concatenate((suu, suv, suv, svv)).reshape(2, 2, -1)
|
|
34
|
+
sUUu = np.concatenate((suuu, suuv, suuv, suvv)).reshape(2, 2, -1)
|
|
35
|
+
sUUv = np.concatenate((suuv, suvv, suvv, svvv)).reshape(2, 2, -1)
|
|
36
|
+
fffI = np.linalg.inv(sU @ sU.T) # Inverse of first fundamental form
|
|
37
|
+
k = fffI @ (sUU @ normal) # Curvature matrix
|
|
38
|
+
ku = fffI @ (sUUu @ normal - (sUu @ sU.T + sU @ sUu.T) @ k - sUU @ (sU.T @ k[:, 0]))
|
|
39
|
+
kv = fffI @ (sUUv @ normal - (sUv @ sU.T + sU @ sUv.T) @ k - sUU @ (sU.T @ k[:, 1]))
|
|
40
|
+
|
|
41
|
+
# Determine principle curvatures and directions, and assign new direction.
|
|
42
|
+
curvatures, directions = np.linalg.eig(k)
|
|
43
|
+
curvatureDelta = curvatures[1] - curvatures[0]
|
|
44
|
+
if abs(curvatureDelta) < tolerance:
|
|
45
|
+
# If we're at an umbilic, use the last direction (jacobian is zero at umbilic).
|
|
46
|
+
direction = u[:, 1]
|
|
47
|
+
jacobian = np.zeros((2,2,1), self.coefs.dtype)
|
|
48
|
+
else:
|
|
49
|
+
# Otherwise, compute the lhs inverse for the jacobian.
|
|
50
|
+
directionsInverse = np.linalg.inv(directions)
|
|
51
|
+
eigenIndex = 0 if bool(curvatures[0] > curvatures[1]) == is_max else 1
|
|
52
|
+
direction = directions[:, eigenIndex]
|
|
53
|
+
B = np.zeros((2, 2), self.coefs.dtype)
|
|
54
|
+
B[0, 1 - eigenIndex] = np.dot(directions[:, 1], direction) / curvatureDelta
|
|
55
|
+
B[1, 1 - eigenIndex] = -np.dot(directions[:, 0], direction) / curvatureDelta
|
|
56
|
+
lhsInv = directions @ B @ directionsInverse
|
|
57
|
+
|
|
58
|
+
# Adjust the direction for consistency.
|
|
59
|
+
if np.dot(direction, u[:, 1]) < -tolerance:
|
|
60
|
+
direction *= -1
|
|
61
|
+
|
|
62
|
+
# Compute the jacobian for the direction.
|
|
63
|
+
jacobian = np.empty((2,2,1), self.coefs.dtype)
|
|
64
|
+
jacobian[:,0,0] = lhsInv @ ku @ direction
|
|
65
|
+
jacobian[:,1,0] = lhsInv @ kv @ direction
|
|
66
|
+
|
|
67
|
+
return direction, jacobian
|
|
68
|
+
|
|
69
|
+
# Generate the initial guess for the line of curvature.
|
|
70
|
+
uvStart = np.atleast_1d(uvStart)
|
|
71
|
+
direction = 0.5 * (uvDomain[:,0] + uvDomain[:,1]) - uvStart # Initial guess toward center
|
|
72
|
+
distanceFromCenter = np.linalg.norm(direction)
|
|
73
|
+
if distanceFromCenter < 10 * tolerance:
|
|
74
|
+
# If we're at the center, just point to the far corner.
|
|
75
|
+
direction = np.array((1.0, 1.0)) / np.sqrt(2)
|
|
76
|
+
else:
|
|
77
|
+
direction /= distanceFromCenter
|
|
78
|
+
|
|
79
|
+
# Compute line of curvature direction at start.
|
|
80
|
+
direction, jacobian = curvatureLineCallback(0.0, np.array(((uvStart[0], direction[0]), (uvStart[1], direction[1]))))
|
|
81
|
+
|
|
82
|
+
# Calculate distance to the boundary in that direction.
|
|
83
|
+
if direction[0] < -tolerance:
|
|
84
|
+
uBoundaryDistance = (uvDomain[0, 0] - uvStart[0]) / direction[0]
|
|
85
|
+
elif direction[0] > tolerance:
|
|
86
|
+
uBoundaryDistance = (uvDomain[0, 1] - uvStart[0]) / direction[0]
|
|
87
|
+
else:
|
|
88
|
+
uBoundaryDistance = np.inf
|
|
89
|
+
if direction[1] < -tolerance:
|
|
90
|
+
vBoundaryDistance = (uvDomain[1, 0] - uvStart[1]) / direction[1]
|
|
91
|
+
elif direction[1] > tolerance:
|
|
92
|
+
vBoundaryDistance = (uvDomain[1, 1] - uvStart[1]) / direction[1]
|
|
93
|
+
else:
|
|
94
|
+
vBoundaryDistance = np.inf
|
|
95
|
+
boundaryDistance = min(uBoundaryDistance, vBoundaryDistance)
|
|
96
|
+
|
|
97
|
+
# Construct the initial guess from start point to boundary.
|
|
98
|
+
initialGuess = bspy.spline.Spline.line(uvStart, uvStart + boundaryDistance * direction).elevate([2])
|
|
99
|
+
|
|
100
|
+
# Solve the ODE and return the line of curvature confined to the surface's domain.
|
|
101
|
+
solution = initialGuess.solve_ode(1, 0, curvatureLineCallback, tolerance, includeEstimate = True)
|
|
102
|
+
return solution.confine(uvDomain)
|
|
103
|
+
|
|
104
|
+
def offset(self, edgeRadius, bitRadius=None, angle=np.pi / 2.2, path=None, subtract=False, removeCusps=False, tolerance = 1.0e-4):
|
|
105
|
+
if self.nDep < 2 or self.nDep > 3 or self.nDep - self.nInd != 1: raise ValueError("The offset is only defined for 2D curves and 3D surfaces with well-defined normals.")
|
|
106
|
+
if edgeRadius < 0:
|
|
107
|
+
raise ValueError("edgeRadius must be >= 0")
|
|
108
|
+
elif edgeRadius == 0:
|
|
109
|
+
return self
|
|
110
|
+
if bitRadius is None:
|
|
111
|
+
bitRadius = edgeRadius
|
|
112
|
+
elif bitRadius < edgeRadius:
|
|
113
|
+
raise ValueError("bitRadius must be >= edgeRadius")
|
|
114
|
+
if angle < 0 or angle >= np.pi / 2: raise ValueError("angle must in the range [0, pi/2)")
|
|
115
|
+
if path is not None and (path.nInd != 1 or path.nDep != 2 or self.nInd != 2):
|
|
116
|
+
raise ValueError("path must be a 2D curve and self must be a 3D surface")
|
|
117
|
+
|
|
118
|
+
# Determine geometry of drill bit.
|
|
119
|
+
if subtract:
|
|
120
|
+
edgeRadius *= -1
|
|
121
|
+
bitRadius *= -1
|
|
122
|
+
w = bitRadius - edgeRadius
|
|
123
|
+
h = w * np.tan(angle)
|
|
124
|
+
bottom = np.sin(angle)
|
|
125
|
+
bottomRadius = edgeRadius + h / bottom
|
|
126
|
+
|
|
127
|
+
# Define drill bit function.
|
|
128
|
+
if abs(w) < tolerance and path is None: # Simple offset curve or surface
|
|
129
|
+
def drillBit(uv):
|
|
130
|
+
return self(uv) + edgeRadius * self.normal(uv)
|
|
131
|
+
elif self.nDep == 2: # General offset curve
|
|
132
|
+
def drillBit(u):
|
|
133
|
+
xy = self(u)
|
|
134
|
+
normal = self.normal(u)
|
|
135
|
+
upward = np.sign(normal[1])
|
|
136
|
+
if upward * normal[1] <= bottom:
|
|
137
|
+
xy[0] += edgeRadius * normal[0] + w * np.sign(normal[0])
|
|
138
|
+
xy[1] += edgeRadius * normal[1]
|
|
139
|
+
else:
|
|
140
|
+
xy[0] += bottomRadius * normal[0]
|
|
141
|
+
xy[1] += bottomRadius * normal[1] - upward * h
|
|
142
|
+
return xy
|
|
143
|
+
elif self.nDep == 3 and path is None: # General offset surface
|
|
144
|
+
def drillBit(uv):
|
|
145
|
+
xyz = self(uv)
|
|
146
|
+
normal = self.normal(uv)
|
|
147
|
+
upward = np.sign(normal[1])
|
|
148
|
+
if upward * normal[1] <= bottom:
|
|
149
|
+
norm = np.sqrt(normal[0] * normal[0] + normal[2] * normal[2])
|
|
150
|
+
xyz[0] += edgeRadius * normal[0] + w * normal[0] / norm
|
|
151
|
+
xyz[1] += edgeRadius * normal[1]
|
|
152
|
+
xyz[2] += edgeRadius * normal[2] + w * normal[2] / norm
|
|
153
|
+
else:
|
|
154
|
+
xyz[0] += bottomRadius * normal[0]
|
|
155
|
+
xyz[1] += bottomRadius * normal[1] - upward * h
|
|
156
|
+
xyz[2] += bottomRadius * normal[2]
|
|
157
|
+
return xyz
|
|
158
|
+
elif self.nDep == 3: # General offset of a given path along a surface
|
|
159
|
+
surface = self
|
|
160
|
+
self = path # Redefine self to be the path (used below for fitting)
|
|
161
|
+
def drillBit(u):
|
|
162
|
+
uv = self(u)
|
|
163
|
+
xyz = surface(uv)
|
|
164
|
+
normal = surface.normal(uv)
|
|
165
|
+
upward = np.sign(normal[1])
|
|
166
|
+
if upward * normal[1] <= bottom:
|
|
167
|
+
norm = np.sqrt(normal[0] * normal[0] + normal[2] * normal[2])
|
|
168
|
+
xyz[0] += edgeRadius * normal[0] + w * normal[0] / norm
|
|
169
|
+
xyz[1] += edgeRadius * normal[1]
|
|
170
|
+
xyz[2] += edgeRadius * normal[2] + w * normal[2] / norm
|
|
171
|
+
else:
|
|
172
|
+
xyz[0] += bottomRadius * normal[0]
|
|
173
|
+
xyz[1] += bottomRadius * normal[1] - upward * h
|
|
174
|
+
xyz[2] += bottomRadius * normal[2]
|
|
175
|
+
return xyz
|
|
176
|
+
else: # Should never get here (exception raised earlier)
|
|
177
|
+
raise ValueError("The offset is only defined for 2D curves and 3D surfaces with well-defined normals.")
|
|
178
|
+
|
|
179
|
+
# Compute new order and knots for offset (ensure order is at least 4).
|
|
180
|
+
newOrder = []
|
|
181
|
+
newKnots = []
|
|
182
|
+
for order, knots in zip(self.order, self.knots):
|
|
183
|
+
min4Order = max(order, 4)
|
|
184
|
+
unique, count = np.unique(knots, return_counts=True)
|
|
185
|
+
count += min4Order - order
|
|
186
|
+
newOrder.append(min4Order)
|
|
187
|
+
newKnots.append(np.repeat(unique, count))
|
|
188
|
+
|
|
189
|
+
# Fit new spline to offset by drill bit.
|
|
190
|
+
offset = bspy.spline.Spline.fit(self.domain(), drillBit, newOrder, newKnots, tolerance)
|
|
191
|
+
|
|
192
|
+
# Remove cusps as required (only applies to offset curves).
|
|
193
|
+
if removeCusps and self.nInd == 1:
|
|
194
|
+
# Find the cusps by checking for tangent direction reversal between the spline and offset.
|
|
195
|
+
cusps = []
|
|
196
|
+
previousKnot = None
|
|
197
|
+
start = None
|
|
198
|
+
for knot in np.unique(offset.knots[0][offset.order[0]:offset.nCoef[0]]):
|
|
199
|
+
tangent = self.derivative((1,), knot)
|
|
200
|
+
if path is not None:
|
|
201
|
+
tangent = surface.jacobian(path(knot)) @ tangent
|
|
202
|
+
flipped = np.dot(tangent, offset.derivative((1,), knot)) < 0
|
|
203
|
+
if flipped and start is None:
|
|
204
|
+
start = knot
|
|
205
|
+
if not flipped and start is not None:
|
|
206
|
+
cusps.append((start, previousKnot))
|
|
207
|
+
start = None
|
|
208
|
+
previousKnot = knot
|
|
209
|
+
|
|
210
|
+
# Remove the cusps by intersecting the offset segments before and after each cusp.
|
|
211
|
+
segmentList = []
|
|
212
|
+
for cusp in cusps:
|
|
213
|
+
domain = offset.domain()
|
|
214
|
+
before = offset.trim(((domain[0][0], cusp[0]),))
|
|
215
|
+
after = -offset.trim(((cusp[1], domain[0][1]),))
|
|
216
|
+
if path is not None:
|
|
217
|
+
# Project before and after onto a 2D plane defined by the offset tangent
|
|
218
|
+
# and the surface normal at the start of the cusp.
|
|
219
|
+
# This is necessary to find the intersection point (2 equations, 2 unknowns).
|
|
220
|
+
tangent = offset.derivative((1,), cusp[0])
|
|
221
|
+
projection = np.concatenate((tangent / np.linalg.norm(tangent),
|
|
222
|
+
surface.normal(path(cusp[0])))).reshape((2,3))
|
|
223
|
+
before = before.transform(projection)
|
|
224
|
+
after = after.transform(projection)
|
|
225
|
+
block = bspy.spline_block.SplineBlock([[before, after]])
|
|
226
|
+
intersections = block.zeros()
|
|
227
|
+
for intersection in intersections:
|
|
228
|
+
segmentList.append(offset.trim(((domain[0][0], intersection[0]),)))
|
|
229
|
+
offset = offset.trim(((intersection[1], domain[0][1]),))
|
|
230
|
+
segmentList.append(offset)
|
|
231
|
+
offset = bspy.spline.Spline.join(segmentList)
|
|
232
|
+
|
|
233
|
+
return offset
|
bspy/_spline_operations.py
CHANGED
|
@@ -71,16 +71,20 @@ def confine(self, range_bounds):
|
|
|
71
71
|
if self.nInd != 1: raise ValueError("Confine only works on curves (nInd == 1)")
|
|
72
72
|
if len(range_bounds) != self.nDep: raise ValueError("len(range_bounds) must equal nDep")
|
|
73
73
|
spline = self.clamp((0,), (0,))
|
|
74
|
+
if spline is self:
|
|
75
|
+
spline = self.copy()
|
|
74
76
|
order = spline.order[0]
|
|
75
77
|
degree = order - 1
|
|
76
78
|
domain = spline.domain()
|
|
79
|
+
dtype = spline.knots[0].dtype
|
|
77
80
|
unique, counts = np.unique(spline.knots[0], return_counts=True)
|
|
78
81
|
machineEpsilon = np.finfo(self.coefs.dtype).eps
|
|
79
82
|
epsilon = np.sqrt(machineEpsilon)
|
|
80
83
|
intersections = [] # List of tuples (u, boundaryPoint, headingOutside)
|
|
81
84
|
|
|
82
85
|
def addIntersection(u, headedOutside = False):
|
|
83
|
-
|
|
86
|
+
u = dtype.type(u) # Cast to spline domain type
|
|
87
|
+
boundaryPoint = spline(u)
|
|
84
88
|
for i in range(spline.nDep):
|
|
85
89
|
if boundaryPoint[i] < range_bounds[i][0]:
|
|
86
90
|
headedOutside = True if boundaryPoint[i] < range_bounds[i][0] - epsilon else headedOutside
|
|
@@ -88,18 +92,18 @@ def confine(self, range_bounds):
|
|
|
88
92
|
if boundaryPoint[i] > range_bounds[i][1]:
|
|
89
93
|
headedOutside = True if boundaryPoint[i] > range_bounds[i][1] + epsilon else headedOutside
|
|
90
94
|
boundaryPoint[i] = range_bounds[i][1]
|
|
91
|
-
intersections.append(
|
|
95
|
+
intersections.append([u, boundaryPoint, headedOutside])
|
|
92
96
|
|
|
93
97
|
def intersectBoundary(i, j):
|
|
94
98
|
zeros = type(spline)(1, 1, spline.order, spline.nCoef, spline.knots, (spline.coefs[i] - range_bounds[i][j],)).zeros()
|
|
95
99
|
for zero in zeros:
|
|
96
100
|
if isinstance(zero, tuple):
|
|
97
|
-
headedOutside = (-1 if j == 0 else 1) * spline.derivative((1,),
|
|
101
|
+
headedOutside = (-1 if j == 0 else 1) * spline.derivative((1,), zero[0])[i] > 0
|
|
98
102
|
addIntersection(zero[0], headedOutside)
|
|
99
|
-
headedOutside = (-1 if j == 0 else 1) * spline.derivative((1,),
|
|
103
|
+
headedOutside = (-1 if j == 0 else 1) * spline.derivative((1,), zero[1])[i] > 0
|
|
100
104
|
addIntersection(zero[1], headedOutside)
|
|
101
105
|
else:
|
|
102
|
-
headedOutside = (-1 if j == 0 else 1) * spline.derivative((1,),
|
|
106
|
+
headedOutside = (-1 if j == 0 else 1) * spline.derivative((1,), zero)[i] > 0
|
|
103
107
|
addIntersection(zero, headedOutside)
|
|
104
108
|
|
|
105
109
|
addIntersection(domain[0][0]) # Confine starting point
|
|
@@ -112,21 +116,22 @@ def confine(self, range_bounds):
|
|
|
112
116
|
# Put the intersection points in order.
|
|
113
117
|
intersections.sort(key=lambda intersection: intersection[0])
|
|
114
118
|
|
|
115
|
-
#
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
# Insert order-1 knots at each intersection point.
|
|
122
|
-
for (knot, boundaryPoint, headedOutside) in intersections:
|
|
123
|
-
ix = np.searchsorted(unique, knot)
|
|
124
|
-
if unique[ix] == knot:
|
|
125
|
-
count = (order - 1) - counts[ix]
|
|
126
|
-
if count > 0:
|
|
127
|
-
spline = spline.insert_knots(((knot, count),))
|
|
119
|
+
# Insert order-1 (degree) knots at each intersection point.
|
|
120
|
+
previousKnot, previousBoundaryPoint, previousHeadedOutside = intersections[0]
|
|
121
|
+
previousIx = 0
|
|
122
|
+
for i, (knot, boundaryPoint, headedOutside) in enumerate(intersections[1:]):
|
|
123
|
+
if knot - previousKnot < epsilon:
|
|
124
|
+
intersections[previousIx][2] = headedOutside # Keep last headed outside
|
|
128
125
|
else:
|
|
129
|
-
|
|
126
|
+
ix = np.searchsorted(unique, knot)
|
|
127
|
+
if unique[ix] == knot:
|
|
128
|
+
count = degree - counts[ix]
|
|
129
|
+
if count > 0:
|
|
130
|
+
spline = spline.insert_knots((((knot, count),),))
|
|
131
|
+
else:
|
|
132
|
+
spline = spline.insert_knots((((knot, degree),),))
|
|
133
|
+
previousKnot = knot
|
|
134
|
+
previousIx = i
|
|
130
135
|
|
|
131
136
|
# Go through the boundary points, assigning boundary coefficients, interpolating between boundary points,
|
|
132
137
|
# and removing knots and coefficients where the curve stalls.
|
|
@@ -139,29 +144,37 @@ def confine(self, range_bounds):
|
|
|
139
144
|
knotAdjustment = 0.0
|
|
140
145
|
for knot, boundaryPoint, headedOutside in intersections[1:]:
|
|
141
146
|
knot += knotAdjustment
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
147
|
+
if knot - previousKnot >= epsilon:
|
|
148
|
+
ix = np.searchsorted(knots, knot, 'right') - order
|
|
149
|
+
ix = min(ix, nCoef - 1)
|
|
150
|
+
coefs[:, ix] = boundaryPoint # Assign boundary coefficients
|
|
151
|
+
if previousHeadedOutside and np.linalg.norm(boundaryPoint - previousBoundaryPoint) < epsilon:
|
|
152
|
+
# Curve has stalled, so remove intervening knots and coefficients, and adjust knot values.
|
|
153
|
+
nCoef -= ix - previousIx
|
|
154
|
+
knots = np.delete(knots, slice(previousIx + 1, ix + 1))
|
|
155
|
+
knots[previousIx + 1:] -= knot - previousKnot
|
|
156
|
+
knotAdjustment -= knot - previousKnot
|
|
157
|
+
coefs = np.delete(coefs, slice(previousIx, ix), axis=1)
|
|
158
|
+
previousHeadedOutside = headedOutside # The previous knot is unchanged, but inherits the new headedOutside value
|
|
159
|
+
else:
|
|
160
|
+
if previousHeadedOutside:
|
|
161
|
+
# If we were outside, linearly interpolate between the previous and current boundary points.
|
|
162
|
+
slope = (boundaryPoint - previousBoundaryPoint) / (knot - previousKnot)
|
|
163
|
+
for i in range(previousIx + 1, ix):
|
|
164
|
+
coefs[:, i] = coefs[:, i - 1] + ((knots[i + degree] - knots[i]) / degree) * slope
|
|
165
|
+
|
|
166
|
+
# Update previous knot
|
|
167
|
+
previousKnot = knot
|
|
168
|
+
previousBoundaryPoint = boundaryPoint
|
|
169
|
+
previousHeadedOutside = headedOutside
|
|
170
|
+
previousIx = ix
|
|
171
|
+
elif previousKnot != knot and knot == domain[0][1] and np.linalg.norm(boundaryPoint - previousBoundaryPoint) < epsilon:
|
|
172
|
+
# Curve stalled at the end. Remove the last knot and its associated coefficients.
|
|
173
|
+
# Keep the last knot if the previous and last knot are the same.
|
|
174
|
+
nCoef -= degree
|
|
175
|
+
knots = knots[:-degree]
|
|
176
|
+
knots[-1] = previousKnot
|
|
177
|
+
coefs = coefs[:,:-degree]
|
|
165
178
|
|
|
166
179
|
spline.nCoef = (nCoef,)
|
|
167
180
|
spline.knots = (knots,)
|
bspy/solid.py
CHANGED
|
@@ -753,7 +753,7 @@ class Solid:
|
|
|
753
753
|
|
|
754
754
|
# Calculate Integral(f) * first cofactor. Note that quad returns a tuple: (integral, error bound).
|
|
755
755
|
returnValue = 0.0
|
|
756
|
-
firstCofactor = boundary.manifold.normal(evalPoint, False, (0,))
|
|
756
|
+
firstCofactor = boundary.manifold.normal(evalPoint, False, (0,))[0]
|
|
757
757
|
if abs(x0 - point[0]) > epsabs and abs(firstCofactor) > epsabs:
|
|
758
758
|
returnValue = integrate.quad(fHat, x0, point[0], epsabs=epsabs, epsrel=epsrel, *quadArgs)[0] * firstCofactor
|
|
759
759
|
return returnValue
|
bspy/spline.py
CHANGED
|
@@ -5,8 +5,9 @@ from bspy.manifold import Manifold
|
|
|
5
5
|
import bspy.spline_block
|
|
6
6
|
import bspy._spline_domain
|
|
7
7
|
import bspy._spline_evaluation
|
|
8
|
-
import bspy._spline_intersection
|
|
9
8
|
import bspy._spline_fitting
|
|
9
|
+
import bspy._spline_intersection
|
|
10
|
+
import bspy._spline_milling
|
|
10
11
|
import bspy._spline_operations
|
|
11
12
|
|
|
12
13
|
@Manifold.register
|
|
@@ -59,8 +60,8 @@ class Spline(Manifold):
|
|
|
59
60
|
self.knots = tuple(np.array(kk) for kk in knots)
|
|
60
61
|
for knots, order, nCoef in zip(self.knots, self.order, self.nCoef):
|
|
61
62
|
for i in range(nCoef):
|
|
62
|
-
if not(knots[i] <= knots[i + 1] and knots[i]
|
|
63
|
-
raise ValueError("
|
|
63
|
+
if not(knots[i] <= knots[i + 1] and knots[i + order] - knots[i] > 0):
|
|
64
|
+
raise ValueError("Improper knot order or multiplicity")
|
|
64
65
|
totalCoefs = 1
|
|
65
66
|
for nCoef in self.nCoef:
|
|
66
67
|
totalCoefs *= nCoef
|
|
@@ -974,15 +975,15 @@ class Spline(Manifold):
|
|
|
974
975
|
resulting spline function will have nInd + number of independent variables
|
|
975
976
|
in the splines returned independent variables and nDep dependent variables.
|
|
976
977
|
|
|
977
|
-
order : `array-like
|
|
978
|
+
order : `array-like`, optional
|
|
978
979
|
An optional integer array of length nInd which specifies the polynomial
|
|
979
980
|
order to use in each of the independent variables. It will default to order
|
|
980
981
|
4 (degree 3) if None is specified (the default)
|
|
981
982
|
|
|
982
|
-
knots : `array-like
|
|
983
|
+
knots : `array-like`, optional
|
|
983
984
|
The initial knot sequence to use, if given
|
|
984
985
|
|
|
985
|
-
tolerance : `scalar
|
|
986
|
+
tolerance : `scalar`, optional
|
|
986
987
|
The maximum 2-norm of the difference between the given function and the
|
|
987
988
|
spline fit. Defaults to 1.0e-4.
|
|
988
989
|
|
|
@@ -1129,7 +1130,7 @@ class Spline(Manifold):
|
|
|
1129
1130
|
"""
|
|
1130
1131
|
return bspy._spline_intersection.full_domain(self)
|
|
1131
1132
|
|
|
1132
|
-
def geodesic(self, uvStart, uvEnd, tolerance = 1.0e-
|
|
1133
|
+
def geodesic(self, uvStart, uvEnd, tolerance = 1.0e-5):
|
|
1133
1134
|
"""
|
|
1134
1135
|
Determine a geodesic between two points on a surface
|
|
1135
1136
|
|
|
@@ -1141,9 +1142,9 @@ class Spline(Manifold):
|
|
|
1141
1142
|
uvEnd : `array-like`
|
|
1142
1143
|
The parameter values for the surface at the other end of the desired geodesic.
|
|
1143
1144
|
|
|
1144
|
-
tolerance : scalar
|
|
1145
|
+
tolerance : scalar, optional
|
|
1145
1146
|
The maximum error in parameter space to which the geodesic should get computed.
|
|
1146
|
-
Defaults to 1.0e-
|
|
1147
|
+
Defaults to 1.0e-5.
|
|
1147
1148
|
|
|
1148
1149
|
Returns
|
|
1149
1150
|
-------
|
|
@@ -1499,6 +1500,36 @@ class Spline(Manifold):
|
|
|
1499
1500
|
"""
|
|
1500
1501
|
return bspy._spline_fitting.line(startPoint, endPoint)
|
|
1501
1502
|
|
|
1503
|
+
def line_of_curvature(self, uvStart, is_max = True, tolerance = 1.0e-3):
|
|
1504
|
+
"""
|
|
1505
|
+
Determine a line of curvature along a surface
|
|
1506
|
+
|
|
1507
|
+
Parameters
|
|
1508
|
+
----------
|
|
1509
|
+
uvStart : `array-like`
|
|
1510
|
+
The parameter values for the surface at one end of the desired line of curvature.
|
|
1511
|
+
|
|
1512
|
+
is_max : `bool`, optional
|
|
1513
|
+
Boolean value indicating that the line of curvature should be the maximal curvature line.
|
|
1514
|
+
If False, the minimal curvature line is returned. Defaults to True.
|
|
1515
|
+
|
|
1516
|
+
tolerance : scalar, optional
|
|
1517
|
+
The maximum error in parameter space to which the geodesic should get computed.
|
|
1518
|
+
Defaults to 1.0e-3.
|
|
1519
|
+
|
|
1520
|
+
Returns
|
|
1521
|
+
-------
|
|
1522
|
+
spline : `Spline`
|
|
1523
|
+
A spline curve whose range is in the domain of the given surface. The range of the
|
|
1524
|
+
curve is the locus of points whose image under the surface map form the line of curvature
|
|
1525
|
+
starting at the given point.
|
|
1526
|
+
|
|
1527
|
+
See Also
|
|
1528
|
+
--------
|
|
1529
|
+
`solve_ode` : Solve an ordinary differential equation using spline collocation.
|
|
1530
|
+
"""
|
|
1531
|
+
return bspy._spline_milling.line_of_curvature(self, uvStart, is_max, tolerance)
|
|
1532
|
+
|
|
1502
1533
|
@staticmethod
|
|
1503
1534
|
def load(fileName):
|
|
1504
1535
|
"""
|
|
@@ -1653,6 +1684,61 @@ class Spline(Manifold):
|
|
|
1653
1684
|
the matrix formed by the tangents of the spline. If the null space is greater than one dimension, the normal will be zero.
|
|
1654
1685
|
"""
|
|
1655
1686
|
return bspy._spline_operations.normal_spline(bspy.spline_block.SplineBlock(self), indices)
|
|
1687
|
+
|
|
1688
|
+
def offset(self, edgeRadius, bitRadius=None, angle=np.pi / 2.2, path=None, subtract=False, removeCusps=False, tolerance = 1.0e-4):
|
|
1689
|
+
"""
|
|
1690
|
+
Compute the offset of a spline to a given tolerance.
|
|
1691
|
+
|
|
1692
|
+
Parameters
|
|
1693
|
+
----------
|
|
1694
|
+
edgeRadius : scalar
|
|
1695
|
+
The radius of offset. If a bit radius is specified, the edge radius is the
|
|
1696
|
+
smaller radius of the cutting edge of the drill bit, whereas bit radius specifies
|
|
1697
|
+
half of the full width of the drill bit.
|
|
1698
|
+
|
|
1699
|
+
bitRadius : scalar, optional
|
|
1700
|
+
The radius of the drill bit (half its full width). For a ball nose cutter (the default),
|
|
1701
|
+
the bit radius is the same as the edge radius. For an end mill,
|
|
1702
|
+
the bit radius is larger (typically much larger) than the edge radius.
|
|
1703
|
+
|
|
1704
|
+
angle : scalar, optional
|
|
1705
|
+
The angle at which the drill bit transitions from the edge radius to the
|
|
1706
|
+
flatter bottom of the drill bit. The angle must be in the range [0, pi/2).
|
|
1707
|
+
Defaults to pi / 2.2.
|
|
1708
|
+
|
|
1709
|
+
path : `Spline`, optional
|
|
1710
|
+
The path along self that the drill bit should contact.
|
|
1711
|
+
If specified, the path must be a 2D curve in the domain of self, self must be a 3D surface,
|
|
1712
|
+
and the offset returned is a 3D curve providing the 3D position of the drill bit,
|
|
1713
|
+
rather than the full offset surface. Defaults to None.
|
|
1714
|
+
|
|
1715
|
+
subtract : boolean, optional
|
|
1716
|
+
Flag indicating if the drill bit should be subtracted from the spline instead of added.
|
|
1717
|
+
Subtracting the drill bit returns the tool path that cuts out the spline. Defaults to False.
|
|
1718
|
+
|
|
1719
|
+
removeCusps : boolean, optional
|
|
1720
|
+
Flag indicating if cusps and their associated self-intersections should be removed from the
|
|
1721
|
+
offset. Only applicable to offset curves and paths along offset surfaces. Defaults to False.
|
|
1722
|
+
|
|
1723
|
+
tolerance : `scalar`, optional
|
|
1724
|
+
The maximum 2-norm of the difference between the offset and the
|
|
1725
|
+
spline fit. Defaults to 1.0e-4.
|
|
1726
|
+
|
|
1727
|
+
Returns
|
|
1728
|
+
-------
|
|
1729
|
+
offset : `Spline`
|
|
1730
|
+
The spline that represents the offset.
|
|
1731
|
+
|
|
1732
|
+
See Also
|
|
1733
|
+
--------
|
|
1734
|
+
`fit` : Fit the function f with a spline to a given tolerance.
|
|
1735
|
+
|
|
1736
|
+
Notes
|
|
1737
|
+
-----
|
|
1738
|
+
The offset is only defined for 2D curves and 3D surfaces with well-defined normals.
|
|
1739
|
+
The bottom of the drill bit is tangent to its lowest y value.
|
|
1740
|
+
"""
|
|
1741
|
+
return bspy._spline_milling.offset(self, edgeRadius, bitRadius, angle, path, subtract, removeCusps, tolerance)
|
|
1656
1742
|
|
|
1657
1743
|
@staticmethod
|
|
1658
1744
|
def point(point):
|
|
@@ -1971,7 +2057,7 @@ class Spline(Manifold):
|
|
|
1971
2057
|
"""
|
|
1972
2058
|
return bspy._spline_fitting.section(xytk)
|
|
1973
2059
|
|
|
1974
|
-
def solve_ode(self, nLeft, nRight, FAndF_u, tolerance = 1.0e-6, args = ()):
|
|
2060
|
+
def solve_ode(self, nLeft, nRight, FAndF_u, tolerance = 1.0e-6, args = (), includeEstimate = False):
|
|
1975
2061
|
"""
|
|
1976
2062
|
Numerically solve an ordinary differential equation with boundary conditions.
|
|
1977
2063
|
|
|
@@ -1994,26 +2080,31 @@ class Spline(Manifold):
|
|
|
1994
2080
|
FAndF_u : Python function
|
|
1995
2081
|
FAndF_u must have exactly this calling sequence: FAndF_u(t, uData, *args). t is a scalar set
|
|
1996
2082
|
to the desired value of the independent variable of the ODE. uData will be a numpy matrix of shape
|
|
1997
|
-
(self.nDep, nOrder) whose columns are
|
|
2083
|
+
(self.nDep, nOrder) whose columns are u, ... , u^(nOrder - 1). It must return a numpy
|
|
1998
2084
|
vector of length self.nDep and a numpy array whose shape is (self.nDep, self.nDep, nOrder).
|
|
1999
2085
|
The first output vector is the value of the forcing function F at (t, uData). The numpy
|
|
2000
2086
|
array is the array of partial derivatives with respect to all the numbers in uData. Thus, if
|
|
2001
2087
|
this array is called jacobian, then jacobian[:, i, j] is the gradient of the forcing function with
|
|
2002
2088
|
respect to uData[i, j].
|
|
2003
2089
|
|
|
2004
|
-
tolerance : scalar
|
|
2005
|
-
The relative error to which the ODE should get solved.
|
|
2090
|
+
tolerance : scalar, optional
|
|
2091
|
+
The relative error to which the ODE should get solved. Default is 1.0e-6.
|
|
2006
2092
|
|
|
2007
|
-
args : tuple
|
|
2093
|
+
args : tuple, optional
|
|
2008
2094
|
Additional arguments to pass to the user-defined function FAndF_u. For example, if FAndF_u has the
|
|
2009
|
-
FAndF_u(t, uData, a, b, c), then args must be a tuple of length 3.
|
|
2095
|
+
FAndF_u(t, uData, a, b, c), then args must be a tuple of length 3. Default is ().
|
|
2096
|
+
|
|
2097
|
+
includeEstimate : bool, optional
|
|
2098
|
+
If `includeEstimate` is True, the uData passed to `FAndF_u` will be a numpy matrix of shape
|
|
2099
|
+
(self.nDep, nOrder + 1) whose columns are u, ... , u^(nOrder). The last column will be the most
|
|
2100
|
+
recent estimate of u^(nOrder)(t). Default is False.
|
|
2010
2101
|
|
|
2011
2102
|
Notes
|
|
2012
2103
|
=====
|
|
2013
2104
|
This method uses B-splines as finite elements. The ODE itself is discretized using
|
|
2014
2105
|
collocation.
|
|
2015
2106
|
"""
|
|
2016
|
-
return bspy._spline_fitting.solve_ode(self, nLeft, nRight, FAndF_u, tolerance, args)
|
|
2107
|
+
return bspy._spline_fitting.solve_ode(self, nLeft, nRight, FAndF_u, tolerance, args, includeEstimate)
|
|
2017
2108
|
|
|
2018
2109
|
@staticmethod
|
|
2019
2110
|
def sphere(radius, tolerance = None):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: bspy
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.4
|
|
4
4
|
Summary: Library for manipulating and rendering non-uniform B-splines
|
|
5
5
|
Home-page: http://github.com/ericbrec/BSpy
|
|
6
6
|
Author: Eric Brechner
|
|
@@ -28,14 +28,13 @@ Requires-Dist: numpy
|
|
|
28
28
|
Requires-Dist: scipy
|
|
29
29
|
Requires-Dist: PyOpenGL
|
|
30
30
|
Requires-Dist: pyopengltk
|
|
31
|
+
Dynamic: license-file
|
|
31
32
|
|
|
32
33
|
# BSpy
|
|
33
34
|
Library for manipulating and rendering B-spline curves, surfaces, and multidimensional manifolds with non-uniform knots in each dimension.
|
|
34
35
|
|
|
35
|
-
The [
|
|
36
|
-
|
|
37
|
-
The [Spline](https://ericbrec.github.io/BSpy/bspy/spline.html) class has a method to fit multidimensional data for scalar and vector functions of single and multiple variables. It also can fit splines to functions, to solutions for ordinary differential equations (ODEs), and to geodesics.
|
|
38
|
-
Spline has methods to create points, lines, circular arcs, spheres, cones, cylinders, tori, ruled surfaces, surfaces of revolution, and four-sided patches.
|
|
36
|
+
The [Spline](https://ericbrec.github.io/BSpy/bspy/spline.html) class has a method to fit multidimensional data for scalar and vector functions of single and multiple variables. It also can fit splines to functions, to solutions for ordinary differential equations (ODEs), geodesics, offsets, and lines of curvature.
|
|
37
|
+
Spline has methods to create points, lines, circular arcs, spheres, cones, cylinders, tori, ruled surfaces, surfaces of revolution, four-sided patches, and compositions of splines.
|
|
39
38
|
Other methods add, subtract, and multiply splines, as well as confine spline curves to a given range.
|
|
40
39
|
There are methods to evaluate spline values, derivatives, normals, integrals, continuity, curvature, and the Jacobian, as well as methods that return spline representations of derivatives, normals, integrals, graphs, and convolutions.
|
|
41
40
|
In addition, there are methods to manipulate the domain of splines, including trim, join, split, reparametrize, transpose, reverse, add and remove knots, elevate and extrapolate, and fold and unfold.
|
|
@@ -43,16 +42,18 @@ There are methods to manipulate the range of splines, including dot product, cro
|
|
|
43
42
|
Finally, there are methods to compute the zeros and contours of a spline and to intersect two splines.
|
|
44
43
|
Splines can be saved and loaded in json format.
|
|
45
44
|
|
|
46
|
-
The [Hyperplane](https://ericbrec.github.io/BSpy/bspy/hyperplane.html) class has methods to create individual hyperplanes in any dimension, along with axis-aligned hyperplanes and hypercubes.
|
|
47
|
-
|
|
48
|
-
The [Solid](https://ericbrec.github.io/BSpy/bspy/solid.html) class has methods to construct n-dimensional solids from trimmed [Manifold](https://ericbrec.github.io/BSpy/bspy/manifold.html) boundaries. Each solid consists of a list of boundaries and a Boolean value that indicates if the solid contains infinity. Each [Boundary](https://ericbrec.github.io/BSpy/bspy/solid.html) consists of a manifold (currently a [Hyperplane](https://ericbrec.github.io/BSpy/bspy/hyperplane.html) or [Spline](https://ericbrec.github.io/BSpy/bspy/spline.html)) and a domain solid that trims the manifold. Solids have methods to form the intersection, union, difference, and complement of solids. There are methods to compute point containment, winding numbers, surface integrals, and volume integrals. There are also methods to translate, transform, and slice solids. Solids can be saved and loaded in json format.
|
|
49
|
-
|
|
50
45
|
The [SplineBlock](https://ericbrec.github.io/BSpy/bspy/spline_block.html) class has methods to process an array-like collection of splines that represent a system of equations. There are highly-optimized methods to compute the contours and zeros of a spline block, as well as a variety of methods to manipulate and evaluate a spline block and its derivatives.
|
|
51
46
|
|
|
52
47
|
The [BSpyConvert](https://pypi.org/project/BSpyConvert/) package converts BSpy splines and solid models to and from [OpenCascade (OCCT)](https://dev.opencascade.org/) equivalents and a variety of geometry and CAD file formats, including STEP, IGES, and STL.
|
|
53
48
|
|
|
49
|
+
The [Hyperplane](https://ericbrec.github.io/BSpy/bspy/hyperplane.html) class has methods to create individual hyperplanes in any dimension, along with axis-aligned hyperplanes and hypercubes.
|
|
50
|
+
|
|
51
|
+
The [Manifold](https://ericbrec.github.io/BSpy/bspy/manifold.html) abstract base class for [Hyperplane](https://ericbrec.github.io/BSpy/bspy/hyperplane.html) and [Spline](https://ericbrec.github.io/BSpy/bspy/spline.html).
|
|
52
|
+
|
|
53
|
+
The [Solid](https://ericbrec.github.io/BSpy/bspy/solid.html) class has methods to construct n-dimensional solids from trimmed [Manifold](https://ericbrec.github.io/BSpy/bspy/manifold.html) boundaries. Each solid consists of a list of boundaries and a Boolean value that indicates if the solid contains infinity. Each [Boundary](https://ericbrec.github.io/BSpy/bspy/solid.html) consists of a manifold (currently a [Hyperplane](https://ericbrec.github.io/BSpy/bspy/hyperplane.html) or [Spline](https://ericbrec.github.io/BSpy/bspy/spline.html)) and a domain solid that trims the manifold. Solids have methods to form the intersection, union, difference, and complement of solids. There are methods to compute point containment, winding numbers, surface integrals, and volume integrals. There are also methods to translate, transform, and slice solids. Solids can be saved and loaded in json format.
|
|
54
|
+
|
|
54
55
|
The [SplineOpenGLFrame](https://ericbrec.github.io/BSpy/bspy/splineOpenGLFrame.html) class is an
|
|
55
|
-
[OpenGLFrame](https://pypi.org/project/pyopengltk/) with custom shaders to render spline curves and
|
|
56
|
+
[OpenGLFrame](https://pypi.org/project/pyopengltk/) with custom shaders to render spline curves, surfaces, and solids. Spline surfaces with more
|
|
56
57
|
than 3 dependent variables will have their added dimensions rendered as colors (up to 6 dependent variables are supported). Only tested on Windows systems.
|
|
57
58
|
|
|
58
59
|
The [Viewer](https://ericbrec.github.io/BSpy/bspy/viewer.html) class is a
|
|
@@ -60,7 +61,7 @@ The [Viewer](https://ericbrec.github.io/BSpy/bspy/viewer.html) class is a
|
|
|
60
61
|
[SplineOpenGLFrame](https://ericbrec.github.io/BSpy/bspy/splineOpenGLFrame.html),
|
|
61
62
|
a tree view full of solids and splines, and a set of controls to adjust and view the selected solids and splines. Only tested on Windows systems.
|
|
62
63
|
|
|
63
|
-
The [Graphics](https://ericbrec.github.io/BSpy/bspy/viewer.html#Graphics) class is a graphics engine to display splines.
|
|
64
|
+
The [Graphics](https://ericbrec.github.io/BSpy/bspy/viewer.html#Graphics) class is a graphics engine to display solids and splines.
|
|
64
65
|
It launches a [Viewer](https://ericbrec.github.io/BSpy/bspy/viewer.html) and issues commands to the viewer for use
|
|
65
66
|
in [jupyter](https://jupyter.org/) notebooks and other scripting environments. Only tested on Windows systems.
|
|
66
67
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
bspy/__init__.py,sha256=LnJx7iHah7A4vud9y64LR61rBtYnuhV4Wno9O2IEK1I,1499
|
|
2
|
+
bspy/_spline_domain.py,sha256=pAoauqMJrY80k6bhnMJKnPWY_p_zx1k4daP3krsAN8M,33303
|
|
3
|
+
bspy/_spline_evaluation.py,sha256=aQ9w4N5MQDol7_OQ6HLljMtCbGVuS5iCAlM7Sg2q8B4,9656
|
|
4
|
+
bspy/_spline_fitting.py,sha256=TH2fwHu_wsRSvFIXngfWxOAHKex0ilP0Xi8ThXYQ0dI,52002
|
|
5
|
+
bspy/_spline_intersection.py,sha256=CErinlW8co_kaamM9Wb1TFrbB6EmjlUh1exYlcXkrpQ,67349
|
|
6
|
+
bspy/_spline_milling.py,sha256=OzhYi3eNPcL0INk7k_ryNwlt8SUniWL10ULxgE_nKHI,11435
|
|
7
|
+
bspy/_spline_operations.py,sha256=O2AsfJb0PvaNbM2Rp3_AbNXKo8X6X6RJDwDclqOwT7o,43564
|
|
8
|
+
bspy/hyperplane.py,sha256=gnVZ7rjisGpzHfm1moItyzq8mO7HguzzpY4dpFwyDiw,24840
|
|
9
|
+
bspy/manifold.py,sha256=vjgyz0M1mkgenUnTIbX7NFg1fUCgXtStr6ofF4oSLgg,14470
|
|
10
|
+
bspy/solid.py,sha256=tsO7fcGj-x3SWH2fBSkeu-Hx5vX0KMFOt1baH-OYNgQ,36995
|
|
11
|
+
bspy/spline.py,sha256=vSscV6FRXjFp1FnhS1IoyFiy2pRFgr8-kZyKXw8weJU,105842
|
|
12
|
+
bspy/splineOpenGLFrame.py,sha256=N8elVJrt24_utOSoTaM5Ue5De2M4DxrquyB7o2lLLD4,96256
|
|
13
|
+
bspy/spline_block.py,sha256=O8MzfBEygVdAx57DoJMwzjkw349BQqht7_RVu8MO0Fg,20127
|
|
14
|
+
bspy/viewer.py,sha256=_iQCyEpsBFPBLLuHq7tc43IPVvlcqxdp0Hig0uvpQns,34349
|
|
15
|
+
bspy-4.4.dist-info/licenses/LICENSE,sha256=nLfJULN68Jw6GfCJp4xeMksGuRdyWNdgEsZGjw2twig,1091
|
|
16
|
+
bspy-4.4.dist-info/METADATA,sha256=8Y8G0jkCCuFdmJVb9CZgPJ5kuwaQivu89loQ6kyqlbc,7138
|
|
17
|
+
bspy-4.4.dist-info/WHEEL,sha256=ck4Vq1_RXyvS4Jt6SI0Vz6fyVs4GWg7AINwpsaGEgPE,91
|
|
18
|
+
bspy-4.4.dist-info/top_level.txt,sha256=fotZnJn6aCwgUbBEV3hslIko7Nw-eqtHLq2eyJLlFsY,5
|
|
19
|
+
bspy-4.4.dist-info/RECORD,,
|
bspy-4.3.dist-info/RECORD
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
bspy/__init__.py,sha256=LnJx7iHah7A4vud9y64LR61rBtYnuhV4Wno9O2IEK1I,1499
|
|
2
|
-
bspy/_spline_domain.py,sha256=bQQsJlKstIYdbEKIW7vr-7nTKat8y9thYc7jxzZHNFQ,33238
|
|
3
|
-
bspy/_spline_evaluation.py,sha256=WIv0tLZNLy0uHNj9YwR7vbputgT2Mn5QDXZzlD6ousk,9638
|
|
4
|
-
bspy/_spline_fitting.py,sha256=CEkmUQalXTT5N5ZOJpFF4Y2DI-ARlZap9IaCJ_Zg2Ws,50762
|
|
5
|
-
bspy/_spline_intersection.py,sha256=8FPTh4IDtzkRpieNtlnw8VhLabPzY4E_LWDaGxHIMTM,66973
|
|
6
|
-
bspy/_spline_operations.py,sha256=8yJGp4iVVvQ1zcUHAKgNq2TMIjDUhacf5XSoHp2jmVo,42799
|
|
7
|
-
bspy/hyperplane.py,sha256=gnVZ7rjisGpzHfm1moItyzq8mO7HguzzpY4dpFwyDiw,24840
|
|
8
|
-
bspy/manifold.py,sha256=vjgyz0M1mkgenUnTIbX7NFg1fUCgXtStr6ofF4oSLgg,14470
|
|
9
|
-
bspy/solid.py,sha256=ufNs5JV0jQ1A13pUY61N0pcW6Ep-DZmXUau7GHHcKk4,36992
|
|
10
|
-
bspy/spline.py,sha256=1USitAm1FIQg5JuWa1xLrVQQ2cWRGfP4VhQnS8XOKc8,101377
|
|
11
|
-
bspy/splineOpenGLFrame.py,sha256=N8elVJrt24_utOSoTaM5Ue5De2M4DxrquyB7o2lLLD4,96256
|
|
12
|
-
bspy/spline_block.py,sha256=O8MzfBEygVdAx57DoJMwzjkw349BQqht7_RVu8MO0Fg,20127
|
|
13
|
-
bspy/viewer.py,sha256=_iQCyEpsBFPBLLuHq7tc43IPVvlcqxdp0Hig0uvpQns,34349
|
|
14
|
-
bspy-4.3.dist-info/LICENSE,sha256=nLfJULN68Jw6GfCJp4xeMksGuRdyWNdgEsZGjw2twig,1091
|
|
15
|
-
bspy-4.3.dist-info/METADATA,sha256=lV6eciVqRf3FqWslbTD5OWb3gMD6-gv6WB-hkSRadPM,7044
|
|
16
|
-
bspy-4.3.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
|
17
|
-
bspy-4.3.dist-info/top_level.txt,sha256=fotZnJn6aCwgUbBEV3hslIko7Nw-eqtHLq2eyJLlFsY,5
|
|
18
|
-
bspy-4.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|