-
Notifications
You must be signed in to change notification settings - Fork 127
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
added squircle to shapes2d.scad #1517
Conversation
Is this shape different than the shape produced by supershape(), e.g. those in example 3? The wikipedia page for "squircle" suggests that the most common "squircle" is the superellipse, and we already have that as a special case of supershape. |
Indeed, supershape example 3 does generate squircles. The difference is in implementation and capability:
The squircle wouldn't be the only thing in BOSL2 in which it's possible to do something multiple different ways. For example, you can use The ability to have a small number of vertices with optimized spacing also makes it feasible to generate a "sphube" (intermediate between shpere and cube) as a VNF with a reasonable facet count. I was planning to do that in a future PR. |
Ok. What is the mathematical meaning of the squareness parameter? It seems for example that it might be nice if it was easily convertible to the cut parameter used in rounding.scad, because that would make it easy to swap between rounded shapes with some amount of commonality. Note, it's not true that an unbounded parameter leads to "inferior control" compared to a parameter on [0,1]. Perhaps more intuitive control, but the same amount of control is available in each case. Are the shapes with exponents between 1 and 2 (the sub-circles) not useful or not different? (Seems possible they are the same shapes, just rotated and differently scaled---I haven't checked.) Also, why are there anchors floating in space in your pic above? You might want to join the gitter developer chat for BOSL2 to talk about this kind of thing before going straight to the PR stage. https://app.gitter.im/#/room/#revarbat_BOSL2:gitter.im |
The squareness parameter is a feature of the Fernández-Guasti squircle described in the Wikipedia article, which says it "allows a smooth parametrization of the transition to a square from a circle, without involving infinity." It's also a feature of the periodic form. As far as I can tell, the parameter behaves the same way in both. As you said, it's more intuitive. However, the squareness doesn't have a linear response. That is, if you set squareness=0.8, you don't get a squircle with corners 80% of the distance between the circle and the corners of the square. This squareness changes mostly in the 0.8-1.0 range. In my latest commit I have made this response approximately linear and included a demo example of this. The anchors are wrong because I stupidly used the width of the squircle instead of the radius to calculate their positions. I'll fix that in my next commit. Honestly I wasn't sure what I should see, because it's a feature I never used, so I didn't notice it was wrong. I've been using this function standalone in other projects, and I was more comfortable using it because it appears more straightforward and friendly than supershape, so I felt it was worth a PR, and something simple before I do the pull request for vnf_tri_array() that we've been discussing in #1514. I didn't know about any discussion area, and that link didn't work for me. It generated a bunch of console errors in Chrome. |
Is there some compelling reason to not make the "squareness" parameter exactly linear? That would make it more usable for somone who wants to know where the curve will end up, for example. Wait, did you implement the FG squircle? That's totally different than the superellipse squircle. If that's the case then exact adherence to the FG equation could be an argument for nonlinearity. This might be something to bounce off Revar and Richard in the chat. We might also want to show some kind of comparison of how this compares to round_corners with different k values or just a circular rounded square. The 8 anchors should lie on the boundary of the shape if you have it right, with the arrow perpendicular to the shape. Try https://gitter.im/revarbat/BOSL2 or if that doesn't work, try going to app.gitter.im and then see if you can find the BOSL2 chat somehow. |
I can make it closer to linear. I could also make it nearly exact, but I would need to introduce a curve-fitted polynomial correction with hard-coded coefficients. The correction there now is a simple formula, basically
This results in a maximum error when squareness=0.48, in which case the corner of the squircle is 0.55 of the way to the corner of the square. The error approaches zero as squareness approaches 0 or 1. If I use the fourth root instead of the square root, the error is even less, with the maximum error at squareness=0.04, making the "corner" (it's almost exactly a circle at that point) 0.036 away from the corner of the square. I fixed the anchor problem and pushed a new commit, but the anchors don't account for the squareness correction. It's past midnight here, so I'll revisit this tomorrow. Update: I just noticed, in the research paper I've been using for this, a linearization of the squareness parameter. It works! If s is squareness and c=2-2*sqrt(2) then
This results in errors no larger than 1e-15, effectively zero. |
Ah, that's better. Perfectly linear now! This is the demo using 10 equal intervals of squareness between 0 and 1:
Yes, I implemented the polar form of the FG squircle, from this paper: https://arxiv.org/pdf/1604.02174v5 - and that paper goes into much detail about linearizing the squareness parameter. I think that justifies linearizing it here too. It's way more convenient to use that way. I've also updated the Wikipedia article with the information from that paper: https://en.wikipedia.org/wiki/Squircle#Fern%C3%A1ndez-Guasti_squircle |
Ah, I was going to say it's easy to transform it to a linear parameter from the equation----no research paper required. You just identify the desired "corner" coordinate R*[a,a], where a needs to go from 1/sqrt(2) for a circle up to 1 for a square. If b is a linear parameter on [0,1] then we have a=lerp(1/sqrt(2),1,b). Plug that into the FG equation and solve and you get s^2 = (2*a^2-1)/a^4. Note that it's equally simple to transform the exponent of the supershape into a linear parameter on [0,1]. I haven't delved into the literature. The argument for using the FG equation squareness parameter directly would be that the optics folks who invented it know and use it with that original functional form and would be confused to encounter it with a different parameter. I'm not saying that is compelling for our situation. Another question arises: is the FG squircle the right one to use? Wikipedia says it's the "less common" version. (I've known about the superellipse for years and never heard of the other thing until yesterday.) Between this one and the superellipse, does one have an advantage? Smoother? Looks better? They are definitely different, so I want to know why we picked FG. Or should we support both with a squircle style flag? |
I took out "most common" from the Wikipedia article. An assertion like that needs sources to back it up, and the sources I have found mention both types. I suspect that it doesn't matter how you arrive at the squircle, I suspect it may be the same shape from either method. I'll have to do some tests on that. The advantage in OpenSCAD is that it's computationally less expensive to avoid dealing with fractional exponents, but this likely wouldn't make much difference in execution time. Should I revise this to use superellipse? We could use the style flag if they're different. |
No, it's definitely not the same shape. I mean, that's obvious from the functional form that it can't be, but I plotted a superellipse on an FG and they were visibly different, with the superellipse being more squarish. You're using trig functions in the computation and worried about some fractional exponents? I expect run time for sin and cos are comparable to pow. Computation time should not be a concern, in any case. My question above was an honest one. While to me the mathematical formulation of the superellipse is more elegant, I don't know for practical applications, what distinguishes the two shapes or what would make the FG shape better/worse than the superellipse. If you had both available...how would you pick? It would also be interesting to compare the FG supershape to a continuous curvature rounded rectangle, which because it uses a 4th degree bezier, could actually be the same shape. (But actually not really because the rounded rectangle has a flat section.) I noticed that the squircle references indicate that the superellipse version is the one specific superellipse with n=4. Regarding which curve is better known or more used, it seems that the superellipse has got to be the winner there. The superellipse equation was described 100 years before FG wrote their paper. It was used in a font in 1952. But under the specific name "squircle"? That name is clearly a recent coinage, apparently much newer than either the superellipse or the FG shape. |
You beat me to it. I was just starting on the comparison myself. Thanks for the plot. Yes, there's a subtle difference. And yes you're correct, I was searching for articles containing the word "squircle", so naturally everything I found mentions the FG squircle as well as the superellipse squircle. You asked:
I'd pick FG because it is mathematically well-behaved. While the expression for the superellipse is elegant in its simplicity, it's inelegant mathematically in its behavior. The main disadvantage to the superellipse is that you're dealing with an infinite exponent for squareness=1. Even when you get close to 1, the exponent gets quite large, potentially causing computational instability (although it's unlikely for expected use cases). As I said near the beginning of this discussion, the FG squircle (quoting Wikipedia) "allows a smooth parametrization of the transition to a square from a circle, without involving infinity." I'd say that would be its main advantage. |
Thinking about it some more, I'll retract my statement about mathematical behavior. The polar form of the FG squircle doesn't behave well either when the angle is multiples of 90° and I deal with that simply by setting the squircle radius to the circle radius in those cases. For the superellipse exponent going to infinity, the squareness parameter could be clipped at 0.99, corresponding to n=236.3 (I've implemented it here and it blows up past squareness=0.993), resulting in a shape indistinguishable from a square with the same number of vertices as the squircles. Retaining the vertex count would be useful for skinning between two squircle shapes. How to proceed? It isn't a problem to support both now that I've written the code for both, but my preference is still for the FG squircle. |
I didn't ask you which you'd pick as a programmer. I want to know which one you'd pick as a user who doesn't care how the thing is implemented, and whether it was computationally tricky or not. It's the job of the library author to handle those challenges and do the right thing rather than the easy thing, because we're doing it once and for all. I'd argue that there's nothing wrong with infinity in math and there's nothing ill behaved about the superellipse. In fact, it's quite neat. The characteristic you're complaining about is a computational issue, not a mathematical one, and it doesn't appear to be particularly bad. In fact, it seems to be surprisingly robust. It appears that maybe in the case of cartesian coordinates, the roundoff error produces errors that give you the right answer (square), for example. In polar coordinates it may not be so tidy, but I still got a good point with an exponent of 2000. I thought it would fail at a lower value. It appears to me that failure is primarily a result of underflow error. The mathematical behavior of the curves I'd consider to be things like its derivatives, or the curvature along the curve: the properties of the curve as an ideal mathematical object. Does the superellipse have anything to recommend it over the FG curve in terms of these properties? Why would someone who knows nothing about the mathematics of the curves pick one or the other? Are there mathematical properties that suggest picking one or the other? Can we even distinguish the curves from each other at all if we see them without context in isolation? (Quick, is this a superellipse or an FG?) One can ask the question, why not just use a circle. When I wrote all the continuous curvature rounding stuff someone had claimed that such rounding was what apple used for their device corners. It seems like the story always changes. But continuous curvatures really does look and feel different. In a 3d printed model you don't see a highlight where the roundover starts and it feels smoother as well. It seems likely that curvature for both FG and superellipse is continuous, though perhaps something odd happens at face centers. So suppose we implemented everything and now we have to write a rounding tutorial and tell a user how to pick between superellipse, FG, using rect() with circular rounded corners, or using continuous curvature bezier corners. How does a user decide? |
As a user concerned primarily with designing objects, (and one who would likely be befuddled trying to work through the underlying math), I would argue for including both forms and using a flag to make the selection. That makes it possible to try both quickly and use the one that provides the best fit for the current model. |
It's curious that you managed a higher exponent; was this in OpenSCAD? I wrote it separately from BOSL2 to test. Good questions.
Well, in polar form, the derivative of r with respect to theta is a cleaner-looking expression for the superellipse than for the FG squircle (I tried both in Wolfram Alpha). For both shapes, the curvature is continuous and smoothly changing. For both shapes, nothing odd happens at face centers because the underlying mathematical functions are differentiable and the derivatives have no discontinuities.
Someone may want the superellipse to duplicate shapes made with it (like fonts, certain buttons on devices). Aside from those who work in optics who already use the FG curve, someone else may want the FG version because literature on superellipse covers vastly more than the specific case of a squircle, and literature on the FG squircle covers exactly that. The reason I picked FG years ago was because it was the first thing I found in polar form during a literature search for "interpolate between a circle and a square" (I didn't even know the term "squircle" at the time), and the Wikipedia article back then didn't help me figure out how to implement it in OpenSCAD, especially with all that stuff about gamma functions. I am the person who added the polar form to that article, just yesterday.
I don't know. From my perspective, infinite exponents and overflow errors are undesirable.
In isolation, they are indistinguishable. Compared overlaid, the superellipse squircle is ever so slightly more rounded than the FG squircle.
These reasons come to mind:
Even though I prefer FG, in light of that question, the best approach to avoid confusion would be simply to not offer FG and base the squircle on the superellipse. This maintains consistency with what BOSL2 already offers.
Continuous curvature bezier corners would be more difficult to deal with if a designer wants an attractive interpolation that can go all the way from circular to sharp square corners. I use beziers in almost all my projects, but for the recent discussion in #1514 about a basket that transitions from a square to a circle, I would want a squircle for the perimeter, not beziers. I would use beziers to control how the squircle radius changes as a function of z. As for squircle versus square with rounded corners, this may be more of an aesthetic choice depending on the rounding. The smooth roundover could simulate a squircle with the right combination of the |
Well, the thing is, they're both visually indistinguishable unless they're overlaid. Just different approaches to create a shape. Would providing two choices to accomplish effectively the same shape confuse the user? The problem here is that I unwittingly introduced a new method to produce a squircle, when other methods to do it were already available in BOSL2, just not implemented for this specific shape with the parametric control offered by the |
I would argue that adding the squircle() function would still be useful. It's an issue of discoverability. For a rank beginner finding the existing squircle methods would require scrolling through the examples and stumbling upon a squircle. Nothing in either the topics index or the function index would help guide the casual user to any of the exiting methods. |
@RAMilewski - Oh, I think we all agree it should be there. The question now is what version of squircle() should be implemented given that they are nearly indistinguishable, or whether we should make a choice with a tag as you suggest, even though the user is unlikely to be able to tell the difference. |
I did my numerical testing in MATLAB with r=1. In the polar case, the R value factors out so that's how the implementation works anyways. I also only spot-checked the point at 45 deg for that test so maybe it fails for some other theta at N=2000. I did the full curve in cartesian, again for r=1. It's actually sort of obvious why it works really well in the cartesian case: The equation is x^n+y^n=1, which solves to y=(1-x^n)^(1/n) and when n is too big, x^n->0, but 1^n is always 1 no matter what. So it reduces to 0 to 1/n which will be zero always until n is so big that 1/n underflows to zero, and then 1 always in the second case. Very tidy actually. Substantially less tidy in polar coordinates, unfortunately. The cartesian representation is obviously bad from a point sampling perspective. Note that by mathematical behavior I don't mean what the expressions look like but what they do. Like what does the graph of curvature look like as a function of theta? Does one of these things have a smaller curvature than the other one, or distribute its curvature in a different way? What is the minimum/maximum curvature as a function of squareness? I'm not sure what you mean about someone preferring the FG because "literature on superellpse covers vastly more than the specific case of a squircle." There is no problem with overflow in computing the superellipse, only underflow, and as previously noted, in cartesian coordinates it seems to actually magically handle itself and producing the n=infinity case automatically, which is pretty tidy. Indistinguishability in isolation is not necessarily an argument that they are completely interchangeable. It's an obvious first question to pose. Maybe a better question is whether a user can tell the difference if they flip the flag that changes the type so they see the curve flip. Heck, maybe we should support both only because nobody does that and it lets someone who is curious easily flip back and forth and see both forms and how different or not they are. Nobody has to use the flag that changes the type if they don't want to. Note that just because the two things are similar doesn't mean we can't have both. We can make one the default, document that they are "slightly different" and let users choose. Maybe somebody seems them as markedly different, or maybe the exact shape of one interacts with a model in some way that works better for somebody. I should have been more clear when I posed the question "why not just use a circle". What I meant was a circular rounded rectangle. You say that continuous curvature bezier corners would be more difficult for going from circular to sharp, which I suppose is true. But since nobody should ever want actual sharp, one can counterargue that for example a rounding with maximum joint value and k going from 0 to 0.92 actually fits the bill quite well. Note that if you're trying to make the maximum width rounding you shouldn't be trying to minimize k but instead should be setting joint to the amount of space you have and then tuning k to control the shape you get. The above shape:
It just occurred to me that you should make sure that the corner anchors are doing something reasonable in the case of non-square sizes. I am thinking "reasonable" means they point out at 45 deg and are located at the corners. I wonder if we need "box" and "perim" anchors e.g. like rect() has. Probably the answer there is yes. |
Here's my test recoded in OpenSCAD. It fails somewhere above n=2150 in OpenSCAD on my machine.
Perhaps there's a complication arising in the non-square case. I really haven't thought about that case. It seems like from a computational point of view it should just be a rescale of the unit case. There may be an issue about what the squareness parameter means, though. Note that your example is not a non-square case for displaying anchor behavior. Wait, actually you have an issue with how anchors are done. We don't want any named anchors, just the basic anchors. I think we need an atype flag and two kinds of anchors, anchors on the notional square the size specifies and anchors actually on the shape like you already have but for the second kind, you need to use the anchor override feature to specify them, not named anchors. The question about curvature isn't whether it's smooth, which it will be since both functions are C^infinity, it's what its max and min values are. That's one thing that could distinguish the two super shapes from each other, if say the FG has a higher curvature at its point (for same parameter) you could say it's "pointier". |
Yes, if you use the extent=true "hull" type anchoring the anchors will be on the path, not on the mathematical perimeter (which is not available to the code so obviously impossible). The yellow squircle seems to have a really large facet right around where the anchor is being placed while the rest of the visible perimeter looks smooth. Is that right? So is this ready to be approved? |
That yellow squircle was made with $fn=40. That's only 10 segments per quadrant, and the segments are made smaller at the corners. So the segment there at the anchor is actually smaller than it normally would be at that $fn value. You can see this with 'show edges' enabled: I observed that if $fn is divisible by 8, the anchors line up with the superellipse anchors. At least for this case. I may as well change the default $fn anyway. Not yet ready -- I have to remove all the stuff about anchor overrides, then I'll do a bit more testing, and push another commit. Hard to believe I spent a couple of days on this only to delete it all, but at least I learned a lot, and I appreciate that. |
I kinda feel a little guilty about having lead you on a run-around on the anchoring business here. Sorry about that. I'll admit that it seems pretty routine that adding some new object is 10% coding the object and 90% coding either the anchors or the parameter error checking and parameter processing, or the docs writing, or some other indirect thing relating to the object rather than the object itself. Someone recently asked for ring() which was in that category, where making a ring is trivial, but doing all the argument checking for all the cases...ug. Wouldn't the alignment to superellipse anchors depend on the step size used for the superellipse? If you configure them to match then they should line up...otherwise no. It seems like there's a sharp corner in the example above right where the anchor connects. It's a little easier to see what's happening if you view 2d stuff directly from above (e.g. click on the z+ view). But I'm sure it gets better with higher $fn, so I doubt there's anything worrisome there. |
Yes, that sharper corner is an artifact of low $fn. I agree the alignment with supershape likely depends on step size, but in any case a high value of $fn makes them align quite well. Hmm, I should have it change dynamically like it does with circle(), with more steps for larger shapes. Don't feel guilty, I appreciate the knowledge, and it was valuable for me to learn about function literals, which in all the years I've been working with OpenSCAD, I never noticed before but often wished for such a capability. |
I'm not sure what you mean about dynamic change "like circle(), with more steps for larger shapes". That is if you use $fa and $fs. With $fn, circle always gives you the specified number of facets. |
BTW, function literals were only added in 2021. But hang out with us library authors and we'll teach you all the OpenSCAD tricks. :) |
I mean if you don't specify $fn, a circle changes the number of segments depending on the size of the circle (at least for small circles), because it uses $fa=12 together with $fs=2 by default. I thought I might increase the resolution of the squircle if $fn isn't set, based on the radius of the circle when squareness=0. |
Your original stated plan was to respect only $fn. It seems reasonable to respect $fa and $fs based on the circle corresponding to the specified size, though it's not entirely obvious how to handle that for ellipses. I looked at ellipse(). It treats the largest semi-axis as the effective radius for calculating the number of segments of the curve. There is infrastructure in place to do this in BOSL2 with the segs() function. |
I am hopelessly confused now. I'm obviously missing something. Three times now, I've restored the original code and change it a bit at a time, and at some point it stops respecting the atype="perim", and I don't know what I did. Here's the module and function. Does anything jump out at you?
|
You are giving a size argument to attachable() in the "perim" branch, that case I specifically warned about earlier where the code decides it's going to "do something" instead of failing with an assert when giving conflicting inputs. (Are you convinced yet that this "do something" strategy is a terrible idea?) Unrelated: I would prefer not to have a default for size. Revar is more a fan of defaults like that. I tend to think they're random and arbitrary and the user should say what they mean. But rect() does have a default so you can choose to follow rect if you prefer. |
Ah, that was it, the size. I wonder why I didn't see it. That fixed the module, but calling the function to use with polygon() still acts like it's using atype="box". If I remove the size perameter from reorient(), it centers the shape but still rotates it. As for defaults, I did that because OpenSCAD uses a default size for things like square() and circle(). I can remove it, however. |
I notice you didn't say whether your perspective on asserting for conflicting inputs has changed. I'm not sure what you mean about the function case. If you remove the size parameter to reorient in the atype=="perim" case is something wrong? And if so, what exactly? Are you saying the shape comes out centered regardless of the anchor value? (Of course you need the size parameter for the "box" case.) I would maybe not pass atype when invoking the function form from the module. It's presumably best to have the simpler anchor case run, though I'm not sure it makes any difference since you just get the center anchor. I think you can set extent to true unconditionally because it's only used when no size is given. But in the module form I would not set extent in the "box" branch just because it's confusing to pass a parameter that's not used. |
As soon as I submit my last comment, I figure out the problem. It works with supershape. In reorient(), the supershape function sets two parameters, p and path, to path. It also omits the size. My call to reorient() was left over from the older override anchor code, setting size and p but not path. I did it like supershape, and it all works now. Correction: now "box" broke with the polygon.... |
Are you doing something like I had forgotten you had to pass the path twice. I think it's one time for purposes of anchoring and one time to just be the path that gets processed, so you don't have to use the path as the anchoring method. |
I ended up doing it like this:
I had to add a parameter |
…ixes, documentation cleanup
All right. I have pushed the latest changes. Have a look. No hurry. |
Of note, |
I used to be of the opinion that args should have "reasonable" defaults. But that gets nonsensical sometimes, and my idea of reasonable is not necessarily the most usable or sensible for all situations. I'm starting to turn to the dark side of not giving defaults, but giving examples that seem sensible to me. |
I can't say I understand those test results. There's a syntax error being reported in shapes2d.scad, referencing a variable name that was used in an older commit but no longer exists, making me think the tests weren't run on the most recent commit. |
You have a trailing comma on line 2054 in the let statement. That is not allowed in the stable version 2021.01. Since that's the official stable version, everything needs to work there, and the trailing comma thing is one of the big irritations, most definitely. BTW, you shouldn't need that _module_version or whatever it was flag. Just make sure to pass atype="box" when invoking from the module. Don't pass the actual user-specified atype. |
…arameter, fixed spelling error in Reuleax documentation
I just pushed a new commit. And then I pushed another after I remembered I still wasn't using 2021.01 and found another trailing comma. I have both 2021.01 and a more recent version of OpenSCAD installed here, but I've been using a more recent one lately due to a new feature required in other projects. Otherwise, I try to write compatibly with 2021.01 also. |
Hopfully that last commit passes the tests. I was using "Examples" instead of "Example" for a multi-line code example. |
Sigh. Another error in the documentation comments. A bad internal link. OK, I pushed another fix. |
I have used this shape in a couple of projects and thought it would be a useful addition to BOSL2.
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.
The basic shape looks like this:
I have implemented a module and function for a squircle in
shapes2d.scad
. It takes a parameter forsquareness
(between 0 and 1, where 0=circle and 1=square), and asize
parameter the same as withsquare()
to define the bounding box for the squircle.Because the curvature can change sharply at the corners, the vertices are distributed uniformly only when the shape is a circle (
squareness=0
). The vertices concentrate more toward the corners assquareness
approaches 1. This results in a fairly smooth shape even with low$fn
values.