bspy 4.4__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
 
@@ -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
 
bspy/_spline_fitting.py CHANGED
@@ -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)
@@ -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,10 +973,10 @@ 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)
976
+ left = Solid(1, False)
978
977
  left.add_boundary(Boundary(Hyperplane(-1.0, zero[0], 0.0), Solid(0, True)))
979
978
  left.add_boundary(Boundary(Hyperplane(1.0, zero[1], 0.0), Solid(0, True)))
980
- right = Solid(nDep, False)
979
+ right = Solid(1, False)
981
980
  if planeBounds[0] > planeBounds[1]:
982
981
  planeBounds = (planeBounds[1], planeBounds[0])
983
982
  right.add_boundary(Boundary(Hyperplane(-1.0, planeBounds[0], 0.0), Solid(0, True)))
@@ -992,7 +991,7 @@ def intersect(self, other):
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.
@@ -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,10 +1040,10 @@ 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)
1043
+ left = Solid(self.nInd, False)
1045
1044
  left.add_boundary(Boundary(Hyperplane(-1.0, zero[0][0], 0.0), Solid(0, True)))
1046
1045
  left.add_boundary(Boundary(Hyperplane(1.0, zero[1][0], 0.0), Solid(0, True)))
1047
- right = Solid(nDep, False)
1046
+ right = Solid(other.nInd, False)
1048
1047
  right.add_boundary(Boundary(Hyperplane(-1.0, zero[0][1], 0.0), Solid(0, True)))
1049
1048
  right.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
@@ -1054,10 +1053,10 @@ def intersect(self, other):
1054
1053
  intersections.append(Manifold.Coincidence(left, right, 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.
@@ -1075,32 +1074,34 @@ def intersect(self, other):
1075
1074
  # Convert each contour into a Manifold.Crossing, swapping the manifolds back.
1076
1075
  for contour in contours:
1077
1076
  # 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)
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)
1080
1079
  intersections.append(Manifold.Crossing(left, right))
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)
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)
1086
1085
  intersections.append(Manifold.Crossing(left, right))
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 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)
1104
1105
 
1105
1106
  return intersections
1106
1107
 
bspy/_spline_milling.py CHANGED
@@ -1,6 +1,7 @@
1
1
  import numpy as np
2
2
  import bspy.spline
3
3
  import bspy.spline_block
4
+ from collections import namedtuple
4
5
 
5
6
  def line_of_curvature(self, uvStart, is_max, tolerance = 1.0e-3):
6
7
  if self.nInd != 2: raise ValueError("Surface must have two independent variables")
