diff --git a/engine/src/flutter/ci/licenses_golden/licenses_flutter b/engine/src/flutter/ci/licenses_golden/licenses_flutter index 9fa0987499..d638e2fdc5 100644 --- a/engine/src/flutter/ci/licenses_golden/licenses_flutter +++ b/engine/src/flutter/ci/licenses_golden/licenses_flutter @@ -42511,6 +42511,8 @@ 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/rounding_radii.cc + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/impeller/geometry/rounding_radii.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/impeller/geometry/saturated_math.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/impeller/geometry/scalar.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/impeller/geometry/separated_vector.cc + ../../../flutter/LICENSE @@ -45452,6 +45454,8 @@ 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/rounding_radii.cc +FILE: ../../../flutter/impeller/geometry/rounding_radii.h FILE: ../../../flutter/impeller/geometry/saturated_math.h FILE: ../../../flutter/impeller/geometry/scalar.h FILE: ../../../flutter/impeller/geometry/separated_vector.cc diff --git a/engine/src/flutter/impeller/entity/entity_unittests.cc b/engine/src/flutter/impeller/entity/entity_unittests.cc index 8dbc6a650d..4e3017a0ac 100644 --- a/engine/src/flutter/impeller/entity/entity_unittests.cc +++ b/engine/src/flutter/impeller/entity/entity_unittests.cc @@ -2325,27 +2325,89 @@ TEST_P(EntityTest, DrawSuperEllipse) { TEST_P(EntityTest, DrawRoundSuperEllipse) { auto callback = [&](ContentContext& context, RenderPass& pass) -> bool { // UI state. - static float center_x = 100; - static float center_y = 100; - static float width = 900; - static float height = 900; - static float corner_radius = 300; - static Color color = Color::Red(); + 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; + + // Initially radius_tl[0] will be mirrored to all 8 values since all 3 + // symmetries are enabled. + static std::array radius_tl = {200}; + static std::array radius_tr; + static std::array radius_bl; + static std::array radius_br; + + auto AddRadiusControl = [](std::array& radii, const char* tb_name, + const char* lr_name) { + std::string name = "Radius"; + if (!horizontal_symmetry || !vertical_symmetry) { + name += ":"; + } + if (!vertical_symmetry) { + name = name + " " + tb_name; + } + if (!horizontal_symmetry) { + name = name + " " + lr_name; + } + if (corner_symmetry) { + ImGui::SliderFloat(name.c_str(), radii.data(), 0, 1000); + } else { + ImGui::SliderFloat2(name.c_str(), radii.data(), 0, 1000); + } + }; + + if (corner_symmetry) { + radius_tl[1] = radius_tl[0]; + radius_tr[1] = radius_tr[0]; + radius_bl[1] = radius_bl[0]; + radius_br[1] = radius_br[0]; + } ImGui::Begin("Controls", nullptr, ImGuiWindowFlags_AlwaysAutoResize); - ImGui::SliderFloat("Center X", ¢er_x, 0, 1000); - ImGui::SliderFloat("Center Y", ¢er_y, 0, 1000); - ImGui::SliderFloat("Width", &width, 0, 1000); - ImGui::SliderFloat("Height", &height, 0, 1000); - ImGui::SliderFloat("Corner radius", &corner_radius, 0, 500); + { + ImGui::SliderFloat2("Center", center, 0, 1000); + ImGui::SliderFloat2("Size", size, 0, 1000); + ImGui::Checkbox("Symmetry: Horizontal", &horizontal_symmetry); + ImGui::Checkbox("Symmetry: Vertical", &vertical_symmetry); + ImGui::Checkbox("Symmetry: Corners", &corner_symmetry); + AddRadiusControl(radius_tl, "Top", "Left"); + if (!horizontal_symmetry) { + AddRadiusControl(radius_tr, "Top", "Right"); + } else { + radius_tr = radius_tl; + } + if (!vertical_symmetry) { + AddRadiusControl(radius_bl, "Bottom", "Left"); + } else { + radius_bl = radius_tl; + } + if (!horizontal_symmetry && !vertical_symmetry) { + AddRadiusControl(radius_br, "Bottom", "Right"); + } else { + if (horizontal_symmetry) { + radius_br = radius_bl; + } else { + radius_br = radius_tr; + } + } + } + ImGui::End(); + RoundingRadii radii{ + .top_left = {radius_tl[0], radius_tl[1]}, + .top_right = {radius_tr[0], radius_tr[1]}, + .bottom_left = {radius_bl[0], radius_bl[1]}, + .bottom_right = {radius_br[0], radius_br[1]}, + }; + auto contents = std::make_shared(); std::unique_ptr geom = std::make_unique( - Rect::MakeOriginSize({center_x, center_y}, {width, height}), - corner_radius); - contents->SetColor(color); + Rect::MakeOriginSize({center[0], center[1]}, {size[0], size[1]}), + radii); + contents->SetColor(Color::Red()); contents->SetGeometry(geom.get()); Entity entity; 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 8b3006c3d6..55a703dfb7 100644 --- a/engine/src/flutter/impeller/entity/geometry/round_superellipse_geometry.cc +++ b/engine/src/flutter/impeller/entity/geometry/round_superellipse_geometry.cc @@ -3,6 +3,7 @@ // found in the LICENSE file. #include +#include #include "flutter/impeller/entity/geometry/round_superellipse_geometry.h" @@ -11,6 +12,157 @@ namespace impeller { namespace { + +// An interface for classes that arranges a point list that forms a convex +// contour into a triangle strip. +class ConvexRearranger { + public: + ConvexRearranger() {} + + virtual ~ConvexRearranger() {} + + virtual size_t ContourLength() const = 0; + + virtual Point GetPoint(size_t i) const = 0; + + void RearrangeIntoTriangleStrip(Point* output) { + size_t index_count = 0; + + output[index_count++] = GetPoint(0); + + size_t a = 1; + size_t contour_length = ContourLength(); + size_t b = contour_length - 1; + while (a < b) { + output[index_count++] = GetPoint(a); + output[index_count++] = GetPoint(b); + a++; + b--; + } + if (a == b) { + output[index_count++] = GetPoint(b); + } + } + + private: + ConvexRearranger(const ConvexRearranger&) = delete; + ConvexRearranger& operator=(const ConvexRearranger&) = delete; +}; + +// A convex rearranger whose contour is concatenated from 4 quadrant segments. +// +// The input quadrant curves must travel from the Y axis to the X axis, and +// include both ends. This means that the points on the axes are duplicate +// between segments, and will be omitted by this class. +class UnevenQuadrantsRearranger : public ConvexRearranger { + public: + UnevenQuadrantsRearranger(Point* cache, size_t segment_capacity) + : cache_(cache), segment_capacity_(segment_capacity) {} + + Point* QuadCache(size_t i) { return cache_ + segment_capacity_ * i; } + + const Point* QuadCache(size_t i) const { + return cache_ + segment_capacity_ * i; + } + + size_t& QuadSize(size_t i) { return lengths_[i]; } + + size_t ContourLength() const override { + return lengths_[0] + lengths_[1] + lengths_[2] + lengths_[3] - 4; + } + + Point GetPoint(size_t i) const override { + // output from index + // 0 ... l0-2 quads[0] 0 ... l0-2 + // next 0 ... l1-2 quads[1] l1-1 ... 1 + // next 0 ... l2-2 quads[2] 0 ... l2-2 + // next 0 ... l3-2 quads[3] l3-1 ... 1 + size_t high = lengths_[0] - 1; + if (i < high) { + return QuadCache(0)[i]; + } + high += lengths_[1] - 1; + if (i < high) { + return QuadCache(1)[high - i]; + } + size_t low = high; + high += lengths_[2] - 1; + if (i < high) { + return QuadCache(2)[i - low]; + } + high += lengths_[3] - 1; + if (i < high) { + return QuadCache(3)[high - i]; + } else { + // Unreachable + return Point(); + } + } + + private: + Point* cache_; + size_t segment_capacity_; + size_t lengths_[4]; +}; + +// A convex rearranger whose contour is concatenated from 4 identical quadrant +// segments. +// +// The input curve must travel from the Y axis to the X axis and include both +// ends. This means that the points on the axes are duplicate between segments, +// and will be omitted by this class. +class MirroredQuadrantRearranger : public ConvexRearranger { + public: + MirroredQuadrantRearranger(Point center, Point* cache) + : center_(center), cache_(cache) {} + + size_t& QuadSize() { return l_; } + + size_t ContourLength() const override { return l_ * 4 - 4; } + + Point GetPoint(size_t i) const override { + // output from index + // 0 ... l-2 quad 0 ... l-2 + // next 0 ... l-2 quad l-1 ... 1 + // next 0 ... l-2 quad 0 ... l-2 + // next 0 ... l-2 quad l-1 ... 1 + size_t high = l_ - 1; + if (i < high) { + return cache_[i] + center_; + } + high += l_ - 1; + if (i < high) { + return cache_[high - i] * Point{1, -1} + center_; + } + size_t low = high; + high += l_ - 1; + if (i < high) { + return cache_[i - low] * Point{-1, -1} + center_; + } + high += l_ - 1; + if (i < high) { + return cache_[high - i] * Point{-1, 1} + center_; + } else { + // Unreachable + return Point(); + } + } + + private: + Point center_; + Point* cache_; + size_t l_ = 0; +}; + +// A matrix that swaps the coordinates of a point. +// clang-format off +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 + // A look up table with precomputed variables. // // The columns represent the following variabls respectively: @@ -64,15 +216,6 @@ Scalar LerpPrecomputedVariable(size_t column, Scalar ratio) { frac * kPrecomputedVariables[left + 1][column]; } -// Return the shortest of `corner_radius`, height/2, and width/2. -// -// Corner radii longer than 1/2 of the side length does not make sense, and will -// be limited to the longest possible. -Scalar LimitRadius(Scalar corner_radius, const Rect& bounds) { - return std::min(corner_radius, - std::min(bounds.GetWidth() / 2, bounds.GetHeight() / 2)); -} - // The max angular step that the algorithm will traverse a quadrant of the // curve. // @@ -102,23 +245,47 @@ Scalar CalculateStep(Scalar minDimension, Scalar fullAngle) { return std::min(kMinAngleStep, angleByDimension); } -// The distance from point M (the 45deg point) to either side of the closer -// bounding box is defined as `CalculateGap`. -constexpr Scalar CalculateGap(Scalar corner_radius) { - // Heuristic formula derived from experimentation. - return 0.2924066406 * corner_radius; +// 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. +// 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 are appended to `output` and include the starting point -// but exclude the ending point. +// 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 the -size_t DrawCircularArc(Point* output, Point start, Point end, Scalar r) { +// 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 --__ @@ -139,35 +306,72 @@ size_t DrawCircularArc(Point* output, Point start, Point end, Scalar r) { Point c = m - distance_cm * c_to_m.Normalize(); Scalar angle_sce = asinf(distance_sm / r) * 2; Point c_to_s = start - c; - - Scalar step = CalculateStep(std::abs(s_to_e.y), angle_sce); + Matrix full_transform = transform * Matrix::MakeTranslation(c); Point* next = output; - Scalar angle = 0; - while (angle < angle_sce) { - *(next++) = c_to_s.Rotate(Radians(-angle)) + c; + 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 +// semi-axes equal to `a`. The arc starts from positive Y axis and spans from 0 +// 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. +// +// Returns the number of generated points. +size_t DrawSuperellipsoidArc(Point* output, + Scalar a, + Scalar n, + Scalar max_theta, + bool reverse, + const Matrix& transform) { + Point* next = output; + Scalar angle = reverse ? max_theta : 0.0f; + Scalar step = + (reverse ? -1 : 1) * + CalculateStep(a - a * pow(abs(cosf(max_theta)), 2 / n), max_theta); + Scalar end = reverse ? 0.0f : max_theta; + while ((angle < end) != reverse) { + Scalar x = a * pow(abs(sinf(angle)), 2 / n); + Scalar y = a * pow(abs(cosf(angle)), 2 / n); + *(next++) = transform * Point(x, y); angle += step; } return next - output; } // Draws an arc representing the top 1/8 segment of a square-like rounded -// superellipse. +// superellipse centered at the origin. // -// The resulting arc centers at the origin, spanning from 0 to pi/4, moving -// clockwise starting from the positive Y-axis, and includes the starting point -// (the middle of the top flat side) while excluding the ending point (the x=y -// point). +// 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. // -// The full square-like rounded superellipse 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` 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. // // Returns the number of points generated. size_t DrawOctantSquareLikeSquircle(Point* output, Scalar size, - Scalar corner_radius) { + Scalar corner_radius, + bool reverse, + const Matrix& transform) { /* 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). @@ -182,10 +386,10 @@ size_t DrawOctantSquareLikeSquircle(Point* output, * | | / ⟋ | * | | ᜱD | * | | / | - * ↑ +----+ | + * ↑ +----+ S | * s | | | * ↓ +----+---------------| A' - * O S + * O * ← s → * ←------ size/2 ------→ * @@ -203,7 +407,7 @@ size_t DrawOctantSquareLikeSquircle(Point* output, Scalar ratio = {std::min(size / corner_radius, kMaxRatio)}; Scalar a = ratio * corner_radius / 2; Scalar s = size / 2 - a; - Scalar g = CalculateGap(corner_radius); + Scalar g = kGapFactor * corner_radius; Scalar n = LerpPrecomputedVariable(1, ratio); Scalar d = LerpPrecomputedVariable(2, ratio) * a; @@ -211,118 +415,95 @@ size_t DrawOctantSquareLikeSquircle(Point* output, Scalar R = (a - d - g) * sqrt(2); - Point pointM(size / 2 - g, size / 2 - g); - - Scalar xJ = a * pow(abs(sinf(thetaJ)), 2 / n); - Scalar yJ = a * pow(abs(cosf(thetaJ)), 2 / n); + 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; - // A - *(next++) = Point(0, size / 2); - // Superellipsoid arc BJ (B inclusive, J exclusive) - { - Scalar step = CalculateStep(a - yJ, thetaJ); - Scalar angle = 0; - while (angle < thetaJ) { - Scalar x = a * pow(abs(sinf(angle)), 2 / n); - Scalar y = a * pow(abs(cosf(angle)), 2 / n); - *(next++) = Point(x + s, y + s); - angle += step; - } + if (!reverse) { + // Point A + *(next++) = transform * pointA; + // Arc [B, J) + next += DrawSuperellipsoidArc(next, a, n, thetaJ, reverse, + transform * translationS); + // Arc [J, M) + next += DrawCircularArc(next, pointJ, pointM, R, reverse, transform); + } else { + // Arc [M, J) + next += DrawCircularArc(next, pointJ, pointM, R, reverse, transform); + // Arc [J, B) + next += DrawSuperellipsoidArc(next, a, n, thetaJ, reverse, + transform * translationS); + // Point B + *(next++) = transform * Point{s, size / 2}; + // Point A + *(next++) = transform * pointA; } - - // Circular arc JM (B inclusive, M exclusive) - next += DrawCircularArc(next, {xJ + s, yJ + s}, pointM, R); return next - output; } -// Optionally `flip` the input points before offsetting it by `center`, and -// append the result to `output`. +// Draw a quadrant curve, both ends included. // -// If `flip` is true, then the entire input list is reversed, and the x and y -// coordinate of each point is swapped as well. This effectively mirrors the -// input point list by the y=x line. -size_t FlipAndOffset(Point* output, - const Point* input, - size_t input_length, - bool flip, - const Point& center) { - if (!flip) { - for (size_t i = 0; i < input_length; i++) { - output[i] = input[i] + center; - } - } else { - for (size_t i = 0; i < input_length; i++) { - const Point& point = input[input_length - i - 1]; - output[i] = Point(point.y + center.x, point.x + center.y); - } - } - return input_length; -} - -constexpr Point kReflection[4] = {{1, 1}, {1, -1}, {-1, -1}, {-1, 1}}; - -// Mirror the point list `quad` into other quadrants and output as a triangle -// strip. +// Returns the number of points. // -// The input arc `quad` should reside in the first quadrant, starting at -// positive Y axis and ending at positive X axis (both ends inclusive), for a -// total of `quad_length` points. This function mirrors the arc into 4 -// quadrants, offset the result by `center`, and rearrange it as a triangle -// strip, which is appended to `output`. -// -// A total of (quad_length - 1) * 4 points will be appended, and `output` must -// have sufficient memory allocated before this call. -void MirrorIntoTriangleStrip(const Point* quad, - size_t quad_length, - const Point& center, - Point* output) { - // The length of 1/4 arc including the starting point but excluding the - // ending point. - const size_t arc_length = quad_length - 1; - auto GetPoint = [quad, arc_length](size_t i) -> Point { - if (i < arc_length) { - return quad[i]; - } - i = i - arc_length; - if (i < arc_length) { - return quad[arc_length - i] * kReflection[1]; - } - i = i - arc_length; - if (i < arc_length) { - return quad[i] * kReflection[2]; - } - i = i - arc_length; - if (i < arc_length) { - return quad[arc_length - i] * kReflection[3]; - } else { - // Unreachable - return Point(); - } - }; - - size_t index_count = 0; - - output[index_count++] = GetPoint(0) + center; - - size_t a = 1; - size_t b = arc_length * 4 - 1; - while (a < b) { - output[index_count++] = GetPoint(a) + center; - output[index_count++] = GetPoint(b) + center; - a++; - b--; - } - if (a == b) { - output[index_count++] = GetPoint(b) + center; +// 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; + + Point* next = output; + + next += DrawOctantSquareLikeSquircle( + next, norm_size.x, norm_radius, /*reverse=*/false, + Matrix::MakeTranslateScale(signed_scale, center) * + Matrix::MakeTranslation(Size{0, -c})); + + next += DrawOctantSquareLikeSquircle( + next, norm_size.y, norm_radius, /*reverse=*/true, + Matrix::MakeTranslateScale(signed_scale, center) * + Matrix::MakeTranslation(Size{c, 0}) * kFlip); + + return next - output; } } // namespace RoundSuperellipseGeometry::RoundSuperellipseGeometry(const Rect& bounds, - Scalar corner_radius) - : bounds_(bounds), corner_radius_(LimitRadius(corner_radius, bounds)) {} + const RoundingRadii& radii) + : bounds_(bounds.GetPositive()), radii_(radii.Scaled(bounds_)) {} + +RoundSuperellipseGeometry::RoundSuperellipseGeometry(const Rect& bounds, + float corner_radius) + : RoundSuperellipseGeometry(bounds, + RoundingRadii::MakeRadius(corner_radius)) {} RoundSuperellipseGeometry::~RoundSuperellipseGeometry() {} @@ -330,66 +511,66 @@ GeometryResult RoundSuperellipseGeometry::GetPositionBuffer( const ContentContext& renderer, const Entity& entity, RenderPass& pass) const { - const Size size = bounds_.GetSize(); - const Point center = bounds_.GetCenter(); - - // The full shape is divided into 4 segments: the top and bottom edges come - // from two square-like rounded superellipses (called "width-aligned"), while - // the left and right squircles come from another two ("height-aligned"). - // - // Denote the distance from the center of the square-like squircles to the - // origin as `c`. The width-aligned square-like squircle and the - // height-aligned one have the same offset in different directions. - const Scalar c = (size.width - size.height) / 2; - - // The cache is allocated as follows: - // - // * The first chunk stores the quadrant arc. - // * The second chunk stores an octant arc before flipping and translation. Point* cache = renderer.GetTessellator().GetStrokePointCache().data(); - // The memory size (in units of Points) allocated to store the first chunk. - constexpr size_t kMaxQuadrantLength = kPointArenaSize / 4; + // The memory size (in units of Points) allocated to store each quadrants. + constexpr size_t kMaxQuadSize = kPointArenaSize / 4; // Since the curve is traversed in steps bounded by kMaxQuadrantSteps, the // curving part will have fewer points than kMaxQuadrantSteps. Multiply it by // 2 for storing other sporatic points (an extremely conservative estimate). - static_assert(kMaxQuadrantLength > 2 * kMaxQuadrantSteps); + static_assert(kMaxQuadSize > 2 * kMaxQuadrantSteps); - // Draw the first quadrant of the shape and store in `quadrant`, including - // both ends. It will be mirrored to other quadrants later. - Point* quadrant = cache; - size_t quadrant_length; - { - Point* next = quadrant; + ConvexRearranger* rearranger; + std::variant + rearranger_holder; - Point* octant_cache = cache + kMaxQuadrantLength; - size_t octant_length; + if (radii_.AreAllCornersSame()) { + rearranger_holder.emplace(bounds_.GetCenter(), + cache); + auto& t = std::get(rearranger_holder); + rearranger = &t; - octant_length = - DrawOctantSquareLikeSquircle(octant_cache, size.width, corner_radius_); - next += FlipAndOffset(next, octant_cache, octant_length, /*flip=*/false, - Point(0, -c)); + // 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); + } else { + rearranger_holder.emplace(cache, kMaxQuadSize); + auto& t = std::get(rearranger_holder); + rearranger = &t; - *(next++) = Point(size / 2) - CalculateGap(corner_radius_); // Point M + 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); - octant_length = - DrawOctantSquareLikeSquircle(octant_cache, size.height, corner_radius_); - next += FlipAndOffset(next, octant_cache, octant_length, /*flip=*/true, - Point(c, 0)); - - quadrant_length = next - quadrant; + 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); } - // The `contour_point_count` include all points on the border. The "-1" comes - // from duplicate ends from the mirrored arcs. - size_t contour_length = 4 * (quadrant_length - 1); + size_t contour_length = rearranger->ContourLength(); BufferView vertex_buffer = renderer.GetTransientsBuffer().Emplace( nullptr, sizeof(Point) * contour_length, alignof(Point)); Point* vertex_data = reinterpret_cast(vertex_buffer.GetBuffer()->OnGetContents() + vertex_buffer.GetRange().offset); - - MirrorIntoTriangleStrip(quadrant, quadrant_length, center, vertex_data); + rearranger->RearrangeIntoTriangleStrip(vertex_data); return GeometryResult{ .type = PrimitiveType::kTriangleStrip, @@ -413,14 +594,18 @@ bool RoundSuperellipseGeometry::CoversArea(const Matrix& transform, if (!transform.IsTranslationScaleOnly()) { return false; } - // Use the rectangle formed by the four 45deg points (point M) as a - // conservative estimate of the inner rectangle. - Scalar g = CalculateGap(corner_radius_); + Scalar left_inset = std::max(radii_.top_left.width, radii_.bottom_left.width); + Scalar right_inset = + std::max(radii_.top_right.width, radii_.bottom_right.width); + Scalar top_inset = std::max(radii_.top_left.height, radii_.top_right.height); + Scalar bottom_inset = + std::max(radii_.bottom_left.height, radii_.bottom_right.height); Rect coverage = - Rect::MakeLTRB(bounds_.GetLeft() + g, bounds_.GetTop() + g, - bounds_.GetRight() - g, bounds_.GetBottom() - g) - .TransformBounds(transform); - return coverage.Contains(rect); + Rect::MakeLTRB(bounds_.GetLeft() + left_inset * kGapFactor, + bounds_.GetTop() + top_inset * kGapFactor, + bounds_.GetRight() - right_inset * kGapFactor, + bounds_.GetBottom() - bottom_inset * kGapFactor); + return coverage.TransformBounds(transform).Contains(rect); } bool RoundSuperellipseGeometry::IsAxisAlignedRect() const { diff --git a/engine/src/flutter/impeller/entity/geometry/round_superellipse_geometry.h b/engine/src/flutter/impeller/entity/geometry/round_superellipse_geometry.h index c5d5c7bfc9..5df22cd29e 100644 --- a/engine/src/flutter/impeller/entity/geometry/round_superellipse_geometry.h +++ b/engine/src/flutter/impeller/entity/geometry/round_superellipse_geometry.h @@ -6,6 +6,7 @@ #define FLUTTER_IMPELLER_ENTITY_GEOMETRY_ROUND_SUPERELLIPSE_GEOMETRY_H_ #include "impeller/entity/geometry/geometry.h" +#include "impeller/geometry/rounding_radii.h" namespace impeller { @@ -25,7 +26,8 @@ namespace impeller { /// not exactly equals to, the radius of the corner circles. class RoundSuperellipseGeometry final : public Geometry { public: - explicit RoundSuperellipseGeometry(const Rect& bounds, Scalar corner_radius); + RoundSuperellipseGeometry(const Rect& bounds, const RoundingRadii& radii); + RoundSuperellipseGeometry(const Rect& bounds, float corner_radius); ~RoundSuperellipseGeometry() override; @@ -45,7 +47,7 @@ class RoundSuperellipseGeometry final : public Geometry { std::optional GetCoverage(const Matrix& transform) const override; const Rect bounds_; - double corner_radius_; + const RoundingRadii radii_; RoundSuperellipseGeometry(const RoundSuperellipseGeometry&) = delete; diff --git a/engine/src/flutter/impeller/geometry/BUILD.gn b/engine/src/flutter/impeller/geometry/BUILD.gn index 5c1d71f4e0..fba613c3aa 100644 --- a/engine/src/flutter/impeller/geometry/BUILD.gn +++ b/engine/src/flutter/impeller/geometry/BUILD.gn @@ -31,6 +31,8 @@ impeller_component("geometry") { "rect.h", "round_rect.cc", "round_rect.h", + "rounding_radii.cc", + "rounding_radii.h", "saturated_math.h", "scalar.h", "separated_vector.cc", diff --git a/engine/src/flutter/impeller/geometry/round_rect.cc b/engine/src/flutter/impeller/geometry/round_rect.cc index 1a321603c1..23d1dd6b76 100644 --- a/engine/src/flutter/impeller/geometry/round_rect.cc +++ b/engine/src/flutter/impeller/geometry/round_rect.cc @@ -6,68 +6,17 @@ namespace impeller { -static inline void NormalizeEmptyToZero(Size& radii) { - if (radii.IsEmpty()) { - radii = Size(); - } -} - -static inline void AdjustScale(Scalar& radius1, - Scalar& radius2, - Scalar dimension, - Scalar& scale) { - FML_DCHECK(radius1 >= 0.0f && radius2 >= 0.0f); - FML_DCHECK(dimension > 0.0f); - if (radius1 + radius2 > dimension) { - scale = std::min(scale, dimension / (radius1 + radius2)); - } -} - RoundRect RoundRect::MakeRectRadii(const Rect& in_bounds, const RoundingRadii& in_radii) { if (!in_bounds.IsFinite()) { return {}; } Rect bounds = in_bounds.GetPositive(); - if (bounds.IsEmpty() || // - in_radii.AreAllCornersEmpty() || !in_radii.IsFinite()) { - // pass along the bounds even if empty as it would still have a valid - // location and/or 1-dimensional size which might appear when stroked - return RoundRect(bounds, RoundingRadii()); - } - - // Copy the incoming radii so that we can work on normalizing them to the - // particular rectangle they are paired with without disturbing the caller. - RoundingRadii radii = in_radii; - - // If any corner is flat or has a negative value, normalize it to zeros - // We do this first so that the unnecessary non-flat part of that radius - // does not contribute to the global scaling below. - NormalizeEmptyToZero(radii.top_left); - NormalizeEmptyToZero(radii.top_right); - NormalizeEmptyToZero(radii.bottom_left); - NormalizeEmptyToZero(radii.bottom_right); - - // Now determine a global scale to apply to all of the radii to ensure - // that none of the adjacent pairs of radius values sum to larger than - // the corresponding dimension of the rectangle. - Size size = bounds.GetSize(); - Scalar scale = 1.0f; - // clang-format off - AdjustScale(radii.top_left.width, radii.top_right.width, size.width, - scale); - AdjustScale(radii.bottom_left.width, radii.bottom_right.width, size.width, - scale); - AdjustScale(radii.top_left.height, radii.bottom_left.height, size.height, - scale); - AdjustScale(radii.top_right.height, radii.bottom_right.height, size.height, - scale); - // clang-format on - if (scale < 1.0f) { - radii = radii * scale; - } - - return RoundRect(bounds, radii); + // 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 RoundRect(bounds, in_radii.Scaled(bounds)); } // Determine if p is inside the elliptical corner curve defined by the diff --git a/engine/src/flutter/impeller/geometry/round_rect.h b/engine/src/flutter/impeller/geometry/round_rect.h index 013a31861e..1e4b45e826 100644 --- a/engine/src/flutter/impeller/geometry/round_rect.h +++ b/engine/src/flutter/impeller/geometry/round_rect.h @@ -7,68 +7,11 @@ #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 RoundingRadii { - Size top_left; - Size top_right; - Size bottom_left; - Size bottom_right; - - constexpr static RoundingRadii MakeRadius(Scalar radius) { - return {Size(radius), Size(radius), Size(radius), Size(radius)}; - } - - constexpr static RoundingRadii MakeRadii(Size radii) { - return {radii, radii, radii, radii}; - } - - constexpr bool IsFinite() const { - return top_left.IsFinite() && // - top_right.IsFinite() && // - bottom_left.IsFinite() && // - bottom_right.IsFinite(); - } - - constexpr bool AreAllCornersEmpty() const { - return top_left.IsEmpty() && // - top_right.IsEmpty() && // - bottom_left.IsEmpty() && // - bottom_right.IsEmpty(); - } - - constexpr bool AreAllCornersSame(Scalar tolerance = kEhCloseEnough) const { - return ScalarNearlyEqual(top_left.width, top_right.width, tolerance) && - ScalarNearlyEqual(top_left.width, bottom_right.width, tolerance) && - ScalarNearlyEqual(top_left.width, bottom_left.width, tolerance) && - ScalarNearlyEqual(top_left.height, top_right.height, tolerance) && - ScalarNearlyEqual(top_left.height, bottom_right.height, tolerance) && - ScalarNearlyEqual(top_left.height, bottom_left.height, tolerance); - } - - constexpr inline RoundingRadii operator*(Scalar scale) { - return { - .top_left = top_left * scale, - .top_right = top_right * scale, - .bottom_left = bottom_left * scale, - .bottom_right = bottom_right * scale, - }; - } - - [[nodiscard]] constexpr bool operator==(const RoundingRadii& rr) const { - return top_left == rr.top_left && // - top_right == rr.top_right && // - bottom_left == rr.bottom_left && // - bottom_right == rr.bottom_right; - } - - [[nodiscard]] constexpr bool operator!=(const RoundingRadii& rr) const { - return !(*this == rr); - } -}; - struct RoundRect { RoundRect() = default; @@ -190,17 +133,6 @@ struct RoundRect { namespace std { -inline std::ostream& operator<<(std::ostream& out, - const impeller::RoundingRadii& rr) { - out << "(" // - << "ul: " << rr.top_left << ", " // - << "ur: " << rr.top_right << ", " // - << "ll: " << rr.bottom_left << ", " // - << "lr: " << rr.bottom_right // - << ")"; - return out; -} - inline std::ostream& operator<<(std::ostream& out, const impeller::RoundRect& rr) { out << "(" // diff --git a/engine/src/flutter/impeller/geometry/rounding_radii.cc b/engine/src/flutter/impeller/geometry/rounding_radii.cc new file mode 100644 index 0000000000..a74d7b64cf --- /dev/null +++ b/engine/src/flutter/impeller/geometry/rounding_radii.cc @@ -0,0 +1,68 @@ +// 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/rounding_radii.h" + +namespace impeller { + +static inline void NormalizeEmptyToZero(Size& radii) { + if (radii.IsEmpty()) { + radii = Size(); + } +} + +static inline void AdjustScale(Scalar& radius1, + Scalar& radius2, + Scalar dimension, + Scalar& scale) { + FML_DCHECK(radius1 >= 0.0f && radius2 >= 0.0f); + FML_DCHECK(dimension > 0.0f); + if (radius1 + radius2 > dimension) { + scale = std::min(scale, dimension / (radius1 + radius2)); + } +} + +RoundingRadii RoundingRadii::Scaled(const Rect& in_bounds) const { + Rect bounds = in_bounds.GetPositive(); + if (bounds.IsEmpty() || // + AreAllCornersEmpty() || !IsFinite()) { + // Normalize empty radii. + return RoundingRadii(); + } + + // Copy the incoming radii so that we can work on normalizing them to the + // particular rectangle they are paired with without disturbing the caller. + RoundingRadii radii = *this; + + // If any corner is flat or has a negative value, normalize it to zeros + // We do this first so that the unnecessary non-flat part of that radius + // does not contribute to the global scaling below. + NormalizeEmptyToZero(radii.top_left); + NormalizeEmptyToZero(radii.top_right); + NormalizeEmptyToZero(radii.bottom_left); + NormalizeEmptyToZero(radii.bottom_right); + + // Now determine a global scale to apply to all of the radii to ensure + // that none of the adjacent pairs of radius values sum to larger than + // the corresponding dimension of the rectangle. + Size size = bounds.GetSize(); + Scalar scale = 1.0f; + // clang-format off + AdjustScale(radii.top_left.width, radii.top_right.width, size.width, + scale); + AdjustScale(radii.bottom_left.width, radii.bottom_right.width, size.width, + scale); + AdjustScale(radii.top_left.height, radii.bottom_left.height, size.height, + scale); + AdjustScale(radii.top_right.height, radii.bottom_right.height, size.height, + scale); + // clang-format on + if (scale < 1.0f) { + radii = radii * scale; + } + + return radii; +} + +} // namespace impeller diff --git a/engine/src/flutter/impeller/geometry/rounding_radii.h b/engine/src/flutter/impeller/geometry/rounding_radii.h new file mode 100644 index 0000000000..09d8a6b7ea --- /dev/null +++ b/engine/src/flutter/impeller/geometry/rounding_radii.h @@ -0,0 +1,98 @@ +// 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_ROUNDING_RADII_H_ +#define FLUTTER_IMPELLER_GEOMETRY_ROUNDING_RADII_H_ + +#include "flutter/impeller/geometry/point.h" +#include "flutter/impeller/geometry/rect.h" +#include "flutter/impeller/geometry/size.h" + +namespace impeller { + +struct RoundingRadii { + Size top_left; + Size top_right; + Size bottom_left; + Size bottom_right; + + constexpr static RoundingRadii MakeRadius(Scalar radius) { + return {Size(radius), Size(radius), Size(radius), Size(radius)}; + } + + constexpr static RoundingRadii MakeRadii(Size radii) { + return {radii, radii, radii, radii}; + } + + constexpr bool IsFinite() const { + return top_left.IsFinite() && // + top_right.IsFinite() && // + bottom_left.IsFinite() && // + bottom_right.IsFinite(); + } + + constexpr bool AreAllCornersEmpty() const { + return top_left.IsEmpty() && // + top_right.IsEmpty() && // + bottom_left.IsEmpty() && // + bottom_right.IsEmpty(); + } + + constexpr bool AreAllCornersSame(Scalar tolerance = kEhCloseEnough) const { + return ScalarNearlyEqual(top_left.width, top_right.width, tolerance) && + ScalarNearlyEqual(top_left.width, bottom_right.width, tolerance) && + ScalarNearlyEqual(top_left.width, bottom_left.width, tolerance) && + ScalarNearlyEqual(top_left.height, top_right.height, tolerance) && + ScalarNearlyEqual(top_left.height, bottom_right.height, tolerance) && + ScalarNearlyEqual(top_left.height, bottom_left.height, tolerance); + } + + /// @brief Returns a scaled copy of this object, ensuring that the sum of the + /// corner radii on each side does not exceed the width or height of + /// the given bounds. + /// + /// See the [Skia scaling + /// implementation](https://github.com/google/skia/blob/main/src/core/SkRRect.cpp) + /// for more details. + RoundingRadii Scaled(const Rect& bounds) const; + + constexpr inline RoundingRadii operator*(Scalar scale) { + return { + .top_left = top_left * scale, + .top_right = top_right * scale, + .bottom_left = bottom_left * scale, + .bottom_right = bottom_right * scale, + }; + } + + [[nodiscard]] constexpr bool operator==(const RoundingRadii& rr) const { + return top_left == rr.top_left && // + top_right == rr.top_right && // + bottom_left == rr.bottom_left && // + bottom_right == rr.bottom_right; + } + + [[nodiscard]] constexpr bool operator!=(const RoundingRadii& rr) const { + return !(*this == rr); + } +}; + +} // namespace impeller + +namespace std { + +inline std::ostream& operator<<(std::ostream& out, + const impeller::RoundingRadii& rr) { + out << "(" // + << "ul: " << rr.top_left << ", " // + << "ur: " << rr.top_right << ", " // + << "ll: " << rr.bottom_left << ", " // + << "lr: " << rr.bottom_right // + << ")"; + return out; +} + +} // namespace std + +#endif // FLUTTER_IMPELLER_GEOMETRY_ROUNDING_RADII_H_ diff --git a/engine/src/flutter/impeller/geometry/size.h b/engine/src/flutter/impeller/geometry/size.h index 524c52f63e..9090d6a1c8 100644 --- a/engine/src/flutter/impeller/geometry/size.h +++ b/engine/src/flutter/impeller/geometry/size.h @@ -101,6 +101,8 @@ struct TSize { }; } + constexpr Type MinDimension() const { return std::min(width, height); } + constexpr Type MaxDimension() const { return std::max(width, height); } constexpr TSize Abs() const { return {std::fabs(width), std::fabs(height)}; }