bspy 4.4__tar.gz → 4.4.1__tar.gz
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-4.4 → bspy-4.4.1}/PKG-INFO +1 -1
- {bspy-4.4 → bspy-4.4.1}/bspy/_spline_domain.py +36 -5
- {bspy-4.4 → bspy-4.4.1}/bspy/_spline_fitting.py +6 -2
- {bspy-4.4 → bspy-4.4.1}/bspy/_spline_intersection.py +35 -34
- {bspy-4.4 → bspy-4.4.1}/bspy/_spline_milling.py +108 -53
- {bspy-4.4 → bspy-4.4.1}/bspy/spline.py +21 -2
- {bspy-4.4 → bspy-4.4.1}/bspy.egg-info/PKG-INFO +1 -1
- {bspy-4.4 → bspy-4.4.1}/setup.cfg +1 -1
- {bspy-4.4 → bspy-4.4.1}/LICENSE +0 -0
- {bspy-4.4 → bspy-4.4.1}/README.md +0 -0
- {bspy-4.4 → bspy-4.4.1}/bspy/__init__.py +0 -0
- {bspy-4.4 → bspy-4.4.1}/bspy/_spline_evaluation.py +0 -0
- {bspy-4.4 → bspy-4.4.1}/bspy/_spline_operations.py +0 -0
- {bspy-4.4 → bspy-4.4.1}/bspy/hyperplane.py +0 -0
- {bspy-4.4 → bspy-4.4.1}/bspy/manifold.py +0 -0
- {bspy-4.4 → bspy-4.4.1}/bspy/solid.py +0 -0
- {bspy-4.4 → bspy-4.4.1}/bspy/splineOpenGLFrame.py +0 -0
- {bspy-4.4 → bspy-4.4.1}/bspy/spline_block.py +0 -0
- {bspy-4.4 → bspy-4.4.1}/bspy/viewer.py +0 -0
- {bspy-4.4 → bspy-4.4.1}/bspy.egg-info/SOURCES.txt +0 -0
- {bspy-4.4 → bspy-4.4.1}/bspy.egg-info/dependency_links.txt +0 -0
- {bspy-4.4 → bspy-4.4.1}/bspy.egg-info/requires.txt +0 -0
- {bspy-4.4 → bspy-4.4.1}/bspy.egg-info/top_level.txt +0 -0
- {bspy-4.4 → bspy-4.4.1}/pyproject.toml +0 -0
{bspy-4.4 → bspy-4.4.1}/PKG-INFO
RENAMED
|
@@ -1,6 +1,34 @@
|
|
|
1
1
|
import numpy as np
|
|
2
|
+
import bspy
|
|
2
3
|
from bspy.manifold import Manifold
|
|
3
4
|
|
|
5
|
+
def arc_length_map(self, tolerance):
|
|
6
|
+
if self.nInd != 1: raise ValueError("Spline doesn't have exactly one independent variable")
|
|
7
|
+
|
|
8
|
+
# Compute the length of the spline
|
|
9
|
+
curveLength = self.integral()
|
|
10
|
+
domain = self.domain()[0]
|
|
11
|
+
guess = bspy.Spline.line([0.0], [1.0])
|
|
12
|
+
guess = guess.elevate([4])
|
|
13
|
+
|
|
14
|
+
# Solve the ODE
|
|
15
|
+
def arcLengthF(t, uData):
|
|
16
|
+
uValue = (1.0 - uData[0][0]) * domain[0] + uData[0][0] * domain[1]
|
|
17
|
+
uValue = np.clip(uValue, domain[0], domain[1])
|
|
18
|
+
d1 = self.derivative([1], [uValue])
|
|
19
|
+
d2 = self.derivative([2], [uValue])
|
|
20
|
+
speed = np.sqrt(d1 @ d1)
|
|
21
|
+
d1d2 = d1 @ d2
|
|
22
|
+
return np.array([curveLength / speed]), np.array([-curveLength * d1d2 / speed ** 3]).reshape((1, 1, 1))
|
|
23
|
+
arcLengthMap = guess.solve_ode(1, 0, arcLengthF, tolerance)
|
|
24
|
+
|
|
25
|
+
# Adjust range to match domain
|
|
26
|
+
|
|
27
|
+
arcLengthMap *= (domain[1] - domain[0]) / arcLengthMap(1.0)[0]
|
|
28
|
+
arcLengthMap += domain[0]
|
|
29
|
+
arcLengthMap.coefs[0][-1] = domain[1]
|
|
30
|
+
return arcLengthMap
|
|
31
|
+
|
|
4
32
|
def clamp(self, left, right):
|
|
5
33
|
bounds = [[None, None] for i in range(self.nInd)]
|
|
6
34
|
|
|
@@ -404,12 +432,11 @@ def join(splineList):
|
|
|
404
432
|
splDomain = spl.domain()[0]
|
|
405
433
|
start2 = spl(splDomain[0])
|
|
406
434
|
end2 = spl(splDomain[1])
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
if minDist == gaps[0] or minDist == gaps[1]:
|
|
410
|
-
workingSpline = workingSpline.reverse()
|
|
411
|
-
if minDist == gaps[1] or minDist == gaps[3]:
|
|
435
|
+
ixMin = np.argmin([np.linalg.norm(vecDiff) for vecDiff in [end1 - start2, end1 - end2, start1 - start2, start1 - end2]])
|
|
436
|
+
if ixMin == 1 or ixMin == 3:
|
|
412
437
|
spl = spl.reverse()
|
|
438
|
+
if ixMin == 2 or ixMin == 3:
|
|
439
|
+
workingSpline = workingSpline.reverse()
|
|
413
440
|
maxOrder = max(workingSpline.order[0], spl.order[0])
|
|
414
441
|
workingSpline = workingSpline.elevate([maxOrder - workingSpline.order[0]])
|
|
415
442
|
spl = spl.elevate([maxOrder - spl.order[0]])
|
|
@@ -610,6 +637,8 @@ def trim(self, newDomain):
|
|
|
610
637
|
if multiplicity > 0:
|
|
611
638
|
newKnots.append((bounds[0], multiplicity))
|
|
612
639
|
noChange = False
|
|
640
|
+
if bounds[0] != knots[order - 1]:
|
|
641
|
+
noChange = False
|
|
613
642
|
|
|
614
643
|
if not np.isnan(bounds[1]):
|
|
615
644
|
if not(knots[order - 1] <= bounds[1] <= knots[-order]): raise ValueError("Invalid newDomain")
|
|
@@ -627,6 +656,8 @@ def trim(self, newDomain):
|
|
|
627
656
|
if multiplicity > 0:
|
|
628
657
|
newKnots.append((bounds[1], multiplicity))
|
|
629
658
|
noChange = False
|
|
659
|
+
if bounds[1] != knots[-order]:
|
|
660
|
+
noChange = False
|
|
630
661
|
|
|
631
662
|
newKnotsList.append(newKnots)
|
|
632
663
|
|
|
@@ -339,6 +339,7 @@ def fit(domain, f, order = None, knots = None, tolerance = 1.0e-4):
|
|
|
339
339
|
nInd = len(domain)
|
|
340
340
|
midPoint = f(0.5 * (domain.T[0] + domain.T[1]))
|
|
341
341
|
if not type(midPoint) is bspy.Spline:
|
|
342
|
+
midPoint = np.array(midPoint).flatten()
|
|
342
343
|
nDep = len(midPoint)
|
|
343
344
|
|
|
344
345
|
# Make sure order and knots conform to this
|
|
@@ -389,7 +390,10 @@ def fit(domain, f, order = None, knots = None, tolerance = 1.0e-4):
|
|
|
389
390
|
# Create a tuple for the u value (must be a tuple to use it as a dictionary key)
|
|
390
391
|
uValue = tuple([uvw[i][indices[i]] for i in range(nInd)])
|
|
391
392
|
if not uValue in fDictionary:
|
|
392
|
-
|
|
393
|
+
newValue = f(np.array(uValue))
|
|
394
|
+
if not type(newValue) is bspy.Spline:
|
|
395
|
+
newValue = np.array(newValue).flatten()
|
|
396
|
+
fDictionary[uValue] = newValue
|
|
393
397
|
fValues.append(fDictionary[uValue])
|
|
394
398
|
iLast = nInd - 1
|
|
395
399
|
while iLast >= 0:
|
|
@@ -1067,7 +1071,7 @@ def solve_ode(self, nLeft, nRight, FAndF_u, tolerance = 1.0e-6, args = (), inclu
|
|
|
1067
1071
|
|
|
1068
1072
|
# Is it time to give up?
|
|
1069
1073
|
|
|
1070
|
-
if (not done or continuation < 1.0) and n >
|
|
1074
|
+
if (not done or continuation < 1.0) and n > 10000:
|
|
1071
1075
|
raise RuntimeError("Can't find solution with given initial guess")
|
|
1072
1076
|
|
|
1073
1077
|
# Estimate the error
|
|
@@ -174,7 +174,6 @@ def _intersect_convex_hull_with_x_interval(lowerHull, upperHull, epsilon, xInter
|
|
|
174
174
|
for p1 in hull[1:]:
|
|
175
175
|
yDelta = p0[1] - p1[1]
|
|
176
176
|
if p0[1] * p1[1] <= 0.0 and yDelta != 0.0:
|
|
177
|
-
yDelta = p0[1] - p1[1]
|
|
178
177
|
alpha = p0[1] / yDelta
|
|
179
178
|
xNew = p0[0] * (1.0 - alpha) + p1[0] * alpha
|
|
180
179
|
if sign * yDelta > 0.0:
|
|
@@ -255,14 +254,13 @@ def _refine_projected_polyhedron(interval):
|
|
|
255
254
|
|
|
256
255
|
# Compute the coefficients for f(x) = x for the independent variable and its knots.
|
|
257
256
|
xData = spline.greville(ind)
|
|
258
|
-
|
|
257
|
+
if len(xData) == 1:
|
|
258
|
+
xData = spline.domain()[ind]
|
|
259
|
+
|
|
259
260
|
# Loop through each dependent variable in this row to refine the interval containing the root for this independent variable.
|
|
260
|
-
for yData, ySplineBounds, yBounds in zip(coefs, spline.range_bounds(),
|
|
261
|
-
interval.bounds[nDep:nDep + spline.nDep]):
|
|
261
|
+
for yData, ySplineBounds, yBounds in zip(coefs, spline.range_bounds(), interval.bounds[nDep:nDep + spline.nDep]):
|
|
262
262
|
# Compute the 2D convex hull of the knot coefficients and the spline's coefficients
|
|
263
263
|
lowerHull, upperHull = _convex_hull_2D(xData, yData.ravel(), yBounds, yBounds - ySplineBounds)
|
|
264
|
-
if lowerHull is None or upperHull is None:
|
|
265
|
-
return roots, intervals
|
|
266
264
|
|
|
267
265
|
# Intersect the convex hull with the xInterval along the x axis (the knot coefficients axis).
|
|
268
266
|
xInterval = _intersect_convex_hull_with_x_interval(lowerHull, upperHull, epsilon, xInterval)
|
|
@@ -943,7 +941,8 @@ def contours(self):
|
|
|
943
941
|
|
|
944
942
|
def intersect(self, other):
|
|
945
943
|
intersections = []
|
|
946
|
-
|
|
944
|
+
# Compute the number of degrees of freedom of the intersection.
|
|
945
|
+
dof = self.nInd + other.domain_dimension() - self.nDep
|
|
947
946
|
|
|
948
947
|
# Spline-Hyperplane intersection.
|
|
949
948
|
if isinstance(other, Hyperplane):
|
|
@@ -953,7 +952,7 @@ def intersect(self, other):
|
|
|
953
952
|
spline = self.dot(other._normal) - np.atleast_1d(np.dot(other._normal, other._point))
|
|
954
953
|
|
|
955
954
|
# Curve-Line intersection.
|
|
956
|
-
if
|
|
955
|
+
if dof == 0:
|
|
957
956
|
# Find the intersection points and intervals.
|
|
958
957
|
zeros = spline.zeros()
|
|
959
958
|
# Convert each intersection point into a Manifold.Crossing and each intersection interval into a Manifold.Coincidence.
|
|
@@ -974,10 +973,10 @@ def intersect(self, other):
|
|
|
974
973
|
intersections.append(Manifold.Crossing(Hyperplane(1.0, zero[1] + epsilon, 0.0), Hyperplane(1.0, planeBounds[1], 0.0)))
|
|
975
974
|
|
|
976
975
|
# Now, create the coincidence.
|
|
977
|
-
left = Solid(
|
|
976
|
+
left = Solid(1, False)
|
|
978
977
|
left.add_boundary(Boundary(Hyperplane(-1.0, zero[0], 0.0), Solid(0, True)))
|
|
979
978
|
left.add_boundary(Boundary(Hyperplane(1.0, zero[1], 0.0), Solid(0, True)))
|
|
980
|
-
right = Solid(
|
|
979
|
+
right = Solid(1, False)
|
|
981
980
|
if planeBounds[0] > planeBounds[1]:
|
|
982
981
|
planeBounds = (planeBounds[1], planeBounds[0])
|
|
983
982
|
right.add_boundary(Boundary(Hyperplane(-1.0, planeBounds[0], 0.0), Solid(0, True)))
|
|
@@ -992,7 +991,7 @@ def intersect(self, other):
|
|
|
992
991
|
intersections.append(Manifold.Crossing(Hyperplane(1.0, zero, 0.0), Hyperplane(1.0, projection @ (self((zero,)) - other._point), 0.0)))
|
|
993
992
|
|
|
994
993
|
# Surface-Plane intersection.
|
|
995
|
-
elif
|
|
994
|
+
elif dof == 1:
|
|
996
995
|
# Find the intersection contours, which are returned as splines.
|
|
997
996
|
contours = spline.contours()
|
|
998
997
|
# Convert each contour into a Manifold.Crossing.
|
|
@@ -1015,8 +1014,8 @@ def intersect(self, other):
|
|
|
1015
1014
|
# Construct a spline block that represents the intersection.
|
|
1016
1015
|
block = bspy.spline_block.SplineBlock([[self, -other]])
|
|
1017
1016
|
|
|
1018
|
-
# Curve-Curve intersection.
|
|
1019
|
-
if
|
|
1017
|
+
# Zero degrees of freedom, typically a Curve-Curve intersection.
|
|
1018
|
+
if dof == 0:
|
|
1020
1019
|
# Find the intersection points and intervals.
|
|
1021
1020
|
zeros = block.zeros()
|
|
1022
1021
|
# Convert each intersection point into a Manifold.Crossing and each intersection interval into a Manifold.Coincidence.
|
|
@@ -1041,10 +1040,10 @@ def intersect(self, other):
|
|
|
1041
1040
|
intersections.append(Manifold.Crossing(Hyperplane(1.0, zero[1][0], 0.0), Hyperplane(1.0, zero[1][1] + epsilon, 0.0)))
|
|
1042
1041
|
|
|
1043
1042
|
# Now, create the coincidence.
|
|
1044
|
-
left = Solid(
|
|
1043
|
+
left = Solid(self.nInd, False)
|
|
1045
1044
|
left.add_boundary(Boundary(Hyperplane(-1.0, zero[0][0], 0.0), Solid(0, True)))
|
|
1046
1045
|
left.add_boundary(Boundary(Hyperplane(1.0, zero[1][0], 0.0), Solid(0, True)))
|
|
1047
|
-
right = Solid(
|
|
1046
|
+
right = Solid(other.nInd, False)
|
|
1048
1047
|
right.add_boundary(Boundary(Hyperplane(-1.0, zero[0][1], 0.0), Solid(0, True)))
|
|
1049
1048
|
right.add_boundary(Boundary(Hyperplane(1.0, zero[1][1], 0.0), Solid(0, True)))
|
|
1050
1049
|
alignment = np.dot(self.normal(zero[0][0]), other.normal(zero[0][1])) # Use the first zeros, since B-splines are closed on the left
|
|
@@ -1054,10 +1053,10 @@ def intersect(self, other):
|
|
|
1054
1053
|
intersections.append(Manifold.Coincidence(left, right, alignment, np.atleast_2d(transform), np.atleast_2d(1.0 / transform), np.atleast_1d(translation)))
|
|
1055
1054
|
else:
|
|
1056
1055
|
# Intersection is a point, so create a Manifold.Crossing.
|
|
1057
|
-
intersections.append(Manifold.Crossing(Hyperplane(1.0, zero[:
|
|
1056
|
+
intersections.append(Manifold.Crossing(Hyperplane(1.0, zero[:self.nInd], 0.0), Hyperplane(1.0, zero[self.nInd:], 0.0)))
|
|
1058
1057
|
|
|
1059
|
-
# Surface-Surface intersection.
|
|
1060
|
-
elif
|
|
1058
|
+
# One degree of freedom, typically a Surface-Surface intersection.
|
|
1059
|
+
elif dof == 1:
|
|
1061
1060
|
if "Name" in self.metadata and "Name" in other.metadata:
|
|
1062
1061
|
logging.info(f"intersect:{self.metadata['Name']}:{other.metadata['Name']}")
|
|
1063
1062
|
# Find the intersection contours, which are returned as splines.
|
|
@@ -1075,32 +1074,34 @@ def intersect(self, other):
|
|
|
1075
1074
|
# Convert each contour into a Manifold.Crossing, swapping the manifolds back.
|
|
1076
1075
|
for contour in contours:
|
|
1077
1076
|
# Swap left and right, compared to not swapped.
|
|
1078
|
-
left = bspy.Spline(contour.nInd,
|
|
1079
|
-
right = bspy.Spline(contour.nInd,
|
|
1077
|
+
left = bspy.Spline(contour.nInd, self.nInd, contour.order, contour.nCoef, contour.knots, contour.coefs[other.nInd:], contour.metadata)
|
|
1078
|
+
right = bspy.Spline(contour.nInd, other.nInd, contour.order, contour.nCoef, contour.knots, contour.coefs[:other.nInd], contour.metadata)
|
|
1080
1079
|
intersections.append(Manifold.Crossing(left, right))
|
|
1081
1080
|
else:
|
|
1082
1081
|
# Convert each contour into a Manifold.Crossing.
|
|
1083
1082
|
for contour in contours:
|
|
1084
|
-
left = bspy.Spline(contour.nInd,
|
|
1085
|
-
right = bspy.Spline(contour.nInd,
|
|
1083
|
+
left = bspy.Spline(contour.nInd, self.nInd, contour.order, contour.nCoef, contour.knots, contour.coefs[:self.nInd], contour.metadata)
|
|
1084
|
+
right = bspy.Spline(contour.nInd, other.nInd, contour.order, contour.nCoef, contour.knots, contour.coefs[self.nInd:], contour.metadata)
|
|
1086
1085
|
intersections.append(Manifold.Crossing(left, right))
|
|
1087
1086
|
else:
|
|
1088
1087
|
return NotImplemented
|
|
1089
1088
|
else:
|
|
1090
1089
|
return NotImplemented
|
|
1091
1090
|
|
|
1092
|
-
#
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1091
|
+
# If self and other have normals, ensure they are pointing in the correct direction.
|
|
1092
|
+
if self.nInd + 1 == self.nDep and other.domain_dimension() + 1 == self.nDep:
|
|
1093
|
+
# Ensure the normals point outwards for both Manifolds in each crossing intersection.
|
|
1094
|
+
# Note that evaluating left and right at 0.5 is always valid because either they are points or curves with [0.0, 1.0] domains.
|
|
1095
|
+
domainPoint = np.atleast_1d(0.5)
|
|
1096
|
+
for i, intersection in enumerate(intersections):
|
|
1097
|
+
if isinstance(intersection, Manifold.Crossing):
|
|
1098
|
+
left = intersection.left
|
|
1099
|
+
right = intersection.right
|
|
1100
|
+
if np.dot(self.tangent_space(left.evaluate(domainPoint)) @ left.normal(domainPoint), other.normal(right.evaluate(domainPoint))) < 0.0:
|
|
1101
|
+
left = left.flip_normal()
|
|
1102
|
+
if np.dot(other.tangent_space(right.evaluate(domainPoint)) @ right.normal(domainPoint), self.normal(left.evaluate(domainPoint))) < 0.0:
|
|
1103
|
+
right = right.flip_normal()
|
|
1104
|
+
intersections[i] = Manifold.Crossing(left, right)
|
|
1104
1105
|
|
|
1105
1106
|
return intersections
|
|
1106
1107
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import numpy as np
|
|
2
2
|
import bspy.spline
|
|
3
3
|
import bspy.spline_block
|
|
4
|
+
from collections import namedtuple
|
|
4
5
|
|
|
5
6
|
def line_of_curvature(self, uvStart, is_max, tolerance = 1.0e-3):
|
|
6
7
|
if self.nInd != 2: raise ValueError("Surface must have two independent variables")
|
|
@@ -115,6 +116,60 @@ def offset(self, edgeRadius, bitRadius=None, angle=np.pi / 2.2, path=None, subtr
|
|
|
115
116
|
if path is not None and (path.nInd != 1 or path.nDep != 2 or self.nInd != 2):
|
|
116
117
|
raise ValueError("path must be a 2D curve and self must be a 3D surface")
|
|
117
118
|
|
|
119
|
+
# Compute new order, knots, and fillets for offset (ensure order is at least 4).
|
|
120
|
+
Fillet = namedtuple('Fillet', ('adjustment', 'isFillet'))
|
|
121
|
+
newOrder = []
|
|
122
|
+
newKnotList = []
|
|
123
|
+
newUniqueList = []
|
|
124
|
+
filletList = []
|
|
125
|
+
for order, knots in zip(self.order, self.knots):
|
|
126
|
+
min4Order = max(order, 4)
|
|
127
|
+
unique, counts = np.unique(knots, return_counts=True)
|
|
128
|
+
counts += min4Order - order # Ensure order is at least 4
|
|
129
|
+
newOrder.append(min4Order)
|
|
130
|
+
adjustment = 0
|
|
131
|
+
epsilon = np.finfo(unique.dtype).eps
|
|
132
|
+
|
|
133
|
+
# Add first knot.
|
|
134
|
+
newKnots = [unique[0]] * counts[0]
|
|
135
|
+
newUnique = [unique[0]]
|
|
136
|
+
fillets = [Fillet(adjustment, False)]
|
|
137
|
+
|
|
138
|
+
# Add internal knots, checking for C1 discontinuities needing fillets.
|
|
139
|
+
for knot, count in zip(unique[1:-1], counts[1:-1]):
|
|
140
|
+
knot += adjustment
|
|
141
|
+
newKnots += [knot] * count
|
|
142
|
+
newUnique.append(knot)
|
|
143
|
+
# Check for lack of C1 continuity (need for a fillet)
|
|
144
|
+
if count >= min4Order - 1:
|
|
145
|
+
fillets.append(Fillet(adjustment, True))
|
|
146
|
+
# Create parametric space for fillet.
|
|
147
|
+
adjustment += 1
|
|
148
|
+
knot += 1 + epsilon # Add additional adjustment and step slightly past discontinuity
|
|
149
|
+
newKnots += [knot] * (min4Order - 1)
|
|
150
|
+
newUnique.append(knot)
|
|
151
|
+
fillets.append(Fillet(adjustment, False))
|
|
152
|
+
|
|
153
|
+
# Add last knot.
|
|
154
|
+
newKnots += [unique[-1] + adjustment] * counts[-1]
|
|
155
|
+
newUnique.append(unique[-1] + adjustment)
|
|
156
|
+
fillets.append(Fillet(adjustment, False))
|
|
157
|
+
|
|
158
|
+
# Build fillet and knot lists.
|
|
159
|
+
newKnotList.append(np.array(newKnots, knots.dtype))
|
|
160
|
+
newUniqueList.append(np.array(newUnique, knots.dtype))
|
|
161
|
+
filletList.append(fillets)
|
|
162
|
+
|
|
163
|
+
if path is not None:
|
|
164
|
+
min4Order = max(path.order[0], 4)
|
|
165
|
+
newOrder = [min4Order]
|
|
166
|
+
unique, counts = np.unique(path.knots[0], return_counts=True)
|
|
167
|
+
counts += min4Order - path.order[0] # Ensure order is at least 4
|
|
168
|
+
newKnotList = [np.repeat(unique, counts)]
|
|
169
|
+
domain = path.domain()
|
|
170
|
+
else:
|
|
171
|
+
domain = [(unique[0], unique[-1]) for unique in newUniqueList]
|
|
172
|
+
|
|
118
173
|
# Determine geometry of drill bit.
|
|
119
174
|
if subtract:
|
|
120
175
|
edgeRadius *= -1
|
|
@@ -126,79 +181,79 @@ def offset(self, edgeRadius, bitRadius=None, angle=np.pi / 2.2, path=None, subtr
|
|
|
126
181
|
|
|
127
182
|
# Define drill bit function.
|
|
128
183
|
if abs(w) < tolerance and path is None: # Simple offset curve or surface
|
|
129
|
-
def drillBit(
|
|
130
|
-
return
|
|
184
|
+
def drillBit(normal):
|
|
185
|
+
return edgeRadius * normal
|
|
131
186
|
elif self.nDep == 2: # General offset curve
|
|
132
|
-
def drillBit(
|
|
133
|
-
xy = self(u)
|
|
134
|
-
normal = self.normal(u)
|
|
187
|
+
def drillBit(normal):
|
|
135
188
|
upward = np.sign(normal[1])
|
|
136
189
|
if upward * normal[1] <= bottom:
|
|
137
|
-
|
|
138
|
-
xy[1] += edgeRadius * normal[1]
|
|
190
|
+
return np.array((edgeRadius * normal[0] + w * np.sign(normal[0]), edgeRadius * normal[1]))
|
|
139
191
|
else:
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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)
|
|
192
|
+
return np.array((bottomRadius * normal[0], bottomRadius * normal[1] - upward * h))
|
|
193
|
+
elif self.nDep == 3: # General offset surface
|
|
194
|
+
def drillBit(normal):
|
|
147
195
|
upward = np.sign(normal[1])
|
|
148
196
|
if upward * normal[1] <= bottom:
|
|
149
197
|
norm = np.sqrt(normal[0] * normal[0] + normal[2] * normal[2])
|
|
150
|
-
|
|
151
|
-
xyz[1] += edgeRadius * normal[1]
|
|
152
|
-
xyz[2] += edgeRadius * normal[2] + w * normal[2] / norm
|
|
198
|
+
return np.array((edgeRadius * normal[0] + w * normal[0] / norm, edgeRadius * normal[1], edgeRadius * normal[2] + w * normal[2] / norm))
|
|
153
199
|
else:
|
|
154
|
-
|
|
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
|
|
200
|
+
return np.array((bottomRadius * normal[0], bottomRadius * normal[1] - upward * h, bottomRadius * normal[2]))
|
|
176
201
|
else: # Should never get here (exception raised earlier)
|
|
177
202
|
raise ValueError("The offset is only defined for 2D curves and 3D surfaces with well-defined normals.")
|
|
178
203
|
|
|
179
|
-
#
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
204
|
+
# Define function to pass to fit.
|
|
205
|
+
def fitFunction(uv):
|
|
206
|
+
if path is not None:
|
|
207
|
+
uv = path(uv)
|
|
208
|
+
|
|
209
|
+
# Compute adjusted spline uv values, accounting for fillets.
|
|
210
|
+
hasFillet = False
|
|
211
|
+
adjustedUV = uv.copy()
|
|
212
|
+
for (i, u), unique, fillets in zip(enumerate(uv), newUniqueList, filletList):
|
|
213
|
+
ix = np.searchsorted(unique, u, 'right') - 1
|
|
214
|
+
fillet = fillets[ix]
|
|
215
|
+
if fillet.isFillet:
|
|
216
|
+
hasFillet = True
|
|
217
|
+
adjustedUV[i] = unique[ix] - fillet.adjustment
|
|
218
|
+
else:
|
|
219
|
+
adjustedUV[i] -= fillet.adjustment
|
|
220
|
+
|
|
221
|
+
# If we have fillets, compute the normal from their normal fan.
|
|
222
|
+
if hasFillet:
|
|
223
|
+
normal = np.zeros(self.nDep, self.coefs.dtype)
|
|
224
|
+
nudged = adjustedUV.copy()
|
|
225
|
+
for (i, u), unique, fillets in zip(enumerate(uv), newUniqueList, filletList):
|
|
226
|
+
ix = np.searchsorted(unique, u, 'right') - 1
|
|
227
|
+
fillet = fillets[ix]
|
|
228
|
+
if fillet.isFillet:
|
|
229
|
+
epsilon = np.finfo(unique.dtype).eps
|
|
230
|
+
alpha = u - unique[ix]
|
|
231
|
+
np.copyto(nudged, adjustedUV)
|
|
232
|
+
nudged[i] -= epsilon
|
|
233
|
+
normal += (1 - alpha) * self.normal(nudged)
|
|
234
|
+
nudged[i] += 2 * epsilon
|
|
235
|
+
normal += alpha * self.normal(nudged)
|
|
236
|
+
normal = normal / np.linalg.norm(normal)
|
|
237
|
+
else:
|
|
238
|
+
normal = self.normal(adjustedUV)
|
|
239
|
+
|
|
240
|
+
# Return the offset based on the normal.
|
|
241
|
+
return self(adjustedUV) + drillBit(normal)
|
|
188
242
|
|
|
189
243
|
# Fit new spline to offset by drill bit.
|
|
190
|
-
offset = bspy.spline.Spline.fit(
|
|
244
|
+
offset = bspy.spline.Spline.fit(domain, fitFunction, newOrder, newKnotList, tolerance)
|
|
191
245
|
|
|
192
246
|
# Remove cusps as required (only applies to offset curves).
|
|
193
|
-
if removeCusps and self.nInd == 1:
|
|
247
|
+
if removeCusps and (self.nInd == 1 or path is not None):
|
|
194
248
|
# Find the cusps by checking for tangent direction reversal between the spline and offset.
|
|
195
249
|
cusps = []
|
|
196
250
|
previousKnot = None
|
|
197
251
|
start = None
|
|
198
252
|
for knot in np.unique(offset.knots[0][offset.order[0]:offset.nCoef[0]]):
|
|
199
|
-
tangent = self.derivative((1,), knot)
|
|
200
253
|
if path is not None:
|
|
201
|
-
tangent =
|
|
254
|
+
tangent = self.jacobian(path(knot)) @ path.derivative((1,), knot)
|
|
255
|
+
else:
|
|
256
|
+
tangent = self.derivative((1,), knot)
|
|
202
257
|
flipped = np.dot(tangent, offset.derivative((1,), knot)) < 0
|
|
203
258
|
if flipped and start is None:
|
|
204
259
|
start = knot
|
|
@@ -219,7 +274,7 @@ def offset(self, edgeRadius, bitRadius=None, angle=np.pi / 2.2, path=None, subtr
|
|
|
219
274
|
# This is necessary to find the intersection point (2 equations, 2 unknowns).
|
|
220
275
|
tangent = offset.derivative((1,), cusp[0])
|
|
221
276
|
projection = np.concatenate((tangent / np.linalg.norm(tangent),
|
|
222
|
-
|
|
277
|
+
self.normal(path(cusp[0])))).reshape((2,3))
|
|
223
278
|
before = before.transform(projection)
|
|
224
279
|
after = after.transform(projection)
|
|
225
280
|
block = bspy.spline_block.SplineBlock([[before, after]])
|
|
@@ -185,6 +185,25 @@ class Spline(Manifold):
|
|
|
185
185
|
indMap = [(mapping, mapping) if np.isscalar(mapping) else mapping for mapping in indMap]
|
|
186
186
|
return bspy._spline_operations.add(self, other, indMap)
|
|
187
187
|
|
|
188
|
+
def arc_length_map(self, tolerance = 1.0e-6):
|
|
189
|
+
"""
|
|
190
|
+
Determine a mapping s -> u such that any curve parametrized arbitrarily can be composed with the
|
|
191
|
+
computed mapping to get the curve parametrized by arc length. Specifically, given a curve x with
|
|
192
|
+
points on the curve given by x(u), determine a mapping u such that the composite curve x o u is
|
|
193
|
+
parametrized by arc length, i.e. x can be thought of as x(u(s)) for an arc length parameter s.
|
|
194
|
+
|
|
195
|
+
Parameters
|
|
196
|
+
----------
|
|
197
|
+
tolerance : the accuracy to which the arc length map should be computed
|
|
198
|
+
|
|
199
|
+
Returns
|
|
200
|
+
-------
|
|
201
|
+
spline : Spline
|
|
202
|
+
The spline mapping from R1 -> R1 which approximates the arc length map to within the specified
|
|
203
|
+
tolerance.
|
|
204
|
+
"""
|
|
205
|
+
return bspy._spline_domain.arc_length_map(self, tolerance)
|
|
206
|
+
|
|
188
207
|
@staticmethod
|
|
189
208
|
def bspline_values(knot, knots, splineOrder, u, derivativeOrder = 0, taylorCoefs = False):
|
|
190
209
|
"""
|
|
@@ -547,13 +566,13 @@ class Spline(Manifold):
|
|
|
547
566
|
|
|
548
567
|
def contract(self, uvw):
|
|
549
568
|
"""
|
|
550
|
-
Contract a spline by assigning a fixed value to one or more of its independent variables.
|
|
569
|
+
Contract a spline, reducing its number of independent variables, by assigning a fixed value to one or more of its independent variables.
|
|
551
570
|
|
|
552
571
|
Parameters
|
|
553
572
|
----------
|
|
554
573
|
uvw : `iterable`
|
|
555
574
|
An iterable of length `nInd` that specifies the values of each independent variable to contract.
|
|
556
|
-
A value of `None` for an independent variable
|
|
575
|
+
A value of `None` for an independent variable retains that independent variable in the contacted spline.
|
|
557
576
|
|
|
558
577
|
Returns
|
|
559
578
|
-------
|
{bspy-4.4 → bspy-4.4.1}/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|