bspy 4.3__py3-none-any.whl → 4.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/_spline_domain.py CHANGED
@@ -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
 
@@ -330,6 +358,7 @@ def insert_knots(self, newKnotList):
330
358
  continue
331
359
 
332
360
  # Check if knot and its total multiplicity is valid.
361
+ knot = knots.dtype.type(knot) # Cast to correct type
333
362
  if knot < knots[degree] or knot > knots[-order]:
334
363
  raise ValueError(f"Knot insertion outside domain: {knot}")
335
364
  position = np.searchsorted(knots, knot, 'right')
@@ -403,12 +432,11 @@ def join(splineList):
403
432
  splDomain = spl.domain()[0]
404
433
  start2 = spl(splDomain[0])
405
434
  end2 = spl(splDomain[1])
406
- gaps = [np.linalg.norm(vecDiff) for vecDiff in [start1 - start2, start1 - end2, end1 - start2, end1 - end2]]
407
- minDist = min(*gaps)
408
- if minDist == gaps[0] or minDist == gaps[1]:
409
- workingSpline = workingSpline.reverse()
410
- 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:
411
437
  spl = spl.reverse()
438
+ if ixMin == 2 or ixMin == 3:
439
+ workingSpline = workingSpline.reverse()
412
440
  maxOrder = max(workingSpline.order[0], spl.order[0])
413
441
  workingSpline = workingSpline.elevate([maxOrder - workingSpline.order[0]])
414
442
  spl = spl.elevate([maxOrder - spl.order[0]])
@@ -499,7 +527,7 @@ def remove_knots(self, tolerance, nLeft = 0, nRight = 0):
499
527
  foldedIndices = list(filter(lambda x: x != id, indIndex))
500
528
  currentFold, foldedBasis = currentSpline.fold(foldedIndices)
501
529
  while True:
502
- bestError = np.finfo(scaleDep[0].dtype).max
530
+ bestError = np.finfo(self.coefs.dtype).max
503
531
  bestSpline = currentFold
504
532
  ix = currentFold.order[0]
505
533
  while ix < currentFold.nCoef[0]:
@@ -609,6 +637,8 @@ def trim(self, newDomain):
609
637
  if multiplicity > 0:
610
638
  newKnots.append((bounds[0], multiplicity))
611
639
  noChange = False
640
+ if bounds[0] != knots[order - 1]:
641
+ noChange = False
612
642
 
613
643
  if not np.isnan(bounds[1]):
614
644
  if not(knots[order - 1] <= bounds[1] <= knots[-order]): raise ValueError("Invalid newDomain")
@@ -626,6 +656,8 @@ def trim(self, newDomain):
626
656
  if multiplicity > 0:
627
657
  newKnots.append((bounds[1], multiplicity))
628
658
  noChange = False
659
+ if bounds[1] != knots[-order]:
660
+ noChange = False
629
661
 
630
662
  newKnotsList.append(newKnots)
631
663
 
@@ -5,7 +5,7 @@ def bspline_values(knot, knots, splineOrder, u, derivativeOrder = 0, taylorCoefs
5
5
  basis = np.zeros(splineOrder, knots.dtype)
6
6
  if knot is None:
7
7
  knot = np.searchsorted(knots, u, side = 'right')
8
- knot = min(knot, len(knots) - splineOrder)
8
+ knot = min(max(knot, splineOrder), len(knots) - splineOrder)
9
9
  if derivativeOrder >= splineOrder:
10
10
  return knot, basis
11
11
  basis[-1] = 1.0
bspy/_spline_fitting.py CHANGED
@@ -1,6 +1,7 @@
1
1
  import numpy as np
2
2
  import scipy as sp
3
3
  import bspy.spline
4
+ import bspy.spline_block
4
5
  import math
5
6
 
6
7
  def circular_arc(radius, angle, tolerance = None):
@@ -12,14 +13,33 @@ def circular_arc(radius, angle, tolerance = None):
12
13
  return bspy.Spline.section([(radius * np.cos(u * angle * np.pi / 180), radius * np.sin(u * angle * np.pi / 180), 90 + u * angle, 1.0 / radius) for u in np.linspace(0.0, 1.0, samples)])
13
14
 
14
15
  def composition(splines, tolerance):
16
+ # Collect domains and check range bounds
17
+ domains = [None]
18
+ domain = None
19
+ for i, spline in enumerate(splines):
20
+ if domain is not None:
21
+ if len(domain) != spline.nDep:
22
+ raise ValueError(f"Domain dimension of spline {i-1} does not match range dimension of spline {i}")
23
+ rangeBounds = spline.range_bounds()
24
+ for ix in range(spline.nDep):
25
+ if rangeBounds[ix][0] < domain[ix][0] or rangeBounds[ix][1] > domain[ix][1]:
26
+ raise ValueError(f"Range of spline {i} exceeds domain of spline {i-1}")
27
+ domains.append(domain)
28
+ domain = spline.domain()
29
+
15
30
  # Define the callback function
16
31
  def composition_of_splines(u):
17
- for f in splines[::-1]:
18
- u = f(u)
32
+ for spline, domain in zip(splines[::-1], domains[::-1]):
33
+ u = spline(u)
34
+ if domain is not None:
35
+ # We've already checked that the range of spline is within the domain
36
+ # of its successor, but numerics may cause the spline value to slightly
37
+ # exceed its range, so we clip the spline value accordingly.
38
+ u = np.clip(u, domain[:, 0], domain[:, 1])
19
39
  return u
20
40
 
21
41
  # Approximate this composition
22
- return bspy.Spline.fit(splines[-1].domain(), composition_of_splines, tolerance = tolerance)
42
+ return bspy.Spline.fit(domain, composition_of_splines, tolerance = tolerance)
23
43
 
24
44
  def cone(radius1, radius2, height, tolerance = None):
25
45
  if tolerance is None:
@@ -319,6 +339,7 @@ def fit(domain, f, order = None, knots = None, tolerance = 1.0e-4):
319
339
  nInd = len(domain)
320
340
  midPoint = f(0.5 * (domain.T[0] + domain.T[1]))
321
341
  if not type(midPoint) is bspy.Spline:
342
+ midPoint = np.array(midPoint).flatten()
322
343
  nDep = len(midPoint)
323
344
 
324
345
  # Make sure order and knots conform to this
@@ -366,9 +387,13 @@ def fit(domain, f, order = None, knots = None, tolerance = 1.0e-4):
366
387
  indices = nInd * [0]
367
388
  iLast = nInd
368
389
  while iLast >= 0:
390
+ # Create a tuple for the u value (must be a tuple to use it as a dictionary key)
369
391
  uValue = tuple([uvw[i][indices[i]] for i in range(nInd)])
370
392
  if not uValue in fDictionary:
371
- fDictionary[uValue] = f(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
372
397
  fValues.append(fDictionary[uValue])
373
398
  iLast = nInd - 1
374
399
  while iLast >= 0:
@@ -518,7 +543,7 @@ def four_sided_patch(bottom, right, top, left, surfParam = 0.5):
518
543
 
519
544
  return (1.0 - surfParam) * coons + surfParam * laplace
520
545
 
521
- def geodesic(self, uvStart, uvEnd, tolerance = 1.0e-6):
546
+ def geodesic(self, uvStart, uvEnd, tolerance = 1.0e-5):
522
547
  # Check validity of input
523
548
  if self.nInd != 2: raise ValueError("Surface must have two independent variables")
524
549
  if len(uvStart) != 2: raise ValueError("uvStart must have two components")
@@ -616,7 +641,7 @@ def geodesic(self, uvStart, uvEnd, tolerance = 1.0e-6):
616
641
  initialGuess = line(uvStart, uvEnd).elevate([2])
617
642
 
618
643
  # Solve the ODE and return the geodesic
619
- solution = initialGuess.solve_ode(1, 1, geodesicCallback, 1.0e-5, (self, uvDomain))
644
+ solution = initialGuess.solve_ode(1, 1, geodesicCallback, tolerance, (self, uvDomain))
620
645
  return solution
621
646
 
622
647
  def least_squares(uValues, dataPoints, order = None, knots = None, compression = 0.0,
@@ -878,7 +903,7 @@ def section(xytk):
878
903
  # Join the pieces together and return
879
904
  return bspy.Spline.join(mySections)
880
905
 
881
- def solve_ode(self, nLeft, nRight, FAndF_u, tolerance = 1.0e-6, args = ()):
906
+ def solve_ode(self, nLeft, nRight, FAndF_u, tolerance = 1.0e-6, args = (), includeEstimate = False):
882
907
  # Ensure that the ODE is properly formulated
883
908
 
884
909
  if nLeft < 0: raise ValueError("Invalid number of left hand boundary conditions")
@@ -970,7 +995,7 @@ def solve_ode(self, nLeft, nRight, FAndF_u, tolerance = 1.0e-6, args = ()):
970
995
  residuals = np.append(residuals, np.zeros((nLeft * nDep,)))
971
996
  collocationMatrix[bandWidth, 0 : nLeft * nDep] = 1.0
972
997
  for iPoint, t in enumerate(collocationPoints[iFirstPoint : iNextPoint]):
973
- uData = np.array([workingSpline.derivative([i], t) for i in range(nOrder)]).T
998
+ uData = np.array([workingSpline.derivative([i], t) for i in range(nOrder + 1 if includeEstimate else nOrder)]).T
974
999
  F, F_u = FAndF_u(t, uData, *args)
975
1000
  residuals = np.append(residuals, workingSpline.derivative([nOrder], t) - continuation * F)
976
1001
  ix = None
@@ -1046,7 +1071,7 @@ def solve_ode(self, nLeft, nRight, FAndF_u, tolerance = 1.0e-6, args = ()):
1046
1071
 
1047
1072
  # Is it time to give up?
1048
1073
 
1049
- if (not done or continuation < 1.0) and n > 1000:
1074
+ if (not done or continuation < 1.0) and n > 10000:
1050
1075
  raise RuntimeError("Can't find solution with given initial guess")
1051
1076
 
1052
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)
@@ -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,11 +952,13 @@ 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.
960
959
  for zero in zeros:
960
+ if isinstance(zero, tuple) and zero[1] - zero[0] < Manifold.minSeparation:
961
+ zero = 0.5 * (zero[0] + zero[1])
961
962
  if isinstance(zero, tuple):
962
963
  # Intersection is an interval, so create a Manifold.Coincidence.
963
964
  planeBounds = (projection @ (self((zero[0],)) - other._point), projection @ (self((zero[1],)) - other._point))
@@ -972,10 +973,10 @@ def intersect(self, other):
972
973
  intersections.append(Manifold.Crossing(Hyperplane(1.0, zero[1] + epsilon, 0.0), Hyperplane(1.0, planeBounds[1], 0.0)))
973
974
 
974
975
  # Now, create the coincidence.
975
- left = Solid(nDep, False)
976
+ left = Solid(1, False)
976
977
  left.add_boundary(Boundary(Hyperplane(-1.0, zero[0], 0.0), Solid(0, True)))
977
978
  left.add_boundary(Boundary(Hyperplane(1.0, zero[1], 0.0), Solid(0, True)))
978
- right = Solid(nDep, False)
979
+ right = Solid(1, False)
979
980
  if planeBounds[0] > planeBounds[1]:
980
981
  planeBounds = (planeBounds[1], planeBounds[0])
981
982
  right.add_boundary(Boundary(Hyperplane(-1.0, planeBounds[0], 0.0), Solid(0, True)))
@@ -990,7 +991,7 @@ def intersect(self, other):
990
991
  intersections.append(Manifold.Crossing(Hyperplane(1.0, zero, 0.0), Hyperplane(1.0, projection @ (self((zero,)) - other._point), 0.0)))
991
992
 
992
993
  # Surface-Plane intersection.
993
- elif nDep == 2:
994
+ elif dof == 1:
994
995
  # Find the intersection contours, which are returned as splines.
995
996
  contours = spline.contours()
996
997
  # Convert each contour into a Manifold.Crossing.
@@ -1013,12 +1014,14 @@ def intersect(self, other):
1013
1014
  # Construct a spline block that represents the intersection.
1014
1015
  block = bspy.spline_block.SplineBlock([[self, -other]])
1015
1016
 
1016
- # Curve-Curve intersection.
1017
- if nDep == 1:
1017
+ # Zero degrees of freedom, typically a Curve-Curve intersection.
1018
+ if dof == 0:
1018
1019
  # Find the intersection points and intervals.
1019
1020
  zeros = block.zeros()
1020
1021
  # Convert each intersection point into a Manifold.Crossing and each intersection interval into a Manifold.Coincidence.
1021
1022
  for zero in zeros:
1023
+ if isinstance(zero, tuple) and zero[1] - zero[0] < Manifold.minSeparation:
1024
+ zero = 0.5 * (zero[0] + zero[1])
1022
1025
  if isinstance(zero, tuple):
1023
1026
  # Intersection is an interval, so create a Manifold.Coincidence.
1024
1027
 
@@ -1037,10 +1040,10 @@ def intersect(self, other):
1037
1040
  intersections.append(Manifold.Crossing(Hyperplane(1.0, zero[1][0], 0.0), Hyperplane(1.0, zero[1][1] + epsilon, 0.0)))
1038
1041
 
1039
1042
  # Now, create the coincidence.
1040
- left = Solid(nDep, False)
1043
+ left = Solid(self.nInd, False)
1041
1044
  left.add_boundary(Boundary(Hyperplane(-1.0, zero[0][0], 0.0), Solid(0, True)))
1042
1045
  left.add_boundary(Boundary(Hyperplane(1.0, zero[1][0], 0.0), Solid(0, True)))
1043
- right = Solid(nDep, False)
1046
+ right = Solid(other.nInd, False)
1044
1047
  right.add_boundary(Boundary(Hyperplane(-1.0, zero[0][1], 0.0), Solid(0, True)))
1045
1048
  right.add_boundary(Boundary(Hyperplane(1.0, zero[1][1], 0.0), Solid(0, True)))
1046
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
@@ -1050,10 +1053,10 @@ def intersect(self, other):
1050
1053
  intersections.append(Manifold.Coincidence(left, right, alignment, np.atleast_2d(transform), np.atleast_2d(1.0 / transform), np.atleast_1d(translation)))
1051
1054
  else:
1052
1055
  # Intersection is a point, so create a Manifold.Crossing.
1053
- 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)))
1054
1057
 
