diff --git a/paths.scad b/paths.scad index a439819..146191c 100644 --- a/paths.scad +++ b/paths.scad @@ -1246,5 +1246,82 @@ function _assemble_path_fragments(fragments, eps=EPSILON, _finished=[]) = ); +/// Internal function: _bend_path_corner() +/// Usage: +/// _bend_path_corner(three_point_path, [sharpness], [cutlimit], [splinesteps], [midpoint]); +/// Description: +/// Used by squircle() in shapes2d.scad and curvy_path() in rounding.scad +/// Given a path with three points [p1, p2, p3] (2D or 3D), return a subdivided path that curves around from p1 to p3, with the sharpness parameter determining how close to a perfect circle (sharpness=0) or the p2 corner (sharpness=1) the path is from the shortest leg, with the amount of corner lopped off limited by cutlimit. The longer leg is stretched appropriately. +/// The error in using a cubic bezier curve to approximate a circular arc is about 0.00026 for a unit circle, with zero error at the endpoint and the corner bisector. +/// Arguments: +/// p = List of 3 points [p1, p2, p3]. The points may be 2D or 3D. +/// sharpness = curve is circular (sharpness=0) or sharp to the corner (sharpness=1) or anywhere in between +/// cutlimit = optionally constrain the curved path to be no farther than this from the corner +/// splinesteps = number of steps to use for each half of the corner +function _bend_path_corner(p, sharpness=0.5, cutlimit=999999, splinesteps=10, midpoint=[true,true]) = + sharpness==1 || cutlimit==0 ? [p[1]] +: let( + p2 = p[1], + p1 = midpoint[0] ? 0.5*(p[0]+p2) : p[0], + p3 = midpoint[1] ? 0.5*(p2+p[2]) : p[2], + a0 = 0.5*vector_angle(p1, p2, p3), + d1 = norm(p1-p2), + d3 = norm(p3-p2), + tana = tan(a0), + rmin = min(d1, d3) * tana, + rmax = max(d1, d3) * tana, + // A "perfect" unit circle quadrant constructed from cubic bezier points [1,0], [1,d], [d,1], [0,1], with d=0.55228474983 has exact radius=1 at 0°, 45°, and 90°, with a maximum radius (at 22.5° and 67.5°) of 1.00026163152; nearly a perfect circle arc. + fleg = let(a2=a0*a0) + // model of "perfect" circle leg lengths for a bezier unit circle arc depending on arc angle a0; the model error is ~1e-5 + -4.4015E-08 * a2*a0 // tiny term, but reduces error by an order of magnitude + +0.0000113366 * a2 + -0.00680018 * a0 + +0.552244, + leglenmin = rmin * fleg, + leglenmax = rmax * fleg, + cp = circle_2tangents(rmin, p1, p2, p3)[0], // circle center + middir = unit(cp-p2), // unit vector from corner pointing to circle center + bzmid = cp - rmin*middir, // location of bezier point joining both halves of curve + maxcut = norm(bzmid-p2), // maximum possible distance from corner to curve + sharp = max(sharpness, 1-min(1, cutlimit/maxcut)), + + bzdist = maxcut * (1-sharp), // distance from corner to tip of curve + cornerlegmin = min(leglenmin, bzdist*tana), + cornerlegmax = min(leglenmax, bzdist*tana), + p21unit = unit(p1-p2), + p23unit = unit(p3-p2), + midto12unit = unit(p21unit-p23unit), + // bezier points around the corner p1,p2,p3 (p2 is the vertex): + // bz0 is p1 + // bz1 is on same leg as p1 + // bz2 is on line perpendicular to bisector for first half of curve + // bz3 is bezier start/end point on the corner bisector + // bz4 is on line perpendicular to bisector for second half of curve + // bz5 is on same leg as p3 + // bz6 is p3 + bz3 = p2 + middir * bzdist, // center control point + bz2 = bz3 + midto12unit*(d1=3, "path must have at least 3 points") +assert(is_undef(sharplen) || sharplen==pathlen, "if sharpness is an array, it must have the same length as path") +assert(is_def(sharplen) || sharpness>=0 && sharpness<=1, "sharpness must be in the range [0...1]") +let( + p = is_1region(path) ? path[0] : path, + istart = closed ? 0 : 1, + istop = closed ? pathlen-1 : pathlen-2, + cutlim = is_undef(cutlimit) ? 999999 : cutlimit, + sharpvec = is_num(sharpness) ? repeat(sharpness, pathlen) : sharpness, + // bend_path_corner() is in paths.scad + roundpath = closed ? [ + for(i=[0:pathlen-1]) + _bend_path_corner(select(p,[i-1:i+1]), sharpvec[i], cutlim, splinesteps) + ] + : [ for(i=[1:pathlen-2]) + _bend_path_corner(select(p,[i-1:i+1]), sharpvec[i], cutlim, splinesteps, midpoint=[i>1,i= 0 && squareness <= 1); +module squircle(size, sharpness=0.5, style="fg", anchor=CENTER, spin=0, atype="box" ) { + check = assert(sharpness >= 0 && sharpness <= 1); anchorchk = assert(in_list(atype, ["box", "perim"])); size = is_num(size) ? [size,size] : point2d(size); assert(all_positive(size), "All components of size must be positive."); - path = squircle(size, squareness, style, atype="box"); + path = squircle(size, sharpness, style, atype="box"); if (atype == "box") { attachable(anchor, spin, two_d=true, size=size, extent=false) { polygon(path); @@ -1814,23 +1823,24 @@ module squircle(size, squareness=0.5, style="fg", anchor=CENTER, spin=0, atype=" } -function squircle(size, squareness=0.5, style="fg", anchor=CENTER, spin=0, atype="box") = - assert(squareness >= 0 && squareness <= 1) +function squircle(size, sharpness=0.5, style="fg", anchor=CENTER, spin=0, atype="box") = + assert(sharpness >= 0 && sharpness <= 1) assert(is_num(size) || is_vector(size,2)) assert(in_list(atype, ["box", "perim"])) let( size = is_num(size) ? [size,size] : point2d(size), - path = style == "fg" ? _squircle_fg(size, squareness) - : style == "superellipse" ? _squircle_se(size, squareness) + path = style == "fg" ? _squircle_fg(size, sharpness) + : style == "superellipse" ? _squircle_se(size, sharpness) + : style == "bezier" ? _squircle_bz(size, sharpness) : assert(false, "Style must be \"fg\" or \"superellipse\"") ) reorient(anchor, spin, two_d=true, size=atype=="box"?size:undef, path=atype=="box"?undef:path, p=path, extent=true); /* FG squircle functions */ -function _squircle_fg(size, squareness) = [ +function _squircle_fg(size, sharpness) = [ let( - sq = _linearize_squareness(squareness), + sq = _linearize_sharpness(sharpness), size = is_num(size) ? [size,size] : point2d(size), aspect = size[1] / size[0], r = 0.5 * size[0], @@ -1841,11 +1851,11 @@ function _squircle_fg(size, squareness) = [ ) p*[cos(theta), aspect*sin(theta)] ]; -function squircle_radius_fg(squareness, r, angle) = let( - s2a = abs(squareness*sin(2*angle)) +function squircle_radius_fg(sharpness, r, angle) = let( + s2a = abs(sharpness*sin(2*angle)) ) s2a>0 ? r*sqrt(2)/s2a * sqrt(1 - sqrt(1 - s2a*s2a)) : r; -function _linearize_squareness(s) = +function _linearize_sharpness(s) = // from Chamberlain Fong (2016). "Squircular Calculations". arXiv. // https://arxiv.org/pdf/1604.02174v5 let(c = 2 - 2*sqrt(2), d = 1 - 0.5*c*s) @@ -1854,14 +1864,14 @@ function _linearize_squareness(s) = /* Superellipse squircle functions */ -function _squircle_se(size, squareness) = [ +function _squircle_se(size, sharpness) = [ let( - n = _squircle_se_exponent(squareness), + n = _squircle_se_exponent(sharpness), size = is_num(size) ? [size,size] : point2d(size), ra = 0.5*size[0], rb = 0.5*size[1], astep = $fn>=12 ? 90/round($fn/4) : 360/48, - fgsq = _linearize_squareness(min(0.998,squareness)) // works well for distributing theta + fgsq = _linearize_sharpness(min(0.998,sharpness)) // works well for distributing theta ) for(a=[360:-astep:0.01]) let( theta = a + fgsq*sin(4*a)*30/PI, // tighter angle steps at corners x = cos(theta), @@ -1876,14 +1886,25 @@ function squircle_radius_se(n, r, angle) = let( y = sin(angle) ) (abs(x)^n + abs(y)^n)^(1/n) / r; -function _squircle_se_exponent(squareness) = let( - // limit squareness; error if >0.99889, limit is smaller for r>1 - s=min(0.998,squareness), +function _squircle_se_exponent(sharpness) = let( + // limit sharpness; error if >0.99889, limit is smaller for r>1 + s=min(0.998,sharpness), rho = 1 + s*(sqrt(2)-1), x = rho / sqrt(2) ) log(0.5) / log(x); +/* Bezier squircle function */ + +function _squircle_bz(size, sharpness) = let( + splinesteps = $fn>=12 ? round($fn/4) : 10, + size = is_num(size) ? [size,size] : point2d(size), + sq = square(size, center=true), + roundpath = [ for(i=[0:3]) + _bend_path_corner(select(sq,[i-1:i+1]), sharpness, 999999, splinesteps) + ] +) flatten(roundpath); + // Function&Module: keyhole() // Synopsis: Creates a 2D keyhole shape.