bspy 3.0.1__py3-none-any.whl → 4.1__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,13 +1,17 @@
1
+ import logging
1
2
  import math
2
3
  import numpy as np
4
+ from bspy.manifold import Manifold
5
+ from bspy.hyperplane import Hyperplane
3
6
  import bspy.spline
7
+ from bspy.solid import Solid, Boundary
4
8
  from collections import namedtuple
5
9
  from multiprocessing import Pool
6
10
 
7
11
  def zeros_using_interval_newton(self):
8
12
  if not(self.nInd == self.nDep): raise ValueError("The number of independent variables (nInd) must match the number of dependent variables (nDep).")
9
13
  if not(self.nInd == 1): raise ValueError("Only works for curves (nInd == 1).")
10
- epsilon = np.finfo(self.coefs.dtype).eps
14
+ epsilon = np.finfo(self.knots[0].dtype).eps
11
15
 
12
16
  # Set initial spline and domain
13
17
 
@@ -40,23 +44,25 @@ def zeros_using_interval_newton(self):
40
44
  (myDomain,) = scaledSpline.domain()
41
45
  intervalSize *= myDomain[1] - myDomain[0]
42
46
  functionMax *= scaleFactor
43
- mySpline = scaledSpline.reparametrize([[0.0, 1.0]])
44
- midPoint = 0.5
45
- [functionValue] = mySpline([midPoint])
47
+ midPoint = 0.5 * (myDomain[0] + myDomain[1])
48
+ [functionValue] = scaledSpline(midPoint)
46
49
 
47
50
  # Root found
48
51
 
49
52
  if intervalSize < epsilon or abs(functionValue) * functionMax < epsilon:
50
53
  if intervalSize < epsilon ** 0.25:
51
- return [0.5 * (myDomain[0] + myDomain[1])]
54
+ return [midPoint]
52
55
  else:
53
- myZeros = refine(mySpline.trim(((0.0, midPoint - np.sqrt(epsilon)),)), intervalSize, functionMax)
54
- myZeros += [0.5 * (myDomain[0] + myDomain[1])]
55
- myZeros += refine(mySpline.trim(((midPoint + np.sqrt(epsilon), 1.0),)), intervalSize, functionMax)
56
+ mySpline = scaledSpline.reparametrize([[0.0, 1.0]])
57
+ myZeros = refine(mySpline.trim(((0.0, 0.5 - np.sqrt(epsilon)),)), intervalSize, functionMax)
58
+ myZeros.append(midPoint)
59
+ myZeros += refine(mySpline.trim(((0.5 + np.sqrt(epsilon), 1.0),)), intervalSize, functionMax)
56
60
  return myZeros
57
61
 
58
62
  # Calculate Newton update
59
63
 
64
+ mySpline = scaledSpline.reparametrize([[0.0, 1.0]])
65
+ midPoint = 0.5
60
66
  (derivativeBounds,) = mySpline.differentiate().range_bounds()
61
67
  if derivativeBounds[0] == 0.0:
62
68
  derivativeBounds[0] = epsilon
@@ -111,34 +117,24 @@ def zeros_using_interval_newton(self):
111
117
  return mySolution
112
118
  return refine(spline, 1.0, 1.0)
113
119
 
114
- def _convex_hull_2D(xData, yData, epsilon = 1.0e-8, evaluationEpsilon = 1.0e-4, xInterval = None):
120
+ def _convex_hull_2D(xData, yData, yBounds, epsilon = 1.0e-8):
115
121
  # Allow xData to be repeated for longer yData, but only if yData is a multiple.
116
- if not(len(yData) % len(xData) == 0): raise ValueError("Size of xData does not divide evenly in size of yData")
122
+ if not(yData.shape[0] % xData.shape[0] == 0): raise ValueError("Size of xData does not divide evenly in size of yData")
117
123
 
