[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:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = ¶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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
//
|
||||
|
||||
@@ -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) \
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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."));
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user