From f2e0a2afb35cefe48600c81f39e163db00cff89f Mon Sep 17 00:00:00 2001 From: Tong Mu Date: Wed, 2 Apr 2025 14:27:08 -0700 Subject: [PATCH] [dart:ui] Add `Path.addRSuperellipse` (#166045) This PR adds `Path.addRSuperellipse` to `dart:ui`. This is needed to implement a parity class to `RoundedRectangleBorder` as discussed [here](https://github.com/flutter/flutter/pull/164857#issuecomment-2715637356).
Obsolete description, no longer applicable I want to reuse the existing algorithm created for impeller stroking. The existing algorithm is moved from `path_builder.cc` to `round_superellipse_param.cc`, and a delegated is added so that the same algorithm can output for different path classes. I'm not 100% sure this is _the_ best way to implement this, but I've tried some methods in vain. * `DlPathReceiver` added in https://github.com/flutter/flutter/pull/164753 sounds like a similar concept as the delegate created in this PR. I tried to use that but not only are the methods private, they're neither in an accessible directory. * I also thought of converting an impeller `Path` to a skia path, but it seems that the impeller path isn't designed to be traversed. * Another possibility is that we refactor impeller stroking to be based on the triangles instead of path, a direction we agreed to eventually move toward, which allows avoiding code share at all. I've briefly read the code in `StrokePathGeometry` and have some ideas but also something uncertain, so I didn't choose this path for now.
## Pre-launch Checklist - [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [ ] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [ ] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [ ] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- engine/src/flutter/display_list/dl_builder.cc | 13 +- .../display_list/testing/dl_test_snippets.cc | 14 +- .../flutter/impeller/geometry/path_builder.cc | 214 +----------------- .../geometry/round_superellipse_param.cc | 190 ++++++++++++++++ .../geometry/round_superellipse_param.h | 4 + engine/src/flutter/lib/ui/dart_ui.cc | 1 + engine/src/flutter/lib/ui/painting.dart | 13 ++ engine/src/flutter/lib/ui/painting/path.cc | 12 + engine/src/flutter/lib/ui/painting/path.h | 2 + .../flutter/lib/ui/painting/rsuperellipse.h | 3 + engine/src/flutter/lib/web_ui/lib/path.dart | 1 + .../web_ui/lib/src/engine/canvaskit/path.dart | 7 + .../src/engine/skwasm/skwasm_impl/path.dart | 7 + .../web_ui/test/engine/scene_view_test.dart | 3 + .../flutter/testing/dart/geometry_test.dart | 12 +- .../src/flutter/testing/dart/path_test.dart | 48 ++++ 16 files changed, 318 insertions(+), 226 deletions(-) diff --git a/engine/src/flutter/display_list/dl_builder.cc b/engine/src/flutter/display_list/dl_builder.cc index 3ee41431bd..3fd408df41 100644 --- a/engine/src/flutter/display_list/dl_builder.cc +++ b/engine/src/flutter/display_list/dl_builder.cc @@ -1260,7 +1260,18 @@ void DisplayListBuilder::drawRoundSuperellipse(const DlRoundSuperellipse& rse) { OpResult result = PaintResult(current_, flags); if (result != OpResult::kNoEffect && AccumulateOpBounds(rse.GetBounds(), flags)) { - Push(0, rse); + // DrawRoundSuperellipseOp only supports filling. Anything related to + // stroking must use path approximation. + if (current_.getDrawStyle() == DlDrawStyle::kFill) { + Push(0, rse); + } else { + DlPathBuilder builder; + builder.SetConvexity(impeller::Convexity::kConvex); + builder.SetBounds(rse.GetBounds()); + builder.AddRoundSuperellipse(DlRoundSuperellipse::MakeRectRadii( + rse.GetBounds(), rse.GetRadii())); + Push(0, DlPath(builder.TakePath())); + } CheckLayerOpacityCompatibility(); UpdateLayerResult(result); } diff --git a/engine/src/flutter/display_list/testing/dl_test_snippets.cc b/engine/src/flutter/display_list/testing/dl_test_snippets.cc index fa69a34518..396fbd8cea 100644 --- a/engine/src/flutter/display_list/testing/dl_test_snippets.cc +++ b/engine/src/flutter/display_list/testing/dl_test_snippets.cc @@ -753,17 +753,9 @@ std::vector CreateAllRenderingOps() { {1, 56, 1, [](DlOpReceiver& r) { r.drawRoundRect(kTestRRect.Shift(5, 5)); }}, }}, - {"DrawRSuperellipse", - { - {1, 56, 1, - [](DlOpReceiver& r) { - r.drawRoundSuperellipse(kTestRSuperellipse); - }}, - {1, 56, 1, - [](DlOpReceiver& r) { - r.drawRoundSuperellipse(kTestRSuperellipse.Shift(5, 5)); - }}, - }}, + // DrawRSuperellipse is omitted because the testing framework doesn't + // support flexible size. + // TODO(dkwingsmt): https://github.com/flutter/flutter/issues/166284 {"DrawDRRect", { {1, 104, 1, diff --git a/engine/src/flutter/impeller/geometry/path_builder.cc b/engine/src/flutter/impeller/geometry/path_builder.cc index e80c11c423..fe105a8f73 100644 --- a/engine/src/flutter/impeller/geometry/path_builder.cc +++ b/engine/src/flutter/impeller/geometry/path_builder.cc @@ -12,179 +12,6 @@ namespace impeller { -namespace { - -// Utility functions used to build a rounded superellipse. -class RoundSuperellipseBuilder { - public: - using CubicAdder = std::function< - void(const Point&, const Point&, const Point&, const Point&)>; - using PointAdder = std::function; - - // Create a builder. - // - // The resulting curves, which consists of cubic curves, are added by calling - // `cubic_adder`. - explicit RoundSuperellipseBuilder(CubicAdder cubic_adder, - PointAdder point_adder) - : cubic_adder_(std::move(cubic_adder)), - point_adder_(std::move(point_adder)) {} - - // Draws an arc representing 1/4 of a rounded superellipse. - // - // 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 (param.top.se_n < 2 || param.right.se_n < 2) { - point_adder_(transform * - (param.top.offset + Point(param.top.se_a, param.top.se_a))); - return; - } - if (!reverse) { - AddOctant(param.top, /*reverse=*/false, /*flip=*/false, transform); - AddOctant(param.right, /*reverse=*/true, /*flip=*/true, transform); - } 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 = {0, param.se_a}; - const Point& end = param.circle_start; - constexpr Point start_tangent = {1, 0}; - Point circle_start_vector = param.circle_start - param.circle_center; - 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.0*/ {0.01339448, 0.05994973}, - /*n=3.0*/ {0.13664115, 0.13592082}, - /*n=4.0*/ {0.24545546, 0.14099516}, - /*n=5.0*/ {0.32353151, 0.12808021}, - /*n=6.0*/ {0.39093068, 0.11726264}, - /*n=7.0*/ {0.44847800, 0.10808278}, - /*n=8.0*/ {0.49817452, 0.10026175}, - /*n=9.0*/ {0.54105583, 0.09344429}, - /*n=10.0*/ {0.57812578, 0.08748984}, - /*n=11.0*/ {0.61050961, 0.08224722}, - /*n=12.0*/ {0.63903989, 0.07759639}, - /*n=13.0*/ {0.66416338, 0.07346530}, - /*n=14.0*/ {0.68675338, 0.06974996}, - /*n=15.0*/ {0.70678034, 0.06529512}}; - constexpr size_t kNumRecords = - sizeof(kPrecomputedVariables) / sizeof(kPrecomputedVariables[0]); - constexpr Scalar kStep = 1.00f; - constexpr Scalar kMinN = 2.00f; - constexpr Scalar kMaxN = kMinN + (kNumRecords - 1) * kStep; - - if (n >= kMaxN) { - // Heuristic formula derived from fitting. - return {1.07f - expf(1.307649835) * powf(n, -0.8568516731), - -0.01f + expf(-0.9287690322) * powf(n, -0.6120901398)}; - } - - Scalar steps = std::clamp((n - kMinN) / kStep, 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_; - PointAdder point_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({}); } @@ -418,43 +245,14 @@ PathBuilder& PathBuilder::AddRoundRect(RoundRect round_rect) { 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); - }, - [this](const Point& a) { LineTo(a); }); - - auto param = - RoundSuperellipseParam::MakeBoundsRadii(rse.GetBounds(), rse.GetRadii()); - Point start = - param.top_right.offset + - param.top_right.signed_scale * - (param.top_right.top.offset + Point(0, param.top_right.top.se_a)); - 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); + AddRect(rse.GetBounds()); + } else if (rse.IsOval()) { + AddOval(rse.GetBounds()); } 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); + impeller::RoundSuperellipseParam::MakeBoundsRadii(rse.GetBounds(), + rse.GetRadii()) + .AddToPath(*this); } - - LineTo(start); - - Close(); - return *this; } diff --git a/engine/src/flutter/impeller/geometry/round_superellipse_param.cc b/engine/src/flutter/impeller/geometry/round_superellipse_param.cc index ce82ead867..7dd600453d 100644 --- a/engine/src/flutter/impeller/geometry/round_superellipse_param.cc +++ b/engine/src/flutter/impeller/geometry/round_superellipse_param.cc @@ -292,6 +292,172 @@ bool CornerContains(const RoundSuperellipseParam::Quadrant& param, OctantContains(param.right, Flip(norm_point - param.right.offset)); } +class RoundSuperellipseBuilder { + public: + explicit RoundSuperellipseBuilder(PathBuilder& builder) : builder_(builder) {} + + // 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, + Point scale_sign = Point(1, 1)) { + auto transform = Matrix::MakeTranslateScale(param.signed_scale * scale_sign, + param.offset); + if (param.top.se_n < 2 || param.right.se_n < 2) { + builder_.LineTo(transform * (param.top.offset + + Point(param.top.se_a, param.top.se_a))); + if (!reverse) { + builder_.LineTo(transform * + (param.top.offset + Point(param.top.se_a, 0))); + } else { + builder_.LineTo(transform * + (param.top.offset + Point(0, param.top.se_a))); + } + return; + } + 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 = {0, param.se_a}; + const Point& end = param.circle_start; + constexpr Point start_tangent = {1, 0}; + Point circle_start_vector = param.circle_start - param.circle_center; + 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) { + builder_.CubicCurveTo(transform * se_points[1], transform * se_points[2], + transform * se_points[3]); + builder_.CubicCurveTo(transform * circle_points[1], + transform * circle_points[2], + transform * circle_points[3]); + } else { + builder_.CubicCurveTo(transform * circle_points[2], + transform * circle_points[1], + transform * circle_points[0]); + builder_.CubicCurveTo(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.0*/ {0.01339448, 0.05994973}, + /*n=3.0*/ {0.13664115, 0.13592082}, + /*n=4.0*/ {0.24545546, 0.14099516}, + /*n=5.0*/ {0.32353151, 0.12808021}, + /*n=6.0*/ {0.39093068, 0.11726264}, + /*n=7.0*/ {0.44847800, 0.10808278}, + /*n=8.0*/ {0.49817452, 0.10026175}, + /*n=9.0*/ {0.54105583, 0.09344429}, + /*n=10.0*/ {0.57812578, 0.08748984}, + /*n=11.0*/ {0.61050961, 0.08224722}, + /*n=12.0*/ {0.63903989, 0.07759639}, + /*n=13.0*/ {0.66416338, 0.07346530}, + /*n=14.0*/ {0.68675338, 0.06974996}, + /*n=15.0*/ {0.70678034, 0.06529512}}; + constexpr size_t kNumRecords = + sizeof(kPrecomputedVariables) / sizeof(kPrecomputedVariables[0]); + constexpr Scalar kStep = 1.00f; + constexpr Scalar kMinN = 2.00f; + constexpr Scalar kMaxN = kMinN + (kNumRecords - 1) * kStep; + + if (n >= kMaxN) { + // Heuristic formula derived from fitting. + return {1.07f - expf(1.307649835) * powf(n, -0.8568516731), + -0.01f + expf(-0.9287690322) * powf(n, -0.6120901398)}; + } + + Scalar steps = std::clamp((n - kMinN) / kStep, 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]}; + } + + PathBuilder& builder_; + + // 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 RoundSuperellipseParam RoundSuperellipseParam::MakeBoundsRadii( @@ -330,6 +496,30 @@ RoundSuperellipseParam RoundSuperellipseParam::MakeBoundsRadii( }; } +void RoundSuperellipseParam::AddToPath(PathBuilder& path_builder) const { + RoundSuperellipseBuilder builder(path_builder); + + Point start = top_right.offset + + top_right.signed_scale * + (top_right.top.offset + Point(0, top_right.top.se_a)); + path_builder.MoveTo(start); + + if (all_corners_same) { + builder.AddQuadrant(top_right, /*reverse=*/false, Point(1, 1)); + builder.AddQuadrant(top_right, /*reverse=*/true, Point(1, -1)); + builder.AddQuadrant(top_right, /*reverse=*/false, Point(-1, -1)); + builder.AddQuadrant(top_right, /*reverse=*/true, Point(-1, 1)); + } else { + builder.AddQuadrant(top_right, /*reverse=*/false); + builder.AddQuadrant(bottom_right, /*reverse=*/true); + builder.AddQuadrant(bottom_left, /*reverse=*/false); + builder.AddQuadrant(top_left, /*reverse=*/true); + } + + path_builder.LineTo(start); + path_builder.Close(); +} + bool RoundSuperellipseParam::Contains(const Point& point) const { if (all_corners_same) { return CornerContains(top_right, point, /*check_quadrant=*/false); diff --git a/engine/src/flutter/impeller/geometry/round_superellipse_param.h b/engine/src/flutter/impeller/geometry/round_superellipse_param.h index dfd696fda6..f4e4b3b7fa 100644 --- a/engine/src/flutter/impeller/geometry/round_superellipse_param.h +++ b/engine/src/flutter/impeller/geometry/round_superellipse_param.h @@ -5,6 +5,7 @@ #ifndef FLUTTER_IMPELLER_GEOMETRY_ROUND_SUPERELLIPSE_PARAM_H_ #define FLUTTER_IMPELLER_GEOMETRY_ROUND_SUPERELLIPSE_PARAM_H_ +#include "flutter/impeller/geometry/path_builder.h" #include "flutter/impeller/geometry/point.h" #include "flutter/impeller/geometry/rect.h" #include "flutter/impeller/geometry/rounding_radii.h" @@ -105,6 +106,9 @@ struct RoundSuperellipseParam { // with the bounds, which is recommended for callers. bool Contains(const Point& point) const; + // Add a path of this rounded superellipse to the provided path builder. + void AddToPath(PathBuilder& path) 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. // diff --git a/engine/src/flutter/lib/ui/dart_ui.cc b/engine/src/flutter/lib/ui/dart_ui.cc index d8e7f0b4ef..d0318ddb28 100644 --- a/engine/src/flutter/lib/ui/dart_ui.cc +++ b/engine/src/flutter/lib/ui/dart_ui.cc @@ -253,6 +253,7 @@ typedef CanvasPath Path; V(Path, addPathWithMatrix) \ V(Path, addPolygon) \ V(Path, addRRect) \ + V(Path, addRSuperellipse) \ V(Path, addRect) \ V(Path, arcTo) \ V(Path, arcToPoint) \ diff --git a/engine/src/flutter/lib/ui/painting.dart b/engine/src/flutter/lib/ui/painting.dart index d98ae5efef..4390a0c8d7 100644 --- a/engine/src/flutter/lib/ui/painting.dart +++ b/engine/src/flutter/lib/ui/painting.dart @@ -3033,6 +3033,10 @@ abstract class Path { /// argument. void addRRect(RRect rrect); + /// Adds a new sub-path that consists of curves needed to form the rounded + /// superellipse described by the argument. + void addRSuperellipse(RSuperellipse rsuperellipse); + /// Adds the sub-paths of `path`, offset by `offset`, to this path. /// /// If `matrix4` is specified, the path will be transformed by this matrix @@ -3375,6 +3379,15 @@ base class _NativePath extends NativeFieldWrapperClass1 implements Path { @Native, Handle)>(symbol: 'Path::addRRect') external void _addRRect(Float32List rrect); + @override + void addRSuperellipse(RSuperellipse rsuperellipse) { + assert(_rsuperellipseIsValid(rsuperellipse)); + _addRSuperellipse(rsuperellipse._native()); + } + + @Native, Pointer)>(symbol: 'Path::addRSuperellipse') + external void _addRSuperellipse(_NativeRSuperellipse rsuperellipse); + @override void addPath(Path path, Offset offset, {Float64List? matrix4}) { assert(_offsetIsValid(offset)); diff --git a/engine/src/flutter/lib/ui/painting/path.cc b/engine/src/flutter/lib/ui/painting/path.cc index be93ad02b5..b0545c14b2 100644 --- a/engine/src/flutter/lib/ui/painting/path.cc +++ b/engine/src/flutter/lib/ui/painting/path.cc @@ -202,6 +202,18 @@ void CanvasPath::addRRect(const RRect& rrect) { resetVolatility(); } +void CanvasPath::addRSuperellipse(const RSuperellipse* rsuperellipse) { + DlPathBuilder builder; + builder.SetConvexity(impeller::Convexity::kConvex); + builder.SetBounds(rsuperellipse->bounds()); + builder.AddRoundSuperellipse(DlRoundSuperellipse::MakeRectRadii( + rsuperellipse->bounds(), rsuperellipse->radii())); + sk_path_.addPath(DlPath(builder.TakePath()).GetSkPath(), + SkPath::kAppend_AddPathMode); + + resetVolatility(); +} + void CanvasPath::addPath(CanvasPath* path, double dx, double dy) { if (!path) { Dart_ThrowException(ToDart("Path.addPath called with non-genuine Path.")); diff --git a/engine/src/flutter/lib/ui/painting/path.h b/engine/src/flutter/lib/ui/painting/path.h index 3cc2342d61..c7816a5b25 100644 --- a/engine/src/flutter/lib/ui/painting/path.h +++ b/engine/src/flutter/lib/ui/painting/path.h @@ -7,6 +7,7 @@ #include "flutter/lib/ui/dart_wrapper.h" #include "flutter/lib/ui/painting/rrect.h" +#include "flutter/lib/ui/painting/rsuperellipse.h" #include "flutter/lib/ui/ui_dart_state.h" #include "third_party/skia/include/core/SkPath.h" #include "third_party/skia/include/pathops/SkPathOps.h" @@ -88,6 +89,7 @@ class CanvasPath : public RefCountedDartWrappable { double sweepAngle); void addPolygon(const tonic::Float32List& points, bool close); void addRRect(const RRect& rrect); + void addRSuperellipse(const RSuperellipse* rse); void addPath(CanvasPath* path, double dx, double dy); void addPathWithMatrix(CanvasPath* path, diff --git a/engine/src/flutter/lib/ui/painting/rsuperellipse.h b/engine/src/flutter/lib/ui/painting/rsuperellipse.h index 9cf9959e89..78f5a60e7b 100644 --- a/engine/src/flutter/lib/ui/painting/rsuperellipse.h +++ b/engine/src/flutter/lib/ui/painting/rsuperellipse.h @@ -35,8 +35,11 @@ class RSuperellipse : public RefCountedDartWrappable { ~RSuperellipse() override; bool contains(double x, double y); + flutter::DlRoundSuperellipse rsuperellipse() const; impeller::RoundSuperellipseParam param() const; + flutter::DlRect bounds() const { return bounds_; } + impeller::RoundingRadii radii() const { return radii_; } private: RSuperellipse(flutter::DlRect bounds, impeller::RoundingRadii radii); diff --git a/engine/src/flutter/lib/web_ui/lib/path.dart b/engine/src/flutter/lib/web_ui/lib/path.dart index 6cc47f222d..6907556616 100644 --- a/engine/src/flutter/lib/web_ui/lib/path.dart +++ b/engine/src/flutter/lib/web_ui/lib/path.dart @@ -41,6 +41,7 @@ abstract class Path { void addArc(Rect oval, double startAngle, double sweepAngle); void addPolygon(List points, bool close); void addRRect(RRect rrect); + void addRSuperellipse(RSuperellipse rsuperellipse); void addPath(Path path, Offset offset, {Float64List? matrix4}); void extendWithPath(Path path, Offset offset, {Float64List? matrix4}); void close(); diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/path.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/path.dart index cab184d802..16b8be4af9 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/path.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/path.dart @@ -107,6 +107,13 @@ class CkPath implements ScenePath { skiaObject.addRRect(toSkRRect(rrect), false); } + @override + void addRSuperellipse(ui.RSuperellipse rsuperellipse) { + // TODO(dkwingsmt): Properly implement RSuperellipse on Web instead of falling + // back to RRect. https://github.com/flutter/flutter/issues/163718 + addRRect(rsuperellipse.toApproximateRRect()); + } + @override void addRect(ui.Rect rect) { skiaObject.addRect(toSkRect(rect)); diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/path.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/path.dart index f327ac12e3..b1753ddb7e 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/path.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/path.dart @@ -170,6 +170,13 @@ class SkwasmPath extends SkwasmObjectWrapper implements ScenePath { }); } + @override + void addRSuperellipse(ui.RSuperellipse rsuperellipse) { + // TODO(dkwingsmt): Properly implement RSuperellipse on Web instead of falling + // back to RRect. https://github.com/flutter/flutter/issues/163718 + addRRect(rsuperellipse.toApproximateRRect()); + } + @override void addPath(ui.Path path, ui.Offset offset, {Float64List? matrix4}) { _addPath(path, offset, false, matrix4: matrix4); diff --git a/engine/src/flutter/lib/web_ui/test/engine/scene_view_test.dart b/engine/src/flutter/lib/web_ui/test/engine/scene_view_test.dart index cd548708c1..1fb5e2de9d 100644 --- a/engine/src/flutter/lib/web_ui/test/engine/scene_view_test.dart +++ b/engine/src/flutter/lib/web_ui/test/engine/scene_view_test.dart @@ -229,6 +229,9 @@ class StubPath implements ScenePath { @override void addRRect(ui.RRect rrect) => throw UnimplementedError(); + @override + void addRSuperellipse(ui.RSuperellipse rsuperellipse) => throw UnimplementedError(); + @override void addPath(ui.Path path, ui.Offset offset, {Float64List? matrix4}) => throw UnimplementedError(); diff --git a/engine/src/flutter/testing/dart/geometry_test.dart b/engine/src/flutter/testing/dart/geometry_test.dart index fda880dbb7..ac6897f469 100644 --- a/engine/src/flutter/testing/dart/geometry_test.dart +++ b/engine/src/flutter/testing/dart/geometry_test.dart @@ -574,7 +574,7 @@ void main() { expect(rrect.brRadiusY, 0); }); - test('RSuperellipse.contains NoCornerRoundSuperellipseContains', () { + test('RSuperellipse.contains is correct with no corners', () { // RSuperellipse of bounds with no corners contains corners just barely. const RSuperellipse rse = RSuperellipse.fromLTRBXY(-50, -50, 50, 50, 0, 0); @@ -594,7 +594,7 @@ void main() { expect(rse.contains(const Offset(50, 50.01)), isFalse); }); - test('RSuperellipse.contains TinyCornerContains', () { + test('RSuperellipse.contains is correct with tiny corners', () { // RSuperellipse of bounds with even the tiniest corners does not contain corners. const RSuperellipse rse = RSuperellipse.fromLTRBXY(-50, -50, 50, 50, 0.01, 0.01); @@ -604,7 +604,7 @@ void main() { expect(rse.contains(const Offset(50, 50)), isFalse); }); - test('RSuperellipse.contains UniformSquareContains', () { + test('RSuperellipse.contains is correct with uniform corners', () { const RSuperellipse rse = RSuperellipse.fromLTRBXY(-50, -50, 50, 50, 5.0, 5.0); void checkPointAndMirrors(Offset p) { @@ -623,7 +623,7 @@ void main() { checkPointAndMirrors(const Offset(49.995, 0)); // Right }); - test('RSuperellipse.contains UniformEllipticalContains', () { + test('RSuperellipse.contains is correct with uniform elliptical corners', () { const RSuperellipse rse = RSuperellipse.fromLTRBXY(-50, -50, 50, 50, 5.0, 10.0); void checkPointAndMirrors(Offset p) { @@ -642,7 +642,7 @@ void main() { checkPointAndMirrors(const Offset(49.995, 0)); // Right }); - test('RSuperellipse.contains UniformRectangularContains', () { + test('RSuperellipse.contains is correct with uniform corners and unequal height and width', () { // The bounds is not centered at the origin and has unequal height and width. const RSuperellipse rse = RSuperellipse.fromLTRBXY(0, 0, 50, 100, 23.0, 30.0); @@ -666,7 +666,7 @@ void main() { checkPointAndMirrors(const Offset(49.99, 49.99)); // Right mid-edge }); - test('RSuperellipse.contains SlimDiagnalContains', () { + test('RSuperellipse.contains is correct for a slim diagnal shape', () { // 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). final RSuperellipse rse = RSuperellipse.fromLTRBAndCorners( diff --git a/engine/src/flutter/testing/dart/path_test.dart b/engine/src/flutter/testing/dart/path_test.dart index 297729ce75..0132d816c0 100644 --- a/engine/src/flutter/testing/dart/path_test.dart +++ b/engine/src/flutter/testing/dart/path_test.dart @@ -257,4 +257,52 @@ void main() { 'PathMetric(length: 120.0, isClosed: true, contourIndex: 1))', ); }); + + test('RSuperellipse path is correct for a slim diagnal shape', () { + // This test mirrors a similar test from "geometry_test.dart" and serves as + // a smoke test. + final RSuperellipse rsuperellipse = RSuperellipse.fromLTRBAndCorners( + -50, + -50, + 50, + 50, + topLeft: const Radius.circular(1.0), + topRight: const Radius.circular(99.0), + bottomLeft: const Radius.circular(99.0), + bottomRight: const Radius.circular(1.0), + ); + final Path path = + Path() + ..addRSuperellipse(rsuperellipse) + ..close(); + + expect(path.contains(Offset.zero), isTrue); + expect(path.contains(const Offset(-49.999, -49.999)), isFalse); + expect(path.contains(const Offset(-49.999, 49.999)), isFalse); + expect(path.contains(const Offset(49.999, 49.999)), isFalse); + expect(path.contains(const Offset(49.999, -49.999)), isFalse); + + // The pointy ends at the NE and SW corners + checkPointWithOffset(path, const Offset(-49.70, -49.70), const Offset(-0.02, -0.02)); + checkPointWithOffset(path, const Offset(49.70, 49.70), const Offset(0.02, 0.02)); + + // Checks two points symmetrical to the origin. + void checkDiagnalPoints(Offset p) { + checkPointWithOffset(path, p, const Offset(0.02, -0.02)); + checkPointWithOffset(path, Offset(-p.dx, -p.dy), const Offset(-0.02, 0.02)); + } + + // A few other points along the edge + checkDiagnalPoints(const Offset(-40.0, -49.59)); + checkDiagnalPoints(const Offset(-20.0, -45.64)); + checkDiagnalPoints(const Offset(0.0, -37.01)); + checkDiagnalPoints(const Offset(20.0, -21.96)); + checkDiagnalPoints(const Offset(21.05, -20.92)); + checkDiagnalPoints(const Offset(40.0, 5.68)); + }); +} + +void checkPointWithOffset(Path path, Offset inPoint, Offset outwardOffset) { + expect(path.contains(inPoint), isTrue); + expect(path.contains(inPoint + outwardOffset), isFalse); }