bspy 4.3__py3-none-any.whl → 4.4__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
@@ -330,6 +330,7 @@ def insert_knots(self, newKnotList):
330
330
  continue
331
331
 
332
332
  # Check if knot and its total multiplicity is valid.
333
+ knot = knots.dtype.type(knot) # Cast to correct type
333
334
  if knot < knots[degree] or knot > knots[-order]:
334
335
  raise ValueError(f"Knot insertion outside domain: {knot}")
335
336
  position = np.searchsorted(knots, knot, 'right')
@@ -499,7 +500,7 @@ def remove_knots(self, tolerance, nLeft = 0, nRight = 0):
499
500
  foldedIndices = list(filter(lambda x: x != id, indIndex))
500
501
  currentFold, foldedBasis = currentSpline.fold(foldedIndices)
501
502
  while True:
502
- bestError = np.finfo(scaleDep[0].dtype).max
503
+ bestError = np.finfo(self.coefs.dtype).max
503
504
  bestSpline = currentFold
504
505
  ix = currentFold.order[0]
505
506
  while ix < currentFold.nCoef[0]:
@@ -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:
@@ -366,9 +386,10 @@ def fit(domain, f, order = None, knots = None, tolerance = 1.0e-4):
366
386
  indices = nInd * [0]
367
387
  iLast = nInd
368
388
  while iLast >= 0:
389
+ # Create a tuple for the u value (must be a tuple to use it as a dictionary key)
369
390
  uValue = tuple([uvw[i][indices[i]] for i in range(nInd)])
370
391
  if not uValue in fDictionary:
371
- fDictionary[uValue] = f(uValue)
392
+ fDictionary[uValue] = f(np.array(uValue))
372
393
  fValues.append(fDictionary[uValue])
373
394
  iLast = nInd - 1
374
395
  while iLast >= 0:
@@ -518,7 +539,7 @@ def four_sided_patch(bottom, right, top, left, surfParam = 0.5):
518
539
 
519
540
  return (1.0 - surfParam) * coons + surfParam * laplace
520
541
 
521
- def geodesic(self, uvStart, uvEnd, tolerance = 1.0e-6):
542
+ def geodesic(self, uvStart, uvEnd, tolerance = 1.0e-5):
522
543
  # Check validity of input
523
544
  if self.nInd != 2: raise ValueError("Surface must have two independent variables")
524
545
  if len(uvStart) != 2: raise ValueError("uvStart must have two components")
@@ -616,7 +637,7 @@ def geodesic(self, uvStart, uvEnd, tolerance = 1.0e-6):
616
637
  initialGuess = line(uvStart, uvEnd).elevate([2])
617
638
 
618
639
  # Solve the ODE and return the geodesic
619
- solution = initialGuess.solve_ode(1, 1, geodesicCallback, 1.0e-5, (self, uvDomain))
640
+ solution = initialGuess.solve_ode(1, 1, geodesicCallback, tolerance, (self, uvDomain))
620
641
  return solution
621
642
 
