Add SkPathPriv::IsSimpleClosedRect

This function looks for "simple" rect paths. Simple here means begins and ends at a corner and is closed (either manually or with a close verb). Unlike SkPath::isRect this returns the starting point index (using the same start indexing scheme as SkPath::addRect).

GOLD_TRYBOT_URL= https://gold.skia.org/search?issue=2017313002

Review-Url: https://codereview.chromium.org/2017313002
diff --git a/src/core/SkPath.cpp b/src/core/SkPath.cpp
index 5cd6316..84181a3 100644
--- a/src/core/SkPath.cpp
+++ b/src/core/SkPath.cpp
@@ -3249,3 +3249,124 @@
     const SkConic conic(p0, p1, p2, w);
     return conic.chopIntoQuadsPOW2(pts, pow2);
 }
+
+bool SkPathPriv::IsSimpleClosedRect(const SkPath& path, SkRect* rect, SkPath::Direction* direction,
+                                    unsigned* start) {
+    if (path.getSegmentMasks() != SkPath::kLine_SegmentMask) {
+        return false;
+    }
+    SkPath::RawIter iter(path);
+    SkPoint verbPts[4];
+    SkPath::Verb v;
+    SkPoint rectPts[5];
+    int rectPtCnt = 0;
+    while ((v = iter.next(verbPts)) != SkPath::kDone_Verb) {
+        switch (v) {
+            case SkPath::kMove_Verb:
+                if (0 != rectPtCnt) {
+                    return false;
+                }
+                rectPts[0] = verbPts[0];
+                ++rectPtCnt;
+                break;
+            case SkPath::kLine_Verb:
+                if (5 == rectPtCnt) {
+                    return false;
+                }
+                rectPts[rectPtCnt] = verbPts[1];
+                ++rectPtCnt;
+                break;
+            case SkPath::kClose_Verb:
+                if (4 == rectPtCnt) {
+                    rectPts[4] = rectPts[0];
+                    rectPtCnt = 5;
+                }
+                break;
+            default:
+                return false;
+        }
+    }
+    if (rectPtCnt < 5) {
+        return false;
+    }
+    if (rectPts[0] != rectPts[4]) {
+        return false;
+    }
+    int verticalCnt = 0;
+    int horizontalCnt = 0;
+    // dirs are 0 - right, 1 - down, 2 - left, 3 - up.
+    int firstDir;
+    int secondDir;
+    SkRect tempRect;
+    for (int i = 0; i < 4; ++i) {
+        int sameCnt = 0;
+        if (rectPts[i].fX == rectPts[i + 1].fX) {
+            verticalCnt += 1;
+            sameCnt = 1;
+            if (0 == i) {
+                if (rectPts[1].fY > rectPts[0].fY) {
+                    firstDir = 1;
+                    tempRect.fTop = rectPts[0].fY;
+                    tempRect.fBottom = rectPts[1].fY;
+                } else {
+                    firstDir = 3;
+                    tempRect.fTop = rectPts[1].fY;
+                    tempRect.fBottom = rectPts[0].fY;
+                }
+            } else if (1 == i) {
+                if (rectPts[2].fY > rectPts[1].fY) {
+                    secondDir = 1;
+                    tempRect.fTop = rectPts[1].fY;
+                    tempRect.fBottom = rectPts[2].fY;
+                } else {
+                    secondDir = 3;
+                    tempRect.fTop = rectPts[2].fY;
+                    tempRect.fBottom = rectPts[1].fY;
+                }
+            }
+        }
+        if (rectPts[i].fY == rectPts[i + 1].fY) {
+            horizontalCnt += 1;
+            sameCnt += 1;
+            if (0 == i) {
+                if (rectPts[1].fX > rectPts[0].fX) {
+                    firstDir = 0;
+                    tempRect.fLeft = rectPts[0].fX;
+                    tempRect.fRight = rectPts[1].fX;
+                } else {
+                    firstDir = 2;
+                    tempRect.fLeft = rectPts[1].fX;
+                    tempRect.fRight = rectPts[0].fX;
+                }
+            } else if (1 == i) {
+                if (rectPts[2].fX > rectPts[1].fX) {
+                    secondDir = 0;
+                    tempRect.fLeft = rectPts[1].fX;
+                    tempRect.fRight = rectPts[2].fX;
+                } else {
+                    secondDir = 2;
+                    tempRect.fLeft = rectPts[2].fX;
+                    tempRect.fRight = rectPts[1].fX;
+                }
+            }
+        }
+        if (sameCnt != 1) {
+            return false;
+        }
+    }
+    if (2 != horizontalCnt || 2 != verticalCnt) {
+        return false;
+    }
+    // low bit indicates a vertical dir
+    SkASSERT((firstDir ^ secondDir) & 0b1);
+    if (((firstDir + 1) & 0b11) == secondDir) {
+        *direction = SkPath::kCW_Direction;
+        *start = firstDir;
+    } else {
+        SkASSERT(((secondDir + 1) & 0b11) == firstDir);
+        *direction = SkPath::kCCW_Direction;
+        *start =  secondDir;
+    }
+    *rect = tempRect;
+    return true;
+}
diff --git a/src/core/SkPathPriv.h b/src/core/SkPathPriv.h
index f0bab95..8689dee 100644
--- a/src/core/SkPathPriv.h
+++ b/src/core/SkPathPriv.h
@@ -83,6 +83,14 @@
     static void AddGenIDChangeListener(const SkPath& path, SkPathRef::GenIDChangeListener* listener) {
         path.fPathRef->addGenIDChangeListener(listener);
     }
