skia2/tests/GrShapeTest.cpp
bsalomon aa840647fc Don't compute path keys for volatile paths in GrShape.
Otherwise, we will compute cache keys for internally transformed paths that don't repeat (e.g. clip paths transformed into device space with a changing view matrix).

BUG=chromium:649562
GOLD_TRYBOT_URL= https://gold.skia.org/search?issue=2369513002

Review-Url: https://codereview.chromium.org/2369513002
2016-09-23 12:09:16 -07:00

1971 lines
86 KiB
C++

/*
* Copyright 2016 Google Inc.
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include <initializer_list>
#include <functional>
#include "Test.h"
#if SK_SUPPORT_GPU
#include "GrShape.h"
#include "SkCanvas.h"
#include "SkDashPathEffect.h"
#include "SkPath.h"
#include "SkPathOps.h"
#include "SkSurface.h"
using Key = SkTArray<uint32_t>;
static bool make_key(Key* key, const GrShape& shape) {
int size = shape.unstyledKeySize();
if (size <= 0) {
key->reset(0);
return false;
}
SkASSERT(size);
key->reset(size);
shape.writeUnstyledKey(key->begin());
return true;
}
static bool paths_fill_same(const SkPath& a, const SkPath& b) {
SkPath pathXor;
Op(a, b, SkPathOp::kXOR_SkPathOp, &pathXor);
return pathXor.isEmpty();
}
static bool test_bounds_by_rasterizing(const SkPath& path, const SkRect& bounds) {
// We test the bounds by rasterizing the path into a kRes by kRes grid. The bounds is
// mapped to the range kRes/4 to 3*kRes/4 in x and y. A difference clip is used to avoid
// rendering within the bounds (with a tolerance). Then we render the path and check that
// everything got clipped out.
static constexpr int kRes = 2000;
// This tolerance is in units of 1/kRes fractions of the bounds width/height.
static constexpr int kTol = 0;
GR_STATIC_ASSERT(kRes % 4 == 0);
SkImageInfo info = SkImageInfo::MakeA8(kRes, kRes);
sk_sp<SkSurface> surface = SkSurface::MakeRaster(info);
surface->getCanvas()->clear(0x0);
SkRect clip = SkRect::MakeXYWH(kRes/4, kRes/4, kRes/2, kRes/2);
SkMatrix matrix;
matrix.setRectToRect(bounds, clip, SkMatrix::kFill_ScaleToFit);
clip.outset(SkIntToScalar(kTol), SkIntToScalar(kTol));
surface->getCanvas()->clipRect(clip, SkCanvas::kDifference_Op);
surface->getCanvas()->concat(matrix);
SkPaint whitePaint;
whitePaint.setColor(SK_ColorWHITE);
surface->getCanvas()->drawPath(path, whitePaint);
SkPixmap pixmap;
surface->getCanvas()->peekPixels(&pixmap);
#if defined(SK_BUILD_FOR_WIN)
// The static constexpr version in #else causes cl.exe to crash.
const uint8_t* kZeros = reinterpret_cast<uint8_t*>(calloc(kRes, 1));
#else
static constexpr uint8_t kZeros[kRes] = {0};
#endif
for (int y = 0; y < kRes; ++y) {
const uint8_t* row = pixmap.addr8(0, y);
if (0 != memcmp(kZeros, row, kRes)) {
return false;
}
}
#ifdef SK_BUILD_FOR_WIN
free(const_cast<uint8_t*>(kZeros));
#endif
return true;
}
namespace {
/**
* Geo is a factory for creating a GrShape from another representation. It also answers some
* questions about expected behavior for GrShape given the inputs.
*/
class Geo {
public:
virtual ~Geo() {};
virtual GrShape makeShape(const SkPaint&) const = 0;
virtual SkPath path() const = 0;
// These functions allow tests to check for special cases where style gets
// applied by GrShape in its constructor (without calling GrShape::applyStyle).
// These unfortunately rely on knowing details of GrShape's implementation.
// These predicates are factored out here to avoid littering the rest of the
// test code with GrShape implementation details.
virtual bool fillChangesGeom() const { return false; }
virtual bool strokeIsConvertedToFill() const { return false; }
virtual bool strokeAndFillIsConvertedToFill(const SkPaint&) const { return false; }
// Is this something we expect GrShape to recognize as something simpler than a path.
virtual bool isNonPath(const SkPaint& paint) const { return true; }
};
class RectGeo : public Geo {
public:
RectGeo(const SkRect& rect) : fRect(rect) {}
SkPath path() const override {
SkPath path;
path.addRect(fRect);
return path;
}
GrShape makeShape(const SkPaint& paint) const override {
return GrShape(fRect, paint);
}
bool strokeAndFillIsConvertedToFill(const SkPaint& paint) const override {
SkASSERT(paint.getStyle() == SkPaint::kStrokeAndFill_Style);
// Converted to an outset rectangle.
return paint.getStrokeJoin() == SkPaint::kMiter_Join &&
paint.getStrokeMiter() >= SK_ScalarSqrt2;
}
private:
SkRect fRect;
};
class RRectGeo : public Geo {
public:
RRectGeo(const SkRRect& rrect) : fRRect(rrect) {}
GrShape makeShape(const SkPaint& paint) const override {
return GrShape(fRRect, paint);
}
SkPath path() const override {
SkPath path;
path.addRRect(fRRect);
return path;
}
bool strokeAndFillIsConvertedToFill(const SkPaint& paint) const override {
SkASSERT(paint.getStyle() == SkPaint::kStrokeAndFill_Style);
if (fRRect.isRect()) {
return RectGeo(fRRect.rect()).strokeAndFillIsConvertedToFill(paint);
}
return false;
}
private:
SkRRect fRRect;
};
class PathGeo : public Geo {
public:
enum class Invert { kNo, kYes };
PathGeo(const SkPath& path, Invert invert) : fPath(path) {
SkASSERT(!path.isInverseFillType());
if (Invert::kYes == invert) {
if (fPath.getFillType() == SkPath::kEvenOdd_FillType) {
fPath.setFillType(SkPath::kInverseEvenOdd_FillType);
} else {
SkASSERT(fPath.getFillType() == SkPath::kWinding_FillType);
fPath.setFillType(SkPath::kInverseWinding_FillType);
}
}
}
GrShape makeShape(const SkPaint& paint) const override {
return GrShape(fPath, paint);
}
SkPath path() const override { return fPath; }
bool fillChangesGeom() const override {
// unclosed rects get closed. Lines get turned into empty geometry
return this->isUnclosedRect() || (fPath.isLine(nullptr) && !fPath.isInverseFillType());
}
bool strokeIsConvertedToFill() const override {
return this->isAxisAlignedLine();
}
bool strokeAndFillIsConvertedToFill(const SkPaint& paint) const override {
SkASSERT(paint.getStyle() == SkPaint::kStrokeAndFill_Style);
if (this->isAxisAlignedLine()) {
// The fill is ignored (zero area) and the stroke is converted to a rrect.
return true;
}
SkRect rect;
unsigned start;
SkPath::Direction dir;
if (SkPathPriv::IsSimpleClosedRect(fPath, &rect, &dir, &start)) {
return RectGeo(rect).strokeAndFillIsConvertedToFill(paint);
}
return false;
}
bool isNonPath(const SkPaint& paint) const override {
return fPath.isLine(nullptr) || fPath.isEmpty();
}
private:
bool isAxisAlignedLine() const {
SkPoint pts[2];
if (!fPath.isLine(pts)) {
return false;
}
return pts[0].fX == pts[1].fX || pts[0].fY == pts[1].fY;
}
bool isUnclosedRect() const {
bool closed;
return fPath.isRect(nullptr, &closed, nullptr) && !closed;
}
SkPath fPath;
};
class RRectPathGeo : public PathGeo {
public:
enum class RRectForStroke { kNo, kYes };
RRectPathGeo(const SkPath& path, const SkRRect& equivalentRRect, RRectForStroke rrectForStroke,
Invert invert)
: PathGeo(path, invert)
, fRRect(equivalentRRect)
, fRRectForStroke(rrectForStroke) {}
RRectPathGeo(const SkPath& path, const SkRect& equivalentRect, RRectForStroke rrectForStroke,
Invert invert)
: RRectPathGeo(path, SkRRect::MakeRect(equivalentRect), rrectForStroke, invert) {}
bool isNonPath(const SkPaint& paint) const override {
if (SkPaint::kFill_Style == paint.getStyle() || RRectForStroke::kYes == fRRectForStroke) {
return true;
}
return false;
}
const SkRRect& rrect() const { return fRRect; }
private:
SkRRect fRRect;
RRectForStroke fRRectForStroke;
};
class TestCase {
public:
TestCase(const Geo& geo, const SkPaint& paint, skiatest::Reporter* r,
SkScalar scale = SK_Scalar1) : fBase(geo.makeShape(paint)) {
this->init(r, scale);
}
template<typename... ShapeArgs>
TestCase(skiatest::Reporter* r, ShapeArgs... shapeArgs)
: fBase(shapeArgs...) {
this->init(r, SK_Scalar1);
}
TestCase(const GrShape& shape, skiatest::Reporter* r, SkScalar scale = SK_Scalar1)
: fBase(shape) {
this->init(r, scale);
}
struct SelfExpectations {
bool fPEHasEffect;
bool fPEHasValidKey;
bool fStrokeApplies;
};
void testExpectations(skiatest::Reporter* reporter, SelfExpectations expectations) const;
enum ComparisonExpecation {
kAllDifferent_ComparisonExpecation,
kSameUpToPE_ComparisonExpecation,
kSameUpToStroke_ComparisonExpecation,
kAllSame_ComparisonExpecation,
};
void compare(skiatest::Reporter*, const TestCase& that, ComparisonExpecation) const;
const GrShape& baseShape() const { return fBase; }
const GrShape& appliedPathEffectShape() const { return fAppliedPE; }
const GrShape& appliedFullStyleShape() const { return fAppliedFull; }
// The returned array's count will be 0 if the key shape has no key.
const Key& baseKey() const { return fBaseKey; }
const Key& appliedPathEffectKey() const { return fAppliedPEKey; }
const Key& appliedFullStyleKey() const { return fAppliedFullKey; }
const Key& appliedPathEffectThenStrokeKey() const { return fAppliedPEThenStrokeKey; }
private:
static void CheckBounds(skiatest::Reporter* r, const GrShape& shape, const SkRect& bounds) {
SkPath path;
shape.asPath(&path);
// If the bounds are empty, the path ought to be as well.
if (bounds.fLeft > bounds.fRight || bounds.fTop > bounds.fBottom) {
REPORTER_ASSERT(r, path.isEmpty());
return;
}
if (path.isEmpty()) {
return;
}
// The bounds API explicitly calls out that it does not consider inverseness.
SkPath p = path;
p.setFillType(SkPath::ConvertToNonInverseFillType(path.getFillType()));
REPORTER_ASSERT(r, test_bounds_by_rasterizing(p, bounds));
}
void init(skiatest::Reporter* r, SkScalar scale) {
fAppliedPE = fBase.applyStyle(GrStyle::Apply::kPathEffectOnly, scale);
fAppliedPEThenStroke = fAppliedPE.applyStyle(GrStyle::Apply::kPathEffectAndStrokeRec,
scale);
fAppliedFull = fBase.applyStyle(GrStyle::Apply::kPathEffectAndStrokeRec, scale);
make_key(&fBaseKey, fBase);
make_key(&fAppliedPEKey, fAppliedPE);
make_key(&fAppliedPEThenStrokeKey, fAppliedPEThenStroke);
make_key(&fAppliedFullKey, fAppliedFull);
// Applying the path effect and then the stroke should always be the same as applying
// both in one go.
REPORTER_ASSERT(r, fAppliedPEThenStrokeKey == fAppliedFullKey);
SkPath a, b;
fAppliedPEThenStroke.asPath(&a);
fAppliedFull.asPath(&b);
// If the output of the path effect is a rrect then it is possible for a and b to be
// different paths that fill identically. The reason is that fAppliedFull will do this:
// base -> apply path effect -> rrect_as_path -> stroke -> stroked_rrect_as_path
// fAppliedPEThenStroke will have converted the rrect_as_path back to a rrect. However,
// now that there is no longer a path effect, the direction and starting index get
// canonicalized before the stroke.
if (fAppliedPE.asRRect(nullptr, nullptr, nullptr, nullptr)) {
REPORTER_ASSERT(r, paths_fill_same(a, b));
} else {
REPORTER_ASSERT(r, a == b);
}
REPORTER_ASSERT(r, fAppliedFull.isEmpty() == fAppliedPEThenStroke.isEmpty());
SkPath path;
fBase.asPath(&path);
REPORTER_ASSERT(r, path.isEmpty() == fBase.isEmpty());
REPORTER_ASSERT(r, path.getSegmentMasks() == fBase.segmentMask());
fAppliedPE.asPath(&path);
REPORTER_ASSERT(r, path.isEmpty() == fAppliedPE.isEmpty());
REPORTER_ASSERT(r, path.getSegmentMasks() == fAppliedPE.segmentMask());
fAppliedFull.asPath(&path);
REPORTER_ASSERT(r, path.isEmpty() == fAppliedFull.isEmpty());
REPORTER_ASSERT(r, path.getSegmentMasks() == fAppliedFull.segmentMask());
CheckBounds(r, fBase, fBase.bounds());
CheckBounds(r, fAppliedPE, fAppliedPE.bounds());
CheckBounds(r, fAppliedPEThenStroke, fAppliedPEThenStroke.bounds());
CheckBounds(r, fAppliedFull, fAppliedFull.bounds());
SkRect styledBounds = fBase.styledBounds();
CheckBounds(r, fAppliedFull, styledBounds);
styledBounds = fAppliedPE.styledBounds();
CheckBounds(r, fAppliedFull, styledBounds);
// Check that the same path is produced when style is applied by GrShape and GrStyle.
SkPath preStyle;
SkPath postPathEffect;
SkPath postAllStyle;
fBase.asPath(&preStyle);
SkStrokeRec postPEStrokeRec(SkStrokeRec::kFill_InitStyle);
if (fBase.style().applyPathEffectToPath(&postPathEffect, &postPEStrokeRec, preStyle,
scale)) {
// run postPathEffect through GrShape to get any geometry reductions that would have
// occurred to fAppliedPE.
GrShape(postPathEffect, GrStyle(postPEStrokeRec, nullptr)).asPath(&postPathEffect);
SkPath testPath;
fAppliedPE.asPath(&testPath);
REPORTER_ASSERT(r, testPath == postPathEffect);
REPORTER_ASSERT(r, postPEStrokeRec.hasEqualEffect(fAppliedPE.style().strokeRec()));
}
SkStrokeRec::InitStyle fillOrHairline;
if (fBase.style().applyToPath(&postAllStyle, &fillOrHairline, preStyle, scale)) {
SkPath testPath;
fAppliedFull.asPath(&testPath);
if (fBase.style().hasPathEffect()) {
// Because GrShape always does two-stage application when there is a path effect
// there may be a reduction/canonicalization step between the path effect and
// strokerec not reflected in postAllStyle since it applied both the path effect
// and strokerec without analyzing the intermediate path.
REPORTER_ASSERT(r, paths_fill_same(postAllStyle, testPath));
} else {
// Make sure that postAllStyle sees any reductions/canonicalizations that GrShape
// would apply.
GrShape(postAllStyle, GrStyle(fillOrHairline)).asPath(&postAllStyle);
REPORTER_ASSERT(r, testPath == postAllStyle);
}
if (fillOrHairline == SkStrokeRec::kFill_InitStyle) {
REPORTER_ASSERT(r, fAppliedFull.style().isSimpleFill());
} else {
REPORTER_ASSERT(r, fAppliedFull.style().isSimpleHairline());
}
}
}
GrShape fBase;
GrShape fAppliedPE;
GrShape fAppliedPEThenStroke;
GrShape fAppliedFull;
Key fBaseKey;
Key fAppliedPEKey;
Key fAppliedPEThenStrokeKey;
Key fAppliedFullKey;
};
void TestCase::testExpectations(skiatest::Reporter* reporter, SelfExpectations expectations) const {
// The base's key should always be valid (unless the path is volatile)
REPORTER_ASSERT(reporter, fBaseKey.count());
if (expectations.fPEHasEffect) {
REPORTER_ASSERT(reporter, fBaseKey != fAppliedPEKey);
REPORTER_ASSERT(reporter, expectations.fPEHasValidKey == SkToBool(fAppliedPEKey.count()));
REPORTER_ASSERT(reporter, fBaseKey != fAppliedFullKey);
REPORTER_ASSERT(reporter, expectations.fPEHasValidKey == SkToBool(fAppliedFullKey.count()));
if (expectations.fStrokeApplies && expectations.fPEHasValidKey) {
REPORTER_ASSERT(reporter, fAppliedPEKey != fAppliedFullKey);
REPORTER_ASSERT(reporter, SkToBool(fAppliedFullKey.count()));
}
} else {
REPORTER_ASSERT(reporter, fBaseKey == fAppliedPEKey);
SkPath a, b;
fBase.asPath(&a);
fAppliedPE.asPath(&b);
REPORTER_ASSERT(reporter, a == b);
if (expectations.fStrokeApplies) {
REPORTER_ASSERT(reporter, fBaseKey != fAppliedFullKey);
} else {
REPORTER_ASSERT(reporter, fBaseKey == fAppliedFullKey);
}
}
}
static bool can_interchange_winding_and_even_odd_fill(const GrShape& shape) {
SkPath path;
shape.asPath(&path);
if (shape.style().hasNonDashPathEffect()) {
return false;
}
const SkStrokeRec::Style strokeRecStyle = shape.style().strokeRec().getStyle();
return strokeRecStyle == SkStrokeRec::kStroke_Style ||
strokeRecStyle == SkStrokeRec::kHairline_Style ||
(shape.style().isSimpleFill() && path.isConvex());
}
static void check_equivalence(skiatest::Reporter* r, const GrShape& a, const GrShape& b,
const Key& keyA, const Key& keyB) {
// GrShape only respects the input winding direction and start point for rrect shapes
// when there is a path effect. Thus, if there are two GrShapes representing the same rrect
// but one has a path effect in its style and the other doesn't then asPath() and the unstyled
// key will differ. GrShape will have canonicalized the direction and start point for the shape
// without the path effect. If *both* have path effects then they should have both preserved
// the direction and starting point.
// The asRRect() output params are all initialized just to silence compiler warnings about
// uninitialized variables.
SkRRect rrectA = SkRRect::MakeEmpty(), rrectB = SkRRect::MakeEmpty();
SkPath::Direction dirA = SkPath::kCW_Direction, dirB = SkPath::kCW_Direction;
unsigned startA = ~0U, startB = ~0U;
bool invertedA = true, invertedB = true;
bool aIsRRect = a.asRRect(&rrectA, &dirA, &startA, &invertedA);
bool bIsRRect = b.asRRect(&rrectB, &dirB, &startB, &invertedB);
bool aHasPE = a.style().hasPathEffect();
bool bHasPE = b.style().hasPathEffect();
bool allowSameRRectButDiffStartAndDir = (aIsRRect && bIsRRect) && (aHasPE != bHasPE);
// GrShape will close paths with simple fill style.
bool allowedClosednessDiff = (a.style().isSimpleFill() != b.style().isSimpleFill());
SkPath pathA, pathB;
a.asPath(&pathA);
b.asPath(&pathB);
// Having a dash path effect can allow 'a' but not 'b' to turn a inverse fill type into a
// non-inverse fill type (or vice versa).
bool ignoreInversenessDifference = false;
if (pathA.isInverseFillType() != pathB.isInverseFillType()) {
const GrShape* s1 = pathA.isInverseFillType() ? &a : &b;
const GrShape* s2 = pathA.isInverseFillType() ? &b : &a;
bool canDropInverse1 = s1->style().isDashed();
bool canDropInverse2 = s2->style().isDashed();
ignoreInversenessDifference = (canDropInverse1 != canDropInverse2);
}
bool ignoreWindingVsEvenOdd = false;
if (SkPath::ConvertToNonInverseFillType(pathA.getFillType()) !=
SkPath::ConvertToNonInverseFillType(pathB.getFillType())) {
bool aCanChange = can_interchange_winding_and_even_odd_fill(a);
bool bCanChange = can_interchange_winding_and_even_odd_fill(b);
if (aCanChange != bCanChange) {
ignoreWindingVsEvenOdd = true;
}
}
if (allowSameRRectButDiffStartAndDir) {
REPORTER_ASSERT(r, rrectA == rrectB);
REPORTER_ASSERT(r, paths_fill_same(pathA, pathB));
REPORTER_ASSERT(r, ignoreInversenessDifference || invertedA == invertedB);
} else {
SkPath pA = pathA;
SkPath pB = pathB;
REPORTER_ASSERT(r, a.inverseFilled() == pA.isInverseFillType());
REPORTER_ASSERT(r, b.inverseFilled() == pB.isInverseFillType());
if (ignoreInversenessDifference) {
pA.setFillType(SkPath::ConvertToNonInverseFillType(pathA.getFillType()));
pB.setFillType(SkPath::ConvertToNonInverseFillType(pathB.getFillType()));
}
if (ignoreWindingVsEvenOdd) {
pA.setFillType(pA.isInverseFillType() ? SkPath::kInverseEvenOdd_FillType
: SkPath::kEvenOdd_FillType);
pB.setFillType(pB.isInverseFillType() ? SkPath::kInverseEvenOdd_FillType
: SkPath::kEvenOdd_FillType);
}
if (!ignoreInversenessDifference && !ignoreWindingVsEvenOdd) {
REPORTER_ASSERT(r, keyA == keyB);
} else {
REPORTER_ASSERT(r, keyA != keyB);
}
if (allowedClosednessDiff) {
// GrShape will close paths with simple fill style. Make the non-filled path closed
// so that the comparision will succeed. Make sure both are closed before comparing.
pA.close();
pB.close();
}
REPORTER_ASSERT(r, pA == pB);
REPORTER_ASSERT(r, aIsRRect == bIsRRect);
if (aIsRRect) {
REPORTER_ASSERT(r, rrectA == rrectB);
REPORTER_ASSERT(r, dirA == dirB);
REPORTER_ASSERT(r, startA == startB);
REPORTER_ASSERT(r, ignoreInversenessDifference || invertedA == invertedB);
}
}
REPORTER_ASSERT(r, a.isEmpty() == b.isEmpty());
REPORTER_ASSERT(r, allowedClosednessDiff || a.knownToBeClosed() == b.knownToBeClosed());
// closedness can affect convexity.
REPORTER_ASSERT(r, allowedClosednessDiff || a.knownToBeConvex() == b.knownToBeConvex());
if (a.knownToBeConvex()) {
REPORTER_ASSERT(r, pathA.isConvex());
}
if (b.knownToBeConvex()) {
REPORTER_ASSERT(r, pathB.isConvex());
}
REPORTER_ASSERT(r, a.bounds() == b.bounds());
REPORTER_ASSERT(r, a.segmentMask() == b.segmentMask());
// Init these to suppress warnings.
SkPoint pts[4] {{0, 0,}, {0, 0}, {0, 0}, {0, 0}} ;
bool invertedLine[2] {true, true};
REPORTER_ASSERT(r, a.asLine(pts, &invertedLine[0]) == b.asLine(pts + 2, &invertedLine[1]));
// mayBeInverseFilledAfterStyling() is allowed to differ if one has a arbitrary PE and the other
// doesn't (since the PE can set any fill type on its output path).
// Moreover, dash style explicitly ignores inverseness. So if one is dashed but not the other
// then they may disagree about inverseness.
if (a.style().hasNonDashPathEffect() == b.style().hasNonDashPathEffect() &&
a.style().isDashed() == b.style().isDashed()) {
REPORTER_ASSERT(r, a.mayBeInverseFilledAfterStyling() ==
b.mayBeInverseFilledAfterStyling());
}
if (a.asLine(nullptr, nullptr)) {
REPORTER_ASSERT(r, pts[2] == pts[0] && pts[3] == pts[1]);
REPORTER_ASSERT(r, ignoreInversenessDifference || invertedLine[0] == invertedLine[1]);
REPORTER_ASSERT(r, invertedLine[0] == a.inverseFilled());
REPORTER_ASSERT(r, invertedLine[1] == b.inverseFilled());
}
REPORTER_ASSERT(r, ignoreInversenessDifference || a.inverseFilled() == b.inverseFilled());
}
void TestCase::compare(skiatest::Reporter* r, const TestCase& that,
ComparisonExpecation expectation) const {
SkPath a, b;
switch (expectation) {
case kAllDifferent_ComparisonExpecation:
REPORTER_ASSERT(r, fBaseKey != that.fBaseKey);
REPORTER_ASSERT(r, fAppliedPEKey != that.fAppliedPEKey);
REPORTER_ASSERT(r, fAppliedFullKey != that.fAppliedFullKey);
break;
case kSameUpToPE_ComparisonExpecation:
check_equivalence(r, fBase, that.fBase, fBaseKey, that.fBaseKey);
REPORTER_ASSERT(r, fAppliedPEKey != that.fAppliedPEKey);
REPORTER_ASSERT(r, fAppliedFullKey != that.fAppliedFullKey);
break;
case kSameUpToStroke_ComparisonExpecation:
check_equivalence(r, fBase, that.fBase, fBaseKey, that.fBaseKey);
check_equivalence(r, fAppliedPE, that.fAppliedPE, fAppliedPEKey, that.fAppliedPEKey);
REPORTER_ASSERT(r, fAppliedFullKey != that.fAppliedFullKey);
break;
case kAllSame_ComparisonExpecation:
check_equivalence(r, fBase, that.fBase, fBaseKey, that.fBaseKey);
check_equivalence(r, fAppliedPE, that.fAppliedPE, fAppliedPEKey, that.fAppliedPEKey);
check_equivalence(r, fAppliedFull, that.fAppliedFull, fAppliedFullKey,
that.fAppliedFullKey);
break;
}
}
} // namespace
static sk_sp<SkPathEffect> make_dash() {
static const SkScalar kIntervals[] = { 0.25, 3.f, 0.5, 2.f };
static const SkScalar kPhase = 0.75;
return SkDashPathEffect::Make(kIntervals, SK_ARRAY_COUNT(kIntervals), kPhase);
}
static sk_sp<SkPathEffect> make_null_dash() {
static const SkScalar kNullIntervals[] = {0, 0, 0, 0, 0, 0};
return SkDashPathEffect::Make(kNullIntervals, SK_ARRAY_COUNT(kNullIntervals), 0.f);
}
static void test_basic(skiatest::Reporter* reporter, const Geo& geo) {
sk_sp<SkPathEffect> dashPE = make_dash();
TestCase::SelfExpectations expectations;
SkPaint fill;
TestCase fillCase(geo, fill, reporter);
expectations.fPEHasEffect = false;
expectations.fPEHasValidKey = false;
expectations.fStrokeApplies = false;
fillCase.testExpectations(reporter, expectations);
// Test that another GrShape instance built from the same primitive is the same.
TestCase(geo, fill, reporter).compare(reporter, fillCase,
TestCase::kAllSame_ComparisonExpecation);
SkPaint stroke2RoundBevel;
stroke2RoundBevel.setStyle(SkPaint::kStroke_Style);
stroke2RoundBevel.setStrokeCap(SkPaint::kRound_Cap);
stroke2RoundBevel.setStrokeJoin(SkPaint::kBevel_Join);
stroke2RoundBevel.setStrokeWidth(2.f);
TestCase stroke2RoundBevelCase(geo, stroke2RoundBevel, reporter);
expectations.fPEHasValidKey = true;
expectations.fPEHasEffect = false;
expectations.fStrokeApplies = !geo.strokeIsConvertedToFill();
stroke2RoundBevelCase.testExpectations(reporter, expectations);
TestCase(geo, stroke2RoundBevel, reporter).compare(reporter, stroke2RoundBevelCase,
TestCase::kAllSame_ComparisonExpecation);
SkPaint stroke2RoundBevelDash = stroke2RoundBevel;
stroke2RoundBevelDash.setPathEffect(make_dash());
TestCase stroke2RoundBevelDashCase(geo, stroke2RoundBevelDash, reporter);
expectations.fPEHasValidKey = true;
expectations.fPEHasEffect = true;
expectations.fStrokeApplies = true;
stroke2RoundBevelDashCase.testExpectations(reporter, expectations);
TestCase(geo, stroke2RoundBevelDash, reporter).compare(reporter, stroke2RoundBevelDashCase,
TestCase::kAllSame_ComparisonExpecation);
if (geo.fillChangesGeom() || geo.strokeIsConvertedToFill()) {
fillCase.compare(reporter, stroke2RoundBevelCase,
TestCase::kAllDifferent_ComparisonExpecation);
fillCase.compare(reporter, stroke2RoundBevelDashCase,
TestCase::kAllDifferent_ComparisonExpecation);
} else {
fillCase.compare(reporter, stroke2RoundBevelCase,
TestCase::kSameUpToStroke_ComparisonExpecation);
fillCase.compare(reporter, stroke2RoundBevelDashCase,
TestCase::kSameUpToPE_ComparisonExpecation);
}
if (geo.strokeIsConvertedToFill()) {
stroke2RoundBevelCase.compare(reporter, stroke2RoundBevelDashCase,
TestCase::kAllDifferent_ComparisonExpecation);
} else {
stroke2RoundBevelCase.compare(reporter, stroke2RoundBevelDashCase,
TestCase::kSameUpToPE_ComparisonExpecation);
}
// Stroke and fill cases
SkPaint stroke2RoundBevelAndFill = stroke2RoundBevel;
stroke2RoundBevelAndFill.setStyle(SkPaint::kStrokeAndFill_Style);
TestCase stroke2RoundBevelAndFillCase(geo, stroke2RoundBevelAndFill, reporter);
expectations.fPEHasValidKey = true;
expectations.fPEHasEffect = false;
expectations.fStrokeApplies = !geo.strokeIsConvertedToFill();
stroke2RoundBevelAndFillCase.testExpectations(reporter, expectations);
TestCase(geo, stroke2RoundBevelAndFill, reporter).compare(reporter,
stroke2RoundBevelAndFillCase, TestCase::kAllSame_ComparisonExpecation);
SkPaint stroke2RoundBevelAndFillDash = stroke2RoundBevelDash;
stroke2RoundBevelAndFillDash.setStyle(SkPaint::kStrokeAndFill_Style);
TestCase stroke2RoundBevelAndFillDashCase(geo, stroke2RoundBevelAndFillDash, reporter);
expectations.fPEHasValidKey = true;
expectations.fPEHasEffect = false;
expectations.fStrokeApplies = !geo.strokeIsConvertedToFill();
stroke2RoundBevelAndFillDashCase.testExpectations(reporter, expectations);
TestCase(geo, stroke2RoundBevelAndFillDash, reporter).compare(
reporter, stroke2RoundBevelAndFillDashCase, TestCase::kAllSame_ComparisonExpecation);
stroke2RoundBevelAndFillDashCase.compare(reporter, stroke2RoundBevelAndFillCase,
TestCase::kAllSame_ComparisonExpecation);
SkPaint hairline;
hairline.setStyle(SkPaint::kStroke_Style);
hairline.setStrokeWidth(0.f);
TestCase hairlineCase(geo, hairline, reporter);
// Since hairline style doesn't change the SkPath data, it is keyed identically to fill (except
// in the line and unclosed rect cases).
if (geo.fillChangesGeom()) {
hairlineCase.compare(reporter, fillCase, TestCase::kAllDifferent_ComparisonExpecation);
} else {
hairlineCase.compare(reporter, fillCase, TestCase::kAllSame_ComparisonExpecation);
}
REPORTER_ASSERT(reporter, hairlineCase.baseShape().style().isSimpleHairline());
REPORTER_ASSERT(reporter, hairlineCase.appliedFullStyleShape().style().isSimpleHairline());
REPORTER_ASSERT(reporter, hairlineCase.appliedPathEffectShape().style().isSimpleHairline());
}
static void test_scale(skiatest::Reporter* reporter, const Geo& geo) {
sk_sp<SkPathEffect> dashPE = make_dash();
static const SkScalar kS1 = 1.f;
static const SkScalar kS2 = 2.f;
SkPaint fill;
TestCase fillCase1(geo, fill, reporter, kS1);
TestCase fillCase2(geo, fill, reporter, kS2);
// Scale doesn't affect fills.
fillCase1.compare(reporter, fillCase2, TestCase::kAllSame_ComparisonExpecation);
SkPaint hairline;
hairline.setStyle(SkPaint::kStroke_Style);
hairline.setStrokeWidth(0.f);
TestCase hairlineCase1(geo, hairline, reporter, kS1);
TestCase hairlineCase2(geo, hairline, reporter, kS2);
// Scale doesn't affect hairlines.
hairlineCase1.compare(reporter, hairlineCase2, TestCase::kAllSame_ComparisonExpecation);
SkPaint stroke;
stroke.setStyle(SkPaint::kStroke_Style);
stroke.setStrokeWidth(2.f);
TestCase strokeCase1(geo, stroke, reporter, kS1);
TestCase strokeCase2(geo, stroke, reporter, kS2);
// Scale affects the stroke
if (geo.strokeIsConvertedToFill()) {
REPORTER_ASSERT(reporter, !strokeCase1.baseShape().style().applies());
strokeCase1.compare(reporter, strokeCase2, TestCase::kAllSame_ComparisonExpecation);
} else {
strokeCase1.compare(reporter, strokeCase2, TestCase::kSameUpToStroke_ComparisonExpecation);
}
SkPaint strokeDash = stroke;
strokeDash.setPathEffect(make_dash());
TestCase strokeDashCase1(geo, strokeDash, reporter, kS1);
TestCase strokeDashCase2(geo, strokeDash, reporter, kS2);
// Scale affects the dash and the stroke.
strokeDashCase1.compare(reporter, strokeDashCase2,
TestCase::kSameUpToPE_ComparisonExpecation);
// Stroke and fill cases
SkPaint strokeAndFill = stroke;
strokeAndFill.setStyle(SkPaint::kStrokeAndFill_Style);
TestCase strokeAndFillCase1(geo, strokeAndFill, reporter, kS1);
TestCase strokeAndFillCase2(geo, strokeAndFill, reporter, kS2);
SkPaint strokeAndFillDash = strokeDash;
strokeAndFillDash.setStyle(SkPaint::kStrokeAndFill_Style);
// Dash is ignored for stroke and fill
TestCase strokeAndFillDashCase1(geo, strokeAndFillDash, reporter, kS1);
TestCase strokeAndFillDashCase2(geo, strokeAndFillDash, reporter, kS2);
// Scale affects the stroke, but check to make sure this didn't become a simpler shape (e.g.
// stroke-and-filled rect can become a rect), in which case the scale shouldn't matter and the
// geometries should agree.
if (geo.strokeAndFillIsConvertedToFill(strokeAndFillDash)) {
REPORTER_ASSERT(reporter, !strokeAndFillCase1.baseShape().style().applies());
strokeAndFillCase1.compare(reporter, strokeAndFillCase2,
TestCase::kAllSame_ComparisonExpecation);
strokeAndFillDashCase1.compare(reporter, strokeAndFillDashCase2,
TestCase::kAllSame_ComparisonExpecation);
} else {
strokeAndFillCase1.compare(reporter, strokeAndFillCase2,
TestCase::kSameUpToStroke_ComparisonExpecation);
}
strokeAndFillDashCase1.compare(reporter, strokeAndFillCase1,
TestCase::kAllSame_ComparisonExpecation);
strokeAndFillDashCase2.compare(reporter, strokeAndFillCase2,
TestCase::kAllSame_ComparisonExpecation);
}
template <typename T>
static void test_stroke_param_impl(skiatest::Reporter* reporter, const Geo& geo,
std::function<void(SkPaint*, T)> setter, T a, T b,
bool paramAffectsStroke,
bool paramAffectsDashAndStroke) {
// Set the stroke width so that we don't get hairline. However, call the setter afterward so
// that it can override the stroke width.
SkPaint strokeA;
strokeA.setStyle(SkPaint::kStroke_Style);
strokeA.setStrokeWidth(2.f);
setter(&strokeA, a);
SkPaint strokeB;
strokeB.setStyle(SkPaint::kStroke_Style);
strokeB.setStrokeWidth(2.f);
setter(&strokeB, b);
TestCase strokeACase(geo, strokeA, reporter);
TestCase strokeBCase(geo, strokeB, reporter);
if (paramAffectsStroke) {
// If stroking is immediately incorporated into a geometric transformation then the base
// shapes will differ.
if (geo.strokeIsConvertedToFill()) {
strokeACase.compare(reporter, strokeBCase,
TestCase::kAllDifferent_ComparisonExpecation);
} else {
strokeACase.compare(reporter, strokeBCase,
TestCase::kSameUpToStroke_ComparisonExpecation);
}
} else {
strokeACase.compare(reporter, strokeBCase, TestCase::kAllSame_ComparisonExpecation);
}
SkPaint strokeAndFillA = strokeA;
SkPaint strokeAndFillB = strokeB;
strokeAndFillA.setStyle(SkPaint::kStrokeAndFill_Style);
strokeAndFillB.setStyle(SkPaint::kStrokeAndFill_Style);
TestCase strokeAndFillACase(geo, strokeAndFillA, reporter);
TestCase strokeAndFillBCase(geo, strokeAndFillB, reporter);
if (paramAffectsStroke) {
// If stroking is immediately incorporated into a geometric transformation then the base
// shapes will differ.
if (geo.strokeAndFillIsConvertedToFill(strokeAndFillA) ||
geo.strokeAndFillIsConvertedToFill(strokeAndFillB)) {
strokeAndFillACase.compare(reporter, strokeAndFillBCase,
TestCase::kAllDifferent_ComparisonExpecation);
} else {
strokeAndFillACase.compare(reporter, strokeAndFillBCase,
TestCase::kSameUpToStroke_ComparisonExpecation);
}
} else {
strokeAndFillACase.compare(reporter, strokeAndFillBCase,
TestCase::kAllSame_ComparisonExpecation);
}
// Make sure stroking params don't affect fill style.
SkPaint fillA = strokeA, fillB = strokeB;
fillA.setStyle(SkPaint::kFill_Style);
fillB.setStyle(SkPaint::kFill_Style);
TestCase fillACase(geo, fillA, reporter);
TestCase fillBCase(geo, fillB, reporter);
fillACase.compare(reporter, fillBCase, TestCase::kAllSame_ComparisonExpecation);
// Make sure just applying the dash but not stroke gives the same key for both stroking
// variations.
SkPaint dashA = strokeA, dashB = strokeB;
dashA.setPathEffect(make_dash());
dashB.setPathEffect(make_dash());
TestCase dashACase(geo, dashA, reporter);
TestCase dashBCase(geo, dashB, reporter);
if (paramAffectsDashAndStroke) {
dashACase.compare(reporter, dashBCase, TestCase::kSameUpToStroke_ComparisonExpecation);
} else {
dashACase.compare(reporter, dashBCase, TestCase::kAllSame_ComparisonExpecation);
}
}
template <typename T>
static void test_stroke_param(skiatest::Reporter* reporter, const Geo& geo,
std::function<void(SkPaint*, T)> setter, T a, T b) {
test_stroke_param_impl(reporter, geo, setter, a, b, true, true);
};
static void test_stroke_cap(skiatest::Reporter* reporter, const Geo& geo) {
SkPaint hairline;
hairline.setStrokeWidth(0);
hairline.setStyle(SkPaint::kStroke_Style);
GrShape shape = geo.makeShape(hairline);
// The cap should only affect shapes that may be open.
bool affectsStroke = !shape.knownToBeClosed();
// Dashing adds ends that need caps.
bool affectsDashAndStroke = true;
test_stroke_param_impl<SkPaint::Cap>(
reporter,
geo,
[](SkPaint* p, SkPaint::Cap c) { p->setStrokeCap(c);},
SkPaint::kButt_Cap, SkPaint::kRound_Cap,
affectsStroke,
affectsDashAndStroke);
};
static bool shape_known_not_to_have_joins(const GrShape& shape) {
return shape.asLine(nullptr, nullptr) || shape.isEmpty();
}
static void test_stroke_join(skiatest::Reporter* reporter, const Geo& geo) {
SkPaint hairline;
hairline.setStrokeWidth(0);
hairline.setStyle(SkPaint::kStroke_Style);
GrShape shape = geo.makeShape(hairline);
// GrShape recognizes certain types don't have joins and will prevent the join type from
// affecting the style key.
// Dashing doesn't add additional joins. However, GrShape currently loses track of this
// after applying the dash.
bool affectsStroke = !shape_known_not_to_have_joins(shape);
test_stroke_param_impl<SkPaint::Join>(
reporter,
geo,
[](SkPaint* p, SkPaint::Join j) { p->setStrokeJoin(j);},
SkPaint::kRound_Join, SkPaint::kBevel_Join,
affectsStroke, true);
};
static void test_miter_limit(skiatest::Reporter* reporter, const Geo& geo) {
auto setMiterJoinAndLimit = [](SkPaint* p, SkScalar miter) {
p->setStrokeJoin(SkPaint::kMiter_Join);
p->setStrokeMiter(miter);
};
auto setOtherJoinAndLimit = [](SkPaint* p, SkScalar miter) {
p->setStrokeJoin(SkPaint::kRound_Join);
p->setStrokeMiter(miter);
};
SkPaint hairline;
hairline.setStrokeWidth(0);
hairline.setStyle(SkPaint::kStroke_Style);
GrShape shape = geo.makeShape(hairline);
bool mayHaveJoins = !shape_known_not_to_have_joins(shape);
// The miter limit should affect stroked and dashed-stroked cases when the join type is
// miter.
test_stroke_param_impl<SkScalar>(
reporter,
geo,
setMiterJoinAndLimit,
0.5f, 0.75f,
mayHaveJoins,
true);
// The miter limit should not affect stroked and dashed-stroked cases when the join type is
// not miter.
test_stroke_param_impl<SkScalar>(
reporter,
geo,
setOtherJoinAndLimit,
0.5f, 0.75f,
false,
false);
}
static void test_dash_fill(skiatest::Reporter* reporter, const Geo& geo) {
// A dash with no stroke should have no effect
using DashFactoryFn = sk_sp<SkPathEffect>(*)();
for (DashFactoryFn md : {&make_dash, &make_null_dash}) {
SkPaint dashFill;
dashFill.setPathEffect((*md)());
TestCase dashFillCase(geo, dashFill, reporter);
TestCase fillCase(geo, SkPaint(), reporter);
dashFillCase.compare(reporter, fillCase, TestCase::kAllSame_ComparisonExpecation);
}
}
void test_null_dash(skiatest::Reporter* reporter, const Geo& geo) {
SkPaint fill;
SkPaint stroke;
stroke.setStyle(SkPaint::kStroke_Style);
stroke.setStrokeWidth(1.f);
SkPaint dash;
dash.setStyle(SkPaint::kStroke_Style);
dash.setStrokeWidth(1.f);
dash.setPathEffect(make_dash());
SkPaint nullDash;
nullDash.setStyle(SkPaint::kStroke_Style);
nullDash.setStrokeWidth(1.f);
nullDash.setPathEffect(make_null_dash());
TestCase fillCase(geo, fill, reporter);
TestCase strokeCase(geo, stroke, reporter);
TestCase dashCase(geo, dash, reporter);
TestCase nullDashCase(geo, nullDash, reporter);
// We expect the null dash to be ignored so nullDashCase should match strokeCase, always.
nullDashCase.compare(reporter, strokeCase, TestCase::kAllSame_ComparisonExpecation);
// Check whether the fillCase or strokeCase/nullDashCase would undergo a geometric tranformation
// on construction in order to determine how to compare the fill and stroke.
if (geo.fillChangesGeom() || geo.strokeIsConvertedToFill()) {
nullDashCase.compare(reporter, fillCase, TestCase::kAllDifferent_ComparisonExpecation);
} else {
nullDashCase.compare(reporter, fillCase, TestCase::kSameUpToStroke_ComparisonExpecation);
}
// In the null dash case we may immediately convert to a fill, but not for the normal dash case.
if (geo.strokeIsConvertedToFill()) {
nullDashCase.compare(reporter, dashCase, TestCase::kAllDifferent_ComparisonExpecation);
} else {
nullDashCase.compare(reporter, dashCase, TestCase::kSameUpToPE_ComparisonExpecation);
}
}
void test_path_effect_makes_rrect(skiatest::Reporter* reporter, const Geo& geo) {
/**
* This path effect takes any input path and turns it into a rrect. It passes through stroke
* info.
*/
class RRectPathEffect : SkPathEffect {
public:
static const SkRRect& RRect() {
static const SkRRect kRRect = SkRRect::MakeRectXY(SkRect::MakeWH(12, 12), 3, 5);
return kRRect;
}
bool filterPath(SkPath* dst, const SkPath& src, SkStrokeRec*,
const SkRect* cullR) const override {
dst->reset();
dst->addRRect(RRect());
return true;
}
void computeFastBounds(SkRect* dst, const SkRect& src) const override {
*dst = RRect().getBounds();
}
static sk_sp<SkPathEffect> Make() { return sk_sp<SkPathEffect>(new RRectPathEffect); }
Factory getFactory() const override { return nullptr; }
void toString(SkString*) const override {}
private:
RRectPathEffect() {}
};
SkPaint fill;
TestCase fillGeoCase(geo, fill, reporter);
SkPaint pe;
pe.setPathEffect(RRectPathEffect::Make());
TestCase geoPECase(geo, pe, reporter);
SkPaint peStroke;
peStroke.setPathEffect(RRectPathEffect::Make());
peStroke.setStrokeWidth(2.f);
peStroke.setStyle(SkPaint::kStroke_Style);
TestCase geoPEStrokeCase(geo, peStroke, reporter);
// Check whether constructing the filled case would cause the base shape to have a different
// geometry (because of a geometric transformation upon initial GrShape construction).
if (geo.fillChangesGeom()) {
fillGeoCase.compare(reporter, geoPECase, TestCase::kAllDifferent_ComparisonExpecation);
fillGeoCase.compare(reporter, geoPEStrokeCase,
TestCase::kAllDifferent_ComparisonExpecation);
} else {
fillGeoCase.compare(reporter, geoPECase, TestCase::kSameUpToPE_ComparisonExpecation);
fillGeoCase.compare(reporter, geoPEStrokeCase, TestCase::kSameUpToPE_ComparisonExpecation);
}
geoPECase.compare(reporter, geoPEStrokeCase,
TestCase::kSameUpToStroke_ComparisonExpecation);
TestCase rrectFillCase(reporter, RRectPathEffect::RRect(), fill);
SkPaint stroke = peStroke;
stroke.setPathEffect(nullptr);
TestCase rrectStrokeCase(reporter, RRectPathEffect::RRect(), stroke);
SkRRect rrect;
// Applying the path effect should make a SkRRect shape. There is no further stroking in the
// geoPECase, so the full style should be the same as just the PE.
REPORTER_ASSERT(reporter, geoPECase.appliedPathEffectShape().asRRect(&rrect, nullptr, nullptr,
nullptr));
REPORTER_ASSERT(reporter, rrect == RRectPathEffect::RRect());
REPORTER_ASSERT(reporter, geoPECase.appliedPathEffectKey() == rrectFillCase.baseKey());
REPORTER_ASSERT(reporter, geoPECase.appliedFullStyleShape().asRRect(&rrect, nullptr, nullptr,
nullptr));
REPORTER_ASSERT(reporter, rrect == RRectPathEffect::RRect());
REPORTER_ASSERT(reporter, geoPECase.appliedFullStyleKey() == rrectFillCase.baseKey());
// In the PE+stroke case applying the full style should be the same as just stroking the rrect.
REPORTER_ASSERT(reporter, geoPEStrokeCase.appliedPathEffectShape().asRRect(&rrect, nullptr,
nullptr, nullptr));
REPORTER_ASSERT(reporter, rrect == RRectPathEffect::RRect());
REPORTER_ASSERT(reporter, geoPEStrokeCase.appliedPathEffectKey() == rrectFillCase.baseKey());
REPORTER_ASSERT(reporter, !geoPEStrokeCase.appliedFullStyleShape().asRRect(&rrect, nullptr,
nullptr, nullptr));
REPORTER_ASSERT(reporter, geoPEStrokeCase.appliedFullStyleKey() ==
rrectStrokeCase.appliedFullStyleKey());
}
void test_unknown_path_effect(skiatest::Reporter* reporter, const Geo& geo) {
/**
* This path effect just adds two lineTos to the input path.
*/
class AddLineTosPathEffect : SkPathEffect {
public:
bool filterPath(SkPath* dst, const SkPath& src, SkStrokeRec*,
const SkRect* cullR) const override {
*dst = src;
// To avoid triggering data-based keying of paths with few verbs we add many segments.
for (int i = 0; i < 100; ++i) {
dst->lineTo(SkIntToScalar(i), SkIntToScalar(i));
}
return true;
}
void computeFastBounds(SkRect* dst, const SkRect& src) const override {
*dst = src;
dst->growToInclude(0, 0);
dst->growToInclude(100, 100);
}
static sk_sp<SkPathEffect> Make() { return sk_sp<SkPathEffect>(new AddLineTosPathEffect); }
Factory getFactory() const override { return nullptr; }
void toString(SkString*) const override {}
private:
AddLineTosPathEffect() {}
};
// This path effect should make the keys invalid when it is applied. We only produce a path
// effect key for dash path effects. So the only way another arbitrary path effect can produce
// a styled result with a key is to produce a non-path shape that has a purely geometric key.
SkPaint peStroke;
peStroke.setPathEffect(AddLineTosPathEffect::Make());
peStroke.setStrokeWidth(2.f);
peStroke.setStyle(SkPaint::kStroke_Style);
TestCase geoPEStrokeCase(geo, peStroke, reporter);
TestCase::SelfExpectations expectations;
expectations.fPEHasEffect = true;
expectations.fPEHasValidKey = false;
expectations.fStrokeApplies = true;
geoPEStrokeCase.testExpectations(reporter, expectations);
}
void test_make_hairline_path_effect(skiatest::Reporter* reporter, const Geo& geo) {
/**
* This path effect just changes the stroke rec to hairline.
*/
class MakeHairlinePathEffect : SkPathEffect {
public:
bool filterPath(SkPath* dst, const SkPath& src, SkStrokeRec* strokeRec,
const SkRect* cullR) const override {
*dst = src;
strokeRec->setHairlineStyle();
return true;
}
void computeFastBounds(SkRect* dst, const SkRect& src) const override { *dst = src; }
static sk_sp<SkPathEffect> Make() {
return sk_sp<SkPathEffect>(new MakeHairlinePathEffect);
}
Factory getFactory() const override { return nullptr; }
void toString(SkString*) const override {}
private:
MakeHairlinePathEffect() {}
};
SkPaint fill;
SkPaint pe;
pe.setPathEffect(MakeHairlinePathEffect::Make());
TestCase peCase(geo, pe, reporter);
SkPath a, b, c;
peCase.baseShape().asPath(&a);
peCase.appliedPathEffectShape().asPath(&b);
peCase.appliedFullStyleShape().asPath(&c);
if (geo.isNonPath(pe)) {
// RRect types can have a change in start index or direction after the PE is applied. This
// is because once the PE is applied, GrShape may canonicalize the dir and index since it
// is not germane to the styling any longer.
// Instead we just check that the paths would fill the same both before and after styling.
REPORTER_ASSERT(reporter, paths_fill_same(a, b));
REPORTER_ASSERT(reporter, paths_fill_same(a, c));
} else {
// The base shape cannot perform canonicalization on the path's fill type because of an
// unknown path effect. However, after the path effect is applied the resulting hairline
// shape will canonicalize the path fill type since hairlines (and stroking in general)
// don't distinguish between even/odd and non-zero winding.
a.setFillType(b.getFillType());
REPORTER_ASSERT(reporter, a == b);
REPORTER_ASSERT(reporter, a == c);
// If the resulting path is small enough then it will have a key.
REPORTER_ASSERT(reporter, paths_fill_same(a, b));
REPORTER_ASSERT(reporter, paths_fill_same(a, c));
REPORTER_ASSERT(reporter, peCase.appliedPathEffectKey().empty());
REPORTER_ASSERT(reporter, peCase.appliedFullStyleKey().empty());
}
REPORTER_ASSERT(reporter, peCase.appliedPathEffectShape().style().isSimpleHairline());
REPORTER_ASSERT(reporter, peCase.appliedFullStyleShape().style().isSimpleHairline());
}
void test_volatile_path(skiatest::Reporter* reporter, const Geo& geo) {
SkPath vPath = geo.path();
vPath.setIsVolatile(true);
SkPaint dashAndStroke;
dashAndStroke.setPathEffect(make_dash());
dashAndStroke.setStrokeWidth(2.f);
dashAndStroke.setStyle(SkPaint::kStroke_Style);
TestCase volatileCase(reporter, vPath, dashAndStroke);
// We expect a shape made from a volatile path to have a key iff the shape is recognized
// as a specialized geometry.
if (geo.isNonPath(dashAndStroke)) {
REPORTER_ASSERT(reporter, SkToBool(volatileCase.baseKey().count()));
// In this case all the keys should be identical to the non-volatile case.
TestCase nonVolatileCase(reporter, geo.path(), dashAndStroke);
volatileCase.compare(reporter, nonVolatileCase, TestCase::kAllSame_ComparisonExpecation);
} else {
// None of the keys should be valid.
REPORTER_ASSERT(reporter, !SkToBool(volatileCase.baseKey().count()));
REPORTER_ASSERT(reporter, !SkToBool(volatileCase.appliedPathEffectKey().count()));
REPORTER_ASSERT(reporter, !SkToBool(volatileCase.appliedFullStyleKey().count()));
REPORTER_ASSERT(reporter, !SkToBool(volatileCase.appliedPathEffectThenStrokeKey().count()));
}
}
void test_path_effect_makes_empty_shape(skiatest::Reporter* reporter, const Geo& geo) {
/**
* This path effect returns an empty path.
*/
class EmptyPathEffect : SkPathEffect {
public:
bool filterPath(SkPath* dst, const SkPath& src, SkStrokeRec*,
const SkRect* cullR) const override {
dst->reset();
return true;
}
void computeFastBounds(SkRect* dst, const SkRect& src) const override {
dst->setEmpty();
}
static sk_sp<SkPathEffect> Make() { return sk_sp<SkPathEffect>(new EmptyPathEffect); }
Factory getFactory() const override { return nullptr; }
void toString(SkString*) const override {}
private:
EmptyPathEffect() {}
};
SkPath emptyPath;
GrShape emptyShape(emptyPath);
Key emptyKey;
make_key(&emptyKey, emptyShape);
REPORTER_ASSERT(reporter, emptyShape.isEmpty());
SkPaint pe;
pe.setPathEffect(EmptyPathEffect::Make());
TestCase geoCase(geo, pe, reporter);
REPORTER_ASSERT(reporter, geoCase.appliedFullStyleKey() == emptyKey);
REPORTER_ASSERT(reporter, geoCase.appliedPathEffectKey() == emptyKey);
REPORTER_ASSERT(reporter, geoCase.appliedPathEffectThenStrokeKey() == emptyKey);
REPORTER_ASSERT(reporter, geoCase.appliedPathEffectShape().isEmpty());
REPORTER_ASSERT(reporter, geoCase.appliedFullStyleShape().isEmpty());
SkPaint peStroke;
peStroke.setPathEffect(EmptyPathEffect::Make());
peStroke.setStrokeWidth(2.f);
peStroke.setStyle(SkPaint::kStroke_Style);
TestCase geoPEStrokeCase(geo, peStroke, reporter);
REPORTER_ASSERT(reporter, geoPEStrokeCase.appliedFullStyleKey() == emptyKey);
REPORTER_ASSERT(reporter, geoPEStrokeCase.appliedPathEffectKey() == emptyKey);
REPORTER_ASSERT(reporter, geoPEStrokeCase.appliedPathEffectThenStrokeKey() == emptyKey);
REPORTER_ASSERT(reporter, geoPEStrokeCase.appliedPathEffectShape().isEmpty());
REPORTER_ASSERT(reporter, geoPEStrokeCase.appliedFullStyleShape().isEmpty());
}
void test_path_effect_fails(skiatest::Reporter* reporter, const Geo& geo) {
/**
* This path effect always fails to apply.
*/
class FailurePathEffect : SkPathEffect {
public:
bool filterPath(SkPath* dst, const SkPath& src, SkStrokeRec*,
const SkRect* cullR) const override {
return false;
}
void computeFastBounds(SkRect* dst, const SkRect& src) const override {
*dst = src;
}
static sk_sp<SkPathEffect> Make() { return sk_sp<SkPathEffect>(new FailurePathEffect); }
Factory getFactory() const override { return nullptr; }
void toString(SkString*) const override {}
private:
FailurePathEffect() {}
};
SkPaint fill;
TestCase fillCase(geo, fill, reporter);
SkPaint pe;
pe.setPathEffect(FailurePathEffect::Make());
TestCase peCase(geo, pe, reporter);
SkPaint stroke;
stroke.setStrokeWidth(2.f);
stroke.setStyle(SkPaint::kStroke_Style);
TestCase strokeCase(geo, stroke, reporter);
SkPaint peStroke = stroke;
peStroke.setPathEffect(FailurePathEffect::Make());
TestCase peStrokeCase(geo, peStroke, reporter);
// In general the path effect failure can cause some of the TestCase::compare() tests to fail
// for at least two reasons: 1) We will initially treat the shape as unkeyable because of the
// path effect, but then when the path effect fails we can key it. 2) GrShape will change its
// mind about whether a unclosed rect is actually rect. The path effect initially bars us from
// closing it but after the effect fails we can (for the fill+pe case). This causes different
// routes through GrShape to have equivalent but different representations of the path (closed
// or not) but that fill the same.
SkPath a;
SkPath b;
fillCase.appliedPathEffectShape().asPath(&a);
peCase.appliedPathEffectShape().asPath(&b);
REPORTER_ASSERT(reporter, paths_fill_same(a, b));
fillCase.appliedFullStyleShape().asPath(&a);
peCase.appliedFullStyleShape().asPath(&b);
REPORTER_ASSERT(reporter, paths_fill_same(a, b));
strokeCase.appliedPathEffectShape().asPath(&a);
peStrokeCase.appliedPathEffectShape().asPath(&b);
REPORTER_ASSERT(reporter, paths_fill_same(a, b));
strokeCase.appliedFullStyleShape().asPath(&a);
peStrokeCase.appliedFullStyleShape().asPath(&b);
REPORTER_ASSERT(reporter, paths_fill_same(a, b));
}
void test_empty_shape(skiatest::Reporter* reporter) {
SkPath emptyPath;
SkPaint fill;
TestCase fillEmptyCase(reporter, emptyPath, fill);
REPORTER_ASSERT(reporter, fillEmptyCase.baseShape().isEmpty());
REPORTER_ASSERT(reporter, fillEmptyCase.appliedPathEffectShape().isEmpty());
REPORTER_ASSERT(reporter, fillEmptyCase.appliedFullStyleShape().isEmpty());
Key emptyKey(fillEmptyCase.baseKey());
REPORTER_ASSERT(reporter, emptyKey.count());
TestCase::SelfExpectations expectations;
expectations.fStrokeApplies = false;
expectations.fPEHasEffect = false;
// This will test whether applying style preserves emptiness
fillEmptyCase.testExpectations(reporter, expectations);
// Stroking an empty path should have no effect
SkPath emptyPath2;
SkPaint stroke;
stroke.setStrokeWidth(2.f);
stroke.setStyle(SkPaint::kStroke_Style);
TestCase strokeEmptyCase(reporter, emptyPath2, stroke);
strokeEmptyCase.compare(reporter, fillEmptyCase, TestCase::kAllSame_ComparisonExpecation);
// Dashing and stroking an empty path should have no effect
SkPath emptyPath3;
SkPaint dashAndStroke;
dashAndStroke.setPathEffect(make_dash());
dashAndStroke.setStrokeWidth(2.f);
dashAndStroke.setStyle(SkPaint::kStroke_Style);
TestCase dashAndStrokeEmptyCase(reporter, emptyPath3, dashAndStroke);
dashAndStrokeEmptyCase.compare(reporter, fillEmptyCase,
TestCase::kAllSame_ComparisonExpecation);
// A shape made from an empty rrect should behave the same as an empty path.
SkRRect emptyRRect = SkRRect::MakeRect(SkRect::MakeEmpty());
REPORTER_ASSERT(reporter, emptyRRect.getType() == SkRRect::kEmpty_Type);
TestCase dashAndStrokeEmptyRRectCase(reporter, emptyRRect, dashAndStroke);
dashAndStrokeEmptyRRectCase.compare(reporter, fillEmptyCase,
TestCase::kAllSame_ComparisonExpecation);
// Same for a rect.
SkRect emptyRect = SkRect::MakeEmpty();
TestCase dashAndStrokeEmptyRectCase(reporter, emptyRect, dashAndStroke);
dashAndStrokeEmptyRectCase.compare(reporter, fillEmptyCase,
TestCase::kAllSame_ComparisonExpecation);
}
// rect and oval types have rrect start indices that collapse to the same point. Here we select the
// canonical point in these cases.
unsigned canonicalize_rrect_start(int s, const SkRRect& rrect) {
switch (rrect.getType()) {
case SkRRect::kRect_Type:
return (s + 1) & 0b110;
case SkRRect::kOval_Type:
return s & 0b110;
default:
return s;
}
}
void test_rrect(skiatest::Reporter* r, const SkRRect& rrect) {
enum Style {
kFill,
kStroke,
kHairline,
kStrokeAndFill
};
// SkStrokeRec has no default cons., so init with kFill before calling the setters below.
SkStrokeRec strokeRecs[4] { SkStrokeRec::kFill_InitStyle, SkStrokeRec::kFill_InitStyle,
SkStrokeRec::kFill_InitStyle, SkStrokeRec::kFill_InitStyle};
strokeRecs[kFill].setFillStyle();
strokeRecs[kStroke].setStrokeStyle(2.f);
strokeRecs[kHairline].setHairlineStyle();
strokeRecs[kStrokeAndFill].setStrokeStyle(3.f, true);
// Use a bevel join to avoid complications of stroke+filled rects becoming filled rects before
// applyStyle() is called.
strokeRecs[kStrokeAndFill].setStrokeParams(SkPaint::kButt_Cap, SkPaint::kBevel_Join, 1.f);
sk_sp<SkPathEffect> dashEffect = make_dash();
static constexpr Style kStyleCnt = static_cast<Style>(SK_ARRAY_COUNT(strokeRecs));
auto index = [](bool inverted,
SkPath::Direction dir,
unsigned start,
Style style,
bool dash) -> int {
return inverted * (2 * 8 * kStyleCnt * 2) +
dir * ( 8 * kStyleCnt * 2) +
start * ( kStyleCnt * 2) +
style * ( 2) +
dash;
};
static const SkPath::Direction kSecondDirection = static_cast<SkPath::Direction>(1);
const int cnt = index(true, kSecondDirection, 7, static_cast<Style>(kStyleCnt - 1), true) + 1;
SkAutoTArray<GrShape> shapes(cnt);
for (bool inverted : {false, true}) {
for (SkPath::Direction dir : {SkPath::kCW_Direction, SkPath::kCCW_Direction}) {
for (unsigned start = 0; start < 8; ++start) {
for (Style style : {kFill, kStroke, kHairline, kStrokeAndFill}) {
for (bool dash : {false, true}) {
SkPathEffect* pe = dash ? dashEffect.get() : nullptr;
shapes[index(inverted, dir, start, style, dash)] =
GrShape(rrect, dir, start, SkToBool(inverted),
GrStyle(strokeRecs[style], pe));
}
}
}
}
}
// Get the keys for some example shape instances that we'll use for comparision against the
// rest.
static constexpr SkPath::Direction kExamplesDir = SkPath::kCW_Direction;
static constexpr unsigned kExamplesStart = 0;
const GrShape& exampleFillCase = shapes[index(false, kExamplesDir, kExamplesStart, kFill,
false)];
Key exampleFillCaseKey;
make_key(&exampleFillCaseKey, exampleFillCase);
const GrShape& exampleStrokeAndFillCase = shapes[index(false, kExamplesDir, kExamplesStart,
kStrokeAndFill, false)];
Key exampleStrokeAndFillCaseKey;
make_key(&exampleStrokeAndFillCaseKey, exampleStrokeAndFillCase);
const GrShape& exampleInvFillCase = shapes[index(true, kExamplesDir, kExamplesStart, kFill,
false)];
Key exampleInvFillCaseKey;
make_key(&exampleInvFillCaseKey, exampleInvFillCase);
const GrShape& exampleInvStrokeAndFillCase = shapes[index(true, kExamplesDir, kExamplesStart,
kStrokeAndFill, false)];
Key exampleInvStrokeAndFillCaseKey;
make_key(&exampleInvStrokeAndFillCaseKey, exampleInvStrokeAndFillCase);
const GrShape& exampleStrokeCase = shapes[index(false, kExamplesDir, kExamplesStart, kStroke,
false)];
Key exampleStrokeCaseKey;
make_key(&exampleStrokeCaseKey, exampleStrokeCase);
const GrShape& exampleInvStrokeCase = shapes[index(true, kExamplesDir, kExamplesStart, kStroke,
false)];
Key exampleInvStrokeCaseKey;
make_key(&exampleInvStrokeCaseKey, exampleInvStrokeCase);
const GrShape& exampleHairlineCase = shapes[index(false, kExamplesDir, kExamplesStart,
kHairline, false)];
Key exampleHairlineCaseKey;
make_key(&exampleHairlineCaseKey, exampleHairlineCase);
const GrShape& exampleInvHairlineCase = shapes[index(true, kExamplesDir, kExamplesStart,
kHairline, false)];
Key exampleInvHairlineCaseKey;
make_key(&exampleInvHairlineCaseKey, exampleInvHairlineCase);
// These are dummy initializations to suppress warnings.
SkRRect queryRR = SkRRect::MakeEmpty();
SkPath::Direction queryDir = SkPath::kCW_Direction;
unsigned queryStart = ~0U;
bool queryInverted = true;
REPORTER_ASSERT(r, exampleFillCase.asRRect(&queryRR, &queryDir, &queryStart, &queryInverted));
REPORTER_ASSERT(r, queryRR == rrect);
REPORTER_ASSERT(r, SkPath::kCW_Direction == queryDir);
REPORTER_ASSERT(r, 0 == queryStart);
REPORTER_ASSERT(r, !queryInverted);
REPORTER_ASSERT(r, exampleInvFillCase.asRRect(&queryRR, &queryDir, &queryStart,
&queryInverted));
REPORTER_ASSERT(r, queryRR == rrect);
REPORTER_ASSERT(r, SkPath::kCW_Direction == queryDir);
REPORTER_ASSERT(r, 0 == queryStart);
REPORTER_ASSERT(r, queryInverted);
REPORTER_ASSERT(r, exampleStrokeAndFillCase.asRRect(&queryRR, &queryDir, &queryStart,
&queryInverted));
REPORTER_ASSERT(r, queryRR == rrect);
REPORTER_ASSERT(r, SkPath::kCW_Direction == queryDir);
REPORTER_ASSERT(r, 0 == queryStart);
REPORTER_ASSERT(r, !queryInverted);
REPORTER_ASSERT(r, exampleInvStrokeAndFillCase.asRRect(&queryRR, &queryDir, &queryStart,
&queryInverted));
REPORTER_ASSERT(r, queryRR == rrect);
REPORTER_ASSERT(r, SkPath::kCW_Direction == queryDir);
REPORTER_ASSERT(r, 0 == queryStart);
REPORTER_ASSERT(r, queryInverted);
REPORTER_ASSERT(r, exampleHairlineCase.asRRect(&queryRR, &queryDir, &queryStart,
&queryInverted));
REPORTER_ASSERT(r, queryRR == rrect);
REPORTER_ASSERT(r, SkPath::kCW_Direction == queryDir);
REPORTER_ASSERT(r, 0 == queryStart);
REPORTER_ASSERT(r, !queryInverted);
REPORTER_ASSERT(r, exampleInvHairlineCase.asRRect(&queryRR, &queryDir, &queryStart,
&queryInverted));
REPORTER_ASSERT(r, queryRR == rrect);
REPORTER_ASSERT(r, SkPath::kCW_Direction == queryDir);
REPORTER_ASSERT(r, 0 == queryStart);
REPORTER_ASSERT(r, queryInverted);
REPORTER_ASSERT(r, exampleStrokeCase.asRRect(&queryRR, &queryDir, &queryStart, &queryInverted));
REPORTER_ASSERT(r, queryRR == rrect);
REPORTER_ASSERT(r, SkPath::kCW_Direction == queryDir);
REPORTER_ASSERT(r, 0 == queryStart);
REPORTER_ASSERT(r, !queryInverted);
REPORTER_ASSERT(r, exampleInvStrokeCase.asRRect(&queryRR, &queryDir, &queryStart,
&queryInverted));
REPORTER_ASSERT(r, queryRR == rrect);
REPORTER_ASSERT(r, SkPath::kCW_Direction == queryDir);
REPORTER_ASSERT(r, 0 == queryStart);
REPORTER_ASSERT(r, queryInverted);
// Remember that the key reflects the geometry before styling is applied.
REPORTER_ASSERT(r, exampleFillCaseKey != exampleInvFillCaseKey);
REPORTER_ASSERT(r, exampleFillCaseKey == exampleStrokeAndFillCaseKey);
REPORTER_ASSERT(r, exampleFillCaseKey != exampleInvStrokeAndFillCaseKey);
REPORTER_ASSERT(r, exampleFillCaseKey == exampleStrokeCaseKey);
REPORTER_ASSERT(r, exampleFillCaseKey != exampleInvStrokeCaseKey);
REPORTER_ASSERT(r, exampleFillCaseKey == exampleHairlineCaseKey);
REPORTER_ASSERT(r, exampleFillCaseKey != exampleInvHairlineCaseKey);
REPORTER_ASSERT(r, exampleInvStrokeAndFillCaseKey == exampleInvFillCaseKey);
REPORTER_ASSERT(r, exampleInvStrokeAndFillCaseKey == exampleInvStrokeCaseKey);
REPORTER_ASSERT(r, exampleInvStrokeAndFillCaseKey == exampleInvHairlineCaseKey);
for (bool inverted : {false, true}) {
for (SkPath::Direction dir : {SkPath::kCW_Direction, SkPath::kCCW_Direction}) {
for (unsigned start = 0; start < 8; ++start) {
for (bool dash : {false, true}) {
const GrShape& fillCase = shapes[index(inverted, dir, start, kFill, dash)];
Key fillCaseKey;
make_key(&fillCaseKey, fillCase);
const GrShape& strokeAndFillCase = shapes[index(inverted, dir, start,
kStrokeAndFill, dash)];
Key strokeAndFillCaseKey;
make_key(&strokeAndFillCaseKey, strokeAndFillCase);
// Both fill and stroke-and-fill shapes must respect the inverseness and both
// ignore dashing.
REPORTER_ASSERT(r, !fillCase.style().pathEffect());
REPORTER_ASSERT(r, !strokeAndFillCase.style().pathEffect());
TestCase a(fillCase, r);
TestCase b(inverted ? exampleInvFillCase : exampleFillCase, r);
TestCase c(strokeAndFillCase, r);
TestCase d(inverted ? exampleInvStrokeAndFillCase
: exampleStrokeAndFillCase, r);
a.compare(r, b, TestCase::kAllSame_ComparisonExpecation);
c.compare(r, d, TestCase::kAllSame_ComparisonExpecation);
const GrShape& strokeCase = shapes[index(inverted, dir, start, kStroke, dash)];
const GrShape& hairlineCase = shapes[index(inverted, dir, start, kHairline,
dash)];
TestCase e(strokeCase, r);
TestCase g(hairlineCase, r);
// Both hairline and stroke shapes must respect the dashing.
if (dash) {
// Dashing always ignores the inverseness. skbug.com/5421
TestCase f(exampleStrokeCase, r);
TestCase h(exampleHairlineCase, r);
unsigned expectedStart = canonicalize_rrect_start(start, rrect);
REPORTER_ASSERT(r, strokeCase.style().pathEffect());
REPORTER_ASSERT(r, hairlineCase.style().pathEffect());
REPORTER_ASSERT(r, strokeCase.asRRect(&queryRR, &queryDir, &queryStart,
&queryInverted));
REPORTER_ASSERT(r, queryRR == rrect);
REPORTER_ASSERT(r, queryDir == dir);
REPORTER_ASSERT(r, queryStart == expectedStart);
REPORTER_ASSERT(r, !queryInverted);
REPORTER_ASSERT(r, hairlineCase.asRRect(&queryRR, &queryDir, &queryStart,
&queryInverted));
REPORTER_ASSERT(r, queryRR == rrect);
REPORTER_ASSERT(r, queryDir == dir);
REPORTER_ASSERT(r, queryStart == expectedStart);
REPORTER_ASSERT(r, !queryInverted);
// The pre-style case for the dash will match the non-dash example iff the
// dir and start match (dir=cw, start=0).
if (0 == expectedStart && SkPath::kCW_Direction == dir) {
e.compare(r, f, TestCase::kSameUpToPE_ComparisonExpecation);
g.compare(r, h, TestCase::kSameUpToPE_ComparisonExpecation);
} else {
e.compare(r, f, TestCase::kAllDifferent_ComparisonExpecation);
g.compare(r, h, TestCase::kAllDifferent_ComparisonExpecation);
}
} else {
TestCase f(inverted ? exampleInvStrokeCase : exampleStrokeCase, r);
TestCase h(inverted ? exampleInvHairlineCase : exampleHairlineCase, r);
REPORTER_ASSERT(r, !strokeCase.style().pathEffect());
REPORTER_ASSERT(r, !hairlineCase.style().pathEffect());
e.compare(r, f, TestCase::kAllSame_ComparisonExpecation);
g.compare(r, h, TestCase::kAllSame_ComparisonExpecation);
}
}
}
}
}
}
void test_lines(skiatest::Reporter* r) {
static constexpr SkPoint kA { 1, 1};
static constexpr SkPoint kB { 5, -9};
static constexpr SkPoint kC {-3, 17};
SkPath lineAB;
lineAB.moveTo(kA);
lineAB.lineTo(kB);
SkPath lineBA;
lineBA.moveTo(kB);
lineBA.lineTo(kA);
SkPath lineAC;
lineAC.moveTo(kB);
lineAC.lineTo(kC);
SkPath invLineAB = lineAB;
invLineAB.setFillType(SkPath::kInverseEvenOdd_FillType);
SkPaint fill;
SkPaint stroke;
stroke.setStyle(SkPaint::kStroke_Style);
stroke.setStrokeWidth(2.f);
SkPaint hairline;
hairline.setStyle(SkPaint::kStroke_Style);
hairline.setStrokeWidth(0.f);
SkPaint dash = stroke;
dash.setPathEffect(make_dash());
TestCase fillAB(r, lineAB, fill);
TestCase fillEmpty(r, SkPath(), fill);
fillAB.compare(r, fillEmpty, TestCase::kAllSame_ComparisonExpecation);
REPORTER_ASSERT(r, !fillAB.baseShape().asLine(nullptr, nullptr));
TestCase strokeAB(r, lineAB, stroke);
TestCase strokeBA(r, lineBA, stroke);
TestCase strokeAC(r, lineAC, stroke);
TestCase hairlineAB(r, lineAB, hairline);
TestCase hairlineBA(r, lineBA, hairline);
TestCase hairlineAC(r, lineAC, hairline);
TestCase dashAB(r, lineAB, dash);
TestCase dashBA(r, lineBA, dash);
TestCase dashAC(r, lineAC, dash);
strokeAB.compare(r, fillAB, TestCase::kAllDifferent_ComparisonExpecation);
strokeAB.compare(r, strokeBA, TestCase::kAllSame_ComparisonExpecation);
strokeAB.compare(r, strokeAC, TestCase::kAllDifferent_ComparisonExpecation);
hairlineAB.compare(r, hairlineBA, TestCase::kAllSame_ComparisonExpecation);
hairlineAB.compare(r, hairlineAC, TestCase::kAllDifferent_ComparisonExpecation);
dashAB.compare(r, dashBA, TestCase::kAllDifferent_ComparisonExpecation);
dashAB.compare(r, dashAC, TestCase::kAllDifferent_ComparisonExpecation);
strokeAB.compare(r, hairlineAB, TestCase::kSameUpToStroke_ComparisonExpecation);
// One of dashAB or dashBA should have the same line as strokeAB. It depends upon how
// GrShape canonicalizes line endpoints (when it can, i.e. when not dashed).
bool canonicalizeAsAB;
SkPoint canonicalPts[2] {kA, kB};
// Init these to suppress warnings.
bool inverted = true;
SkPoint pts[2] {{0, 0}, {0, 0}};
REPORTER_ASSERT(r, strokeAB.baseShape().asLine(pts, &inverted) && !inverted);
if (pts[0] == kA && pts[1] == kB) {
canonicalizeAsAB = true;
} else if (pts[1] == kA && pts[0] == kB) {
canonicalizeAsAB = false;
SkTSwap(canonicalPts[0], canonicalPts[1]);
} else {
ERRORF(r, "Should return pts (a,b) or (b, a)");
return;
};
strokeAB.compare(r, canonicalizeAsAB ? dashAB : dashBA,
TestCase::kSameUpToPE_ComparisonExpecation);
REPORTER_ASSERT(r, strokeAB.baseShape().asLine(pts, &inverted) && !inverted &&
pts[0] == canonicalPts[0] && pts[1] == canonicalPts[1]);
REPORTER_ASSERT(r, hairlineAB.baseShape().asLine(pts, &inverted) && !inverted &&
pts[0] == canonicalPts[0] && pts[1] == canonicalPts[1]);
REPORTER_ASSERT(r, dashAB.baseShape().asLine(pts, &inverted) && !inverted &&
pts[0] == kA && pts[1] == kB);
REPORTER_ASSERT(r, dashBA.baseShape().asLine(pts, &inverted) && !inverted &&
pts[0] == kB && pts[1] == kA);
TestCase strokeInvAB(r, invLineAB, stroke);
TestCase hairlineInvAB(r, invLineAB, hairline);
TestCase dashInvAB(r, invLineAB, dash);
strokeInvAB.compare(r, strokeAB, TestCase::kAllDifferent_ComparisonExpecation);
hairlineInvAB.compare(r, hairlineAB, TestCase::kAllDifferent_ComparisonExpecation);
// Dashing ignores inverse.
dashInvAB.compare(r, dashAB, TestCase::kAllSame_ComparisonExpecation);
REPORTER_ASSERT(r, strokeInvAB.baseShape().asLine(pts, &inverted) && inverted &&
pts[0] == canonicalPts[0] && pts[1] == canonicalPts[1]);
REPORTER_ASSERT(r, hairlineInvAB.baseShape().asLine(pts, &inverted) && inverted &&
pts[0] == canonicalPts[0] && pts[1] == canonicalPts[1]);
// Dashing ignores inverse.
REPORTER_ASSERT(r, dashInvAB.baseShape().asLine(pts, &inverted) && !inverted &&
pts[0] == kA && pts[1] == kB);
}
static void test_stroked_lines(skiatest::Reporter* r) {
// Paints to try
SkPaint buttCap;
buttCap.setStyle(SkPaint::kStroke_Style);
buttCap.setStrokeWidth(4);
buttCap.setStrokeCap(SkPaint::kButt_Cap);
SkPaint squareCap = buttCap;
squareCap.setStrokeCap(SkPaint::kSquare_Cap);
SkPaint roundCap = buttCap;
roundCap.setStrokeCap(SkPaint::kRound_Cap);
// vertical
SkPath linePath;
linePath.moveTo(4, 4);
linePath.lineTo(4, 5);
SkPaint fill;
TestCase(r, linePath, buttCap).compare(r, TestCase(r, SkRect::MakeLTRB(2, 4, 6, 5), fill),
TestCase::kAllSame_ComparisonExpecation);
TestCase(r, linePath, squareCap).compare(r, TestCase(r, SkRect::MakeLTRB(2, 2, 6, 7), fill),
TestCase::kAllSame_ComparisonExpecation);
TestCase(r, linePath, roundCap).compare(r,
TestCase(r, SkRRect::MakeRectXY(SkRect::MakeLTRB(2, 2, 6, 7), 2, 2), fill),
TestCase::kAllSame_ComparisonExpecation);
// horizontal
linePath.reset();
linePath.moveTo(4, 4);
linePath.lineTo(5, 4);
TestCase(r, linePath, buttCap).compare(r, TestCase(r, SkRect::MakeLTRB(4, 2, 5, 6), fill),
TestCase::kAllSame_ComparisonExpecation);
TestCase(r, linePath, squareCap).compare(r, TestCase(r, SkRect::MakeLTRB(2, 2, 7, 6), fill),
TestCase::kAllSame_ComparisonExpecation);
TestCase(r, linePath, roundCap).compare(r,
TestCase(r, SkRRect::MakeRectXY(SkRect::MakeLTRB(2, 2, 7, 6), 2, 2), fill),
TestCase::kAllSame_ComparisonExpecation);
// point
linePath.reset();
linePath.moveTo(4, 4);
linePath.lineTo(4, 4);
TestCase(r, linePath, buttCap).compare(r, TestCase(r, SkRect::MakeEmpty(), fill),
TestCase::kAllSame_ComparisonExpecation);
TestCase(r, linePath, squareCap).compare(r, TestCase(r, SkRect::MakeLTRB(2, 2, 6, 6), fill),
TestCase::kAllSame_ComparisonExpecation);
TestCase(r, linePath, roundCap).compare(r,
TestCase(r, SkRRect::MakeRectXY(SkRect::MakeLTRB(2, 2, 6, 6), 2, 2), fill),
TestCase::kAllSame_ComparisonExpecation);
}
static void test_short_path_keys(skiatest::Reporter* r) {
SkPaint paints[4];
paints[1].setStyle(SkPaint::kStroke_Style);
paints[1].setStrokeWidth(5.f);
paints[2].setStyle(SkPaint::kStroke_Style);
paints[2].setStrokeWidth(0.f);
paints[3].setStyle(SkPaint::kStrokeAndFill_Style);
paints[3].setStrokeWidth(5.f);
auto compare = [r, &paints] (const SkPath& pathA, const SkPath& pathB,
TestCase::ComparisonExpecation expectation) {
SkPath volatileA = pathA;
SkPath volatileB = pathB;
volatileA.setIsVolatile(true);
volatileB.setIsVolatile(true);
for (const SkPaint& paint : paints) {
REPORTER_ASSERT(r, !GrShape(volatileA, paint).hasUnstyledKey());
REPORTER_ASSERT(r, !GrShape(volatileB, paint).hasUnstyledKey());
for (PathGeo::Invert invert : {PathGeo::Invert::kNo, PathGeo::Invert::kYes}) {
TestCase caseA(PathGeo(pathA, invert), paint, r);
TestCase caseB(PathGeo(pathB, invert), paint, r);
caseA.compare(r, caseB, expectation);
}
}
};
SkPath pathA;
SkPath pathB;
// Two identical paths
pathA.lineTo(10.f, 10.f);
pathA.conicTo(20.f, 20.f, 20.f, 30.f, 0.7f);
pathB.lineTo(10.f, 10.f);
pathB.conicTo(20.f, 20.f, 20.f, 30.f, 0.7f);
compare(pathA, pathB, TestCase::kAllSame_ComparisonExpecation);
// Give path b a different point
pathB.reset();
pathB.lineTo(10.f, 10.f);
pathB.conicTo(21.f, 20.f, 20.f, 30.f, 0.7f);
compare(pathA, pathB, TestCase::kAllDifferent_ComparisonExpecation);
// Give path b a different conic weight
pathB.reset();
pathB.lineTo(10.f, 10.f);
pathB.conicTo(20.f, 20.f, 20.f, 30.f, 0.6f);
compare(pathA, pathB, TestCase::kAllDifferent_ComparisonExpecation);
// Give path b an extra lineTo verb
pathB.reset();
pathB.lineTo(10.f, 10.f);
pathB.conicTo(20.f, 20.f, 20.f, 30.f, 0.6f);
pathB.lineTo(50.f, 50.f);
compare(pathA, pathB, TestCase::kAllDifferent_ComparisonExpecation);
// Give path b a close
pathB.reset();
pathB.lineTo(10.f, 10.f);
pathB.conicTo(20.f, 20.f, 20.f, 30.f, 0.7f);
pathB.close();
compare(pathA, pathB, TestCase::kAllDifferent_ComparisonExpecation);
}
DEF_TEST(GrShape, reporter) {
SkTArray<std::unique_ptr<Geo>> geos;
SkTArray<std::unique_ptr<RRectPathGeo>> rrectPathGeos;
for (auto r : { SkRect::MakeWH(10, 20),
SkRect::MakeWH(-10, -20),
SkRect::MakeWH(-10, 20),
SkRect::MakeWH(10, -20)}) {
geos.emplace_back(new RectGeo(r));
SkPath rectPath;
rectPath.addRect(r);
geos.emplace_back(new RRectPathGeo(rectPath, r, RRectPathGeo::RRectForStroke::kYes,
PathGeo::Invert::kNo));
geos.emplace_back(new RRectPathGeo(rectPath, r, RRectPathGeo::RRectForStroke::kYes,
PathGeo::Invert::kYes));
rrectPathGeos.emplace_back(new RRectPathGeo(rectPath, r, RRectPathGeo::RRectForStroke::kYes,
PathGeo::Invert::kNo));
}
for (auto rr : { SkRRect::MakeRect(SkRect::MakeWH(10, 10)),
SkRRect::MakeRectXY(SkRect::MakeWH(10, 10), 3, 4),
SkRRect::MakeOval(SkRect::MakeWH(20, 20))}) {
geos.emplace_back(new RRectGeo(rr));
test_rrect(reporter, rr);
SkPath rectPath;
rectPath.addRRect(rr);
geos.emplace_back(new RRectPathGeo(rectPath, rr, RRectPathGeo::RRectForStroke::kYes,
PathGeo::Invert::kNo));
geos.emplace_back(new RRectPathGeo(rectPath, rr, RRectPathGeo::RRectForStroke::kYes,
PathGeo::Invert::kYes));
rrectPathGeos.emplace_back(new RRectPathGeo(rectPath, rr,
RRectPathGeo::RRectForStroke::kYes,
PathGeo::Invert::kNo));
}
SkPath openRectPath;
openRectPath.moveTo(0, 0);
openRectPath.lineTo(10, 0);
openRectPath.lineTo(10, 10);
openRectPath.lineTo(0, 10);
geos.emplace_back(new RRectPathGeo(openRectPath, SkRect::MakeWH(10, 10),
RRectPathGeo::RRectForStroke::kNo, PathGeo::Invert::kNo));
geos.emplace_back(new RRectPathGeo(openRectPath, SkRect::MakeWH(10, 10),
RRectPathGeo::RRectForStroke::kNo, PathGeo::Invert::kYes));
rrectPathGeos.emplace_back(new RRectPathGeo(openRectPath, SkRect::MakeWH(10, 10),
RRectPathGeo::RRectForStroke::kNo,
PathGeo::Invert::kNo));
SkPath quadPath;
quadPath.quadTo(10, 10, 5, 8);
geos.emplace_back(new PathGeo(quadPath, PathGeo::Invert::kNo));
geos.emplace_back(new PathGeo(quadPath, PathGeo::Invert::kYes));
SkPath linePath;
linePath.lineTo(10, 10);
geos.emplace_back(new PathGeo(linePath, PathGeo::Invert::kNo));
geos.emplace_back(new PathGeo(linePath, PathGeo::Invert::kYes));
// Horizontal and vertical paths become rrects when stroked.
SkPath vLinePath;
vLinePath.lineTo(0, 10);
geos.emplace_back(new PathGeo(vLinePath, PathGeo::Invert::kNo));
geos.emplace_back(new PathGeo(vLinePath, PathGeo::Invert::kYes));
SkPath hLinePath;
hLinePath.lineTo(10, 0);
geos.emplace_back(new PathGeo(hLinePath, PathGeo::Invert::kNo));
geos.emplace_back(new PathGeo(hLinePath, PathGeo::Invert::kYes));
for (int i = 0; i < geos.count(); ++i) {
test_basic(reporter, *geos[i]);
test_scale(reporter, *geos[i]);
test_dash_fill(reporter, *geos[i]);
test_null_dash(reporter, *geos[i]);
// Test modifying various stroke params.
test_stroke_param<SkScalar>(
reporter, *geos[i],
[](SkPaint* p, SkScalar w) { p->setStrokeWidth(w);},
SkIntToScalar(2), SkIntToScalar(4));
test_stroke_join(reporter, *geos[i]);
test_stroke_cap(reporter, *geos[i]);
test_miter_limit(reporter, *geos[i]);
test_path_effect_makes_rrect(reporter, *geos[i]);
test_unknown_path_effect(reporter, *geos[i]);
test_path_effect_makes_empty_shape(reporter, *geos[i]);
test_path_effect_fails(reporter, *geos[i]);
test_make_hairline_path_effect(reporter, *geos[i]);
test_volatile_path(reporter, *geos[i]);
}
for (int i = 0; i < rrectPathGeos.count(); ++i) {
const RRectPathGeo& rrgeo = *rrectPathGeos[i];
SkPaint fillPaint;
TestCase fillPathCase(reporter, rrgeo.path(), fillPaint);
SkRRect rrect;
REPORTER_ASSERT(reporter, rrgeo.isNonPath(fillPaint) ==
fillPathCase.baseShape().asRRect(&rrect, nullptr, nullptr,
nullptr));
if (rrgeo.isNonPath(fillPaint)) {
TestCase fillPathCase2(reporter, rrgeo.path(), fillPaint);
REPORTER_ASSERT(reporter, rrect == rrgeo.rrect());
TestCase fillRRectCase(reporter, rrect, fillPaint);
fillPathCase2.compare(reporter, fillRRectCase,
TestCase::kAllSame_ComparisonExpecation);
}
SkPaint strokePaint;
strokePaint.setStrokeWidth(3.f);
strokePaint.setStyle(SkPaint::kStroke_Style);
TestCase strokePathCase(reporter, rrgeo.path(), strokePaint);
if (rrgeo.isNonPath(strokePaint)) {
REPORTER_ASSERT(reporter, strokePathCase.baseShape().asRRect(&rrect, nullptr, nullptr,
nullptr));
REPORTER_ASSERT(reporter, rrect == rrgeo.rrect());
TestCase strokeRRectCase(reporter, rrect, strokePaint);
strokePathCase.compare(reporter, strokeRRectCase,
TestCase::kAllSame_ComparisonExpecation);
}
}
// Test a volatile empty path.
test_volatile_path(reporter, PathGeo(SkPath(), PathGeo::Invert::kNo));
test_empty_shape(reporter);
test_lines(reporter);
test_stroked_lines(reporter);
test_short_path_keys(reporter);
}
#endif