bspy 4.2__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/__init__.py +1 -1
- bspy/_spline_domain.py +74 -92
- bspy/_spline_evaluation.py +3 -3
- bspy/_spline_fitting.py +33 -12
- bspy/_spline_intersection.py +294 -279
- bspy/_spline_milling.py +233 -0
- bspy/_spline_operations.py +98 -81
- bspy/hyperplane.py +7 -3
- bspy/manifold.py +8 -3
- bspy/solid.py +8 -4
- bspy/spline.py +124 -21
- bspy/splineOpenGLFrame.py +346 -303
- bspy/spline_block.py +155 -38
- bspy/viewer.py +20 -11
- {bspy-4.2.dist-info → bspy-4.4.dist-info}/METADATA +14 -11
- bspy-4.4.dist-info/RECORD +19 -0
- {bspy-4.2.dist-info → bspy-4.4.dist-info}/WHEEL +1 -1
- bspy-4.2.dist-info/RECORD +0 -18
- {bspy-4.2.dist-info → bspy-4.4.dist-info/licenses}/LICENSE +0 -0
- {bspy-4.2.dist-info → bspy-4.4.dist-info}/top_level.txt +0 -0
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
|
@@ -13,47 +13,55 @@ def _shiftPolynomial(polynomial, delta):
|
|
|
13
13
|
|
|
14
14
|
def add(self, other, indMap = None):
|
|
15
15
|
if not(self.nDep == other.nDep): raise ValueError("self and other must have same nDep")
|
|
16
|
-
selfMapped =
|
|
17
|
-
otherMapped = []
|
|
16
|
+
selfMapped = set()
|
|
18
17
|
otherToSelf = {}
|
|
19
18
|
if indMap is not None:
|
|
20
19
|
(self, other) = bspy.Spline.common_basis((self, other), indMap)
|
|
21
20
|
for map in indMap:
|
|
22
|
-
selfMapped.
|
|
23
|
-
otherMapped.append(map[1])
|
|
21
|
+
selfMapped.add(map[0])
|
|
24
22
|
otherToSelf[map[1]] = map[0]
|
|
25
23
|
|
|
26
24
|
# Construct new spline parameters.
|
|
27
|
-
# We index backwards because we're adding transposed coefficients (see below).
|
|
28
25
|
nInd = self.nInd
|
|
29
26
|
order = [*self.order]
|
|
30
27
|
nCoef = [*self.nCoef]
|
|
31
28
|
knots = list(self.knots)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if i not in otherMapped:
|
|
38
|
-
order.append(other.order[other.nInd - 1 - i])
|
|
39
|
-
nCoef.append(other.nCoef[other.nInd - 1 - i])
|
|
40
|
-
knots.append(other.knots[other.nInd - 1 - i])
|
|
41
|
-
permutation.append(self.nInd + i + 1) # Add 1 to account for dependent variables.
|
|
29
|
+
for i in range(other.nInd):
|
|
30
|
+
if i not in otherToSelf:
|
|
31
|
+
order.append(other.order[i])
|
|
32
|
+
nCoef.append(other.nCoef[i])
|
|
33
|
+
knots.append(other.knots[i])
|
|
42
34
|
nInd += 1
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
permutation.append(0) # Account for dependent variables.
|
|
46
|
-
permutation = np.array(permutation)
|
|
35
|
+
|
|
36
|
+
# Build coefs array.
|
|
47
37
|
coefs = np.zeros((self.nDep, *nCoef), self.coefs.dtype)
|
|
48
38
|
|
|
49
|
-
#
|
|
50
|
-
# First, add in self.coefs.
|
|
39
|
+
# Add in self.coefs (you need to transpose coefs for the addition to work properly).
|
|
51
40
|
coefs = coefs.T
|
|
52
41
|
coefs += self.coefs.T
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
#
|
|
42
|
+
coefs = coefs.T
|
|
43
|
+
|
|
44
|
+
# Construct permutation of coefs to transpose coefs to match other.coefs.
|
|
45
|
+
otherUnmappedCount = 0
|
|
46
|
+
permutation = [0] # Account for dependent variables
|
|
47
|
+
for i in range(other.nInd):
|
|
48
|
+
if i in otherToSelf:
|
|
49
|
+
permutation.append(otherToSelf[i] + 1) # Add 1 to account for dependent variables.
|
|
50
|
+
else:
|
|
51
|
+
permutation.append(self.nInd + otherUnmappedCount + 1) # Add 1 to account for dependent variables.
|
|
52
|
+
otherUnmappedCount += 1
|
|
53
|
+
for i in range(self.nInd):
|
|
54
|
+
if i not in selfMapped:
|
|
55
|
+
permutation.append(i + 1) # Add 1 to account for dependent variables.
|
|
56
|
+
|
|
57
|
+
# Permute coefs to match other.coefs
|
|
58
|
+
coefs = coefs.transpose(permutation)
|
|
59
|
+
|
|
60
|
+
# Add in other.coefs (you need to transpose coefs for the addition to work properly).
|
|
61
|
+
coefs = coefs.T
|
|
56
62
|
coefs += other.coefs.T
|
|
63
|
+
coefs = coefs.T
|
|
64
|
+
|
|
57
65
|
# Reverse the permutation.
|
|
58
66
|
coefs = coefs.transpose(np.argsort(permutation))
|
|
59
67
|
|
|
@@ -63,16 +71,20 @@ def confine(self, range_bounds):
|
|
|
63
71
|
if self.nInd != 1: raise ValueError("Confine only works on curves (nInd == 1)")
|
|
64
72
|
if len(range_bounds) != self.nDep: raise ValueError("len(range_bounds) must equal nDep")
|
|
65
73
|
spline = self.clamp((0,), (0,))
|
|
74
|
+
if spline is self:
|
|
75
|
+
spline = self.copy()
|
|
66
76
|
order = spline.order[0]
|
|
67
77
|
degree = order - 1
|
|
68
78
|
domain = spline.domain()
|
|
79
|
+
dtype = spline.knots[0].dtype
|
|
69
80
|
unique, counts = np.unique(spline.knots[0], return_counts=True)
|
|
70
81
|
machineEpsilon = np.finfo(self.coefs.dtype).eps
|
|
71
82
|
epsilon = np.sqrt(machineEpsilon)
|
|
72
83
|
intersections = [] # List of tuples (u, boundaryPoint, headingOutside)
|
|
73
84
|
|
|
74
85
|
def addIntersection(u, headedOutside = False):
|
|
75
|
-
|
|
86
|
+
u = dtype.type(u) # Cast to spline domain type
|
|
87
|
+
boundaryPoint = spline(u)
|
|
76
88
|
for i in range(spline.nDep):
|
|
77
89
|
if boundaryPoint[i] < range_bounds[i][0]:
|
|
78
90
|
headedOutside = True if boundaryPoint[i] < range_bounds[i][0] - epsilon else headedOutside
|
|
@@ -80,18 +92,18 @@ def confine(self, range_bounds):
|
|
|
80
92
|
if boundaryPoint[i] > range_bounds[i][1]:
|
|
81
93
|
headedOutside = True if boundaryPoint[i] > range_bounds[i][1] + epsilon else headedOutside
|
|
82
94
|
boundaryPoint[i] = range_bounds[i][1]
|
|
83
|
-
intersections.append(
|
|
95
|
+
intersections.append([u, boundaryPoint, headedOutside])
|
|
84
96
|
|
|
85
97
|
def intersectBoundary(i, j):
|
|
86
98
|
zeros = type(spline)(1, 1, spline.order, spline.nCoef, spline.knots, (spline.coefs[i] - range_bounds[i][j],)).zeros()
|
|
87
99
|
for zero in zeros:
|
|
88
100
|
if isinstance(zero, tuple):
|
|
89
|
-
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
|
|
90
102
|
addIntersection(zero[0], headedOutside)
|
|
91
|
-
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
|
|
92
104
|
addIntersection(zero[1], headedOutside)
|
|
93
105
|
else:
|
|
94
|
-
headedOutside = (-1 if j == 0 else 1) * spline.derivative((1,),
|
|
106
|
+
headedOutside = (-1 if j == 0 else 1) * spline.derivative((1,), zero)[i] > 0
|
|
95
107
|
addIntersection(zero, headedOutside)
|
|
96
108
|
|
|
97
109
|
addIntersection(domain[0][0]) # Confine starting point
|
|
@@ -104,21 +116,22 @@ def confine(self, range_bounds):
|
|
|
104
116
|
# Put the intersection points in order.
|
|
105
117
|
intersections.sort(key=lambda intersection: intersection[0])
|
|
106
118
|
|
|
107
|
-
#
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
# Insert order-1 knots at each intersection point.
|
|
114
|
-
for (knot, boundaryPoint, headedOutside) in intersections:
|
|
115
|
-
ix = np.searchsorted(unique, knot)
|
|
116
|
-
if unique[ix] == knot:
|
|
117
|
-
count = (order - 1) - counts[ix]
|
|
118
|
-
if count > 0:
|
|
119
|
-
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
|
|
120
125
|
else:
|
|
121
|
-
|
|
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
|
|
122
135
|
|
|
123
136
|
# Go through the boundary points, assigning boundary coefficients, interpolating between boundary points,
|
|
124
137
|
# and removing knots and coefficients where the curve stalls.
|
|
@@ -131,29 +144,37 @@ def confine(self, range_bounds):
|
|
|
131
144
|
knotAdjustment = 0.0
|
|
132
145
|
for knot, boundaryPoint, headedOutside in intersections[1:]:
|
|
133
146
|
knot += knotAdjustment
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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]
|
|
157
178
|
|
|
158
179
|
spline.nCoef = (nCoef,)
|
|
159
180
|
spline.knots = (knots,)
|
|
@@ -643,21 +664,20 @@ def normal_spline(self, indices=None):
|
|
|
643
664
|
knots = None
|
|
644
665
|
counts = None
|
|
645
666
|
maxOrder = 0
|
|
646
|
-
|
|
647
|
-
endInd = 0
|
|
667
|
+
maxMap = []
|
|
648
668
|
# First, collect the order, knots, and number of relevant columns for this independent variable.
|
|
649
669
|
for row in self.block:
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
ind = nInd - rowInd
|
|
670
|
+
for map, spline in row:
|
|
671
|
+
if nInd in map:
|
|
672
|
+
ind = map.index(nInd)
|
|
654
673
|
order = spline.order[ind]
|
|
655
674
|
k, c = np.unique(spline.knots[ind][order-1:spline.nCoef[ind]+1], return_counts=True)
|
|
656
675
|
if knots:
|
|
657
676
|
if maxOrder < order:
|
|
658
677
|
counts += order - maxOrder
|
|
659
678
|
maxOrder = order
|
|
660
|
-
|
|
679
|
+
if len(maxMap) < len(map):
|
|
680
|
+
maxMap = map
|
|
661
681
|
for knot, count in zip(k[1:-1], c[1:-1]):
|
|
662
682
|
ix = np.searchsorted(knots, knot)
|
|
663
683
|
if knots[ix] == knot:
|
|
@@ -669,30 +689,27 @@ def normal_spline(self, indices=None):
|
|
|
669
689
|
knots = k
|
|
670
690
|
counts = c
|
|
671
691
|
maxOrder = order
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
692
|
+
maxMap = map
|
|
693
|
+
|
|
675
694
|
break
|
|
676
695
|
|
|
677
|
-
rowInd += spline.nInd
|
|
678
|
-
|
|
679
696
|
# Next, calculate the order of the normal for this independent variable.
|
|
680
697
|
# Note that the total order will be one less than usual, because one of
|
|
681
698
|
# the tangents is the derivative with respect to that independent variable.
|
|
682
699
|
if self.nInd < self.nDep:
|
|
683
700
|
# If this normal involves all tangents, simply add the degree of each,
|
|
684
701
|
# so long as that tangent contains the independent variable.
|
|
685
|
-
order = (maxOrder - 1) * (
|
|
702
|
+
order = (maxOrder - 1) * len(maxMap)
|
|
686
703
|
else:
|
|
687
704
|
# If this normal doesn't involve all tangents, find the max order of
|
|
688
705
|
# each returned combination (as defined by the indices).
|
|
689
706
|
order = 0
|
|
690
|
-
for index in
|
|
707
|
+
for index in maxMap if indices is None else indices:
|
|
691
708
|
# The order will be one larger if this independent variable's tangent is excluded by the index.
|
|
692
709
|
ord = 0 if index != nInd else 1
|
|
693
710
|
# Add the degree of each tangent, so long as that tangent contains the
|
|
694
711
|
# independent variable and is not excluded by the index.
|
|
695
|
-
for ind in
|
|
712
|
+
for ind in maxMap:
|
|
696
713
|
ord += maxOrder - 1 if index != ind else 0
|
|
697
714
|
order = max(order, ord)
|
|
698
715
|
|
bspy/hyperplane.py
CHANGED
|
@@ -18,6 +18,9 @@ class Hyperplane(Manifold):
|
|
|
18
18
|
|
|
19
19
|
tangentSpace : array-like
|
|
20
20
|
A array of tangents that are linearly independent and orthogonal to the normal.
|
|
21
|
+
|
|
22
|
+
metadata : `dict`, optional
|
|
23
|
+
A dictionary of ancillary data to store with the hyperplane. Default is {}.
|
|
21
24
|
|
|
22
25
|
Notes
|
|
23
26
|
-----
|
|
@@ -28,11 +31,12 @@ class Hyperplane(Manifold):
|
|
|
28
31
|
maxAlignment = 0.9999 # 1 - 1/10^4
|
|
29
32
|
""" If the absolute value of the dot product of two unit normals is greater than maxAlignment, the manifolds are parallel."""
|
|
30
33
|
|
|
31
|
-
def __init__(self, normal, point, tangentSpace):
|
|
34
|
+
def __init__(self, normal, point, tangentSpace, metadata = {}):
|
|
32
35
|
self._normal = np.atleast_1d(normal)
|
|
33
36
|
self._point = np.atleast_1d(point)
|
|
34
37
|
self._tangentSpace = np.atleast_1d(tangentSpace)
|
|
35
38
|
if not np.allclose(self._tangentSpace.T @ self._normal, 0.0): raise ValueError("normal must be orthogonal to tangent space")
|
|
39
|
+
self.metadata = dict(metadata)
|
|
36
40
|
|
|
37
41
|
def __repr__(self):
|
|
38
42
|
return "Hyperplane({0}, {1}, {2})".format(self._normal, self._point, self._tangentSpace)
|
|
@@ -207,7 +211,7 @@ class Hyperplane(Manifold):
|
|
|
207
211
|
--------
|
|
208
212
|
`to_dict` : Return a `dict` with `Hyperplane` data.
|
|
209
213
|
"""
|
|
210
|
-
return Hyperplane(dictionary["normal"], dictionary["point"], dictionary["tangentSpace"])
|
|
214
|
+
return Hyperplane(dictionary["normal"], dictionary["point"], dictionary["tangentSpace"], dictionary.get("metadata", {}))
|
|
211
215
|
|
|
212
216
|
def full_domain(self):
|
|
213
217
|
"""
|
|
@@ -455,7 +459,7 @@ class Hyperplane(Manifold):
|
|
|
455
459
|
--------
|
|
456
460
|
`from_dict` : Create a `Hyperplane` from a data in a `dict`.
|
|
457
461
|
"""
|
|
458
|
-
return {"type" : "Hyperplane", "normal" : self._normal, "point" : self._point, "tangentSpace" : self._tangentSpace}
|
|
462
|
+
return {"type" : "Hyperplane", "normal" : self._normal, "point" : self._point, "tangentSpace" : self._tangentSpace, "metadata" : self.metadata}
|
|
459
463
|
|
|
460
464
|
def transform(self, matrix, matrixInverseTranspose = None):
|
|
461
465
|
"""
|
bspy/manifold.py
CHANGED
|
@@ -5,6 +5,11 @@ class Manifold:
|
|
|
5
5
|
"""
|
|
6
6
|
A manifold is an abstract base class for differentiable functions with
|
|
7
7
|
normals and tangent spaces whose range is one dimension higher than their domain.
|
|
8
|
+
|
|
9
|
+
Parameters
|
|
10
|
+
----------
|
|
11
|
+
metadata : `dict`, optional
|
|
12
|
+
A dictionary of ancillary data to store with the manifold. Default is {}.
|
|
8
13
|
"""
|
|
9
14
|
|
|
10
15
|
minSeparation = 0.0001
|
|
@@ -17,8 +22,8 @@ class Manifold:
|
|
|
17
22
|
factory = {}
|
|
18
23
|
"""Factory dictionary for creating manifolds."""
|
|
19
24
|
|
|
20
|
-
def __init__(self):
|
|
21
|
-
|
|
25
|
+
def __init__(self, metadata = {}):
|
|
26
|
+
self.metadata = dict(metadata)
|
|
22
27
|
|
|
23
28
|
def cached_intersect(self, other, cache = None):
|
|
24
29
|
"""
|
|
@@ -322,7 +327,7 @@ class Manifold:
|
|
|
322
327
|
--------
|
|
323
328
|
`from_dict` : Create a `Manifold` from a data in a `dict`.
|
|
324
329
|
"""
|
|
325
|
-
return
|
|
330
|
+
return {"metadata" : self.metadata}
|
|
326
331
|
|
|
327
332
|
def transform(self, matrix, matrixInverseTranspose = None):
|
|
328
333
|
"""
|
bspy/solid.py
CHANGED
|
@@ -59,6 +59,9 @@ class Solid:
|
|
|
59
59
|
|
|
60
60
|
containsInfinity : `bool`
|
|
61
61
|
Indicates whether or not the solid contains infinity.
|
|
62
|
+
|
|
63
|
+
metadata : `dict`, optional
|
|
64
|
+
A dictionary of ancillary data to store with the solid. Default is {}.
|
|
62
65
|
|
|
63
66
|
See also
|
|
64
67
|
--------
|
|
@@ -70,10 +73,11 @@ class Solid:
|
|
|
70
73
|
|
|
71
74
|
Solids can be of zero dimension, typically acting as the domain of boundary endpoints. Zero-dimension solids have no boundaries, they only contain infinity or not.
|
|
72
75
|
"""
|
|
73
|
-
def __init__(self, dimension, containsInfinity):
|
|
76
|
+
def __init__(self, dimension, containsInfinity, metadata = {}):
|
|
74
77
|
assert dimension >= 0
|
|
75
78
|
self.dimension = dimension
|
|
76
79
|
self.containsInfinity = containsInfinity
|
|
80
|
+
self.metadata = dict(metadata)
|
|
77
81
|
self.boundaries = []
|
|
78
82
|
self.bounds = None
|
|
79
83
|
|
|
@@ -356,7 +360,7 @@ class Solid:
|
|
|
356
360
|
`save` : Save a solids and/or manifolds in json format to the specified filename (full path).
|
|
357
361
|
"""
|
|
358
362
|
def from_dict(dictionary):
|
|
359
|
-
solid = Solid(dictionary["dimension"], dictionary["containsInfinity"])
|
|
363
|
+
solid = Solid(dictionary["dimension"], dictionary["containsInfinity"], dictionary.get("metadata", {}))
|
|
360
364
|
for boundary in dictionary["boundaries"]:
|
|
361
365
|
manifold = boundary["manifold"]
|
|
362
366
|
solid.add_boundary(Boundary(Manifold.factory[manifold.get("type", "Spline")].from_dict(manifold), from_dict(boundary["domain"])))
|
|
@@ -428,7 +432,7 @@ class Solid:
|
|
|
428
432
|
if isinstance(obj, Boundary):
|
|
429
433
|
return {"type" : "Boundary", "manifold" : obj.manifold, "domain" : obj.domain}
|
|
430
434
|
if isinstance(obj, Solid):
|
|
431
|
-
return {"type" : "Solid", "dimension" : obj.dimension, "containsInfinity" : obj.containsInfinity, "boundaries" : obj.boundaries}
|
|
435
|
+
return {"type" : "Solid", "dimension" : obj.dimension, "containsInfinity" : obj.containsInfinity, "boundaries" : obj.boundaries, "metadata" : obj.metadata}
|
|
432
436
|
return super().default(obj)
|
|
433
437
|
|
|
434
438
|
with open(fileName, 'w', encoding='utf-8') as file:
|
|
@@ -749,7 +753,7 @@ class Solid:
|
|
|
749
753
|
|
|
750
754
|
# Calculate Integral(f) * first cofactor. Note that quad returns a tuple: (integral, error bound).
|
|
751
755
|
returnValue = 0.0
|
|
752
|
-
firstCofactor = boundary.manifold.normal(evalPoint, False, (0,))
|
|
756
|
+
firstCofactor = boundary.manifold.normal(evalPoint, False, (0,))[0]
|
|
753
757
|
if abs(x0 - point[0]) > epsabs and abs(firstCofactor) > epsabs:
|
|
754
758
|
returnValue = integrate.quad(fHat, x0, point[0], epsabs=epsabs, epsrel=epsrel, *quadArgs)[0] * firstCofactor
|
|
755
759
|
return returnValue
|