skia2/tests/GrStyledShapeTest.cpp
Michael Ludwig f38b711222 Reland "Refactor geometry union capabilities out of GrStyledShape"
This reverts commit af312c9d40.

Reason for revert: improved performance, updated empty point cap behavior
to make chrome happy.

Because of the performance regression in the original CL, this is a bit
more to it than just updating cap behavior. Summary of changes for perf:
1. In asPath(), only call reset() if the type isn't a path or arc.
   Otherwise it was just a wasted realloc of an empty path ref.
2. Rewrote the GrShape::simplify() to not progress through every shape
   type in order, it just jumps to the appropriate type.
3. Have simplify() return whether or not the shape started out closed,
   so we don't have to call GrShape::closed(), which is costly when the
   shape is a path.
4. Expose the GrShape's type enum so GrStyledShape's key writing can use
   switches instead of a giant block of ifs (where path happened to be
   last)

The regressions showed up most heavily on desk_mapsvg and desk_chalkboard
SKPs on the Android skpbench marks. On my system, I was able to
reproduce a similar %-regression from ToT and the original CL on the
chalkboard (but not mapsvg).

Master ranged between 5.1 and 5.3ms, original CL ranged from 5.6-5.8
and after the changes listed above, I got it down to 5.3-5.5. It's not
ideal but I haven't been able to figure out anything more substantial
that it could be. At this point it may just be code layout and/or the
fact that it's now split into two types.


Original change's description:
> Revert "Refactor geometry union capabilities out of GrStyledShape"
>
> This reverts commit 2becdde074.
>
> Reason for revert: likely breaking cc unit test due to empty shape cap change.
>
> Original change's description:
> > Refactor geometry union capabilities out of GrStyledShape
> >
> > The geometry union part of GrStyledShape is now held in GrShape. For the
> > most part, GrShape is entirely style agnostic and focuses on storing
> > the various types of geometry, and destructing them gracefully. It also
> > provides a public API that unifies functionality across all shape types,
> > such as contains() and bounds().
> >
> > GrStyledShape now just owns a GrShape and a GrStyle, and handles the
> > additional simplification logic that relies on knowing the effects of
> > the style on the draw. This is where GrShape makes some allowances for
> > style. Its simplify() function accepts flags that enable/disable various
> > simplification optimizations. Currently these are designed around
> > what is needed to respect path effects and stroking behaviors in
> > GrStyledShape. The main other user of GrShape (the new clip stack) will
> > always provide all flags since it treats every shape as if it were
> > simply filled.
> >
> > Several other related refactorings were taken at the same time:
> > 1. The implementations for asNestedRects, asRRect, etc. were moved out
> >    of the header and into the cpp file for GrStyledShape.
> > 2. GrRenderTargetContext relies on GrStyledShape for its stroke rect
> >    fallbacks.
> > 3. GrShape can hold points, lines, and rects explicitly. This let me
> >    simplify the stroke reasoning.
> >
> > Change-Id: I9fe75613fee51c30b4049b2b5a422daf80a1a86e
> > Reviewed-on: https://skia-review.googlesource.com/c/skia/+/284803
> > Commit-Queue: Michael Ludwig <michaelludwig@google.com>
> > Reviewed-by: Brian Salomon <bsalomon@google.com>
> > Reviewed-by: Chris Dalton <csmartdalton@google.com>
>
> TBR=bsalomon@google.com,csmartdalton@google.com,michaelludwig@google.com
>
> Change-Id: I2af5782e072e0ccb4a87f903bb88cbe335b9613f
> No-Presubmit: true
> No-Tree-Checks: true
> No-Try: true
> Reviewed-on: https://skia-review.googlesource.com/c/skia/+/286039
> Reviewed-by: Michael Ludwig <michaelludwig@google.com>
> Commit-Queue: Michael Ludwig <michaelludwig@google.com>

Change-Id: I8c614573582084f2e9ee0d73f93812e0a7c13983
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/286396
Reviewed-by: Brian Salomon <bsalomon@google.com>
Commit-Queue: Michael Ludwig <michaelludwig@google.com>
2020-04-30 19:36:43 +00:00

