Merge pull request #1534 from amatulic/anachronist_dev

added curvy_path as a complement to smooth_path, updated squircle()
This commit is contained in:
adrianVmariano 2025-01-05 13:55:45 -05:00 committed by GitHub
commit 5e96d1772e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 299 additions and 89 deletions

View file

@ -559,16 +559,17 @@ function bezpath_length(bezpath, N=3, max_deflect=0.001) =
// bezpath = path_to_bezpath(path, [closed], [tangents], [uniform], [size=]|[relsize=]); // bezpath = path_to_bezpath(path, [closed], [tangents], [uniform], [size=]|[relsize=]);
// Description: // Description:
// Given a 2d or 3d input path and optional list of tangent vectors, computes a cubic (degree 3) bezier // 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 // path that passes through every point on the input path and matches the tangent vectors. If you do not
// not supply the tangent it will be computed using `path_tangents()`. If the path is closed specify this // supply the tangents then they are computed using `path_tangents()` with `uniform=false` by default.
// by setting `closed=true`. The size or relsize parameter determines how far the curve can deviate from // 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 // 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 // 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 // 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 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 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 // the sum of the maxima minus the minimum. Tangents computed on non-uniform data tend
// using `path_tangents()` with `uniform=false` by default. Tangents computed on non-uniform data tend
// to display overshoots. See `smooth_path()` for examples. // to display overshoots. See `smooth_path()` for examples.
// Arguments: // Arguments:
// path = 2D or 3D point list or 1-region that the curve must pass through // path = 2D or 3D point list or 1-region that the curve must pass through
@ -637,6 +638,118 @@ function path_to_bezpath(path, closed, tangents, uniform=false, size, relsize) =
/// 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], i<pathlen-2?0.5*(p3[1]+p3[2]):p3[2]],
sizevect[i], relative),
[path[pathlen-1]]
]
)
flatten(roundpath);
/// Internal function: _bez_path_corner()
/// Usage:
/// _bez_path_corner(three_point_path, curvesize, relative);
/// Description:
/// Used by path_to_bezcornerpath()
/// Given a path with three points [p1, p2, p3] (2D or 3D), return a bezier path (minus the last control point) that creates a curve from p1 to p3.
/// The curvesize (roundness or inverse sharpness) parameter determines how close to a perfect circle (curvesize=1) or the p2 corner (curvesize=0) the path is, coming from the shortest leg. The longer leg path 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.
/// curvesize = curve is circular (curvesize=1) or sharp to the corner (curvesize=0) or anywhere in between
/// relative = if true, curvesize is a proportion between 0 and 1. If false, curvesize is an absolute distance that gets converted to a proportion internally.
function _bez_path_corner(p, curvesize, relative, mincurvesize=0.001) =
let(
p1 = p[0], p2 = p[1], p3 = 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
roundness = max(mincurvesize, relative ? curvesize : min(1, curvesize/maxcut)),
bzdist = maxcut * roundness, // 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<d3 ? cornerlegmin : cornerlegmax),
bz1 = p1 - (d1<=d3 ? leglenmin : leglenmax)*p21unit,
bz4 = bz3 - midto12unit*(d3<d1 ? cornerlegmin : cornerlegmax),
bz5 = p3 - (d3<=d1 ? leglenmin : leglenmax)*p23unit
) [p1, bz1, bz2, bz3, bz4, bz5]; // do not include last control point
// Function: bezpath_close_to_axis() // Function: bezpath_close_to_axis()
// Synopsis: Closes a 2D bezier path to the specified axis. // Synopsis: Closes a 2D bezier path to the specified axis.

View file

