From 856f54df32bde075b691d6619d89134ee27dec47 Mon Sep 17 00:00:00 2001 From: Adrian Mariano Date: Wed, 15 Sep 2021 23:12:51 -0400 Subject: [PATCH] removed old triangulation.scad other file name changes --- drawing.scad | 934 ++++++++++++++++++ geometry.scad | 2 +- paths.scad | 30 - shapes2d.scad | 897 +---------------- shapes.scad => shapes3d.scad | 0 std.scad | 3 +- tests/test_shapes2d.scad | 49 - .../{test_shapes.scad => test_shapes3d.scad} | 0 triangulation.scad | 194 ---- vnf.scad | 5 +- 10 files changed, 940 insertions(+), 1174 deletions(-) create mode 100644 drawing.scad rename shapes.scad => shapes3d.scad (100%) rename tests/{test_shapes.scad => test_shapes3d.scad} (100%) delete mode 100644 triangulation.scad diff --git a/drawing.scad b/drawing.scad new file mode 100644 index 0000000..8064ebc --- /dev/null +++ b/drawing.scad @@ -0,0 +1,934 @@ +////////////////////////////////////////////////////////////////////// +// LibFile: drawing.scad +// This file includes stroke(), which converts a path into a +// geometric object, like drawing with a pen. It even works on +// three-dimensional paths. You can make a dashed line or add arrow +// heads. The turtle() function provides a turtle graphics style +// approach for producing paths. The arc() function produces arc paths, +// and helix() produces helix paths. +// Includes: +// include +////////////////////////////////////////////////////////////////////// + + +// Section: Line Drawing + +// Module: stroke() +// Usage: +// stroke(path, [width], [closed], [endcaps], [endcap_width], [endcap_length], [endcap_extent], [trim]); +// stroke(path, [width], [closed], [endcap1], [endcap2], [endcap_width1], [endcap_width2], [endcap_length1], [endcap_length2], [endcap_extent1], [endcap_extent2], [trim1], [trim2]); +// Topics: Paths (2D), Paths (3D), Drawing Tools +// Description: +// Draws a 2D or 3D path with a given line width. Endcaps can be specified for each end individually. +// Figure(Med,NoAxes,2D,VPR=[0,0,0],VPD=250): Endcap Types +// cap_pairs = [ +// ["butt", "chisel" ], +// ["round", "square" ], +// ["line", "cross" ], +// ["x", "diamond"], +// ["dot", "block" ], +// ["tail", "arrow" ], +// ["tail2", "arrow2" ] +// ]; +// for (i = idx(cap_pairs)) { +// fwd((i-len(cap_pairs)/2+0.5)*13) { +// stroke([[-20,0], [20,0]], width=3, endcap1=cap_pairs[i][0], endcap2=cap_pairs[i][1]); +// color("black") { +// stroke([[-20,0], [20,0]], width=0.25, endcaps=false); +// left(28) text(text=cap_pairs[i][0], size=5, halign="right", valign="center"); +// right(28) text(text=cap_pairs[i][1], size=5, halign="left", valign="center"); +// } +// } +// } +// Arguments: +// path = The path to draw along. +// 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. +// closed = If true, draw an additional line from the end of the path to the start. +// plots = Specifies the plot point shape for every point of the line. If a 2D path is given, use that to draw custom plot points. +// joints = Specifies the joint shape for each joint of the line. If a 2D path is given, use that to draw custom joints. +// endcaps = Specifies the endcap type for both ends of the line. If a 2D path is given, use that to draw custom endcaps. +// endcap1 = Specifies the endcap type for the start of the line. If a 2D path is given, use that to draw a custom endcap. +// endcap2 = Specifies the endcap type for the end of the line. If a 2D path is given, use that to draw a custom endcap. +// plot_width = Some plot point shapes are wider than the line. This specifies the width of the shape, in multiples of the line width. +// joint_width = Some joint shapes are wider than the line. This specifies the width of the shape, in multiples of the line width. +// endcap_width = Some endcap types are wider than the line. This specifies the size of endcaps, in multiples of the line width. +// endcap_width1 = This specifies the size of starting endcap, in multiples of the line width. +// endcap_width2 = This specifies the size of ending endcap, in multiples of the line width. +// plot_length = Length of plot point shape, in multiples of the line width. +// joint_length = Length of joint shape, in multiples of the line width. +// endcap_length = Length of endcaps, in multiples of the line width. +// endcap_length1 = Length of starting endcap, in multiples of the line width. +// endcap_length2 = Length of ending endcap, in multiples of the line width. +// plot_extent = Extents length of plot point shape, in multiples of the line width. +// joint_extent = Extents length of joint shape, in multiples of the line width. +// endcap_extent = Extents length of endcaps, in multiples of the line width. +// endcap_extent1 = Extents length of starting endcap, in multiples of the line width. +// endcap_extent2 = Extents length of ending endcap, in multiples of the line width. +// plot_angle = Extra rotation given to plot point shapes, in degrees. If not given, the shapes are fully spun. +// joint_angle = Extra rotation given to joint shapes, in degrees. If not given, the shapes are fully spun. +// endcap_angle = Extra rotation given to endcaps, in degrees. If not given, the endcaps are fully spun. +// endcap_angle1 = Extra rotation given to a starting endcap, in degrees. If not given, the endcap is fully spun. +// endcap_angle2 = Extra rotation given to a ending endcap, in degrees. If not given, the endcap is fully spun. +// trim = Trim the the start and end line segments by this much, to keep them from interfering with custom endcaps. +// trim1 = Trim the the starting line segment by this much, to keep it from interfering with a custom endcap. +// trim2 = Trim the the ending line segment by this much, to keep it from interfering with a custom endcap. +// convexity = Max number of times a line could intersect a wall of an endcap. +// hull = If true, use `hull()` to make higher quality joints between segments, at the cost of being much slower. Default: true +// Example(2D): Drawing a Path +// path = [[0,100], [100,100], [200,0], [100,-100], [100,0]]; +// stroke(path, width=20); +// Example(2D): Closing a Path +// path = [[0,100], [100,100], [200,0], [100,-100], [100,0]]; +// stroke(path, width=20, endcaps=true, closed=true); +// Example(2D): Fancy Arrow Endcaps +// path = [[0,100], [100,100], [200,0], [100,-100], [100,0]]; +// stroke(path, width=10, endcaps="arrow2"); +// Example(2D): Modified Fancy Arrow Endcaps +// path = [[0,100], [100,100], [200,0], [100,-100], [100,0]]; +// stroke(path, width=10, endcaps="arrow2", endcap_width=6, endcap_length=3, endcap_extent=2); +// Example(2D): Mixed Endcaps +// path = [[0,100], [100,100], [200,0], [100,-100], [100,0]]; +// stroke(path, width=10, endcap1="tail2", endcap2="arrow2"); +// Example(2D): Plotting Points +// path = [for (a=[0:30:360]) [a-180, 60*sin(a)]]; +// stroke(path, width=3, joints="diamond", endcaps="arrow2", plot_angle=0, plot_width=5); +// Example(2D): Joints and Endcaps +// path = [for (a=[0:30:360]) [a-180, 60*sin(a)]]; +// stroke(path, width=3, joints="dot", endcaps="arrow2", joint_angle=0); +// Example(2D): Custom Endcap Shapes +// path = [[0,100], [100,100], [200,0], [100,-100], [100,0]]; +// 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]]; +// stroke(path, width=10, trim=3.5, endcaps=arrow); +// Example(2D): Variable Line Width +// path = circle(d=50,$fn=18); +// widths = [for (i=idx(path)) 10*i/len(path)+2]; +// stroke(path,width=widths,$fa=1,$fs=1); +// Example: 3D Path with Endcaps +// path = rot([15,30,0], p=path3d(pentagon(d=50))); +// stroke(path, width=2, endcaps="arrow2", $fn=18); +// Example: 3D Path with Flat Endcaps +// path = rot([15,30,0], p=path3d(pentagon(d=50))); +// stroke(path, width=2, endcaps="arrow2", endcap_angle=0, $fn=18); +// Example: 3D Path with Mixed Endcaps +// path = rot([15,30,0], p=path3d(pentagon(d=50))); +// stroke(path, width=2, endcap1="arrow2", endcap2="tail", endcap_angle2=0, $fn=18); +// Example: 3D Path with Joints and Endcaps +// path = [for (i=[0:10:360]) [(i-180)/2,20*cos(3*i),20*sin(3*i)]]; +// stroke(path, width=2, joints="dot", endcap1="round", endcap2="arrow2", joint_width=2.0, endcap_width2=3, $fn=18); +function stroke( + path, width=1, closed=false, + endcaps, endcap1, endcap2, joints, plots, + endcap_width, endcap_width1, endcap_width2, joint_width, plot_width, + endcap_length, endcap_length1, endcap_length2, joint_length, plot_length, + endcap_extent, endcap_extent1, endcap_extent2, joint_extent, plot_extent, + endcap_angle, endcap_angle1, endcap_angle2, joint_angle, plot_angle, + trim, trim1, trim2, + convexity=10, hull=true +) = no_function("stroke"); +module stroke( + path, width=1, closed=false, + endcaps, endcap1, endcap2, joints, plots, + endcap_width, endcap_width1, endcap_width2, joint_width, plot_width, + endcap_length, endcap_length1, endcap_length2, joint_length, plot_length, + endcap_extent, endcap_extent1, endcap_extent2, joint_extent, plot_extent, + endcap_angle, endcap_angle1, endcap_angle2, joint_angle, plot_angle, + trim, trim1, trim2, + convexity=10, hull=true +) { + function _shape_defaults(cap) = + cap==undef? [1.00, 0.00, 0.00] : + cap==false? [1.00, 0.00, 0.00] : + cap==true? [1.00, 1.00, 0.00] : + cap=="butt"? [1.00, 0.00, 0.00] : + cap=="round"? [1.00, 1.00, 0.00] : + cap=="chisel"? [1.00, 1.00, 0.00] : + cap=="square"? [1.00, 1.00, 0.00] : + cap=="block"? [3.00, 1.00, 0.00] : + cap=="diamond"? [3.50, 1.00, 0.00] : + cap=="dot"? [3.00, 1.00, 0.00] : + cap=="x"? [3.50, 0.40, 0.00] : + cap=="cross"? [4.50, 0.22, 0.00] : + cap=="line"? [4.50, 0.22, 0.00] : + cap=="arrow"? [3.50, 0.40, 0.50] : + cap=="arrow2"? [3.50, 1.00, 0.14] : + cap=="tail"? [3.50, 0.47, 0.50] : + cap=="tail2"? [3.50, 0.28, 0.50] : + is_path(cap)? [0.00, 0.00, 0.00] : + assert(false, str("Invalid cap or joint: ",cap)); + + function _shape_path(cap,linewidth,w,l,l2) = ( + (cap=="butt" || cap==false || cap==undef)? [] : + (cap=="round" || cap==true)? scale([w,l], p=circle(d=1, $fn=max(8, segs(w/2)))) : + cap=="chisel"? scale([w,l], p=circle(d=1,$fn=4)) : + cap=="diamond"? circle(d=w,$fn=4) : + cap=="square"? scale([w,l], p=square(1,center=true)) : + cap=="block"? scale([w,l], p=square(1,center=true)) : + cap=="dot"? circle(d=w, $fn=max(12, segs(w*3/2))) : + 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]]) ] : + cap=="cross"? [for (a=[0:90:270]) each rot(a,p=[[l,w]/2, [-l,w]/2, [-l,l]/2]) ] : + cap=="line"? scale([w,l], p=square(1,center=true)) : + cap=="arrow"? [[0,0], [w/2,-l2], [w/2,-l2-l], [0,-l], [-w/2,-l2-l], [-w/2,-l2]] : + cap=="arrow2"? [[0,0], [w/2,-l2-l], [0,-l], [-w/2,-l2-l]] : + cap=="tail"? [[0,0], [w/2,l2], [w/2,l2-l], [0,-l], [-w/2,l2-l], [-w/2,l2]] : + cap=="tail2"? [[w/2,0], [w/2,-l], [0,-l-l2], [-w/2,-l], [-w/2,0]] : + is_path(cap)? cap : + assert(false, str("Invalid endcap: ",cap)) + ) * linewidth; + + assert(is_bool(closed)); + assert(is_list(path)); + if (len(path) > 1) { + assert(is_path(path,[2,3]), "The path argument must be a list of 2D or 3D points."); + } + path = deduplicate( closed? close_path(path) : path ); + + assert(is_num(width) || (is_vector(width) && len(width)==len(path))); + width = is_num(width)? [for (x=path) width] : width; + assert(all([for (w=width) w>0])); + + endcap1 = first_defined([endcap1, endcaps, if(!closed) plots, "round"]); + endcap2 = first_defined([endcap2, endcaps, plots, "round"]); + joints = first_defined([joints, plots, "round"]); + assert(is_bool(endcap1) || is_string(endcap1) || is_path(endcap1)); + assert(is_bool(endcap2) || is_string(endcap2) || is_path(endcap2)); + assert(is_bool(joints) || is_string(joints) || is_path(joints)); + + endcap1_dflts = _shape_defaults(endcap1); + endcap2_dflts = _shape_defaults(endcap2); + joint_dflts = _shape_defaults(joints); + + endcap_width1 = first_defined([endcap_width1, endcap_width, plot_width, endcap1_dflts[0]]); + endcap_width2 = first_defined([endcap_width2, endcap_width, plot_width, endcap2_dflts[0]]); + joint_width = first_defined([joint_width, plot_width, joint_dflts[0]]); + assert(is_num(endcap_width1)); + assert(is_num(endcap_width2)); + assert(is_num(joint_width)); + + endcap_length1 = first_defined([endcap_length1, endcap_length, plot_length, endcap1_dflts[1]*endcap_width1]); + endcap_length2 = first_defined([endcap_length2, endcap_length, plot_length, endcap2_dflts[1]*endcap_width2]); + joint_length = first_defined([joint_length, plot_length, joint_dflts[1]*joint_width]); + assert(is_num(endcap_length1)); + assert(is_num(endcap_length2)); + assert(is_num(joint_length)); + + endcap_extent1 = first_defined([endcap_extent1, endcap_extent, plot_extent, endcap1_dflts[2]*endcap_width1]); + endcap_extent2 = first_defined([endcap_extent2, endcap_extent, plot_extent, endcap2_dflts[2]*endcap_width2]); + joint_extent = first_defined([joint_extent, plot_extent, joint_dflts[2]*joint_width]); + assert(is_num(endcap_extent1)); + assert(is_num(endcap_extent2)); + assert(is_num(joint_extent)); + + endcap_angle1 = first_defined([endcap_angle1, endcap_angle, plot_angle]); + endcap_angle2 = first_defined([endcap_angle2, endcap_angle, plot_angle]); + joint_angle = first_defined([joint_angle, plot_angle]); + assert(is_undef(endcap_angle1)||is_num(endcap_angle1)); + assert(is_undef(endcap_angle2)||is_num(endcap_angle2)); + assert(is_undef(joint_angle)||is_num(joint_angle)); + + endcap_shape1 = _shape_path(endcap1, width[0], endcap_width1, endcap_length1, endcap_extent1); + endcap_shape2 = _shape_path(endcap2, last(width), endcap_width2, endcap_length2, endcap_extent2); + + trim1 = width[0] * first_defined([ + trim1, trim, + (endcap1=="arrow")? endcap_length1-0.01 : + (endcap1=="arrow2")? endcap_length1*3/4 : + 0 + ]); + assert(is_num(trim1)); + + trim2 = last(width) * first_defined([ + trim2, trim, + (endcap2=="arrow")? endcap_length2-0.01 : + (endcap2=="arrow2")? endcap_length2*3/4 : + 0 + ]); + assert(is_num(trim2)); + + if (len(path) == 1) { + if (len(path[0]) == 2) { + translate(path[0]) circle(d=width[0]); + } else { + translate(path[0]) sphere(d=width[0]); + } + } else { + spos = path_pos_from_start(path,trim1,closed=false); + epos = path_pos_from_end(path,trim2,closed=false); + path2 = path_subselect(path, spos[0], spos[1], epos[0], epos[1]); + widths = concat( + [lerp(width[spos[0]], width[(spos[0]+1)%len(width)], spos[1])], + [for (i = [spos[0]+1:1:epos[0]]) width[i]], + [lerp(width[epos[0]], width[(epos[0]+1)%len(width)], epos[1])] + ); + + start_vec = path[0] - path[1]; + end_vec = last(path) - select(path,-2); + + if (len(path[0]) == 2) { + // Straight segments + for (i = idx(path2,e=-2)) { + seg = select(path2,i,i+1); + delt = seg[1] - seg[0]; + translate(seg[0]) { + rot(from=BACK,to=delt) { + trapezoid(w1=widths[i], w2=widths[i+1], h=norm(delt), anchor=FRONT); + } + } + } + + // Joints + for (i = [1:1:len(path2)-2]) { + $fn = quantup(segs(widths[i]/2),4); + translate(path2[i]) { + if (joints != undef) { + joint_shape = _shape_path( + joints, width[i], + joint_width, + joint_length, + joint_extent + ); + v1 = unit(path2[i] - path2[i-1]); + v2 = unit(path2[i+1] - path2[i]); + vec = unit((v1+v2)/2); + mat = is_undef(joint_angle) + ? rot(from=BACK,to=v1) + : zrot(joint_angle); + multmatrix(mat) polygon(joint_shape); + } else if (hull) { + hull() { + rot(from=BACK, to=path2[i]-path2[i-1]) + circle(d=widths[i]); + rot(from=BACK, to=path2[i+1]-path2[i]) + circle(d=widths[i]); + } + } else { + rot(from=BACK, to=path2[i]-path2[i-1]) + circle(d=widths[i]); + rot(from=BACK, to=path2[i+1]-path2[i]) + circle(d=widths[i]); + } + } + } + + // Endcap1 + translate(path[0]) { + mat = is_undef(endcap_angle1)? rot(from=BACK,to=start_vec) : + zrot(endcap_angle1); + multmatrix(mat) polygon(endcap_shape1); + } + + // Endcap2 + translate(last(path)) { + mat = is_undef(endcap_angle2)? rot(from=BACK,to=end_vec) : + zrot(endcap_angle2); + multmatrix(mat) polygon(endcap_shape2); + } + } else { + quatsums = q_cumulative([ + for (i = idx(path2,e=-2)) let( + vec1 = i==0? UP : unit(path2[i]-path2[i-1], UP), + vec2 = unit(path2[i+1]-path2[i], UP), + axis = vector_axis(vec1,vec2), + ang = vector_angle(vec1,vec2) + ) quat(axis,ang) + ]); + rotmats = [for (q=quatsums) q_matrix4(q)]; + sides = [ + for (i = idx(path2,e=-2)) + quantup(segs(max(widths[i],widths[i+1])/2),4) + ]; + + // Straight segments + for (i = idx(path2,e=-2)) { + dist = norm(path2[i+1] - path2[i]); + w1 = widths[i]/2; + w2 = widths[i+1]/2; + $fn = sides[i]; + translate(path2[i]) { + multmatrix(rotmats[i]) { + cylinder(r1=w1, r2=w2, h=dist, center=false); + } + } + } + + // Joints + for (i = [1:1:len(path2)-2]) { + $fn = sides[i]; + translate(path2[i]) { + if (joints != undef) { + joint_shape = _shape_path( + joints, width[i], + joint_width, + joint_length, + joint_extent + ); + multmatrix(rotmats[i] * xrot(180)) { + $fn = sides[i]; + if (is_undef(joint_angle)) { + rotate_extrude(convexity=convexity) { + right_half(planar=true) { + polygon(joint_shape); + } + } + } else { + rotate([90,0,joint_angle]) { + linear_extrude(height=max(widths[i],0.001), center=true, convexity=convexity) { + polygon(joint_shape); + } + } + } + } + } else if (hull) { + hull(){ + multmatrix(rotmats[i]) { + sphere(d=widths[i],style="aligned"); + } + multmatrix(rotmats[i-1]) { + sphere(d=widths[i],style="aligned"); + } + } + } else { + multmatrix(rotmats[i]) { + sphere(d=widths[i],style="aligned"); + } + multmatrix(rotmats[i-1]) { + sphere(d=widths[i],style="aligned"); + } + } + } + } + + // Endcap1 + translate(path[0]) { + multmatrix(rotmats[0] * xrot(180)) { + $fn = sides[0]; + if (is_undef(endcap_angle1)) { + rotate_extrude(convexity=convexity) { + right_half(planar=true) { + polygon(endcap_shape1); + } + } + } else { + rotate([90,0,endcap_angle1]) { + linear_extrude(height=max(widths[0],0.001), center=true, convexity=convexity) { + polygon(endcap_shape1); + } + } + } + } + } + + // Endcap2 + translate(last(path)) { + multmatrix(last(rotmats)) { + $fn = last(sides); + if (is_undef(endcap_angle2)) { + rotate_extrude(convexity=convexity) { + right_half(planar=true) { + polygon(endcap_shape2); + } + } + } else { + rotate([90,0,endcap_angle2]) { + linear_extrude(height=max(last(widths),0.001), center=true, convexity=convexity) { + polygon(endcap_shape2); + } + } + } + } + } + } + } +} + + +// Function&Module: dashed_stroke() +// Usage: As a Module +// dashed_stroke(path, dashpat, [closed=]); +// Usage: As a Function +// dashes = dashed_stroke(path, dashpat, width=, [closed=]); +// Topics: Paths, Drawing Tools +// See Also: stroke(), path_cut() +// Description: +// Given a path and a dash pattern, creates a dashed line that follows that +// path with the given dash pattern. +// - When called as a function, returns a list of dash sub-paths. +// - When called as a module, draws all those subpaths using `stroke()`. +// Arguments: +// path = The path to subdivide into dashes. +// dashpat = A list of alternating dash lengths and space lengths for the dash pattern. This will be scaled by the width of the line. +// --- +// width = The width of the dashed line to draw. Module only. Default: 1 +// closed = If true, treat path as a closed polygon. Default: false +// Example(2D): Open Path +// path = [for (a=[-180:10:180]) [a/3,20*sin(a)]]; +// dashed_stroke(path, [3,2], width=1); +// Example(2D): Closed Polygon +// path = circle(d=100,$fn=72); +// dashpat = [10,2,3,2,3,2]; +// dashed_stroke(path, dashpat, width=1, closed=true); +// Example(FlatSpin,VPD=250): 3D Dashed Path +// path = [for (a=[-180:5:180]) [a/3, 20*cos(3*a), 20*sin(3*a)]]; +// dashed_stroke(path, [3,2], width=1); +function dashed_stroke(path, dashpat=[3,3], closed=false) = + let( + path = closed? close_path(path) : path, + dashpat = len(dashpat)%2==0? dashpat : concat(dashpat,[0]), + plen = path_length(path), + dlen = sum(dashpat), + doff = cumsum(dashpat), + reps = floor(plen / dlen), + step = plen / reps, + cuts = [ + for (i=[0:1:reps-1], off=doff) + let (st=i*step, x=st+off) + if (x>0 && x=2), "Number of points must be an integer 2 or larger") + // First try for 2D arc specified by width and thickness + is_def(width) && is_def(thickness)? ( + assert(!any_defined([r,cp,points]) && !any([cw,ccw,long]),"Conflicting or invalid parameters to arc") + assert(width>0, "Width must be postive") + assert(thickness>0, "Thickness must be positive") + arc(N,points=[[width/2,0], [0,thickness], [-width/2,0]],wedge=wedge) + ) : is_def(angle)? ( + let( + parmok = !any_defined([points,width,thickness]) && + ((is_vector(angle,2) && is_undef(start)) || is_num(angle)) + ) + assert(parmok,"Invalid parameters in arc") + let( + cp = first_defined([cp,[0,0]]), + start = is_def(start)? start : is_vector(angle) ? angle[0] : 0, + angle = is_vector(angle)? angle[1]-angle[0] : angle, + r = get_radius(r=r, d=d) + ) + assert(is_vector(cp,2),"Centerpoint must be a 2d vector") + assert(angle!=0, "Arc has zero length") + assert(is_def(r) && r>0, "Arc radius invalid") + let( + N = is_def(N) ? N : max(3, ceil(segs(r)*abs(angle)/360)), + arcpoints = [for(i=[0:N-1]) let(theta = start + i*angle/(N-1)) r*[cos(theta),sin(theta)]+cp], + extra = wedge? [cp] : [] + ) + concat(extra,arcpoints) + ) : + assert(is_path(points,[2,3]),"Point list is invalid") + // Arc is 3D, so transform points to 2D and make a recursive call, then remap back to 3D + len(points[0])==3? ( + assert(!(cw || ccw), "(Counter)clockwise isn't meaningful in 3d, so `cw` and `ccw` must be false") + assert(is_undef(cp) || is_vector(cp,3),"points are 3d so cp must be 3d") + let( + plane = [is_def(cp) ? cp : points[2], points[0], points[1]], + center2d = is_def(cp) ? project_plane(plane,cp) : undef, + points2d = project_plane(plane, points) + ) + lift_plane(plane,arc(N,cp=center2d,points=points2d,wedge=wedge,long=long)) + ) : is_def(cp)? ( + // Arc defined by center plus two points, will have radius defined by center and points[0] + // and extent defined by direction of point[1] from the center + assert(is_vector(cp,2), "Centerpoint must be a 2d vector") + assert(len(points)==2, "When pointlist has length 3 centerpoint is not allowed") + assert(points[0]!=points[1], "Arc endpoints are equal") + assert(cp!=points[0]&&cp!=points[1], "Centerpoint equals an arc endpoint") + assert(count_true([long,cw,ccw])<=1, str("Only one of `long`, `cw` and `ccw` can be true",cw,ccw,long)) + let( + angle = vector_angle(points[0], cp, points[1]), + v1 = points[0]-cp, + v2 = points[1]-cp, + prelim_dir = sign(det2([v1,v2])), // z component of cross product + dir = prelim_dir != 0 + ? prelim_dir + : assert(cw || ccw, "Collinear inputs don't define a unique arc") + 1, + r=norm(v1), + final_angle = long || (ccw && dir<0) || (cw && dir>0) ? -dir*(360-angle) : dir*angle + ) + arc(N,cp=cp,r=r,start=atan2(v1.y,v1.x),angle=final_angle,wedge=wedge) + ) : ( + // Final case is arc passing through three points, starting at point[0] and ending at point[3] + let(col = is_collinear(points[0],points[1],points[2])) + assert(!col, "Collinear inputs do not define an arc") + let( + cp = line_intersection(_normal_segment(points[0],points[1]),_normal_segment(points[1],points[2])), + // select order to be counterclockwise + dir = det2([points[1]-points[0],points[2]-points[1]]) > 0, + points = dir? select(points,[0,2]) : select(points,[2,0]), + r = norm(points[0]-cp), + theta_start = atan2(points[0].y-cp.y, points[0].x-cp.x), + theta_end = atan2(points[1].y-cp.y, points[1].x-cp.x), + angle = posmod(theta_end-theta_start, 360), + arcpts = arc(N,cp=cp,r=r,start=theta_start,angle=angle,wedge=wedge) + ) + dir ? arcpts : reverse(arcpts) + ); + + +module arc(N, r, angle, d, cp, points, width, thickness, start, wedge=false) +{ + path = arc(N=N, r=r, angle=angle, d=d, cp=cp, points=points, width=width, thickness=thickness, start=start, wedge=wedge); + polygon(path); +} + + +// Function: helix() +// Description: +// Returns a 3D helical path. +// Usage: +// helix(turns, h, n, r|d, [cp], [scale]); +// Arguments: +// h = Height of spiral. +// turns = Number of turns in spiral. +// n = Number of spiral sides. +// r = Radius of spiral. +// d = Radius of spiral. +// cp = Centerpoint of spiral. Default: `[0,0]` +// scale = [X,Y] scaling factors for each axis. Default: `[1,1]` +// Example(3D): +// trace_path(helix(turns=2.5, h=100, n=24, r=50), N=1, showpts=true); +function helix(turns=3, h=100, n=12, r, d, cp=[0,0], scale=[1,1]) = let( + rr=get_radius(r=r, d=d, dflt=100), + cnt=floor(turns*n), + dz=h/cnt + ) [ + for (i=[0:1:cnt]) [ + rr * cos(i*360/n) * scale.x + cp.x, + rr * sin(i*360/n) * scale.y + cp.y, + i*dz + ] + ]; + + + +function _normal_segment(p1,p2) = + let(center = (p1+p2)/2) + [center, center + norm(p1-p2)/2 * line_normal(p1,p2)]; + + +// Function: turtle() +// Usage: +// turtle(commands, [state], [full_state=], [repeat=]) +// Topics: Shapes (2D), Path Generators (2D), Mini-Language +// See Also: turtle3d() +// Description: +// Use a sequence of turtle graphics commands to generate a path. The parameter `commands` is a list of +// turtle commands and optional parameters for each command. The turtle state has a position, movement direction, +// movement distance, and default turn angle. If you do not give `state` as input then the turtle starts at the +// origin, pointed along the positive x axis with a movement distance of 1. By default, `turtle` returns just +// the computed turtle path. If you set `full_state` to true then it instead returns the full turtle state. +// You can invoke `turtle` again with this full state to continue the turtle path where you left off. +// . +// 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, +// and the current arcsteps setting. +// . +// Commands | Arguments | What it does +// ------------ | ------------------ | ------------------------------- +// "move" | [dist] | Move turtle scale*dist units in the turtle direction. Default dist=1. +// "xmove" | [dist] | Move turtle scale*dist units in the x direction. Default dist=1. Does not change turtle direction. +// "ymove" | [dist] | Move turtle scale*dist units in the y direction. Default dist=1. Does not change turtle direction. +// "xymove" | vector | Move turtle by the specified vector. Does not change turtle direction. +// "untilx" | xtarget | Move turtle in turtle direction until x==xtarget. Produces an error if xtarget is not reachable. +// "untily" | ytarget | Move turtle in turtle direction until y==ytarget. Produces an error if xtarget is not reachable. +// "jump" | point | Move the turtle to the specified point +// "xjump" | x | Move the turtle's x position to the specified value +// "yjump | y | Move the turtle's y position to the specified value +// "turn" | [angle] | Turn turtle direction by specified angle, or the turtle's default turn angle. The default angle starts at 90. +// "left" | [angle] | Same as "turn" +// "right" | [angle] | Same as "turn", -angle +// "angle" | angle | Set the default turn angle. +// "setdir" | dir | Set turtle direction. The parameter `dir` can be an angle or a vector. +// "length" | length | Change the turtle move distance to `length` +// "scale" | factor | Multiply turtle move distance by `factor` +// "addlength" | length | Add `length` to the turtle move distance +// "repeat" | count, commands | Repeats a list of commands `count` times. +// "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. +// "arcright" | radius, [angle] | Draw an arc from the current position toward the right at the specified radius and angle +// "arcleftto" | radius, angle | Draw an arc at the given radius turning toward the left until reaching the specified absolute angle. +// "arcrightto" | radius, angle | Draw an arc at the given radius turning toward the right until reaching the specified absolute angle. +// "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. +// +// Arguments: +// commands = List of turtle commands +// state = Starting turtle state (from previous call) or starting point. Default: start at the origin, pointing right. +// --- +// full_state = If true return the full turtle state for continuing the path in subsequent turtle calls. Default: false +// repeat = Number of times to repeat the command list. Default: 1 +// +// Example(2D): Simple rectangle +// path = turtle(["xmove",3, "ymove", "xmove",-3, "ymove",-1]); +// stroke(path,width=.1); +// Example(2D): Pentagon +// path=turtle(["angle",360/5,"move","turn","move","turn","move","turn","move"]); +// stroke(path,width=.1,closed=true); +// Example(2D): Pentagon using the repeat argument +// path=turtle(["move","turn",360/5],repeat=5); +// stroke(path,width=.1,closed=true); +// Example(2D): Pentagon using the repeat turtle command, setting the turn angle +// path=turtle(["angle",360/5,"repeat",5,["move","turn"]]); +// stroke(path,width=.1,closed=true); +// Example(2D): Pentagram +// path = turtle(["move","left",144], repeat=4); +// stroke(path,width=.05,closed=true); +// Example(2D): Sawtooth path +// path = turtle([ +// "turn", 55, +// "untily", 2, +// "turn", -55-90, +// "untily", 0, +// "turn", 55+90, +// "untily", 2.5, +// "turn", -55-90, +// "untily", 0, +// "turn", 55+90, +// "untily", 3, +// "turn", -55-90, +// "untily", 0 +// ]); +// stroke(path, width=.1); +// Example(2D): Simpler way to draw the sawtooth. The direction of the turtle is preserved when executing "yjump". +// path = turtle([ +// "turn", 55, +// "untily", 2, +// "yjump", 0, +// "untily", 2.5, +// "yjump", 0, +// "untily", 3, +// "yjump", 0, +// ]); +// stroke(path, width=.1); +// Example(2DMed): square spiral +// path = turtle(["move","left","addlength",1],repeat=50); +// stroke(path,width=.2); +// Example(2DMed): pentagonal spiral +// path = turtle(["move","left",360/5,"addlength",1],repeat=50); +// stroke(path,width=.2); +// Example(2DMed): yet another spiral, without using `repeat` +// path = turtle(concat(["angle",71],flatten(repeat(["move","left","addlength",1],50)))); +// stroke(path,width=.2); +// Example(2DMed): The previous spiral grows linearly and eventually intersects itself. This one grows geometrically and does not. +// path = turtle(["move","left",71,"scale",1.05],repeat=50); +// stroke(path,width=.05); +// Example(2D): Koch Snowflake +// function koch_unit(depth) = +// depth==0 ? ["move"] : +// concat( +// koch_unit(depth-1), +// ["right"], +// koch_unit(depth-1), +// ["left","left"], +// koch_unit(depth-1), +// ["right"], +// koch_unit(depth-1) +// ); +// koch=concat(["angle",60,"repeat",3],[concat(koch_unit(3),["left","left"])]); +// polygon(turtle(koch)); +module turtle(commands, state=[[[0,0]],[1,0],90,0], full_state=false, repeat=1) {no_module();} +function turtle(commands, state=[[[0,0]],[1,0],90,0], full_state=false, repeat=1) = + let( state = is_vector(state) ? [[state],[1,0],90,0] : state ) + repeat == 1? + _turtle(commands,state,full_state) : + _turtle_repeat(commands, state, full_state, repeat); + +function _turtle_repeat(commands, state, full_state, repeat) = + repeat==1? + _turtle(commands,state,full_state) : + _turtle_repeat(commands, _turtle(commands, state, true), full_state, repeat-1); + +function _turtle_command_len(commands, index) = + let( one_or_two_arg = ["arcleft","arcright", "arcleftto", "arcrightto"] ) + commands[index] == "repeat"? 3 : // Repeat command requires 2 args + // For these, the first arg is required, second arg is present if it is not a string + in_list(commands[index], one_or_two_arg) && len(commands)>index+2 && !is_string(commands[index+2]) ? 3 : + is_string(commands[index+1])? 1 : // If 2nd item is a string it's must be a new command + 2; // Otherwise we have command and arg + +function _turtle(commands, state, full_state, index=0) = + index < len(commands) ? + _turtle(commands, + _turtle_command(commands[index],commands[index+1],commands[index+2],state,index), + full_state, + index+_turtle_command_len(commands,index) + ) : + ( full_state ? state : state[0] ); + +// Turtle state: state = [path, step_vector, default angle, default arcsteps] + +function _turtle_command(command, parm, parm2, state, index) = + command == "repeat"? + assert(is_num(parm),str("\"repeat\" command requires a numeric repeat count at index ",index)) + assert(is_list(parm2),str("\"repeat\" command requires a command list parameter at index ",index)) + _turtle_repeat(parm2, state, true, parm) : + let( + path = 0, + step=1, + angle=2, + arcsteps=3, + parm = !is_string(parm) ? parm : undef, + parm2 = !is_string(parm2) ? parm2 : undef, + needvec = ["jump", "xymove"], + neednum = ["untilx","untily","xjump","yjump","angle","length","scale","addlength"], + needeither = ["setdir"], + chvec = !in_list(command,needvec) || is_vector(parm,2), + chnum = !in_list(command,neednum) || is_num(parm), + vec_or_num = !in_list(command,needeither) || (is_num(parm) || is_vector(parm,2)), + lastpt = last(state[path]) + ) + assert(chvec,str("\"",command,"\" requires a vector parameter at index ",index)) + assert(chnum,str("\"",command,"\" requires a numeric parameter at index ",index)) + assert(vec_or_num,str("\"",command,"\" requires a vector or numeric parameter at index ",index)) + + command=="move" ? list_set(state, path, concat(state[path],[default(parm,1)*state[step]+lastpt])) : + command=="untilx" ? ( + let( + int = line_intersection([lastpt,lastpt+state[step]], [[parm,0],[parm,1]]), + xgood = sign(state[step].x) == sign(int.x-lastpt.x) + ) + assert(xgood,str("\"untilx\" never reaches desired goal at index ",index)) + list_set(state,path,concat(state[path],[int])) + ) : + command=="untily" ? ( + let( + int = line_intersection([lastpt,lastpt+state[step]], [[0,parm],[1,parm]]), + ygood = is_def(int) && sign(state[step].y) == sign(int.y-lastpt.y) + ) + assert(ygood,str("\"untily\" never reaches desired goal at index ",index)) + list_set(state,path,concat(state[path],[int])) + ) : + command=="xmove" ? list_set(state, path, concat(state[path],[default(parm,1)*norm(state[step])*[1,0]+lastpt])): + command=="ymove" ? list_set(state, path, concat(state[path],[default(parm,1)*norm(state[step])*[0,1]+lastpt])): + command=="xymove" ? list_set(state, path, concat(state[path], [lastpt+parm])): + command=="jump" ? list_set(state, path, concat(state[path],[parm])): + command=="xjump" ? list_set(state, path, concat(state[path],[[parm,lastpt.y]])): + command=="yjump" ? list_set(state, path, concat(state[path],[[lastpt.x,parm]])): + command=="turn" || command=="left" ? list_set(state, step, rot(default(parm,state[angle]),p=state[step],planar=true)) : + command=="right" ? list_set(state, step, rot(-default(parm,state[angle]),p=state[step],planar=true)) : + command=="angle" ? list_set(state, angle, parm) : + command=="setdir" ? ( + is_vector(parm) ? + list_set(state, step, norm(state[step]) * unit(parm)) : + list_set(state, step, norm(state[step]) * [cos(parm),sin(parm)]) + ) : + command=="length" ? list_set(state, step, parm*unit(state[step])) : + command=="scale" ? list_set(state, step, parm*state[step]) : + command=="addlength" ? list_set(state, step, state[step]+unit(state[step])*parm) : + command=="arcsteps" ? list_set(state, arcsteps, parm) : + command=="arcleft" || command=="arcright" ? + assert(is_num(parm),str("\"",command,"\" command requires a numeric radius value at index ",index)) + let( + myangle = default(parm2,state[angle]), + lrsign = command=="arcleft" ? 1 : -1, + radius = parm*sign(myangle), + center = lastpt + lrsign*radius*line_normal([0,0],state[step]), + steps = state[arcsteps]==0 ? segs(abs(radius)) : state[arcsteps], + arcpath = myangle == 0 || radius == 0 ? [] : arc( + steps, + points = [ + lastpt, + rot(cp=center, p=lastpt, a=sign(parm)*lrsign*myangle/2), + rot(cp=center, p=lastpt, a=sign(parm)*lrsign*myangle) + ] + ) + ) + list_set( + state, [path,step], [ + concat(state[path], list_tail(arcpath)), + rot(lrsign * myangle,p=state[step],planar=true) + ] + ) : + command=="arcleftto" || command=="arcrightto" ? + assert(is_num(parm),str("\"",command,"\" command requires a numeric radius value at index ",index)) + assert(is_num(parm2),str("\"",command,"\" command requires a numeric angle value at index ",index)) + let( + radius = parm, + lrsign = command=="arcleftto" ? 1 : -1, + center = lastpt + lrsign*radius*line_normal([0,0],state[step]), + steps = state[arcsteps]==0 ? segs(abs(radius)) : state[arcsteps], + start_angle = posmod(atan2(state[step].y, state[step].x),360), + end_angle = posmod(parm2,360), + delta_angle = -start_angle + (lrsign * end_angle < lrsign*start_angle ? end_angle+lrsign*360 : end_angle), + arcpath = delta_angle == 0 || radius==0 ? [] : arc( + steps, + points = [ + lastpt, + rot(cp=center, p=lastpt, a=sign(radius)*delta_angle/2), + rot(cp=center, p=lastpt, a=sign(radius)*delta_angle) + ] + ) + ) + list_set( + state, [path,step], [ + concat(state[path], list_tail(arcpath)), + rot(delta_angle,p=state[step],planar=true) + ] + ) : + assert(false,str("Unknown turtle command \"",command,"\" at index",index)) + []; + diff --git a/geometry.scad b/geometry.scad index b1b0370..82b0e33 100644 --- a/geometry.scad +++ b/geometry.scad @@ -1604,8 +1604,8 @@ function polygon_triangulate(poly, ind, eps=EPSILON) = || (is_vector(ind) && min(ind)>=0 && max(ind) - - // Section: Functions @@ -609,33 +606,6 @@ function path_add_jitter(path, dist=1/512, closed=true) = ]; -// Function: path3d_spiral() -// Description: -// Returns a 3D spiral path. -// Usage: -// path3d_spiral(turns, h, n, r|d, [cp], [scale]); -// Arguments: -// h = Height of spiral. -// turns = Number of turns in spiral. -// n = Number of spiral sides. -// r = Radius of spiral. -// d = Radius of spiral. -// cp = Centerpoint of spiral. Default: `[0,0]` -// scale = [X,Y] scaling factors for each axis. Default: `[1,1]` -// Example(3D): -// trace_path(path3d_spiral(turns=2.5, h=100, n=24, r=50), N=1, showpts=true); -function path3d_spiral(turns=3, h=100, n=12, r, d, cp=[0,0], scale=[1,1]) = let( - rr=get_radius(r=r, d=d, dflt=100), - cnt=floor(turns*n), - dz=h/cnt - ) [ - for (i=[0:1:cnt]) [ - rr * cos(i*360/n) * scale.x + cp.x, - rr * sin(i*360/n) * scale.y + cp.y, - i*dz - ] - ]; - // Function: path_self_intersections() // Usage: diff --git a/shapes2d.scad b/shapes2d.scad index ea1cd05..51b70e2 100644 --- a/shapes2d.scad +++ b/shapes2d.scad @@ -1,10 +1,6 @@ ////////////////////////////////////////////////////////////////////// // LibFile: shapes2d.scad -// This file includes stroke(), which converts a path into a -// geometric object, like drawing with a pen. It even works on -// three-dimensional paths. You can make a dashed line or add arrow -// heads. The turtle() function provides a turtle graphics style -// approach for producing paths. You can create regular polygons +// This file lets you create regular polygons // with optional rounded corners and alignment features not // available with circle(). The file also provides teardrop2d, // which is useful for 3d printable holes. Lastly you can use the @@ -16,897 +12,6 @@ // include ////////////////////////////////////////////////////////////////////// -// Section: Line Drawing - -// Module: stroke() -// Usage: -// stroke(path, [width], [closed], [endcaps], [endcap_width], [endcap_length], [endcap_extent], [trim]); -// stroke(path, [width], [closed], [endcap1], [endcap2], [endcap_width1], [endcap_width2], [endcap_length1], [endcap_length2], [endcap_extent1], [endcap_extent2], [trim1], [trim2]); -// Topics: Paths (2D), Paths (3D), Drawing Tools -// Description: -// Draws a 2D or 3D path with a given line width. Endcaps can be specified for each end individually. -// Figure(Med,NoAxes,2D,VPR=[0,0,0],VPD=250): Endcap Types -// cap_pairs = [ -// ["butt", "chisel" ], -// ["round", "square" ], -// ["line", "cross" ], -// ["x", "diamond"], -// ["dot", "block" ], -// ["tail", "arrow" ], -// ["tail2", "arrow2" ] -// ]; -// for (i = idx(cap_pairs)) { -// fwd((i-len(cap_pairs)/2+0.5)*13) { -// stroke([[-20,0], [20,0]], width=3, endcap1=cap_pairs[i][0], endcap2=cap_pairs[i][1]); -// color("black") { -// stroke([[-20,0], [20,0]], width=0.25, endcaps=false); -// left(28) text(text=cap_pairs[i][0], size=5, halign="right", valign="center"); -// right(28) text(text=cap_pairs[i][1], size=5, halign="left", valign="center"); -// } -// } -// } -// Arguments: -// path = The path to draw along. -// 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. -// closed = If true, draw an additional line from the end of the path to the start. -// plots = Specifies the plot point shape for every point of the line. If a 2D path is given, use that to draw custom plot points. -// joints = Specifies the joint shape for each joint of the line. If a 2D path is given, use that to draw custom joints. -// endcaps = Specifies the endcap type for both ends of the line. If a 2D path is given, use that to draw custom endcaps. -// endcap1 = Specifies the endcap type for the start of the line. If a 2D path is given, use that to draw a custom endcap. -// endcap2 = Specifies the endcap type for the end of the line. If a 2D path is given, use that to draw a custom endcap. -// plot_width = Some plot point shapes are wider than the line. This specifies the width of the shape, in multiples of the line width. -// joint_width = Some joint shapes are wider than the line. This specifies the width of the shape, in multiples of the line width. -// endcap_width = Some endcap types are wider than the line. This specifies the size of endcaps, in multiples of the line width. -// endcap_width1 = This specifies the size of starting endcap, in multiples of the line width. -// endcap_width2 = This specifies the size of ending endcap, in multiples of the line width. -// plot_length = Length of plot point shape, in multiples of the line width. -// joint_length = Length of joint shape, in multiples of the line width. -// endcap_length = Length of endcaps, in multiples of the line width. -// endcap_length1 = Length of starting endcap, in multiples of the line width. -// endcap_length2 = Length of ending endcap, in multiples of the line width. -// plot_extent = Extents length of plot point shape, in multiples of the line width. -// joint_extent = Extents length of joint shape, in multiples of the line width. -// endcap_extent = Extents length of endcaps, in multiples of the line width. -// endcap_extent1 = Extents length of starting endcap, in multiples of the line width. -// endcap_extent2 = Extents length of ending endcap, in multiples of the line width. -// plot_angle = Extra rotation given to plot point shapes, in degrees. If not given, the shapes are fully spun. -// joint_angle = Extra rotation given to joint shapes, in degrees. If not given, the shapes are fully spun. -// endcap_angle = Extra rotation given to endcaps, in degrees. If not given, the endcaps are fully spun. -// endcap_angle1 = Extra rotation given to a starting endcap, in degrees. If not given, the endcap is fully spun. -// endcap_angle2 = Extra rotation given to a ending endcap, in degrees. If not given, the endcap is fully spun. -// trim = Trim the the start and end line segments by this much, to keep them from interfering with custom endcaps. -// trim1 = Trim the the starting line segment by this much, to keep it from interfering with a custom endcap. -// trim2 = Trim the the ending line segment by this much, to keep it from interfering with a custom endcap. -// convexity = Max number of times a line could intersect a wall of an endcap. -// hull = If true, use `hull()` to make higher quality joints between segments, at the cost of being much slower. Default: true -// Example(2D): Drawing a Path -// path = [[0,100], [100,100], [200,0], [100,-100], [100,0]]; -// stroke(path, width=20); -// Example(2D): Closing a Path -// path = [[0,100], [100,100], [200,0], [100,-100], [100,0]]; -// stroke(path, width=20, endcaps=true, closed=true); -// Example(2D): Fancy Arrow Endcaps -// path = [[0,100], [100,100], [200,0], [100,-100], [100,0]]; -// stroke(path, width=10, endcaps="arrow2"); -// Example(2D): Modified Fancy Arrow Endcaps -// path = [[0,100], [100,100], [200,0], [100,-100], [100,0]]; -// stroke(path, width=10, endcaps="arrow2", endcap_width=6, endcap_length=3, endcap_extent=2); -// Example(2D): Mixed Endcaps -// path = [[0,100], [100,100], [200,0], [100,-100], [100,0]]; -// stroke(path, width=10, endcap1="tail2", endcap2="arrow2"); -// Example(2D): Plotting Points -// path = [for (a=[0:30:360]) [a-180, 60*sin(a)]]; -// stroke(path, width=3, joints="diamond", endcaps="arrow2", plot_angle=0, plot_width=5); -// Example(2D): Joints and Endcaps -// path = [for (a=[0:30:360]) [a-180, 60*sin(a)]]; -// stroke(path, width=3, joints="dot", endcaps="arrow2", joint_angle=0); -// Example(2D): Custom Endcap Shapes -// path = [[0,100], [100,100], [200,0], [100,-100], [100,0]]; -// 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]]; -// stroke(path, width=10, trim=3.5, endcaps=arrow); -// Example(2D): Variable Line Width -// path = circle(d=50,$fn=18); -// widths = [for (i=idx(path)) 10*i/len(path)+2]; -// stroke(path,width=widths,$fa=1,$fs=1); -// Example: 3D Path with Endcaps -// path = rot([15,30,0], p=path3d(pentagon(d=50))); -// stroke(path, width=2, endcaps="arrow2", $fn=18); -// Example: 3D Path with Flat Endcaps -// path = rot([15,30,0], p=path3d(pentagon(d=50))); -// stroke(path, width=2, endcaps="arrow2", endcap_angle=0, $fn=18); -// Example: 3D Path with Mixed Endcaps -// path = rot([15,30,0], p=path3d(pentagon(d=50))); -// stroke(path, width=2, endcap1="arrow2", endcap2="tail", endcap_angle2=0, $fn=18); -// Example: 3D Path with Joints and Endcaps -// path = [for (i=[0:10:360]) [(i-180)/2,20*cos(3*i),20*sin(3*i)]]; -// stroke(path, width=2, joints="dot", endcap1="round", endcap2="arrow2", joint_width=2.0, endcap_width2=3, $fn=18); -function stroke( - path, width=1, closed=false, - endcaps, endcap1, endcap2, joints, plots, - endcap_width, endcap_width1, endcap_width2, joint_width, plot_width, - endcap_length, endcap_length1, endcap_length2, joint_length, plot_length, - endcap_extent, endcap_extent1, endcap_extent2, joint_extent, plot_extent, - endcap_angle, endcap_angle1, endcap_angle2, joint_angle, plot_angle, - trim, trim1, trim2, - convexity=10, hull=true -) = no_function("stroke"); -module stroke( - path, width=1, closed=false, - endcaps, endcap1, endcap2, joints, plots, - endcap_width, endcap_width1, endcap_width2, joint_width, plot_width, - endcap_length, endcap_length1, endcap_length2, joint_length, plot_length, - endcap_extent, endcap_extent1, endcap_extent2, joint_extent, plot_extent, - endcap_angle, endcap_angle1, endcap_angle2, joint_angle, plot_angle, - trim, trim1, trim2, - convexity=10, hull=true -) { - function _shape_defaults(cap) = - cap==undef? [1.00, 0.00, 0.00] : - cap==false? [1.00, 0.00, 0.00] : - cap==true? [1.00, 1.00, 0.00] : - cap=="butt"? [1.00, 0.00, 0.00] : - cap=="round"? [1.00, 1.00, 0.00] : - cap=="chisel"? [1.00, 1.00, 0.00] : - cap=="square"? [1.00, 1.00, 0.00] : - cap=="block"? [3.00, 1.00, 0.00] : - cap=="diamond"? [3.50, 1.00, 0.00] : - cap=="dot"? [3.00, 1.00, 0.00] : - cap=="x"? [3.50, 0.40, 0.00] : - cap=="cross"? [4.50, 0.22, 0.00] : - cap=="line"? [4.50, 0.22, 0.00] : - cap=="arrow"? [3.50, 0.40, 0.50] : - cap=="arrow2"? [3.50, 1.00, 0.14] : - cap=="tail"? [3.50, 0.47, 0.50] : - cap=="tail2"? [3.50, 0.28, 0.50] : - is_path(cap)? [0.00, 0.00, 0.00] : - assert(false, str("Invalid cap or joint: ",cap)); - - function _shape_path(cap,linewidth,w,l,l2) = ( - (cap=="butt" || cap==false || cap==undef)? [] : - (cap=="round" || cap==true)? scale([w,l], p=circle(d=1, $fn=max(8, segs(w/2)))) : - cap=="chisel"? scale([w,l], p=circle(d=1,$fn=4)) : - cap=="diamond"? circle(d=w,$fn=4) : - cap=="square"? scale([w,l], p=square(1,center=true)) : - cap=="block"? scale([w,l], p=square(1,center=true)) : - cap=="dot"? circle(d=w, $fn=max(12, segs(w*3/2))) : - 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]]) ] : - cap=="cross"? [for (a=[0:90:270]) each rot(a,p=[[l,w]/2, [-l,w]/2, [-l,l]/2]) ] : - cap=="line"? scale([w,l], p=square(1,center=true)) : - cap=="arrow"? [[0,0], [w/2,-l2], [w/2,-l2-l], [0,-l], [-w/2,-l2-l], [-w/2,-l2]] : - cap=="arrow2"? [[0,0], [w/2,-l2-l], [0,-l], [-w/2,-l2-l]] : - cap=="tail"? [[0,0], [w/2,l2], [w/2,l2-l], [0,-l], [-w/2,l2-l], [-w/2,l2]] : - cap=="tail2"? [[w/2,0], [w/2,-l], [0,-l-l2], [-w/2,-l], [-w/2,0]] : - is_path(cap)? cap : - assert(false, str("Invalid endcap: ",cap)) - ) * linewidth; - - assert(is_bool(closed)); - assert(is_list(path)); - if (len(path) > 1) { - assert(is_path(path,[2,3]), "The path argument must be a list of 2D or 3D points."); - } - path = deduplicate( closed? close_path(path) : path ); - - assert(is_num(width) || (is_vector(width) && len(width)==len(path))); - width = is_num(width)? [for (x=path) width] : width; - assert(all([for (w=width) w>0])); - - endcap1 = first_defined([endcap1, endcaps, if(!closed) plots, "round"]); - endcap2 = first_defined([endcap2, endcaps, plots, "round"]); - joints = first_defined([joints, plots, "round"]); - assert(is_bool(endcap1) || is_string(endcap1) || is_path(endcap1)); - assert(is_bool(endcap2) || is_string(endcap2) || is_path(endcap2)); - assert(is_bool(joints) || is_string(joints) || is_path(joints)); - - endcap1_dflts = _shape_defaults(endcap1); - endcap2_dflts = _shape_defaults(endcap2); - joint_dflts = _shape_defaults(joints); - - endcap_width1 = first_defined([endcap_width1, endcap_width, plot_width, endcap1_dflts[0]]); - endcap_width2 = first_defined([endcap_width2, endcap_width, plot_width, endcap2_dflts[0]]); - joint_width = first_defined([joint_width, plot_width, joint_dflts[0]]); - assert(is_num(endcap_width1)); - assert(is_num(endcap_width2)); - assert(is_num(joint_width)); - - endcap_length1 = first_defined([endcap_length1, endcap_length, plot_length, endcap1_dflts[1]*endcap_width1]); - endcap_length2 = first_defined([endcap_length2, endcap_length, plot_length, endcap2_dflts[1]*endcap_width2]); - joint_length = first_defined([joint_length, plot_length, joint_dflts[1]*joint_width]); - assert(is_num(endcap_length1)); - assert(is_num(endcap_length2)); - assert(is_num(joint_length)); - - endcap_extent1 = first_defined([endcap_extent1, endcap_extent, plot_extent, endcap1_dflts[2]*endcap_width1]); - endcap_extent2 = first_defined([endcap_extent2, endcap_extent, plot_extent, endcap2_dflts[2]*endcap_width2]); - joint_extent = first_defined([joint_extent, plot_extent, joint_dflts[2]*joint_width]); - assert(is_num(endcap_extent1)); - assert(is_num(endcap_extent2)); - assert(is_num(joint_extent)); - - endcap_angle1 = first_defined([endcap_angle1, endcap_angle, plot_angle]); - endcap_angle2 = first_defined([endcap_angle2, endcap_angle, plot_angle]); - joint_angle = first_defined([joint_angle, plot_angle]); - assert(is_undef(endcap_angle1)||is_num(endcap_angle1)); - assert(is_undef(endcap_angle2)||is_num(endcap_angle2)); - assert(is_undef(joint_angle)||is_num(joint_angle)); - - endcap_shape1 = _shape_path(endcap1, width[0], endcap_width1, endcap_length1, endcap_extent1); - endcap_shape2 = _shape_path(endcap2, last(width), endcap_width2, endcap_length2, endcap_extent2); - - trim1 = width[0] * first_defined([ - trim1, trim, - (endcap1=="arrow")? endcap_length1-0.01 : - (endcap1=="arrow2")? endcap_length1*3/4 : - 0 - ]); - assert(is_num(trim1)); - - trim2 = last(width) * first_defined([ - trim2, trim, - (endcap2=="arrow")? endcap_length2-0.01 : - (endcap2=="arrow2")? endcap_length2*3/4 : - 0 - ]); - assert(is_num(trim2)); - - if (len(path) == 1) { - if (len(path[0]) == 2) { - translate(path[0]) circle(d=width[0]); - } else { - translate(path[0]) sphere(d=width[0]); - } - } else { - spos = path_pos_from_start(path,trim1,closed=false); - epos = path_pos_from_end(path,trim2,closed=false); - path2 = path_subselect(path, spos[0], spos[1], epos[0], epos[1]); - widths = concat( - [lerp(width[spos[0]], width[(spos[0]+1)%len(width)], spos[1])], - [for (i = [spos[0]+1:1:epos[0]]) width[i]], - [lerp(width[epos[0]], width[(epos[0]+1)%len(width)], epos[1])] - ); - - start_vec = path[0] - path[1]; - end_vec = last(path) - select(path,-2); - - if (len(path[0]) == 2) { - // Straight segments - for (i = idx(path2,e=-2)) { - seg = select(path2,i,i+1); - delt = seg[1] - seg[0]; - translate(seg[0]) { - rot(from=BACK,to=delt) { - trapezoid(w1=widths[i], w2=widths[i+1], h=norm(delt), anchor=FRONT); - } - } - } - - // Joints - for (i = [1:1:len(path2)-2]) { - $fn = quantup(segs(widths[i]/2),4); - translate(path2[i]) { - if (joints != undef) { - joint_shape = _shape_path( - joints, width[i], - joint_width, - joint_length, - joint_extent - ); - v1 = unit(path2[i] - path2[i-1]); - v2 = unit(path2[i+1] - path2[i]); - vec = unit((v1+v2)/2); - mat = is_undef(joint_angle) - ? rot(from=BACK,to=v1) - : zrot(joint_angle); - multmatrix(mat) polygon(joint_shape); - } else if (hull) { - hull() { - rot(from=BACK, to=path2[i]-path2[i-1]) - circle(d=widths[i]); - rot(from=BACK, to=path2[i+1]-path2[i]) - circle(d=widths[i]); - } - } else { - rot(from=BACK, to=path2[i]-path2[i-1]) - circle(d=widths[i]); - rot(from=BACK, to=path2[i+1]-path2[i]) - circle(d=widths[i]); - } - } - } - - // Endcap1 - translate(path[0]) { - mat = is_undef(endcap_angle1)? rot(from=BACK,to=start_vec) : - zrot(endcap_angle1); - multmatrix(mat) polygon(endcap_shape1); - } - - // Endcap2 - translate(last(path)) { - mat = is_undef(endcap_angle2)? rot(from=BACK,to=end_vec) : - zrot(endcap_angle2); - multmatrix(mat) polygon(endcap_shape2); - } - } else { - quatsums = q_cumulative([ - for (i = idx(path2,e=-2)) let( - vec1 = i==0? UP : unit(path2[i]-path2[i-1], UP), - vec2 = unit(path2[i+1]-path2[i], UP), - axis = vector_axis(vec1,vec2), - ang = vector_angle(vec1,vec2) - ) quat(axis,ang) - ]); - rotmats = [for (q=quatsums) q_matrix4(q)]; - sides = [ - for (i = idx(path2,e=-2)) - quantup(segs(max(widths[i],widths[i+1])/2),4) - ]; - - // Straight segments - for (i = idx(path2,e=-2)) { - dist = norm(path2[i+1] - path2[i]); - w1 = widths[i]/2; - w2 = widths[i+1]/2; - $fn = sides[i]; - translate(path2[i]) { - multmatrix(rotmats[i]) { - cylinder(r1=w1, r2=w2, h=dist, center=false); - } - } - } - - // Joints - for (i = [1:1:len(path2)-2]) { - $fn = sides[i]; - translate(path2[i]) { - if (joints != undef) { - joint_shape = _shape_path( - joints, width[i], - joint_width, - joint_length, - joint_extent - ); - multmatrix(rotmats[i] * xrot(180)) { - $fn = sides[i]; - if (is_undef(joint_angle)) { - rotate_extrude(convexity=convexity) { - right_half(planar=true) { - polygon(joint_shape); - } - } - } else { - rotate([90,0,joint_angle]) { - linear_extrude(height=max(widths[i],0.001), center=true, convexity=convexity) { - polygon(joint_shape); - } - } - } - } - } else if (hull) { - hull(){ - multmatrix(rotmats[i]) { - sphere(d=widths[i],style="aligned"); - } - multmatrix(rotmats[i-1]) { - sphere(d=widths[i],style="aligned"); - } - } - } else { - multmatrix(rotmats[i]) { - sphere(d=widths[i],style="aligned"); - } - multmatrix(rotmats[i-1]) { - sphere(d=widths[i],style="aligned"); - } - } - } - } - - // Endcap1 - translate(path[0]) { - multmatrix(rotmats[0] * xrot(180)) { - $fn = sides[0]; - if (is_undef(endcap_angle1)) { - rotate_extrude(convexity=convexity) { - right_half(planar=true) { - polygon(endcap_shape1); - } - } - } else { - rotate([90,0,endcap_angle1]) { - linear_extrude(height=max(widths[0],0.001), center=true, convexity=convexity) { - polygon(endcap_shape1); - } - } - } - } - } - - // Endcap2 - translate(last(path)) { - multmatrix(last(rotmats)) { - $fn = last(sides); - if (is_undef(endcap_angle2)) { - rotate_extrude(convexity=convexity) { - right_half(planar=true) { - polygon(endcap_shape2); - } - } - } else { - rotate([90,0,endcap_angle2]) { - linear_extrude(height=max(last(widths),0.001), center=true, convexity=convexity) { - polygon(endcap_shape2); - } - } - } - } - } - } - } -} - - -// Function&Module: dashed_stroke() -// Usage: As a Module -// dashed_stroke(path, dashpat, [closed=]); -// Usage: As a Function -// dashes = dashed_stroke(path, dashpat, width=, [closed=]); -// Topics: Paths, Drawing Tools -// See Also: stroke(), path_cut() -// Description: -// Given a path and a dash pattern, creates a dashed line that follows that -// path with the given dash pattern. -// - When called as a function, returns a list of dash sub-paths. -// - When called as a module, draws all those subpaths using `stroke()`. -// Arguments: -// path = The path to subdivide into dashes. -// dashpat = A list of alternating dash lengths and space lengths for the dash pattern. This will be scaled by the width of the line. -// --- -// width = The width of the dashed line to draw. Module only. Default: 1 -// closed = If true, treat path as a closed polygon. Default: false -// Example(2D): Open Path -// path = [for (a=[-180:10:180]) [a/3,20*sin(a)]]; -// dashed_stroke(path, [3,2], width=1); -// Example(2D): Closed Polygon -// path = circle(d=100,$fn=72); -// dashpat = [10,2,3,2,3,2]; -// dashed_stroke(path, dashpat, width=1, closed=true); -// Example(FlatSpin,VPD=250): 3D Dashed Path -// path = [for (a=[-180:5:180]) [a/3, 20*cos(3*a), 20*sin(3*a)]]; -// dashed_stroke(path, [3,2], width=1); -function dashed_stroke(path, dashpat=[3,3], closed=false) = - let( - path = closed? close_path(path) : path, - dashpat = len(dashpat)%2==0? dashpat : concat(dashpat,[0]), - plen = path_length(path), - dlen = sum(dashpat), - doff = cumsum(dashpat), - reps = floor(plen / dlen), - step = plen / reps, - cuts = [ - for (i=[0:1:reps-1], off=doff) - let (st=i*step, x=st+off) - if (x>0 && x=2), "Number of points must be an integer 2 or larger") - // First try for 2D arc specified by width and thickness - is_def(width) && is_def(thickness)? ( - assert(!any_defined([r,cp,points]) && !any([cw,ccw,long]),"Conflicting or invalid parameters to arc") - assert(width>0, "Width must be postive") - assert(thickness>0, "Thickness must be positive") - arc(N,points=[[width/2,0], [0,thickness], [-width/2,0]],wedge=wedge) - ) : is_def(angle)? ( - let( - parmok = !any_defined([points,width,thickness]) && - ((is_vector(angle,2) && is_undef(start)) || is_num(angle)) - ) - assert(parmok,"Invalid parameters in arc") - let( - cp = first_defined([cp,[0,0]]), - start = is_def(start)? start : is_vector(angle) ? angle[0] : 0, - angle = is_vector(angle)? angle[1]-angle[0] : angle, - r = get_radius(r=r, d=d) - ) - assert(is_vector(cp,2),"Centerpoint must be a 2d vector") - assert(angle!=0, "Arc has zero length") - assert(is_def(r) && r>0, "Arc radius invalid") - let( - N = is_def(N) ? N : max(3, ceil(segs(r)*abs(angle)/360)), - arcpoints = [for(i=[0:N-1]) let(theta = start + i*angle/(N-1)) r*[cos(theta),sin(theta)]+cp], - extra = wedge? [cp] : [] - ) - concat(extra,arcpoints) - ) : - assert(is_path(points,[2,3]),"Point list is invalid") - // Arc is 3D, so transform points to 2D and make a recursive call, then remap back to 3D - len(points[0])==3? ( - assert(!(cw || ccw), "(Counter)clockwise isn't meaningful in 3d, so `cw` and `ccw` must be false") - assert(is_undef(cp) || is_vector(cp,3),"points are 3d so cp must be 3d") - let( - plane = [is_def(cp) ? cp : points[2], points[0], points[1]], - center2d = is_def(cp) ? project_plane(plane,cp) : undef, - points2d = project_plane(plane, points) - ) - lift_plane(plane,arc(N,cp=center2d,points=points2d,wedge=wedge,long=long)) - ) : is_def(cp)? ( - // Arc defined by center plus two points, will have radius defined by center and points[0] - // and extent defined by direction of point[1] from the center - assert(is_vector(cp,2), "Centerpoint must be a 2d vector") - assert(len(points)==2, "When pointlist has length 3 centerpoint is not allowed") - assert(points[0]!=points[1], "Arc endpoints are equal") - assert(cp!=points[0]&&cp!=points[1], "Centerpoint equals an arc endpoint") - assert(count_true([long,cw,ccw])<=1, str("Only one of `long`, `cw` and `ccw` can be true",cw,ccw,long)) - let( - angle = vector_angle(points[0], cp, points[1]), - v1 = points[0]-cp, - v2 = points[1]-cp, - prelim_dir = sign(det2([v1,v2])), // z component of cross product - dir = prelim_dir != 0 - ? prelim_dir - : assert(cw || ccw, "Collinear inputs don't define a unique arc") - 1, - r=norm(v1), - final_angle = long || (ccw && dir<0) || (cw && dir>0) ? -dir*(360-angle) : dir*angle - ) - arc(N,cp=cp,r=r,start=atan2(v1.y,v1.x),angle=final_angle,wedge=wedge) - ) : ( - // Final case is arc passing through three points, starting at point[0] and ending at point[3] - let(col = is_collinear(points[0],points[1],points[2])) - assert(!col, "Collinear inputs do not define an arc") - let( - cp = line_intersection(_normal_segment(points[0],points[1]),_normal_segment(points[1],points[2])), - // select order to be counterclockwise - dir = det2([points[1]-points[0],points[2]-points[1]]) > 0, - points = dir? select(points,[0,2]) : select(points,[2,0]), - r = norm(points[0]-cp), - theta_start = atan2(points[0].y-cp.y, points[0].x-cp.x), - theta_end = atan2(points[1].y-cp.y, points[1].x-cp.x), - angle = posmod(theta_end-theta_start, 360), - arcpts = arc(N,cp=cp,r=r,start=theta_start,angle=angle,wedge=wedge) - ) - dir ? arcpts : reverse(arcpts) - ); - - -module arc(N, r, angle, d, cp, points, width, thickness, start, wedge=false) -{ - path = arc(N=N, r=r, angle=angle, d=d, cp=cp, points=points, width=width, thickness=thickness, start=start, wedge=wedge); - polygon(path); -} - - -function _normal_segment(p1,p2) = - let(center = (p1+p2)/2) - [center, center + norm(p1-p2)/2 * line_normal(p1,p2)]; - - -// Function: turtle() -// Usage: -// turtle(commands, [state], [full_state=], [repeat=]) -// Topics: Shapes (2D), Path Generators (2D), Mini-Language -// See Also: turtle3d() -// Description: -// Use a sequence of turtle graphics commands to generate a path. The parameter `commands` is a list of -// turtle commands and optional parameters for each command. The turtle state has a position, movement direction, -// movement distance, and default turn angle. If you do not give `state` as input then the turtle starts at the -// origin, pointed along the positive x axis with a movement distance of 1. By default, `turtle` returns just -// the computed turtle path. If you set `full_state` to true then it instead returns the full turtle state. -// You can invoke `turtle` again with this full state to continue the turtle path where you left off. -// . -// 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, -// and the current arcsteps setting. -// . -// Commands | Arguments | What it does -// ------------ | ------------------ | ------------------------------- -// "move" | [dist] | Move turtle scale*dist units in the turtle direction. Default dist=1. -// "xmove" | [dist] | Move turtle scale*dist units in the x direction. Default dist=1. Does not change turtle direction. -// "ymove" | [dist] | Move turtle scale*dist units in the y direction. Default dist=1. Does not change turtle direction. -// "xymove" | vector | Move turtle by the specified vector. Does not change turtle direction. -// "untilx" | xtarget | Move turtle in turtle direction until x==xtarget. Produces an error if xtarget is not reachable. -// "untily" | ytarget | Move turtle in turtle direction until y==ytarget. Produces an error if xtarget is not reachable. -// "jump" | point | Move the turtle to the specified point -// "xjump" | x | Move the turtle's x position to the specified value -// "yjump | y | Move the turtle's y position to the specified value -// "turn" | [angle] | Turn turtle direction by specified angle, or the turtle's default turn angle. The default angle starts at 90. -// "left" | [angle] | Same as "turn" -// "right" | [angle] | Same as "turn", -angle -// "angle" | angle | Set the default turn angle. -// "setdir" | dir | Set turtle direction. The parameter `dir` can be an angle or a vector. -// "length" | length | Change the turtle move distance to `length` -// "scale" | factor | Multiply turtle move distance by `factor` -// "addlength" | length | Add `length` to the turtle move distance -// "repeat" | count, commands | Repeats a list of commands `count` times. -// "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. -// "arcright" | radius, [angle] | Draw an arc from the current position toward the right at the specified radius and angle -// "arcleftto" | radius, angle | Draw an arc at the given radius turning toward the left until reaching the specified absolute angle. -// "arcrightto" | radius, angle | Draw an arc at the given radius turning toward the right until reaching the specified absolute angle. -// "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. -// -// Arguments: -// commands = List of turtle commands -// state = Starting turtle state (from previous call) or starting point. Default: start at the origin, pointing right. -// --- -// full_state = If true return the full turtle state for continuing the path in subsequent turtle calls. Default: false -// repeat = Number of times to repeat the command list. Default: 1 -// -// Example(2D): Simple rectangle -// path = turtle(["xmove",3, "ymove", "xmove",-3, "ymove",-1]); -// stroke(path,width=.1); -// Example(2D): Pentagon -// path=turtle(["angle",360/5,"move","turn","move","turn","move","turn","move"]); -// stroke(path,width=.1,closed=true); -// Example(2D): Pentagon using the repeat argument -// path=turtle(["move","turn",360/5],repeat=5); -// stroke(path,width=.1,closed=true); -// Example(2D): Pentagon using the repeat turtle command, setting the turn angle -// path=turtle(["angle",360/5,"repeat",5,["move","turn"]]); -// stroke(path,width=.1,closed=true); -// Example(2D): Pentagram -// path = turtle(["move","left",144], repeat=4); -// stroke(path,width=.05,closed=true); -// Example(2D): Sawtooth path -// path = turtle([ -// "turn", 55, -// "untily", 2, -// "turn", -55-90, -// "untily", 0, -// "turn", 55+90, -// "untily", 2.5, -// "turn", -55-90, -// "untily", 0, -// "turn", 55+90, -// "untily", 3, -// "turn", -55-90, -// "untily", 0 -// ]); -// stroke(path, width=.1); -// Example(2D): Simpler way to draw the sawtooth. The direction of the turtle is preserved when executing "yjump". -// path = turtle([ -// "turn", 55, -// "untily", 2, -// "yjump", 0, -// "untily", 2.5, -// "yjump", 0, -// "untily", 3, -// "yjump", 0, -// ]); -// stroke(path, width=.1); -// Example(2DMed): square spiral -// path = turtle(["move","left","addlength",1],repeat=50); -// stroke(path,width=.2); -// Example(2DMed): pentagonal spiral -// path = turtle(["move","left",360/5,"addlength",1],repeat=50); -// stroke(path,width=.2); -// Example(2DMed): yet another spiral, without using `repeat` -// path = turtle(concat(["angle",71],flatten(repeat(["move","left","addlength",1],50)))); -// stroke(path,width=.2); -// Example(2DMed): The previous spiral grows linearly and eventually intersects itself. This one grows geometrically and does not. -// path = turtle(["move","left",71,"scale",1.05],repeat=50); -// stroke(path,width=.05); -// Example(2D): Koch Snowflake -// function koch_unit(depth) = -// depth==0 ? ["move"] : -// concat( -// koch_unit(depth-1), -// ["right"], -// koch_unit(depth-1), -// ["left","left"], -// koch_unit(depth-1), -// ["right"], -// koch_unit(depth-1) -// ); -// koch=concat(["angle",60,"repeat",3],[concat(koch_unit(3),["left","left"])]); -// polygon(turtle(koch)); -module turtle(commands, state=[[[0,0]],[1,0],90,0], full_state=false, repeat=1) {no_module();} -function turtle(commands, state=[[[0,0]],[1,0],90,0], full_state=false, repeat=1) = - let( state = is_vector(state) ? [[state],[1,0],90,0] : state ) - repeat == 1? - _turtle(commands,state,full_state) : - _turtle_repeat(commands, state, full_state, repeat); - -function _turtle_repeat(commands, state, full_state, repeat) = - repeat==1? - _turtle(commands,state,full_state) : - _turtle_repeat(commands, _turtle(commands, state, true), full_state, repeat-1); - -function _turtle_command_len(commands, index) = - let( one_or_two_arg = ["arcleft","arcright", "arcleftto", "arcrightto"] ) - commands[index] == "repeat"? 3 : // Repeat command requires 2 args - // For these, the first arg is required, second arg is present if it is not a string - in_list(commands[index], one_or_two_arg) && len(commands)>index+2 && !is_string(commands[index+2]) ? 3 : - is_string(commands[index+1])? 1 : // If 2nd item is a string it's must be a new command - 2; // Otherwise we have command and arg - -function _turtle(commands, state, full_state, index=0) = - index < len(commands) ? - _turtle(commands, - _turtle_command(commands[index],commands[index+1],commands[index+2],state,index), - full_state, - index+_turtle_command_len(commands,index) - ) : - ( full_state ? state : state[0] ); - -// Turtle state: state = [path, step_vector, default angle, default arcsteps] - -function _turtle_command(command, parm, parm2, state, index) = - command == "repeat"? - assert(is_num(parm),str("\"repeat\" command requires a numeric repeat count at index ",index)) - assert(is_list(parm2),str("\"repeat\" command requires a command list parameter at index ",index)) - _turtle_repeat(parm2, state, true, parm) : - let( - path = 0, - step=1, - angle=2, - arcsteps=3, - parm = !is_string(parm) ? parm : undef, - parm2 = !is_string(parm2) ? parm2 : undef, - needvec = ["jump", "xymove"], - neednum = ["untilx","untily","xjump","yjump","angle","length","scale","addlength"], - needeither = ["setdir"], - chvec = !in_list(command,needvec) || is_vector(parm,2), - chnum = !in_list(command,neednum) || is_num(parm), - vec_or_num = !in_list(command,needeither) || (is_num(parm) || is_vector(parm,2)), - lastpt = last(state[path]) - ) - assert(chvec,str("\"",command,"\" requires a vector parameter at index ",index)) - assert(chnum,str("\"",command,"\" requires a numeric parameter at index ",index)) - assert(vec_or_num,str("\"",command,"\" requires a vector or numeric parameter at index ",index)) - - command=="move" ? list_set(state, path, concat(state[path],[default(parm,1)*state[step]+lastpt])) : - command=="untilx" ? ( - let( - int = line_intersection([lastpt,lastpt+state[step]], [[parm,0],[parm,1]]), - xgood = sign(state[step].x) == sign(int.x-lastpt.x) - ) - assert(xgood,str("\"untilx\" never reaches desired goal at index ",index)) - list_set(state,path,concat(state[path],[int])) - ) : - command=="untily" ? ( - let( - int = line_intersection([lastpt,lastpt+state[step]], [[0,parm],[1,parm]]), - ygood = is_def(int) && sign(state[step].y) == sign(int.y-lastpt.y) - ) - assert(ygood,str("\"untily\" never reaches desired goal at index ",index)) - list_set(state,path,concat(state[path],[int])) - ) : - command=="xmove" ? list_set(state, path, concat(state[path],[default(parm,1)*norm(state[step])*[1,0]+lastpt])): - command=="ymove" ? list_set(state, path, concat(state[path],[default(parm,1)*norm(state[step])*[0,1]+lastpt])): - command=="xymove" ? list_set(state, path, concat(state[path], [lastpt+parm])): - command=="jump" ? list_set(state, path, concat(state[path],[parm])): - command=="xjump" ? list_set(state, path, concat(state[path],[[parm,lastpt.y]])): - command=="yjump" ? list_set(state, path, concat(state[path],[[lastpt.x,parm]])): - command=="turn" || command=="left" ? list_set(state, step, rot(default(parm,state[angle]),p=state[step],planar=true)) : - command=="right" ? list_set(state, step, rot(-default(parm,state[angle]),p=state[step],planar=true)) : - command=="angle" ? list_set(state, angle, parm) : - command=="setdir" ? ( - is_vector(parm) ? - list_set(state, step, norm(state[step]) * unit(parm)) : - list_set(state, step, norm(state[step]) * [cos(parm),sin(parm)]) - ) : - command=="length" ? list_set(state, step, parm*unit(state[step])) : - command=="scale" ? list_set(state, step, parm*state[step]) : - command=="addlength" ? list_set(state, step, state[step]+unit(state[step])*parm) : - command=="arcsteps" ? list_set(state, arcsteps, parm) : - command=="arcleft" || command=="arcright" ? - assert(is_num(parm),str("\"",command,"\" command requires a numeric radius value at index ",index)) - let( - myangle = default(parm2,state[angle]), - lrsign = command=="arcleft" ? 1 : -1, - radius = parm*sign(myangle), - center = lastpt + lrsign*radius*line_normal([0,0],state[step]), - steps = state[arcsteps]==0 ? segs(abs(radius)) : state[arcsteps], - arcpath = myangle == 0 || radius == 0 ? [] : arc( - steps, - points = [ - lastpt, - rot(cp=center, p=lastpt, a=sign(parm)*lrsign*myangle/2), - rot(cp=center, p=lastpt, a=sign(parm)*lrsign*myangle) - ] - ) - ) - list_set( - state, [path,step], [ - concat(state[path], list_tail(arcpath)), - rot(lrsign * myangle,p=state[step],planar=true) - ] - ) : - command=="arcleftto" || command=="arcrightto" ? - assert(is_num(parm),str("\"",command,"\" command requires a numeric radius value at index ",index)) - assert(is_num(parm2),str("\"",command,"\" command requires a numeric angle value at index ",index)) - let( - radius = parm, - lrsign = command=="arcleftto" ? 1 : -1, - center = lastpt + lrsign*radius*line_normal([0,0],state[step]), - steps = state[arcsteps]==0 ? segs(abs(radius)) : state[arcsteps], - start_angle = posmod(atan2(state[step].y, state[step].x),360), - end_angle = posmod(parm2,360), - delta_angle = -start_angle + (lrsign * end_angle < lrsign*start_angle ? end_angle+lrsign*360 : end_angle), - arcpath = delta_angle == 0 || radius==0 ? [] : arc( - steps, - points = [ - lastpt, - rot(cp=center, p=lastpt, a=sign(radius)*delta_angle/2), - rot(cp=center, p=lastpt, a=sign(radius)*delta_angle) - ] - ) - ) - list_set( - state, [path,step], [ - concat(state[path], list_tail(arcpath)), - rot(delta_angle,p=state[step],planar=true) - ] - ) : - assert(false,str("Unknown turtle command \"",command,"\" at index",index)) - []; - - // Section: 2D Primitives diff --git a/shapes.scad b/shapes3d.scad similarity index 100% rename from shapes.scad rename to shapes3d.scad diff --git a/std.scad b/std.scad index 39d4544..c9b7f53 100644 --- a/std.scad +++ b/std.scad @@ -15,8 +15,9 @@ include include include include -include +include include +include include include include diff --git a/tests/test_shapes2d.scad b/tests/test_shapes2d.scad index 4c69e25..d535e71 100644 --- a/tests/test_shapes2d.scad +++ b/tests/test_shapes2d.scad @@ -1,55 +1,6 @@ include <../std.scad> -module test_turtle() { - assert_approx( - turtle([ - "move", 10, - "ymove", 5, - "xmove", 5, - "xymove", [10,15], - "left", 135, - "untilx", 0, - "turn", 90, - "untily", 0, - "right", 135, - "arcsteps", 5, - "arcright", 15, 30, - "arcleft", 15, 30, - "arcsteps", 0, - "arcrightto", 15, 90, - "arcleftto", 15, 180, - "jump", [10,10], - "xjump", 15, - "yjump", 15, - "angle", 30, - "length", 15, - "right", - "move", - "scale", 2, - "left", - "move", - "addlength", 5, - "repeat", 3, ["move"], - ], $fn=24), - [[0,0],[10,0],[10,5],[15,5],[25,20],[-3.5527136788e-15,45],[-45,0],[-44.8716729206,1.9578928833],[-44.4888873943,3.88228567654],[-43.8581929877,5.74025148548],[-42.9903810568,7.5],[-42.1225691259,9.25974851452],[-41.4918747192,11.1177143235],[-41.1090891929,13.0421071167],[-40.9807621135,15],[-41.0157305757,16.0236362005],[-41.120472923,17.0424997364],[-41.2945007983,18.0518401958],[-41.5370028033,19.0469515674],[-41.8468482818,20.0231941826],[-42.222592591,20.9760163477],[-42.6624838375,21.900975566],[-43.1644710453,22.7937592505],[-43.7262137184,23.6502048317],[-44.345092753,24.4663191649],[-45.0182226494,25.2382971483],[-45.7424649653,25.9625394642],[-46.5144429486,26.6356693606],[-47.3305572818,27.2545483952],[-48.187002863,27.8162910682],[-49.0797865476,28.318278276],[-50.0047457658,28.7581695226],[-50.957567931,29.1339138318],[-51.9338105462,29.4437593102],[-52.9289219177,29.6862613152],[-53.9382623771,29.8602891905],[-54.9571259131,29.9650315379],[-55.9807621135,30],[10,10],[15,10],[15,15],[2.00961894323,22.5],[-27.9903810568,22.5],[-62.9903810568,22.5],[-97.9903810568,22.5],[-132.990381057,22.5]] - ); -} -test_turtle(); - - -module test_arc() { - assert_approx(arc(N=8, d=100, angle=135, cp=[10,10]), [[60,10],[57.1941665154,26.5139530978],[49.0915741234,41.1744900929],[36.6016038258,52.3362099614],[21.1260466978,58.7463956091],[4.40177619483,59.6856104947],[-11.6941869559,55.0484433951],[-25.3553390593,45.3553390593]]); - assert_approx(arc(N=8, d=100, angle=135, cp=[10,10],endpoint=false), [[60,10],[57.8470167866,24.5142338627],[51.5734806151,37.778511651],[41.7196642082,48.6505226681],[29.1341716183,56.1939766256],[14.9008570165,59.7592363336],[0.245483899194,59.0392640202],[-13.5698368413,54.0960632174]]); - assert_approx(arc(N=8, d=100, angle=[45,225], cp=[10,10]), [[45.3553390593,45.3553390593],[26.5139530978,57.1941665154],[4.40177619483,59.6856104947],[-16.6016038258,52.3362099614],[-32.3362099614,36.6016038258],[-39.6856104947,15.5982238052],[-37.1941665154,-6.51395309776],[-25.3553390593,-25.3553390593]]); - assert_approx(arc(N=8, d=100, start=45, angle=135, cp=[10,10]), [[45.3553390593,45.3553390593],[31.6941869559,55.0484433951],[15.5982238052,59.6856104947],[-1.12604669782,58.7463956091],[-16.6016038258,52.3362099614],[-29.0915741234,41.1744900929],[-37.1941665154,26.5139530978],[-40,10]]); - assert_approx(arc(N=8, d=100, start=45, angle=-90, cp=[10,10]), [[45.3553390593,45.3553390593],[52.3362099614,36.6016038258],[57.1941665154,26.5139530978],[59.6856104947,15.5982238052],[59.6856104947,4.40177619483],[57.1941665154,-6.51395309776],[52.3362099614,-16.6016038258],[45.3553390593,-25.3553390593]]); - assert_approx(arc(N=8, width=100, thickness=30), [[50,-3.5527136788e-15],[39.5300788555,13.9348601124],[25.3202618476,24.0284558904],[8.71492362453,29.3258437015],[-8.71492362453,29.3258437015],[-25.3202618476,24.0284558904],[-39.5300788555,13.9348601124],[-50,-1.42108547152e-14]]); - assert_approx(arc(N=8, cp=[10,10], points=[[45,45],[-25,45]]), [[45,45],[36.3342442379,51.9107096148],[26.3479795075,56.7198412457],[15.5419588213,59.1862449514],[4.45804117867,59.1862449514],[-6.34797950747,56.7198412457],[-16.3342442379,51.9107096148],[-25,45]]); - assert_approx(arc(N=24, cp=[10,10], points=[[45,45],[-25,45]], long=true), [[45,45],[51.3889035257,37.146982612],[56.0464336973,28.1583574081],[58.7777575294,18.4101349813],[59.4686187624,8.31010126292],[58.0901174104,-1.71924090789],[54.6999187001,-11.2583458482],[49.4398408296,-19.9081753929],[42.5299224539,-27.3068913894],[34.2592180667,-33.1449920477],[24.9737063235,-37.1782589647],[15.0618171232,-39.2379732261],[4.93818287676,-39.2379732261],[-4.97370632349,-37.1782589647],[-14.2592180667,-33.1449920477],[-22.5299224539,-27.3068913894],[-29.4398408296,-19.9081753929],[-34.6999187001,-11.2583458482],[-38.0901174104,-1.71924090789],[-39.4686187624,8.31010126292],[-38.7777575294,18.4101349813],[-36.0464336973,28.1583574081],[-31.3889035257,37.146982612],[-25,45]]); - assert_approx(arc($fn=24, cp=[10,10], points=[[45,45],[-25,45]], long=true), [[45,45],[53.2421021636,34.0856928585],[58.1827254512,21.3324740498],[59.4446596304,7.71403542491],[56.9315576496,-5.72987274525],[50.8352916125,-17.9728253654],[41.6213035891,-28.0800887515],[29.9930697126,-35.2799863457],[16.8383906815,-39.0228152281],[3.16160931847,-39.0228152281],[-9.9930697126,-35.2799863457],[-21.6213035891,-28.0800887515],[-30.8352916125,-17.9728253654],[-36.9315576496,-5.72987274525],[-39.4446596304,7.71403542491],[-38.1827254512,21.3324740498],[-33.2421021636,34.0856928585],[-25,45]]); -} -test_arc(); module test_rect() { diff --git a/tests/test_shapes.scad b/tests/test_shapes3d.scad similarity index 100% rename from tests/test_shapes.scad rename to tests/test_shapes3d.scad diff --git a/triangulation.scad b/triangulation.scad deleted file mode 100644 index 24f69cb..0000000 --- a/triangulation.scad +++ /dev/null @@ -1,194 +0,0 @@ -////////////////////////////////////////////////////////////////////// -// LibFile: triangulation.scad -// Functions to triangulate polyhedron faces. -// Includes: -// include -// include -////////////////////////////////////////////////////////////////////// - - -// Section: Functions - - -// Function: face_normal() -// Description: -// Given an array of vertices (`points`), and a list of indexes into the -// vertex array (`face`), returns the normal vector of the face. -// Arguments: -// points = Array of vertices for the polyhedron. -// face = The face, given as a list of indices into the vertex array `points`. -function face_normal(points, face) = - let(count=len(face)) - unit( - sum( - [ - for(i=[0:1:count-1]) cross( - points[face[(i+1)%count]]-points[face[0]], - points[face[(i+2)%count]]-points[face[(i+1)%count]] - ) - ] - ) - ) -; - - -// Function: find_convex_vertex() -// Description: -// Returns the index of a convex point on the given face. -// Arguments: -// points = Array of vertices for the polyhedron. -// face = The face, given as a list of indices into the vertex array `points`. -// facenorm = The normal vector of the face. -function find_convex_vertex(points, face, facenorm, i=0) = - let(count=len(face), - p0=points[face[i]], - p1=points[face[(i+1)%count]], - p2=points[face[(i+2)%count]] - ) - (len(face)>i)? ( - (cross(p1-p0, p2-p1)*facenorm>0)? (i+1)%count : - find_convex_vertex(points, face, facenorm, i+1) - ) : //This should never happen since there is at least 1 convex vertex. - undef -; - - -// Function: point_in_ear() -// Description: Determine if a point is in a clipable convex ear. -// Arguments: -// points = Array of vertices for the polyhedron. -// face = The face, given as a list of indices into the vertex array `points`. -function point_in_ear(points, face, tests, i=0) = - (iprev[0])? [test, i] : prev - : - [_check_point_in_ear(points[face[i]], tests), i] -; - - -// Internal non-exposed function. -function _check_point_in_ear(point, tests) = - let( - result=[ - (point*tests[0][0])-tests[0][1], - (point*tests[1][0])-tests[1][1], - (point*tests[2][0])-tests[2][1] - ] - ) - (result[0]>0 && result[1]>0 && result[2]>0)? result[0] : -1 -; - - -// Function: normalize_vertex_perimeter() -// Description: Removes the last item in an array if it is the same as the first item. -// Arguments: -// v = The array to normalize. -function normalize_vertex_perimeter(v) = - let(lv = len(v)) - (lv < 2)? v : - (v[lv-1] != v[0])? v : - [for (i=[0:1:lv-2]) v[i]] -; - - -// Function: is_only_noncolinear_vertex() -// Description: -// Given a face in a polyhedron, and a vertex in that face, returns true -// if that vertex is the only non-colinear vertex in the face. -// Arguments: -// points = Array of vertices for the polyhedron. -// facelist = The face, given as a list of indices into the vertex array `points`. -// vertex = The index into `facelist`, of the vertex to test. -function is_only_noncolinear_vertex(points, facelist, vertex) = - let( - face=select(facelist, vertex+1, vertex-1), - count=len(face) - ) - 0==sum( - [ - for(i=[0:1:count-1]) norm( - cross( - points[face[(i+1)%count]]-points[face[0]], - points[face[(i+2)%count]]-points[face[(i+1)%count]] - ) - ) - ] - ) -; - - -// Function: triangulate_face() -// Description: -// Given a face in a polyhedron, subdivides the face into triangular faces. -// Returns an array of faces, where each face is a list of three vertex indices. -// Arguments: -// points = Array of vertices for the polyhedron. -// face = The face, given as a list of indices into the vertex array `points`. -function triangulate_face(points, face) = - let( - points = path3d(points), - face = deduplicate_indexed(points,face), - count = len(face) - ) - (count < 3)? [] : - (count == 3)? [face] : - let( - facenorm=face_normal(points, face), - cv=find_convex_vertex(points, face, facenorm) - ) - assert(!is_undef(cv), "Cannot triangulate self-crossing face perimeters.") - let( - pv=(count+cv-1)%count, - nv=(cv+1)%count, - p0=points[face[pv]], - p1=points[face[cv]], - p2=points[face[nv]], - tests=[ - [cross(facenorm, p0-p2), cross(facenorm, p0-p2)*p0], - [cross(facenorm, p1-p0), cross(facenorm, p1-p0)*p1], - [cross(facenorm, p2-p1), cross(facenorm, p2-p1)*p2] - ], - ear_test=point_in_ear(points, face, tests), - clipable_ear=(ear_test[0]<0), - diagonal_point=ear_test[1] - ) - (clipable_ear)? // There is no point inside the ear. - is_only_noncolinear_vertex(points, face, cv)? - // In the point&line degeneracy clip to somewhere in the middle of the line. - concat( - triangulate_face(points, select(face, cv, (cv+2)%count)), - triangulate_face(points, select(face, (cv+2)%count, cv)) - ) - : - // Otherwise the ear is safe to clip. - [ - select(face, pv, nv), - each triangulate_face(points, select(face, nv, pv)) - ] - : // If there is a point inside the ear, make a diagonal and clip along that. - concat( - triangulate_face(points, select(face, cv, diagonal_point)), - triangulate_face(points, select(face, diagonal_point, cv)) - ); - - -// Function: triangulate_faces() -// Description: -// Subdivides all faces for the given polyhedron that have more than three vertices. -// Returns an array of faces where each face is a list of three vertex array indices. -// Arguments: -// points = Array of vertices for the polyhedron. -// faces = Array of faces for the polyhedron. Each face is a list of 3 or more indices into the `points` array. -function triangulate_faces(points, faces) = - [ - for (face=faces) each - len(face)==3? [face] : - triangulate_face(points, normalize_vertex_perimeter(face)) - ]; - - -// vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap diff --git a/vnf.scad b/vnf.scad index 34bd6b5..58afed8 100644 --- a/vnf.scad +++ b/vnf.scad @@ -9,8 +9,6 @@ ////////////////////////////////////////////////////////////////////// -include - // Creating Polyhedrons with VNF Structures // Section: VNF Testing and Access @@ -474,7 +472,8 @@ function vnf_triangulate(vnf) = let( vnf = is_vnf_list(vnf)? vnf_merge(vnf) : vnf, verts = vnf[0], - faces = [for (face=vnf[1]) each polygon_triangulate(verts, face)] + faces = [for (face=vnf[1]) each len(face)==3 ? [face] : + polygon_triangulate(verts, face)] ) [verts, faces];