| /* | |
| * Copyright 2025 Google LLC | |
| * | |
| * Use of this source code is governed by a BSD-style license that can be | |
| * found in the LICENSE file. | |
| */ | |
| #include "include/core/SkPathBuilder.h" | |
| #include "include/core/SkPathTypes.h" | |
| #include "src/core/SkPathData.h" | |
| #include "src/core/SkPathPriv.h" | |
| #include "tests/Test.h" | |
| #include <functional> | |
| #include <limits> | |
| namespace { | |
| template <typename T> bool spaneq(SkSpan<T> a, SkSpan<T> b) { | |
| if (a.size() != b.size()) { | |
| return false; | |
| } | |
| for (size_t i = 0; i < a.size(); ++i) { | |
| if (a[i] != b[i]) { | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| } | |
| DEF_TEST(pathdata_empty, reporter) { | |
| auto pdata = SkPathData::Empty(); | |
| REPORTER_ASSERT(reporter, pdata->empty()); | |
| REPORTER_ASSERT(reporter, pdata->points().empty()); | |
| REPORTER_ASSERT(reporter, pdata->conics().empty()); | |
| REPORTER_ASSERT(reporter, pdata->verbs().empty()); | |
| REPORTER_ASSERT(reporter, pdata->bounds() == SkRect::MakeEmpty()); | |
| REPORTER_ASSERT(reporter, pdata->segmentMask() == 0); | |
| REPORTER_ASSERT(reporter, !pdata->asLine()); | |
| REPORTER_ASSERT(reporter, !pdata->asOval()); | |
| REPORTER_ASSERT(reporter, !pdata->asRect()); | |
| REPORTER_ASSERT(reporter, !pdata->asRRect()); | |
| auto xformed = pdata->makeTransform(SkMatrix::Scale(2, 3)); | |
| REPORTER_ASSERT(reporter, *pdata == *xformed); | |
| } | |
| using IsAPredicate = bool(const SkPathData&); | |
| static void check_asA_transforms(skiatest::Reporter* reporter, sk_sp<SkPathData> orig, | |
| IsAPredicate returns_asA) { | |
| SkASSERT(returns_asA(*orig)); | |
| const struct { | |
| SkMatrix mx; | |
| bool expectedAsA; | |
| } gPairs[] = { | |
| { SkMatrix::I(), true }, | |
| { SkMatrix::Translate(1, 2), true }, | |
| { SkMatrix::Scale(2, 3), true }, | |
| { SkMatrix::RotateDeg(30), false }, | |
| }; | |
| for (const auto& pair : gPairs) { | |
| auto pdata = orig->makeTransform(pair.mx); | |
| REPORTER_ASSERT(reporter, returns_asA(*pdata) == pair.expectedAsA); | |
| } | |
| } | |
| /* | |
| * Differe ways to "make" a rectangular PathData | |
| */ | |
| using RectMaker = sk_sp<SkPathData>(const SkRect&, SkPathDirection); | |
| static sk_sp<SkPathData> factory_rect(const SkRect& r, SkPathDirection d) { | |
| return SkPathData::Rect(r, d); | |
| } | |
| static sk_sp<SkPathData> poly4_rect(const SkRect& r, SkPathDirection d) { | |
| std::array<SkPoint, 4> pts = r.toQuad(d); | |
| return SkPathData::Polygon(pts, true); | |
| } | |
| static sk_sp<SkPathData> poly5_rect(const SkRect& r, SkPathDirection d) { | |
| std::array<SkPoint, 5> pts; | |
| r.copyToQuad(pts, d); | |
| pts[4] = pts[0]; // explicly add the closing line | |
| return SkPathData::Polygon(pts, true); | |
| } | |
| static sk_sp<SkPathData> builder_rect_rect(const SkRect& r, SkPathDirection d) { | |
| SkPathBuilder bu; | |
| bu.addRect(r, d); | |
| return bu.detachData(); | |
| } | |
| static sk_sp<SkPathData> builder_poly4_rect(const SkRect& r, SkPathDirection d) { | |
| std::array<SkPoint, 4> pts = r.toQuad(d); | |
| SkPathBuilder bu; | |
| bu.addPolygon(pts, true); | |
| return bu.detachData(); | |
| } | |
| static sk_sp<SkPathData> builder_poly5_rect(const SkRect& r, SkPathDirection d) { | |
| std::array<SkPoint, 5> pts; | |
| r.copyToQuad(pts, d); | |
| pts[4] = pts[0]; // explicly add the closing line | |
| SkPathBuilder bu; | |
| bu.addPolygon(pts, true); | |
| return bu.detachData(); | |
| } | |
| RectMaker* const gRectMakers[] = { | |
| factory_rect, | |
| poly4_rect, | |
| poly5_rect, | |
| builder_rect_rect, | |
| builder_poly4_rect, | |
| builder_poly5_rect, | |
| }; | |
| DEF_TEST(pathdata_rect, reporter) { | |
| const SkRect r = {1, 2, 3, 4}; | |
| auto sign = [](float x) -> float { | |
| if (x == 0) { | |
| return 0; | |
| } else { | |
| return x > 0 ? 1 : -1; | |
| } | |
| }; | |
| for (auto maker : gRectMakers) { | |
| for (auto dir : {SkPathDirection::kCW, SkPathDirection::kCCW}) { | |
| auto pdata = maker(r, dir); | |
| REPORTER_ASSERT(reporter, r == pdata->bounds()); | |
| // 1. manually determine if *we* think it is a rect | |
| const SkSpan<const SkPoint> pts = pdata->points(); | |
| REPORTER_ASSERT(reporter, r == SkRect::Bounds(pts).value()); | |
| const float crossSign = (dir == SkPathDirection::kCW) ? 1 : -1; | |
| for (int i = 1; i < 3; ++i) { | |
| SkVector u = pts[i] - pts[i-1], | |
| v = pts[i+1] - pts[i]; | |
| const float cross = u.cross(v), | |
| dot = u.dot(v); | |
| REPORTER_ASSERT(reporter, dot == 0); | |
| REPORTER_ASSERT(reporter, crossSign == sign(cross)); | |
| } | |
| // 2. now ask the pathdata | |
| auto isa = pdata->asRect(); | |
| REPORTER_ASSERT(reporter, isa.has_value()); | |
| REPORTER_ASSERT(reporter, isa->fRect == r); | |
| REPORTER_ASSERT(reporter, isa->fDirection == dir); | |
| REPORTER_ASSERT(reporter, isa->fStartIndex == 0); | |
| check_asA_transforms(reporter, pdata, [](const SkPathData& pd) { | |
| return pd.asRect().has_value(); | |
| }); | |
| } | |
| } | |
| } | |
| static sk_sp<SkPathData> factory_poly(SkSpan<const SkPoint> pts, bool isClosed) { | |
| return SkPathData::Polygon(pts, isClosed); | |
| } | |
| static sk_sp<SkPathData> builder_poly(SkSpan<const SkPoint> pts, bool isClosed) { | |
| SkPathBuilder bu; | |
| bu.addPolygon(pts, isClosed); | |
| return bu.detachData(); | |
| } | |
| DEF_TEST(pathdata_polygon, reporter) { | |
| const SkPoint points[] = { | |
| {0, 1}, {2, 3}, {4, 5}, {6, 7}, {8, 9}, | |
| }; | |
| for (auto maker : {factory_poly, builder_poly}) { | |
| for (auto isClosed : {false, true}) { | |
| for (size_t n = 0; n <= std::size(points); ++n) { | |
| const SkSpan<const SkPoint> pts = {points, n}; | |
| auto pdata = maker(pts, isClosed); | |
| const bool shouldBeEmpty = isClosed ? n == 0 : n <= 1; | |
| if (shouldBeEmpty) { | |
| REPORTER_ASSERT(reporter, pdata->empty()); | |
| continue; | |
| } | |
| REPORTER_ASSERT(reporter, spaneq(pdata->points(), pts)); | |
| REPORTER_ASSERT(reporter, pdata->conics().empty()); | |
| auto line = pdata->asLine(); | |
| if (n == 2 && !isClosed) { | |
| REPORTER_ASSERT(reporter, line.has_value()); | |
| REPORTER_ASSERT(reporter, line.value()[0] == points[0]); | |
| REPORTER_ASSERT(reporter, line.value()[1] == points[1]); | |
| auto pline = SkPathData::Line(points[0], points[1]); | |
| REPORTER_ASSERT(reporter, *pline == *pdata); | |
| } else { | |
| REPORTER_ASSERT(reporter, !line.has_value()); | |
| } | |
| const size_t expectedVerbs = pts.size() + isClosed; | |
| auto vbs = pdata->verbs(); | |
| REPORTER_ASSERT(reporter, vbs.size() == expectedVerbs); | |
| REPORTER_ASSERT(reporter, vbs[0] == SkPathVerb::kMove); | |
| for (size_t i = 1; i < pts.size(); ++i) { | |
| REPORTER_ASSERT(reporter, vbs[i] == SkPathVerb::kLine); | |
| } | |
| if (isClosed) { | |
| REPORTER_ASSERT(reporter, vbs.back() == SkPathVerb::kClose); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| static sk_sp<SkPathData> factory_oval(const SkRect& r, SkPathDirection dir, unsigned start) { | |
| return SkPathData::Oval(r, dir, start); | |
| } | |
| static sk_sp<SkPathData> builder_oval(const SkRect& r, SkPathDirection dir, unsigned start) { | |
| SkPathBuilder bu; | |
| bu.addOval(r, dir, start); | |
| return bu.detachData(); | |
| } | |
| DEF_TEST(pathdata_oval, reporter) { | |
| const SkRect bounds = {1, 2, 3, 4}; | |
| const unsigned kStartIndexCount = 4; | |
| for (auto maker : {factory_oval, builder_oval}) { | |
| for (auto dir : {SkPathDirection::kCW, SkPathDirection::kCCW}) { | |
| for (unsigned start = 0; start < kStartIndexCount; ++start) { | |
| auto pdata = maker(bounds, dir, start); | |
| REPORTER_ASSERT(reporter, pdata->bounds() == bounds); | |
| auto oval = pdata->asOval(); | |
| REPORTER_ASSERT(reporter, oval.has_value()); | |
| REPORTER_ASSERT(reporter, oval->fBounds == bounds); | |
| REPORTER_ASSERT(reporter, oval->fDirection == dir); | |
| REPORTER_ASSERT(reporter, oval->fStartIndex == start); | |
| check_asA_transforms(reporter, pdata, [](const SkPathData& pd) { | |
| return pd.asOval().has_value(); | |
| }); | |
| } | |
| } | |
| } | |
| } | |
| static sk_sp<SkPathData> factory_rrect(const SkRRect& r, SkPathDirection dir, unsigned start) { | |
| return SkPathData::RRect(r, dir, start); | |
| } | |
| static sk_sp<SkPathData> builder_rrect(const SkRRect& r, SkPathDirection dir, unsigned start) { | |
| SkPathBuilder bu; | |
| bu.addRRect(r, dir, start); | |
| return bu.detachData(); | |
| } | |
| DEF_TEST(pathdata_rrect, reporter) { | |
| const SkRect bounds = {0, 0, 20, 30}; | |
| const SkRRect rrect = SkRRect::MakeRectXY(bounds, 2, 3); | |
| const unsigned kStartIndexCount = 8; | |
| for (auto maker : {factory_rrect, builder_rrect}) { | |
| for (auto dir : {SkPathDirection::kCW, SkPathDirection::kCCW}) { | |
| for (unsigned start = 0; start < kStartIndexCount; ++start) { | |
| auto pdata = maker(rrect, dir, start); | |
| REPORTER_ASSERT(reporter, pdata->bounds() == bounds); | |
| auto rr = pdata->asRRect(); | |
| REPORTER_ASSERT(reporter, rr.has_value()); | |
| REPORTER_ASSERT(reporter, rr->fRRect == rrect); | |
| REPORTER_ASSERT(reporter, rr->fDirection == dir); | |
| REPORTER_ASSERT(reporter, rr->fStartIndex == start); | |
| check_asA_transforms(reporter, pdata, [](const SkPathData& pd) { | |
| return pd.asRRect().has_value(); | |
| }); | |
| } | |
| } | |
| } | |
| } | |
| DEF_TEST(pathdata_make_edgecases, reporter) { | |
| // just create some points for our tests | |
| SkPoint pts[20]; | |
| for (size_t i = 0; i < std::size(pts); ++i) { | |
| pts[i] = {i * 1.0f, i * 1.0f }; | |
| } | |
| const float conicWeights[] = {1.5f, 2, 3}; | |
| constexpr SkPathVerb M = SkPathVerb::kMove, | |
| L = SkPathVerb::kLine, | |
| Q = SkPathVerb::kQuad, | |
| K = SkPathVerb::kConic, | |
| C = SkPathVerb::kCubic, | |
| X = SkPathVerb::kClose; | |
| // only these two sequence will result in an "empty" PathData | |
| const SkPathVerb move[] = { M }; // the M will not be trimmed | |
| REPORTER_ASSERT(reporter, SkPathData::Make({}, {}, {})->empty()); | |
| REPORTER_ASSERT(reporter, !SkPathData::Make({pts, 1}, move, {})->empty()); | |
| // these sequenes are all illegal (bad verb sequencing) | |
| const SkPathVerb bad0[] = { L }; // didn't start with M | |
| const SkPathVerb bad1[] = { M, M, L }; // consecutive Ms | |
| const SkPathVerb bad2[] = { M, L, X, X}; // consecutive Xs | |
| const SkPathVerb bad3[] = { M, L, M, M}; // consecutive Ms | |
| REPORTER_ASSERT(reporter, SkPathData::Make({pts, 1}, bad0, {}) == nullptr); | |
| REPORTER_ASSERT(reporter, SkPathData::Make({pts, 3}, bad1, {}) == nullptr); | |
| REPORTER_ASSERT(reporter, SkPathData::Make({pts, 2}, bad2, {}) == nullptr); | |
| REPORTER_ASSERT(reporter, SkPathData::Make({pts, 4}, bad3, {}) == nullptr); | |
| // Odd but legal, the trailing M is preserved, but not part of the bounds | |
| const SkPathVerb trimmed[] = { M, L, M }; //legal | |
| auto pdata = SkPathData::Make({pts, 3}, trimmed, {}); | |
| REPORTER_ASSERT(reporter, pdata->points().size() == 3); | |
| REPORTER_ASSERT(reporter, pdata->verbs().size() == 3); | |
| REPORTER_ASSERT(reporter, pdata->bounds() == SkRect::Bounds({pts, 2}).value()); | |
| // Now check on # of points and conic weights | |
| const SkPathVerb verbs[] = { M, L, Q, K, C, X, M }; // 1+1+2+2+3+0+1 = 10 + 1 conic weight | |
| const struct { | |
| size_t nPts, nConics; | |
| bool success; | |
| } combos[] = { | |
| { 10, 1, true }, // just right | |
| { 9, 1, false }, // not enough points | |
| { 11, 1, false }, // too many points | |
| { 10, 0, false }, // not enough conics | |
| { 10, 2, false }, // too many conics | |
| { 0, 1, false }, // degenerate, should not crash on moveto trim | |
| }; | |
| for (auto c : combos) { | |
| pdata = SkPathData::Make({pts, c.nPts}, verbs, {conicWeights, c.nConics}); | |
| if (c.success) { | |
| REPORTER_ASSERT(reporter, pdata != nullptr); | |
| } else { | |
| REPORTER_ASSERT(reporter, pdata == nullptr); | |
| } | |
| } | |
| } | |
| static inline std::optional<SkRRect> make_bad_rrect() { | |
| constexpr float big = std::numeric_limits<float>::max(); | |
| SkRRect rr = SkRRect::MakeRectXY({0, 0, big, big}, 4, 4); | |
| rr.offset(big, big); | |
| if (!rr.rect().isFinite()) { | |
| return rr; | |
| } | |
| return {}; // failed to make a non-finite rrect | |
| } | |
| /* | |
| * Test that we cannot make a non-finite PathData | |
| */ | |
| DEF_TEST(pathdata_make_nonfinite, reporter) { | |
| const float inf = SK_FloatInfinity; | |
| SkPoint pts[] = { | |
| {0, 0}, {inf, 1}, {2, 4}, | |
| }; | |
| SkPathVerb vbs[] = { | |
| SkPathVerb::kMove, SkPathVerb::kConic, | |
| }; | |
| float weights[] = { 2 }; | |
| sk_sp<SkPathData> pdata = SkPathData::Make(pts, vbs, weights); | |
| REPORTER_ASSERT(reporter, pdata == nullptr); | |
| pts[1].fX = 3; // remove non-finite from pts | |
| const float badWValues[] = { -1, inf, -inf, inf * 0 /* nan */ }; | |
| for (auto bad : badWValues) { | |
| weights[0] = bad; | |
| REPORTER_ASSERT(reporter, SkPathData::Make(pts, vbs, weights) == nullptr); | |
| } | |
| SkRect r = {1, 2, inf, 4}; | |
| REPORTER_ASSERT(reporter, SkPathData::Rect(r) == nullptr); | |
| REPORTER_ASSERT(reporter, SkPathData::Oval(r) == nullptr); | |
| // Most RRect methods 'sanitize' the values before returning the RRect, so it hard to | |
| // actually make one for testing. If our attempt suceeds, we will test with it. | |
| if (auto rr = make_bad_rrect()) { | |
| REPORTER_ASSERT(reporter, SkPathData::RRect(*rr) == nullptr); | |
| } | |
| pts[1].fX = inf; // restore non-finite value | |
| REPORTER_ASSERT(reporter, SkPathData::Polygon(pts, false) == nullptr); | |
| { | |
| // Non-finite trailing moves should also be rejected. | |
| SkPathVerb v[] = { SkPathVerb::kMove, SkPathVerb::kLine, SkPathVerb::kMove }; | |
| SkPoint p[] = { {0, 0}, {10, 10}, {inf, 20} }; | |
| REPORTER_ASSERT(reporter, SkPathData::Make(p, v) == nullptr); | |
| } | |
| } | |
| DEF_TEST(pathdata_transform, reporter) { | |
| SkMatrix mx; | |
| const SkRect r = {10, 20, 30, 40}; | |
| auto data = SkPathData::Oval(r); | |
| mx = SkMatrix::I(); | |
| auto newd = data->makeTransform(mx); | |
| REPORTER_ASSERT(reporter, *newd == *data); | |
| mx = SkMatrix::Translate(5, 6); | |
| newd = data->makeTransform(mx); | |
| REPORTER_ASSERT(reporter, newd->bounds() == r.makeOffset(5, 6)); | |
| mx = SkMatrix::Scale(0.5f, 2); | |
| newd = data->makeTransform(mx); | |
| SkRect r2 = { | |
| r.fLeft * 0.5f, | |
| r.fTop * 2, | |
| r.fRight * 0.5f, | |
| r.fBottom * 2, | |
| }; | |
| REPORTER_ASSERT(reporter, newd->bounds() == r2); | |
| mx = SkMatrix::Scale(SK_FloatInfinity, 2); | |
| newd = data->makeTransform(mx); | |
| REPORTER_ASSERT(reporter, newd == nullptr); | |
| mx = SkMatrix::Scale(SK_ScalarNaN, 2); | |
| newd = data->makeTransform(mx); | |
| REPORTER_ASSERT(reporter, newd == nullptr); | |
| } | |
| /* | |
| * This tests how convexity is tracked under transformation | |
| * 1. unknown stays unknown (we don't actively compute convexity) | |
| * 2. concave stays concave | |
| * 3. convex ... may stay convex -- it depends if we feel it is (numerically) safe. | |
| * See SkPathPriv::TransformConvexity() for the current heuristics. | |
| * 4. The (above) helper is shared with SkPath::transform(), so it and SkPathData | |
| * should handle transforms + convexity the same. | |
| */ | |
| DEF_TEST(pathdata_transform_convexity, reporter) { | |
| const SkPoint pts[] = { | |
| {0, 0}, {100, 0}, {200, 0}, {200, 200}, | |
| }; | |
| // needed late for our assumpts about convexity preservation | |
| REPORTER_ASSERT(reporter, SkPathPriv::IsAxisAligned(pts)); | |
| auto src = SkPathData::Polygon(pts, true); | |
| auto convexity = SkPathPriv::GetConvexityOrUnknown(*src); | |
| // don't do any work we didn't ask for | |
| REPORTER_ASSERT(reporter, convexity == SkPathConvexity::kUnknown); | |
| auto raw = src->raw(SkPathFillType::kDefault, SkResolveConvexity::kNo); | |
| REPORTER_ASSERT(reporter, raw.fConvexity == SkPathConvexity::kUnknown); | |
| // now ask for it | |
| raw = src->raw(SkPathFillType::kDefault, SkResolveConvexity::kYes); | |
| REPORTER_ASSERT(reporter, raw.isKnownToBeConvex()); | |
| // For these matrices, given that our points are axis-aligned, we should be able | |
| // to preserve whatever convexity our src has. | |
| const SkMatrix safeMatrices[] = { | |
| SkMatrix(), SkMatrix::Translate(1, 2), SkMatrix::Scale(2, 3), | |
| }; | |
| const SkPathConvexity convexities[] = { | |
| SkPathConvexity::kUnknown, | |
| SkPathConvexity::kConvex_CW, // matches our test data | |
| SkPathConvexity::kConcave, | |
| }; | |
| for (const auto& mx : safeMatrices) { | |
| for (auto conv : convexities) { | |
| raw.fConvexity = conv; | |
| auto dst = SkPathData::MakeTransform(raw, mx); | |
| convexity = SkPathPriv::GetConvexityOrUnknown(*dst); | |
| REPORTER_ASSERT(reporter, convexity == conv); | |
| } | |
| } | |
| // for this matrix, we do not expect to preserve convexity | |
| // (since we don't choose to actually compute convexity at this stage) | |
| SkMatrix mx = SkMatrix::RotateDeg(30); | |
| for (auto conv : convexities) { | |
| raw.fConvexity = conv; | |
| auto dst = SkPathData::MakeTransform(raw, mx); | |
| convexity = SkPathPriv::GetConvexityOrUnknown(*dst); | |
| auto expected = SkPathConvexity_IsConvex(conv) ? SkPathConvexity::kUnknown | |
| : conv; | |
| REPORTER_ASSERT(reporter, convexity == expected); | |
| } | |
| } | |
| DEF_TEST(pathdata_inverted_bounds, reporter) { | |
| using makerT = std::function<sk_sp<SkPathData>(const SkRect&)>; | |
| const auto check = [&reporter](const makerT& maker) { | |
| constexpr SkRect bounds = {-10, -10, 10, 10}; | |
| constexpr SkRect inverted_bounds = {10, 10, -10, -10}; | |
| REPORTER_ASSERT(reporter, maker(bounds)->bounds() == bounds); | |
| REPORTER_ASSERT(reporter, maker(inverted_bounds)->bounds() == bounds); | |
| }; | |
| { | |
| for (auto maker : {factory_rect, builder_rect_rect}) { | |
| for (auto dir : {SkPathDirection::kCW, SkPathDirection::kCCW}) { | |
| check([&maker, dir](const SkRect& r) { return maker(r, dir); }); | |
| } | |
| } | |
| } | |
| { | |
| constexpr unsigned kStartIndexCount = 4; | |
| for (auto maker : {factory_oval, builder_oval}) { | |
| for (auto dir : {SkPathDirection::kCW, SkPathDirection::kCCW}) { | |
| for (unsigned start = 0; start < kStartIndexCount; ++start) { | |
| check([&maker, dir, start](const SkRect& r) { return maker(r, dir, start); }); | |
| } | |
| } | |
| } | |
| } | |
| { | |
| constexpr unsigned kStartIndexCount = 8; | |
| for (auto maker : {factory_rrect, builder_rrect}) { | |
| for (auto dir : {SkPathDirection::kCW, SkPathDirection::kCCW}) { | |
| for (unsigned start = 0; start < kStartIndexCount; ++start) { | |
| for (int sign : {1, -1}) { | |
| check([&maker, dir, start, sign](const SkRect& r) { | |
| const SkRRect rrect = SkRRect::MakeRectXY(r, sign * 2, sign * 3); | |
| return maker(rrect, dir, start); | |
| }); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } |