Add isArc to SkPath

This remembers when a path is a simple elliptical arc (ie, it could be
rendered by a call to SkCanvas::drawArc).

For now, this only remembers perimeter arcs (when useCenter is false).
I intend to support useCenter arcs as well, but those will be more
complicated. Regardless, the new interface will support both uses.

Bug: b/330506337
Change-Id: I5c43809e0d57d2e1a4e5704d4027863412d6a739
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/841396
Reviewed-by: Ben Wagner <bungeman@google.com>
Commit-Queue: Brian Osman <brianosman@google.com>
diff --git a/include/core/SkPath.h b/include/core/SkPath.h
index a0faea5..8936c9b 100644
--- a/include/core/SkPath.h
+++ b/include/core/SkPath.h
@@ -26,6 +26,7 @@
 #include <tuple>
 #include <type_traits>
 
+struct SkArc;
 class SkData;
 class SkPathRef;
 class SkRRect;
@@ -280,6 +281,16 @@
     */
     bool isRRect(SkRRect* rrect) const;
 
+    /** Returns true if path is representable as an oval arc. In other words, could this
+        path be drawn using SkCanvas::drawArc.
+
+        arc  receives parameters of arc
+
+       @param arc  storage for arc; may be nullptr
+       @return     true if SkPath contains only a single arc from an oval
+    */
+    bool isArc(SkArc* arc) const;
+
     /** 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.
diff --git a/include/private/SkPathRef.h b/include/private/SkPathRef.h
index 5bf2a39..ba8ef80 100644
--- a/include/private/SkPathRef.h
+++ b/include/private/SkPathRef.h
@@ -8,6 +8,7 @@
 #ifndef SkPathRef_DEFINED
 #define SkPathRef_DEFINED
 
+#include "include/core/SkArc.h"
 #include "include/core/SkPoint.h"
 #include "include/core/SkRect.h"
 #include "include/core/SkRefCnt.h"
@@ -63,6 +64,7 @@
         kGeneral,
         kOval,
         kRRect,
+        kArc,
     };
 
     SkPathRef(PointsArray points, VerbsArray verbs, ConicWeightsArray weights,
@@ -78,6 +80,9 @@
         // The next two values don't matter unless fType is kOval or kRRect
         fRRectOrOvalIsCCW = false;
         fRRectOrOvalStartIdx = 0xAC;
+        fArcOval.setEmpty();
+        fArcStartAngle = fArcSweepAngle = 0.0f;
+        fArcUseCenter = false;
         SkDEBUGCODE(fEditorsAttached.store(0);)
 
         this->computeBounds();  // do this now, before we worry about multiple owners/threads
@@ -160,6 +165,10 @@
             fPathRef->setIsRRect(isCCW, start);
         }
 
+        void setIsArc(const SkArc& arc) {
+            fPathRef->setIsArc(arc);
+        }
+
         void setBounds(const SkRect& rect) { fPathRef->setBounds(rect); }
 
     private:
@@ -249,6 +258,19 @@
 
     bool isRRect(SkRRect* rrect, bool* isCCW, unsigned* start) const;
 
+    bool isArc(SkArc* arc) const {
+        if (fType == PathType::kArc) {
+            if (arc) {
+                arc->fOval = fArcOval;
+                arc->fStartAngle = fArcStartAngle;
+                arc->fSweepAngle = fArcSweepAngle;
+                arc->fUseCenter = fArcUseCenter;
+            }
+        }
+
+        return fType == PathType::kArc;
+    }
+
     bool hasComputedBounds() const {
         return !fBoundsIsDirty;
     }
@@ -365,6 +387,9 @@
         // The next two values don't matter unless fType is kOval or kRRect
         fRRectOrOvalIsCCW = false;
         fRRectOrOvalStartIdx = 0xAC;
+        fArcOval.setEmpty();
+        fArcStartAngle = fArcSweepAngle = 0.0f;
+        fArcUseCenter = false;
         if (numPoints > 0) {
             fPoints.reserve_exact(numPoints);
         }
@@ -497,6 +522,14 @@
         fRRectOrOvalStartIdx = SkToU8(start);
     }
 
+    void setIsArc(const SkArc& arc) {
+        fType = PathType::kArc;
+        fArcOval = arc.fOval;
+        fArcStartAngle = arc.fStartAngle;
+        fArcSweepAngle = arc.fSweepAngle;
+        fArcUseCenter = arc.fUseCenter;
+    }
+
     // called only by the editor. Note that this is not a const function.
     SkPoint* getWritablePoints() {
         SkDEBUGCODE(this->validate();)
@@ -534,6 +567,12 @@
     bool     fRRectOrOvalIsCCW;
     uint8_t  fRRectOrOvalStartIdx;
     uint8_t  fSegmentMask;
+    // If the path is an arc, these four variables store that information.
+    // We should just store an SkArc, but alignment would cost us 8 more bytes.
+    bool     fArcUseCenter;
+    SkRect   fArcOval;
+    SkScalar fArcStartAngle;
+    SkScalar fArcSweepAngle;
 
     friend class PathRefTest_Private;
     friend class ForceIsRRect_Private; // unit test isRRect
diff --git a/src/core/SkPath.cpp b/src/core/SkPath.cpp
index 2625cf3..27046f7 100644
--- a/src/core/SkPath.cpp
+++ b/src/core/SkPath.cpp
@@ -518,6 +518,10 @@
     return SkPathPriv::IsRRect(*this, rrect, nullptr, nullptr);
 }
 
+bool SkPath::isArc(SkArc* arc) const {
+    return fPathRef->isArc(arc);
+}
+
 int SkPath::countPoints() const {
     return fPathRef->countPoints();
 }
@@ -1172,10 +1176,12 @@
 
     SkPoint singlePt;
 
+    bool isArc = this->hasOnlyMoveTos();
+
     // Adds a move-to to 'pt' if forceMoveTo is true. Otherwise a lineTo unless we're sufficiently
     // close to 'pt' currently. This prevents spurious lineTos when adding a series of contiguous
     // arcs from the same oval.
-    auto addPt = [&forceMoveTo, this](const SkPoint& pt) {
+    auto addPt = [&forceMoveTo, &isArc, this](const SkPoint& pt) {
         SkPoint lastPt;
         if (forceMoveTo) {
             this->moveTo(pt);
@@ -1183,6 +1189,7 @@
                    !SkScalarNearlyEqual(lastPt.fX, pt.fX) ||
                    !SkScalarNearlyEqual(lastPt.fY, pt.fY)) {
             this->lineTo(pt);
+            isArc = false;
         }
     };
 
@@ -1212,6 +1219,10 @@
         for (int i = 0; i < count; ++i) {
             this->conicTo(conics[i].fPts[1], conics[i].fPts[2], conics[i].fW);
         }
+        if (isArc) {
+            SkPathRef::Editor ed(&fPathRef);
+            ed.setIsArc({oval, startAngle, sweepAngle, false});
+        }
     } else {
         addPt(singlePt);
     }
diff --git a/src/core/SkPathRef.cpp b/src/core/SkPathRef.cpp
index f53b7cf..d2299c7 100644
--- a/src/core/SkPathRef.cpp
+++ b/src/core/SkPathRef.cpp
@@ -10,6 +10,7 @@
 #include "include/core/SkMatrix.h"
 #include "include/core/SkPath.h"
 #include "include/core/SkRRect.h"
+#include "include/private/base/SkFloatingPoint.h"
 #include "include/private/base/SkOnce.h"
 #include "src/base/SkVx.h"
 
@@ -200,9 +201,12 @@
 
     (*dst)->fSegmentMask = src.fSegmentMask;
 
-    // It's an oval only if it stays a rect.
+    // It's an oval only if it stays a rect. Technically if scale is uniform, then it would stay an
+    // arc. For now, don't bother handling that (we'd also need to fixup the angles for negative
+    // scale, etc.)
     bool rectStaysRect = matrix.rectStaysRect();
-    const PathType newType = rectStaysRect ? src.fType : PathType::kGeneral;
+    const PathType newType =
+            (rectStaysRect && src.fType != PathType::kArc) ? src.fType : PathType::kGeneral;
     (*dst)->fType = newType;
     if (newType == PathType::kOval || newType == PathType::kRRect) {
         unsigned start = src.fRRectOrOvalStartIdx;
@@ -286,6 +290,10 @@
     fType = ref.fType;
     fRRectOrOvalIsCCW = ref.fRRectOrOvalIsCCW;
     fRRectOrOvalStartIdx = ref.fRRectOrOvalStartIdx;
+    fArcOval = ref.fArcOval;
+    fArcStartAngle = ref.fArcStartAngle;
+    fArcSweepAngle = ref.fArcSweepAngle;
+    fArcUseCenter = ref.fArcUseCenter;
     SkDEBUGCODE(this->validate();)
 }
 
@@ -621,6 +629,11 @@
                 return false;
             }
             break;
+        case PathType::kArc:
+            if (!(fArcOval.isFinite() && SkIsFinite(fArcStartAngle, fArcSweepAngle))) {
+                return false;
+            }
+            break;
     }
 
     if (!fBoundsIsDirty && !fBounds.isEmpty()) {
diff --git a/tests/PathTest.cpp b/tests/PathTest.cpp
index 897583a..c289189 100644
--- a/tests/PathTest.cpp
+++ b/tests/PathTest.cpp
@@ -2438,6 +2438,84 @@
 
 }
 
+static void test_isArc(skiatest::Reporter* reporter) {
+    SkPath path;
+    REPORTER_ASSERT(reporter, !path.isArc(nullptr));
+
+    // One circle, one oval:
+    const SkRect kOvals[] = { SkRect::MakeWH(100, 100), SkRect::MakeWH(100, 200)};
+
+    // Various start and sweep angles. Note that we can't test with more than a full revolution,
+    // those cases are automatically converted to ovals by SkPath.
+    const SkScalar kStartAngles[] = { -270, -135, -45, 0, 10, 70, 180, 350 };
+    const SkScalar kSweepAngles[] = { -350, -190, -90, -5, 5, 89, 180, 270, 350 };
+
+    int mutator = 0;
+
+    for (SkRect oval : kOvals) {
+        for (SkScalar startAngle : kStartAngles) {
+            for (SkScalar sweepAngle : kSweepAngles) {
+                // For now, isArc only works for arcs where useCenter is false!
+                // TODO: When that's fixed, add more tests cases here.
+                path.rewind();
+                // Include an extra moveTo at the start - this should not interfere with isArc
+                path.moveTo(oval.center());
+                path.addArc(oval, startAngle, sweepAngle);
+
+                SkArc arc;
+                REPORTER_ASSERT(reporter, path.isArc(&arc));
+                REPORTER_ASSERT(reporter,
+                                oval == arc.fOval &&
+                                startAngle == arc.fStartAngle &&
+                                sweepAngle == arc.fSweepAngle &&
+                                !arc.fUseCenter);
+
+                // Apply some mutation. All of these should cause the path to no longer be an arc:
+                switch (mutator) {
+                    case 0:
+                        path.addArc(oval, startAngle, sweepAngle);
+                        break;
+                    case 1:
+                        path.lineTo(oval.center());
+                        break;
+                    case 2:
+                        path.lineTo(path.getPoint(0));
+                        break;
+                    case 3:
+                        path.close();
+                        break;
+                    case 4:
+                        path.moveTo(oval.center());
+                        break;
+                    default:
+                        SkUNREACHABLE;
+                }
+                mutator = (mutator + 1) % 5;
+                REPORTER_ASSERT(reporter, !path.isArc(nullptr));
+            }
+        }
+    }
+
+    // Having any non-move verb before the arc should cause isArc to return false:
+    path.rewind();
+    path.lineTo(kOvals[0].center());
+    path.addArc(kOvals[0], kStartAngles[0], kSweepAngles[0]);
+    REPORTER_ASSERT(reporter, !path.isArc(nullptr));
+
+    // Finally, transforming an arc path by a non-identity should always result in a non-arc path:
+    // TODO: We could clearly preserve arcs for translation, and for scale/rotation with extra work.
+    for (SkMatrix m :
+         {SkMatrix::Translate(10, 10), SkMatrix::RotateDeg(90), SkMatrix::Scale(2, 2)}) {
+        path.rewind();
+        path.addArc(kOvals[0], kStartAngles[0], kSweepAngles[0]);
+        REPORTER_ASSERT(reporter, path.isArc(nullptr));
+        path.transform(SkMatrix::I());
+        REPORTER_ASSERT(reporter, path.isArc(nullptr));
+        path.transform(m);
+        REPORTER_ASSERT(reporter, !path.isArc(nullptr));
+    }
+}
+
 static void test_isNestedFillRects(skiatest::Reporter* reporter) {
     // passing tests (all moveTo / lineTo...
     SkPoint r1[] = {{0, 0}, {1, 0}, {1, 1}, {0, 1}}; // CW
@@ -5014,6 +5092,7 @@
     test_isRect(reporter);
     test_is_closed_rect(reporter);
     test_isNestedFillRects(reporter);
+    test_isArc(reporter);
     test_zero_length_paths(reporter);
     test_direction(reporter);
     test_convexity(reporter);