diff --git a/arrays.scad b/arrays.scad index d04bd13..7a4acb7 100644 --- a/arrays.scad +++ b/arrays.scad @@ -153,9 +153,9 @@ function list_decreasing(list) = // Section: Basic List Generation -// Function: replist() +// Function: repeat() // Usage: -// replist(val, n) +// repeat(val, n) // Description: // Generates a list or array of `n` copies of the given `list`. // If the count `n` is given as a list of counts, then this creates a @@ -164,14 +164,14 @@ function list_decreasing(list) = // val = The value to repeat to make the list or array. // n = The number of copies to make of `val`. // Example: -// replist(1, 4); // Returns [1,1,1,1] -// replist(8, [2,3]); // Returns [[8,8,8], [8,8,8]] -// replist(0, [2,2,3]); // Returns [[[0,0,0],[0,0,0]], [[0,0,0],[0,0,0]]] -// replist([1,2,3],3); // Returns [[1,2,3], [1,2,3], [1,2,3]] -function replist(val, n, i=0) = +// repeat(1, 4); // Returns [1,1,1,1] +// repeat(8, [2,3]); // Returns [[8,8,8], [8,8,8]] +// repeat(0, [2,2,3]); // Returns [[[0,0,0],[0,0,0]], [[0,0,0],[0,0,0]]] +// repeat([1,2,3],3); // Returns [[1,2,3], [1,2,3], [1,2,3]] +function repeat(val, n, i=0) = is_num(n)? [for(j=[1:1:n]) val] : (i>=len(n))? val : - [for (j=[1:1:n[i]]) replist(val, n, i+1)]; + [for (j=[1:1:n[i]]) repeat(val, n, i+1)]; // Function: list_range() @@ -308,11 +308,11 @@ function repeat_entries(list, N, exact = true) = length = len(list), reps_guess = is_list(N)? assert(len(N)==len(list), "Vector parameter N to repeat_entries has the wrong length") - N : replist(N/length,length), + N : repeat(N/length,length), reps = exact? _sum_preserving_round(reps_guess) : [for (val=reps_guess) round(val)] ) - [for(i=[0:length-1]) each replist(list[i],reps[i])]; + [for(i=[0:length-1]) each repeat(list[i],reps[i])]; // Function: list_set() @@ -357,7 +357,7 @@ function list_set(list=[],indices,values,dflt=0,minlen=0) = ) ], slice(list,1+lastind, len(list)), - replist(dflt, minlen-lastind-1) + repeat(dflt, minlen-lastind-1) ); @@ -487,7 +487,7 @@ function list_bset(indexset, valuelist, dflt=0) = trueind = search([true], indexset,0)[0] ) concat( list_set([],trueind, valuelist, dflt=dflt), // Fill in all of the values - replist(dflt,len(indexset)-max(trueind)-1) // Add trailing values so length matches indexset + repeat(dflt,len(indexset)-max(trueind)-1) // Add trailing values so length matches indexset ); @@ -523,7 +523,7 @@ function list_longest(vecs) = // fill = The value to pad the list with. function list_pad(v, minlen, fill=undef) = assert(is_list(v)||is_string(list)) - concat(v,replist(fill,minlen-len(v))); + concat(v,repeat(fill,minlen-len(v))); // Function: list_trim() @@ -851,6 +851,15 @@ function enumerate(l,idx=undef) = [for (i=[0:1:len(l)-1]) concat([i], [for (j=idx) l[i][j]])]; +// Function: force_list() +// Usage: +// list = force_list(value) +// Description: +// If value is a list returns value, otherwise returns [value]. Makes it easy to +// treat a scalar input consistently as a singleton list along with list inputs. +function force_list(value) = is_list(value) ? value : [value]; + + // Function: pair() // Usage: // pair(v) diff --git a/beziers.scad b/beziers.scad index 801afde..f9f31f0 100644 --- a/beziers.scad +++ b/beziers.scad @@ -9,8 +9,8 @@ ////////////////////////////////////////////////////////////////////// -include - +include +include // Section: Terminology // **Polyline**: A series of points joined by straight line segements. @@ -318,6 +318,28 @@ function bezier_polyline(bezier, splinesteps=16, N=3) = let( ); +// Function: path_to_bezier() +// Usage: +// path_to_bezier(path,[tangent],[closed]); +// Description: +// Given an input path and optional path of tangent vectors, computes a cubic (degree 3) bezier path that passes +// through every point on the input path and matches the tangent vectors. If you do not supply +// the tangent it will be computed using path_tangents. If the path is closed specify this +// by setting closed=true. +// Arguments: +// path = path of points to define the bezier +// tangents = optional list of tangent vectors at every point +// closed = set to true for a closed path. Default: false +function path_to_bezier(path, tangents, closed=false) = + assert(is_path(path,dim=undef),"Input path is not a valid path") + assert(is_undef(tangents) || is_path(tangents,dim=len(path[0])),"Tangents must be a path of the same dimension as the input path") + let( + tangents = is_def(tangents)? tangents : deriv(path, closed=closed), + lastpt = len(path) - (closed?0:1) + ) + [for(i=[0:lastpt-1]) each [path[i], path[i]+tangents[i]/3, select(path,i+1)-select(tangents,i+1)/3], + select(path,lastpt)]; + // Function: fillet_path() // Usage: diff --git a/coords.scad b/coords.scad index 033830f..2d3edd0 100644 --- a/coords.scad +++ b/coords.scad @@ -31,7 +31,7 @@ function point2d(p, fill=0) = [for (i=[0:1]) (p[i]==undef)? fill : p[i]]; // fill = Value to fill missing values in vectors with. function path2d(points) = assert(is_path(points,dim=undef,fast=true),"Input to path2d is not a path") - let (result = points * concat(ident(2), replist([0,0], len(points[0])-2))) + let (result = points * concat(ident(2), repeat([0,0], len(points[0])-2))) assert(is_def(result), "Invalid input to path2d") result; @@ -58,11 +58,11 @@ function path3d(points, fill=0) = let ( change = len(points[0])-3, M = change < 0? [[1,0,0],[0,1,0]] : - concat(ident(3), replist([0,0,0],change)), + concat(ident(3), repeat([0,0,0],change)), result = points*M ) assert(is_def(result), "Input to path3d is invalid") - fill == 0 || change>=0 ? result : result + replist([0,0,fill], len(result)); + fill == 0 || change>=0 ? result : result + repeat([0,0,fill], len(result)); // Function: point4d() @@ -87,17 +87,17 @@ function path4d(points, fill=0) = let ( change = len(points[0])-4, M = change < 0 ? select(ident(4), 0, len(points[0])-1) : - concat(ident(4), replist([0,0,0,0],change)), + concat(ident(4), repeat([0,0,0,0],change)), result = points*M ) assert(is_def(result), "Input to path4d is invalid") fill == 0 || change >= 0 ? result : let( addition = is_list(fill) ? concat(0*points[0],fill) : - concat(0*points[0],replist(fill,-change)) + concat(0*points[0],repeat(fill,-change)) ) assert(len(addition) == 4, "Fill is the wrong length") - result + replist(addition, len(result)); + result + repeat(addition, len(result)); // Function: translate_points() diff --git a/joiners.scad b/joiners.scad index a6303ee..d4b17ac 100644 --- a/joiners.scad +++ b/joiners.scad @@ -565,4 +565,220 @@ module dovetail(gender, length, l, width, w, height, h, angle, slope, taper, bac +// h is total height above 0 of the nub +// nub extends below xy plane by distance nub/2 +module _pin_nub(r, nub, h) +{ + L = h / 4; + rotate_extrude(){ + polygon( + [[ 0,-nub/2], + [-r,-nub/2], + [-r-nub, nub/2], + [-r-nub, nub/2+L], + [-r, h], + [0, h]]); + } +} + + +module _pin_slot(l, r, t, d, nub, depth, stretch) { + yscale(4) + intersection() { + translate([t, 0, d + t / 4]) + _pin_nub(r = r + t, nub = nub, h = l - (d + t / 4)); + translate([-t, 0, d + t / 4]) + _pin_nub(r = r + t, nub = nub, h = l - (d + t / 4)); + } + cube([2 * r, depth, 2 * l], center = true); + up(l) + zscale(stretch) + ycyl(r = r, h = depth); +} + + +module _pin_shaft(r, lStraight, nub, nubscale, stretch, d, pointed) +{ + extra = 0.02; + rPoint = r / sqrt(2); + down(extra) cylinder(r = r, h = lStraight + extra); + up(lStraight) { + zscale(stretch) { + sphere(r = r); + if (pointed) up(rPoint) cylinder(r1 = rPoint, r2 = 0, h = rPoint); + } + } + up(d) yscale(nubscale) _pin_nub(r = r, nub = nub, h = lStraight - d); +} + +function _pin_size(size) = + is_undef(size) ? [] : + let(sizeok = in_list(size,["tiny", "small","medium", "large", "standard"])) + assert(sizeok,"Pin size must be one of \"tiny\", \"small\", or \"standard\"") + size=="standard" || size=="large" ? + struct_set([], ["length", 10.8, + "diameter", 7, + "snap", 0.5, + "nub_depth", 1.8, + "thickness", 1.8, + "preload", 0.2]): + size=="medium" ? + struct_set([], ["length", 8, + "diameter", 4.6, + "snap", 0.45, + "nub_depth", 1.5, + "thickness", 1.4, + "preload", 0.2]) : + size=="small" ? + struct_set([], ["length", 6, + "diameter", 3.2, + "snap", 0.4, + "nub_depth", 1.2, + "thickness", 1.0, + "preload", 0.16]) : + size=="tiny" ? + struct_set([], ["length", 4, + "diameter", 2.5, + "snap", 0.25, + "nub_depth", 0.9, + "thickness", 0.8, + "preload", 0.1]): + undef; + + +// Module: snap_pin() +// Usage: +// snap_pin(size, [pointed], [anchor], [spin], [orient]) +// snap_pin(r|radius|d|diameter, l|length, nub_depth, snap, thickness, [clearance], [preload], [pointed], [anchor], [spin], [orient]) +// Description: +// Creates a snap pin that can be inserted into an appropriate socket to connect two objects together. You can choose from some standard +// pin dimensions by giving a size, or you can specify all the pin geometry parameters yourself. If you use a standard size you can +// override the standard parameters by specifying other ones. The pins have flat sides so they can +// be printed. When oriented UP the shaft of the pin runs in the Z direction and the flat sides are the front and back. The default +// orientation (FRONT) and anchor (FRONT) places the pin in a printable configuration, flat side down on the xy plane. +// The tightness of fit is determined by `preload` and `clearance`. To make pins tighter increase `preload` and/or decrease `clearance`. +// +// The "large" or "standard" size pin has a length of 10.8 and diameter of 7. The "medium" pin has a length of 8 and diameter of 4.6. The "small" pin +// has a length of 6 and diameter of 3.2. The "tiny" pin has a length of 4 and a diameter of 2.5. +// +// This pin is based on https://www.thingiverse.com/thing:213310 by Emmett Lalishe +// and a modified version at https://www.thingiverse.com/thing:3218332 by acwest +// and distributed under the Creative Commons - Attribution - Share Alike License +// Arguments: +// size = text string to select from a list of predefined sizes, one of "standard", "small", or "tiny". +// pointed = set to true to get a pointed pin, false to get one with a rounded end. Default: true +// r|radius = radius of the pin +// d|diameter = diameter of the pin +// l|length = length of the pin +// nub_depth = the distance of the nub from the base of the pin +// snap = how much snap the pin provides (the nub projection) +// thickness = thickness of the pin walls +// pointed = if true the pin is pointed, otherwise it has a rounded tip. Default: true +// clearance = how far to shrink the pin away from the socket walls. Default: 0.2 +// preload = amount to move the nub towards the pin base, which can create tension from the misalignment with the socket. Default: 0.2 +// Example: Pin in native orientation +// snap_pin("standard", anchor=CENTER, orient=UP, thickness = 1, $fn=40); +// Example: Pins oriented for printing +// xspread(spacing=10, n=4) snap_pin("standard", $fn=40); +module snap_pin(size,r,radius,d,diameter, l,length, nub_depth, snap, thickness, clearance=0.2, preload, pointed=true, anchor=FRONT, spin=0, orient=FRONT, center) { + preload_default = 0.2; + sizedat = _pin_size(size); + radius = get_radius(r1=r,r2=radius,d1=d,d2=diameter,dflt=struct_val(sizedat,"diameter")/2); + length = first_defined([l,length,struct_val(sizedat,"length")]); + snap = first_defined([snap, struct_val(sizedat,"snap")]); + thickness = first_defined([thickness, struct_val(sizedat,"thickness")]); + nub_depth = first_defined([nub_depth, struct_val(sizedat,"nub_depth")]); + preload = first_defined([first_defined([preload, struct_val(sizedat, "preload")]),preload_default]); + + nubscale = 0.9; // Mysterious arbitrary parameter + + // The basic pin assumes a rounded cap of length sqrt(2)*r, which defines lStraight. + // If the point is enabled the cap length is instead 2*r + // preload shrinks the length, bringing the nubs closer together + + rInner = radius - clearance; + stretch = sqrt(2)*radius/rInner; // extra stretch factor to make cap have proper length even though r is reduced. + lStraight = length - sqrt(2) * radius - clearance; + lPin = lStraight + (pointed ? 2*radius : sqrt(2)*radius); + attachable(anchor=anchor,spin=spin, orient=orient, + size=[nubscale*(2*rInner+2*snap + clearance),radius*sqrt(2)-2*clearance,2*lPin]){ + zflip_copy() + difference() { + intersection() { + cube([3 * (radius + snap), radius * sqrt(2) - 2 * clearance, 2 * length + 3 * radius], center = true); + _pin_shaft(rInner, lStraight, snap+clearance/2, nubscale, stretch, nub_depth-preload, pointed); + } + _pin_slot(l = lStraight, r = rInner - thickness, t = thickness, d = nub_depth - preload, nub = snap, depth = 2 * radius + 0.02, stretch = stretch); + } + children(); + } +} + +// Module: snap_pin_socket() +// Usage: +// snap_pin_socket(size, [fixed], [fins], [pointed], [anchor], [spin], [orient]); +// snap_pin_socket(r|radius|d|diameter, l|length, nub_depth, snap, [fixed], [pointed], [fins], [anchor], [spin], [orient]) +// Description: +// Constructs a socket suitable for a snap_pin with the same parameters. If `fixed` is true then the socket has flat walls and the +// pin will not rotate in the socket. If `fixed` is false then the socket is round and the pin will rotate, particularly well +// if you add a lubricant. If `pointed` is true the socket is pointed to receive a pointed pin, otherwise it has a rounded and and +// will be shorter. If `fins` is set to true then two fins are included inside the socket to act as supports (which may help when printing tip up, +// especially when `pointed=false`). The default orientation is DOWN with anchor BOTTOM so that you can difference() the socket away from an object. +// +// The "large" or "standard" size pin has a length of 10.8 and diameter of 7. The "medium" pin has a length of 8 and diameter of 4.6. The "small" pin +// has a length of 6 and diameter of 3.2. The "tiny" pin has a length of 4 and a diameter of 2.5. +// Arguments: +// size = text string to select from a list of predefined sizes, one of "standard", "small", or "tiny". +// pointed = set to true to get a pointed pin, false to get one with a rounded end. Default: true +// r|radius = radius of the pin +// d|diameter = diameter of the pin +// l|length = length of the pin +// nub_depth = the distance of the nub from the base of the pin +// snap = how much snap the pin provides (the nub projection) +// fixed = if true the pin cannot rotate, if false it can. Default: true +// pointed = if true the socket has a pointed tip. Default: true +// fins = if true supporting fins are included. Default: false +// Example: The socket shape itself in native orientation. +// snap_pin_socket("standard", anchor=CENTER, orient=UP, fins=true, $fn=40); +// Example: A spinning socket with fins: +// snap_pin_socket("standard", anchor=CENTER, orient=UP, fins=true, fixed=false, $fn=40); +// Example: A cube with a socket in the middle and one half-way off the front edge so you can see inside: +// $fn=40; +// diff("socket") cuboid([20,20,20]) { +// attach(TOP) snap_pin_socket("standard", $tags="socket"); +// position(TOP+FRONT)snap_pin_socket("standard", $tags="socket"); +// } +module snap_pin_socket(size, r, radius, l,length, d,diameter,nub_depth, snap, fixed=true, pointed=true, fins=false, anchor=BOTTOM, spin=0, orient=DOWN) { + sizedat = _pin_size(size); + radius = get_radius(r1=r,r2=radius,d1=d,d2=diameter,dflt=struct_val(sizedat,"diameter")/2); + length = first_defined([l,length,struct_val(sizedat,"length")]); + snap = first_defined([snap, struct_val(sizedat,"snap")]); + nub_depth = first_defined([nub_depth, struct_val(sizedat,"nub_depth")]); + + tip = pointed ? sqrt(2) * radius : radius; + lPin = length + (pointed?(2-sqrt(2))*radius:0); + lStraight = lPin - (pointed?sqrt(2)*radius:radius); + attachable(anchor=anchor,spin=spin,orient=orient, + size=[2*(radius+snap),radius*sqrt(2),lPin]) + { + down(lPin/2) + intersection() { + if (fixed) + cube([3 * (radius + snap), radius * sqrt(2), 3 * lPin + 3 * radius], center = true); + union() { + _pin_shaft(radius,lStraight,snap,1,1,nub_depth,pointed); + if (fins) + up(lStraight){ + cube([2 * radius, 0.01, 2 * tip], center = true); + cube([0.01, 2 * radius, 2 * tip], center = true); + } + } + } + children(); + } +} + + + + // vim: noexpandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap diff --git a/math.scad b/math.scad index f0637a7..74a4a8b 100644 --- a/math.scad +++ b/math.scad @@ -412,7 +412,7 @@ function _lcmlist(a) = function lcm(a,b=[]) = !is_list(a) && !is_list(b) ? _lcm(a,b) : let( - arglist = concat((is_list(a)?a:[a]), (is_list(b)?b:[b])) + arglist = concat(force_list(a),force_list(b)) ) assert(len(arglist)>0,"invalid call to lcm with empty list(s)") _lcmlist(arglist); @@ -590,7 +590,7 @@ function _qr_factor(A,Q, column, m, n) = let( x = [for(i=[column:1:m-1]) A[i][column]], alpha = (x[0]<=0 ? 1 : -1) * norm(x), - u = x - concat([alpha],replist(0,m-1)), + u = x - concat([alpha],repeat(0,m-1)), v = u / norm(u), Qc = ident(len(x)) - 2*transpose([v])*[v], Qf = [for(i=[0:m-1]) [for(j=[0:m-1]) i1 && let( d = len(list[0]) ) - (is_undef(dim) || in_list(d, is_list(dim)?dim:[dim]) ) && - is_list_of(list, replist(0,d)); + (is_undef(dim) || in_list(d, force_list(dim))) && + is_list_of(list, repeat(0,d)); // Function: is_closed_path() @@ -287,7 +287,7 @@ function path_closest_point(path, pt) = // The returns vectors will be normalized to length 1. function path_tangents(path, closed=false) = assert(is_path(path)) - [for(t=deriv(path)) unit(t)]; + [for(t=deriv(path,closed=closed)) unit(t)]; // Function: path_normals() @@ -862,6 +862,7 @@ module path_extrude(path, convexity=10, clipsize=100) { // polyline = [for (a=[0:30:210]) 10*[cos(a), sin(a), sin(a)]]; // trace_polyline(polyline, showpts=true, size=0.5, color="lightgreen"); module trace_polyline(pline, closed=false, showpts=false, N=1, size=1, color="yellow") { + assert(is_path(pline),"Input pline is not a path"); sides = segs(size/2); pline = closed? close_path(pline) : pline; if (showpts) { @@ -1125,7 +1126,7 @@ function _path_cut(path, dists, closed=false, pind=0, dtotal=0, dind=0, result=[ [lerp(lastpt,path[pind], (dists[dind]-dtotal)/dpartial),pind] : _path_cut_single(path, dists[dind]-dtotal-dpartial, closed, pind) ) is_undef(nextpoint)? - concat(result, replist(undef,len(dists)-dind)) : + concat(result, repeat(undef,len(dists)-dind)) : _path_cut(path, dists, closed, nextpoint[1], dists[dind],dind+1, concat(result, [nextpoint])); // Search for a single cut point in the path @@ -1259,7 +1260,7 @@ function subdivide_path(path, N, closed=true, exact=true, method="length") = is_list(N)? ( assert(len(N)==count,"Vector parameter N to subdivide_path has the wrong length") add_scalar(N,-1) - ) : replist((N-len(path)) / count, count) + ) : repeat((N-len(path)) / count, count) ) : // method=="length" assert(is_num(N),"Parameter N to subdivide path must be a number when method=\"length\"") let( diff --git a/regions.scad b/regions.scad index 09f7c91..b8939dc 100644 --- a/regions.scad +++ b/regions.scad @@ -499,7 +499,7 @@ function offset( d = flip_dir * (is_def(r) ? r : delta), shiftsegs = [for(i=[0:len(path)-1]) _shift_segment(select(path,i,i+1), d)], // good segments are ones where no point on the segment is less than distance d from any point on the path - good = check_valid ? _good_segments(path, abs(d), shiftsegs, closed, quality) : replist(true,len(shiftsegs)), + good = check_valid ? _good_segments(path, abs(d), shiftsegs, closed, quality) : repeat(true,len(shiftsegs)), goodsegs = bselect(shiftsegs, good), goodpath = bselect(path,good) ) @@ -569,7 +569,7 @@ function offset( ) ], pointcount = (is_def(delta) && !chamfer)? - replist(1,len(sharpcorners)) : + repeat(1,len(sharpcorners)) : [for(i=[0:len(goodsegs)-1]) len(newcorners[i])], start = [goodsegs[0][0]], end = [goodsegs[len(goodsegs)-2][1]], diff --git a/rounding.scad b/rounding.scad index 2088e92..4987f21 100644 --- a/rounding.scad +++ b/rounding.scad @@ -161,7 +161,7 @@ include // $fs=.25; // $fa=1; // zigzagx = [-10, 0, 10, 20, 29, 38, 46, 52, 59, 66, 72, 78, 83, 88, 92, 96, 99, 102, 112]; -// zigzagy = concat([0], flatten(replist([-10,10],8)), [-10,0]); +// zigzagy = concat([0], flatten(repeat([-10,10],8)), [-10,0]); // zig = zip(zigzagx,zigzagy); // stroke(zig,width=1); // Original shape // fwd(20) // Smooth size corners with a cut of 4 and curvature parameter 0.6 @@ -194,7 +194,7 @@ include // Example(FlatSpin): Rounding a spiral with increased rounding along the length // // Construct a square spiral path in 3D // square = [[0,0],[1,0],[1,1],[0,1]]; -// spiral = flatten(replist(concat(square,reverse(square)),5)); // Squares repeat 10 times, forward and backward +// spiral = flatten(repeat(concat(square,reverse(square)),5)); // Squares repeat 10 times, forward and backward // squareind = [for(i=[0:9]) each [i,i,i,i]]; // Index of the square for each point // z = list_range(40)*.2+squareind; // path3d = zip(spiral,z); // 3D spiral @@ -246,7 +246,7 @@ function round_corners(path, curve="circle", measure="cut", size=undef, k=0.5, dim = pathdim - 1 + have_size, points = have_size ? path : subindex(path, [0:dim-1]), parm = have_size && is_list(size) && len(size)>2? size : - have_size? replist(size, len(path)) : + have_size? repeat(size, len(path)) : subindex(path, dim), // dk will be a list of parameters, for the "smooth" curve the distance and curvature parameter pair, // and for the "circle" curve, distance and radius. @@ -407,6 +407,41 @@ function _rounding_offsets(edgespec,z_dir=1) = +// Function: smooth_path() +// Usage: +// smooth_path(path, [tangents], [splinesteps], [closed] +// Description: +// Smooths the input path using a cubic spline. Every segment of the path will be replaced by a cubic curve +// with `splinesteps` points. The cubic interpolation will pass through every input point on the path +// and will match the tangents at every point. If you do not specify tangents they will be computed using +// deriv(). Note that the magnitude of the tangents affects the result. See also path_to_bezier(). +// Arguments: +// path = path to smooth +// tangents = tangent vectors of the path +// splinesteps = number of points to insert between the path points. Default: 10 +// closed = set to true for a closed path. Default: false +// Example(2D): Original path in green, smoothed path in yellow: +// color("green")stroke(square(4), width=0.1); +// stroke(smooth_path(square(4)), width=0.1); +// Example(2D): Closing the path changes the end tangents +// polygon(smooth_path(square(4), closed=true)); +// Example(2D): A more interesting shape: +// path = [[0,0], [4,0], [7,14], [-3,12]]; +// polygon(smooth_path(path,closed=true)); +// Example(2D): Scaling the tangent data can decrease or increase the amount of smoothing: +// shape = square(4); +// polygon(smooth_path(shape, tangents=0.5*deriv(shape, closed=true),closed=true)); +// Example(2D): Or you can specify your own tangent values to alter the shape of the curve +// polygon(smooth_path(square(4),tangents=1.25*[[-2,-1], [-2,1], [1,2], [2,-1]],closed=true)); +// Example(FlatSpin): Works on 3d paths as well +// path = [[0,0,0],[3,3,2],[6,0,1],[9,9,0]]; +// trace_polyline(smooth_path(path),size=.3); +function smooth_path(path, tangents, splinesteps=10, closed=false) = + let( + bez = path_to_bezier(path, tangents=tangents, closed=closed) + ) + bezier_polyline(bez,splinesteps=splinesteps); + // Module: offset_sweep() // @@ -497,11 +532,11 @@ function _rounding_offsets(edgespec,z_dir=1) = // // Example: Rounding a star shaped prism with postive radius values // star = star(5, r=22, ir=13); -// rounded_star = round_corners(zip(star, flatten(replist([.5,0],5))), curve="circle", measure="cut", $fn=12); +// rounded_star = round_corners(zip(star, flatten(repeat([.5,0],5))), curve="circle", measure="cut", $fn=12); // offset_sweep(rounded_star, height=20, bottom=os_circle(r=4), top=os_circle(r=1), steps=15); // Example: Rounding a star shaped prism with negative radius values // star = star(5, r=22, ir=13); -// rounded_star = round_corners(zip(star, flatten(replist([.5,0],5))), curve="circle", measure="cut", $fn=12); +// rounded_star = round_corners(zip(star, flatten(repeat([.5,0],5))), curve="circle", measure="cut", $fn=12); // offset_sweep(rounded_star, height=20, bottom=os_circle(r=-4), top=os_circle(r=-1), steps=15); // Example: Unexpected corners in the result even with `offset="round"` (the default), even with offset_maxstep set small. // triangle = [[0,0],[10,0],[5,10]]; @@ -514,7 +549,7 @@ function _rounding_offsets(edgespec,z_dir=1) = // offset_sweep(triangle, height=6, bottom = os_circle(r=-2),steps=16,offset_maxstep=0.01); // Example: Here is the star chamfered at the top with a teardrop rounding at the bottom. Check out the rounded corners on the chamfer. Note that a very small value of `offset_maxstep` is needed to keep these round. Observe how the rounded star points vanish at the bottom in the teardrop: the number of vertices does not remain constant from layer to layer. // star = star(5, r=22, ir=13); -// rounded_star = round_corners(zip(star, flatten(replist([.5,0],5))), curve="circle", measure="cut", $fn=12); +// rounded_star = round_corners(zip(star, flatten(repeat([.5,0],5))), curve="circle", measure="cut", $fn=12); // offset_sweep(rounded_star, height=20, bottom=os_teardrop(r=4), top=os_chamfer(width=4,offset_maxstep=.1)); // Example: We round a cube using the continous curvature rounding profile. But note that the corners are not smooth because the curved square collapses into a square with corners. When a collapse like this occurs, we cannot turn `check_valid` off. // square = [[0,0],[1,0],[1,1],[0,1]]; @@ -565,7 +600,7 @@ function _rounding_offsets(edgespec,z_dir=1) = // } // Example: Star shaped box // star = star(5, r=22, ir=13); -// rounded_star = round_corners(zip(star, flatten(replist([.5,0],5))), curve="circle", measure="cut", $fn=12); +// rounded_star = round_corners(zip(star, flatten(repeat([.5,0],5))), curve="circle", measure="cut", $fn=12); // thickness = 2; // ht=20; // difference(){ @@ -577,12 +612,12 @@ function _rounding_offsets(edgespec,z_dir=1) = // } // Example: A profile defined by an arbitrary sequence of points. // star = star(5, r=22, ir=13); -// rounded_star = round_corners(zip(star, flatten(replist([.5,0],5))), curve="circle", measure="cut", $fn=12); +// rounded_star = round_corners(zip(star, flatten(repeat([.5,0],5))), curve="circle", measure="cut", $fn=12); // profile = os_profile(points=[[0,0],[.3,.1],[.6,.3],[.9,.9], [1.2, 2.7],[.8,2.7],[.8,3]]); // offset_sweep(reverse(rounded_star), height=20, top=profile, bottom=profile); // Example: Parabolic rounding // star = star(5, r=22, ir=13); -// rounded_star = round_corners(zip(star, flatten(replist([.5,0],5))), curve="circle", measure="cut", $fn=12); +// rounded_star = round_corners(zip(star, flatten(repeat([.5,0],5))), curve="circle", measure="cut", $fn=12); // offset_sweep(rounded_star, height=20, top=os_profile(points=[for(r=[0:.1:2])[sqr(r),r]]), // bottom=os_profile(points=[for(r=[0:.2:5])[-sqrt(r),r]])); // Example: This example uses a sine wave offset profile. Note that because the offsets occur sequentially and the path grows incrementally the offset needs a very fine resolution to produce the proper result. Note that we give no specification for the bottom, so it is straight. @@ -642,7 +677,7 @@ module offset_sweep( offsetind+1, vertexcount+len(path), vertices=concat( vertices, - zip(vertices_faces[0],replist(offsets[offsetind][1],len(vertices_faces[0]))) + zip(vertices_faces[0],repeat(offsets[offsetind][1],len(vertices_faces[0]))) ), faces=concat(faces, vertices_faces[1]) ) @@ -711,7 +746,7 @@ module offset_sweep( ); top_start_ind = len(vertices_faces_bot[0]); - initial_vertices_top = zip(path, replist(middle,len(path))); + initial_vertices_top = zip(path, repeat(middle,len(path))); vertices_faces_top = make_polyhedron( path, translate_points(offsets_top,[0,middle]), struct_val(top,"offset"), !clockwise, diff --git a/shapes2d.scad b/shapes2d.scad index d9148f6..5ba8578 100644 --- a/shapes2d.scad +++ b/shapes2d.scad @@ -406,7 +406,7 @@ function _normal_segment(p1,p2) = // 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(replist(["move","left","addlength",1],50)))); +// 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); diff --git a/skin.scad b/skin.scad index 62a4a0a..346b173 100644 --- a/skin.scad +++ b/skin.scad @@ -211,7 +211,7 @@ include // skin( shapes, slices=0); // Example: You can fix it by specifying "tangent" for the first method, but you still need "direct" for the rest. // shapes = [for(i=[0:.2:1]) path3d(regular_ngon(n=4, side=4, rounding=i, $fn=32),i*5)]; -// skin( shapes, slices=0, method=concat(["tangent"],replist("direct",len(shapes)-2))); +// skin( shapes, slices=0, method=concat(["tangent"],repeat("direct",len(shapes)-2))); // Example(FlatSpin): Connecting square to pentagon using "direct" method. // skin([regular_ngon(n=4, r=4), regular_ngon(n=5,r=5)], z=[0,4], refine=10, slices=10); // Example(FlatSpin): Connecting square to shifted pentagon using "direct" method. @@ -324,6 +324,7 @@ module skin(profiles, slices, refine=1, method="direct", sampling, caps, closed= function skin(profiles, slices, refine=1, method="direct", sampling, caps, closed=false, z) = + assert(is_def(slices),"The slices argument must be specified.") assert(is_list(profiles) && len(profiles)>1, "Must provide at least two profiles") let( bad = [for(i=idx(profiles)) if (!(is_path(profiles[i]) && len(profiles[i])>2)) i]) assert(len(bad)==0, str("Profiles ",bad," are not a paths or have length less than 3")) @@ -334,14 +335,14 @@ function skin(profiles, slices, refine=1, method="direct", sampling, caps, close closed ? false : true, capsOK = is_bool(caps) || (is_list(caps) && len(caps)==2 && is_bool(caps[0]) && is_bool(caps[1])), fullcaps = is_bool(caps) ? [caps,caps] : caps, - refine = is_list(refine) ? refine : replist(refine, len(profiles)), - slices = is_list(slices) ? slices : replist(slices, profcount), + refine = is_list(refine) ? refine : repeat(refine, len(profiles)), + slices = is_list(slices) ? slices : repeat(slices, profcount), refineOK = [for(i=idx(refine)) if (refine[i]<=0 || !is_integer(refine[i])) i], slicesOK = [for(i=idx(slices)) if (!is_integer(slices[i]) || slices[i]<0) i], maxsize = list_longest(profiles), methodok = is_list(method) || in_list(method, legal_methods), methodlistok = is_list(method) ? [for(i=idx(method)) if (!in_list(method[i], legal_methods)) i] : [], - method = is_string(method) ? replist(method, profcount) : method, + method = is_string(method) ? repeat(method, profcount) : method, // Define to be zero where a resampling method is used and 1 where a vertex duplicator is used RESAMPLING = 0, DUPLICATOR = 1, @@ -381,7 +382,7 @@ function skin(profiles, slices, refine=1, method="direct", sampling, caps, close method_type[i] * method_type[i-1])], parts = search(1,[1,for(i=[0:1:len(profile_resampled)-2]) profile_resampled[i]!=profile_resampled[i+1] ? 1 : 0],0), plen = [for(i=idx(parts)) (i== len(parts)-1? len(refined_len) : parts[i+1]) - parts[i]], - max_list = [for(i=idx(parts)) each replist(max(select(refined_len, parts[i], parts[i]+plen[i]-1)), plen[i])], + max_list = [for(i=idx(parts)) each repeat(max(select(refined_len, parts[i], parts[i]+plen[i]-1)), plen[i])], transition_profiles = [for(i=[(closed?0:1):1:profcount-1]) if (select(method_type,i-1) != method_type[i]) i], badind = [for(tranprof=transition_profiles) if (refined_len[tranprof] != max_list[tranprof]) tranprof] ) @@ -514,7 +515,7 @@ function slice_profiles(profiles,slices,closed=false) = let(listok = !is_list(slices) || len(slices)==len(profiles)-(closed?0:1)) assert(listok, "Input slices to slice_profiles is a list with the wrong length") let( - count = is_num(slices) ? replist(slices,len(profiles)-(closed?0:1)) : slices, + count = is_num(slices) ? repeat(slices,len(profiles)-(closed?0:1)) : slices, slicelist = [for (i=[0:len(profiles)-(closed?1:2)]) each [for(j = [0:count[i]]) lerp(profiles[i],select(profiles,i+1),j/(count[i]+1))] ] @@ -590,7 +591,7 @@ function _dp_distance_array(small, big, abort_thresh=1/0) = function _dp_distance_row(small, big, small_ind, tdist) = // Top left corner is zero because it gets counted at the end in bottom right corner - small_ind == 0 ? [cumsum([0,for(i=[1:len(big)]) norm(big[i%len(big)]-small[0])]), replist(_MAP_LEFT,len(big)+1)] : + small_ind == 0 ? [cumsum([0,for(i=[1:len(big)]) norm(big[i%len(big)]-small[0])]), repeat(_MAP_LEFT,len(big)+1)] : [for(big_ind=1, newrow=[ norm(big[0] - small[small_ind%len(small)]) + tdist[small_ind-1][0] ], newmap = [_MAP_UP] @@ -715,6 +716,71 @@ function _find_one_tangent(curve, edge, curve_offset=[0,0,0], closed=true) = zero_cross[min_index(d)]; +// Function: associate_vertices() +// Usage: +// associate_vertices(polygons, split) +// Description: +// Takes as input a list of polygons and duplicates specified vertices in each polygon in the list through the series so +// that the input can be passed to `skin()`. This allows you to decide how the vertices are linked up rather than accepting +// the automatically computed minimal distance linkage. However, the number of vertices in the polygons must not decrease in the list. +// The output is a list of polygons that all have the same number of vertices with some duplicates. You specify the vertix splitting +// using the `split` which is a list where each entry corresponds to a polygon: split[i] is a value or list specfying which vertices in polygon i to split. +// Give the empty list if you don't want a split for a particular polygon. If you list a vertex once then it will be split and mapped to +// two vertices in the next polygon. If you list it N times then N copies will be created to map to N+1 vertices in the next polygon. +// You must ensure that each mapping produces the correct number of vertices to exactly map onto every vertex of the next polygon. +// Note that if you split (only) vertex i of a polygon that means it will map to vertices i and i+1 of the next polygon. Vertex 0 will always +// map to vertex 0 and the last vertices will always map to each other, so if you want something different than that you'll need to reindex +// your polygons. +// Arguments: +// polygons = list of polygons to split +// split = list of lists of split vertices +// Example(FlatSpin): If you skin together a square and hexagon using the optimal distance method you get two triangular faces on opposite sides: +// sq = regular_ngon(4,side=2); +// hex = apply(rot(15),hexagon(side=2)); +// skin([sq,hex], slices=10, refine=10, method="distance", z=[0,4]); +// Example(FlatSpin): Using associate_vertices you can change the location of the triangular faces. Here they are connect to two adjacent vertices of the square: +// sq = regular_ngon(4,side=2); +// hex = apply(rot(15),hexagon(side=2)); +// skin(associate_vertices([sq,hex],[[1,2]]), slices=10, refine=10, sampling="segment", z=[0,4]); +// Example(FlatSpin): Here the two triangular faces connect to a single vertex on the square. Note that we had to rotate the hexagon to line them up because the vertices match counting forward, so in this case vertex 0 of the square matches to vertices 0, 1, and 2 of the hexagon. +// sq = regular_ngon(4,side=2); +// hex = apply(rot(60),hexagon(side=2)); +// skin(associate_vertices([sq,hex],[[0,0]]), slices=10, refine=10, sampling="segment", z=[0,4]); +// Example: This example shows several polygons, with only a single vertex split at each step: +// sq = regular_ngon(4,side=2); +// pent = pentagon(side=2); +// hex = hexagon(side=2); +// sep = regular_ngon(7,side=2); +// skin(associate_vertices([sq,pent,hex,sep], [1,3,4]) ,slices=10, refine=10, method="distance", z=[0,2,4,6]); +// Example: The polygons cannot shrink, so if you want to have decreasing polygons you'll need to concatenate multiple results. Note that it is perfectly ok to duplicate a profile as shown here, where the pentagon is duplicated: +// sq = regular_ngon(4,side=2); +// pent = pentagon(side=2); +// grow = associate_vertices([sq,pent],[1]); +// shrink = associate_vertices([sq,pent],[2]); +// skin(concat(grow, reverse(shrink)), slices=10, refine=10, method="distance", z=[0,2,2,4]); +function associate_vertices(polygons, split, curpoly=0) = + curpoly==len(polygons)-1 ? polygons : + let( + polylen = len(polygons[curpoly]), + cursplit = force_list(split[curpoly]), + fdsa= echo(cursplit=cursplit) + ) + assert(len(split)==len(polygons)-1,str(split,"Split list length mismatch: it has length ", len(split)," but must have length ",len(polygons)-1)) + assert(polylen<=len(polygons[curpoly+1]),str("Polygon ",curpoly," has more vertices than the next one.")) + assert(len(cursplit)+polylen == len(polygons[curpoly+1]), + str("Polygon ", curpoly, " has ", polylen, " vertices. Next polygon has ", len(polygons[curpoly+1]), + " vertices. Split list has length ", len(cursplit), " but must have length ", len(polygons[curpoly+1])-polylen)) + assert(max(cursplit)=0, + str("Split ",cursplit," at polygon ",curpoly," has invalid vertices. Must be in [0:",polylen-1,"]")) + len(cursplit)==0 ? associate_vertices(polygons,split,curpoly+1) : + let( + splitindex = sort(concat(list_range(polylen), cursplit)), + newpoly = [for(i=[0:len(polygons)-1]) i<=curpoly ? select(polygons[i],splitindex) : polygons[i]] + ) + associate_vertices(newpoly, split, curpoly+1); + + + // Function&Module: sweep() // Usage: sweep(shape, transformations, [closed], [caps]) // Description: @@ -759,18 +825,16 @@ function _find_one_tangent(curve, edge, curve_offset=[0,0,0], closed=true) = // sweep(shape, concat(outside,inside)); function sweep(shape, transformations, closed=false, caps) = + assert(is_list_of(transformations, ident(4)), "Input transformations must be a list of numeric 4x4 matrices in sweep") + assert(is_path(shape,2), "Input shape must be a 2d path") let( - tdim = array_dim(transformations), - shapedim = array_dim(shape), caps = is_def(caps) ? caps : closed ? false : true, capsOK = is_bool(caps) || (is_list(caps) && len(caps)==2 && is_bool(caps[0]) && is_bool(caps[1])), fullcaps = is_bool(caps) ? [caps,caps] : caps ) - assert(len(tdim)==3 && tdim[1]==4 && tdim[2]==4, "transformations must be a list of 4x4 matrices in sweep") - assert(tdim[0]>1, "transformation must be length 2 or more") - assert(len(shapedim)==2 && shapedim[0]>2, "shape must be a path of at least 3 points") - assert(shapedim[1]==2, "shape must be a path in 2-dimensions") + assert(len(transformations), "transformation must be length 2 or more") + assert(len(shape)>=3, "shape must be a path of at least 3 points") assert(capsOK, "caps must be boolean or a list of two booleans") assert(!closed || !caps, "Cannot make closed shape with caps") _skin_core([for(i=[0:len(transformations)-(closed?0:1)]) apply(transformations[i%len(transformations)],path3d(shape))],caps=fullcaps); @@ -783,7 +847,7 @@ module sweep(shape, transformations, closed=false, caps, convexity=10) { // Function&Module: path_sweep() // Usage: path_sweep(shape, path, [method], [normal], [closed], [twist], [twist_by_length], [symmetry], [last_normal], [tangent], [relaxed], [caps], [convexity], [transforms]) // Description: -// Takes as input a 2d shape (specified as a point list) and a 3d path and constructs a polyhedron by sweeping the shape along the path. +// Takes as input a 2d shape (specified as a point list) and a 2d or 3d path and constructs a polyhedron by sweeping the shape along the path. // When run as a module returns the polyhedron geometry. When run as a function returns a VNF by default or if you set `transforms=true` then // it returns a list of transformations suitable as input to `sweep`. // @@ -858,7 +922,7 @@ module sweep(shape, transformations, closed=false, caps, convexity=10) { // Example: Sweep along a clockwise elliptical arc, using "natural" method, which lines up the X axis of the shape with the direction of curvature. This means the X axis will point inward, so a counterclockwise arc gives: // ushape = [[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[ 7, 2],[ 7, 7],[ 10, 7],[ 10, 0]]; // elliptic_arc = xscale(2, p=arc($fn=64,angle=[0,180], r=30)); // Counter-clockwise -// path_sweep(ushape, path3d(elliptic_arc), method="natural"); +// path_sweep(ushape, elliptic_arc, method="natural"); // Example: Sweep along a clockwise elliptical arc, using "natural" method. If the curve is clockwise than the shape flips upside-down to align the X axis. // ushape = [[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[ 7, 2],[ 7, 7],[ 10, 7],[ 10, 0]]; // elliptic_arc = xscale(2, p=arc($fn=64,angle=[180,0], r=30)); // Clockwise @@ -1069,9 +1133,10 @@ function path_sweep(shape, path, method="incremental", normal, closed=false, twi assert(!closed || twist % (360/symmetry)==0, str("For a closed sweep, twist must be a multiple of 360/symmetry = ",360/symmetry)) assert(closed || symmetry==1, "symmetry must be 1 when closed is false") assert(is_integer(symmetry) && symmetry>0, "symmetry must be a positive integer") - assert(is_path(shape) && len(shape[0])==2, "shape must be a 2d path") - assert(is_path(path) && len(path[0])==3, "path must be a 3d path") + assert(is_path(shape,2), "shape must be a 2d path") + assert(is_path(path), "input path is not a path") let( + path = path3d(path), caps = is_def(caps) ? caps : closed ? false : true, capsOK = is_bool(caps) || (is_list(caps) && len(caps)==2 && is_bool(caps[0]) && is_bool(caps[1])), @@ -1086,7 +1151,7 @@ function path_sweep(shape, path, method="incremental", normal, closed=false, twi normal = is_path(normal) ? [for(n=normal) unit(n)] : is_def(normal) ? unit(normal) : method =="incremental" && abs(tangents[0].z) > 1/sqrt(2) ? BACK : UP, - normals = is_path(normal) ? normal : replist(normal,len(path)), + normals = is_path(normal) ? normal : repeat(normal,len(path)), pathfrac = twist_by_length ? path_length_fractions(path, closed) : [for(i=[0:1:len(path)]) i / (len(path)-(closed?0:1))], L = len(path), transform_list =