2356 lines
104 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 "include/core/SkCanvas.h"
#include "include/core/SkPath.h"
#include "include/core/SkSurface.h"
#include "include/effects/SkDashPathEffect.h"
#include "include/pathops/SkPathOps.h"
#include "src/core/SkClipOpPriv.h"
#include "src/core/SkRectPriv.h"
#include "src/gpu/geometry/GrStyledShape.h"
#include "tests/Test.h"
#include <initializer_list>
#include <functional>
#include <utility>
uint32_t GrStyledShape::testingOnly_getOriginalGenerationID() const {
if (const auto* lp = this->originalPathForListeners()) {
return lp->getGenerationID();
}
return SkPath().getGenerationID();
}
bool GrStyledShape::testingOnly_isPath() const {
return fShape.isPath();
}
bool GrStyledShape::testingOnly_isNonVolatilePath() const {
return fShape.isPath() && !fShape.path().isVolatile();
}
using Key = SkTArray<uint32_t>;
static bool make_key(Key* key, const GrStyledShape& 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 = 2;
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, kDifference_SkClipOp);
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;
}
static bool can_interchange_winding_and_even_odd_fill(const GrStyledShape& 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 GrStyledShape& a, const GrStyledShape& b,
const Key& keyA, const Key& keyB) {
// GrStyledShape only respects the input winding direction and start point for rrect shapes
// when there is a path effect. Thus, if there are two GrStyledShapes 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. GrStyledShape 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();
SkPathDirection dirA = SkPathDirection::kCW, dirB = SkPathDirection::kCW;
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);
// GrStyledShape 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 GrStyledShape* s1 = pathA.isInverseFillType() ? &a : &b;
const GrStyledShape* s2 = pathA.isInverseFillType() ? &b : &a;
bool canDropInverse1 = s1->style().isDashed();
bool canDropInverse2 = s2->style().isDashed();
ignoreInversenessDifference = (canDropInverse1 != canDropInverse2);
}
bool ignoreWindingVsEvenOdd = false;
if (SkPathFillType_ConvertToNonInverse(pathA.getFillType()) !=
SkPathFillType_ConvertToNonInverse(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(SkPathFillType_ConvertToNonInverse(pathA.getFillType()));
pB.setFillType(SkPathFillType_ConvertToNonInverse(pathB.getFillType()));
}
if (ignoreWindingVsEvenOdd) {
pA.setFillType(pA.isInverseFillType() ? SkPathFillType::kInverseEvenOdd
: SkPathFillType::kEvenOdd);
pB.setFillType(pB.isInverseFillType() ? SkPathFillType::kInverseEvenOdd
: SkPathFillType::kEvenOdd);
}
if (!ignoreInversenessDifference && !ignoreWindingVsEvenOdd) {
REPORTER_ASSERT(r, keyA == keyB);
} else {
REPORTER_ASSERT(r, keyA != keyB);
}
if (allowedClosednessDiff) {
// GrStyledShape 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());
}
static void check_original_path_ids(skiatest::Reporter* r, const GrStyledShape& base,
const GrStyledShape& pe, const GrStyledShape& peStroke,
const GrStyledShape& full) {
bool baseIsNonVolatilePath = base.testingOnly_isNonVolatilePath();
bool peIsPath = pe.testingOnly_isPath();
bool peStrokeIsPath = peStroke.testingOnly_isPath();
bool fullIsPath = full.testingOnly_isPath();
REPORTER_ASSERT(r, peStrokeIsPath == fullIsPath);
uint32_t baseID = base.testingOnly_getOriginalGenerationID();
uint32_t peID = pe.testingOnly_getOriginalGenerationID();
uint32_t peStrokeID = peStroke.testingOnly_getOriginalGenerationID();
uint32_t fullID = full.testingOnly_getOriginalGenerationID();
// All empty paths have the same gen ID
uint32_t emptyID = SkPath().getGenerationID();
// If we started with a real path, then our genID should match that path's gen ID (and not be
// empty). If we started with a simple shape or a volatile path, our original path should have
// been reset.
REPORTER_ASSERT(r, baseIsNonVolatilePath == (baseID != emptyID));
// For the derived shapes, if they're simple types, their original paths should have been reset
REPORTER_ASSERT(r, peIsPath || (peID == emptyID));
REPORTER_ASSERT(r, peStrokeIsPath || (peStrokeID == emptyID));
REPORTER_ASSERT(r, fullIsPath || (fullID == emptyID));
if (!peIsPath) {
// If the path effect produces a simple shape, then there are no unbroken chains to test
return;
}
// From here on, we know that the path effect produced a shape that was a "real" path
if (baseIsNonVolatilePath) {
REPORTER_ASSERT(r, baseID == peID);
}
if (peStrokeIsPath) {
REPORTER_ASSERT(r, peID == peStrokeID);
REPORTER_ASSERT(r, peStrokeID == fullID);
}
if (baseIsNonVolatilePath && peStrokeIsPath) {
REPORTER_ASSERT(r, baseID == peStrokeID);
REPORTER_ASSERT(r, baseID == fullID);
}
}
void test_inversions(skiatest::Reporter* r, const GrStyledShape& shape, const Key& shapeKey) {
GrStyledShape preserve = GrStyledShape::MakeFilled(
shape, GrStyledShape::FillInversion::kPreserve);
Key preserveKey;
make_key(&preserveKey, preserve);
GrStyledShape flip = GrStyledShape::MakeFilled(shape, GrStyledShape::FillInversion::kFlip);
Key flipKey;
make_key(&flipKey, flip);
GrStyledShape inverted = GrStyledShape::MakeFilled(
shape, GrStyledShape::FillInversion::kForceInverted);
Key invertedKey;
make_key(&invertedKey, inverted);
GrStyledShape noninverted = GrStyledShape::MakeFilled(
shape, GrStyledShape::FillInversion::kForceNoninverted);
Key noninvertedKey;
make_key(&noninvertedKey, noninverted);
if (invertedKey.count() || noninvertedKey.count()) {
REPORTER_ASSERT(r, invertedKey != noninvertedKey);
}
if (shape.style().isSimpleFill()) {
check_equivalence(r, shape, preserve, shapeKey, preserveKey);
}
if (shape.inverseFilled()) {
check_equivalence(r, preserve, inverted, preserveKey, invertedKey);
check_equivalence(r, flip, noninverted, flipKey, noninvertedKey);
} else {
check_equivalence(r, preserve, noninverted, preserveKey, noninvertedKey);
check_equivalence(r, flip, inverted, flipKey, invertedKey);
}
GrStyledShape doubleFlip = GrStyledShape::MakeFilled(flip, GrStyledShape::FillInversion::kFlip);
Key doubleFlipKey;
make_key(&doubleFlipKey, doubleFlip);
// It can be the case that the double flip has no key but preserve does. This happens when the
// original shape has an inherited style key. That gets dropped on the first inversion flip.
if (preserveKey.count() && !doubleFlipKey.count()) {
preserveKey.reset();
}
check_equivalence(r, preserve, doubleFlip, preserveKey, doubleFlipKey);
}
namespace {
/**
* Geo is a factory for creating a GrStyledShape from another representation. It also answers some
* questions about expected behavior for GrStyledShape given the inputs.
*/
class Geo {
public:
virtual ~Geo() {}
virtual GrStyledShape makeShape(const SkPaint&) const = 0;
virtual SkPath path() const = 0;
// These functions allow tests to check for special cases where style gets
// applied by GrStyledShape in its constructor (without calling GrStyledShape::applyStyle).
// These unfortunately rely on knowing details of GrStyledShape's implementation.
// These predicates are factored out here to avoid littering the rest of the
// test code with GrStyledShape 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 GrStyledShape 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;
}
GrStyledShape makeShape(const SkPaint& paint) const override {
return GrStyledShape(fRect, paint);
}
bool strokeAndFillIsConvertedToFill(const SkPaint& paint) const override {
SkASSERT(paint.getStyle() == SkPaint::kStrokeAndFill_Style);
// Converted to an outset rectangle or round rect
return (paint.getStrokeJoin() == SkPaint::kMiter_Join &&
paint.getStrokeMiter() >= SK_ScalarSqrt2) ||
paint.getStrokeJoin() == SkPaint::kRound_Join;
}
private:
SkRect fRect;
};
class RRectGeo : public Geo {
public:
RRectGeo(const SkRRect& rrect) : fRRect(rrect) {}
GrStyledShape makeShape(const SkPaint& paint) const override {
return GrStyledShape(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 ArcGeo : public Geo {
public:
ArcGeo(const SkRect& oval, SkScalar startAngle, SkScalar sweepAngle, bool useCenter)
: fOval(oval)
, fStartAngle(startAngle)
, fSweepAngle(sweepAngle)
, fUseCenter(useCenter) {}
SkPath path() const override {
SkPath path;
SkPathPriv::CreateDrawArcPath(&path, fOval, fStartAngle, fSweepAngle, fUseCenter, false);
return path;
}
GrStyledShape makeShape(const SkPaint& paint) const override {
return GrStyledShape::MakeArc(fOval, fStartAngle, fSweepAngle, fUseCenter, GrStyle(paint));
}
// GrStyledShape specializes when created from arc params but it doesn't recognize arcs from
// SkPath.
bool isNonPath(const SkPaint& paint) const override { return false; }
private:
SkRect fOval;
SkScalar fStartAngle;
SkScalar fSweepAngle;
bool fUseCenter;
};
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() == SkPathFillType::kEvenOdd) {
fPath.setFillType(SkPathFillType::kInverseEvenOdd);
} else {
SkASSERT(fPath.getFillType() == SkPathFillType::kWinding);
fPath.setFillType(SkPathFillType::kInverseWinding);
}
}
}
GrStyledShape makeShape(const SkPaint& paint) const override {
return GrStyledShape(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);
}
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;
SkPathDirection 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(new GrStyledShape(geo.makeShape(paint))) {
this->init(r, scale);
}
template <typename... ShapeArgs>
TestCase(skiatest::Reporter* r, ShapeArgs... shapeArgs)
: fBase(new GrStyledShape(shapeArgs...)) {
this->init(r, SK_Scalar1);
}
TestCase(const GrStyledShape& shape, skiatest::Reporter* r, SkScalar scale = SK_Scalar1)
: fBase(new GrStyledShape(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 GrStyledShape& baseShape() const { return *fBase; }
const GrStyledShape& appliedPathEffectShape() const { return *fAppliedPE; }
const GrStyledShape& 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 GrStyledShape& 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(SkPathFillType_ConvertToNonInverse(path.getFillType()));
REPORTER_ASSERT(r, test_bounds_by_rasterizing(p, bounds));
}
void init(skiatest::Reporter* r, SkScalar scale) {
fAppliedPE.reset(new GrStyledShape);
fAppliedPEThenStroke.reset(new GrStyledShape);
fAppliedFull.reset(new GrStyledShape);
*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);
// All shapes should report the same "original" path, so that path renderers can get to it
// if necessary.
check_original_path_ids(r, *fBase, *fAppliedPE, *fAppliedPEThenStroke, *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 GrStyledShape 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 GrStyledShape to get any geometry reductions that would
// have occurred to fAppliedPE.
GrStyledShape(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 GrStyledShape 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
// GrStyledShape would apply.
GrStyledShape(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());
}
}
test_inversions(r, *fBase, fBaseKey);
test_inversions(r, *fAppliedPE, fAppliedPEKey);
test_inversions(r, *fAppliedFull, fAppliedFullKey);
}
std::unique_ptr<GrStyledShape> fBase;
std::unique_ptr<GrStyledShape> fAppliedPE;
std::unique_ptr<GrStyledShape> fAppliedPEThenStroke;
std::unique_ptr<GrStyledShape> 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);
}
}
}
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);
}
// We make enough TestCases, and they're large enough, that on Google3 builds we exceed
// the maximum stack frame limit. make_TestCase() moves those temporaries over to the heap.
template <typename... Args>
static std::unique_ptr<TestCase> make_TestCase(Args&&... args) {
return std::unique_ptr<TestCase>{ new TestCase(std::forward<Args>(args)...) };
}
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 GrStyledShape instance built from the same primitive is the same.
make_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);
make_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);
make_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);
make_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);
make_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);
GrStyledShape 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 GrStyledShape& 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);
GrStyledShape shape = geo.makeShape(hairline);
// GrStyledShape 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, GrStyledShape 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);
GrStyledShape 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;
}
static sk_sp<SkPathEffect> Make() { return sk_sp<SkPathEffect>(new RRectPathEffect); }
Factory getFactory() const override { return nullptr; }
const char* getTypeName() const override { return nullptr; }
protected:
bool onFilterPath(SkPath* dst, const SkPath& src, SkStrokeRec*,
const SkRect* cullR) const override {
dst->reset();
dst->addRRect(RRect());
return true;
}
SkRect onComputeFastBounds(const SkRect& src) const override {
return RRect().getBounds();
}
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 GrStyledShape 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:
static sk_sp<SkPathEffect> Make() { return sk_sp<SkPathEffect>(new AddLineTosPathEffect); }
Factory getFactory() const override { return nullptr; }
const char* getTypeName() const override { return nullptr; }
protected:
bool onFilterPath(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;
}
SkRect onComputeFastBounds(const SkRect& src) const override {
SkRect dst = src;
SkRectPriv::GrowToInclude(&dst, {0, 0});
SkRectPriv::GrowToInclude(&dst, {100, 100});
return dst;
}
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:
static sk_sp<SkPathEffect> Make() {
return sk_sp<SkPathEffect>(new MakeHairlinePathEffect);
}
Factory getFactory() const override { return nullptr; }
const char* getTypeName() const override { return nullptr; }
protected:
bool onFilterPath(SkPath* dst, const SkPath& src, SkStrokeRec* strokeRec,
const SkRect* cullR) const override {
*dst = src;
strokeRec->setHairlineStyle();
return true;
}
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, GrStyledShape 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 (possibly inverted)
*/
class EmptyPathEffect : SkPathEffect {
public:
static sk_sp<SkPathEffect> Make(bool invert) {
return sk_sp<SkPathEffect>(new EmptyPathEffect(invert));
}
Factory getFactory() const override { return nullptr; }
const char* getTypeName() const override { return nullptr; }
protected:
bool onFilterPath(SkPath* dst, const SkPath& src, SkStrokeRec*,
const SkRect* cullR) const override {
dst->reset();
if (fInvert) {
dst->toggleInverseFillType();
}
return true;
}
SkRect onComputeFastBounds(const SkRect& src) const override {
return { 0, 0, 0, 0 };
}
private:
bool fInvert;
EmptyPathEffect(bool invert) : fInvert(invert) {}
};
SkPath emptyPath;
GrStyledShape emptyShape(emptyPath);
Key emptyKey;
make_key(&emptyKey, emptyShape);
REPORTER_ASSERT(reporter, emptyShape.isEmpty());
emptyPath.toggleInverseFillType();
GrStyledShape invertedEmptyShape(emptyPath);
Key invertedEmptyKey;
make_key(&invertedEmptyKey, invertedEmptyShape);
REPORTER_ASSERT(reporter, invertedEmptyShape.isEmpty());
REPORTER_ASSERT(reporter, invertedEmptyKey != emptyKey);
SkPaint pe;
pe.setPathEffect(EmptyPathEffect::Make(false));
TestCase geoPECase(geo, pe, reporter);
REPORTER_ASSERT(reporter, geoPECase.appliedFullStyleKey() == emptyKey);
REPORTER_ASSERT(reporter, geoPECase.appliedPathEffectKey() == emptyKey);
REPORTER_ASSERT(reporter, geoPECase.appliedPathEffectThenStrokeKey() == emptyKey);
REPORTER_ASSERT(reporter, geoPECase.appliedPathEffectShape().isEmpty());
REPORTER_ASSERT(reporter, geoPECase.appliedFullStyleShape().isEmpty());
REPORTER_ASSERT(reporter, !geoPECase.appliedPathEffectShape().inverseFilled());
REPORTER_ASSERT(reporter, !geoPECase.appliedFullStyleShape().inverseFilled());
SkPaint peStroke;
peStroke.setPathEffect(EmptyPathEffect::Make(false));
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());
REPORTER_ASSERT(reporter, !geoPEStrokeCase.appliedPathEffectShape().inverseFilled());
REPORTER_ASSERT(reporter, !geoPEStrokeCase.appliedFullStyleShape().inverseFilled());
pe.setPathEffect(EmptyPathEffect::Make(true));
TestCase geoPEInvertCase(geo, pe, reporter);
REPORTER_ASSERT(reporter, geoPEInvertCase.appliedFullStyleKey() == invertedEmptyKey);
REPORTER_ASSERT(reporter, geoPEInvertCase.appliedPathEffectKey() == invertedEmptyKey);
REPORTER_ASSERT(reporter, geoPEInvertCase.appliedPathEffectThenStrokeKey() == invertedEmptyKey);
REPORTER_ASSERT(reporter, geoPEInvertCase.appliedPathEffectShape().isEmpty());
REPORTER_ASSERT(reporter, geoPEInvertCase.appliedFullStyleShape().isEmpty());
REPORTER_ASSERT(reporter, geoPEInvertCase.appliedPathEffectShape().inverseFilled());
REPORTER_ASSERT(reporter, geoPEInvertCase.appliedFullStyleShape().inverseFilled());
peStroke.setPathEffect(EmptyPathEffect::Make(true));
TestCase geoPEInvertStrokeCase(geo, peStroke, reporter);
REPORTER_ASSERT(reporter, geoPEInvertStrokeCase.appliedFullStyleKey() == invertedEmptyKey);
REPORTER_ASSERT(reporter, geoPEInvertStrokeCase.appliedPathEffectKey() == invertedEmptyKey);
REPORTER_ASSERT(reporter,
geoPEInvertStrokeCase.appliedPathEffectThenStrokeKey() == invertedEmptyKey);
REPORTER_ASSERT(reporter, geoPEInvertStrokeCase.appliedPathEffectShape().isEmpty());
REPORTER_ASSERT(reporter, geoPEInvertStrokeCase.appliedFullStyleShape().isEmpty());
REPORTER_ASSERT(reporter, geoPEInvertStrokeCase.appliedPathEffectShape().inverseFilled());
REPORTER_ASSERT(reporter, geoPEInvertStrokeCase.appliedFullStyleShape().inverseFilled());
}
void test_path_effect_fails(skiatest::Reporter* reporter, const Geo& geo) {
/**
* This path effect always fails to apply.
*/
class FailurePathEffect : SkPathEffect {
public:
static sk_sp<SkPathEffect> Make() { return sk_sp<SkPathEffect>(new FailurePathEffect); }
Factory getFactory() const override { return nullptr; }
const char* getTypeName() const override { return nullptr; }
protected:
bool onFilterPath(SkPath* dst, const SkPath& src, SkStrokeRec*,
const SkRect* cullR) const override {
return false;
}
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) GrStyledShape 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 GrStyledShape 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));
}
DEF_TEST(GrStyledShape_empty_shape, reporter) {
SkPath emptyPath;
SkPath invertedEmptyPath;
invertedEmptyPath.toggleInverseFillType();
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());
REPORTER_ASSERT(reporter, !fillEmptyCase.baseShape().inverseFilled());
REPORTER_ASSERT(reporter, !fillEmptyCase.appliedPathEffectShape().inverseFilled());
REPORTER_ASSERT(reporter, !fillEmptyCase.appliedFullStyleShape().inverseFilled());
TestCase fillInvertedEmptyCase(reporter, invertedEmptyPath, fill);
REPORTER_ASSERT(reporter, fillInvertedEmptyCase.baseShape().isEmpty());
REPORTER_ASSERT(reporter, fillInvertedEmptyCase.appliedPathEffectShape().isEmpty());
REPORTER_ASSERT(reporter, fillInvertedEmptyCase.appliedFullStyleShape().isEmpty());
REPORTER_ASSERT(reporter, fillInvertedEmptyCase.baseShape().inverseFilled());
REPORTER_ASSERT(reporter, fillInvertedEmptyCase.appliedPathEffectShape().inverseFilled());
REPORTER_ASSERT(reporter, fillInvertedEmptyCase.appliedFullStyleShape().inverseFilled());
Key emptyKey(fillEmptyCase.baseKey());
REPORTER_ASSERT(reporter, emptyKey.count());
Key inverseEmptyKey(fillInvertedEmptyCase.baseKey());
REPORTER_ASSERT(reporter, inverseEmptyKey.count());
TestCase::SelfExpectations expectations;
expectations.fStrokeApplies = false;
expectations.fPEHasEffect = false;
// This will test whether applying style preserves emptiness
fillEmptyCase.testExpectations(reporter, expectations);
fillInvertedEmptyCase.testExpectations(reporter, expectations);
// Stroking an empty path should have no effect
SkPaint stroke;
stroke.setStrokeWidth(2.f);
stroke.setStyle(SkPaint::kStroke_Style);
stroke.setStrokeJoin(SkPaint::kRound_Join);
stroke.setStrokeCap(SkPaint::kRound_Cap);
TestCase strokeEmptyCase(reporter, emptyPath, stroke);
strokeEmptyCase.compare(reporter, fillEmptyCase, TestCase::kAllSame_ComparisonExpecation);
TestCase strokeInvertedEmptyCase(reporter, invertedEmptyPath, stroke);
strokeInvertedEmptyCase.compare(reporter, fillInvertedEmptyCase,
TestCase::kAllSame_ComparisonExpecation);
// Dashing and stroking an empty path should have no effect
SkPaint dashAndStroke;
dashAndStroke.setPathEffect(make_dash());
dashAndStroke.setStrokeWidth(2.f);
dashAndStroke.setStyle(SkPaint::kStroke_Style);
TestCase dashAndStrokeEmptyCase(reporter, emptyPath, dashAndStroke);
dashAndStrokeEmptyCase.compare(reporter, fillEmptyCase,
TestCase::kAllSame_ComparisonExpecation);
TestCase dashAndStrokeInvertexEmptyCase(reporter, invertedEmptyPath, dashAndStroke);
// Dashing ignores inverseness so this is equivalent to the non-inverted empty fill.
dashAndStrokeInvertexEmptyCase.compare(reporter, fillEmptyCase,
TestCase::kAllSame_ComparisonExpecation);
// A shape made from an empty rrect should behave the same as an empty path when filled and
// when stroked. The shape is closed so it does not produce caps when stroked. When dashed there
// is no path to dash along, making it equivalent as well.
SkRRect emptyRRect = SkRRect::MakeEmpty();
REPORTER_ASSERT(reporter, emptyRRect.getType() == SkRRect::kEmpty_Type);
TestCase fillEmptyRRectCase(reporter, emptyRRect, fill);
fillEmptyRRectCase.compare(reporter, fillEmptyCase, TestCase::kAllSame_ComparisonExpecation);
TestCase strokeEmptyRRectCase(reporter, emptyRRect, stroke);
strokeEmptyRRectCase.compare(reporter, strokeEmptyCase,
TestCase::kAllSame_ComparisonExpecation);
TestCase dashAndStrokeEmptyRRectCase(reporter, emptyRRect, dashAndStroke);
dashAndStrokeEmptyRRectCase.compare(reporter, fillEmptyCase,
TestCase::kAllSame_ComparisonExpecation);
static constexpr SkPathDirection kDir = SkPathDirection::kCCW;
static constexpr int kStart = 0;
TestCase fillInvertedEmptyRRectCase(reporter, emptyRRect, kDir, kStart, true, GrStyle(fill));
fillInvertedEmptyRRectCase.compare(reporter, fillInvertedEmptyCase,
TestCase::kAllSame_ComparisonExpecation);
TestCase strokeInvertedEmptyRRectCase(reporter, emptyRRect, kDir, kStart, true,
GrStyle(stroke));
strokeInvertedEmptyRRectCase.compare(reporter, strokeInvertedEmptyCase,
TestCase::kAllSame_ComparisonExpecation);
TestCase dashAndStrokeEmptyInvertedRRectCase(reporter, emptyRRect, kDir, kStart, true,
GrStyle(dashAndStroke));
dashAndStrokeEmptyInvertedRRectCase.compare(reporter, fillEmptyCase,
TestCase::kAllSame_ComparisonExpecation);
// Same for a rect.
SkRect emptyRect = SkRect::MakeEmpty();
TestCase fillEmptyRectCase(reporter, emptyRect, fill);
fillEmptyRectCase.compare(reporter, fillEmptyCase, TestCase::kAllSame_ComparisonExpecation);
TestCase dashAndStrokeEmptyRectCase(reporter, emptyRect, dashAndStroke);
dashAndStrokeEmptyRectCase.compare(reporter, fillEmptyCase,
TestCase::kAllSame_ComparisonExpecation);
TestCase dashAndStrokeEmptyInvertedRectCase(reporter, SkRRect::MakeRect(emptyRect), kDir,
kStart, true, GrStyle(dashAndStroke));
// Dashing ignores inverseness so this is equivalent to the non-inverted empty fill.
dashAndStrokeEmptyInvertedRectCase.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,
SkPathDirection dir,
unsigned start,
Style style,
bool dash) -> int {
return inverted * (2 * 8 * kStyleCnt * 2) +
(int)dir * ( 8 * kStyleCnt * 2) +
start * ( kStyleCnt * 2) +
style * ( 2) +
dash;
};
static const SkPathDirection kSecondDirection = static_cast<SkPathDirection>(1);
const int cnt = index(true, kSecondDirection, 7, static_cast<Style>(kStyleCnt - 1), true) + 1;
SkAutoTArray<GrStyledShape> shapes(cnt);
for (bool inverted : {false, true}) {
for (SkPathDirection dir : {SkPathDirection::kCW, SkPathDirection::kCCW}) {
for (unsigned start = 0; start < 8; ++start) {
for (Style style : {kFill, kStroke, kHairline, kStrokeAndFill}) {
for (bool dash : {false, true}) {
sk_sp<SkPathEffect> pe = dash ? dashEffect : nullptr;
shapes[index(inverted, dir, start, style, dash)] =
GrStyledShape(rrect, dir, start, SkToBool(inverted),
GrStyle(strokeRecs[style], std::move(pe)));
}
}
}
}
}
// Get the keys for some example shape instances that we'll use for comparision against the
// rest.
static constexpr SkPathDirection kExamplesDir = SkPathDirection::kCW;
static constexpr unsigned kExamplesStart = 0;
const GrStyledShape& exampleFillCase = shapes[index(false, kExamplesDir, kExamplesStart, kFill,
false)];
Key exampleFillCaseKey;
make_key(&exampleFillCaseKey, exampleFillCase);
const GrStyledShape& exampleStrokeAndFillCase = shapes[index(false, kExamplesDir,
kExamplesStart, kStrokeAndFill, false)];
Key exampleStrokeAndFillCaseKey;
make_key(&exampleStrokeAndFillCaseKey, exampleStrokeAndFillCase);
const GrStyledShape& exampleInvFillCase = shapes[index(true, kExamplesDir,
kExamplesStart, kFill, false)];
Key exampleInvFillCaseKey;
make_key(&exampleInvFillCaseKey, exampleInvFillCase);
const GrStyledShape& exampleInvStrokeAndFillCase = shapes[index(true, kExamplesDir,
kExamplesStart, kStrokeAndFill,
false)];
Key exampleInvStrokeAndFillCaseKey;
make_key(&exampleInvStrokeAndFillCaseKey, exampleInvStrokeAndFillCase);
const GrStyledShape& exampleStrokeCase = shapes[index(false, kExamplesDir, kExamplesStart,
kStroke, false)];
Key exampleStrokeCaseKey;
make_key(&exampleStrokeCaseKey, exampleStrokeCase);
const GrStyledShape& exampleInvStrokeCase = shapes[index(true, kExamplesDir, kExamplesStart,
kStroke, false)];
Key exampleInvStrokeCaseKey;
make_key(&exampleInvStrokeCaseKey, exampleInvStrokeCase);
const GrStyledShape& exampleHairlineCase = shapes[index(false, kExamplesDir, kExamplesStart,
kHairline, false)];
Key exampleHairlineCaseKey;
make_key(&exampleHairlineCaseKey, exampleHairlineCase);
const GrStyledShape& 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();
SkPathDirection queryDir = SkPathDirection::kCW;
unsigned queryStart = ~0U;
bool queryInverted = true;
REPORTER_ASSERT(r, exampleFillCase.asRRect(&queryRR, &queryDir, &queryStart, &queryInverted));
REPORTER_ASSERT(r, queryRR == rrect);
REPORTER_ASSERT(r, SkPathDirection::kCW == 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, SkPathDirection::kCW == 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, SkPathDirection::kCW == 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, SkPathDirection::kCW == 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, SkPathDirection::kCW == 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, SkPathDirection::kCW == 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, SkPathDirection::kCW == 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, SkPathDirection::kCW == 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 (SkPathDirection dir : {SkPathDirection::kCW, SkPathDirection::kCCW}) {
for (unsigned start = 0; start < 8; ++start) {
for (bool dash : {false, true}) {
const GrStyledShape& fillCase = shapes[index(inverted, dir, start, kFill,
dash)];
Key fillCaseKey;
make_key(&fillCaseKey, fillCase);
const GrStyledShape& 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 GrStyledShape& strokeCase = shapes[index(inverted, dir, start, kStroke,
dash)];
const GrStyledShape& 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 && SkPathDirection::kCW == 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);
}
}
}
}
}
}
DEF_TEST(GrStyledShape_lines, 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(SkPathFillType::kInverseEvenOdd);
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));
SkPath path;
path.toggleInverseFillType();
TestCase fillEmptyInverted(r, path, fill);
TestCase fillABInverted(r, invLineAB, fill);
fillABInverted.compare(r, fillEmptyInverted, TestCase::kAllSame_ComparisonExpecation);
REPORTER_ASSERT(r, !fillABInverted.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
// GrStyledShape 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;
using std::swap;
swap(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);
}
DEF_TEST(GrStyledShape_stroked_lines, r) {
static constexpr SkScalar kIntervals1[] = {1.f, 0.f};
auto dash1 = SkDashPathEffect::Make(kIntervals1, SK_ARRAY_COUNT(kIntervals1), 0.f);
REPORTER_ASSERT(r, dash1);
static constexpr SkScalar kIntervals2[] = {10.f, 0.f, 5.f, 0.f};
auto dash2 = SkDashPathEffect::Make(kIntervals2, SK_ARRAY_COUNT(kIntervals2), 10.f);
REPORTER_ASSERT(r, dash2);
sk_sp<SkPathEffect> pathEffects[] = {nullptr, std::move(dash1), std::move(dash2)};
for (const auto& pe : pathEffects) {
// Paints to try
SkPaint buttCap;
buttCap.setStyle(SkPaint::kStroke_Style);
buttCap.setStrokeWidth(4);
buttCap.setStrokeCap(SkPaint::kButt_Cap);
buttCap.setPathEffect(pe);
SkPaint squareCap = buttCap;
squareCap.setStrokeCap(SkPaint::kSquare_Cap);
squareCap.setPathEffect(pe);
SkPaint roundCap = buttCap;
roundCap.setStrokeCap(SkPaint::kRound_Cap);
roundCap.setPathEffect(pe);
// vertical
SkPath linePath;
linePath.moveTo(4, 4);
linePath.lineTo(4, 5);
SkPaint fill;
make_TestCase(r, linePath, buttCap)->compare(
r, TestCase(r, SkRect::MakeLTRB(2, 4, 6, 5), fill),
TestCase::kAllSame_ComparisonExpecation);
make_TestCase(r, linePath, squareCap)->compare(
r, TestCase(r, SkRect::MakeLTRB(2, 2, 6, 7), fill),
TestCase::kAllSame_ComparisonExpecation);
make_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);
make_TestCase(r, linePath, buttCap)->compare(
r, TestCase(r, SkRect::MakeLTRB(4, 2, 5, 6), fill),
TestCase::kAllSame_ComparisonExpecation);
make_TestCase(r, linePath, squareCap)->compare(
r, TestCase(r, SkRect::MakeLTRB(2, 2, 7, 6), fill),
TestCase::kAllSame_ComparisonExpecation);
make_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);
make_TestCase(r, linePath, buttCap)->compare(
r, TestCase(r, SkRect::MakeEmpty(), fill),
TestCase::kAllSame_ComparisonExpecation);
make_TestCase(r, linePath, squareCap)->compare(
r, TestCase(r, SkRect::MakeLTRB(2, 2, 6, 6), fill),
TestCase::kAllSame_ComparisonExpecation);
make_TestCase(r, linePath, roundCap)->compare(
r, TestCase(r, SkRRect::MakeRectXY(SkRect::MakeLTRB(2, 2, 6, 6), 2, 2), fill),
TestCase::kAllSame_ComparisonExpecation);
}
}
DEF_TEST(GrStyledShape_short_path_keys, 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, !GrStyledShape(volatileA, paint).hasUnstyledKey());
REPORTER_ASSERT(r, !GrStyledShape(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(GrStyledShape, 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));
}
// Arcs
geos.emplace_back(new ArcGeo(SkRect::MakeWH(200, 100), 12.f, 110.f, false));
geos.emplace_back(new ArcGeo(SkRect::MakeWH(200, 100), 12.f, 110.f, true));
{
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));
}
DEF_TEST(GrStyledShape_arcs, reporter) {
SkStrokeRec roundStroke(SkStrokeRec::kFill_InitStyle);
roundStroke.setStrokeStyle(2.f);
roundStroke.setStrokeParams(SkPaint::kRound_Cap, SkPaint::kRound_Join, 1.f);
SkStrokeRec squareStroke(roundStroke);
squareStroke.setStrokeParams(SkPaint::kSquare_Cap, SkPaint::kRound_Join, 1.f);
SkStrokeRec roundStrokeAndFill(roundStroke);
roundStrokeAndFill.setStrokeStyle(2.f, true);
static constexpr SkScalar kIntervals[] = {1, 2};
auto dash = SkDashPathEffect::Make(kIntervals, SK_ARRAY_COUNT(kIntervals), 1.5f);
SkTArray<GrStyle> styles;
styles.push_back(GrStyle::SimpleFill());
styles.push_back(GrStyle::SimpleHairline());
styles.push_back(GrStyle(roundStroke, nullptr));
styles.push_back(GrStyle(squareStroke, nullptr));
styles.push_back(GrStyle(roundStrokeAndFill, nullptr));
styles.push_back(GrStyle(roundStroke, dash));
for (const auto& style : styles) {
// An empty rect never draws anything according to SkCanvas::drawArc() docs.
TestCase emptyArc(GrStyledShape::MakeArc(SkRect::MakeEmpty(), 0, 90.f, false, style),
reporter);
TestCase emptyPath(reporter, SkPath(), style);
emptyArc.compare(reporter, emptyPath, TestCase::kAllSame_ComparisonExpecation);
static constexpr SkRect kOval1{0, 0, 50, 50};
static constexpr SkRect kOval2{50, 0, 100, 50};
// Test that swapping starting and ending angle doesn't change the shape unless the arc
// has a path effect. Also test that different ovals produce different shapes.
TestCase arc1CW(GrStyledShape::MakeArc(kOval1, 0, 90.f, false, style), reporter);
TestCase arc1CCW(GrStyledShape::MakeArc(kOval1, 90.f, -90.f, false, style), reporter);
TestCase arc1CWWithCenter(GrStyledShape::MakeArc(kOval1, 0, 90.f, true, style), reporter);
TestCase arc1CCWWithCenter(GrStyledShape::MakeArc(kOval1, 90.f, -90.f, true, style),
reporter);
TestCase arc2CW(GrStyledShape::MakeArc(kOval2, 0, 90.f, false, style), reporter);
TestCase arc2CWWithCenter(GrStyledShape::MakeArc(kOval2, 0, 90.f, true, style), reporter);
auto reversedExepectations = style.hasPathEffect()
? TestCase::kAllDifferent_ComparisonExpecation
: TestCase::kAllSame_ComparisonExpecation;
arc1CW.compare(reporter, arc1CCW, reversedExepectations);
arc1CWWithCenter.compare(reporter, arc1CCWWithCenter, reversedExepectations);
arc1CW.compare(reporter, arc2CW, TestCase::kAllDifferent_ComparisonExpecation);
arc1CW.compare(reporter, arc1CWWithCenter, TestCase::kAllDifferent_ComparisonExpecation);
arc1CWWithCenter.compare(reporter, arc2CWWithCenter,
TestCase::kAllDifferent_ComparisonExpecation);
// Test that two arcs that start at the same angle but specified differently are equivalent.
TestCase arc3A(GrStyledShape::MakeArc(kOval1, 224.f, 73.f, false, style), reporter);
TestCase arc3B(GrStyledShape::MakeArc(kOval1, 224.f - 360.f, 73.f, false, style), reporter);
arc3A.compare(reporter, arc3B, TestCase::kAllDifferent_ComparisonExpecation);
// Test that an arc that traverses the entire oval (and then some) is equivalent to the
// oval itself unless there is a path effect.
TestCase ovalArc(GrStyledShape::MakeArc(kOval1, 150.f, -790.f, false, style), reporter);
TestCase oval(GrStyledShape(SkRRect::MakeOval(kOval1)), reporter);
auto ovalExpectations = style.hasPathEffect() ? TestCase::kAllDifferent_ComparisonExpecation
: TestCase::kAllSame_ComparisonExpecation;
if (style.strokeRec().getWidth() >= 0 && style.strokeRec().getCap() != SkPaint::kButt_Cap) {
ovalExpectations = TestCase::kAllDifferent_ComparisonExpecation;
}
ovalArc.compare(reporter, oval, ovalExpectations);
// If the the arc starts/ends at the center then it is then equivalent to the oval only for
// simple fills.
TestCase ovalArcWithCenter(GrStyledShape::MakeArc(kOval1, 304.f, 1225.f, true, style),
reporter);
ovalExpectations = style.isSimpleFill() ? TestCase::kAllSame_ComparisonExpecation
: TestCase::kAllDifferent_ComparisonExpecation;
ovalArcWithCenter.compare(reporter, oval, ovalExpectations);
}
}