622
643
  def least_squares(uValues, dataPoints, order = None, knots = None, compression = 0.0,
@@ -878,7 +899,7 @@ def section(xytk):
878
899
  # Join the pieces together and return
879
900
  return bspy.Spline.join(mySections)
880
901
 
881
- def solve_ode(self, nLeft, nRight, FAndF_u, tolerance = 1.0e-6, args = ()):
902
+ def solve_ode(self, nLeft, nRight, FAndF_u, tolerance = 1.0e-6, args = (), includeEstimate = False):
882
903
  # Ensure that the ODE is properly formulated
883
904
 
884
905
  if nLeft < 0: raise ValueError("Invalid number of left hand boundary conditions")
@@ -970,7 +991,7 @@ def solve_ode(self, nLeft, nRight, FAndF_u, tolerance = 1.0e-6, args = ()):
970
991
  residuals = np.append(residuals, np.zeros((nLeft * nDep,)))
971
992
  collocationMatrix[bandWidth, 0 : nLeft * nDep] = 1.0
972
993
  for iPoint, t in enumerate(collocationPoints[iFirstPoint : iNextPoint]):
973
- uData = np.array([workingSpline.derivative([i], t) for i in range(nOrder)]).T
994
+ uData = np.array([workingSpline.derivative([i], t) for i in range(nOrder + 1 if includeEstimate else nOrder)]).T
974
995
  F, F_u = FAndF_u(t, uData, *args)
975
996
  residuals = np.append(residuals, workingSpline.derivative([nOrder], t) - continuation * F)
976
997
  ix = None
@@ -958,6 +958,8 @@ def intersect(self, other):
958
958
  zeros = spline.zeros()
959
959
  # Convert each intersection point into a Manifold.Crossing and each intersection interval into a Manifold.Coincidence.
960
960
  for zero in zeros:
961
+ if isinstance(zero, tuple) and zero[1] - zero[0] < Manifold.minSeparation:
962
+ zero = 0.5 * (zero[0] + zero[1])
961
963
  if isinstance(zero, tuple):
962
964
  # Intersection is an interval, so create a Manifold.Coincidence.
963
965
  planeBounds = (projection @ (self((zero[0],)) - other._point), projection @ (self((zero[1],)) - other._point))
@@ -1019,6 +1021,8 @@ def intersect(self, other):
1019
1021
  zeros = block.zeros()
1020
1022
  # Convert each intersection point into a Manifold.Crossing and each intersection interval into a Manifold.Coincidence.
1021
1023
  for zero in zeros:
1024
+ if isinstance(zero, tuple) and zero[1] - zero[0] < Manifold.minSeparation:
1025
+ zero = 0.5 * (zero[0] + zero[1])
1022
1026
  if isinstance(zero, tuple):
1023
1027
  # Intersection is an interval, so create a Manifold.Coincidence.
1024
1028
 
@@ -1135,14 +1139,14 @@ def complete_slice(self, slice, solid):
1135
1139
  newBoundary.touched = False
1136
1140
 
1137
1141
  # Define function for adding slice points to full domain boundaries.
1138
- def process_domain_point(boundary, domainPoint):
1142
+ def process_domain_point(boundary, domainPoint, adjustment):
1139
1143
  point = boundary.manifold.evaluate(domainPoint)
1140
1144
  # See if and where point touches full domain.
1141
1145
  for newBoundary in fullDomain.boundaries:
1142
1146
  vector = point - newBoundary.manifold._point
1143
1147
  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))
1148
+ # Add the point onto the new boundary (adjust normal evaluation point to move away from boundary).
1149
+ normal = np.sign(newBoundary.manifold._tangentSpace.T @ boundary.manifold.normal(domainPoint + adjustment))
1146
1150
  newBoundary.domain.add_boundary(Boundary(Hyperplane(normal, newBoundary.manifold._tangentSpace.T @ vector, 0.0), Solid(0, True)))
1147
1151
  newBoundary.touched = True
1148
1152
  break
@@ -1151,9 +1155,9 @@ def complete_slice(self, slice, solid):
1151
1155
  for boundary in slice.boundaries:
1152
1156
  domainBoundaries = boundary.domain.boundaries
1153
1157
  domainBoundaries.sort(key=lambda boundary: (boundary.manifold.evaluate(0.0), boundary.manifold.normal(0.0)))
1154
- process_domain_point(boundary, domainBoundaries[0].manifold._point)
1158
+ process_domain_point(boundary, domainBoundaries[0].manifold._point, Manifold.minSeparation)
1155
1159
  if len(domainBoundaries) > 1:
1156
- process_domain_point(boundary, domainBoundaries[-1].manifold._point)
1160
+ process_domain_point(boundary, domainBoundaries[-1].manifold._point, -Manifold.minSeparation)
1157
1161
 
1158
1162
  # For touched boundaries, remove domain bounds that aren't needed and then add boundary to slice.
1159
1163
  boundaryWasTouched = False
@@ -1161,7 +1165,6 @@ def complete_slice(self, slice, solid):
1161
1165
  if newBoundary.touched:
1162
1166
  boundaryWasTouched = True
1163
1167
  domainBoundaries = newBoundary.domain.boundaries
