1//////////////////////////////////////////////////////////////////////
2// LibFile: drawing.scad
3// This file includes stroke(), which converts a path into a
4// geometric object, like drawing with a pen. It even works on
5// three-dimensional paths. You can make a dashed line or add arrow
6// heads. The turtle() function provides a turtle graphics style
7// approach for producing paths. The arc() function produces arc paths,
8// and helix() produces helical paths.
9// Includes:
10// include <BOSL2/std.scad>
11// FileGroup: Basic Modeling
12// FileSummary: Create and draw 2D and 3D paths: arc, helix, turtle graphics
13// FileFootnotes: STD=Included in std.scad
14//////////////////////////////////////////////////////////////////////
15
16
17// Section: Line Drawing
18
19// Module: stroke()
20// Usage:
21// stroke(path, [width], [closed], [endcaps], [endcap_width], [endcap_length], [endcap_extent], [trim]);
22// stroke(path, [width], [closed], [endcap1], [endcap2], [endcap_width1], [endcap_width2], [endcap_length1], [endcap_length2], [endcap_extent1], [endcap_extent2], [trim1], [trim2]);
23// Topics: Paths (2D), Paths (3D), Drawing Tools
24// Description:
25// Draws a 2D or 3D path with a given line width. Joints and each endcap can be replaced with
26// various marker shapes, and can be assigned different colors. If passed a region instead of
27// a path, draws each path in the region as a closed polygon by default. If `closed=false` is
28// given with a region or list of paths, then each path is drawn without the closing line segment.
29// To facilitate debugging, stroke() accepts "paths" that have a single point. These are drawn with
30// the style of endcap1, but have their own scale parameter, `singleton_scale`, which defaults to 2
31// so that singleton dots with endcap "round" are clearly visible.
32// Figure(Med,NoAxes,2D,VPR=[0,0,0],VPD=250): Endcap Types
33// cap_pairs = [
34// ["butt", "chisel" ],
35// ["round", "square" ],
36// ["line", "cross" ],
37// ["x", "diamond"],
38// ["dot", "block" ],
39// ["tail", "arrow" ],
40// ["tail2", "arrow2" ]
41// ];
42// for (i = idx(cap_pairs)) {
43// fwd((i-len(cap_pairs)/2+0.5)*13) {
44// stroke([[-20,0], [20,0]], width=3, endcap1=cap_pairs[i][0], endcap2=cap_pairs[i][1]);
45// color("black") {
46// stroke([[-20,0], [20,0]], width=0.25, endcaps=false);
47// left(28) text(text=cap_pairs[i][0], size=5, halign="right", valign="center");
48// right(28) text(text=cap_pairs[i][1], size=5, halign="left", valign="center");
49// }
50// }
51// }
52// Arguments:
53// path = The path to draw along.
54// width = The width of the line to draw. If given as a list of widths, (one for each path point), draws the line with varying thickness to each point.
55// closed = If true, draw an additional line from the end of the path to the start.
56// joints = Specifies the joint shape for each joint of the line. If a 2D polygon is given, use that to draw custom joints.
57// endcaps = Specifies the endcap type for both ends of the line. If a 2D polygon is given, use that to draw custom endcaps.
58// endcap1 = Specifies the endcap type for the start of the line. If a 2D polygon is given, use that to draw a custom endcap.
59// endcap2 = Specifies the endcap type for the end of the line. If a 2D polygon is given, use that to draw a custom endcap.
60// dots = Specifies both the endcap and joint types with one argument. If given `true`, sets both to "dot". If a 2D polygon is given, uses that to draw custom dots.
61// joint_width = Some joint shapes are wider than the line. This specifies the width of the shape, in multiples of the line width.
62// endcap_width = Some endcap types are wider than the line. This specifies the size of endcaps, in multiples of the line width.
63// endcap_width1 = This specifies the size of starting endcap, in multiples of the line width.
64// endcap_width2 = This specifies the size of ending endcap, in multiples of the line width.
65// dots_width = This specifies the size of the joints and endcaps, in multiples of the line width.
66// joint_length = Length of joint shape, in multiples of the line width.
67// endcap_length = Length of endcaps, in multiples of the line width.
68// endcap_length1 = Length of starting endcap, in multiples of the line width.
69// endcap_length2 = Length of ending endcap, in multiples of the line width.
70// dots_length = Length of both joints and endcaps, in multiples of the line width.
71// joint_extent = Extents length of joint shape, in multiples of the line width.
72// endcap_extent = Extents length of endcaps, in multiples of the line width.
73// endcap_extent1 = Extents length of starting endcap, in multiples of the line width.
74// endcap_extent2 = Extents length of ending endcap, in multiples of the line width.
75// dots_extent = Extents length of both joints and endcaps, in multiples of the line width.
76// joint_angle = Extra rotation given to joint shapes, in degrees. If not given, the shapes are fully spun (for 3D lines).
77// endcap_angle = Extra rotation given to endcaps, in degrees. If not given, the endcaps are fully spun (for 3D lines).
78// endcap_angle1 = Extra rotation given to a starting endcap, in degrees. If not given, the endcap is fully spun (for 3D lines).
79// endcap_angle2 = Extra rotation given to a ending endcap, in degrees. If not given, the endcap is fully spun (for 3D lines).
80// dots_angle = Extra rotation given to both joints and endcaps, in degrees. If not given, the endcap is fully spun (for 3D lines).
81// trim = Trim the the start and end line segments by this much, to keep them from interfering with custom endcaps.
82// trim1 = Trim the the starting line segment by this much, to keep it from interfering with a custom endcap.
83// trim2 = Trim the the ending line segment by this much, to keep it from interfering with a custom endcap.
84// color = If given, sets the color of the line segments, joints and endcap.
85// endcap_color = If given, sets the color of both endcaps. Overrides `color=` and `dots_color=`.
86// endcap_color1 = If give, sets the color of the starting endcap. Overrides `color=`, `dots_color=`, and `endcap_color=`.
87// endcap_color2 = If given, sets the color of the ending endcap. Overrides `color=`, `dots_color=`, and `endcap_color=`.
88// joint_color = If given, sets the color of the joints. Overrides `color=` and `dots_color=`.
89// dots_color = If given, sets the color of the endcaps and joints. Overrides `color=`.
90// singleton_scale = Change the scale of the endcap shape drawn for singleton paths. Default: 2.
91// convexity = Max number of times a line could intersect a wall of an endcap.
92// Example(2D): Drawing a Path
93// path = [[0,100], [100,100], [200,0], [100,-100], [100,0]];
94// stroke(path, width=20);
95// Example(2D): Closing a Path
96// path = [[0,100], [100,100], [200,0], [100,-100], [100,0]];
97// stroke(path, width=20, endcaps=true, closed=true);
98// Example(2D): Fancy Arrow Endcaps
99// path = [[0,100], [100,100], [200,0], [100,-100], [100,0]];
100// stroke(path, width=10, endcaps="arrow2");
101// Example(2D): Modified Fancy Arrow Endcaps
102// path = [[0,100], [100,100], [200,0], [100,-100], [100,0]];
103// stroke(path, width=10, endcaps="arrow2", endcap_width=6, endcap_length=3, endcap_extent=2);
104// Example(2D): Mixed Endcaps
105// path = [[0,100], [100,100], [200,0], [100,-100], [100,0]];
106// stroke(path, width=10, endcap1="tail2", endcap2="arrow2");
107// Example(2D): Plotting Points. Setting endcap_angle to zero results in the weird arrow orientation.
108// path = [for (a=[0:30:360]) [a-180, 60*sin(a)]];
109// stroke(path, width=3, joints="diamond", endcaps="arrow2", endcap_angle=0, endcap_width=5, joint_angle=0, joint_width=5);
110// Example(2D): Joints and Endcaps
111// path = [for (a=[0:30:360]) [a-180, 60*sin(a)]];
112// stroke(path, width=8, joints="dot", endcaps="arrow2");
113// Example(2D): Custom Endcap Shapes
114// path = [[0,100], [100,100], [200,0], [100,-100], [100,0]];
115// arrow = [[0,0], [2,-3], [0.5,-2.3], [2,-4], [0.5,-3.5], [-0.5,-3.5], [-2,-4], [-0.5,-2.3], [-2,-3]];
116// stroke(path, width=10, trim=3.5, endcaps=arrow);
117// Example(2D): Variable Line Width
118// path = circle(d=50,$fn=18);
119// widths = [for (i=idx(path)) 10*i/len(path)+2];
120// stroke(path,width=widths,$fa=1,$fs=1);
121// Example: 3D Path with Endcaps
122// path = rot([15,30,0], p=path3d(pentagon(d=50)));
123// stroke(path, width=2, endcaps="arrow2", $fn=18);
124// Example: 3D Path with Flat Endcaps
125// path = rot([15,30,0], p=path3d(pentagon(d=50)));
126// stroke(path, width=2, endcaps="arrow2", endcap_angle=0, $fn=18);
127// Example: 3D Path with Mixed Endcaps
128// path = rot([15,30,0], p=path3d(pentagon(d=50)));
129// stroke(path, width=2, endcap1="arrow2", endcap2="tail", endcap_angle2=0, $fn=18);
130// Example: 3D Path with Joints and Endcaps
131// path = [for (i=[0:10:360]) [(i-180)/2,20*cos(3*i),20*sin(3*i)]];
132// stroke(path, width=2, joints="dot", endcap1="round", endcap2="arrow2", joint_width=2.0, endcap_width2=3, $fn=18);
133// Example: Coloring Lines, Joints, and Endcaps
134// path = [for (i=[0:15:360]) [(i-180)/3,20*cos(2*i),20*sin(2*i)]];
135// stroke(
136// path, width=2, joints="dot", endcap1="dot", endcap2="arrow2",
137// color="lightgreen", joint_color="red", endcap_color="blue",
138// joint_width=2.0, endcap_width2=3, $fn=18
139// );
140// Example(2D): Simplified Plotting
141// path = [for (i=[0:15:360]) [(i-180)/3,20*cos(2*i)]];
142// stroke(path, width=2, dots=true, color="lightgreen", dots_color="red", $fn=18);
143// Example(2D): Drawing a Region
144// rgn = [square(100,center=true), circle(d=60,$fn=18)];
145// stroke(rgn, width=2);
146// Example(2D): Drawing a List of Lines
147// paths = [
148// for (y=[-60:60:60]) [
149// for (a=[-180:15:180])
150// [a, 2*y+60*sin(a+y)]
151// ]
152// ];
153// stroke(paths, closed=false, width=5);
154// Example(2D): Paths with a singleton. Note that the singleton is not a single point, but a list containing a single point.
155// stroke([
156// [[0,0],[1,1]],
157// [[1.5,1.5]],
158// [[2,2],[3,3]]
159// ],width=0.2,closed=false,$fn=16);
160function stroke(
161 path, width=1, closed,
162 endcaps, endcap1, endcap2, joints, dots,
163 endcap_width, endcap_width1, endcap_width2, joint_width, dots_width,
164 endcap_length, endcap_length1, endcap_length2, joint_length, dots_length,
165 endcap_extent, endcap_extent1, endcap_extent2, joint_extent, dots_extent,
166 endcap_angle, endcap_angle1, endcap_angle2, joint_angle, dots_angle,
167 endcap_color, endcap_color1, endcap_color2, joint_color, dots_color, color,
168 trim, trim1, trim2, singleton_scale=2,
169 convexity=10
170) = no_function("stroke");
171
172
173module stroke(
174 path, width=1, closed,
175 endcaps, endcap1, endcap2, joints, dots,
176 endcap_width, endcap_width1, endcap_width2, joint_width, dots_width,
177 endcap_length, endcap_length1, endcap_length2, joint_length, dots_length,
178 endcap_extent, endcap_extent1, endcap_extent2, joint_extent, dots_extent,
179 endcap_angle, endcap_angle1, endcap_angle2, joint_angle, dots_angle,
180 endcap_color, endcap_color1, endcap_color2, joint_color, dots_color, color,
181 trim, trim1, trim2, singleton_scale=2,
182 convexity=10
183) {
184 no_children($children);
185 module setcolor(clr) {
186 if (clr==undef) {
187 children();
188 } else {
189 color(clr) children();
190 }
191 }
192 function _shape_defaults(cap) =
193 cap==undef? [1.00, 0.00, 0.00] :
194 cap==false? [1.00, 0.00, 0.00] :
195 cap==true? [1.00, 1.00, 0.00] :
196 cap=="butt"? [1.00, 0.00, 0.00] :
197 cap=="round"? [1.00, 1.00, 0.00] :
198 cap=="chisel"? [1.00, 1.00, 0.00] :
199 cap=="square"? [1.00, 1.00, 0.00] :
200 cap=="block"? [2.00, 1.00, 0.00] :
201 cap=="diamond"? [2.50, 1.00, 0.00] :
202 cap=="dot"? [2.00, 1.00, 0.00] :
203 cap=="x"? [2.50, 0.40, 0.00] :
204 cap=="cross"? [3.00, 0.33, 0.00] :
205 cap=="line"? [3.50, 0.22, 0.00] :
206 cap=="arrow"? [3.50, 0.40, 0.50] :
207 cap=="arrow2"? [3.50, 1.00, 0.14] :
208 cap=="tail"? [3.50, 0.47, 0.50] :
209 cap=="tail2"? [3.50, 0.28, 0.50] :
210 is_path(cap)? [0.00, 0.00, 0.00] :
211 assert(false, str("Invalid cap or joint: ",cap));
212
213 function _shape_path(cap,linewidth,w,l,l2) = (
214 cap=="butt" || cap==false || cap==undef ? [] :
215 cap=="round" || cap==true ? scale([w,l], p=circle(d=1, $fn=max(8, segs(w/2)))) :
216 cap=="chisel"? scale([w,l], p=circle(d=1,$fn=4)) :
217 cap=="diamond"? circle(d=w,$fn=4) :
218 cap=="square"? scale([w,l], p=square(1,center=true)) :
219 cap=="block"? scale([w,l], p=square(1,center=true)) :
220 cap=="dot"? circle(d=w, $fn=max(12, segs(w*3/2))) :
221 cap=="x"? [for (a=[0:90:270]) each rot(a,p=[[w+l/2,w-l/2]/2, [w-l/2,w+l/2]/2, [0,l/2]]) ] :
222 cap=="cross"? [for (a=[0:90:270]) each rot(a,p=[[l,w]/2, [-l,w]/2, [-l,l]/2]) ] :
223 cap=="line"? scale([w,l], p=square(1,center=true)) :
224 cap=="arrow"? [[0,0], [w/2,-l2], [w/2,-l2-l], [0,-l], [-w/2,-l2-l], [-w/2,-l2]] :
225 cap=="arrow2"? [[0,0], [w/2,-l2-l], [0,-l], [-w/2,-l2-l]] :
226 cap=="tail"? [[0,0], [w/2,l2], [w/2,l2-l], [0,-l], [-w/2,l2-l], [-w/2,l2]] :
227 cap=="tail2"? [[w/2,0], [w/2,-l], [0,-l-l2], [-w/2,-l], [-w/2,0]] :
228 is_path(cap)? cap :
229 assert(false, str("Invalid endcap: ",cap))
230 ) * linewidth;
231
232 closed = default(closed, is_region(path));
233 check1 = assert(is_bool(closed));
234
235 dots = dots==true? "dot" : dots;
236
237 endcap1 = first_defined([endcap1, endcaps, dots, "round"]);
238 endcap2 = first_defined([endcap2, endcaps, if (!closed) dots, "round"]);
239 joints = first_defined([joints, dots, "round"]);
240 check2 =
241 assert(is_bool(endcap1) || is_string(endcap1) || is_path(endcap1))
242 assert(is_bool(endcap2) || is_string(endcap2) || is_path(endcap2))
243 assert(is_bool(joints) || is_string(joints) || is_path(joints));
244
245 endcap1_dflts = _shape_defaults(endcap1);
246 endcap2_dflts = _shape_defaults(endcap2);
247 joint_dflts = _shape_defaults(joints);
248
249 endcap_width1 = first_defined([endcap_width1, endcap_width, dots_width, endcap1_dflts[0]]);
250 endcap_width2 = first_defined([endcap_width2, endcap_width, dots_width, endcap2_dflts[0]]);
251 joint_width = first_defined([joint_width, dots_width, joint_dflts[0]]);
252
253 endcap_length1 = first_defined([endcap_length1, endcap_length, dots_length, endcap1_dflts[1]*endcap_width1]);
254 endcap_length2 = first_defined([endcap_length2, endcap_length, dots_length, endcap2_dflts[1]*endcap_width2]);
255 joint_length = first_defined([joint_length, dots_length, joint_dflts[1]*joint_width]);
256
257 endcap_extent1 = first_defined([endcap_extent1, endcap_extent, dots_extent, endcap1_dflts[2]*endcap_width1]);
258 endcap_extent2 = first_defined([endcap_extent2, endcap_extent, dots_extent, endcap2_dflts[2]*endcap_width2]);
259 joint_extent = first_defined([joint_extent, dots_extent, joint_dflts[2]*joint_width]);
260
261 endcap_angle1 = first_defined([endcap_angle1, endcap_angle, dots_angle]);
262 endcap_angle2 = first_defined([endcap_angle2, endcap_angle, dots_angle]);
263 joint_angle = first_defined([joint_angle, dots_angle]);
264
265 check3 =
266 assert(all_nonnegative([endcap_length1]))
267 assert(all_nonnegative([endcap_length2]))
268 assert(all_nonnegative([joint_length]));
269 assert(all_nonnegative([endcap_extent1]))
270 assert(all_nonnegative([endcap_extent2]))
271 assert(all_nonnegative([joint_extent]));
272 assert(is_undef(endcap_angle1)||is_finite(endcap_angle1))
273 assert(is_undef(endcap_angle2)||is_finite(endcap_angle2))
274 assert(is_undef(joint_angle)||is_finite(joint_angle))
275 assert(all_positive([singleton_scale]))
276 assert(all_positive(width));
277
278 endcap_color1 = first_defined([endcap_color1, endcap_color, dots_color, color]);
279 endcap_color2 = first_defined([endcap_color2, endcap_color, dots_color, color]);
280 joint_color = first_defined([joint_color, dots_color, color]);
281
282 // We want to allow "paths" with length 1, so we can't use the normal path/region checks
283 paths = is_matrix(path) ? [path] : path;
284 assert(is_list(paths),"The path argument must be a list of 2D or 3D points, or a region.");
285 for (path = paths) {
286 pathvalid = is_path(path,[2,3]) || same_shape(path,[[0,0]]) || same_shape(path,[[0,0,0]]);
287 assert(pathvalid,"The path argument must be a list of 2D or 3D points, or a region.");
288 path = deduplicate( closed? close_path(path) : path );
289
290 check4 = assert(is_num(width) || len(width)==len(path),
291 "width must be a number or a vector the same length as the path (or all components of a region)");
292 width = is_num(width)? [for (x=path) width] : width;
293
294 endcap_shape1 = _shape_path(endcap1, width[0], endcap_width1, endcap_length1, endcap_extent1);
295 endcap_shape2 = _shape_path(endcap2, last(width), endcap_width2, endcap_length2, endcap_extent2);
296
297 trim1 = width[0] * first_defined([
298 trim1, trim,
299 (endcap1=="arrow")? endcap_length1-0.01 :
300 (endcap1=="arrow2")? endcap_length1*3/4 :
301 0
302 ]);
303
304 trim2 = last(width) * first_defined([
305 trim2, trim,
306 (endcap2=="arrow")? endcap_length2-0.01 :
307 (endcap2=="arrow2")? endcap_length2*3/4 :
308 0
309 ]);
310 check10 = assert(is_finite(trim1))
311 assert(is_finite(trim2));
312
313 if (len(path) == 1) {
314 if (len(path[0]) == 2) {
315 // Endcap1
316 setcolor(endcap_color1) {
317 translate(path[0]) {
318 mat = is_undef(endcap_angle1)? ident(3) : zrot(endcap_angle1);
319 multmatrix(mat) polygon(scale(singleton_scale,endcap_shape1));
320 }
321 }
322 } else {
323 // Endcap1
324 setcolor(endcap_color1) {
325 translate(path[0]) {
326 $fn = segs(width[0]/2);
327 if (is_undef(endcap_angle1)) {
328 rotate_extrude(convexity=convexity) {
329 right_half(planar=true) {
330 polygon(endcap_shape1);
331 }
332 }
333 } else {
334 rotate([90,0,endcap_angle1]) {
335 linear_extrude(height=max(widths[0],0.001), center=true, convexity=convexity) {
336 polygon(endcap_shape1);
337 }
338 }
339 }
340 }
341 }
342 }
343 } else {
344 dummy=assert(trim1<path_length(path)-trim2, "Path is too short for endcap(s). Try a smaller width, or set endcap_length to a smaller value.");
345 pathcut = path_cut_points(path, [trim1, path_length(path)-trim2], closed=false);
346 pathcut_su = _cut_to_seg_u_form(pathcut,path);
347 path2 = _path_cut_getpaths(path, pathcut, closed=false)[1];
348 widths = _path_select(width, pathcut_su[0][0], pathcut_su[0][1], pathcut_su[1][0], pathcut_su[1][1]);
349 start_vec = path[0] - path[1];
350 end_vec = last(path) - select(path,-2);
351
352 if (len(path[0]) == 2) {
353 // Straight segments
354 setcolor(color) {
355 for (i = idx(path2,e=-2)) {
356 seg = select(path2,i,i+1);
357 delt = seg[1] - seg[0];
358 translate(seg[0]) {
359 rot(from=BACK,to=delt) {
360 trapezoid(w1=widths[i], w2=widths[i+1], h=norm(delt), anchor=FRONT);
361 }
362 }
363 }
364 }
365
366 // Joints
367 setcolor(joint_color) {
368 for (i = [1:1:len(path2)-2]) {
369 $fn = quantup(segs(widths[i]/2),4);
370 translate(path2[i]) {
371 if (joints != undef && joints != "round") {
372 joint_shape = _shape_path(
373 joints, width[i],
374 joint_width,
375 joint_length,
376 joint_extent
377 );
378 v1 = unit(path2[i] - path2[i-1]);
379 v2 = unit(path2[i+1] - path2[i]);
380 mat = is_undef(joint_angle)
381 ? rot(from=BACK,to=v1)
382 : zrot(joint_angle);
383 multmatrix(mat) polygon(joint_shape);
384 } else {
385 // These are parallel to the path
386 v1 = path2[i] - path2[i-1];
387 v2 = path2[i+1] - path2[i];
388 ang = modang(v_theta(v2) - v_theta(v1));
389 // Need 90 deg offset to make wedge perpendicular to path, and the wedge
390 // position depends on whether we turn left (ang<0) or right (ang>0)
391 theta = v_theta(v1) - sign(ang)*90;
392 ang_eps = 0.1;
393 if (!approx(ang,0))
394 arc(d=widths[i], angle=[theta-ang_eps, theta+ang+ang_eps], wedge=true);
395 }
396 }
397 }
398 }
399
400 // Endcap1
401 setcolor(endcap_color1) {
402 translate(path[0]) {
403 mat = is_undef(endcap_angle1)? rot(from=BACK,to=start_vec) :
404 zrot(endcap_angle1);
405 multmatrix(mat) polygon(endcap_shape1);
406 }
407 }
408
409 // Endcap2
410 setcolor(endcap_color2) {
411 translate(last(path)) {
412 mat = is_undef(endcap_angle2)? rot(from=BACK,to=end_vec) :
413 zrot(endcap_angle2);
414 multmatrix(mat) polygon(endcap_shape2);
415 }
416 }
417 } else {
418 rotmats = cumprod([
419 for (i = idx(path2,e=-2)) let(
420 vec1 = i==0? UP : unit(path2[i]-path2[i-1], UP),
421 vec2 = unit(path2[i+1]-path2[i], UP)
422 ) rot(from=vec1,to=vec2)
423 ]);
424
425 sides = [
426 for (i = idx(path2,e=-2))
427 quantup(segs(max(widths[i],widths[i+1])/2),4)
428 ];
429
430 // Straight segments
431 setcolor(color) {
432 for (i = idx(path2,e=-2)) {
433 dist = norm(path2[i+1] - path2[i]);
434 w1 = widths[i]/2;
435 w2 = widths[i+1]/2;
436 $fn = sides[i];
437 translate(path2[i]) {
438 multmatrix(rotmats[i]) {
439 cylinder(r1=w1, r2=w2, h=dist, center=false);
440 }
441 }
442 }
443 }
444
445 // Joints
446 setcolor(joint_color) {
447 for (i = [1:1:len(path2)-2]) {
448 $fn = sides[i];
449 translate(path2[i]) {
450 if (joints != undef && joints != "round") {
451 joint_shape = _shape_path(
452 joints, width[i],
453 joint_width,
454 joint_length,
455 joint_extent
456 );
457 multmatrix(rotmats[i] * xrot(180)) {
458 $fn = sides[i];
459 if (is_undef(joint_angle)) {
460 rotate_extrude(convexity=convexity) {
461 right_half(planar=true) {
462 polygon(joint_shape);
463 }
464 }
465 } else {
466 rotate([90,0,joint_angle]) {
467 linear_extrude(height=max(widths[i],0.001), center=true, convexity=convexity) {
468 polygon(joint_shape);
469 }
470 }
471 }
472 }
473 } else {
474 corner = select(path2,i-1,i+1);
475 axis = vector_axis(corner);
476 ang = vector_angle(corner);
477 if (!approx(ang,0)) {
478 frame_map(x=path2[i-1]-path2[i], z=-axis) {
479 zrot(90-0.5) {
480 rotate_extrude(angle=180-ang+1) {
481 arc(d=widths[i], start=-90, angle=180);
482 }
483 }
484 }
485 }
486 }
487 }
488 }
489 }
490
491 // Endcap1
492 setcolor(endcap_color1) {
493 translate(path[0]) {
494 multmatrix(rotmats[0] * xrot(180)) {
495 $fn = sides[0];
496 if (is_undef(endcap_angle1)) {
497 rotate_extrude(convexity=convexity) {
498 right_half(planar=true) {
499 polygon(endcap_shape1);
500 }
501 }
502 } else {
503 rotate([90,0,endcap_angle1]) {
504 linear_extrude(height=max(widths[0],0.001), center=true, convexity=convexity) {
505 polygon(endcap_shape1);
506 }
507 }
508 }
509 }
510 }
511 }
512
513 // Endcap2
514 setcolor(endcap_color2) {
515 translate(last(path)) {
516 multmatrix(last(rotmats)) {
517 $fn = last(sides);
518 if (is_undef(endcap_angle2)) {
519 rotate_extrude(convexity=convexity) {
520 right_half(planar=true) {
521 polygon(endcap_shape2);
522 }
523 }
524 } else {
525 rotate([90,0,endcap_angle2]) {
526 linear_extrude(height=max(last(widths),0.001), center=true, convexity=convexity) {
527 polygon(endcap_shape2);
528 }
529 }
530 }
531 }
532 }
533 }
534 }
535 }
536 }
537}
538
539
540// Function&Module: dashed_stroke()
541// Usage: As a Module
542// dashed_stroke(path, dashpat, [width=], [closed=]);
543// Usage: As a Function
544// dashes = dashed_stroke(path, dashpat, [closed=]);
545// Topics: Paths, Drawing Tools
546// See Also: stroke(), path_cut()
547// Description:
548// Given a path (or region) and a dash pattern, creates a dashed line that follows that
549// path or region boundary with the given dash pattern.
550// - When called as a function, returns a list of dash sub-paths.
551// - When called as a module, draws all those subpaths using `stroke()`.
552// When called as a module the dash pattern is multiplied by the line width. When called as
553// a function the dash pattern applies as you specify it.
554// Arguments:
555// path = The path or region to subdivide into dashes.
556// dashpat = A list of alternating dash lengths and space lengths for the dash pattern. This will be scaled by the width of the line.
557// ---
558// width = The width of the dashed line to draw. Module only. Default: 1
559// closed = If true, treat path as a closed polygon. Default: false
560// fit = If true, shrink or stretch the dash pattern so that the path ends ofter a logical dash. Default: true
561// roundcaps = (Module only) If true, draws dashes with rounded caps. This often looks better. Default: true
562// mindash = (Function only) Specifies the minimal dash length to return at the end of a path when fit is false. Default: 0.5
563// Example(2D): Open Path
564// path = [for (a=[-180:10:180]) [a/3,20*sin(a)]];
565// dashed_stroke(path, [3,2], width=1);
566// Example(2D): Closed Polygon
567// path = circle(d=100,$fn=72);
568// dashpat = [10,2, 3,2, 3,2];
569// dashed_stroke(path, dashpat, width=1, closed=true);
570// Example(FlatSpin,VPD=250): 3D Dashed Path
571// path = [for (a=[-180:5:180]) [a/3, 20*cos(3*a), 20*sin(3*a)]];
572// dashed_stroke(path, [3,2], width=1);
573function dashed_stroke(path, dashpat=[3,3], closed=false, fit=true, mindash=0.5) =
574 is_region(path) ? [
575 for (p = path)
576 each dashed_stroke(p, dashpat, closed=true, fit=fit)
577 ] :
578 let(
579 path = closed? close_path(path) : path,
580 dashpat = len(dashpat)%2==0? dashpat : concat(dashpat,[0]),
581 plen = path_length(path),
582 dlen = sum(dashpat),
583 doff = cumsum(dashpat),
584 freps = plen / dlen,
585 reps = max(1, fit? round(freps) : floor(freps)),
586 tlen = !fit? plen :
587 reps * dlen + (closed? 0 : dashpat[0]),
588 sc = plen / tlen,
589 cuts = [
590 for (i = [0:1:reps], off = doff*sc)
591 let (x = i*dlen*sc + off)
592 if (x > 0 && x < plen) x
593 ],
594 dashes = path_cut(path, cuts, closed=false),
595 dcnt = len(dashes),
596 evens = [
597 for (i = idx(dashes))
598 if (i % 2 == 0)
599 let( dash = dashes[i] )
600 if (i < dcnt-1 || path_length(dash) > mindash)
601 dashes[i]
602 ]
603 ) evens;
604
605
606module dashed_stroke(path, dashpat=[3,3], width=1, closed=false, fit=true, roundcaps=false) {
607 no_children($children);
608 segs = dashed_stroke(path, dashpat=dashpat*width, closed=closed, fit=fit, mindash=0.5*width);
609 for (seg = segs)
610 stroke(seg, width=width, endcaps=roundcaps? "round" : false);
611}
612
613
614
615// Section: Computing paths
616
617// Function&Module: arc()
618// Usage: 2D arc from 0º to `angle` degrees.
619// path=arc(n, r|d=, angle);
620// Usage: 2D arc from START to END degrees.
621// path=arc(n, r|d=, angle=[START,END]);
622// Usage: 2D arc from `start` to `start+angle` degrees.
623// path=arc(n, r|d=, start=, angle=);
624// Usage: 2D circle segment by `width` and `thickness`, starting and ending on the X axis.
625// path=arc(n, width=, thickness=);
626// Usage: Shortest 2D or 3D arc around centerpoint `cp`, starting at P0 and ending on the vector pointing from `cp` to `P1`.
627// path=arc(n, cp=, points=[P0,P1], [long=], [cw=], [ccw=]);
628// Usage: 2D or 3D arc, starting at `P0`, passing through `P1` and ending at `P2`.
629// path=arc(n, points=[P0,P1,P2]);
630// Usage: 2D or 3D arc, fron tangent point on segment `[P0,P1]` to the tangent point on segment `[P1,P2]`.
631// path=arc(n, corner=[P0,P1,P2], r=);
632// Usage: as module
633// arc(...) [ATTACHMENTS];
634// Topics: Paths (2D), Paths (3D), Shapes (2D), Path Generators
635// Description:
636// If called as a function, returns a 2D or 3D path forming an arc.
637// If called as a module, creates a 2D arc polygon or pie slice shape.
638// Arguments:
639// n = Number of vertices to form the arc curve from.
640// r = Radius of the arc.
641// angle = If a scalar, specifies the end angle in degrees (relative to start parameter). If a vector of two scalars, specifies start and end angles.
642// ---
643// d = Diameter of the arc.
644// cp = Centerpoint of arc.
645// points = Points on the arc.
646// corner = A path of two segments to fit an arc tangent to.
647// long = if given with cp and points takes the long arc instead of the default short arc. Default: false
648// cw = if given with cp and 2 points takes the arc in the clockwise direction. Default: false
649// ccw = if given with cp and 2 points takes the arc in the counter-clockwise direction. Default: false
650// width = If given with `thickness`, arc starts and ends on X axis, to make a circle segment.
651// thickness = If given with `width`, arc starts and ends on X axis, to make a circle segment.
652// start = Start angle of arc.
653// wedge = If true, include centerpoint `cp` in output to form pie slice shape.
654// endpoint = If false exclude the last point (function only). Default: true
655// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). (Module only) Default: `CENTER`
656// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#subsection-spin). (Module only) Default: `0`
657// Examples(2D):
658// arc(n=4, r=30, angle=30, wedge=true);
659// arc(r=30, angle=30, wedge=true);
660// arc(d=60, angle=30, wedge=true);
661// arc(d=60, angle=120);
662// arc(d=60, angle=120, wedge=true);
663// arc(r=30, angle=[75,135], wedge=true);
664// arc(r=30, start=45, angle=75, wedge=true);
665// arc(width=60, thickness=20);
666// arc(cp=[-10,5], points=[[20,10],[0,35]], wedge=true);
667// arc(points=[[30,-5],[20,10],[-10,20]], wedge=true);
668// Example(2D): Fit to three points.
669// arc(points=[[5,30],[-10,-10],[30,5]], wedge=true);
670// Example(2D):
671// path = arc(points=[[5,30],[-10,-10],[30,5]], wedge=true);
672// stroke(closed=true, path);
673// Example(FlatSpin,VPD=175):
674// path = arc(points=[[0,30,0],[0,0,30],[30,0,0]]);
675// stroke(path, dots=true, dots_color="blue");
676// Example(2D): Fit to a corner.
677// pts = [[0,40], [-40,-10], [30,0]];
678// path = arc(corner=pts, r=20);
679// stroke(pts, endcaps="arrow2");
680// stroke(path, endcap2="arrow2", color="blue");
681function arc(n, r, angle, d, cp, points, corner, width, thickness, start, wedge=false, long=false, cw=false, ccw=false, endpoint=true) =
682 assert(is_bool(endpoint))
683 !endpoint ?
684 assert(!wedge, "endpoint cannot be false if wedge is true")
685 list_head(arc(u_add(n,1),r,angle,d,cp,points,corner,width,thickness,start,wedge,long,cw,ccw,true)) :
686 assert(is_undef(n) || (is_integer(n) && n>=2), "Number of points must be an integer 2 or larger")
687 // First try for 2D arc specified by width and thickness
688 is_def(width) && is_def(thickness)? (
689 assert(!any_defined([r,cp,points]) && !any([cw,ccw,long]),"Conflicting or invalid parameters to arc")
690 assert(width>0, "Width must be postive")
691 assert(thickness>0, "Thickness must be positive")
692 arc(n,points=[[width/2,0], [0,thickness], [-width/2,0]],wedge=wedge)
693 ) :
694 is_def(angle)? (
695 let(
696 parmok = !any_defined([points,width,thickness]) &&
697 ((is_vector(angle,2) && is_undef(start)) || is_finite(angle))
698 )
699 assert(parmok,"Invalid parameters in arc")
700 let(
701 cp = first_defined([cp,[0,0]]),
702 start = is_def(start)? start : is_vector(angle) ? angle[0] : 0,
703 angle = is_vector(angle)? angle[1]-angle[0] : angle,
704 r = get_radius(r=r, d=d)
705 )
706 assert(is_vector(cp,2),"Centerpoint must be a 2d vector")
707 assert(angle!=0, "Arc has zero length")
708 assert(is_def(r) && r>0, "Arc radius invalid")
709 let(
710 n = is_def(n) ? n : max(3, ceil(segs(r)*abs(angle)/360)),
711 arcpoints = [for(i=[0:n-1]) let(theta = start + i*angle/(n-1)) r*[cos(theta),sin(theta)]+cp],
712 extra = wedge? [cp] : []
713 )
714 concat(extra,arcpoints)
715 ) : is_def(corner)? (
716 assert(is_path(corner,[2,3]),"Point list is invalid")
717 // Arc is 3D, so transform corner to 2D and make a recursive call, then remap back to 3D
718 len(corner[0]) == 3? (
719 assert(!(cw || ccw), "(Counter)clockwise isn't meaningful in 3d, so `cw` and `ccw` must be false")
720 assert(is_undef(cp) || is_vector(cp,3),"corner are 3d so cp must be 3d")
721 let(
722 plane = [is_def(cp) ? cp : corner[2], corner[0], corner[1]],
723 center2d = is_def(cp) ? project_plane(plane,cp) : undef,
724 points2d = project_plane(plane, corner)
725 )
726 lift_plane(plane,arc(n,cp=center2d,corner=points2d,wedge=wedge,long=long))
727 ) :
728 assert(is_path(corner) && len(corner) == 3)
729 let(col = is_collinear(corner[0],corner[1],corner[2]))
730 assert(!col, "Collinear inputs do not define an arc")
731 let( r = get_radius(r=r, d=d) )
732 assert(is_finite(r) && r>0, "Must specify r= or d= when corner= is given.")
733 let(
734 ci = circle_2tangents(r, corner[0], corner[1], corner[2], tangents=true),
735 cp = ci[0], nrm = ci[1], tp1 = ci[2], tp2 = ci[3],
736 dir = det2([corner[1]-corner[0],corner[2]-corner[1]]) > 0,
737 corner = dir? [tp1,tp2] : [tp2,tp1],
738 theta_start = atan2(corner[0].y-cp.y, corner[0].x-cp.x),
739 theta_end = atan2(corner[1].y-cp.y, corner[1].x-cp.x),
740 angle = posmod(theta_end-theta_start, 360),
741 arcpts = arc(n,cp=cp,r=r,start=theta_start,angle=angle,wedge=wedge)
742 )
743 dir ? arcpts : reverse(arcpts)
744 ) :
745 assert(is_path(points,[2,3]),"Point list is invalid")
746 // Arc is 3D, so transform points to 2D and make a recursive call, then remap back to 3D
747 len(points[0]) == 3? (
748 assert(!(cw || ccw), "(Counter)clockwise isn't meaningful in 3d, so `cw` and `ccw` must be false")
749 assert(is_undef(cp) || is_vector(cp,3),"points are 3d so cp must be 3d")
750 let(
751 plane = [is_def(cp) ? cp : points[2], points[0], points[1]],
752 center2d = is_def(cp) ? project_plane(plane,cp) : undef,
753 points2d = project_plane(plane, points)
754 )
755 lift_plane(plane,arc(n,cp=center2d,points=points2d,wedge=wedge,long=long))
756 ) :
757 is_def(cp)? (
758 // Arc defined by center plus two points, will have radius defined by center and points[0]
759 // and extent defined by direction of point[1] from the center
760 assert(is_vector(cp,2), "Centerpoint must be a 2d vector")
761 assert(len(points)==2, "When pointlist has length 3 centerpoint is not allowed")
762 assert(points[0]!=points[1], "Arc endpoints are equal")
763 assert(cp!=points[0]&&cp!=points[1], "Centerpoint equals an arc endpoint")
764 assert(num_true([long,cw,ccw])<=1, str("Only one of `long`, `cw` and `ccw` can be true",cw,ccw,long))
765 let(
766 angle = vector_angle(points[0], cp, points[1]),
767 v1 = points[0]-cp,
768 v2 = points[1]-cp,
769 prelim_dir = sign(det2([v1,v2])), // z component of cross product
770 dir = prelim_dir != 0 ? prelim_dir :
771 assert(cw || ccw, "Collinear inputs don't define a unique arc")
772 1,
773 r = norm(v1),
774 final_angle = long || (ccw && dir<0) || (cw && dir>0) ?
775 -dir*(360-angle) :
776 dir*angle,
777 sa = atan2(v1.y,v1.x)
778 )
779 arc(n,cp=cp,r=r,start=sa,angle=final_angle,wedge=wedge)
780 ) : (
781 // Final case is arc passing through three points, starting at point[0] and ending at point[3]
782 let(col = is_collinear(points[0],points[1],points[2]))
783 assert(!col, "Collinear inputs do not define an arc")
784 let(
785 cp = line_intersection(_normal_segment(points[0],points[1]),_normal_segment(points[1],points[2])),
786 // select order to be counterclockwise
787 dir = det2([points[1]-points[0],points[2]-points[1]]) > 0,
788 points = dir? select(points,[0,2]) : select(points,[2,0]),
789 r = norm(points[0]-cp),
790 theta_start = atan2(points[0].y-cp.y, points[0].x-cp.x),
791 theta_end = atan2(points[1].y-cp.y, points[1].x-cp.x),
792 angle = posmod(theta_end-theta_start, 360),
793 arcpts = arc(n,cp=cp,r=r,start=theta_start,angle=angle,wedge=wedge)
794 )
795 dir ? arcpts : reverse(arcpts)
796 );
797
798
799module arc(n, r, angle, d, cp, points, corner, width, thickness, start, wedge=false, anchor=CENTER, spin=0)
800{
801 path = arc(n=n, r=r, angle=angle, d=d, cp=cp, points=points, corner=corner, width=width, thickness=thickness, start=start, wedge=wedge);
802 attachable(anchor,spin, two_d=true, path=path, extent=false) {
803 polygon(path);
804 children();
805 }
806}
807
808
809// Function: helix()
810// Usage:
811// path = helix(l|h, [turns=], [angle=], r=|r1=|r2=, d=|d1=|d2=);
812// Description:
813// Returns a 3D helical path on a cone, including the degerate case of flat spirals.
814// You can specify start and end radii. You can give the length, the helix angle, or the number of turns: two
815// of these three parameters define the helix. For a flat helix you must give length 0 and a turn count.
816// Helix will be right handed if turns is positive and left handed if it is negative.
817// The angle is calculateld based on the radius at the base of the helix.
818// Arguments:
819// h/l = Height/length of helix, zero for a flat spiral
820// ---
821// turns = Number of turns in helix, positive for right handed
822// angle = helix angle
823// r = Radius of helix
824// r1 = Radius of bottom of helix
825// r2 = Radius of top of helix
826// d = Diameter of helix
827// d1 = Diameter of bottom of helix
828// d2 = Diameter of top of helix
829// Example(3D):
830// stroke(helix(turns=2.5, h=100, r=50), dots=true, dots_color="blue");
831// Example(3D): Helix that turns the other way
832// stroke(helix(turns=-2.5, h=100, r=50), dots=true, dots_color="blue");
833// Example(3D): Flat helix (note points are still 3d)
834// stroke(helix(h=0,r1=50,r2=25,l=0, turns=4));
835module helix(l,h,turns,angle, r, r1, r2, d, d1, d2) {no_module();}
836function helix(l,h,turns,angle, r, r1, r2, d, d1, d2)=
837 let(
838 r1=get_radius(r=r,r1=r1,d=d,d1=d1,dflt=1),
839 r2=get_radius(r=r,r1=r2,d=d,d1=d2,dflt=1),
840 length = first_defined([l,h])
841 )
842 assert(num_defined([length,turns,angle])==2,"Must define exactly two of l/h, turns, and angle")
843 assert(is_undef(angle) || length!=0, "Cannot give length 0 with an angle")
844 let(
845 // length advances dz for each turn
846 dz = is_def(angle) && length!=0 ? 2*PI*r1*tan(angle) : length/abs(turns),
847
848 maxtheta = is_def(turns) ? 360*turns : 360*length/dz,
849 N = segs(max(r1,r2))
850 )
851 [for(theta=lerpn(0,maxtheta, max(3,ceil(abs(maxtheta)*N/360))))
852 let(R=lerp(r1,r2,theta/maxtheta))
853 [R*cos(theta), R*sin(theta), abs(theta)/360 * dz]];
854
855
856function _normal_segment(p1,p2) =
857 let(center = (p1+p2)/2)
858 [center, center + norm(p1-p2)/2 * line_normal(p1,p2)];
859
860
861// Function: turtle()
862// Usage:
863// turtle(commands, [state], [full_state=], [repeat=])
864// Topics: Shapes (2D), Path Generators (2D), Mini-Language
865// See Also: turtle3d()
866// Description:
867// Use a sequence of turtle graphics commands to generate a path. The parameter `commands` is a list of
868// turtle commands and optional parameters for each command. The turtle state has a position, movement direction,
869// movement distance, and default turn angle. If you do not give `state` as input then the turtle starts at the
870// origin, pointed along the positive x axis with a movement distance of 1. By default, `turtle` returns just
871// the computed turtle path. If you set `full_state` to true then it instead returns the full turtle state.
872// You can invoke `turtle` again with this full state to continue the turtle path where you left off.
873// .
874// The turtle state is a list with three entries: the path constructed so far, the current step as a 2-vector, the current default angle,
875// and the current arcsteps setting.
876// .
877// Commands | Arguments | What it does
878// ------------ | ------------------ | -------------------------------
879// "move" | [dist] | Move turtle scale*dist units in the turtle direction. Default dist=1.
880// "xmove" | [dist] | Move turtle scale*dist units in the x direction. Default dist=1. Does not change turtle direction.
881// "ymove" | [dist] | Move turtle scale*dist units in the y direction. Default dist=1. Does not change turtle direction.
882// "xymove" | vector | Move turtle by the specified vector. Does not change turtle direction.
883// "untilx" | xtarget | Move turtle in turtle direction until x==xtarget. Produces an error if xtarget is not reachable.
884// "untily" | ytarget | Move turtle in turtle direction until y==ytarget. Produces an error if ytarget is not reachable.
885// "jump" | point | Move the turtle to the specified point
886// "xjump" | x | Move the turtle's x position to the specified value
887// "yjump | y | Move the turtle's y position to the specified value
888// "turn" | [angle] | Turn turtle direction by specified angle, or the turtle's default turn angle. The default angle starts at 90.
889// "left" | [angle] | Same as "turn"
890// "right" | [angle] | Same as "turn", -angle
891// "angle" | angle | Set the default turn angle.
892// "setdir" | dir | Set turtle direction. The parameter `dir` can be an angle or a vector.
893// "length" | length | Change the turtle move distance to `length`
894// "scale" | factor | Multiply turtle move distance by `factor`
895// "addlength" | length | Add `length` to the turtle move distance
896// "repeat" | count, commands | Repeats a list of commands `count` times.
897// "arcleft" | radius, [angle] | Draw an arc from the current position toward the left at the specified radius and angle. The turtle turns by `angle`. A negative angle draws the arc to the right instead of the left, and leaves the turtle facing right. A negative radius draws the arc to the right but leaves the turtle facing left.
898// "arcright" | radius, [angle] | Draw an arc from the current position toward the right at the specified radius and angle
899// "arcleftto" | radius, angle | Draw an arc at the given radius turning toward the left until reaching the specified absolute angle.
900// "arcrightto" | radius, angle | Draw an arc at the given radius turning toward the right until reaching the specified absolute angle.
901// "arcsteps" | count | Specifies the number of segments to use for drawing arcs. If you set it to zero then the standard `$fn`, `$fa` and `$fs` variables define the number of segments.
902//
903// Arguments:
904// commands = List of turtle commands
905// state = Starting turtle state (from previous call) or starting point. Default: start at the origin, pointing right.
906// ---
907// full_state = If true return the full turtle state for continuing the path in subsequent turtle calls. Default: false
908// repeat = Number of times to repeat the command list. Default: 1
909//
910// Example(2D): Simple rectangle
911// path = turtle(["xmove",3, "ymove", "xmove",-3, "ymove",-1]);
912// stroke(path,width=.1);
913// Example(2D): Pentagon
914// path=turtle(["angle",360/5,"move","turn","move","turn","move","turn","move"]);
915// stroke(path,width=.1,closed=true);
916// Example(2D): Pentagon using the repeat argument
917// path=turtle(["move","turn",360/5],repeat=5);
918// stroke(path,width=.1,closed=true);
919// Example(2D): Pentagon using the repeat turtle command, setting the turn angle
920// path=turtle(["angle",360/5,"repeat",5,["move","turn"]]);
921// stroke(path,width=.1,closed=true);
922// Example(2D): Pentagram
923// path = turtle(["move","left",144], repeat=4);
924// stroke(path,width=.05,closed=true);
925// Example(2D): Sawtooth path
926// path = turtle([
927// "turn", 55,
928// "untily", 2,
929// "turn", -55-90,
930// "untily", 0,
931// "turn", 55+90,
932// "untily", 2.5,
933// "turn", -55-90,
934// "untily", 0,
935// "turn", 55+90,
936// "untily", 3,
937// "turn", -55-90,
938// "untily", 0
939// ]);
940// stroke(path, width=.1);
941// Example(2D): Simpler way to draw the sawtooth. The direction of the turtle is preserved when executing "yjump".
942// path = turtle([
943// "turn", 55,
944// "untily", 2,
945// "yjump", 0,
946// "untily", 2.5,
947// "yjump", 0,
948// "untily", 3,
949// "yjump", 0,
950// ]);
951// stroke(path, width=.1);
952// Example(2DMed): square spiral
953// path = turtle(["move","left","addlength",1],repeat=50);
954// stroke(path,width=.2);
955// Example(2DMed): pentagonal spiral
956// path = turtle(["move","left",360/5,"addlength",1],repeat=50);
957// stroke(path,width=.7);
958// Example(2DMed): yet another spiral, without using `repeat`
959// path = turtle(concat(["angle",71],flatten(repeat(["move","left","addlength",1],50))));
960// stroke(path,width=.7);
961// Example(2DMed): The previous spiral grows linearly and eventually intersects itself. This one grows geometrically and does not.
962// path = turtle(["move","left",71,"scale",1.05],repeat=50);
963// stroke(path,width=.15);
964// Example(2D): Koch Snowflake
965// function koch_unit(depth) =
966// depth==0 ? ["move"] :
967// concat(
968// koch_unit(depth-1),
969// ["right"],
970// koch_unit(depth-1),
971// ["left","left"],
972// koch_unit(depth-1),
973// ["right"],
974// koch_unit(depth-1)
975// );
976// koch=concat(["angle",60,"repeat",3],[concat(koch_unit(3),["left","left"])]);
977// polygon(turtle(koch));
978module turtle(commands, state=[[[0,0]],[1,0],90,0], full_state=false, repeat=1) {no_module();}
979function turtle(commands, state=[[[0,0]],[1,0],90,0], full_state=false, repeat=1) =
980 let( state = is_vector(state) ? [[state],[1,0],90,0] : state )
981 repeat == 1?
982 _turtle(commands,state,full_state) :
983 _turtle_repeat(commands, state, full_state, repeat);
984
985function _turtle_repeat(commands, state, full_state, repeat) =
986 repeat==1?
987 _turtle(commands,state,full_state) :
988 _turtle_repeat(commands, _turtle(commands, state, true), full_state, repeat-1);
989
990function _turtle_command_len(commands, index) =
991 let( one_or_two_arg = ["arcleft","arcright", "arcleftto", "arcrightto"] )
992 commands[index] == "repeat"? 3 : // Repeat command requires 2 args
993 // For these, the first arg is required, second arg is present if it is not a string
994 in_list(commands[index], one_or_two_arg) && len(commands)>index+2 && !is_string(commands[index+2]) ? 3 :
995 is_string(commands[index+1])? 1 : // If 2nd item is a string it's must be a new command
996 2; // Otherwise we have command and arg
997
998function _turtle(commands, state, full_state, index=0) =
999 index < len(commands) ?
1000 _turtle(commands,
1001 _turtle_command(commands[index],commands[index+1],commands[index+2],state,index),
1002 full_state,
1003 index+_turtle_command_len(commands,index)
1004 ) :
1005 ( full_state ? state : state[0] );
1006
1007// Turtle state: state = [path, step_vector, default angle, default arcsteps]
1008
1009function _turtle_command(command, parm, parm2, state, index) =
1010 command == "repeat"?
1011 assert(is_num(parm),str("\"repeat\" command requires a numeric repeat count at index ",index))
1012 assert(is_list(parm2),str("\"repeat\" command requires a command list parameter at index ",index))
1013 _turtle_repeat(parm2, state, true, parm) :
1014 let(
1015 path = 0,
1016 step=1,
1017 angle=2,
1018 arcsteps=3,
1019 parm = !is_string(parm) ? parm : undef,
1020 parm2 = !is_string(parm2) ? parm2 : undef,
1021 needvec = ["jump", "xymove"],
1022 neednum = ["untilx","untily","xjump","yjump","angle","length","scale","addlength"],
1023 needeither = ["setdir"],
1024 chvec = !in_list(command,needvec) || is_vector(parm,2),
1025 chnum = !in_list(command,neednum) || is_num(parm),
1026 vec_or_num = !in_list(command,needeither) || (is_num(parm) || is_vector(parm,2)),
1027 lastpt = last(state[path])
1028 )
1029 assert(chvec,str("\"",command,"\" requires a vector parameter at index ",index))
1030 assert(chnum,str("\"",command,"\" requires a numeric parameter at index ",index))
1031 assert(vec_or_num,str("\"",command,"\" requires a vector or numeric parameter at index ",index))
1032
1033 command=="move" ? list_set(state, path, concat(state[path],[default(parm,1)*state[step]+lastpt])) :
1034 command=="untilx" ? (
1035 let(
1036 int = line_intersection([lastpt,lastpt+state[step]], [[parm,0],[parm,1]]),
1037 xgood = sign(state[step].x) == sign(int.x-lastpt.x)
1038 )
1039 assert(xgood,str("\"untilx\" never reaches desired goal at index ",index))
1040 list_set(state,path,concat(state[path],[int]))
1041 ) :
1042 command=="untily" ? (
1043 let(
1044 int = line_intersection([lastpt,lastpt+state[step]], [[0,parm],[1,parm]]),
1045 ygood = is_def(int) && sign(state[step].y) == sign(int.y-lastpt.y)
1046 )
1047 assert(ygood,str("\"untily\" never reaches desired goal at index ",index))
1048 list_set(state,path,concat(state[path],[int]))
1049 ) :
1050 command=="xmove" ? list_set(state, path, concat(state[path],[default(parm,1)*norm(state[step])*[1,0]+lastpt])):
1051 command=="ymove" ? list_set(state, path, concat(state[path],[default(parm,1)*norm(state[step])*[0,1]+lastpt])):
1052 command=="xymove" ? list_set(state, path, concat(state[path], [lastpt+parm])):
1053 command=="jump" ? list_set(state, path, concat(state[path],[parm])):
1054 command=="xjump" ? list_set(state, path, concat(state[path],[[parm,lastpt.y]])):
1055 command=="yjump" ? list_set(state, path, concat(state[path],[[lastpt.x,parm]])):
1056 command=="turn" || command=="left" ? list_set(state, step, rot(default(parm,state[angle]),p=state[step])) :
1057 command=="right" ? list_set(state, step, rot(-default(parm,state[angle]),p=state[step])) :
1058 command=="angle" ? list_set(state, angle, parm) :
1059 command=="setdir" ? (
1060 is_vector(parm) ?
1061 list_set(state, step, norm(state[step]) * unit(parm)) :
1062 list_set(state, step, norm(state[step]) * [cos(parm),sin(parm)])
1063 ) :
1064 command=="length" ? list_set(state, step, parm*unit(state[step])) :
1065 command=="scale" ? list_set(state, step, parm*state[step]) :
1066 command=="addlength" ? list_set(state, step, state[step]+unit(state[step])*parm) :
1067 command=="arcsteps" ? list_set(state, arcsteps, parm) :
1068 command=="arcleft" || command=="arcright" ?
1069 assert(is_num(parm),str("\"",command,"\" command requires a numeric radius value at index ",index))
1070 let(
1071 myangle = default(parm2,state[angle]),
1072 lrsign = command=="arcleft" ? 1 : -1,
1073 radius = parm*sign(myangle),
1074 center = lastpt + lrsign*radius*line_normal([0,0],state[step]),
1075 steps = state[arcsteps]==0 ? segs(abs(radius)) : state[arcsteps],
1076 arcpath = myangle == 0 || radius == 0 ? [] : arc(
1077 steps,
1078 points = [
1079 lastpt,
1080 rot(cp=center, p=lastpt, a=sign(parm)*lrsign*myangle/2),
1081 rot(cp=center, p=lastpt, a=sign(parm)*lrsign*myangle)
1082 ]
1083 )
1084 )
1085 list_set(
1086 state, [path,step], [
1087 concat(state[path], list_tail(arcpath)),
1088 rot(lrsign * myangle,p=state[step])
1089 ]
1090 ) :
1091 command=="arcleftto" || command=="arcrightto" ?
1092 assert(is_num(parm),str("\"",command,"\" command requires a numeric radius value at index ",index))
1093 assert(is_num(parm2),str("\"",command,"\" command requires a numeric angle value at index ",index))
1094 let(
1095 radius = parm,
1096 lrsign = command=="arcleftto" ? 1 : -1,
1097 center = lastpt + lrsign*radius*line_normal([0,0],state[step]),
1098 steps = state[arcsteps]==0 ? segs(abs(radius)) : state[arcsteps],
1099 start_angle = posmod(atan2(state[step].y, state[step].x),360),
1100 end_angle = posmod(parm2,360),
1101 delta_angle = -start_angle + (lrsign * end_angle < lrsign*start_angle ? end_angle+lrsign*360 : end_angle),
1102 arcpath = delta_angle == 0 || radius==0 ? [] : arc(
1103 steps,
1104 points = [
1105 lastpt,
1106 rot(cp=center, p=lastpt, a=sign(radius)*delta_angle/2),
1107 rot(cp=center, p=lastpt, a=sign(radius)*delta_angle)
1108 ]
1109 )
1110 )
1111 list_set(
1112 state, [path,step], [
1113 concat(state[path], list_tail(arcpath)),
1114 rot(delta_angle,p=state[step])
1115 ]
1116 ) :
1117 assert(false,str("Unknown turtle command \"",command,"\" at index",index))
1118 [];
1119
1120
1121// Section: Debugging polygons
1122
1123// Module: debug_polygon()
1124// Usage:
1125// debug_polygon(points, paths, [vertices=], [edges=], [convexity=], [size=]);
1126// Description:
1127// A drop-in replacement for `polygon()` that renders and labels the path points and
1128// edges. The start of each path is marked with a blue circle and the end with a pink diamond.
1129// You can suppress the display of vertex or edge labeling using the `vertices` and `edges` arguments.
1130// Arguments:
1131// points = The array of 2D polygon vertices.
1132// paths = The path connections between the vertices.
1133// ---
1134// vertices = if true display vertex labels and start/end markers. Default: true
1135// edges = if true display edge labels. Default: true
1136// convexity = The max number of walls a ray can pass through the given polygon paths.
1137// size = The base size of the line and labels.
1138// Example(Big2D):
1139// debug_polygon(
1140// points=concat(
1141// regular_ngon(or=10, n=8),
1142// regular_ngon(or=8, n=8)
1143// ),
1144// paths=[
1145// [for (i=[0:7]) i],
1146// [for (i=[15:-1:8]) i]
1147// ]
1148// );
1149module debug_polygon(points, paths, vertices=true, edges=true, convexity=2, size=1)
1150{
1151 no_children($children);
1152 paths = is_undef(paths)? [count(points)] :
1153 is_num(paths[0])? [paths] :
1154 paths;
1155 echo(points=points);
1156 echo(paths=paths);
1157 linear_extrude(height=0.01, convexity=convexity, center=true) {
1158 polygon(points=points, paths=paths, convexity=convexity);
1159 }
1160 dups = vector_search(points, EPSILON, points);
1161
1162 if (vertices) color("red") {
1163 for (ind=dups){
1164 numstr = str_join([for(i=ind) str(i)],",");
1165 up(0.2) {
1166 translate(points[ind[0]]) {
1167 linear_extrude(height=0.1, convexity=10, center=true) {
1168 text(text=numstr, size=size, halign="center", valign="center");
1169 }
1170 }
1171 }
1172 }
1173 }
1174 if (edges)
1175 for (j = [0:1:len(paths)-1]) {
1176 path = paths[j];
1177 if (vertices){
1178 translate(points[path[0]]) {
1179 color("cyan") up(0.1) cylinder(d=size*1.5, h=0.01, center=false, $fn=12);
1180 }
1181 translate(points[path[len(path)-1]]) {
1182 color("pink") up(0.11) cylinder(d=size*1.5, h=0.01, center=false, $fn=4);
1183 }
1184 }
1185 for (i = [0:1:len(path)-1]) {
1186 midpt = (points[path[i]] + points[path[(i+1)%len(path)]])/2;
1187 color("blue") {
1188 up(0.2) {
1189 translate(midpt) {
1190 linear_extrude(height=0.1, convexity=10, center=true) {
1191 text(text=str(chr(65+j),i), size=size/2, halign="center", valign="center");
1192 }
1193 }
1194 }
1195 }
1196 }
1197 }
1198}
1199
1200
1201// vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap