move perspective-clipper into SkPathPriv

From here we can start trying it out for real
- skottie could call it directly
- SkDraw could call it for the raster-backend

Change-Id: I703838241d67c539afd3d3e31d0ddf1c66645799
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/258001
Reviewed-by: Jim Van Verth <jvanverth@google.com>
Commit-Queue: Mike Reed <reed@google.com>
diff --git a/samplecode/SampleClip.cpp b/samplecode/SampleClip.cpp
index bff7704..ccfda1e 100644
--- a/samplecode/SampleClip.cpp
+++ b/samplecode/SampleClip.cpp
@@ -13,6 +13,7 @@
 #include "include/utils/SkRandom.h"
 #include "samplecode/Sample.h"
 #include "src/core/SkClipOpPriv.h"
+#include "src/core/SkPathPriv.h"
 
 constexpr int W = 150;
 constexpr int H = 200;
@@ -270,26 +271,6 @@
     rec.fResult->transform(mx);
 }
 
-// true means use clippedPath.
-// false means there was no clipping -- use the original path
-static bool clip(const SkPath& path, const SkHalfPlane& plane, SkPath* clippedPath) {
-    switch (plane.test(path.getBounds())) {
-        case SkHalfPlane::kAllPositive:
-            return false;
-        case SkHalfPlane::kMixed: {
-            SkPoint pts[2];
-            if (plane.twoPts(pts)) {
-                clip(path, pts[0], pts[1], clippedPath);
-                return true;
-            }
-        } break;
-        default: break; // handled outside of the switch
-    }
-    // clipped out (or failed)
-    clippedPath->reset();
-    return true;
-}
-
 static void draw_halfplane(SkCanvas* canvas, SkPoint p0, SkPoint p1, SkColor c) {
     SkVector v = p1 - p0;
     p0 = p0 - v * 1000;
@@ -540,11 +521,10 @@
             canvas->restore();
         }
 
-        SkHalfPlane hpw = half_plane_w0(mx);
 
         SkColor planeColor = SK_ColorBLUE;
         SkPath clippedPath, *path = &fPath;
-        if (clip(fPath, hpw, &clippedPath)) {
+        if (SkPathPriv::PerspectiveClip(fPath, mx, &clippedPath)) {
             path = &clippedPath;
             planeColor = SK_ColorRED;
         }
@@ -553,6 +533,7 @@
         canvas->drawPath(*path, paint);
         canvas->restore();
 
+        SkHalfPlane hpw = half_plane_w0(mx);
         draw_halfplane(canvas, hpw, planeColor);
     }
 
diff --git a/src/core/SkPath.cpp b/src/core/SkPath.cpp
index 5679a9d..2ffa9e4 100644
--- a/src/core/SkPath.cpp
+++ b/src/core/SkPath.cpp
@@ -3450,3 +3450,168 @@
     }
     return false;
 }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+#include "src/core/SkEdgeClipper.h"
