blob: 19f8e271127cee45c02a278feb88ab80d412e228 [file] [log] [blame]
/*
* Copyright 2012 Google Inc.
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include "include/core/SkMatrix.h"
#include "include/core/SkPath.h"
#include "include/core/SkPoint.h"
#include "include/core/SkRRect.h"
#include "include/core/SkRect.h"
#include "include/core/SkScalar.h"
#include "include/core/SkTypes.h"
#include "include/pathops/SkPathOps.h"
#include "include/utils/SkRandom.h"
#include "src/core/SkPointPriv.h"
#include "src/core/SkRRectPriv.h"
#include "tests/Test.h"
#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdint>
static void test_tricky_radii(skiatest::Reporter* reporter) {
{
// crbug.com/458522
SkRRect rr;
const SkRect bounds = { 3709, 3709, 3709 + 7402, 3709 + 29825 };
const SkScalar rad = 12814;
const SkVector vec[] = { { rad, rad }, { 0, rad }, { rad, rad }, { 0, rad } };
rr.setRectRadii(bounds, vec);
}
{
// crbug.com//463920
SkRect r = SkRect::MakeLTRB(0, 0, 1009, 33554432.0);
SkVector radii[4] = {
{ 13.0f, 8.0f }, { 170.0f, 2.0 }, { 256.0f, 33554432.0 }, { 110.0f, 5.0f }
};
SkRRect rr;
rr.setRectRadii(r, radii);
REPORTER_ASSERT(reporter, (double) rr.radii(SkRRect::kUpperRight_Corner).fY +
(double) rr.radii(SkRRect::kLowerRight_Corner).fY <=
rr.height());
}
}
static void test_empty_crbug_458524(skiatest::Reporter* reporter) {
SkRRect rr;
const SkRect bounds = { 3709, 3709, 3709 + 7402, 3709 + 29825 };
const SkScalar rad = 40;
rr.setRectXY(bounds, rad, rad);
SkRRect other;
SkMatrix matrix;
matrix.setScale(0, 1);
rr.transform(matrix, &other);
REPORTER_ASSERT(reporter, SkRRect::kEmpty_Type == other.getType());
}
// Test that all the SkRRect entry points correctly handle un-sorted and
// zero-sized input rects
static void test_empty(skiatest::Reporter* reporter) {
static const SkRect oooRects[] = { // out of order
{ 100, 0, 0, 100 }, // ooo horizontal
{ 0, 100, 100, 0 }, // ooo vertical
{ 100, 100, 0, 0 }, // ooo both
};
static const SkRect emptyRects[] = {
{ 100, 100, 100, 200 }, // empty horizontal
{ 100, 100, 200, 100 }, // empty vertical
{ 100, 100, 100, 100 }, // empty both
{ 0, 0, 0, 0 } // setEmpty-empty
};
static const SkVector radii[4] = { { 0, 1 }, { 2, 3 }, { 4, 5 }, { 6, 7 } };
SkRRect r;
for (size_t i = 0; i < std::size(oooRects); ++i) {
r.setRect(oooRects[i]);
REPORTER_ASSERT(reporter, !r.isEmpty());
REPORTER_ASSERT(reporter, r.rect() == oooRects[i].makeSorted());
r.setOval(oooRects[i]);
REPORTER_ASSERT(reporter, !r.isEmpty());
REPORTER_ASSERT(reporter, r.rect() == oooRects[i].makeSorted());
r.setRectXY(oooRects[i], 1, 2);
REPORTER_ASSERT(reporter, !r.isEmpty());
REPORTER_ASSERT(reporter, r.rect() == oooRects[i].makeSorted());
r.setNinePatch(oooRects[i], 0, 1, 2, 3);
REPORTER_ASSERT(reporter, !r.isEmpty());
REPORTER_ASSERT(reporter, r.rect() == oooRects[i].makeSorted());
r.setRectRadii(oooRects[i], radii);
REPORTER_ASSERT(reporter, !r.isEmpty());
REPORTER_ASSERT(reporter, r.rect() == oooRects[i].makeSorted());
}
for (size_t i = 0; i < std::size(emptyRects); ++i) {
r.setRect(emptyRects[i]);
REPORTER_ASSERT(reporter, r.isEmpty());
REPORTER_ASSERT(reporter, r.rect() == emptyRects[i]);
r.setOval(emptyRects[i]);
REPORTER_ASSERT(reporter, r.isEmpty());
REPORTER_ASSERT(reporter, r.rect() == emptyRects[i]);
r.setRectXY(emptyRects[i], 1, 2);
REPORTER_ASSERT(reporter, r.isEmpty());
REPORTER_ASSERT(reporter, r.rect() == emptyRects[i]);
r.setNinePatch(emptyRects[i], 0, 1, 2, 3);
REPORTER_ASSERT(reporter, r.isEmpty());
REPORTER_ASSERT(reporter, r.rect() == emptyRects[i]);
r.setRectRadii(emptyRects[i], radii);
REPORTER_ASSERT(reporter, r.isEmpty());
REPORTER_ASSERT(reporter, r.rect() == emptyRects[i]);
}
r.setRect({SK_ScalarNaN, 10, 10, 20});
REPORTER_ASSERT(reporter, r == SkRRect::MakeEmpty());
r.setRect({0, 10, 10, SK_ScalarInfinity});
REPORTER_ASSERT(reporter, r == SkRRect::MakeEmpty());
}
static const SkScalar kWidth = 100.0f;
static const SkScalar kHeight = 100.0f;
static void test_inset(skiatest::Reporter* reporter) {
SkRRect rr, rr2;
SkRect r = { 0, 0, 100, 100 };
rr.setRect(r);
rr.inset(-20, -20, &rr2);
REPORTER_ASSERT(reporter, rr2.isRect());
rr.inset(20, 20, &rr2);
REPORTER_ASSERT(reporter, rr2.isRect());
rr.inset(r.width()/2, r.height()/2, &rr2);
REPORTER_ASSERT(reporter, rr2.isEmpty());
rr.setRectXY(r, 20, 20);
rr.inset(19, 19, &rr2);
REPORTER_ASSERT(reporter, rr2.isSimple());
rr.inset(20, 20, &rr2);
REPORTER_ASSERT(reporter, rr2.isRect());
}
static void test_9patch_rrect(skiatest::Reporter* reporter,
const SkRect& rect,
SkScalar l, SkScalar t, SkScalar r, SkScalar b,
bool checkRadii) {
SkRRect rr;
rr.setNinePatch(rect, l, t, r, b);
REPORTER_ASSERT(reporter, SkRRect::kNinePatch_Type == rr.type());
REPORTER_ASSERT(reporter, rr.rect() == rect);
if (checkRadii) {
// This test doesn't hold if the radii will be rescaled by SkRRect
SkRect ninePatchRadii = { l, t, r, b };
SkPoint rquad[4];
ninePatchRadii.toQuad(rquad);
for (int i = 0; i < 4; ++i) {
REPORTER_ASSERT(reporter, rquad[i] == rr.radii((SkRRect::Corner) i));
}
}
SkRRect rr2; // construct the same RR using the most general set function
SkVector radii[4] = { { l, t }, { r, t }, { r, b }, { l, b } };
rr2.setRectRadii(rect, radii);
REPORTER_ASSERT(reporter, rr2 == rr && rr2.getType() == rr.getType());
}
// Test out the basic API entry points
static void test_round_rect_basic(skiatest::Reporter* reporter) {
// Test out initialization methods
SkPoint zeroPt = { 0, 0 };
SkRRect empty;
empty.setEmpty();
REPORTER_ASSERT(reporter, SkRRect::kEmpty_Type == empty.type());
REPORTER_ASSERT(reporter, empty.rect().isEmpty());
for (int i = 0; i < 4; ++i) {
REPORTER_ASSERT(reporter, zeroPt == empty.radii((SkRRect::Corner) i));
}
//----
SkRect rect = SkRect::MakeLTRB(0, 0, kWidth, kHeight);
SkRRect rr1;
rr1.setRect(rect);
REPORTER_ASSERT(reporter, SkRRect::kRect_Type == rr1.type());
REPORTER_ASSERT(reporter, rr1.rect() == rect);
for (int i = 0; i < 4; ++i) {
REPORTER_ASSERT(reporter, zeroPt == rr1.radii((SkRRect::Corner) i));
}
SkRRect rr1_2; // construct the same RR using the most general set function
SkVector rr1_2_radii[4] = { { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 } };
rr1_2.setRectRadii(rect, rr1_2_radii);
REPORTER_ASSERT(reporter, rr1_2 == rr1 && rr1_2.getType() == rr1.getType());
SkRRect rr1_3; // construct the same RR using the nine patch set function
rr1_3.setNinePatch(rect, 0, 0, 0, 0);
REPORTER_ASSERT(reporter, rr1_3 == rr1 && rr1_3.getType() == rr1.getType());
//----
SkPoint halfPoint = { SkScalarHalf(kWidth), SkScalarHalf(kHeight) };
SkRRect rr2;
rr2.setOval(rect);
REPORTER_ASSERT(reporter, SkRRect::kOval_Type == rr2.type());
REPORTER_ASSERT(reporter, rr2.rect() == rect);
for (int i = 0; i < 4; ++i) {
REPORTER_ASSERT(reporter,
SkPointPriv::EqualsWithinTolerance(rr2.radii((SkRRect::Corner) i),
halfPoint));
}
SkRRect rr2_2; // construct the same RR using the most general set function
SkVector rr2_2_radii[4] = { { halfPoint.fX, halfPoint.fY }, { halfPoint.fX, halfPoint.fY },
{ halfPoint.fX, halfPoint.fY }, { halfPoint.fX, halfPoint.fY } };
rr2_2.setRectRadii(rect, rr2_2_radii);
REPORTER_ASSERT(reporter, rr2_2 == rr2 && rr2_2.getType() == rr2.getType());
SkRRect rr2_3; // construct the same RR using the nine patch set function
rr2_3.setNinePatch(rect, halfPoint.fX, halfPoint.fY, halfPoint.fX, halfPoint.fY);
REPORTER_ASSERT(reporter, rr2_3 == rr2 && rr2_3.getType() == rr2.getType());
//----
SkPoint p = { 5, 5 };
SkRRect rr3;
rr3.setRectXY(rect, p.fX, p.fY);
REPORTER_ASSERT(reporter, SkRRect::kSimple_Type == rr3.type());
REPORTER_ASSERT(reporter, rr3.rect() == rect);
for (int i = 0; i < 4; ++i) {
REPORTER_ASSERT(reporter, p == rr3.radii((SkRRect::Corner) i));
}
SkRRect rr3_2; // construct the same RR using the most general set function
SkVector rr3_2_radii[4] = { { 5, 5 }, { 5, 5 }, { 5, 5 }, { 5, 5 } };
rr3_2.setRectRadii(rect, rr3_2_radii);
REPORTER_ASSERT(reporter, rr3_2 == rr3 && rr3_2.getType() == rr3.getType());
SkRRect rr3_3; // construct the same RR using the nine patch set function
rr3_3.setNinePatch(rect, 5, 5, 5, 5);
REPORTER_ASSERT(reporter, rr3_3 == rr3 && rr3_3.getType() == rr3.getType());
//----
test_9patch_rrect(reporter, rect, 10, 9, 8, 7, true);
{
// Test out the rrect from skia:3466
SkRect rect2 = SkRect::MakeLTRB(0.358211994f, 0.755430222f, 0.872866154f, 0.806214333f);
test_9patch_rrect(reporter,
rect2,
0.926942348f, 0.642850280f, 0.529063463f, 0.587844372f,
false);
}
//----
SkPoint radii2[4] = { { 0, 0 }, { 0, 0 }, { 50, 50 }, { 20, 50 } };
SkRRect rr5;
rr5.setRectRadii(rect, radii2);
REPORTER_ASSERT(reporter, SkRRect::kComplex_Type == rr5.type());
REPORTER_ASSERT(reporter, rr5.rect() == rect);
for (int i = 0; i < 4; ++i) {
REPORTER_ASSERT(reporter, radii2[i] == rr5.radii((SkRRect::Corner) i));
}
// Test out == & !=
REPORTER_ASSERT(reporter, empty != rr3);
REPORTER_ASSERT(reporter, rr3 != rr5);
}
// Test out the cases when the RR degenerates to a rect
static void test_round_rect_rects(skiatest::Reporter* reporter) {
SkRect r;
//----
SkRRect empty;
empty.setEmpty();
REPORTER_ASSERT(reporter, SkRRect::kEmpty_Type == empty.type());
r = empty.rect();
REPORTER_ASSERT(reporter, 0 == r.fLeft && 0 == r.fTop && 0 == r.fRight && 0 == r.fBottom);
//----
SkRect rect = SkRect::MakeLTRB(0, 0, kWidth, kHeight);
SkRRect rr1;
rr1.setRectXY(rect, 0, 0);
REPORTER_ASSERT(reporter, SkRRect::kRect_Type == rr1.type());
r = rr1.rect();
REPORTER_ASSERT(reporter, rect == r);
//----
SkPoint radii[4] = { { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 } };
SkRRect rr2;
rr2.setRectRadii(rect, radii);
REPORTER_ASSERT(reporter, SkRRect::kRect_Type == rr2.type());
r = rr2.rect();
REPORTER_ASSERT(reporter, rect == r);
//----
SkPoint radii2[4] = { { 0, 0 }, { 20, 20 }, { 50, 50 }, { 20, 50 } };
SkRRect rr3;
rr3.setRectRadii(rect, radii2);
REPORTER_ASSERT(reporter, SkRRect::kComplex_Type == rr3.type());
}
// Test out the cases when the RR degenerates to an oval
static void test_round_rect_ovals(skiatest::Reporter* reporter) {
//----
SkRect oval;
SkRect rect = SkRect::MakeLTRB(0, 0, kWidth, kHeight);
SkRRect rr1;
rr1.setRectXY(rect, SkScalarHalf(kWidth), SkScalarHalf(kHeight));
REPORTER_ASSERT(reporter, SkRRect::kOval_Type == rr1.type());
oval = rr1.rect();
REPORTER_ASSERT(reporter, oval == rect);
}
// Test out the non-degenerate RR cases
static void test_round_rect_general(skiatest::Reporter* reporter) {
//----
SkRect rect = SkRect::MakeLTRB(0, 0, kWidth, kHeight);
SkRRect rr1;
rr1.setRectXY(rect, 20, 20);
REPORTER_ASSERT(reporter, SkRRect::kSimple_Type == rr1.type());
//----
SkPoint radii[4] = { { 0, 0 }, { 20, 20 }, { 50, 50 }, { 20, 50 } };
SkRRect rr2;
rr2.setRectRadii(rect, radii);
REPORTER_ASSERT(reporter, SkRRect::kComplex_Type == rr2.type());
}
// Test out questionable-parameter handling
static void test_round_rect_iffy_parameters(skiatest::Reporter* reporter) {
// When the radii exceed the base rect they are proportionally scaled down
// to fit
SkRect rect = SkRect::MakeLTRB(0, 0, kWidth, kHeight);
SkPoint radii[4] = { { 50, 100 }, { 100, 50 }, { 50, 100 }, { 100, 50 } };
SkRRect rr1;
rr1.setRectRadii(rect, radii);
REPORTER_ASSERT(reporter, SkRRect::kComplex_Type == rr1.type());
const SkPoint& p = rr1.radii(SkRRect::kUpperLeft_Corner);
REPORTER_ASSERT(reporter, SkScalarNearlyEqual(p.fX, 33.33333f));
REPORTER_ASSERT(reporter, SkScalarNearlyEqual(p.fY, 66.66666f));
// Negative radii should be capped at zero
SkRRect rr2;
rr2.setRectXY(rect, -10, -20);
REPORTER_ASSERT(reporter, SkRRect::kRect_Type == rr2.type());
const SkPoint& p2 = rr2.radii(SkRRect::kUpperLeft_Corner);
REPORTER_ASSERT(reporter, 0.0f == p2.fX);
REPORTER_ASSERT(reporter, 0.0f == p2.fY);
}
// Move a small box from the start position by (stepX, stepY) 'numSteps' times
// testing for containment in 'rr' at each step.
static void test_direction(skiatest::Reporter* reporter, const SkRRect &rr,
SkScalar initX, int stepX, SkScalar initY, int stepY,
int numSteps, const bool* contains) {
SkScalar x = initX, y = initY;
for (int i = 0; i < numSteps; ++i) {
SkRect test = SkRect::MakeXYWH(x, y,
stepX ? SkIntToScalar(stepX) : SK_Scalar1,
stepY ? SkIntToScalar(stepY) : SK_Scalar1);
test.sort();
REPORTER_ASSERT(reporter, contains[i] == rr.contains(test));
x += stepX;
y += stepY;
}
}
// Exercise the RR's contains rect method
static void test_round_rect_contains_rect(skiatest::Reporter* reporter) {
static const int kNumRRects = 4;
static const SkVector gRadii[kNumRRects][4] = {
{ { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 } }, // rect
{ { 20, 20 }, { 20, 20 }, { 20, 20 }, { 20, 20 } }, // circle
{ { 10, 10 }, { 10, 10 }, { 10, 10 }, { 10, 10 } }, // simple
{ { 0, 0 }, { 20, 20 }, { 10, 10 }, { 30, 30 } } // complex
};
SkRRect rrects[kNumRRects];
for (int i = 0; i < kNumRRects; ++i) {
rrects[i].setRectRadii(SkRect::MakeWH(40, 40), gRadii[i]);
}
// First test easy outs - boxes that are obviously out on
// each corner and edge
static const SkRect easyOuts[] = {
{ -5, -5, 5, 5 }, // NW
{ 15, -5, 20, 5 }, // N
{ 35, -5, 45, 5 }, // NE
{ 35, 15, 45, 20 }, // E
{ 35, 45, 35, 45 }, // SE
{ 15, 35, 20, 45 }, // S
{ -5, 35, 5, 45 }, // SW
{ -5, 15, 5, 20 } // W
};
for (int i = 0; i < kNumRRects; ++i) {
for (size_t j = 0; j < std::size(easyOuts); ++j) {
REPORTER_ASSERT(reporter, !rrects[i].contains(easyOuts[j]));
}
}
// Now test non-trivial containment. For each compass
// point walk a 1x1 rect in from the edge of the bounding
// rect
static const int kNumSteps = 15;
bool answers[kNumRRects][8][kNumSteps] = {
// all the test rects are inside the degenerate rrect
{
// rect
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
},
// for the circle we expect 6 blocks to be out on the
// corners (then the rest in) and only the first block
// out on the vertical and horizontal axes (then
// the rest in)
{
// circle
{ 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
{ 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
{ 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
{ 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
{ 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
{ 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
{ 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
{ 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
},
// for the simple round rect we expect 3 out on
// the corners (then the rest in) and no blocks out
// on the vertical and horizontal axes
{
// simple RR
{ 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
{ 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
{ 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
{ 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
},
// for the complex case the answer is different for each direction
{
// complex RR
// all in for NW (rect) corner (same as rect case)
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
// only first block out for N (same as circle case)
{ 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
// first 6 blocks out for NE (same as circle case)
{ 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
// only first block out for E (same as circle case)
{ 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
// first 3 blocks out for SE (same as simple case)
{ 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
// first two blocks out for S
{ 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
// first 9 blocks out for SW
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1 },
// first two blocks out for W (same as S)
{ 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
}
};
for (int i = 0; i < kNumRRects; ++i) {
test_direction(reporter, rrects[i], 0, 1, 0, 1, kNumSteps, answers[i][0]); // NW
test_direction(reporter, rrects[i], 19.5f, 0, 0, 1, kNumSteps, answers[i][1]); // N
test_direction(reporter, rrects[i], 40, -1, 0, 1, kNumSteps, answers[i][2]); // NE
test_direction(reporter, rrects[i], 40, -1, 19.5f, 0, kNumSteps, answers[i][3]); // E
test_direction(reporter, rrects[i], 40, -1, 40, -1, kNumSteps, answers[i][4]); // SE
test_direction(reporter, rrects[i], 19.5f, 0, 40, -1, kNumSteps, answers[i][5]); // S
test_direction(reporter, rrects[i], 0, 1, 40, -1, kNumSteps, answers[i][6]); // SW
test_direction(reporter, rrects[i], 0, 1, 19.5f, 0, kNumSteps, answers[i][7]); // W
}
}
// Called for a matrix that should cause SkRRect::transform to fail.
static void assert_transform_failure(skiatest::Reporter* reporter, const SkRRect& orig,
const SkMatrix& matrix) {
// The test depends on the fact that the original is not empty.
SkASSERT(!orig.isEmpty());
SkRRect dst;
dst.setEmpty();
const SkRRect copyOfDst = dst;
const SkRRect copyOfOrig = orig;
bool success = orig.transform(matrix, &dst);
// This transform should fail.
REPORTER_ASSERT(reporter, !success);
// Since the transform failed, dst should be unchanged.
REPORTER_ASSERT(reporter, copyOfDst == dst);
// original should not be modified.
REPORTER_ASSERT(reporter, copyOfOrig == orig);
REPORTER_ASSERT(reporter, orig != dst);
}
#define GET_RADII \
const SkVector& origUL = orig.radii(SkRRect::kUpperLeft_Corner); \
const SkVector& origUR = orig.radii(SkRRect::kUpperRight_Corner); \
const SkVector& origLR = orig.radii(SkRRect::kLowerRight_Corner); \
const SkVector& origLL = orig.radii(SkRRect::kLowerLeft_Corner); \
const SkVector& dstUL = dst.radii(SkRRect::kUpperLeft_Corner); \
const SkVector& dstUR = dst.radii(SkRRect::kUpperRight_Corner); \
const SkVector& dstLR = dst.radii(SkRRect::kLowerRight_Corner); \
const SkVector& dstLL = dst.radii(SkRRect::kLowerLeft_Corner)
// Called to test various transforms on a single SkRRect.
static void test_transform_helper(skiatest::Reporter* reporter, const SkRRect& orig) {
SkRRect dst;
dst.setEmpty();
// The identity matrix will duplicate the rrect.
bool success = orig.transform(SkMatrix::I(), &dst);
REPORTER_ASSERT(reporter, success);
REPORTER_ASSERT(reporter, orig == dst);
// Skew and Perspective make transform fail.
SkMatrix matrix;
matrix.reset();
matrix.setSkewX(SkIntToScalar(2));
assert_transform_failure(reporter, orig, matrix);
matrix.reset();
matrix.setSkewY(SkIntToScalar(3));
assert_transform_failure(reporter, orig, matrix);
matrix.reset();
matrix.setPerspX(4);
assert_transform_failure(reporter, orig, matrix);
matrix.reset();
matrix.setPerspY(5);
assert_transform_failure(reporter, orig, matrix);
// Rotation fails.
matrix.reset();
matrix.setRotate(SkIntToScalar(37));
assert_transform_failure(reporter, orig, matrix);
// Translate will keep the rect moved, but otherwise the same.
matrix.reset();
SkScalar translateX = SkIntToScalar(32);
SkScalar translateY = SkIntToScalar(15);
matrix.setTranslateX(translateX);
matrix.setTranslateY(translateY);
dst.setEmpty();
success = orig.transform(matrix, &dst);
REPORTER_ASSERT(reporter, success);
for (int i = 0; i < 4; ++i) {
REPORTER_ASSERT(reporter,
orig.radii((SkRRect::Corner) i) == dst.radii((SkRRect::Corner) i));
}
REPORTER_ASSERT(reporter, orig.rect().width() == dst.rect().width());
REPORTER_ASSERT(reporter, orig.rect().height() == dst.rect().height());
REPORTER_ASSERT(reporter, dst.rect().left() == orig.rect().left() + translateX);
REPORTER_ASSERT(reporter, dst.rect().top() == orig.rect().top() + translateY);
// Keeping the translation, but adding skew will make transform fail.
matrix.setSkewY(SkIntToScalar(7));
assert_transform_failure(reporter, orig, matrix);
// Scaling in -x will flip the round rect horizontally.
matrix.reset();
matrix.setScaleX(SkIntToScalar(-1));
dst.setEmpty();
success = orig.transform(matrix, &dst);
REPORTER_ASSERT(reporter, success);
{
GET_RADII;
// Radii have swapped in x.
REPORTER_ASSERT(reporter, origUL == dstUR);
REPORTER_ASSERT(reporter, origUR == dstUL);
REPORTER_ASSERT(reporter, origLR == dstLL);
REPORTER_ASSERT(reporter, origLL == dstLR);
}
// Width and height remain the same.
REPORTER_ASSERT(reporter, orig.rect().width() == dst.rect().width());
REPORTER_ASSERT(reporter, orig.rect().height() == dst.rect().height());
// Right and left have swapped (sort of)
REPORTER_ASSERT(reporter, orig.rect().right() == -dst.rect().left());
// Top has stayed the same.
REPORTER_ASSERT(reporter, orig.rect().top() == dst.rect().top());
// Keeping the scale, but adding a persp will make transform fail.
matrix.setPerspX(7);
assert_transform_failure(reporter, orig, matrix);
// Scaling in -y will flip the round rect vertically.
matrix.reset();
matrix.setScaleY(SkIntToScalar(-1));
dst.setEmpty();
success = orig.transform(matrix, &dst);
REPORTER_ASSERT(reporter, success);
{
GET_RADII;
// Radii have swapped in y.
REPORTER_ASSERT(reporter, origUL == dstLL);
REPORTER_ASSERT(reporter, origUR == dstLR);
REPORTER_ASSERT(reporter, origLR == dstUR);
REPORTER_ASSERT(reporter, origLL == dstUL);
}
// Width and height remain the same.
REPORTER_ASSERT(reporter, orig.rect().width() == dst.rect().width());
REPORTER_ASSERT(reporter, orig.rect().height() == dst.rect().height());
// Top and bottom have swapped (sort of)
REPORTER_ASSERT(reporter, orig.rect().top() == -dst.rect().bottom());
// Left has stayed the same.
REPORTER_ASSERT(reporter, orig.rect().left() == dst.rect().left());
// Scaling in -x and -y will swap in both directions.
matrix.reset();
matrix.setScaleY(SkIntToScalar(-1));
matrix.setScaleX(SkIntToScalar(-1));
dst.setEmpty();
success = orig.transform(matrix, &dst);
REPORTER_ASSERT(reporter, success);
{
GET_RADII;
REPORTER_ASSERT(reporter, origUL == dstLR);
REPORTER_ASSERT(reporter, origUR == dstLL);
REPORTER_ASSERT(reporter, origLR == dstUL);
REPORTER_ASSERT(reporter, origLL == dstUR);
}
// Width and height remain the same.
REPORTER_ASSERT(reporter, orig.rect().width() == dst.rect().width());
REPORTER_ASSERT(reporter, orig.rect().height() == dst.rect().height());
REPORTER_ASSERT(reporter, orig.rect().top() == -dst.rect().bottom());
REPORTER_ASSERT(reporter, orig.rect().right() == -dst.rect().left());
// Scale in both directions.
SkScalar xScale = SkIntToScalar(3);
SkScalar yScale = 3.2f;
matrix.reset();
matrix.setScaleX(xScale);
matrix.setScaleY(yScale);
dst.setEmpty();
success = orig.transform(matrix, &dst);
REPORTER_ASSERT(reporter, success);
// Radii are scaled.
for (int i = 0; i < 4; ++i) {
REPORTER_ASSERT(reporter, SkScalarNearlyEqual(dst.radii((SkRRect::Corner) i).fX,
orig.radii((SkRRect::Corner) i).fX * xScale));
REPORTER_ASSERT(reporter, SkScalarNearlyEqual(dst.radii((SkRRect::Corner) i).fY,
orig.radii((SkRRect::Corner) i).fY * yScale));
}
REPORTER_ASSERT(reporter, SkScalarNearlyEqual(dst.rect().width(),
orig.rect().width() * xScale));
REPORTER_ASSERT(reporter, SkScalarNearlyEqual(dst.rect().height(),
orig.rect().height() * yScale));
REPORTER_ASSERT(reporter, SkScalarNearlyEqual(dst.rect().left(),
orig.rect().left() * xScale));
REPORTER_ASSERT(reporter, SkScalarNearlyEqual(dst.rect().top(),
orig.rect().top() * yScale));
// a-----b d-----a
// | | -> | |
// | | Rotate 90 | |
// d-----c c-----b
matrix.reset();
matrix.setRotate(SkIntToScalar(90));
dst.setEmpty();
success = orig.transform(matrix, &dst);
REPORTER_ASSERT(reporter, success);
{
GET_RADII;
// Radii have cycled clockwise and swapped their x and y axis.
REPORTER_ASSERT(reporter, dstUL.x() == origLL.y());
REPORTER_ASSERT(reporter, dstUL.y() == origLL.x());
REPORTER_ASSERT(reporter, dstUR.x() == origUL.y());
REPORTER_ASSERT(reporter, dstUR.y() == origUL.x());
REPORTER_ASSERT(reporter, dstLR.x() == origUR.y());
REPORTER_ASSERT(reporter, dstLR.y() == origUR.x());
REPORTER_ASSERT(reporter, dstLL.x() == origLR.y());
REPORTER_ASSERT(reporter, dstLL.y() == origLR.x());
}
// Width and height would get swapped.
REPORTER_ASSERT(reporter, orig.rect().width() == dst.rect().height());
REPORTER_ASSERT(reporter, orig.rect().height() == dst.rect().width());
// a-----b b-----a c-----b
// | | -> | | -> | |
// | | Flip X | | Rotate 90 | |
// d-----c c-----d d-----a
matrix.reset();
matrix.setRotate(SkIntToScalar(90));
matrix.postScale(SkIntToScalar(-1), SkIntToScalar(1));
dst.setEmpty();
success = orig.transform(matrix, &dst);
REPORTER_ASSERT(reporter, success);
{
GET_RADII;
REPORTER_ASSERT(reporter, dstUL.x() == origLR.y());
REPORTER_ASSERT(reporter, dstUL.y() == origLR.x());
REPORTER_ASSERT(reporter, dstUR.x() == origUR.y());
REPORTER_ASSERT(reporter, dstUR.y() == origUR.x());
REPORTER_ASSERT(reporter, dstLR.x() == origUL.y());
REPORTER_ASSERT(reporter, dstLR.y() == origUL.x());
REPORTER_ASSERT(reporter, dstLL.x() == origLL.y());
REPORTER_ASSERT(reporter, dstLL.y() == origLL.x());
}
// Width and height would get swapped.
REPORTER_ASSERT(reporter, orig.rect().width() == dst.rect().height());
REPORTER_ASSERT(reporter, orig.rect().height() == dst.rect().width());
// a-----b d-----a c-----b
// | | -> | | -> | |
// | | Rotate 90 | | Flip Y | |
// d-----c c-----b d-----a
//
// This is the same as Flip X and Rotate 90.
matrix.reset();
matrix.setScale(SkIntToScalar(1), SkIntToScalar(-1));
matrix.postRotate(SkIntToScalar(90));
SkRRect dst2;
dst2.setEmpty();
success = orig.transform(matrix, &dst2);
REPORTER_ASSERT(reporter, success);
REPORTER_ASSERT(reporter, dst == dst2);
// a-----b b-----c c-----b
// | | -> | | -> | |
// | | Rotate 270 | | Flip X | |
// d-----c a-----d d-----a
matrix.reset();
matrix.setScale(SkIntToScalar(-1), SkIntToScalar(1));
matrix.postRotate(SkIntToScalar(270));
dst2.setEmpty();
success = orig.transform(matrix, &dst2);
REPORTER_ASSERT(reporter, success);
REPORTER_ASSERT(reporter, dst == dst2);
// a-----b d-----c c-----b
// | | -> | | -> | |
// | | Flip Y | | Rotate 270 | |
// d-----c a-----b d-----a
matrix.reset();
matrix.setRotate(SkIntToScalar(270));
matrix.postScale(SkIntToScalar(1), SkIntToScalar(-1));
dst2.setEmpty();
success = orig.transform(matrix, &dst2);
REPORTER_ASSERT(reporter, success);
REPORTER_ASSERT(reporter, dst == dst2);
// a-----b d-----a a-----d
// | | -> | | -> | |
// | | Rotate 90 | | Flip X | |
// d-----c c-----b b-----c
matrix.reset();
matrix.setScale(SkIntToScalar(-1), SkIntToScalar(1));
matrix.postRotate(SkIntToScalar(90));
dst.setEmpty();
success = orig.transform(matrix, &dst);
REPORTER_ASSERT(reporter, success);
{
GET_RADII;
REPORTER_ASSERT(reporter, dstUL.x() == origUL.y());
REPORTER_ASSERT(reporter, dstUL.y() == origUL.x());
REPORTER_ASSERT(reporter, dstUR.x() == origLL.y());
REPORTER_ASSERT(reporter, dstUR.y() == origLL.x());
REPORTER_ASSERT(reporter, dstLR.x() == origLR.y());
REPORTER_ASSERT(reporter, dstLR.y() == origLR.x());
REPORTER_ASSERT(reporter, dstLL.x() == origUR.y());
REPORTER_ASSERT(reporter, dstLL.y() == origUR.x());
}
// Width and height would get swapped.
REPORTER_ASSERT(reporter, orig.rect().width() == dst.rect().height());
REPORTER_ASSERT(reporter, orig.rect().height() == dst.rect().width());
// a-----b d-----c a-----d
// | | -> | | -> | |
// | | Flip Y | | Rotate 90 | |
// d-----c a-----b b-----c
// This is the same as rotate 90 and flip x.
matrix.reset();
matrix.setRotate(SkIntToScalar(90));
matrix.postScale(SkIntToScalar(1), SkIntToScalar(-1));
dst2.setEmpty();
success = orig.transform(matrix, &dst2);
REPORTER_ASSERT(reporter, success);
REPORTER_ASSERT(reporter, dst == dst2);
// a-----b b-----a a-----d
// | | -> | | -> | |
// | | Flip X | | Rotate 270 | |
// d-----c c-----d b-----c
matrix.reset();
matrix.setRotate(SkIntToScalar(270));
matrix.postScale(SkIntToScalar(-1), SkIntToScalar(1));
dst2.setEmpty();
success = orig.transform(matrix, &dst2);
REPORTER_ASSERT(reporter, success);
REPORTER_ASSERT(reporter, dst == dst2);
// a-----b b-----c a-----d
// | | -> | | -> | |
// | | Rotate 270 | | Flip Y | |
// d-----c a-----d b-----c
matrix.reset();
matrix.setScale(SkIntToScalar(1), SkIntToScalar(-1));
matrix.postRotate(SkIntToScalar(270));
dst2.setEmpty();
success = orig.transform(matrix, &dst2);
REPORTER_ASSERT(reporter, success);
REPORTER_ASSERT(reporter, dst == dst2);
// a-----b b-----a c-----d b-----c
// | | -> | | -> | | -> | |
// | | Flip X | | Flip Y | | Rotate 90 | |
// d-----c c-----d b-----a a-----d
//
// This is the same as rotation by 270.
matrix.reset();
matrix.setRotate(SkIntToScalar(90));
matrix.postScale(SkIntToScalar(-1), SkIntToScalar(-1));
dst.setEmpty();
success = orig.transform(matrix, &dst);
REPORTER_ASSERT(reporter, success);
{
GET_RADII;
// Radii have cycled clockwise and swapped their x and y axis.
REPORTER_ASSERT(reporter, dstUL.x() == origUR.y());
REPORTER_ASSERT(reporter, dstUL.y() == origUR.x());
REPORTER_ASSERT(reporter, dstUR.x() == origLR.y());
REPORTER_ASSERT(reporter, dstUR.y() == origLR.x());
REPORTER_ASSERT(reporter, dstLR.x() == origLL.y());
REPORTER_ASSERT(reporter, dstLR.y() == origLL.x());
REPORTER_ASSERT(reporter, dstLL.x() == origUL.y());
REPORTER_ASSERT(reporter, dstLL.y() == origUL.x());
}
// Width and height would get swapped.
REPORTER_ASSERT(reporter, orig.rect().width() == dst.rect().height());
REPORTER_ASSERT(reporter, orig.rect().height() == dst.rect().width());
// a-----b b-----c
// | | -> | |
// | | Rotate 270 | |
// d-----c a-----d
//
dst2.setEmpty();
matrix.reset();
matrix.setRotate(SkIntToScalar(270));
success = orig.transform(matrix, &dst2);
REPORTER_ASSERT(reporter, success);
REPORTER_ASSERT(reporter, dst == dst2);
// a-----b b-----a c-----d d-----a
// | | -> | | -> | | -> | |
// | | Flip X | | Flip Y | | Rotate 270 | |
// d-----c c-----d b-----a c-----b
//
// This is the same as rotation by 90 degrees.
matrix.reset();
matrix.setRotate(SkIntToScalar(270));
matrix.postScale(SkIntToScalar(-1), SkIntToScalar(-1));
dst.setEmpty();
success = orig.transform(matrix, &dst);
REPORTER_ASSERT(reporter, success);
matrix.reset();
matrix.setRotate(SkIntToScalar(90));
dst2.setEmpty();
success = orig.transform(matrix, &dst2);
REPORTER_ASSERT(reporter, dst == dst2);
}
static void test_round_rect_transform(skiatest::Reporter* reporter) {
SkRRect rrect;
{
SkRect r = { 0, 0, kWidth, kHeight };
rrect.setRectXY(r, SkIntToScalar(4), SkIntToScalar(7));
test_transform_helper(reporter, rrect);
}
{
SkRect r = { SkIntToScalar(5), SkIntToScalar(15),
SkIntToScalar(27), SkIntToScalar(34) };
SkVector radii[4] = { { 0, SkIntToScalar(1) },
{ SkIntToScalar(2), SkIntToScalar(3) },
{ SkIntToScalar(4), SkIntToScalar(5) },
{ SkIntToScalar(6), SkIntToScalar(7) } };
rrect.setRectRadii(r, radii);
test_transform_helper(reporter, rrect);
}
}
// Test out the case where an oval already off in space is translated/scaled
// further off into space - yielding numerical issues when the rect & radii
// are transformed separatly
// BUG=skia:2696
static void test_issue_2696(skiatest::Reporter* reporter) {
SkRRect rrect;
SkRect r = { 28443.8594f, 53.1428604f, 28446.7148f, 56.0000038f };
rrect.setOval(r);
SkMatrix xform;
xform.setAll(2.44f, 0.0f, 485411.7f,
0.0f, 2.44f, -438.7f,
0.0f, 0.0f, 1.0f);
SkRRect dst;
bool success = rrect.transform(xform, &dst);
REPORTER_ASSERT(reporter, success);
SkScalar halfWidth = SkScalarHalf(dst.width());
SkScalar halfHeight = SkScalarHalf(dst.height());
for (int i = 0; i < 4; ++i) {
REPORTER_ASSERT(reporter,
SkScalarNearlyEqual(dst.radii((SkRRect::Corner)i).fX, halfWidth));
REPORTER_ASSERT(reporter,
SkScalarNearlyEqual(dst.radii((SkRRect::Corner)i).fY, halfHeight));
}
}
void test_read_rrect(skiatest::Reporter* reporter, const SkRRect& rrect, bool shouldEqualSrc) {
// It would be cleaner to call rrect.writeToMemory into a buffer. However, writeToMemory asserts
// that the rrect is valid and our caller may have fiddled with the internals of rrect to make
// it invalid.
const void* buffer = reinterpret_cast<const void*>(&rrect);
SkRRect deserialized;
size_t size = deserialized.readFromMemory(buffer, sizeof(SkRRect));
REPORTER_ASSERT(reporter, size == SkRRect::kSizeInMemory);
REPORTER_ASSERT(reporter, deserialized.isValid());
if (shouldEqualSrc) {
REPORTER_ASSERT(reporter, rrect == deserialized);
}
}
static void test_read(skiatest::Reporter* reporter) {
static const SkRect kRect = {10.f, 10.f, 20.f, 20.f};
static const SkRect kNaNRect = {10.f, 10.f, 20.f, SK_ScalarNaN};
static const SkRect kInfRect = {10.f, 10.f, SK_ScalarInfinity, 20.f};
SkRRect rrect;
test_read_rrect(reporter, SkRRect::MakeEmpty(), true);
test_read_rrect(reporter, SkRRect::MakeRect(kRect), true);
// These get coerced to empty.
test_read_rrect(reporter, SkRRect::MakeRect(kInfRect), true);
test_read_rrect(reporter, SkRRect::MakeRect(kNaNRect), true);
rrect.setRect(kRect);
SkRect* innerRect = reinterpret_cast<SkRect*>(&rrect);
SkASSERT(*innerRect == kRect);
*innerRect = kInfRect;
test_read_rrect(reporter, rrect, false);
*innerRect = kNaNRect;
test_read_rrect(reporter, rrect, false);
test_read_rrect(reporter, SkRRect::MakeOval(kRect), true);
test_read_rrect(reporter, SkRRect::MakeOval(kInfRect), true);
test_read_rrect(reporter, SkRRect::MakeOval(kNaNRect), true);
rrect.setOval(kRect);
*innerRect = kInfRect;
test_read_rrect(reporter, rrect, false);
*innerRect = kNaNRect;
test_read_rrect(reporter, rrect, false);
test_read_rrect(reporter, SkRRect::MakeRectXY(kRect, 5.f, 5.f), true);
// rrect should scale down the radii to make this legal
test_read_rrect(reporter, SkRRect::MakeRectXY(kRect, 5.f, 400.f), true);
static const SkVector kRadii[4] = {{0.5f, 1.f}, {1.5f, 2.f}, {2.5f, 3.f}, {3.5f, 4.f}};
rrect.setRectRadii(kRect, kRadii);
test_read_rrect(reporter, rrect, true);
SkScalar* innerRadius = reinterpret_cast<SkScalar*>(&rrect) + 6;
SkASSERT(*innerRadius == 1.5f);
*innerRadius = 400.f;
test_read_rrect(reporter, rrect, false);
*innerRadius = SK_ScalarInfinity;
test_read_rrect(reporter, rrect, false);
*innerRadius = SK_ScalarNaN;
test_read_rrect(reporter, rrect, false);
*innerRadius = -10.f;
test_read_rrect(reporter, rrect, false);
}
static void test_inner_bounds(skiatest::Reporter* reporter) {
// Because InnerBounds() insets the computed bounds slightly to correct for numerical inaccuracy
// when finding the maximum inscribed point on a curve, we use a larger epsilon for comparing
// expected areas.
static constexpr SkScalar kEpsilon = 0.005f;
// Test that an empty rrect reports empty inner bounds
REPORTER_ASSERT(reporter, SkRRectPriv::InnerBounds(SkRRect::MakeEmpty()).isEmpty());
// Test that a rect rrect reports itself as the inner bounds
SkRect r = SkRect::MakeLTRB(0, 1, 2, 3);
REPORTER_ASSERT(reporter, SkRRectPriv::InnerBounds(SkRRect::MakeRect(r)) == r);
// Test that a circle rrect has an inner bounds area equal to 2*radius^2
float radius = 5.f;
SkRect inner = SkRRectPriv::InnerBounds(SkRRect::MakeOval(SkRect::MakeWH(2.f * radius,
2.f * radius)));
REPORTER_ASSERT(reporter, SkScalarNearlyEqual(inner.width() * inner.height(),
2.f * radius * radius, kEpsilon));
float width = 20.f;
float height = 25.f;
r = SkRect::MakeWH(width, height);
// Test that a rrect with circular corners has an area equal to:
float expectedArea =
(2.f * radius * radius) + // area in the 4 circular corners
(width-2.f*radius) * (height-2.f*radius) + // inner area excluding corners and edges
SK_ScalarSqrt2 * radius * (width-2.f*radius) + // two horiz. rects between corners
SK_ScalarSqrt2 * radius * (height-2.f*radius); // two vert. rects between corners
inner = SkRRectPriv::InnerBounds(SkRRect::MakeRectXY(r, radius, radius));
REPORTER_ASSERT(reporter, SkScalarNearlyEqual(inner.width() * inner.height(),
expectedArea, kEpsilon));
// Test that a rrect with a small y radius but large x radius selects the horizontal interior
SkRRect rr = SkRRect::MakeRectXY(r, 2.f * radius, 0.1f * radius);
REPORTER_ASSERT(reporter, SkRRectPriv::InnerBounds(rr) ==
SkRect::MakeLTRB(0.f, 0.1f * radius, width, height - 0.1f * radius));
// And vice versa with large y and small x radii
rr = SkRRect::MakeRectXY(r, 0.1f * radius, 2.f * radius);
REPORTER_ASSERT(reporter, SkRRectPriv::InnerBounds(rr) ==
SkRect::MakeLTRB(0.1f * radius, 0.f, width - 0.1f * radius, height));
// Test a variety of complex round rects produce a non-empty rect that is at least contained,
// and larger than the inner area avoiding all corners.
SkRandom rng;
for (int i = 0; i < 1000; ++i) {
float maxRadiusX = rng.nextRangeF(0.f, 40.f);
float maxRadiusY = rng.nextRangeF(0.f, 40.f);
float innerWidth = rng.nextRangeF(0.f, 40.f);
float innerHeight = rng.nextRangeF(0.f, 40.f);
SkVector radii[4] = {{rng.nextRangeF(0.f, maxRadiusX), rng.nextRangeF(0.f, maxRadiusY)},
{rng.nextRangeF(0.f, maxRadiusX), rng.nextRangeF(0.f, maxRadiusY)},
{rng.nextRangeF(0.f, maxRadiusX), rng.nextRangeF(0.f, maxRadiusY)},
{rng.nextRangeF(0.f, maxRadiusX), rng.nextRangeF(0.f, maxRadiusY)}};
float maxLeft = std::max(radii[0].fX, radii[3].fX);
float maxTop = std::max(radii[0].fY, radii[1].fY);
float maxRight = std::max(radii[1].fX, radii[2].fX);
float maxBottom = std::max(radii[2].fY, radii[3].fY);
SkRect outer = SkRect::MakeWH(maxLeft + maxRight + innerWidth,
maxTop + maxBottom + innerHeight);
rr.setRectRadii(outer, radii);
SkRect maxInner = SkRRectPriv::InnerBounds(rr);
// Test upper limit on the size of 'maxInner'
REPORTER_ASSERT(reporter, outer.contains(maxInner));
REPORTER_ASSERT(reporter, rr.contains(maxInner));
// Test lower limit on the size of 'maxInner'
inner = SkRect::MakeXYWH(maxLeft, maxTop, innerWidth, innerHeight);
inner.inset(kEpsilon, kEpsilon);
if (inner.isSorted()) {
REPORTER_ASSERT(reporter, maxInner.contains(inner));
} else {
// Flipped from the inset, just test two points of inner
float midX = maxLeft + 0.5f * innerWidth;
float midY = maxTop + 0.5f * innerHeight;
REPORTER_ASSERT(reporter, maxInner.contains(midX, maxTop));
REPORTER_ASSERT(reporter, maxInner.contains(midX, maxTop + innerHeight));
REPORTER_ASSERT(reporter, maxInner.contains(maxLeft, midY));
REPORTER_ASSERT(reporter, maxInner.contains(maxLeft + innerWidth, midY));
}
}
}
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());
}
} // namespace
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);
// A rectangle exactly matching the corners of the rrect bounds keeps the rrect radii,
// regardless of whether or not it's the 1st or 2nd arg to ConservativeIntersect.
SkRRect c = SkRRect::MakeRectXY({0.f, 0.f, 10.f, 10.f}, 2.f, 2.f);
SkRRect cT = SkRRect::MakeRect({0.f, 0.f, 10.f, 5.f});
verify_success(reporter, c, cT, kA, kA, kRect, kRect);
verify_success(reporter, cT, c, kB, kB, kRect, kRect);
SkRRect cB = SkRRect::MakeRect({0.f, 5.f, 10.f, 10.});
verify_success(reporter, c, cB, kRect, kRect, kA, kA);
verify_success(reporter, cB, c, kRect, kRect, kB, kB);
SkRRect cL = SkRRect::MakeRect({0.f, 0.f, 5.f, 10.f});
verify_success(reporter, c, cL, kA, kRect, kRect, kA);
verify_success(reporter, cL, c, kB, kRect, kRect, kB);
SkRRect cR = SkRRect::MakeRect({5.f, 0.f, 10.f, 10.f});
verify_success(reporter, c, cR, kRect, kA, kA, kRect);
verify_success(reporter, cR, c, kRect, kB, kB, kRect);
// 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);
// Another variant of corners overlapping, this is two circles of radius r that overlap by r
// pixels (e.g. the leftmost point of the right circle touches the center of the left circle).
// The key difference with the above case is that the intersection of the circle bounds have
// corners that are contained in both circles, but because it is only r wide, can not satisfy
// all corners having radii = r.
float r = 100.f;
a = SkRRect::MakeOval(SkRect::MakeWH(2*r, 2*r));
verify_failure(reporter, a, a.makeOffset(r, 0.f));
}
DEF_TEST(RoundRect, reporter) {
test_round_rect_basic(reporter);
test_round_rect_rects(reporter);
test_round_rect_ovals(reporter);
test_round_rect_general(reporter);
test_round_rect_iffy_parameters(reporter);
test_inset(reporter);
test_round_rect_contains_rect(reporter);
test_round_rect_transform(reporter);
test_issue_2696(reporter);
test_tricky_radii(reporter);
test_empty_crbug_458524(reporter);
test_empty(reporter);
test_read(reporter);
test_inner_bounds(reporter);
test_conservative_intersection(reporter);
}
DEF_TEST(RRect_fuzzer_regressions, r) {
{
unsigned char buf[] = {
0x0a, 0x00, 0x00, 0xff, 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f,
0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f,
0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f,
0x7f, 0x7f, 0x7f, 0x02, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x02, 0x00
};
REPORTER_ASSERT(r, sizeof(buf) == SkRRect{}.readFromMemory(buf, sizeof(buf)));
}
{
unsigned char buf[] = {
0x5d, 0xff, 0xff, 0x5d, 0x0a, 0x60, 0x0a, 0x0a, 0x0a, 0x7e, 0x0a, 0x5a,
0x0a, 0x12, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a,
0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x00, 0x00, 0x00, 0x0a,
0x0a, 0x0a, 0x0a, 0x26, 0x0a, 0x0a, 0x0a, 0x0a, 0xff, 0xff, 0x0a, 0x0a
};
REPORTER_ASSERT(r, sizeof(buf) == SkRRect{}.readFromMemory(buf, sizeof(buf)));
}
{
unsigned char buf[] = {
0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x04, 0xdd, 0xdd, 0x15,
0xfe, 0x00, 0x00, 0x04, 0x05, 0x7e, 0x00, 0x00, 0x00, 0xff, 0x08, 0x04,
0xff, 0xff, 0xfe, 0xfe, 0xff, 0x32, 0x32, 0x32, 0x32, 0x00, 0x32, 0x32,
0x04, 0xdd, 0x3d, 0x1c, 0xfe, 0x89, 0x04, 0x0a, 0x0e, 0x05, 0x7e, 0x0a
};
REPORTER_ASSERT(r, sizeof(buf) == SkRRect{}.readFromMemory(buf, sizeof(buf)));
}
}