blob: 9bab3dc143cf90d28fe8b178ad2d11e569149f09 [file] [log] [blame]
/*
* Copyright 2025 Google Inc.
*
* 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 "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 empty[] = { M }; // the M will be trimmed
REPORTER_ASSERT(reporter, SkPathData::Make({}, {empty, 0}, {})->empty());
REPORTER_ASSERT(reporter, SkPathData::Make({pts, 1}, empty, {})->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 will be removed
const SkPathVerb trimmed[] = { M, L, M }; //legal, but will trim the last M
auto pdata = SkPathData::Make({pts, 3}, trimmed, {});
REPORTER_ASSERT(reporter, pdata->points().size() == 2);
REPORTER_ASSERT(reporter, pdata->verbs().size() == 2);
// 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);
}
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);
});
}
}
}
}
}
}