Add conservative round rect intersect function
Change-Id: I1012a4b6c6eb67e01923f767baeb78ebc18a0fd5 Reviewed-on: https://skia-review.googlesource.com/c/skia/+/284477 Commit-Queue: Michael Ludwig <michaelludwig@google.com> Reviewed-by: Brian Osman <brianosman@google.com>
This commit is contained in:
parent
57ed6b8f12
commit
76312fbf97
@ -751,3 +751,97 @@ SkRect SkRRectPriv::InnerBounds(const SkRRect& rr) {
|
||||
SkASSERT(rr.contains(innerBounds));
|
||||
return innerBounds;
|
||||
}
|
||||
|
||||
SkRRect SkRRectPriv::ConservativeIntersect(const SkRRect& a, const SkRRect& b) {
|
||||
// Returns the coordinate of the rect matching the corner enum.
|
||||
auto getCorner = [](const SkRect& r, SkRRect::Corner corner) -> SkPoint {
|
||||
switch(corner) {
|
||||
case SkRRect::kUpperLeft_Corner: return {r.fLeft, r.fTop};
|
||||
case SkRRect::kUpperRight_Corner: return {r.fRight, r.fTop};
|
||||
case SkRRect::kLowerLeft_Corner: return {r.fLeft, r.fBottom};
|
||||
case SkRRect::kLowerRight_Corner: return {r.fRight, r.fBottom};
|
||||
default: SkUNREACHABLE;
|
||||
}
|
||||
};
|
||||
// Returns true if shape A's extreme point is contained within shape B's extreme point, relative
|
||||
// to the 'corner' location. If the two shapes' corners have the same ellipse radii, this
|
||||
// is sufficient for A's ellipse arc to be contained by B's ellipse arc.
|
||||
auto insideCorner = [](SkRRect::Corner corner, const SkPoint& a, const SkPoint& b) {
|
||||
switch(corner) {
|
||||
case SkRRect::kUpperLeft_Corner: return a.fX >= b.fX && a.fY >= b.fY;
|
||||
case SkRRect::kUpperRight_Corner: return a.fX <= b.fX && a.fY >= b.fY;
|
||||
case SkRRect::kLowerRight_Corner: return a.fX <= b.fX && a.fY <= b.fY;
|
||||
case SkRRect::kLowerLeft_Corner: return a.fX >= b.fX && a.fY <= b.fY;
|
||||
default: SkUNREACHABLE;
|
||||
}
|
||||
};
|
||||
|
||||
auto getIntersectionRadii = [&](const SkRect& r, SkRRect::Corner corner, SkVector* radii) {
|
||||
SkPoint test = getCorner(r, corner);
|
||||
SkPoint aCorner = getCorner(a.rect(), corner);
|
||||
SkPoint bCorner = getCorner(b.rect(), corner);
|
||||
|
||||
if (test == aCorner) {
|
||||
// Test that A's ellipse is contained by B. This is a non-trivial function to evaluate
|
||||
// so we resrict it to when the corners have the same radii. If not, we use the more
|
||||
// conservative test that the extreme point of A's bounding box is contained in B.
|
||||
*radii = a.radii(corner);
|
||||
if (*radii == b.radii(corner)) {
|
||||
return insideCorner(corner, aCorner, bCorner); // A inside B
|
||||
} else {
|
||||
return b.checkCornerContainment(aCorner.fX, aCorner.fY);
|
||||
}
|
||||
} else if (test == bCorner) {
|
||||
// Mirror of the above
|
||||
*radii = b.radii(corner);
|
||||
if (*radii == a.radii(corner)) {
|
||||
return insideCorner(corner, bCorner, aCorner); // B inside A
|
||||
} else {
|
||||
return a.checkCornerContainment(bCorner.fX, bCorner.fY);
|
||||
}
|
||||
} else {
|
||||
// This is a corner formed by two straight edges of A and B, so confirm that it is
|
||||
// contained in both (if not, then the intersection can't be a round rect).
|
||||
*radii = {0.f, 0.f};
|
||||
return a.checkCornerContainment(test.fX, test.fY) &&
|
||||
b.checkCornerContainment(test.fX, test.fY);
|
||||
}
|
||||
};
|
||||
|
||||
SkRect edges;
|
||||
if (!edges.intersect(a.rect(), b.rect())) {
|
||||
// Definitely no intersection
|
||||
return SkRRect::MakeEmpty();
|
||||
}
|
||||
|
||||
const SkRRect::Corner corners[] = {
|
||||
SkRRect::kUpperLeft_Corner,
|
||||
SkRRect::kUpperRight_Corner,
|
||||
SkRRect::kLowerRight_Corner,
|
||||
SkRRect::kLowerLeft_Corner
|
||||
};
|
||||
// By definition, edges is contained in the bounds of 'a' and 'b', but now we need to consider
|
||||
// the corners. If the bound's corner point is in both rrects, the corner radii will be 0s.
|
||||
// If the bound's corner point matches a's edges and is inside 'b', we use a's radii.
|
||||
// Same for b's radii. If any corner fails these conditions, we reject the intersection as an
|
||||
// rrect. If after determining radii for all 4 corners, they would overlap, we also reject the
|
||||
// intersection shape.
|
||||
SkVector radii[4];
|
||||
for (auto c : corners) {
|
||||
if (!getIntersectionRadii(edges, c, &radii[c])) {
|
||||
return SkRRect::MakeEmpty(); // Resulting intersection is not a rrect
|
||||
}
|
||||
}
|
||||
|
||||
// Check for radius overlap along the four edges, since the earlier evaluation was only a
|
||||
// one-sided corner check.
|
||||
if (!SkRRect::AreRectAndRadiiValid(edges, radii)) {
|
||||
return SkRRect::MakeEmpty();
|
||||
}
|
||||
|
||||
// The intersection is an rrect of the given radii. Potentially all 4 corners could have
|
||||
// been simplified to (0,0) radii, making the intersection a rectangle.
|
||||
SkRRect intersection;
|
||||
intersection.setRectRadii(edges, radii);
|
||||
return intersection;
|
||||
}
|
||||
|
@ -45,6 +45,14 @@ public:
|
||||
// not be the global maximum, but will be non-empty, touch at least one edge and be contained
|
||||
// in the round rect.
|
||||
static SkRect InnerBounds(const SkRRect& rr);
|
||||
|
||||
// Attempt to compute the intersection of two round rects. The intersection is not necessarily
|
||||
// a round rect. This returns intersections only when the shape is representable as a new
|
||||
// round rect (or rect). Empty is returned if 'a' and 'b' do not intersect or if the
|
||||
// intersection is too complicated. This is conservative, it may not always detect that an
|
||||
// intersection could be represented as a round rect. However, when it does return a round rect
|
||||
// that intersection will be exact (i.e. it is NOT just a subset of the actual intersection).
|
||||
static SkRRect ConservativeIntersect(const SkRRect& a, const SkRRect& b);
|
||||
};
|
||||
|
||||
#endif
|
||||
|
@ -7,6 +7,7 @@
|
||||
|
||||
#include "include/core/SkMatrix.h"
|
||||
#include "include/core/SkRRect.h"
|
||||
#include "include/pathops/SkPathOps.h"
|
||||
#include "include/utils/SkRandom.h"
|
||||
#include "src/core/SkPointPriv.h"
|
||||
#include "src/core/SkRRectPriv.h"
|
||||
@ -1102,6 +1103,144 @@ static void test_inner_bounds(skiatest::Reporter* reporter) {
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
// Helper to test expected intersection, relying on the fact that all round rect intersections
|
||||
// will have their bounds equal to the intersection of the bounds of the input round rects, and
|
||||
// their corner radii will be a one of A's, B's, or rectangular.
|
||||
enum CornerChoice : uint8_t {
|
||||
kA, kB, kRect
|
||||
};
|
||||
|
||||
static void verify_success(skiatest::Reporter* reporter, const SkRRect& a, const SkRRect& b,
|
||||
CornerChoice tl, CornerChoice tr, CornerChoice br, CornerChoice bl) {
|
||||
static const SkRRect kRect = SkRRect::MakeEmpty(); // has (0,0) for all corners
|
||||
|
||||
// Compute expected round rect intersection given bounds of A and B, and the specified
|
||||
// corner choices for the 4 corners.
|
||||
SkRect expectedBounds;
|
||||
SkAssertResult(expectedBounds.intersect(a.rect(), b.rect()));
|
||||
|
||||
SkVector radii[4] = {
|
||||
(tl == kA ? a : (tl == kB ? b : kRect)).radii(SkRRect::kUpperLeft_Corner),
|
||||
(tr == kA ? a : (tr == kB ? b : kRect)).radii(SkRRect::kUpperRight_Corner),
|
||||
(br == kA ? a : (br == kB ? b : kRect)).radii(SkRRect::kLowerRight_Corner),
|
||||
(bl == kA ? a : (bl == kB ? b : kRect)).radii(SkRRect::kLowerLeft_Corner)
|
||||
};
|
||||
SkRRect expected;
|
||||
expected.setRectRadii(expectedBounds, radii);
|
||||
|
||||
SkRRect actual = SkRRectPriv::ConservativeIntersect(a, b);
|
||||
// Intersections are commutative so ba and ab should be the same
|
||||
REPORTER_ASSERT(reporter, actual == SkRRectPriv::ConservativeIntersect(b, a));
|
||||
|
||||
// Intersection of the result with either A or B should remain the intersection
|
||||
REPORTER_ASSERT(reporter, actual == SkRRectPriv::ConservativeIntersect(actual, a));
|
||||
REPORTER_ASSERT(reporter, actual == SkRRectPriv::ConservativeIntersect(actual, b));
|
||||
|
||||
// Bounds of intersection round rect should equal intersection of bounds of a and b
|
||||
REPORTER_ASSERT(reporter, actual.rect() == expectedBounds);
|
||||
|
||||
// Use PathOps to confirm that the explicit round rect is correct.
|
||||
SkPath aPath, bPath, expectedPath;
|
||||
aPath.addRRect(a);
|
||||
bPath.addRRect(b);
|
||||
SkAssertResult(Op(aPath, bPath, kIntersect_SkPathOp, &expectedPath));
|
||||
|
||||
// The isRRect() heuristics in SkPath are based on having called addRRect(), so a path from
|
||||
// path ops that is a rounded rectangle will return false. However, if test XOR expected is
|
||||
// empty, then we know that the shapes were the same.
|
||||
SkPath testPath;
|
||||
testPath.addRRect(actual);
|
||||
|
||||
SkPath empty;
|
||||
SkAssertResult(Op(testPath, expectedPath, kXOR_SkPathOp, &empty));
|
||||
REPORTER_ASSERT(reporter, empty.isEmpty());
|
||||
}
|
||||
|
||||
static void verify_failure(skiatest::Reporter* reporter, const SkRRect& a, const SkRRect& b) {
|
||||
SkRRect intersection = SkRRectPriv::ConservativeIntersect(a, b);
|
||||
// Expected the intersection to fail (no intersection or complex intersection is not
|
||||
// disambiguated).
|
||||
REPORTER_ASSERT(reporter, intersection.isEmpty());
|
||||
REPORTER_ASSERT(reporter, SkRRectPriv::ConservativeIntersect(b, a).isEmpty());
|
||||
}
|
||||
} // anonymous
|
||||
|
||||
static void test_conservative_intersection(skiatest::Reporter* reporter) {
|
||||
// Helper to inline making an inset round rect
|
||||
auto make_inset = [](const SkRRect& r, float dx, float dy) {
|
||||
SkRRect i = r;
|
||||
i.inset(dx, dy);
|
||||
return i;
|
||||
};
|
||||
|
||||
// A is a wide, short round rect
|
||||
SkRRect a = SkRRect::MakeRectXY({0.f, 4.f, 16.f, 12.f}, 2.f, 2.f);
|
||||
// B is a narrow, tall round rect
|
||||
SkRRect b = SkRRect::MakeRectXY({4.f, 0.f, 12.f, 16.f}, 3.f, 3.f);
|
||||
// NOTE: As positioned by default, A and B intersect as the rectangle {4, 4, 12, 12}.
|
||||
// There is a 2 px buffer between the corner curves of A and the vertical edges of B, and
|
||||
// a 1 px buffer between the corner curves of B and the horizontal edges of A. Since the shapes
|
||||
// form a symmetric rounded cross, we can easily test edge and corner combinations by simply
|
||||
// flipping signs and/or swapping x and y offsets.
|
||||
|
||||
// Successful intersection operations:
|
||||
// - for clarity these are formed by moving A around to intersect with B in different ways.
|
||||
// - the expected bounds of the round rect intersection is calculated automatically
|
||||
// in check_success, so all we have to specify are the expected corner radii
|
||||
|
||||
// A and B intersect as a rectangle
|
||||
verify_success(reporter, a, b, kRect, kRect, kRect, kRect);
|
||||
// Move A to intersect B on a vertical edge, preserving two corners of A inside B
|
||||
verify_success(reporter, a.makeOffset(6.f, 0.f), b, kA, kRect, kRect, kA);
|
||||
verify_success(reporter, a.makeOffset(-6.f, 0.f), b, kRect, kA, kA, kRect);
|
||||
// Move B to intersect A on a horizontal edge, preserving two corners of B inside A
|
||||
verify_success(reporter, a, b.makeOffset(0.f, 6.f), kB, kB, kRect, kRect);
|
||||
verify_success(reporter, a, b.makeOffset(0.f, -6.f), kRect, kRect, kB, kB);
|
||||
// Move A to intersect B on a corner, preserving one corner of A and one of B
|
||||
verify_success(reporter, a.makeOffset(-7.f, -8.f), b, kB, kRect, kA, kRect); // TL of B
|
||||
verify_success(reporter, a.makeOffset(7.f, -8.f), b, kRect, kB, kRect, kA); // TR of B
|
||||
verify_success(reporter, a.makeOffset(7.f, 8.f), b, kA, kRect, kB, kRect); // BR of B
|
||||
verify_success(reporter, a.makeOffset(-7.f, 8.f), b, kRect, kA, kRect, kB); // BL of B
|
||||
// An inset is contained inside the original (note that SkRRect::inset modifies radii too) so
|
||||
// is returned unmodified when intersected.
|
||||
verify_success(reporter, a, make_inset(a, 1.f, 1.f), kB, kB, kB, kB);
|
||||
verify_success(reporter, make_inset(b, 2.f, 2.f), b, kA, kA, kA, kA);
|
||||
|
||||
// Failed intersection operations:
|
||||
|
||||
// A and B's bounds do not intersect
|
||||
verify_failure(reporter, a.makeOffset(32.f, 0.f), b);
|
||||
// A and B's bounds intersect, but corner curves do not -> no intersection
|
||||
verify_failure(reporter, a.makeOffset(11.5f, -11.5f), b);
|
||||
// A is empty -> no intersection
|
||||
verify_failure(reporter, SkRRect::MakeEmpty(), b);
|
||||
// A is contained in B, but is too close to the corner curves for the conservative
|
||||
// approximations to construct a valid round rect intersection.
|
||||
verify_failure(reporter, make_inset(b, 0.3f, 0.3f), b);
|
||||
// A intersects a straight edge, but not far enough for B to contain A's corners
|
||||
verify_failure(reporter, a.makeOffset(2.5f, 0.f), b);
|
||||
verify_failure(reporter, a.makeOffset(-2.5f, 0.f), b);
|
||||
// And vice versa for B into A
|
||||
verify_failure(reporter, a, b.makeOffset(0.f, 1.5f));
|
||||
verify_failure(reporter, a, b.makeOffset(0.f, -1.5f));
|
||||
// A intersects a straight edge and part of B's corner
|
||||
verify_failure(reporter, a.makeOffset(5.f, -2.f), b);
|
||||
verify_failure(reporter, a.makeOffset(-5.f, -2.f), b);
|
||||
verify_failure(reporter, a.makeOffset(5.f, 2.f), b);
|
||||
verify_failure(reporter, a.makeOffset(-5.f, 2.f), b);
|
||||
// And vice versa
|
||||
verify_failure(reporter, a, b.makeOffset(3.f, -5.f));
|
||||
verify_failure(reporter, a, b.makeOffset(-3.f, -5.f));
|
||||
verify_failure(reporter, a, b.makeOffset(3.f, 5.f));
|
||||
verify_failure(reporter, a, b.makeOffset(-3.f, 5.f));
|
||||
// A intersects B on a corner, but the corner curves overlap each other
|
||||
verify_failure(reporter, a.makeOffset(8.f, 10.f), b);
|
||||
verify_failure(reporter, a.makeOffset(-8.f, 10.f), b);
|
||||
verify_failure(reporter, a.makeOffset(8.f, -10.f), b);
|
||||
verify_failure(reporter, a.makeOffset(-8.f, -10.f), b);
|
||||
}
|
||||
|
||||
DEF_TEST(RoundRect, reporter) {
|
||||
test_round_rect_basic(reporter);
|
||||
test_round_rect_rects(reporter);
|
||||
@ -1117,4 +1256,5 @@ DEF_TEST(RoundRect, reporter) {
|
||||
test_empty(reporter);
|
||||
test_read(reporter);
|
||||
test_inner_bounds(reporter);
|
||||
test_conservative_intersection(reporter);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user