@ -603,87 +603,153 @@ function _rounding_offsets(edgespec,z_dir=1) =
// Usage: // Usage:
// smoothed = smooth_path(path, [tangents], [size=|relsize=], [splinesteps=], [closed=], [uniform=]); // smoothed = smooth_path(path, [tangents], [size=|relsize=], [splinesteps=], [closed=], [uniform=]);
// Description: // Description:
// Smooths the input path using a cubic spline. Every segment of the path will be replaced by a cubic curve // Smooths the input path, creating a continuous curve using a cubic spline, using one of two methods.
// with `splinesteps` points. The cubic interpolation will pass through every input point on the path
// and will match the tangents at every point. If you do not specify tangents they will be computed using
// path_tangents with uniform=false by default. Note that setting uniform to true with non-uniform
// sampling may be desirable in some cases but tends to produces curves that overshoot the point on the path.
// . // .
// The size or relsize parameter determines how far the curve can bend away from // For `method="edges"`, every segment (edge) of the path is replaced by a cubic curve with `splinesteps` points,
// the input path. In the case where the curve has a single hump, the size specifies the exact distance // and the cubic interpolation passes through every input point on the path, matching the tangents at every
// between the specified path and the curve. If you give relsize then it is relative to the segment // point. If you do not specify `tangents`, they are computed using path_tangents with `uniform=false` by
// length (e.g. 0.05 means 5% of the segment length). In 2d when the spline may make an S-curve, // default. Only the dirction of a tangent vector matters, not the vector length.
// in which case the size parameter specifies the sum of the deviations of the two peaks of the curve. In 3-space // Setting `uniform=true` with non-uniform sampling may be desirable in some cases but tends to
// the bezier curve may have three extrema: two maxima and one minimum. In this case the size specifies // produces curves that overshoot the point on the path.
// the sum of the maxima minus the minimum. At a given segment there is a maximum size: if your size // .
// value is too large it will be rounded down. See also path_to_bezpath(). // 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 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
// 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
// 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`
// 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().
// Arguments: // Arguments:
// path = path to smooth // path = path to smooth
// tangents = tangents constraining curve direction at each point. Default: computed automatically // tangents = tangents constraining curve direction vectors (vector length doesn't matter) at each point for `method="edges"`. Default: computed automatically
// --- // ---
// relsize = relative size specification for the curve, a number or vector. Default: 0.1 // relsize = relative maximum devation between the curve and edge (for method="edges") or corner (for method="corner"), a number or vector, expressed as proportion of edge length or proportion of max distance from corner (typically between 0 and 1). Default: 0.1
// size = absolute size specification for the curve, a number or vector // size = absolute deviation between the curve and edge (for method="edges") or corner (for method="corner"), a number or vector.
// uniform = set to true to compute tangents with uniform=true. Default: false // method = type of curve; "edges" makes a curve that intersects all the path vertices but deviates from the path edges, and "corners" makes a curve that is tangent to all segment midpoints but deviates from the corners. Default: "edges"
// splinesteps = Number of steps for each bezier curve section. Default: 10
// uniform = set to true to compute tangents with uniform=true. Applies only to "edges" method. Default: false
// closed = true if the curve is closed. Default: false. // closed = true if the curve is closed. Default: false.
// Example(2D): Original path in green, smoothed path in yellow: // Example(2D): Original path in green, smoothed "edges" path in yellow, "corners" path in red:
// color("green")stroke(square(4), width=0.1); // color("green")stroke(square(4), width=0.06);
// stroke(smooth_path(square(4),size=0.4), width=0.1); // stroke(smooth_path(square(4),size=0.4), width=0.1);
// Example(2D): Closing the path changes the end tangents // stroke(smooth_path(square(4),method="corners",size=0.4),
// polygon(smooth_path(square(4),size=0.4,closed=true)); // color="red", width=0.1);
// Example(2D): Turning on uniform tangent calculation also changes the end derivatives: // Example(2D): Closing the path changes the end tangents. Original path in green, "edges" path in yellow, "corners" in red.
// polygon(smooth_path(square(4),method="edges",size=0.4,closed=true));
// color("red")
// polygon(smooth_path(square(4),method="corners",size=0.4,closed=true));
// stroke(square(4), color="green", closed=true, width=0.06);
// Example(2D): Here's the square again with less smoothing. The "edges" curve is closer to the edge of the square, and the "corners" curve is closer to the square's corners.
// polygon(smooth_path(square(4), size=.25,closed=true));
// color("red") polygon(smooth_path(square(4),
// method="corners",size=.25,closed=true));
// stroke(square(4), closed=true, color="green", width=0.05);
// Example(2D): Turning on uniform tangent calculation also changes the end derivatives for the "edges" curve (it has no effect on the "corners" curve):
// color("green")stroke(square(4), width=0.1); // color("green")stroke(square(4), width=0.1);
// stroke(smooth_path(square(4),size=0.4,uniform=true), // stroke(smooth_path(square(4),size=0.4,uniform=true),
// width=0.1); // width=0.1);
// Example(2D): Here's a wide rectangle. Using size means all edges bulge the same amount, regardless of their length. // Example(2D): Here's a wide rectangle. With `method="edges" (yellow), using `size` means all edges bulge the same amount, regardless of their length. With `method="corners"` (red), the curve is `size' distance from the corners (up to a maximum theoretical circular arc).
// color("green") // color("green")
// stroke(square([10,4]), closed=true, width=0.1); // stroke(square([10,5]), closed=true, width=0.06);
// stroke(smooth_path(square([10,4]),size=1,closed=true), // stroke(smooth_path(square([10,5]), method="edges",
// width=0.1); // size=1, closed=true), width=0.1);
// Example(2D): With relsize the bulge is proportional to the side length. // stroke(smooth_path(square([10,5]), method="corners",
// size=1, closed=true), width=0.1, color="red");
// Example(2D): For the "edges" curve, with relsize the bulge is proportional to the side length.
// color("green")stroke(square([10,4]), closed=true, width=0.1); // color("green")stroke(square([10,4]), closed=true, width=0.1);
// stroke(smooth_path(square([10,4]),relsize=0.1,closed=true), // stroke(smooth_path(square([10,4]),relsize=0.1,closed=true),
// width=0.1); // width=0.1);
// Example(2D): Settting uniform to true biases the tangents to aline more with the line sides // Example(2D): For the "corners" curve, with relsize the distance from the corner is proportional to the maximum distance corresponding to a circular arc (shown in red) from the shorter leg of the corner. As `relsize` approaches zero, the curve approaches the corner.
// stroke(smooth_path(square([20,15]), method="corners",
// relsize=1, closed=true),
// color="red", closed=true, width=0.1);
// stroke(smooth_path(square([20,15]), method="corners",
// relsize=0.66, closed=true),
// color="gold", closed=true, width=0.1);
// stroke(smooth_path(square([20,15]), method="corners",
// relsize=0.33, closed=true),
// color="blue", closed=true, width=0.1);
// stroke(smooth_path(square([20,15]), method="corners",
// relsize=0.001, closed=true), // relsize must be >0
// 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") // color("green")
// stroke(square([10,4]), closed=true, width=0.1); // stroke(square([10,4]), closed=true, width=0.1);
// stroke(smooth_path(square([10,4]),uniform=true, // stroke(smooth_path(square([10,4]),uniform=true,
// relsize=0.1,closed=true), // relsize=0.1,closed=true),
// width=0.1); // 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]]; // path = [[0,0], [4,0], [7,14], [-3,12]];
// polygon(smooth_path(path,size=1,closed=true)); // polygon(smooth_path(path,size=1,closed=true));
// Example(2D): Here's the square again with less smoothing. // color("red") polygon(smooth_path(path,method="corners",relsize=0.7,closed=true));
// polygon(smooth_path(square(4), size=.25,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, so you get the maximum possible curve: // 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.1,closed=true); // color("green")stroke(square(4), width=0.06,closed=true);
// stroke(smooth_path(square(4), size=4, closed=true), // stroke(smooth_path(square(4), method="edges", size=4, closed=true),
// closed=true,width=.1); // closed=true, width=0.1);
// Example(2D): You can alter the shape of the curve by specifying your own arbitrary tangent values // 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), // 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)); // 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], // polygon(smooth_path(square(4),size = [.4, .05, 1, .3],
// closed=true)); // method="edges", closed=true));
// Example(FlatSpin,VPD=35,VPT=[4.5,4.5,1]): Works on 3d paths as well // 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]]; // path = [[0,0,0],[3,3,2],[6,0,1],[9,9,0]];
// stroke(smooth_path(path,relsize=.1),width=.3); // 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]]; // 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); // stroke(smooth_path(pts, uniform=true, relsize=0.1),width=.1);
// color("red")move_copies(pts)circle(r=.15,$fn=12); // 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]]; // 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); // stroke(smooth_path(pts, uniform=false, relsize=0.1),width=.1);
// color("red")move_copies(pts)circle(r=.15,$fn=12); // 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();} module smooth_path(path, tangents, size, relsize, method="edges", splinesteps=10, uniform, closed=false) {no_module();}
function smooth_path(path, tangents, size, relsize, splinesteps=10, uniform=false, closed) = function smooth_path(path, tangents, size, relsize, method="edges", splinesteps=10, uniform, closed) =
is_1region(path) ? smooth_path(path[0], tangents, size, relsize, splinesteps, uniform, default(closed,true)) : is_1region(path)
let ( ? smooth_path(path[0], tangents, size, relsize, method, splinesteps, uniform, default(closed,true))
bez = path_to_bezpath(path, tangents=tangents, size=size, relsize=relsize, uniform=uniform, closed=default(closed,false)), : assert(method=="edges" || method=="corners", "method must be \"edges\" or \"corners\".")
assert(method=="edges" || (is_undef(tangents) && is_undef(uniform)), "The tangents and uniform parameters are incompatible with method=\"corners\".")
let (
uniform = default(uniform,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) smoothed = bezpath_curve(bez,splinesteps=splinesteps)
) )
closed ? list_unwrap(smoothed) : smoothed; closed ? list_unwrap(smoothed) : smoothed;
function _scalar_to_vector(value,length,varname) = function _scalar_to_vector(value,length,varname) =

