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