1010FacetIndices = Tuple [int , int , int ]
1111Point3Transform = Callable [[Point3 , Optional [float ], Optional [float ]], Point3 ]
1212
13+
1314# ==========================
1415# = Extrusion along a path =
1516# ==========================
16- def extrude_along_path ( shape_pts :Points ,
17- path_pts :Points ,
18- scales :Sequence [Union [Vector2 , float , Tuple2 ]] = None ,
19- rotations : Sequence [float ] = None ,
20- transforms : Sequence [Point3Transform ] = None ,
21- connect_ends = False ,
22- cap_ends = True ) -> OpenSCADObject :
23- '''
17+ def extrude_along_path (
18+ shape_pts : Points ,
19+ path_pts : Points ,
20+ scales : Sequence [Union [Vector2 , float , Tuple2 ]] = None ,
21+ rotations : Sequence [float ] = None ,
22+ transforms : Sequence [Callable ] = None ,
23+ connect_ends = False ,
24+ cap_ends = True ,
25+ transform_args : List = None ,
26+ transform_kwargs : Dict = None ,
27+ ) -> OpenSCADObject :
28+ """
2429 Extrude the curve defined by shape_pts along path_pts.
2530 -- For predictable results, shape_pts must be planar, convex, and lie
2631 in the XY plane centered around the origin. *Some* nonconvexity (e.g, star shapes)
2732 and nonplanarity will generally work fine
28-
33+
2934 -- len(scales) should equal len(path_pts). No-op if not supplied
30- Each entry may be a single number for uniform scaling, or a pair of
35+ Each entry may be a single number for uniform scaling, or a pair of
3136 numbers (or Point2) for differential X/Y scaling
3237 If not supplied, no scaling will occur.
33-
38+
3439 -- len(rotations) should equal 1 or len(path_pts). No-op if not supplied.
3540 Each point in shape_pts will be rotated by rotations[i] degrees at
3641 each point in path_pts. Or, if only one rotation is supplied, the shape
3742 will be rotated smoothly over rotations[0] degrees in the course of the extrusion
38-
43+
3944 -- len(transforms) should be 1 or be equal to len(path_pts). No-op if not supplied.
40- Each entry should be have the signature:
45+ Each entry should be have the signature:
4146 def transform_func(p:Point3, path_norm:float, loop_norm:float): Point3
4247 where path_norm is in [0,1] and expresses progress through the extrusion
4348 and loop_norm is in [0,1] and express progress through a single loop of the extrusion
44-
49+
4550 -- if connect_ends is True, the first and last loops of the extrusion will
4651 be joined, which is useful for toroidal geometries. Overrides cap_ends
4752
4853 -- if cap_ends is True, each point in the first and last loops of the extrusion
4954 will be connected to the centroid of that loop. For planar, convex shapes, this
5055 works nicely. If shape is less planar or convex, some self-intersection may happen.
5156 Not applied if connect_ends is True
52- '''
53-
57+ """
5458
55- polyhedron_pts :Points = []
56- facet_indices :List [Tuple [int , int , int ]] = []
59+ polyhedron_pts : Points = []
60+ facet_indices : List [Tuple [int , int , int ]] = []
5761
5862 # Make sure we've got Euclid Point3's for all elements
5963 shape_pts = euclidify (shape_pts , Point3 )
@@ -66,7 +70,7 @@ def transform_func(p:Point3, path_norm:float, loop_norm:float): Point3
6670 tangent_path_points : List [Point3 ] = []
6771
6872 # If first & last points are the same, let's close the shape
69- first_last_equal = (( path_pts [0 ] - path_pts [- 1 ]).magnitude_squared () < EPSILON )
73+ first_last_equal = (path_pts [0 ] - path_pts [- 1 ]).magnitude_squared () < EPSILON
7074 if first_last_equal :
7175 connect_ends = True
7276 path_pts = path_pts [:][:- 1 ]
@@ -77,33 +81,68 @@ def transform_func(p:Point3, path_norm:float, loop_norm:float): Point3
7781 first = Point3 (* (path_pts [0 ] - (path_pts [1 ] - path_pts [0 ])))
7882 last = Point3 (* (path_pts [- 1 ] - (path_pts [- 2 ] - path_pts [- 1 ])))
7983 tangent_path_points = [first ] + path_pts + [last ]
80- tangents = [tangent_path_points [i + 2 ] - tangent_path_points [i ] for i in range (len (path_pts ))]
84+ tangents = [
85+ tangent_path_points [i + 2 ] - tangent_path_points [i ]
86+ for i in range (len (path_pts ))
87+ ]
8188
8289 for which_loop in range (len (path_pts )):
8390 # path_normal is 0 at the first path_pts and 1 at the last
84- path_normal = which_loop / (len (path_pts ) - 1 )
91+ path_normal = which_loop / (len (path_pts ) - 1 )
8592
8693 path_pt = path_pts [which_loop ]
8794 tangent = tangents [which_loop ]
8895 scale = scales [which_loop ] if scales else 1
8996
9097 rotate_degrees = None
9198 if rotations :
92- rotate_degrees = rotations [which_loop ] if len (rotations ) > 1 else rotations [0 ] * path_normal
99+ rotate_degrees = (
100+ rotations [which_loop ]
101+ if len (rotations ) > 1
102+ else rotations [0 ] * path_normal
103+ )
93104
94105 transform_func = None
95106 if transforms :
96- transform_func = transforms [which_loop ] if len (transforms ) > 1 else transforms [0 ]
107+ transform_func = (
108+ transforms [which_loop ] if len (transforms ) > 1 else transforms [0 ]
109+ )
110+ # Default to no *args or **kwargs
111+ this_transform_func_args = None
112+ if transform_args :
113+ this_transform_func_args = (
114+ transform_args [which_loop ]
115+ if len (transform_args ) > 1
116+ else transform_args [0 ]
117+ )
118+ this_transform_func_kwargs = None
119+ if transform_kwargs :
120+ this_transform_func_kwargs = (
121+ transform_kwargs [which_loop ]
122+ if len (transform_kwargs ) > 1
123+ else transform_kwargs [0 ]
124+ )
97125
98126 this_loop = shape_pts [:]
99- this_loop = _scale_loop (this_loop , scale )
127+ this_loop = _scale_loop (
128+ this_loop ,
129+ scale ,
130+ )
100131 this_loop = _rotate_loop (this_loop , rotate_degrees )
101- this_loop = _transform_loop (this_loop , transform_func , path_normal )
102-
103- this_loop = transform_to_point (this_loop , dest_point = path_pt , dest_normal = tangent , src_up = src_up )
132+ this_loop = _transform_loop (
133+ this_loop ,
134+ transform_func ,
135+ path_normal ,
136+ this_transform_func_args ,
137+ this_transform_func_kwargs ,
138+ )
139+
140+ this_loop = transform_to_point (
141+ this_loop , dest_point = path_pt , dest_normal = tangent , src_up = src_up
142+ )
104143 loop_start_index = which_loop * shape_pt_count
105144
106- if ( which_loop < len (path_pts ) - 1 ) :
145+ if which_loop < len (path_pts ) - 1 :
107146 loop_facets = _loop_facet_indices (loop_start_index , shape_pt_count )
108147 facet_indices += loop_facets
109148
@@ -117,54 +156,76 @@ def transform_func(p:Point3, path_norm:float, loop_norm:float): Point3
117156
118157 elif cap_ends :
119158 # OpenSCAD's polyhedron will automatically triangulate faces as needed.
120- # So just include all points at each end of the tube
121- last_loop_start_index = len (polyhedron_pts ) - shape_pt_count
159+ # So just include all points at each end of the tube
160+ last_loop_start_index = len (polyhedron_pts ) - shape_pt_count
122161 start_loop_indices = list (reversed (range (shape_pt_count )))
123- end_loop_indices = list (range (last_loop_start_index , last_loop_start_index + shape_pt_count ))
162+ end_loop_indices = list (
163+ range (last_loop_start_index , last_loop_start_index + shape_pt_count )
164+ )
124165 facet_indices .append (start_loop_indices )
125166 facet_indices .append (end_loop_indices )
126167
127- return polyhedron (points = euc_to_arr (polyhedron_pts ), faces = facet_indices ) # type: ignore
168+ return polyhedron (points = euc_to_arr (polyhedron_pts ), faces = facet_indices ) # type: ignore
169+
128170
129- def _loop_facet_indices (loop_start_index :int , loop_pt_count :int , next_loop_start_index = None ) -> List [FacetIndices ]:
171+ def _loop_facet_indices (
172+ loop_start_index : int , loop_pt_count : int , next_loop_start_index = None
173+ ) -> List [FacetIndices ]:
130174 facet_indices : List [FacetIndices ] = []
131175 # nlsi == next_loop_start_index
132176 if next_loop_start_index == None :
133177 next_loop_start_index = loop_start_index + loop_pt_count
134- loop_indices = list (range (loop_start_index , loop_pt_count + loop_start_index )) + [loop_start_index ]
135- next_loop_indices = list (range (next_loop_start_index , loop_pt_count + next_loop_start_index )) + [next_loop_start_index ]
178+ loop_indices = list (range (loop_start_index , loop_pt_count + loop_start_index )) + [
179+ loop_start_index
180+ ]
181+ next_loop_indices = list (
182+ range (next_loop_start_index , loop_pt_count + next_loop_start_index )
183+ ) + [next_loop_start_index ]
136184
137185 for i , (a , b ) in enumerate (zip (loop_indices [:- 1 ], loop_indices [1 :])):
138- c , d = next_loop_indices [i : i + 2 ]
186+ c , d = next_loop_indices [i : i + 2 ]
139187 # OpenSCAD's polyhedron will accept quads and do its own triangulation with them,
140- # so we could just append (a,b,d,c).
188+ # so we could just append (a,b,d,c).
141189 # However, this lets OpenSCAD (Or CGAL?) do its own triangulation, leading
142190 # to some strange outcomes. Prefer to do our own triangulation.
143191 # c--d
144192 # |\ |
145193 # | \|
146- # a--b
194+ # a--b
147195 # facet_indices.append((a,b,d,c))
148- facet_indices .append ((a ,b , c ))
149- facet_indices .append ((b ,d , c ))
196+ facet_indices .append ((a , b , c ))
197+ facet_indices .append ((b , d , c ))
150198 return facet_indices
151199
152- def _rotate_loop (points :Sequence [Point3 ], rotation_degrees :float = None ) -> List [Point3 ]:
200+
201+ def _rotate_loop (
202+ points : Sequence [Point3 ], rotation_degrees : float = None
203+ ) -> List [Point3 ]:
153204 if rotation_degrees is None :
154205 return points
155- up = Vector3 (0 ,0 , 1 )
206+ up = Vector3 (0 , 0 , 1 )
156207 rads = radians (rotation_degrees )
157208 return [p .rotate_around (up , rads ) for p in points ]
158209
159- def _scale_loop (points :Sequence [Point3 ], scale :Union [float , Point2 , Tuple2 ]= None ) -> List [Point3 ]:
210+
211+ def _scale_loop (
212+ points : Sequence [Point3 ], scale : Union [float , Point2 , Tuple2 ] = None
213+ ) -> List [Point3 ]:
160214 if scale is None :
161215 return points
162216
163217 if isinstance (scale , (float , int )):
164218 scale = [scale ] * 2
165219 return [Point3 (point .x * scale [0 ], point .y * scale [1 ], point .z ) for point in points ]
166220
167- def _transform_loop (points :Sequence [Point3 ], transform_func :Point3Transform = None , path_normal :float = None ) -> List [Point3 ]:
221+
222+ def _transform_loop (
223+ points : Sequence [Point3 ],
224+ transform_func : Callable = None ,
225+ path_normal : float = None ,
226+ * args ,
227+ ** kwargs ,
228+ ) -> List [Point3 ]:
168229 # transform_func is a function that takes a point and optionally two floats,
169230 # a `path_normal`, in [0,1] that indicates where this loop is in a path extrusion,
170231 # and `loop_normal` in [0,1] that indicates where this point is in a list of points
@@ -174,8 +235,9 @@ def _transform_loop(points:Sequence[Point3], transform_func:Point3Transform = No
174235 result = []
175236 for i , p in enumerate (points ):
176237 # i goes from 0 to 1 across points
177- loop_normal = i / (len (points ) - 1 )
178- new_p = transform_func (p , path_normal , loop_normal )
238+ loop_normal = i / (len (points ) - 1 )
239+ # print(f"args:\t{args}")
240+ # print(f"kwargs:\t{kwargs}")
241+ new_p = transform_func (p , path_normal , loop_normal , * args , ** kwargs )
179242 result .append (new_p )
180243 return result
181-
0 commit comments