View file

@ -16,6 +16,8 @@
////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////
use <builtins.scad> use <builtins.scad>
include <beziers.scad>
// Section: 2D Primitives // Section: 2D Primitives
@ -1747,6 +1749,7 @@ module glued_circles(r, spread=10, tangent=30, d, anchor=CENTER, spin=0) {
} }
// Function&Module: squircle() // Function&Module: squircle()
// Synopsis: Creates a shape between a circle and a square. // Synopsis: Creates a shape between a circle and a square.
// SynTags: Geom, Path // SynTags: Geom, Path
@ -1757,19 +1760,24 @@ module glued_circles(r, spread=10, tangent=30, d, anchor=CENTER, spin=0) {
// Usage: As Function // Usage: As Function
// path = squircle(size, [squareness], [style=]); // path = squircle(size, [squareness], [style=]);
// Description: // Description:
// A [squircle](https://en.wikipedia.org/wiki/Squircle) is a shape intermediate between a square/rectangle and a circle/ellipse. // A [squircle](https://en.wikipedia.org/wiki/Squircle) is a shape intermediate between a square/rectangle and a
// Squircles are sometimes used to make dinner plates (more area for the same radius as a circle), keyboard buttons, and smartphone // circle/ellipse. Squircles are sometimes used to make dinner plates (more area for the same radius as a circle), keyboard
// icons. Old CRT television screens also resembled elongated squircles. // 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 // Multiple definitions exist for the squircle. We support three versions: the Fernandez-Guasti squircle, the superellipse
// ({{supershape()}} Example 3, also known as the Lamé upper squircle), and the . They are visually almost indistinguishable, // (see {{supershape()}} Example 3, also known as the Lamé upper squircle), and a squircle constructed from Bezier curves.
// with the superellipse having slightly rounder "corners" than FG at the same corner radius. These two squircles have different, // They are visually almost indistinguishable, with the superellipse having slightly rounder "corners" than FG at the same
// unintuitive methods for controlling how square or circular the shape is. The `squareness` parameter determines the shape, specifying // corner radius, and the Bezier version having slightly sharper corners. These squircles have different, unintuitive methods
// the corner position linearly, with 0 giving the circle and 1 giving the square. Vertices are positioned to be more dense near the // for controlling how square or circular the shape is. The `squareness` parameter determines the shape, specifying the
// corners to preserve smoothness at low values of `$fn`. // 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 // For the "superellipse" style, the special case where the superellipse exponent is 4 results in a squircle with corners at
// between radial points on the circle and square, corresponding to squareness=0.456786. // 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 module, creates a 2D squircle with the specified squareness.
// When called as a function, returns a 2D path for a squircle. // 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]`. // 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 // 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` // 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` // 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" // 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): // Examples(2D):
// squircle(size=50, squareness=0.4); // squircle(size=50, squareness=0.4);
// squircle([80,60], 0.7, $fn=64); // squircle([80,60], 0.7, $fn=64);
// 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);
// Example(2D,VPD=265,NoAxes): Ten increments of squareness parameter for a superellipse squircle // Example(2D,VPD=265,NoAxes): Ten increments of squareness parameter for a superellipse squircle
// color("green") for(sq=[0:0.1:1]) // color("green") for(sq=[0:0.1:1])
// stroke(squircle(100, sq, style="superellipse", $fn=96), closed=true, width=0.5); // 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), size = is_num(size) ? [size,size] : point2d(size),
path = style == "fg" ? _squircle_fg(size, squareness) path = style == "fg" ? _squircle_fg(size, squareness)
: style == "superellipse" ? _squircle_se(size, squareness) : style == "superellipse" ? _squircle_se(size, squareness)
: style == "bezier" ? _squircle_bz(size, squareness)
: assert(false, "Style must be \"fg\" or \"superellipse\"") : 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); ) reorient(anchor, spin, two_d=true, size=atype=="box"?size:undef, path=atype=="box"?undef:path, p=path, extent=true);
@ -1841,9 +1854,11 @@ function _squircle_fg(size, squareness) = [
) p*[cos(theta), aspect*sin(theta)] ) p*[cos(theta), aspect*sin(theta)]
]; ];
function squircle_radius_fg(squareness, r, angle) = let( function squircle_radius_fg(squareness, r, angle) =
let(
s2a = abs(squareness*sin(2*angle)) s2a = abs(squareness*sin(2*angle))
) s2a>0 ? r*sqrt(2)/s2a * sqrt(1 - sqrt(1 - s2a*s2a)) : r; )
s2a>0 ? r*sqrt(2)/s2a * sqrt(1 - sqrt(1 - s2a*s2a)) : r;
function _linearize_squareness(s) = function _linearize_squareness(s) =
// from Chamberlain Fong (2016). "Squircular Calculations". arXiv. // from Chamberlain Fong (2016). "Squircular Calculations". arXiv.
@ -1871,17 +1886,33 @@ function _squircle_se(size, squareness) = [
) [ra*x, rb*y] / r ) [ra*x, rb*y] / r
]; ];
function squircle_radius_se(n, r, angle) = let( function squircle_radius_se(n, r, angle) =
let(
x = cos(angle), x = cos(angle),
y = sin(angle) y = sin(angle)
) (abs(x)^n + abs(y)^n)^(1/n) / r; )
(abs(x)^n + abs(y)^n)^(1/n) / r;
function _squircle_se_exponent(squareness) = let( function _squircle_se_exponent(squareness) =
let(
// limit squareness; error if >0.99889, limit is smaller for r>1 // limit squareness; error if >0.99889, limit is smaller for r>1
s=min(0.998,squareness), s=min(0.998,squareness),
rho = 1 + s*(sqrt(2)-1), rho = 1 + s*(sqrt(2)-1),
x = rho / sqrt(2) x = rho / sqrt(2)
) log(0.5) / log(x); )
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);