1164
- assert len(domainBoundaries) > 2
1165
1168
  domainBoundaries.sort(key=lambda boundary: (boundary.manifold.evaluate(0.0), boundary.manifold.normal(0.0)))
1166
1169
  # Ensure domain endpoints don't overlap and their normals are consistent.
1167
1170
  if abs(domainBoundaries[0].manifold._point - domainBoundaries[1].manifold._point) < Manifold.minSeparation or \
@@ -0,0 +1,233 @@
1
+ import numpy as np
2
+ import bspy.spline
3
+ import bspy.spline_block
4
+
5
+ def line_of_curvature(self, uvStart, is_max, tolerance = 1.0e-3):
6
+ if self.nInd != 2: raise ValueError("Surface must have two independent variables")
7
+ if len(uvStart) != 2: raise ValueError("uvStart must have two components")
8
+ uvDomain = self.domain()
9
+ if uvStart[0] < uvDomain[0, 0] or uvStart[0] > uvDomain[0, 1] or \
10
+ uvStart[1] < uvDomain[1, 0] or uvStart[1] > uvDomain[1, 1]:
11
+ raise ValueError("uvStart is outside domain of the surface")
12
+ is_max = bool(is_max) # Ensure is_max is a boolean for XNOR operation
13
+
14
+ # Define the callback function for the ODE solver
15
+ def curvatureLineCallback(t, u):
16
+ # Evaluate the surface information needed.
17
+ uv = np.maximum(uvDomain[:, 0], np.minimum(uvDomain[:, 1], u[:, 0]))
18
+ su = self.derivative((1, 0), uv)
19
+ sv = self.derivative((0, 1), uv)
20
+ suu = self.derivative((2, 0), uv)
21
+ suv = self.derivative((1, 1), uv)
22
+ svv = self.derivative((0, 2), uv)
23
+ suuu = self.derivative((3, 0), uv)
24
+ suuv = self.derivative((2, 1), uv)
25
+ suvv = self.derivative((1, 2), uv)
26
+ svvv = self.derivative((0, 3), uv)
27
+ normal = self.normal(uv)
28
+
29
+ # Calculate curvature matrix and its derivatives.
30
+ sU = np.concatenate((su, sv)).reshape(2, -1)
31
+ sUu = np.concatenate((suu, suv)).reshape(2, -1)
32
+ sUv = np.concatenate((suv, svv)).reshape(2, -1)
33
+ sUU = np.concatenate((suu, suv, suv, svv)).reshape(2, 2, -1)
34
+ sUUu = np.concatenate((suuu, suuv, suuv, suvv)).reshape(2, 2, -1)
35
+ sUUv = np.concatenate((suuv, suvv, suvv, svvv)).reshape(2, 2, -1)
36
+ fffI = np.linalg.inv(sU @ sU.T) # Inverse of first fundamental form
37
+ k = fffI @ (sUU @ normal) # Curvature matrix
38
+ ku = fffI @ (sUUu @ normal - (sUu @ sU.T + sU @ sUu.T) @ k - sUU @ (sU.T @ k[:, 0]))
39
+ kv = fffI @ (sUUv @ normal - (sUv @ sU.T + sU @ sUv.T) @ k - sUU @ (sU.T @ k[:, 1]))
40
+
41
+ # Determine principle curvatures and directions, and assign new direction.
42
+ curvatures, directions = np.linalg.eig(k)
43
+ curvatureDelta = curvatures[1] - curvatures[0]
44
+ if abs(curvatureDelta) < tolerance:
45
+ # If we're at an umbilic, use the last direction (jacobian is zero at umbilic).
46
+ direction = u[:, 1]
47
+ jacobian = np.zeros((2,2,1), self.coefs.dtype)
48
+ else:
49
+ # Otherwise, compute the lhs inverse for the jacobian.
50
+ directionsInverse = np.linalg.inv(directions)
51
+ eigenIndex = 0 if bool(curvatures[0] > curvatures[1]) == is_max else 1
52
+ direction = directions[:, eigenIndex]
53
+ B = np.zeros((2, 2), self.coefs.dtype)
54
+ B[0, 1 - eigenIndex] = np.dot(directions[:, 1], direction) / curvatureDelta
55
+ B[1, 1 - eigenIndex] = -np.dot(directions[:, 0], direction) / curvatureDelta
56
+ lhsInv = directions @ B @ directionsInverse
57
+
58
+ # Adjust the direction for consistency.
59
+ if np.dot(direction, u[:, 1]) < -tolerance:
60
+ direction *= -1
61
+
62
+ # Compute the jacobian for the direction.
63
+ jacobian = np.empty((2,2,1), self.coefs.dtype)
64
+ jacobian[:,0,0] = lhsInv @ ku @ direction
65
+ jacobian[:,1,0] = lhsInv @ kv @ direction
66
+
67
+ return direction, jacobian
68
+
69
+ # Generate the initial guess for the line of curvature.
70
+ uvStart = np.atleast_1d(uvStart)
71
+ direction = 0.5 * (uvDomain[:,0] + uvDomain[:,1]) - uvStart # Initial guess toward center
72
+ distanceFromCenter = np.linalg.norm(direction)
73
+ if distanceFromCenter < 10 * tolerance:
74
+ # If we're at the center, just point to the far corner.
75
+ direction = np.array((1.0, 1.0)) / np.sqrt(2)
76
+ else:
77
+ direction /= distanceFromCenter
78
+
79
+ # Compute line of curvature direction at start.
80
+ direction, jacobian = curvatureLineCallback(0.0, np.array(((uvStart[0], direction[0]), (uvStart[1], direction[1]))))
81
+
82
+ # Calculate distance to the boundary in that direction.
83
+ if direction[0] < -tolerance:
84
+ uBoundaryDistance = (uvDomain[0, 0] - uvStart[0]) / direction[0]
85
+ elif direction[0] > tolerance:
86
+ uBoundaryDistance = (uvDomain[0, 1] - uvStart[0]) / direction[0]
87
+ else:
88
+ uBoundaryDistance = np.inf
89
+ if direction[1] < -tolerance:
90
+ vBoundaryDistance = (uvDomain[1, 0] - uvStart[1]) / direction[1]
91
+ elif direction[1] > tolerance:
92
+ vBoundaryDistance = (uvDomain[1, 1] - uvStart[1]) / direction[1]
93
+ else:
94
+ vBoundaryDistance = np.inf
95
+ boundaryDistance = min(uBoundaryDistance, vBoundaryDistance)
96
+
97
+ # Construct the initial guess from start point to boundary.
98
+ initialGuess = bspy.spline.Spline.line(uvStart, uvStart + boundaryDistance * direction).elevate([2])
99
+
100
+ # Solve the ODE and return the line of curvature confined to the surface's domain.
101
+ solution = initialGuess.solve_ode(1, 0, curvatureLineCallback, tolerance, includeEstimate = True)
102
+ return solution.confine(uvDomain)
103
+
104
+ def offset(self, edgeRadius, bitRadius=None, angle=np.pi / 2.2, path=None, subtract=False, removeCusps=False, tolerance = 1.0e-4):
105
+ 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.")
106
+ if edgeRadius < 0:
107
+ raise ValueError("edgeRadius must be >= 0")
108
+ elif edgeRadius == 0:
109
+ return self
110
+ if bitRadius is None:
111
+ bitRadius = edgeRadius
112
+ elif bitRadius < edgeRadius:
113
+ raise ValueError("bitRadius must be >= edgeRadius")
114
+ if angle < 0 or angle >= np.pi / 2: raise ValueError("angle must in the range [0, pi/2)")
115
+ if path is not None and (path.nInd != 1 or path.nDep != 2 or self.nInd != 2):
116
+ raise ValueError("path must be a 2D curve and self must be a 3D surface")
117
+
118
+ # Determine geometry of drill bit.
119
+ if subtract:
120
+ edgeRadius *= -1
121
+ bitRadius *= -1
122
+ w = bitRadius - edgeRadius
123
+ h = w * np.tan(angle)
124
+ bottom = np.sin(angle)
125
+ bottomRadius = edgeRadius + h / bottom
126
+
127
+ # Define drill bit function.
128
+ 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)
131
+ elif self.nDep == 2: # General offset curve
132
+ def drillBit(u):
133
+ xy = self(u)
134
+ normal = self.normal(u)
135
+ upward = np.sign(normal[1])
136
+ if upward * normal[1] <= bottom:
137
+ xy[0] += edgeRadius * normal[0] + w * np.sign(normal[0])
138
+ xy[1] += edgeRadius * normal[1]
139
+ 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)
147
+ upward = np.sign(normal[1])
148
+ if upward * normal[1] <= bottom:
149
+ 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
153
+ 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
176
+ else: # Should never get here (exception raised earlier)
177
+ raise ValueError("The offset is only defined for 2D curves and 3D surfaces with well-defined normals.")
178
+
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))
188
+
189
+ # Fit new spline to offset by drill bit.
190
+ offset = bspy.spline.Spline.fit(self.domain(), drillBit, newOrder, newKnots, tolerance)
191
+
192
+ # Remove cusps as required (only applies to offset curves).
193
+ if removeCusps and self.nInd == 1:
194
+ # Find the cusps by checking for tangent direction reversal between the spline and offset.
195
+ cusps = []
196
+ previousKnot = None
197
+ start = None
198
+ for knot in np.unique(offset.knots[0][offset.order[0]:offset.nCoef[0]]):
199
+ tangent = self.derivative((1,), knot)
200
+ if path is not None:
201
+ tangent = surface.jacobian(path(knot)) @ tangent
202
+ flipped = np.dot(tangent, offset.derivative((1,), knot)) < 0
203
+ if flipped and start is None:
204
+ start = knot
205
+ if not flipped and start is not None:
206
+ cusps.append((start, previousKnot))
207
+ start = None
208
+ previousKnot = knot
209
+
210
+ # Remove the cusps by intersecting the offset segments before and after each cusp.
211
+ segmentList = []
212
+ for cusp in cusps:
213
+ domain = offset.domain()
214
+ before = offset.trim(((domain[0][0], cusp[0]),))
215
+ after = -offset.trim(((cusp[1], domain[0][1]),))
216
+ if path is not None:
217
+ # Project before and after onto a 2D plane defined by the offset tangent
218
+ # and the surface normal at the start of the cusp.
219
+ # This is necessary to find the intersection point (2 equations, 2 unknowns).
220
+ tangent = offset.derivative((1,), cusp[0])
221
+ projection = np.concatenate((tangent / np.linalg.norm(tangent),
222
+ surface.normal(path(cusp[0])))).reshape((2,3))
223
+ before = before.transform(projection)
224
+ after = after.transform(projection)
225
+ block = bspy.spline_block.SplineBlock([[before, after]])
226
+ intersections = block.zeros()
227
+ for intersection in intersections:
228
+ segmentList.append(offset.trim(((domain[0][0], intersection[0]),)))
229
+ offset = offset.trim(((intersection[1], domain[0][1]),))
230
+ segmentList.append(offset)
231
+ offset = bspy.spline.Spline.join(segmentList)
232
+
233
+ 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
@@ -974,15 +975,15 @@ class Spline(Manifold):
974
975
  resulting spline function will have nInd + number of independent variables
