diff --git a/attachments.scad b/attachments.scad index de6135a..835c3f6 100644 --- a/attachments.scad +++ b/attachments.scad @@ -1731,33 +1731,48 @@ module face_profile(faces=[], r, d, excess=0.01, convexity=10) { module edge_profile(edges=EDGES_ALL, except=[], excess=0.01, convexity=10) { req_children($children); check1 = assert($parent_geom != 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] - ]; + conoid = $parent_geom[0] == "conoid"; + edges = !conoid? _edges(edges, except=except) : + edges==EDGES_ALL? [TOP,BOT] : + assert(all([for (e=edges) in_list(e,[TOP,BOT])]), "Invalid conoid edge spec.") + edges; + vecs = conoid + ? [for (e=edges) e+FWD] + : [ + for (i = [0:3], axis=[0:2]) + if (edges[axis][i]>0) + EDGE_OFFSETS[axis][i] + ]; all_vecs_are_edges = all([for (vec = vecs) sum(v_abs(vec))==2]); check2 = assert(all_vecs_are_edges, "All vectors must be edges."); + default_tag("remove") for ($idx = idx(vecs)) { vec = vecs[$idx]; anch = _find_anchor(vec, $parent_geom); + path_angs_T = _attach_geom_edge_path($parent_geom, vec); + path = path_angs_T[0]; + vecs = path_angs_T[1]; + post_T = path_angs_T[2]; $attach_to = undef; $attach_anchor = anch; $attach_norot = true; $profile_type = "edge"; - psize = point3d($parent_size); - length = [for (i=[0:2]) if(!vec[i]) psize[i]][0] + excess; - rotang = - vec.z<0? [90,0,180+v_theta(vec)] : - vec.z==0 && sign(vec.x)==sign(vec.y)? 135+v_theta(vec) : - vec.z==0 && sign(vec.x)!=sign(vec.y)? [0,180,45+v_theta(vec)] : - [-90,0,180+v_theta(vec)]; - translate(anch[1]) { - rot(rotang) { - linear_extrude(height=length, center=true, convexity=convexity) { - if ($tag=="") tag("remove") children(); - else children(); + multmatrix(post_T) { + for (i = idx(path,e=-2)) { + pt1 = select(path,i); + pt2 = select(path,i+1); + cp = (pt1 + pt2) / 2; + v1 = vecs[i][0]; + v2 = vecs[i][1]; + $edge_angle = 180 - vector_angle(v1,v2); + if (!approx(pt1,pt2)) { + seglen = norm(pt2-pt1) + 2 * excess; + move(cp) { + frame_map(y=-v1, z=unit(pt2-pt1)) { + linear_extrude(height=seglen, center=true, convexity=convexity) + children(); + } + } } } } @@ -1787,6 +1802,7 @@ module edge_profile(edges=EDGES_ALL, except=[], excess=0.01, convexity=10) { // convexity = Max number of times a line could intersect the perimeter of the mask shape. Default: 10 // flip = If true, reverses the orientation of any external profile parts at each edge. Default false // corner_type = Specifies how exterior corners should be formed. Must be one of `"none"`, `"chamfer"`, `"round"`, or `"sharp"`. Default: `"none"` +// size = If given the width and height of the 2D profile, will enable rounding and chamfering of internal corners when given a negative profile. // Side Effects: // Tags the children with "remove" (and hence sets `$tag`) if no tag is already set. // `$idx` is set to the index number of each edge. @@ -1837,17 +1853,43 @@ module edge_profile(edges=EDGES_ALL, except=[], excess=0.01, convexity=10) { // Example: More complicated edge sets // cuboid(50) { // edge_profile_asym( -// "ALL", except=[TOP+FWD+RIGHT, BOT+BACK+LEFT], -// corner_type="chamfer" +// [FWD,BACK,BOT+RIGHT], except=[FWD+RIGHT,BOT+BACK], +// corner_type="round" +// ) xflip() mask2d_roundover(10); +// } +// Example: Mixing it up a bit. +// diff() +// cuboid(60) { +// tag("keep") edge_profile_asym(LEFT, flip=true, corner_type="chamfer") +// xflip() mask2d_chamfer(10); +// edge_profile_asym(RIGHT) +// mask2d_roundover(10); +// } +// Example: Chamfering internal corners. +// cuboid(40) { +// edge_profile_asym( +// [FWD+DOWN,FWD+LEFT], +// corner_type="chamfer", size=[7,10] +// ) xflip() mask2d_chamfer(10); +// } +// Example: Rounding internal corners. +// cuboid(40) { +// edge_profile_asym( +// [FWD+DOWN,FWD+LEFT], +// corner_type="round", size=[10,10] // ) xflip() mask2d_roundover(10); // } -module edge_profile_asym(edges=EDGES_ALL, except=[], excess=0.01, convexity=10, flip=false, corner_type="none") { +module edge_profile_asym( + edges=EDGES_ALL, except=[], + excess=0.01, convexity=10, + flip=false, corner_type="none", + size=[0,0] +) { function _corner_orientation(pos,pvec) = let( j = [for (i=[0:2]) if (pvec[i]) i][0], - T = - (pos.x>0? xflip() : ident(4)) * + T = (pos.x>0? xflip() : ident(4)) * (pos.y>0? yflip() : ident(4)) * (pos.z>0? zflip() : ident(4)) * rot(-120*(2-j), v=[1,1,1]) @@ -1989,6 +2031,7 @@ module edge_profile_asym(edges=EDGES_ALL, except=[], excess=0.01, convexity=10, check2 = assert(all_vecs_are_edges, "All vectors must be edges."); edge_corners = [for (vec = vecs) [vec, _edge_corner_numbers(vec)]]; edge_strings = _gather_contiguous_edges(edge_corners); + default_tag("remove") for (edge_string = edge_strings) { inverts = _edge_transition_inversions(edge_string); flipverts = [for (x = inverts) flip? !x : x]; @@ -2006,11 +2049,37 @@ module edge_profile_asym(edges=EDGES_ALL, except=[], excess=0.01, convexity=10, vp2 = select(vecpairs,i); pvec = _edge_pair_perp_vec(e1,e2); pos = [for (i=[0:2]) e1[i]? e1[i] : e2[i]]; - if (vp1.y == vp2.y) { - default_tag("remove") - position(pos) { - mirT = _corner_orientation(pos, pvec); - multmatrix(mirT) { + mirT = _corner_orientation(pos, pvec); + $attach_to = undef; + $attach_anchor = _find_anchor(pos, $parent_geom); + $attach_norot = true; + $profile_type = "corner"; + position(pos) { + multmatrix(mirT) { + if (vp1.x == vp2.x && size.y > 0) { + zflip() { + if (corner_type=="chamfer") { + fn = $fn; + move([size.y,size.y]) { + rotate_extrude(angle=90, $fn=4) + left_half(planar=true, $fn=fn) + zrot(-90) fwd(size.y) children(); + } + linear_extrude(height=size.x) { + mask2d_roundover(size.y, inset=0.01, $fn=4); + } + } else if (corner_type=="round") { + move([size.y,size.y]) { + rotate_extrude(angle=90) + left_half(planar=true) + zrot(-90) fwd(size.y) children(); + } + linear_extrude(height=size.x) { + mask2d_roundover(size.y, inset=0.01); + } + } + } + } else if (vp1.y == vp2.y) { if (corner_type=="chamfer") { fn = $fn; rotate_extrude(angle=90, $fn=4) @@ -2038,6 +2107,10 @@ module edge_profile_asym(edges=EDGES_ALL, except=[], excess=0.01, convexity=10, } } for (i = idx(edge_string)) { + $attach_to = undef; + $attach_anchor = _find_anchor(edge_string[i], $parent_geom); + $attach_norot = true; + $profile_type = "edge"; edge_profile(edge_string[i], excess=excess, convexity=convexity) { if (flipverts[i]) { mirror([-1,1]) children(); @@ -2050,6 +2123,7 @@ module edge_profile_asym(edges=EDGES_ALL, except=[], excess=0.01, convexity=10, } + // Module: corner_profile() // Synopsis: Rotationally extrudes a 2d edge profile into corner mask on the given corners of the parent. // SynTags: Geom @@ -2886,6 +2960,101 @@ function _attach_geom_size(geom) = assert(false, "Unknown attachment geometry type."); + +/// Internal Function: _attach_geom_edge_path() +/// Usage: +/// angle = _attach_geom_edge_path(geom, edge); +/// Topics: Attachments +/// See Also: reorient(), attachable() +/// Description: +/// Returns the path and post-transform matrix of the indicated edge. +/// If the edge is invalid for the geometry, returns `undef`. +function _attach_geom_edge_path(geom, edge) = + assert(is_vector(edge),str("Invalid edge: edge=",edge)) + let( + type = geom[0], + cp = _get_cp(geom), + offset_raw = select(geom,-2), + offset = [for (i=[0:2]) edge[i]==0? 0 : offset_raw[i]], // prevents bad centering. + edge = point3d(edge) + ) + type == "prismoid"? ( //size, size2, shift, axis + let(all_comps_good = [for (c=edge) if (c!=sign(c)) 1]==[]) + assert(all_comps_good, "All components of an edge for a cuboid/prismoid must be -1, 0, or 1") + let(edge_good = len([for (c=edge) if(c) 1])==2) + assert(edge_good, "Invalid edge.") + let( + size = geom[1], + size2 = geom[2], + shift = point2d(geom[3]), + axis = point3d(geom[4]), + edge = rot(from=axis, to=UP, p=edge), + offset = rot(from=axis, to=UP, p=offset), + h = size.z, + cpos = function(vec) let( + u = (vec.z + 1) / 2, + siz = lerp(point2d(size), size2, u) / 2, + z = vec.z * h / 2, + pos = point3d(v_mul(siz, point2d(vec)) + shift * u, z) + ) pos, + ep1 = cpos([for (c=edge) c? c : -1]), + ep2 = cpos([for (c=edge) c? c : 1]), + cp = (ep1 + ep2) / 2, + axy = point2d(edge), + bot = point3d(v_mul(point2d(size )/2, axy), -h/2), + top = point3d(v_mul(point2d(size2)/2, axy) + shift, h/2), + xang = atan2(h,(top-bot).x), + yang = atan2(h,(top-bot).y), + vecs = [ + if (edge.x) yrot(90-xang, p=sign(axy.x)*RIGHT), + if (edge.y) xrot(yang-90, p=sign(axy.y)*BACK), + if (edge.z) [0,0,sign(edge.z)] + ], + segvec = cross(unit(vecs[1]), unit(vecs[0])), + seglen = norm(ep2 - ep1), + path = [ + cp - segvec * seglen/2, + cp + segvec * seglen/2 + ], + m = rot(from=UP,to=axis) * move(offset) + ) [path, [vecs], m] + ) : type == "conoid"? ( //r1, r2, l, shift, axis + assert(edge.z && edge.z == sign(edge.z), "The Z component of an edge for a cylinder/cone must be -1 or 1") + let( + rr1 = geom[1], + rr2 = geom[2], + l = geom[3], + shift = point2d(geom[4]), + axis = point3d(geom[5]), + r1 = is_num(rr1)? [rr1,rr1] : point2d(rr1), + r2 = is_num(rr2)? [rr2,rr2] : point2d(rr2), + edge = rot(from=axis, to=UP, p=edge), + offset = rot(from=axis, to=UP, p=offset), + maxr = max([each r1, each r2]), + sides = segs(maxr), + top = path3d(move(shift, p=ellipse(r=r2, $fn=sides)), l/2), + bot = path3d(ellipse(r=r1, $fn=sides), -l/2), + path = edge.z < 0 ? bot : top, + path2 = edge.z < 0 ? top : bot, + zed = edge.z<0? [0,0,-l/2] : point3d(shift,l/2), + vecs = [ + for (i = idx(top)) let( + pt1 = (path[i] + select(path,i+1)) /2, + pt2 = (path2[i] + select(path2,i+1)) /2, + v1 = unit(zed - pt1), + v2 = unit(pt2 - pt1), + v3 = unit(cross(v1,v2)), + v4 = cross(v3,v2), + v5 = cross(v1,v3) + ) [v4, v5] + ], + m = rot(from=UP,to=axis) * move(offset) + ) edge.z>0 + ? [reverse(list_wrap(path)), reverse(vecs), m] + : [list_wrap(path), vecs, m] + ) : undef; + + /// Internal Function: _attach_transform() /// Usage: To Get a Transformation Matrix /// mat = _attach_transform(anchor, spin, orient, geom); diff --git a/gears.scad b/gears.scad index dd23150..e431a78 100644 --- a/gears.scad +++ b/gears.scad @@ -284,6 +284,29 @@ module spur_gear( // spur_gear2d(pitch=5, teeth=20, pressure_angle=20); // Example(2D): Partial Gear // spur_gear2d(pitch=5, teeth=20, hide=15, pressure_angle=20); +// Example(2D): Planetary Gear Assembly +// rteeth=56; pteeth=16; cteeth=24; +// pitch=5; pa=20; +// prad = (pitch_radius(pitch,rteeth) + +// pitch_radius(pitch,cteeth)) / 2; +// rrad = outer_radius(pitch,rteeth,interior=true) + 5; +// difference() { +// circle(r=rrad); +// spur_gear2d( +// pitch=pitch, teeth=rteeth, +// pressure_angle=pa, interior=true); +// } +// for (a=[0:3]) { +// zrot(a*90) back(prad) { +// color("green") +// spur_gear2d( +// pitch=pitch, teeth=pteeth, +// pressure_angle=pa); +// } +// } +// color("orange") +// zrot(180/cteeth) +// spur_gear2d(pitch=pitch, teeth=cteeth, pressure_angle=pa); // Example(2D): Called as a Function // path = spur_gear2d(pitch=8, teeth=16); // polygon(path); diff --git a/masks2d.scad b/masks2d.scad index 960b145..96c2aea 100644 --- a/masks2d.scad +++ b/masks2d.scad @@ -15,22 +15,23 @@ // Section: 2D Masking Shapes // Function&Module: mask2d_roundover() -// Synopsis: Creates a 2D beading mask shape useful for rounding 90° edges. +// Synopsis: Creates a 2D beading mask shape useful for rounding edges. // SynTags: Geom, Path // Topics: Shapes (2D), Paths (2D), Path Generators, Attachable, Masks (2D) // See Also: corner_profile(), edge_profile(), face_profile(), fillet() // Usage: As module -// mask2d_roundover(r|d=, [inset], [excess]) [ATTACHMENTS]; +// mask2d_roundover(r|d=, [inset], [mask_angle], [excess]) [ATTACHMENTS]; // Usage: As function -// path = mask2d_roundover(r|d=, [inset], [excess]); +// path = mask2d_roundover(r|d=, [inset], [mask_angle], [excess]); // Description: -// Creates a 2D roundover/bead mask shape that is useful for extruding into a 3D mask for a 90° edge. -// Conversely, you can use that same extruded shape to make an interior fillet between two walls at a 90º angle. +// Creates a 2D roundover/bead mask shape that is useful for extruding into a 3D mask for an edge. +// Conversely, you can use that same extruded shape to make an interior fillet between two walls. // As a 2D mask, this 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. // inset = Optional bead inset size. Default: 0 +// mask_angle = Number of degrees in the corner angle to mask. Default: 90 // excess = Extra amount of mask shape to creates on the X- and Y- sides of the shape. Default: 0.01 // --- // d = Diameter of the roundover. @@ -40,6 +41,10 @@ // mask2d_roundover(r=10); // Example(2D): 2D Bead Mask // mask2d_roundover(r=10,inset=2); +// Example(2D): 2D Bead Mask for a Non-Right Edge. +// mask2d_roundover(r=10, inset=2, mask_angle=75); +// Example(2D): Increasing the Excess +// mask2d_roundover(r=10, inset=2, mask_angle=75, excess=2); // Example: Masking by Edge Attachment // diff() // cube([50,60,70],center=true) @@ -53,29 +58,36 @@ // xrot(90) // linear_extrude(height=30, center=true) // mask2d_roundover(r=10); -module mask2d_roundover(r, inset=0, excess=0.01, d, anchor=CENTER,spin=0) { - path = mask2d_roundover(r=r,d=d,excess=excess,inset=inset); +module mask2d_roundover(r, inset=0, mask_angle=90, excess=0.01, d, anchor=CENTER,spin=0) { + path = mask2d_roundover(r=r, d=d, inset=inset, mask_angle=mask_angle, excess=excess); attachable(anchor,spin, two_d=true, path=path) { polygon(path); children(); } } -function mask2d_roundover(r, inset=0, excess=0.01, d, anchor=CENTER,spin=0) = +function mask2d_roundover(r, inset=0, mask_angle=90, excess=0.01, d, anchor=CENTER, spin=0) = assert(is_finite(r)||is_finite(d)) assert(is_finite(excess)) + assert(is_finite(mask_angle) && mask_angle>0 && mask_angle<180) assert(is_finite(inset)||(is_vector(inset)&&len(inset)==2)) let( inset = is_list(inset)? inset : [inset,inset], r = get_radius(r=r,d=d,dflt=1), - steps = quantup(segs(r),4)/4, - step = 90/steps, - path = [ - [r+inset.x,-excess], + avec = polar_to_xy(inset.x,mask_angle-90), + line1 = [[0,inset.y], [100,inset.y]], + line2 = [avec, polar_to_xy(100,mask_angle)+avec], + corner = line_intersection(line1,line2), + arcpts = arc(r=r, corner=[line2.y, corner, line1.y]), + ipath = [ + arcpts[0] + polar_to_xy(inset.x+excess, mask_angle+90), + each arcpts, + last(arcpts) + polar_to_xy(inset.y+excess, -90), + [0,-excess], [-excess,-excess], - [-excess, r+inset.y], - for (i=[0:1:steps]) [r,r] + inset + polar_to_xy(r,180+i*step) - ] + [-excess,0] + ], + path = deduplicate(ipath) ) reorient(anchor,spin, two_d=true, path=path, extent=false, p=path); @@ -85,17 +97,18 @@ function mask2d_roundover(r, inset=0, excess=0.01, d, anchor=CENTER,spin=0) = // Topics: Shapes (2D), Paths (2D), Path Generators, Attachable, Masks (2D) // See Also: corner_profile(), edge_profile(), face_profile() // Usage: As module -// mask2d_cove(r|d=, [inset], [excess]) [ATTACHMENTS]; +// mask2d_cove(r|d=, [inset], [mask_angle], [excess]) [ATTACHMENTS]; // Usage: As function -// path = mask2d_cove(r|d=, [inset], [excess]); +// path = mask2d_cove(r|d=, [inset], [mask_angle], [excess]); // Description: -// Creates a 2D cove mask shape that is useful for extruding into a 3D mask for a 90° edge. -// Conversely, you can use that same extruded shape to make an interior rounded shelf decoration between two walls at a 90º angle. +// Creates a 2D cove mask shape that is useful for extruding into a 3D mask for an edge. +// Conversely, you can use that same extruded shape to make an interior rounded shelf decoration between two walls. // As a 2D mask, this 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. // inset = Optional amount to inset code from corner. Default: 0 +// mask_angle = Number of degrees in the corner angle to mask. Default: 90 // excess = Extra amount of mask shape to creates on the X- and Y- sides of the shape. Default: 0.01 // --- // d = Diameter of the cove. @@ -105,6 +118,10 @@ function mask2d_roundover(r, inset=0, excess=0.01, d, anchor=CENTER,spin=0) = // mask2d_cove(r=10); // Example(2D): 2D Inset Cove Mask // mask2d_cove(r=10,inset=3); +// Example(2D): 2D Inset Cove Mask for a Non-Right Edge +// mask2d_cove(r=10,inset=3,mask_angle=75); +// Example(2D): Increasing the Excess +// mask2d_cove(r=10,inset=3,mask_angle=75, excess=2); // Example: Masking by Edge Attachment // diff() // cube([50,60,70],center=true) @@ -118,29 +135,36 @@ function mask2d_roundover(r, inset=0, excess=0.01, d, anchor=CENTER,spin=0) = // xrot(90) // linear_extrude(height=30, center=true) // mask2d_cove(r=5, inset=5); -module mask2d_cove(r, inset=0, excess=0.01, d, anchor=CENTER,spin=0) { - path = mask2d_cove(r=r,d=d,excess=excess,inset=inset); +module mask2d_cove(r, inset=0, mask_angle=90, excess=0.01, d, anchor=CENTER, spin=0) { + path = mask2d_cove(r=r, d=d, inset=inset, mask_angle=mask_angle, excess=excess); attachable(anchor,spin, two_d=true, path=path) { polygon(path); children(); } } -function mask2d_cove(r, inset=0, excess=0.01, d, anchor=CENTER,spin=0) = +function mask2d_cove(r, inset=0, mask_angle=90, excess=0.01, d, anchor=CENTER, spin=0) = assert(is_finite(r)||is_finite(d)) + assert(is_finite(mask_angle) && mask_angle>0 && mask_angle<180) assert(is_finite(excess)) assert(is_finite(inset)||(is_vector(inset)&&len(inset)==2)) let( inset = is_list(inset)? inset : [inset,inset], r = get_radius(r=r,d=d,dflt=1), - steps = quantup(segs(r),4)/4, - step = 90/steps, - path = [ - [r+inset.x,-excess], + avec = polar_to_xy(inset.x,mask_angle-90), + line1 = [[0,inset.y], [100,inset.y]], + line2 = [avec, polar_to_xy(100,mask_angle)+avec], + corner = line_intersection(line1,line2), + arcpts = arc(r=r, cp=corner, start=mask_angle, angle=-mask_angle), + ipath = [ + arcpts[0] + polar_to_xy(inset.x+excess, mask_angle+90), + each arcpts, + last(arcpts) + polar_to_xy(inset.y+excess, -90), + [0,-excess], [-excess,-excess], - [-excess, r+inset.y], - for (i=[0:1:steps]) inset + polar_to_xy(r,90-i*step) - ] + [-excess,0] + ], + path = deduplicate(ipath) ) reorient(anchor,spin, two_d=true, path=path, p=path); @@ -230,16 +254,17 @@ function mask2d_chamfer(edge, angle=45, inset=0, excess=0.01, x, y, anchor=CENTE // Topics: Shapes (2D), Paths (2D), Path Generators, Attachable, Masks (2D) // See Also: corner_profile(), edge_profile(), face_profile() // Usage: As Module -// mask2d_rabbet(size, [excess]) [ATTACHMENTS]; +// mask2d_rabbet(size, [mask_angle], [excess]) [ATTACHMENTS]; // Usage: As Function -// path = mask2d_rabbet(size, [excess]); +// path = mask2d_rabbet(size, [mask_angle], [excess]); // Description: -// Creates a 2D rabbet mask shape that is useful for extruding into a 3D mask for a 90° edge. -// Conversely, you can use that same extruded shape to make an interior shelf decoration between two walls at a 90º angle. +// Creates a 2D rabbet mask shape that is useful for extruding into a 3D mask for an edge. +// Conversely, you can use that same extruded shape to make an interior shelf decoration between two walls. // As a 2D mask, this 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. +// mask_angle = Number of degrees in the corner angle to mask. Default: 90 // excess = Extra amount of mask shape to creates on the X- and Y- sides of the shape. Default: 0.01 // --- // anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `CENTER` @@ -248,6 +273,8 @@ function mask2d_chamfer(edge, angle=45, inset=0, excess=0.01, x, y, anchor=CENTE // mask2d_rabbet(size=10); // Example(2D): 2D Asymmetrical Rabbet Mask // mask2d_rabbet(size=[5,10]); +// Example(2D): 2D Mask for a Non-Right Edge +// mask2d_rabbet(size=10,mask_angle=75); // Example: Masking by Edge Attachment // diff() // cube([50,60,70],center=true) @@ -261,24 +288,31 @@ function mask2d_chamfer(edge, angle=45, inset=0, excess=0.01, x, y, anchor=CENTE // xrot(90) // linear_extrude(height=30, center=true) // mask2d_rabbet(size=[5,10]); -module mask2d_rabbet(size, excess=0.01, anchor=CENTER,spin=0) { - path = mask2d_rabbet(size=size, excess=excess); +module mask2d_rabbet(size, mask_angle=90, excess=0.01, anchor=CTR, spin=0) { + path = mask2d_rabbet(size=size, mask_angle=mask_angle, excess=excess); attachable(anchor,spin, two_d=true, path=path, extent=false) { polygon(path); children(); } } -function mask2d_rabbet(size, excess=0.01, anchor=CENTER,spin=0) = +function mask2d_rabbet(size, mask_angle=90, excess=0.01, anchor=CTR, spin=0) = assert(is_finite(size)||(is_vector(size)&&len(size)==2)) + assert(is_finite(mask_angle) && mask_angle>0 && mask_angle<180) assert(is_finite(excess)) let( size = is_list(size)? size : [size,size], + avec = polar_to_xy(size.x,mask_angle-90), + line1 = [[0,size.y], [100,size.y]], + line2 = [avec, polar_to_xy(100,mask_angle)+avec], + cp = line_intersection(line1,line2), path = [ - [size.x, -excess], - [-excess, -excess], - [-excess, size.y], - size + cp + polar_to_xy(size.x+excess, mask_angle+90), + cp, + cp + polar_to_xy(size.y+excess, -90), + [0,-excess], + [-excess,-excess], + [-excess,0] ] ) reorient(anchor,spin, two_d=true, path=path, extent=false, p=path); @@ -368,18 +402,19 @@ function mask2d_dovetail(edge, angle=30, inset=0, shelf=0, excess=0.01, x, y, an // Topics: Shapes (2D), Paths (2D), Path Generators, Attachable, Masks (2D), FDM Optimized // See Also: corner_profile(), edge_profile(), face_profile() // Usage: As Module -// mask2d_teardrop(r|d=, [angle], [excess]) [ATTACHMENTS]; +// mask2d_teardrop(r|d=, [angle], [mask_angle], [excess]) [ATTACHMENTS]; // Usage: As Function -// path = mask2d_teardrop(r|d=, [angle], [excess]); +// path = mask2d_teardrop(r|d=, [angle], [mask_angle], [excess]); // Description: -// Creates a 2D teardrop mask shape that is useful for extruding into a 3D mask for a 90° edge. -// Conversely, you can use that same extruded shape to make an interior teardrop fillet between two walls at a 90º angle. +// Creates a 2D teardrop mask shape that is useful for extruding into a 3D mask for an edge. +// Conversely, you can use that same extruded shape to make an interior teardrop fillet between two walls. // As a 2D mask, this 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. // This is particularly useful to make partially rounded bottoms, that don't need support to print. // Arguments: // r = Radius of the rounding. // angle = The maximum angle from vertical. +// mask_angle = Number of degrees in the corner angle to mask. Default: 90 // excess = Extra amount of mask shape to creates on the X- and Y- sides of the shape. Default: 0.01 // --- // d = Diameter of the rounding. @@ -387,6 +422,10 @@ function mask2d_dovetail(edge, angle=30, inset=0, shelf=0, excess=0.01, x, y, an // spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#subsection-spin). Default: `0` // Example(2D): 2D Teardrop Mask // mask2d_teardrop(r=10); +// Example(2D): 2D Teardrop Mask for a Non-Right Edge +// mask2d_teardrop(r=10, mask_angle=75); +// Example(2D): Increasing Excess +// mask2d_teardrop(r=10, mask_angle=75, excess=2); // Example(2D): Using a Custom Angle // mask2d_teardrop(r=10,angle=30); // Example: Masking by Edge Attachment @@ -402,25 +441,34 @@ function mask2d_dovetail(edge, angle=30, inset=0, shelf=0, excess=0.01, x, y, an // xrot(90) // linear_extrude(height=30, center=true) // mask2d_teardrop(r=10); -function mask2d_teardrop(r, angle=45, excess=0.01, d, anchor=CENTER, spin=0) = +function mask2d_teardrop(r, angle=45, mask_angle=90, excess=0.01, d, anchor=CENTER, spin=0) = assert(is_finite(angle)) assert(angle>0 && angle<90) + assert(is_finite(mask_angle) && mask_angle>0 && mask_angle<180) assert(is_finite(excess)) let( r = get_radius(r=r, d=d, dflt=1), - n = ceil(segs(r) * angle/360), - cp = [r,r], + avec = polar_to_xy(r,mask_angle-90), + line1 = [[0,r], [100,r]], + line2 = [avec, polar_to_xy(100,mask_angle)+avec], + cp = line_intersection(line1,line2), tp = cp + polar_to_xy(r,180+angle), bp = [tp.x+adj_ang_to_opp(tp.y,angle), 0], - step = angle/n, - path = [ - bp, bp-[0,excess], [-excess,-excess], [-excess,r], - for (i=[0:1:n]) cp+polar_to_xy(r,180+i*step) - ] + arcpts = arc(r=r, cp=cp, angle=[mask_angle+90,180+angle]), + ipath = [ + arcpts[0] + polar_to_xy(excess, mask_angle+90), + each arcpts, + bp, + bp + [0,-excess], + [0,-excess], + [-excess,-excess], + [-excess,0] + ], + path = deduplicate(ipath) ) reorient(anchor,spin, two_d=true, path=path, p=path); -module mask2d_teardrop(r, angle=45, excess=0.01, d, anchor=CENTER, spin=0) { - path = mask2d_teardrop(r=r, d=d, angle=angle, excess=excess); +module mask2d_teardrop(r, angle=45, mask_angle=90, excess=0.01, d, anchor=CENTER, spin=0) { + path = mask2d_teardrop(r=r, d=d, angle=angle, mask_angle=mask_angle, excess=excess); attachable(anchor,spin, two_d=true, path=path) { polygon(path); children(); diff --git a/tests/test_masks2d.scad b/tests/test_masks2d.scad index 13052db..ca4ccd4 100644 --- a/tests/test_masks2d.scad +++ b/tests/test_masks2d.scad @@ -19,24 +19,24 @@ test_mask2d_chamfer(); module test_mask2d_cove() { $fn = 24; - assert_approx(mask2d_cove(r=10),[[10,-0.01],[-0.01,-0.01],[-0.01,10],[0,10],[2.58819045103,9.65925826289],[5,8.66025403784],[7.07106781187,7.07106781187],[8.66025403784,5],[9.65925826289,2.58819045103],[10,0]]); - assert_approx(mask2d_cove(d=20),[[10,-0.01],[-0.01,-0.01],[-0.01,10],[0,10],[2.58819045103,9.65925826289],[5,8.66025403784],[7.07106781187,7.07106781187],[8.66025403784,5],[9.65925826289,2.58819045103],[10,0]]); - assert_approx(mask2d_cove(r=10,inset=1),[[11,-0.01],[-0.01,-0.01],[-0.01,11],[1,11],[3.58819045103,10.6592582629],[6,9.66025403784],[8.07106781187,8.07106781187],[9.66025403784,6],[10.6592582629,3.58819045103],[11,1]]); - assert_approx(mask2d_cove(d=20,inset=1),[[11,-0.01],[-0.01,-0.01],[-0.01,11],[1,11],[3.58819045103,10.6592582629],[6,9.66025403784],[8.07106781187,8.07106781187],[9.66025403784,6],[10.6592582629,3.58819045103],[11,1]]); - assert_approx(mask2d_cove(r=10,inset=1,excess=1),[[11,-1],[-1,-1],[-1,11],[1,11],[3.58819045103,10.6592582629],[6,9.66025403784],[8.07106781187,8.07106781187],[9.66025403784,6],[10.6592582629,3.58819045103],[11,1]]); - assert_approx(mask2d_cove(d=20,inset=1,excess=1),[[11,-1],[-1,-1],[-1,11],[1,11],[3.58819045103,10.6592582629],[6,9.66025403784],[8.07106781187,8.07106781187],[9.66025403784,6],[10.6592582629,3.58819045103],[11,1]]); + assert_approx(mask2d_cove(r=10),[[-0.01,10],[0,10],[3.09016994375,9.51056516295],[5.87785252292,8.09016994375],[8.09016994375,5.87785252292],[9.51056516295,3.09016994375],[10,0],[10,-0.01],[0,-0.01],[-0.01,-0.01],[-0.01,0]]); + assert_approx(mask2d_cove(d=20),[[-0.01,10],[0,10],[3.09016994375,9.51056516295],[5.87785252292,8.09016994375],[8.09016994375,5.87785252292],[9.51056516295,3.09016994375],[10,0],[10,-0.01],[0,-0.01],[-0.01,-0.01],[-0.01,0]]); + assert_approx(mask2d_cove(r=10,inset=1),[[-0.01,11],[1,11],[4.09016994375,10.510565163],[6.87785252292,9.09016994375],[9.09016994375,6.87785252292],[10.510565163,4.09016994375],[11,1],[11,-0.01],[0,-0.01],[-0.01,-0.01],[-0.01,0]]); + assert_approx(mask2d_cove(d=20,inset=1),[[-0.01,11],[1,11],[4.09016994375,10.510565163],[6.87785252292,9.09016994375],[9.09016994375,6.87785252292],[10.510565163,4.09016994375],[11,1],[11,-0.01],[0,-0.01],[-0.01,-0.01],[-0.01,0]]); + assert_approx(mask2d_cove(r=10,inset=1,excess=1),[[-1,11],[1,11],[4.09016994375,10.510565163],[6.87785252292,9.09016994375],[9.09016994375,6.87785252292],[10.510565163,4.09016994375],[11,1],[11,-1],[0,-1],[-1,-1],[-1,0]]); + assert_approx(mask2d_cove(d=20,inset=1,excess=1),[[-1,11],[1,11],[4.09016994375,10.510565163],[6.87785252292,9.09016994375],[9.09016994375,6.87785252292],[10.510565163,4.09016994375],[11,1],[11,-1],[0,-1],[-1,-1],[-1,0]]); } test_mask2d_cove(); module test_mask2d_roundover() { $fn = 24; - assert_approx(mask2d_roundover(r=10),[[10,-0.01],[-0.01,-0.01],[-0.01,10],[0,10],[0.340741737109,7.41180954897],[1.33974596216,5],[2.92893218813,2.92893218813],[5,1.33974596216],[7.41180954897,0.340741737109],[10,0]]); - assert_approx(mask2d_roundover(d=20),[[10,-0.01],[-0.01,-0.01],[-0.01,10],[0,10],[0.340741737109,7.41180954897],[1.33974596216,5],[2.92893218813,2.92893218813],[5,1.33974596216],[7.41180954897,0.340741737109],[10,0]]); - assert_approx(mask2d_roundover(r=10,inset=1),[[11,-0.01],[-0.01,-0.01],[-0.01,11],[1,11],[1.34074173711,8.41180954897],[2.33974596216,6],[3.92893218813,3.92893218813],[6,2.33974596216],[8.41180954897,1.34074173711],[11,1]]); - assert_approx(mask2d_roundover(d=20,inset=1),[[11,-0.01],[-0.01,-0.01],[-0.01,11],[1,11],[1.34074173711,8.41180954897],[2.33974596216,6],[3.92893218813,3.92893218813],[6,2.33974596216],[8.41180954897,1.34074173711],[11,1]]); - assert_approx(mask2d_roundover(r=10,inset=1,excess=1),[[11,-1],[-1,-1],[-1,11],[1,11],[1.34074173711,8.41180954897],[2.33974596216,6],[3.92893218813,3.92893218813],[6,2.33974596216],[8.41180954897,1.34074173711],[11,1]]); - assert_approx(mask2d_roundover(d=20,inset=1,excess=1),[[11,-1],[-1,-1],[-1,11],[1,11],[1.34074173711,8.41180954897],[2.33974596216,6],[3.92893218813,3.92893218813],[6,2.33974596216],[8.41180954897,1.34074173711],[11,1]]); + assert_approx(mask2d_roundover(r=10),[[-0.01,10],[-1.7763568394e-15,10],[0.489434837048,6.90983005625],[1.90983005625,4.12214747708],[4.12214747708,1.90983005625],[6.90983005625,0.489434837048],[10,-1.7763568394e-15],[10,-0.01],[0,-0.01],[-0.01,-0.01],[-0.01,0]]); + assert_approx(mask2d_roundover(d=20),[[-0.01,10],[-1.7763568394e-15,10],[0.489434837048,6.90983005625],[1.90983005625,4.12214747708],[4.12214747708,1.90983005625],[6.90983005625,0.489434837048],[10,-1.7763568394e-15],[10,-0.01],[0,-0.01],[-0.01,-0.01],[-0.01,0]]); + assert_approx(mask2d_roundover(r=10,inset=1),[[-0.01,11],[1,11],[1.48943483705,7.90983005625],[2.90983005625,5.12214747708],[5.12214747708,2.90983005625],[7.90983005625,1.48943483705],[11,1],[11,-0.01],[0,-0.01],[-0.01,-0.01],[-0.01,0]]); + assert_approx(mask2d_roundover(d=20,inset=1),[[-0.01,11],[1,11],[1.48943483705,7.90983005625],[2.90983005625,5.12214747708],[5.12214747708,2.90983005625],[7.90983005625,1.48943483705],[11,1],[11,-0.01],[0,-0.01],[-0.01,-0.01],[-0.01,0]]); + assert_approx(mask2d_roundover(r=10,inset=1,excess=1),[[-1,11],[1,11],[1.48943483705,7.90983005625],[2.90983005625,5.12214747708],[5.12214747708,2.90983005625],[7.90983005625,1.48943483705],[11,1],[11,-1],[0,-1],[-1,-1],[-1,0]]); + assert_approx(mask2d_roundover(d=20,inset=1,excess=1),[[-1,11],[1,11],[1.48943483705,7.90983005625],[2.90983005625,5.12214747708],[5.12214747708,2.90983005625],[7.90983005625,1.48943483705],[11,1],[11,-1],[0,-1],[-1,-1],[-1,0]]); } test_mask2d_roundover(); @@ -59,20 +59,20 @@ test_mask2d_dovetail(); module test_mask2d_rabbet() { - assert_approx(mask2d_rabbet(10), [[10,-0.01],[-0.01,-0.01],[-0.01,10],[10,10]]); - assert_approx(mask2d_rabbet(size=10), [[10,-0.01],[-0.01,-0.01],[-0.01,10],[10,10]]); - assert_approx(mask2d_rabbet(size=[10,15]), [[10,-0.01],[-0.01,-0.01],[-0.01,15],[10,15]]); - assert_approx(mask2d_rabbet(size=[10,15],excess=1), [[10,-1],[-1,-1],[-1,15],[10,15]]); + assert_approx(mask2d_rabbet(10), [[-0.01,10],[10,10],[10,-0.01],[0,-0.01],[-0.01,-0.01],[-0.01,0]]); + assert_approx(mask2d_rabbet(size=10), [[-0.01,10],[10,10],[10,-0.01],[0,-0.01],[-0.01,-0.01],[-0.01,0]]); + assert_approx(mask2d_rabbet(size=[10,15]), [[-0.01,15],[10,15],[10,-0.01],[0,-0.01],[-0.01,-0.01],[-0.01,0]]); + assert_approx(mask2d_rabbet(size=[10,15],excess=1), [[-1,15],[10,15],[10,-1],[0,-1],[-1,-1],[-1,0]]); } test_mask2d_rabbet(); module test_mask2d_teardrop() { $fn=24; - assert_approx(mask2d_teardrop(r=10), [[5.85786437627,0],[5.85786437627,-0.01],[-0.01,-0.01],[-0.01,10],[0,10],[0.340741737109,7.41180954897],[1.33974596216,5],[2.92893218813,2.92893218813]]); - assert_approx(mask2d_teardrop(d=20), [[5.85786437627,0],[5.85786437627,-0.01],[-0.01,-0.01],[-0.01,10],[0,10],[0.340741737109,7.41180954897],[1.33974596216,5],[2.92893218813,2.92893218813]]); - assert_approx(mask2d_teardrop(r=10,angle=30), [[4.2264973081,0],[4.2264973081,-0.01],[-0.01,-0.01],[-0.01,10],[0,10],[0.340741737109,7.41180954897],[1.33974596216,5]]); - assert_approx(mask2d_teardrop(r=10,angle=30,excess=1), [[4.2264973081,0],[4.2264973081,-1],[-1,-1],[-1,10],[0,10],[0.340741737109,7.41180954897],[1.33974596216,5]]); + assert_approx(mask2d_teardrop(r=10), [[-0.01,10],[0,10],[0.761204674887,6.17316567635],[2.92893218813,2.92893218813],[5.85786437627,0],[5.85786437627,-0.01],[0,-0.01],[-0.01,-0.01],[-0.01,0]]); + assert_approx(mask2d_teardrop(d=20), [[-0.01,10],[0,10],[0.761204674887,6.17316567635],[2.92893218813,2.92893218813],[5.85786437627,0],[5.85786437627,-0.01],[0,-0.01],[-0.01,-0.01],[-0.01,0]]); + assert_approx(mask2d_teardrop(r=10,angle=30), [[-0.01,10],[0,10],[0.340741737109,7.41180954897],[1.33974596216,5],[4.2264973081,0],[4.2264973081,-0.01],[0,-0.01],[-0.01,-0.01],[-0.01,0]]); + assert_approx(mask2d_teardrop(r=10,angle=30,excess=1), [[-1,10],[0,10],[0.340741737109,7.41180954897],[1.33974596216,5],[4.2264973081,0],[4.2264973081,-1],[0,-1],[-1,-1],[-1,0]]); } test_mask2d_teardrop();