118
- # Assign p0 to the leftmost lowest point. Also compute xMin, xMax, and yMax.
119
- xMin = xMax = x0 = xData[0]
120
- yMax = y0 = yData[0]
121
- xIter = iter(xData[1:])
122
- for y in yData[1:]:
123
- x = next(xIter, None)
124
- if x is None:
125
- xIter = iter(xData)
126
- x = next(xIter)
127
-
128
- if y < y0 or (y == y0 and x < x0):
129
- (x0, y0) = (x, y)
130
- xMin = min(xMin, x)
131
- xMax = max(xMax, x)
132
- yMax = max(yMax, y)
133
-
134
- # Only return convex null if it contains y = 0 and x within xInterval.
135
- if xInterval is not None and (y0 > evaluationEpsilon or yMax < -evaluationEpsilon or xMin > xInterval[1] or xMax < xInterval[0]):
136
- return None
124
+ # Assign (x0, y0) to the lowest point.
125
+ yMinIndex = np.argmin(yData)
126
+ x0 = xData[yMinIndex % xData.shape[0]]
127
+ y0 = yData[yMinIndex]
128
+
129
+ # Calculate y adjustment as needed for values close to zero
130
+ yAdjustment = -yBounds[0] if yBounds[0] > 0.0 else -yBounds[1] if yBounds[1] < 0.0 else 0.0
131
+ y0 += yAdjustment
137
132
 
138
133
  # Sort points by angle around p0.
139
134
  sortedPoints = []
140
135
  xIter = iter(xData)
141
136
  for y in yData:
137
+ y += yAdjustment
142
138
  x = next(xIter, None)
143
139
  if x is None:
144
140
  xIter = iter(xData)
