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.
@@ -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
- from bspy.manifold import Manifold, Hyperplane
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 = [0.5 * (projectedLeftStep + projectedRightStep)]
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
- return []
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', ('spline', 'unknowns', 'scale', 'slope', 'intercept', 'epsilon', 'atMachineEpsilon'))
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
- coefs = spline.coefs
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
- for yData, yBounds in zip(coefs, bounds):
250
- # Compute the 2D convex hull of the knot coefficients and the spline's coefficients
251
- hull = _convex_hull_2D(xData, yData.ravel(), yBounds, epsilon)
252
- if hull is None:
253
- return roots, intervals
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
- # Intersect the convex hull with the xInterval along the x axis (the knot coefficients axis).
256
- xInterval = _intersect_convex_hull_with_x_interval(hull, epsilon, xInterval)
257
- if xInterval is None:
258
- return roots, intervals
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, spline.knots[0].dtype).T
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 unknowns.
284
- if interval.atMachineEpsilon or len(newUnknowns) == 0:
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
- spline = spline.contract(uvw)
291
-
292
- # Use interval newton for one-dimensional splines.
293
- if spline.nInd == 1 and spline.nDep == 1:
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 zip(range(spline.nInd), width):
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
- intervals.append(Interval(spline.trim(domain.T).reparametrize(((0.0, 1.0),) * spline.nInd), newUnknowns, newScale, splitSlope, splitIntercept, epsilon, np.dot(splitSlope, splitSlope) < machineEpsilon))
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 not(self.nInd == self.nDep): raise ValueError("The number of independent variables (nInd) must match the number of dependent variables (nDep).")
343
- machineEpsilon = np.finfo(self.knots[0].dtype).eps
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 spline, domain, and interval.
417
+ # Set initial interval.
351
418
  domain = self.domain().T
352
- intervals = [Interval(self.trim(domain.T).reparametrize(((0.0, 1.0),) * self.nInd), [*range(self.nInd)], 1.0, domain[1] - domain[0], domain[0], epsilon, False)]
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
- if np.linalg.norm(self(rootCenter)) >= evaluationEpsilon:
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 contours(self):
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
- epsilon = np.sqrt(np.finfo(self.coefs.dtype).eps)
424
- evaluationEpsilon = np.sqrt(epsilon)
425
-
426
- # Go through each nDep of the spline, checking bounds.
427
- for coefs in self.coefs:
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 tangents and normal.
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
- theta = np.sqrt(2) # Arbitrary starting value for theta (picked one in [0, pi/2] unlikely to be a stationary point)
445
- # Try different theta values until no border or turning points are degenerate or we run out of attempts.
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
- det = (0.5 - boundary) * normal(uvw)[nInd] * turningPointDeterminant(uvw)
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](uvw)
586
+ columns[:, i] = tangents[j]
499
587
  i += 1
500
- duv = np.linalg.solve(columns, -tangents[nInd](uvw))
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
- systemSelf, systemTurningPointDeterminant = bspy.Spline.common_basis((self, turningPointDeterminant))
519
- system = type(systemSelf)(self.nInd, self.nInd, systemSelf.order, systemSelf.nCoef, systemSelf.knots, \
520
- np.concatenate((systemSelf.coefs, systemTurningPointDeterminant.coefs)), systemSelf.metadata)
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 attempts <= 0: raise ValueError("No contours. Degenerate equations.")
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.empty((self.nDep + 1, *self.coefs.shape[1:]), self.coefs.dtype) # Note that self.nDep + 1 == self.nInd
555
- panelCoefs[:self.nDep] = self.coefs
556
- # The following value should be -d. We're setting it for d = 0 to start.
557
- panelCoefs[self.nDep, 0, 0] = 0.0
558
- degree = self.order[0] - 1
559
- for i in range(1, self.nCoef[0]):
560
- panelCoefs[self.nDep, i, 0] = panelCoefs[self.nDep, i - 1, 0] + ((self.knots[0][degree + i] - self.knots[0][i]) / degree) * cosTheta
561
- degree = self.order[1] - 1
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
- panel.coefs[self.nDep] -= point.d
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.spline.Spline.contour(self, points)
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-Spline intersection.
787
- if isinstance(other, bspy.Spline):
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.subtract(other)
941
+ spline = self.dot(other._normal) - np.atleast_1d(np.dot(other._normal, other._point))
790
942
 
791
- # Curve-Curve intersection.
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.boundaries.append(Boundary(Hyperplane(-1.0, zero[0][0], 0.0), Solid(0, True)))
819
- left.boundaries.append(Boundary(Hyperplane(1.0, zero[1][0], 0.0), Solid(0, True)))
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.boundaries.append(Boundary(Hyperplane(-1.0, zero[0][1], 0.0), Solid(0, True)))
822
- right.boundaries.append(Boundary(Hyperplane(1.0, zero[1][1], 0.0), Solid(0, True)))
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
- #logging.info(f"intersect_manifold({self.metadata['Name']}, {other.metadata['Name']})")
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 = spline.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
- swap = True
844
-
845
- # Convert each contour into a Manifold.Crossing.
846
- if swap:
847
- spline = other.subtract(self)
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
- if np.dot(self.tangent_space(intersection.left.point(domainPoint)) @ intersection.left.normal(domainPoint), other.normal(intersection.right.point(domainPoint))) < 0.0:
873
- intersection.left.flip_normal()
874
- if np.dot(other.tangent_space(intersection.right.point(domainPoint)) @ intersection.right.normal(domainPoint), self.normal(intersection.left.point(domainPoint))) < 0.0:
875
- intersection.right.flip_normal()
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
- return intersections
1188
+ def full_domain(self):
1189
+ return Hyperplane.create_hypercube(self.domain())