+
+struct SkHalfPlane {
+    SkScalar fA, fB, fC;
+
+    SkScalar eval(SkScalar x, SkScalar y) const {
+        return fA * x + fB * y + fC;
+    }
+    SkScalar operator()(SkScalar x, SkScalar y) const { return this->eval(x, y); }
+
+    bool twoPts(SkPoint pts[2]) const {
+        // normalize plane to help with the perpendicular step, below
+        SkScalar len = SkScalarSqrt(fA*fA + fB*fB);
+        if (!len) {
+            return false;
+        }
+        SkScalar denom = SkScalarInvert(len);
+        SkScalar a = fA * denom;
+        SkScalar b = fB * denom;
+        SkScalar c = fC * denom;
+
+        // We compute p0 on the half-plane by setting one of the components to 0
+        // We compute p1 by stepping from p0 along a perpendicular to the normal
+        if (b) {
+            pts[0] = { 0, -c / b };
+            pts[1] = { b, pts[0].fY - a};
+        } else if (a) {
+            pts[0] = { -c / a,        0 };
+            pts[1] = { pts[0].fX + b, -a };
+        } else {
+            return false;
+        }
+
+        SkASSERT(SkScalarNearlyZero(this->operator()(pts[0].fX, pts[0].fY)));
+        SkASSERT(SkScalarNearlyZero(this->operator()(pts[1].fX, pts[1].fY)));
+        return true;
+    }
+
+    enum Result {
+        kAllNegative,
+        kAllPositive,
+        kMixed
+    };
+    Result test(const SkRect& bounds) const {
+        // check whether the diagonal aligned with the normal crosses the plane
+        SkPoint diagMin, diagMax;
+        if (fA >= 0) {
+            diagMin.fX = bounds.fLeft;
+            diagMax.fX = bounds.fRight;
+        } else {
+            diagMin.fX = bounds.fRight;
+            diagMax.fX = bounds.fLeft;
+        }
+        if (fB >= 0) {
+            diagMin.fY = bounds.fTop;
+            diagMax.fY = bounds.fBottom;
+        } else {
+            diagMin.fY = bounds.fBottom;
+            diagMax.fY = bounds.fTop;
+        }
+        SkScalar test = this->eval(diagMin.fX, diagMin.fY);
+        SkScalar sign = test*this->eval(diagMax.fX, diagMin.fY);
+        if (sign > 0) {
+            // the path is either all on one side of the half-plane or the other
+            if (test < 0) {
+                return kAllNegative;
+            } else {
+                return kAllPositive;
+            }
+        }
+        return kMixed;
+    }
+};
+
+static void clip(const SkPath& path, SkPoint p0, SkPoint p1, SkPath* clippedPath) {
+    SkMatrix mx, inv;
+    SkVector v = p1 - p0;
+    mx.setAll(v.fX, -v.fY, p0.fX,
+              v.fY,  v.fX, p0.fY,
+                 0,     0,     1);
+    SkAssertResult(mx.invert(&inv));
+
+    SkPath rotated;
+    path.transform(inv, &rotated);
+
+    SkScalar big = 1e28f;
+    SkRect clip = {-big, 0, big, big };
+
+    struct Rec {
+        SkPath* fResult;
+        SkPoint fPrev;
+    } rec = { clippedPath, {0, 0} };
+
+    SkEdgeClipper::ClipPath(rotated, clip, false,
+                            [](SkEdgeClipper* clipper, bool newCtr, void* ctx) {
+        Rec* rec = (Rec*)ctx;
+
+        bool addLineTo = false;
+        SkPoint      pts[4];
+        SkPath::Verb verb;
+        while ((verb = clipper->next(pts)) != SkPath::kDone_Verb) {
+            if (newCtr) {
+                rec->fResult->moveTo(pts[0]);
+                rec->fPrev = pts[0];
+                newCtr = false;
+            }
+
+            if (addLineTo || pts[0] != rec->fPrev) {
+                rec->fResult->lineTo(pts[0]);
+            }
+
+            switch (verb) {
+                case SkPath::kLine_Verb:
+                    rec->fResult->lineTo(pts[1]);
+                    rec->fPrev = pts[1];
+                    break;
+                case SkPath::kQuad_Verb:
+                    rec->fResult->quadTo(pts[1], pts[2]);
+                    rec->fPrev = pts[2];
+                    break;
+                case SkPath::kCubic_Verb:
+                    rec->fResult->cubicTo(pts[1], pts[2], pts[3]);
+                    rec->fPrev = pts[3];
+                    break;
+                default: break;
+            }
+            addLineTo = true;
+        }
+    }, &rec);
+
+    rec.fResult->transform(mx);
+}
+
+// true means we have written to clippedPath
+bool SkPathPriv::PerspectiveClip(const SkPath& path, const SkMatrix& matrix, SkPath* clippedPath) {
+    if (!matrix.hasPerspective()) {
+        return false;
+    }
+
+    constexpr SkScalar kW0PlaneDistance = 0.05f;
+    const SkHalfPlane plane {
+        matrix[SkMatrix::kMPersp0],
+        matrix[SkMatrix::kMPersp1],
+        matrix[SkMatrix::kMPersp2] - kW0PlaneDistance
+    };
+
+    switch (plane.test(path.getBounds())) {
+        case SkHalfPlane::kAllPositive:
+            return false;
+        case SkHalfPlane::kMixed: {
+            SkPoint pts[2];
+            if (plane.twoPts(pts)) {
+                clip(path, pts[0], pts[1], clippedPath);
+                return true;
+            }
+        } break;
+        default: break; // handled outside of the switch
+    }
+    // clipped out (or failed)
+    clippedPath->reset();
+    return true;
+}
diff --git a/src/core/SkPathPriv.h b/src/core/SkPathPriv.h
index 803d8f4..a1d838a 100644
--- a/src/core/SkPathPriv.h
+++ b/src/core/SkPathPriv.h
@@ -316,6 +316,16 @@
     static SkPathFillType ConvertToNonInverseFillType(SkPathFillType fill) {
         return (SkPathFillType)(static_cast<int>(fill) & 1);
     }
+
+    /**
+     *  If needed (to not blow-up under a perspective matrix), clip the path, returning the
+     *  answer in "result", and return true.
+     *
+     *  Note result might be empty (if the path was completely clipped out).
+     *
+     *  If no clipping is needed, returns false and "result" is left unchanged.
+     */
+    static bool PerspectiveClip(const SkPath& src, const SkMatrix&, SkPath* result);
 };
 
 // Lightweight variant of SkPath::Iter that only returns segments (e.g. lines/conics).