From ed683bde031d974c8758e3f669256912c12fb938 Mon Sep 17 00:00:00 2001 From: Alex Matulich Date: Sun, 29 Dec 2024 21:06:35 -0800 Subject: [PATCH 1/9] added curvy_path as a complement to smooth_path, updated squircle() --- paths.scad | 77 +++++++++++++++++++++++++++++++++++++++++ rounding.scad | 96 +++++++++++++++++++++++++++++++++++++++++++++++++-- shapes2d.scad | 95 ++++++++++++++++++++++++++++++-------------------- 3 files changed, 228 insertions(+), 40 deletions(-) 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. From c0086bf830b4a4ad94d6fc1b165454031c944913 Mon Sep 17 00:00:00 2001 From: Alex Matulich Date: Sun, 29 Dec 2024 21:59:48 -0800 Subject: [PATCH 2/9] removed link to curvy_path in shapes2d.scad because it hasn't been processed in rounding.scad yet --- shapes2d.scad | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shapes2d.scad b/shapes2d.scad index ce4ffd7..4823c5e 100644 --- a/shapes2d.scad +++ b/shapes2d.scad @@ -1752,7 +1752,7 @@ module glued_circles(r, spread=10, tangent=30, d, anchor=CENTER, spin=0) { // Synopsis: Creates a shape between a circle and a square. // SynTags: Geom, Path // Topics: Shapes (2D), Paths (2D), Path Generators, Attachable -// See Also: circle(), square(), rect(), ellipse(), supershape(), curvy_path() +// See Also: circle(), square(), rect(), ellipse(), supershape() // Usage: As Module // squircle(size, [sharpness], [style=]) [ATTACHMENTS]; // Usage: As Function From 6967134404a5995b144bc043aff7fa1086c88bdf Mon Sep 17 00:00:00 2001 From: Alex Matulich Date: Sat, 4 Jan 2025 20:57:53 -0800 Subject: [PATCH 3/9] refactored to integrate curvy_path into smooth_path with new 'corners' method --- beziers.scad | 126 +++++++++++++++++++++++-- rounding.scad | 248 ++++++++++++++++++++++---------------------------- shapes2d.scad | 82 +++++++++-------- 3 files changed, 273 insertions(+), 183 deletions(-) diff --git a/beziers.scad b/beziers.scad index 9159a09..9738cab 100644 --- a/beziers.scad +++ b/beziers.scad @@ -559,16 +559,17 @@ function bezpath_length(bezpath, N=3, max_deflect=0.001) = // bezpath = path_to_bezpath(path, [closed], [tangents], [uniform], [size=]|[relsize=]); // Description: // Given a 2d or 3d input path and optional list 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`. The size or relsize parameter determines how far the curve can deviate from +// path that passes through every point on the input path and matches the tangent vectors. If you do not +// supply the tangents then they are computed using `path_tangents()` with `uniform=false` by default. +// Only the direction of the tangent vectors matter, not their magnitudes. +// If the path is closed, specify this by setting `closed=true`. +// The `size` or `relsize` parameter determines how far the curve can deviate from // the input path. In the case where the curve has a single hump, the size specifies the exact distance // between the specified path and the bezier. If you give relsize then it is relative to the segment // length (e.g. 0.05 means 5% of the segment length). In 2d when the bezier curve makes an S-curve // the size parameter specifies the sum of the deviations of the two peaks of the curve. In 3-space // the bezier curve may have three extrema: two maxima and one minimum. In this case the size specifies -// the sum of the maxima minus the minimum. If you do not supply the tangents then they are computed -// using `path_tangents()` with `uniform=false` by default. Tangents computed on non-uniform data tend +// the sum of the maxima minus the minimum. Tangents computed on non-uniform data tend // to display overshoots. See `smooth_path()` for examples. // Arguments: // path = 2D or 3D point list or 1-region that the curve must pass through @@ -633,10 +634,123 @@ function path_to_bezpath(path, closed, tangents, uniform=false, size, relsize) = second + L*tangent2 ], select(path,lastpt) - ]; +]; +/// Function: path_to_bezcornerpath() +/// Synopsis: Generates a bezier path tangent to all midpoints of the path segments, deviating from the corners by a specified amount or proportion. +/// SynTags: Path +/// Topics: Bezier Paths, Rounding +/// See Also: path_to_bezpath() +/// Usage: +/// bezpath = path_to_bezcornerpath(path, [closed], [size=]|[relsize=]); +/// Description: +/// Given a 2d or 3d input path, computes a cubic (degree 3) bezier path passing through, and tangent to, +/// every segment midpoint on the input path and deviating from the corners by a specified amount. +/// If the path is closed, specify this by setting `closed=true`. +/// The `size` or `relsize` parameter determines how far the curve can deviate from +/// the corners of the input path. The `size` parameter specifies the exact distance +/// between the specified path and the corner. If you give a `relsize` between 0 and 1, then it is +/// relative to the maximum distance from the corner that would produce a circular rounding, with 0 being +/// the actual corner and 1 being the circular rounding from the midpoint of the shortest leg of the corner. +/// For example, `relsize=0.25` means the "corner" of the rounded path is 25% of the distance from the path +/// corner to the theoretical circular rounding. +/// See `smooth_path()` for examples. +/// Arguments: +/// path = 2D or 3D point list or 1-region that the curve must pass through +/// closed = true if the curve is closed . Default: false +/// --- +/// size = absolute curve deviation from the corners, a number or vector +/// relsize = relative curve deviation (between 0 and 1) from the corners, a number or vector. Default: 0.5. +function path_to_bezcornerpath(path, closed, size, relsize) = + is_1region(path) ? path_to_bezcornerpath(path[0], default(closed,true), tangents, size, relsize) : + let(closed=default(closed,false)) + assert(is_bool(closed)) + assert(num_defined([size,relsize])<=1, "Can't define both size and relsize") + assert(is_path(path,[2,3]),"Input path is not a valid 2d or 3d path") + let( + curvesize = first_defined([size,relsize,0.5]), + relative = is_undef(size), + pathlen = len(path) + ) + assert(is_num(curvesize) || len(curvesize)==pathlen, str("Size or relsize must have length ",pathlen)) + let(sizevect = is_num(curvesize) ? repeat(curvesize, pathlen) : curvesize) + assert(min(sizevect)>0, "Size or relsize must be greater than zero") + let( + roundpath = closed ? [ + for(i=[0:pathlen-1]) let(p3=select(path,[i-1:i+1])) + _bez_path_corner([0.5*(p3[0]+p3[1]), p3[1], 0.5*(p3[1]+p3[2])], sizevect[i], relative), + [0.5*(path[0]+path[pathlen-1])] + ] + : [ for(i=[1:pathlen-2]) let(p3=select(path,[i-1:i+1])) + _bez_path_corner( + [i>1?0.5*(p3[0]+p3[1]):p3[0], p3[1], i0 +// color="green", closed=true, width=0.1); +// Example(2D): Settting uniform to true biases the tangents to align more with the line sides (applicable only to "edges" method). // color("green") // stroke(square([10,4]), closed=true, width=0.1); // stroke(smooth_path(square([10,4]),uniform=true, // relsize=0.1,closed=true), // width=0.1); -// Example(2D): A more interesting shape: +// Example(2D): A more interesting shape, comparing the "edges" method (yellow) with "corners" method (red). // path = [[0,0], [4,0], [7,14], [-3,12]]; // polygon(smooth_path(path,size=1,closed=true)); -// Example(2D): Here's the square again with less smoothing. -// polygon(smooth_path(square(4), size=.25,closed=true)); -// Example(2D): Here's the square with a size that's too big to achieve, so you get the maximum possible curve: -// color("green")stroke(square(4), width=0.1,closed=true); -// stroke(smooth_path(square(4), size=4, closed=true), -// closed=true,width=.1); -// Example(2D): You can alter the shape of the curve by specifying your own arbitrary tangent values +// color("red") polygon(smooth_path(path,method="corners",relsize=0.7,closed=true)); +// stroke(path, color="green", width=0.2, closed=true); +// Example(2D): Here's the square with a size that's too big to achieve, giving the the maximum possible curve with `method="edges"` (yellow). For `method="corners"` (red), the maximum possible distance from the corners is a circle. +// color("green")stroke(square(4), width=0.06,closed=true); +// stroke(smooth_path(square(4), method="edges", size=4, closed=true), +// closed=true, width=0.1); +// stroke(smooth_path(square(4), method="corners", size=4, closed=true), +// color="red", closed=true, width=0.1); +// Example(2D): For `method="edges"`, you can alter the shape of the curve by specifying your own arbitrary tangent values. Only the vector direction matters, not the vector length. // polygon(smooth_path(square(4), -// tangents=1.25*[[-2,-1], [-4,1], [1,2], [6,-1]], +// tangents=[[-2,-1], [-4,1], [1,2], [6,-1]], // size=0.4,closed=true)); -// Example(2D): Or you can give a different size for each segment +// Example(2D): You can give a different size for each segment ("edges" method in yellow) or corner ("corners" method in red). The first vertex of the square (green) is the lower right corner, and the first edge is the bottom segment. // polygon(smooth_path(square(4),size = [.4, .05, 1, .3], -// closed=true)); +// method="edges", closed=true)); +// color("red") +// polygon(smooth_path(square(4), size = [.4, .05, 1, .3], +// method="corners", closed=true)); +// stroke(square(4), color="green", width=0.03,closed=true); // Example(FlatSpin,VPD=35,VPT=[4.5,4.5,1]): Works on 3d paths also. // path = [[0,0,0],[3,3,2],[6,0,1],[9,9,0]]; // stroke(smooth_path(path,relsize=.1),width=.3); // color("red") for(p=path) translate(p) sphere(d=0.3); // stroke(path, width=0.1, color="red"); -// Example(2D): This shows the type of overshoot that can occur with uniform=true. You can produce overshoots like this if you supply a tangent that is difficult to connect to the adjacent points -// pts = [[-3.3, 1.7], [-3.7, -2.2], [3.8, -4.8], [-0.9, -2.4]]; -// stroke(smooth_path(pts, uniform=true, relsize=0.1),width=.1); -// color("red")move_copies(pts)circle(r=.15,$fn=12); -// Example(2D): With the default of uniform false no overshoot occurs. Note that the shape of the curve is quite different. -// pts = [[-3.3, 1.7], [-3.7, -2.2], [3.8, -4.8], [-0.9, -2.4]]; -// stroke(smooth_path(pts, uniform=false, relsize=0.1),width=.1); -// color("red")move_copies(pts)circle(r=.15,$fn=12); -module smooth_path(path, tangents, size, relsize, splinesteps=10, uniform=false, closed=false) {no_module();} -function smooth_path(path, tangents, size, relsize, splinesteps=10, uniform=false, closed) = - is_1region(path) ? smooth_path(path[0], tangents, size, relsize, splinesteps, uniform, default(closed,true)) : - let ( - bez = path_to_bezpath(path, tangents=tangents, size=size, relsize=relsize, uniform=uniform, closed=default(closed,false)), - smoothed = bezpath_curve(bez,splinesteps=splinesteps) - ) - closed ? list_unwrap(smoothed) : smoothed; - - - // Function: curvy_path() -// Synopsis: Create smoothed path that passes tangentially through all the midpoints of each segment of a given path. -// SynTags: Path -// Topics: Rounding, Paths -// See Also: round_corners(), smooth_path(), path_join(), offset_stroke(), squircle() -// Usage: -// curve = curvy_path(path, [sharpness], [cutlimit], [splinesteps=], [closed=]); -// Description: -// This is a complement to `smooth_path()`, which also smooths the input path using a cubic spline. However, instead of -// connecting each corner by a curved path, `curvy_path()` *replaces* every corner of the path by two cubic curves that join at -// the corner bisector, and are tangent to the midpoints of the segments making up the corner. -// The shortest leg of the corner determines the maximum radius of a circular corner for half the corner, and this half is -// circular when `sharpness=0`. The other half uses a bigger circle arc stretched to fit. The circle arc at `sharpness=0` -// is approximated by a cubic Bezier curve such that the maximum radial error is less than 0.0003 of the actual radius. -// . -// A sharply acute corner can result in much of the corner being cut off by the curve. -// An optional parameter `cutlimit` can be used to limit how much is cut from the corner vertex. -// Arguments: -// path = path to smooth -// sharpness = the sharpness of the curve from the midpoint of the shortest side of a corner to the corner bisector. When `sharpness=0`, the curve is circular. When `sharpness=1`, the curve matches the corner. Default: 0.5 -// --- -// cutlimit = if included, limits the depth of the amount "cut" from the curner by the curve. If the `sharpness` value results in a curve less than the cut limit, the curved corner is unaffected. A curved corner that cuts off more than this limit is stretched outward to the corner, to the cut limit. -// splinesteps = Number of steps for each bezier curve section (two per corner). Default: 10 -// closed = true if the curve is closed. If false, the curve starts from the start of the first segment and ends at the end of the last segment, rather than making curves from the midpoint of each segment as is done for corners. Default: false. -// Example(2D): Original path in green, curved path in yellow. Compare to `smooth_path()` Example 1 above. -// color("green")stroke(square(4), width=0.1); -// stroke(curvy_path(square(4), sharpness=0.2), width=0.1); -// Example(2D): Closing the path changes the end behavior. See also {{squircle()}}. -// color("green")stroke(square(4), width=0.1, closed=true); -// stroke(curvy_path(square(4), sharpness=0.2, closed=true), -// width=0.1, closed=true); -// Example(2D): Here's a wide rectangle. -// color("green") -// stroke(square([10,4]), closed=true, width=0.1); -// stroke(curvy_path(square([10,4]), sharpness=0.2, closed=true), -// width=0.1); -// Example(2D): In the top figure, the rounding cuts off a significant length of the upper right corner. Setting `cutlimit=3` in the bottom figure limits the amount cut from the corner to 3 units. -// shape = [[0,0], [10,0], [15,12], [6,5], [-1,7]]; -// polygon(curvy_path(shape, sharpness=0, closed=true)); -// color("red") down(.1) polygon(path2d(shape)); -// curve2 = curvy_path(shape, sharpness=0, cutlimit=2, closed=false); -// translate([0,-10,0]) { -// polygon(curvy_path(shape, sharpness=0, cutlimit=3, closed=true)); -// color("red") down(.1) polygon(path2d(shape)); -// } -// Example(2D): Comparison of `smooth_path()` (yellow) with `curvy_path()` (red), showing the different behaviors of each type of curve, with `smooth_path()` passing through the vertices of the polygon, and `curvy_path()` passing through the polygon's segment midpoint tangents. -// path = [[0,0], [4,0], [7,14], [-3,12]]; -// polygon(smooth_path(path,size=1,closed=true)); -// color("red") polygon(curvy_path(path,sharpness=0.3,closed=true)); -// stroke(path, color="green", width=0.2, closed=true); -// Example(2D): You can give a different sharpness for each corner. The first corner starts at the lower right, going around clockwise: -// polygon(curvy_path(square(4), -// sharpness = [0, 0.25, 0.75, 1], -// closed=true)); -// Example(FlatSpin,VPD=45: A curvy 3D path resembling a trefoil knot. +// Example(FlatSpin,VPD=45: Comparison of "edges" and "corners" 3D path resembling a [trefoil knot](https://en.wikipedia.org/wiki/Trefoil_knot). // shape = [[8.66, -5, -5], [8.66, 5, 5], [-2, 3.46, 0], // [-8.66, -5, -5], [0, -10, 5], [4, 0, 0], // [0, 10, -5], [-8.66, 5, 5], [-2, -3.46, 0]]; -// stroke(curvy_path(shape, sharpness=0, closed=true), closed=true); +// stroke(smooth_path(shape, method="corners", relsize=1, closed=true), color="red", closed=true, width=0.5); +// stroke(smooth_path(shape, method="edges", size=1.5, closed=true, splinesteps=20), closed=true, width=0.5); // stroke(shape, color="green", width=0.15, closed=true); - -module curvy_path(path, sharpness=0.5, splinesteps=10, cutlimit, closed=false) {no_module();} - -function curvy_path(path, sharpness=0.5, splinesteps=16, cutlimit, closed=false) = -let(pathlen = len(path), sharplen = is_num(sharpness) ? undef : len(sharpness)) -assert(pathlen>=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 +include + // Section: 2D Primitives @@ -1754,9 +1756,9 @@ module glued_circles(r, spread=10, tangent=30, d, anchor=CENTER, spin=0) { // Topics: Shapes (2D), Paths (2D), Path Generators, Attachable // See Also: circle(), square(), rect(), ellipse(), supershape() // Usage: As Module -// squircle(size, [sharpness], [style=]) [ATTACHMENTS]; +// squircle(size, [squareness], [style=]) [ATTACHMENTS]; // Usage: As Function -// path = squircle(size, [sharpness], [style=]); +// path = squircle(size, [squareness], [style=]); // Description: // A [squircle](https://en.wikipedia.org/wiki/Squircle) is a shape intermediate between a square/rectangle and a // circle/ellipse. Squircles are sometimes used to make dinner plates (more area for the same radius as a circle), keyboard @@ -1766,21 +1768,22 @@ module glued_circles(r, spread=10, tangent=30, d, anchor=CENTER, spin=0) { // (see {{supershape()}} Example 3, also known as the Lamé upper squircle), and a squircle constructed from Bezier curves. // They are visually almost indistinguishable, with the superellipse having slightly rounder "corners" than FG at the same // corner radius, and the Bezier version having slightly sharper corners. These squircles have different, unintuitive methods -// for controlling how square or circular the shape is. The `sharpness` parameter determines the shape, specifying the -// corner position linearly, with 0 giving the circle and 1 giving the square. Vertices are positioned to be more dense near -// the corners to preserve smoothness at low values of `$fn`. +// for controlling how square or circular the shape is. The `squareness` parameter determines the shape, specifying the +// corner position linearly, with 0 giving the circle and 1 giving the square. For the FG and superellipse squircles, +// vertices are positioned to be more dense near the corners to preserve smoothness at low values of `$fn`. // . // For the "superellipse" style, the special case where the superellipse exponent is 4 results in a squircle with corners at -// the geometric mean between radial points on the circle and square, corresponding to sharpness=0.456786. +// the geometric mean between radial points on the circle and square, corresponding to squareness=0.456786. // . -// For the "bezier" style with sharpness=0, the "circle" approximated by Bezier curves is exact every 45°, with a maximum -// error of 0.00026 times the radius in between those eight angles. +// For the "bezier" style with `squareness=0`, the ideal circular arc corner is closely approximated by Bezier curves. +// Unlike the other styles, when the `size` parameter defines a rectangle, the bezier style retains the the corner +// proportions for the short side of the corner rather than stretching the entire corner. // . -// When called as a module, creates a 2D squircle with the specified sharpness. +// When called as a module, creates a 2D squircle with the specified squareness. // When called as a function, returns a 2D path for a squircle. // Arguments: // size = Same as the `size` parameter in `square()`, can be a single number or a vector `[xsize,ysize]`. -// sharpness = Value between 0 and 1. Controls the shape, setting the location of a squircle "corner" at the specified interpolated position between a circle and a square. When `sharpness=0` the shape is a circle, and when `sharpness=1` the shape is a square. Default: 0.5 +// squareness = Value between 0 and 1. Controls the shape, setting the location of a squircle "corner" at the specified interpolated position between a circle and a square. When `squareness=0` the shape is a circle, and when `squareness=1` the shape is a square. Default: 0.5 // --- // style = method for generating a squircle, "fg" for Fernández-Guasti, "superellipse" for superellipse, or "bezier" for Bezier. Default: "fg" // anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `CENTER` @@ -1788,13 +1791,13 @@ module glued_circles(r, spread=10, tangent=30, d, anchor=CENTER, spin=0) { // atype = anchor type, "box" for bounding box corners and sides, "perim" for the squircle corners. Default: "box" // $fn = Number of points. The special variables `$fs` and `$fa` are ignored. If set, `$fn` must be 12 or greater, and is rounded to the nearest multiple of 4. Points are generated so they are more dense around sharper curves. Default if not set: 48 // Examples(2D): -// squircle(size=50, sharpness=0.4); +// squircle(size=50, squareness=0.4); // squircle([80,60], 0.7, $fn=64); -// Example(2D,VPD=48,VPR=[40,0,40],NoAxes): Corner differences between the three squircle styles for sharpness=0.5. Style "superellipse" is pink, "fg" is gold, "bezier" is blue. -// color("pink") squircle(size=50, style="superellipse", sharpness=0.5, $fn=256); -// color("yellow") up(1) squircle(size=50, style="fg", sharpness=0.5, $fn=256); -// color("lightblue") up(2) squircle(size=50, style="bezier", sharpness=0.5, $fn=256); -// Example(2D,VPD=265,NoAxes): Ten increments of sharpness parameter for a superellipse squircle +// Example(2D,VPD=48,VPR=[40,0,40],NoAxes): Corner differences between the three squircle styles for squareness=0.5. Style "superellipse" is pink, "fg" is gold, "bezier" is blue. +// color("pink") squircle(size=50, style="superellipse", squareness=0.5, $fn=256); +// color("yellow") up(1) squircle(size=50, style="fg", squareness=0.5, $fn=256); +// color("lightblue") up(2) squircle(size=50, style="bezier", squareness=0.5, $fn=256); +// Example(2D,VPD=265,NoAxes): Ten increments of squareness parameter for a superellipse squircle // color("green") for(sq=[0:0.1:1]) // stroke(squircle(100, sq, style="superellipse", $fn=96), closed=true, width=0.5); // Example(2D): Standard vector anchors are based on the bounding box @@ -1803,12 +1806,12 @@ module glued_circles(r, spread=10, tangent=30, d, anchor=CENTER, spin=0) { // squircle([60,40], 0.5, anchor=(BOTTOM+LEFT), atype="perim", spin=20) // show_anchors(); -module squircle(size, sharpness=0.5, style="fg", anchor=CENTER, spin=0, atype="box" ) { - check = assert(sharpness >= 0 && sharpness <= 1); +module squircle(size, squareness=0.5, style="fg", anchor=CENTER, spin=0, atype="box" ) { + check = assert(squareness >= 0 && squareness <= 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, sharpness, style, atype="box"); + path = squircle(size, squareness, style, atype="box"); if (atype == "box") { attachable(anchor, spin, two_d=true, size=size, extent=false) { polygon(path); @@ -1823,24 +1826,24 @@ module squircle(size, sharpness=0.5, style="fg", anchor=CENTER, spin=0, atype="b } -function squircle(size, sharpness=0.5, style="fg", anchor=CENTER, spin=0, atype="box") = - assert(sharpness >= 0 && sharpness <= 1) +function squircle(size, squareness=0.5, style="fg", anchor=CENTER, spin=0, atype="box") = + assert(squareness >= 0 && squareness <= 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, sharpness) - : style == "superellipse" ? _squircle_se(size, sharpness) - : style == "bezier" ? _squircle_bz(size, sharpness) + path = style == "fg" ? _squircle_fg(size, squareness) + : style == "superellipse" ? _squircle_se(size, squareness) + : style == "bezier" ? _squircle_bz(size, squareness) : 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, sharpness) = [ +function _squircle_fg(size, squareness) = [ let( - sq = _linearize_sharpness(sharpness), + sq = _linearize_squareness(squareness), size = is_num(size) ? [size,size] : point2d(size), aspect = size[1] / size[0], r = 0.5 * size[0], @@ -1851,11 +1854,11 @@ function _squircle_fg(size, sharpness) = [ ) p*[cos(theta), aspect*sin(theta)] ]; -function squircle_radius_fg(sharpness, r, angle) = let( - s2a = abs(sharpness*sin(2*angle)) +function squircle_radius_fg(squareness, r, angle) = let( + s2a = abs(squareness*sin(2*angle)) ) s2a>0 ? r*sqrt(2)/s2a * sqrt(1 - sqrt(1 - s2a*s2a)) : r; -function _linearize_sharpness(s) = +function _linearize_squareness(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) @@ -1864,14 +1867,14 @@ function _linearize_sharpness(s) = /* Superellipse squircle functions */ -function _squircle_se(size, sharpness) = [ +function _squircle_se(size, squareness) = [ let( - n = _squircle_se_exponent(sharpness), + n = _squircle_se_exponent(squareness), 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_sharpness(min(0.998,sharpness)) // works well for distributing theta + fgsq = _linearize_squareness(min(0.998,squareness)) // 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), @@ -1886,9 +1889,9 @@ function squircle_radius_se(n, r, angle) = let( y = sin(angle) ) (abs(x)^n + abs(y)^n)^(1/n) / r; -function _squircle_se_exponent(sharpness) = let( - // limit sharpness; error if >0.99889, limit is smaller for r>1 - s=min(0.998,sharpness), +function _squircle_se_exponent(squareness) = let( + // limit squareness; error if >0.99889, limit is smaller for r>1 + s=min(0.998,squareness), rho = 1 + s*(sqrt(2)-1), x = rho / sqrt(2) ) log(0.5) / log(x); @@ -1896,14 +1899,13 @@ function _squircle_se_exponent(sharpness) = let( /* Bezier squircle function */ -function _squircle_bz(size, sharpness) = let( +function _squircle_bz(size, squareness) = 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); + bez = path_to_bezcornerpath(sq, relsize=1-squareness, closed=true) +) bezpath_curve(bez, splinesteps=splinesteps); + // Function&Module: keyhole() From 6448782a5317473100bbc90faf520377a3c2a6fb Mon Sep 17 00:00:00 2001 From: Alex Matulich Date: Sat, 4 Jan 2025 21:05:44 -0800 Subject: [PATCH 4/9] Update paths.scad paths.scad should no longer be part of this PR, it should be unchanged. Removed stuff that was added in the first commit. --- paths.scad | 78 ------------------------------------------------------ 1 file changed, 78 deletions(-) diff --git a/paths.scad b/paths.scad index 146191c..eb04f18 100644 --- a/paths.scad +++ b/paths.scad @@ -1246,82 +1246,4 @@ 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 Date: Sat, 4 Jan 2025 21:08:13 -0800 Subject: [PATCH 5/9] Update paths.scad One more change to make paths.scad identical to original (added blank line) --- paths.scad | 1 + 1 file changed, 1 insertion(+) diff --git a/paths.scad b/paths.scad index eb04f18..a439819 100644 --- a/paths.scad +++ b/paths.scad @@ -1246,4 +1246,5 @@ function _assemble_path_fragments(fragments, eps=EPSILON, _finished=[]) = ); + // vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap From 652d404ff1205d7951c60b90ece107852ffdce2f Mon Sep 17 00:00:00 2001 From: Alex Matulich Date: Sat, 4 Jan 2025 22:20:17 -0800 Subject: [PATCH 6/9] minor copyedits to smooth_path documentation --- rounding.scad | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/rounding.scad b/rounding.scad index 72b1585..e944cae 100644 --- a/rounding.scad +++ b/rounding.scad @@ -605,7 +605,7 @@ function _rounding_offsets(edgespec,z_dir=1) = // Description: // Smooths the input path, creating a continuous curve using a cubic spline, using one of two methods. // . -// For `method="edges"`, every segment of the path is replaced by a cubic curve with `splinesteps` points, +// For `method="edges"`, every segment (edge) of the path is replaced by a cubic curve with `splinesteps` points, // and the cubic interpolation passes through every input point on the path, matching the tangents at every // point. If you do not specify `tangents`, they are computed using path_tangents with `uniform=false` by // default. Only the dirction of a tangent vector matters, not the vector length. @@ -613,24 +613,24 @@ function _rounding_offsets(edgespec,z_dir=1) = // produces curves that overshoot the point on the path. // . // For `method="corners"`, every corner of the path is replaced by two cubic curves, each with -// `splinesteps` points. The two curves are joined at the corner bisector, and the cubic interpolation -// is tangent to the midpoint of every segment. -// The `tangents` and `uniform` parameters don't apply to the "corners" method. Using `tangents` with "corners" causes an error. +// `splinesteps` points. The two curves are joined at the corner bisector, and the cubic interpolations +// are tangent to the midpoint of every segment. The `tangents` and `uniform` parameters don't apply to the +// "corners" method. Using `tangents` with "corners" causes an error. // . // The `size` or `relsize` parameters apply to both methods. They determine how far the curve can bend away // from the input path. In the case where the path has three non-collinear points, the size specifies the // exact distance between the specified path and the curve (maximum distance from edge if for the "edges" // method, or distance from corner with the "corners" method). -// In 2d when the spline may make an S-curve, for the "edges" method the size parameter specifies the sum +// In 2D when the spline may make an S-curve, for the "edges" method the size parameter specifies the sum // of the deviations of the two peaks of the curve. In 3-space the bezier curve may have three extrema: two // maxima and one minimum. In this case the size specifies the sum of the maxima minus the minimum. // . // If you give `relsize` instead, then for the "edges" method, the maximum deviation from the segment is // relative to the segment length (e.g. 0.05 means 5% of the segment length). For the "corners" method, -// `relsize` determines where the curve intersects the corner bisector relative to the maximum deviation +// `relsize` determines where the curve intersects the corner bisector, relative to the maximum deviation // possible (which corresponds to a circle rounding from the shortest leg of the corner). For example, -// `relsize=1` is the maximum deviation from the corner, and `relsize=0.5` causes the curve to intersect the -// corner bisector halfway between the maximum and the tip of the corner. +// `relsize=1` is the maximum deviation from the corner (a circle arc from the shortest leg), and `relsize=0.5` +// causes the curve to intersect the corner bisector halfway between the maximum and the tip of the corner. // . // At a given segment or corner (depending on the method) there is a maximum size: a size value that is too // large is rounded down. See also path_to_bezpath(). From 4940551b0590549174220a989d2bf54d92de16e7 Mon Sep 17 00:00:00 2001 From: Alex Matulich Date: Sun, 5 Jan 2025 05:52:39 -0800 Subject: [PATCH 7/9] fixed 2D example that should have been 3D in squircle --- shapes2d.scad | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shapes2d.scad b/shapes2d.scad index c01b597..a45fa2a 100644 --- a/shapes2d.scad +++ b/shapes2d.scad @@ -1793,7 +1793,7 @@ module glued_circles(r, spread=10, tangent=30, d, anchor=CENTER, spin=0) { // Examples(2D): // squircle(size=50, squareness=0.4); // squircle([80,60], 0.7, $fn=64); -// Example(2D,VPD=48,VPR=[40,0,40],NoAxes): Corner differences between the three squircle styles for squareness=0.5. Style "superellipse" is pink, "fg" is gold, "bezier" is blue. +// Example(3D,VPD=48,VPR=[40,0,40],NoAxes): Corner differences between the three squircle styles for squareness=0.5. Style "superellipse" is pink, "fg" is gold, "bezier" is blue. // color("pink") squircle(size=50, style="superellipse", squareness=0.5, $fn=256); // color("yellow") up(1) squircle(size=50, style="fg", squareness=0.5, $fn=256); // color("lightblue") up(2) squircle(size=50, style="bezier", squareness=0.5, $fn=256); From eb66ba3e9d377c7f91f50c11dc10da2d1a40f151 Mon Sep 17 00:00:00 2001 From: Alex Matulich Date: Sun, 5 Jan 2025 06:19:12 -0800 Subject: [PATCH 8/9] fixed description indentation error in rounding.scad --- rounding.scad | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/rounding.scad b/rounding.scad index e944cae..e314502 100644 --- a/rounding.scad +++ b/rounding.scad @@ -629,7 +629,7 @@ function _rounding_offsets(edgespec,z_dir=1) = // relative to the segment length (e.g. 0.05 means 5% of the segment length). For the "corners" method, // `relsize` determines where the curve intersects the corner bisector, relative to the maximum deviation // possible (which corresponds to a circle rounding from the shortest leg of the corner). For example, -// `relsize=1` is the maximum deviation from the corner (a circle arc from the shortest leg), and `relsize=0.5` +// `relsize=1` is the maximum deviation from the corner (a circle arc from the shortest leg), and `relsize=0.5` // causes the curve to intersect the corner bisector halfway between the maximum and the tip of the corner. // . // At a given segment or corner (depending on the method) there is a maximum size: a size value that is too @@ -3402,11 +3402,11 @@ Access to the derivative smoothing parameter? // aux="sphere",aux_r=-30,fillet=8, overlap=17); // } // Example(3D,VPT=[0.59633,-3.01826,-3.89606],VPR=[129.2,0,26.4],VPD=192.044,NoScales): Here we have rotated the auxiliary sphere which results in a hole that is off-center through the sphere. Because we rotate the auxiliary object, both ends of the prism have moved. Note that setting k to a large value better matches the bezier curve to the curvature of the sphere, resulting in a better result. -// difference(){ -// spheroid(r=30,circum=true); -// join_prism(circle(r=15),base="sphere",base_r=-30, n=15, -// aux="sphere",aux_T=xrot(30), aux_r=-30,fillet=8, overlap=17, k=0.9); -// } +// difference(){ +// spheroid(r=30,circum=true); +// join_prism(circle(r=15),base="sphere",base_r=-30, n=15, +// aux="sphere",aux_T=xrot(30), aux_r=-30,fillet=8, overlap=17, k=0.9); +// } // Example(3D,VPT=[-12.5956,-5.1125,-0.322237],VPR=[82.3,0,116.7],VPD=213.382,NoScales): Here we adjust just the auxiliary end, which note is at the bottom. We rotate it by 45 deg, but this rotation would normally be relative to the other prism end, so we add a centerpoint based on the radius so that the rotation is relative to the sphere center instead. // difference(){ // spheroid(r=30,circum=true); @@ -3414,14 +3414,14 @@ Access to the derivative smoothing parameter? // aux="sphere",prism_end_T=xrot(45,cp=[0,0,-30]), aux_r=-30,fillet=8, overlap=17, k=0.9); // } // Example(3D,NoScales,VPT=[12.3373,11.6037,-1.87883],VPR=[40.3,0,323.4],VPD=292.705): A diagonal hole through a cylinder with rounded ends, created by shifting the auxiliary prism end along the prism length. -// back_half(200) +// back_half(200) // difference(){ // right(15)xcyl(r=30,l=100,circum=true); // join_prism(circle(r=15),base="cyl",base_r=-30, n=15, // aux="cyl",prism_end_T=right(35),aux_r=-30,fillet=7, overlap=17); // } // Example(3D,NoScales,VPT=[-7.63774,-0.808304,13.8874],VPR=[46.6,0,71.2],VPD=237.091): A hole created by shifting along prism width. -// left_half() +// left_half() // difference(){ // xcyl(r=30,l=100,circum=true); // join_prism(circle(r=15),base="cyl",base_r=-30, n=15, From 11b801ec4691a4bcf3d46a1be7bf66bc1305fead Mon Sep 17 00:00:00 2001 From: Alex Matulich Date: Sun, 5 Jan 2025 08:06:52 -0800 Subject: [PATCH 9/9] added error check for 'uniform' parameter, reformatted some code --- beziers.scad | 53 +++++++++++++++++++++++++-------------------------- rounding.scad | 24 ++++++++++++----------- shapes2d.scad | 48 +++++++++++++++++++++++++++------------------- 3 files changed, 67 insertions(+), 58 deletions(-) diff --git a/beziers.scad b/beziers.scad index 9738cab..e6e8091 100644 --- a/beziers.scad +++ b/beziers.scad @@ -634,7 +634,7 @@ function path_to_bezpath(path, closed, tangents, uniform=false, size, relsize) = second + L*tangent2 ], select(path,lastpt) -]; + ]; @@ -666,30 +666,31 @@ function path_to_bezpath(path, closed, tangents, uniform=false, size, relsize) = function path_to_bezcornerpath(path, closed, size, relsize) = is_1region(path) ? path_to_bezcornerpath(path[0], default(closed,true), tangents, size, relsize) : let(closed=default(closed,false)) - assert(is_bool(closed)) - assert(num_defined([size,relsize])<=1, "Can't define both size and relsize") - assert(is_path(path,[2,3]),"Input path is not a valid 2d or 3d path") - let( - curvesize = first_defined([size,relsize,0.5]), - relative = is_undef(size), - pathlen = len(path) + assert(is_bool(closed)) + assert(num_defined([size,relsize])<=1, "Can't define both size and relsize") + assert(is_path(path,[2,3]),"Input path is not a valid 2d or 3d path") + let( + curvesize = first_defined([size,relsize,0.5]), + relative = is_undef(size), + pathlen = len(path) + ) + assert(is_num(curvesize) || len(curvesize)==pathlen, str("Size or relsize must have length ",pathlen)) + let(sizevect = is_num(curvesize) ? repeat(curvesize, pathlen) : curvesize) + assert(min(sizevect)>0, "Size or relsize must be greater than zero") + let( + roundpath = closed ? [ + for(i=[0:pathlen-1]) let(p3=select(path,[i-1:i+1])) + _bez_path_corner([0.5*(p3[0]+p3[1]), p3[1], 0.5*(p3[1]+p3[2])], sizevect[i], relative), + [0.5*(path[0]+path[pathlen-1])] + ] + : [ for(i=[1:pathlen-2]) let(p3=select(path,[i-1:i+1])) + _bez_path_corner( + [i>1?0.5*(p3[0]+p3[1]):p3[0], p3[1], i0, "Size or relsize must be greater than zero") - let( - roundpath = closed ? [ - for(i=[0:pathlen-1]) let(p3=select(path,[i-1:i+1])) - _bez_path_corner([0.5*(p3[0]+p3[1]), p3[1], 0.5*(p3[1]+p3[2])], sizevect[i], relative), - [0.5*(path[0]+path[pathlen-1])] - ] - : [ for(i=[1:pathlen-2]) let(p3=select(path,[i-1:i+1])) - _bez_path_corner( - [i>1?0.5*(p3[0]+p3[1]):p3[0], p3[1], i0 ? r*sqrt(2)/s2a * sqrt(1 - sqrt(1 - s2a*s2a)) : r; +function squircle_radius_fg(squareness, r, angle) = + let( + s2a = abs(squareness*sin(2*angle)) + ) + s2a>0 ? r*sqrt(2)/s2a * sqrt(1 - sqrt(1 - s2a*s2a)) : r; function _linearize_squareness(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) - 2 * sqrt((1+c)*s*s - c*s) / (d*d); + 2 * sqrt((1+c)*s*s - c*s) / (d*d); /* Superellipse squircle functions */ @@ -1884,27 +1886,33 @@ function _squircle_se(size, squareness) = [ ) [ra*x, rb*y] / r ]; -function squircle_radius_se(n, r, angle) = let( - x = cos(angle), - y = sin(angle) -) (abs(x)^n + abs(y)^n)^(1/n) / r; +function squircle_radius_se(n, r, angle) = + let( + x = cos(angle), + 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), - rho = 1 + s*(sqrt(2)-1), - x = rho / sqrt(2) -) log(0.5) / log(x); +function _squircle_se_exponent(squareness) = + let( + // limit squareness; error if >0.99889, limit is smaller for r>1 + s=min(0.998,squareness), + rho = 1 + s*(sqrt(2)-1), + x = rho / sqrt(2) + ) + log(0.5) / log(x); /* Bezier squircle function */ -function _squircle_bz(size, squareness) = let( - splinesteps = $fn>=12 ? round($fn/4) : 10, - size = is_num(size) ? [size,size] : point2d(size), - sq = square(size, center=true), - bez = path_to_bezcornerpath(sq, relsize=1-squareness, closed=true) -) bezpath_curve(bez, splinesteps=splinesteps); +function _squircle_bz(size, squareness) = + let( + splinesteps = $fn>=12 ? round($fn/4) : 10, + size = is_num(size) ? [size,size] : point2d(size), + sq = square(size, center=true), + bez = path_to_bezcornerpath(sq, relsize=1-squareness, closed=true) + ) + bezpath_curve(bez, splinesteps=splinesteps);