1055
- # Surface-Surface intersection.
1056
- elif nDep == 2:
1058
+ # One degree of freedom, typically a Surface-Surface intersection.
1059
+ elif dof == 1:
1057
1060
  if "Name" in self.metadata and "Name" in other.metadata:
1058
1061
  logging.info(f"intersect:{self.metadata['Name']}:{other.metadata['Name']}")
1059
1062
  # Find the intersection contours, which are returned as splines.
@@ -1071,32 +1074,34 @@ def intersect(self, other):
1071
1074
  # Convert each contour into a Manifold.Crossing, swapping the manifolds back.
1072
1075
  for contour in contours:
1073
1076
  # Swap left and right, compared to not swapped.
1074
- left = bspy.Spline(contour.nInd, nDep, contour.order, contour.nCoef, contour.knots, contour.coefs[nDep:], contour.metadata)
1075
- right = bspy.Spline(contour.nInd, nDep, contour.order, contour.nCoef, contour.knots, contour.coefs[:nDep], contour.metadata)
1077
+ left = bspy.Spline(contour.nInd, self.nInd, contour.order, contour.nCoef, contour.knots, contour.coefs[other.nInd:], contour.metadata)
1078
+ right = bspy.Spline(contour.nInd, other.nInd, contour.order, contour.nCoef, contour.knots, contour.coefs[:other.nInd], contour.metadata)
1076
1079
  intersections.append(Manifold.Crossing(left, right))
1077
1080
  else:
1078
1081
  # Convert each contour into a Manifold.Crossing.
1079
1082
  for contour in contours:
1080
- left = bspy.Spline(contour.nInd, nDep, contour.order, contour.nCoef, contour.knots, contour.coefs[:nDep], contour.metadata)
1081
- right = bspy.Spline(contour.nInd, nDep, contour.order, contour.nCoef, contour.knots, contour.coefs[nDep:], contour.metadata)
1083
+ left = bspy.Spline(contour.nInd, self.nInd, contour.order, contour.nCoef, contour.knots, contour.coefs[:self.nInd], contour.metadata)
1084
+ right = bspy.Spline(contour.nInd, other.nInd, contour.order, contour.nCoef, contour.knots, contour.coefs[self.nInd:], contour.metadata)
1082
1085
  intersections.append(Manifold.Crossing(left, right))
1083
1086
  else:
1084
1087
  return NotImplemented
1085
1088
  else:
1086
1089
  return NotImplemented
1087
1090
 
