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)); -// Example(FlatSpin,VPD=35,VPT=[4.5,4.5,1]): Works on 3d paths as well +// 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); -// 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 +// color("red") for(p=path) translate(p) sphere(d=0.3); +// stroke(path, width=0.1, color="red"); +// 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(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); +// Example(2D): For the default "edges" method, 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. +// 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)) : +module smooth_path(path, tangents, size, relsize, method="edges", splinesteps=10, uniform=false, closed=false) {no_module();} +function smooth_path(path, tangents, size, relsize, method="edges", splinesteps=10, uniform=false, closed) = + is_1region(path) ? smooth_path(path[0], tangents, size, relsize, method, splinesteps, uniform, default(closed,true)) : + assert(method=="edges" || method=="corners", "method must be \"edges\" or \"corners\".") + assert(method=="edges" || is_undef(tangent), "The tangents parameter is incompatible with method=\"corners\".") let ( - bez = path_to_bezpath(path, tangents=tangents, size=size, relsize=relsize, uniform=uniform, closed=default(closed,false)), + bez = method=="edges" ? + path_to_bezpath(path, tangents=tangents, size=size, relsize=relsize, uniform=uniform, closed=default(closed,false)) + : path_to_bezcornerpath(path, size=size, relsize=relsize, closed=default(closed,false)), smoothed = bezpath_curve(bez,splinesteps=splinesteps) ) closed ? list_unwrap(smoothed) : smoothed; + function _scalar_to_vector(value,length,varname) = is_vector(value) ? assert(len(value)==length, str(varname," must be length ",length)) diff --git a/shapes2d.scad b/shapes2d.scad index ec7a5ed..c01b597 100644 --- a/shapes2d.scad +++ b/shapes2d.scad @@ -16,6 +16,8 @@ ////////////////////////////////////////////////////////////////////// use +include + // Section: 2D Primitives @@ -1747,6 +1749,7 @@ module glued_circles(r, spread=10, tangent=30, d, anchor=CENTER, spin=0) { } + // Function&Module: squircle() // Synopsis: Creates a shape between a circle and a square. // SynTags: Geom, Path @@ -1757,19 +1760,24 @@ module glued_circles(r, spread=10, tangent=30, d, anchor=CENTER, spin=0) { // Usage: As Function // 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 buttons, and smartphone -// icons. Old CRT television screens also resembled elongated squircles. +// 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 +// buttons, and smartphone icons. Old CRT television screens also resembled elongated squircles. // . -// Multiple definitions exist for the squircle. We support two versions: the Fernandez-Guasti squircle and the superellipse -// ({{supershape()}} Example 3, also known as the Lamé upper squircle), and the . They are visually almost indistinguishable, -// with the superellipse having slightly rounder "corners" than FG at the same corner radius. These two squircles have different, -// unintuitive methods 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. Vertices are positioned to be more dense near the -// corners to preserve smoothness at low values of `$fn`. +// Multiple definitions exist for the squircle. We support three versions: the Fernandez-Guasti squircle, the superellipse +// (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 `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 at the geometric mean -// between radial points on the circle and square, corresponding to squareness=0.456786. +// 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 squareness=0.456786. +// . +// 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 squareness. // When called as a function, returns a 2D path for a squircle. @@ -1777,7 +1785,7 @@ module glued_circles(r, spread=10, tangent=30, d, anchor=CENTER, spin=0) { // size = Same as the `size` parameter in `square()`, can be a single number or a vector `[xsize,ysize]`. // 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 and "superellipse" for superellipse. Default: "fg" +// 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` // spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#subsection-spin). Default: `0` // atype = anchor type, "box" for bounding box corners and sides, "perim" for the squircle corners. Default: "box" @@ -1785,6 +1793,10 @@ 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. +// 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); @@ -1822,6 +1834,7 @@ function squircle(size, squareness=0.5, style="fg", anchor=CENTER, spin=0, atype size = is_num(size) ? [size,size] : point2d(size), 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); @@ -1884,6 +1897,16 @@ function _squircle_se_exponent(squareness) = let( ) 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&Module: keyhole() // Synopsis: Creates a 2D keyhole shape.