diff --git a/attachments.scad b/attachments.scad index 6b1f1630..4be6ffab 100644 --- a/attachments.scad +++ b/attachments.scad @@ -382,6 +382,138 @@ module attach(from, to=undef, overlap=undef, norot=false) } +// Module: edge_profile() +// Usage: +// edge_profile([edges], [except], [convexity]) ... +// Description: +// Takes a 2D mask shape and attaches it to the selected edges, with the appropriate orientation +// and extruded length to be `diff()`ed away, to give the edge a matching profile. +// Arguments: +// edges = Edges to mask. See the docs for [`edges()`](edges.scad#edges) to see acceptable values. Default: All edges. +// except = Edges to explicitly NOT mask. See the docs for [`edges()`](edges.scad#edges) to see acceptable values. Default: No edges. +// convexity = Max number of times a line could intersect the perimeter of the mask shape. Default: 10 +// Side Effects: +// Sets `$tags = "mask"` for all children. +// Example: +// diff("mask") +// cube([50,60,70],center=true) +// edge_profile([TOP,"Z"],except=[BACK,TOP+LEFT]) +// mask2d_roundover(r=10, inset=2); +module edge_profile(edges=EDGES_ALL, except=[], convexity=10) { + assert($parent_size != undef, "No object to attach to!"); + edges = edges(edges, except=except); + vecs = [ + for (i = [0:3], axis=[0:2]) + if (edges[axis][i]>0) + EDGE_OFFSETS[axis][i] + ]; + for (vec = vecs) { + vcount = (vec.x?1:0) + (vec.y?1:0) + (vec.z?1:0); + assert(vcount == 2, "Not an edge vector!"); + anch = find_anchor(vec, $parent_size.z, point2d($parent_size), size2=$parent_size2, shift=$parent_shift, offset=$parent_offset, anchors=$parent_anchors, geometry=$parent_geom, two_d=$parent_2d); + $attach_to = undef; + $attach_anchor = anch; + $attach_norot = true; + $tags = "mask"; + length = sum(vmul($parent_size, [for (x=vec) x?0:1]))+0.1; + rotang = + vec.z<0? [90,0,180+vang(point2d(vec))] : + vec.z==0 && sign(vec.x)==sign(vec.y)? 135+vang(point2d(vec)) : + vec.z==0 && sign(vec.x)!=sign(vec.y)? [0,180,45+vang(point2d(vec))] : + [-90,0,180+vang(point2d(vec))]; + translate(anch[1]) { + rot(rotang) { + linear_extrude(height=length, center=true, convexity=convexity) { + children(); + } + } + } + } +} + + +// Module: edge_mask() +// Usage: +// edge_mask([edges], [except]) ... +// Description: +// Takes a 3D mask shape, and attaches it to the given edges, with the +// appropriate orientation to be `diff()`ed away. +// Arguments: +// edges = Edges to mask. See the docs for [`edges()`](edges.scad#edges) to see acceptable values. Default: All edges. +// except = Edges to explicitly NOT mask. See the docs for [`edges()`](edges.scad#edges) to see acceptable values. Default: No edges. +// Side Effects: +// Sets `$tags = "mask"` for all children. +// Example: +// diff("mask") +// cube([50,60,70],center=true) +// edge_mask([TOP,"Z"],except=[BACK,TOP+LEFT]) +// rounding_mask_z(l=71,r=10); +module edge_mask(edges=EDGES_ALL, except=[]) { + assert($parent_size != undef, "No object to attach to!"); + edges = edges(edges, except=except); + vecs = [ + for (i = [0:3], axis=[0:2]) + if (edges[axis][i]>0) + EDGE_OFFSETS[axis][i] + ]; + for (vec = vecs) { + vcount = (vec.x?1:0) + (vec.y?1:0) + (vec.z?1:0); + assert(vcount == 2, "Not an edge vector!"); + anch = find_anchor(vec, $parent_size.z, point2d($parent_size), size2=$parent_size2, shift=$parent_shift, offset=$parent_offset, anchors=$parent_anchors, geometry=$parent_geom, two_d=$parent_2d); + $attach_to = undef; + $attach_anchor = anch; + $attach_norot = true; + $tags = "mask"; + rotang = + vec.z<0? [90,0,180+vang(point2d(vec))] : + vec.z==0 && sign(vec.x)==sign(vec.y)? 135+vang(point2d(vec)) : + vec.z==0 && sign(vec.x)!=sign(vec.y)? [0,180,45+vang(point2d(vec))] : + [-90,0,180+vang(point2d(vec))]; + translate(anch[1]) rot(rotang) children(); + } +} + + +// Module: corner_mask() +// Usage: +// corner_mask([corners], [except]) ... +// Description: +// Takes a 3D mask shape, and attaches it to the given corners, with the appropriate +// orientation to be `diff()`ed away. The 3D corner mask shape should be designed to +// mask away the X+Y+Z+ octant. +// Arguments: +// corners = Edges to mask. See the docs for [`corners()`](edges.scad#corners) to see acceptable values. Default: All corners. +// except = Edges to explicitly NOT mask. See the docs for [`corners()`](edges.scad#corners) to see acceptable values. Default: No corners. +// Side Effects: +// Sets `$tags = "mask"` for all children. +// Example: +// diff("mask") +// cube(100, center=true) +// corner_mask([TOP,FRONT],LEFT+FRONT+TOP) +// difference() { +// translate(-0.01*[1,1,1]) cube(20); +// translate([20,20,20]) sphere(r=20); +// } +module corner_mask(corners=CORNERS_ALL, except=[]) { + assert($parent_size != undef, "No object to attach to!"); + corners = corners(corners, except=except); + vecs = [for (i = [0:7]) if (corners[i]>0) CORNER_OFFSETS[i]]; + for (vec = vecs) { + vcount = (vec.x?1:0) + (vec.y?1:0) + (vec.z?1:0); + assert(vcount == 3, "Not an edge vector!"); + anch = find_anchor(vec, $parent_size.z, point2d($parent_size), size2=$parent_size2, shift=$parent_shift, offset=$parent_offset, anchors=$parent_anchors, geometry=$parent_geom, two_d=$parent_2d); + $attach_to = undef; + $attach_anchor = anch; + $attach_norot = true; + $tags = "mask"; + rotang = vec.z<0? + [ 0,0,180+vang(point2d(vec))-45] : + [180,0,-90+vang(point2d(vec))-45]; + translate(anch[1]) rot(rotang) children(); + } +} + + // Module: tags() // Usage: // tags(tags) ... diff --git a/common.scad b/common.scad index 2761025b..7e031890 100644 --- a/common.scad +++ b/common.scad @@ -73,6 +73,14 @@ function is_int(n) = is_num(n) && n == round(n); function is_integer(n) = is_num(n) && n == round(n); +// Function: is_nan() +// Usage: +// is_nan(x); +// Description: +// Returns true if a given value `x` is nan, a floating point value representing "not a number". +function is_nan(x) = (x!=x); + + // Section: Handling `undef`s. diff --git a/edges.scad b/edges.scad index 6ac3d377..c4344559 100644 --- a/edges.scad +++ b/edges.scad @@ -33,6 +33,21 @@ // cuboid(size=size,chamfer=chamfer,edges=edges); // fwd(size/2) text3d(lbl2, size=txtsize); // } +// module corner_cube(size=20, txtsize=3, corners="ALL") { +// corner_set = _corner_set(corners); +// lbl = is_string(corners)? [str("\"",corners,"\"")] : concat( +// corners.z>0? ["TOP"] : corners.z<0? ["BTM"] : [], +// corners.y>0? ["BACK"] : corners.y<0? ["FWD"] : [], +// corners.x>0? ["RIGHT"] : corners.x<0? ["LEFT"] : [] +// ); +// lbl2 = [for (i=idx(lbl)) i0) +// translate(CORNER_OFFSETS[i]*size/2) +// color("red") +// cube(1, center=true); +// fwd(size/2) text3d(lbl2, size=txtsize); +// color("yellow",0.7) cuboid(size=size); +// } // Section: Sets of Edges @@ -225,6 +240,151 @@ EDGE_OFFSETS = [ // Array of XYZ offsets to the center of each edge. ]; +// Section: Corner Sets +// Constants for specifying corners. + +CORNERS_NONE = [0,0,0,0,0,0,0,0]; // No corners. +CORNERS_ALL = [1,1,1,1,1,1,1,1]; // All corners. + + +// Section: Corner Helpers + +// Function: is_corner_array() +// Usage: +// is_corner_array(v) +// Description: +// Returns true if the given value has the form of a corner array. +function is_corner_array(v) = is_vector(v) && len(v)==8 && all([for (x=v) x==1||x==0]); + + +// Function: normalize_corners() +// Usage: +// normalize_corners(v); +// Description: +// Normalizes all values in a corner array to be `1`, if it was originally greater than `0`, +// or `0`, if it was originally less than or equal to `0`. +function normalize_corners(v) = [for (x=v) x>0? 1 : 0]; + + +function _corner_set(v) = + is_corner_array(v)? v : [ + for (i=[0:7]) let( + v2 = CORNER_OFFSETS[i] + ) ( + is_string(v)? ( + v=="ALL"? true : // Return all corners. + v=="NONE"? false : // Return no corners. + let(valid_values = ["ALL", "NONE"]) + assert( + in_list(v, valid_values), + str(v, " must be a vector, corner array, or one of ", valid_values) + ) v + ) : + all([for (i=[0:2]) !v[i] || (v[i]==v2[i])]) + )? 1 : 0 +]; + + +// Function: corners() +// Usage: +// corners(v) +// corners(v, except) +// Description: +// Takes a list of corner set descriptors, and returns a normalized corners array +// that represents all those given corners. If the `except` argument is given +// a list of corner set descriptors, then all those corners will be removed +// from the returned corners array. If either argument only has a single corner +// set descriptor, you do not have to pass it in a list. +// Each corner set descriptor can be any of: +// - A vector pointing towards an edge indicating both corners at the ends of that edge. +// - A vector pointing towards a face, indicating all the corners of that face. +// - A vector pointing towards a corner, indicating just that corner. +// - The string `"ALL"`, indicating all corners. +// - The string `"NONE"`, indicating no corners at all. +// - A raw corners array, where each corner is represented by a 1 or a 0. The corner ordering is: +// ``` +// [X-Y-Z-, X+Y-Z-, X-Y+Z-, X+Y+Z-, X-Y-Z+, X+Y-Z+, X-Y+Z+, X+Y+Z+] +// ``` +// Figure(3DBig): Edge Vectors +// ydistribute(55) { +// xdistribute(35) { +// corner_cube(corners=BOT+RIGHT); +// corner_cube(corners=BOT+BACK); +// corner_cube(corners=BOT+LEFT); +// corner_cube(corners=BOT+FRONT); +// } +// xdistribute(35) { +// corner_cube(corners=FWD+RIGHT); +// corner_cube(corners=BACK+RIGHT); +// corner_cube(corners=BACK+LEFT); +// corner_cube(corners=FWD+LEFT); +// } +// xdistribute(35) { +// corner_cube(corners=TOP+RIGHT); +// corner_cube(corners=TOP+BACK); +// corner_cube(corners=TOP+LEFT); +// corner_cube(corners=TOP+FRONT); +// } +// } +// Figure(3DBig): Corner Vector Edge Sets +// ydistribute(55) { +// xdistribute(35) { +// corner_cube(corners=FRONT+LEFT+TOP); +// corner_cube(corners=FRONT+RIGHT+TOP); +// corner_cube(corners=FRONT+LEFT+BOT); +// corner_cube(corners=FRONT+RIGHT+BOT); +// } +// xdistribute(35) { +// corner_cube(corners=TOP+LEFT+BACK); +// corner_cube(corners=TOP+RIGHT+BACK); +// corner_cube(corners=BOT+LEFT+BACK); +// corner_cube(corners=BOT+RIGHT+BACK); +// } +// } +// Figure(3D): Face Vector Edge Sets +// ydistribute(55) { +// xdistribute(35) { +// corner_cube(corners=LEFT); +// corner_cube(corners=FRONT); +// corner_cube(corners=RIGHT); +// } +// xdistribute(35) { +// corner_cube(corners=TOP); +// corner_cube(corners=BACK); +// corner_cube(corners=BOTTOM); +// } +// } +// Figure(3D): Named Edge Sets +// xdistribute(35) { +// corner_cube(corners="ALL"); +// corner_cube(corners="NONE"); +// } +// Example: Just the front-top-right corner +// corners(FRONT+TOP+RIGHT) +// Example: All corners surrounding either the front or top faces +// corners([FRONT,TOP]) +// Example: All corners around the bottom face, except any that are also on the front +// corners(BTM, except=FRONT) +// Example: All corners except those around the bottom face. +// corners("ALL", except=BOTTOM) +// Example: All corners around the bottom or front faces, except those on the bottom-front edge. +// corners([BOTTOM,FRONT], except=BOTTOM+FRONT) +function corners(v, except=[]) = + (is_string(v) || is_vector(v) || is_corner_array(v))? corners([v], except=except) : + (is_string(except) || is_vector(except) || is_corner_array(except))? corners(v, except=[except]) : + except==[]? normalize_corners(sum([for (x=v) _corner_set(x)])) : + let( + a = normalize_corners(sum([for (x=v) _corner_set(x)])), + b = normalize_corners(sum([for (x=except) _corner_set(x)])) + ) normalize_corners(a - b); + + +CORNER_OFFSETS = [ // Array of XYZ offsets to each corner. + [-1,-1,-1], [ 1,-1,-1], [-1, 1,-1], [ 1, 1,-1], + [-1,-1, 1], [ 1,-1, 1], [-1, 1, 1], [ 1, 1, 1] +]; + + // Function: corner_edge_count() // Description: Counts how many given edges intersect at a specific corner. // Arguments: diff --git a/masks.scad b/masks.scad index cb9a0159..76fc781d 100644 --- a/masks.scad +++ b/masks.scad @@ -411,10 +411,21 @@ module rounding_mask(l=undef, r=undef, r1=undef, r2=undef, anchor=CENTER, spin=0 r2 = get_radius(r1=r2, r=r, dflt=1); sides = quantup(segs(max(r1,r2)),4); orient_and_anchor([2*r1, 2*r1, l], orient, anchor, spin=spin, size2=[2*r2,2*r2], chain=true) { - linear_extrude(height=l+0.1, convexity=4, center=true, scale=r2/r1) { - difference() { - square(2*r1, center=true); - xspread(2*r1) yspread(2*r1) circle(r=r1, $fn=sides); + if (r1=len(v)? _acc : - sum(v, _i=_i+1, _acc=is_undef(_acc)? v[_i] : _acc+v[_i]); +function sum(v, dflt=0, _i=0, _acc) = + _i>=len(v)? (len(v)? _acc : dflt) : + sum(v, dflt=dflt, _i=_i+1, _acc=is_undef(_acc)? v[_i] : _acc+v[_i]); // Function: cumsum() diff --git a/shapes2d.scad b/shapes2d.scad index d6e21225..d4d08f69 100644 --- a/shapes2d.scad +++ b/shapes2d.scad @@ -1010,5 +1010,331 @@ module supershape(step=0.5,m1=4,m2=undef,n1,n2=undef,n3=undef,a=1,b=undef, r=und polygon(supershape(step=step,m1=m1,m2=m2,n1=n1,n2=n2,n3=n3,a=a,b=b, r=r,d=d, anchor=anchor, spin=spin)); +// Section: 2D Masking Shapes + +// Function&Module: mask2d_roundover() +// Usage: +// mask2d_roundover(r|d, [inset], [excess]); +// Description: +// Creates a 2D roundover/bead mask shape that is useful for extruding into a 3D mask for a 90º edge. +// This 2D mask is designed to be differenced away from the edge of a shape that is in the first (X+Y+) quadrant. +// If called as a function, this just returns a 2D path of the outline of the mask shape. +// Arguments: +// r = Radius of the roundover. +// d = Diameter of the roundover. +// inset = Optional bead inset size. Default: 0 +// excess = Extra amount of mask shape to creates on the X- and Y- sides of the shape. +// Example(2D): 2D Roundover Mask +// mask2d_roundover(r=10); +// Example(2D): 2D Bead Mask +// mask2d_roundover(r=10,inset=2); +// Example: Masking by Edge Attachment +// diff("mask") +// cube([50,60,70],center=true) +// edge_profile([TOP,"Z"],except=[BACK,TOP+LEFT]) +// mask2d_roundover(r=10, inset=2); +module mask2d_roundover(r, d, excess, inset=0) { + polygon(mask2d_roundover(r=r,d=d,excess=excess,inset=inset)); +} + +function mask2d_roundover(r, d, excess, inset=0) = + assert(is_num(r)||is_num(d)) + assert(is_undef(excess)||is_num(excess)) + assert(is_num(inset)||(is_vector(inset)&&len(inset)==2)) + let( + inset = is_list(inset)? inset : [inset,inset], + excess = default(excess,$overlap), + r = get_radius(r=r,d=d,dflt=1), + steps = quantup(segs(r),4)/4, + step = 90/steps + ) [ + [-excess,-excess], [-excess, r+inset.y], + for (i=[0:1:steps]) [r,r] + inset + polar_to_xy(r,180+i*step), + [r+inset.x,-excess] + ]; + + +// Function&Module: mask2d_cove() +// Usage: +// mask2d_cove(r|d, [inset], [excess]); +// Description: +// Creates a 2D cove mask shape that is useful for extruding into a 3D mask for a 90º edge. +// This 2D mask is designed to be differenced away from the edge of a shape that is in the first (X+Y+) quadrant. +// If called as a function, this just returns a 2D path of the outline of the mask shape. +// Arguments: +// r = Radius of the cove. +// d = Diameter of the cove. +// inset = Optional amount to inset code from corner. Default: 0 +// excess = Extra amount of mask shape to creates on the X- and Y- sides of the shape. +// Example(2D): 2D Cove Mask +// mask2d_cove(r=10); +// Example(2D): 2D Inset Cove Mask +// mask2d_cove(r=10,inset=3); +// Example: Masking by Edge Attachment +// diff("mask") +// cube([50,60,70],center=true) +// edge_profile([TOP,"Z"],except=[BACK,TOP+LEFT]) +// mask2d_cove(r=10, inset=2); +module mask2d_cove(r, d, inset=0, excess) { + polygon(mask2d_cove(r=r,d=d,excess=excess,inset=inset)); +} + +function mask2d_cove(r, d, inset=0, excess) = + assert(is_num(r)||is_num(d)) + assert(is_undef(excess)||is_num(excess)) + assert(is_num(inset)||(is_vector(inset)&&len(inset)==2)) + let( + inset = is_list(inset)? inset : [inset,inset], + excess = default(excess,$overlap), + r = get_radius(r=r,d=d,dflt=1), + steps = quantup(segs(r),4)/4, + step = 90/steps + ) [ + [-excess,-excess], [-excess, r+inset.y], + for (i=[0:1:steps]) inset + polar_to_xy(r,90-i*step), + [r+inset.x,-excess] + ]; + + +// Function&Module: mask2d_chamfer() +// Usage: +// mask2d_chamfer(x|y|edge, [angle], [inset], [excess]); +// Description: +// Creates a 2D chamfer mask shape that is useful for extruding into a 3D mask for a 90º edge. +// This 2D mask is designed to be differenced away from the edge of a shape that is in the first (X+Y+) quadrant. +// If called as a function, this just returns a 2D path of the outline of the mask shape. +// Arguments: +// x = The width of the chamfer. +// y = The height of the chamfer. +// edge = The length of the edge of the chamfer. +// angle = The angle of the chamfer edge, away from vertical. Default: 45. +// inset = Optional amount to inset code from corner. Default: 0 +// excess = Extra amount of mask shape to creates on the X- and Y- sides of the shape. +// Example(2D): 2D Chamfer Mask +// mask2d_chamfer(x=10); +// Example(2D): 2D Chamfer Mask by Width. +// mask2d_chamfer(x=10, angle=30); +// Example(2D): 2D Chamfer Mask by Height. +// mask2d_chamfer(y=10, angle=30); +// Example(2D): 2D Inset Chamfer Mask +// mask2d_chamfer(x=10, inset=2); +// Example: Masking by Edge Attachment +// diff("mask") +// cube([50,60,70],center=true) +// edge_profile([TOP,"Z"],except=[BACK,TOP+LEFT]) +// mask2d_chamfer(x=10, inset=2); +module mask2d_chamfer(x, y, edge, angle=45, excess, inset=0) { + polygon(mask2d_chamfer(x=x, y=y, edge=edge, angle=angle, excess=excess, inset=inset)); +} + +function mask2d_chamfer(x, y, edge, angle=45, excess, inset=0) = + assert(num_defined([x,y,edge])==1) + assert(is_num(first_defined([x,y,edge]))) + assert(is_num(angle)) + assert(is_undef(excess)||is_num(excess)) + assert(is_num(inset)||(is_vector(inset)&&len(inset)==2)) + let( + inset = is_list(inset)? inset : [inset,inset], + excess = default(excess,$overlap), + x = !is_undef(x)? x : + !is_undef(y)? adj_ang_to_opp(adj=y,ang=angle) : + hyp_ang_to_opp(hyp=edge,ang=angle), + y = opp_ang_to_adj(opp=x,ang=angle) + ) [ + [-excess, -excess], [-excess, y+inset.y], + [inset.x, y+inset.y], [x+inset.x, inset.y], + [x+inset.x, -excess] + ]; + + +// Function&Module: mask2d_rabbet() +// Usage: +// mask2d_rabbet(size, [excess]); +// Description: +// Creates a 2D rabbet mask shape that is useful for extruding into a 3D mask for a 90º edge. +// This 2D mask is designed to be differenced away from the edge of a shape that is in the first (X+Y+) quadrant. +// If called as a function, this just returns a 2D path of the outline of the mask shape. +// Arguments: +// size = The size of the rabbet, either as a scalar or an [X,Y] list. +// inset = Optional amount to inset code from corner. Default: 0 +// excess = Extra amount of mask shape to creates on the X- and Y- sides of the shape. +// Example(2D): 2D Rabbet Mask +// mask2d_rabbet(size=10); +// Example(2D): 2D Asymmetrical Rabbet Mask +// mask2d_rabbet(size=[5,10]); +// Example: Masking by Edge Attachment +// diff("mask") +// cube([50,60,70],center=true) +// edge_profile([TOP,"Z"],except=[BACK,TOP+LEFT]) +// mask2d_rabbet(size=10); +module mask2d_rabbet(size, excess) { + polygon(mask2d_rabbet(size=size, excess=excess)); +} + +function mask2d_rabbet(size, excess) = + assert(is_num(size)||(is_vector(size)&&len(size)==2)) + assert(is_undef(excess)||is_num(excess)) + let( + excess = default(excess,$overlap), + size = is_list(size)? size : [size,size] + ) [ + [-excess, -excess], [-excess, size.y], size, [size.x, -excess] + ]; + + +// Function&Module: mask2d_dovetail() +// Usage: +// mask2d_dovetail(x|y|edge, [angle], [inset], [shelf], [excess]); +// Description: +// Creates a 2D dovetail mask shape that is useful for extruding into a 3D mask for a 90º edge. +// This 2D mask is designed to be differenced away from the edge of a shape that is in the first (X+Y+) quadrant. +// If called as a function, this just returns a 2D path of the outline of the mask shape. +// Arguments: +// x = The width of the dovetail. +// y = The height of the dovetail. +// edge = The length of the edge of the dovetail. +// angle = The angle of the chamfer edge, away from vertical. Default: 30. +// inset = Optional amount to inset code from corner. Default: 0 +// shelf = The extra height to add to the inside corner of the dovetail. Default: 0 +// excess = Extra amount of mask shape to creates on the X- and Y- sides of the shape. +// Example(2D): 2D Dovetail Mask +// mask2d_dovetail(x=10); +// Example(2D): 2D Dovetail Mask by Width. +// mask2d_dovetail(x=10, angle=30); +// Example(2D): 2D Dovetail Mask by Height. +// mask2d_dovetail(y=10, angle=30); +// Example(2D): 2D Inset Dovetail Mask +// mask2d_dovetail(x=10, inset=2); +// Example: Masking by Edge Attachment +// diff("mask") +// cube([50,60,70],center=true) +// edge_profile([TOP,"Z"],except=[BACK,TOP+LEFT]) +// mask2d_dovetail(x=10, inset=2); +module mask2d_dovetail(x, y, edge, angle=30, inset=0, shelf=0, excess) { + polygon(mask2d_dovetail(x=x, y=y, edge=edge, angle=angle, inset=inset, shelf=shelf, excess=excess)); +} + +function mask2d_dovetail(x, y, edge, angle=30, inset=0, shelf=0, excess) = + assert(num_defined([x,y,edge])==1) + assert(is_num(first_defined([x,y,edge]))) + assert(is_num(angle)) + assert(is_undef(excess)||is_num(excess)) + assert(is_num(inset)||(is_vector(inset)&&len(inset)==2)) + let( + inset = is_list(inset)? inset : [inset,inset], + excess = default(excess,$overlap), + x = !is_undef(x)? x : + !is_undef(y)? adj_ang_to_opp(adj=y,ang=angle) : + hyp_ang_to_opp(hyp=edge,ang=angle), + y = opp_ang_to_adj(opp=x,ang=angle) + ) [ + [-excess, 0], [-excess, y+inset.y+shelf], + inset+[x,y+shelf], inset+[x,y], inset, [inset.x,0] + ]; + + +// Function&Module: mask2d_ogee() +// Usage: +// mask2d_ogee(x|y|edge, [angle], [inset], [shelf], [excess]); +// +// Description: +// Creates a 2D Ogee mask shape that is useful for extruding into a 3D mask for a 90º edge. +// This 2D mask is designed to be `difference()`d away from the edge of a shape that is in the first (X+Y+) quadrant. +// Since there are a number of shapes that fall under the name ogee, the shape of this mask is given as a pattern. +// Patterns are given as TYPE, VALUE pairs. ie: `["fillet",10, "xstep",2, "step",[5,5], ...]`. See Patterns below. +// If called as a function, this just returns a 2D path of the outline of the mask shape. +// +// ### Patterns +// +// Type | Argument | Description +// -------- | --------- | ---------------- +// "step" | [x,y] | Makes a line to a point `x` right and `y` down. +// "xstep" | dist | Makes a `dist` length line towards X+. +// "ystep" | dist | Makes a `dist` length line towards Y-. +// "round" | radius | Makes an arc that will mask a roundover. +// "fillet" | radius | Makes an arc that will mask a fillet. +// +// Arguments: +// pattern = A list of pattern pieces to describe the Ogee. +// excess = Extra amount of mask shape to creates on the X- and Y- sides of the shape. +// +// Example(2D): 2D Ogee Mask +// mask2d_ogee([ +// "xstep",1, "ystep",1, // Starting shoulder. +// "fillet",5, "round",5, // S-curve. +// "ystep",1, "xstep",1 // Ending shoulder. +// ]); +module mask2d_ogee(pattern, excess) { + polygon(mask2d_ogee(pattern, excess=excess)); +} + +function mask2d_ogee(pattern, excess) = + assert(is_list(pattern)) + assert(len(pattern)>0) + assert(len(pattern)%2==0,"pattern must be a list of TYPE, VAL pairs.") + assert(all([for (i = idx(pattern,step=2)) in_list(pattern[i],["step","xstep","ystep","round","fillet"])])) + let( + excess = default(excess,$overlap), + x = concat([0], cumsum([ + for (i=idx(pattern,step=2)) let( + type = pattern[i], + val = pattern[i+1] + ) ( + type=="step"? val.x : + type=="xstep"? val : + type=="round"? val : + type=="fillet"? val : + 0 + ) + ])), + y = concat([0], cumsum([ + for (i=idx(pattern,step=2)) let( + type = pattern[i], + val = pattern[i+1] + ) ( + type=="step"? val.y : + type=="ystep"? val : + type=="round"? val : + type=="fillet"? val : + 0 + ) + ])), + tot_x = select(x,-1), + tot_y = select(y,-1), + data = [ + for (i=idx(pattern,step=2)) let( + type = pattern[i], + val = pattern[i+1], + pt = [x[i/2], tot_y-y[i/2]] + ( + type=="step"? [val.x,-val.y] : + type=="xstep"? [val,0] : + type=="ystep"? [0,-val] : + type=="round"? [val,0] : + type=="fillet"? [0,-val] : + [0,0] + ) + ) [type, val, pt] + ], + path = [ + [tot_x,-excess], + [-excess,-excess], + [-excess,tot_y], + for (pat = data) each + pat[0]=="step"? [pat[2]] : + pat[0]=="xstep"? [pat[2]] : + pat[0]=="ystep"? [pat[2]] : + let( + r = pat[1], + steps = segs(abs(r)), + step = 90/steps + ) [ + for (i=[0:1:steps]) let( + a = pat[0]=="round"? (180+i*step) : (90-i*step) + ) pat[2] + abs(r)*[cos(a),sin(a)] + ] + ] + ) deduplicate(path); + + // vim: noexpandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap diff --git a/tests/test_math.scad b/tests/test_math.scad index 5a93cfcc..2103ed08 100644 --- a/tests/test_math.scad +++ b/tests/test_math.scad @@ -293,6 +293,8 @@ test_atanh(); module test_sum() { + assert(sum([]) == 0); + assert(sum([],dflt=undef) == undef); assert(sum([1,2,3]) == 6); assert(sum([-2,-1,0,1,2]) == 0); assert(sum([[1,2,3], [3,4,5], [5,6,7]]) == [9,12,15]); @@ -301,6 +303,7 @@ test_sum(); module test_cumsum() { + assert(cumsum([]) == []); assert(cumsum([1,1,1]) == [1,2,3]); assert(cumsum([2,2,2]) == [2,4,6]); assert(cumsum([1,2,3]) == [1,3,6]); diff --git a/version.scad b/version.scad index e67f65ec..8ae7b075 100644 --- a/version.scad +++ b/version.scad @@ -8,7 +8,7 @@ ////////////////////////////////////////////////////////////////////// -BOSL_VERSION = [2,0,117]; +BOSL_VERSION = [2,0,124]; // Section: BOSL Library Version Functions