bspy 4.4__tar.gz → 5.0.0__tar.gz

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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bspy
3
- Version: 4.4
3
+ Version: 5.0.0
4
4
  Summary: Library for manipulating and rendering non-uniform B-splines
5
5
  Home-page: http://github.com/ericbrec/BSpy
6
6
  Author: Eric Brechner
@@ -50,7 +50,7 @@ The [Hyperplane](https://ericbrec.github.io/BSpy/bspy/hyperplane.html) class has
50
50
 
51
51
  The [Manifold](https://ericbrec.github.io/BSpy/bspy/manifold.html) abstract base class for [Hyperplane](https://ericbrec.github.io/BSpy/bspy/hyperplane.html) and [Spline](https://ericbrec.github.io/BSpy/bspy/spline.html).
52
52
 
53
- The [Solid](https://ericbrec.github.io/BSpy/bspy/solid.html) class has methods to construct n-dimensional solids from trimmed [Manifold](https://ericbrec.github.io/BSpy/bspy/manifold.html) boundaries. Each solid consists of a list of boundaries and a Boolean value that indicates if the solid contains infinity. Each [Boundary](https://ericbrec.github.io/BSpy/bspy/solid.html) consists of a manifold (currently a [Hyperplane](https://ericbrec.github.io/BSpy/bspy/hyperplane.html) or [Spline](https://ericbrec.github.io/BSpy/bspy/spline.html)) and a domain solid that trims the manifold. Solids have methods to form the intersection, union, difference, and complement of solids. There are methods to compute point containment, winding numbers, surface integrals, and volume integrals. There are also methods to translate, transform, and slice solids. Solids can be saved and loaded in json format.
53
+ The [Solid](https://ericbrec.github.io/BSpy/bspy/solid.html) class has methods to construct n-dimensional solids from trimmed [Manifold](https://ericbrec.github.io/BSpy/bspy/manifold.html) boundaries. Each solid consists of a list of boundaries and a Boolean value that indicates if the solid contains infinity. Each [Boundary](https://ericbrec.github.io/BSpy/bspy/solid.html) consists of a manifold (currently a [Hyperplane](https://ericbrec.github.io/BSpy/bspy/hyperplane.html) or [Spline](https://ericbrec.github.io/BSpy/bspy/spline.html)) and a domain solid that trims the manifold. Solids have methods to form the intersection, union, difference, and complement of solids. There are methods to compute point containment, winding numbers, surface integrals, and volume integrals. There are also methods to translate, transform, and compute cutouts of solids. Solids can be saved and loaded in json format.
54
54
 
55
55
  The [SplineOpenGLFrame](https://ericbrec.github.io/BSpy/bspy/splineOpenGLFrame.html) class is an
56
56
  [OpenGLFrame](https://pypi.org/project/pyopengltk/) with custom shaders to render spline curves, surfaces, and solids. Spline surfaces with more
@@ -86,3 +86,9 @@ a set of examples, including a jupyter notebook, can be found [here](https://git
86
86
  * Moved DrawableSpine methods for adjusting spline appearance to Viewer (see documentation for details)
87
87
  * Spline.bspline_values changed arguments (see documentation for details)
88
88
  * Spline.intersect changed return values (see documentation for details)
89
+
90
+ ### Release 5.0 breaking changes
91
+ * Renamed Boundary.domain member to Boundary.trim
92
+ * Renamed Solid.slice method to Solid.compute_cutout
93
+ * Renamed Manifold.complete_slice method to Manifold.complete_cutout (also applies to Hyperplane and Spline classes)
94
+ * Renamed Manifold.flip_normal method to Manifold.negate_normal (also applies to Hyperplane and Spline classes)
@@ -18,7 +18,7 @@ The [Hyperplane](https://ericbrec.github.io/BSpy/bspy/hyperplane.html) class has
18
18
 
19
19
  The [Manifold](https://ericbrec.github.io/BSpy/bspy/manifold.html) abstract base class for [Hyperplane](https://ericbrec.github.io/BSpy/bspy/hyperplane.html) and [Spline](https://ericbrec.github.io/BSpy/bspy/spline.html).
20
20
 
21
- The [Solid](https://ericbrec.github.io/BSpy/bspy/solid.html) class has methods to construct n-dimensional solids from trimmed [Manifold](https://ericbrec.github.io/BSpy/bspy/manifold.html) boundaries. Each solid consists of a list of boundaries and a Boolean value that indicates if the solid contains infinity. Each [Boundary](https://ericbrec.github.io/BSpy/bspy/solid.html) consists of a manifold (currently a [Hyperplane](https://ericbrec.github.io/BSpy/bspy/hyperplane.html) or [Spline](https://ericbrec.github.io/BSpy/bspy/spline.html)) and a domain solid that trims the manifold. Solids have methods to form the intersection, union, difference, and complement of solids. There are methods to compute point containment, winding numbers, surface integrals, and volume integrals. There are also methods to translate, transform, and slice solids. Solids can be saved and loaded in json format.
21
+ The [Solid](https://ericbrec.github.io/BSpy/bspy/solid.html) class has methods to construct n-dimensional solids from trimmed [Manifold](https://ericbrec.github.io/BSpy/bspy/manifold.html) boundaries. Each solid consists of a list of boundaries and a Boolean value that indicates if the solid contains infinity. Each [Boundary](https://ericbrec.github.io/BSpy/bspy/solid.html) consists of a manifold (currently a [Hyperplane](https://ericbrec.github.io/BSpy/bspy/hyperplane.html) or [Spline](https://ericbrec.github.io/BSpy/bspy/spline.html)) and a domain solid that trims the manifold. Solids have methods to form the intersection, union, difference, and complement of solids. There are methods to compute point containment, winding numbers, surface integrals, and volume integrals. There are also methods to translate, transform, and compute cutouts of solids. Solids can be saved and loaded in json format.
22
22
 
23
23
  The [SplineOpenGLFrame](https://ericbrec.github.io/BSpy/bspy/splineOpenGLFrame.html) class is an
24
24
  [OpenGLFrame](https://pypi.org/project/pyopengltk/) with custom shaders to render spline curves, surfaces, and solids. Spline surfaces with more
@@ -54,3 +54,9 @@ a set of examples, including a jupyter notebook, can be found [here](https://git
54
54
  * Moved DrawableSpine methods for adjusting spline appearance to Viewer (see documentation for details)
55
55
  * Spline.bspline_values changed arguments (see documentation for details)
56
56
  * Spline.intersect changed return values (see documentation for details)
57
+
58
+ ### Release 5.0 breaking changes
59
+ * Renamed Boundary.domain member to Boundary.trim
60
+ * Renamed Solid.slice method to Solid.compute_cutout
61
+ * Renamed Manifold.complete_slice method to Manifold.complete_cutout (also applies to Hyperplane and Spline classes)
62
+ * Renamed Manifold.flip_normal method to Manifold.negate_normal (also applies to Hyperplane and Spline classes)
@@ -1,6 +1,34 @@
1
1
  import numpy as np
2
+ import bspy
2
3
  from bspy.manifold import Manifold
3
4
 
5
+ def arc_length_map(self, tolerance):
6
+ if self.nInd != 1: raise ValueError("Spline doesn't have exactly one independent variable")
7
+
8
+ # Compute the length of the spline
9
+ curveLength = self.integral()
10
+ domain = self.domain()[0]
11
+ guess = bspy.Spline.line([0.0], [1.0])
12
+ guess = guess.elevate([4])
13
+
14
+ # Solve the ODE
15
+ def arcLengthF(t, uData):
16
+ uValue = (1.0 - uData[0][0]) * domain[0] + uData[0][0] * domain[1]
17
+ uValue = np.clip(uValue, domain[0], domain[1])
18
+ d1 = self.derivative([1], [uValue])
19
+ d2 = self.derivative([2], [uValue])
20
+ speed = np.sqrt(d1 @ d1)
21
+ d1d2 = d1 @ d2
22
+ return np.array([curveLength / speed]), np.array([-curveLength * d1d2 / speed ** 3]).reshape((1, 1, 1))
23
+ arcLengthMap = guess.solve_ode(1, 0, arcLengthF, tolerance)
24
+
25
+ # Adjust range to match domain
26
+
27
+ arcLengthMap *= (domain[1] - domain[0]) / arcLengthMap(1.0)[0]
28
+ arcLengthMap += domain[0]
29
+ arcLengthMap.coefs[0][-1] = domain[1]
30
+ return arcLengthMap
31
+
4
32
  def clamp(self, left, right):
5
33
  bounds = [[None, None] for i in range(self.nInd)]
6
34
 
@@ -404,12 +432,11 @@ def join(splineList):
404
432
  splDomain = spl.domain()[0]
405
433
  start2 = spl(splDomain[0])
406
434
  end2 = spl(splDomain[1])
407
- gaps = [np.linalg.norm(vecDiff) for vecDiff in [start1 - start2, start1 - end2, end1 - start2, end1 - end2]]
408
- minDist = min(*gaps)
409
- if minDist == gaps[0] or minDist == gaps[1]:
410
- workingSpline = workingSpline.reverse()
411
- if minDist == gaps[1] or minDist == gaps[3]:
435
+ ixMin = np.argmin([np.linalg.norm(vecDiff) for vecDiff in [end1 - start2, end1 - end2, start1 - start2, start1 - end2]])
436
+ if ixMin == 1 or ixMin == 3:
412
437
  spl = spl.reverse()
438
+ if ixMin == 2 or ixMin == 3:
439
+ workingSpline = workingSpline.reverse()
413
440
  maxOrder = max(workingSpline.order[0], spl.order[0])
414
441
  workingSpline = workingSpline.elevate([maxOrder - workingSpline.order[0]])
415
442
  spl = spl.elevate([maxOrder - spl.order[0]])
@@ -610,6 +637,8 @@ def trim(self, newDomain):
610
637
  if multiplicity > 0:
611
638
  newKnots.append((bounds[0], multiplicity))
612
639
  noChange = False
640
+ if bounds[0] != knots[order - 1]:
641
+ noChange = False
613
642
 
614
643
  if not np.isnan(bounds[1]):
615
644
  if not(knots[order - 1] <= bounds[1] <= knots[-order]): raise ValueError("Invalid newDomain")
@@ -627,6 +656,8 @@ def trim(self, newDomain):
627
656
  if multiplicity > 0:
628
657
  newKnots.append((bounds[1], multiplicity))
629
658
  noChange = False
659
+ if bounds[1] != knots[-order]:
660
+ noChange = False
630
661
 
631
662
  newKnotsList.append(newKnots)
632
663
 
@@ -229,7 +229,7 @@ def normal(self, uvw, normalize=True, indices=None):
229
229
  nDep = self.nDep
230
230
 
231
231
  # Compute the normal using cofactors (determinants of subsets of the tangent space).
232
- sign = -1 if hasattr(self, "metadata") and self.metadata.get("flipNormal", False) else 1
232
+ sign = -1 if hasattr(self, "metadata") and self.metadata.get("negateNormal", False) else 1
233
233
  dtype = self.coefs.dtype if hasattr(self, "coefs") else self.coefsDtype
234
234
  if indices is None:
235
235
  indices = range(nDep)
@@ -339,6 +339,7 @@ def fit(domain, f, order = None, knots = None, tolerance = 1.0e-4):
339
339
  nInd = len(domain)
340
340
  midPoint = f(0.5 * (domain.T[0] + domain.T[1]))
341
341
  if not type(midPoint) is bspy.Spline:
342
+ midPoint = np.array(midPoint).flatten()
342
343
  nDep = len(midPoint)
343
344
 
344
345
  # Make sure order and knots conform to this
@@ -389,7 +390,10 @@ def fit(domain, f, order = None, knots = None, tolerance = 1.0e-4):
389
390
  # Create a tuple for the u value (must be a tuple to use it as a dictionary key)
390
391
  uValue = tuple([uvw[i][indices[i]] for i in range(nInd)])
391
392
  if not uValue in fDictionary:
392
- fDictionary[uValue] = f(np.array(uValue))
393
+ newValue = f(np.array(uValue))
394
+ if not type(newValue) is bspy.Spline:
395
+ newValue = np.array(newValue).flatten()
396
+ fDictionary[uValue] = newValue
393
397
  fValues.append(fDictionary[uValue])
394
398
  iLast = nInd - 1
395
399
  while iLast >= 0:
@@ -1067,7 +1071,7 @@ def solve_ode(self, nLeft, nRight, FAndF_u, tolerance = 1.0e-6, args = (), inclu
1067
1071
 
1068
1072
  # Is it time to give up?
1069
1073
 
1070
- if (not done or continuation < 1.0) and n > 1000:
1074
+ if (not done or continuation < 1.0) and n > 10000:
1071
1075
  raise RuntimeError("Can't find solution with given initial guess")
1072
1076
 
1073
1077
  # Estimate the error
@@ -174,7 +174,6 @@ def _intersect_convex_hull_with_x_interval(lowerHull, upperHull, epsilon, xInter
174
174
  for p1 in hull[1:]:
175
175
  yDelta = p0[1] - p1[1]
176
176
  if p0[1] * p1[1] <= 0.0 and yDelta != 0.0:
177
- yDelta = p0[1] - p1[1]
178
177
  alpha = p0[1] / yDelta
179
178
  xNew = p0[0] * (1.0 - alpha) + p1[0] * alpha
180
179
  if sign * yDelta > 0.0:
@@ -255,14 +254,13 @@ def _refine_projected_polyhedron(interval):
255
254
 
256
255
  # Compute the coefficients for f(x) = x for the independent variable and its knots.
257
256
  xData = spline.greville(ind)
258
-
257
+ if len(xData) == 1:
258
+ xData = spline.domain()[ind]
259
+
259
260
  # Loop through each dependent variable in this row to refine the interval containing the root for this independent variable.
260
- for yData, ySplineBounds, yBounds in zip(coefs, spline.range_bounds(),
261
- interval.bounds[nDep:nDep + spline.nDep]):
261
+ for yData, ySplineBounds, yBounds in zip(coefs, spline.range_bounds(), interval.bounds[nDep:nDep + spline.nDep]):
262
262
  # Compute the 2D convex hull of the knot coefficients and the spline's coefficients
263
263
  lowerHull, upperHull = _convex_hull_2D(xData, yData.ravel(), yBounds, yBounds - ySplineBounds)
264
- if lowerHull is None or upperHull is None:
265
- return roots, intervals
266
264
 
267
265
  # Intersect the convex hull with the xInterval along the x axis (the knot coefficients axis).
268
266
  xInterval = _intersect_convex_hull_with_x_interval(lowerHull, upperHull, epsilon, xInterval)
@@ -437,7 +435,7 @@ def zeros_using_projected_polyhedron(self, epsilon=None, initialScale=None):
437
435
  return roots
438
436
 
439
437
  def _turning_point_determinant(self, uvw, cosTheta, sinTheta):
440
- sign = -1 if hasattr(self, "metadata") and self.metadata.get("flipNormal", False) else 1
438
+ sign = -1 if hasattr(self, "metadata") and self.metadata.get("negateNormal", False) else 1
441
439
  tangentSpace = self.jacobian(uvw).T
442
440
  return cosTheta * sign * np.linalg.det(tangentSpace[[j for j in range(self.nInd) if j != 0]]) - \
443
441
  sinTheta * sign * np.linalg.det(tangentSpace[[j for j in range(self.nInd) if j != 1]])
@@ -446,7 +444,7 @@ def _turning_point_determinant_gradient(self, uvw, cosTheta, sinTheta):
446
444
  dtype = self.coefs.dtype if hasattr(self, "coefs") else self.coefsDtype
447
445
  gradient = np.zeros(self.nInd, dtype)
448
446
 
449
- sign = -1 if hasattr(self, "metadata") and self.metadata.get("flipNormal", False) else 1
447
+ sign = -1 if hasattr(self, "metadata") and self.metadata.get("negateNormal", False) else 1
450
448
  tangentSpace = self.jacobian(uvw).T
451
449
  dTangentSpace = tangentSpace.copy()
452
450
 
@@ -700,7 +698,7 @@ def _contours_of_C1_spline_block(self, epsilon, evaluationEpsilon):
700
698
 
701
699
  # Extra step not in paper.
702
700
  # Run a checksum on the points, ensuring starting and ending points balance.
703
- # Start by flipping endpoints as needed, since we can miss turning points near endpoints.
701
+ # Start by negating endpoints as needed, since we can miss turning points near endpoints.
704
702
  if points[0].det < 0.0:
705
703
  point = points[0]
706
704
  points[0] = Point(point.d, -point.det, point.onUVBoundary, point.turningPoint, point.uvw)
@@ -943,7 +941,8 @@ def contours(self):
943
941
 
944
942
  def intersect(self, other):
945
943
  intersections = []
946
- nDep = self.nInd # The dimension of the intersection's range
944
+ # Compute the number of degrees of freedom of the intersection.
945
+ dof = self.nInd + other.domain_dimension() - self.nDep
947
946
 
948
947
  # Spline-Hyperplane intersection.
949
948
  if isinstance(other, Hyperplane):
@@ -953,7 +952,7 @@ def intersect(self, other):
953
952
  spline = self.dot(other._normal) - np.atleast_1d(np.dot(other._normal, other._point))
954
953
 
955
954
  # Curve-Line intersection.
956
- if nDep == 1:
955
+ if dof == 0:
957
956
  # Find the intersection points and intervals.
958
957
  zeros = spline.zeros()
959
958
  # Convert each intersection point into a Manifold.Crossing and each intersection interval into a Manifold.Coincidence.
@@ -974,39 +973,39 @@ def intersect(self, other):
974
973
  intersections.append(Manifold.Crossing(Hyperplane(1.0, zero[1] + epsilon, 0.0), Hyperplane(1.0, planeBounds[1], 0.0)))
975
974
 
976
975
  # Now, create the coincidence.
977
- left = Solid(nDep, False)
978
- left.add_boundary(Boundary(Hyperplane(-1.0, zero[0], 0.0), Solid(0, True)))
979
- left.add_boundary(Boundary(Hyperplane(1.0, zero[1], 0.0), Solid(0, True)))
980
- right = Solid(nDep, False)
976
+ firstPart = Solid(1, False)
977
+ firstPart.add_boundary(Boundary(Hyperplane(-1.0, zero[0], 0.0), Solid(0, True)))
978
+ firstPart.add_boundary(Boundary(Hyperplane(1.0, zero[1], 0.0), Solid(0, True)))
979
+ secondPart = Solid(1, False)
981
980
  if planeBounds[0] > planeBounds[1]:
982
981
  planeBounds = (planeBounds[1], planeBounds[0])
983
- right.add_boundary(Boundary(Hyperplane(-1.0, planeBounds[0], 0.0), Solid(0, True)))
984
- right.add_boundary(Boundary(Hyperplane(1.0, planeBounds[1], 0.0), Solid(0, True)))
982
+ secondPart.add_boundary(Boundary(Hyperplane(-1.0, planeBounds[0], 0.0), Solid(0, True)))
983
+ secondPart.add_boundary(Boundary(Hyperplane(1.0, planeBounds[1], 0.0), Solid(0, True)))
985
984
  alignment = np.dot(self.normal((zero[0],)), other._normal) # Use the first zero, since B-splines are closed on the left
986
985
  width = zero[1] - zero[0]
987
986
  transform = (planeBounds[1] - planeBounds[0]) / width
988
987
  translation = (planeBounds[0] * zero[1] - planeBounds[1] * zero[0]) / width
989
- intersections.append(Manifold.Coincidence(left, right, alignment, np.atleast_2d(transform), np.atleast_2d(1.0 / transform), np.atleast_1d(translation)))
988
+ intersections.append(Manifold.Coincidence(firstPart, secondPart, alignment, np.atleast_2d(transform), np.atleast_2d(1.0 / transform), np.atleast_1d(translation)))
990
989
  else:
991
990
  # Intersection is a point, so create a Manifold.Crossing.
992
991
  intersections.append(Manifold.Crossing(Hyperplane(1.0, zero, 0.0), Hyperplane(1.0, projection @ (self((zero,)) - other._point), 0.0)))
993
992
 
994
993
  # Surface-Plane intersection.
995
- elif nDep == 2:
994
+ elif dof == 1:
996
995
  # Find the intersection contours, which are returned as splines.
997
996
  contours = spline.contours()
998
997
  # Convert each contour into a Manifold.Crossing.
999
998
  for contour in contours:
1000
- # The left portion is the contour returned for the spline-plane intersection.
1001
- left = contour
1002
- # The right portion is the contour projected onto the plane's domain, which we compute with samples and a least squares fit.
999
+ # The firstPart portion is the contour returned for the spline-plane intersection.
1000
+ firstPart = contour
1001
+ # The secondPart portion is the contour projected onto the plane's domain, which we compute with samples and a least squares fit.
1003
1002
  tValues = np.linspace(0.0, 1.0, contour.nCoef[0] + 5) # Over-sample a bit to reduce the condition number and avoid singular matrix
1004
1003
  points = []
1005
1004
  for t in tValues:
1006
1005
  zero = contour((t,))
1007
1006
  points.append(projection @ (self(zero) - other._point))
1008
- right = bspy.Spline.least_squares(tValues, np.array(points).T, contour.order, contour.knots)
1009
- intersections.append(Manifold.Crossing(left, right))
1007
+ secondPart = bspy.Spline.least_squares(tValues, np.array(points).T, contour.order, contour.knots)
1008
+ intersections.append(Manifold.Crossing(firstPart, secondPart))
1010
1009
  else:
1011
1010
  return NotImplemented
1012
1011
 
@@ -1015,8 +1014,8 @@ def intersect(self, other):
1015
1014
  # Construct a spline block that represents the intersection.
1016
1015
  block = bspy.spline_block.SplineBlock([[self, -other]])
1017
1016
 
1018
- # Curve-Curve intersection.
1019
- if nDep == 1:
1017
+ # Zero degrees of freedom, typically a Curve-Curve intersection.
1018
+ if dof == 0:
1020
1019
  # Find the intersection points and intervals.
1021
1020
  zeros = block.zeros()
1022
1021
  # Convert each intersection point into a Manifold.Crossing and each intersection interval into a Manifold.Coincidence.
@@ -1041,23 +1040,23 @@ def intersect(self, other):
1041
1040
  intersections.append(Manifold.Crossing(Hyperplane(1.0, zero[1][0], 0.0), Hyperplane(1.0, zero[1][1] + epsilon, 0.0)))
1042
1041
 
1043
1042
  # Now, create the coincidence.
1044
- left = Solid(nDep, False)
1045
- left.add_boundary(Boundary(Hyperplane(-1.0, zero[0][0], 0.0), Solid(0, True)))
1046
- left.add_boundary(Boundary(Hyperplane(1.0, zero[1][0], 0.0), Solid(0, True)))
1047
- right = Solid(nDep, False)
1048
- right.add_boundary(Boundary(Hyperplane(-1.0, zero[0][1], 0.0), Solid(0, True)))
1049
- right.add_boundary(Boundary(Hyperplane(1.0, zero[1][1], 0.0), Solid(0, True)))
1043
+ firstPart = Solid(self.nInd, False)
1044
+ firstPart.add_boundary(Boundary(Hyperplane(-1.0, zero[0][0], 0.0), Solid(0, True)))
1045
+ firstPart.add_boundary(Boundary(Hyperplane(1.0, zero[1][0], 0.0), Solid(0, True)))
1046
+ secondPart = Solid(other.nInd, False)
1047
+ secondPart.add_boundary(Boundary(Hyperplane(-1.0, zero[0][1], 0.0), Solid(0, True)))
1048
+ secondPart.add_boundary(Boundary(Hyperplane(1.0, zero[1][1], 0.0), Solid(0, True)))
1050
1049
  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
1051
1050
  width = zero[1][0] - zero[0][0]
1052
1051
  transform = (zero[1][1] - zero[0][1]) / width
1053
1052
  translation = (zero[0][1] * zero[1][0] - zero[1][1] * zero[0][0]) / width
1054
- intersections.append(Manifold.Coincidence(left, right, alignment, np.atleast_2d(transform), np.atleast_2d(1.0 / transform), np.atleast_1d(translation)))
1053
+ intersections.append(Manifold.Coincidence(firstPart, secondPart, alignment, np.atleast_2d(transform), np.atleast_2d(1.0 / transform), np.atleast_1d(translation)))
1055
1054
  else:
1056
1055
  # Intersection is a point, so create a Manifold.Crossing.
1057
- intersections.append(Manifold.Crossing(Hyperplane(1.0, zero[:nDep], 0.0), Hyperplane(1.0, zero[nDep:], 0.0)))
1056
+ intersections.append(Manifold.Crossing(Hyperplane(1.0, zero[:self.nInd], 0.0), Hyperplane(1.0, zero[self.nInd:], 0.0)))
1058
1057
 
1059
- # Surface-Surface intersection.
1060
- elif nDep == 2:
1058
+ # One degree of freedom, typically a Surface-Surface intersection.
1059
+ elif dof == 1:
1061
1060
  if "Name" in self.metadata and "Name" in other.metadata:
1062
1061
  logging.info(f"intersect:{self.metadata['Name']}:{other.metadata['Name']}")
1063
1062
  # Find the intersection contours, which are returned as splines.
@@ -1074,71 +1073,73 @@ def intersect(self, other):
1074
1073
  contours = block.contours()
1075
1074
  # Convert each contour into a Manifold.Crossing, swapping the manifolds back.
1076
1075
  for contour in contours:
1077
- # Swap left and right, compared to not swapped.
1078
- left = bspy.Spline(contour.nInd, nDep, contour.order, contour.nCoef, contour.knots, contour.coefs[nDep:], contour.metadata)
1079
- right = bspy.Spline(contour.nInd, nDep, contour.order, contour.nCoef, contour.knots, contour.coefs[:nDep], contour.metadata)
1080
- intersections.append(Manifold.Crossing(left, right))
1076
+ # Swap firstPart and secondPart, compared to not swapped.
1077
+ firstPart = bspy.Spline(contour.nInd, self.nInd, contour.order, contour.nCoef, contour.knots, contour.coefs[other.nInd:], contour.metadata)
1078
+ secondPart = bspy.Spline(contour.nInd, other.nInd, contour.order, contour.nCoef, contour.knots, contour.coefs[:other.nInd], contour.metadata)
1079
+ intersections.append(Manifold.Crossing(firstPart, secondPart))
1081
1080
  else:
1082
1081
  # Convert each contour into a Manifold.Crossing.
1083
1082
  for contour in contours:
1084
- left = bspy.Spline(contour.nInd, nDep, contour.order, contour.nCoef, contour.knots, contour.coefs[:nDep], contour.metadata)
1085
- right = bspy.Spline(contour.nInd, nDep, contour.order, contour.nCoef, contour.knots, contour.coefs[nDep:], contour.metadata)
1086
- intersections.append(Manifold.Crossing(left, right))
1083
+ firstPart = bspy.Spline(contour.nInd, self.nInd, contour.order, contour.nCoef, contour.knots, contour.coefs[:self.nInd], contour.metadata)
1084
+ secondPart = bspy.Spline(contour.nInd, other.nInd, contour.order, contour.nCoef, contour.knots, contour.coefs[self.nInd:], contour.metadata)
1085
+ intersections.append(Manifold.Crossing(firstPart, secondPart))
1087
1086
  else:
1088
1087
  return NotImplemented
1089
1088
  else:
1090
1089
  return NotImplemented
1091
1090
 
1092
- # Ensure the normals point outwards for both Manifolds in each crossing intersection.
1093
- # 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.
1094
- domainPoint = np.atleast_1d(0.5)
1095
- for i, intersection in enumerate(intersections):
1096
- if isinstance(intersection, Manifold.Crossing):
1097
- left = intersection.left
1098
- right = intersection.right
1099
- if np.dot(self.tangent_space(left.evaluate(domainPoint)) @ left.normal(domainPoint), other.normal(right.evaluate(domainPoint))) < 0.0:
1100
- left = left.flip_normal()
1101
- if np.dot(other.tangent_space(right.evaluate(domainPoint)) @ right.normal(domainPoint), self.normal(left.evaluate(domainPoint))) < 0.0:
1102
- right = right.flip_normal()
1103
- intersections[i] = Manifold.Crossing(left, right)
1091
+ # If self and other have normals, ensure they are pointing in the correct direction.
1092
+ if self.nInd + 1 == self.nDep and other.domain_dimension() + 1 == self.nDep:
1093
+ # Ensure the normals point outwards for both Manifolds in each crossing intersection.
1094
+ # Note that evaluating firstPart and secondPart at 0.5 is always valid because either they are points or curves with [0.0, 1.0] domains.
1095
+ domainPoint = np.atleast_1d(0.5)
1096
+ for i, intersection in enumerate(intersections):
1097
+ if isinstance(intersection, Manifold.Crossing):
1098
+ firstPart = intersection.firstPart
1099
+ secondPart = intersection.secondPart
1100
+ if np.dot(self.tangent_space(firstPart.evaluate(domainPoint)) @ firstPart.normal(domainPoint), other.normal(secondPart.evaluate(domainPoint))) < 0.0:
1101
+ firstPart = firstPart.negate_normal()
1102
+ if np.dot(other.tangent_space(secondPart.evaluate(domainPoint)) @ secondPart.normal(domainPoint), self.normal(firstPart.evaluate(domainPoint))) < 0.0:
1103
+ secondPart = secondPart.negate_normal()
1104
+ intersections[i] = Manifold.Crossing(firstPart, secondPart)
1104
1105
 
1105
1106
  return intersections
1106
1107
 
1107
- def complete_slice(self, slice, solid):
1108
+ def complete_cutout(self, cutout, solid):
1108
1109
  # Spline manifold domains have finite bounds.
1109
- slice.containsInfinity = False
1110
+ cutout.containsInfinity = False
1110
1111
  bounds = self.domain()
1111
1112
 
1112
1113
  # If manifold (self) has no intersections with solid, just check containment.
1113
- if not slice.boundaries:
1114
- if slice.dimension == 2:
1114
+ if not cutout.boundaries:
1115
+ if cutout.dimension == 2:
1115
1116
  if "Name" in self.metadata:
1116
1117
  logging.info(f"check containment:{self.metadata['Name']}")
1117
1118
  domain = bounds.T
1118
1119
  if solid.contains_point(self(0.5 * (domain[0] + domain[1]))):
1119
1120
  for boundary in Hyperplane.create_hypercube(bounds).boundaries:
1120
- slice.add_boundary(boundary)
1121
+ cutout.add_boundary(boundary)
1121
1122
  return
1122
1123
 
1123
1124
  # For curves, add domain bounds as needed.
1124
- if slice.dimension == 1:
1125
- slice.boundaries.sort(key=lambda b: (b.manifold.evaluate(0.0), b.manifold.normal(0.0)))
1125
+ if cutout.dimension == 1:
1126
+ cutout.boundaries.sort(key=lambda b: (b.manifold.evaluate(0.0), b.manifold.normal(0.0)))
1126
1127
  # First, check right end since we add new boundary to the end.
1127
- if abs(slice.boundaries[-1].manifold._point - bounds[0][1]) >= Manifold.minSeparation and \
1128
- slice.boundaries[-1].manifold._normal < 0.0:
1129
- slice.add_boundary(Boundary(Hyperplane(-slice.boundaries[-1].manifold._normal, bounds[0][1], 0.0), Solid(0, True)))
1128
+ if abs(cutout.boundaries[-1].manifold._point - bounds[0][1]) >= Manifold.minSeparation and \
1129
+ cutout.boundaries[-1].manifold._normal < 0.0:
1130
+ cutout.add_boundary(Boundary(Hyperplane(-cutout.boundaries[-1].manifold._normal, bounds[0][1], 0.0), Solid(0, True)))
1130
1131
  # Next, check left end since it's still untouched.
1131
- if abs(slice.boundaries[0].manifold._point - bounds[0][0]) >= Manifold.minSeparation and \
1132
- slice.boundaries[0].manifold._normal > 0.0:
1133
- slice.add_boundary(Boundary(Hyperplane(-slice.boundaries[0].manifold._normal, bounds[0][0], 0.0), Solid(0, True)))
1132
+ if abs(cutout.boundaries[0].manifold._point - bounds[0][0]) >= Manifold.minSeparation and \
1133
+ cutout.boundaries[0].manifold._normal > 0.0:
1134
+ cutout.add_boundary(Boundary(Hyperplane(-cutout.boundaries[0].manifold._normal, bounds[0][0], 0.0), Solid(0, True)))
1134
1135
 
1135
- # For surfaces, intersect full spline domain with existing slice boundaries.
1136
- if slice.dimension == 2:
1136
+ # For surfaces, intersect full spline domain with existing cutout boundaries.
1137
+ if cutout.dimension == 2:
1137
1138
  fullDomain = Hyperplane.create_hypercube(bounds)
1138
1139
  for newBoundary in fullDomain.boundaries: # Mark full domain boundaries as untouched
1139
1140
  newBoundary.touched = False
1140
1141
 
1141
- # Define function for adding slice points to full domain boundaries.
1142
+ # Define function for adding cutout points to full domain boundaries.
1142
1143
  def process_domain_point(boundary, domainPoint, adjustment):
1143
1144
  point = boundary.manifold.evaluate(domainPoint)
1144
1145
  # See if and where point touches full domain.
@@ -1147,24 +1148,24 @@ def complete_slice(self, slice, solid):
1147
1148
  if abs(np.dot(newBoundary.manifold._normal, vector)) < Manifold.minSeparation:
1148
1149
  # Add the point onto the new boundary (adjust normal evaluation point to move away from boundary).
1149
1150
  normal = np.sign(newBoundary.manifold._tangentSpace.T @ boundary.manifold.normal(domainPoint + adjustment))
1150
- newBoundary.domain.add_boundary(Boundary(Hyperplane(normal, newBoundary.manifold._tangentSpace.T @ vector, 0.0), Solid(0, True)))
1151
+ newBoundary.trim.add_boundary(Boundary(Hyperplane(normal, newBoundary.manifold._tangentSpace.T @ vector, 0.0), Solid(0, True)))
1151
1152
  newBoundary.touched = True
1152
1153
  break
1153
1154
 
1154
1155
  # Go through existing boundaries and check if either of their endpoints lies on the spline's bounds.
1155
- for boundary in slice.boundaries:
1156
- domainBoundaries = boundary.domain.boundaries
1156
+ for boundary in cutout.boundaries:
1157
+ domainBoundaries = boundary.trim.boundaries
1157
1158
  domainBoundaries.sort(key=lambda boundary: (boundary.manifold.evaluate(0.0), boundary.manifold.normal(0.0)))
1158
1159
  process_domain_point(boundary, domainBoundaries[0].manifold._point, Manifold.minSeparation)
1159
1160
  if len(domainBoundaries) > 1:
1160
1161
  process_domain_point(boundary, domainBoundaries[-1].manifold._point, -Manifold.minSeparation)
1161
1162
 
1162
- # For touched boundaries, remove domain bounds that aren't needed and then add boundary to slice.
1163
+ # For touched boundaries, remove domain bounds that aren't needed and then add boundary to cutout.
1163
1164
  boundaryWasTouched = False
1164
1165
  for newBoundary in fullDomain.boundaries:
1165
1166
  if newBoundary.touched:
1166
1167
  boundaryWasTouched = True
1167
- domainBoundaries = newBoundary.domain.boundaries
1168
+ domainBoundaries = newBoundary.trim.boundaries
1168
1169
  domainBoundaries.sort(key=lambda boundary: (boundary.manifold.evaluate(0.0), boundary.manifold.normal(0.0)))
1169
1170
  # Ensure domain endpoints don't overlap and their normals are consistent.
1170
1171
  if abs(domainBoundaries[0].manifold._point - domainBoundaries[1].manifold._point) < Manifold.minSeparation or \
@@ -1173,10 +1174,10 @@ def complete_slice(self, slice, solid):
1173
1174
  if abs(domainBoundaries[-1].manifold._point - domainBoundaries[-2].manifold._point) < Manifold.minSeparation or \
1174
1175
  domainBoundaries[-2].manifold._normal > 0.0:
1175
1176
  del domainBoundaries[-1]
1176
- slice.add_boundary(newBoundary)
1177
+ cutout.add_boundary(newBoundary)
1177
1178
 
1178
1179
  if boundaryWasTouched:
1179
- # Touch untouched boundaries that are connected to touched boundary endpoints and add them to slice.
1180
+ # Touch untouched boundaries that are connected to touched boundary endpoints and add them to cutout.
1180
1181
  boundaryMap = ((2, 3, 0), (2, 3, -1), (0, 1, 0), (0, 1, -1)) # Map of which full domain boundaries touch each other
1181
1182
  while True:
1182
1183
  noTouches = True
@@ -1184,21 +1185,21 @@ def complete_slice(self, slice, solid):
1184
1185
  if not newBoundary.touched:
1185
1186
  leftBoundary = fullDomain.boundaries[map[0]]
1186
1187
  rightBoundary = fullDomain.boundaries[map[1]]
1187
- if leftBoundary.touched and abs(leftBoundary.domain.boundaries[map[2]].manifold._point - bound) < Manifold.minSeparation:
1188
+ if leftBoundary.touched and abs(leftBoundary.trim.boundaries[map[2]].manifold._point - bound) < Manifold.minSeparation:
1188
1189
  newBoundary.touched = True
1189
- slice.add_boundary(newBoundary)
1190
+ cutout.add_boundary(newBoundary)
1190
1191
  noTouches = False
1191
- elif rightBoundary.touched and abs(rightBoundary.domain.boundaries[map[2]].manifold._point - bound) < Manifold.minSeparation:
1192
+ elif rightBoundary.touched and abs(rightBoundary.trim.boundaries[map[2]].manifold._point - bound) < Manifold.minSeparation:
1192
1193
  newBoundary.touched = True
1193
- slice.add_boundary(newBoundary)
1194
+ cutout.add_boundary(newBoundary)
1194
1195
  noTouches = False
1195
1196
  if noTouches:
1196
1197
  break
1197
1198
  else:
1198
- # No slice boundaries touched the full domain (a hole), so only add full domain if it is contained in the solid.
1199
+ # No cutout boundaries touched the full domain (a hole), so only add full domain if it is contained in the solid.
1199
1200
  if solid.contains_point(self.evaluate(bounds[:,0])):
1200
1201
  for newBoundary in fullDomain.boundaries:
1201
- slice.add_boundary(newBoundary)
1202
+ cutout.add_boundary(newBoundary)
1202
1203
 
1203
1204
  def full_domain(self):
1204
1205
  return Hyperplane.create_hypercube(self.domain())