1088
- # Ensure the normals point outwards for both Manifolds in each crossing intersection.
1089
- # 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.
1090
- domainPoint = np.atleast_1d(0.5)
1091
- for i, intersection in enumerate(intersections):
1092
- if isinstance(intersection, Manifold.Crossing):
1093
- left = intersection.left
1094
- right = intersection.right
1095
- if np.dot(self.tangent_space(left.evaluate(domainPoint)) @ left.normal(domainPoint), other.normal(right.evaluate(domainPoint))) < 0.0:
1096
- left = left.flip_normal()
1097
- if np.dot(other.tangent_space(right.evaluate(domainPoint)) @ right.normal(domainPoint), self.normal(left.evaluate(domainPoint))) < 0.0:
1098
- right = right.flip_normal()
1099
- 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 left and right 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
+ left = intersection.left
1099
+ right = intersection.right
1100
+ if np.dot(self.tangent_space(left.evaluate(domainPoint)) @ left.normal(domainPoint), other.normal(right.evaluate(domainPoint))) < 0.0:
1101
+ left = left.flip_normal()
1102
+ if np.dot(other.tangent_space(right.evaluate(domainPoint)) @ right.normal(domainPoint), self.normal(left.evaluate(domainPoint))) < 0.0:
1103
+ right = right.flip_normal()
1104
+ intersections[i] = Manifold.Crossing(left, right)
1100
1105
 
1101
1106
  return intersections
1102
1107
 
@@ -1135,14 +1140,14 @@ def complete_slice(self, slice, solid):
1135
1140
  newBoundary.touched = False
1136
1141
 
1137
1142
  # Define function for adding slice points to full domain boundaries.
1138
- def process_domain_point(boundary, domainPoint):
1143
+ def process_domain_point(boundary, domainPoint, adjustment):
1139
1144
  point = boundary.manifold.evaluate(domainPoint)
1140
1145
  # See if and where point touches full domain.
1141
1146
  for newBoundary in fullDomain.boundaries:
1142
1147
  vector = point - newBoundary.manifold._point
1143
1148
  if abs(np.dot(newBoundary.manifold._normal, vector)) < Manifold.minSeparation:
1144
- # Add the point onto the new boundary.
1145
- normal = np.sign(newBoundary.manifold._tangentSpace.T @ boundary.manifold.normal(domainPoint))
1149
+ # Add the point onto the new boundary (adjust normal evaluation point to move away from boundary).
1150
+ normal = np.sign(newBoundary.manifold._tangentSpace.T @ boundary.manifold.normal(domainPoint + adjustment))
1146
1151
  newBoundary.domain.add_boundary(Boundary(Hyperplane(normal, newBoundary.manifold._tangentSpace.T @ vector, 0.0), Solid(0, True)))
1147
1152
  newBoundary.touched = True
1148
1153
  break
@@ -1151,9 +1156,9 @@ def complete_slice(self, slice, solid):
1151
1156
  for boundary in slice.boundaries:
1152
1157
  domainBoundaries = boundary.domain.boundaries
1153
1158
  domainBoundaries.sort(key=lambda boundary: (boundary.manifold.evaluate(0.0), boundary.manifold.normal(0.0)))
1154
- process_domain_point(boundary, domainBoundaries[0].manifold._point)
1159
+ process_domain_point(boundary, domainBoundaries[0].manifold._point, Manifold.minSeparation)
1155
1160
  if len(domainBoundaries) > 1:
1156
- process_domain_point(boundary, domainBoundaries[-1].manifold._point)
1161
+ process_domain_point(boundary, domainBoundaries[-1].manifold._point, -Manifold.minSeparation)
1157
1162
 
1158
1163
  # For touched boundaries, remove domain bounds that aren't needed and then add boundary to slice.
1159
1164
  boundaryWasTouched = False
@@ -1161,7 +1166,6 @@ def complete_slice(self, slice, solid):
1161
1166
  if newBoundary.touched:
1162
1167
  boundaryWasTouched = True
1163
1168
  domainBoundaries = newBoundary.domain.boundaries
1164
- assert len(domainBoundaries) > 2
1165
1169
  domainBoundaries.sort(key=lambda boundary: (boundary.manifold.evaluate(0.0), boundary.manifold.normal(0.0)))
1166
1170
  # Ensure domain endpoints don't overlap and their normals are consistent.
1167
1171
  if abs(domainBoundaries[0].manifold._point - domainBoundaries[1].manifold._point) < Manifold.minSeparation or \
