11#! /usr/bin/env python3
2+ from solid .objects import linear_extrude
23from solid .solidpython import OpenSCADObject
34import sys
45from math import cos , radians , sin , pi , tau
56from pathlib import Path
67
7- from euclid3 import Point2 , Point3
8+ from euclid3 import Point2 , Point3 , Vector3
89
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
1113
1214
13- from typing import Set , Sequence , List , Callable , Optional , Union , Iterable
15+ from typing import Set , Sequence , List , Callable , Optional , Union , Iterable , Tuple
1416
1517SEGMENTS = 48
18+ PATH_RAD = 50
19+ SHAPE_RAD = 15
20+
21+ TEXT_LOC = [- 0.6 * PATH_RAD , 1.6 * PATH_RAD ]
1622
1723def basic_extrude_example ():
18- path_rad = 50
24+ path_rad = PATH_RAD
1925 shape = star (num_points = 5 )
2026 path = sinusoidal_ring (rad = path_rad , segments = 240 )
2127
28+ # At its simplest, just sweep a shape along a path
2229 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' )
2531 return extruded
2632
2733def extrude_example_xy_scaling () -> OpenSCADObject :
2834 num_points = SEGMENTS
29- path_rad = 50
35+ path_rad = PATH_RAD
3036 circle = circle_points (15 )
3137 path = circle_points (rad = path_rad )
3238
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
3740 # 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' )
3942 no_scale_obj += extrude_along_path (circle , path )
4043
44+ # angles: from 0 to 6*Pi
45+ angles = list ((frange (0 , 3 * tau , num_steps = len (path ))))
46+
4147 # With a 1-D scale factor, an extrusion grows and shrinks uniformly
4248 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 )
4551
4652 # With a 2D scale factor, a shape's X & Y dimensions can scale
4753 # independently, leading to more interesting shapes
4854 # X & Y scales vary between 0.5 & 1.5
4955 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 )
5258
5359 obj = no_scale_obj + right (3 * path_rad )(x_obj ) + right (6 * path_rad )(xy_obj )
5460 return obj
@@ -57,20 +63,117 @@ def extrude_example_capped_ends() -> OpenSCADObject:
5763 num_points = SEGMENTS / 2
5864 path_rad = 50
5965 circle = star (6 )
60- path = circle_points (rad = path_rad )
66+ path = circle_points (rad = path_rad )[: - 4 ]
6167
6268 # If `connect_ends` is False or unspecified, ends will be capped.
6369 # 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 )
6672
6773 # 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' )
6975 connected_obj += extrude_along_path (circle , path , connect_ends = True )
7076
7177 return capped_obj + right (3 * path_rad )(connected_obj )
7278
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 ]:
74177 outline = []
75178 for i in range (segments ):
76179 angle = radians (i * 360 / segments )
@@ -83,26 +186,42 @@ def sinusoidal_ring(rad=25, segments=SEGMENTS):
83186 outline .append (Point3 (x , y , z ))
84187 return outline
85188
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 ] :
87190 star_pts = []
88191 for i in range (2 * num_points ):
89192 rad = outer_rad - i % 2 * dip_factor * outer_rad
90193 angle = radians (360 / (2 * num_points ) * i )
91194 star_pts .append (Point3 (rad * cos (angle ), rad * sin (angle ), 0 ))
92195 return star_pts
93196
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 )
96199 points = list ([Point2 (rad * cos (a ), rad * sin (a )) for a in angles ])
97200 return points
98201
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+ # ===============
99212if __name__ == "__main__" :
100213 out_dir = sys .argv [1 ] if len (sys .argv ) > 1 else Path (__file__ ).parent
101214
102215 basic_extrude = basic_extrude_example ()
103216 scaled_extrusions = extrude_example_xy_scaling ()
104217 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 ])
106225
107226 file_out = scad_render_to_file (a , out_dir = out_dir , include_orig_code = True )
108227 print (f"{ __file__ } : SCAD file written to: \n { file_out } " )
0 commit comments