From 65ec17f4b33d9545b091c41832fabba1fbbc9933 Mon Sep 17 00:00:00 2001 From: Alex Matulich Date: Wed, 4 Dec 2024 23:26:54 -0800 Subject: [PATCH 01/10] added squircle to shapes2d.scad --- shapes2d.scad | 69 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 68 insertions(+), 1 deletion(-) diff --git a/shapes2d.scad b/shapes2d.scad index 45fda07..d87f58f 100644 --- a/shapes2d.scad +++ b/shapes2d.scad @@ -1929,7 +1929,7 @@ function _superformula(theta,m1,m2,n1,n2=1,n3=1,a=1,b=1) = // Usage: As Function // path = reuleaux_polygon(n, r|d=, ...); // Description: -// When called as a module, reates a 2D Reuleaux Polygon; a constant width shape that is not circular. Uses "intersect" type anchoring. +// When called as a module, creates a 2D Reuleaux Polygon; a constant width shape that is not circular. Uses "intersect" type anchoring. // When called as a function, returns a 2D path for a Reulaux Polygon. // Arguments: // n = Number of "sides" to the Reuleaux Polygon. Must be an odd positive number. Default: 3 @@ -1988,6 +1988,73 @@ function reuleaux_polygon(n=3, r, d, anchor=CENTER, spin=0) = +// Function&Module: squircle() +// Synopsis: Creates a shape between a circle and a square, centered on the origin. +// SynTags: Geom, Path +// Topics: Shapes (2D), Paths (2D), Path Generators, Attachable +// See Also: circle(), square() +// Usage: As Module +// squircle(squareness, size) [ATTACHMENTS]; +// Usage: As Function +// path = squircle(squareness, size); +// Description: +// A 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 squircles. +// When called as a module, creates a 2D squircle with the desired squareness. Uses "intersect" type anchoring. +// When called as a function, returns a 2D path for a squircle. +// Arguments: +// squareness = Value between 0 and 1. Controls the shape of the squircle. When `squareness=0` the shape is a circle, and when `squareness=1` the shape is a square. Default: 0.8 +// size = Bounding box of the squircle, same as the `size` parameter in `square()`, can be a single number or an `[xsize,ysize]` vector. Default: [10,10] +// $fn = Number of points. 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 non-uniformly around the squircle so they are more dense sharper curves. Default if not set: 40 +// Examples(2D): +// squircle(squareness=0.5, size=50); +// squircle(0.95, [80,60], $fn=64); +// Examples(2D): Standard vector anchors are based on extents +// squircle(0.8, 50) show_anchors(custom=false); +// Examples(2D): Named anchors exist for the sides and corners +// squircle(0.8, 50) show_anchors(std=false); +module squircle(squareness=0.8, size=[10,10], anchor=CENTER, spin=0) { + check = assert(squareness >= 0 && squareness <= 1); + bbox = is_num(size) ? [size,size] : point2d(size); + assert(all_positive(bbox), "All components of size must be positive."); + path = squircle(squareness, size); + anchors = [ + for (i = [0:1:3]) let( + ca = 360 - i*90, + cp = polar_to_xy(squircle_radius(squareness, bbox[0], ca), ca) + ) named_anchor(str("side",i), cp, unit(cp,BACK), 0), + for (i = [0:1:3]) let( + ca = 360-45 - i*90, + cp = polar_to_xy(squircle_radius(squareness, bbox[0], ca), ca) + ) named_anchor(str("corner",i), cp, unit(cp,BACK), 0) + ]; + attachable(anchor,spin, two_d=true, path=path, extent=false, anchors=anchors) { + polygon(path); + children(); + } +} + + +function squircle(squareness=0.8, size=[10,10]) = + assert(squareness >= 0 && squareness <= 1) [ + let( + sq = sqrt(squareness), // somewhat linearize the squareness response + bbox = is_num(size) ? [size,size] : point2d(size), + aspect = bbox[1] / bbox[0], + r = 0.5 * bbox[0], + astep = $fn>=12 ? 90/round($fn/4) : 9 + ) for(a=[360:-astep:0.01]) let( + theta = a + sq * sin(4*a) * 30/PI, // tighter angle steps at corners + p = squircle_radius(sq, r, theta) + ) [p*cos(theta), aspect*p*sin(theta)] +]; + + +function squircle_radius(squareness, r, angle) = let( + s2a = abs(squareness*sin(2*angle)) + ) s2a>0 ? r*sqrt(2)/s2a * sqrt(1 - sqrt(1 - s2a*s2a)) : r; + + + // Section: Text // Module: text() From dd96a30c92731c60cf94f82395596b617b7098fa Mon Sep 17 00:00:00 2001 From: Alex Matulich Date: Thu, 5 Dec 2024 10:18:10 -0800 Subject: [PATCH 02/10] Expanded documentation to explain differences from supershape() --- shapes2d.scad | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/shapes2d.scad b/shapes2d.scad index d87f58f..cb63af3 100644 --- a/shapes2d.scad +++ b/shapes2d.scad @@ -1992,14 +1992,15 @@ function reuleaux_polygon(n=3, r, d, anchor=CENTER, spin=0) = // Synopsis: Creates a shape between a circle and a square, centered on the origin. // SynTags: Geom, Path // Topics: Shapes (2D), Paths (2D), Path Generators, Attachable -// See Also: circle(), square() +// See Also: circle(), square(), supershape() // Usage: As Module // squircle(squareness, size) [ATTACHMENTS]; // Usage: As Function // path = squircle(squareness, size); // Description: -// A 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 squircles. -// When called as a module, creates a 2D squircle with the desired squareness. Uses "intersect" type anchoring. +// A squircle is a shape intermediate between a square/rectangle and a circle/ellipse. A squircle is a special case of supershape (shown in `supershape()` example 3), but this implementation uses a different method that requires just two parameters, and the vertex distribution is optimized for smoothness. +// 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 squircles. +// When called as a module, creates a 2D squircle with the desired squareness. Uses "intersect" type anchoring. // When called as a function, returns a 2D path for a squircle. // Arguments: // squareness = Value between 0 and 1. Controls the shape of the squircle. When `squareness=0` the shape is a circle, and when `squareness=1` the shape is a square. Default: 0.8 From 495ebbefd85025111cc889f2787e11dce536757b Mon Sep 17 00:00:00 2001 From: Alex Matulich Date: Thu, 5 Dec 2024 10:18:10 -0800 Subject: [PATCH 03/10] Expanded documentation to explain differences from supershape() --- shapes2d.scad | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/shapes2d.scad b/shapes2d.scad index d87f58f..3de6bcb 100644 --- a/shapes2d.scad +++ b/shapes2d.scad @@ -1992,27 +1992,31 @@ function reuleaux_polygon(n=3, r, d, anchor=CENTER, spin=0) = // Synopsis: Creates a shape between a circle and a square, centered on the origin. // SynTags: Geom, Path // Topics: Shapes (2D), Paths (2D), Path Generators, Attachable -// See Also: circle(), square() +// See Also: circle(), square(), supershape() // Usage: As Module // squircle(squareness, size) [ATTACHMENTS]; // Usage: As Function // path = squircle(squareness, size); // Description: -// A 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 squircles. -// When called as a module, creates a 2D squircle with the desired squareness. Uses "intersect" type anchoring. +// A squircle is a shape intermediate between a square/rectangle and a circle/ellipse. A squircle is a special case of supershape (shown in `supershape()` example 3), but this implementation uses a different method that requires just two parameters, and the vertex distribution is optimized for smoothness. +// 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 squircles. +// When called as a module, creates a 2D squircle with the desired squareness. Uses "intersect" type anchoring. // When called as a function, returns a 2D path for a squircle. // Arguments: -// squareness = Value between 0 and 1. Controls the shape of the squircle. When `squareness=0` the shape is a circle, and when `squareness=1` the shape is a square. Default: 0.8 +// squareness = Value between 0 and 1. Controls the shape of the squircle. When `squareness=0` the shape is a circle, and when `squareness=1` the shape is a square. Default: 0.7 // size = Bounding box of the squircle, same as the `size` parameter in `square()`, can be a single number or an `[xsize,ysize]` vector. Default: [10,10] // $fn = Number of points. 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 non-uniformly around the squircle so they are more dense sharper curves. Default if not set: 40 // Examples(2D): -// squircle(squareness=0.5, size=50); -// squircle(0.95, [80,60], $fn=64); +// squircle(squareness=0.4, size=50); +// squircle(0.8, [80,60], $fn=64); +// Examples(2D): Ten increments of squareness parameter +// for(sq=[0:0.1:1]) +// stroke(squircle(sq, 100, $fn=128), closed=true, width=0.5); // Examples(2D): Standard vector anchors are based on extents // squircle(0.8, 50) show_anchors(custom=false); // Examples(2D): Named anchors exist for the sides and corners // squircle(0.8, 50) show_anchors(std=false); -module squircle(squareness=0.8, size=[10,10], anchor=CENTER, spin=0) { +module squircle(squareness=0.7, size=[10,10], anchor=CENTER, spin=0) { check = assert(squareness >= 0 && squareness <= 1); bbox = is_num(size) ? [size,size] : point2d(size); assert(all_positive(bbox), "All components of size must be positive."); @@ -2034,10 +2038,11 @@ module squircle(squareness=0.8, size=[10,10], anchor=CENTER, spin=0) { } -function squircle(squareness=0.8, size=[10,10]) = +function squircle(squareness=0.7, size=[10,10]) = assert(squareness >= 0 && squareness <= 1) [ let( - sq = sqrt(squareness), // somewhat linearize the squareness response + sqlim = max(0, min(1, squareness)), + sq = sqrt(sqlim*(2-sqlim)), // somewhat linearize squareness response bbox = is_num(size) ? [size,size] : point2d(size), aspect = bbox[1] / bbox[0], r = 0.5 * bbox[0], From 504c92bba9c1eb4a0260a3f0abfcae6d3e9dc4d3 Mon Sep 17 00:00:00 2001 From: Alex Matulich Date: Thu, 5 Dec 2024 16:59:58 -0800 Subject: [PATCH 04/10] Corrected anchor positions, improved linear response of squareness, added example --- shapes2d.scad | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shapes2d.scad b/shapes2d.scad index 3de6bcb..9a9f5a8 100644 --- a/shapes2d.scad +++ b/shapes2d.scad @@ -2024,11 +2024,11 @@ module squircle(squareness=0.7, size=[10,10], anchor=CENTER, spin=0) { anchors = [ for (i = [0:1:3]) let( ca = 360 - i*90, - cp = polar_to_xy(squircle_radius(squareness, bbox[0], ca), ca) + cp = polar_to_xy(squircle_radius(squareness, bbox[0]/2, ca), ca) ) named_anchor(str("side",i), cp, unit(cp,BACK), 0), for (i = [0:1:3]) let( ca = 360-45 - i*90, - cp = polar_to_xy(squircle_radius(squareness, bbox[0], ca), ca) + cp = polar_to_xy(squircle_radius(squareness, bbox[0]/2, ca), ca) ) named_anchor(str("corner",i), cp, unit(cp,BACK), 0) ]; attachable(anchor,spin, two_d=true, path=path, extent=false, anchors=anchors) { From 6c92e0313ad4b1afc4a9bafe33a3dcb2a79b922c Mon Sep 17 00:00:00 2001 From: Alex Matulich Date: Fri, 6 Dec 2024 08:28:03 -0800 Subject: [PATCH 05/10] Final fix for exact squareness linearity in squircle() --- shapes2d.scad | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/shapes2d.scad b/shapes2d.scad index 9a9f5a8..11a5a30 100644 --- a/shapes2d.scad +++ b/shapes2d.scad @@ -1929,7 +1929,7 @@ function _superformula(theta,m1,m2,n1,n2=1,n3=1,a=1,b=1) = // Usage: As Function // path = reuleaux_polygon(n, r|d=, ...); // Description: -// When called as a module, creates a 2D Reuleaux Polygon; a constant width shape that is not circular. Uses "intersect" type anchoring. +// When called as a module, reates a 2D Reuleaux Polygon; a constant width shape that is not circular. Uses "intersect" type anchoring. // When called as a function, returns a 2D path for a Reulaux Polygon. // Arguments: // n = Number of "sides" to the Reuleaux Polygon. Must be an odd positive number. Default: 3 @@ -2021,14 +2021,14 @@ module squircle(squareness=0.7, size=[10,10], anchor=CENTER, spin=0) { bbox = is_num(size) ? [size,size] : point2d(size); assert(all_positive(bbox), "All components of size must be positive."); path = squircle(squareness, size); - anchors = [ + anchors = let(sq = _linearize_squareness(squareness)) [ for (i = [0:1:3]) let( ca = 360 - i*90, - cp = polar_to_xy(squircle_radius(squareness, bbox[0]/2, ca), ca) + cp = polar_to_xy(squircle_radius(sq, bbox[0]/2, ca), ca) ) named_anchor(str("side",i), cp, unit(cp,BACK), 0), for (i = [0:1:3]) let( ca = 360-45 - i*90, - cp = polar_to_xy(squircle_radius(squareness, bbox[0]/2, ca), ca) + cp = polar_to_xy(squircle_radius(sq, bbox[0]/2, ca), ca) ) named_anchor(str("corner",i), cp, unit(cp,BACK), 0) ]; attachable(anchor,spin, two_d=true, path=path, extent=false, anchors=anchors) { @@ -2041,8 +2041,7 @@ module squircle(squareness=0.7, size=[10,10], anchor=CENTER, spin=0) { function squircle(squareness=0.7, size=[10,10]) = assert(squareness >= 0 && squareness <= 1) [ let( - sqlim = max(0, min(1, squareness)), - sq = sqrt(sqlim*(2-sqlim)), // somewhat linearize squareness response + sq = _linearize_squareness(squareness), bbox = is_num(size) ? [size,size] : point2d(size), aspect = bbox[1] / bbox[0], r = 0.5 * bbox[0], @@ -2058,6 +2057,13 @@ function squircle_radius(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/vc/arxiv/papers/1604/1604.02174v1.pdf + let(c = 2 - 2*sqrt(2), d = 1 - 0.5*c*s) + 2 * sqrt((1+c)*s*s - c*s) / (d*d); + // Section: Text From a2c30affeab03f23083d10ea1c1266ac8a267d87 Mon Sep 17 00:00:00 2001 From: Alex Matulich Date: Fri, 6 Dec 2024 09:07:44 -0800 Subject: [PATCH 06/10] Corrected link in comment about linearizing squareness in squircle() --- shapes2d.scad | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shapes2d.scad b/shapes2d.scad index 11a5a30..865a14d 100644 --- a/shapes2d.scad +++ b/shapes2d.scad @@ -2060,7 +2060,7 @@ function squircle_radius(squareness, r, angle) = let( function _linearize_squareness(s) = // from Chamberlain Fong (2016). "Squircular Calculations". arXiv. - // https://arxiv.org/vc/arxiv/papers/1604/1604.02174v1.pdf + // 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); From c18c9553765484110047b3a37738766977b85ab9 Mon Sep 17 00:00:00 2001 From: Alex Matulich Date: Mon, 9 Dec 2024 11:59:46 -0800 Subject: [PATCH 07/10] Revised to manage attachments --- shapes2d.scad | 156 +++++++++++++++++++++++++++++++++++++------------- 1 file changed, 115 insertions(+), 41 deletions(-) diff --git a/shapes2d.scad b/shapes2d.scad index 865a14d..e9c5285 100644 --- a/shapes2d.scad +++ b/shapes2d.scad @@ -1994,70 +1994,112 @@ function reuleaux_polygon(n=3, r, d, anchor=CENTER, spin=0) = // Topics: Shapes (2D), Paths (2D), Path Generators, Attachable // See Also: circle(), square(), supershape() // Usage: As Module -// squircle(squareness, size) [ATTACHMENTS]; +// squircle(squareness, size, [style]) [ATTACHMENTS]; // Usage: As Function -// path = squircle(squareness, size); +// path = squircle(squareness, size, [style]); // Description: -// A squircle is a shape intermediate between a square/rectangle and a circle/ellipse. A squircle is a special case of supershape (shown in `supershape()` example 3), but this implementation uses a different method that requires just two parameters, and the vertex distribution is optimized for smoothness. -// 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 squircles. -// When called as a module, creates a 2D squircle with the desired squareness. Uses "intersect" type anchoring. +// 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. +// There are multiple approaches to constructing a squircle. One approach is a special case of superellipse (shown in {{supershape}} example 3), and uses exponents to adjust the shape. Another, called Fernández-Guasti squircle or FG squircle, arises from work in optics and uses a "squareness" parameter between 0 and 1 to adjust the shape. +// The FG style and superellipse style squircles are visually almost indistinguishable, with the superellipse having slightly rounder "corners" than FG for a given value of squareness. Either style requires just the two parameters `squareness` and `size`. The vertex distribution is adjusted to be more dense at the corners for smoothness at low values of `$fn`. +// When called as a module, creates a 2D squircle with the desired squareness. // When called as a function, returns a 2D path for a squircle. // Arguments: -// squareness = Value between 0 and 1. Controls the shape of the squircle. When `squareness=0` the shape is a circle, and when `squareness=1` the shape is a square. Default: 0.7 -// size = Bounding box of the squircle, same as the `size` parameter in `square()`, can be a single number or an `[xsize,ysize]` vector. Default: [10,10] -// $fn = Number of points. 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 non-uniformly around the squircle so they are more dense sharper curves. Default if not set: 40 +// squareness = Value between 0 and 1. Controls the shape of the squircle. When `squareness=0` the shape is a circle, and when `squareness=1` the shape is a square. Otherwise, this parameter sets the location of a squircle "corner" at the specified interpolated position between a circle and a square. For the "superellipse" style, the special case where the superellipse exponent is 4 (also known as *Lamé's quartic curve*) results in a squircle at the geometric mean between radial points on the circle and square, corresponding to squareness=0.456786. Default: 0.5 +// size = Same as the `size` parameter in `square()`, can be a single number or an `[xsize,ysize]` vector. Default: [1,1] +// style = method for generating a squircle, "fg" for Fernández-Guasti and "superellipse" for superellipse. Default: "fg" +// atype = anchor type, "box" for bounding box corners and sides, "perim" for the squircle corners +// $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 non-uniformly around the squircle so they are more dense at sharper curves. Default if not set: 40 // Examples(2D): // squircle(squareness=0.4, size=50); // squircle(0.8, [80,60], $fn=64); -// Examples(2D): Ten increments of squareness parameter +// Examples(2D): Ten increments of squareness parameter for a superellipse squircle // for(sq=[0:0.1:1]) -// stroke(squircle(sq, 100, $fn=128), closed=true, width=0.5); -// Examples(2D): Standard vector anchors are based on extents -// squircle(0.8, 50) show_anchors(custom=false); -// Examples(2D): Named anchors exist for the sides and corners -// squircle(0.8, 50) show_anchors(std=false); -module squircle(squareness=0.7, size=[10,10], anchor=CENTER, spin=0) { +// stroke(squircle(sq, 100, style="superellipse", $fn=128), closed=true, width=0.5); +// Examples(2D): Standard vector anchors are based on the bounding box +// squircle(0.6, 50) show_anchors(); +// Examples(2D): Perimeter anchors, anchoring at bottom left and spinning 20° +// squircle(0.5, [60,40], anchor=(BOTTOM+LEFT), atype="perim", spin=20) +// show_anchors(); + +module squircle(squareness=0.5, size=[1,1], style="fg", atype="box", anchor=CENTER, spin=0) { check = assert(squareness >= 0 && squareness <= 1); - bbox = is_num(size) ? [size,size] : point2d(size); - assert(all_positive(bbox), "All components of size must be positive."); - path = squircle(squareness, size); - anchors = let(sq = _linearize_squareness(squareness)) [ - for (i = [0:1:3]) let( - ca = 360 - i*90, - cp = polar_to_xy(squircle_radius(sq, bbox[0]/2, ca), ca) - ) named_anchor(str("side",i), cp, unit(cp,BACK), 0), - for (i = [0:1:3]) let( - ca = 360-45 - i*90, - cp = polar_to_xy(squircle_radius(sq, bbox[0]/2, ca), ca) - ) named_anchor(str("corner",i), cp, unit(cp,BACK), 0) - ]; - attachable(anchor,spin, two_d=true, path=path, extent=false, anchors=anchors) { - polygon(path); - children(); + 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."); + if (atype == "box") { + path = squircle(squareness, size, style); + attachable(anchor, spin, two_d=true, size=size) { + polygon(path); + children(); + } + } else { // atype=="perim" + override_path = squircle(squareness, size, style, atype, _return_override=true); + attachable(anchor, spin, two_d=true, size=size, extent=false, override=override_path[1]) { + polygon(override_path[0]); + children(); + } } } -function squircle(squareness=0.7, size=[10,10]) = - assert(squareness >= 0 && squareness <= 1) [ +function squircle(squareness=0.5, size=[1,1], style="fg", atype="box", anchor=CENTER, spin=0, _return_override=false) = + assert(squareness >= 0 && squareness <= 1) + assert(is_num(size) || is_vector(size,2)) + assert(in_list(atype, ["box", "perim"])) + let( + path = + style == "fg" ? _squircle_fg(squareness, size) + : style == "superellipse" ? _squircle_se(squareness, size) + : assert(false, "Style must be \"fg\" or \"superellipse\""), + size = is_num(size) ? [size,size] : point2d(size), + a = 0.5 * size[0], + b = 0.5 * size[1], + override = atype == "box" ? undef + : let( + sn = style=="fg" ? _linearize_squareness(squareness) + : _squircle_se_exponent(squareness), + derivq1 = style=="fg" ? // 1+derivative of squircle in first quadrant + function (x) let(s2=sn*sn, a2=a*a, b2=b*b, x2=x*x, denom=a2-s2*x2) a2*b*(s2-1)*x/(denom*denom*sqrt((a2-x2)/denom)) + 1 + : function (x) let(n=sn) 1 - (b/a)*((a/x)^n - 1)^(1/n-1), + xc = root_find(derivq1, 0.01, a-0.01), // find where slope=-1 + yc = style=="fg" ? + let(s2=sn*sn, a2=a*a, b2=b*b, x2=xc*xc) sqrt(b2*(a2-x2)/(a2-s2*x2)) + : b*(1-(xc/a)^sn)^(1/sn), + corners = [[xc,yc], [-xc,yc], [-xc,-yc], [xc,-yc]], + anchorpos = [[1,1],[-1,1],[-1,-1],[1,-1]] + ) [ for(i=[0:3]) [anchorpos[i], [corners[i]]] ] + ) _return_override + ? [reorient(anchor, spin, two_d=true, size=size, p=path, extent=false, override=override), override] + : reorient(anchor, spin, two_d=true, size=size, p=path, extent=false, override=override); + +function _squircle_anchor_radius(squareness, angle, style) = + style == "fg" + ? let(sq = _linearize_squareness(squareness)) + squircle_radius_fg(sq, 1, angle) + : let(n = _squircle_se_exponent(squareness)) + squircle_radius_se(n, 1, angle); + //_superformula(theta=angle, m1=4,m2=4,n1=n,n2=n,n3=n,a=1,b=1); + + +/* FG squircle functions */ + +function _squircle_fg(squareness, size) = [ let( sq = _linearize_squareness(squareness), - bbox = is_num(size) ? [size,size] : point2d(size), - aspect = bbox[1] / bbox[0], - r = 0.5 * bbox[0], + size = is_num(size) ? [size,size] : point2d(size), + aspect = size[1] / size[0], + r = 0.5 * size[0], astep = $fn>=12 ? 90/round($fn/4) : 9 ) for(a=[360:-astep:0.01]) let( theta = a + sq * sin(4*a) * 30/PI, // tighter angle steps at corners - p = squircle_radius(sq, r, theta) - ) [p*cos(theta), aspect*p*sin(theta)] + p = squircle_radius_fg(sq, r, theta) + ) p*[cos(theta), aspect*sin(theta)] ]; - -function squircle_radius(squareness, r, angle) = let( +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 @@ -2065,6 +2107,38 @@ function _linearize_squareness(s) = 2 * sqrt((1+c)*s*s - c*s) / (d*d); +/* Superellipse squircle functions */ + +function _squircle_se(squareness, size) = [ + let( + 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) : 9, + 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), + y = sin(theta), + r = (abs(x)^n + abs(y)^n)^(1/n), // superellipse + //r = _superformula(theta=theta, m1=4,m2=4,n1=n,n2=n,n3=n,a=1,b=1) + ) [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_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); + + // Section: Text From 039485e913d19e40bfdf77cc465fef6e46688fd4 Mon Sep 17 00:00:00 2001 From: Alex Matulich Date: Mon, 9 Dec 2024 12:21:53 -0800 Subject: [PATCH 08/10] Added paragraph breaks in coments for squircle --- shapes2d.scad | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/shapes2d.scad b/shapes2d.scad index e9c5285..0af1133 100644 --- a/shapes2d.scad +++ b/shapes2d.scad @@ -1999,9 +1999,12 @@ function reuleaux_polygon(n=3, r, d, anchor=CENTER, spin=0) = // path = squircle(squareness, size, [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. +// . // There are multiple approaches to constructing a squircle. One approach is a special case of superellipse (shown in {{supershape}} example 3), and uses exponents to adjust the shape. Another, called Fernández-Guasti squircle or FG squircle, arises from work in optics and uses a "squareness" parameter between 0 and 1 to adjust the shape. +// . // The FG style and superellipse style squircles are visually almost indistinguishable, with the superellipse having slightly rounder "corners" than FG for a given value of squareness. Either style requires just the two parameters `squareness` and `size`. The vertex distribution is adjusted to be more dense at the corners for smoothness at low values of `$fn`. -// When called as a module, creates a 2D squircle with the desired squareness. +// . +// When called as a module, creates a 2D squircle with the desired squareness. // When called as a function, returns a 2D path for a squircle. // Arguments: // squareness = Value between 0 and 1. Controls the shape of the squircle. When `squareness=0` the shape is a circle, and when `squareness=1` the shape is a square. Otherwise, this parameter sets the location of a squircle "corner" at the specified interpolated position between a circle and a square. For the "superellipse" style, the special case where the superellipse exponent is 4 (also known as *Lamé's quartic curve*) results in a squircle at the geometric mean between radial points on the circle and square, corresponding to squareness=0.456786. Default: 0.5 From 835cbc0f000a8eea9835b07ed02912590b3cc3d7 Mon Sep 17 00:00:00 2001 From: Alex Matulich Date: Mon, 9 Dec 2024 12:29:03 -0800 Subject: [PATCH 09/10] Removed _squircle_anchor_radius() function no longer needed --- shapes2d.scad | 8 -------- 1 file changed, 8 deletions(-) diff --git a/shapes2d.scad b/shapes2d.scad index 0af1133..8688c2c 100644 --- a/shapes2d.scad +++ b/shapes2d.scad @@ -2075,14 +2075,6 @@ function squircle(squareness=0.5, size=[1,1], style="fg", atype="box", anchor=CE ? [reorient(anchor, spin, two_d=true, size=size, p=path, extent=false, override=override), override] : reorient(anchor, spin, two_d=true, size=size, p=path, extent=false, override=override); -function _squircle_anchor_radius(squareness, angle, style) = - style == "fg" - ? let(sq = _linearize_squareness(squareness)) - squircle_radius_fg(sq, 1, angle) - : let(n = _squircle_se_exponent(squareness)) - squircle_radius_se(n, 1, angle); - //_superformula(theta=angle, m1=4,m2=4,n1=n,n2=n,n3=n,a=1,b=1); - /* FG squircle functions */ From cba3391131358c114d80001fdf0bbeef96222298 Mon Sep 17 00:00:00 2001 From: Alex Matulich Date: Mon, 9 Dec 2024 20:40:42 -0800 Subject: [PATCH 10/10] Replaced override anchoring with builtin perimeter anchoring, minor fixes, documentation cleanup --- shapes2d.scad | 72 +++++++++++++++++++-------------------------------- 1 file changed, 26 insertions(+), 46 deletions(-) diff --git a/shapes2d.scad b/shapes2d.scad index 8688c2c..11fb6e6 100644 --- a/shapes2d.scad +++ b/shapes2d.scad @@ -1994,97 +1994,77 @@ function reuleaux_polygon(n=3, r, d, anchor=CENTER, spin=0) = // Topics: Shapes (2D), Paths (2D), Path Generators, Attachable // See Also: circle(), square(), supershape() // Usage: As Module -// squircle(squareness, size, [style]) [ATTACHMENTS]; +// squircle(size, [squareness], [style]) [ATTACHMENTS]; // Usage: As Function -// path = squircle(squareness, size, [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 buttons, and smartphone icons. Old CRT television screens also resembled elongated squircles. // . -// There are multiple approaches to constructing a squircle. One approach is a special case of superellipse (shown in {{supershape}} example 3), and uses exponents to adjust the shape. Another, called Fernández-Guasti squircle or FG squircle, arises from work in optics and uses a "squareness" parameter between 0 and 1 to adjust the shape. +// There are multiple approaches to constructing a squircle. One approach is a special case of superellipse (shown in {{supershape}} example 3), and uses exponents between 2 and infinity to adjust the shape. Another, the Fernández-Guasti squircle or FG squircle, arises from work in optics and uses a "squareness" parameter between 0 and 1 to adjust the shape. We use the same squareness parameter for both types, adjusting the internal FG parameter or superellipse exponent as needed to achieve the same squircle corner extents. // . // The FG style and superellipse style squircles are visually almost indistinguishable, with the superellipse having slightly rounder "corners" than FG for a given value of squareness. Either style requires just the two parameters `squareness` and `size`. The vertex distribution is adjusted to be more dense at the corners for smoothness at low values of `$fn`. // . // When called as a module, creates a 2D squircle with the desired squareness. // When called as a function, returns a 2D path for a squircle. // Arguments: -// squareness = Value between 0 and 1. Controls the shape of the squircle. When `squareness=0` the shape is a circle, and when `squareness=1` the shape is a square. Otherwise, this parameter sets the location of a squircle "corner" at the specified interpolated position between a circle and a square. For the "superellipse" style, the special case where the superellipse exponent is 4 (also known as *Lamé's quartic curve*) results in a squircle at the geometric mean between radial points on the circle and square, corresponding to squareness=0.456786. Default: 0.5 -// size = Same as the `size` parameter in `square()`, can be a single number or an `[xsize,ysize]` vector. Default: [1,1] +// 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. For the "superellipse" style, the special case where the superellipse exponent is 4 (also known as *Lamé's quartic curve*) results in a squircle at the geometric mean between radial points on the circle and square, corresponding to squareness=0.456786. Default: 0.5 // style = method for generating a squircle, "fg" for Fernández-Guasti and "superellipse" for superellipse. Default: "fg" // atype = anchor type, "box" for bounding box corners and sides, "perim" for the squircle corners -// $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 non-uniformly around the squircle so they are more dense at sharper curves. Default if not set: 40 +// $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(squareness=0.4, size=50); -// squircle(0.8, [80,60], $fn=64); +// squircle(size=50, squareness=0.4); +// squircle([80,60], 0.7, $fn=64); // Examples(2D): Ten increments of squareness parameter for a superellipse squircle // for(sq=[0:0.1:1]) -// stroke(squircle(sq, 100, style="superellipse", $fn=128), closed=true, width=0.5); +// stroke(squircle(100, sq, style="superellipse", $fn=128), closed=true, width=0.5); // Examples(2D): Standard vector anchors are based on the bounding box -// squircle(0.6, 50) show_anchors(); +// squircle(50, 0.6) show_anchors(); // Examples(2D): Perimeter anchors, anchoring at bottom left and spinning 20° -// squircle(0.5, [60,40], anchor=(BOTTOM+LEFT), atype="perim", spin=20) +// squircle([60,40], 0.5, anchor=(BOTTOM+LEFT), atype="perim", spin=20) // show_anchors(); -module squircle(squareness=0.5, size=[1,1], style="fg", atype="box", anchor=CENTER, spin=0) { +module squircle(size, squareness=0.5, style="fg", atype="box", anchor=CENTER, spin=0) { 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, squareness, style, atype, _module_call=true); if (atype == "box") { - path = squircle(squareness, size, style); - attachable(anchor, spin, two_d=true, size=size) { + attachable(anchor, spin, two_d=true, size=size, extent=false) { polygon(path); children(); } } else { // atype=="perim" - override_path = squircle(squareness, size, style, atype, _return_override=true); - attachable(anchor, spin, two_d=true, size=size, extent=false, override=override_path[1]) { - polygon(override_path[0]); + attachable(anchor, spin, two_d=true, extent=true, path=path) { + polygon(path); children(); } } } -function squircle(squareness=0.5, size=[1,1], style="fg", atype="box", anchor=CENTER, spin=0, _return_override=false) = +function squircle(size, squareness=0.5, style="fg", atype="box", anchor=CENTER, spin=0, _module_call=false) = assert(squareness >= 0 && squareness <= 1) - assert(is_num(size) || is_vector(size,2)) + assert(is_num(size) || is_vector(size,2)) assert(in_list(atype, ["box", "perim"])) let( - path = - style == "fg" ? _squircle_fg(squareness, size) - : style == "superellipse" ? _squircle_se(squareness, size) - : assert(false, "Style must be \"fg\" or \"superellipse\""), size = is_num(size) ? [size,size] : point2d(size), - a = 0.5 * size[0], - b = 0.5 * size[1], - override = atype == "box" ? undef - : let( - sn = style=="fg" ? _linearize_squareness(squareness) - : _squircle_se_exponent(squareness), - derivq1 = style=="fg" ? // 1+derivative of squircle in first quadrant - function (x) let(s2=sn*sn, a2=a*a, b2=b*b, x2=x*x, denom=a2-s2*x2) a2*b*(s2-1)*x/(denom*denom*sqrt((a2-x2)/denom)) + 1 - : function (x) let(n=sn) 1 - (b/a)*((a/x)^n - 1)^(1/n-1), - xc = root_find(derivq1, 0.01, a-0.01), // find where slope=-1 - yc = style=="fg" ? - let(s2=sn*sn, a2=a*a, b2=b*b, x2=xc*xc) sqrt(b2*(a2-x2)/(a2-s2*x2)) - : b*(1-(xc/a)^sn)^(1/sn), - corners = [[xc,yc], [-xc,yc], [-xc,-yc], [xc,-yc]], - anchorpos = [[1,1],[-1,1],[-1,-1],[1,-1]] - ) [ for(i=[0:3]) [anchorpos[i], [corners[i]]] ] - ) _return_override - ? [reorient(anchor, spin, two_d=true, size=size, p=path, extent=false, override=override), override] - : reorient(anchor, spin, two_d=true, size=size, p=path, extent=false, override=override); + path = style == "fg" ? _squircle_fg(size, squareness) + : style == "superellipse" ? _squircle_se(size, squareness) + : assert(false, "Style must be \"fg\" or \"superellipse\""), + ) reorient(anchor, spin, two_d=true, size=atype=="box"?size:undef, path=_module_call?undef:path, p=path, extent=true); /* FG squircle functions */ -function _squircle_fg(squareness, size) = [ +function _squircle_fg(size, squareness) = [ let( sq = _linearize_squareness(squareness), size = is_num(size) ? [size,size] : point2d(size), aspect = size[1] / size[0], r = 0.5 * size[0], - astep = $fn>=12 ? 90/round($fn/4) : 9 + astep = $fn>=12 ? 90/round($fn/4) : 360/48 ) for(a=[360:-astep:0.01]) let( theta = a + sq * sin(4*a) * 30/PI, // tighter angle steps at corners p = squircle_radius_fg(sq, r, theta) @@ -2104,13 +2084,13 @@ function _linearize_squareness(s) = /* Superellipse squircle functions */ -function _squircle_se(squareness, size) = [ +function _squircle_se(size, squareness) = [ let( 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) : 9, + astep = $fn>=12 ? 90/round($fn/4) : 360/48, 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