bspy 4.4.1__py3-none-any.whl → 5.0.0__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_evaluation.py +1 -1
- bspy/_spline_intersection.py +66 -66
- bspy/_spline_milling.py +3 -3
- bspy/hyperplane.py +35 -35
- bspy/manifold.py +40 -40
- bspy/solid.py +162 -159
- bspy/spline.py +28 -28
- bspy/splineOpenGLFrame.py +7 -7
- bspy/viewer.py +3 -3
- {bspy-4.4.1.dist-info → bspy-5.0.0.dist-info}/METADATA +8 -2
- bspy-5.0.0.dist-info/RECORD +19 -0
- {bspy-4.4.1.dist-info → bspy-5.0.0.dist-info}/WHEEL +1 -1
- bspy-4.4.1.dist-info/RECORD +0 -19
- {bspy-4.4.1.dist-info → bspy-5.0.0.dist-info}/licenses/LICENSE +0 -0
- {bspy-4.4.1.dist-info → bspy-5.0.0.dist-info}/top_level.txt +0 -0
bspy/solid.py
CHANGED
|
@@ -12,7 +12,7 @@ class Boundary:
|
|
|
12
12
|
manifold : `Manifold`
|
|
13
13
|
The differentiable function whose range is one dimension higher than its domain that defines the range of the boundary.
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
trim : `Solid`, optional
|
|
16
16
|
The region of the domain of the manifold that's within the boundary. The default is the full domain of the manifold.
|
|
17
17
|
|
|
18
18
|
See also
|
|
@@ -20,14 +20,14 @@ class Boundary:
|
|
|
20
20
|
`Solid` : A region that separates space into an inside and outside, defined by a collection of boundaries.
|
|
21
21
|
`Manifold.full_domain` : Return a solid that represents the full domain of the manifold.
|
|
22
22
|
"""
|
|
23
|
-
def __init__(self, manifold,
|
|
24
|
-
self.
|
|
25
|
-
if manifold.domain_dimension() != self.
|
|
23
|
+
def __init__(self, manifold, trim = None):
|
|
24
|
+
self.trim = manifold.full_domain() if trim is None else trim
|
|
25
|
+
if manifold.domain_dimension() != self.trim.dimension: raise ValueError("Domain dimensions don't match")
|
|
26
26
|
if manifold.domain_dimension() + 1 != manifold.range_dimension(): raise ValueError("Manifold range is not one dimension higher than domain")
|
|
27
|
-
self.manifold, self.bounds = manifold.trimmed_range_bounds(self.
|
|
27
|
+
self.manifold, self.bounds = manifold.trimmed_range_bounds(self.trim.bounds)
|
|
28
28
|
|
|
29
29
|
def __repr__(self):
|
|
30
|
-
return "Boundary({0}, {1})".format(self.manifold.__repr__(), self.
|
|
30
|
+
return "Boundary({0}, {1})".format(self.manifold.__repr__(), self.trim.__repr__())
|
|
31
31
|
|
|
32
32
|
def any_point(self):
|
|
33
33
|
"""
|
|
@@ -44,9 +44,9 @@ class Boundary:
|
|
|
44
44
|
|
|
45
45
|
Notes
|
|
46
46
|
-----
|
|
47
|
-
The point is computed by evaluating the boundary manifold by an arbitrary point in the
|
|
47
|
+
The point is computed by evaluating the boundary manifold by an arbitrary point in the trim of the boundary.
|
|
48
48
|
"""
|
|
49
|
-
return self.manifold.evaluate(self.
|
|
49
|
+
return self.manifold.evaluate(self.trim.any_point())
|
|
50
50
|
|
|
51
51
|
class Solid:
|
|
52
52
|
"""
|
|
@@ -71,7 +71,7 @@ class Solid:
|
|
|
71
71
|
-----
|
|
72
72
|
Solids also contain a `list` of `boundaries`. That list may be empty.
|
|
73
73
|
|
|
74
|
-
Solids can be of zero dimension, typically acting as the
|
|
74
|
+
Solids can be of zero dimension, typically acting as the trim of boundary endpoints. Zero-dimension solids have no boundaries, they only contain infinity or not.
|
|
75
75
|
"""
|
|
76
76
|
def __init__(self, dimension, containsInfinity, metadata = {}):
|
|
77
77
|
assert dimension >= 0
|
|
@@ -171,9 +171,123 @@ class Solid:
|
|
|
171
171
|
"""
|
|
172
172
|
solid = Solid(self.dimension, not self.containsInfinity)
|
|
173
173
|
for boundary in self.boundaries:
|
|
174
|
-
solid.add_boundary(Boundary(boundary.manifold.
|
|
174
|
+
solid.add_boundary(Boundary(boundary.manifold.negate_normal(), boundary.trim))
|
|
175
175
|
return solid
|
|
176
176
|
|
|
177
|
+
def compute_cutout(self, manifold, cache = None, trimTwin = False):
|
|
178
|
+
"""
|
|
179
|
+
Compute the cutout portion of the manifold within the solid.
|
|
180
|
+
|
|
181
|
+
Parameters
|
|
182
|
+
----------
|
|
183
|
+
manifold : `Manifold`
|
|
184
|
+
The `Manifold` for which we are computing a cutout within this solid.
|
|
185
|
+
|
|
186
|
+
cache : `dict`, optional
|
|
187
|
+
A dictionary to cache `Manifold` intersections, speeding computation.
|
|
188
|
+
|
|
189
|
+
trimTwin : `bool`, default: False
|
|
190
|
+
Trim coincident boundary twins on subsequent calls to cutout (avoids duplication of overlapping regions).
|
|
191
|
+
Trimming twins is typically only used in conjunction with `intersection`.
|
|
192
|
+
|
|
193
|
+
Returns
|
|
194
|
+
-------
|
|
195
|
+
cutout : `Solid`
|
|
196
|
+
A region in the domain of `manifold` that intersects with the solid. The region may contain infinity.
|
|
197
|
+
|
|
198
|
+
See Also
|
|
199
|
+
--------
|
|
200
|
+
`intersection` : Intersect two solids.
|
|
201
|
+
`Manifold.intersect` : Intersect two manifolds.
|
|
202
|
+
`Manifold.cached_intersect` : Intersect two manifolds, caching the result for twins.
|
|
203
|
+
`Manifold.complete_cutout` : Add any missing inherent (implicit) boundaries of this manifold's domain to the given cutout.
|
|
204
|
+
|
|
205
|
+
Notes
|
|
206
|
+
-----
|
|
207
|
+
The dimension of the cutout is always one less than the dimension of the solid, since the cutout is a region in the domain of the manifold.
|
|
208
|
+
|
|
209
|
+
To compute the cutout of a manifold intersecting the solid, we intersect the manifold with each boundary of the solid. There may be multiple intersections
|
|
210
|
+
between the manifold and the boundary. Each is either a crossing or a coincident region.
|
|
211
|
+
|
|
212
|
+
Crossings result in two intersection manifolds: one in the domain of the manifold and one in the trim of the boundary. By construction, both intersection manifolds have the
|
|
213
|
+
same domain and the same range of the manifold and boundary (the crossing itself). The intersection manifold in the domain of the manifold becomes a boundary of the cutout,
|
|
214
|
+
but we must determine the intersection's trim. For that, we compute the cutout of the boundary's intersection manifold with the boundary's trim. This recursion continues
|
|
215
|
+
until the cutout is just a point with no trim.
|
|
216
|
+
|
|
217
|
+
Coincident regions appear in the trims of the manifold and the boundary. We intersect the boundary's coincident region with the trim of the boundary and then map
|
|
218
|
+
it to the domain of the manifold. If the coincident regions have normals in opposite directions, they cancel each other out, so we subtract them from the cutout by
|
|
219
|
+
inverting the region and intersecting it with the cutout. We use this same technique for removing overlapping coincident regions. If the coincident regions have normals
|
|
220
|
+
in the same direction, we union them with the cutout.
|
|
221
|
+
"""
|
|
222
|
+
assert manifold.range_dimension() == self.dimension
|
|
223
|
+
|
|
224
|
+
# Start with an empty cutout and no domain coincidences.
|
|
225
|
+
cutout = Solid(self.dimension-1, self.containsInfinity)
|
|
226
|
+
bounds = manifold.range_bounds()
|
|
227
|
+
if Solid.disjoint_bounds(bounds, self.bounds):
|
|
228
|
+
manifold.complete_cutout(cutout, self)
|
|
229
|
+
return cutout
|
|
230
|
+
coincidences = []
|
|
231
|
+
|
|
232
|
+
# Intersect each of this solid's boundaries with the manifold.
|
|
233
|
+
for boundary in self.boundaries:
|
|
234
|
+
if Solid.disjoint_bounds(boundary.bounds, bounds):
|
|
235
|
+
continue
|
|
236
|
+
|
|
237
|
+
# Intersect manifolds, checking if the intersection is already in the cache.
|
|
238
|
+
intersections, isTwin = boundary.manifold.cached_intersect(manifold, cache)
|
|
239
|
+
if intersections is NotImplemented:
|
|
240
|
+
raise NotImplementedError()
|
|
241
|
+
|
|
242
|
+
# Each intersection is either a crossing (domain manifold) or a coincidence (solid within the domain).
|
|
243
|
+
for intersection in intersections:
|
|
244
|
+
(firstPart, secondPart) = (intersection.secondPart, intersection.firstPart) if isTwin else (intersection.firstPart, intersection.secondPart)
|
|
245
|
+
|
|
246
|
+
if isinstance(intersection, Manifold.Crossing):
|
|
247
|
+
trimCutout = boundary.trim.compute_cutout(firstPart, cache)
|
|
248
|
+
if trimCutout:
|
|
249
|
+
cutout.add_boundary(Boundary(secondPart, trimCutout))
|
|
250
|
+
|
|
251
|
+
elif isinstance(intersection, Manifold.Coincidence):
|
|
252
|
+
# Intersect domain coincidence with the boundary's domain.
|
|
253
|
+
firstPart = firstPart.intersection(boundary.trim)
|
|
254
|
+
# Invert the domain coincidence (which will remove it) if this is a twin or if the normals point in opposite directions.
|
|
255
|
+
#invertCoincidence = trimTwin and (isTwin or intersection.alignment < 0.0)
|
|
256
|
+
invertCoincidence = (trimTwin and isTwin) or intersection.alignment < 0.0
|
|
257
|
+
# Create the coincidence to hold the trimmed and transformed domain coincidence (firstPart).
|
|
258
|
+
coincidence = Solid(firstPart.dimension, firstPart.containsInfinity)
|
|
259
|
+
if invertCoincidence:
|
|
260
|
+
coincidence.containsInfinity = not coincidence.containsInfinity
|
|
261
|
+
# Next, transform the domain coincidence from the boundary to the given manifold.
|
|
262
|
+
# Create copies of the manifolds and boundaries, since we are changing them.
|
|
263
|
+
for coincidenceBoundary in firstPart.boundaries:
|
|
264
|
+
coincidenceManifold = coincidenceBoundary.manifold
|
|
265
|
+
if invertCoincidence:
|
|
266
|
+
coincidenceManifold = coincidenceManifold.negate_normal()
|
|
267
|
+
if isTwin:
|
|
268
|
+
coincidenceManifold = coincidenceManifold.translate(-intersection.translation)
|
|
269
|
+
coincidenceManifold = coincidenceManifold.transform(intersection.inverse, intersection.transform.T)
|
|
270
|
+
else:
|
|
271
|
+
coincidenceManifold = coincidenceManifold.transform(intersection.transform, intersection.inverse.T)
|
|
272
|
+
coincidenceManifold = coincidenceManifold.translate(intersection.translation)
|
|
273
|
+
coincidence.add_boundary(Boundary(coincidenceManifold, coincidenceBoundary.trim))
|
|
274
|
+
# Finally, add the domain coincidence to the list of coincidences.
|
|
275
|
+
coincidences.append((invertCoincidence, coincidence))
|
|
276
|
+
|
|
277
|
+
# Ensure the cutout includes the manifold's inherent (implicit) boundaries, making it valid and complete.
|
|
278
|
+
manifold.complete_cutout(cutout, self)
|
|
279
|
+
|
|
280
|
+
# Now that we have a complete cutout, join it with each domain coincidence.
|
|
281
|
+
for coincidence in coincidences:
|
|
282
|
+
if coincidence[0]:
|
|
283
|
+
# If the domain coincidence is inverted (coincidence[0]), intersect it with the cutout, thus removing it.
|
|
284
|
+
cutout = cutout.intersection(coincidence[1], cache)
|
|
285
|
+
else:
|
|
286
|
+
# Otherwise, union the domain coincidence with the cutout, thus adding it.
|
|
287
|
+
cutout = cutout.union(coincidence[1])
|
|
288
|
+
|
|
289
|
+
return cutout
|
|
290
|
+
|
|
177
291
|
def contains_point(self, point):
|
|
178
292
|
"""
|
|
179
293
|
Test if a point lies within the solid.
|
|
@@ -270,19 +384,19 @@ class Solid:
|
|
|
270
384
|
|
|
271
385
|
See Also
|
|
272
386
|
--------
|
|
273
|
-
`
|
|
387
|
+
`compute_cutout` : Compute the cutout portion of the manifold within the solid.
|
|
274
388
|
`union` : Union two solids.
|
|
275
389
|
`difference` : Subtract one solid from another.
|
|
276
390
|
|
|
277
391
|
Notes
|
|
278
392
|
-----
|
|
279
|
-
To intersect two solids, we
|
|
280
|
-
|
|
281
|
-
the intersection of two solids becomes a set of intersections within the
|
|
282
|
-
until we are intersecting points whose
|
|
393
|
+
To intersect two solids, we compute the cutout portions of each solid's untrimmed boundaries (their manifolds) that lie within the other solid.
|
|
394
|
+
We then intersect the trim of each boundary (the trim of its manifold) with its cutout. Thus,
|
|
395
|
+
the intersection of two solids becomes a set of intersections within the trims of their boundaries. This recursion continues
|
|
396
|
+
until we are intersecting points whose trims have no boundaries.
|
|
283
397
|
|
|
284
398
|
The only subtlety is when two boundaries are coincident. To avoid overlapping the coincident region, we keep that region
|
|
285
|
-
for one
|
|
399
|
+
for one cutout and trim it away for the other. We use a manifold intersection cache to keep track of these pairs, as well as to reduce computation.
|
|
286
400
|
"""
|
|
287
401
|
assert self.dimension == other.dimension
|
|
288
402
|
|
|
@@ -306,22 +420,22 @@ class Solid:
|
|
|
306
420
|
return combinedSolid
|
|
307
421
|
|
|
308
422
|
for boundary in self.boundaries:
|
|
309
|
-
#
|
|
310
|
-
|
|
311
|
-
# Intersect
|
|
312
|
-
|
|
313
|
-
if
|
|
314
|
-
# Self boundary intersects other, so create a new boundary with the intersected
|
|
315
|
-
combinedSolid.add_boundary(Boundary(boundary.manifold,
|
|
423
|
+
# Compute the cutout portion of self's boundary manifold that lies within other.
|
|
424
|
+
cutout = other.compute_cutout(boundary.manifold, cache, True)
|
|
425
|
+
# Intersect cutout with the boundary's trim.
|
|
426
|
+
newTrim = boundary.trim.intersection(cutout, cache)
|
|
427
|
+
if newTrim:
|
|
428
|
+
# Self boundary intersects other, so create a new boundary with the intersected trim.
|
|
429
|
+
combinedSolid.add_boundary(Boundary(boundary.manifold, newTrim))
|
|
316
430
|
|
|
317
431
|
for boundary in other.boundaries:
|
|
318
|
-
#
|
|
319
|
-
|
|
320
|
-
# Intersect
|
|
321
|
-
|
|
322
|
-
if
|
|
323
|
-
# Other boundary intersects self, so create a new boundary with the intersected
|
|
324
|
-
combinedSolid.add_boundary(Boundary(boundary.manifold,
|
|
432
|
+
# Compute the cutout portion of other's boundary manifold that lies within self.
|
|
433
|
+
cutout = self.compute_cutout(boundary.manifold, cache, True)
|
|
434
|
+
# Intersect cutout with the boundary's trim.
|
|
435
|
+
newTrim = boundary.trim.intersection(cutout, cache)
|
|
436
|
+
if newTrim:
|
|
437
|
+
# Other boundary intersects self, so create a new boundary with the intersected trim.
|
|
438
|
+
combinedSolid.add_boundary(Boundary(boundary.manifold, newTrim))
|
|
325
439
|
|
|
326
440
|
return combinedSolid
|
|
327
441
|
|
|
@@ -363,7 +477,10 @@ class Solid:
|
|
|
363
477
|
solid = Solid(dictionary["dimension"], dictionary["containsInfinity"], dictionary.get("metadata", {}))
|
|
364
478
|
for boundary in dictionary["boundaries"]:
|
|
365
479
|
manifold = boundary["manifold"]
|
|
366
|
-
solid.add_boundary(
|
|
480
|
+
solid.add_boundary(
|
|
481
|
+
Boundary(
|
|
482
|
+
Manifold.factory[manifold.get("type", "Spline")].from_dict(manifold),
|
|
483
|
+
from_dict(boundary.get("trim", boundary.get("domain", None)))))
|
|
367
484
|
return solid
|
|
368
485
|
|
|
369
486
|
# Load json file.
|
|
@@ -430,7 +547,7 @@ class Solid:
|
|
|
430
547
|
if isinstance(obj, Manifold):
|
|
431
548
|
return obj.to_dict()
|
|
432
549
|
if isinstance(obj, Boundary):
|
|
433
|
-
return {"type" : "Boundary", "manifold" : obj.manifold, "
|
|
550
|
+
return {"type" : "Boundary", "manifold" : obj.manifold, "trim" : obj.trim}
|
|
434
551
|
if isinstance(obj, Solid):
|
|
435
552
|
return {"type" : "Solid", "dimension" : obj.dimension, "containsInfinity" : obj.containsInfinity, "boundaries" : obj.boundaries, "metadata" : obj.metadata}
|
|
436
553
|
return super().default(obj)
|
|
@@ -438,120 +555,6 @@ class Solid:
|
|
|
438
555
|
with open(fileName, 'w', encoding='utf-8') as file:
|
|
439
556
|
json.dump(solids_or_manifolds, file, indent=4, cls=Encoder)
|
|
440
557
|
|
|
441
|
-
def slice(self, manifold, cache = None, trimTwin = False):
|
|
442
|
-
"""
|
|
443
|
-
Slice the solid by a manifold.
|
|
444
|
-
|
|
445
|
-
Parameters
|
|
446
|
-
----------
|
|
447
|
-
manifold : `Manifold`
|
|
448
|
-
The `Manifold` used to slice the solid.
|
|
449
|
-
|
|
450
|
-
cache : `dict`, optional
|
|
451
|
-
A dictionary to cache `Manifold` intersections, speeding computation.
|
|
452
|
-
|
|
453
|
-
trimTwin : `bool`, default: False
|
|
454
|
-
Trim coincident boundary twins on subsequent calls to slice (avoids duplication of overlapping regions).
|
|
455
|
-
Trimming twins is typically only used in conjunction with `intersection`.
|
|
456
|
-
|
|
457
|
-
Returns
|
|
458
|
-
-------
|
|
459
|
-
slice : `Solid`
|
|
460
|
-
A region in the domain of `manifold` that intersects with the solid. The region may contain infinity.
|
|
461
|
-
|
|
462
|
-
See Also
|
|
463
|
-
--------
|
|
464
|
-
`intersection` : Intersect two solids.
|
|
465
|
-
`Manifold.intersect` : Intersect two manifolds.
|
|
466
|
-
`Manifold.cached_intersect` : Intersect two manifolds, caching the result for twins.
|
|
467
|
-
`Manifold.complete_slice` : Add any missing inherent (implicit) boundaries of this manifold's domain to the given slice.
|
|
468
|
-
|
|
469
|
-
Notes
|
|
470
|
-
-----
|
|
471
|
-
The dimension of the slice is always one less than the dimension of the solid, since the slice is a region in the domain of the manifold slicing the solid.
|
|
472
|
-
|
|
473
|
-
To compute the slice of a manifold intersecting the solid, we intersect the manifold with each boundary of the solid. There may be multiple intersections
|
|
474
|
-
between the manifold and the boundary. Each is either a crossing or a coincident region.
|
|
475
|
-
|
|
476
|
-
Crossings result in two intersection manifolds: one in the domain of the manifold and one in the domain of the boundary. By construction, both intersection manifolds have the
|
|
477
|
-
same domain and the same range of the manifold and boundary (the crossing itself). The intersection manifold in the domain of the manifold becomes a boundary of the slice,
|
|
478
|
-
but we must determine the intersection's domain. For that, we slice the boundary's intersection manifold with the boundary's domain. This recursion continues
|
|
479
|
-
until the slice is just a point with no domain.
|
|
480
|
-
|
|
481
|
-
Coincident regions appear in the domains of the manifold and the boundary. We intersect the boundary's coincident region with the domain of the boundary and then map
|
|
482
|
-
it to the domain of the manifold. If the coincident regions have normals in opposite directions, they cancel each other out, so we subtract them from the slice by
|
|
483
|
-
inverting the region and intersecting it with the slice. We use this same technique for removing overlapping coincident regions. If the coincident regions have normals
|
|
484
|
-
in the same direction, we union them with the slice.
|
|
485
|
-
"""
|
|
486
|
-
assert manifold.range_dimension() == self.dimension
|
|
487
|
-
|
|
488
|
-
# Start with an empty slice and no domain coincidences.
|
|
489
|
-
slice = Solid(self.dimension-1, self.containsInfinity)
|
|
490
|
-
bounds = manifold.range_bounds()
|
|
491
|
-
if Solid.disjoint_bounds(bounds, self.bounds):
|
|
492
|
-
manifold.complete_slice(slice, self)
|
|
493
|
-
return slice
|
|
494
|
-
coincidences = []
|
|
495
|
-
|
|
496
|
-
# Intersect each of this solid's boundaries with the manifold.
|
|
497
|
-
for boundary in self.boundaries:
|
|
498
|
-
if Solid.disjoint_bounds(boundary.bounds, bounds):
|
|
499
|
-
continue
|
|
500
|
-
|
|
501
|
-
# Intersect manifolds, checking if the intersection is already in the cache.
|
|
502
|
-
intersections, isTwin = boundary.manifold.cached_intersect(manifold, cache)
|
|
503
|
-
if intersections is NotImplemented:
|
|
504
|
-
raise NotImplementedError()
|
|
505
|
-
|
|
506
|
-
# Each intersection is either a crossing (domain manifold) or a coincidence (solid within the domain).
|
|
507
|
-
for intersection in intersections:
|
|
508
|
-
(left, right) = (intersection.right, intersection.left) if isTwin else (intersection.left, intersection.right)
|
|
509
|
-
|
|
510
|
-
if isinstance(intersection, Manifold.Crossing):
|
|
511
|
-
domainSlice = boundary.domain.slice(left, cache)
|
|
512
|
-
if domainSlice:
|
|
513
|
-
slice.add_boundary(Boundary(right, domainSlice))
|
|
514
|
-
|
|
515
|
-
elif isinstance(intersection, Manifold.Coincidence):
|
|
516
|
-
# Intersect domain coincidence with the boundary's domain.
|
|
517
|
-
left = left.intersection(boundary.domain)
|
|
518
|
-
# Invert the domain coincidence (which will remove it) if this is a twin or if the normals point in opposite directions.
|
|
519
|
-
#invertCoincidence = trimTwin and (isTwin or intersection.alignment < 0.0)
|
|
520
|
-
invertCoincidence = (trimTwin and isTwin) or intersection.alignment < 0.0
|
|
521
|
-
# Create the coincidence to hold the trimmed and transformed domain coincidence (left).
|
|
522
|
-
coincidence = Solid(left.dimension, left.containsInfinity)
|
|
523
|
-
if invertCoincidence:
|
|
524
|
-
coincidence.containsInfinity = not coincidence.containsInfinity
|
|
525
|
-
# Next, transform the domain coincidence from the boundary to the given manifold.
|
|
526
|
-
# Create copies of the manifolds and boundaries, since we are changing them.
|
|
527
|
-
for coincidenceBoundary in left.boundaries:
|
|
528
|
-
coincidenceManifold = coincidenceBoundary.manifold
|
|
529
|
-
if invertCoincidence:
|
|
530
|
-
coincidenceManifold = coincidenceManifold.flip_normal()
|
|
531
|
-
if isTwin:
|
|
532
|
-
coincidenceManifold = coincidenceManifold.translate(-intersection.translation)
|
|
533
|
-
coincidenceManifold = coincidenceManifold.transform(intersection.inverse, intersection.transform.T)
|
|
534
|
-
else:
|
|
535
|
-
coincidenceManifold = coincidenceManifold.transform(intersection.transform, intersection.inverse.T)
|
|
536
|
-
coincidenceManifold = coincidenceManifold.translate(intersection.translation)
|
|
537
|
-
coincidence.add_boundary(Boundary(coincidenceManifold, coincidenceBoundary.domain))
|
|
538
|
-
# Finally, add the domain coincidence to the list of coincidences.
|
|
539
|
-
coincidences.append((invertCoincidence, coincidence))
|
|
540
|
-
|
|
541
|
-
# Ensure the slice includes the manifold's inherent (implicit) boundaries, making it valid and complete.
|
|
542
|
-
manifold.complete_slice(slice, self)
|
|
543
|
-
|
|
544
|
-
# Now that we have a complete manifold domain, join it with each domain coincidence.
|
|
545
|
-
for coincidence in coincidences:
|
|
546
|
-
if coincidence[0]:
|
|
547
|
-
# If the domain coincidence is inverted (coincidence[0]), intersect it with the slice, thus removing it.
|
|
548
|
-
slice = slice.intersection(coincidence[1], cache)
|
|
549
|
-
else:
|
|
550
|
-
# Otherwise, union the domain coincidence with the slice, thus adding it.
|
|
551
|
-
slice = slice.union(coincidence[1])
|
|
552
|
-
|
|
553
|
-
return slice
|
|
554
|
-
|
|
555
558
|
def surface_integral(self, f, args=(), epsabs=None, epsrel=None, *quadArgs):
|
|
556
559
|
"""
|
|
557
560
|
Compute the surface integral of a vector field on the boundary of the solid.
|
|
@@ -581,7 +584,7 @@ class Solid:
|
|
|
581
584
|
-----
|
|
582
585
|
To compute the surface integral of a scalar function on the boundary, have `f` return the product of the `normal` times the scalar function for the `point`.
|
|
583
586
|
|
|
584
|
-
`surface_integral` sums the `volume_integral` over the
|
|
587
|
+
`surface_integral` sums the `volume_integral` over the trim of the solid's boundaries, using the integrand: `numpy.dot(f(point, normal), normal)`,
|
|
585
588
|
where `normal` is the cross-product of the boundary tangents (the normal before normalization).
|
|
586
589
|
"""
|
|
587
590
|
if not isinstance(args, tuple):
|
|
@@ -595,17 +598,17 @@ class Solid:
|
|
|
595
598
|
sum = 0.0
|
|
596
599
|
|
|
597
600
|
for boundary in self.boundaries:
|
|
598
|
-
def integrand(
|
|
599
|
-
evalPoint = np.atleast_1d(
|
|
601
|
+
def integrand(trimPoint):
|
|
602
|
+
evalPoint = np.atleast_1d(trimPoint)
|
|
600
603
|
point = boundary.manifold.evaluate(evalPoint)
|
|
601
604
|
cofactorNormal = boundary.manifold.normal(evalPoint, False)
|
|
602
605
|
normal = cofactorNormal / np.linalg.norm(cofactorNormal)
|
|
603
606
|
fValue = f(point, normal, *args)
|
|
604
607
|
return np.dot(fValue, cofactorNormal)
|
|
605
608
|
|
|
606
|
-
if boundary.
|
|
609
|
+
if boundary.trim.dimension > 0:
|
|
607
610
|
# Add the contribution to the Volume integral from this boundary.
|
|
608
|
-
sum += boundary.
|
|
611
|
+
sum += boundary.trim.volume_integral(integrand)
|
|
609
612
|
else:
|
|
610
613
|
# This is a 1-D boundary (line interval, no domain), so just add the integrand.
|
|
611
614
|
sum += integrand(0.0)
|
|
@@ -636,7 +639,7 @@ class Solid:
|
|
|
636
639
|
|
|
637
640
|
solid = Solid(self.dimension, self.containsInfinity)
|
|
638
641
|
for boundary in self.boundaries:
|
|
639
|
-
solid.add_boundary(Boundary(boundary.manifold.transform(matrix, matrixInverseTranspose), boundary.
|
|
642
|
+
solid.add_boundary(Boundary(boundary.manifold.transform(matrix, matrixInverseTranspose), boundary.trim))
|
|
640
643
|
return solid
|
|
641
644
|
|
|
642
645
|
def translate(self, delta):
|
|
@@ -657,7 +660,7 @@ class Solid:
|
|
|
657
660
|
|
|
658
661
|
solid = Solid(self.dimension, self.containsInfinity)
|
|
659
662
|
for boundary in self.boundaries:
|
|
660
|
-
solid.add_boundary(Boundary(boundary.manifold.translate(delta), boundary.
|
|
663
|
+
solid.add_boundary(Boundary(boundary.manifold.translate(delta), boundary.trim))
|
|
661
664
|
return solid
|
|
662
665
|
|
|
663
666
|
def union(self, other):
|
|
@@ -723,7 +726,7 @@ class Solid:
|
|
|
723
726
|
And so, `surface_integral(Integral(f) * n[0]) = volume_integral(Integral(f) * first cofactor)` over each boundary manifold's domain.
|
|
724
727
|
|
|
725
728
|
So, we have `volume_integral(f) = volume_integral(Integral(f) * first cofactor)` over each boundary manifold's domain.
|
|
726
|
-
To compute the volume integral we sum `volume_integral` over the
|
|
729
|
+
To compute the volume integral we sum `volume_integral` over the trim of the solid's boundaries, using the integrand:
|
|
727
730
|
`scipy.integrate.quad(f, x0, x [other coordinates fixed]) * first cofactor`.
|
|
728
731
|
This recursion continues until the boundaries are only points, where we can just sum the integrand.
|
|
729
732
|
"""
|
|
@@ -741,8 +744,8 @@ class Solid:
|
|
|
741
744
|
x0 = self.any_point()[0]
|
|
742
745
|
|
|
743
746
|
for boundary in self.boundaries:
|
|
744
|
-
def
|
|
745
|
-
evalPoint = np.atleast_1d(
|
|
747
|
+
def trimF(trimPoint):
|
|
748
|
+
evalPoint = np.atleast_1d(trimPoint)
|
|
746
749
|
point = boundary.manifold.evaluate(evalPoint)
|
|
747
750
|
|
|
748
751
|
# fHat passes the scalar given by integrate.quad into the first coordinate of the vector for f.
|
|
@@ -758,12 +761,12 @@ class Solid:
|
|
|
758
761
|
returnValue = integrate.quad(fHat, x0, point[0], epsabs=epsabs, epsrel=epsrel, *quadArgs)[0] * firstCofactor
|
|
759
762
|
return returnValue
|
|
760
763
|
|
|
761
|
-
if boundary.
|
|
764
|
+
if boundary.trim.dimension > 0:
|
|
762
765
|
# Add the contribution to the Volume integral from this boundary.
|
|
763
|
-
sum += boundary.
|
|
766
|
+
sum += boundary.trim.volume_integral(trimF)
|
|
764
767
|
else:
|
|
765
768
|
# This is a 1-D boundary (line interval, no domain), so just add the integrand.
|
|
766
|
-
sum +=
|
|
769
|
+
sum += trimF(0.0)
|
|
767
770
|
|
|
768
771
|
return sum
|
|
769
772
|
|
bspy/spline.py
CHANGED
|
@@ -338,23 +338,23 @@ class Spline(Manifold):
|
|
|
338
338
|
"""
|
|
339
339
|
return bspy._spline_domain.common_basis(splines, indMap)
|
|
340
340
|
|
|
341
|
-
def
|
|
341
|
+
def complete_cutout(self, cutout, solid):
|
|
342
342
|
"""
|
|
343
|
-
Add any missing inherent (implicit) boundaries of this spline's domain to the given
|
|
344
|
-
given solid that are needed to make the
|
|
343
|
+
Add any missing inherent (implicit) boundaries of this spline's domain to the given cutout of the
|
|
344
|
+
given solid that are needed to make the cutout valid and complete.
|
|
345
345
|
|
|
346
346
|
Parameters
|
|
347
347
|
----------
|
|
348
|
-
|
|
349
|
-
The
|
|
348
|
+
cutout : `solid.Solid`
|
|
349
|
+
The cutout of the given solid formed by the spline. The cutout may be incomplete, missing some of the
|
|
350
350
|
spline's inherent domain boundaries. Its dimension must match `self.domain_dimension()`.
|
|
351
351
|
|
|
352
352
|
solid : `solid.Solid`
|
|
353
|
-
The solid
|
|
353
|
+
The solid determining the cutout of the manifold. Its dimension must match `self.range_dimension()`.
|
|
354
354
|
|
|
355
355
|
See Also
|
|
356
356
|
--------
|
|
357
|
-
`solid.Solid.
|
|
357
|
+
`solid.Solid.compute_cutout` : Compute the cutout portion of the manifold within the solid.
|
|
358
358
|
`domain` : Return the domain of a spline.
|
|
359
359
|
|
|
360
360
|
Notes
|
|
@@ -362,10 +362,10 @@ class Spline(Manifold):
|
|
|
362
362
|
A spline's inherent domain is determined by its knot array for each dimension. This method only works for
|
|
363
363
|
nInd of 1 or 2.
|
|
364
364
|
"""
|
|
365
|
-
if self.domain_dimension() !=
|
|
365
|
+
if self.domain_dimension() != cutout.dimension: raise ValueError("Spline domain dimension must match cutout dimension")
|
|
366
366
|
if self.range_dimension() != solid.dimension: raise ValueError("Spline range dimension must match solid dimension")
|
|
367
|
-
if
|
|
368
|
-
return bspy._spline_intersection.
|
|
367
|
+
if cutout.dimension != 1 and cutout.dimension != 2: raise ValueError("Only works for nInd = 1 or 2")
|
|
368
|
+
return bspy._spline_intersection.complete_cutout(self, cutout, solid)
|
|
369
369
|
|
|
370
370
|
@staticmethod
|
|
371
371
|
def composition(splines, tolerance = 1.0e-6):
|
|
@@ -1017,23 +1017,6 @@ class Spline(Manifold):
|
|
|
1017
1017
|
`least_squares` : Fit a least squares approximation to given data.
|
|
1018
1018
|
"""
|
|
1019
1019
|
return bspy._spline_fitting.fit(domain, f, order, knots, tolerance)
|
|
1020
|
-
|
|
1021
|
-
def flip_normal(self):
|
|
1022
|
-
"""
|
|
1023
|
-
Flip the direction of the normal.
|
|
1024
|
-
|
|
1025
|
-
Returns
|
|
1026
|
-
-------
|
|
1027
|
-
spline : `Spline`
|
|
1028
|
-
The spline with flipped normal. The spline retains the same tangent space.
|
|
1029
|
-
|
|
1030
|
-
See Also
|
|
1031
|
-
--------
|
|
1032
|
-
`solid.Solid.complement` : Return the complement of the solid: whatever was inside is outside and vice-versa.
|
|
1033
|
-
"""
|
|
1034
|
-
spline = self.copy()
|
|
1035
|
-
spline.metadata["flipNormal"] = not self.metadata.get("flipNormal", False)
|
|
1036
|
-
return spline
|
|
1037
1020
|
|
|
1038
1021
|
def fold(self, foldedInd):
|
|
1039
1022
|
"""
|
|
@@ -1352,7 +1335,7 @@ class Spline(Manifold):
|
|
|
1352
1335
|
--------
|
|
1353
1336
|
`zeros` : Find the roots of a spline (nInd must match nDep).
|
|
1354
1337
|
`contours` : Find all the contour curves of a spline.
|
|
1355
|
-
`solid.Solid.
|
|
1338
|
+
`solid.Solid.compute_cutout` : Compute the cutout portion of the manifold within the solid.
|
|
1356
1339
|
|
|
1357
1340
|
Notes
|
|
1358
1341
|
-----
|
|
@@ -1637,6 +1620,23 @@ class Spline(Manifold):
|
|
|
1637
1620
|
if indMap is not None:
|
|
1638
1621
|
indMap = [(mapping, mapping, False) if np.isscalar(mapping) else (*mapping, False) for mapping in indMap]
|
|
1639
1622
|
return bspy._spline_operations.multiplyAndConvolve(self, other, indMap, productType)
|
|
1623
|
+
|
|
1624
|
+
def negate_normal(self):
|
|
1625
|
+
"""
|
|
1626
|
+
Negate the direction of the normal.
|
|
1627
|
+
|
|
1628
|
+
Returns
|
|
1629
|
+
-------
|
|
1630
|
+
spline : `Spline`
|
|
1631
|
+
The spline with negated normal. The spline retains the same tangent space.
|
|
1632
|
+
|
|
1633
|
+
See Also
|
|
1634
|
+
--------
|
|
1635
|
+
`solid.Solid.complement` : Return the complement of the solid: whatever was inside is outside and vice-versa.
|
|
1636
|
+
"""
|
|
1637
|
+
spline = self.copy()
|
|
1638
|
+
spline.metadata["negateNormal"] = not self.metadata.get("negateNormal", False)
|
|
1639
|
+
return spline
|
|
1640
1640
|
|
|
1641
1641
|
def normal(self, uvw, normalize=True, indices=None):
|
|
1642
1642
|
"""
|
bspy/splineOpenGLFrame.py
CHANGED
|
@@ -1543,16 +1543,16 @@ class SplineOpenGLFrame(OpenGLFrame):
|
|
|
1543
1543
|
self.connection = None
|
|
1544
1544
|
endpoints = []
|
|
1545
1545
|
for curve in solid.boundaries:
|
|
1546
|
-
curve.
|
|
1546
|
+
curve.trim.boundaries.sort(key=lambda boundary: (boundary.manifold.evaluate(0.0), -boundary.manifold.normal(0.0)))
|
|
1547
1547
|
leftB = 0
|
|
1548
1548
|
rightB = 0
|
|
1549
|
-
boundaryCount = len(curve.
|
|
1549
|
+
boundaryCount = len(curve.trim.boundaries)
|
|
1550
1550
|
while leftB < boundaryCount:
|
|
1551
|
-
if curve.
|
|
1552
|
-
leftPoint = curve.
|
|
1551
|
+
if curve.trim.boundaries[leftB].manifold.normal(0.0) < 0.0:
|
|
1552
|
+
leftPoint = curve.trim.boundaries[leftB].manifold.evaluate(0.0)[0]
|
|
1553
1553
|
while rightB < boundaryCount:
|
|
1554
|
-
rightPoint = curve.
|
|
1555
|
-
if leftPoint - Manifold.minSeparation < rightPoint and curve.
|
|
1554
|
+
rightPoint = curve.trim.boundaries[rightB].manifold.evaluate(0.0)[0]
|
|
1555
|
+
if leftPoint - Manifold.minSeparation < rightPoint and curve.trim.boundaries[rightB].manifold.normal(0.0) > 0.0:
|
|
1556
1556
|
t = curve.manifold.tangent_space(leftPoint)[:,0]
|
|
1557
1557
|
n = curve.manifold.normal(leftPoint)
|
|
1558
1558
|
clockwise = t[0] * n[1] - t[1] * n[0] > 0.0
|
|
@@ -1567,7 +1567,7 @@ class SplineOpenGLFrame(OpenGLFrame):
|
|
|
1567
1567
|
rightB += 1
|
|
1568
1568
|
leftB += 1
|
|
1569
1569
|
|
|
1570
|
-
# Second, collect all valid pairings of endpoints (normal not
|
|
1570
|
+
# Second, collect all valid pairings of endpoints (normal not negated between segments).
|
|
1571
1571
|
Connection = namedtuple('Connection', ('distance', 'ep1', 'ep2'))
|
|
1572
1572
|
connections = []
|
|
1573
1573
|
for i, ep1 in enumerate(endpoints[:-1]):
|
bspy/viewer.py
CHANGED
|
@@ -160,8 +160,8 @@ class Viewer(tk.Tk):
|
|
|
160
160
|
elif isinstance(spline, Boundary):
|
|
161
161
|
boundary = spline
|
|
162
162
|
if isinstance(boundary.manifold, Hyperplane):
|
|
163
|
-
uvMin = boundary.
|
|
164
|
-
uvMax = boundary.
|
|
163
|
+
uvMin = boundary.trim.bounds[:,0]
|
|
164
|
+
uvMax = boundary.trim.bounds[:,1]
|
|
165
165
|
if (uvMax - uvMin).min() < 1.0e-8:
|
|
166
166
|
return
|
|
167
167
|
xyzMinMin = boundary.manifold.evaluate(uvMin)
|
|
@@ -174,7 +174,7 @@ class Viewer(tk.Tk):
|
|
|
174
174
|
spline.metadata = boundary.manifold.metadata # Ensure the spline representing the hyperplane shares the same metadata
|
|
175
175
|
elif isinstance(boundary.manifold, Spline):
|
|
176
176
|
spline = boundary.manifold
|
|
177
|
-
tesselation = self.frame.tessellate2DSolid(boundary.
|
|
177
|
+
tesselation = self.frame.tessellate2DSolid(boundary.trim)
|
|
178
178
|
if tesselation is not None:
|
|
179
179
|
if not hasattr(spline, "cache"):
|
|
180
180
|
spline.cache = {}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bspy
|
|
3
|
-
Version:
|
|
3
|
+
Version: 5.0.0
|
|
4
4
|
Summary: Library for manipulating and rendering non-uniform B-splines
|
|
5
5
|
Home-page: http://github.com/ericbrec/BSpy
|
|
6
6
|
Author: Eric Brechner
|
|
@@ -50,7 +50,7 @@ The [Hyperplane](https://ericbrec.github.io/BSpy/bspy/hyperplane.html) class has
|
|
|
50
50
|
|
|
51
51
|
The [Manifold](https://ericbrec.github.io/BSpy/bspy/manifold.html) abstract base class for [Hyperplane](https://ericbrec.github.io/BSpy/bspy/hyperplane.html) and [Spline](https://ericbrec.github.io/BSpy/bspy/spline.html).
|
|
52
52
|
|
|
53
|
-
The [Solid](https://ericbrec.github.io/BSpy/bspy/solid.html) class has methods to construct n-dimensional solids from trimmed [Manifold](https://ericbrec.github.io/BSpy/bspy/manifold.html) boundaries. Each solid consists of a list of boundaries and a Boolean value that indicates if the solid contains infinity. Each [Boundary](https://ericbrec.github.io/BSpy/bspy/solid.html) consists of a manifold (currently a [Hyperplane](https://ericbrec.github.io/BSpy/bspy/hyperplane.html) or [Spline](https://ericbrec.github.io/BSpy/bspy/spline.html)) and a domain solid that trims the manifold. Solids have methods to form the intersection, union, difference, and complement of solids. There are methods to compute point containment, winding numbers, surface integrals, and volume integrals. There are also methods to translate, transform, and
|
|
53
|
+
The [Solid](https://ericbrec.github.io/BSpy/bspy/solid.html) class has methods to construct n-dimensional solids from trimmed [Manifold](https://ericbrec.github.io/BSpy/bspy/manifold.html) boundaries. Each solid consists of a list of boundaries and a Boolean value that indicates if the solid contains infinity. Each [Boundary](https://ericbrec.github.io/BSpy/bspy/solid.html) consists of a manifold (currently a [Hyperplane](https://ericbrec.github.io/BSpy/bspy/hyperplane.html) or [Spline](https://ericbrec.github.io/BSpy/bspy/spline.html)) and a domain solid that trims the manifold. Solids have methods to form the intersection, union, difference, and complement of solids. There are methods to compute point containment, winding numbers, surface integrals, and volume integrals. There are also methods to translate, transform, and compute cutouts of solids. Solids can be saved and loaded in json format.
|
|
54
54
|
|
|
55
55
|
The [SplineOpenGLFrame](https://ericbrec.github.io/BSpy/bspy/splineOpenGLFrame.html) class is an
|
|
56
56
|
[OpenGLFrame](https://pypi.org/project/pyopengltk/) with custom shaders to render spline curves, surfaces, and solids. Spline surfaces with more
|
|
@@ -86,3 +86,9 @@ a set of examples, including a jupyter notebook, can be found [here](https://git
|
|
|
86
86
|
* Moved DrawableSpine methods for adjusting spline appearance to Viewer (see documentation for details)
|
|
87
87
|
* Spline.bspline_values changed arguments (see documentation for details)
|
|
88
88
|
* Spline.intersect changed return values (see documentation for details)
|
|
89
|
+
|
|
90
|
+
### Release 5.0 breaking changes
|
|
91
|
+
* Renamed Boundary.domain member to Boundary.trim
|
|
92
|
+
* Renamed Solid.slice method to Solid.compute_cutout
|
|
93
|
+
* Renamed Manifold.complete_slice method to Manifold.complete_cutout (also applies to Hyperplane and Spline classes)
|
|
94
|
+
* Renamed Manifold.flip_normal method to Manifold.negate_normal (also applies to Hyperplane and Spline classes)
|