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.
- bspy/__init__.py +16 -9
- bspy/_spline_domain.py +83 -47
- bspy/_spline_evaluation.py +44 -62
- bspy/_spline_fitting.py +353 -75
- bspy/_spline_intersection.py +332 -59
- bspy/_spline_operations.py +33 -38
- bspy/hyperplane.py +540 -0
- bspy/manifold.py +391 -0
- bspy/solid.py +839 -0
- bspy/spline.py +310 -77
- bspy/splineOpenGLFrame.py +683 -19
- bspy/viewer.py +795 -0
- {bspy-3.0.1.dist-info → bspy-4.1.dist-info}/METADATA +25 -13
- bspy-4.1.dist-info/RECORD +17 -0
- {bspy-3.0.1.dist-info → bspy-4.1.dist-info}/WHEEL +1 -1
- bspy/bspyApp.py +0 -426
- bspy/drawableSpline.py +0 -585
- bspy-3.0.1.dist-info/RECORD +0 -15
- {bspy-3.0.1.dist-info → bspy-4.1.dist-info}/LICENSE +0 -0
- {bspy-3.0.1.dist-info → bspy-4.1.dist-info}/top_level.txt +0 -0
bspy/_spline_intersection.py
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
44
|
-
|
|
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 [
|
|
54
|
+
return [midPoint]
|
|
52
55
|
else:
|
|
53
|
-
|
|
54
|
-
myZeros
|
|
55
|
-
myZeros
|
|
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
|
|
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(
|
|
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
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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),
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
nDep
|
|
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
|
|
225
|
-
#
|
|
226
|
-
|
|
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
|
-
|
|
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
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
250
|
-
|
|
244
|
+
xData = np.empty((nCoef,), knots.dtype)
|
|
245
|
+
xData[0] = knots[1]
|
|
251
246
|
for i in range(1, nCoef):
|
|
252
|
-
|
|
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
|
|
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(
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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())
|