[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).

<details>
<summary>
Obsolete description, no longer applicable
</summary>
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.
</details>

## Pre-launch Checklist

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

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

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
This commit is contained in:
Tong Mu
2025-04-02 14:27:08 -07:00
committed by GitHub
parent 9a6ae31e1e
commit f2e0a2afb3
16 changed files with 318 additions and 226 deletions

View File

@@ -1260,7 +1260,18 @@ void DisplayListBuilder::drawRoundSuperellipse(const DlRoundSuperellipse& rse) {
OpResult result = PaintResult(current_, flags);
if (result != OpResult::kNoEffect &&
AccumulateOpBounds(rse.GetBounds(), flags)) {
Push<DrawRoundSuperellipseOp>(0, rse);
// DrawRoundSuperellipseOp only supports filling. Anything related to
// stroking must use path approximation.
if (current_.getDrawStyle() == DlDrawStyle::kFill) {
Push<DrawRoundSuperellipseOp>(0, rse);
} else {
DlPathBuilder builder;
builder.SetConvexity(impeller::Convexity::kConvex);
builder.SetBounds(rse.GetBounds());
builder.AddRoundSuperellipse(DlRoundSuperellipse::MakeRectRadii(
rse.GetBounds(), rse.GetRadii()));
Push<DrawPathOp>(0, DlPath(builder.TakePath()));
}
CheckLayerOpacityCompatibility();
UpdateLayerResult(result);
}

View File

@@ -753,17 +753,9 @@ std::vector<DisplayListInvocationGroup> 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,

View File

@@ -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<void(const Point&)>;
// Create a builder.
//
// The resulting curves, which consists of cubic curves, are added by calling
// `cubic_adder`.
explicit RoundSuperellipseBuilder(CubicAdder cubic_adder,
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<Point, 4> 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<Scalar, 2> factors = SuperellipseBezierFactors(param.se_n);
return std::array<Point, 4>{
start, start + start_tangent * factors[0] * param.se_a,
end + end_tangent * factors[1] * param.se_a, end};
};
std::array<Point, 4> CircularArcPoints(
const RoundSuperellipseParam::Octant& param) {
Point start_vector = param.circle_start - param.circle_center;
Point end_vector =
start_vector.Rotate(Radians(-param.circle_max_angle.radians));
Point circle_end = param.circle_center + end_vector;
Point start_tangent = Point{start_vector.y, -start_vector.x}.Normalize();
Point end_tangent = Point{-end_vector.y, end_vector.x}.Normalize();
Scalar bezier_factor = std::tan(param.circle_max_angle.radians / 4) * 4 / 3;
Scalar radius = start_vector.GetLength();
return std::array<Point, 4>{
param.circle_start,
param.circle_start + start_tangent * bezier_factor * radius,
circle_end + end_tangent * bezier_factor * radius, circle_end};
};
// Draws an arc representing 1/8 of a rounded superellipse.
//
// If `reverse` is false, the resulting arc spans from 0 to pi/4, moving
// clockwise starting from the positive Y-axis. Otherwise it moves from pi/4
// to 0.
//
// If `flip` is true, all points have their X and Y coordinates swapped,
// effectively mirrowing each point by the y=x line.
//
// All points are transformed by `external_transform` after the optional
// flipping before being used as control points for the cubic curves.
void AddOctant(const RoundSuperellipseParam::Octant& param,
bool reverse,
bool flip,
const Matrix& external_transform) {
Matrix transform =
external_transform * Matrix::MakeTranslation(param.offset);
if (flip) {
transform = transform * kFlip;
}
auto circle_points = CircularArcPoints(param);
auto se_points = SuperellipseArcPoints(param);
if (!reverse) {
cubic_adder_(transform * se_points[0], transform * se_points[1],
transform * se_points[2], transform * se_points[3]);
cubic_adder_(transform * circle_points[0], transform * circle_points[1],
transform * circle_points[2], transform * circle_points[3]);
} else {
cubic_adder_(transform * circle_points[3], transform * circle_points[2],
transform * circle_points[1], transform * circle_points[0]);
cubic_adder_(transform * se_points[3], transform * se_points[2],
transform * se_points[1], transform * se_points[0]);
}
};
// Get the Bezier factor for the superellipse arc in a rounded superellipse.
//
// The result will be assigned to output, where [0] will be the factor for the
// starting tangent and [1] for the ending tangent.
//
// These values are computed by brute-force searching for the minimal distance
// on a rounded superellipse and are not for general purpose superellipses.
std::array<Scalar, 2> SuperellipseBezierFactors(Scalar n) {
constexpr Scalar kPrecomputedVariables[][2] = {
/*n=2.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<Scalar>((n - kMinN) / kStep, 0, kNumRecords - 1);
size_t left = std::clamp<size_t>(static_cast<size_t>(std::floor(steps)), 0,
kNumRecords - 2);
Scalar frac = steps - left;
return std::array<Scalar, 2>{(1 - frac) * kPrecomputedVariables[left][0] +
frac * kPrecomputedVariables[left + 1][0],
(1 - frac) * kPrecomputedVariables[left][1] +
frac * kPrecomputedVariables[left + 1][1]};
}
CubicAdder cubic_adder_;
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 = &param.top_right;
builder.AddQuadrant(*quadrant, /*reverse=*/false);
quadrant->signed_scale.y *= -1;
builder.AddQuadrant(*quadrant, /*reverse=*/true);
quadrant->signed_scale.x *= -1;
builder.AddQuadrant(*quadrant, /*reverse=*/false);
quadrant->signed_scale.y *= -1;
builder.AddQuadrant(*quadrant, /*reverse=*/true);
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;
}

View File

@@ -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<Point, 4> 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<Scalar, 2> factors = SuperellipseBezierFactors(param.se_n);
return std::array<Point, 4>{
start, start + start_tangent * factors[0] * param.se_a,
end + end_tangent * factors[1] * param.se_a, end};
};
std::array<Point, 4> CircularArcPoints(
const RoundSuperellipseParam::Octant& param) {
Point start_vector = param.circle_start - param.circle_center;
Point end_vector =
start_vector.Rotate(Radians(-param.circle_max_angle.radians));
Point circle_end = param.circle_center + end_vector;
Point start_tangent = Point{start_vector.y, -start_vector.x}.Normalize();
Point end_tangent = Point{-end_vector.y, end_vector.x}.Normalize();
Scalar bezier_factor = std::tan(param.circle_max_angle.radians / 4) * 4 / 3;
Scalar radius = start_vector.GetLength();
return std::array<Point, 4>{
param.circle_start,
param.circle_start + start_tangent * bezier_factor * radius,
circle_end + end_tangent * bezier_factor * radius, circle_end};
};
// Draws an arc representing 1/8 of a rounded superellipse.
//
// If `reverse` is false, the resulting arc spans from 0 to pi/4, moving
// clockwise starting from the positive Y-axis. Otherwise it moves from pi/4
// to 0.
//
// If `flip` is true, all points have their X and Y coordinates swapped,
// effectively mirrowing each point by the y=x line.
//
// All points are transformed by `external_transform` after the optional
// flipping before being used as control points for the cubic curves.
void AddOctant(const RoundSuperellipseParam::Octant& param,
bool reverse,
bool flip,
const Matrix& external_transform) {
Matrix transform =
external_transform * Matrix::MakeTranslation(param.offset);
if (flip) {
transform = transform * kFlip;
}
auto circle_points = CircularArcPoints(param);
auto se_points = SuperellipseArcPoints(param);
if (!reverse) {
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<Scalar, 2> 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<Scalar>((n - kMinN) / kStep, 0, kNumRecords - 1);
size_t left = std::clamp<size_t>(static_cast<size_t>(std::floor(steps)), 0,
kNumRecords - 2);
Scalar frac = steps - left;
return std::array<Scalar, 2>{(1 - frac) * kPrecomputedVariables[left][0] +
frac * kPrecomputedVariables[left + 1][0],
(1 - frac) * kPrecomputedVariables[left][1] +
frac * kPrecomputedVariables[left + 1][1]};
}
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);