@@ -0,0 +1,288 @@
1
+ import numpy as np
2
+ import bspy.spline
3
+ import bspy.spline_block
4
+ from collections import namedtuple
5
+
6
+ def line_of_curvature(self, uvStart, is_max, tolerance = 1.0e-3):
7
+ if self.nInd != 2: raise ValueError("Surface must have two independent variables")
8
+ if len(uvStart) != 2: raise ValueError("uvStart must have two components")
9
+ uvDomain = self.domain()
10
+ if uvStart[0] < uvDomain[0, 0] or uvStart[0] > uvDomain[0, 1] or \
11
+ uvStart[1] < uvDomain[1, 0] or uvStart[1] > uvDomain[1, 1]:
12
+ raise ValueError("uvStart is outside domain of the surface")
13
+ is_max = bool(is_max) # Ensure is_max is a boolean for XNOR operation
14
+
15
+ # Define the callback function for the ODE solver
16
+ def curvatureLineCallback(t, u):
17
+ # Evaluate the surface information needed.
18
+ uv = np.maximum(uvDomain[:, 0], np.minimum(uvDomain[:, 1], u[:, 0]))
19
+ su = self.derivative((1, 0), uv)
20
+ sv = self.derivative((0, 1), uv)
21
+ suu = self.derivative((2, 0), uv)
22
+ suv = self.derivative((1, 1), uv)
23
+ svv = self.derivative((0, 2), uv)
24
+ suuu = self.derivative((3, 0), uv)
25
+ suuv = self.derivative((2, 1), uv)
26
+ suvv = self.derivative((1, 2), uv)
27
+ svvv = self.derivative((0, 3), uv)
28
+ normal = self.normal(uv)
29
+
30
+ # Calculate curvature matrix and its derivatives.
31
+ sU = np.concatenate((su, sv)).reshape(2, -1)
32
+ sUu = np.concatenate((suu, suv)).reshape(2, -1)
33
+ sUv = np.concatenate((suv, svv)).reshape(2, -1)
34
+ sUU = np.concatenate((suu, suv, suv, svv)).reshape(2, 2, -1)
35
+ sUUu = np.concatenate((suuu, suuv, suuv, suvv)).reshape(2, 2, -1)
36
+ sUUv = np.concatenate((suuv, suvv, suvv, svvv)).reshape(2, 2, -1)
37
+ fffI = np.linalg.inv(sU @ sU.T) # Inverse of first fundamental form
38
+ k = fffI @ (sUU @ normal) # Curvature matrix
39
+ ku = fffI @ (sUUu @ normal - (sUu @ sU.T + sU @ sUu.T) @ k - sUU @ (sU.T @ k[:, 0]))
40
+ kv = fffI @ (sUUv @ normal - (sUv @ sU.T + sU @ sUv.T) @ k - sUU @ (sU.T @ k[:, 1]))
41
+
42
+ # Determine principle curvatures and directions, and assign new direction.
43
+ curvatures, directions = np.linalg.eig(k)
44
+ curvatureDelta = curvatures[1] - curvatures[0]
45
+ if abs(curvatureDelta) < tolerance:
46
+ # If we're at an umbilic, use the last direction (jacobian is zero at umbilic).
47
+ direction = u[:, 1]
48
+ jacobian = np.zeros((2,2,1), self.coefs.dtype)
49
+ else:
50
+ # Otherwise, compute the lhs inverse for the jacobian.
51
+ directionsInverse = np.linalg.inv(directions)
52
+ eigenIndex = 0 if bool(curvatures[0] > curvatures[1]) == is_max else 1
53
+ direction = directions[:, eigenIndex]
54
+ B = np.zeros((2, 2), self.coefs.dtype)
55
+ B[0, 1 - eigenIndex] = np.dot(directions[:, 1], direction) / curvatureDelta
56
+ B[1, 1 - eigenIndex] = -np.dot(directions[:, 0], direction) / curvatureDelta
57
+ lhsInv = directions @ B @ directionsInverse
58
+
59
+ # Adjust the direction for consistency.
60
+ if np.dot(direction, u[:, 1]) < -tolerance:
61
+ direction *= -1
62
+
63
+ # Compute the jacobian for the direction.
64
+ jacobian = np.empty((2,2,1), self.coefs.dtype)
65
+ jacobian[:,0,0] = lhsInv @ ku @ direction
66
+ jacobian[:,1,0] = lhsInv @ kv @ direction
67
+
68
+ return direction, jacobian
69
+
70
+ # Generate the initial guess for the line of curvature.
71
+ uvStart = np.atleast_1d(uvStart)
72
+ direction = 0.5 * (uvDomain[:,0] + uvDomain[:,1]) - uvStart # Initial guess toward center
73
+ distanceFromCenter = np.linalg.norm(direction)
74
+ if distanceFromCenter < 10 * tolerance:
75
+ # If we're at the center, just point to the far corner.
76
+ direction = np.array((1.0, 1.0)) / np.sqrt(2)
77
+ else:
78
+ direction /= distanceFromCenter
79
+
80
+ # Compute line of curvature direction at start.
81
+ direction, jacobian = curvatureLineCallback(0.0, np.array(((uvStart[0], direction[0]), (uvStart[1], direction[1]))))
82
+
83
+ # Calculate distance to the boundary in that direction.
84
+ if direction[0] < -tolerance:
85
+ uBoundaryDistance = (uvDomain[0, 0] - uvStart[0]) / direction[0]
86
+ elif direction[0] > tolerance:
87
+ uBoundaryDistance = (uvDomain[0, 1] - uvStart[0]) / direction[0]
88
+ else:
89
+ uBoundaryDistance = np.inf
90
+ if direction[1] < -tolerance:
91
+ vBoundaryDistance = (uvDomain[1, 0] - uvStart[1]) / direction[1]
92
+ elif direction[1] > tolerance:
93
+ vBoundaryDistance = (uvDomain[1, 1] - uvStart[1]) / direction[1]
94
+ else:
95
+ vBoundaryDistance = np.inf
96
+ boundaryDistance = min(uBoundaryDistance, vBoundaryDistance)
97
+
98
+ # Construct the initial guess from start point to boundary.
99
+ initialGuess = bspy.spline.Spline.line(uvStart, uvStart + boundaryDistance * direction).elevate([2])
100
+
101
+ # Solve the ODE and return the line of curvature confined to the surface's domain.
102
+ solution = initialGuess.solve_ode(1, 0, curvatureLineCallback, tolerance, includeEstimate = True)
103
+ return solution.confine(uvDomain)
104
+
105
+ def offset(self, edgeRadius, bitRadius=None, angle=np.pi / 2.2, path=None, subtract=False, removeCusps=False, tolerance = 1.0e-4):
106
+ if self.nDep < 2 or self.nDep > 3 or self.nDep - self.nInd != 1: raise ValueError("The offset is only defined for 2D curves and 3D surfaces with well-defined normals.")
107
+ if edgeRadius < 0:
108
+ raise ValueError("edgeRadius must be >= 0")
109
+ elif edgeRadius == 0:
110
+ return self
111
+ if bitRadius is None:
112
+ bitRadius = edgeRadius
113
+ elif bitRadius < edgeRadius:
114
+ raise ValueError("bitRadius must be >= edgeRadius")
115
+ if angle < 0 or angle >= np.pi / 2: raise ValueError("angle must in the range [0, pi/2)")
116
+ if path is not None and (path.nInd != 1 or path.nDep != 2 or self.nInd != 2):
117
+ raise ValueError("path must be a 2D curve and self must be a 3D surface")
118
+
119
+ # Compute new order, knots, and fillets for offset (ensure order is at least 4).
120
+ Fillet = namedtuple('Fillet', ('adjustment', 'isFillet'))
121
+ newOrder = []
122
+ newKnotList = []
123
+ newUniqueList = []
124
+ filletList = []
125
+ for order, knots in zip(self.order, self.knots):
126
+ min4Order = max(order, 4)
127
+ unique, counts = np.unique(knots, return_counts=True)
128
+ counts += min4Order - order # Ensure order is at least 4
129
+ newOrder.append(min4Order)
130
+ adjustment = 0
131
+ epsilon = np.finfo(unique.dtype).eps
132
+
133
+ # Add first knot.
134
+ newKnots = [unique[0]] * counts[0]
135
+ newUnique = [unique[0]]
136
+ fillets = [Fillet(adjustment, False)]
137
+
138
+ # Add internal knots, checking for C1 discontinuities needing fillets.
139
+ for knot, count in zip(unique[1:-1], counts[1:-1]):
140
+ knot += adjustment
141
+ newKnots += [knot] * count
142
+ newUnique.append(knot)
143
+ # Check for lack of C1 continuity (need for a fillet)
144
+ if count >= min4Order - 1:
145
+ fillets.append(Fillet(adjustment, True))
146
+ # Create parametric space for fillet.
147
+ adjustment += 1
148
+ knot += 1 + epsilon # Add additional adjustment and step slightly past discontinuity
149
+ newKnots += [knot] * (min4Order - 1)
150
+ newUnique.append(knot)
151
+ fillets.append(Fillet(adjustment, False))
152
+
153
+ # Add last knot.
154
+ newKnots += [unique[-1] + adjustment] * counts[-1]
155
+ newUnique.append(unique[-1] + adjustment)
156
+ fillets.append(Fillet(adjustment, False))
157
+
158
+ # Build fillet and knot lists.
159
+ newKnotList.append(np.array(newKnots, knots.dtype))
160
+ newUniqueList.append(np.array(newUnique, knots.dtype))
161
+ filletList.append(fillets)
162
+
163
+ if path is not None:
164
+ min4Order = max(path.order[0], 4)
165
+ newOrder = [min4Order]
166
+ unique, counts = np.unique(path.knots[0], return_counts=True)
167
+ counts += min4Order - path.order[0] # Ensure order is at least 4
168
+ newKnotList = [np.repeat(unique, counts)]
169
+ domain = path.domain()
170
+ else:
171
+ domain = [(unique[0], unique[-1]) for unique in newUniqueList]
172
+
173
+ # Determine geometry of drill bit.
174
+ if subtract:
175
+ edgeRadius *= -1
176
+ bitRadius *= -1
177
+ w = bitRadius - edgeRadius
178
+ h = w * np.tan(angle)
179
+ bottom = np.sin(angle)
180
+ bottomRadius = edgeRadius + h / bottom
181
+
182
+ # Define drill bit function.
183
+ if abs(w) < tolerance and path is None: # Simple offset curve or surface
184
+ def drillBit(normal):
185
+ return edgeRadius * normal
186
+ elif self.nDep == 2: # General offset curve
187
+ def drillBit(normal):
188
+ upward = np.sign(normal[1])
189
+ if upward * normal[1] <= bottom:
190
+ return np.array((edgeRadius * normal[0] + w * np.sign(normal[0]), edgeRadius * normal[1]))
191
+ else:
192
+ return np.array((bottomRadius * normal[0], bottomRadius * normal[1] - upward * h))
193
+ elif self.nDep == 3: # General offset surface
194
+ def drillBit(normal):
195
+ upward = np.sign(normal[1])
196
+ if upward * normal[1] <= bottom:
197
+ norm = np.sqrt(normal[0] * normal[0] + normal[2] * normal[2])
198
+ return np.array((edgeRadius * normal[0] + w * normal[0] / norm, edgeRadius * normal[1], edgeRadius * normal[2] + w * normal[2] / norm))
199
+ else:
200
+ return np.array((bottomRadius * normal[0], bottomRadius * normal[1] - upward * h, bottomRadius * normal[2]))
201
+ else: # Should never get here (exception raised earlier)
202
+ raise ValueError("The offset is only defined for 2D curves and 3D surfaces with well-defined normals.")
203
+
204
+ # Define function to pass to fit.
205
+ def fitFunction(uv):
206
+ if path is not None:
207
+ uv = path(uv)
208
+
209
+ # Compute adjusted spline uv values, accounting for fillets.
210
+ hasFillet = False
211
+ adjustedUV = uv.copy()
212
+ for (i, u), unique, fillets in zip(enumerate(uv), newUniqueList, filletList):
213
+ ix = np.searchsorted(unique, u, 'right') - 1
214
+ fillet = fillets[ix]
215
+ if fillet.isFillet:
216
+ hasFillet = True
217
+ adjustedUV[i] = unique[ix] - fillet.adjustment
218
+ else:
219
+ adjustedUV[i] -= fillet.adjustment
220
+
221
+ # If we have fillets, compute the normal from their normal fan.
222
+ if hasFillet:
223
+ normal = np.zeros(self.nDep, self.coefs.dtype)
224
+ nudged = adjustedUV.copy()
225
+ for (i, u), unique, fillets in zip(enumerate(uv), newUniqueList, filletList):
226
+ ix = np.searchsorted(unique, u, 'right') - 1
227
+ fillet = fillets[ix]
228
+ if fillet.isFillet:
229
+ epsilon = np.finfo(unique.dtype).eps
230
+ alpha = u - unique[ix]
231
+ np.copyto(nudged, adjustedUV)
232
+ nudged[i] -= epsilon
233
+ normal += (1 - alpha) * self.normal(nudged)
234
+ nudged[i] += 2 * epsilon
235
+ normal += alpha * self.normal(nudged)
236
+ normal = normal / np.linalg.norm(normal)
237
+ else:
238
+ normal = self.normal(adjustedUV)
239
+
240
+ # Return the offset based on the normal.
241
+ return self(adjustedUV) + drillBit(normal)
242
+
243
+ # Fit new spline to offset by drill bit.
244
+ offset = bspy.spline.Spline.fit(domain, fitFunction, newOrder, newKnotList, tolerance)
245
+
246
+ # Remove cusps as required (only applies to offset curves).
247
+ if removeCusps and (self.nInd == 1 or path is not None):
248
+ # Find the cusps by checking for tangent direction reversal between the spline and offset.
249
+ cusps = []
250
+ previousKnot = None
251
+ start = None
252
+ for knot in np.unique(offset.knots[0][offset.order[0]:offset.nCoef[0]]):
253
+ if path is not None:
254
+ tangent = self.jacobian(path(knot)) @ path.derivative((1,), knot)
255
+ else:
256
+ tangent = self.derivative((1,), knot)
257
+ flipped = np.dot(tangent, offset.derivative((1,), knot)) < 0
258
+ if flipped and start is None:
259
+ start = knot
260
+ if not flipped and start is not None:
261
+ cusps.append((start, previousKnot))
262
+ start = None
263
+ previousKnot = knot
264
+
265
+ # Remove the cusps by intersecting the offset segments before and after each cusp.
266
+ segmentList = []
267
+ for cusp in cusps:
268
+ domain = offset.domain()
269
+ before = offset.trim(((domain[0][0], cusp[0]),))
270
+ after = -offset.trim(((cusp[1], domain[0][1]),))
271
+ if path is not None:
272
+ # Project before and after onto a 2D plane defined by the offset tangent
273
+ # and the surface normal at the start of the cusp.
274
+ # This is necessary to find the intersection point (2 equations, 2 unknowns).
275
+ tangent = offset.derivative((1,), cusp[0])
276
+ projection = np.concatenate((tangent / np.linalg.norm(tangent),
277
+ self.normal(path(cusp[0])))).reshape((2,3))
278
+ before = before.transform(projection)
279
+ after = after.transform(projection)
280
+ block = bspy.spline_block.SplineBlock([[before, after]])
281
+ intersections = block.zeros()
282
+ for intersection in intersections:
283
+ segmentList.append(offset.trim(((domain[0][0], intersection[0]),)))
284
+ offset = offset.trim(((intersection[1], domain[0][1]),))
285
+ segmentList.append(offset)
286
+ offset = bspy.spline.Spline.join(segmentList)
287
+
288
+ return offset
@@ -71,16 +71,20 @@ def confine(self, range_bounds):
71
71
  if self.nInd != 1: raise ValueError("Confine only works on curves (nInd == 1)")
