diff --git a/engine/src/flutter/ci/licenses_golden/excluded_files b/engine/src/flutter/ci/licenses_golden/excluded_files index 5dba32b5fb..1ed250e2a7 100644 --- a/engine/src/flutter/ci/licenses_golden/excluded_files +++ b/engine/src/flutter/ci/licenses_golden/excluded_files @@ -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 diff --git a/engine/src/flutter/ci/licenses_golden/licenses_flutter b/engine/src/flutter/ci/licenses_golden/licenses_flutter index 0843940136..8357d60e6e 100644 --- a/engine/src/flutter/ci/licenses_golden/licenses_flutter +++ b/engine/src/flutter/ci/licenses_golden/licenses_flutter @@ -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 diff --git a/engine/src/flutter/impeller/entity/entity_unittests.cc b/engine/src/flutter/impeller/entity/entity_unittests.cc index 4e3017a0ac..a14dd87d35 100644 --- a/engine/src/flutter/impeller/entity/entity_unittests.cc +++ b/engine/src/flutter/impeller/entity/entity_unittests.cc @@ -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 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 geom; + if (style_index == 0) { + geom = std::make_unique( + 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(); - std::unique_ptr geom = - std::make_unique( - Rect::MakeOriginSize({center[0], center[1]}, {size[0], size[1]}), - radii); contents->SetColor(Color::Red()); contents->SetGeometry(geom.get()); diff --git a/engine/src/flutter/impeller/entity/geometry/round_superellipse_geometry.cc b/engine/src/flutter/impeller/entity/geometry/round_superellipse_geometry.cc index 55a703dfb7..fddaea410f 100644 --- a/engine/src/flutter/impeller/entity/geometry/round_superellipse_geometry.cc +++ b/engine/src/flutter/impeller/entity/geometry/round_superellipse_geometry.cc @@ -6,6 +6,7 @@ #include #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((ratio - kMinRatio) / kRatioStep, 0, kNumRecords - 1); - size_t left = std::clamp(static_cast(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(bounds_.GetCenter(), cache); auto& t = std::get(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(cache, kMaxQuadSize); auto& t = std::get(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(); diff --git a/engine/src/flutter/impeller/geometry/BUILD.gn b/engine/src/flutter/impeller/geometry/BUILD.gn index 9f2d831846..4ecde22748 100644 --- a/engine/src/flutter/impeller/geometry/BUILD.gn +++ b/engine/src/flutter/impeller/geometry/BUILD.gn @@ -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", diff --git a/engine/src/flutter/impeller/geometry/geometry_benchmarks.cc b/engine/src/flutter/impeller/geometry/geometry_benchmarks.cc index d59650be5c..fa3128c605 100644 --- a/engine/src/flutter/impeller/geometry/geometry_benchmarks.cc +++ b/engine/src/flutter/impeller/geometry/geometry_benchmarks.cc @@ -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 // diff --git a/engine/src/flutter/impeller/geometry/path_builder.cc b/engine/src/flutter/impeller/geometry/path_builder.cc index a42a3adfd0..4923c54371 100644 --- a/engine/src/flutter/impeller/geometry/path_builder.cc +++ b/engine/src/flutter/impeller/geometry/path_builder.cc @@ -4,12 +4,170 @@ #include "path_builder.h" +#include #include #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 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 factors = SuperellipseBezierFactors(param.se_n); + + return std::array{ + start, start + start_tangent * factors[0] * param.se_a, + end + end_tangent * factors[1] * param.se_a, end}; + }; + + std::array 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{ + 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 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((n - kMinN) * kNStepInverse, 0, kNumRecords - 1); + size_t left = std::clamp(static_cast(std::floor(steps)), 0, + kNumRecords - 2); + Scalar frac = steps - left; + + return std::array{(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 = ¶m.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; diff --git a/engine/src/flutter/impeller/geometry/path_builder.h b/engine/src/flutter/impeller/geometry/path_builder.h index 0184687cb6..78a570b2f6 100644 --- a/engine/src/flutter/impeller/geometry/path_builder.h +++ b/engine/src/flutter/impeller/geometry/path_builder.h @@ -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: diff --git a/engine/src/flutter/impeller/geometry/path_unittests.cc b/engine/src/flutter/impeller/geometry/path_unittests.cc index bef20f68c6..531a43f82b 100644 --- a/engine/src/flutter/impeller/geometry/path_unittests.cc +++ b/engine/src/flutter/impeller/geometry/path_unittests.cc @@ -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); diff --git a/engine/src/flutter/impeller/geometry/round_rect_unittests.cc b/engine/src/flutter/impeller/geometry/round_rect_unittests.cc index 30c6c0d024..b9e3d12127 100644 --- a/engine/src/flutter/impeller/geometry/round_rect_unittests.cc +++ b/engine/src/flutter/impeller/geometry/round_rect_unittests.cc @@ -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; diff --git a/engine/src/flutter/impeller/geometry/round_superellipse.cc b/engine/src/flutter/impeller/geometry/round_superellipse.cc new file mode 100644 index 0000000000..8c74c35fc4 --- /dev/null +++ b/engine/src/flutter/impeller/geometry/round_superellipse.cc @@ -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 diff --git a/engine/src/flutter/impeller/geometry/round_superellipse.h b/engine/src/flutter/impeller/geometry/round_superellipse.h new file mode 100644 index 0000000000..d18044b192 --- /dev/null +++ b/engine/src/flutter/impeller/geometry/round_superellipse.h @@ -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_ diff --git a/engine/src/flutter/impeller/geometry/round_superellipse_param.cc b/engine/src/flutter/impeller/geometry/round_superellipse_param.cc new file mode 100644 index 0000000000..26c5e84c26 --- /dev/null +++ b/engine/src/flutter/impeller/geometry/round_superellipse_param.cc @@ -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((ratio - kMinRatio) * kRatioStepInverse, 0, + kNumRecords - 1); + size_t left = std::clamp(static_cast(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 diff --git a/engine/src/flutter/impeller/geometry/round_superellipse_param.h b/engine/src/flutter/impeller/geometry/round_superellipse_param.h new file mode 100644 index 0000000000..a4b70b869d --- /dev/null +++ b/engine/src/flutter/impeller/geometry/round_superellipse_param.h @@ -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_ diff --git a/engine/src/flutter/impeller/geometry/round_superellipse_unittests.cc b/engine/src/flutter/impeller/geometry/round_superellipse_unittests.cc new file mode 100644 index 0000000000..e8cc34a8d3 --- /dev/null +++ b/engine/src/flutter/impeller/geometry/round_superellipse_unittests.cc @@ -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 diff --git a/engine/src/flutter/impeller/geometry/rounding_radii_unittests.cc b/engine/src/flutter/impeller/geometry/rounding_radii_unittests.cc new file mode 100644 index 0000000000..89cdd3a092 --- /dev/null +++ b/engine/src/flutter/impeller/geometry/rounding_radii_unittests.cc @@ -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