yapCAD 0.5.0__py2.py3-none-any.whl → 0.5.1__py2.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.
- yapcad/boolean/__init__.py +21 -0
- yapcad/boolean/native.py +1012 -0
- yapcad/boolean/trimesh_engine.py +155 -0
- yapcad/combine.py +52 -14
- yapcad/drawable.py +404 -26
- yapcad/geom.py +116 -0
- yapcad/geom3d.py +237 -7
- yapcad/geom3d_util.py +486 -30
- yapcad/geom_util.py +160 -61
- yapcad/io/__init__.py +2 -1
- yapcad/io/step.py +323 -0
- yapcad/spline.py +232 -0
- {yapcad-0.5.0.dist-info → yapcad-0.5.1.dist-info}/METADATA +60 -14
- yapcad-0.5.1.dist-info/RECORD +32 -0
- yapcad-0.5.0.dist-info/RECORD +0 -27
- {yapcad-0.5.0.dist-info → yapcad-0.5.1.dist-info}/WHEEL +0 -0
- {yapcad-0.5.0.dist-info → yapcad-0.5.1.dist-info}/licenses/AUTHORS.rst +0 -0
- {yapcad-0.5.0.dist-info → yapcad-0.5.1.dist-info}/licenses/LICENSE +0 -0
- {yapcad-0.5.0.dist-info → yapcad-0.5.1.dist-info}/licenses/LICENSE.txt +0 -0
- {yapcad-0.5.0.dist-info → yapcad-0.5.1.dist-info}/top_level.txt +0 -0
yapcad/geom3d_util.py
CHANGED
@@ -81,6 +81,7 @@ icaIndices = [ [1,11,3],[3,11,5],[5,11,7],[7,11,9],[9,11,1],
|
|
81
81
|
[0,2,4],[0,4,6],[0,6,8],[0,8,10],[0,10,2] ]
|
82
82
|
|
83
83
|
vertexHash = {}
|
84
|
+
_vertexHash_owner = None
|
84
85
|
def addVertex(nv,nn,verts,normals):
|
85
86
|
"""
|
86
87
|
Utility function that takes a vertex and associated normal and a
|
@@ -91,18 +92,29 @@ def addVertex(nv,nn,verts,normals):
|
|
91
92
|
|
92
93
|
returns the index, and the (potentiall updated) lists
|
93
94
|
"""
|
94
|
-
global vertexHash
|
95
|
-
|
95
|
+
global vertexHash, _vertexHash_owner
|
96
|
+
owner_id = id(verts)
|
97
|
+
if _vertexHash_owner != owner_id or len(verts) == 0:
|
96
98
|
vertexHash = {}
|
99
|
+
_vertexHash_owner = owner_id
|
97
100
|
|
98
101
|
found = False
|
99
|
-
|
102
|
+
# Normalize tiny values to avoid "-0.00" != "0.00" hash key mismatch
|
103
|
+
x = 0.0 if abs(nv[0]) < epsilon else nv[0]
|
104
|
+
y = 0.0 if abs(nv[1]) < epsilon else nv[1]
|
105
|
+
z = 0.0 if abs(nv[2]) < epsilon else nv[2]
|
106
|
+
vkey = f"{x:.2f}{y:.2f}{z:.2f}"
|
100
107
|
if vkey in vertexHash:
|
101
108
|
found = True
|
102
109
|
inds = vertexHash[vkey]
|
110
|
+
valid_inds = []
|
103
111
|
for i in inds:
|
112
|
+
if i >= len(verts):
|
113
|
+
continue
|
104
114
|
if vclose(nv,verts[i]):
|
105
115
|
return i,verts,normals
|
116
|
+
valid_inds.append(i)
|
117
|
+
vertexHash[vkey] = valid_inds
|
106
118
|
|
107
119
|
verts.append(nv)
|
108
120
|
normals.append(nn)
|
@@ -347,20 +359,32 @@ def conic(baser,topr,height, center=point(0,0,0),angr=10):
|
|
347
359
|
['procedure',call])
|
348
360
|
else:
|
349
361
|
topP = add(center,point(0,0,height))
|
350
|
-
|
362
|
+
# Only use perimeter vertices (baseV[1:]), skip the center point
|
363
|
+
conV = [ topP ] + baseV[1:]
|
351
364
|
ll = len(conV)
|
352
|
-
|
365
|
+
# Initialize all normals to a default value
|
366
|
+
conN = [[0,0,1,0] for _ in range(ll)]
|
353
367
|
conF = []
|
354
368
|
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
369
|
+
# ll = apex(1) + perimeter vertices(36) = 37
|
370
|
+
# Perimeter vertex indices: 1 to ll-1
|
371
|
+
num_perimeter = ll - 1
|
372
|
+
for i in range(1, ll):
|
373
|
+
p0 = conV[0] # apex
|
374
|
+
# Wrap indices within perimeter range [1, ll-1]
|
375
|
+
prev_idx = ((i - 2) % num_perimeter) + 1
|
376
|
+
next_idx = (i % num_perimeter) + 1
|
377
|
+
p1 = conV[prev_idx]
|
378
|
+
p2 = conV[next_idx]
|
359
379
|
|
360
|
-
|
361
|
-
|
380
|
+
try:
|
381
|
+
pp, n0 = tri2p0n([p0, p1, p2])
|
382
|
+
except ValueError:
|
383
|
+
# Skip degenerate faces near the apex
|
384
|
+
continue
|
362
385
|
|
363
|
-
|
386
|
+
conF.append([0, i, next_idx])
|
387
|
+
conN[i] = n0
|
364
388
|
|
365
389
|
conS = surface(conV,conN,conF)
|
366
390
|
|
@@ -384,28 +408,112 @@ def makeRevolutionSurface(contour,zStart,zEnd,steps,arcSamples=36):
|
|
384
408
|
|
385
409
|
degStep = 360.0/arcSamples
|
386
410
|
radStep = pi2/arcSamples
|
411
|
+
|
412
|
+
# Pre-compute cos/sin values to avoid floating point errors at the seam
|
413
|
+
# Explicitly ensure that index 0 uses exact values
|
414
|
+
angle_cos = []
|
415
|
+
angle_sin = []
|
416
|
+
for i in range(arcSamples):
|
417
|
+
if i == 0:
|
418
|
+
angle_cos.append(1.0)
|
419
|
+
angle_sin.append(0.0)
|
420
|
+
else:
|
421
|
+
angle = i * radStep
|
422
|
+
angle_cos.append(math.cos(angle))
|
423
|
+
angle_sin.append(math.sin(angle))
|
424
|
+
|
425
|
+
# Check if we need pole caps
|
426
|
+
r_start = contour(zStart)
|
427
|
+
r_end = contour(zEnd)
|
428
|
+
need_start_cap = r_start < epsilon * 10
|
429
|
+
need_end_cap = r_end < epsilon * 10
|
430
|
+
|
431
|
+
# Add pole vertices if needed
|
432
|
+
start_pole_idx = None
|
433
|
+
end_pole_idx = None
|
434
|
+
if need_start_cap:
|
435
|
+
pole_point = [0.0, 0.0, zStart, 1.0]
|
436
|
+
pole_normal = [0.0, 0.0, -1.0, 0.0] # Points downward for bottom pole
|
437
|
+
start_pole_idx, sV, sN = addVertex(pole_point, pole_normal, sV, sN)
|
438
|
+
|
439
|
+
if need_end_cap:
|
440
|
+
pole_point = [0.0, 0.0, zEnd, 1.0]
|
441
|
+
pole_normal = [0.0, 0.0, 1.0, 0.0] # Points upward for top pole
|
442
|
+
end_pole_idx, sV, sN = addVertex(pole_point, pole_normal, sV, sN)
|
443
|
+
|
387
444
|
for i in range(steps):
|
388
445
|
z = i*zD+zStart
|
389
446
|
r0 = contour(z)
|
390
447
|
r1 = contour(z+zD)
|
391
|
-
|
392
|
-
|
393
|
-
if
|
394
|
-
|
448
|
+
|
449
|
+
# Handle pole caps
|
450
|
+
if i == 0 and need_start_cap:
|
451
|
+
# Create triangular faces from pole to first ring
|
452
|
+
if r1 < epsilon:
|
453
|
+
r1 = epsilon
|
454
|
+
for j in range(arcSamples):
|
455
|
+
a1_idx = j
|
456
|
+
a2_idx = (j+1) % arcSamples
|
457
|
+
|
458
|
+
pp1 = [angle_cos[a1_idx]*r1, angle_sin[a1_idx]*r1, z+zD, 1.0]
|
459
|
+
pp2 = [angle_cos[a2_idx]*r1, angle_sin[a2_idx]*r1, z+zD, 1.0]
|
460
|
+
|
461
|
+
try:
|
462
|
+
_, n = tri2p0n([sV[start_pole_idx], pp1, pp2])
|
463
|
+
except ValueError:
|
464
|
+
continue
|
465
|
+
|
466
|
+
k1, sV, sN = addVertex(pp1, n, sV, sN)
|
467
|
+
k2, sV, sN = addVertex(pp2, n, sV, sN)
|
468
|
+
sF.append([start_pole_idx, k1, k2])
|
469
|
+
continue
|
470
|
+
|
471
|
+
if i == steps - 1 and need_end_cap:
|
472
|
+
# Create triangular faces from last ring to pole
|
473
|
+
if r0 < epsilon:
|
474
|
+
r0 = epsilon
|
475
|
+
for j in range(arcSamples):
|
476
|
+
a1_idx = j
|
477
|
+
a2_idx = (j+1) % arcSamples
|
478
|
+
|
479
|
+
p1 = [angle_cos[a1_idx]*r0, angle_sin[a1_idx]*r0, z, 1.0]
|
480
|
+
p2 = [angle_cos[a2_idx]*r0, angle_sin[a2_idx]*r0, z, 1.0]
|
481
|
+
|
482
|
+
try:
|
483
|
+
_, n = tri2p0n([p1, sV[end_pole_idx], p2])
|
484
|
+
except ValueError:
|
485
|
+
continue
|
486
|
+
|
487
|
+
k1, sV, sN = addVertex(p1, n, sV, sN)
|
488
|
+
k2, sV, sN = addVertex(p2, n, sV, sN)
|
489
|
+
sF.append([k1, end_pole_idx, k2])
|
490
|
+
continue
|
491
|
+
|
492
|
+
# Regular quad strips for non-pole sections
|
493
|
+
if r0 < epsilon:
|
494
|
+
r0 = epsilon
|
495
|
+
if r1 < epsilon:
|
496
|
+
r1 = epsilon
|
497
|
+
|
395
498
|
for j in range(arcSamples):
|
396
|
-
|
397
|
-
|
398
|
-
|
499
|
+
# Use pre-computed values with proper wrapping
|
500
|
+
a0_idx = (j-1) % arcSamples
|
501
|
+
a1_idx = j
|
502
|
+
a2_idx = (j+1) % arcSamples
|
399
503
|
|
400
|
-
p0 = [
|
401
|
-
p1 = [
|
402
|
-
p2 = [
|
504
|
+
p0 = [angle_cos[a0_idx]*r0, angle_sin[a0_idx]*r0, z, 1.0]
|
505
|
+
p1 = [angle_cos[a1_idx]*r0, angle_sin[a1_idx]*r0, z, 1.0]
|
506
|
+
p2 = [angle_cos[a2_idx]*r0, angle_sin[a2_idx]*r0, z, 1.0]
|
403
507
|
|
404
|
-
pp1 = [
|
405
|
-
pp2 = [
|
508
|
+
pp1 = [angle_cos[a1_idx]*r1, angle_sin[a1_idx]*r1, z+zD, 1.0]
|
509
|
+
pp2 = [angle_cos[a2_idx]*r1, angle_sin[a2_idx]*r1, z+zD, 1.0]
|
510
|
+
|
511
|
+
try:
|
512
|
+
p,n = tri2p0n([p0,p2,pp1])
|
513
|
+
except ValueError:
|
514
|
+
# Skip degenerate faces
|
515
|
+
continue
|
406
516
|
|
407
|
-
p,n = tri2p0n([p0,p2,pp1])
|
408
|
-
|
409
517
|
k1,sV,sN = addVertex(p1,n,sV,sN)
|
410
518
|
k2,sV,sN = addVertex(p2,n,sV,sN)
|
411
519
|
k3,sV,sN = addVertex(pp2,n,sV,sN)
|
@@ -499,14 +607,51 @@ def extrude(surf,distance,direction=vect(0,0,1,0)):
|
|
499
607
|
|
500
608
|
loops = []
|
501
609
|
if s2[4]:
|
502
|
-
loops.append(s2[4])
|
503
|
-
loops.extend([loop for loop in s2[5] if loop])
|
610
|
+
loops.append(list(s2[4]))
|
611
|
+
loops.extend([list(loop) for loop in s2[5] if loop])
|
504
612
|
|
505
613
|
stripV = s2[1] + s1[1] # vertices for the edge strips
|
506
614
|
stripN = [vect(0, 0, 1, 0)] * len(stripV) # placeholder normals
|
507
615
|
stripF: list[list[int]] = []
|
508
616
|
offset = len(s2[1])
|
509
617
|
|
618
|
+
def _project(idx):
|
619
|
+
point3 = stripV[idx]
|
620
|
+
return point3[0], point3[1]
|
621
|
+
|
622
|
+
if surf[3]:
|
623
|
+
try:
|
624
|
+
tri = surf[3][0]
|
625
|
+
basis = tri2p0n([surf[1][tri[0]], surf[1][tri[1]], surf[1][tri[2]]], basis=True)
|
626
|
+
if basis:
|
627
|
+
forward = basis[2]
|
628
|
+
|
629
|
+
def _project(idx): # type: ignore[redefinition]
|
630
|
+
vec = forward.mul(stripV[idx])
|
631
|
+
return vec[0], vec[1]
|
632
|
+
except Exception: # pragma: no cover
|
633
|
+
pass
|
634
|
+
|
635
|
+
def loop_area(loop):
|
636
|
+
if len(loop) < 3:
|
637
|
+
return 0.0
|
638
|
+
area = 0.0
|
639
|
+
for i in range(len(loop)):
|
640
|
+
x0, y0 = _project(loop[i])
|
641
|
+
x1, y1 = _project(loop[(i + 1) % len(loop)])
|
642
|
+
area += x0 * y1 - x1 * y0
|
643
|
+
return area / 2.0
|
644
|
+
|
645
|
+
if loops:
|
646
|
+
outer_area = loop_area(loops[0])
|
647
|
+
if outer_area < 0:
|
648
|
+
loops[0].reverse()
|
649
|
+
outer_area = -outer_area
|
650
|
+
outer_sign = 1 if outer_area >= 0 else -1
|
651
|
+
for loop in loops[1:]:
|
652
|
+
if loop_area(loop) * outer_sign > 0:
|
653
|
+
loop.reverse()
|
654
|
+
|
510
655
|
for bndry in loops:
|
511
656
|
if len(bndry) < 2:
|
512
657
|
continue
|
@@ -534,8 +679,319 @@ def extrude(surf,distance,direction=vect(0,0,1,0)):
|
|
534
679
|
return solid([s2,strip,s1],
|
535
680
|
[],
|
536
681
|
['procedure',call])
|
537
|
-
|
538
|
-
|
682
|
+
|
683
|
+
|
684
|
+
|
685
|
+
def _loft_surface(lower_loop, upper_loop, invert=False):
|
686
|
+
"""Create a surface connecting two loops.
|
687
|
+
|
688
|
+
Args:
|
689
|
+
lower_loop: List of points forming the lower loop (must be open, not closed)
|
690
|
+
upper_loop: List of points forming the upper loop (must be open, not closed)
|
691
|
+
invert: If True, reverse the face winding order
|
692
|
+
|
693
|
+
Returns:
|
694
|
+
A yapCAD surface connecting the two loops with triangle strips
|
695
|
+
"""
|
696
|
+
|
697
|
+
if not lower_loop or not upper_loop:
|
698
|
+
raise ValueError('invalid loops passed to loft surface')
|
699
|
+
lower = [point(p) for p in lower_loop]
|
700
|
+
upper = [point(p) for p in upper_loop]
|
701
|
+
if len(lower) != len(upper):
|
702
|
+
raise ValueError('loop length mismatch in loft surface')
|
703
|
+
|
704
|
+
vertices = lower + upper
|
705
|
+
normals = [[0, 0, 1, 0] for _ in vertices]
|
706
|
+
faces = []
|
707
|
+
count = len(lower)
|
708
|
+
|
709
|
+
for idx in range(count):
|
710
|
+
j0 = idx
|
711
|
+
j1 = (idx + 1) % count
|
712
|
+
j2 = j1 + count
|
713
|
+
j3 = idx + count
|
714
|
+
|
715
|
+
tri1 = [j0, j1, j2]
|
716
|
+
tri2 = [j0, j2, j3]
|
717
|
+
if invert:
|
718
|
+
tri1 = list(reversed(tri1))
|
719
|
+
tri2 = list(reversed(tri2))
|
720
|
+
|
721
|
+
try:
|
722
|
+
_, normal = tri2p0n([vertices[tri1[0]],
|
723
|
+
vertices[tri1[1]],
|
724
|
+
vertices[tri1[2]]])
|
725
|
+
except ValueError:
|
726
|
+
continue
|
727
|
+
|
728
|
+
for vid in {tri1[0], tri1[1], tri1[2], tri2[0], tri2[1], tri2[2]}:
|
729
|
+
normals[vid] = normal
|
730
|
+
|
731
|
+
faces.append(tri1)
|
732
|
+
faces.append(tri2)
|
733
|
+
|
734
|
+
return surface(vertices, normals, faces)
|
735
|
+
|
736
|
+
|
539
737
|
|
540
738
|
|
541
739
|
|
740
|
+
|
741
|
+
def _circle_loop(center_xy, radius, minang):
|
742
|
+
arc_geom = [arc(point(center_xy[0], center_xy[1]), radius)]
|
743
|
+
loop = geomlist2poly(arc_geom, minang=minang, minlen=0.0)
|
744
|
+
if not loop:
|
745
|
+
raise ValueError('failed to generate circle loop')
|
746
|
+
return loop
|
747
|
+
|
748
|
+
|
749
|
+
def tube(outer_diameter, wall_thickness, length,
|
750
|
+
center=None, *, base_point=None, minang=5.0, include_caps=True):
|
751
|
+
"""Create a cylindrical tube solid.
|
752
|
+
|
753
|
+
``base_point`` (or legacy ``center`` argument) identifies the base of the
|
754
|
+
cylindrical wall, i.e. the plane where ``z == base_point[2]``.
|
755
|
+
"""
|
756
|
+
|
757
|
+
if base_point is not None and center is not None:
|
758
|
+
raise ValueError('specify only base_point (preferred) or center, not both')
|
759
|
+
if base_point is None:
|
760
|
+
base_point = center if center is not None else point(0, 0, 0)
|
761
|
+
if len(base_point) < 3:
|
762
|
+
raise ValueError('base_point must contain x, y, z components')
|
763
|
+
base_point = point(base_point)
|
764
|
+
|
765
|
+
if wall_thickness <= epsilon:
|
766
|
+
raise ValueError('wall thickness must be positive')
|
767
|
+
|
768
|
+
outer_radius = outer_diameter / 2.0
|
769
|
+
inner_radius = outer_radius - wall_thickness
|
770
|
+
if inner_radius <= epsilon:
|
771
|
+
raise ValueError('wall thickness too large for tube')
|
772
|
+
|
773
|
+
base_z = base_point[2]
|
774
|
+
center_xy = (base_point[0], base_point[1])
|
775
|
+
|
776
|
+
base_loop_xy = _circle_loop(center_xy, outer_radius, minang)
|
777
|
+
inner_loop_xy = list(reversed(_circle_loop(center_xy, inner_radius, minang)))
|
778
|
+
|
779
|
+
base_surface, _ = poly2surfaceXY(base_loop_xy, holepolys=[inner_loop_xy])
|
780
|
+
base_surface = reversesurface(base_surface)
|
781
|
+
base_surface = translatesurface(base_surface, point(0, 0, base_z))
|
782
|
+
|
783
|
+
top_surface, _ = poly2surfaceXY(base_loop_xy, holepolys=[inner_loop_xy])
|
784
|
+
top_surface = translatesurface(top_surface, point(0, 0, base_z + length))
|
785
|
+
|
786
|
+
outer_base = [point(p[0], p[1], base_z, 1.0) for p in base_loop_xy[:-1]]
|
787
|
+
inner_base = [point(p[0], p[1], base_z, 1.0) for p in inner_loop_xy[:-1]]
|
788
|
+
outer_top = [point(p[0], p[1], base_z + length, 1.0) for p in base_loop_xy[:-1]]
|
789
|
+
inner_top = [point(p[0], p[1], base_z + length, 1.0) for p in inner_loop_xy[:-1]]
|
790
|
+
|
791
|
+
outer_side = _loft_surface(outer_base, outer_top)
|
792
|
+
inner_side = _loft_surface(inner_top, inner_base, invert=True)
|
793
|
+
|
794
|
+
call = f"yapcad.geom3d_util.tube({outer_diameter}, {wall_thickness}, {length}, base_point={base_point})"
|
795
|
+
surfaces = [base_surface, outer_side, top_surface, inner_side]
|
796
|
+
if not include_caps:
|
797
|
+
surfaces = [outer_side, inner_side]
|
798
|
+
return solid(surfaces,
|
799
|
+
[],
|
800
|
+
['procedure', call])
|
801
|
+
|
802
|
+
|
803
|
+
def conic_tube(bottom_outer_diameter, top_outer_diameter, wall_thickness,
|
804
|
+
length, center=None, *, base_point=None, minang=5.0, include_caps=True):
|
805
|
+
"""Create a conic tube with varying outer diameter.
|
806
|
+
|
807
|
+
``base_point`` (or ``center`` legacy argument) marks the axial base of the
|
808
|
+
frustum (the larger-diameter end when stacked)."""
|
809
|
+
|
810
|
+
if base_point is not None and center is not None:
|
811
|
+
raise ValueError('specify only base_point (preferred) or center, not both')
|
812
|
+
if base_point is None:
|
813
|
+
base_point = center if center is not None else point(0, 0, 0)
|
814
|
+
base_point = point(base_point)
|
815
|
+
|
816
|
+
if wall_thickness <= epsilon:
|
817
|
+
raise ValueError('wall thickness must be positive')
|
818
|
+
|
819
|
+
r0_outer = bottom_outer_diameter / 2.0
|
820
|
+
r1_outer = top_outer_diameter / 2.0
|
821
|
+
r0_inner = r0_outer - wall_thickness
|
822
|
+
r1_inner = r1_outer - wall_thickness
|
823
|
+
if r0_inner <= epsilon or r1_inner <= epsilon:
|
824
|
+
raise ValueError('wall thickness too large for conic tube')
|
825
|
+
|
826
|
+
base_z = base_point[2]
|
827
|
+
center_xy = (base_point[0], base_point[1])
|
828
|
+
|
829
|
+
base_outer_loop = _circle_loop(center_xy, r0_outer, minang)
|
830
|
+
base_inner_loop = list(reversed(_circle_loop(center_xy, r0_inner, minang)))
|
831
|
+
|
832
|
+
top_outer_loop = _circle_loop(center_xy, r1_outer, minang)
|
833
|
+
top_inner_loop = list(reversed(_circle_loop(center_xy, r1_inner, minang)))
|
834
|
+
|
835
|
+
base_surface, _ = poly2surfaceXY(base_outer_loop, holepolys=[base_inner_loop])
|
836
|
+
base_surface = reversesurface(base_surface)
|
837
|
+
base_surface = translatesurface(base_surface, point(0, 0, base_z))
|
838
|
+
|
839
|
+
top_surface, _ = poly2surfaceXY(top_outer_loop, holepolys=[top_inner_loop])
|
840
|
+
top_surface = translatesurface(top_surface, point(0, 0, base_z + length))
|
841
|
+
|
842
|
+
outer_base = [point(p[0], p[1], base_z, 1.0) for p in base_outer_loop[:-1]]
|
843
|
+
outer_top = [point(p[0], p[1], base_z + length, 1.0) for p in top_outer_loop[:-1]]
|
844
|
+
inner_base = [point(p[0], p[1], base_z, 1.0) for p in base_inner_loop[:-1]]
|
845
|
+
inner_top = [point(p[0], p[1], base_z + length, 1.0) for p in top_inner_loop[:-1]]
|
846
|
+
|
847
|
+
outer_side = _loft_surface(outer_base, outer_top)
|
848
|
+
inner_side = _loft_surface(inner_top, inner_base, invert=True)
|
849
|
+
|
850
|
+
call = ("yapcad.geom3d_util.conic_tube("
|
851
|
+
f"{bottom_outer_diameter}, {top_outer_diameter}, {wall_thickness}, {length}, base_point={base_point})")
|
852
|
+
surfaces = [base_surface, outer_side, top_surface, inner_side]
|
853
|
+
if not include_caps:
|
854
|
+
surfaces = [outer_side, inner_side]
|
855
|
+
return solid(surfaces,
|
856
|
+
[],
|
857
|
+
['procedure', call])
|
858
|
+
|
859
|
+
|
860
|
+
def spherical_shell(outer_diameter, wall_thickness,
|
861
|
+
solid_angle=4 * math.pi, center=point(0, 0, 0),
|
862
|
+
*, minang=5.0, steps=24):
|
863
|
+
"""Create a spherical shell or cap defined by a solid angle."""
|
864
|
+
|
865
|
+
if wall_thickness <= epsilon:
|
866
|
+
raise ValueError('wall thickness must be positive')
|
867
|
+
|
868
|
+
outer_radius = outer_diameter / 2.0
|
869
|
+
inner_radius = outer_radius - wall_thickness
|
870
|
+
if inner_radius <= epsilon:
|
871
|
+
raise ValueError('wall thickness too large for spherical shell')
|
872
|
+
|
873
|
+
solid_angle = max(min(solid_angle, 4 * math.pi), epsilon)
|
874
|
+
cos_theta = 1.0 - solid_angle / (2.0 * math.pi)
|
875
|
+
cos_theta = max(-1.0, min(1.0, cos_theta))
|
876
|
+
theta = math.acos(cos_theta)
|
877
|
+
|
878
|
+
arc_samples = max(12, int(round(360.0 / minang)))
|
879
|
+
lat_steps = max(4, int(math.ceil(theta / math.radians(minang))))
|
880
|
+
|
881
|
+
cz = center[2]
|
882
|
+
|
883
|
+
def _sphere_contour(radius):
|
884
|
+
def contour(z):
|
885
|
+
dz = z - cz
|
886
|
+
dz = max(min(dz, radius), -radius)
|
887
|
+
return math.sqrt(max(radius * radius - dz * dz, 0.0))
|
888
|
+
return contour
|
889
|
+
|
890
|
+
z_start_outer = cz + outer_radius * math.cos(theta)
|
891
|
+
z_start_inner = cz + inner_radius * math.cos(theta)
|
892
|
+
z_end_outer = cz + outer_radius
|
893
|
+
z_end_inner = cz + inner_radius
|
894
|
+
|
895
|
+
outer_surface = makeRevolutionSurface(_sphere_contour(outer_radius),
|
896
|
+
z_start_outer, z_end_outer,
|
897
|
+
max(steps, lat_steps),
|
898
|
+
arcSamples=arc_samples)
|
899
|
+
outer_surface = translatesurface(outer_surface, point(center[0], center[1], 0))
|
900
|
+
|
901
|
+
inner_surface = makeRevolutionSurface(_sphere_contour(inner_radius),
|
902
|
+
z_start_inner, z_end_inner,
|
903
|
+
max(steps, lat_steps),
|
904
|
+
arcSamples=arc_samples)
|
905
|
+
inner_surface = translatesurface(inner_surface, point(center[0], center[1], 0))
|
906
|
+
inner_surface = reversesurface(inner_surface)
|
907
|
+
|
908
|
+
surfaces = [outer_surface, inner_surface]
|
909
|
+
|
910
|
+
if theta < math.pi - epsilon:
|
911
|
+
r_outer_ring = outer_radius * math.sin(theta)
|
912
|
+
r_inner_ring = inner_radius * math.sin(theta)
|
913
|
+
|
914
|
+
base_outer = [point(center[0] + p[0], center[1] + p[1], cz + outer_radius * math.cos(theta), 1.0)
|
915
|
+
for p in _circle_loop((0, 0), r_outer_ring, minang)[:-1]]
|
916
|
+
base_inner = [point(center[0] + p[0], center[1] + p[1], cz + inner_radius * math.cos(theta), 1.0)
|
917
|
+
# for p in list(reversed(_circle_loop((0, 0), r_inner_ring, minang)))[:-1]]
|
918
|
+
for p in _circle_loop((0, 0), r_inner_ring, minang)[:-1]]
|
919
|
+
|
920
|
+
conic_surface = _loft_surface(base_outer, base_inner, invert=False)
|
921
|
+
surfaces.append(conic_surface)
|
922
|
+
|
923
|
+
call = ("yapcad.geom3d_util.spherical_shell("
|
924
|
+
f"{outer_diameter}, {wall_thickness}, {solid_angle}, center={center})")
|
925
|
+
return solid(surfaces,
|
926
|
+
[],
|
927
|
+
['procedure', call])
|
928
|
+
|
929
|
+
|
930
|
+
def stack_solids(solids, *, axis='z', start=0.0, gap=0.0, align='center'):
|
931
|
+
"""Return translated copies of ``solids`` stacked along an axis."""
|
932
|
+
|
933
|
+
if not solids:
|
934
|
+
return []
|
935
|
+
|
936
|
+
axis = axis.lower()
|
937
|
+
if axis not in ('x', 'y', 'z'):
|
938
|
+
raise ValueError('axis must be one of x, y, or z')
|
939
|
+
|
940
|
+
axis_idx = {'x': 0, 'y': 1, 'z': 2}[axis]
|
941
|
+
other_idx = [i for i in range(3) if i != axis_idx]
|
942
|
+
|
943
|
+
placed = []
|
944
|
+
cursor = start
|
945
|
+
reference = None
|
946
|
+
pending_gap = 0.0
|
947
|
+
|
948
|
+
for entry in solids:
|
949
|
+
if isinstance(entry, str):
|
950
|
+
directive = entry.strip().lower()
|
951
|
+
if directive.startswith('space:'):
|
952
|
+
try:
|
953
|
+
value = float(directive.split(':', 1)[1])
|
954
|
+
except ValueError as exc:
|
955
|
+
raise ValueError(f'bad spacing directive {entry}') from exc
|
956
|
+
pending_gap += value
|
957
|
+
continue
|
958
|
+
raise ValueError(f'unsupported directive {entry!r} in stack_solids')
|
959
|
+
|
960
|
+
solid_obj = entry
|
961
|
+
bbox = solidbbox(solid_obj)
|
962
|
+
length = bbox[1][axis_idx] - bbox[0][axis_idx]
|
963
|
+
if length < epsilon:
|
964
|
+
raise ValueError('solid has zero length along stacking axis')
|
965
|
+
|
966
|
+
cursor += pending_gap
|
967
|
+
pending_gap = 0.0
|
968
|
+
|
969
|
+
translation = [0.0, 0.0, 0.0]
|
970
|
+
translation[axis_idx] = cursor - bbox[0][axis_idx]
|
971
|
+
|
972
|
+
if reference is None:
|
973
|
+
if align == 'center':
|
974
|
+
reference = [
|
975
|
+
(bbox[0][idx] + bbox[1][idx]) / 2.0 for idx in other_idx
|
976
|
+
]
|
977
|
+
elif align == 'min':
|
978
|
+
reference = [bbox[0][idx] for idx in other_idx]
|
979
|
+
elif align == 'max':
|
980
|
+
reference = [bbox[1][idx] for idx in other_idx]
|
981
|
+
else:
|
982
|
+
raise ValueError('align must be center, min, or max')
|
983
|
+
|
984
|
+
if align == 'center':
|
985
|
+
for ref_val, idx in zip(reference, other_idx):
|
986
|
+
translation[idx] = ref_val - (bbox[0][idx] + bbox[1][idx]) / 2.0
|
987
|
+
elif align == 'min':
|
988
|
+
for ref_val, idx in zip(reference, other_idx):
|
989
|
+
translation[idx] = ref_val - bbox[0][idx]
|
990
|
+
elif align == 'max':
|
991
|
+
for ref_val, idx in zip(reference, other_idx):
|
992
|
+
translation[idx] = ref_val - bbox[1][idx]
|
993
|
+
|
994
|
+
placed.append(translatesolid(solid_obj, vect(translation[0], translation[1], translation[2], 0)))
|
995
|
+
cursor += length + gap
|
996
|
+
|
997
|
+
return placed
|