+
+    /**
+     * This returns true for a rect that begins and ends at the same corner and has either a move
+     * followed by four lines or a move followed by 3 lines and a close. None of the parameters are
+     * optional. This does not permit degenerate line or point rectangles.
+     */
+    static bool IsSimpleClosedRect(const SkPath& path, SkRect* rect, SkPath::Direction* direction,
+                                   unsigned* start);
 };
 
 #endif
diff --git a/tests/PathTest.cpp b/tests/PathTest.cpp
index f4b488b..b406f25 100644
--- a/tests/PathTest.cpp
+++ b/tests/PathTest.cpp
@@ -1975,6 +1975,103 @@
     REPORTER_ASSERT(reporter, !path1.isRect(nullptr));
 }
 
+static void check_simple_closed_rect(skiatest::Reporter* reporter, const SkPath& path,
+                                     const SkRect& rect, SkPath::Direction dir, unsigned start) {
+    SkRect r = SkRect::MakeEmpty();
+    SkPath::Direction d = SkPath::kCCW_Direction;
+    unsigned s = ~0U;
+
+    REPORTER_ASSERT(reporter, SkPathPriv::IsSimpleClosedRect(path, &r, &d, &s));
+    REPORTER_ASSERT(reporter, r == rect);
+    REPORTER_ASSERT(reporter, d == dir);
+    REPORTER_ASSERT(reporter, s == start);
+}
+
+static void test_is_simple_closed_rect(skiatest::Reporter* reporter) {
+    SkRect r = SkRect::MakeEmpty();
+    SkPath::Direction d = SkPath::kCCW_Direction;
+    unsigned s = ~0U;
+
+    const SkRect testRect = SkRect::MakeXYWH(10, 10, 50, 70);
+    const SkRect emptyRect = SkRect::MakeEmpty();
+    SkPath path;
+    for (int start = 0; start < 4; ++start) {
+        for (auto dir : {SkPath::kCCW_Direction, SkPath::kCW_Direction}) {
+            SkPath path;
+            path.addRect(testRect, dir, start);
+            check_simple_closed_rect(reporter, path, testRect, dir, start);
+            path.close();
+            check_simple_closed_rect(reporter, path, testRect, dir, start);
+            SkPath path2 = path;
+            path2.lineTo(10, 10);
+            REPORTER_ASSERT(reporter, !SkPathPriv::IsSimpleClosedRect(path2, &r, &d, &s));
+            path2 = path;
+            path2.moveTo(10, 10);
+            REPORTER_ASSERT(reporter, !SkPathPriv::IsSimpleClosedRect(path2, &r, &d, &s));
+            path2 = path;
+            path2.addRect(testRect, dir, start);
+            REPORTER_ASSERT(reporter, !SkPathPriv::IsSimpleClosedRect(path2, &r, &d, &s));
+            // Make the path by hand, manually closing it.
+            path2.reset();
+            SkPath::RawIter iter(path);
+            SkPath::Verb v;
+            SkPoint verbPts[4];
+            SkPoint firstPt = {0.f, 0.f};
+            while ((v = iter.next(verbPts)) != SkPath::kDone_Verb) {
+                switch(v) {
+                    case SkPath::kMove_Verb:
+                        firstPt = verbPts[0];
+                        path2.moveTo(verbPts[0]);
+                        break;
+                    case SkPath::kLine_Verb:
+                        path2.lineTo(verbPts[1]);
+                        break;
+                    default:
+                        break;
+                }
+            }
+            // We haven't closed it yet...
+            REPORTER_ASSERT(reporter, !SkPathPriv::IsSimpleClosedRect(path2, &r, &d, &s));
+            // ... now we do and test again.
+            path2.lineTo(firstPt);
+            check_simple_closed_rect(reporter, path2, testRect, dir, start);
+            // A redundant close shouldn't cause a failure.
+            path2.close();
+            check_simple_closed_rect(reporter, path2, testRect, dir, start);
+            // Degenerate point and line rects are not allowed
+            path2.reset();
+            path2.addRect(emptyRect, dir, start);
+            REPORTER_ASSERT(reporter, !SkPathPriv::IsSimpleClosedRect(path2, &r, &d, &s));
+            SkRect degenRect = testRect;
+            degenRect.fLeft = degenRect.fRight;
+            path2.reset();
+            path2.addRect(degenRect, dir, start);
+            REPORTER_ASSERT(reporter, !SkPathPriv::IsSimpleClosedRect(path2, &r, &d, &s));
+            degenRect = testRect;
+            degenRect.fTop = degenRect.fBottom;
+            path2.reset();
+            path2.addRect(degenRect, dir, start);
+            REPORTER_ASSERT(reporter, !SkPathPriv::IsSimpleClosedRect(path2, &r, &d, &s));
+            // An inverted rect makes a rect path, but changes the winding dir and start point.
+            SkPath::Direction swapDir = (dir == SkPath::kCW_Direction)
+                                            ? SkPath::kCCW_Direction
+                                            : SkPath::kCW_Direction;
+            static constexpr unsigned kXSwapStarts[] = { 1, 0, 3, 2 };
+            static constexpr unsigned kYSwapStarts[] = { 3, 2, 1, 0 };
+            SkRect swapRect = testRect;
+            SkTSwap(swapRect.fLeft, swapRect.fRight);
+            path2.reset();
+            path2.addRect(swapRect, dir, start);
+            check_simple_closed_rect(reporter, path2, testRect, swapDir, kXSwapStarts[start]);
+            swapRect = testRect;
+            SkTSwap(swapRect.fTop, swapRect.fBottom);
+            path2.reset();
+            path2.addRect(swapRect, dir, start);
+            check_simple_closed_rect(reporter, path2, testRect, swapDir, kYSwapStarts[start]);
+        }
+    }
+}
+
 static void test_isNestedFillRects(skiatest::Reporter* reporter) {
     // passing tests (all moveTo / lineTo...
     SkPoint r1[] = {{0, 0}, {1, 0}, {1, 1}, {0, 1}}; // CW
@@ -4150,6 +4247,7 @@
     test_operatorEqual(reporter);
     test_isLine(reporter);
     test_isRect(reporter);
+    test_is_simple_closed_rect(reporter);
     test_isNestedFillRects(reporter);
     test_zero_length_paths(reporter);
     test_direction(reporter);