RoundSuperellipse algorithm v3: Ultrawideband heuristic formula (#164755)

This PR revises the algorithm for RoundSuperellipses, replacing the
current "max ratio" approximation with an algorithm that works for
ratios from 2.0 to infinity.

The previous "max ratio" approximation, which replaces the middle of
edges with straight lines when the ratio is above 2.3, turns out to
produce results too close to classic RRects. After reexamining the
shapes and more calculation, I discovered that the max-ratio
approximation is flawed. Even squircles with with really high ratios
(~100) have a significant part of the edges that must not be
approximated by straight lines.

The new version is much closer to native.

### Comparison
Native: (Notice the long wedgy gap at the end of curves)
<img
src="https://github.com/user-attachments/assets/61b60191-7d45-4c49-9e09-b0422243cd8c"
width="400"/>

Before PR: (Notice the short wedgy gap at the end of curves)
<img
src="https://github.com/user-attachments/assets/15ea374b-4b16-4187-aaa4-94f432fbb61e"
width="400"/>

After PR:
<img
src="https://github.com/user-attachments/assets/973ef4d1-7c26-44a9-b45e-10d109d5618b"
width="400"/>

Another example (after PR). Even though the rectangular RSE has ratios
of around 4, there are still curvature near the middle section of edges,
which can be identified with the help of antialias pixels.
<img width="838" alt="image"
src="https://github.com/user-attachments/assets/5078d098-c582-48a8-81e5-615909def675"
/>

### Details

I found that `n` has really good linearity towards larger ratios.

<img width="844" alt="image"
src="https://github.com/user-attachments/assets/73e99e45-a0f0-450b-8e2b-f6fd97082958"
/>

I also found a good candidate for the precomputed unknown (called
`k_xJ`), which has a smooth curve at the beginning and almost straight
line towards larger `n`, removing the need to cap the scope of
application of the formula.

<img width="1203" alt="image"
src="https://github.com/user-attachments/assets/67664898-2dbd-4f00-a9ba-d76030cf3742"
/>

The algorithm for paths are also updated in a similar way and
approximated the Bezier factors with heuristic formulae for bigger `n`s.
I've also verified that the path deviates from the geometry by no more
than 0.01% over the range of n [15, 100]

Theoretically removing "stretch" should simplify the algorithms.
Unfortunately I had to spend more lines to process cases of zero radii,
which were conveniently handled by stretches.


## Pre-launch Checklist

- [ ] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [ ] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [ ] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [ ] I signed the [CLA].
- [ ] I listed at least one issue that this PR fixes in the description
above.
- [ ] I updated/added relevant documentation (doc comments with `///`).
- [ ] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [ ] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [ ] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
This commit is contained in:
Tong Mu 2025-03-07 18:39:09 -08:00 committed by GitHub
parent 3f90143391
commit 83781ae65c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 207 additions and 170 deletions

View File

@ -291,37 +291,38 @@ size_t DrawOctantSquareLikeSquircle(Point* output,
if (reverse_and_flip) {
transform = transform * kFlip;
}
if (param.se_n < 2) {
// It's a square.
*output = transform * Point(param.se_a, param.se_a);
return 1;
}
/* The following figure shows the first quadrant of a square-like rounded
* superellipse. The target arc consists of the "stretch" (AB), a
* superellipsoid arc (BJ), and a circular arc (JM).
* superellipse. The target arc consists a superellipsoid arc (AJ) and a
* circular arc (JM).
*
* straight superelipse
*
* A B J circular arc
* ---------...._
* | | / ` M (where y=x)
* | | /
* | | / \
* | | / |
* | | D |
* | | / |
* +----+ S |
* s | | |
* +----+---------------| A'
* superelipse
* A circular arc
* ---------...._J
* | / ` M (where x=y)
* | /
* | / \
* | / |
* | D |
* | |
* | |
* | |
* +--------------------| A'
* O
* s
* ------ size/2 ------
* -------- a ---------
*/
Point* next = output;
if (!reverse_and_flip) {
// Point A
*(next++) = transform * param.edge_mid;
// Arc [B, J)
next += DrawSuperellipsoidArc(
next, param.se_a, param.se_n, param.se_max_theta, reverse_and_flip,
transform * Matrix::MakeTranslation(param.se_center));
// Arc [A, J)
next +=
DrawSuperellipsoidArc(next, param.se_a, param.se_n, param.se_max_theta,
reverse_and_flip, transform);
// Arc [J, M)
next += DrawCircularArc(
next, param.circle_start - param.circle_center,
@ -333,14 +334,12 @@ size_t DrawOctantSquareLikeSquircle(Point* output,
next, param.circle_start - param.circle_center,
param.circle_max_angle.radians, reverse_and_flip,
transform * Matrix::MakeTranslation(param.circle_center));
// Arc [J, B)
next += DrawSuperellipsoidArc(
next, param.se_a, param.se_n, param.se_max_theta, reverse_and_flip,
transform * Matrix::MakeTranslation(param.se_center));
// Point B
*(next++) = transform * (param.se_center + Point{0, param.se_a});
// Arc [J, A)
next +=
DrawSuperellipsoidArc(next, param.se_a, param.se_n, param.se_max_theta,
reverse_and_flip, transform);
// Point A
*(next++) = transform * param.edge_mid;
*(next++) = transform * Point(0, param.se_a);
}
return next - output;
}

View File

@ -17,16 +17,18 @@ namespace {
// Utility functions used to build a rounded superellipse.
class RoundSuperellipseBuilder {
public:
typedef std::function<
void(const Point&, const Point&, const Point&, const Point&)>
CubicAdder;
using CubicAdder = std::function<
void(const Point&, const Point&, const Point&, const Point&)>;
using PointAdder = std::function<void(const Point&)>;
// Create a builder.
//
// The resulting curves, which consists of cubic curves, are added by calling
// `cubic_adder`.
explicit RoundSuperellipseBuilder(CubicAdder cubic_adder)
: cubic_adder_(std::move(cubic_adder)) {}
explicit RoundSuperellipseBuilder(CubicAdder cubic_adder,
PointAdder point_adder)
: cubic_adder_(std::move(cubic_adder)),
point_adder_(std::move(point_adder)) {}
// Draws an arc representing 1/4 of a rounded superellipse.
//
@ -37,6 +39,11 @@ class RoundSuperellipseBuilder {
bool reverse) {
auto transform =
Matrix::MakeTranslateScale(param.signed_scale, param.offset);
if (param.top.se_n < 2 || param.right.se_n < 2) {
point_adder_(transform *
(param.top.offset + Point(param.top.se_a, param.top.se_a)));
return;
}
if (!reverse) {
AddOctant(param.top, /*reverse=*/false, /*flip=*/false, transform);
AddOctant(param.right, /*reverse=*/true, /*flip=*/true, transform);
@ -49,7 +56,7 @@ class RoundSuperellipseBuilder {
private:
std::array<Point, 4> SuperellipseArcPoints(
const RoundSuperellipseParam::Octant& param) {
Point start = {param.se_center.x, param.edge_mid.y};
Point start = {0, param.se_a};
const Point& end = param.circle_start;
constexpr Point start_tangent = {1, 0};
Point circle_start_vector = param.circle_start - param.circle_center;
@ -126,24 +133,33 @@ class RoundSuperellipseBuilder {
// on a rounded superellipse and are not for general purpose superellipses.
std::array<Scalar, 2> SuperellipseBezierFactors(Scalar n) {
constexpr Scalar kPrecomputedVariables[][2] = {
/*n=2.000*/ {0.02927797, 0.05200645},
/*n=2.050*/ {0.02927797, 0.05200645},
/*n=2.100*/ {0.03288032, 0.06051731},
/*n=2.150*/ {0.03719241, 0.06818433},
/*n=2.200*/ {0.04009513, 0.07196947},
/*n=2.250*/ {0.04504750, 0.07860258},
/*n=2.300*/ {0.05038706, 0.08498836},
/*n=2.350*/ {0.05580771, 0.09071105},
/*n=2.400*/ {0.06002306, 0.09363976},
/*n=2.450*/ {0.06630048, 0.09946086},
/*n=2.500*/ {0.07200351, 0.10384857}};
constexpr Scalar kNStepInverse = 20; // = 1 / 0.05
/*n=2.0*/ {0.01339448, 0.05994973},
/*n=3.0*/ {0.13664115, 0.13592082},
/*n=4.0*/ {0.24545546, 0.14099516},
/*n=5.0*/ {0.32353151, 0.12808021},
/*n=6.0*/ {0.39093068, 0.11726264},
/*n=7.0*/ {0.44847800, 0.10808278},
/*n=8.0*/ {0.49817452, 0.10026175},
/*n=9.0*/ {0.54105583, 0.09344429},
/*n=10.0*/ {0.57812578, 0.08748984},
/*n=11.0*/ {0.61050961, 0.08224722},
/*n=12.0*/ {0.63903989, 0.07759639},
/*n=13.0*/ {0.66416338, 0.07346530},
/*n=14.0*/ {0.68675338, 0.06974996},
/*n=15.0*/ {0.70678034, 0.06529512}};
constexpr size_t kNumRecords =
sizeof(kPrecomputedVariables) / sizeof(kPrecomputedVariables[0]);
constexpr Scalar kStep = 1.00f;
constexpr Scalar kMinN = 2.00f;
constexpr Scalar kMaxN = kMinN + (kNumRecords - 1) * kStep;
Scalar steps =
std::clamp<Scalar>((n - kMinN) * kNStepInverse, 0, kNumRecords - 1);
if (n >= kMaxN) {
// Heuristic formula derived from fitting.
return {1.07f - expf(1.307649835) * powf(n, -0.8568516731),
-0.01f + expf(-0.9287690322) * powf(n, -0.6120901398)};
}
Scalar steps = std::clamp<Scalar>((n - kMinN) / kStep, 0, kNumRecords - 1);
size_t left = std::clamp<size_t>(static_cast<size_t>(std::floor(steps)), 0,
kNumRecords - 2);
Scalar frac = steps - left;
@ -155,6 +171,7 @@ class RoundSuperellipseBuilder {
}
CubicAdder cubic_adder_;
PointAdder point_adder_;
// A matrix that swaps the coordinates of a point.
// clang-format off
@ -407,13 +424,15 @@ PathBuilder& PathBuilder::AddRoundSuperellipse(RoundSuperellipse rse) {
RoundSuperellipseBuilder builder(
[this](const Point& a, const Point& b, const Point& c, const Point& d) {
AddCubicComponent(a, b, c, d);
});
},
[this](const Point& a) { LineTo(a); });
auto param =
RoundSuperellipseParam::MakeBoundsRadii(rse.GetBounds(), rse.GetRadii());
Point start = param.top_right.offset +
param.top_right.signed_scale *
(param.top_right.top.offset + param.top_right.top.edge_mid);
Point start =
param.top_right.offset +
param.top_right.signed_scale *
(param.top_right.top.offset + Point(0, param.top_right.top.se_a));
MoveTo(start);
if (param.all_corners_same) {

View File

@ -34,45 +34,68 @@ inline Point Flip(Point a) {
// The columns represent the following variabls respectively:
//
// * n
// * sin(thetaJ)
// * k_xJ, which is defined as 1 / (1 - xJ / a)
//
// For definition of the variables, see ComputeOctant.
constexpr Scalar kPrecomputedVariables[][2] = {
/*ratio=2.00*/ {2.00000000, 0.117205737},
/*ratio=2.02*/ {2.03999083, 0.117205737},
/*ratio=2.04*/ {2.07976152, 0.119418745},
/*ratio=2.06*/ {2.11195967, 0.136274515},
/*ratio=2.08*/ {2.14721808, 0.141289310},
/*ratio=2.10*/ {2.18349805, 0.143410679},
/*ratio=2.12*/ {2.21858213, 0.146668334},
/*ratio=2.14*/ {2.24861661, 0.154985392},
/*ratio=2.16*/ {2.28146030, 0.158932848},
/*ratio=2.18*/ {2.30842385, 0.168182439},
/*ratio=2.20*/ {2.33888662, 0.172911853},
/*ratio=2.22*/ {2.36937163, 0.177039959},
/*ratio=2.24*/ {2.40317673, 0.177839181},
/*ratio=2.26*/ {2.42840031, 0.185615110},
/*ratio=2.28*/ {2.45838300, 0.188905374},
/*ratio=2.30*/ {2.48660575, 0.193273145}};
constexpr Scalar kRatioStepInverse = 50; // = 1 / 0.02
/*ratio=2.00*/ {2.00000000, 1.13276676},
/*ratio=2.10*/ {2.18349805, 1.20311921},
/*ratio=2.20*/ {2.33888662, 1.28698796},
/*ratio=2.30*/ {2.48660575, 1.36351941},
/*ratio=2.40*/ {2.62226596, 1.44717976},
/*ratio=2.50*/ {2.75148990, 1.53385819},
/*ratio=3.00*/ {3.36298265, 1.98288283},
/*ratio=3.50*/ {4.08649929, 2.23811846},
/*ratio=4.00*/ {4.85481134, 2.47563463},
/*ratio=4.50*/ {5.62945551, 2.72948597},
/*ratio=5.00*/ {6.43023796, 2.98020421}};
constexpr Scalar kMinRatio = 2.00;
// The curve is split into 3 parts:
// * The first part uses a denser look up table.
// * The second part uses a sparser look up table.
// * The third part uses a straight line.
constexpr Scalar kFirstStepInverse = 10; // = 1 / 0.10
constexpr Scalar kFirstMaxRatio = 2.50;
constexpr Scalar kFirstNumRecords = 6;
constexpr Scalar kSecondStepInverse = 2; // = 1 / 0.50
constexpr Scalar kSecondMaxRatio = 5.00;
constexpr Scalar kThirdNSlope = 1.559599389;
constexpr Scalar kThirdKxjSlope = 0.522807185;
constexpr size_t kNumRecords =
sizeof(kPrecomputedVariables) / sizeof(kPrecomputedVariables[0]);
constexpr Scalar kMinRatio = 2.00f;
constexpr Scalar kMaxRatio = kMinRatio + (kNumRecords - 1) / kRatioStepInverse;
// Linear interpolation for `kPrecomputedVariables`.
//
// The `column` is a 0-based index that decides the target variable.
Scalar LerpPrecomputedVariable(size_t column, Scalar ratio) {
Scalar steps = std::clamp<Scalar>((ratio - kMinRatio) * kRatioStepInverse, 0,
kNumRecords - 1);
// Compute the `n` and `xJ / a` for the given ratio.
std::array<Scalar, 2> ComputeNAndXj(Scalar ratio) {
if (ratio > kSecondMaxRatio) {
Scalar n = kThirdNSlope * (ratio - kSecondMaxRatio) +
kPrecomputedVariables[kNumRecords - 1][0];
Scalar k_xJ = kThirdKxjSlope * (ratio - kSecondMaxRatio) +
kPrecomputedVariables[kNumRecords - 1][1];
return {n, 1 - 1 / k_xJ};
}
ratio = std::clamp(ratio, kMinRatio, kSecondMaxRatio);
Scalar steps;
if (ratio < kFirstMaxRatio) {
steps = (ratio - kMinRatio) * kFirstStepInverse;
} else {
steps =
(ratio - kFirstMaxRatio) * kSecondStepInverse + kFirstNumRecords - 1;
}
size_t left = std::clamp<size_t>(static_cast<size_t>(std::floor(steps)), 0,
kNumRecords - 2);
Scalar frac = steps - left;
return (1 - frac) * kPrecomputedVariables[left][column] +
frac * kPrecomputedVariables[left + 1][column];
Scalar n = (1 - frac) * kPrecomputedVariables[left][0] +
frac * kPrecomputedVariables[left + 1][0];
Scalar k_xJ = (1 - frac) * kPrecomputedVariables[left][1] +
frac * kPrecomputedVariables[left + 1][1];
return {n, 1 - 1 / k_xJ};
}
// Find the center of the circle that passes the given two points and have the
@ -101,53 +124,51 @@ Point FindCircleCenter(Point a, Point b, Scalar r) {
// Compute parameters for a square-like rounded superellipse with a symmetrical
// radius.
RoundSuperellipseParam::Octant ComputeOctant(Point center,
Scalar half_size,
Scalar a,
Scalar radius) {
/* The following figure shows the first quadrant of a square-like rounded
* superellipse. The target arc consists of the "stretch" (AB), a
* superellipsoid arc (BJ), and a circular arc (JM).
* superellipse.
*
* straight superelipse
*
* A B J circular arc
* ---------...._
* | | / ` M
* | | /
* | | / \
* | | / |
* | | D |
* | | / |
* +----+ S |
* s | | |
* +----+---------------| A'
* superelipse
* A circular arc
* ---------...._J
* | / ` M (where x=y)
* | /
* | / \
* | / |
* | D |
* | |
* | |
* | |
* +--------------------| A'
* O
* s
* ---- half_size -----
* -------- a ---------
*/
Scalar ratio =
radius == 0 ? kMaxRatio : std::min(half_size * 2 / radius, kMaxRatio);
Scalar a = ratio * radius / 2;
Scalar s = half_size - a;
if (radius <= 0) {
return RoundSuperellipseParam::Octant{
.offset = center,
.se_a = a,
.se_n = 0,
};
}
Scalar ratio = a * 2 / radius;
Scalar g = RoundSuperellipseParam::kGapFactor * radius;
Scalar n = LerpPrecomputedVariable(0, ratio);
Scalar sin_thetaJ = radius == 0 ? 0 : LerpPrecomputedVariable(1, ratio);
auto precomputed_vars = ComputeNAndXj(ratio);
Scalar n = precomputed_vars[0];
Scalar xJ = precomputed_vars[1] * a;
Scalar yJ = pow(1 - pow(precomputed_vars[1], n), 1 / n) * a;
Scalar max_theta = asinf(pow(precomputed_vars[1], n / 2));
Scalar sin_thetaJ_sq = sin_thetaJ * sin_thetaJ;
Scalar cos_thetaJ_sq = 1 - sin_thetaJ_sq;
Scalar tan_thetaJ_sq = sin_thetaJ_sq / cos_thetaJ_sq;
Scalar xJ = a * pow(sin_thetaJ_sq, 1 / n);
Scalar yJ = a * pow(cos_thetaJ_sq, 1 / n);
Scalar tan_phiJ = pow(tan_thetaJ_sq, (n - 1) / n);
Scalar tan_phiJ = pow(xJ / yJ, n - 1);
Scalar d = (xJ - tan_phiJ * yJ) / (1 - tan_phiJ);
Scalar R = (a - d - g) * sqrt(2);
Point pointA{0, half_size};
Point pointM{half_size - g, half_size - g};
Point pointS{s, s};
Point pointJ = Point{xJ, yJ} + pointS;
Point pointM{a - g, a - g};
Point pointJ = Point{xJ, yJ};
Point circle_center =
radius == 0 ? pointM : FindCircleCenter(pointJ, pointM, R);
Radians circle_max_angle =
@ -157,12 +178,9 @@ RoundSuperellipseParam::Octant ComputeOctant(Point center,
return RoundSuperellipseParam::Octant{
.offset = center,
.edge_mid = pointA,
.se_center = pointS,
.se_a = a,
.se_n = n,
.se_max_theta = asin(sin_thetaJ),
.se_max_theta = max_theta,
.circle_start = pointJ,
.circle_center = circle_center,
@ -225,13 +243,9 @@ bool OctantContains(const RoundSuperellipseParam::Octant& param,
if (p.x < 0 || p.y < 0 || p.y < p.x) {
return true;
}
// Check if the point is within the stretch segment.
if (p.x <= param.se_center.x) {
return p.y <= param.edge_mid.y;
}
// Check if the point is within the superellipsoid segment.
if (p.x <= param.circle_start.x) {
Point p_se = (p - param.se_center) / param.se_a;
Point p_se = p / param.se_a;
return powf(p_se.x, param.se_n) + powf(p_se.y, param.se_n) <= 1;
}
Scalar circle_radius =
@ -265,6 +279,15 @@ bool CornerContains(const RoundSuperellipseParam::Quadrant& param,
} else {
norm_point = norm_point.Abs();
}
if (param.top.se_n < 2 || param.right.se_n < 2) {
// A rectangular corner. The top and left sides contain the borders
// while the bottom and right sides don't (see `Rect.contains`).
Scalar x_delta = param.right.offset.x + param.right.se_a - norm_point.x;
Scalar y_delta = param.top.offset.y + param.top.se_a - norm_point.y;
bool x_within = x_delta > 0 || (x_delta == 0 && param.signed_scale.x < 0);
bool y_within = y_delta > 0 || (y_delta == 0 && param.signed_scale.y < 0);
return x_within && y_within;
}
return OctantContains(param.top, norm_point - param.top.offset) &&
OctantContains(param.right, Flip(norm_point - param.right.offset));
}
@ -272,37 +295,37 @@ bool CornerContains(const RoundSuperellipseParam::Quadrant& param,
} // namespace
RoundSuperellipseParam RoundSuperellipseParam::MakeBoundsRadii(
const Rect& bounds_,
const RoundingRadii& radii_) {
if (radii_.AreAllCornersSame()) {
const Rect& bounds,
const RoundingRadii& radii) {
if (radii.AreAllCornersSame() && !radii.top_left.IsEmpty()) {
// Having four empty corners indicate a rectangle, which needs special
// treatment on border containment and therefore is not `all_corners_same`.
return RoundSuperellipseParam{
.top_right = ComputeQuadrant(bounds_.GetCenter(), bounds_.GetRightTop(),
radii_.top_right),
.top_right = ComputeQuadrant(bounds.GetCenter(), bounds.GetRightTop(),
radii.top_right),
.all_corners_same = true,
};
}
Scalar top_split = Split(bounds_.GetLeft(), bounds_.GetRight(),
radii_.top_left.width, radii_.top_right.width);
Scalar right_split =
Split(bounds_.GetTop(), bounds_.GetBottom(), radii_.top_right.height,
radii_.bottom_right.height);
Scalar top_split = Split(bounds.GetLeft(), bounds.GetRight(),
radii.top_left.width, radii.top_right.width);
Scalar right_split = Split(bounds.GetTop(), bounds.GetBottom(),
radii.top_right.height, radii.bottom_right.height);
Scalar bottom_split =
Split(bounds_.GetLeft(), bounds_.GetRight(), radii_.bottom_left.width,
radii_.bottom_right.width);
Scalar left_split = Split(bounds_.GetTop(), bounds_.GetBottom(),
radii_.top_left.height, radii_.bottom_left.height);
Split(bounds.GetLeft(), bounds.GetRight(), radii.bottom_left.width,
radii.bottom_right.width);
Scalar left_split = Split(bounds.GetTop(), bounds.GetBottom(),
radii.top_left.height, radii.bottom_left.height);
return RoundSuperellipseParam{
.top_right = ComputeQuadrant(Point{top_split, right_split},
bounds_.GetRightTop(), radii_.top_right),
bounds.GetRightTop(), radii.top_right),
.bottom_right =
ComputeQuadrant(Point{bottom_split, right_split},
bounds_.GetRightBottom(), radii_.bottom_right),
.bottom_left =
ComputeQuadrant(Point{bottom_split, left_split},
bounds_.GetLeftBottom(), radii_.bottom_left),
bounds.GetRightBottom(), radii.bottom_right),
.bottom_left = ComputeQuadrant(Point{bottom_split, left_split},
bounds.GetLeftBottom(), radii.bottom_left),
.top_left = ComputeQuadrant(Point{top_split, left_split},
bounds_.GetLeftTop(), radii_.top_left),
bounds.GetLeftTop(), radii.top_left),
.all_corners_same = false,
};
}

View File

@ -19,6 +19,9 @@ struct RoundSuperellipseParam {
//
// This structure is used to define an octant of an arbitrary rounded
// superellipse.
//
// A `se_n` of 0 means that the radius is 0, and this octant is a square
// of size `se_a` at `offset` and all other fields are ignored.
struct Octant {
// The offset of the square-like rounded superellipse's center from the
// origin.
@ -26,18 +29,11 @@ struct RoundSuperellipseParam {
// All other coordinates in this structure are relative to this point.
Point offset;
// The coordinate of the midpoint of the top edge, relative to the `offset`
// point.
//
// This is the starting point of the octant curve.
Point edge_mid;
// The coordinate of the superellipse's center, relative to the `offset`
// point.
Point se_center;
// The semi-axis length of the superellipse.
Scalar se_a;
// The degree of the superellipse.
//
// If this value is 0, then this octant is a square of size `se_a`.
Scalar se_n;
// The range of the parameter "theta" used to define the superellipse curve.
//

View File

@ -592,13 +592,13 @@ TEST(RoundSuperellipseTest, UniformSquareContains) {
CHECK_POINT_WITH_OFFSET(rr, (p) * Point(-1, 1), Point(-0.02, 0.02)); \
CHECK_POINT_WITH_OFFSET(rr, (p) * Point(-1, -1), Point(-0.02, -0.02));
CHECK_POINT_AND_MIRRORS(Point(0, 49.995)); // Top
CHECK_POINT_AND_MIRRORS(Point(44.245, 49.995)); // Top stretch end
CHECK_POINT_AND_MIRRORS(Point(45.72, 49.92)); // Top joint
CHECK_POINT_AND_MIRRORS(Point(48.53, 48.53)); // Circular arc mid
CHECK_POINT_AND_MIRRORS(Point(49.92, 45.72)); // Right joint
CHECK_POINT_AND_MIRRORS(Point(49.995, 44.245)); // Right stretch end
CHECK_POINT_AND_MIRRORS(Point(49.995, 0)); // Right
CHECK_POINT_AND_MIRRORS(Point(0, 49.995)); // Top
CHECK_POINT_AND_MIRRORS(Point(44.245, 49.95)); // Top curve start
CHECK_POINT_AND_MIRRORS(Point(45.72, 49.87)); // Top joint
CHECK_POINT_AND_MIRRORS(Point(48.53, 48.53)); // Circular arc mid
CHECK_POINT_AND_MIRRORS(Point(49.87, 45.72)); // Right joint
CHECK_POINT_AND_MIRRORS(Point(49.95, 44.245)); // Right curve start
CHECK_POINT_AND_MIRRORS(Point(49.995, 0)); // Right
#undef CHECK_POINT_AND_MIRRORS
}
@ -614,11 +614,11 @@ TEST(RoundSuperellipseTest, UniformEllipticalContains) {
CHECK_POINT_WITH_OFFSET(rr, (p) * Point(-1, -1), Point(-0.02, -0.02));
CHECK_POINT_AND_MIRRORS(Point(0, 49.995)); // Top
CHECK_POINT_AND_MIRRORS(Point(44.245, 49.995)); // Top stretch end
CHECK_POINT_AND_MIRRORS(Point(45.72, 49.84)); // Top joint
CHECK_POINT_AND_MIRRORS(Point(44.245, 49.911)); // Top curve start
CHECK_POINT_AND_MIRRORS(Point(45.72, 49.75)); // Top joint
CHECK_POINT_AND_MIRRORS(Point(48.51, 47.07)); // Circular arc mid
CHECK_POINT_AND_MIRRORS(Point(49.92, 41.44)); // Right joint
CHECK_POINT_AND_MIRRORS(Point(49.995, 38.49)); // Right stretch end
CHECK_POINT_AND_MIRRORS(Point(49.87, 41.44)); // Right joint
CHECK_POINT_AND_MIRRORS(Point(49.95, 38.49)); // Right curve start
CHECK_POINT_AND_MIRRORS(Point(49.995, 0)); // Right
#undef CHECK_POINT_AND_MIRRORS
}
@ -645,9 +645,9 @@ TEST(RoundSuperellipseTest, UniformRectangularContains) {
CHECK_POINT_AND_MIRRORS(Point(34.99, 98.06));
CHECK_POINT_AND_MIRRORS(Point(39.99, 94.73));
CHECK_POINT_AND_MIRRORS(Point(44.13, 89.99));
CHECK_POINT_AND_MIRRORS(Point(48.60, 79.99));
CHECK_POINT_AND_MIRRORS(Point(49.93, 69.99));
CHECK_POINT_AND_MIRRORS(Point(49.99, 59.99));
CHECK_POINT_AND_MIRRORS(Point(48.46, 79.99));
CHECK_POINT_AND_MIRRORS(Point(49.70, 69.99));
CHECK_POINT_AND_MIRRORS(Point(49.97, 59.99));
CHECK_POINT_AND_MIRRORS(Point(49.99, 49.99)); // Right mid edge
#undef CHECK_POINT_AND_MIRRORS

View File

@ -570,7 +570,7 @@ class CupertinoPopupSurface extends StatelessWidget {
static const double defaultBlurSigma = 30.0;
/// The default corner radius of a [CupertinoPopupSurface].
static const BorderRadius _clipper = BorderRadius.all(Radius.circular(12));
static const BorderRadius _clipper = BorderRadius.all(Radius.circular(13));
// The [ColorFilter] matrix used to saturate widgets underlying a
// [CupertinoPopupSurface] when the ambient [CupertinoThemeData.brightness] is