Add a conservativelyContainsRect function to SkPath.
authorbsalomon@google.com <bsalomon@google.com@2bbb7eff-a529-9590-31e7-b0007b416f81>
Tue, 13 Nov 2012 21:51:38 +0000 (21:51 +0000)
committerbsalomon@google.com <bsalomon@google.com@2bbb7eff-a529-9590-31e7-b0007b416f81>
Tue, 13 Nov 2012 21:51:38 +0000 (21:51 +0000)
Review URL: https://codereview.appspot.com/6852044

git-svn-id: http://skia.googlecode.com/svn/trunk@6411 2bbb7eff-a529-9590-31e7-b0007b416f81

bench/PathBench.cpp
include/core/SkPath.h
src/core/SkPath.cpp
tests/PathTest.cpp

index 6e522ec..390c532 100644 (file)
@@ -779,6 +779,90 @@ private:
     typedef SkBenchmark INHERITED;
 };
 
+class ConservativelyContainsBench : public SkBenchmark {
+public:
+    enum Type {
+        kRect_Type,
+        kRoundRect_Type,
+        kOval_Type,
+    };
+
+    ConservativelyContainsBench(void* param, Type type) : INHERITED(param) {
+        fIsRendering = false;
+        fParity = false;
+        fName = "conservatively_contains_";
+        switch (type) {
+            case kRect_Type:
+                fName.append("rect");
+                fPath.addRect(kBaseRect);
+                break;
+            case kRoundRect_Type:
+                fName.append("round_rect");
+                fPath.addRoundRect(kBaseRect, kRRRadii[0], kRRRadii[1]);
+                break;
+            case kOval_Type:
+                fName.append("oval");
+                fPath.addOval(kBaseRect);
+                break;
+        }
+    }
+
+private:
+    virtual const char* onGetName() SK_OVERRIDE {
+        return fName.c_str();
+    }
+
+    virtual void onDraw(SkCanvas* canvas) SK_OVERRIDE {
+        for (int i = 0; i < N; ++i) {
+            const SkRect& rect = fQueryRects[i % kQueryRectCnt];
+            fParity = fParity != fPath.conservativelyContainsRect(rect);
+        }
+    }
+
+    virtual void onPreDraw() SK_OVERRIDE {
+        fQueryRects.setCount(kQueryRectCnt);
+
+        SkRandom rand;
+        for (int i = 0; i < kQueryRectCnt; ++i) {
+            SkSize size;
+            SkPoint xy;
+            size.fWidth = rand.nextRangeScalar(kQueryMin.fWidth,  kQueryMax.fWidth);
+            size.fHeight = rand.nextRangeScalar(kQueryMin.fHeight, kQueryMax.fHeight);
+            xy.fX = rand.nextRangeScalar(kBounds.fLeft, kBounds.fRight - size.fWidth);
+            xy.fY = rand.nextRangeScalar(kBounds.fTop, kBounds.fBottom - size.fHeight);
+
+            fQueryRects[i] = SkRect::MakeXYWH(xy.fX, xy.fY, size.fWidth, size.fHeight);
+        }
+    }
+
+    virtual void onPostDraw() SK_OVERRIDE {
+        fQueryRects.setCount(0);
+    }
+
+    enum {
+        N = SkBENCHLOOP(100000),
+        kQueryRectCnt = 400,
+    };
+    static const SkRect kBounds;   // bounds for all random query rects
+    static const SkSize kQueryMin; // minimum query rect size, should be <= kQueryMax
+    static const SkSize kQueryMax; // max query rect size, should < kBounds
+    static const SkRect kBaseRect; // rect that is used to construct the path
+    static const SkScalar kRRRadii[2]; // x and y radii for round rect
+
+    SkString            fName;
+    SkPath              fPath;
+    bool                fParity;
+    SkTDArray<SkRect>   fQueryRects;
+
+    typedef SkBenchmark INHERITED;
+};
+
+const SkRect ConservativelyContainsBench::kBounds = SkRect::MakeWH(SkIntToScalar(100), SkIntToScalar(100));
+const SkSize ConservativelyContainsBench::kQueryMin = SkSize::Make(SkIntToScalar(1), SkIntToScalar(1));
+const SkSize ConservativelyContainsBench::kQueryMax = SkSize::Make(SkIntToScalar(40), SkIntToScalar(40));
+const SkRect ConservativelyContainsBench::kBaseRect = SkRect::MakeXYWH(SkIntToScalar(25), SkIntToScalar(25), SkIntToScalar(50), SkIntToScalar(50));
+const SkScalar ConservativelyContainsBench::kRRRadii[2] = {SkIntToScalar(5), SkIntToScalar(10)};
+
 static SkBenchmark* FactT00(void* p) { return new TrianglePathBench(p, FLAGS00); }
 static SkBenchmark* FactT01(void* p) { return new TrianglePathBench(p, FLAGS01); }
 static SkBenchmark* FactT10(void* p) { return new TrianglePathBench(p, FLAGS10); }
