Use pathbuilder to keep paths' immutable (their geometry)
- add == helper to pathbuilder
- allow path.reset(), is it doesn't violate immutable path geometry.
- (safety) disallow passing a temporary path to Iterator
Notes on operator==
- mimics the checks done in today's SkPath==
- like SkPath's, does not look at bounds, convexity, IsA
- if those might be useful for quickrejects, suggest a separate CL
Change-Id: I6a30464d21006808501b2bd1ddf47d5d4eb00de7
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1067736
Reviewed-by: Daniel Dilan <danieldilan@google.com>
Reviewed-by: Florin Malita <fmalita@google.com>
Commit-Queue: Mike Reed <mike@reedtribe.org>
diff --git a/include/core/SkPath.h b/include/core/SkPath.h
index a39c1cc..0797154 100644
--- a/include/core/SkPath.h
+++ b/include/core/SkPath.h
@@ -740,16 +740,6 @@
fFillType ^= 2;
}
-#ifdef SK_HIDE_PATH_EDIT_METHODS
-private:
-#endif
- /** Returns a copy of this path in the current state, and resets the path to empty. */
- SkPath detach() {
- SkPath result = *this;
- this->reset();
- return result;
- }
-
/** Sets SkPath to its initial state.
Removes verb array, SkPoint array, and weights, and sets FillType to kWinding.
Internal storage associated with SkPath is released.
@@ -760,6 +750,16 @@
*/
SkPath& reset();
+#ifdef SK_HIDE_PATH_EDIT_METHODS
+private:
+#endif
+ /** Returns a copy of this path in the current state, and resets the path to empty. */
+ SkPath detach() {
+ SkPath result = *this;
+ this->reset();
+ return result;
+ }
+
/** Sets SkPath to its initial state, preserving internal storage.
Removes verb array, SkPoint array, and weights, and sets FillType to kWinding.
Internal storage associated with SkPath is retained.
diff --git a/include/core/SkPathBuilder.h b/include/core/SkPathBuilder.h
index bfd302d..46eeff2 100644
--- a/include/core/SkPathBuilder.h
+++ b/include/core/SkPathBuilder.h
@@ -63,9 +63,11 @@
@return SkPathBuilder
*/
SkPathBuilder& operator=(const SkPath&);
-
SkPathBuilder& operator=(const SkPathBuilder&) = default;
+ bool operator==(const SkPathBuilder&) const;
+ bool operator!=(const SkPathBuilder& o) const { return !(*this == o); }
+
/** Returns SkPathFillType, the rule used to fill SkPath.
@return current SkPathFillType setting
diff --git a/src/core/SkPathBuilder.cpp b/src/core/SkPathBuilder.cpp
index 760570b..53be20f 100644
--- a/src/core/SkPathBuilder.cpp
+++ b/src/core/SkPathBuilder.cpp
@@ -105,6 +105,19 @@
return *this;
}
+bool SkPathBuilder::operator==(const SkPathBuilder& o) const {
+ // quick-accept
+ if (this == &o) {
+ return true;
+ }
+ // quick-reject
+ if (fSegmentMask != o.fSegmentMask || fFillType != o.fFillType) {
+ return false;
+ }
+ // deep compare
+ return fVerbs == o.fVerbs && fPts == o.fPts && fConicWeights == o.fConicWeights;
+}
+
void SkPathBuilder::incReserve(int extraPtCount, int extraVbCount, int extraCnCount) {
fPts.reserve_exact(Sk32_sat_add(fPts.size(), extraPtCount));
fVerbs.reserve_exact(Sk32_sat_add(fVerbs.size(), extraVbCount));
diff --git a/src/core/SkPathPriv.h b/src/core/SkPathPriv.h
index 0dcd796..771902d 100644
--- a/src/core/SkPathPriv.h
+++ b/src/core/SkPathPriv.h
@@ -170,6 +170,7 @@
*/
struct Iterate {
public:
+ Iterate(SkPath&&) = delete;
Iterate(const SkPath& path)
: Iterate(path.fPathRef->verbsBegin(),
// Don't allow iteration through non-finite points.
diff --git a/tests/PathBuilderTest.cpp b/tests/PathBuilderTest.cpp
index 84c2350..d2bdf5a 100644
--- a/tests/PathBuilderTest.cpp
+++ b/tests/PathBuilderTest.cpp
@@ -1010,3 +1010,61 @@
// PathIter, for compat, is snuffing out trailing moves
check_done_and_reset(reporter, &p, &iter);
}
+
+const SkPathFillType gFillTypes[] = {
+ SkPathFillType::kWinding,
+ SkPathFillType::kEvenOdd,
+ SkPathFillType::kInverseWinding,
+ SkPathFillType::kInverseEvenOdd,
+};
+
+DEF_TEST(SkPathBuilder_equality, reporter) {
+ auto check_filltype_eq = [reporter](const SkPathBuilder& a) {
+ SkPathBuilder copy = a;
+ REPORTER_ASSERT(reporter, a == copy);
+
+ for (auto ft : gFillTypes) {
+ if (ft != a.fillType()) {
+ copy.setFillType(ft);
+ REPORTER_ASSERT(reporter, a != copy);
+ }
+ }
+ };
+
+
+ SkPathBuilder a, b;
+
+ REPORTER_ASSERT(reporter, a == b);
+ check_filltype_eq(a);
+
+ a.moveTo(0, 0);
+ REPORTER_ASSERT(reporter, a != b);
+ b.moveTo(0, 0);
+ REPORTER_ASSERT(reporter, a == b);
+ check_filltype_eq(a);
+
+ b.close();
+ REPORTER_ASSERT(reporter, a != b);
+ a.close();
+ REPORTER_ASSERT(reporter, a == b);
+ check_filltype_eq(a);
+
+ auto set_segments = [](SkPathBuilder& bu) {
+ bu.reset()
+ .moveTo(1, 2)
+ .lineTo(3, 4)
+ .quadTo(5, 6, 7, 8)
+ .conicTo(9, 10, 11, 12, 0.5f)
+ .cubicTo(13, 14, 15, 16, 17, 18)
+ .close();
+ };
+ set_segments(a);
+ set_segments(b);
+ REPORTER_ASSERT(reporter, a == b);
+ check_filltype_eq(a);
+
+ // mutate point value, but not verb sequence
+ a.setLastPt(-1, -2);
+ REPORTER_ASSERT(reporter, a != b);
+ check_filltype_eq(a);
+}
diff --git a/tests/PathTest.cpp b/tests/PathTest.cpp
index b53d58f..30dbd09 100644
--- a/tests/PathTest.cpp
+++ b/tests/PathTest.cpp
@@ -2367,65 +2367,81 @@
const SkRect emptyRect = SkRect::MakeEmpty();
for (int start = 0; start < 4; ++start) {
for (auto dir : {SkPathDirection::kCCW, SkPathDirection::kCW}) {
- SkPath path;
- path.addRect(testRect, dir, start);
- check_simple_rect(reporter, path, true, testRect, dir, start);
- path.close();
- check_simple_rect(reporter, path, true, testRect, dir, start);
- SkPath path2 = path;
- path2.lineTo(10, 10);
+ SkPathBuilder builder;
+ builder.addRect(testRect, dir, start);
+ check_simple_rect(reporter, builder.snapshot(), true, testRect, dir, start);
+ builder.close();
+ check_simple_rect(reporter, builder.snapshot(), true, testRect, dir, start);
+
+ SkPathBuilder builder2 = builder;
+ builder2.lineTo(10, 10);
+ SkPath path2 = builder2.detach();
REPORTER_ASSERT(reporter, !SkPathPriv::IsSimpleRect(path2, false));
REPORTER_ASSERT(reporter, !SkPathPriv::IsSimpleRect(path2, true));
- path2 = path;
- path2.moveTo(10, 10);
+
+ builder2 = builder;
+ builder2.moveTo(10, 10);
+ path2 = builder2.detach();
REPORTER_ASSERT(reporter, !SkPathPriv::IsSimpleRect(path2, false));
REPORTER_ASSERT(reporter, !SkPathPriv::IsSimpleRect(path2, true));
- path2 = path;
- path2.addRect(testRect, dir, start);
+
+ builder2 = builder;
+ builder2.addRect(testRect, dir, start);
+ path2 = builder2.detach();
REPORTER_ASSERT(reporter, !SkPathPriv::IsSimpleRect(path2, false));
REPORTER_ASSERT(reporter, !SkPathPriv::IsSimpleRect(path2, true));
+
// Make the path by hand, manually closing it.
- path2.reset();
+ builder2.reset();
+ path2 = builder.detach();
SkPoint firstPt = {0.f, 0.f};
- for (auto [v, verbPts, w] : SkPathPriv::Iterate(path)) {
+ for (auto [v, verbPts, w] : SkPathPriv::Iterate(path2)) {
switch(v) {
case SkPathVerb::kMove:
firstPt = verbPts[0];
- path2.moveTo(verbPts[0]);
+ builder2.moveTo(verbPts[0]);
break;
case SkPathVerb::kLine:
- path2.lineTo(verbPts[1]);
+ builder2.lineTo(verbPts[1]);
break;
default:
break;
}
}
+ path2 = builder2.snapshot();
// We haven't closed it yet...
REPORTER_ASSERT(reporter, !SkPathPriv::IsSimpleRect(path2, false));
REPORTER_ASSERT(reporter, !SkPathPriv::IsSimpleRect(path2, true));
// ... now we do and test again.
- path2.lineTo(firstPt);
- check_simple_rect(reporter, path2, false, testRect, dir, start);
+ builder2.lineTo(firstPt);
+ check_simple_rect(reporter, builder2.snapshot(), false, testRect, dir, start);
// A redundant close shouldn't cause a failure.
- path2.close();
- check_simple_rect(reporter, path2, true, testRect, dir, start);
+ builder2.close();
+ check_simple_rect(reporter, builder2.snapshot(), true, testRect, dir, start);
+
// Degenerate point and line rects are not allowed
- path2.reset();
- path2.addRect(emptyRect, dir, start);
+ builder2.reset();
+ builder2.addRect(emptyRect, dir, start);
+ path2 = builder2.snapshot();
REPORTER_ASSERT(reporter, !SkPathPriv::IsSimpleRect(path2, false));
REPORTER_ASSERT(reporter, !SkPathPriv::IsSimpleRect(path2, true));
SkRect degenRect = testRect;
degenRect.fLeft = degenRect.fRight;
- path2.reset();
- path2.addRect(degenRect, dir, start);
+
+ builder2.reset();
+ builder2.addRect(degenRect, dir, start);
+ path2 = builder2.snapshot();
REPORTER_ASSERT(reporter, !SkPathPriv::IsSimpleRect(path2, false));
REPORTER_ASSERT(reporter, !SkPathPriv::IsSimpleRect(path2, true));
degenRect = testRect;
degenRect.fTop = degenRect.fBottom;
- path2.reset();
- path2.addRect(degenRect, dir, start);
+
+ builder2.reset();
+ builder2.addRect(degenRect, dir, start);
+ path2 = builder2.snapshot();
REPORTER_ASSERT(reporter, !SkPathPriv::IsSimpleRect(path2, false));
REPORTER_ASSERT(reporter, !SkPathPriv::IsSimpleRect(path2, true));
+
// An inverted rect makes a rect path, but changes the winding dir and start point.
SkPathDirection swapDir = (dir == SkPathDirection::kCW)
? SkPathDirection::kCCW
@@ -2434,50 +2450,57 @@
static constexpr unsigned kYSwapStarts[] = { 3, 2, 1, 0 };
SkRect swapRect = testRect;
swap(swapRect.fLeft, swapRect.fRight);
- path2.reset();
- path2.addRect(swapRect, dir, start);
+ builder2.reset();
+ builder2.addRect(swapRect, dir, start);
+ path2 = builder2.snapshot();
check_simple_rect(reporter, path2, true, testRect, swapDir, kXSwapStarts[start]);
swapRect = testRect;
swap(swapRect.fTop, swapRect.fBottom);
- path2.reset();
- path2.addRect(swapRect, dir, start);
+
+ builder2.reset();
+ builder2.addRect(swapRect, dir, start);
+ path2 = builder2.snapshot();
check_simple_rect(reporter, path2, true, testRect, swapDir, kYSwapStarts[start]);
}
}
// down, up, left, close
- SkPath path;
- path.moveTo(1, 1);
- path.lineTo(1, 2);
- path.lineTo(1, 1);
- path.lineTo(0, 1);
- path.close();
+ SkPath path = SkPathBuilder()
+ .moveTo(1, 1)
+ .lineTo(1, 2)
+ .lineTo(1, 1)
+ .lineTo(0, 1)
+ .close()
+ .detach();
REPORTER_ASSERT(reporter, !SkPathPriv::IsSimpleRect(path, false));
REPORTER_ASSERT(reporter, !SkPathPriv::IsSimpleRect(path, true));
// right, left, up, close
- path.reset();
- path.moveTo(1, 1);
- path.lineTo(2, 1);
- path.lineTo(1, 1);
- path.lineTo(1, 0);
- path.close();
+ path = SkPathBuilder()
+ .moveTo(1, 1)
+ .lineTo(2, 1)
+ .lineTo(1, 1)
+ .lineTo(1, 0)
+ .close()
+ .detach();
REPORTER_ASSERT(reporter, !SkPathPriv::IsSimpleRect(path, false));
REPORTER_ASSERT(reporter, !SkPathPriv::IsSimpleRect(path, true));
// parallelogram with horizontal edges
- path.reset();
- path.moveTo(1, 0);
- path.lineTo(3, 0);
- path.lineTo(2, 1);
- path.lineTo(0, 1);
- path.close();
+ path = SkPathBuilder()
+ .moveTo(1, 0)
+ .lineTo(3, 0)
+ .lineTo(2, 1)
+ .lineTo(0, 1)
+ .close()
+ .detach();
REPORTER_ASSERT(reporter, !SkPathPriv::IsSimpleRect(path, false));
REPORTER_ASSERT(reporter, !SkPathPriv::IsSimpleRect(path, true));
// parallelogram with vertical edges
- path.reset();
- path.moveTo(0, 1);
- path.lineTo(0, 3);
- path.lineTo(1, 2);
- path.lineTo(1, 0);
- path.close();
+ path = SkPathBuilder()
+ .moveTo(0, 1)
+ .lineTo(0, 3)
+ .lineTo(1, 2)
+ .lineTo(1, 0)
+ .close()
+ .detach();
REPORTER_ASSERT(reporter, !SkPathPriv::IsSimpleRect(path, false));
REPORTER_ASSERT(reporter, !SkPathPriv::IsSimpleRect(path, true));
@@ -3098,18 +3121,16 @@
p.reset();
iter.setPath(p, false);
REPORTER_ASSERT(reporter, !iter.isClosedContour());
- p.lineTo(1, 1);
- p.close();
+ p = SkPathBuilder().lineTo(1, 1).close().detach();
iter.setPath(p, false);
REPORTER_ASSERT(reporter, iter.isClosedContour());
p.reset();
iter.setPath(p, true);
REPORTER_ASSERT(reporter, !iter.isClosedContour());
- p.lineTo(1, 1);
+ p = SkPathBuilder().lineTo(1, 1).detach();
iter.setPath(p, true);
REPORTER_ASSERT(reporter, iter.isClosedContour());
- p.moveTo(0, 0);
- p.lineTo(2, 2);
+ p = SkPathBuilder().lineTo(1, 1).moveTo(0, 0).lineTo(2, 2).detach();
iter.setPath(p, false);
REPORTER_ASSERT(reporter, !iter.isClosedContour());
@@ -4016,7 +4037,7 @@
}
static void test_addPathMode(skiatest::Reporter* reporter, bool explicitMoveTo, bool extend) {
- SkPath p, q;
+ SkPathBuilder p, q;
if (explicitMoveTo) {
p.moveTo(1, 1);
}
@@ -4025,52 +4046,56 @@
q.moveTo(2, 1);
}
q.lineTo(2, 2);
- p.addPath(q, extend ? SkPath::kExtend_AddPathMode : SkPath::kAppend_AddPathMode);
- uint8_t verbs[4];
- int verbcount = p.getVerbs(verbs);
- REPORTER_ASSERT(reporter, verbcount == 4);
- REPORTER_ASSERT(reporter, verbs[0] == SkPath::kMove_Verb);
- REPORTER_ASSERT(reporter, verbs[1] == SkPath::kLine_Verb);
- REPORTER_ASSERT(reporter, verbs[2] == (extend ? SkPath::kLine_Verb : SkPath::kMove_Verb));
- REPORTER_ASSERT(reporter, verbs[3] == SkPath::kLine_Verb);
+ p.addPath(q.detach(), extend ? SkPath::kExtend_AddPathMode : SkPath::kAppend_AddPathMode);
+
+ SkSpan<const SkPathVerb> verbs = p.verbs();
+ REPORTER_ASSERT(reporter, verbs.size() == 4);
+ REPORTER_ASSERT(reporter, verbs[0] == SkPathVerb::kMove);
+ REPORTER_ASSERT(reporter, verbs[1] == SkPathVerb::kLine);
+ REPORTER_ASSERT(reporter, verbs[2] == (extend ? SkPathVerb::kLine : SkPathVerb::kMove));
+ REPORTER_ASSERT(reporter, verbs[3] == SkPathVerb::kLine);
}
static void test_extendClosedPath(skiatest::Reporter* reporter) {
- SkPath p, q;
- p.moveTo(1, 1);
- p.lineTo(1, 2);
- p.lineTo(2, 2);
- p.close();
- q.moveTo(2, 1);
- q.lineTo(2, 3);
- p.addPath(q, SkPath::kExtend_AddPathMode);
- uint8_t verbs[7];
- int verbcount = p.getVerbs(verbs);
- REPORTER_ASSERT(reporter, verbcount == 7);
- REPORTER_ASSERT(reporter, verbs[0] == SkPath::kMove_Verb);
- REPORTER_ASSERT(reporter, verbs[1] == SkPath::kLine_Verb);
- REPORTER_ASSERT(reporter, verbs[2] == SkPath::kLine_Verb);
- REPORTER_ASSERT(reporter, verbs[3] == SkPath::kClose_Verb);
- REPORTER_ASSERT(reporter, verbs[4] == SkPath::kMove_Verb);
- REPORTER_ASSERT(reporter, verbs[5] == SkPath::kLine_Verb);
- REPORTER_ASSERT(reporter, verbs[6] == SkPath::kLine_Verb);
+ SkPath q = SkPathBuilder().moveTo(2, 1).lineTo(2, 3).detach();
- SkPoint pt;
- REPORTER_ASSERT(reporter, p.getLastPt(&pt));
- REPORTER_ASSERT(reporter, pt == SkPoint::Make(2, 3));
- REPORTER_ASSERT(reporter, p.getPoint(3) == SkPoint::Make(1, 1));
+ SkPathBuilder p;
+ p.moveTo(1, 1)
+ .lineTo(1, 2)
+ .lineTo(2, 2)
+ .close()
+ .addPath(q, SkPath::kExtend_AddPathMode);
+
+ SkSpan<const SkPathVerb> verbs = p.verbs();
+ REPORTER_ASSERT(reporter, verbs.size() == 7);
+ REPORTER_ASSERT(reporter, verbs[0] == SkPathVerb::kMove);
+ REPORTER_ASSERT(reporter, verbs[1] == SkPathVerb::kLine);
+ REPORTER_ASSERT(reporter, verbs[2] == SkPathVerb::kLine);
+ REPORTER_ASSERT(reporter, verbs[3] == SkPathVerb::kClose);
+ REPORTER_ASSERT(reporter, verbs[4] == SkPathVerb::kMove);
+ REPORTER_ASSERT(reporter, verbs[5] == SkPathVerb::kLine);
+ REPORTER_ASSERT(reporter, verbs[6] == SkPathVerb::kLine);
+
+ auto pt = p.getLastPt();
+ REPORTER_ASSERT(reporter, pt.has_value());
+ REPORTER_ASSERT(reporter, pt.value() == SkPoint::Make(2, 3));
+ REPORTER_ASSERT(reporter, p.points()[3] == SkPoint::Make(1, 1));
}
static void test_addEmptyPath(skiatest::Reporter* reporter, SkPath::AddPathMode mode) {
- SkPath p, q, r;
// case 1: dst is empty
+ SkPathBuilder p;
p.moveTo(2, 1);
p.lineTo(2, 3);
- q.addPath(p, mode);
+ SkPathBuilder q;
+ q.addPath(p.snapshot(), mode);
REPORTER_ASSERT(reporter, q == p);
+
// case 2: src is empty
+ SkPath r; // empty
p.addPath(r, mode);
REPORTER_ASSERT(reporter, q == p);
+
// case 3: src and dst are empty
q.reset();
q.addPath(r, mode);