@@ -115,6 +116,60 @@ def offset(self, edgeRadius, bitRadius=None, angle=np.pi / 2.2, path=None, subtr
115
116
  if path is not None and (path.nInd != 1 or path.nDep != 2 or self.nInd != 2):
116
117
  raise ValueError("path must be a 2D curve and self must be a 3D surface")
117
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
+
118
173
  # Determine geometry of drill bit.
119
174
  if subtract:
120
175
  edgeRadius *= -1
@@ -126,79 +181,79 @@ def offset(self, edgeRadius, bitRadius=None, angle=np.pi / 2.2, path=None, subtr
126
181
 
127
182
  # Define drill bit function.
128
183
  if abs(w) < tolerance and path is None: # Simple offset curve or surface
129
- def drillBit(uv):
130
- return self(uv) + edgeRadius * self.normal(uv)
184
+ def drillBit(normal):
185
+ return edgeRadius * normal
131
186
  elif self.nDep == 2: # General offset curve
132
- def drillBit(u):
133
- xy = self(u)
134
- normal = self.normal(u)
187
+ def drillBit(normal):
135
188
  upward = np.sign(normal[1])
136
189
  if upward * normal[1] <= bottom:
137
- xy[0] += edgeRadius * normal[0] + w * np.sign(normal[0])
138
- xy[1] += edgeRadius * normal[1]
190
+ return np.array((edgeRadius * normal[0] + w * np.sign(normal[0]), edgeRadius * normal[1]))
139
191
  else:
140
- xy[0] += bottomRadius * normal[0]
141
- xy[1] += bottomRadius * normal[1] - upward * h
142
- return xy
143
- elif self.nDep == 3 and path is None: # General offset surface
144
- def drillBit(uv):
145
- xyz = self(uv)
146
- normal = self.normal(uv)
192
+ return np.array((bottomRadius * normal[0], bottomRadius * normal[1] - upward * h))
193
+ elif self.nDep == 3: # General offset surface
194
+ def drillBit(normal):
147
195
  upward = np.sign(normal[1])
148
196
  if upward * normal[1] <= bottom:
149
197
  norm = np.sqrt(normal[0] * normal[0] + normal[2] * normal[2])
150
- xyz[0] += edgeRadius * normal[0] + w * normal[0] / norm
151
- xyz[1] += edgeRadius * normal[1]
152
- xyz[2] += edgeRadius * normal[2] + w * normal[2] / norm
198
+ return np.array((edgeRadius * normal[0] + w * normal[0] / norm, edgeRadius * normal[1], edgeRadius * normal[2] + w * normal[2] / norm))
153
199
  else:
154
- xyz[0] += bottomRadius * normal[0]
155
- xyz[1] += bottomRadius * normal[1] - upward * h
156
- xyz[2] += bottomRadius * normal[2]
157
- return xyz
158
- elif self.nDep == 3: # General offset of a given path along a surface
159
- surface = self
160
- self = path # Redefine self to be the path (used below for fitting)
161
- def drillBit(u):
162
- uv = self(u)
163
- xyz = surface(uv)
164
- normal = surface.normal(uv)
165
- upward = np.sign(normal[1])
166
- if upward * normal[1] <= bottom:
167
- norm = np.sqrt(normal[0] * normal[0] + normal[2] * normal[2])
168
- xyz[0] += edgeRadius * normal[0] + w * normal[0] / norm
169
- xyz[1] += edgeRadius * normal[1]
170
- xyz[2] += edgeRadius * normal[2] + w * normal[2] / norm
171
- else:
172
- xyz[0] += bottomRadius * normal[0]
173
- xyz[1] += bottomRadius * normal[1] - upward * h
174
- xyz[2] += bottomRadius * normal[2]
175
- return xyz
200
+ return np.array((bottomRadius * normal[0], bottomRadius * normal[1] - upward * h, bottomRadius * normal[2]))
176
201
  else: # Should never get here (exception raised earlier)
177
202
  raise ValueError("The offset is only defined for 2D curves and 3D surfaces with well-defined normals.")
178
203
 
179
- # Compute new order and knots for offset (ensure order is at least 4).
180
- newOrder = []
181
- newKnots = []
182
- for order, knots in zip(self.order, self.knots):
183
- min4Order = max(order, 4)
184
- unique, count = np.unique(knots, return_counts=True)
185
- count += min4Order - order
186
- newOrder.append(min4Order)
187
- newKnots.append(np.repeat(unique, count))
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)
188
242
 
189
243
  # Fit new spline to offset by drill bit.
190
- offset = bspy.spline.Spline.fit(self.domain(), drillBit, newOrder, newKnots, tolerance)
244
+ offset = bspy.spline.Spline.fit(domain, fitFunction, newOrder, newKnotList, tolerance)
191
245
 
192
246
  # Remove cusps as required (only applies to offset curves).
193
- if removeCusps and self.nInd == 1:
247
+ if removeCusps and (self.nInd == 1 or path is not None):
194
248
  # Find the cusps by checking for tangent direction reversal between the spline and offset.
195
249
  cusps = []
196
250
  previousKnot = None
197
251
  start = None
198
252
  for knot in np.unique(offset.knots[0][offset.order[0]:offset.nCoef[0]]):
199
- tangent = self.derivative((1,), knot)
200
253
  if path is not None:
201
- tangent = surface.jacobian(path(knot)) @ tangent
254
+ tangent = self.jacobian(path(knot)) @ path.derivative((1,), knot)
255
+ else:
256
+ tangent = self.derivative((1,), knot)
202
257
  flipped = np.dot(tangent, offset.derivative((1,), knot)) < 0
203
258
  if flipped and start is None:
204
259
  start = knot
@@ -219,7 +274,7 @@ def offset(self, edgeRadius, bitRadius=None, angle=np.pi / 2.2, path=None, subtr
219
274
  # This is necessary to find the intersection point (2 equations, 2 unknowns).
220
275
  tangent = offset.derivative((1,), cusp[0])
221
276
  projection = np.concatenate((tangent / np.linalg.norm(tangent),
222
- surface.normal(path(cusp[0])))).reshape((2,3))
277
+ self.normal(path(cusp[0])))).reshape((2,3))
223
278
  before = before.transform(projection)