72
72
  if len(range_bounds) != self.nDep: raise ValueError("len(range_bounds) must equal nDep")
73
73
  spline = self.clamp((0,), (0,))
74
+ if spline is self:
75
+ spline = self.copy()
74
76
  order = spline.order[0]
75
77
  degree = order - 1
76
78
  domain = spline.domain()
79
+ dtype = spline.knots[0].dtype
77
80
  unique, counts = np.unique(spline.knots[0], return_counts=True)
78
81
  machineEpsilon = np.finfo(self.coefs.dtype).eps
79
82
  epsilon = np.sqrt(machineEpsilon)
80
83
  intersections = [] # List of tuples (u, boundaryPoint, headingOutside)
81
84
 
82
85
  def addIntersection(u, headedOutside = False):
83
- boundaryPoint = spline(np.atleast_1d(u))
86
+ u = dtype.type(u) # Cast to spline domain type
87
+ boundaryPoint = spline(u)
84
88
  for i in range(spline.nDep):
85
89
  if boundaryPoint[i] < range_bounds[i][0]:
86
90
  headedOutside = True if boundaryPoint[i] < range_bounds[i][0] - epsilon else headedOutside
@@ -88,18 +92,18 @@ def confine(self, range_bounds):
88
92
  if boundaryPoint[i] > range_bounds[i][1]:
89
93
  headedOutside = True if boundaryPoint[i] > range_bounds[i][1] + epsilon else headedOutside
90
94
  boundaryPoint[i] = range_bounds[i][1]
91
- intersections.append((u, boundaryPoint, headedOutside))
95
+ intersections.append([u, boundaryPoint, headedOutside])
92
96
 
93
97
  def intersectBoundary(i, j):
94
98
  zeros = type(spline)(1, 1, spline.order, spline.nCoef, spline.knots, (spline.coefs[i] - range_bounds[i][j],)).zeros()
95
99
  for zero in zeros:
96
100
  if isinstance(zero, tuple):
97
- headedOutside = (-1 if j == 0 else 1) * spline.derivative((1,), np.atleast_1d(zero[0]))[i] > epsilon
101
+ headedOutside = (-1 if j == 0 else 1) * spline.derivative((1,), zero[0])[i] > 0
98
102
  addIntersection(zero[0], headedOutside)
99
- headedOutside = (-1 if j == 0 else 1) * spline.derivative((1,), np.atleast_1d(zero[1]))[i] > epsilon
103
+ headedOutside = (-1 if j == 0 else 1) * spline.derivative((1,), zero[1])[i] > 0
100
104
  addIntersection(zero[1], headedOutside)
101
105
  else:
102
- headedOutside = (-1 if j == 0 else 1) * spline.derivative((1,), np.atleast_1d(zero))[i] > epsilon
106
+ headedOutside = (-1 if j == 0 else 1) * spline.derivative((1,), zero)[i] > 0
103
107
  addIntersection(zero, headedOutside)
104
108
 
105
109
  addIntersection(domain[0][0]) # Confine starting point
@@ -112,21 +116,22 @@ def confine(self, range_bounds):
112
116
  # Put the intersection points in order.
113
117
  intersections.sort(key=lambda intersection: intersection[0])
114
118
 
115
- # Remove repeat points at start and end.
116
- while intersections[1][0] - intersections[0][0] < epsilon:
117
- del intersections[1]
118
- while intersections[-1][0] - intersections[-2][0] < epsilon:
119
- del intersections[-2]
120
-
121
- # Insert order-1 knots at each intersection point.
122
- for (knot, boundaryPoint, headedOutside) in intersections:
123
- ix = np.searchsorted(unique, knot)
124
- if unique[ix] == knot:
125
- count = (order - 1) - counts[ix]
126
- if count > 0:
127
- spline = spline.insert_knots(((knot, count),))
119
+ # Insert order-1 (degree) knots at each intersection point.
120
+ previousKnot, previousBoundaryPoint, previousHeadedOutside = intersections[0]
121
+ previousIx = 0
122
+ for i, (knot, boundaryPoint, headedOutside) in enumerate(intersections[1:]):
123
+ if knot - previousKnot < epsilon:
124
+ intersections[previousIx][2] = headedOutside # Keep last headed outside
128
125
  else:
129
- spline = spline.insert_knots(((knot, order - 1),))
126
+ ix = np.searchsorted(unique, knot)
127
+ if unique[ix] == knot:
128
+ count = degree - counts[ix]
129
+ if count > 0:
130
+ spline = spline.insert_knots((((knot, count),),))
131
+ else:
132
+ spline = spline.insert_knots((((knot, degree),),))
133
+ previousKnot = knot
134
+ previousIx = i
130
135
 
131
136
  # Go through the boundary points, assigning boundary coefficients, interpolating between boundary points,
132
137
  # and removing knots and coefficients where the curve stalls.
@@ -139,29 +144,37 @@ def confine(self, range_bounds):
139
144
  knotAdjustment = 0.0
140
145
  for knot, boundaryPoint, headedOutside in intersections[1:]:
141
146
  knot += knotAdjustment
