bspy 4.0__py3-none-any.whl → 4.2__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 +16 -3
- bspy/_spline_domain.py +101 -9
- bspy/_spline_evaluation.py +82 -17
- bspy/_spline_fitting.py +244 -114
- bspy/_spline_intersection.py +467 -156
- bspy/_spline_operations.py +70 -49
- bspy/hyperplane.py +540 -0
- bspy/manifold.py +334 -31
- bspy/solid.py +842 -0
- bspy/spline.py +350 -71
- bspy/splineOpenGLFrame.py +262 -14
- bspy/spline_block.py +343 -0
- bspy/viewer.py +134 -90
- {bspy-4.0.dist-info → bspy-4.2.dist-info}/METADATA +17 -6
- bspy-4.2.dist-info/RECORD +18 -0
- {bspy-4.0.dist-info → bspy-4.2.dist-info}/WHEEL +1 -1
- bspy-4.0.dist-info/RECORD +0 -15
- {bspy-4.0.dist-info → bspy-4.2.dist-info}/LICENSE +0 -0
- {bspy-4.0.dist-info → bspy-4.2.dist-info}/top_level.txt +0 -0
bspy/_spline_intersection.py
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import math
|
|
3
3
|
import numpy as np
|
|
4
|
+
from bspy.manifold import Manifold
|
|
5
|
+
from bspy.hyperplane import Hyperplane
|
|
4
6
|
import bspy.spline
|
|
5
|
-
|
|
7
|
+
import bspy.spline_block
|
|
8
|
+
from bspy.solid import Solid, Boundary
|
|
6
9
|
from collections import namedtuple
|
|
7
10
|
from multiprocessing import Pool
|
|
8
11
|
|
|
@@ -73,14 +76,22 @@ def zeros_using_interval_newton(self):
|
|
|
73
76
|
if derivativeBounds[0] * derivativeBounds[1] >= 0.0: # Refine interval
|
|
74
77
|
projectedLeftStep = max(0.0, adjustedLeftStep)
|
|
75
78
|
projectedRightStep = min(1.0, adjustedRightStep)
|
|
79
|
+
provisionalZero = [0.5 * (projectedLeftStep + projectedRightStep)]
|
|
76
80
|
if projectedLeftStep <= projectedRightStep:
|
|
77
81
|
if projectedRightStep - projectedLeftStep <= epsilon:
|
|
78
|
-
myZeros =
|
|
82
|
+
myZeros = provisionalZero
|
|
79
83
|
else:
|
|
80
84
|
trimmedSpline = mySpline.trim(((projectedLeftStep, projectedRightStep),))
|
|
81
85
|
myZeros = refine(trimmedSpline, intervalSize, functionMax)
|
|
86
|
+
if len(myZeros) == 0 and mySpline.order[0] == mySpline.nCoef[0] and \
|
|
87
|
+
mySpline.coefs[0][0] * mySpline.coefs[0][-1] < 0.0:
|
|
88
|
+
myZeros = provisionalZero
|
|
82
89
|
else:
|
|
83
|
-
|
|
90
|
+
if mySpline.order[0] == mySpline.nCoef[0] and \
|
|
91
|
+
mySpline.coefs[0][0] * mySpline.coefs[0][-1] < 0.0:
|
|
92
|
+
myZeros = provisionalZero
|
|
93
|
+
else:
|
|
94
|
+
return []
|
|
84
95
|
else: # . . . or split as needed
|
|
85
96
|
myZeros = []
|
|
86
97
|
if adjustedLeftStep > 0.0:
|
|
@@ -115,18 +126,19 @@ def zeros_using_interval_newton(self):
|
|
|
115
126
|
return mySolution
|
|
116
127
|
return refine(spline, 1.0, 1.0)
|
|
117
128
|
|
|
118
|
-
def _convex_hull_2D(xData, yData, yBounds, epsilon = 1.0e-8):
|
|
129
|
+
def _convex_hull_2D(xData, yData, yBounds, yOtherBounds, epsilon = 1.0e-8):
|
|
119
130
|
# Allow xData to be repeated for longer yData, but only if yData is a multiple.
|
|
120
131
|
if not(yData.shape[0] % xData.shape[0] == 0): raise ValueError("Size of xData does not divide evenly in size of yData")
|
|
121
132
|
|
|
122
133
|
# Assign (x0, y0) to the lowest point.
|
|
123
134
|
yMinIndex = np.argmin(yData)
|
|
124
135
|
x0 = xData[yMinIndex % xData.shape[0]]
|
|
125
|
-
y0 = yData[yMinIndex]
|
|
136
|
+
y0 = yOtherBounds[0] + yData[yMinIndex]
|
|
126
137
|
|
|
127
138
|
# Calculate y adjustment as needed for values close to zero
|
|
128
139
|
yAdjustment = -yBounds[0] if yBounds[0] > 0.0 else -yBounds[1] if yBounds[1] < 0.0 else 0.0
|
|
129
140
|
y0 += yAdjustment
|
|
141
|
+
additionalPoint = yOtherBounds[1] > yOtherBounds[0] + epsilon
|
|
130
142
|
|
|
131
143
|
# Sort points by angle around p0.
|
|
132
144
|
sortedPoints = []
|
|
@@ -137,7 +149,9 @@ def _convex_hull_2D(xData, yData, yBounds, epsilon = 1.0e-8):
|
|
|
137
149
|
if x is None:
|
|
138
150
|
xIter = iter(xData)
|
|
139
151
|
x = next(xIter)
|
|
140
|
-
sortedPoints.append((math.atan2(y - y0, x - x0), x, y))
|
|
152
|
+
sortedPoints.append((math.atan2(yOtherBounds[0] + y - y0, x - x0), x, yOtherBounds[0] + y))
|
|
153
|
+
if additionalPoint:
|
|
154
|
+
sortedPoints.append((math.atan2(yOtherBounds[1] + y - y0, x - x0), x, yOtherBounds[1] + y))
|
|
141
155
|
sortedPoints.sort()
|
|
142
156
|
|
|
143
157
|
# Trim away points with the same angle (keep furthest point from p0), removing the angle from the list.
|
|
@@ -193,74 +207,113 @@ def _intersect_convex_hull_with_x_interval(hullPoints, epsilon, xInterval):
|
|
|
193
207
|
else:
|
|
194
208
|
return (min(max(xMin, xInterval[0]), xInterval[1]), max(min(xMax, xInterval[1]), xInterval[0]))
|
|
195
209
|
|
|
196
|
-
Interval = namedtuple('Interval', ('
|
|
210
|
+
Interval = namedtuple('Interval', ('block', 'unknowns', 'scale', 'bounds', 'slope', 'intercept', 'epsilon', 'atMachineEpsilon'))
|
|
211
|
+
|
|
212
|
+
def _create_interval(domain, block, unknowns, scale, slope, intercept, epsilon):
|
|
213
|
+
nDep = 0
|
|
214
|
+
bounds = np.zeros((len(scale), 2), scale.dtype)
|
|
215
|
+
newScale = np.empty_like(scale)
|
|
216
|
+
newBlock = []
|
|
217
|
+
for row in block:
|
|
218
|
+
newRow = []
|
|
219
|
+
nInd = 0
|
|
220
|
+
keepDep = []
|
|
221
|
+
# Trim and reparametrize splines, and sum bounds.
|
|
222
|
+
for spline in row:
|
|
223
|
+
spline = spline.trim(domain[nInd:nInd + spline.nInd]).reparametrize(((0.0, 1.0),) * spline.nInd)
|
|
224
|
+
bounds[nDep:nDep + spline.nDep] += spline.range_bounds()
|
|
225
|
+
nInd += spline.nInd
|
|
226
|
+
newRow.append(spline)
|
|
227
|
+
|
|
228
|
+
# Check row bounds for potential roots.
|
|
229
|
+
for dep in range(spline.nDep):
|
|
230
|
+
coefsMin = bounds[nDep, 0] * scale[nDep]
|
|
231
|
+
coefsMax = bounds[nDep, 1] * scale[nDep]
|
|
232
|
+
if coefsMax < -epsilon or coefsMin > epsilon:
|
|
233
|
+
# No roots in this interval.
|
|
234
|
+
return None
|
|
235
|
+
if coefsMin < -epsilon or coefsMax > epsilon:
|
|
236
|
+
# Dependent variable not near zero for entire interval.
|
|
237
|
+
keepDep.append(dep)
|
|
238
|
+
newScale[nDep] = max(-coefsMin, coefsMax)
|
|
239
|
+
# Rescale spline coefficients to max 1.0.
|
|
240
|
+
rescale = 1.0 / max(-bounds[nDep, 0], bounds[nDep, 1])
|
|
241
|
+
for spline in newRow:
|
|
242
|
+
spline.coefs[dep] *= rescale
|
|
243
|
+
bounds[nDep] *= rescale
|
|
244
|
+
nDep += 1
|
|
245
|
+
else:
|
|
246
|
+
# Dependent variable near zero for entire interval.
|
|
247
|
+
bounds = np.delete(bounds, nDep, 0)
|
|
248
|
+
scale = np.delete(scale, nDep, 0)
|
|
249
|
+
|
|
250
|
+
if keepDep:
|
|
251
|
+
# Remove dependent variables that are zero over the domain
|
|
252
|
+
for spline in newRow:
|
|
253
|
+
spline.nDep = len(keepDep)
|
|
254
|
+
spline.coefs = spline.coefs[keepDep]
|
|
255
|
+
|
|
256
|
+
newBlock.append(newRow)
|
|
257
|
+
|
|
258
|
+
return Interval(newBlock, unknowns, newScale[:nDep], bounds, slope, intercept, epsilon, np.dot(slope, slope) < np.finfo(slope.dtype).eps)
|
|
197
259
|
|
|
198
260
|
# We use multiprocessing.Pool to call this function in parallel, so it cannot be nested and must take a single argument.
|
|
199
261
|
def _refine_projected_polyhedron(interval):
|
|
200
262
|
Crit = 0.85 # Required percentage decrease in domain per iteration.
|
|
201
263
|
epsilon = interval.epsilon
|
|
202
|
-
evaluationEpsilon = np.sqrt(epsilon)
|
|
203
|
-
machineEpsilon = np.finfo(interval.spline.coefs.dtype).eps
|
|
204
264
|
roots = []
|
|
205
265
|
intervals = []
|
|
206
|
-
|
|
207
|
-
# Remove dependent variables that are near zero and compute newScale.
|
|
208
|
-
spline = interval.spline.copy()
|
|
209
|
-
bounds = spline.range_bounds()
|
|
210
|
-
keepDep = []
|
|
211
|
-
for nDep, (coefsMin, coefsMax) in enumerate(bounds * interval.scale):
|
|
212
|
-
if coefsMax < -epsilon or coefsMin > epsilon:
|
|
213
|
-
# No roots in this interval.
|
|
214
|
-
return roots, intervals
|
|
215
|
-
if coefsMin < -epsilon or coefsMax > epsilon:
|
|
216
|
-
# Dependent variable not near zero for entire interval.
|
|
217
|
-
keepDep.append(nDep)
|
|
218
|
-
|
|
219
|
-
spline.nDep = len(keepDep)
|
|
220
|
-
if spline.nDep == 0:
|
|
221
|
-
# Return the interval center and radius.
|
|
222
|
-
roots.append((interval.intercept + 0.5 * interval.slope, 0.5 * np.linalg.norm(interval.slope)))
|
|
223
|
-
return roots, intervals
|
|
224
|
-
|
|
225
|
-
# Rescale remaining spline coefficients to max 1.0.
|
|
226
|
-
bounds = bounds[keepDep]
|
|
227
|
-
newScale = np.abs(bounds).max()
|
|
228
|
-
spline.coefs = spline.coefs[keepDep]
|
|
229
|
-
spline.coefs *= 1.0 / newScale
|
|
230
|
-
bounds *= 1.0 / newScale
|
|
231
|
-
newScale *= interval.scale
|
|
232
266
|
|
|
233
267
|
# Loop through each independent variable to determine a tighter domain around roots.
|
|
268
|
+
# The interval block's remaining number of independent variables (nInd) is len(interval.unknowns).
|
|
269
|
+
# The interval block's remaining number of dependent variables (nDep) is len(interval.scale).
|
|
234
270
|
domain = []
|
|
235
|
-
|
|
236
|
-
for nInd, order, knots, nCoef, s in zip(range(spline.nInd), spline.order, spline.knots, spline.nCoef, interval.slope):
|
|
237
|
-
# Move independent variable to the last (fastest) axis, adding 1 to account for the dependent variables.
|
|
238
|
-
coefs = np.moveaxis(spline.coefs, nInd + 1, -1)
|
|
239
|
-
|
|
240
|
-
# Compute the coefficients for f(x) = x for the independent variable and its knots.
|
|
241
|
-
degree = order - 1
|
|
242
|
-
xData = np.empty((nCoef,), knots.dtype)
|
|
243
|
-
xData[0] = knots[1]
|
|
244
|
-
for i in range(1, nCoef):
|
|
245
|
-
xData[i] = xData[i - 1] + (knots[i + degree] - knots[i])/degree
|
|
246
|
-
|
|
271
|
+
for nInd in range(len(interval.unknowns)):
|
|
247
272
|
# Loop through each dependent variable to compute the interval containing the root for this independent variable.
|
|
248
273
|
xInterval = (0.0, 1.0)
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
274
|
+
nDep = 0
|
|
275
|
+
for row in interval.block:
|
|
276
|
+
rowInd = 0
|
|
277
|
+
order = 0
|
|
278
|
+
for spline in row:
|
|
279
|
+
if rowInd <= nInd < rowInd + spline.nInd:
|
|
280
|
+
order = spline.order[nInd - rowInd]
|
|
281
|
+
nCoef = spline.nCoef[nInd - rowInd]
|
|
282
|
+
knots = spline.knots[nInd - rowInd]
|
|
283
|
+
# Move independent variable to the last (fastest) axis, adding 1 to account for the dependent variables.
|
|
284
|
+
coefs = np.moveaxis(spline.coefs, nInd - rowInd + 1, -1)
|
|
285
|
+
break
|
|
286
|
+
rowInd += spline.nInd
|
|
254
287
|
|
|
255
|
-
#
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
288
|
+
# Skip this row if it doesn't contains this independent variable.
|
|
289
|
+
if order < 1:
|
|
290
|
+
continue
|
|
291
|
+
|
|
292
|
+
# Compute the coefficients for f(x) = x for the independent variable and its knots.
|
|
293
|
+
degree = order - 1
|
|
294
|
+
xData = np.empty((nCoef,), knots.dtype)
|
|
295
|
+
xData[0] = knots[1]
|
|
296
|
+
for i in range(1, nCoef):
|
|
297
|
+
xData[i] = xData[i - 1] + (knots[i + degree] - knots[i])/degree
|
|
298
|
+
|
|
299
|
+
# Loop through each dependent variable in this row to refine the interval containing the root for this independent variable.
|
|
300
|
+
for yData, ySplineBounds, yBounds in zip(coefs, spline.range_bounds(), interval.bounds[nDep:nDep + spline.nDep]):
|
|
301
|
+
# Compute the 2D convex hull of the knot coefficients and the spline's coefficients
|
|
302
|
+
hull = _convex_hull_2D(xData, yData.ravel(), yBounds, yBounds - ySplineBounds, epsilon)
|
|
303
|
+
if hull is None:
|
|
304
|
+
return roots, intervals
|
|
305
|
+
|
|
306
|
+
# Intersect the convex hull with the xInterval along the x axis (the knot coefficients axis).
|
|
307
|
+
xInterval = _intersect_convex_hull_with_x_interval(hull, epsilon, xInterval)
|
|
308
|
+
if xInterval is None:
|
|
309
|
+
return roots, intervals
|
|
310
|
+
|
|
311
|
+
nDep += spline.nDep
|
|
259
312
|
|
|
260
313
|
domain.append(xInterval)
|
|
261
314
|
|
|
262
315
|
# Compute new slope, intercept, and unknowns.
|
|
263
|
-
domain = np.array(domain,
|
|
316
|
+
domain = np.array(domain, interval.slope.dtype).T
|
|
264
317
|
width = domain[1] - domain[0]
|
|
265
318
|
newSlope = interval.slope.copy()
|
|
266
319
|
newIntercept = interval.intercept.copy()
|
|
@@ -280,17 +333,23 @@ def _refine_projected_polyhedron(interval):
|
|
|
280
333
|
nInd += 1
|
|
281
334
|
|
|
282
335
|
# Iteration is complete if the interval actual width (slope) is either
|
|
283
|
-
# one iteration past being less than sqrt(machineEpsilon) or there are no remaining
|
|
284
|
-
if interval.atMachineEpsilon or
|
|
336
|
+
# one iteration past being less than sqrt(machineEpsilon) or there are no remaining independent variables.
|
|
337
|
+
if interval.atMachineEpsilon or nInd == 0:
|
|
285
338
|
# Return the interval center and radius.
|
|
286
339
|
roots.append((newIntercept + 0.5 * newSlope, epsilon))
|
|
287
340
|
return roots, intervals
|
|
288
341
|
|
|
289
|
-
# Contract spline as needed.
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
342
|
+
# Contract spline matrix as needed.
|
|
343
|
+
if newDomain.shape[1] < domain.shape[1]:
|
|
344
|
+
for row in interval.block:
|
|
345
|
+
rowInd = 0
|
|
346
|
+
for i, spline in enumerate(row):
|
|
347
|
+
row[i] = spline.contract(uvw[rowInd:rowInd + spline.nInd])
|
|
348
|
+
rowInd += spline.nInd
|
|
349
|
+
|
|
350
|
+
# Special case optimization: Use interval newton for one-dimensional splines.
|
|
351
|
+
if nInd == 1 and nDep == 1 and len(interval.block[0]) == 1:
|
|
352
|
+
spline = interval.block[0][0]
|
|
294
353
|
i = newUnknowns[0]
|
|
295
354
|
for root in zeros_using_interval_newton(spline):
|
|
296
355
|
if not isinstance(root, tuple):
|
|
@@ -308,7 +367,7 @@ def _refine_projected_polyhedron(interval):
|
|
|
308
367
|
# Split domain in dimensions that aren't decreasing in width sufficiently.
|
|
309
368
|
width = newDomain[1] - newDomain[0]
|
|
310
369
|
domains = [newDomain]
|
|
311
|
-
for nInd, w in
|
|
370
|
+
for nInd, w in enumerate(width):
|
|
312
371
|
if w > Crit:
|
|
313
372
|
# Didn't get the required decrease in width, so split the domain.
|
|
314
373
|
domainCount = len(domains) # Cache the domain list size, since we're increasing it mid loop
|
|
@@ -328,7 +387,12 @@ def _refine_projected_polyhedron(interval):
|
|
|
328
387
|
for i, w, d in zip(newUnknowns, width, domain.T):
|
|
329
388
|
splitSlope[i] = w * interval.slope[i]
|
|
330
389
|
splitIntercept[i] = d[0] * interval.slope[i] + interval.intercept[i]
|
|
331
|
-
|
|
390
|
+
newInterval = _create_interval(domain.T, interval.block, newUnknowns, interval.scale, splitSlope, splitIntercept, epsilon)
|
|
391
|
+
if newInterval:
|
|
392
|
+
if newInterval.block:
|
|
393
|
+
intervals.append(newInterval)
|
|
394
|
+
else:
|
|
395
|
+
roots.append((newInterval.intercept + 0.5 * newInterval.slope, 0.5 * np.linalg.norm(newInterval.slope)))
|
|
332
396
|
|
|
333
397
|
return roots, intervals
|
|
334
398
|
|
|
@@ -338,18 +402,27 @@ class _Region:
|
|
|
338
402
|
self.radius = radius
|
|
339
403
|
self.count = count
|
|
340
404
|
|
|
341
|
-
def zeros_using_projected_polyhedron(self, epsilon=None):
|
|
342
|
-
if
|
|
343
|
-
|
|
405
|
+
def zeros_using_projected_polyhedron(self, epsilon=None, initialScale=None):
|
|
406
|
+
if self.nInd != self.nDep: raise ValueError("The number of independent variables (nInd) must match the number of dependent variables (nDep).")
|
|
407
|
+
|
|
408
|
+
# Determine epsilon and initialize roots.
|
|
409
|
+
machineEpsilon = np.finfo(self.knotsDtype).eps
|
|
344
410
|
if epsilon is None:
|
|
345
411
|
epsilon = 0.0
|
|
346
|
-
epsilon = max(epsilon, np.sqrt(machineEpsilon))
|
|
347
|
-
evaluationEpsilon = np.sqrt(epsilon)
|
|
412
|
+
epsilon = max(epsilon, np.sqrt(machineEpsilon)) if epsilon else np.sqrt(machineEpsilon)
|
|
413
|
+
evaluationEpsilon = max(np.sqrt(epsilon), np.finfo(self.coefsDtype).eps ** 0.25)
|
|
414
|
+
intervals = []
|
|
348
415
|
roots = []
|
|
349
416
|
|
|
350
|
-
# Set initial
|
|
417
|
+
# Set initial interval.
|
|
351
418
|
domain = self.domain().T
|
|
352
|
-
|
|
419
|
+
initialScale = np.full(self.nDep, 1.0, self.coefsDtype) if initialScale is None else np.array(initialScale, self.coefsDtype)
|
|
420
|
+
newInterval = _create_interval(domain.T, self.block, [*range(self.nInd)], initialScale, domain[1] - domain[0], domain[0], epsilon)
|
|
421
|
+
if newInterval:
|
|
422
|
+
if newInterval.block:
|
|
423
|
+
intervals.append(newInterval)
|
|
424
|
+
else:
|
|
425
|
+
roots.append((newInterval.intercept + 0.5 * newInterval.slope, 0.5 * np.linalg.norm(newInterval.slope)))
|
|
353
426
|
chunkSize = 8
|
|
354
427
|
#pool = Pool() # Pool size matches CPU count
|
|
355
428
|
|
|
@@ -374,7 +447,8 @@ def zeros_using_projected_polyhedron(self, epsilon=None):
|
|
|
374
447
|
rootRadius = root[1]
|
|
375
448
|
|
|
376
449
|
# Ensure we have a real root (not a boundary special case).
|
|
377
|
-
|
|
450
|
+
value = self.evaluate(rootCenter)
|
|
451
|
+
if np.linalg.norm(value) >= evaluationEpsilon:
|
|
378
452
|
continue
|
|
379
453
|
|
|
380
454
|
# Expand the radius of the root based on the approximate distance from the center needed
|
|
@@ -416,48 +490,47 @@ def zeros_using_projected_polyhedron(self, epsilon=None):
|
|
|
416
490
|
|
|
417
491
|
return roots
|
|
418
492
|
|
|
419
|
-
def
|
|
420
|
-
if self.nInd - self.nDep != 1: raise ValueError("The number of free variables (self.nInd - self.nDep) must be one.")
|
|
421
|
-
|
|
493
|
+
def _contours_of_C1_spline_block(self, epsilon, evaluationEpsilon):
|
|
422
494
|
Point = namedtuple('Point', ('d', 'det', 'onUVBoundary', 'turningPoint', 'uvw'))
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
coefsMin = coefs.min()
|
|
429
|
-
coefsMax = coefs.max()
|
|
430
|
-
if coefsMax < -evaluationEpsilon or coefsMin > evaluationEpsilon:
|
|
495
|
+
|
|
496
|
+
# Go through each nDep of the spline block, checking bounds.
|
|
497
|
+
bounds = self.range_bounds()
|
|
498
|
+
for bound in bounds:
|
|
499
|
+
if bound[1] < -evaluationEpsilon or bound[0] > evaluationEpsilon:
|
|
431
500
|
# No contours for this spline.
|
|
432
501
|
return []
|
|
433
502
|
|
|
434
503
|
# Record self's original domain and then reparametrize self's domain to [0, 1]^nInd.
|
|
435
504
|
domain = self.domain().T
|
|
436
505
|
self = self.reparametrize(((0.0, 1.0),) * self.nInd)
|
|
506
|
+
|
|
507
|
+
# Rescale self in all dimensions.
|
|
508
|
+
nDep = 0
|
|
509
|
+
initialScale = np.max(np.abs(bounds), axis=1)
|
|
510
|
+
rescale = np.reciprocal(initialScale)
|
|
511
|
+
for row in self.block:
|
|
512
|
+
nInd = 0
|
|
513
|
+
for spline in row:
|
|
514
|
+
for coefs, scale in zip(spline.coefs, rescale):
|
|
515
|
+
coefs *= scale
|
|
516
|
+
nDep += spline.nDep
|
|
437
517
|
|
|
438
|
-
# Construct self's
|
|
439
|
-
tangents = []
|
|
440
|
-
for nInd in range(self.nInd):
|
|
441
|
-
tangents.append(self.differentiate(nInd))
|
|
518
|
+
# Construct self's normal.
|
|
442
519
|
normal = self.normal_spline((0, 1)) # We only need the first two indices
|
|
443
520
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
attempts = 3
|
|
447
|
-
while attempts > 0:
|
|
521
|
+
# Try arbitrary values for theta between [0, pi/2] that are unlikely to be a stationary points.
|
|
522
|
+
for theta in (1.0 / np.sqrt(2), np.pi / 6.0, 1.0/ np.e):
|
|
448
523
|
points = []
|
|
449
|
-
theta *= 0.607
|
|
450
524
|
cosTheta = np.cos(theta)
|
|
451
525
|
sinTheta = np.sin(theta)
|
|
452
526
|
abort = False
|
|
453
|
-
attempts -=1
|
|
454
527
|
|
|
455
528
|
# Construct the turning point determinant.
|
|
456
529
|
turningPointDeterminant = normal.dot((cosTheta, sinTheta))
|
|
457
530
|
|
|
458
531
|
# Find intersections with u and v boundaries.
|
|
459
532
|
def uvIntersections(nInd, boundary):
|
|
460
|
-
zeros = self.contract([None] * nInd + [boundary] + [None] * (self.nInd - nInd - 1)).zeros()
|
|
533
|
+
zeros = self.contract([None] * nInd + [boundary] + [None] * (self.nInd - nInd - 1)).zeros(epsilon, initialScale)
|
|
461
534
|
abort = False
|
|
462
535
|
for zero in zeros:
|
|
463
536
|
if isinstance(zero, tuple):
|
|
@@ -465,10 +538,24 @@ def contours(self):
|
|
|
465
538
|
break
|
|
466
539
|
uvw = np.insert(np.array(zero), nInd, boundary)
|
|
467
540
|
d = uvw[0] * cosTheta + uvw[1] * sinTheta
|
|
468
|
-
|
|
541
|
+
n = normal(uvw)
|
|
542
|
+
tpd = turningPointDeterminant(uvw)
|
|
543
|
+
det = (0.5 - boundary) * n[nInd] * tpd
|
|
469
544
|
if abs(det) < epsilon:
|
|
470
545
|
abort = True
|
|
471
546
|
break
|
|
547
|
+
# Check for literal corner case.
|
|
548
|
+
otherInd = 1 - nInd
|
|
549
|
+
otherValue = uvw[otherInd]
|
|
550
|
+
if otherValue < epsilon or otherValue + epsilon > 1.0:
|
|
551
|
+
otherDet = (0.5 - otherValue) * n[otherInd] * tpd
|
|
552
|
+
if det * otherDet < 0.0:
|
|
553
|
+
continue # Corner that starts and ends, ignore it
|
|
554
|
+
elif max(otherValue, boundary) < epsilon and det < 0.0:
|
|
555
|
+
continue # End point at (0, 0), ignore it
|
|
556
|
+
elif min(otherValue, boundary) + epsilon > 1.0 and det > 0.0:
|
|
557
|
+
continue # Start point at (1, 1), ignore it
|
|
558
|
+
# Append boundary point.
|
|
472
559
|
points.append(Point(d, det, True, False, uvw))
|
|
473
560
|
return abort
|
|
474
561
|
for nInd in range(2):
|
|
@@ -483,7 +570,7 @@ def contours(self):
|
|
|
483
570
|
|
|
484
571
|
# Find intersections with other boundaries.
|
|
485
572
|
def otherIntersections(nInd, boundary):
|
|
486
|
-
zeros = self.contract([None] * nInd + [boundary] + [None] * (self.nInd - nInd - 1)).zeros()
|
|
573
|
+
zeros = self.contract([None] * nInd + [boundary] + [None] * (self.nInd - nInd - 1)).zeros(epsilon, initialScale)
|
|
487
574
|
abort = False
|
|
488
575
|
for zero in zeros:
|
|
489
576
|
if isinstance(zero, tuple):
|
|
@@ -492,12 +579,13 @@ def contours(self):
|
|
|
492
579
|
uvw = np.insert(np.array(zero), nInd, boundary)
|
|
493
580
|
d = uvw[0] * cosTheta + uvw[1] * sinTheta
|
|
494
581
|
columns = np.empty((self.nDep, self.nInd - 1))
|
|
582
|
+
tangents = self.jacobian(uvw).T
|
|
495
583
|
i = 0
|
|
496
584
|
for j in range(self.nInd):
|
|
497
585
|
if j != nInd:
|
|
498
|
-
columns[:, i] = tangents[j]
|
|
586
|
+
columns[:, i] = tangents[j]
|
|
499
587
|
i += 1
|
|
500
|
-
duv = np.linalg.solve(columns, -tangents[nInd]
|
|
588
|
+
duv = np.linalg.solve(columns, -tangents[nInd])
|
|
501
589
|
det = np.arctan2((0.5 - boundary) * (duv[0] * cosTheta + duv[1] * sinTheta), (0.5 - boundary) * (duv[0] * cosTheta - duv[1] * sinTheta))
|
|
502
590
|
if abs(det) < epsilon:
|
|
503
591
|
abort = True
|
|
@@ -515,10 +603,9 @@ def contours(self):
|
|
|
515
603
|
continue # Try a different theta
|
|
516
604
|
|
|
517
605
|
# Find turning points by combining self and turningPointDeterminant into a system and processing its zeros.
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
zeros = system.zeros()
|
|
606
|
+
turningPointBlock = self.block.copy()
|
|
607
|
+
turningPointBlock.append([turningPointDeterminant])
|
|
608
|
+
zeros = bspy.spline_block.SplineBlock(turningPointBlock).zeros(epsilon, np.append(initialScale, 1.0))
|
|
522
609
|
for uvw in zeros:
|
|
523
610
|
if isinstance(uvw, tuple):
|
|
524
611
|
abort = True
|
|
@@ -538,7 +625,7 @@ def contours(self):
|
|
|
538
625
|
if not abort:
|
|
539
626
|
break # We're done!
|
|
540
627
|
|
|
541
|
-
if
|
|
628
|
+
if abort: raise ValueError("No contours. Degenerate equations.")
|
|
542
629
|
|
|
543
630
|
if not points:
|
|
544
631
|
return [] # No contours
|
|
@@ -551,17 +638,14 @@ def contours(self):
|
|
|
551
638
|
# a panel boundary: u * cosTheta + v * sinTheta = d. Basically, we add this panel boundary plane
|
|
552
639
|
# to the contour condition. We'll define it for d = 0, and add the actual d later.
|
|
553
640
|
# We didn't construct the panel system earlier, because we didn't have theta.
|
|
554
|
-
panelCoefs = np.
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
for i in range(1, self.nCoef[1]):
|
|
563
|
-
panelCoefs[self.nDep, :, i] = panelCoefs[self.nDep, :, i - 1] + ((self.knots[1][degree + i] - self.knots[1][i]) / degree) * sinTheta
|
|
564
|
-
panel = type(self)(self.nInd, self.nInd, self.order, self.nCoef, self.knots, panelCoefs, self.metadata)
|
|
641
|
+
panelCoefs = np.array((((0.0, sinTheta), (cosTheta, cosTheta + sinTheta)),), self.coefsDtype)
|
|
642
|
+
panelSpline = bspy.spline.Spline(2, 1, (2, 2), (2, 2),
|
|
643
|
+
(np.array((0.0, 0.0, 1.0, 1.0), self.knotsDtype), np.array((0.0, 0.0, 1.0, 1.0), self.knotsDtype)),
|
|
644
|
+
panelCoefs)
|
|
645
|
+
panelBlock = self.block.copy()
|
|
646
|
+
panelBlock.append([panelSpline])
|
|
647
|
+
panel = bspy.spline_block.SplineBlock(panelBlock)
|
|
648
|
+
panelInitialScale = np.append(initialScale, 1.0)
|
|
565
649
|
|
|
566
650
|
# Okay, we have everything we need to determine the contour topology and points along each contour.
|
|
567
651
|
# We've done the first two steps of Grandine and Klein's algorithm:
|
|
@@ -572,6 +656,23 @@ def contours(self):
|
|
|
572
656
|
# (3) Take all the points found in Step (1) and Step (2) and order them by distance in the theta direction from the origin.
|
|
573
657
|
points.sort()
|
|
574
658
|
|
|
659
|
+
# Extra step not in paper.
|
|
660
|
+
# Remove duplicate points (typically appear at corners).
|
|
661
|
+
i = 0
|
|
662
|
+
while i < len(points):
|
|
663
|
+
previousPoint = points[i]
|
|
664
|
+
j = i + 1
|
|
665
|
+
while j < len(points):
|
|
666
|
+
point = points[j]
|
|
667
|
+
if point.d < previousPoint.d + epsilon:
|
|
668
|
+
if np.linalg.norm(point.uvw - previousPoint.uvw) < epsilon:
|
|
669
|
+
del points[j]
|
|
670
|
+
else:
|
|
671
|
+
j += 1
|
|
672
|
+
else:
|
|
673
|
+
break
|
|
674
|
+
i += 1
|
|
675
|
+
|
|
575
676
|
# Extra step not in paper.
|
|
576
677
|
# Run a checksum on the points, ensuring starting and ending points balance.
|
|
577
678
|
# Start by flipping endpoints as needed, since we can miss turning points near endpoints.
|
|
@@ -648,11 +749,11 @@ def contours(self):
|
|
|
648
749
|
# points. Either insert two new contours in the list or delete two existing ones from
|
|
649
750
|
# the list. Go back to Step (5).
|
|
650
751
|
# First, construct panel, whose zeros lie along the panel boundary, u * cosTheta + v * sinTheta - d = 0.
|
|
651
|
-
|
|
752
|
+
panelSpline.coefs = panelCoefs - point.d
|
|
652
753
|
|
|
653
754
|
if point.turningPoint and point.uvw is None:
|
|
654
755
|
# For an inserted panel between two consecutive turning points, just find zeros along the panel.
|
|
655
|
-
panelPoints = panel.zeros()
|
|
756
|
+
panelPoints = panel.zeros(epsilon, panelInitialScale)
|
|
656
757
|
elif point.turningPoint:
|
|
657
758
|
# Split panel below and above the known zero point.
|
|
658
759
|
# This avoids extra computation and the high-zero at the known zero point, while ensuring we match the turning point.
|
|
@@ -674,15 +775,15 @@ def contours(self):
|
|
|
674
775
|
np.linalg.norm(selfUU * sinTheta * sinTheta - 2.0 * selfUV * sinTheta * cosTheta + selfVV * cosTheta * cosTheta))
|
|
675
776
|
# Now, we can find the zeros of the split panel, checking to ensure each panel is within bounds first.
|
|
676
777
|
if point.uvw[0] + sinTheta * offset < 1.0 - epsilon and epsilon < point.uvw[1] - cosTheta * offset:
|
|
677
|
-
panelPoints += panel.trim(((point.uvw[0] + sinTheta * offset, 1.0), (0.0, point.uvw[1] - cosTheta * offset)) + ((None, None),) * (self.nInd - 2)).zeros()
|
|
778
|
+
panelPoints += panel.trim(((point.uvw[0] + sinTheta * offset, 1.0), (0.0, point.uvw[1] - cosTheta * offset)) + ((None, None),) * (self.nInd - 2)).zeros(epsilon, panelInitialScale)
|
|
678
779
|
expectedPanelPoints -= len(panelPoints) - 1 # Discount the turning point itself
|
|
679
780
|
if expectedPanelPoints > 0 and epsilon < point.uvw[0] - sinTheta * offset and point.uvw[1] + cosTheta * offset < 1.0 - epsilon:
|
|
680
|
-
panelPoints += panel.trim(((0.0, point.uvw[0] - sinTheta * offset), (point.uvw[1] + cosTheta * offset, 1.0)) + ((None, None),) * (self.nInd - 2)).zeros()
|
|
781
|
+
panelPoints += panel.trim(((0.0, point.uvw[0] - sinTheta * offset), (point.uvw[1] + cosTheta * offset, 1.0)) + ((None, None),) * (self.nInd - 2)).zeros(epsilon, panelInitialScale)
|
|
681
782
|
else: # It's an other-boundary point.
|
|
682
783
|
# Only find extra zeros along the panel if any are expected (> 0 for starting point, > 1 for ending one).
|
|
683
784
|
expectedPanelPoints = len(currentContourPoints) - (0 if point.det > 0.0 else 1)
|
|
684
785
|
if expectedPanelPoints > 0:
|
|
685
|
-
panelPoints = panel.zeros()
|
|
786
|
+
panelPoints = panel.zeros(epsilon, panelInitialScale)
|
|
686
787
|
panelPoints.sort(key=lambda uvw: np.linalg.norm(point.uvw - uvw)) # Sort by distance from boundary point
|
|
687
788
|
while len(panelPoints) > expectedPanelPoints:
|
|
688
789
|
panelPoints.pop(0) # Drop points closest to the boundary point
|
|
@@ -690,8 +791,6 @@ def contours(self):
|
|
|
690
791
|
else:
|
|
691
792
|
panelPoints = [point.uvw]
|
|
692
793
|
|
|
693
|
-
# Add d back to prepare for next turning point.
|
|
694
|
-
panel.coefs[self.nDep] += point.d
|
|
695
794
|
# Sort zero points by their position along the panel boundary (using vector orthogonal to its normal).
|
|
696
795
|
panelPoints.sort(key=lambda uvw: uvw[1] * cosTheta - uvw[0] * sinTheta)
|
|
697
796
|
# Go through panel points, adding them to existing contours, creating new ones, or closing old ones.
|
|
@@ -768,31 +867,145 @@ def contours(self):
|
|
|
768
867
|
currentContourPoints[i + adjustment].append(uvw)
|
|
769
868
|
|
|
770
869
|
# We've determined a bunch of points along all the contours, including starting and ending points.
|
|
771
|
-
# Now we just need to create splines for those contours using the Spline.contour method.
|
|
870
|
+
# Now we just need to create splines for those contours using the bspy.Spline.contour method.
|
|
772
871
|
splineContours = []
|
|
773
872
|
for points in contourPoints:
|
|
774
|
-
contour = bspy.
|
|
873
|
+
contour = bspy.Spline.contour(self, points)
|
|
775
874
|
# Transform the contour to self's original domain.
|
|
776
875
|
contour.coefs = (contour.coefs.T * (domain[1] - domain[0]) + domain[0]).T
|
|
777
876
|
splineContours.append(contour)
|
|
778
877
|
|
|
779
878
|
return splineContours
|
|
780
879
|
|
|
880
|
+
def contours(self):
|
|
881
|
+
if self.nInd - self.nDep != 1: raise ValueError("The number of free variables (self.nInd - self.nDep) must be one.")
|
|
882
|
+
epsilon = np.sqrt(np.finfo(self.knotsDtype).eps)
|
|
883
|
+
evaluationEpsilon = max(np.sqrt(epsilon), np.finfo(self.coefsDtype).eps ** 0.25)
|
|
884
|
+
|
|
885
|
+
# Split the splines in the block to ensure C1 continuity within each block
|
|
886
|
+
blocks = [self]
|
|
887
|
+
for i, row in enumerate(self.block):
|
|
888
|
+
for j, spline in enumerate(row):
|
|
889
|
+
splines = spline.split(minContinuity = 1)
|
|
890
|
+
if splines.size == 1 and self.size == 1:
|
|
891
|
+
break # Special case of a block with one C1 spline
|
|
892
|
+
newBlocks = []
|
|
893
|
+
for spline in splines.ravel():
|
|
894
|
+
for block in blocks:
|
|
895
|
+
newBlock = block.block.copy()
|
|
896
|
+
newRow = newBlock[i].copy()
|
|
897
|
+
newBlock[i] = newRow
|
|
898
|
+
newRow[j] = spline
|
|
899
|
+
newBlocks.append(bspy.spline_block.SplineBlock(newBlock))
|
|
900
|
+
blocks = newBlocks
|
|
901
|
+
|
|
902
|
+
contours = []
|
|
903
|
+
for block in blocks:
|
|
904
|
+
splineContours = _contours_of_C1_spline_block(block, epsilon, evaluationEpsilon)
|
|
905
|
+
for newContour in splineContours:
|
|
906
|
+
newStart = newContour(0.0)
|
|
907
|
+
newFinish = newContour(1.0)
|
|
908
|
+
joined = False
|
|
909
|
+
for i, oldContour in enumerate(contours):
|
|
910
|
+
oldStart = oldContour(0.0)
|
|
911
|
+
oldFinish = oldContour(1.0)
|
|
912
|
+
if np.linalg.norm(newStart - oldFinish) < evaluationEpsilon:
|
|
913
|
+
contours[i] = bspy.Spline.join((oldContour, newContour))
|
|
914
|
+
joined = True
|
|
915
|
+
break
|
|
916
|
+
if np.linalg.norm(newStart - oldStart) < evaluationEpsilon:
|
|
917
|
+
contours[i] = bspy.Spline.join((oldContour, newContour.reverse()))
|
|
918
|
+
joined = True
|
|
919
|
+
break
|
|
920
|
+
if np.linalg.norm(newFinish - oldStart) < evaluationEpsilon:
|
|
921
|
+
contours[i] = bspy.Spline.join((newContour, oldContour))
|
|
922
|
+
joined = True
|
|
923
|
+
break
|
|
924
|
+
if np.linalg.norm(newFinish - oldFinish) < evaluationEpsilon:
|
|
925
|
+
contours[i] = bspy.Spline.join((newContour, oldContour.reverse()))
|
|
926
|
+
joined = True
|
|
927
|
+
break
|
|
928
|
+
if not joined:
|
|
929
|
+
contours.append(newContour)
|
|
930
|
+
return contours
|
|
931
|
+
|
|
781
932
|
def intersect(self, other):
|
|
782
|
-
#assert self.range_dimension() == other.range_dimension() TODO: Put back this assertion
|
|
783
933
|
intersections = []
|
|
784
934
|
nDep = self.nInd # The dimension of the intersection's range
|
|
785
935
|
|
|
786
|
-
# Spline-
|
|
787
|
-
if isinstance(other,
|
|
936
|
+
# Spline-Hyperplane intersection.
|
|
937
|
+
if isinstance(other, Hyperplane):
|
|
938
|
+
# Compute the projection onto the hyperplane to map Spline-Hyperplane intersection points to the domain of the Hyperplane.
|
|
939
|
+
projection = np.linalg.inv(other._tangentSpace.T @ other._tangentSpace) @ other._tangentSpace.T
|
|
788
940
|
# Construct a new spline that represents the intersection.
|
|
789
|
-
spline = self.
|
|
941
|
+
spline = self.dot(other._normal) - np.atleast_1d(np.dot(other._normal, other._point))
|
|
790
942
|
|
|
791
|
-
# Curve-
|
|
943
|
+
# Curve-Line intersection.
|
|
792
944
|
if nDep == 1:
|
|
793
945
|
# Find the intersection points and intervals.
|
|
794
946
|
zeros = spline.zeros()
|
|
795
947
|
# Convert each intersection point into a Manifold.Crossing and each intersection interval into a Manifold.Coincidence.
|
|
948
|
+
for zero in zeros:
|
|
949
|
+
if isinstance(zero, tuple):
|
|
950
|
+
# Intersection is an interval, so create a Manifold.Coincidence.
|
|
951
|
+
planeBounds = (projection @ (self((zero[0],)) - other._point), projection @ (self((zero[1],)) - other._point))
|
|
952
|
+
|
|
953
|
+
# First, check for crossings at the boundaries of the coincidence, since splines can have discontinuous tangents.
|
|
954
|
+
# We do this first because later we may change the order of the plane bounds.
|
|
955
|
+
(bounds,) = self.domain()
|
|
956
|
+
epsilon = 0.1 * Manifold.minSeparation
|
|
957
|
+
if zero[0] - epsilon > bounds[0]:
|
|
958
|
+
intersections.append(Manifold.Crossing(Hyperplane(1.0, zero[0] - epsilon, 0.0), Hyperplane(1.0, planeBounds[0], 0.0)))
|
|
959
|
+
if zero[1] + epsilon < bounds[1]:
|
|
960
|
+
intersections.append(Manifold.Crossing(Hyperplane(1.0, zero[1] + epsilon, 0.0), Hyperplane(1.0, planeBounds[1], 0.0)))
|
|
961
|
+
|
|
962
|
+
# Now, create the coincidence.
|
|
963
|
+
left = Solid(nDep, False)
|
|
964
|
+
left.add_boundary(Boundary(Hyperplane(-1.0, zero[0], 0.0), Solid(0, True)))
|
|
965
|
+
left.add_boundary(Boundary(Hyperplane(1.0, zero[1], 0.0), Solid(0, True)))
|
|
966
|
+
right = Solid(nDep, False)
|
|
967
|
+
if planeBounds[0] > planeBounds[1]:
|
|
968
|
+
planeBounds = (planeBounds[1], planeBounds[0])
|
|
969
|
+
right.add_boundary(Boundary(Hyperplane(-1.0, planeBounds[0], 0.0), Solid(0, True)))
|
|
970
|
+
right.add_boundary(Boundary(Hyperplane(1.0, planeBounds[1], 0.0), Solid(0, True)))
|
|
971
|
+
alignment = np.dot(self.normal((zero[0],)), other._normal) # Use the first zero, since B-splines are closed on the left
|
|
972
|
+
width = zero[1] - zero[0]
|
|
973
|
+
transform = (planeBounds[1] - planeBounds[0]) / width
|
|
974
|
+
translation = (planeBounds[0] * zero[1] - planeBounds[1] * zero[0]) / width
|
|
975
|
+
intersections.append(Manifold.Coincidence(left, right, alignment, np.atleast_2d(transform), np.atleast_2d(1.0 / transform), np.atleast_1d(translation)))
|
|
976
|
+
else:
|
|
977
|
+
# Intersection is a point, so create a Manifold.Crossing.
|
|
978
|
+
intersections.append(Manifold.Crossing(Hyperplane(1.0, zero, 0.0), Hyperplane(1.0, projection @ (self((zero,)) - other._point), 0.0)))
|
|
979
|
+
|
|
980
|
+
# Surface-Plane intersection.
|
|
981
|
+
elif nDep == 2:
|
|
982
|
+
# Find the intersection contours, which are returned as splines.
|
|
983
|
+
contours = spline.contours()
|
|
984
|
+
# Convert each contour into a Manifold.Crossing.
|
|
985
|
+
for contour in contours:
|
|
986
|
+
# The left portion is the contour returned for the spline-plane intersection.
|
|
987
|
+
left = contour
|
|
988
|
+
# The right portion is the contour projected onto the plane's domain, which we compute with samples and a least squares fit.
|
|
989
|
+
tValues = np.linspace(0.0, 1.0, contour.nCoef[0] + 5) # Over-sample a bit to reduce the condition number and avoid singular matrix
|
|
990
|
+
points = []
|
|
991
|
+
for t in tValues:
|
|
992
|
+
zero = contour((t,))
|
|
993
|
+
points.append(projection @ (self(zero) - other._point))
|
|
994
|
+
right = bspy.Spline.least_squares(tValues, np.array(points).T, contour.order, contour.knots)
|
|
995
|
+
intersections.append(Manifold.Crossing(left, right))
|
|
996
|
+
else:
|
|
997
|
+
return NotImplemented
|
|
998
|
+
|
|
999
|
+
# Spline-Spline intersection.
|
|
1000
|
+
elif isinstance(other, bspy.Spline):
|
|
1001
|
+
# Construct a spline block that represents the intersection.
|
|
1002
|
+
block = bspy.spline_block.SplineBlock([[self, -other]])
|
|
1003
|
+
|
|
1004
|
+
# Curve-Curve intersection.
|
|
1005
|
+
if nDep == 1:
|
|
1006
|
+
# Find the intersection points and intervals.
|
|
1007
|
+
zeros = block.zeros()
|
|
1008
|
+
# Convert each intersection point into a Manifold.Crossing and each intersection interval into a Manifold.Coincidence.
|
|
796
1009
|
for zero in zeros:
|
|
797
1010
|
if isinstance(zero, tuple):
|
|
798
1011
|
# Intersection is an interval, so create a Manifold.Coincidence.
|
|
@@ -812,47 +1025,45 @@ def intersect(self, other):
|
|
|
812
1025
|
intersections.append(Manifold.Crossing(Hyperplane(1.0, zero[1][0], 0.0), Hyperplane(1.0, zero[1][1] + epsilon, 0.0)))
|
|
813
1026
|
|
|
814
1027
|
# Now, create the coincidence.
|
|
815
|
-
# TODO: Remove the quoted section.
|
|
816
|
-
"""
|
|
817
1028
|
left = Solid(nDep, False)
|
|
818
|
-
left.
|
|
819
|
-
left.
|
|
1029
|
+
left.add_boundary(Boundary(Hyperplane(-1.0, zero[0][0], 0.0), Solid(0, True)))
|
|
1030
|
+
left.add_boundary(Boundary(Hyperplane(1.0, zero[1][0], 0.0), Solid(0, True)))
|
|
820
1031
|
right = Solid(nDep, False)
|
|
821
|
-
right.
|
|
822
|
-
right.
|
|
1032
|
+
right.add_boundary(Boundary(Hyperplane(-1.0, zero[0][1], 0.0), Solid(0, True)))
|
|
1033
|
+
right.add_boundary(Boundary(Hyperplane(1.0, zero[1][1], 0.0), Solid(0, True)))
|
|
823
1034
|
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
|
|
824
1035
|
width = zero[1][0] - zero[0][0]
|
|
825
1036
|
transform = (zero[1][1] - zero[0][1]) / width
|
|
826
1037
|
translation = (zero[0][1] * zero[1][0] - zero[1][1] * zero[0][0]) / width
|
|
827
1038
|
intersections.append(Manifold.Coincidence(left, right, alignment, np.atleast_2d(transform), np.atleast_2d(1.0 / transform), np.atleast_1d(translation)))
|
|
828
|
-
"""
|
|
829
1039
|
else:
|
|
830
1040
|
# Intersection is a point, so create a Manifold.Crossing.
|
|
831
1041
|
intersections.append(Manifold.Crossing(Hyperplane(1.0, zero[:nDep], 0.0), Hyperplane(1.0, zero[nDep:], 0.0)))
|
|
832
1042
|
|
|
833
1043
|
# Surface-Surface intersection.
|
|
834
1044
|
elif nDep == 2:
|
|
835
|
-
|
|
1045
|
+
if "Name" in self.metadata and "Name" in other.metadata:
|
|
1046
|
+
logging.info(f"intersect:{self.metadata['Name']}:{other.metadata['Name']}")
|
|
836
1047
|
# Find the intersection contours, which are returned as splines.
|
|
837
1048
|
swap = False
|
|
838
1049
|
try:
|
|
839
1050
|
# First try the intersection as is.
|
|
840
|
-
contours =
|
|
841
|
-
except ValueError:
|
|
1051
|
+
contours = block.contours()
|
|
1052
|
+
except ValueError as e:
|
|
1053
|
+
logging.info(e)
|
|
842
1054
|
# If that fails, swap the manifolds. Worth a shot since intersections are touchy.
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
#logging.info(f"intersect_manifold({other.metadata['Name']}, {self.metadata['Name']})")
|
|
849
|
-
contours = spline.contours()
|
|
1055
|
+
block = bspy.spline_block.SplineBlock([[other, -self]])
|
|
1056
|
+
if "Name" in self.metadata and "Name" in other.metadata:
|
|
1057
|
+
logging.info(f"intersect:{other.metadata['Name']}:{self.metadata['Name']}")
|
|
1058
|
+
contours = block.contours()
|
|
1059
|
+
# Convert each contour into a Manifold.Crossing, swapping the manifolds back.
|
|
850
1060
|
for contour in contours:
|
|
851
1061
|
# Swap left and right, compared to not swapped.
|
|
852
1062
|
left = bspy.Spline(contour.nInd, nDep, contour.order, contour.nCoef, contour.knots, contour.coefs[nDep:], contour.metadata)
|
|
853
1063
|
right = bspy.Spline(contour.nInd, nDep, contour.order, contour.nCoef, contour.knots, contour.coefs[:nDep], contour.metadata)
|
|
854
1064
|
intersections.append(Manifold.Crossing(left, right))
|
|
855
1065
|
else:
|
|
1066
|
+
# Convert each contour into a Manifold.Crossing.
|
|
856
1067
|
for contour in contours:
|
|
857
1068
|
left = bspy.Spline(contour.nInd, nDep, contour.order, contour.nCoef, contour.knots, contour.coefs[:nDep], contour.metadata)
|
|
858
1069
|
right = bspy.Spline(contour.nInd, nDep, contour.order, contour.nCoef, contour.knots, contour.coefs[nDep:], contour.metadata)
|
|
@@ -864,15 +1075,115 @@ def intersect(self, other):
|
|
|
864
1075
|
|
|
865
1076
|
# Ensure the normals point outwards for both Manifolds in each crossing intersection.
|
|
866
1077
|
# 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.
|
|
867
|
-
# TODO: Remove quoted section.
|
|
868
|
-
"""
|
|
869
1078
|
domainPoint = np.atleast_1d(0.5)
|
|
870
|
-
for intersection in intersections:
|
|
1079
|
+
for i, intersection in enumerate(intersections):
|
|
871
1080
|
if isinstance(intersection, Manifold.Crossing):
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
if np.dot(
|
|
875
|
-
|
|
876
|
-
|
|
1081
|
+
left = intersection.left
|
|
1082
|
+
right = intersection.right
|
|
1083
|
+
if np.dot(self.tangent_space(left.evaluate(domainPoint)) @ left.normal(domainPoint), other.normal(right.evaluate(domainPoint))) < 0.0:
|
|
1084
|
+
left = left.flip_normal()
|
|
1085
|
+
if np.dot(other.tangent_space(right.evaluate(domainPoint)) @ right.normal(domainPoint), self.normal(left.evaluate(domainPoint))) < 0.0:
|
|
1086
|
+
right = right.flip_normal()
|
|
1087
|
+
intersections[i] = Manifold.Crossing(left, right)
|
|
1088
|
+
|
|
1089
|
+
return intersections
|
|
1090
|
+
|
|
1091
|
+
def complete_slice(self, slice, solid):
|
|
1092
|
+
# Spline manifold domains have finite bounds.
|
|
1093
|
+
slice.containsInfinity = False
|
|
1094
|
+
bounds = self.domain()
|
|
1095
|
+
|
|
1096
|
+
# If manifold (self) has no intersections with solid, just check containment.
|
|
1097
|
+
if not slice.boundaries:
|
|
1098
|
+
if slice.dimension == 2:
|
|
1099
|
+
if "Name" in self.metadata:
|
|
1100
|
+
logging.info(f"check containment:{self.metadata['Name']}")
|
|
1101
|
+
domain = bounds.T
|
|
1102
|
+
if solid.contains_point(self(0.5 * (domain[0] + domain[1]))):
|
|
1103
|
+
for boundary in Hyperplane.create_hypercube(bounds).boundaries:
|
|
1104
|
+
slice.add_boundary(boundary)
|
|
1105
|
+
return
|
|
1106
|
+
|
|
1107
|
+
# For curves, add domain bounds as needed.
|
|
1108
|
+
if slice.dimension == 1:
|
|
1109
|
+
slice.boundaries.sort(key=lambda b: (b.manifold.evaluate(0.0), b.manifold.normal(0.0)))
|
|
1110
|
+
# First, check right end since we add new boundary to the end.
|
|
1111
|
+
if abs(slice.boundaries[-1].manifold._point - bounds[0][1]) >= Manifold.minSeparation and \
|
|
1112
|
+
slice.boundaries[-1].manifold._normal < 0.0:
|
|
1113
|
+
slice.add_boundary(Boundary(Hyperplane(-slice.boundaries[-1].manifold._normal, bounds[0][1], 0.0), Solid(0, True)))
|
|
1114
|
+
# Next, check left end since it's still untouched.
|
|
1115
|
+
if abs(slice.boundaries[0].manifold._point - bounds[0][0]) >= Manifold.minSeparation and \
|
|
1116
|
+
slice.boundaries[0].manifold._normal > 0.0:
|
|
1117
|
+
slice.add_boundary(Boundary(Hyperplane(-slice.boundaries[0].manifold._normal, bounds[0][0], 0.0), Solid(0, True)))
|
|
1118
|
+
|
|
1119
|
+
# For surfaces, intersect full spline domain with existing slice boundaries.
|
|
1120
|
+
if slice.dimension == 2:
|
|
1121
|
+
fullDomain = Hyperplane.create_hypercube(bounds)
|
|
1122
|
+
for newBoundary in fullDomain.boundaries: # Mark full domain boundaries as untouched
|
|
1123
|
+
newBoundary.touched = False
|
|
1124
|
+
|
|
1125
|
+
# Define function for adding slice points to full domain boundaries.
|
|
1126
|
+
def process_domain_point(boundary, domainPoint):
|
|
1127
|
+
point = boundary.manifold.evaluate(domainPoint)
|
|
1128
|
+
# See if and where point touches full domain.
|
|
1129
|
+
for newBoundary in fullDomain.boundaries:
|
|
1130
|
+
vector = point - newBoundary.manifold._point
|
|
1131
|
+
if abs(np.dot(newBoundary.manifold._normal, vector)) < Manifold.minSeparation:
|
|
1132
|
+
# Add the point onto the new boundary.
|
|
1133
|
+
normal = np.sign(newBoundary.manifold._tangentSpace.T @ boundary.manifold.normal(domainPoint))
|
|
1134
|
+
newBoundary.domain.add_boundary(Boundary(Hyperplane(normal, newBoundary.manifold._tangentSpace.T @ vector, 0.0), Solid(0, True)))
|
|
1135
|
+
newBoundary.touched = True
|
|
1136
|
+
break
|
|
1137
|
+
|
|
1138
|
+
# Go through existing boundaries and check if either of their endpoints lies on the spline's bounds.
|
|
1139
|
+
for boundary in slice.boundaries:
|
|
1140
|
+
domainBoundaries = boundary.domain.boundaries
|
|
1141
|
+
domainBoundaries.sort(key=lambda boundary: (boundary.manifold.evaluate(0.0), boundary.manifold.normal(0.0)))
|
|
1142
|
+
process_domain_point(boundary, domainBoundaries[0].manifold._point)
|
|
1143
|
+
if len(domainBoundaries) > 1:
|
|
1144
|
+
process_domain_point(boundary, domainBoundaries[-1].manifold._point)
|
|
1145
|
+
|
|
1146
|
+
# For touched boundaries, remove domain bounds that aren't needed and then add boundary to slice.
|
|
1147
|
+
boundaryWasTouched = False
|
|
1148
|
+
for newBoundary in fullDomain.boundaries:
|
|
1149
|
+
if newBoundary.touched:
|
|
1150
|
+
boundaryWasTouched = True
|
|
1151
|
+
domainBoundaries = newBoundary.domain.boundaries
|
|
1152
|
+
assert len(domainBoundaries) > 2
|
|
1153
|
+
domainBoundaries.sort(key=lambda boundary: (boundary.manifold.evaluate(0.0), boundary.manifold.normal(0.0)))
|
|
1154
|
+
# Ensure domain endpoints don't overlap and their normals are consistent.
|
|
1155
|
+
if abs(domainBoundaries[0].manifold._point - domainBoundaries[1].manifold._point) < Manifold.minSeparation or \
|
|
1156
|
+
domainBoundaries[1].manifold._normal < 0.0:
|
|
1157
|
+
del domainBoundaries[0]
|
|
1158
|
+
if abs(domainBoundaries[-1].manifold._point - domainBoundaries[-2].manifold._point) < Manifold.minSeparation or \
|
|
1159
|
+
domainBoundaries[-2].manifold._normal > 0.0:
|
|
1160
|
+
del domainBoundaries[-1]
|
|
1161
|
+
slice.add_boundary(newBoundary)
|
|
1162
|
+
|
|
1163
|
+
if boundaryWasTouched:
|
|
1164
|
+
# Touch untouched boundaries that are connected to touched boundary endpoints and add them to slice.
|
|
1165
|
+
boundaryMap = ((2, 3, 0), (2, 3, -1), (0, 1, 0), (0, 1, -1)) # Map of which full domain boundaries touch each other
|
|
1166
|
+
while True:
|
|
1167
|
+
noTouches = True
|
|
1168
|
+
for map, newBoundary, bound in zip(boundaryMap, fullDomain.boundaries, bounds.flatten()):
|
|
1169
|
+
if not newBoundary.touched:
|
|
1170
|
+
leftBoundary = fullDomain.boundaries[map[0]]
|
|
1171
|
+
rightBoundary = fullDomain.boundaries[map[1]]
|
|
1172
|
+
if leftBoundary.touched and abs(leftBoundary.domain.boundaries[map[2]].manifold._point - bound) < Manifold.minSeparation:
|
|
1173
|
+
newBoundary.touched = True
|
|
1174
|
+
slice.add_boundary(newBoundary)
|
|
1175
|
+
noTouches = False
|
|
1176
|
+
elif rightBoundary.touched and abs(rightBoundary.domain.boundaries[map[2]].manifold._point - bound) < Manifold.minSeparation:
|
|
1177
|
+
newBoundary.touched = True
|
|
1178
|
+
slice.add_boundary(newBoundary)
|
|
1179
|
+
noTouches = False
|
|
1180
|
+
if noTouches:
|
|
1181
|
+
break
|
|
1182
|
+
else:
|
|
1183
|
+
# No slice boundaries touched the full domain (a hole), so only add full domain if it is contained in the solid.
|
|
1184
|
+
if solid.contains_point(self.evaluate(bounds[:,0])):
|
|
1185
|
+
for newBoundary in fullDomain.boundaries:
|
|
1186
|
+
slice.add_boundary(newBoundary)
|
|
877
1187
|
|
|
878
|
-
|
|
1188
|
+
def full_domain(self):
|
|
1189
|
+
return Hyperplane.create_hypercube(self.domain())
|