975
976
  in the splines returned independent variables and nDep dependent variables.
976
977
 
977
- order : `array-like`
978
+ order : `array-like`, optional
978
979
  An optional integer array of length nInd which specifies the polynomial
979
980
  order to use in each of the independent variables. It will default to order
980
981
  4 (degree 3) if None is specified (the default)
981
982
 
982
- knots : `array-like`
983
+ knots : `array-like`, optional
983
984
  The initial knot sequence to use, if given
984
985
 
985
- tolerance : `scalar`
986
+ tolerance : `scalar`, optional
986
987
  The maximum 2-norm of the difference between the given function and the
987
988
  spline fit. Defaults to 1.0e-4.
988
989
 
@@ -1129,7 +1130,7 @@ class Spline(Manifold):
1129
1130
  """
1130
1131
  return bspy._spline_intersection.full_domain(self)
1131
1132
 
1132
- def geodesic(self, uvStart, uvEnd, tolerance = 1.0e-6):
1133
+ def geodesic(self, uvStart, uvEnd, tolerance = 1.0e-5):
1133
1134
  """
1134
1135
  Determine a geodesic between two points on a surface
1135
1136
 
@@ -1141,9 +1142,9 @@ class Spline(Manifold):
1141
1142
  uvEnd : `array-like`