@@ -883,3 +967,12 @@ static BenchRegistry gRegArbRoundRectTest(ArbRoundRectTest);
 
 static SkBenchmark* ZeroRadRoundRectTest(void* p) { return new ArbRoundRectBench(p, true); }
 static BenchRegistry gRegZeroRadRoundRectTest(ZeroRadRoundRectTest);
+
+static SkBenchmark* RectConservativelyContainsTest(void* p) { return new ConservativelyContainsBench(p, ConservativelyContainsBench::kRect_Type); }
+static BenchRegistry gRegRectConservativelyContainsTest(RectConservativelyContainsTest);
+
+static SkBenchmark* RoundRectConservativelyContainsTest(void* p) { return new ConservativelyContainsBench(p, ConservativelyContainsBench::kRoundRect_Type); }
+static BenchRegistry gRegRoundRectConservativelyContainsTest(RoundRectConservativelyContainsTest);
+
+static SkBenchmark* OvalConservativelyContainsTest(void* p) { return new ConservativelyContainsBench(p, ConservativelyContainsBench::kOval_Type); }
+static BenchRegistry gRegOvalConservativelyContainsTest(OvalConservativelyContainsTest);
index ee02c65..20d041d 100644 (file)
@@ -292,7 +292,7 @@ public:
     }
 
     /** Calling this will, if the internal cache of the bounds is out of date,
-        update it so that subsequent calls to getBounds will be instanteous.
+        update it so that subsequent calls to getBounds will be instantaneous.
         This also means that any copies or simple transformations of the path
         will inherit the cached bounds.
      */
@@ -301,6 +301,14 @@ public:
         this->getBounds();
     }
 
