[Impeller] Add RoundSuperellipse class, containment check and stroking (#162826)

This PR:
* Adds a `RoundSuperellipse` class, which mirrors the current
`RoundRect` class.
* Implements `RoundSuperellipse::Contains`, which checks if a point is
contained by the RSE.
* Adds `Path::AddSuperellipse`, which draws an RSE with Bezier
approximation.
* Adds a `RoundSuperellipseParam` class, which is the common computation
shared by geometry drawing, stroking, and containment check.


https://github.com/user-attachments/assets/883c7762-7b35-432e-9b31-d204db3bd6e1


This PR also updates the RSE algorithm according to my recent research,
which uses one fewer precomputed variable (no more `d`), shares the same
gap factor with RRect, and allows much better precision. The result
shape is almost unchanged (~0.2% slimmer).

> For reviewers: The `RoundSuperellipseParam` and
`RoundSuperellipse::Contains` parts are repurposed from the abandoned
https://github.com/flutter/flutter/pull/162349. This PR is a preparation
for https://github.com/flutter/flutter/pull/160883.

## 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-02-12 09:45:56 -08:00 committed by GitHub
parent 09fdf159b9
commit ef927e85d5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 2082 additions and 557 deletions

View File

@ -175,6 +175,8 @@
../../../flutter/impeller/geometry/path_unittests.cc
../../../flutter/impeller/geometry/rect_unittests.cc
../../../flutter/impeller/geometry/round_rect_unittests.cc
../../../flutter/impeller/geometry/round_superellipse_unittests.cc
../../../flutter/impeller/geometry/rounding_radii_unittests.cc
../../../flutter/impeller/geometry/rstransform_unittests.cc
../../../flutter/impeller/geometry/saturated_math_unittests.cc
../../../flutter/impeller/geometry/size_unittests.cc

View File

@ -41930,6 +41930,10 @@ ORIGIN: ../../../flutter/impeller/geometry/rect.cc + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/impeller/geometry/rect.h + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/impeller/geometry/round_rect.cc + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/impeller/geometry/round_rect.h + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/impeller/geometry/round_superellipse.cc + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/impeller/geometry/round_superellipse.h + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/impeller/geometry/round_superellipse_param.cc + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/impeller/geometry/round_superellipse_param.h + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/impeller/geometry/rounding_radii.cc + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/impeller/geometry/rounding_radii.h + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/impeller/geometry/rstransform.cc + ../../../flutter/LICENSE
@ -44897,6 +44901,10 @@ FILE: ../../../flutter/impeller/geometry/rect.cc
FILE: ../../../flutter/impeller/geometry/rect.h
FILE: ../../../flutter/impeller/geometry/round_rect.cc
FILE: ../../../flutter/impeller/geometry/round_rect.h
FILE: ../../../flutter/impeller/geometry/round_superellipse.cc
FILE: ../../../flutter/impeller/geometry/round_superellipse.h
FILE: ../../../flutter/impeller/geometry/round_superellipse_param.cc
FILE: ../../../flutter/impeller/geometry/round_superellipse_param.h
FILE: ../../../flutter/impeller/geometry/rounding_radii.cc
FILE: ../../../flutter/impeller/geometry/rounding_radii.h
FILE: ../../../flutter/impeller/geometry/rstransform.cc

View File

@ -69,6 +69,10 @@ namespace testing {
using EntityTest = EntityPlayground;
INSTANTIATE_PLAYGROUND_SUITE(EntityTest);
Rect RectMakeCenterSize(Point center, Size size) {
return Rect::MakeSize(size).Shift(center - size / 2);
}
TEST_P(EntityTest, CanCreateEntity) {
Entity entity;
ASSERT_TRUE(entity.GetTransform().IsIdentity());
@ -2325,12 +2329,15 @@ TEST_P(EntityTest, DrawSuperEllipse) {
TEST_P(EntityTest, DrawRoundSuperEllipse) {
auto callback = [&](ContentContext& context, RenderPass& pass) -> bool {
// UI state.
static int style_index = 0;
static float center[2] = {830, 830};
static float size[2] = {600, 600};
static bool horizontal_symmetry = true;
static bool vertical_symmetry = true;
static bool corner_symmetry = true;
const char* style_options[] = {"Fill", "Stroke"};
// Initially radius_tl[0] will be mirrored to all 8 values since all 3
// symmetries are enabled.
static std::array<float, 2> radius_tl = {200};
@ -2366,6 +2373,8 @@ TEST_P(EntityTest, DrawRoundSuperEllipse) {
ImGui::Begin("Controls", nullptr, ImGuiWindowFlags_AlwaysAutoResize);
{
ImGui::Combo("Style", &style_index, style_options,
sizeof(style_options) / sizeof(char*));
ImGui::SliderFloat2("Center", center, 0, 1000);
ImGui::SliderFloat2("Size", size, 0, 1000);
ImGui::Checkbox("Symmetry: Horizontal", &horizontal_symmetry);
@ -2402,11 +2411,25 @@ TEST_P(EntityTest, DrawRoundSuperEllipse) {
.bottom_right = {radius_br[0], radius_br[1]},
};
auto rse = RoundSuperellipse::MakeRectRadii(
RectMakeCenterSize({center[0], center[1]}, {size[0], size[1]}), radii);
Path path;
std::unique_ptr<Geometry> geom;
if (style_index == 0) {
geom = std::make_unique<RoundSuperellipseGeometry>(
RectMakeCenterSize({center[0], center[1]}, {size[0], size[1]}),
radii);
} else {
path = PathBuilder{}
.SetConvexity(Convexity::kConvex)
.AddRoundSuperellipse(rse)
.SetBounds(rse.GetBounds())
.TakePath();
geom = Geometry::MakeStrokePath(path, /*stroke_width=*/2);
}
auto contents = std::make_shared<SolidColorContents>();
std::unique_ptr<RoundSuperellipseGeometry> geom =
std::make_unique<RoundSuperellipseGeometry>(
Rect::MakeOriginSize({center[0], center[1]}, {size[0], size[1]}),
radii);
contents->SetColor(Color::Red());
contents->SetGeometry(geom.get());

View File

@ -6,6 +6,7 @@
#include <variant>
#include "flutter/impeller/entity/geometry/round_superellipse_geometry.h"
#include "flutter/impeller/geometry/round_superellipse_param.h"
#include "impeller/geometry/constants.h"
@ -13,6 +14,8 @@ namespace impeller {
namespace {
constexpr auto kGapFactor = RoundSuperellipseParam::kGapFactor;
// An interface for classes that arranges a point list that forms a convex
// contour into a triangle strip.
class ConvexRearranger {
@ -163,59 +166,6 @@ constexpr Matrix kFlip = Matrix(
0.0f, 0.0f, 0.0f, 1.0f);
// clang-format on
// A look up table with precomputed variables.
//
// The columns represent the following variabls respectively:
//
// * ratio = size / a
// * n
// * d / a
// * thetaJ
//
// For definition of the variables, see DrawOctantSquareLikeSquircle.
constexpr Scalar kPrecomputedVariables[][4] = {
{2.000, 2.00000, 0.00000, 0.24040}, //
{2.020, 2.03340, 0.01447, 0.24040}, //
{2.040, 2.06540, 0.02575, 0.21167}, //
{2.060, 2.09800, 0.03668, 0.20118}, //
{2.080, 2.13160, 0.04719, 0.19367}, //
{2.100, 2.17840, 0.05603, 0.16233}, //
{2.120, 2.19310, 0.06816, 0.20020}, //
{2.140, 2.22990, 0.07746, 0.19131}, //
{2.160, 2.26360, 0.08693, 0.19008}, //
{2.180, 2.30540, 0.09536, 0.17935}, //
{2.200, 2.32900, 0.10541, 0.19136}, //
{2.220, 2.38330, 0.11237, 0.17130}, //
{2.240, 2.39770, 0.12271, 0.18956}, //
{2.260, 2.41770, 0.13251, 0.20254}, //
{2.280, 2.47180, 0.13879, 0.18454}, //
{2.300, 2.50910, 0.14658, 0.18261} //
};
constexpr size_t kNumRecords =
sizeof(kPrecomputedVariables) / sizeof(kPrecomputedVariables[0]);
constexpr Scalar kMinRatio = kPrecomputedVariables[0][0];
constexpr Scalar kMaxRatio = kPrecomputedVariables[kNumRecords - 1][0];
constexpr Scalar kRatioStep =
kPrecomputedVariables[1][0] - kPrecomputedVariables[0][0];
// Linear interpolation for `kPrecomputedVariables`.
//
// The `column` is a 0-based index that decides the target variable, where 1
// corresponds to the 2nd element of each row, etc.
//
// The `ratio` corresponds to column 0, on which the lerp is calculated.
Scalar LerpPrecomputedVariable(size_t column, Scalar ratio) {
Scalar steps =
std::clamp<Scalar>((ratio - kMinRatio) / kRatioStep, 0, kNumRecords - 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];
}
// The max angular step that the algorithm will traverse a quadrant of the
// curve.
//
@ -245,82 +195,6 @@ Scalar CalculateStep(Scalar minDimension, Scalar fullAngle) {
return std::min(kMinAngleStep, angleByDimension);
}
// A factor used to calculate the "gap", defined as the distance from the
// midpoint of the curved corners to the nearest sides of the bounding box.
//
// When the corner radius is symmetrical on both dimensions, the midpoint of the
// corner is where the circular arc intersects its quadrant bisector. When the
// corner radius is asymmetrical, since the corner can be considered "elongated"
// from a symmetrical corner, the midpoint is transformed in the same way.
//
// Experiments indicate that the gap is linear with respect to the corner
// radius on that dimension.
//
// The formula should be kept in sync with a few files, as documented in
// `CalculateGap` in round_superellipse_geometry.cc.
constexpr Scalar kGapFactor = 0.2924066406;
// Return the value that splits the range from `left` to `right` into two
// portions whose ratio equals to `ratio_left` : `ratio_right`.
static Scalar Split(Scalar left,
Scalar right,
Scalar ratio_left,
Scalar ratio_right) {
return (left * ratio_right + right * ratio_left) / (ratio_left + ratio_right);
}
// Draw a circular arc from `start` to `end` with a radius of `r`.
//
// It is assumed that `start` is north-west to `end`, and the center of the
// circle is south-west to both points. If `reverse` is true, then the curve
// goes from `end` to `start` instead.
//
// The resulting points, after applying `transform`, are appended to `output`
// and include the effective starting point but exclude the effective ending
// point.
//
// Returns the number of generated points.
size_t DrawCircularArc(Point* output,
Point start,
Point end,
Scalar r,
bool reverse,
const Matrix& transform) {
/* Denote the middle point of S and E as M. The key is to find the center of
* the circle.
* S --__
* / `
* / M \
* / E
* /
* /
* / r
* C
*/
Point s_to_e = end - start;
Point m = (start + end) / 2;
Point c_to_m = Point(-s_to_e.y, s_to_e.x);
Scalar distance_sm = s_to_e.GetLength() / 2;
Scalar distance_cm = sqrt(r * r - distance_sm * distance_sm);
Point c = m - distance_cm * c_to_m.Normalize();
Scalar angle_sce = asinf(distance_sm / r) * 2;
Point c_to_s = start - c;
Matrix full_transform = transform * Matrix::MakeTranslation(c);
Point* next = output;
Scalar angle = reverse ? angle_sce : 0.0f;
Scalar step =
(reverse ? -1 : 1) * CalculateStep(std::abs(s_to_e.y), angle_sce);
Scalar end_angle = reverse ? 0.0f : angle_sce;
while ((angle < end_angle) != reverse) {
*(next++) = full_transform * c_to_s.Rotate(Radians(-angle));
angle += step;
}
return next - output;
}
// Draw a superellipsoid arc.
//
// The superellipse is centered at the origin and has degree `n` and both
@ -328,10 +202,10 @@ size_t DrawCircularArc(Point* output,
// to `max_theta` radiance clockwise if `reverse` is false, or from `max_theta`
// to 0 otherwise.
//
// The resulting points, after applying `transform`, are appended to `output`
// and include the starting point but exclude the ending point.
// The resulting points, transformed by `transform`, are appended to `output`.
// The starting point is included, but the ending point is excluded.
//
// Returns the number of generated points.
// Returns the number of points generated.
size_t DrawSuperellipsoidArc(Point* output,
Scalar a,
Scalar n,
@ -353,25 +227,71 @@ size_t DrawSuperellipsoidArc(Point* output,
return next - output;
}
// Draws a circular arc centered at the origin with a radius of `r`, starting at
// `start`, and spanning `max_angle` clockwise.
//
// If `reverse` is false, points are generated from `start` to `start +
// max_angle`. If `reverse` is true, points are generated from `start +
// max_angle` back to `start`.
//
// The generated points, transformed by `transform`, are appended to `output`.
// The starting point is included, but the ending point is excluded.
//
// Returns the number of points generated.
size_t DrawCircularArc(Point* output,
Point start,
Scalar max_angle,
bool reverse,
const Matrix& transform) {
/* Denote the middle point of S and E as M. The key is to find the center of
* the circle.
* S --__
* / `
* / M \
* / E
* /
* /
* / r
* C
*/
Point end = start.Rotate(Radians(-max_angle));
Point* next = output;
Scalar angle = reverse ? max_angle : 0.0f;
Scalar step =
(reverse ? -1 : 1) * CalculateStep(std::abs(start.y - end.y), max_angle);
Scalar end_angle = reverse ? 0.0f : max_angle;
while ((angle < end_angle) != reverse) {
*(next++) = transform * start.Rotate(Radians(-angle));
angle += step;
}
return next - output;
}
// Draws an arc representing the top 1/8 segment of a square-like rounded
// superellipse centered at the origin.
//
// The square-like rounded superellipse that this arc belongs to has a width and
// height specified by `size` and features rounded corners determined by
// `corner_radius`. The `corner_radius` corresponds to the `cornerRadius`
// parameter in SwiftUI, rather than the literal radius of corner circles.
// If `reverse_and_flip` is false, the resulting arc spans from 0 (inclusive) to
// pi/4 (exclusive), moving clockwise starting from the positive Y-axis. If
// `reverse` is true, the curve spans from pi/4 (inclusive) to 0 (inclusive)
// counterclockwise instead, and all points have their x and y coordinates
// flipped.
//
// If `reverse` is false, the resulting arc spans from 0 (inclusive) to pi/4
// (exclusive), moving clockwise starting from the positive Y-axis. If `reverse`
// is true, the curve spans from pi/4 (inclusive) to 0 (inclusive)
// counterclockwise instead.
// Either way, each point is then transformed by `external_transform` and
// appended to `output`.
//
// Returns the number of points generated.
size_t DrawOctantSquareLikeSquircle(Point* output,
Scalar size,
Scalar corner_radius,
bool reverse,
const Matrix& transform) {
const RoundSuperellipseParam::Octant& param,
bool reverse_and_flip,
const Matrix& external_transform) {
Matrix transform = external_transform * Matrix::MakeTranslation(param.offset);
if (reverse_and_flip) {
transform = transform * kFlip;
}
/* 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).
@ -380,7 +300,7 @@ size_t DrawOctantSquareLikeSquircle(Point* output,
*
* A B J circular arc
* ---------...._
* | | / ` M
* | | / ` M (where y=x)
* | | /
* | | / \
* | | / |
@ -392,56 +312,35 @@ size_t DrawOctantSquareLikeSquircle(Point* output,
* O
* s
* ------ size/2 ------
*
* Define gap (g) as the distance between point M and the bounding box,
* therefore point M is at (size/2 - g, size/2 - g).
*
* The superellipsoid curve can be drawn with an implicit parameter θ:
* x = a * sinθ ^ (2/n)
* y = a * cosθ ^ (2/n)
* https://math.stackexchange.com/questions/2573746/superellipse-parametric-equation
*
* Define thetaJ as the θ at point J.
*/
Scalar ratio = {std::min(size / corner_radius, kMaxRatio)};
Scalar a = ratio * corner_radius / 2;
Scalar s = size / 2 - a;
Scalar g = kGapFactor * corner_radius;
Scalar n = LerpPrecomputedVariable(1, ratio);
Scalar d = LerpPrecomputedVariable(2, ratio) * a;
Scalar thetaJ = LerpPrecomputedVariable(3, ratio);
Scalar R = (a - d - g) * sqrt(2);
Point pointA{0, size / 2};
Point pointM{size / 2 - g, size / 2 - g};
Point pointS{s, s};
Point pointJ =
Point{pow(abs(sinf(thetaJ)), 2 / n), pow(abs(cosf(thetaJ)), 2 / n)} * a +
pointS;
Matrix translationS = Matrix::MakeTranslation(pointS);
Point* next = output;
if (!reverse) {
if (!reverse_and_flip) {
// Point A
*(next++) = transform * pointA;
*(next++) = transform * param.edge_mid;
// Arc [B, J)
next += DrawSuperellipsoidArc(next, a, n, thetaJ, reverse,
transform * translationS);
next += DrawSuperellipsoidArc(
next, param.se_a, param.se_n, param.se_max_theta, reverse_and_flip,
transform * Matrix::MakeTranslation(param.se_center));
// Arc [J, M)
next += DrawCircularArc(next, pointJ, pointM, R, reverse, transform);
next += DrawCircularArc(
next, param.circle_start - param.circle_center,
param.circle_max_angle.radians, reverse_and_flip,
transform * Matrix::MakeTranslation(param.circle_center));
} else {
// Arc [M, J)
next += DrawCircularArc(next, pointJ, pointM, R, reverse, transform);
next += DrawCircularArc(
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, a, n, thetaJ, reverse,
transform * translationS);
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 * Point{s, size / 2};
*(next++) = transform * (param.se_center + Point{0, param.se_a});
// Point A
*(next++) = transform * pointA;
*(next++) = transform * param.edge_mid;
}
return next - output;
}
@ -449,47 +348,16 @@ size_t DrawOctantSquareLikeSquircle(Point* output,
// Draw a quadrant curve, both ends included.
//
// Returns the number of points.
//
// The eact quadrant is specified by the direction of `outer` relative to
// `center`. The curve goes from the X axis to the Y axis.
static size_t DrawQuadrant(Point* output,
Point center,
Point outer,
Size radii) {
if (radii.width == 0 || radii.height == 0) {
// Degrade to rectangle. (A zero radius causes error below.)
output[0] = {center.x, outer.y};
output[1] = outer;
output[2] = {outer.x, center.y};
return 3;
}
// Normalize sizes and radii into symmetrical radius by scaling the longer of
// `radii` to the shorter. For example, to draw a RSE with size (200, 300)
// and radii (20, 10), this function draws one with size (100, 300) and radii
// (10, 10) and then scales it by (2x, 1x).
Scalar norm_radius = radii.MinDimension();
Size radius_scale = radii / norm_radius;
Point signed_size = (outer - center) * 2;
Point norm_size = signed_size.Abs() / radius_scale;
Point signed_scale = signed_size / norm_size;
// Each quadrant curve is composed of two octant curves, each of which belongs
// to a square-like rounded rectangle. When `norm_size`'s width != height, the
// centers of such square-like rounded rectangles are offset from the origin
// by a distance denoted as `c`.
Scalar c = (norm_size.x - norm_size.y) / 2;
const RoundSuperellipseParam::Quadrant& param) {
Point* next = output;
auto transform = Matrix::MakeTranslateScale(param.signed_scale, param.offset);
next += DrawOctantSquareLikeSquircle(
next, norm_size.x, norm_radius, /*reverse=*/false,
Matrix::MakeTranslateScale(signed_scale, center) *
Matrix::MakeTranslation(Size{0, -c}));
next += DrawOctantSquareLikeSquircle(next, param.top,
/*reverse_and_flip=*/false, transform);
next += DrawOctantSquareLikeSquircle(
next, norm_size.y, norm_radius, /*reverse=*/true,
Matrix::MakeTranslateScale(signed_scale, center) *
Matrix::MakeTranslation(Size{c, 0}) * kFlip);
next += DrawOctantSquareLikeSquircle(next, param.right,
/*reverse_and_flip=*/true, transform);
return next - output;
}
@ -525,43 +393,26 @@ GeometryResult RoundSuperellipseGeometry::GetPositionBuffer(
UnevenQuadrantsRearranger>
rearranger_holder;
if (radii_.AreAllCornersSame()) {
auto param = RoundSuperellipseParam::MakeBoundsRadii(bounds_, radii_);
if (param.all_corners_same) {
rearranger_holder.emplace<MirroredQuadrantRearranger>(bounds_.GetCenter(),
cache);
auto& t = std::get<MirroredQuadrantRearranger>(rearranger_holder);
rearranger = &t;
// The quadrant must be drawn at the origin so that it can be rotated later.
t.QuadSize() = DrawQuadrant(cache, Point(),
bounds_.GetRightTop() - bounds_.GetCenter(),
radii_.top_right);
param.top_right.offset = Point();
t.QuadSize() = DrawQuadrant(cache, param.top_right);
} else {
rearranger_holder.emplace<UnevenQuadrantsRearranger>(cache, kMaxQuadSize);
auto& t = std::get<UnevenQuadrantsRearranger>(rearranger_holder);
rearranger = &t;
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);
t.QuadSize(0) = DrawQuadrant(t.QuadCache(0), Point{top_split, right_split},
bounds_.GetRightTop(), radii_.top_right);
t.QuadSize(1) =
DrawQuadrant(t.QuadCache(1), Point{bottom_split, right_split},
bounds_.GetRightBottom(), radii_.bottom_right);
t.QuadSize(2) =
DrawQuadrant(t.QuadCache(2), Point{bottom_split, left_split},
bounds_.GetLeftBottom(), radii_.bottom_left);
t.QuadSize(3) = DrawQuadrant(t.QuadCache(3), Point{top_split, left_split},
bounds_.GetLeftTop(), radii_.top_left);
t.QuadSize(0) = DrawQuadrant(t.QuadCache(0), param.top_right);
t.QuadSize(1) = DrawQuadrant(t.QuadCache(1), param.bottom_right);
t.QuadSize(2) = DrawQuadrant(t.QuadCache(2), param.bottom_left);
t.QuadSize(3) = DrawQuadrant(t.QuadCache(3), param.top_left);
}
size_t contour_length = rearranger->ContourLength();

View File

@ -31,6 +31,10 @@ impeller_component("geometry") {
"rect.h",
"round_rect.cc",
"round_rect.h",
"round_superellipse.cc",
"round_superellipse.h",
"round_superellipse_param.cc",
"round_superellipse_param.h",
"rounding_radii.cc",
"rounding_radii.h",
"rstransform.cc",
@ -78,6 +82,8 @@ impeller_component("geometry_unittests") {
"path_unittests.cc",
"rect_unittests.cc",
"round_rect_unittests.cc",
"round_superellipse_unittests.cc",
"rounding_radii_unittests.cc",
"rstransform_unittests.cc",
"saturated_math_unittests.cc",
"size_unittests.cc",

View File

@ -33,6 +33,8 @@ Path CreateCubic(bool closed);
Path CreateQuadratic(bool closed);
/// Create a rounded rect.
Path CreateRRect();
/// Create a rounded superellipse.
Path CreateRSuperellipse();
} // namespace
static TessellatorLibtess tess;
@ -141,6 +143,12 @@ MAKE_STROKE_BENCHMARK_CAPTURE(RRect, Butt, Bevel, );
MAKE_STROKE_BENCHMARK_CAPTURE(RRect, Butt, Miter, );
MAKE_STROKE_BENCHMARK_CAPTURE(RRect, Butt, Round, );
// Same as RRect
BENCHMARK_CAPTURE(BM_Convex, rse_convex, CreateRSuperellipse(), true);
MAKE_STROKE_BENCHMARK_CAPTURE(RSuperellipse, Butt, Bevel, );
MAKE_STROKE_BENCHMARK_CAPTURE(RSuperellipse, Butt, Miter, );
MAKE_STROKE_BENCHMARK_CAPTURE(RSuperellipse, Butt, Round, );
namespace {
Path CreateRRect() {
@ -150,6 +158,13 @@ Path CreateRRect() {
.TakePath();
}
Path CreateRSuperellipse() {
return PathBuilder{}
.AddRoundSuperellipse(
RoundSuperellipse::MakeRectXY(Rect::MakeLTRB(0, 0, 400, 400), 16, 16))
.TakePath();
}
Path CreateCubic(bool closed) {
auto builder = PathBuilder{};
builder //

View File

@ -4,12 +4,170 @@
#include "path_builder.h"
#include <array>
#include <cmath>
#include "impeller/geometry/path_component.h"
#include "impeller/geometry/round_superellipse_param.h"
namespace impeller {
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;
// 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)) {}
// Draws an arc representing 1/4 of a rounded superellipse.
//
// If `reverse` is false, the resulting arc spans from 0 to pi/2, moving
// clockwise starting from the positive Y-axis. Otherwise it moves from pi/2
// to 0.
void AddQuadrant(const RoundSuperellipseParam::Quadrant& param,
bool reverse) {
auto transform =
Matrix::MakeTranslateScale(param.signed_scale, param.offset);
if (!reverse) {
AddOctant(param.top, /*reverse=*/false, /*flip=*/false, transform);
AddOctant(param.right, /*reverse=*/true, /*flip=*/true, transform);
} else {
AddOctant(param.right, /*reverse=*/false, /*flip=*/true, transform);
AddOctant(param.top, /*reverse=*/true, /*flip=*/false, transform);
}
}
private:
std::array<Point, 4> SuperellipseArcPoints(
const RoundSuperellipseParam::Octant& param) {
Point start = {param.se_center.x, param.edge_mid.y};
const Point& end = param.circle_start;
constexpr Point start_tangent = {1, 0};
Point circle_start_vector = param.circle_start - param.circle_center;
Point end_tangent =
Point{-circle_start_vector.y, circle_start_vector.x}.Normalize();
std::array<Scalar, 2> factors = SuperellipseBezierFactors(param.se_n);
return std::array<Point, 4>{
start, start + start_tangent * factors[0] * param.se_a,
end + end_tangent * factors[1] * param.se_a, end};
};
std::array<Point, 4> CircularArcPoints(
const RoundSuperellipseParam::Octant& param) {
Point start_vector = param.circle_start - param.circle_center;
Point end_vector =
start_vector.Rotate(Radians(-param.circle_max_angle.radians));
Point circle_end = param.circle_center + end_vector;
Point start_tangent = Point{start_vector.y, -start_vector.x}.Normalize();
Point end_tangent = Point{-end_vector.y, end_vector.x}.Normalize();
Scalar bezier_factor = std::tan(param.circle_max_angle.radians / 4) * 4 / 3;
Scalar radius = start_vector.GetLength();
return std::array<Point, 4>{
param.circle_start,
param.circle_start + start_tangent * bezier_factor * radius,
circle_end + end_tangent * bezier_factor * radius, circle_end};
};
// Draws an arc representing 1/8 of a rounded superellipse.
//
// If `reverse` is false, the resulting arc spans from 0 to pi/4, moving
// clockwise starting from the positive Y-axis. Otherwise it moves from pi/4
// to 0.
//
// If `flip` is true, all points have their X and Y coordinates swapped,
// effectively mirrowing each point by the y=x line.
//
// All points are transformed by `external_transform` after the optional
// flipping before being used as control points for the cubic curves.
void AddOctant(const RoundSuperellipseParam::Octant& param,
bool reverse,
bool flip,
const Matrix& external_transform) {
Matrix transform =
external_transform * Matrix::MakeTranslation(param.offset);
if (flip) {
transform = transform * kFlip;
}
auto circle_points = CircularArcPoints(param);
auto se_points = SuperellipseArcPoints(param);
if (!reverse) {
cubic_adder_(transform * se_points[0], transform * se_points[1],
transform * se_points[2], transform * se_points[3]);
cubic_adder_(transform * circle_points[0], transform * circle_points[1],
transform * circle_points[2], transform * circle_points[3]);
} else {
cubic_adder_(transform * circle_points[3], transform * circle_points[2],
transform * circle_points[1], transform * circle_points[0]);
cubic_adder_(transform * se_points[3], transform * se_points[2],
transform * se_points[1], transform * se_points[0]);
}
};
// Get the Bezier factor for the superellipse arc in a rounded superellipse.
//
// The result will be assigned to output, where [0] will be the factor for the
// starting tangent and [1] for the ending tangent.
//
// These values are computed by brute-force searching for the minimal distance
// 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
constexpr size_t kNumRecords =
sizeof(kPrecomputedVariables) / sizeof(kPrecomputedVariables[0]);
constexpr Scalar kMinN = 2.00f;
Scalar steps =
std::clamp<Scalar>((n - kMinN) * kNStepInverse, 0, kNumRecords - 1);
size_t left = std::clamp<size_t>(static_cast<size_t>(std::floor(steps)), 0,
kNumRecords - 2);
Scalar frac = steps - left;
return std::array<Scalar, 2>{(1 - frac) * kPrecomputedVariables[left][0] +
frac * kPrecomputedVariables[left + 1][0],
(1 - frac) * kPrecomputedVariables[left][1] +
frac * kPrecomputedVariables[left + 1][1]};
}
CubicAdder cubic_adder_;
// A matrix that swaps the coordinates of a point.
// clang-format off
static constexpr Matrix kFlip = Matrix(
0.0f, 1.0f, 0.0f, 0.0f,
1.0f, 0.0f, 0.0f, 0.0f,
0.0f, 0.0f, 1.0f, 0.0f,
0.0f, 0.0f, 0.0f, 1.0f);
// clang-format on
};
} // namespace
PathBuilder::PathBuilder() {
AddContourComponent({});
}
@ -219,6 +377,46 @@ PathBuilder& PathBuilder::AddRoundRect(RoundRect round_rect) {
return *this;
}
PathBuilder& PathBuilder::AddRoundSuperellipse(RoundSuperellipse rse) {
if (rse.IsRect()) {
return AddRect(rse.GetBounds());
}
RoundSuperellipseBuilder builder(
[this](const Point& a, const Point& b, const Point& c, const Point& d) {
AddCubicComponent(a, b, c, d);
});
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);
MoveTo(start);
if (param.all_corners_same) {
auto* quadrant = &param.top_right;
builder.AddQuadrant(*quadrant, /*reverse=*/false);
quadrant->signed_scale.y *= -1;
builder.AddQuadrant(*quadrant, /*reverse=*/true);
quadrant->signed_scale.x *= -1;
builder.AddQuadrant(*quadrant, /*reverse=*/false);
quadrant->signed_scale.y *= -1;
builder.AddQuadrant(*quadrant, /*reverse=*/true);
} else {
builder.AddQuadrant(param.top_right, /*reverse=*/false);
builder.AddQuadrant(param.bottom_right, /*reverse=*/true);
builder.AddQuadrant(param.bottom_left, /*reverse=*/false);
builder.AddQuadrant(param.top_left, /*reverse=*/true);
}
LineTo(start);
Close();
return *this;
}
PathBuilder& PathBuilder::AddRoundedRectTopLeft(Rect rect,
RoundingRadii radii) {
const auto magic_top_left = radii.top_left * kArcApproximationMagic;

View File

@ -8,6 +8,7 @@
#include "impeller/geometry/path.h"
#include "impeller/geometry/rect.h"
#include "impeller/geometry/round_rect.h"
#include "impeller/geometry/round_superellipse.h"
#include "impeller/geometry/scalar.h"
namespace impeller {
@ -105,6 +106,8 @@ class PathBuilder {
PathBuilder& AddRoundRect(RoundRect rect);
PathBuilder& AddRoundSuperellipse(RoundSuperellipse rse);
PathBuilder& AddPath(const Path& path);
private:

View File

@ -80,6 +80,15 @@ TEST(PathTest, PathSingleContour) {
EXPECT_TRUE(path.IsSingleContour());
}
{
Path path = PathBuilder{}
.AddRoundSuperellipse(RoundSuperellipse::MakeRectRadius(
Rect::MakeXYWH(100, 100, 100, 100), 10))
.TakePath();
EXPECT_TRUE(path.IsSingleContour());
}
// Open shapes.
{
Point p(100, 100);
@ -156,6 +165,28 @@ TEST(PathTest, PathSingleContourDoubleShapes) {
EXPECT_FALSE(path.IsSingleContour());
}
{
Path path = PathBuilder{}
.AddRoundSuperellipse(RoundSuperellipse::MakeRectRadius(
Rect::MakeXYWH(100, 100, 100, 100), 10))
.AddRoundSuperellipse(RoundSuperellipse::MakeRectRadius(
Rect::MakeXYWH(100, 100, 100, 100), 10))
.TakePath();
EXPECT_FALSE(path.IsSingleContour());
}
{
Path path = PathBuilder{}
.AddRoundSuperellipse(RoundSuperellipse::MakeRectXY(
Rect::MakeXYWH(100, 100, 100, 100), Size(10, 20)))
.AddRoundSuperellipse(RoundSuperellipse::MakeRectXY(
Rect::MakeXYWH(100, 100, 100, 100), Size(10, 20)))
.TakePath();
EXPECT_FALSE(path.IsSingleContour());
}
// Open shapes.
{
Point p(100, 100);
@ -236,6 +267,28 @@ TEST(PathTest, PathBuilderSetsCorrectContourPropertiesForAddCommands) {
EXPECT_TRUE(contour.IsClosed());
}
{
Path path = PathBuilder{}
.AddRoundSuperellipse(RoundSuperellipse::MakeRectRadius(
Rect::MakeXYWH(100, 100, 100, 100), 10))
.TakePath();
ContourComponent contour;
path.GetContourComponentAtIndex(0, contour);
EXPECT_POINT_NEAR(contour.destination, Point(150, 100));
EXPECT_TRUE(contour.IsClosed());
}
{
Path path = PathBuilder{}
.AddRoundSuperellipse(RoundSuperellipse::MakeRectXY(
Rect::MakeXYWH(100, 100, 100, 100), Size(10, 20)))
.TakePath();
ContourComponent contour;
path.GetContourComponentAtIndex(0, contour);
EXPECT_POINT_NEAR(contour.destination, Point(150, 100));
EXPECT_TRUE(contour.IsClosed());
}
// Open shapes.
{
Point p(100, 100);

View File

@ -11,312 +11,6 @@
namespace impeller {
namespace testing {
TEST(RoundRectTest, RoundingRadiiEmptyDeclaration) {
RoundingRadii radii;
EXPECT_TRUE(radii.AreAllCornersEmpty());
EXPECT_TRUE(radii.AreAllCornersSame());
EXPECT_TRUE(radii.IsFinite());
EXPECT_EQ(radii.top_left, Size());
EXPECT_EQ(radii.top_right, Size());
EXPECT_EQ(radii.bottom_left, Size());
EXPECT_EQ(radii.bottom_right, Size());
EXPECT_EQ(radii.top_left.width, 0.0f);
EXPECT_EQ(radii.top_left.height, 0.0f);
EXPECT_EQ(radii.top_right.width, 0.0f);
EXPECT_EQ(radii.top_right.height, 0.0f);
EXPECT_EQ(radii.bottom_left.width, 0.0f);
EXPECT_EQ(radii.bottom_left.height, 0.0f);
EXPECT_EQ(radii.bottom_right.width, 0.0f);
EXPECT_EQ(radii.bottom_right.height, 0.0f);
}
TEST(RoundRectTest, RoundingRadiiDefaultConstructor) {
RoundingRadii radii = RoundingRadii();
EXPECT_TRUE(radii.AreAllCornersEmpty());
EXPECT_TRUE(radii.AreAllCornersSame());
EXPECT_TRUE(radii.IsFinite());
EXPECT_EQ(radii.top_left, Size());
EXPECT_EQ(radii.top_right, Size());
EXPECT_EQ(radii.bottom_left, Size());
EXPECT_EQ(radii.bottom_right, Size());
}
TEST(RoundRectTest, RoundingRadiiScalarConstructor) {
RoundingRadii radii = RoundingRadii::MakeRadius(5.0f);
EXPECT_FALSE(radii.AreAllCornersEmpty());
EXPECT_TRUE(radii.AreAllCornersSame());
EXPECT_TRUE(radii.IsFinite());
EXPECT_EQ(radii.top_left, Size(5.0f, 5.0f));
EXPECT_EQ(radii.top_right, Size(5.0f, 5.0f));
EXPECT_EQ(radii.bottom_left, Size(5.0f, 5.0f));
EXPECT_EQ(radii.bottom_right, Size(5.0f, 5.0f));
}
TEST(RoundRectTest, RoundingRadiiEmptyScalarConstructor) {
RoundingRadii radii = RoundingRadii::MakeRadius(-5.0f);
EXPECT_TRUE(radii.AreAllCornersEmpty());
EXPECT_TRUE(radii.AreAllCornersSame());
EXPECT_TRUE(radii.IsFinite());
EXPECT_EQ(radii.top_left, Size(-5.0f, -5.0f));
EXPECT_EQ(radii.top_right, Size(-5.0f, -5.0f));
EXPECT_EQ(radii.bottom_left, Size(-5.0f, -5.0f));
EXPECT_EQ(radii.bottom_right, Size(-5.0f, -5.0f));
}
TEST(RoundRectTest, RoundingRadiiSizeConstructor) {
RoundingRadii radii = RoundingRadii::MakeRadii(Size(5.0f, 6.0f));
EXPECT_FALSE(radii.AreAllCornersEmpty());
EXPECT_TRUE(radii.AreAllCornersSame());
EXPECT_TRUE(radii.IsFinite());
EXPECT_EQ(radii.top_left, Size(5.0f, 6.0f));
EXPECT_EQ(radii.top_right, Size(5.0f, 6.0f));
EXPECT_EQ(radii.bottom_left, Size(5.0f, 6.0f));
EXPECT_EQ(radii.bottom_right, Size(5.0f, 6.0f));
}
TEST(RoundRectTest, RoundingRadiiEmptySizeConstructor) {
{
RoundingRadii radii = RoundingRadii::MakeRadii(Size(-5.0f, 6.0f));
EXPECT_TRUE(radii.AreAllCornersEmpty());
EXPECT_TRUE(radii.AreAllCornersSame());
EXPECT_TRUE(radii.IsFinite());
EXPECT_EQ(radii.top_left, Size(-5.0f, 6.0f));
EXPECT_EQ(radii.top_right, Size(-5.0f, 6.0f));
EXPECT_EQ(radii.bottom_left, Size(-5.0f, 6.0f));
EXPECT_EQ(radii.bottom_right, Size(-5.0f, 6.0f));
}
{
RoundingRadii radii = RoundingRadii::MakeRadii(Size(5.0f, -6.0f));
EXPECT_TRUE(radii.AreAllCornersEmpty());
EXPECT_TRUE(radii.AreAllCornersSame());
EXPECT_TRUE(radii.IsFinite());
EXPECT_EQ(radii.top_left, Size(5.0f, -6.0f));
EXPECT_EQ(radii.top_right, Size(5.0f, -6.0f));
EXPECT_EQ(radii.bottom_left, Size(5.0f, -6.0f));
EXPECT_EQ(radii.bottom_right, Size(5.0f, -6.0f));
}
}
TEST(RoundRectTest, RoundingRadiiNamedSizesConstructor) {
RoundingRadii radii = {
.top_left = Size(5.0f, 5.5f),
.top_right = Size(6.0f, 6.5f),
.bottom_left = Size(7.0f, 7.5f),
.bottom_right = Size(8.0f, 8.5f),
};
EXPECT_FALSE(radii.AreAllCornersEmpty());
EXPECT_FALSE(radii.AreAllCornersSame());
EXPECT_TRUE(radii.IsFinite());
EXPECT_EQ(radii.top_left, Size(5.0f, 5.5f));
EXPECT_EQ(radii.top_right, Size(6.0f, 6.5f));
EXPECT_EQ(radii.bottom_left, Size(7.0f, 7.5f));
EXPECT_EQ(radii.bottom_right, Size(8.0f, 8.5f));
}
TEST(RoundRectTest, RoundingRadiiPartialNamedSizesConstructor) {
{
RoundingRadii radii = {
.top_left = Size(5.0f, 5.5f),
};
EXPECT_FALSE(radii.AreAllCornersEmpty());
EXPECT_FALSE(radii.AreAllCornersSame());
EXPECT_TRUE(radii.IsFinite());
EXPECT_EQ(radii.top_left, Size(5.0f, 5.5f));
EXPECT_EQ(radii.top_right, Size());
EXPECT_EQ(radii.bottom_left, Size());
EXPECT_EQ(radii.bottom_right, Size());
}
{
RoundingRadii radii = {
.top_right = Size(6.0f, 6.5f),
};
EXPECT_FALSE(radii.AreAllCornersEmpty());
EXPECT_FALSE(radii.AreAllCornersSame());
EXPECT_TRUE(radii.IsFinite());
EXPECT_EQ(radii.top_left, Size());
EXPECT_EQ(radii.top_right, Size(6.0f, 6.5f));
EXPECT_EQ(radii.bottom_left, Size());
EXPECT_EQ(radii.bottom_right, Size());
}
{
RoundingRadii radii = {
.bottom_left = Size(7.0f, 7.5f),
};
EXPECT_FALSE(radii.AreAllCornersEmpty());
EXPECT_FALSE(radii.AreAllCornersSame());
EXPECT_TRUE(radii.IsFinite());
EXPECT_EQ(radii.top_left, Size());
EXPECT_EQ(radii.top_right, Size());
EXPECT_EQ(radii.bottom_left, Size(7.0f, 7.5f));
EXPECT_EQ(radii.bottom_right, Size());
}
{
RoundingRadii radii = {
.bottom_right = Size(8.0f, 8.5f),
};
EXPECT_FALSE(radii.AreAllCornersEmpty());
EXPECT_FALSE(radii.AreAllCornersSame());
EXPECT_TRUE(radii.IsFinite());
EXPECT_EQ(radii.top_left, Size());
EXPECT_EQ(radii.top_right, Size());
EXPECT_EQ(radii.bottom_left, Size());
EXPECT_EQ(radii.bottom_right, Size(8.0f, 8.5f));
}
}
TEST(RoundRectTest, RoundingRadiiMultiply) {
RoundingRadii radii = {
.top_left = Size(5.0f, 5.5f),
.top_right = Size(6.0f, 6.5f),
.bottom_left = Size(7.0f, 7.5f),
.bottom_right = Size(8.0f, 8.5f),
};
RoundingRadii doubled = radii * 2.0f;
EXPECT_FALSE(doubled.AreAllCornersEmpty());
EXPECT_FALSE(doubled.AreAllCornersSame());
EXPECT_TRUE(doubled.IsFinite());
EXPECT_EQ(doubled.top_left, Size(10.0f, 11.0f));
EXPECT_EQ(doubled.top_right, Size(12.0f, 13.0f));
EXPECT_EQ(doubled.bottom_left, Size(14.0f, 15.0f));
EXPECT_EQ(doubled.bottom_right, Size(16.0f, 17.0f));
}
TEST(RoundRectTest, RoundingRadiiEquals) {
RoundingRadii radii = {
.top_left = Size(5.0f, 5.5f),
.top_right = Size(6.0f, 6.5f),
.bottom_left = Size(7.0f, 7.5f),
.bottom_right = Size(8.0f, 8.5f),
};
RoundingRadii other = {
.top_left = Size(5.0f, 5.5f),
.top_right = Size(6.0f, 6.5f),
.bottom_left = Size(7.0f, 7.5f),
.bottom_right = Size(8.0f, 8.5f),
};
EXPECT_EQ(radii, other);
}
TEST(RoundRectTest, RoundingRadiiNotEquals) {
const RoundingRadii radii = {
.top_left = Size(5.0f, 5.5f),
.top_right = Size(6.0f, 6.5f),
.bottom_left = Size(7.0f, 7.5f),
.bottom_right = Size(8.0f, 8.5f),
};
{
RoundingRadii different = radii;
different.top_left.width = 100.0f;
EXPECT_NE(different, radii);
}
{
RoundingRadii different = radii;
different.top_left.height = 100.0f;
EXPECT_NE(different, radii);
}
{
RoundingRadii different = radii;
different.top_right.width = 100.0f;
EXPECT_NE(different, radii);
}
{
RoundingRadii different = radii;
different.top_right.height = 100.0f;
EXPECT_NE(different, radii);
}
{
RoundingRadii different = radii;
different.bottom_left.width = 100.0f;
EXPECT_NE(different, radii);
}
{
RoundingRadii different = radii;
different.bottom_left.height = 100.0f;
EXPECT_NE(different, radii);
}
{
RoundingRadii different = radii;
different.bottom_right.width = 100.0f;
EXPECT_NE(different, radii);
}
{
RoundingRadii different = radii;
different.bottom_right.height = 100.0f;
EXPECT_NE(different, radii);
}
}
TEST(RoundRectTest, RoundingRadiiCornersSameTolerance) {
RoundingRadii radii{
.top_left = {10, 20},
.top_right = {10.01, 20.01},
.bottom_left = {9.99, 19.99},
.bottom_right = {9.99, 20.01},
};
EXPECT_TRUE(radii.AreAllCornersSame(.02));
{
RoundingRadii different = radii;
different.top_left.width = 10.03;
EXPECT_FALSE(different.AreAllCornersSame(.02));
}
{
RoundingRadii different = radii;
different.top_left.height = 20.03;
EXPECT_FALSE(different.AreAllCornersSame(.02));
}
{
RoundingRadii different = radii;
different.top_right.width = 10.03;
EXPECT_FALSE(different.AreAllCornersSame(.02));
}
{
RoundingRadii different = radii;
different.top_right.height = 20.03;
EXPECT_FALSE(different.AreAllCornersSame(.02));
}
{
RoundingRadii different = radii;
different.bottom_left.width = 9.97;
EXPECT_FALSE(different.AreAllCornersSame(.02));
}
{
RoundingRadii different = radii;
different.bottom_left.height = 19.97;
EXPECT_FALSE(different.AreAllCornersSame(.02));
}
{
RoundingRadii different = radii;
different.bottom_right.width = 9.97;
EXPECT_FALSE(different.AreAllCornersSame(.02));
}
{
RoundingRadii different = radii;
different.bottom_right.height = 20.03;
EXPECT_FALSE(different.AreAllCornersSame(.02));
}
}
TEST(RoundRectTest, EmptyDeclaration) {
RoundRect round_rect;

View File

@ -0,0 +1,33 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "flutter/impeller/geometry/round_superellipse.h"
#include "flutter/impeller/geometry/round_superellipse_param.h"
namespace impeller {
RoundSuperellipse RoundSuperellipse::MakeRectRadii(
const Rect& in_bounds,
const RoundingRadii& in_radii) {
if (!in_bounds.IsFinite()) {
return {};
}
Rect bounds = in_bounds.GetPositive();
// RoundingRadii::Scaled might return an empty radii if bounds or in_radii is
// empty, which is expected. Pass along the bounds even if the radii is empty
// as it would still have a valid location and/or 1-dimensional size which
// might appear when stroked
return RoundSuperellipse(bounds, in_radii.Scaled(bounds));
}
[[nodiscard]] bool RoundSuperellipse::Contains(const Point& p) const {
if (!bounds_.Contains(p)) {
return false;
}
auto param = RoundSuperellipseParam::MakeBoundsRadii(bounds_, radii_);
return param.Contains(p);
}
} // namespace impeller

View File

@ -0,0 +1,150 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef FLUTTER_IMPELLER_GEOMETRY_ROUND_SUPERELLIPSE_H_
#define FLUTTER_IMPELLER_GEOMETRY_ROUND_SUPERELLIPSE_H_
#include "flutter/impeller/geometry/point.h"
#include "flutter/impeller/geometry/rect.h"
#include "flutter/impeller/geometry/rounding_radii.h"
#include "flutter/impeller/geometry/size.h"
namespace impeller {
struct RoundSuperellipse {
RoundSuperellipse() = default;
constexpr static RoundSuperellipse MakeRect(const Rect& rect) {
return MakeRectRadii(rect, RoundingRadii());
}
constexpr static RoundSuperellipse MakeOval(const Rect& rect) {
return MakeRectRadii(rect, RoundingRadii::MakeRadii(rect.GetSize() * 0.5f));
}
constexpr static RoundSuperellipse MakeRectRadius(const Rect& rect,
Scalar radius) {
return MakeRectRadii(rect, RoundingRadii::MakeRadius(radius));
}
constexpr static RoundSuperellipse MakeRectXY(const Rect& rect,
Scalar x_radius,
Scalar y_radius) {
return MakeRectRadii(rect,
RoundingRadii::MakeRadii(Size(x_radius, y_radius)));
}
constexpr static RoundSuperellipse MakeRectXY(const Rect& rect,
Size corner_radii) {
return MakeRectRadii(rect, RoundingRadii::MakeRadii(corner_radii));
}
static RoundSuperellipse MakeRectRadii(const Rect& rect,
const RoundingRadii& radii);
constexpr const Rect& GetBounds() const { return bounds_; }
constexpr const RoundingRadii& GetRadii() const { return radii_; }
[[nodiscard]] constexpr bool IsFinite() const {
return bounds_.IsFinite() && //
radii_.top_left.IsFinite() && //
radii_.top_right.IsFinite() && //
radii_.bottom_left.IsFinite() && //
radii_.bottom_right.IsFinite();
}
[[nodiscard]] constexpr bool IsEmpty() const { return bounds_.IsEmpty(); }
[[nodiscard]] constexpr bool IsRect() const {
return !bounds_.IsEmpty() && radii_.AreAllCornersEmpty();
}
[[nodiscard]] constexpr bool IsOval() const {
return !bounds_.IsEmpty() && radii_.AreAllCornersSame() &&
ScalarNearlyEqual(radii_.top_left.width,
bounds_.GetWidth() * 0.5f) &&
ScalarNearlyEqual(radii_.top_left.height,
bounds_.GetHeight() * 0.5f);
}
/// @brief Returns true iff the provided point |p| is inside the
/// half-open interior of this rectangle.
///
/// For purposes of containment, a rectangle contains points
/// along the top and left edges but not points along the
/// right and bottom edges so that a point is only ever
/// considered inside one of two abutting rectangles.
[[nodiscard]] bool Contains(const Point& p) const;
/// @brief Returns a new round rectangle translated by the given offset.
[[nodiscard]] constexpr RoundSuperellipse Shift(Scalar dx, Scalar dy) const {
// Just in case, use the factory rather than the internal constructor
// as shifting the rectangle may increase/decrease its bit precision
// so we should re-validate the radii to the newly located rectangle.
return MakeRectRadii(bounds_.Shift(dx, dy), radii_);
}
/// @brief Returns a round rectangle with expanded edges. Negative expansion
/// results in shrinking.
[[nodiscard]] constexpr RoundSuperellipse Expand(Scalar left,
Scalar top,
Scalar right,
Scalar bottom) const {
// Use the factory rather than the internal constructor as the changing
// size of the rectangle requires that we re-validate the radii to the
// newly sized rectangle.
return MakeRectRadii(bounds_.Expand(left, top, right, bottom), radii_);
}
/// @brief Returns a round rectangle with expanded edges. Negative expansion
/// results in shrinking.
[[nodiscard]] constexpr RoundSuperellipse Expand(Scalar horizontal,
Scalar vertical) const {
// Use the factory rather than the internal constructor as the changing
// size of the rectangle requires that we re-validate the radii to the
// newly sized rectangle.
return MakeRectRadii(bounds_.Expand(horizontal, vertical), radii_);
}
/// @brief Returns a round rectangle with expanded edges. Negative expansion
/// results in shrinking.
[[nodiscard]] constexpr RoundSuperellipse Expand(Scalar amount) const {
// Use the factory rather than the internal constructor as the changing
// size of the rectangle requires that we re-validate the radii to the
// newly sized rectangle.
return MakeRectRadii(bounds_.Expand(amount), radii_);
}
[[nodiscard]] constexpr bool operator==(const RoundSuperellipse& rr) const {
return bounds_ == rr.bounds_ && radii_ == rr.radii_;
}
[[nodiscard]] constexpr bool operator!=(const RoundSuperellipse& r) const {
return !(*this == r);
}
private:
constexpr RoundSuperellipse(const Rect& bounds, const RoundingRadii& radii)
: bounds_(bounds), radii_(radii) {}
Rect bounds_;
RoundingRadii radii_;
};
} // namespace impeller
namespace std {
inline std::ostream& operator<<(std::ostream& out,
const impeller::RoundSuperellipse& rr) {
out << "(" //
<< "rect: " << rr.GetBounds() << ", " //
<< "radii: " << rr.GetRadii();
out << ")";
return out;
}
} // namespace std
#endif // FLUTTER_IMPELLER_GEOMETRY_ROUND_SUPERELLIPSE_H_

View File

@ -0,0 +1,340 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "flutter/impeller/geometry/round_superellipse_param.h"
namespace impeller {
namespace {
// Return the value that splits the range from `left` to `right` into two
// portions whose ratio equals to `ratio_left` : `ratio_right`.
Scalar Split(Scalar left, Scalar right, Scalar ratio_left, Scalar ratio_right) {
if (ratio_left == 0 && ratio_right == 0) {
return (left + right) / 2;
}
return (left * ratio_right + right * ratio_left) / (ratio_left + ratio_right);
}
// Return the same Point, but each NaN coordinate is replaced by 1.
inline Point ReplanceNaNWithOne(Point in) {
return Point{std::isnan(in.x) ? 1 : in.x, std::isnan(in.y) ? 1 : in.y};
}
// Swap the x and y coordinate of a point.
//
// Effectively mirrors the point by the y=x line.
inline Point Flip(Point a) {
return Point{a.y, a.x};
}
// A look up table with precomputed variables.
//
// The columns represent the following variabls respectively:
//
// * n
// * sin(thetaJ)
//
// 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
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);
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];
}
// Find the center of the circle that passes the given two points and have the
// given radius.
Point FindCircleCenter(Point a, Point b, Scalar r) {
/* Denote the middle point of A and B as M. The key is to find the center of
* the circle.
* A --__
* / `
* / M \
* / B
* /
* /
* / r
* C
*/
Point a_to_b = b - a;
Point m = (a + b) / 2;
Point c_to_m = Point(-a_to_b.y, a_to_b.x);
Scalar distance_am = a_to_b.GetLength() / 2;
Scalar distance_cm = sqrt(r * r - distance_am * distance_am);
return m - distance_cm * c_to_m.Normalize();
}
// Compute parameters for a square-like rounded superellipse with a symmetrical
// radius.
RoundSuperellipseParam::Octant ComputeOctant(Point center,
Scalar half_size,
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).
*
* straight superelipse
*
* A B J circular arc
* ---------...._
* | | / ` M
* | | /
* | | / \
* | | / |
* | | D |
* | | / |
* +----+ S |
* s | | |
* +----+---------------| A'
* O
* s
* ---- half_size -----
*/
Scalar ratio =
radius == 0 ? kMaxRatio : std::min(half_size * 2 / radius, kMaxRatio);
Scalar a = ratio * radius / 2;
Scalar s = half_size - a;
Scalar g = RoundSuperellipseParam::kGapFactor * radius;
Scalar n = LerpPrecomputedVariable(0, ratio);
Scalar sin_thetaJ = radius == 0 ? 0 : LerpPrecomputedVariable(1, ratio);
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 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 circle_center =
radius == 0 ? pointM : FindCircleCenter(pointJ, pointM, R);
Radians circle_max_angle =
radius == 0 ? Radians(0)
: (pointM - circle_center).AngleTo(pointJ - circle_center);
return RoundSuperellipseParam::Octant{
.offset = center,
.edge_mid = pointA,
.se_center = pointS,
.se_a = a,
.se_n = n,
.se_max_theta = asin(sin_thetaJ),
.ratio = ratio,
.circle_start = pointJ,
.circle_center = circle_center,
.circle_max_angle = circle_max_angle,
};
}
// Compute parameters for a quadrant of a rounded superellipse with asymmetrical
// radii.
//
// The `corner` is the coordinate of the corner point in the same coordinate
// space as `center`, which specifies both the half size of the bounding box and
// which quadrant the curve should be.
RoundSuperellipseParam::Quadrant ComputeQuadrant(Point center,
Point corner,
Size in_radii) {
Point corner_vector = corner - center;
Size radii = in_radii.Abs();
// The prefix "norm" is short for "normalized".
//
// Be extra careful to avoid NaNs in cases that some coordinates of `in_radii`
// or `corner_vector` are zero.
Scalar norm_radius = radii.MinDimension();
Size forward_scale = norm_radius == 0 ? Size{1, 1} : radii / norm_radius;
Point norm_half_size = corner_vector.Abs() / forward_scale;
Point signed_scale = ReplanceNaNWithOne(corner_vector / norm_half_size);
// Each quadrant curve is composed of two octant curves, each of which belongs
// to a square-like rounded rectangle. For the two octants to connect at the
// circular arc, the centers these two square-like rounded rectangle must be
// offset from the quadrant center by a same distance in different directions.
// The distance is denoted as `c`.
Scalar c = norm_half_size.x - norm_half_size.y;
return RoundSuperellipseParam::Quadrant{
.offset = center,
.signed_scale = signed_scale,
.top = ComputeOctant(Point{0, -c}, norm_half_size.x, norm_radius),
.right = ComputeOctant(Point{c, 0}, norm_half_size.y, norm_radius),
};
}
// Checks whether the given point is contained in the first octant of the given
// square-like rounded superellipse.
//
// The first octant refers to the region that spans from 0 to pi/4 starting from
// positive Y axis clockwise.
//
// If the point is not within this octant at all, then this function always
// returns true. Otherwise this function returns whether the point is contained
// within the rounded superellipse.
//
// The `param.offset` is ignored. The input point should have been transformed
// to the coordinate space where the rounded superellipse is centered at the
// origin.
bool OctantContains(const RoundSuperellipseParam::Octant& param,
const Point& p) {
// Check whether the point is within the octant.
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;
return powf(p_se.x, param.se_n) + powf(p_se.y, param.se_n) <= 1;
}
Scalar circle_radius =
param.circle_start.GetDistanceSquared(param.circle_center);
Point p_circle = p - param.circle_center;
return p_circle.GetDistanceSquared(Point()) < circle_radius;
}
// Determine if p is inside the corner curve defined by the indicated corner
// param.
//
// The coordinates of p should be within the same coordinate space with
// `param.offset`.
//
// If `check_quadrant` is true, then this function first checks if the point is
// within the quadrant of given corner. If not, this function returns true,
// otherwise this method continues to check whether the point is contained in
// the rounded superellipse.
//
// If `check_quadrant` is false, then the first step above is skipped, and the
// function checks whether the absolute (relative to the center) coordinate of p
// is contained in the rounded superellipse.
bool CornerContains(const RoundSuperellipseParam::Quadrant& param,
const Point& p,
bool check_quadrant = true) {
Point norm_point = (p - param.offset) / param.signed_scale;
if (check_quadrant) {
if (norm_point.x < 0 || norm_point.y < 0) {
return true;
}
} else {
norm_point = norm_point.Abs();
}
return OctantContains(param.top, norm_point - param.top.offset) &&
OctantContains(param.right, Flip(norm_point - param.right.offset));
}
} // namespace
RoundSuperellipseParam RoundSuperellipseParam::MakeBoundsRadii(
const Rect& bounds_,
const RoundingRadii& radii_) {
if (radii_.AreAllCornersSame()) {
return RoundSuperellipseParam{
.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 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);
return RoundSuperellipseParam{
.top_right = ComputeQuadrant(Point{top_split, right_split},
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),
.top_left = ComputeQuadrant(Point{top_split, left_split},
bounds_.GetLeftTop(), radii_.top_left),
.all_corners_same = false,
};
}
bool RoundSuperellipseParam::Contains(const Point& point) const {
if (all_corners_same) {
return CornerContains(top_right, point, /*check_quadrant=*/false);
}
return CornerContains(top_right, point) &&
CornerContains(bottom_right, point) &&
CornerContains(bottom_left, point) && CornerContains(top_left, point);
}
void RoundSuperellipseParam::SuperellipseBezierArc(
Point* output,
const RoundSuperellipseParam::Octant& param) {
Point start = {param.se_center.x, param.edge_mid.y};
const Point& end = param.circle_start;
constexpr Point start_tangent = {1, 0};
Point circle_start_vector = param.circle_start - param.circle_center;
Point end_tangent =
Point{-circle_start_vector.y, circle_start_vector.x}.Normalize();
Scalar start_factor = LerpPrecomputedVariable(0, param.ratio);
Scalar end_factor = LerpPrecomputedVariable(1, param.ratio);
output[0] = start;
output[1] = start + start_tangent * start_factor * param.se_a;
output[2] = end + end_tangent * end_factor * param.se_a;
output[3] = end;
}
} // namespace impeller

View File

@ -0,0 +1,134 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef FLUTTER_IMPELLER_GEOMETRY_ROUND_SUPERELLIPSE_PARAM_H_
#define FLUTTER_IMPELLER_GEOMETRY_ROUND_SUPERELLIPSE_PARAM_H_
#include "flutter/impeller/geometry/point.h"
#include "flutter/impeller/geometry/rect.h"
#include "flutter/impeller/geometry/rounding_radii.h"
#include "flutter/impeller/geometry/size.h"
namespace impeller {
// A utility struct that expands input parameters for a rounded superellipse to
// drawing variables.
struct RoundSuperellipseParam {
// Parameters for drawing a square-like rounded superellipse.
//
// This structure is used to define an octant of an arbitrary rounded
// superellipse.
struct Octant {
// The offset of the square-like rounded superellipse's center from the
// origin.
//
// 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.
Scalar se_n;
// The range of the parameter "theta" used to define the superellipse curve.
//
// The "theta" is not the angle of the curve but the implicit parameter
// used in the curve's parametric equation.
Scalar se_max_theta;
Scalar ratio;
// The coordinate of the top left end of the circular arc, relative to the
// `offset` point.
Point circle_start;
// The center of the circular arc, relative to the `offset` point.
Point circle_center;
// The angular span of the circular arc, measured in radians.
Radians circle_max_angle;
};
// Parameters for drawing a rounded superellipse with equal radius size for
// all corners.
//
// This structure is used to define a quadrant of an arbitrary rounded
// superellipse.
struct Quadrant {
// The offset of the rounded superellipse's center from the origin.
//
// All other coordinates in this structure are relative to this point.
Point offset;
// The scaling factor used to transform a normalized rounded superellipse
// back to its original, unnormalized shape.
//
// Normalization refers to adjusting the original curve, which may have
// asymmetrical corner sizes, into a symmetrical one by reducing the longer
// radius to match the shorter one. For instance, to draw a rounded
// superellipse with size (200, 300) and radii (20, 10), the function first
// draws a normalized RSE with size (100, 300) and radii (10, 10), then
// scales it by (2x, 1x) to restore the original proportions.
//
// Normalization also flips the curve to the first quadrant (positive x and
// y) if it originally resides in another quadrant. This affects the signs
// of `signed_scale`.
Point signed_scale;
// The parameters for the two octants that make up this quadrant after
// normalization.
Octant top;
Octant right;
};
// The parameters for the four quadrants that make up the full contour.
//
// If `all_corners_same` is true, then only `top_right` is popularized.
Quadrant top_right;
Quadrant bottom_right;
Quadrant bottom_left;
Quadrant top_left;
// If true, all corners are the same and only `top_right` is popularized.
bool all_corners_same;
// Create a param for a rounded superellipse with the specific bounds and
// radii.
[[nodiscard]] static RoundSuperellipseParam MakeBoundsRadii(
const Rect& bounds,
const RoundingRadii& radii);
// Returns whether this rounded superellipse contains the point.
//
// This method does not perform any prescreening such as comparing the point
// with the bounds, which is recommended for callers.
bool Contains(const Point& point) const;
// A factor used to calculate the "gap", defined as the distance from the
// midpoint of the curved corners to the nearest sides of the bounding box.
//
// When the corner radius is symmetrical on both dimensions, the midpoint of
// the corner is where the circular arc intersects its quadrant bisector. When
// the corner radius is asymmetrical, since the corner can be considered
// "elongated" from a symmetrical corner, the midpoint is transformed in the
// same way.
//
// Experiments indicate that the gap is linear with respect to the corner
// radius on that dimension.
static constexpr Scalar kGapFactor = 0.29289321881f; // 1-cos(pi/4)
static void SuperellipseBezierArc(
Point* output,
const RoundSuperellipseParam::Octant& param);
};
} // namespace impeller
#endif // FLUTTER_IMPELLER_GEOMETRY_ROUND_SUPERELLIPSE_PARAM_H_

View File

@ -0,0 +1,694 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "gtest/gtest.h"
#include "flutter/impeller/geometry/round_superellipse.h"
#include "flutter/impeller/geometry/geometry_asserts.h"
#define CHECK_POINT_WITH_OFFSET(rr, p, outward_offset) \
EXPECT_TRUE(rr.Contains(p)); \
EXPECT_FALSE(rr.Contains(p + outward_offset));
namespace impeller {
namespace testing {
TEST(RoundSuperellipseTest, EmptyDeclaration) {
RoundSuperellipse rse;
EXPECT_TRUE(rse.IsEmpty());
EXPECT_FALSE(rse.IsRect());
EXPECT_FALSE(rse.IsOval());
EXPECT_TRUE(rse.IsFinite());
EXPECT_TRUE(rse.GetBounds().IsEmpty());
EXPECT_EQ(rse.GetBounds(), Rect());
EXPECT_EQ(rse.GetBounds().GetLeft(), 0.0f);
EXPECT_EQ(rse.GetBounds().GetTop(), 0.0f);
EXPECT_EQ(rse.GetBounds().GetRight(), 0.0f);
EXPECT_EQ(rse.GetBounds().GetBottom(), 0.0f);
EXPECT_EQ(rse.GetRadii().top_left, Size());
EXPECT_EQ(rse.GetRadii().top_right, Size());
EXPECT_EQ(rse.GetRadii().bottom_left, Size());
EXPECT_EQ(rse.GetRadii().bottom_right, Size());
EXPECT_EQ(rse.GetRadii().top_left.width, 0.0f);
EXPECT_EQ(rse.GetRadii().top_left.height, 0.0f);
EXPECT_EQ(rse.GetRadii().top_right.width, 0.0f);
EXPECT_EQ(rse.GetRadii().top_right.height, 0.0f);
EXPECT_EQ(rse.GetRadii().bottom_left.width, 0.0f);
EXPECT_EQ(rse.GetRadii().bottom_left.height, 0.0f);
EXPECT_EQ(rse.GetRadii().bottom_right.width, 0.0f);
EXPECT_EQ(rse.GetRadii().bottom_right.height, 0.0f);
}
TEST(RoundSuperellipseTest, DefaultConstructor) {
RoundSuperellipse rse = RoundSuperellipse();
EXPECT_TRUE(rse.IsEmpty());
EXPECT_FALSE(rse.IsRect());
EXPECT_FALSE(rse.IsOval());
EXPECT_TRUE(rse.IsFinite());
EXPECT_TRUE(rse.GetBounds().IsEmpty());
EXPECT_EQ(rse.GetBounds(), Rect());
EXPECT_EQ(rse.GetRadii().top_left, Size());
EXPECT_EQ(rse.GetRadii().top_right, Size());
EXPECT_EQ(rse.GetRadii().bottom_left, Size());
EXPECT_EQ(rse.GetRadii().bottom_right, Size());
}
TEST(RoundSuperellipseTest, EmptyRectConstruction) {
RoundSuperellipse rse =
RoundSuperellipse::MakeRect(Rect::MakeLTRB(20.0f, 20.0f, 20.0f, 20.0f));
EXPECT_TRUE(rse.IsEmpty());
EXPECT_FALSE(rse.IsRect());
EXPECT_FALSE(rse.IsOval());
EXPECT_TRUE(rse.IsFinite());
EXPECT_TRUE(rse.GetBounds().IsEmpty());
EXPECT_EQ(rse.GetBounds(), Rect::MakeLTRB(20.0f, 20.0f, 20.0f, 20.0f));
EXPECT_EQ(rse.GetRadii().top_left, Size());
EXPECT_EQ(rse.GetRadii().top_right, Size());
EXPECT_EQ(rse.GetRadii().bottom_left, Size());
EXPECT_EQ(rse.GetRadii().bottom_right, Size());
}
TEST(RoundSuperellipseTest, RectConstructor) {
RoundSuperellipse rse =
RoundSuperellipse::MakeRect(Rect::MakeLTRB(10.0f, 10.0f, 20.0f, 20.0f));
EXPECT_FALSE(rse.IsEmpty());
EXPECT_TRUE(rse.IsRect());
EXPECT_FALSE(rse.IsOval());
EXPECT_TRUE(rse.IsFinite());
EXPECT_FALSE(rse.GetBounds().IsEmpty());
EXPECT_EQ(rse.GetBounds(), Rect::MakeLTRB(10.0f, 10.0f, 20.0f, 20.0f));
EXPECT_EQ(rse.GetRadii().top_left, Size());
EXPECT_EQ(rse.GetRadii().top_right, Size());
EXPECT_EQ(rse.GetRadii().bottom_left, Size());
EXPECT_EQ(rse.GetRadii().bottom_right, Size());
}
TEST(RoundSuperellipseTest, InvertedRectConstruction) {
RoundSuperellipse rse =
RoundSuperellipse::MakeRect(Rect::MakeLTRB(20.0f, 20.0f, 10.0f, 10.0f));
EXPECT_FALSE(rse.IsEmpty());
EXPECT_TRUE(rse.IsRect());
EXPECT_FALSE(rse.IsOval());
EXPECT_TRUE(rse.IsFinite());
EXPECT_FALSE(rse.GetBounds().IsEmpty());
EXPECT_EQ(rse.GetBounds(), Rect::MakeLTRB(10.0f, 10.0f, 20.0f, 20.0f));
EXPECT_EQ(rse.GetRadii().top_left, Size());
EXPECT_EQ(rse.GetRadii().top_right, Size());
EXPECT_EQ(rse.GetRadii().bottom_left, Size());
EXPECT_EQ(rse.GetRadii().bottom_right, Size());
}
TEST(RoundSuperellipseTest, EmptyOvalConstruction) {
RoundSuperellipse rse = RoundSuperellipse::MakeRectXY(
Rect::MakeLTRB(20.0f, 20.0f, 20.0f, 20.0f), 10.0f, 10.0f);
EXPECT_TRUE(rse.IsEmpty());
EXPECT_FALSE(rse.IsRect());
EXPECT_FALSE(rse.IsOval());
EXPECT_TRUE(rse.IsFinite());
EXPECT_TRUE(rse.GetBounds().IsEmpty());
EXPECT_EQ(rse.GetBounds(), Rect::MakeLTRB(20.0f, 20.0f, 20.0f, 20.0f));
EXPECT_EQ(rse.GetRadii().top_left, Size());
EXPECT_EQ(rse.GetRadii().top_right, Size());
EXPECT_EQ(rse.GetRadii().bottom_left, Size());
EXPECT_EQ(rse.GetRadii().bottom_right, Size());
}
TEST(RoundSuperellipseTest, OvalConstructor) {
RoundSuperellipse rse =
RoundSuperellipse::MakeOval(Rect::MakeLTRB(10.0f, 10.0f, 20.0f, 20.0f));
EXPECT_FALSE(rse.IsEmpty());
EXPECT_FALSE(rse.IsRect());
EXPECT_TRUE(rse.IsOval());
EXPECT_TRUE(rse.IsFinite());
EXPECT_FALSE(rse.GetBounds().IsEmpty());
EXPECT_EQ(rse.GetBounds(), Rect::MakeLTRB(10.0f, 10.0f, 20.0f, 20.0f));
EXPECT_EQ(rse.GetRadii().top_left, Size(5.0f, 5.0f));
EXPECT_EQ(rse.GetRadii().top_right, Size(5.0f, 5.0f));
EXPECT_EQ(rse.GetRadii().bottom_left, Size(5.0f, 5.0f));
EXPECT_EQ(rse.GetRadii().bottom_right, Size(5.0f, 5.0f));
}
TEST(RoundSuperellipseTest, InvertedOvalConstruction) {
RoundSuperellipse rse = RoundSuperellipse::MakeRectXY(
Rect::MakeLTRB(20.0f, 20.0f, 10.0f, 10.0f), 10.0f, 10.0f);
EXPECT_FALSE(rse.IsEmpty());
EXPECT_FALSE(rse.IsRect());
EXPECT_TRUE(rse.IsOval());
EXPECT_TRUE(rse.IsFinite());
EXPECT_FALSE(rse.GetBounds().IsEmpty());
EXPECT_EQ(rse.GetBounds(), Rect::MakeLTRB(10.0f, 10.0f, 20.0f, 20.0f));
EXPECT_EQ(rse.GetRadii().top_left, Size(5.0f, 5.0f));
EXPECT_EQ(rse.GetRadii().top_right, Size(5.0f, 5.0f));
EXPECT_EQ(rse.GetRadii().bottom_left, Size(5.0f, 5.0f));
EXPECT_EQ(rse.GetRadii().bottom_right, Size(5.0f, 5.0f));
}
TEST(RoundSuperellipseTest, RectRadiusConstructor) {
RoundSuperellipse rse = RoundSuperellipse::MakeRectRadius(
Rect::MakeLTRB(10.0f, 10.0f, 20.0f, 20.0f), 2.0f);
EXPECT_FALSE(rse.IsEmpty());
EXPECT_FALSE(rse.IsRect());
EXPECT_FALSE(rse.IsOval());
EXPECT_TRUE(rse.IsFinite());
EXPECT_FALSE(rse.GetBounds().IsEmpty());
EXPECT_EQ(rse.GetBounds(), Rect::MakeLTRB(10.0f, 10.0f, 20.0f, 20.0f));
EXPECT_EQ(rse.GetRadii().top_left, Size(2.0f, 2.0f));
EXPECT_EQ(rse.GetRadii().top_right, Size(2.0f, 2.0f));
EXPECT_EQ(rse.GetRadii().bottom_left, Size(2.0f, 2.0f));
EXPECT_EQ(rse.GetRadii().bottom_right, Size(2.0f, 2.0f));
}
TEST(RoundSuperellipseTest, RectXYConstructor) {
RoundSuperellipse rse = RoundSuperellipse::MakeRectXY(
Rect::MakeLTRB(10.0f, 10.0f, 20.0f, 20.0f), 2.0f, 3.0f);
EXPECT_FALSE(rse.IsEmpty());
EXPECT_FALSE(rse.IsRect());
EXPECT_FALSE(rse.IsOval());
EXPECT_TRUE(rse.IsFinite());
EXPECT_FALSE(rse.GetBounds().IsEmpty());
EXPECT_EQ(rse.GetBounds(), Rect::MakeLTRB(10.0f, 10.0f, 20.0f, 20.0f));
EXPECT_EQ(rse.GetRadii().top_left, Size(2.0f, 3.0f));
EXPECT_EQ(rse.GetRadii().top_right, Size(2.0f, 3.0f));
EXPECT_EQ(rse.GetRadii().bottom_left, Size(2.0f, 3.0f));
EXPECT_EQ(rse.GetRadii().bottom_right, Size(2.0f, 3.0f));
}
TEST(RoundSuperellipseTest, RectSizeConstructor) {
RoundSuperellipse rse = RoundSuperellipse::MakeRectXY(
Rect::MakeLTRB(10.0f, 10.0f, 20.0f, 20.0f), Size(2.0f, 3.0f));
EXPECT_FALSE(rse.IsEmpty());
EXPECT_FALSE(rse.IsRect());
EXPECT_FALSE(rse.IsOval());
EXPECT_TRUE(rse.IsFinite());
EXPECT_FALSE(rse.GetBounds().IsEmpty());
EXPECT_EQ(rse.GetBounds(), Rect::MakeLTRB(10.0f, 10.0f, 20.0f, 20.0f));
EXPECT_EQ(rse.GetRadii().top_left, Size(2.0f, 3.0f));
EXPECT_EQ(rse.GetRadii().top_right, Size(2.0f, 3.0f));
EXPECT_EQ(rse.GetRadii().bottom_left, Size(2.0f, 3.0f));
EXPECT_EQ(rse.GetRadii().bottom_right, Size(2.0f, 3.0f));
}
TEST(RoundSuperellipseTest, RectRadiiConstructor) {
RoundSuperellipse rse = RoundSuperellipse::MakeRectRadii(
Rect::MakeLTRB(10.0f, 10.0f, 20.0f, 20.0f),
{
.top_left = Size(1.0, 1.5),
.top_right = Size(2.0, 2.5f),
.bottom_left = Size(3.0, 3.5f),
.bottom_right = Size(4.0, 4.5f),
});
EXPECT_FALSE(rse.IsEmpty());
EXPECT_FALSE(rse.IsRect());
EXPECT_FALSE(rse.IsOval());
EXPECT_TRUE(rse.IsFinite());
EXPECT_FALSE(rse.GetBounds().IsEmpty());
EXPECT_EQ(rse.GetBounds(), Rect::MakeLTRB(10.0f, 10.0f, 20.0f, 20.0f));
EXPECT_EQ(rse.GetRadii().top_left, Size(1.0f, 1.5f));
EXPECT_EQ(rse.GetRadii().top_right, Size(2.0f, 2.5f));
EXPECT_EQ(rse.GetRadii().bottom_left, Size(3.0f, 3.5f));
EXPECT_EQ(rse.GetRadii().bottom_right, Size(4.0f, 4.5f));
}
TEST(RoundSuperellipseTest, RectRadiiOverflowWidthConstructor) {
RoundSuperellipse rse = RoundSuperellipse::MakeRectRadii(
Rect::MakeXYWH(10.0f, 10.0f, 6.0f, 30.0f),
{
.top_left = Size(1.0f, 2.0f),
.top_right = Size(3.0f, 4.0f),
.bottom_left = Size(5.0f, 6.0f),
.bottom_right = Size(7.0f, 8.0f),
});
// Largest sum of paired radii widths is the bottom edge which sums to 12
// Rect is only 6 wide so all radii are scaled by half
// Rect is 30 tall so no scaling should happen due to radii heights
EXPECT_FALSE(rse.IsEmpty());
EXPECT_FALSE(rse.IsRect());
EXPECT_FALSE(rse.IsOval());
EXPECT_TRUE(rse.IsFinite());
EXPECT_FALSE(rse.GetBounds().IsEmpty());
EXPECT_EQ(rse.GetBounds(), Rect::MakeLTRB(10.0f, 10.0f, 16.0f, 40.0f));
EXPECT_EQ(rse.GetRadii().top_left, Size(0.5f, 1.0f));
EXPECT_EQ(rse.GetRadii().top_right, Size(1.5f, 2.0f));
EXPECT_EQ(rse.GetRadii().bottom_left, Size(2.5f, 3.0f));
EXPECT_EQ(rse.GetRadii().bottom_right, Size(3.5f, 4.0f));
}
TEST(RoundSuperellipseTest, RectRadiiOverflowHeightConstructor) {
RoundSuperellipse rse = RoundSuperellipse::MakeRectRadii(
Rect::MakeXYWH(10.0f, 10.0f, 30.0f, 6.0f),
{
.top_left = Size(1.0f, 2.0f),
.top_right = Size(3.0f, 4.0f),
.bottom_left = Size(5.0f, 6.0f),
.bottom_right = Size(7.0f, 8.0f),
});
// Largest sum of paired radii heights is the right edge which sums to 12
// Rect is only 6 tall so all radii are scaled by half
// Rect is 30 wide so no scaling should happen due to radii widths
EXPECT_FALSE(rse.IsEmpty());
EXPECT_FALSE(rse.IsRect());
EXPECT_FALSE(rse.IsOval());
EXPECT_TRUE(rse.IsFinite());
EXPECT_FALSE(rse.GetBounds().IsEmpty());
EXPECT_EQ(rse.GetBounds(), Rect::MakeLTRB(10.0f, 10.0f, 40.0f, 16.0f));
EXPECT_EQ(rse.GetRadii().top_left, Size(0.5f, 1.0f));
EXPECT_EQ(rse.GetRadii().top_right, Size(1.5f, 2.0f));
EXPECT_EQ(rse.GetRadii().bottom_left, Size(2.5f, 3.0f));
EXPECT_EQ(rse.GetRadii().bottom_right, Size(3.5f, 4.0f));
}
TEST(RoundSuperellipseTest, Shift) {
RoundSuperellipse rse = RoundSuperellipse::MakeRectRadii(
Rect::MakeXYWH(10.0f, 10.0f, 30.0f, 30.0f),
{
.top_left = Size(1.0f, 2.0f),
.top_right = Size(3.0f, 4.0f),
.bottom_left = Size(5.0f, 6.0f),
.bottom_right = Size(7.0f, 8.0f),
});
RoundSuperellipse shifted = rse.Shift(5.0, 6.0);
EXPECT_FALSE(shifted.IsEmpty());
EXPECT_FALSE(shifted.IsRect());
EXPECT_FALSE(shifted.IsOval());
EXPECT_TRUE(shifted.IsFinite());
EXPECT_FALSE(shifted.GetBounds().IsEmpty());
EXPECT_EQ(shifted.GetBounds(), Rect::MakeLTRB(15.0f, 16.0f, 45.0f, 46.0f));
EXPECT_EQ(shifted.GetRadii().top_left, Size(1.0f, 2.0f));
EXPECT_EQ(shifted.GetRadii().top_right, Size(3.0f, 4.0f));
EXPECT_EQ(shifted.GetRadii().bottom_left, Size(5.0f, 6.0f));
EXPECT_EQ(shifted.GetRadii().bottom_right, Size(7.0f, 8.0f));
EXPECT_EQ(shifted, RoundSuperellipse::MakeRectRadii(
Rect::MakeXYWH(15.0f, 16.0f, 30.0f, 30.0f),
{
.top_left = Size(1.0f, 2.0f),
.top_right = Size(3.0f, 4.0f),
.bottom_left = Size(5.0f, 6.0f),
.bottom_right = Size(7.0f, 8.0f),
}));
}
TEST(RoundSuperellipseTest, ExpandScalar) {
RoundSuperellipse rse = RoundSuperellipse::MakeRectRadii(
Rect::MakeXYWH(10.0f, 10.0f, 30.0f, 30.0f),
{
.top_left = Size(1.0f, 2.0f),
.top_right = Size(3.0f, 4.0f),
.bottom_left = Size(5.0f, 6.0f),
.bottom_right = Size(7.0f, 8.0f),
});
RoundSuperellipse expanded = rse.Expand(5.0);
EXPECT_FALSE(expanded.IsEmpty());
EXPECT_FALSE(expanded.IsRect());
EXPECT_FALSE(expanded.IsOval());
EXPECT_TRUE(expanded.IsFinite());
EXPECT_FALSE(expanded.GetBounds().IsEmpty());
EXPECT_EQ(expanded.GetBounds(), Rect::MakeLTRB(5.0f, 5.0f, 45.0f, 45.0f));
EXPECT_EQ(expanded.GetRadii().top_left, Size(1.0f, 2.0f));
EXPECT_EQ(expanded.GetRadii().top_right, Size(3.0f, 4.0f));
EXPECT_EQ(expanded.GetRadii().bottom_left, Size(5.0f, 6.0f));
EXPECT_EQ(expanded.GetRadii().bottom_right, Size(7.0f, 8.0f));
EXPECT_EQ(expanded, RoundSuperellipse::MakeRectRadii(
Rect::MakeXYWH(5.0f, 5.0f, 40.0f, 40.0f),
{
.top_left = Size(1.0f, 2.0f),
.top_right = Size(3.0f, 4.0f),
.bottom_left = Size(5.0f, 6.0f),
.bottom_right = Size(7.0f, 8.0f),
}));
}
TEST(RoundSuperellipseTest, ExpandTwoScalars) {
RoundSuperellipse rse = RoundSuperellipse::MakeRectRadii(
Rect::MakeXYWH(10.0f, 10.0f, 30.0f, 30.0f),
{
.top_left = Size(1.0f, 2.0f),
.top_right = Size(3.0f, 4.0f),
.bottom_left = Size(5.0f, 6.0f),
.bottom_right = Size(7.0f, 8.0f),
});
RoundSuperellipse expanded = rse.Expand(5.0, 6.0);
EXPECT_FALSE(expanded.IsEmpty());
EXPECT_FALSE(expanded.IsRect());
EXPECT_FALSE(expanded.IsOval());
EXPECT_TRUE(expanded.IsFinite());
EXPECT_FALSE(expanded.GetBounds().IsEmpty());
EXPECT_EQ(expanded.GetBounds(), Rect::MakeLTRB(5.0f, 4.0f, 45.0f, 46.0f));
EXPECT_EQ(expanded.GetRadii().top_left, Size(1.0f, 2.0f));
EXPECT_EQ(expanded.GetRadii().top_right, Size(3.0f, 4.0f));
EXPECT_EQ(expanded.GetRadii().bottom_left, Size(5.0f, 6.0f));
EXPECT_EQ(expanded.GetRadii().bottom_right, Size(7.0f, 8.0f));
EXPECT_EQ(expanded, RoundSuperellipse::MakeRectRadii(
Rect::MakeXYWH(5.0f, 4.0f, 40.0f, 42.0f),
{
.top_left = Size(1.0f, 2.0f),
.top_right = Size(3.0f, 4.0f),
.bottom_left = Size(5.0f, 6.0f),
.bottom_right = Size(7.0f, 8.0f),
}));
}
TEST(RoundSuperellipseTest, ExpandFourScalars) {
RoundSuperellipse rse = RoundSuperellipse::MakeRectRadii(
Rect::MakeXYWH(10.0f, 10.0f, 30.0f, 30.0f),
{
.top_left = Size(1.0f, 2.0f),
.top_right = Size(3.0f, 4.0f),
.bottom_left = Size(5.0f, 6.0f),
.bottom_right = Size(7.0f, 8.0f),
});
RoundSuperellipse expanded = rse.Expand(5.0, 6.0, 7.0, 8.0);
EXPECT_FALSE(expanded.IsEmpty());
EXPECT_FALSE(expanded.IsRect());
EXPECT_FALSE(expanded.IsOval());
EXPECT_TRUE(expanded.IsFinite());
EXPECT_FALSE(expanded.GetBounds().IsEmpty());
EXPECT_EQ(expanded.GetBounds(), Rect::MakeLTRB(5.0f, 4.0f, 47.0f, 48.0f));
EXPECT_EQ(expanded.GetRadii().top_left, Size(1.0f, 2.0f));
EXPECT_EQ(expanded.GetRadii().top_right, Size(3.0f, 4.0f));
EXPECT_EQ(expanded.GetRadii().bottom_left, Size(5.0f, 6.0f));
EXPECT_EQ(expanded.GetRadii().bottom_right, Size(7.0f, 8.0f));
EXPECT_EQ(expanded, RoundSuperellipse::MakeRectRadii(
Rect::MakeXYWH(5.0f, 4.0f, 42.0f, 44.0f),
{
.top_left = Size(1.0f, 2.0f),
.top_right = Size(3.0f, 4.0f),
.bottom_left = Size(5.0f, 6.0f),
.bottom_right = Size(7.0f, 8.0f),
}));
}
TEST(RoundSuperellipseTest, ContractScalar) {
RoundSuperellipse rse = RoundSuperellipse::MakeRectRadii(
Rect::MakeXYWH(10.0f, 10.0f, 30.0f, 30.0f),
{
.top_left = Size(1.0f, 2.0f),
.top_right = Size(3.0f, 4.0f),
.bottom_left = Size(5.0f, 6.0f),
.bottom_right = Size(7.0f, 8.0f),
});
RoundSuperellipse expanded = rse.Expand(-2.0);
EXPECT_FALSE(expanded.IsEmpty());
EXPECT_FALSE(expanded.IsRect());
EXPECT_FALSE(expanded.IsOval());
EXPECT_TRUE(expanded.IsFinite());
EXPECT_FALSE(expanded.GetBounds().IsEmpty());
EXPECT_EQ(expanded.GetBounds(), Rect::MakeLTRB(12.0f, 12.0f, 38.0f, 38.0f));
EXPECT_EQ(expanded.GetRadii().top_left, Size(1.0f, 2.0f));
EXPECT_EQ(expanded.GetRadii().top_right, Size(3.0f, 4.0f));
EXPECT_EQ(expanded.GetRadii().bottom_left, Size(5.0f, 6.0f));
EXPECT_EQ(expanded.GetRadii().bottom_right, Size(7.0f, 8.0f));
EXPECT_EQ(expanded, RoundSuperellipse::MakeRectRadii(
Rect::MakeXYWH(12.0f, 12.0f, 26.0f, 26.0f),
{
.top_left = Size(1.0f, 2.0f),
.top_right = Size(3.0f, 4.0f),
.bottom_left = Size(5.0f, 6.0f),
.bottom_right = Size(7.0f, 8.0f),
}));
}
TEST(RoundSuperellipseTest, ContractTwoScalars) {
RoundSuperellipse rse = RoundSuperellipse::MakeRectRadii(
Rect::MakeXYWH(10.0f, 10.0f, 30.0f, 30.0f),
{
.top_left = Size(1.0f, 2.0f),
.top_right = Size(3.0f, 4.0f),
.bottom_left = Size(5.0f, 6.0f),
.bottom_right = Size(7.0f, 8.0f),
});
RoundSuperellipse expanded = rse.Expand(-1.0, -2.0);
EXPECT_FALSE(expanded.IsEmpty());
EXPECT_FALSE(expanded.IsRect());
EXPECT_FALSE(expanded.IsOval());
EXPECT_TRUE(expanded.IsFinite());
EXPECT_FALSE(expanded.GetBounds().IsEmpty());
EXPECT_EQ(expanded.GetBounds(), Rect::MakeLTRB(11.0f, 12.0f, 39.0f, 38.0f));
EXPECT_EQ(expanded.GetRadii().top_left, Size(1.0f, 2.0f));
EXPECT_EQ(expanded.GetRadii().top_right, Size(3.0f, 4.0f));
EXPECT_EQ(expanded.GetRadii().bottom_left, Size(5.0f, 6.0f));
EXPECT_EQ(expanded.GetRadii().bottom_right, Size(7.0f, 8.0f));
EXPECT_EQ(expanded, RoundSuperellipse::MakeRectRadii(
Rect::MakeXYWH(11.0f, 12.0f, 28.0f, 26.0f),
{
.top_left = Size(1.0f, 2.0f),
.top_right = Size(3.0f, 4.0f),
.bottom_left = Size(5.0f, 6.0f),
.bottom_right = Size(7.0f, 8.0f),
}));
}
TEST(RoundSuperellipseTest, ContractFourScalars) {
RoundSuperellipse rse = RoundSuperellipse::MakeRectRadii(
Rect::MakeXYWH(10.0f, 10.0f, 30.0f, 30.0f),
{
.top_left = Size(1.0f, 2.0f),
.top_right = Size(3.0f, 4.0f),
.bottom_left = Size(5.0f, 6.0f),
.bottom_right = Size(7.0f, 8.0f),
});
RoundSuperellipse expanded = rse.Expand(-1.0, -1.5, -2.0, -2.5);
EXPECT_FALSE(expanded.IsEmpty());
EXPECT_FALSE(expanded.IsRect());
EXPECT_FALSE(expanded.IsOval());
EXPECT_TRUE(expanded.IsFinite());
EXPECT_FALSE(expanded.GetBounds().IsEmpty());
EXPECT_EQ(expanded.GetBounds(), Rect::MakeLTRB(11.0f, 11.5f, 38.0f, 37.5f));
EXPECT_EQ(expanded.GetRadii().top_left, Size(1.0f, 2.0f));
EXPECT_EQ(expanded.GetRadii().top_right, Size(3.0f, 4.0f));
EXPECT_EQ(expanded.GetRadii().bottom_left, Size(5.0f, 6.0f));
EXPECT_EQ(expanded.GetRadii().bottom_right, Size(7.0f, 8.0f));
EXPECT_EQ(expanded, RoundSuperellipse::MakeRectRadii(
Rect::MakeXYWH(11.0f, 11.5f, 27.0f, 26.0f),
{
.top_left = Size(1.0f, 2.0f),
.top_right = Size(3.0f, 4.0f),
.bottom_left = Size(5.0f, 6.0f),
.bottom_right = Size(7.0f, 8.0f),
}));
}
TEST(RoundSuperellipseTest, ContractAndRequireRadiiAdjustment) {
RoundSuperellipse rse = RoundSuperellipse::MakeRectRadii(
Rect::MakeXYWH(10.0f, 10.0f, 30.0f, 30.0f),
{
.top_left = Size(1.0f, 2.0f),
.top_right = Size(3.0f, 4.0f),
.bottom_left = Size(5.0f, 6.0f),
.bottom_right = Size(7.0f, 8.0f),
});
RoundSuperellipse expanded = rse.Expand(-12.0);
// Largest sum of paired radii sizes are the bottom and right edges
// both of which sum to 12
// Rect was 30x30 reduced by 12 on all sides leaving only 6x6, so all
// radii are scaled by half to avoid overflowing the contracted rect
EXPECT_FALSE(expanded.IsEmpty());
EXPECT_FALSE(expanded.IsRect());
EXPECT_FALSE(expanded.IsOval());
EXPECT_TRUE(expanded.IsFinite());
EXPECT_FALSE(expanded.GetBounds().IsEmpty());
EXPECT_EQ(expanded.GetBounds(), Rect::MakeLTRB(22.0f, 22.0f, 28.0f, 28.0f));
EXPECT_EQ(expanded.GetRadii().top_left, Size(0.5f, 1.0f));
EXPECT_EQ(expanded.GetRadii().top_right, Size(1.5f, 2.0f));
EXPECT_EQ(expanded.GetRadii().bottom_left, Size(2.5f, 3.0f));
EXPECT_EQ(expanded.GetRadii().bottom_right, Size(3.5f, 4.0f));
// In this test, the MakeRectRadii constructor will make the same
// adjustment to the radii that the Expand method applied.
EXPECT_EQ(expanded, RoundSuperellipse::MakeRectRadii(
Rect::MakeXYWH(22.0f, 22.0f, 6.0f, 6.0f),
{
.top_left = Size(1.0f, 2.0f),
.top_right = Size(3.0f, 4.0f),
.bottom_left = Size(5.0f, 6.0f),
.bottom_right = Size(7.0f, 8.0f),
}));
// In this test, the arguments to the constructor supply the correctly
// adjusted radii (though there is no real way to tell other than
// the result is the same).
EXPECT_EQ(expanded, RoundSuperellipse::MakeRectRadii(
Rect::MakeXYWH(22.0f, 22.0f, 6.0f, 6.0f),
{
.top_left = Size(0.5f, 1.0f),
.top_right = Size(1.5f, 2.0f),
.bottom_left = Size(2.5f, 3.0f),
.bottom_right = Size(3.5f, 4.0f),
}));
}
TEST(RoundSuperellipseTest, NoCornerRoundSuperellipseContains) {
Rect bounds = Rect::MakeLTRB(-50.0f, -50.0f, 50.0f, 50.0f);
// RRect of bounds with no corners contains corners just barely
auto no_corners = RoundSuperellipse::MakeRectRadii(
bounds, RoundingRadii::MakeRadii({0.0f, 0.0f}));
EXPECT_TRUE(no_corners.Contains({-50, -50}));
// Rectangles have half-in, half-out containment so we need
// to be careful about testing containment of right/bottom corners.
EXPECT_TRUE(no_corners.Contains({-50, 49.99}));
EXPECT_TRUE(no_corners.Contains({49.99, -50}));
EXPECT_TRUE(no_corners.Contains({49.99, 49.99}));
EXPECT_FALSE(no_corners.Contains({-50.01, -50}));
EXPECT_FALSE(no_corners.Contains({-50, -50.01}));
EXPECT_FALSE(no_corners.Contains({-50.01, 50}));
EXPECT_FALSE(no_corners.Contains({-50, 50.01}));
EXPECT_FALSE(no_corners.Contains({50.01, -50}));
EXPECT_FALSE(no_corners.Contains({50, -50.01}));
EXPECT_FALSE(no_corners.Contains({50.01, 50}));
EXPECT_FALSE(no_corners.Contains({50, 50.01}));
}
TEST(RoundSuperellipseTest, TinyCornerContains) {
Rect bounds = Rect::MakeLTRB(-50.0f, -50.0f, 50.0f, 50.0f);
// RRect of bounds with even the tiniest corners does not contain corners
auto tiny_corners = RoundSuperellipse::MakeRectRadii(
bounds, RoundingRadii::MakeRadii({0.01f, 0.01f}));
EXPECT_FALSE(tiny_corners.Contains({-50, -50}));
EXPECT_FALSE(tiny_corners.Contains({-50, 50}));
EXPECT_FALSE(tiny_corners.Contains({50, -50}));
EXPECT_FALSE(tiny_corners.Contains({50, 50}));
}
TEST(RoundSuperellipseTest, UniformSquareContains) {
Rect bounds = Rect::MakeLTRB(-50.0f, -50.0f, 50.0f, 50.0f);
auto rr = RoundSuperellipse::MakeRectRadii(
bounds, RoundingRadii::MakeRadii({5.0f, 5.0f}));
#define CHECK_POINT_AND_MIRRORS(p) \
CHECK_POINT_WITH_OFFSET(rr, (p), Point(0.02, 0.02)); \
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_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
#undef CHECK_POINT_AND_MIRRORS
}
TEST(RoundSuperellipseTest, UniformEllipticalContains) {
Rect bounds = Rect::MakeLTRB(-50.0f, -50.0f, 50.0f, 50.0f);
auto rr = RoundSuperellipse::MakeRectRadii(
bounds, RoundingRadii::MakeRadii({5.0f, 10.0f}));
#define CHECK_POINT_AND_MIRRORS(p) \
CHECK_POINT_WITH_OFFSET(rr, (p), Point(0.02, 0.02)); \
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_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(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.995, 0)); // Right
#undef CHECK_POINT_AND_MIRRORS
}
TEST(RoundSuperellipseTest, UniformRectangularContains) {
// The bounds is not centered at the origin and has unequal height and width.
Rect bounds = Rect::MakeLTRB(0.0f, 0.0f, 50.0f, 100.0f);
auto rr = RoundSuperellipse::MakeRectRadii(
bounds, RoundingRadii::MakeRadii({23.0f, 30.0f}));
Point center = bounds.GetCenter();
#define CHECK_POINT_AND_MIRRORS(p) \
CHECK_POINT_WITH_OFFSET(rr, (p - center) * Point(1, 1) + center, \
Point(0.02, 0.02)); \
CHECK_POINT_WITH_OFFSET(rr, (p - center) * Point(1, -1) + center, \
Point(0.02, -0.02)); \
CHECK_POINT_WITH_OFFSET(rr, (p - center) * Point(-1, 1) + center, \
Point(-0.02, 0.02)); \
CHECK_POINT_WITH_OFFSET(rr, (p - center) * Point(-1, -1) + center, \
Point(-0.02, -0.02));
CHECK_POINT_AND_MIRRORS(Point(24.99, 99.99)); // Bottom mid edge
CHECK_POINT_AND_MIRRORS(Point(29.99, 99.64));
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(49.99, 49.99)); // Right mid edge
#undef CHECK_POINT_AND_MIRRORS
}
TEST(RoundSuperellipseTest, SlimDiagnalContains) {
// This shape has large radii on one diagnal and tiny radii on the other,
// resulting in a almond-like shape placed diagnally (NW to SE).
Rect bounds = Rect::MakeLTRB(-50.0f, -50.0f, 50.0f, 50.0f);
auto rr = RoundSuperellipse::MakeRectRadii(
bounds, {
.top_left = Size(1.0, 1.0),
.top_right = Size(99.0, 99.0),
.bottom_left = Size(99.0, 99.0),
.bottom_right = Size(1.0, 1.0),
});
EXPECT_TRUE(rr.Contains(Point{0, 0}));
EXPECT_FALSE(rr.Contains(Point{-49.999, -49.999}));
EXPECT_FALSE(rr.Contains(Point{-49.999, 49.999}));
EXPECT_FALSE(rr.Contains(Point{49.999, 49.999}));
EXPECT_FALSE(rr.Contains(Point{49.999, -49.999}));
// The pointy ends at the NE and SW corners
CHECK_POINT_WITH_OFFSET(rr, Point(-49.70, -49.70), Point(-0.02, -0.02));
CHECK_POINT_WITH_OFFSET(rr, Point(49.70, 49.70), Point(0.02, 0.02));
// Checks two points symmetrical to the origin.
#define CHECK_DIAGNAL_POINTS(p) \
CHECK_POINT_WITH_OFFSET(rr, (p), Point(0.02, -0.02)); \
CHECK_POINT_WITH_OFFSET(rr, (p) * Point(-1, -1), Point(-0.02, 0.02));
// A few other points along the edge
CHECK_DIAGNAL_POINTS(Point(-40.0, -49.59));
CHECK_DIAGNAL_POINTS(Point(-20.0, -45.64));
CHECK_DIAGNAL_POINTS(Point(0.0, -37.01));
CHECK_DIAGNAL_POINTS(Point(20.0, -21.96));
CHECK_DIAGNAL_POINTS(Point(21.05, -20.92));
CHECK_DIAGNAL_POINTS(Point(40.0, 5.68));
#undef CHECK_POINT_AND_MIRRORS
}
} // namespace testing
} // namespace impeller

View File

@ -0,0 +1,321 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "gtest/gtest.h"
#include "flutter/impeller/geometry/rounding_radii.h"
#include "flutter/impeller/geometry/geometry_asserts.h"
namespace impeller {
namespace testing {
TEST(RoudingRadiiTest, RoundingRadiiEmptyDeclaration) {
RoundingRadii radii;
EXPECT_TRUE(radii.AreAllCornersEmpty());
EXPECT_TRUE(radii.AreAllCornersSame());
EXPECT_TRUE(radii.IsFinite());
EXPECT_EQ(radii.top_left, Size());
EXPECT_EQ(radii.top_right, Size());
EXPECT_EQ(radii.bottom_left, Size());
EXPECT_EQ(radii.bottom_right, Size());
EXPECT_EQ(radii.top_left.width, 0.0f);
EXPECT_EQ(radii.top_left.height, 0.0f);
EXPECT_EQ(radii.top_right.width, 0.0f);
EXPECT_EQ(radii.top_right.height, 0.0f);
EXPECT_EQ(radii.bottom_left.width, 0.0f);
EXPECT_EQ(radii.bottom_left.height, 0.0f);
EXPECT_EQ(radii.bottom_right.width, 0.0f);
EXPECT_EQ(radii.bottom_right.height, 0.0f);
}
TEST(RoudingRadiiTest, RoundingRadiiDefaultConstructor) {
RoundingRadii radii = RoundingRadii();
EXPECT_TRUE(radii.AreAllCornersEmpty());
EXPECT_TRUE(radii.AreAllCornersSame());
EXPECT_TRUE(radii.IsFinite());
EXPECT_EQ(radii.top_left, Size());
EXPECT_EQ(radii.top_right, Size());
EXPECT_EQ(radii.bottom_left, Size());
EXPECT_EQ(radii.bottom_right, Size());
}
TEST(RoudingRadiiTest, RoundingRadiiScalarConstructor) {
RoundingRadii radii = RoundingRadii::MakeRadius(5.0f);
EXPECT_FALSE(radii.AreAllCornersEmpty());
EXPECT_TRUE(radii.AreAllCornersSame());
EXPECT_TRUE(radii.IsFinite());
EXPECT_EQ(radii.top_left, Size(5.0f, 5.0f));
EXPECT_EQ(radii.top_right, Size(5.0f, 5.0f));
EXPECT_EQ(radii.bottom_left, Size(5.0f, 5.0f));
EXPECT_EQ(radii.bottom_right, Size(5.0f, 5.0f));
}
TEST(RoudingRadiiTest, RoundingRadiiEmptyScalarConstructor) {
RoundingRadii radii = RoundingRadii::MakeRadius(-5.0f);
EXPECT_TRUE(radii.AreAllCornersEmpty());
EXPECT_TRUE(radii.AreAllCornersSame());
EXPECT_TRUE(radii.IsFinite());
EXPECT_EQ(radii.top_left, Size(-5.0f, -5.0f));
EXPECT_EQ(radii.top_right, Size(-5.0f, -5.0f));
EXPECT_EQ(radii.bottom_left, Size(-5.0f, -5.0f));
EXPECT_EQ(radii.bottom_right, Size(-5.0f, -5.0f));
}
TEST(RoudingRadiiTest, RoundingRadiiSizeConstructor) {
RoundingRadii radii = RoundingRadii::MakeRadii(Size(5.0f, 6.0f));
EXPECT_FALSE(radii.AreAllCornersEmpty());
EXPECT_TRUE(radii.AreAllCornersSame());
EXPECT_TRUE(radii.IsFinite());
EXPECT_EQ(radii.top_left, Size(5.0f, 6.0f));
EXPECT_EQ(radii.top_right, Size(5.0f, 6.0f));
EXPECT_EQ(radii.bottom_left, Size(5.0f, 6.0f));
EXPECT_EQ(radii.bottom_right, Size(5.0f, 6.0f));
}
TEST(RoudingRadiiTest, RoundingRadiiEmptySizeConstructor) {
{
RoundingRadii radii = RoundingRadii::MakeRadii(Size(-5.0f, 6.0f));
EXPECT_TRUE(radii.AreAllCornersEmpty());
EXPECT_TRUE(radii.AreAllCornersSame());
EXPECT_TRUE(radii.IsFinite());
EXPECT_EQ(radii.top_left, Size(-5.0f, 6.0f));
EXPECT_EQ(radii.top_right, Size(-5.0f, 6.0f));
EXPECT_EQ(radii.bottom_left, Size(-5.0f, 6.0f));
EXPECT_EQ(radii.bottom_right, Size(-5.0f, 6.0f));
}
{
RoundingRadii radii = RoundingRadii::MakeRadii(Size(5.0f, -6.0f));
EXPECT_TRUE(radii.AreAllCornersEmpty());
EXPECT_TRUE(radii.AreAllCornersSame());
EXPECT_TRUE(radii.IsFinite());
EXPECT_EQ(radii.top_left, Size(5.0f, -6.0f));
EXPECT_EQ(radii.top_right, Size(5.0f, -6.0f));
EXPECT_EQ(radii.bottom_left, Size(5.0f, -6.0f));
EXPECT_EQ(radii.bottom_right, Size(5.0f, -6.0f));
}
}
TEST(RoudingRadiiTest, RoundingRadiiNamedSizesConstructor) {
RoundingRadii radii = {
.top_left = Size(5.0f, 5.5f),
.top_right = Size(6.0f, 6.5f),
.bottom_left = Size(7.0f, 7.5f),
.bottom_right = Size(8.0f, 8.5f),
};
EXPECT_FALSE(radii.AreAllCornersEmpty());
EXPECT_FALSE(radii.AreAllCornersSame());
EXPECT_TRUE(radii.IsFinite());
EXPECT_EQ(radii.top_left, Size(5.0f, 5.5f));
EXPECT_EQ(radii.top_right, Size(6.0f, 6.5f));
EXPECT_EQ(radii.bottom_left, Size(7.0f, 7.5f));
EXPECT_EQ(radii.bottom_right, Size(8.0f, 8.5f));
}
TEST(RoudingRadiiTest, RoundingRadiiPartialNamedSizesConstructor) {
{
RoundingRadii radii = {
.top_left = Size(5.0f, 5.5f),
};
EXPECT_FALSE(radii.AreAllCornersEmpty());
EXPECT_FALSE(radii.AreAllCornersSame());
EXPECT_TRUE(radii.IsFinite());
EXPECT_EQ(radii.top_left, Size(5.0f, 5.5f));
EXPECT_EQ(radii.top_right, Size());
EXPECT_EQ(radii.bottom_left, Size());
EXPECT_EQ(radii.bottom_right, Size());
}
{
RoundingRadii radii = {
.top_right = Size(6.0f, 6.5f),
};
EXPECT_FALSE(radii.AreAllCornersEmpty());
EXPECT_FALSE(radii.AreAllCornersSame());
EXPECT_TRUE(radii.IsFinite());
EXPECT_EQ(radii.top_left, Size());
EXPECT_EQ(radii.top_right, Size(6.0f, 6.5f));
EXPECT_EQ(radii.bottom_left, Size());
EXPECT_EQ(radii.bottom_right, Size());
}
{
RoundingRadii radii = {
.bottom_left = Size(7.0f, 7.5f),
};
EXPECT_FALSE(radii.AreAllCornersEmpty());
EXPECT_FALSE(radii.AreAllCornersSame());
EXPECT_TRUE(radii.IsFinite());
EXPECT_EQ(radii.top_left, Size());
EXPECT_EQ(radii.top_right, Size());
EXPECT_EQ(radii.bottom_left, Size(7.0f, 7.5f));
EXPECT_EQ(radii.bottom_right, Size());
}
{
RoundingRadii radii = {
.bottom_right = Size(8.0f, 8.5f),
};
EXPECT_FALSE(radii.AreAllCornersEmpty());
EXPECT_FALSE(radii.AreAllCornersSame());
EXPECT_TRUE(radii.IsFinite());
EXPECT_EQ(radii.top_left, Size());
EXPECT_EQ(radii.top_right, Size());
EXPECT_EQ(radii.bottom_left, Size());
EXPECT_EQ(radii.bottom_right, Size(8.0f, 8.5f));
}
}
TEST(RoudingRadiiTest, RoundingRadiiMultiply) {
RoundingRadii radii = {
.top_left = Size(5.0f, 5.5f),
.top_right = Size(6.0f, 6.5f),
.bottom_left = Size(7.0f, 7.5f),
.bottom_right = Size(8.0f, 8.5f),
};
RoundingRadii doubled = radii * 2.0f;
EXPECT_FALSE(doubled.AreAllCornersEmpty());
EXPECT_FALSE(doubled.AreAllCornersSame());
EXPECT_TRUE(doubled.IsFinite());
EXPECT_EQ(doubled.top_left, Size(10.0f, 11.0f));
EXPECT_EQ(doubled.top_right, Size(12.0f, 13.0f));
EXPECT_EQ(doubled.bottom_left, Size(14.0f, 15.0f));
EXPECT_EQ(doubled.bottom_right, Size(16.0f, 17.0f));
}
TEST(RoudingRadiiTest, RoundingRadiiEquals) {
RoundingRadii radii = {
.top_left = Size(5.0f, 5.5f),
.top_right = Size(6.0f, 6.5f),
.bottom_left = Size(7.0f, 7.5f),
.bottom_right = Size(8.0f, 8.5f),
};
RoundingRadii other = {
.top_left = Size(5.0f, 5.5f),
.top_right = Size(6.0f, 6.5f),
.bottom_left = Size(7.0f, 7.5f),
.bottom_right = Size(8.0f, 8.5f),
};
EXPECT_EQ(radii, other);
}
TEST(RoudingRadiiTest, RoundingRadiiNotEquals) {
const RoundingRadii radii = {
.top_left = Size(5.0f, 5.5f),
.top_right = Size(6.0f, 6.5f),
.bottom_left = Size(7.0f, 7.5f),
.bottom_right = Size(8.0f, 8.5f),
};
{
RoundingRadii different = radii;
different.top_left.width = 100.0f;
EXPECT_NE(different, radii);
}
{
RoundingRadii different = radii;
different.top_left.height = 100.0f;
EXPECT_NE(different, radii);
}
{
RoundingRadii different = radii;
different.top_right.width = 100.0f;
EXPECT_NE(different, radii);
}
{
RoundingRadii different = radii;
different.top_right.height = 100.0f;
EXPECT_NE(different, radii);
}
{
RoundingRadii different = radii;
different.bottom_left.width = 100.0f;
EXPECT_NE(different, radii);
}
{
RoundingRadii different = radii;
different.bottom_left.height = 100.0f;
EXPECT_NE(different, radii);
}
{
RoundingRadii different = radii;
different.bottom_right.width = 100.0f;
EXPECT_NE(different, radii);
}
{
RoundingRadii different = radii;
different.bottom_right.height = 100.0f;
EXPECT_NE(different, radii);
}
}
TEST(RoudingRadiiTest, RoundingRadiiCornersSameTolerance) {
RoundingRadii radii{
.top_left = {10, 20},
.top_right = {10.01, 20.01},
.bottom_left = {9.99, 19.99},
.bottom_right = {9.99, 20.01},
};
EXPECT_TRUE(radii.AreAllCornersSame(.02));
{
RoundingRadii different = radii;
different.top_left.width = 10.03;
EXPECT_FALSE(different.AreAllCornersSame(.02));
}
{
RoundingRadii different = radii;
different.top_left.height = 20.03;
EXPECT_FALSE(different.AreAllCornersSame(.02));
}
{
RoundingRadii different = radii;
different.top_right.width = 10.03;
EXPECT_FALSE(different.AreAllCornersSame(.02));
}
{
RoundingRadii different = radii;
different.top_right.height = 20.03;
EXPECT_FALSE(different.AreAllCornersSame(.02));
}
{
RoundingRadii different = radii;
different.bottom_left.width = 9.97;
EXPECT_FALSE(different.AreAllCornersSame(.02));
}
{
RoundingRadii different = radii;
different.bottom_left.height = 19.97;
EXPECT_FALSE(different.AreAllCornersSame(.02));
}
{
RoundingRadii different = radii;
different.bottom_right.width = 9.97;
EXPECT_FALSE(different.AreAllCornersSame(.02));
}
{
RoundingRadii different = radii;
different.bottom_right.height = 20.03;
EXPECT_FALSE(different.AreAllCornersSame(.02));
}
}
} // namespace testing
} // namespace impeller