1142
1143
  The parameter values for the surface at the other end of the desired geodesic.
1143
1144
 
1144
- tolerance : scalar
1145
+ tolerance : scalar, optional
1145
1146
  The maximum error in parameter space to which the geodesic should get computed.
1146
- Defaults to 1.0e-6.
1147
+ Defaults to 1.0e-5.
1147
1148
 
1148
1149
  Returns
1149
1150
  -------
@@ -1499,6 +1500,36 @@ class Spline(Manifold):
1499
1500
  """
1500
1501
  return bspy._spline_fitting.line(startPoint, endPoint)
1501
1502
 
1503
+ def line_of_curvature(self, uvStart, is_max = True, tolerance = 1.0e-3):
1504
+ """
1505
+ Determine a line of curvature along a surface
1506
+
1507
+ Parameters
1508
+ ----------
1509
+ uvStart : `array-like`
1510
+ The parameter values for the surface at one end of the desired line of curvature.
1511
+
1512
+ is_max : `bool`, optional
1513
+ Boolean value indicating that the line of curvature should be the maximal curvature line.
1514
+ If False, the minimal curvature line is returned. Defaults to True.
1515
+
1516
+ tolerance : scalar, optional
1517
+ The maximum error in parameter space to which the geodesic should get computed.
1518
+ Defaults to 1.0e-3.
1519
+
1520
+ Returns
1521
+ -------
1522
+ spline : `Spline`
1523
+ A spline curve whose range is in the domain of the given surface. The range of the
1524
+ curve is the locus of points whose image under the surface map form the line of curvature
1525
+ starting at the given point.
1526
+
1527
+ See Also
1528
+ --------
1529
+ `solve_ode` : Solve an ordinary differential equation using spline collocation.
1530
+ """
1531
+ return bspy._spline_milling.line_of_curvature(self, uvStart, is_max, tolerance)
1532
+
1502
1533
  @staticmethod
1503
1534
  def load(fileName):
1504
1535
  """