142
- ix = np.searchsorted(knots, knot, 'right') - order
143
- ix = min(ix, nCoef - 1)
144
- coefs[:, ix] = boundaryPoint # Assign boundary coefficients
145
- if previousHeadedOutside and np.linalg.norm(boundaryPoint - previousBoundaryPoint) < epsilon:
146
- # Curve has stalled, so remove intervening knots and coefficients, and adjust knot values.
147
- nCoef -= ix - previousIx
148
- knots = np.delete(knots, slice(previousIx + 1, ix + 1))
149
- knots[previousIx + 1:] -= knot - previousKnot
150
- knotAdjustment -= knot - previousKnot
151
- coefs = np.delete(coefs, slice(previousIx, ix), axis=1)
152
- previousHeadedOutside = headedOutside # The previous knot is unchanged, but inherits the new headedOutside value
153
- else:
154
- if previousHeadedOutside:
155
- # If we were outside, linearly interpolate between the previous and current boundary points.
156
- slope = (boundaryPoint - previousBoundaryPoint) / (knot - previousKnot)
157
- for i in range(previousIx + 1, ix):
158
- coefs[:, i] = coefs[:, i - 1] + ((knots[i + degree] - knots[i]) / degree) * slope
159
-
160
- # Update previous knot
161
- previousKnot = knot
162
- previousBoundaryPoint = boundaryPoint
163
- previousHeadedOutside = headedOutside
164
- previousIx = ix
147
+ if knot - previousKnot >= epsilon:
148
+ ix = np.searchsorted(knots, knot, 'right') - order
149
+ ix = min(ix, nCoef - 1)
150
+ coefs[:, ix] = boundaryPoint # Assign boundary coefficients
151
+ if previousHeadedOutside and np.linalg.norm(boundaryPoint - previousBoundaryPoint) < epsilon:
152
+ # Curve has stalled, so remove intervening knots and coefficients, and adjust knot values.
153
+ nCoef -= ix - previousIx
154
+ knots = np.delete(knots, slice(previousIx + 1, ix + 1))
155
+ knots[previousIx + 1:] -= knot - previousKnot
156
+ knotAdjustment -= knot - previousKnot
157
+ coefs = np.delete(coefs, slice(previousIx, ix), axis=1)
158
+ previousHeadedOutside = headedOutside # The previous knot is unchanged, but inherits the new headedOutside value
159
+ else:
160
+ if previousHeadedOutside:
161
+ # If we were outside, linearly interpolate between the previous and current boundary points.
162
+ slope = (boundaryPoint - previousBoundaryPoint) / (knot - previousKnot)
163
+ for i in range(previousIx + 1, ix):
164
+ coefs[:, i] = coefs[:, i - 1] + ((knots[i + degree] - knots[i]) / degree) * slope
165
+
166
+ # Update previous knot
167
+ previousKnot = knot
168
+ previousBoundaryPoint = boundaryPoint
169
+ previousHeadedOutside = headedOutside
170
+ previousIx = ix
171
+ elif previousKnot != knot and knot == domain[0][1] and np.linalg.norm(boundaryPoint - previousBoundaryPoint) < epsilon:
172
+ # Curve stalled at the end. Remove the last knot and its associated coefficients.
173
+ # Keep the last knot if the previous and last knot are the same.
174
+ nCoef -= degree
175
+ knots = knots[:-degree]
176
+ knots[-1] = previousKnot
177
+ coefs = coefs[:,:-degree]
165
178
 
166
179
  spline.nCoef = (nCoef,)
167
180
  spline.knots = (knots,)
bspy/solid.py CHANGED
@@ -753,7 +753,7 @@ class Solid:
753
753
 
754
754
  # Calculate Integral(f) * first cofactor. Note that quad returns a tuple: (integral, error bound).
755
755
  returnValue = 0.0
756
- firstCofactor = boundary.manifold.normal(evalPoint, False, (0,))
756
+ firstCofactor = boundary.manifold.normal(evalPoint, False, (0,))[0]
757
757
  if abs(x0 - point[0]) > epsabs and abs(firstCofactor) > epsabs:
758
758
  returnValue = integrate.quad(fHat, x0, point[0], epsabs=epsabs, epsrel=epsrel, *quadArgs)[0] * firstCofactor
759
759
  return returnValue
bspy/spline.py CHANGED
@@ -5,8 +5,9 @@ from bspy.manifold import Manifold
5
5
  import bspy.spline_block
6
6
  import bspy._spline_domain
7
7
  import bspy._spline_evaluation
8
- import bspy._spline_intersection
9
8
  import bspy._spline_fitting
9
+ import bspy._spline_intersection
10
+ import bspy._spline_milling
10
11
  import bspy._spline_operations
11
12
 
12
13
  @Manifold.register
@@ -59,8 +60,8 @@ class Spline(Manifold):
59
60
  self.knots = tuple(np.array(kk) for kk in knots)
60
61
  for knots, order, nCoef in zip(self.knots, self.order, self.nCoef):
61
62
  for i in range(nCoef):
62
- if not(knots[i] <= knots[i + 1] and knots[i] < knots[i + order]):
63
- raise ValueError("Improperly ordered knot sequence")
63
+ if not(knots[i] <= knots[i + 1] and knots[i + order] - knots[i] > 0):
64
+ raise ValueError("Improper knot order or multiplicity")
64
65
  totalCoefs = 1
65
66
  for nCoef in self.nCoef:
66
67
  totalCoefs *= nCoef
@@ -184,6 +185,25 @@ class Spline(Manifold):
184
185
  indMap = [(mapping, mapping) if np.isscalar(mapping) else mapping for mapping in indMap]
185
186
  return bspy._spline_operations.add(self, other, indMap)
186
187
 
188
+ def arc_length_map(self, tolerance = 1.0e-6):
189
+ """
190
+ Determine a mapping s -> u such that any curve parametrized arbitrarily can be composed with the
191
+ computed mapping to get the curve parametrized by arc length. Specifically, given a curve x with
192
+ points on the curve given by x(u), determine a mapping u such that the composite curve x o u is
193
+ parametrized by arc length, i.e. x can be thought of as x(u(s)) for an arc length parameter s.
194
+
195
+ Parameters
196
+ ----------
197
+ tolerance : the accuracy to which the arc length map should be computed
198
+
199
+ Returns
200
+ -------
201
+ spline : Spline
202
+ The spline mapping from R1 -> R1 which approximates the arc length map to within the specified
203
+ tolerance.
204
+ """
205
+ return bspy._spline_domain.arc_length_map(self, tolerance)
206
+
187
207
  @staticmethod
188
208
  def bspline_values(knot, knots, splineOrder, u, derivativeOrder = 0, taylorCoefs = False):
189
209
  """
@@ -546,13 +566,13 @@ class Spline(Manifold):
546
566
 
547
567
  def contract(self, uvw):
548
568
  """
549
- Contract a spline by assigning a fixed value to one or more of its independent variables.
569
+ Contract a spline, reducing its number of independent variables, by assigning a fixed value to one or more of its independent variables.
550
570
 
551
571
  Parameters
552
572
  ----------
553
573
  uvw : `iterable`
554
574
  An iterable of length `nInd` that specifies the values of each independent variable to contract.
555
- A value of `None` for an independent variable indicates that variable should remain unchanged.
575
+ A value of `None` for an independent variable retains that independent variable in the contacted spline.
556
576
 
557
577
  Returns
558
578
  -------
@@ -974,15 +994,15 @@ class Spline(Manifold):
974
994
  resulting spline function will have nInd + number of independent variables
975
995
  in the splines returned independent variables and nDep dependent variables.
976
996
 
977
- order : `array-like`
997
+ order : `array-like`, optional
978
998
  An optional integer array of length nInd which specifies the polynomial
979
999
  order to use in each of the independent variables. It will default to order
980
1000
  4 (degree 3) if None is specified (the default)
981
1001
 
982
- knots : `array-like`
1002
+ knots : `array-like`, optional
983
1003
  The initial knot sequence to use, if given
984
1004
 
985
- tolerance : `scalar`
1005
+ tolerance : `scalar`, optional
986
1006
  The maximum 2-norm of the difference between the given function and the
987
1007
  spline fit. Defaults to 1.0e-4.
988
1008
 
@@ -1129,7 +1149,7 @@ class Spline(Manifold):
1129
1149
  """
1130
1150
  return bspy._spline_intersection.full_domain(self)
1131
1151
 
1132
- def geodesic(self, uvStart, uvEnd, tolerance = 1.0e-6):
1152
+ def geodesic(self, uvStart, uvEnd, tolerance = 1.0e-5):
1133
1153
  """
1134
1154
  Determine a geodesic between two points on a surface
1135
1155
 
@@ -1141,9 +1161,9 @@ class Spline(Manifold):
1141
1161
  uvEnd : `array-like`
1142
1162
  The parameter values for the surface at the other end of the desired geodesic.
1143
1163
 
1144
- tolerance : scalar
1164
+ tolerance : scalar, optional
1145
1165
  The maximum error in parameter space to which the geodesic should get computed.
1146
- Defaults to 1.0e-6.
1166
+ Defaults to 1.0e-5.
1147
1167
 
1148
1168
  Returns
1149
1169
  -------
@@ -1499,6 +1519,36 @@ class Spline(Manifold):
1499
1519
  """
1500
1520
  return bspy._spline_fitting.line(startPoint, endPoint)
1501
1521
 
1522
+ def line_of_curvature(self, uvStart, is_max = True, tolerance = 1.0e-3):
1523
+ """
1524
+ Determine a line of curvature along a surface
1525
+
1526
+ Parameters
1527
+ ----------
1528
+ uvStart : `array-like`
1529
+ The parameter values for the surface at one end of the desired line of curvature.
1530
+
1531
+ is_max : `bool`, optional
1532
+ Boolean value indicating that the line of curvature should be the maximal curvature line.
1533
+ If False, the minimal curvature line is returned. Defaults to True.
1534
+
1535
+ tolerance : scalar, optional
1536
+ The maximum error in parameter space to which the geodesic should get computed.
1537
+ Defaults to 1.0e-3.
1538
+
1539
+ Returns
1540
+ -------
1541
+ spline : `Spline`
1542
+ A spline curve whose range is in the domain of the given surface. The range of the
1543
+ curve is the locus of points whose image under the surface map form the line of curvature
1544
+ starting at the given point.
1545
+
1546
+ See Also
1547
+ --------
1548
+ `solve_ode` : Solve an ordinary differential equation using spline collocation.
1549
+ """
1550
+ return bspy._spline_milling.line_of_curvature(self, uvStart, is_max, tolerance)
1551
+
1502
1552
  @staticmethod
1503
1553
  def load(fileName):
1504
1554
  """
