Add methods for converting GrShape to filled style
Bug: skia:
Change-Id: I6726428d1358909972adec8d63686b637ef5bb5e
Reviewed-on: https://skia-review.googlesource.com/40222
Reviewed-by: Robert Phillips <robertphillips@google.com>
Commit-Queue: Brian Salomon <bsalomon@google.com>
diff --git a/src/gpu/GrShape.cpp b/src/gpu/GrShape.cpp
index 2212478..4691ede 100644
--- a/src/gpu/GrShape.cpp
+++ b/src/gpu/GrShape.cpp
@@ -31,6 +31,82 @@
return *this;
}
+static bool flip_inversion(bool originalIsInverted, GrShape::FillInversion inversion) {
+ switch (inversion) {
+ case GrShape::FillInversion::kPreserve:
+ return false;
+ case GrShape::FillInversion::kFlip:
+ return true;
+ case GrShape::FillInversion::kForceInverted:
+ return !originalIsInverted;
+ case GrShape::FillInversion::kForceNoninverted:
+ return originalIsInverted;
+ }
+ return false;
+}
+
+static bool is_inverted(bool originalIsInverted, GrShape::FillInversion inversion) {
+ switch (inversion) {
+ case GrShape::FillInversion::kPreserve:
+ return originalIsInverted;
+ case GrShape::FillInversion::kFlip:
+ return !originalIsInverted;
+ case GrShape::FillInversion::kForceInverted:
+ return true;
+ case GrShape::FillInversion::kForceNoninverted:
+ return false;
+ }
+ return false;
+}
+
+GrShape GrShape::MakeFilled(const GrShape& original, FillInversion inversion) {
+ if (original.style().isSimpleFill() && !flip_inversion(original.inverseFilled(), inversion)) {
+ // By returning the original rather than falling through we can preserve any inherited style
+ // key. Otherwise, we wipe it out below since the style change invalidates it.
+ return original;
+ }
+ GrShape result;
+ switch (original.fType) {
+ case Type::kRRect:
+ result.fType = original.fType;
+ result.fRRectData.fRRect = original.fRRectData.fRRect;
+ result.fRRectData.fDir = kDefaultRRectDir;
+ result.fRRectData.fStart = kDefaultRRectStart;
+ result.fRRectData.fInverted = is_inverted(original.fRRectData.fInverted, inversion);
+ break;
+ case Type::kLine:
+ // Lines don't fill.
+ if (is_inverted(original.fLineData.fInverted, inversion)) {
+ result.fType = Type::kInvertedEmpty;
+ } else {
+ result.fType = Type::kEmpty;
+ }
+ break;
+ case Type::kEmpty:
+ result.fType = is_inverted(false, inversion) ? Type::kInvertedEmpty : Type::kEmpty;
+ break;
+ case Type::kInvertedEmpty:
+ result.fType = is_inverted(true, inversion) ? Type::kInvertedEmpty : Type::kEmpty;
+ break;
+ case Type::kPath:
+ result.initType(Type::kPath, &original.fPathData.fPath);
+ result.fPathData.fGenID = original.fPathData.fGenID;
+ if (flip_inversion(original.fPathData.fPath.isInverseFillType(), inversion)) {
+ result.fPathData.fPath.toggleInverseFillType();
+ }
+ if (!original.style().isSimpleFill()) {
+ // Going from a non-filled style to fill may allow additional simplifications (e.g.
+ // closing an open rect that wasn't closed in the original shape because it had
+ // stroke style).
+ result.attemptToSimplifyPath();
+ }
+ break;
+ }
+ // We don't copy the inherited key since it can contain path effect information that we just
+ // stripped.
+ return result;
+}
+
SkRect GrShape::bounds() const {
// Bounds where left == bottom or top == right can indicate a line or point shape. We return
// inverted bounds for a truly empty shape.
@@ -206,7 +282,7 @@
// We want ApplyFullStyle(ApplyPathEffect(shape)) to have the same key as
// ApplyFullStyle(shape).
// The full key is structured as (geo,path_effect,stroke).
- // If we do ApplyPathEffect we get get,path_effect as the inherited key. If we then
+ // If we do ApplyPathEffect we get geo,path_effect as the inherited key. If we then
// do ApplyFullStyle we'll memcpy geo,path_effect into the new inherited key
// and then append the style key (which should now be stroke only) at the end.
int parentCnt = parent.fInheritedKey.count();
diff --git a/src/gpu/GrShape.h b/src/gpu/GrShape.h
index 5226c23..2b48eca 100644
--- a/src/gpu/GrShape.h
+++ b/src/gpu/GrShape.h
@@ -30,7 +30,7 @@
*
* Currently this can only be constructed from a path, rect, or rrect though it can become a path
* applying style to the geometry. The idea is to expand this to cover most or all of the geometries
- * that have SkCanvas::draw APIs.
+ * that have fast paths in the GPU backend.
*/
class GrShape {
public:
@@ -121,6 +121,25 @@
~GrShape() { this->changeType(Type::kEmpty); }
+ /**
+ * Informs MakeFilled on how to modify that shape's fill rule when making a simple filled
+ * version of the shape.
+ */
+ enum class FillInversion {
+ kPreserve,
+ kFlip,
+ kForceNoninverted,
+ kForceInverted
+ };
+ /**
+ * Makes a filled shape from the pre-styled original shape and optionally modifies whether
+ * the fill is inverted or not. It's important to note that the original shape's geometry
+ * may already have been modified if doing so was neutral with respect to its style
+ * (e.g. filled paths are always closed when stored in a shape and dashed paths are always
+ * made non-inverted since dashing ignores inverseness).
+ */
+ static GrShape MakeFilled(const GrShape& original, FillInversion = FillInversion::kPreserve);
+
const GrStyle& style() const { return fStyle; }
/**
@@ -347,6 +366,7 @@
void writeUnstyledKey(uint32_t* key) const;
private:
+
enum class Type {
kEmpty,
kInvertedEmpty,
diff --git a/tests/GrShapeTest.cpp b/tests/GrShapeTest.cpp
index 719e5cd..087ac52 100644
--- a/tests/GrShapeTest.cpp
+++ b/tests/GrShapeTest.cpp
@@ -78,6 +78,179 @@
return true;
}
+static bool can_interchange_winding_and_even_odd_fill(const GrShape& shape) {
+ SkPath path;
+ shape.asPath(&path);
+ if (shape.style().hasNonDashPathEffect()) {
+ return false;
+ }
+ const SkStrokeRec::Style strokeRecStyle = shape.style().strokeRec().getStyle();
+ return strokeRecStyle == SkStrokeRec::kStroke_Style ||
+ strokeRecStyle == SkStrokeRec::kHairline_Style ||
+ (shape.style().isSimpleFill() && path.isConvex());
+}
+
+static void check_equivalence(skiatest::Reporter* r, const GrShape& a, const GrShape& b,
+ const Key& keyA, const Key& keyB) {
+ // GrShape only respects the input winding direction and start point for rrect shapes
+ // when there is a path effect. Thus, if there are two GrShapes representing the same rrect
+ // but one has a path effect in its style and the other doesn't then asPath() and the unstyled
+ // key will differ. GrShape will have canonicalized the direction and start point for the shape
+ // without the path effect. If *both* have path effects then they should have both preserved
+ // the direction and starting point.
+
+ // The asRRect() output params are all initialized just to silence compiler warnings about
+ // uninitialized variables.
+ SkRRect rrectA = SkRRect::MakeEmpty(), rrectB = SkRRect::MakeEmpty();
+ SkPath::Direction dirA = SkPath::kCW_Direction, dirB = SkPath::kCW_Direction;
+ unsigned startA = ~0U, startB = ~0U;
+ bool invertedA = true, invertedB = true;
+
+ bool aIsRRect = a.asRRect(&rrectA, &dirA, &startA, &invertedA);
+ bool bIsRRect = b.asRRect(&rrectB, &dirB, &startB, &invertedB);
+ bool aHasPE = a.style().hasPathEffect();
+ bool bHasPE = b.style().hasPathEffect();
+ bool allowSameRRectButDiffStartAndDir = (aIsRRect && bIsRRect) && (aHasPE != bHasPE);
+ // GrShape will close paths with simple fill style.
+ bool allowedClosednessDiff = (a.style().isSimpleFill() != b.style().isSimpleFill());
+ SkPath pathA, pathB;
+ a.asPath(&pathA);
+ b.asPath(&pathB);
+
+ // Having a dash path effect can allow 'a' but not 'b' to turn a inverse fill type into a
+ // non-inverse fill type (or vice versa).
+ bool ignoreInversenessDifference = false;
+ if (pathA.isInverseFillType() != pathB.isInverseFillType()) {
+ const GrShape* s1 = pathA.isInverseFillType() ? &a : &b;
+ const GrShape* s2 = pathA.isInverseFillType() ? &b : &a;
+ bool canDropInverse1 = s1->style().isDashed();
+ bool canDropInverse2 = s2->style().isDashed();
+ ignoreInversenessDifference = (canDropInverse1 != canDropInverse2);
+ }
+ bool ignoreWindingVsEvenOdd = false;
+ if (SkPath::ConvertToNonInverseFillType(pathA.getFillType()) !=
+ SkPath::ConvertToNonInverseFillType(pathB.getFillType())) {
+ bool aCanChange = can_interchange_winding_and_even_odd_fill(a);
+ bool bCanChange = can_interchange_winding_and_even_odd_fill(b);
+ if (aCanChange != bCanChange) {
+ ignoreWindingVsEvenOdd = true;
+ }
+ }
+ if (allowSameRRectButDiffStartAndDir) {
+ REPORTER_ASSERT(r, rrectA == rrectB);
+ REPORTER_ASSERT(r, paths_fill_same(pathA, pathB));
+ REPORTER_ASSERT(r, ignoreInversenessDifference || invertedA == invertedB);
+ } else {
+ SkPath pA = pathA;
+ SkPath pB = pathB;
+ REPORTER_ASSERT(r, a.inverseFilled() == pA.isInverseFillType());
+ REPORTER_ASSERT(r, b.inverseFilled() == pB.isInverseFillType());
+ if (ignoreInversenessDifference) {
+ pA.setFillType(SkPath::ConvertToNonInverseFillType(pathA.getFillType()));
+ pB.setFillType(SkPath::ConvertToNonInverseFillType(pathB.getFillType()));
+ }
+ if (ignoreWindingVsEvenOdd) {
+ pA.setFillType(pA.isInverseFillType() ? SkPath::kInverseEvenOdd_FillType
+ : SkPath::kEvenOdd_FillType);
+ pB.setFillType(pB.isInverseFillType() ? SkPath::kInverseEvenOdd_FillType
+ : SkPath::kEvenOdd_FillType);
+ }
+ if (!ignoreInversenessDifference && !ignoreWindingVsEvenOdd) {
+ REPORTER_ASSERT(r, keyA == keyB);
+ } else {
+ REPORTER_ASSERT(r, keyA != keyB);
+ }
+ if (allowedClosednessDiff) {
+ // GrShape will close paths with simple fill style. Make the non-filled path closed
+ // so that the comparision will succeed. Make sure both are closed before comparing.
+ pA.close();
+ pB.close();
+ }
+ REPORTER_ASSERT(r, pA == pB);
+ REPORTER_ASSERT(r, aIsRRect == bIsRRect);
+ if (aIsRRect) {
+ REPORTER_ASSERT(r, rrectA == rrectB);
+ REPORTER_ASSERT(r, dirA == dirB);
+ REPORTER_ASSERT(r, startA == startB);
+ REPORTER_ASSERT(r, ignoreInversenessDifference || invertedA == invertedB);
+ }
+ }
+ REPORTER_ASSERT(r, a.isEmpty() == b.isEmpty());
+ REPORTER_ASSERT(r, allowedClosednessDiff || a.knownToBeClosed() == b.knownToBeClosed());
+ // closedness can affect convexity.
+ REPORTER_ASSERT(r, allowedClosednessDiff || a.knownToBeConvex() == b.knownToBeConvex());
+ if (a.knownToBeConvex()) {
+ REPORTER_ASSERT(r, pathA.isConvex());
+ }
+ if (b.knownToBeConvex()) {
+ REPORTER_ASSERT(r, pathB.isConvex());
+ }
+ REPORTER_ASSERT(r, a.bounds() == b.bounds());
+ REPORTER_ASSERT(r, a.segmentMask() == b.segmentMask());
+ // Init these to suppress warnings.
+ SkPoint pts[4] {{0, 0,}, {0, 0}, {0, 0}, {0, 0}} ;
+ bool invertedLine[2] {true, true};
+ REPORTER_ASSERT(r, a.asLine(pts, &invertedLine[0]) == b.asLine(pts + 2, &invertedLine[1]));
+ // mayBeInverseFilledAfterStyling() is allowed to differ if one has a arbitrary PE and the other
+ // doesn't (since the PE can set any fill type on its output path).
+ // Moreover, dash style explicitly ignores inverseness. So if one is dashed but not the other
+ // then they may disagree about inverseness.
+ if (a.style().hasNonDashPathEffect() == b.style().hasNonDashPathEffect() &&
+ a.style().isDashed() == b.style().isDashed()) {
+ REPORTER_ASSERT(r, a.mayBeInverseFilledAfterStyling() ==
+ b.mayBeInverseFilledAfterStyling());
+ }
+ if (a.asLine(nullptr, nullptr)) {
+ REPORTER_ASSERT(r, pts[2] == pts[0] && pts[3] == pts[1]);
+ REPORTER_ASSERT(r, ignoreInversenessDifference || invertedLine[0] == invertedLine[1]);
+ REPORTER_ASSERT(r, invertedLine[0] == a.inverseFilled());
+ REPORTER_ASSERT(r, invertedLine[1] == b.inverseFilled());
+ }
+ REPORTER_ASSERT(r, ignoreInversenessDifference || a.inverseFilled() == b.inverseFilled());
+}
+
+void test_inversions(skiatest::Reporter* r, const GrShape& shape, const Key& shapeKey) {
+ GrShape preserve = GrShape::MakeFilled(shape, GrShape::FillInversion::kPreserve);
+ Key preserveKey;
+ make_key(&preserveKey, preserve);
+
+ GrShape flip = GrShape::MakeFilled(shape, GrShape::FillInversion::kFlip);
+ Key flipKey;
+ make_key(&flipKey, flip);
+
+ GrShape inverted = GrShape::MakeFilled(shape, GrShape::FillInversion::kForceInverted);
+ Key invertedKey;
+ make_key(&invertedKey, inverted);
+
+ GrShape noninverted = GrShape::MakeFilled(shape, GrShape::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);
+ }
+
+ GrShape doubleFlip = GrShape::MakeFilled(flip, GrShape::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 GrShape from another representation. It also answers some
@@ -400,6 +573,9 @@
REPORTER_ASSERT(r, fAppliedFull.style().isSimpleHairline());
}
}
+ test_inversions(r, fBase, fBaseKey);
+ test_inversions(r, fAppliedPE, fAppliedPEKey);
+ test_inversions(r, fAppliedFull, fAppliedFullKey);
}
GrShape fBase;
@@ -439,137 +615,6 @@
}
}
-static bool can_interchange_winding_and_even_odd_fill(const GrShape& shape) {
- SkPath path;
- shape.asPath(&path);
- if (shape.style().hasNonDashPathEffect()) {
- return false;
- }
- const SkStrokeRec::Style strokeRecStyle = shape.style().strokeRec().getStyle();
- return strokeRecStyle == SkStrokeRec::kStroke_Style ||
- strokeRecStyle == SkStrokeRec::kHairline_Style ||
- (shape.style().isSimpleFill() && path.isConvex());
-}
-
-static void check_equivalence(skiatest::Reporter* r, const GrShape& a, const GrShape& b,
- const Key& keyA, const Key& keyB) {
- // GrShape only respects the input winding direction and start point for rrect shapes
- // when there is a path effect. Thus, if there are two GrShapes representing the same rrect
- // but one has a path effect in its style and the other doesn't then asPath() and the unstyled
- // key will differ. GrShape will have canonicalized the direction and start point for the shape
- // without the path effect. If *both* have path effects then they should have both preserved
- // the direction and starting point.
-
- // The asRRect() output params are all initialized just to silence compiler warnings about
- // uninitialized variables.
- SkRRect rrectA = SkRRect::MakeEmpty(), rrectB = SkRRect::MakeEmpty();
- SkPath::Direction dirA = SkPath::kCW_Direction, dirB = SkPath::kCW_Direction;
- unsigned startA = ~0U, startB = ~0U;
- bool invertedA = true, invertedB = true;
-
- bool aIsRRect = a.asRRect(&rrectA, &dirA, &startA, &invertedA);
- bool bIsRRect = b.asRRect(&rrectB, &dirB, &startB, &invertedB);
- bool aHasPE = a.style().hasPathEffect();
- bool bHasPE = b.style().hasPathEffect();
- bool allowSameRRectButDiffStartAndDir = (aIsRRect && bIsRRect) && (aHasPE != bHasPE);
- // GrShape will close paths with simple fill style.
- bool allowedClosednessDiff = (a.style().isSimpleFill() != b.style().isSimpleFill());
- SkPath pathA, pathB;
- a.asPath(&pathA);
- b.asPath(&pathB);
-
- // Having a dash path effect can allow 'a' but not 'b' to turn a inverse fill type into a
- // non-inverse fill type (or vice versa).
- bool ignoreInversenessDifference = false;
- if (pathA.isInverseFillType() != pathB.isInverseFillType()) {
- const GrShape* s1 = pathA.isInverseFillType() ? &a : &b;
- const GrShape* s2 = pathA.isInverseFillType() ? &b : &a;
- bool canDropInverse1 = s1->style().isDashed();
- bool canDropInverse2 = s2->style().isDashed();
- ignoreInversenessDifference = (canDropInverse1 != canDropInverse2);
- }
- bool ignoreWindingVsEvenOdd = false;
- if (SkPath::ConvertToNonInverseFillType(pathA.getFillType()) !=
- SkPath::ConvertToNonInverseFillType(pathB.getFillType())) {
- bool aCanChange = can_interchange_winding_and_even_odd_fill(a);
- bool bCanChange = can_interchange_winding_and_even_odd_fill(b);
- if (aCanChange != bCanChange) {
- ignoreWindingVsEvenOdd = true;
- }
- }
- if (allowSameRRectButDiffStartAndDir) {
- REPORTER_ASSERT(r, rrectA == rrectB);
- REPORTER_ASSERT(r, paths_fill_same(pathA, pathB));
- REPORTER_ASSERT(r, ignoreInversenessDifference || invertedA == invertedB);
- } else {
- SkPath pA = pathA;
- SkPath pB = pathB;
- REPORTER_ASSERT(r, a.inverseFilled() == pA.isInverseFillType());
- REPORTER_ASSERT(r, b.inverseFilled() == pB.isInverseFillType());
- if (ignoreInversenessDifference) {
- pA.setFillType(SkPath::ConvertToNonInverseFillType(pathA.getFillType()));
- pB.setFillType(SkPath::ConvertToNonInverseFillType(pathB.getFillType()));
- }
- if (ignoreWindingVsEvenOdd) {
- pA.setFillType(pA.isInverseFillType() ? SkPath::kInverseEvenOdd_FillType
- : SkPath::kEvenOdd_FillType);
- pB.setFillType(pB.isInverseFillType() ? SkPath::kInverseEvenOdd_FillType
- : SkPath::kEvenOdd_FillType);
- }
- if (!ignoreInversenessDifference && !ignoreWindingVsEvenOdd) {
- REPORTER_ASSERT(r, keyA == keyB);
- } else {
- REPORTER_ASSERT(r, keyA != keyB);
- }
- if (allowedClosednessDiff) {
- // GrShape will close paths with simple fill style. Make the non-filled path closed
- // so that the comparision will succeed. Make sure both are closed before comparing.
- pA.close();
- pB.close();
- }
- REPORTER_ASSERT(r, pA == pB);
- REPORTER_ASSERT(r, aIsRRect == bIsRRect);
- if (aIsRRect) {
- REPORTER_ASSERT(r, rrectA == rrectB);
- REPORTER_ASSERT(r, dirA == dirB);
- REPORTER_ASSERT(r, startA == startB);
- REPORTER_ASSERT(r, ignoreInversenessDifference || invertedA == invertedB);
- }
- }
- REPORTER_ASSERT(r, a.isEmpty() == b.isEmpty());
- REPORTER_ASSERT(r, allowedClosednessDiff || a.knownToBeClosed() == b.knownToBeClosed());
- // closedness can affect convexity.
- REPORTER_ASSERT(r, allowedClosednessDiff || a.knownToBeConvex() == b.knownToBeConvex());
- if (a.knownToBeConvex()) {
- REPORTER_ASSERT(r, pathA.isConvex());
- }
- if (b.knownToBeConvex()) {
- REPORTER_ASSERT(r, pathB.isConvex());
- }
- REPORTER_ASSERT(r, a.bounds() == b.bounds());
- REPORTER_ASSERT(r, a.segmentMask() == b.segmentMask());
- // Init these to suppress warnings.
- SkPoint pts[4] {{0, 0,}, {0, 0}, {0, 0}, {0, 0}} ;
- bool invertedLine[2] {true, true};
- REPORTER_ASSERT(r, a.asLine(pts, &invertedLine[0]) == b.asLine(pts + 2, &invertedLine[1]));
- // mayBeInverseFilledAfterStyling() is allowed to differ if one has a arbitrary PE and the other
- // doesn't (since the PE can set any fill type on its output path).
- // Moreover, dash style explicitly ignores inverseness. So if one is dashed but not the other
- // then they may disagree about inverseness.
- if (a.style().hasNonDashPathEffect() == b.style().hasNonDashPathEffect() &&
- a.style().isDashed() == b.style().isDashed()) {
- REPORTER_ASSERT(r, a.mayBeInverseFilledAfterStyling() ==
- b.mayBeInverseFilledAfterStyling());
- }
- if (a.asLine(nullptr, nullptr)) {
- REPORTER_ASSERT(r, pts[2] == pts[0] && pts[3] == pts[1]);
- REPORTER_ASSERT(r, ignoreInversenessDifference || invertedLine[0] == invertedLine[1]);
- REPORTER_ASSERT(r, invertedLine[0] == a.inverseFilled());
- REPORTER_ASSERT(r, invertedLine[1] == b.inverseFilled());
- }
- REPORTER_ASSERT(r, ignoreInversenessDifference || a.inverseFilled() == b.inverseFilled());
-}
-
void TestCase::compare(skiatest::Reporter* r, const TestCase& that,
ComparisonExpecation expectation) const {
SkPath a, b;