@@ -1653,6 +1684,61 @@ class Spline(Manifold):
1653
1684
  the matrix formed by the tangents of the spline. If the null space is greater than one dimension, the normal will be zero.
1654
1685
  """
1655
1686
  return bspy._spline_operations.normal_spline(bspy.spline_block.SplineBlock(self), indices)
1687
+
1688
+ def offset(self, edgeRadius, bitRadius=None, angle=np.pi / 2.2, path=None, subtract=False, removeCusps=False, tolerance = 1.0e-4):
1689
+ """
1690
+ Compute the offset of a spline to a given tolerance.
1691
+
1692
+ Parameters
1693
+ ----------
1694
+ edgeRadius : scalar
1695
+ The radius of offset. If a bit radius is specified, the edge radius is the
1696
+ smaller radius of the cutting edge of the drill bit, whereas bit radius specifies
1697
+ half of the full width of the drill bit.
1698
+
1699
+ bitRadius : scalar, optional
1700
+ The radius of the drill bit (half its full width). For a ball nose cutter (the default),
1701
+ the bit radius is the same as the edge radius. For an end mill,
1702
+ the bit radius is larger (typically much larger) than the edge radius.
1703
+
1704
+ angle : scalar, optional
1705
+ The angle at which the drill bit transitions from the edge radius to the
1706
+ flatter bottom of the drill bit. The angle must be in the range [0, pi/2).
1707
+ Defaults to pi / 2.2.
1708
+
1709
+ path : `Spline`, optional
1710
+ The path along self that the drill bit should contact.
1711
+ If specified, the path must be a 2D curve in the domain of self, self must be a 3D surface,
1712
+ and the offset returned is a 3D curve providing the 3D position of the drill bit,
1713
+ rather than the full offset surface. Defaults to None.
1714
+
1715
+ subtract : boolean, optional
1716
+ Flag indicating if the drill bit should be subtracted from the spline instead of added.
1717
+ Subtracting the drill bit returns the tool path that cuts out the spline. Defaults to False.
1718
+
1719
+ removeCusps : boolean, optional
1720
+ Flag indicating if cusps and their associated self-intersections should be removed from the
1721
+ offset. Only applicable to offset curves and paths along offset surfaces. Defaults to False.
1722
+
1723
+ tolerance : `scalar`, optional
1724
+ The maximum 2-norm of the difference between the offset and the
1725
+ spline fit. Defaults to 1.0e-4.
1726
+
1727
+ Returns
1728
+ -------
1729
+ offset : `Spline`
1730
+ The spline that represents the offset.
1731
+
1732
+ See Also
1733
+ --------
1734
+ `fit` : Fit the function f with a spline to a given tolerance.
1735
+
1736
+ Notes
1737
+ -----
1738
+ The offset is only defined for 2D curves and 3D surfaces with well-defined normals.
1739
+ The bottom of the drill bit is tangent to its lowest y value.
1740
+ """
1741
+ return bspy._spline_milling.offset(self, edgeRadius, bitRadius, angle, path, subtract, removeCusps, tolerance)
1656
1742
 
1657
1743
  @staticmethod
1658
1744
  def point(point):
@@ -1971,7 +2057,7 @@ class Spline(Manifold):
1971
2057
  """