@@ -1653,6 +1703,61 @@ class Spline(Manifold):
1653
1703
  the matrix formed by the tangents of the spline. If the null space is greater than one dimension, the normal will be zero.
1654
1704
  """
1655
1705
  return bspy._spline_operations.normal_spline(bspy.spline_block.SplineBlock(self), indices)
1706
+
1707
+ def offset(self, edgeRadius, bitRadius=None, angle=np.pi / 2.2, path=None, subtract=False, removeCusps=False, tolerance = 1.0e-4):
1708
+ """
1709
+ Compute the offset of a spline to a given tolerance.
1710
+
1711
+ Parameters
1712
+ ----------
1713
+ edgeRadius : scalar
1714
+ The radius of offset. If a bit radius is specified, the edge radius is the
1715
+ smaller radius of the cutting edge of the drill bit, whereas bit radius specifies
1716
+ half of the full width of the drill bit.
1717
+
1718
+ bitRadius : scalar, optional
1719
+ The radius of the drill bit (half its full width). For a ball nose cutter (the default),
1720
+ the bit radius is the same as the edge radius. For an end mill,
1721
+ the bit radius is larger (typically much larger) than the edge radius.
1722
+
1723
+ angle : scalar, optional
1724
+ The angle at which the drill bit transitions from the edge radius to the
1725
+ flatter bottom of the drill bit. The angle must be in the range [0, pi/2).
1726
+ Defaults to pi / 2.2.
1727
+
1728
+ path : `Spline`, optional
1729
+ The path along self that the drill bit should contact.
1730
+ If specified, the path must be a 2D curve in the domain of self, self must be a 3D surface,
1731
+ and the offset returned is a 3D curve providing the 3D position of the drill bit,
1732
+ rather than the full offset surface. Defaults to None.
1733
+
1734
+ subtract : boolean, optional
1735
+ Flag indicating if the drill bit should be subtracted from the spline instead of added.
1736
+ Subtracting the drill bit returns the tool path that cuts out the spline. Defaults to False.
1737
+
1738
+ removeCusps : boolean, optional
1739
+ Flag indicating if cusps and their associated self-intersections should be removed from the
1740
+ offset. Only applicable to offset curves and paths along offset surfaces. Defaults to False.
1741
+
1742
+ tolerance : `scalar`, optional
1743
+ The maximum 2-norm of the difference between the offset and the
1744
+ spline fit. Defaults to 1.0e-4.
1745
+
1746
+ Returns
1747
+ -------
1748
+ offset : `Spline`
1749
+ The spline that represents the offset.
1750
+
1751
+ See Also
1752
+ --------
1753
+ `fit` : Fit the function f with a spline to a given tolerance.
1754
+
1755
+ Notes
1756
+ -----
1757
+ The offset is only defined for 2D curves and 3D surfaces with well-defined normals.
1758
+ The bottom of the drill bit is tangent to its lowest y value.
1759
+ """
1760
+ return bspy._spline_milling.offset(self, edgeRadius, bitRadius, angle, path, subtract, removeCusps, tolerance)
1656
1761
 
1657
1762
  @staticmethod
1658
1763
  def point(point):
@@ -1971,7 +2076,7 @@ class Spline(Manifold):
1971
2076
  """
1972
2077
  return bspy._spline_fitting.section(xytk)
1973
2078
 
1974
- def solve_ode(self, nLeft, nRight, FAndF_u, tolerance = 1.0e-6, args = ()):
2079
+ def solve_ode(self, nLeft, nRight, FAndF_u, tolerance = 1.0e-6, args = (), includeEstimate = False):
1975
2080
  """
1976
2081
  Numerically solve an ordinary differential equation with boundary conditions.
1977
2082
 
@@ -1994,26 +2099,31 @@ class Spline(Manifold):
1994
2099
  FAndF_u : Python function
1995
2100
  FAndF_u must have exactly this calling sequence: FAndF_u(t, uData, *args). t is a scalar set
1996
2101
  to the desired value of the independent variable of the ODE. uData will be a numpy matrix of shape
1997
- (self.nDep, nOrder) whose columns are (u, ... , u^(nOrder - 1). It must return a numpy
2102
+ (self.nDep, nOrder) whose columns are u, ... , u^(nOrder - 1). It must return a numpy
1998
2103
  vector of length self.nDep and a numpy array whose shape is (self.nDep, self.nDep, nOrder).
1999
2104
  The first output vector is the value of the forcing function F at (t, uData). The numpy
2000
2105
  array is the array of partial derivatives with respect to all the numbers in uData. Thus, if
2001
2106
  this array is called jacobian, then jacobian[:, i, j] is the gradient of the forcing function with
2002
2107
  respect to uData[i, j].
2003
2108
 
2004
- tolerance : scalar
2005
- The relative error to which the ODE should get solved.
2109
+ tolerance : scalar, optional
2110
+ The relative error to which the ODE should get solved. Default is 1.0e-6.
2006
2111
 
2007
- args : tuple
2112
+ args : tuple, optional
2008
2113
  Additional arguments to pass to the user-defined function FAndF_u. For example, if FAndF_u has the
2009
- FAndF_u(t, uData, a, b, c), then args must be a tuple of length 3.
2114
+ FAndF_u(t, uData, a, b, c), then args must be a tuple of length 3. Default is ().
2115
+
2116
+ includeEstimate : bool, optional
2117
+ If `includeEstimate` is True, the uData passed to `FAndF_u` will be a numpy matrix of shape
2118
+ (self.nDep, nOrder + 1) whose columns are u, ... , u^(nOrder). The last column will be the most
2119
+ recent estimate of u^(nOrder)(t). Default is False.
2010
2120
 
2011
2121
  Notes
2012
2122
  =====
2013
2123
  This method uses B-splines as finite elements. The ODE itself is discretized using
2014
2124
  collocation.
2015
2125
  """
2016
- return bspy._spline_fitting.solve_ode(self, nLeft, nRight, FAndF_u, tolerance, args)
2126
+ return bspy._spline_fitting.solve_ode(self, nLeft, nRight, FAndF_u, tolerance, args, includeEstimate)
2017
2127
 
2018
2128
  @staticmethod
2019
2129
  def sphere(radius, tolerance = None):
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: bspy
3
- Version: 4.3
3
+ Version: 4.4.1
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
@@ -28,14 +28,13 @@ Requires-Dist: numpy
28
28
  Requires-Dist: scipy
29
29
  Requires-Dist: PyOpenGL
30
30
  Requires-Dist: pyopengltk
31
+ Dynamic: license-file
31
32
 
32
33
  # BSpy
33
34
  Library for manipulating and rendering B-spline curves, surfaces, and multidimensional manifolds with non-uniform knots in each dimension.
34
35
 