@@ -146,7 +142,7 @@ def _convex_hull_2D(xData, yData, epsilon = 1.0e-8, evaluationEpsilon = 1.0e-4,
146
142
  sortedPoints.append((math.atan2(y - y0, x - x0), x, y))
147
143
  sortedPoints.sort()
148
144
 
149
- # Trim away points with the same angle (keep furthest point from p0), and then remove angle.
145
+ # Trim away points with the same angle (keep furthest point from p0), removing the angle from the list.
150
146
  trimmedPoints = [sortedPoints[0][1:]] # Ensure we keep the first point
151
147
  previousPoint = None
152
148
  previousDistance = -1.0
@@ -212,50 +208,49 @@ def _refine_projected_polyhedron(interval):
212
208
 
213
209
  # Remove dependent variables that are near zero and compute newScale.
214
210
  spline = interval.spline.copy()
215
- coefs = spline.coefs
216
- newScale = 0.0
217
- nDep = 0
218
- while nDep < len(coefs):
219
- coefsMin = coefs[nDep].min() * interval.scale
220
- coefsMax = coefs[nDep].max() * interval.scale
211
+ bounds = spline.range_bounds()
212
+ keepDep = []
213
+ for nDep, (coefsMin, coefsMax) in enumerate(bounds * interval.scale):
221
214
  if coefsMax < -epsilon or coefsMin > epsilon:
222
215
  # No roots in this interval.
223
216
  return roots, intervals
224
- if -epsilon < coefsMin and coefsMax < epsilon:
225
- # Near zero along this axis for entire interval.
226
- coefs = np.delete(coefs, nDep, axis = 0)
227
- else:
228
- nDep += 1
229
- newScale = max(newScale, abs(coefsMin), abs(coefsMax))
217
+ if coefsMin < -epsilon or coefsMax > epsilon:
218
+ # Dependent variable not near zero for entire interval.
219
+ keepDep.append(nDep)
230
220
 
231
- if nDep == 0:
221
+ spline.nDep = len(keepDep)
222
+ if spline.nDep == 0:
232
223
  # Return the interval center and radius.
233
224
  roots.append((interval.intercept + 0.5 * interval.slope, 0.5 * np.linalg.norm(interval.slope)))
234
225
  return roots, intervals
235
226
 
236
- # Rescale the spline to max 1.0.
237
- spline.nDep = nDep
238
- coefs *= interval.scale / newScale
239
- spline.coefs = coefs
227
+ # Rescale remaining spline coefficients to max 1.0.
228
+ bounds = bounds[keepDep]
229
+ newScale = np.abs(bounds).max()
230
+ spline.coefs = spline.coefs[keepDep]
231
+ spline.coefs *= 1.0 / newScale
232
+ bounds *= 1.0 / newScale
233
+ newScale *= interval.scale
240
234
 
241
235
  # Loop through each independent variable to determine a tighter domain around roots.
242
236
  domain = []
237
+ coefs = spline.coefs
243
238
  for nInd, order, knots, nCoef, s in zip(range(spline.nInd), spline.order, spline.knots, spline.nCoef, interval.slope):
244
239
  # Move independent variable to the last (fastest) axis, adding 1 to account for the dependent variables.
245
240
  coefs = np.moveaxis(spline.coefs, nInd + 1, -1)
246
241
 
247
242
  # Compute the coefficients for f(x) = x for the independent variable and its knots.
248
243
  degree = order - 1
249
- knotCoefs = np.empty((nCoef,), knots.dtype)
250
- knotCoefs[0] = knots[1]
244
+ xData = np.empty((nCoef,), knots.dtype)
245
+ xData[0] = knots[1]
251
246
  for i in range(1, nCoef):
252
- knotCoefs[i] = knotCoefs[i - 1] + (knots[i + degree] - knots[i])/degree
247
+ xData[i] = xData[i - 1] + (knots[i + degree] - knots[i])/degree
253
248
 
254
249
  # Loop through each dependent variable to compute the interval containing the root for this independent variable.
255
250
  xInterval = (0.0, 1.0)
256
- for nDep in range(spline.nDep):
251
+ for yData, yBounds in zip(coefs, bounds):
257
252
  # Compute the 2D convex hull of the knot coefficients and the spline's coefficients
258
- hull = _convex_hull_2D(knotCoefs, coefs[nDep].ravel(), epsilon, evaluationEpsilon, xInterval)
253
+ hull = _convex_hull_2D(xData, yData.ravel(), yBounds, epsilon)
259
254
  if hull is None:
260
255
  return roots, intervals
261
256
 
@@ -267,7 +262,7 @@ def _refine_projected_polyhedron(interval):
267
262
  domain.append(xInterval)
268
263
 
269
264
  # Compute new slope, intercept, and unknowns.
270
- domain = np.array(domain).T
265
+ domain = np.array(domain, spline.knots[0].dtype).T
271
266
  width = domain[1] - domain[0]
272
267
  newSlope = interval.slope.copy()
273
268
  newIntercept = interval.intercept.copy()
@@ -347,7 +342,7 @@ class _Region:
347
342
 
348
343
  def zeros_using_projected_polyhedron(self, epsilon=None):
349
344
  if not(self.nInd == self.nDep): raise ValueError("The number of independent variables (nInd) must match the number of dependent variables (nDep).")
350
- machineEpsilon = np.finfo(self.coefs.dtype).eps
345
+ machineEpsilon = np.finfo(self.knots[0].dtype).eps
351
346
  if epsilon is None:
352
347
  epsilon = 0.0
353
348
  epsilon = max(epsilon, np.sqrt(machineEpsilon))
@@ -545,8 +540,10 @@ def contours(self):
545
540
  if not abort:
546
541
  break # We're done!
547
542
 
548
- if attempts <= 0:
549
- raise ValueError("No contours. Degenerate equations.")
543
+ if attempts <= 0: raise ValueError("No contours. Degenerate equations.")
544
+
545
+ if not points:
546
+ return [] # No contours
550
547
 
551
548
  # We've got all the contour points, now we bucket them into individual contours using the algorithm
552
549
  # from Grandine, Thomas A., and Frederick W. Klein IV. "A new approach to the surface intersection problem."
@@ -577,6 +574,21 @@ def contours(self):
577
574
  # (3) Take all the points found in Step (1) and Step (2) and order them by distance in the theta direction from the origin.
578
575
  points.sort()
579
576
 
577
+ # Extra step not in paper.
578
+ # Run a checksum on the points, ensuring starting and ending points balance.
579
+ # Start by flipping endpoints as needed, since we can miss turning points near endpoints.
580
+
581
+ if points[0].det < 0.0:
582
+ point = points[0]
583
+ points[0] = Point(point.d, -point.det, point.onUVBoundary, point.turningPoint, point.uvw)
584
+ if points[-1].det > 0.0:
585
+ point = points[-1]
586
+ points[-1] = Point(point.d, -point.det, point.onUVBoundary, point.turningPoint, point.uvw)
587
+ checksum = 0
588
+ for i, point in enumerate(points): # Ensure checksum stays non-negative front to back
589
+ checksum += (1 if point.det > 0 else -1) * (2 if point.turningPoint else 1)
590
+ if checksum != 0: raise ValueError("No contours. Inconsistent contour topology.")
591
+
580
592
  # Extra step not in the paper:
581
593
  # Add a panel between two consecutive open/close turning points to uniquely determine contours between them.
582
594
  if len(points) > 1:
@@ -746,6 +758,7 @@ def contours(self):
746
758
  currentContourPoints[index] = [upperConnection] + upperHalf + [point.uvw] + lowerHalf + currentContourPoints[index][2:]
747
759
  else:
748
760
  # It's an ending point on an other boundary (same steps as uv boundary).
761
+ adjustment = -1
749
762
  fullList = currentContourPoints.pop(i) + [point.uvw]
750
763
  connection = fullList.pop(0)
751
764
  if connection == 0:
@@ -758,12 +771,272 @@ def contours(self):
758
771
  currentContourPoints[i + adjustment].append(uvw)
759
772
 
760
773
  # We've determined a bunch of points along all the contours, including starting and ending points.
761
- # Now we just need to create splines for those contours using the Spline.contour method.
774
+ # Now we just need to create splines for those contours using the bspy.Spline.contour method.
762
775
  splineContours = []
763
776
  for points in contourPoints:
764
- contour = bspy.spline.Spline.contour(self, points)
777
+ contour = bspy.Spline.contour(self, points)
765
778
  # Transform the contour to self's original domain.
766
779
  contour.coefs = (contour.coefs.T * (domain[1] - domain[0]) + domain[0]).T
767
780
  splineContours.append(contour)
768
781
 
769
- return splineContours
782
+ return splineContours
783
+
784
+ def intersect(self, other):
785
+ intersections = []
786
+ nDep = self.nInd # The dimension of the intersection's range
787
+
788
+ # Spline-Hyperplane intersection.
789
+ if isinstance(other, Hyperplane):
790
+ # Compute the projection onto the hyperplane to map Spline-Hyperplane intersection points to the domain of the Hyperplane.
791
+ projection = np.linalg.inv(other._tangentSpace.T @ other._tangentSpace) @ other._tangentSpace.T
792
+ # Construct a new spline that represents the intersection.
793
+ spline = self.dot(other._normal) - np.atleast_1d(np.dot(other._normal, other._point))
794
+
795
+ # Curve-Line intersection.
796
+ if nDep == 1:
797
+ # Find the intersection points and intervals.
798
+ zeros = spline.zeros()
799
+ # Convert each intersection point into a Manifold.Crossing and each intersection interval into a Manifold.Coincidence.
800
+ for zero in zeros:
801
+ if isinstance(zero, tuple):
802
+ # Intersection is an interval, so create a Manifold.Coincidence.
803
+ planeBounds = (projection @ (self((zero[0],)) - other._point), projection @ (self((zero[1],)) - other._point))
804
+
805
+ # First, check for crossings at the boundaries of the coincidence, since splines can have discontinuous tangents.
806
+ # We do this first because later we may change the order of the plane bounds.
807
+ (bounds,) = self.domain()
808
+ epsilon = 0.1 * Manifold.minSeparation
809
+ if zero[0] - epsilon > bounds[0]:
810
+ intersections.append(Manifold.Crossing(Hyperplane(1.0, zero[0] - epsilon, 0.0), Hyperplane(1.0, planeBounds[0], 0.0)))
811
+ if zero[1] + epsilon < bounds[1]:
812
+ intersections.append(Manifold.Crossing(Hyperplane(1.0, zero[1] + epsilon, 0.0), Hyperplane(1.0, planeBounds[1], 0.0)))
813
+
814
+ # Now, create the coincidence.
815
+ left = Solid(nDep, False)
816
+ left.add_boundary(Boundary(Hyperplane(-1.0, zero[0], 0.0), Solid(0, True)))
817
+ left.add_boundary(Boundary(Hyperplane(1.0, zero[1], 0.0), Solid(0, True)))
818
+ right = Solid(nDep, False)
819
+ if planeBounds[0] > planeBounds[1]:
820
+ planeBounds = (planeBounds[1], planeBounds[0])
821
+ right.add_boundary(Boundary(Hyperplane(-1.0, planeBounds[0], 0.0), Solid(0, True)))
822
+ right.add_boundary(Boundary(Hyperplane(1.0, planeBounds[1], 0.0), Solid(0, True)))
823
+ alignment = np.dot(self.normal((zero[0],)), other._normal) # Use the first zero, since B-splines are closed on the left
824
+ width = zero[1] - zero[0]
825
+ transform = (planeBounds[1] - planeBounds[0]) / width
826
+ translation = (planeBounds[0] * zero[1] - planeBounds[1] * zero[0]) / width
827
+ intersections.append(Manifold.Coincidence(left, right, alignment, np.atleast_2d(transform), np.atleast_2d(1.0 / transform), np.atleast_1d(translation)))
828
+ else:
829
+ # Intersection is a point, so create a Manifold.Crossing.
830
+ intersections.append(Manifold.Crossing(Hyperplane(1.0, zero, 0.0), Hyperplane(1.0, projection @ (self((zero,)) - other._point), 0.0)))
831
+
832
+ # Surface-Plane intersection.
833
+ elif nDep == 2:
834
+ # Find the intersection contours, which are returned as splines.
835
+ contours = spline.contours()
836
+ # Convert each contour into a Manifold.Crossing.
837
+ for contour in contours:
838
+ # The left portion is the contour returned for the spline-plane intersection.
839
+ left = contour
840
+ # The right portion is the contour projected onto the plane's domain, which we compute with samples and a least squares fit.
841
+ tValues = np.linspace(0.0, 1.0, contour.nCoef[0] + 5) # Over-sample a bit to reduce the condition number and avoid singular matrix
842
+ points = []
843
+ for t in tValues:
844
+ zero = contour((t,))
845
+ points.append(projection @ (self(zero) - other._point))
846
+ right = bspy.Spline.least_squares(tValues, np.array(points).T, contour.order, contour.knots)
847
+ intersections.append(Manifold.Crossing(left, right))
848
+ else:
849
+ return NotImplemented
850
+
851
+ # Spline-Spline intersection.
852
+ elif isinstance(other, bspy.Spline):
853
+ # Construct a new spline that represents the intersection.
854
+ spline = self.subtract(other)
855
+
856
+ # Curve-Curve intersection.
857
+ if nDep == 1:
858
+ # Find the intersection points and intervals.
859
+ zeros = spline.zeros()
860
+ # Convert each intersection point into a Manifold.Crossing and each intersection interval into a Manifold.Coincidence.
861
+ for zero in zeros:
862
+ if isinstance(zero, tuple):
863
+ # Intersection is an interval, so create a Manifold.Coincidence.
864
+
865
+ # First, check for crossings at the boundaries of the coincidence, since splines can have discontinuous tangents.
866
+ # We do this first to match the approach for Curve-Line intersections.
867
+ (boundsSelf,) = self.domain()
868
+ (boundsOther,) = other.domain()
869
+ epsilon = 0.1 * Manifold.minSeparation
870
+ if zero[0][0] - epsilon > boundsSelf[0]:
871
+ intersections.append(Manifold.Crossing(Hyperplane(1.0, zero[0][0] - epsilon, 0.0), Hyperplane(1.0, zero[0][1], 0.0)))
872
+ elif zero[0][1] - epsilon > boundsOther[0]:
873
+ intersections.append(Manifold.Crossing(Hyperplane(1.0, zero[0][0], 0.0), Hyperplane(1.0, zero[0][1] - epsilon, 0.0)))
874
+ if zero[1][0] + epsilon < boundsSelf[1]:
875
+ intersections.append(Manifold.Crossing(Hyperplane(1.0, zero[1][0] + epsilon, 0.0), Hyperplane(1.0, zero[1][1], 0.0)))
876
+ elif zero[1][1] + epsilon < boundsOther[1]:
877
+ intersections.append(Manifold.Crossing(Hyperplane(1.0, zero[1][0], 0.0), Hyperplane(1.0, zero[1][1] + epsilon, 0.0)))
878
+
879
+ # Now, create the coincidence.
880
+ left = Solid(nDep, False)
881
+ left.add_boundary(Boundary(Hyperplane(-1.0, zero[0][0], 0.0), Solid(0, True)))
882
+ left.add_boundary(Boundary(Hyperplane(1.0, zero[1][0], 0.0), Solid(0, True)))
883
+ right = Solid(nDep, False)
884
+ right.add_boundary(Boundary(Hyperplane(-1.0, zero[0][1], 0.0), Solid(0, True)))
885
+ right.add_boundary(Boundary(Hyperplane(1.0, zero[1][1], 0.0), Solid(0, True)))
886
+ 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
887
+ width = zero[1][0] - zero[0][0]
888
+ transform = (zero[1][1] - zero[0][1]) / width
889
+ translation = (zero[0][1] * zero[1][0] - zero[1][1] * zero[0][0]) / width
890
+ intersections.append(Manifold.Coincidence(left, right, alignment, np.atleast_2d(transform), np.atleast_2d(1.0 / transform), np.atleast_1d(translation)))
891
+ else:
892
+ # Intersection is a point, so create a Manifold.Crossing.
893
+ intersections.append(Manifold.Crossing(Hyperplane(1.0, zero[:nDep], 0.0), Hyperplane(1.0, zero[nDep:], 0.0)))
894
+
895
+ # Surface-Surface intersection.
896
+ elif nDep == 2:
897
+ if "Name" in self.metadata and "Name" in other.metadata:
898
+ logging.info(f"intersect({self.metadata['Name']}, {other.metadata['Name']})")
899
+ # Find the intersection contours, which are returned as splines.
900
+ swap = False
901
+ try:
902
+ # First try the intersection as is.
903
+ contours = spline.contours()
904
+ except ValueError:
905
+ # If that fails, swap the manifolds. Worth a shot since intersections are touchy.
906
+ swap = True
907
+
908
+ # Convert each contour into a Manifold.Crossing.
909
+ if swap:
910
+ spline = other.subtract(self)
911
+ if "Name" in self.metadata and "Name" in other.metadata:
912
+ logging.info(f"intersect({other.metadata['Name']}, {self.metadata['Name']})")
913
+ contours = spline.contours()
914
+ for contour in contours:
915
+ # Swap left and right, compared to not swapped.
916
+ left = bspy.Spline(contour.nInd, nDep, contour.order, contour.nCoef, contour.knots, contour.coefs[nDep:], contour.metadata)
917
+ right = bspy.Spline(contour.nInd, nDep, contour.order, contour.nCoef, contour.knots, contour.coefs[:nDep], contour.metadata)
918
+ intersections.append(Manifold.Crossing(left, right))
919
+ else:
920
+ for contour in contours:
921
+ left = bspy.Spline(contour.nInd, nDep, contour.order, contour.nCoef, contour.knots, contour.coefs[:nDep], contour.metadata)
922
+ right = bspy.Spline(contour.nInd, nDep, contour.order, contour.nCoef, contour.knots, contour.coefs[nDep:], contour.metadata)
923
+ intersections.append(Manifold.Crossing(left, right))
924
+ else:
925
+ return NotImplemented
926
+ else:
927
+ return NotImplemented
928
+
929
+ # Ensure the normals point outwards for both Manifolds in each crossing intersection.
930
+ # 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.
931
+ domainPoint = np.atleast_1d(0.5)
932
+ for i, intersection in enumerate(intersections):
933
+ if isinstance(intersection, Manifold.Crossing):
934
+ left = intersection.left
935
+ right = intersection.right
936
+ if np.dot(self.tangent_space(left.evaluate(domainPoint)) @ left.normal(domainPoint), other.normal(right.evaluate(domainPoint))) < 0.0:
937
+ left = left.flip_normal()
938
+ if np.dot(other.tangent_space(right.evaluate(domainPoint)) @ right.normal(domainPoint), self.normal(left.evaluate(domainPoint))) < 0.0:
939
+ right = right.flip_normal()
940
+ intersections[i] = Manifold.Crossing(left, right)
941
+
942
+ return intersections
943
+
944
+ def complete_slice(self, slice, solid):
945
+ # Spline manifold domains have finite bounds.
946
+ slice.containsInfinity = False
947
+ bounds = self.domain()
948
+
949
+ # If manifold (self) has no intersections with solid, just check containment.
950
+ if not slice.boundaries:
951
+ if slice.dimension == 2:
952
+ if "Name" in self.metadata:
953
+ logging.info(f"check containment: {self.metadata['Name']}")
954
+ domain = bounds.T
955
+ if solid.contains_point(self(0.5 * (domain[0] + domain[1]))):
956
+ for boundary in Hyperplane.create_hypercube(bounds).boundaries:
957
+ slice.add_boundary(boundary)
958
+ return
959
+
960
+ # For curves, add domain bounds as needed.
961
+ if slice.dimension == 1:
962
+ slice.boundaries.sort(key=lambda b: (b.manifold.evaluate(0.0), b.manifold.normal(0.0)))
963
+ # First, check right end since we add new boundary to the end.
964
+ if abs(slice.boundaries[-1].manifold._point - bounds[0][1]) >= Manifold.minSeparation and \
965
+ slice.boundaries[-1].manifold._normal < 0.0:
966
+ slice.add_boundary(Boundary(Hyperplane(-slice.boundaries[-1].manifold._normal, bounds[0][1], 0.0), Solid(0, True)))
967
+ # Next, check left end since it's still untouched.
968
+ if abs(slice.boundaries[0].manifold._point - bounds[0][0]) >= Manifold.minSeparation and \
969
+ slice.boundaries[0].manifold._normal > 0.0:
970
+ slice.add_boundary(Boundary(Hyperplane(-slice.boundaries[0].manifold._normal, bounds[0][0], 0.0), Solid(0, True)))
971
+
972
+ # For surfaces, intersect full spline domain with existing slice boundaries.
973
+ if slice.dimension == 2:
974
+ fullDomain = Hyperplane.create_hypercube(bounds)
975
+ for newBoundary in fullDomain.boundaries: # Mark full domain boundaries as untouched
976
+ newBoundary.touched = False
977
+
978
+ # Define function for adding slice points to full domain boundaries.
979
+ def process_domain_point(boundary, domainPoint):
980
+ point = boundary.manifold.evaluate(domainPoint)
981
+ # See if and where point touches full domain.
982
+ for newBoundary in fullDomain.boundaries:
983
+ vector = point - newBoundary.manifold._point
984
+ if abs(np.dot(newBoundary.manifold._normal, vector)) < Manifold.minSeparation:
985
+ # Add the point onto the new boundary.
986
+ normal = np.sign(newBoundary.manifold._tangentSpace.T @ boundary.manifold.normal(domainPoint))
987
+ newBoundary.domain.add_boundary(Boundary(Hyperplane(normal, newBoundary.manifold._tangentSpace.T @ vector, 0.0), Solid(0, True)))
988
+ newBoundary.touched = True
989
+ break
990
+
991
+ # Go through existing boundaries and check if either of their endpoints lies on the spline's bounds.
992
+ for boundary in slice.boundaries:
993
+ domainBoundaries = boundary.domain.boundaries
994
+ domainBoundaries.sort(key=lambda boundary: (boundary.manifold.evaluate(0.0), boundary.manifold.normal(0.0)))
995
+ process_domain_point(boundary, domainBoundaries[0].manifold._point)
996
+ if len(domainBoundaries) > 1:
997
+ process_domain_point(boundary, domainBoundaries[-1].manifold._point)
998
+
999
+ # For touched boundaries, remove domain bounds that aren't needed and then add boundary to slice.
1000
+ boundaryWasTouched = False
1001
+ for newBoundary in fullDomain.boundaries:
1002
+ if newBoundary.touched:
1003
+ boundaryWasTouched = True
1004
+ domainBoundaries = newBoundary.domain.boundaries
1005
+ assert len(domainBoundaries) > 2
1006
+ domainBoundaries.sort(key=lambda boundary: (boundary.manifold.evaluate(0.0), boundary.manifold.normal(0.0)))
1007
+ # Ensure domain endpoints don't overlap and their normals are consistent.
1008
+ if abs(domainBoundaries[0].manifold._point - domainBoundaries[1].manifold._point) < Manifold.minSeparation or \
1009
+ domainBoundaries[1].manifold._normal < 0.0:
1010
+ del domainBoundaries[0]
1011
+ if abs(domainBoundaries[-1].manifold._point - domainBoundaries[-2].manifold._point) < Manifold.minSeparation or \
1012
+ domainBoundaries[-2].manifold._normal > 0.0:
1013
+ del domainBoundaries[-1]
1014
+ slice.add_boundary(newBoundary)
1015
+
1016
+ if boundaryWasTouched:
1017
+ # Touch untouched boundaries that are connected to touched boundary endpoints and add them to slice.
1018
+ boundaryMap = ((2, 3, 0), (2, 3, -1), (0, 1, 0), (0, 1, -1)) # Map of which full domain boundaries touch each other
1019
+ while True:
1020
+ noTouches = True
1021
+ for map, newBoundary, bound in zip(boundaryMap, fullDomain.boundaries, bounds.flatten()):
1022
+ if not newBoundary.touched:
1023
+ leftBoundary = fullDomain.boundaries[map[0]]
1024
+ rightBoundary = fullDomain.boundaries[map[1]]
1025
+ if leftBoundary.touched and abs(leftBoundary.domain.boundaries[map[2]].manifold._point - bound) < Manifold.minSeparation:
1026
+ newBoundary.touched = True
1027
+ slice.add_boundary(newBoundary)
1028
+ noTouches = False
1029
+ elif rightBoundary.touched and abs(rightBoundary.domain.boundaries[map[2]].manifold._point - bound) < Manifold.minSeparation:
1030
+ newBoundary.touched = True
1031
+ slice.add_boundary(newBoundary)
1032
+ noTouches = False
1033
+ if noTouches:
1034
+ break
1035
+ else:
1036
+ # No slice boundaries touched the full domain (a hole), so only add full domain if it is contained in the solid.
1037
+ if solid.contains_point(self.evaluate(bounds[:,0])):
1038
+ for newBoundary in fullDomain.boundaries:
1039
+ slice.add_boundary(newBoundary)
1040
+
1041
+ def full_domain(self):
1042
+ return Hyperplane.create_hypercube(self.domain())