View File

@@ -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.
//

View File

@@ -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) \

View File

@@ -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<Void Function(Pointer<Void>, Handle)>(symbol: 'Path::addRRect')
external void _addRRect(Float32List rrect);
@override
void addRSuperellipse(RSuperellipse rsuperellipse) {
assert(_rsuperellipseIsValid(rsuperellipse));
_addRSuperellipse(rsuperellipse._native());
}
@Native<Void Function(Pointer<Void>, Pointer<Void>)>(symbol: 'Path::addRSuperellipse')
external void _addRSuperellipse(_NativeRSuperellipse rsuperellipse);
@override
void addPath(Path path, Offset offset, {Float64List? matrix4}) {
assert(_offsetIsValid(offset));

View File

@@ -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."));

View File

@@ -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<CanvasPath> {
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,

View File

@@ -35,8 +35,11 @@ class RSuperellipse : public RefCountedDartWrappable<RSuperellipse> {
~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);

View File

@@ -41,6 +41,7 @@ abstract class Path {
void addArc(Rect oval, double startAngle, double sweepAngle);
void addPolygon(List<Offset> 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();

View File

@@ -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));

View File

@@ -170,6 +170,13 @@ class SkwasmPath extends SkwasmObjectWrapper<RawPath> 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);

View File

@@ -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();

View File

@@ -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(

View File

@@ -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);
}