35
- 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).
36
-
37
- The [Spline](https://ericbrec.github.io/BSpy/bspy/spline.html) class has a method to fit multidimensional data for scalar and vector functions of single and multiple variables. It also can fit splines to functions, to solutions for ordinary differential equations (ODEs), and to geodesics.
38
- Spline has methods to create points, lines, circular arcs, spheres, cones, cylinders, tori, ruled surfaces, surfaces of revolution, and four-sided patches.
36
+ The [Spline](https://ericbrec.github.io/BSpy/bspy/spline.html) class has a method to fit multidimensional data for scalar and vector functions of single and multiple variables. It also can fit splines to functions, to solutions for ordinary differential equations (ODEs), geodesics, offsets, and lines of curvature.
37
+ Spline has methods to create points, lines, circular arcs, spheres, cones, cylinders, tori, ruled surfaces, surfaces of revolution, four-sided patches, and compositions of splines.
39
38
  Other methods add, subtract, and multiply splines, as well as confine spline curves to a given range.
40
39
  There are methods to evaluate spline values, derivatives, normals, integrals, continuity, curvature, and the Jacobian, as well as methods that return spline representations of derivatives, normals, integrals, graphs, and convolutions.
41
40
  In addition, there are methods to manipulate the domain of splines, including trim, join, split, reparametrize, transpose, reverse, add and remove knots, elevate and extrapolate, and fold and unfold.
@@ -43,16 +42,18 @@ There are methods to manipulate the range of splines, including dot product, cro
43
42
  Finally, there are methods to compute the zeros and contours of a spline and to intersect two splines.
44
43
  Splines can be saved and loaded in json format.
45
44
 
46
- The [Hyperplane](https://ericbrec.github.io/BSpy/bspy/hyperplane.html) class has methods to create individual hyperplanes in any dimension, along with axis-aligned hyperplanes and hypercubes.
47
-
48
- 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.
49
-
50
45
  The [SplineBlock](https://ericbrec.github.io/BSpy/bspy/spline_block.html) class has methods to process an array-like collection of splines that represent a system of equations. There are highly-optimized methods to compute the contours and zeros of a spline block, as well as a variety of methods to manipulate and evaluate a spline block and its derivatives.
51
46
 
52
47
  The [BSpyConvert](https://pypi.org/project/BSpyConvert/) package converts BSpy splines and solid models to and from [OpenCascade (OCCT)](https://dev.opencascade.org/) equivalents and a variety of geometry and CAD file formats, including STEP, IGES, and STL.
53
48
 
49
+ The [Hyperplane](https://ericbrec.github.io/BSpy/bspy/hyperplane.html) class has methods to create individual hyperplanes in any dimension, along with axis-aligned hyperplanes and hypercubes.
50
+
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
+
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.
54
+
54
55
  The [SplineOpenGLFrame](https://ericbrec.github.io/BSpy/bspy/splineOpenGLFrame.html) class is an
55
- [OpenGLFrame](https://pypi.org/project/pyopengltk/) with custom shaders to render spline curves and surfaces. Spline surfaces with more
56
+ [OpenGLFrame](https://pypi.org/project/pyopengltk/) with custom shaders to render spline curves, surfaces, and solids. Spline surfaces with more
56
57
  than 3 dependent variables will have their added dimensions rendered as colors (up to 6 dependent variables are supported). Only tested on Windows systems.
57
58
 
58
59
  The [Viewer](https://ericbrec.github.io/BSpy/bspy/viewer.html) class is a
@@ -60,7 +61,7 @@ The [Viewer](https://ericbrec.github.io/BSpy/bspy/viewer.html) class is a
60
61
  [SplineOpenGLFrame](https://ericbrec.github.io/BSpy/bspy/splineOpenGLFrame.html),
61
62
  a tree view full of solids and splines, and a set of controls to adjust and view the selected solids and splines. Only tested on Windows systems.
62
63
 
63
- The [Graphics](https://ericbrec.github.io/BSpy/bspy/viewer.html#Graphics) class is a graphics engine to display splines.
64
+ The [Graphics](https://ericbrec.github.io/BSpy/bspy/viewer.html#Graphics) class is a graphics engine to display solids and splines.
64
65
  It launches a [Viewer](https://ericbrec.github.io/BSpy/bspy/viewer.html) and issues commands to the viewer for use
65
66
  in [jupyter](https://jupyter.org/) notebooks and other scripting environments. Only tested on Windows systems.
66
67
 
@@ -0,0 +1,19 @@
1
+ bspy/__init__.py,sha256=LnJx7iHah7A4vud9y64LR61rBtYnuhV4Wno9O2IEK1I,1499
2
+ bspy/_spline_domain.py,sha256=NW9BJell382gbogq4lRlcDcaTCy3UZEd_9RrDM_hEYg,34466
3
+ bspy/_spline_evaluation.py,sha256=aQ9w4N5MQDol7_OQ6HLljMtCbGVuS5iCAlM7Sg2q8B4,9656
4
+ bspy/_spline_fitting.py,sha256=Rd_75bCNrd4fpKOcA0ujPD2hS-pvUQewaZqmDtld5QM,52205
5
+ bspy/_spline_intersection.py,sha256=SukAnnS9AFcBNkJfUFrIGWtd7HCp8P-WR8BmLw2IbUA,67635
6
+ bspy/_spline_milling.py,sha256=RTxCIVzFiVyypx3-1R9ZjtQEokz_yyERPNdJRLhU36U,13899
7
+ bspy/_spline_operations.py,sha256=O2AsfJb0PvaNbM2Rp3_AbNXKo8X6X6RJDwDclqOwT7o,43564
8
+ bspy/hyperplane.py,sha256=gnVZ7rjisGpzHfm1moItyzq8mO7HguzzpY4dpFwyDiw,24840
9
+ bspy/manifold.py,sha256=vjgyz0M1mkgenUnTIbX7NFg1fUCgXtStr6ofF4oSLgg,14470
10
+ bspy/solid.py,sha256=tsO7fcGj-x3SWH2fBSkeu-Hx5vX0KMFOt1baH-OYNgQ,36995
11
+ bspy/spline.py,sha256=PSfOCSBzoxNY5tnkTv_6yKOKToFiEnBR2r9-S8JaHd8,106788
12
+ bspy/splineOpenGLFrame.py,sha256=N8elVJrt24_utOSoTaM5Ue5De2M4DxrquyB7o2lLLD4,96256
13
+ bspy/spline_block.py,sha256=O8MzfBEygVdAx57DoJMwzjkw349BQqht7_RVu8MO0Fg,20127
14
+ bspy/viewer.py,sha256=_iQCyEpsBFPBLLuHq7tc43IPVvlcqxdp0Hig0uvpQns,34349
15
+ bspy-4.4.1.dist-info/licenses/LICENSE,sha256=nLfJULN68Jw6GfCJp4xeMksGuRdyWNdgEsZGjw2twig,1091
16
+ bspy-4.4.1.dist-info/METADATA,sha256=AHsaONNlgb7bZBRS-kyO4gmZTgmE7RjcdPZ_hgVQMt0,7140
17
+ bspy-4.4.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
18
+ bspy-4.4.1.dist-info/top_level.txt,sha256=fotZnJn6aCwgUbBEV3hslIko7Nw-eqtHLq2eyJLlFsY,5
19
+ bspy-4.4.1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.6.0)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
bspy-4.3.dist-info/RECORD DELETED
@@ -1,18 +0,0 @@
1
- bspy/__init__.py,sha256=LnJx7iHah7A4vud9y64LR61rBtYnuhV4Wno9O2IEK1I,1499
2
- bspy/_spline_domain.py,sha256=bQQsJlKstIYdbEKIW7vr-7nTKat8y9thYc7jxzZHNFQ,33238
3
- bspy/_spline_evaluation.py,sha256=WIv0tLZNLy0uHNj9YwR7vbputgT2Mn5QDXZzlD6ousk,9638
4
- bspy/_spline_fitting.py,sha256=CEkmUQalXTT5N5ZOJpFF4Y2DI-ARlZap9IaCJ_Zg2Ws,50762
5
- bspy/_spline_intersection.py,sha256=8FPTh4IDtzkRpieNtlnw8VhLabPzY4E_LWDaGxHIMTM,66973
6
- bspy/_spline_operations.py,sha256=8yJGp4iVVvQ1zcUHAKgNq2TMIjDUhacf5XSoHp2jmVo,42799
7
- bspy/hyperplane.py,sha256=gnVZ7rjisGpzHfm1moItyzq8mO7HguzzpY4dpFwyDiw,24840
8
- bspy/manifold.py,sha256=vjgyz0M1mkgenUnTIbX7NFg1fUCgXtStr6ofF4oSLgg,14470
9
- bspy/solid.py,sha256=ufNs5JV0jQ1A13pUY61N0pcW6Ep-DZmXUau7GHHcKk4,36992
10
- bspy/spline.py,sha256=1USitAm1FIQg5JuWa1xLrVQQ2cWRGfP4VhQnS8XOKc8,101377
11
- bspy/splineOpenGLFrame.py,sha256=N8elVJrt24_utOSoTaM5Ue5De2M4DxrquyB7o2lLLD4,96256
12
- bspy/spline_block.py,sha256=O8MzfBEygVdAx57DoJMwzjkw349BQqht7_RVu8MO0Fg,20127
13
- bspy/viewer.py,sha256=_iQCyEpsBFPBLLuHq7tc43IPVvlcqxdp0Hig0uvpQns,34349
14
- bspy-4.3.dist-info/LICENSE,sha256=nLfJULN68Jw6GfCJp4xeMksGuRdyWNdgEsZGjw2twig,1091
15
- bspy-4.3.dist-info/METADATA,sha256=lV6eciVqRf3FqWslbTD5OWb3gMD6-gv6WB-hkSRadPM,7044
16
- bspy-4.3.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
17
- bspy-4.3.dist-info/top_level.txt,sha256=fotZnJn6aCwgUbBEV3hslIko7Nw-eqtHLq2eyJLlFsY,5
18
- bspy-4.3.dist-info/RECORD,,