224
279
  after = after.transform(projection)
225
280
  block = bspy.spline_block.SplineBlock([[before, after]])
bspy/spline.py CHANGED
@@ -185,6 +185,25 @@ class Spline(Manifold):
185
185
  indMap = [(mapping, mapping) if np.isscalar(mapping) else mapping for mapping in indMap]
186
186
  return bspy._spline_operations.add(self, other, indMap)
187
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
+
188
207
  @staticmethod
189
208
  def bspline_values(knot, knots, splineOrder, u, derivativeOrder = 0, taylorCoefs = False):
190
209
  """
@@ -547,13 +566,13 @@ class Spline(Manifold):
547
566
 
548
567
  def contract(self, uvw):
549
568
  """
550
- 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.
551
570
 
552
571
  Parameters
553
572
  ----------
554
573
  uvw : `iterable`
555
574
  An iterable of length `nInd` that specifies the values of each independent variable to contract.
556
- 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.
557
576
 
558
577
  Returns
559
578
  -------
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bspy
3
- Version: 4.4
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
@@ -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 (80.0.0)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
bspy-4.4.dist-info/RECORD DELETED
@@ -1,19 +0,0 @@
1
- bspy/__init__.py,sha256=LnJx7iHah7A4vud9y64LR61rBtYnuhV4Wno9O2IEK1I,1499
2
- bspy/_spline_domain.py,sha256=pAoauqMJrY80k6bhnMJKnPWY_p_zx1k4daP3krsAN8M,33303
3
- bspy/_spline_evaluation.py,sha256=aQ9w4N5MQDol7_OQ6HLljMtCbGVuS5iCAlM7Sg2q8B4,9656
4
- bspy/_spline_fitting.py,sha256=TH2fwHu_wsRSvFIXngfWxOAHKex0ilP0Xi8ThXYQ0dI,52002
5
- bspy/_spline_intersection.py,sha256=CErinlW8co_kaamM9Wb1TFrbB6EmjlUh1exYlcXkrpQ,67349
6
- bspy/_spline_milling.py,sha256=OzhYi3eNPcL0INk7k_ryNwlt8SUniWL10ULxgE_nKHI,11435
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=vSscV6FRXjFp1FnhS1IoyFiy2pRFgr8-kZyKXw8weJU,105842
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.dist-info/licenses/LICENSE,sha256=nLfJULN68Jw6GfCJp4xeMksGuRdyWNdgEsZGjw2twig,1091
16
- bspy-4.4.dist-info/METADATA,sha256=8Y8G0jkCCuFdmJVb9CZgPJ5kuwaQivu89loQ6kyqlbc,7138
17
- bspy-4.4.dist-info/WHEEL,sha256=ck4Vq1_RXyvS4Jt6SI0Vz6fyVs4GWg7AINwpsaGEgPE,91
18
- bspy-4.4.dist-info/top_level.txt,sha256=fotZnJn6aCwgUbBEV3hslIko7Nw-eqtHLq2eyJLlFsY,5
19
- bspy-4.4.dist-info/RECORD,,