1972
2058
  return bspy._spline_fitting.section(xytk)
1973
2059
 
1974
- def solve_ode(self, nLeft, nRight, FAndF_u, tolerance = 1.0e-6, args = ()):
2060
+ def solve_ode(self, nLeft, nRight, FAndF_u, tolerance = 1.0e-6, args = (), includeEstimate = False):
1975
2061
  """
1976
2062
  Numerically solve an ordinary differential equation with boundary conditions.
1977
2063
 
@@ -1994,26 +2080,31 @@ class Spline(Manifold):
1994
2080
  FAndF_u : Python function
1995
2081
  FAndF_u must have exactly this calling sequence: FAndF_u(t, uData, *args). t is a scalar set
1996
2082
  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
2083
+ (self.nDep, nOrder) whose columns are u, ... , u^(nOrder - 1). It must return a numpy
1998
2084
  vector of length self.nDep and a numpy array whose shape is (self.nDep, self.nDep, nOrder).
1999
2085
  The first output vector is the value of the forcing function F at (t, uData). The numpy
2000
2086
  array is the array of partial derivatives with respect to all the numbers in uData. Thus, if
2001
2087
  this array is called jacobian, then jacobian[:, i, j] is the gradient of the forcing function with
2002
2088
  respect to uData[i, j].
2003
2089
 
2004
- tolerance : scalar
2005
- The relative error to which the ODE should get solved.
2090
+ tolerance : scalar, optional
2091
+ The relative error to which the ODE should get solved. Default is 1.0e-6.
2006
2092
 
2007
- args : tuple
2093
+ args : tuple, optional
2008
2094
  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.
2095
+ FAndF_u(t, uData, a, b, c), then args must be a tuple of length 3. Default is ().
2096
+
2097
+ includeEstimate : bool, optional
2098
+ If `includeEstimate` is True, the uData passed to `FAndF_u` will be a numpy matrix of shape
2099
+ (self.nDep, nOrder + 1) whose columns are u, ... , u^(nOrder). The last column will be the most
2100
+ recent estimate of u^(nOrder)(t). Default is False.
2010
2101
 
2011
2102
  Notes
2012
2103
  =====
2013
2104
  This method uses B-splines as finite elements. The ODE itself is discretized using
2014
2105
  collocation.
2015
2106
  """
2016
- return bspy._spline_fitting.solve_ode(self, nLeft, nRight, FAndF_u, tolerance, args)
2107
+ return bspy._spline_fitting.solve_ode(self, nLeft, nRight, FAndF_u, tolerance, args, includeEstimate)
2017
2108
 
2018
2109
  @staticmethod
2019
2110
  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
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=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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.6.0)
2
+ Generator: setuptools (80.0.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,,
File without changes