+    /**
+     * Does a conservative test to see whether a rectangle is inside a path. Currently it only
+     * will ever return true for single convex contour paths. The empty-status of the rect is not
+     * considered (e.g. a rect that is a point can be inside a path). Points or line segments where
+     * the rect edge touches the path border are not considered containment violations.
+     */
+    bool conservativelyContainsRect(const SkRect& rect) const;
+
     //  Construction methods
 
     /** Hint to the path to prepare for adding more points. This can allow the
index d1b90dd..b7c2162 100644 (file)
@@ -311,6 +311,86 @@ void SkPath::swap(SkPath& other) {
     }
 }
 
+static inline bool check_edge_against_rect(const SkPoint& p0,
+                                           const SkPoint& p1,
+                                           const SkRect& rect,
+                                           SkPath::Direction dir) {
+    const SkPoint* edgeBegin;
+    SkVector v;
+    if (SkPath::kCW_Direction == dir) {
+        v = p1 - p0;
+        edgeBegin = &p0;
+    } else {
+        v = p0 - p1;
+        edgeBegin = &p1;
+    }
+    if (v.fX || v.fY) {
+        // check the cross product of v with the vec from edgeBegin to each rect corner
+        SkScalar yL = SkScalarMul(v.fY, rect.fLeft - edgeBegin->fX);
+        SkScalar xT = SkScalarMul(v.fX, rect.fTop - edgeBegin->fY);
+        SkScalar yR = SkScalarMul(v.fY, rect.fRight - edgeBegin->fX);
+        SkScalar xB = SkScalarMul(v.fX, rect.fBottom - edgeBegin->fY);
+        if ((xT < yL) || (xT < yR) || (xB < yL) || (xB < yR)) {
+            return false;
+        }
+    }
+    return true;
+}
+
+bool SkPath::conservativelyContainsRect(const SkRect& rect) const {
+    // This only handles non-degenerate convex paths currently.
+    if (kConvex_Convexity != this->getConvexity()) {
+        return false;
+    }
+    
+    Direction direction;
+    if (!this->cheapComputeDirection(&direction)) {
+        return false;
+    }
+
+    SkPoint firstPt;
+    SkPoint prevPt;
+    RawIter iter(*this);
+    SkPath::Verb verb;
+    SkPoint pts[4];
+    SkDEBUGCODE(int moveCnt = 0;)
+
+    while ((verb = iter.next(pts)) != kDone_Verb) {
+        int nextPt = -1;
+        switch (verb) {
+            case kMove_Verb:
+                SkASSERT(!moveCnt);
+                SkDEBUGCODE(++moveCnt);
+                firstPt = prevPt = pts[0];
+                break;
+            case kLine_Verb:
+                nextPt = 1;
+                SkASSERT(moveCnt);
+                break;
+            case kQuad_Verb:
+                SkASSERT(moveCnt);
+                nextPt = 2;
+                break;
+            case kCubic_Verb:
+                SkASSERT(moveCnt);
+                nextPt = 3;
+                break;
+            case kClose_Verb:
+                break;
+            default:
+                SkDEBUGFAIL("unknown verb");
+        }
+        if (-1 != nextPt) {
+            if (!check_edge_against_rect(prevPt, pts[nextPt], rect, direction)) {
+                return false;
+            }
+            prevPt = pts[nextPt];
+        }
+    }
+
+    return check_edge_against_rect(prevPt, firstPt, rect, direction);
+}
+
 #ifdef SK_BUILD_FOR_ANDROID
 uint32_t SkPath::getGenerationID() const {
     return fGenerationID;
index 627ed18..a48fc9f 100644 (file)
@@ -822,6 +822,162 @@ static void test_isLine(skiatest::Reporter* reporter) {
     REPORTER_ASSERT(reporter, pts[1].equals(lineX, lineY));
 }
 
+static void test_conservativelyContains(skiatest::Reporter* reporter) {
+    SkPath path;
+
+    // kBaseRect is used to construct most our test paths: a rect, a circle, and a round-rect.
+    static const SkRect kBaseRect = SkRect::MakeWH(SkIntToScalar(100), SkIntToScalar(100));
+
+    // A circle that bounds kBaseRect (with a significant amount of slop)
+    SkScalar circleR = SkMaxScalar(kBaseRect.width(), kBaseRect.height());
+    circleR = SkScalarMul(circleR, SkFloatToScalar(1.75f)) / 2;
+    static const SkPoint kCircleC = {kBaseRect.centerX(), kBaseRect.centerY()};
+
+    // round-rect radii
+    static const SkScalar kRRRadii[] = {SkIntToScalar(5), SkIntToScalar(3)};
+    
+    static const struct {
+        SkRect fQueryRect;
+        bool   fInRect;
+        bool   fInCircle;
+        bool   fInRR;
+    } kQueries[] = {
+        {kBaseRect, true, true, false},
+
+        // rect well inside of kBaseRect
+        {SkRect::MakeLTRB(kBaseRect.fLeft + SkFloatToScalar(0.25f)*kBaseRect.width(),
+                          kBaseRect.fTop + SkFloatToScalar(0.25f)*kBaseRect.height(),
+                          kBaseRect.fRight - SkFloatToScalar(0.25f)*kBaseRect.width(),
+                          kBaseRect.fBottom - SkFloatToScalar(0.25f)*kBaseRect.height()),
+                          true, true, true},
+
+        // rects with edges off by one from kBaseRect's edges
+        {SkRect::MakeXYWH(kBaseRect.fLeft, kBaseRect.fTop,
+                          kBaseRect.width(), kBaseRect.height() + 1),
+         false, true, false},
+        {SkRect::MakeXYWH(kBaseRect.fLeft, kBaseRect.fTop,
+                          kBaseRect.width() + 1, kBaseRect.height()),
+         false, true, false},
+        {SkRect::MakeXYWH(kBaseRect.fLeft, kBaseRect.fTop,
+                          kBaseRect.width() + 1, kBaseRect.height() + 1),
+         false, true, false},
+        {SkRect::MakeXYWH(kBaseRect.fLeft - 1, kBaseRect.fTop,
+                          kBaseRect.width(), kBaseRect.height()),
+         false, true, false},
+        {SkRect::MakeXYWH(kBaseRect.fLeft, kBaseRect.fTop - 1,
+                          kBaseRect.width(), kBaseRect.height()),
+         false, true, false},
+        {SkRect::MakeXYWH(kBaseRect.fLeft - 1, kBaseRect.fTop,
+                          kBaseRect.width() + 2, kBaseRect.height()),
+         false, true, false},
+        {SkRect::MakeXYWH(kBaseRect.fLeft, kBaseRect.fTop - 1,
+                          kBaseRect.width() + 2, kBaseRect.height()),
+         false, true, false},
+
+        // zero-w/h rects at each corner of kBaseRect
+        {SkRect::MakeXYWH(kBaseRect.fLeft, kBaseRect.fTop, 0, 0), true, true, false},
+        {SkRect::MakeXYWH(kBaseRect.fRight, kBaseRect.fTop, 0, 0), true, true, false},
+        {SkRect::MakeXYWH(kBaseRect.fLeft, kBaseRect.fBottom, 0, 0), true, true, false},
+        {SkRect::MakeXYWH(kBaseRect.fRight, kBaseRect.fBottom, 0, 0), true, true, false},
+
+        // far away rect
+        {SkRect::MakeXYWH(10 * kBaseRect.fRight, 10 * kBaseRect.fBottom,
+                          SkIntToScalar(10), SkIntToScalar(10)),
+         false, false, false},
+
+        // very large rect containing kBaseRect
+        {SkRect::MakeXYWH(kBaseRect.fLeft - 5 * kBaseRect.width(),
+                          kBaseRect.fTop - 5 * kBaseRect.height(),
+                          11 * kBaseRect.width(), 11 * kBaseRect.height()),
+         false, false, false},
+
+        // skinny rect that spans same y-range as kBaseRect
+        {SkRect::MakeXYWH(kBaseRect.centerX(), kBaseRect.fTop,
+                          SkIntToScalar(1), kBaseRect.height()),
+         true, true, true},
+
+        // short rect that spans same x-range as kBaseRect
+        {SkRect::MakeXYWH(kBaseRect.fLeft, kBaseRect.centerY(), kBaseRect.width(), SkScalar(1)),
+         true, true, true},
+
+        // skinny rect that spans slightly larger y-range than kBaseRect
+        {SkRect::MakeXYWH(kBaseRect.centerX(), kBaseRect.fTop,
+                          SkIntToScalar(1), kBaseRect.height() + 1),
+         false, true, false},
+
+        // short rect that spans slightly larger x-range than kBaseRect
+        {SkRect::MakeXYWH(kBaseRect.fLeft, kBaseRect.centerY(),
+                          kBaseRect.width() + 1, SkScalar(1)),
+         false, true, false},
+    };
+
+    for (int inv = 0; inv < 4; ++inv) {
+        for (int q = 0; q < SK_ARRAY_COUNT(kQueries); ++q) {
+            SkRect qRect = kQueries[q].fQueryRect;
+            if (inv & 0x1) {
+                SkTSwap(qRect.fLeft, qRect.fRight);
+            }
+            if (inv & 0x2) {
+                SkTSwap(qRect.fTop, qRect.fBottom);
+            }
+            for (int d = 0; d < 2; ++d) {
+                SkPath::Direction dir = d ? SkPath::kCCW_Direction : SkPath::kCW_Direction;
+                path.reset();
+                path.addRect(kBaseRect, dir);
+                REPORTER_ASSERT(reporter, kQueries[q].fInRect ==
+                                          path.conservativelyContainsRect(qRect));
+
+                path.reset();
+                path.addCircle(kCircleC.fX, kCircleC.fY, circleR, dir);
+                REPORTER_ASSERT(reporter, kQueries[q].fInCircle ==
+                                          path.conservativelyContainsRect(qRect));
+
+                path.reset();
+                path.addRoundRect(kBaseRect, kRRRadii[0], kRRRadii[1], dir);
+                REPORTER_ASSERT(reporter, kQueries[q].fInRR ==
+                                          path.conservativelyContainsRect(qRect));
+            }
+            // Slightly non-convex shape, shouldn't contain any rects.
+            path.reset();
+            path.moveTo(0, 0);
+            path.lineTo(SkIntToScalar(50), SkFloatToScalar(0.05f));
+            path.lineTo(SkIntToScalar(100), 0);
+            path.lineTo(SkIntToScalar(100), SkIntToScalar(100));
+            path.lineTo(0, SkIntToScalar(100));
+            path.close();
+            REPORTER_ASSERT(reporter, !path.conservativelyContainsRect(qRect));
+        }
+    }
+
+    // make sure a minimal convex shape works, a right tri with edges along pos x and y axes.
+    path.reset();
+    path.moveTo(0, 0);
+    path.lineTo(SkIntToScalar(100), 0);
+    path.lineTo(0, SkIntToScalar(100));
+
+    // inside, on along top edge
+    REPORTER_ASSERT(reporter, path.conservativelyContainsRect(SkRect::MakeXYWH(SkIntToScalar(50), 0,
+                                                                               SkIntToScalar(10),
+                                                                               SkIntToScalar(10))));
+    // above
+    REPORTER_ASSERT(reporter, !path.conservativelyContainsRect(
+        SkRect::MakeXYWH(SkIntToScalar(50),
+                         SkIntToScalar(-10),
+                         SkIntToScalar(10),
+                         SkIntToScalar(10))));
+    // to the left
+    REPORTER_ASSERT(reporter, !path.conservativelyContainsRect(SkRect::MakeXYWH(SkIntToScalar(-10),
+                                                                                SkIntToScalar(5),
+                                                                                SkIntToScalar(5),
+                                                                                SkIntToScalar(5))));
+
+    // outside the diagonal edge
+    REPORTER_ASSERT(reporter, !path.conservativelyContainsRect(SkRect::MakeXYWH(SkIntToScalar(10),
+                                                                                SkIntToScalar(200),
+                                                                                SkIntToScalar(20),
+                                                                                SkIntToScalar(5))));
+}
+
 // Simple isRect test is inline TestPath, below.
 // test_isRect provides more extensive testing.
 static void test_isRect(skiatest::Reporter* reporter) {
@@ -1810,6 +1966,7 @@ static void TestPath(skiatest::Reporter* reporter) {
     test_direction(reporter);
     test_convexity(reporter);
     test_convexity2(reporter);
+    test_conservativelyContains(reporter);
     test_close(reporter);
     test_segment_masks(reporter);
     test_flattening(reporter);