mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Improve the algorithm for rounded superellipse paths to work better at very large ratio (#180453)
This PR uses a new algorithm that approximates the superellipse curve with two conic curves, instead of one cubic curves. This algorithm works much better at very large ratio. Fixes https://github.com/flutter/flutter/issues/179875 . A new playground is added in this PR to show enlarged corner of a rounded superellipse at very large ratio and compares its path version and filled version. <details> <summary> For comparison: The best result possible with one cubic curve </summary> As the following video shows, the flat segment of the superellipse curve is the hardest to approximate, since most of its curvature happens near the right end. The best that a single cubic curve can do is close to a straight line. https://github.com/user-attachments/assets/2ed1666d-f96c-4465-9d5c-1f8f4a660bba Although the largest deviation is only ~1e-6, the "feeling" from the lack of curvature is sensible as shown in the following screenshot. <img width="476" height="543" alt="image" src="https://github.com/user-attachments/assets/6e6b96ed-80b3-487c-b621-3cddd4537ec0" /> </details> **Result after this PR:** https://github.com/user-attachments/assets/d0012e62-f61f-4d69-95a5-27646c0bb750 The following screenshot shows the reproduction app as commented in https://github.com/flutter/flutter/issues/179875#issuecomment-3672426282 after the PR: <img width="476" height="543" alt="image" src="https://github.com/user-attachments/assets/d93b83be-a9fc-4b25-aca2-0e2c3f9a7e56" /> For comparison, the following screenshot shows the same shape drawn as filled: <img width="476" height="543" alt="image" src="https://github.com/user-attachments/assets/bf94a20a-278b-42a2-9162-5aa4a61915af" /> **Before the PR:** <img width="1024" height="796" alt="image" src="https://github.com/user-attachments/assets/053cc229-01f1-4181-97a6-48b96bf16fcf" /> Also, a new golden test: <img width="2400" height="1800" alt="image" src="https://github.com/user-attachments/assets/6e741066-89a3-4282-bdc2-fa02b1eea14d" /> ## 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]. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. <!-- 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:
parent
ea614598f1
commit
5d93b2e71b
@ -10,6 +10,11 @@
|
||||
|
||||
namespace {
|
||||
using ::testing::Return;
|
||||
|
||||
MATCHER_P(ScalarEq, a, "") {
|
||||
*result_listener << "isn't equal to " << a;
|
||||
return abs(arg - a) <= impeller::kEhCloseEnough;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
namespace flutter {
|
||||
@ -316,55 +321,112 @@ TEST(DisplayListPathBuilder, AddRoundSuperellipse) {
|
||||
::testing::InSequence sequence;
|
||||
|
||||
EXPECT_CALL(mock_receiver, MoveTo(DlPoint(46, 10), true));
|
||||
EXPECT_CALL(mock_receiver, CubicTo(PointEq(DlPoint(97.3149f, 9.99998f)),
|
||||
PointEq(DlPoint(95.8867f, 9.99486f)),
|
||||
PointEq(DlPoint(97.1856f, 10.2338f))));
|
||||
|
||||
// 1
|
||||
EXPECT_CALL(mock_receiver, ConicTo(PointEq(DlPoint(94.8672, 9.99998)),
|
||||
PointEq(DlPoint(95.7708, 10.0493)),
|
||||
ScalarEq(3.63127851)))
|
||||
.WillOnce(Return(true));
|
||||
EXPECT_CALL(mock_receiver, ConicTo(PointEq(DlPoint(96.6558, 10.0976)),
|
||||
PointEq(DlPoint(97.1856, 10.2338)),
|
||||
ScalarEq(1.22087204)))
|
||||
.WillOnce(Return(true));
|
||||
EXPECT_CALL(mock_receiver, CubicTo(PointEq(DlPoint(97.9096f, 10.367f)),
|
||||
PointEq(DlPoint(98.5976f, 11.6373f)),
|
||||
PointEq(DlPoint(99.1213f, 13.8076f))));
|
||||
EXPECT_CALL(mock_receiver, CubicTo(PointEq(DlPoint(99.5784f, 15.7987f)),
|
||||
PointEq(DlPoint(99.8618f, 18.4156f)),
|
||||
PointEq(DlPoint(99.9232f, 21.2114f))));
|
||||
EXPECT_CALL(mock_receiver, CubicTo(PointEq(DlPoint(100.011f, 25.1954f)),
|
||||
PointEq(DlPoint(100.0f, 29.7897f)),
|
||||
PointEq(DlPoint(100.0f, 51.7857f))));
|
||||
EXPECT_CALL(mock_receiver, CubicTo(PointEq(DlPoint(100.0f, 77.1657f)),
|
||||
PointEq(DlPoint(100.018f, 82.4668f)),
|
||||
PointEq(DlPoint(99.872f, 87.0638f))));
|
||||
EXPECT_CALL(mock_receiver, CubicTo(PointEq(DlPoint(99.7697f, 90.2897f)),
|
||||
PointEq(DlPoint(99.2973f, 93.3092f)),
|
||||
PointEq(DlPoint(98.5355f, 95.6066f))));
|
||||
EXPECT_CALL(mock_receiver, CubicTo(PointEq(DlPoint(97.6834f, 98.0734f)),
|
||||
PointEq(DlPoint(96.5625f, 99.532f)),
|
||||
PointEq(DlPoint(95.3799f, 99.7131f))));
|
||||
EXPECT_CALL(mock_receiver, CubicTo(PointEq(DlPoint(93.4105f, 100.015f)),
|
||||
PointEq(DlPoint(93.4217f, 100.0f)),
|
||||
PointEq(DlPoint(50.0f, 100.0f))));
|
||||
EXPECT_CALL(mock_receiver, CubicTo(PointEq(DlPoint(15.2626f, 100.0f)),
|
||||
PointEq(DlPoint(15.2716f, 100.014f)),
|
||||
PointEq(DlPoint(13.6961f, 99.7323f))));
|
||||
EXPECT_CALL(mock_receiver, CubicTo(PointEq(DlPoint(12.75f, 99.5632f)),
|
||||
PointEq(DlPoint(11.8533f, 98.2018f)),
|
||||
PointEq(DlPoint(11.1716f, 95.8995f))));
|
||||
EXPECT_CALL(mock_receiver, CubicTo(PointEq(DlPoint(10.5569f, 93.7323f)),
|
||||
PointEq(DlPoint(10.1777f, 90.8822f)),
|
||||
PointEq(DlPoint(10.0993f, 87.8409f))));
|
||||
EXPECT_CALL(mock_receiver, CubicTo(PointEq(DlPoint(9.98623f, 83.4541f)),
|
||||
PointEq(DlPoint(10.0f, 78.5304f)),
|
||||
PointEq(DlPoint(10.0f, 51.5385f))));
|
||||
EXPECT_CALL(mock_receiver, CubicTo(PointEq(DlPoint(10.0f, 28.4025f)),
|
||||
PointEq(DlPoint(9.99311f, 24.1822f)),
|
||||
PointEq(DlPoint(10.0496f, 20.4221f))));
|
||||
EXPECT_CALL(mock_receiver, CubicTo(PointEq(DlPoint(10.0888f, 17.8153f)),
|
||||
PointEq(DlPoint(10.2785f, 15.3723f)),
|
||||
PointEq(DlPoint(10.5858f, 13.5147f))));
|
||||
EXPECT_CALL(mock_receiver, CubicTo(PointEq(DlPoint(10.9349f, 11.5114f)),
|
||||
PointEq(DlPoint(11.3936f, 10.3388f)),
|
||||
PointEq(DlPoint(11.8762f, 10.2158f))));
|
||||
EXPECT_CALL(mock_receiver, CubicTo(PointEq(DlPoint(12.7422f, 9.99527f)),
|
||||
PointEq(DlPoint(11.79f, 10.0f)),
|
||||
PointEq(DlPoint(46.0f, 10.0f))));
|
||||
EXPECT_CALL(mock_receiver, LineTo(DlPoint(46, 10)));
|
||||
PointEq(DlPoint(98.5976, 11.6373)),
|
||||
PointEq(DlPoint(99.1213, 13.8076))));
|
||||
|
||||
// 2
|
||||
EXPECT_CALL(mock_receiver, CubicTo(PointEq(DlPoint(99.5784, 15.7987)),
|
||||
PointEq(DlPoint(99.8618, 18.4156)),
|
||||
PointEq(DlPoint(99.9232, 21.2114))));
|
||||
EXPECT_CALL(mock_receiver, ConicTo(PointEq(DlPoint(99.9636, 23.0102)),
|
||||
PointEq(DlPoint(99.9805, 25.6229)),
|
||||
ScalarEq(1.17059147)))
|
||||
.WillOnce(Return(true));
|
||||
EXPECT_CALL(mock_receiver,
|
||||
ConicTo(PointEq(DlPoint(100, 28.6213)),
|
||||
PointEq(DlPoint(100, 51.7857)), ScalarEq(2.12785244)))
|
||||
.WillOnce(Return(true));
|
||||
|
||||
// 3
|
||||
EXPECT_CALL(mock_receiver, ConicTo(PointEq(DlPoint(100, 78.514)),
|
||||
PointEq(DlPoint(99.9675, 81.9736)),
|
||||
ScalarEq(2.12785244)))
|
||||
.WillOnce(Return(true));
|
||||
EXPECT_CALL(mock_receiver, ConicTo(PointEq(DlPoint(99.9393, 84.9882)),
|
||||
PointEq(DlPoint(99.872, 87.0638)),
|
||||
ScalarEq(1.17059147)))
|
||||
.WillOnce(Return(true));
|
||||
EXPECT_CALL(mock_receiver, CubicTo(PointEq(DlPoint(99.7697, 90.2897)),
|
||||
PointEq(DlPoint(99.2973, 93.3092)),
|
||||
PointEq(DlPoint(98.5355, 95.6066))));
|
||||
|
||||
// 4
|
||||
EXPECT_CALL(mock_receiver, CubicTo(PointEq(DlPoint(97.6834, 98.0734)),
|
||||
PointEq(DlPoint(96.5625, 99.532)),
|
||||
PointEq(DlPoint(95.3799, 99.7131))));
|
||||
EXPECT_CALL(mock_receiver, ConicTo(PointEq(DlPoint(94.5196, 99.8799)),
|
||||
PointEq(DlPoint(93.1037, 99.9395)),
|
||||
ScalarEq(1.16898668)))
|
||||
.WillOnce(Return(true));
|
||||
EXPECT_CALL(mock_receiver,
|
||||
ConicTo(PointEq(DlPoint(91.6668, 100)),
|
||||
PointEq(DlPoint(50, 100)), ScalarEq(3.63127851)))
|
||||
.WillOnce(Return(true));
|
||||
|
||||
// 5
|
||||
EXPECT_CALL(mock_receiver, ConicTo(PointEq(DlPoint(16.6667, 100)),
|
||||
PointEq(DlPoint(15.5171, 99.9435)),
|
||||
ScalarEq(3.63127851)))
|
||||
.WillOnce(Return(true));
|
||||
EXPECT_CALL(mock_receiver, ConicTo(PointEq(DlPoint(14.3843, 99.8879)),
|
||||
PointEq(DlPoint(13.6961, 99.7323)),
|
||||
ScalarEq(1.16898668)))
|
||||
.WillOnce(Return(true));
|
||||
EXPECT_CALL(mock_receiver, CubicTo(PointEq(DlPoint(12.75, 99.5632)),
|
||||
PointEq(DlPoint(11.8533, 98.2018)),
|
||||
PointEq(DlPoint(11.1716, 95.8995))));
|
||||
|
||||
// 6
|
||||
EXPECT_CALL(mock_receiver, CubicTo(PointEq(DlPoint(10.5569, 93.7323)),
|
||||
PointEq(DlPoint(10.1777, 90.8822)),
|
||||
PointEq(DlPoint(10.0993, 87.8409))));
|
||||
EXPECT_CALL(mock_receiver, ConicTo(PointEq(DlPoint(10.0462, 85.8435)),
|
||||
PointEq(DlPoint(10.0244, 82.8947)),
|
||||
ScalarEq(1.2416352)))
|
||||
.WillOnce(Return(true));
|
||||
EXPECT_CALL(mock_receiver,
|
||||
ConicTo(PointEq(DlPoint(10, 79.594)),
|
||||
PointEq(DlPoint(10, 51.5385)), ScalarEq(1.77972043)))
|
||||
.WillOnce(Return(true));
|
||||
|
||||
// 7
|
||||
EXPECT_CALL(mock_receiver, ConicTo(PointEq(DlPoint(10, 27.4909)),
|
||||
PointEq(DlPoint(10.0122, 24.6616)),
|
||||
ScalarEq(1.77972043)))
|
||||
.WillOnce(Return(true));
|
||||
EXPECT_CALL(mock_receiver, ConicTo(PointEq(DlPoint(10.0231, 22.1342)),
|
||||
PointEq(DlPoint(10.0496, 20.4221)),
|
||||
ScalarEq(1.2416352)))
|
||||
.WillOnce(Return(true));
|
||||
EXPECT_CALL(mock_receiver, CubicTo(PointEq(DlPoint(10.0888, 17.8153)),
|
||||
PointEq(DlPoint(10.2785, 15.3723)),
|
||||
PointEq(DlPoint(10.5858, 13.5147))));
|
||||
|
||||
// 8
|
||||
EXPECT_CALL(mock_receiver, CubicTo(PointEq(DlPoint(10.9349, 11.5114)),
|
||||
PointEq(DlPoint(11.3936, 10.3388)),
|
||||
PointEq(DlPoint(11.8762, 10.2158))));
|
||||
EXPECT_CALL(mock_receiver, ConicTo(PointEq(DlPoint(12.2295, 10.0901)),
|
||||
PointEq(DlPoint(12.8195, 10.0455)),
|
||||
ScalarEq(1.22087204)))
|
||||
.WillOnce(Return(true));
|
||||
EXPECT_CALL(mock_receiver,
|
||||
ConicTo(PointEq(DlPoint(13.4216, 10)), PointEq(DlPoint(46, 10)),
|
||||
ScalarEq(3.63127851)))
|
||||
.WillOnce(Return(true));
|
||||
|
||||
EXPECT_CALL(mock_receiver, LineTo(PointEq(DlPoint(46, 10))));
|
||||
EXPECT_CALL(mock_receiver, Close());
|
||||
}
|
||||
|
||||
|
||||
@ -2622,6 +2622,104 @@ TEST_P(EntityTest, DrawRoundSuperEllipse) {
|
||||
ASSERT_TRUE(OpenPlaygroundHere(callback));
|
||||
}
|
||||
|
||||
TEST_P(EntityTest, DrawRoundSuperEllipseWithLargeN) {
|
||||
// This playground shows the enlarged corner of a rounded superellipse to
|
||||
// compare pathing algorithm against the filling algorithm (benchmark) at very
|
||||
// large ratio.
|
||||
auto callback = [&](ContentContext& context, RenderPass& pass) -> bool {
|
||||
constexpr float corner_radius = 100.0;
|
||||
|
||||
// UI state.
|
||||
static float logarithm_of_ratio = 1.5; // ratio = size / corner_radius
|
||||
|
||||
float ratio = std::exp(logarithm_of_ratio);
|
||||
|
||||
float rect_size = corner_radius * ratio;
|
||||
Rect rect = Rect::MakeLTRB(0, 0, rect_size, rect_size);
|
||||
constexpr float screen_canvas_padding = 200.0f;
|
||||
constexpr float screen_canvas_size = 1000.0f;
|
||||
|
||||
// Scale so that "corner radius" is as long as half the canvas.
|
||||
float scale = screen_canvas_size / 2 / corner_radius;
|
||||
|
||||
ImGui::Begin("Controls", nullptr, ImGuiWindowFlags_AlwaysAutoResize);
|
||||
{
|
||||
ImGui::SliderFloat("log(Ratio)", &logarithm_of_ratio, 1.0, 8.0);
|
||||
ImGui::LabelText("Ratio", "%.2g", static_cast<double>(ratio));
|
||||
ImGui::Text(" where Ratio = RectSize / CornerRadius");
|
||||
}
|
||||
ImGui::End();
|
||||
|
||||
auto top_right = Vector2(screen_canvas_size * 1.3f, screen_canvas_padding);
|
||||
auto transform = Matrix::MakeTranslation(top_right) *
|
||||
Matrix::MakeScale(Vector2(scale, scale)) *
|
||||
Matrix::MakeTranslation(Vector2(-rect_size, 0));
|
||||
bool success = true;
|
||||
|
||||
auto fill_geom =
|
||||
std::make_unique<RoundSuperellipseGeometry>(rect, corner_radius);
|
||||
// Fill
|
||||
{
|
||||
auto contents = std::make_shared<SolidColorContents>();
|
||||
contents->SetColor(Color::Red());
|
||||
contents->SetGeometry(fill_geom.get());
|
||||
|
||||
Entity entity;
|
||||
entity.SetContents(contents);
|
||||
entity.SetTransform(transform);
|
||||
|
||||
success = success && entity.Render(context, pass);
|
||||
}
|
||||
|
||||
// Stroke
|
||||
auto path = flutter::DlPath::MakeRoundSuperellipse(
|
||||
RoundSuperellipse::MakeRectRadius(rect, corner_radius));
|
||||
auto stroke_geom = Geometry::MakeStrokePath(path, {.width = 2 / scale});
|
||||
{
|
||||
auto contents = std::make_shared<SolidColorContents>();
|
||||
contents->SetColor(Color::Blue());
|
||||
contents->SetGeometry(stroke_geom.get());
|
||||
|
||||
Entity entity;
|
||||
entity.SetContents(contents);
|
||||
entity.SetTransform(transform);
|
||||
|
||||
success = success && entity.Render(context, pass);
|
||||
}
|
||||
|
||||
// Draw a ruler to show the length in portion of rect size.
|
||||
auto screen_top_right =
|
||||
Matrix::MakeScale(GetContentScale()).Invert() * top_right;
|
||||
constexpr float font_size = 13.0f;
|
||||
for (int i = -1; i < 100; i++) {
|
||||
float screen_offset_y = static_cast<float>(i) * 20.0f;
|
||||
std::string label;
|
||||
if (i == -1) {
|
||||
label = "Ruler: (in portion of rect size)";
|
||||
} else if (i == 0) {
|
||||
label = "- 0.0";
|
||||
} else {
|
||||
float portion_of_rect =
|
||||
screen_offset_y * GetContentScale().y / scale / rect_size;
|
||||
label = std::format("- {:.2g}", portion_of_rect);
|
||||
}
|
||||
ImGui::GetBackgroundDrawList()->AddText(
|
||||
nullptr, font_size,
|
||||
// Draw the ruler at around the flat part of the curve, which is
|
||||
// somewhere to the left of the top right corner.
|
||||
//
|
||||
// Offset vertically by font_size/2 so that the hyphen aligns with the
|
||||
// top of shape.
|
||||
{screen_top_right.x - 500,
|
||||
screen_top_right.y + screen_offset_y - font_size / 2},
|
||||
IM_COL32_WHITE, label.c_str());
|
||||
}
|
||||
return success;
|
||||
};
|
||||
|
||||
ASSERT_TRUE(OpenPlaygroundHere(callback));
|
||||
}
|
||||
|
||||
TEST_P(EntityTest, CanDrawRoundSuperEllipseWithTinyRadius) {
|
||||
// Regression test for https://github.com/flutter/flutter/issues/176894
|
||||
// Verify that a radius marginally below the minimum threshold can be
|
||||
|
||||
@ -306,6 +306,19 @@ bool CornerContains(const RoundSuperellipseParam::Quadrant& param,
|
||||
}
|
||||
|
||||
class RoundSuperellipseBuilder {
|
||||
private:
|
||||
// The parameters that describes a conic curve.
|
||||
struct ConicParam {
|
||||
// The end point closer to point A.
|
||||
Point p1;
|
||||
// The control point.
|
||||
Point c;
|
||||
// The end point closer to point J.
|
||||
Point p2;
|
||||
// The conic weight.
|
||||
Scalar weight;
|
||||
};
|
||||
|
||||
public:
|
||||
explicit RoundSuperellipseBuilder(PathReceiver& receiver)
|
||||
: receiver_(receiver) {}
|
||||
@ -346,21 +359,37 @@ class RoundSuperellipseBuilder {
|
||||
}
|
||||
|
||||
private:
|
||||
std::array<Point, 4> SuperellipseArcPoints(
|
||||
std::tuple<ConicParam, ConicParam> 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();
|
||||
// The superellipse arc consists of two conic curves: A-H and H-J.
|
||||
Point A = {0, param.se_a};
|
||||
const Point& J = param.circle_start;
|
||||
|
||||
std::array<Scalar, 2> factors = SuperellipseBezierFactors(param.se_n);
|
||||
std::tuple<Scalar, Scalar, Scalar> factors = SuperellipseBezierFactors(
|
||||
param.se_n, J.x / param.se_a, J.y / param.se_a);
|
||||
Scalar weight1 = std::get<0>(factors);
|
||||
Scalar weight2 = std::get<1>(factors);
|
||||
Scalar yHOverA = std::get<2>(factors);
|
||||
|
||||
return std::array<Point, 4>{
|
||||
start, start + start_tangent * factors[0] * param.se_a,
|
||||
end + end_tangent * factors[1] * param.se_a, end};
|
||||
};
|
||||
Point H = {
|
||||
powf(1.f - powf(yHOverA, param.se_n), 1.f / param.se_n) * param.se_a,
|
||||
yHOverA * param.se_a};
|
||||
|
||||
Scalar kA = 0;
|
||||
Scalar kJ = -powf(J.x / J.y, param.se_n - 1.f);
|
||||
Scalar kH = -powf(H.x / H.y, param.se_n - 1.f);
|
||||
|
||||
// The control points are determined by the intersection of the tangents for
|
||||
// smoothness.
|
||||
return {ConicParam{.p1 = A,
|
||||
.c = Intersection(A, kA, H, kH),
|
||||
.p2 = H,
|
||||
.weight = weight1},
|
||||
ConicParam{.p1 = H,
|
||||
.c = Intersection(H, kH, J, kJ),
|
||||
.p2 = J,
|
||||
.weight = weight2}};
|
||||
}
|
||||
|
||||
std::array<Point, 4> CircularArcPoints(
|
||||
const RoundSuperellipseParam::Octant& param) {
|
||||
@ -401,11 +430,15 @@ class RoundSuperellipseBuilder {
|
||||
}
|
||||
|
||||
auto circle_points = CircularArcPoints(param);
|
||||
auto se_points = SuperellipseArcPoints(param);
|
||||
auto se_conics = SuperellipseArcPoints(param);
|
||||
|
||||
if (!reverse) {
|
||||
receiver_.CubicTo(transform * se_points[1], transform * se_points[2],
|
||||
transform * se_points[3]);
|
||||
receiver_.ConicTo(transform * std::get<0>(se_conics).c,
|
||||
transform * std::get<0>(se_conics).p2,
|
||||
std::get<0>(se_conics).weight);
|
||||
receiver_.ConicTo(transform * std::get<1>(se_conics).c,
|
||||
transform * std::get<1>(se_conics).p2,
|
||||
std::get<1>(se_conics).weight);
|
||||
receiver_.CubicTo(transform * circle_points[1],
|
||||
transform * circle_points[2],
|
||||
transform * circle_points[3]);
|
||||
@ -413,34 +446,51 @@ class RoundSuperellipseBuilder {
|
||||
receiver_.CubicTo(transform * circle_points[2],
|
||||
transform * circle_points[1],
|
||||
transform * circle_points[0]);
|
||||
receiver_.CubicTo(transform * se_points[2], transform * se_points[1],
|
||||
transform * se_points[0]);
|
||||
receiver_.ConicTo(transform * std::get<1>(se_conics).c,
|
||||
transform * std::get<1>(se_conics).p1,
|
||||
std::get<1>(se_conics).weight);
|
||||
receiver_.ConicTo(transform * std::get<0>(se_conics).c,
|
||||
transform * std::get<0>(se_conics).p1,
|
||||
std::get<0>(se_conics).weight);
|
||||
}
|
||||
};
|
||||
|
||||
// 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}};
|
||||
// The resulting tuple consists of:
|
||||
// [0] weight1, weight for the conic A-H
|
||||
// [1] weight2, weight for the conic H-J
|
||||
// [2] yHOverA
|
||||
std::tuple<Scalar, Scalar, Scalar> SuperellipseBezierFactors(Scalar n,
|
||||
Scalar xJOverA,
|
||||
Scalar yJOverA) {
|
||||
// Precomputed (factor1, factor2) tuples for interpolation and
|
||||
// extrapolation.
|
||||
//
|
||||
// These factors are normalized conic weights defined as:
|
||||
// weight1 = factor1 * sqrt(n)
|
||||
// weight2 = factor2 * xJOverA
|
||||
//
|
||||
// Rationale: Empirical analysis shows these quotients converge to stable
|
||||
// constants as n -> infinity. Normalizing by sqrt(n) and xJOverA ensures
|
||||
// the table remains well-behaved for linear extrapolation when 'n' exceeds
|
||||
// the precomputed range.
|
||||
//
|
||||
// Optimal weights were originally derived via brute-force search for
|
||||
// minimal curve distance on a rounded superellipse.
|
||||
constexpr Scalar kPrecomputedVariables[][2] = {/*n= 2.0*/ {0.7078, 8.3194},
|
||||
/*n= 3.0*/ {0.7895, 2.4523},
|
||||
/*n= 4.0*/ {0.8379, 1.8528},
|
||||
/*n= 5.0*/ {0.8701, 1.6891},
|
||||
/*n= 6.0*/ {0.8932, 1.5806},
|
||||
/*n= 7.0*/ {0.9107, 1.5043},
|
||||
/*n= 8.0*/ {0.9244, 1.4470},
|
||||
/*n= 9.0*/ {0.9355, 1.4037},
|
||||
/*n=10.0*/ {0.9448, 1.3701},
|
||||
/*n=11.0*/ {0.9526, 1.3431},
|
||||
/*n=12.0*/ {0.9594, 1.3212},
|
||||
/*n=13.0*/ {0.9653, 1.3032},
|
||||
/*n=14.0*/ {0.9705, 1.2880}};
|
||||
constexpr size_t kNumRecords =
|
||||
sizeof(kPrecomputedVariables) / sizeof(kPrecomputedVariables[0]);
|
||||
constexpr Scalar kStep = 1.00f;
|
||||
@ -448,20 +498,54 @@ class RoundSuperellipseBuilder {
|
||||
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)};
|
||||
// The optimized factors stabilize as n grows large.
|
||||
n = kMaxN;
|
||||
}
|
||||
|
||||
// Compute weight1 and weight 2
|
||||
|
||||
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]};
|
||||
Scalar weight1 = (1.f - frac) * kPrecomputedVariables[left][0] +
|
||||
frac * kPrecomputedVariables[left + 1][0] * sqrt(n);
|
||||
Scalar weight2 = (1.f - frac) * kPrecomputedVariables[left][1] +
|
||||
frac * kPrecomputedVariables[left + 1][1] * xJOverA;
|
||||
|
||||
// Compute yHOverA
|
||||
|
||||
// H, the splitting point between the two conic curves, is picked by its y
|
||||
// coordinate proportionally between A and J.
|
||||
//
|
||||
// The proportion between (yA-yH) and (yH-yJ) is positively correlated to n,
|
||||
// so that when n increases, H moves closer to A. This is because the
|
||||
// flatter segment of the superellipse curve is much harder to approximate.
|
||||
// The exact formula of sqrt(n) is found empirically.
|
||||
Scalar yH_proportion = sqrt(n);
|
||||
constexpr Scalar yAOverA = 1.0;
|
||||
Scalar yHOverA =
|
||||
(yAOverA * yH_proportion + yJOverA) / (yH_proportion + 1.0f);
|
||||
|
||||
return {weight1, weight2, yHOverA};
|
||||
}
|
||||
|
||||
// Find the intersection point of two lines defined by two points and their
|
||||
// slopes.
|
||||
static Point Intersection(Point p1, Scalar k1, Point p2, Scalar k2) {
|
||||
if (std::fabs(k1 - k2) < kEhCloseEnough) {
|
||||
return (p1 + p2) / 2;
|
||||
}
|
||||
|
||||
// Line 1: y - y1 = k1(x - x1)
|
||||
// Line 2: y - y2 = k2(x - x2)
|
||||
// => x = (k1*x1 - k2*x2 + y2 - y1) / (k1 - k2)
|
||||
|
||||
Scalar x = (k1 * p1.x - k2 * p2.x + p2.y - p1.y) / (k1 - k2);
|
||||
Scalar y = k1 * (x - p1.x) + p1.y;
|
||||
|
||||
return Point(x, y);
|
||||
}
|
||||
|
||||
PathReceiver& receiver_;
|
||||
|
||||
@ -26,10 +26,14 @@ class SpyPathReceiver : public PathReceiver {
|
||||
using CubicSegment =
|
||||
std::function<void(const Point&, const Point&, const Point&)>;
|
||||
|
||||
using ConicSegment = std::function<void(const Point&, const Point&, Scalar)>;
|
||||
|
||||
void SpyLineTo(LineSegment line_to) { line_to_ = std::move(line_to); }
|
||||
|
||||
void SpyCubicTo(CubicSegment cubic_to) { cubic_to_ = std::move(cubic_to); }
|
||||
|
||||
void SpyConicTo(ConicSegment conic_to) { conic_to_ = std::move(conic_to); }
|
||||
|
||||
// |PathReceiver|
|
||||
void MoveTo(const Point& p2, bool will_be_closed) override {}
|
||||
// |PathReceiver|
|
||||
@ -46,12 +50,20 @@ class SpyPathReceiver : public PathReceiver {
|
||||
cubic_to_(cp1, cp2, p2);
|
||||
}
|
||||
}
|
||||
bool ConicTo(const Point& cp, const Point& p2, Scalar weight) override {
|
||||
if (conic_to_) {
|
||||
conic_to_(cp, p2, weight);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// |PathReceiver|
|
||||
void Close() override {}
|
||||
|
||||
private:
|
||||
LineSegment line_to_;
|
||||
CubicSegment cubic_to_;
|
||||
ConicSegment conic_to_;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
@ -779,5 +791,26 @@ TEST(RoundSuperellipseTest,
|
||||
rr.Dispatch(receiver);
|
||||
}
|
||||
|
||||
TEST(RoundSuperellipseTest, PathForLongRseShouldBeCorrect) {
|
||||
Rect bounds = Rect::MakeLTRB(0, 0, 300, 100000);
|
||||
// Regression test for https://github.com/flutter/flutter/issues/179875.
|
||||
|
||||
auto rr = RoundSuperellipseParam::MakeBoundsRadius(bounds, 100);
|
||||
SpyPathReceiver receiver;
|
||||
receiver.SpyLineTo(
|
||||
[&](const Point& p2) { EXPECT_TRUE(bounds.ContainsInclusive(p2)); });
|
||||
receiver.SpyCubicTo([&](const Point& cp1, const Point& cp2, const Point& p2) {
|
||||
EXPECT_TRUE(bounds.ContainsInclusive(cp1));
|
||||
EXPECT_TRUE(bounds.ContainsInclusive(cp2));
|
||||
EXPECT_TRUE(bounds.ContainsInclusive(p2));
|
||||
});
|
||||
receiver.SpyConicTo([&](const Point& cp, const Point& p2, Scalar weight) {
|
||||
EXPECT_TRUE(bounds.ContainsInclusive(cp));
|
||||
EXPECT_TRUE(bounds.ContainsInclusive(p2));
|
||||
});
|
||||
|
||||
rr.Dispatch(receiver);
|
||||
}
|
||||
|
||||
} // namespace testing
|
||||
} // namespace impeller
|
||||
|
||||
@ -35,11 +35,35 @@ extension type _RSuperellipsePath(Path path) {
|
||||
path.cubicTo(p2.dx, p2.dy, p3.dx, p3.dy, p4.dx, p4.dy);
|
||||
}
|
||||
|
||||
void conicToPoints(Offset cp, Offset p2, double weight) {
|
||||
path.conicTo(cp.dx, cp.dy, p2.dx, p2.dy, weight);
|
||||
}
|
||||
|
||||
void lineToPoint(Offset p) {
|
||||
path.lineTo(p.dx, p.dy);
|
||||
}
|
||||
}
|
||||
|
||||
class _ConicParam {
|
||||
const _ConicParam({required this.p1, required this.cp, required this.p2, required this.weight});
|
||||
|
||||
final Offset p1;
|
||||
final Offset cp;
|
||||
final Offset p2;
|
||||
final double weight;
|
||||
}
|
||||
|
||||
Offset _intersection(Offset p1, double k1, Offset p2, double k2) {
|
||||
if ((k1 - k2).abs() < 1e-5) {
|
||||
return (p1 + p2) / 2;
|
||||
}
|
||||
|
||||
final double x = (k1 * p1.dx - k2 * p2.dx + p2.dy - p1.dy) / (k1 - k2);
|
||||
final double y = k1 * (x - p1.dx) + p1.dy;
|
||||
|
||||
return Offset(x, y);
|
||||
}
|
||||
|
||||
// An octant of an RSuperellipse, used in _RSuperellipseQuadrant.
|
||||
class _RSuperellipseOctant {
|
||||
factory _RSuperellipseOctant(Offset center, double a, double radius) {
|
||||
@ -121,14 +145,11 @@ class _RSuperellipseOctant {
|
||||
}
|
||||
|
||||
final List<Offset> circlePoints = _circularArcPoints();
|
||||
final List<Offset> sePoints = _superellipseArcPoints();
|
||||
final (_ConicParam conic1, _ConicParam conic2) = _superellipseArcPoints();
|
||||
|
||||
if (!reverse) {
|
||||
path.cubicToPoints(
|
||||
transform.apply(sePoints[1]),
|
||||
transform.apply(sePoints[2]),
|
||||
transform.apply(sePoints[3]),
|
||||
);
|
||||
path.conicToPoints(transform.apply(conic1.cp), transform.apply(conic1.p2), conic1.weight);
|
||||
path.conicToPoints(transform.apply(conic2.cp), transform.apply(conic2.p2), conic2.weight);
|
||||
path.cubicToPoints(
|
||||
transform.apply(circlePoints[1]),
|
||||
transform.apply(circlePoints[2]),
|
||||
@ -140,29 +161,31 @@ class _RSuperellipseOctant {
|
||||
transform.apply(circlePoints[1]),
|
||||
transform.apply(circlePoints[0]),
|
||||
);
|
||||
path.cubicToPoints(
|
||||
transform.apply(sePoints[2]),
|
||||
transform.apply(sePoints[1]),
|
||||
transform.apply(sePoints[0]),
|
||||
);
|
||||
path.conicToPoints(transform.apply(conic2.cp), transform.apply(conic2.p1), conic2.weight);
|
||||
path.conicToPoints(transform.apply(conic1.cp), transform.apply(conic1.p1), conic1.weight);
|
||||
}
|
||||
}
|
||||
|
||||
List<Offset> _superellipseArcPoints() {
|
||||
final start = Offset(0, seA);
|
||||
final Offset end = circleStart;
|
||||
const startTangent = Offset(1, 0);
|
||||
final Offset circleStartVector = circleStart - circleCenter;
|
||||
final Offset endTangent =
|
||||
Offset(-circleStartVector.dy, circleStartVector.dx) / circleStartVector.distance;
|
||||
(_ConicParam, _ConicParam) _superellipseArcPoints() {
|
||||
final posA = Offset(0, seA);
|
||||
final Offset posJ = circleStart;
|
||||
|
||||
final (double startFactor, double endFactor) = _superellipseBezierFactors(seN);
|
||||
return <Offset>[
|
||||
start,
|
||||
start + startTangent * startFactor * seA,
|
||||
end + endTangent * endFactor * seA,
|
||||
end,
|
||||
];
|
||||
final (:double weight1, :double weight2, :double yHOverA) = _superellipseBezierFactors(
|
||||
seN,
|
||||
posJ.dx / seA,
|
||||
posJ.dy / seA,
|
||||
);
|
||||
|
||||
final posH = Offset(math.pow(1 - math.pow(yHOverA, seN), 1 / seN) * seA, yHOverA * seA);
|
||||
|
||||
const kA = 0.0;
|
||||
final kJ = -math.pow(posJ.dx / posJ.dy, seN - 1) as double;
|
||||
final kH = -math.pow(posH.dx / posH.dy, seN - 1) as double;
|
||||
|
||||
return (
|
||||
_ConicParam(p1: posA, cp: _intersection(posA, kA, posH, kH), p2: posH, weight: weight1),
|
||||
_ConicParam(p1: posH, cp: _intersection(posH, kH, posJ, kJ), p2: posJ, weight: weight2),
|
||||
);
|
||||
}
|
||||
|
||||
List<Offset> _circularArcPoints() {
|
||||
@ -188,22 +211,25 @@ class _RSuperellipseOctant {
|
||||
return Offset(p.dx * cosine - p.dy * sine, p.dx * sine + p.dy * cosine);
|
||||
}
|
||||
|
||||
static (double, double) _superellipseBezierFactors(double n) {
|
||||
static ({double weight1, double weight2, double yHOverA}) _superellipseBezierFactors(
|
||||
double n,
|
||||
double xJOverA,
|
||||
double yJOverA,
|
||||
) {
|
||||
const kPrecomputedVariables = <(double, double)>[
|
||||
/*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),
|
||||
/*n= 2.0*/ (0.7078, 8.3194),
|
||||
/*n= 3.0*/ (0.7895, 2.4523),
|
||||
/*n= 4.0*/ (0.8379, 1.8528),
|
||||
/*n= 5.0*/ (0.8701, 1.6891),
|
||||
/*n= 6.0*/ (0.8932, 1.5806),
|
||||
/*n= 7.0*/ (0.9107, 1.5043),
|
||||
/*n= 8.0*/ (0.9244, 1.4470),
|
||||
/*n= 9.0*/ (0.9355, 1.4037),
|
||||
/*n=10.0*/ (0.9448, 1.3701),
|
||||
/*n=11.0*/ (0.9526, 1.3431),
|
||||
/*n=12.0*/ (0.9594, 1.3212),
|
||||
/*n=13.0*/ (0.9653, 1.3032),
|
||||
/*n=14.0*/ (0.9705, 1.2880),
|
||||
];
|
||||
final int kNumRecords = kPrecomputedVariables.length;
|
||||
const kStep = 1.00;
|
||||
@ -211,19 +237,25 @@ class _RSuperellipseOctant {
|
||||
final double kMaxN = kMinN + (kNumRecords - 1) * kStep;
|
||||
|
||||
if (n >= kMaxN) {
|
||||
return (
|
||||
1.07 - math.exp(1.307649835) * math.pow(n, -0.8568516731),
|
||||
-0.01 + math.exp(-0.9287690322) * math.pow(n, -0.6120901398),
|
||||
);
|
||||
n = kMaxN;
|
||||
}
|
||||
|
||||
final double steps = ((n - kMinN) / kStep).clamp(0, kNumRecords - 1);
|
||||
final int left = steps.floor().clamp(0, kNumRecords - 2);
|
||||
final double frac = steps - left;
|
||||
|
||||
final double factor1 =
|
||||
(1 - frac) * kPrecomputedVariables[left].$1 + frac * kPrecomputedVariables[left + 1].$1;
|
||||
final double factor2 =
|
||||
(1 - frac) * kPrecomputedVariables[left].$2 + frac * kPrecomputedVariables[left + 1].$2;
|
||||
|
||||
final double yHProportion = math.sqrt(n);
|
||||
const yAOverA = 1.0;
|
||||
|
||||
return (
|
||||
(1 - frac) * kPrecomputedVariables[left].$1 + frac * kPrecomputedVariables[left + 1].$1,
|
||||
(1 - frac) * kPrecomputedVariables[left].$2 + frac * kPrecomputedVariables[left + 1].$2,
|
||||
weight1: factor1 * math.sqrt(n),
|
||||
weight2: factor2 * xJOverA,
|
||||
yHOverA: (yAOverA * yHProportion + yJOverA) / (yHProportion + 1.0),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -115,7 +115,7 @@ void testMain() {
|
||||
checkPointAndMirrors(const Offset(39.99, 94.73));
|
||||
checkPointAndMirrors(const Offset(44.13, 89.99));
|
||||
checkPointAndMirrors(const Offset(48.46, 79.99));
|
||||
checkPointAndMirrors(const Offset(49.68, 69.99));
|
||||
checkPointAndMirrors(const Offset(49.70, 69.99));
|
||||
checkPointAndMirrors(const Offset(49.97, 59.99));
|
||||
checkPointAndMirrors(const Offset(49.99, 49.99)); // Right mid-edge
|
||||
});
|
||||
@ -151,7 +151,7 @@ void testMain() {
|
||||
}
|
||||
|
||||
// A few other points along the edge
|
||||
checkDiagonalPoints(const Offset(-40.0, -49.59));
|
||||
checkDiagonalPoints(const Offset(-40.0, -49.58));
|
||||
checkDiagonalPoints(const Offset(-20.0, -45.64));
|
||||
checkDiagonalPoints(const Offset(0.0, -37.01));
|
||||
checkDiagonalPoints(const Offset(20.0, -21.96));
|
||||
|
||||
@ -290,7 +290,7 @@ void main() {
|
||||
}
|
||||
|
||||
// A few other points along the edge
|
||||
checkDiagonalPoints(const Offset(-40.0, -49.59));
|
||||
checkDiagonalPoints(const Offset(-40.0, -49.58));
|
||||
checkDiagonalPoints(const Offset(-20.0, -45.64));
|
||||
checkDiagonalPoints(const Offset(0.0, -37.01));
|
||||
checkDiagonalPoints(const Offset(20.0, -21.96));
|
||||
|
||||
@ -206,7 +206,7 @@ void main() {
|
||||
expect(ShapeBorder.lerp(rInside, rOutside, 0.5), rCenter);
|
||||
});
|
||||
|
||||
testWidgets('RoundedSuperellipseBorder looks correct', (WidgetTester tester) async {
|
||||
group('RoundedSuperellipseBorder looks correct', () {
|
||||
Widget containerWithBorder(Size size, BorderRadiusGeometry radius) {
|
||||
return Center(
|
||||
child: Container(
|
||||
@ -223,62 +223,113 @@ void main() {
|
||||
);
|
||||
}
|
||||
|
||||
await tester.pumpWidget(containerWithBorder(const Size(120, 300), BorderRadius.zero));
|
||||
await expectLater(
|
||||
find.byType(Container),
|
||||
matchesGoldenFile('painting.rounded_superellipse_border.all_zero.png'),
|
||||
);
|
||||
testWidgets('for all zero corners', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(containerWithBorder(const Size(120, 300), BorderRadius.zero));
|
||||
await expectLater(
|
||||
find.byType(Container),
|
||||
matchesGoldenFile('painting.rounded_superellipse_border.all_zero.png'),
|
||||
);
|
||||
});
|
||||
|
||||
await tester.pumpWidget(
|
||||
containerWithBorder(const Size(120, 300), const BorderRadius.all(Radius.circular(36))),
|
||||
);
|
||||
await expectLater(
|
||||
find.byType(Container),
|
||||
matchesGoldenFile('painting.rounded_superellipse_border.all_circular.png'),
|
||||
);
|
||||
testWidgets('for all circular corners', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
containerWithBorder(const Size(120, 300), const BorderRadius.all(Radius.circular(36))),
|
||||
);
|
||||
await expectLater(
|
||||
find.byType(Container),
|
||||
matchesGoldenFile('painting.rounded_superellipse_border.all_circular.png'),
|
||||
);
|
||||
});
|
||||
|
||||
await tester.pumpWidget(
|
||||
containerWithBorder(const Size(120, 300), const BorderRadius.all(Radius.elliptical(20, 50))),
|
||||
);
|
||||
await expectLater(
|
||||
find.byType(Container),
|
||||
matchesGoldenFile('painting.rounded_superellipse_border.all_elliptical.png'),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
containerWithBorder(const Size(120, 300), const BorderRadius.all(Radius.circular(600))),
|
||||
);
|
||||
await expectLater(
|
||||
find.byType(Container),
|
||||
matchesGoldenFile('painting.rounded_superellipse_border.clamping_uniform.png'),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
containerWithBorder(
|
||||
const Size(120, 300),
|
||||
const BorderRadius.only(
|
||||
topLeft: Radius.elliptical(1000, 1000),
|
||||
topRight: Radius.elliptical(0, 1000),
|
||||
bottomRight: Radius.elliptical(800, 1000),
|
||||
bottomLeft: Radius.elliptical(100, 500),
|
||||
testWidgets('for all elliptical corners', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
containerWithBorder(
|
||||
const Size(120, 300),
|
||||
const BorderRadius.all(Radius.elliptical(20, 50)),
|
||||
),
|
||||
),
|
||||
);
|
||||
await expectLater(
|
||||
find.byType(Container),
|
||||
matchesGoldenFile('painting.rounded_superellipse_border.clamping_non_uniform.png'),
|
||||
);
|
||||
);
|
||||
await expectLater(
|
||||
find.byType(Container),
|
||||
matchesGoldenFile('painting.rounded_superellipse_border.all_elliptical.png'),
|
||||
);
|
||||
});
|
||||
|
||||
// Regression test for https://github.com/flutter/flutter/issues/170593
|
||||
await tester.pumpWidget(
|
||||
containerWithBorder(
|
||||
const Size(120, 300),
|
||||
const BorderRadius.only(topLeft: Radius.circular(16), topRight: Radius.circular(16)),
|
||||
),
|
||||
);
|
||||
await expectLater(
|
||||
find.byType(Container),
|
||||
matchesGoldenFile('painting.rounded_superellipse_border.regression_1.png'),
|
||||
);
|
||||
testWidgets('for clamping uniform corners', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
containerWithBorder(const Size(120, 300), const BorderRadius.all(Radius.circular(600))),
|
||||
);
|
||||
await expectLater(
|
||||
find.byType(Container),
|
||||
matchesGoldenFile('painting.rounded_superellipse_border.clamping_uniform.png'),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('for clamping non-uniform corners', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
containerWithBorder(
|
||||
const Size(120, 300),
|
||||
const BorderRadius.only(
|
||||
topLeft: Radius.elliptical(1000, 1000),
|
||||
topRight: Radius.elliptical(0, 1000),
|
||||
bottomRight: Radius.elliptical(800, 1000),
|
||||
bottomLeft: Radius.elliptical(100, 500),
|
||||
),
|
||||
),
|
||||
);
|
||||
await expectLater(
|
||||
find.byType(Container),
|
||||
matchesGoldenFile('painting.rounded_superellipse_border.clamping_non_uniform.png'),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('for non-uniform sharp corners', (WidgetTester tester) async {
|
||||
// Regression test for https://github.com/flutter/flutter/issues/170593
|
||||
await tester.pumpWidget(
|
||||
containerWithBorder(
|
||||
const Size(120, 300),
|
||||
const BorderRadius.only(topLeft: Radius.circular(16), topRight: Radius.circular(16)),
|
||||
),
|
||||
);
|
||||
await expectLater(
|
||||
find.byType(Container),
|
||||
matchesGoldenFile('painting.rounded_superellipse_border.regression_1.png'),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('for very large ratio', (WidgetTester tester) async {
|
||||
// Regression test for https://github.com/flutter/flutter/issues/179875
|
||||
await tester.pumpWidget(
|
||||
ClipRect(
|
||||
child: OverflowBox(
|
||||
maxWidth: double.infinity,
|
||||
maxHeight: double.infinity,
|
||||
alignment: Alignment.topRight,
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(50.0),
|
||||
child: Container(
|
||||
width: 1e5,
|
||||
height: 500,
|
||||
decoration: const ShapeDecoration(
|
||||
color: Color.fromARGB(255, 120, 120, 120),
|
||||
shape: RoundedSuperellipseBorder(
|
||||
side: BorderSide(color: Color.fromARGB(255, 255, 0, 0)),
|
||||
borderRadius: BorderRadius.only(
|
||||
topRight: Radius.circular(100),
|
||||
bottomRight: Radius.circular(30),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await expectLater(
|
||||
find.byType(ClipRect),
|
||||
matchesGoldenFile('painting.rounded_superellipse_border.long.png'),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user