1
1
#! /usr/bin/env python3
2
+ from solid .objects import linear_extrude
2
3
from solid .solidpython import OpenSCADObject
3
4
import sys
4
5
from math import cos , radians , sin , pi , tau
5
6
from pathlib import Path
6
7
7
- from euclid3 import Point2 , Point3
8
+ from euclid3 import Point2 , Point3 , Vector3
8
9
9
- from solid import scad_render_to_file , text , translate
10
- from solid .utils import extrude_along_path , right
10
+ from solid import scad_render_to_file , text , translate , cube , color , rotate
11
+ from solid .utils import UP_VEC , Vector23 , distribute_in_grid , extrude_along_path
12
+ from solid .utils import down , right , frange , lerp
11
13
12
14
13
- from typing import Set , Sequence , List , Callable , Optional , Union , Iterable
15
+ from typing import Set , Sequence , List , Callable , Optional , Union , Iterable , Tuple
14
16
15
17
SEGMENTS = 48
18
+ PATH_RAD = 50
19
+ SHAPE_RAD = 15
20
+
21
+ TEXT_LOC = [- 0.6 * PATH_RAD , 1.6 * PATH_RAD ]
16
22
17
23
def basic_extrude_example ():
18
- path_rad = 50
24
+ path_rad = PATH_RAD
19
25
shape = star (num_points = 5 )
20
26
path = sinusoidal_ring (rad = path_rad , segments = 240 )
21
27
28
+ # At its simplest, just sweep a shape along a path
22
29
extruded = extrude_along_path ( shape_pts = shape , path_pts = path )
23
- # Label
24
- extruded += translate ([- path_rad / 2 , 2 * path_rad ])(text ('Basic Extrude' ))
30
+ extruded += make_label ('Basic Extrude' )
25
31
return extruded
26
32
27
33
def extrude_example_xy_scaling () -> OpenSCADObject :
28
34
num_points = SEGMENTS
29
- path_rad = 50
35
+ path_rad = PATH_RAD
30
36
circle = circle_points (15 )
31
37
path = circle_points (rad = path_rad )
32
38
33
- # angle: from 0 to 6*Pi
34
- angles = list ((i / (num_points - 1 )* tau * 3 for i in range (len (path ))))
35
-
36
- # If scale_factors aren't included, they'll default to
39
+ # If scales aren't included, they'll default to
37
40
# no scaling at each step along path.
38
- no_scale_obj = translate ([ - path_rad / 2 , 2 * path_rad ])( text ( 'No Scale' ) )
41
+ no_scale_obj = make_label ( 'No Scale' )
39
42
no_scale_obj += extrude_along_path (circle , path )
40
43
44
+ # angles: from 0 to 6*Pi
45
+ angles = list ((frange (0 , 3 * tau , num_steps = len (path ))))
46
+
41
47
# With a 1-D scale factor, an extrusion grows and shrinks uniformly
42
48
x_scales = [(1 + cos (a )/ 2 ) for a in angles ]
43
- x_obj = translate ([ - path_rad / 2 , 2 * path_rad ])( text ( '1D Scale' ) )
44
- x_obj += extrude_along_path (circle , path , scale_factors = x_scales )
49
+ x_obj = make_label ( '1D Scale' )
50
+ x_obj += extrude_along_path (circle , path , scales = x_scales )
45
51
46
52
# With a 2D scale factor, a shape's X & Y dimensions can scale
47
53
# independently, leading to more interesting shapes
48
54
# X & Y scales vary between 0.5 & 1.5
49
55
xy_scales = [Point2 ( 1 + cos (a )/ 2 , 1 + sin (a )/ 2 ) for a in angles ]
50
- xy_obj = translate ([ - path_rad / 2 , 2 * path_rad ])( text ( '2D Scale' ) )
51
- xy_obj += extrude_along_path (circle , path , scale_factors = xy_scales )
56
+ xy_obj = make_label ( '2D Scale' )
57
+ xy_obj += extrude_along_path (circle , path , scales = xy_scales )
52
58
53
59
obj = no_scale_obj + right (3 * path_rad )(x_obj ) + right (6 * path_rad )(xy_obj )
54
60
return obj
@@ -57,20 +63,117 @@ def extrude_example_capped_ends() -> OpenSCADObject:
57
63
num_points = SEGMENTS / 2
58
64
path_rad = 50
59
65
circle = star (6 )
60
- path = circle_points (rad = path_rad )
66
+ path = circle_points (rad = path_rad )[: - 4 ]
61
67
62
68
# If `connect_ends` is False or unspecified, ends will be capped.
63
69
# Endcaps will be correct for most convex or mildly concave (e.g. stars) cross sections
64
- capped_obj = translate ([ - path_rad / 2 , 2 * path_rad ])( text ( 'Capped Ends' ) )
65
- capped_obj += extrude_along_path (circle , path , connect_ends = False )
70
+ capped_obj = make_label ( 'Capped Ends' )
71
+ capped_obj += extrude_along_path (circle , path , connect_ends = False , cap_ends = True )
66
72
67
73
# If `connect_ends` is specified, create a continuous manifold object
68
- connected_obj = translate ([ - path_rad / 2 , 2 * path_rad ])( text ( 'Connected Ends' ) )
74
+ connected_obj = make_label ( 'Connected Ends' )
69
75
connected_obj += extrude_along_path (circle , path , connect_ends = True )
70
76
71
77
return capped_obj + right (3 * path_rad )(connected_obj )
72
78
73
- def sinusoidal_ring (rad = 25 , segments = SEGMENTS ):
79
+ def extrude_example_rotations () -> OpenSCADObject :
80
+ path_rad = PATH_RAD
81
+ shape = star (num_points = 5 )
82
+ path = circle_points (path_rad , num_points = 240 )
83
+
84
+ # For a simple example, make one complete revolution by the end of the extrusion
85
+ simple_rot = make_label ('Simple Rotation' )
86
+ simple_rot += extrude_along_path (shape , path , rotations = [360 ], connect_ends = True )
87
+
88
+ # For a more complex set of rotations, add a rotation degree for each point in path
89
+ complex_rotations = []
90
+ degs = 0
91
+ oscillation_max = 60
92
+
93
+ for i in frange (0 , 1 , num_steps = len (path )):
94
+ # For the first third of the path, do one complete rotation
95
+ if i <= 0.333 :
96
+ degs = i / 0.333 * 360
97
+ # For the second third of the path, oscillate between +/- oscillation_max degrees
98
+ elif i <= 0.666 :
99
+ angle = lerp (i , 0.333 , 0.666 , 0 , 2 * tau )
100
+ degs = oscillation_max * sin (angle )
101
+ # For the last third of the path, oscillate increasingly fast but with smaller magnitude
102
+ else :
103
+ # angle increases in a nonlinear curve, so
104
+ # oscillations should get quicker and quicker
105
+ x = lerp (i , 0.666 , 1.0 , 0 , 2 )
106
+ angle = pow (x , 2.2 ) * tau
107
+ # decrease the size of the oscillations by a factor of 10
108
+ # over the course of this stretch
109
+ osc = lerp (i , 0.666 , 1.0 , oscillation_max , oscillation_max / 10 )
110
+ degs = osc * sin (angle )
111
+ complex_rotations .append (degs )
112
+
113
+ complex_rot = make_label ('Complex Rotation' )
114
+ complex_rot += extrude_along_path (shape , path , rotations = complex_rotations )
115
+
116
+ # Make some red markers to show the boundaries between the three sections of this path
117
+ marker_w = SHAPE_RAD * 1.5
118
+ marker = translate ([path_rad , 0 , 0 ])(
119
+ cube ([marker_w , 1 , marker_w ], center = True )
120
+ )
121
+ markers = [color ('red' )(rotate ([0 ,0 ,120 * i ])(marker )) for i in range (3 )]
122
+ complex_rot += markers
123
+
124
+ return simple_rot + right (3 * path_rad )(complex_rot )
125
+
126
+ def extrude_example_transforms () -> OpenSCADObject :
127
+ path_rad = PATH_RAD
128
+ height = 2 * SHAPE_RAD
129
+ num_steps = 120
130
+
131
+ shape = circle_points (rad = path_rad , num_points = 120 )
132
+ path = [Point3 (0 ,0 ,i ) for i in frange (0 , height , num_steps = num_steps )]
133
+
134
+ max_rotation = radians (15 )
135
+ max_z_displacement = height / 10
136
+ up = Vector3 (0 ,0 ,1 )
137
+
138
+ # The transforms argument is powerful.
139
+ # Each point in the entire extrusion will call this function with unique arguments:
140
+ # -- `path_norm` in [0, 1] specifying how far along in the extrusion a point's loop is
141
+ # -- `loop_norm` in [0, 1] specifying where in its loop a point is.
142
+ def point_trans (point : Point3 , path_norm :float , loop_norm : float ) -> Point3 :
143
+ # scale the point from 1x to 2x in the course of the
144
+ # extrusion,
145
+ scale = 1 + path_norm * path_norm / 2
146
+ p = scale * point
147
+
148
+ # Rotate the points sinusoidally up to max_rotation
149
+ p = p .rotate_around (up , max_rotation * sin (tau * path_norm ))
150
+
151
+
152
+ # Oscillate z values sinusoidally, growing from
153
+ # 0 magnitude to max_z_displacement
154
+ max_z = lerp (path_norm , 0 , 1 , 0 , max_z_displacement )
155
+ angle = lerp (loop_norm , 0 , 1 , 0 , 10 * tau )
156
+ p .z += max_z * sin (angle )
157
+ return p
158
+
159
+ no_trans = make_label ('No Transform' )
160
+ no_trans += down (height / 2 )(
161
+ extrude_along_path (shape , path , cap_ends = False )
162
+ )
163
+
164
+ # We can pass transforms a single function that will be called on all points,
165
+ # or pass a list with a transform function for each point along path
166
+ arb_trans = make_label ('Arbitrary Transform' )
167
+ arb_trans += down (height / 2 )(
168
+ extrude_along_path (shape , path , transforms = [point_trans ], cap_ends = False )
169
+ )
170
+
171
+ return no_trans + right (3 * path_rad )(arb_trans )
172
+
173
+ # ============
174
+ # = GEOMETRY =
175
+ # ============
176
+ def sinusoidal_ring (rad = 25 , segments = SEGMENTS ) -> List [Point3 ]:
74
177
outline = []
75
178
for i in range (segments ):
76
179
angle = radians (i * 360 / segments )
@@ -83,26 +186,42 @@ def sinusoidal_ring(rad=25, segments=SEGMENTS):
83
186
outline .append (Point3 (x , y , z ))
84
187
return outline
85
188
86
- def star (num_points = 5 , outer_rad = 15 , dip_factor = 0.5 ):
189
+ def star (num_points = 5 , outer_rad = SHAPE_RAD , dip_factor = 0.5 ) -> List [ Point3 ] :
87
190
star_pts = []
88
191
for i in range (2 * num_points ):
89
192
rad = outer_rad - i % 2 * dip_factor * outer_rad
90
193
angle = radians (360 / (2 * num_points ) * i )
91
194
star_pts .append (Point3 (rad * cos (angle ), rad * sin (angle ), 0 ))
92
195
return star_pts
93
196
94
- def circle_points (rad : float = 15 , num_points : int = SEGMENTS ) -> List [Point2 ]:
95
- angles = [ tau / num_points * i for i in range ( num_points )]
197
+ def circle_points (rad : float = SHAPE_RAD , num_points : int = SEGMENTS ) -> List [Point2 ]:
198
+ angles = frange ( 0 , tau , num_steps = num_points , include_end = True )
96
199
points = list ([Point2 (rad * cos (a ), rad * sin (a )) for a in angles ])
97
200
return points
98
201
202
+ def make_label (message :str , text_loc :Tuple [float , float ]= TEXT_LOC , height = 5 ) -> OpenSCADObject :
203
+ return translate (text_loc )(
204
+ linear_extrude (height )(
205
+ text (message )
206
+ )
207
+ )
208
+
209
+ # ===============
210
+ # = ENTRY POINT =
211
+ # ===============
99
212
if __name__ == "__main__" :
100
213
out_dir = sys .argv [1 ] if len (sys .argv ) > 1 else Path (__file__ ).parent
101
214
102
215
basic_extrude = basic_extrude_example ()
103
216
scaled_extrusions = extrude_example_xy_scaling ()
104
217
capped_extrusions = extrude_example_capped_ends ()
105
- a = basic_extrude + translate ([0 ,- 250 ])(scaled_extrusions ) + translate ([0 , - 500 ])(capped_extrusions )
218
+ rotated_extrusions = extrude_example_rotations ()
219
+ arbitrary_transforms = extrude_example_transforms ()
220
+ all_objs = [basic_extrude , scaled_extrusions , capped_extrusions , rotated_extrusions , arbitrary_transforms ]
221
+
222
+ a = distribute_in_grid (all_objs ,
223
+ max_bounding_box = [4 * PATH_RAD , 4 * PATH_RAD ],
224
+ rows_and_cols = [len (all_objs ), 1 ])
106
225
107
226
file_out = scad_render_to_file (a , out_dir = out_dir , include_orig_code = True )
108
227
print (f"{ __file__ } : SCAD file written to: \n { file_out } " )
0 commit comments