Add cusp detection to GrPathUtils::findCubicConvex180Chops

The stroker will need to know the cusps in order to draw circles around
them.

Bug: skia:10419
Change-Id: I05b7e9f4a5ed06bd36450e73edfaf36c4b3f5a6c
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/337945
Commit-Queue: Chris Dalton <csmartdalton@google.com>
Reviewed-by: Michael Ludwig <michaelludwig@google.com>
This commit is contained in:
Chris Dalton 2020-11-24 09:12:32 -07:00 committed by Skia Commit-Bot
parent 1cf4bc796c
commit 98c3aea806
3 changed files with 84 additions and 22 deletions

View File

@ -585,7 +585,10 @@ void GrPathUtils::convertCubicToQuadsConstrainToTangents(const SkPoint p[4],
}
}
int GrPathUtils::findCubicConvex180Chops(const SkPoint pts[], float T[2]) {
int GrPathUtils::findCubicConvex180Chops(const SkPoint pts[], float T[2], bool* areCusps) {
// It's slow for us to write out a value to "areCaps", especially when 99% of the time we aren't
// drawing cusps. The caller must initialize this value to false.
SkASSERT(!areCusps || *areCusps == false);
using grvx::float2;
// If a chop falls within a distance of "kEpsilon" from 0 or 1, throw it out. Tangents become
@ -593,9 +596,9 @@ int GrPathUtils::findCubicConvex180Chops(const SkPoint pts[], float T[2]) {
// shaders don't allow more than 2^10 parametric segments, and they snap the beginning and
// ending edges at 0 and 1. So if we overstep an inflection or point of 180-degree rotation by a
// fraction of a tessellation segment, it just gets snapped.
constexpr static float kEpsilon = 1.f / (1 << 12);
constexpr static float kEpsilon = 1.f / (1 << 11);
// Floating-point representation of "1 - 2*kEpsilon".
constexpr static uint32_t kIEEE_one_minus_2_epsilon = (127 << 23) - 2*(1 << 12);
constexpr static uint32_t kIEEE_one_minus_2_epsilon = (127 << 23) - 2 * (1 << (24 - 11));
// Unfortunately we don't have a way to static_assert this, but we can runtime assert that the
// kIEEE_one_minus_2_epsilon bits are correct.
SkASSERT(sk_bit_cast<float>(kIEEE_one_minus_2_epsilon) == 1 - 2*kEpsilon);
@ -633,23 +636,43 @@ int GrPathUtils::findCubicConvex180Chops(const SkPoint pts[], float T[2]) {
float b_over_minus_2 = -.5f * b;
float discr_over_4 = b_over_minus_2*b_over_minus_2 - a*c;
if (discr_over_4 <= 0) {
// If -cuspThreshold <= discr_over_4 <= cuspThreshold, it means the two roots are within
// kEpsilon of one another (in parametric space). This is close enough for our purposes to
// consider them a single cusp.
float cuspThreshold = a * (kEpsilon/2);
cuspThreshold *= cuspThreshold;
if (discr_over_4 < -cuspThreshold) {
// The curve does not inflect or cusp. This means it might rotate more than 180 degrees
// instead. Chop were rotation == 180 deg. (This is the 2nd root where the tangent is
// parallel to tan0.)
//
// Tangent_Direction(T) x tan0 == 0
// (AT^2 x tan0) + (2BT x tan0) + (C x tan0) == 0
// (A x C)T^2 + (2B x C)T + (C x C) == 0 [[because tan0 == P1 - P0 == C]]
// bT^2 + 2c + 0 == 0 [[because A x C == b, B x C == c]]
// T = [0, -2c/b]
//
// NOTE: if C == 0, then C != tan0. But this is fine because the curve is definitely
// convex-180 if any points are colocated, and T[0] will equal NaN which returns 0 chops.
float root = sk_ieee_float_divide(c, b_over_minus_2);
// Is "root" inside the range [kEpsilon, 1 - kEpsilon)?
if (sk_bit_cast<uint32_t>(root - kEpsilon) < kIEEE_one_minus_2_epsilon) {
T[0] = root;
return 1;
}
return 0;
}
if (discr_over_4 <= cuspThreshold) {
// The two roots are close enough that we can consider them a single cusp.
if (areCusps) {
*areCusps = true;
}
if (a != 0 || b_over_minus_2 != 0 || c != 0) {
// The curve does not inflect or cusp. This means it might rotate more than 180 degrees
// instead. Chop were rotation == 180 deg. (This is the 2nd root where the tangent is
// parallel to tan0.)
//
// Tangent_Direction(T) x tan0 == 0
// (AT^2 x tan0) + (2BT x tan0) + (C x tan0) == 0
// (A x C)T^2 + (2B x C)T + (C x C) == 0 [[because tan0 == P1 - P0 == C]]
// bT^2 + 2c + 0 == 0 [[because A x C == b, B x C == c]]
// T = [0, -2c/b]
//
// NOTE: if C == 0, then C != tan0. But this is fine because the curve is definitely
// convex-180 if any points are colocated, and T[0] will equal NaN which returns 0
// chops.
float root = sk_ieee_float_divide(c, b_over_minus_2);
// Is "root" inside the range [epsilon, 1 - epsilon)?
// Pick the average of both roots.
float root = sk_ieee_float_divide(b_over_minus_2, a);
// Is "root" inside the range [kEpsilon, 1 - kEpsilon)?
if (sk_bit_cast<uint32_t>(root - kEpsilon) < kIEEE_one_minus_2_epsilon) {
T[0] = root;
return 1;

View File

@ -160,7 +160,7 @@ inline void convertQuadToCubic(const SkPoint p[3], SkPoint out[4]) {
//
// - Otherwise the T value is the point at which rotation reaches 180 degrees, iff in [0 < T < 1].
//
int findCubicConvex180Chops(const SkPoint[], float T[2]);
int findCubicConvex180Chops(const SkPoint[], float T[2], bool* areCusps = nullptr);
} // namespace GrPathUtils

View File

@ -19,18 +19,23 @@ static bool is_linear(const SkPoint p[4]) {
}
static void check_cubic_convex_180(skiatest::Reporter* r, const SkPoint p[4]) {
bool areCusps = false;
float inflectT[2], convex180T[2];
if (int inflectN = SkFindCubicInflections(p, inflectT)) {
// The curve has inflections. findCubicConvex180Chops should return the inflection
// points.
int convex180N = GrPathUtils::findCubicConvex180Chops(p, convex180T);
int convex180N = GrPathUtils::findCubicConvex180Chops(p, convex180T, &areCusps);
REPORTER_ASSERT(r, inflectN == convex180N);
if (!areCusps) {
REPORTER_ASSERT(r, inflectN == 1 ||
fabsf(inflectT[0] - inflectT[1]) >= SK_ScalarNearlyZero);
}
for (int i = 0; i < convex180N; ++i) {
REPORTER_ASSERT(r, SkScalarNearlyEqual(inflectT[i], convex180T[i]));
}
} else {
float totalRotation = SkMeasureNonInflectCubicRotation(p);
int convex180N = GrPathUtils::findCubicConvex180Chops(p, convex180T);
int convex180N = GrPathUtils::findCubicConvex180Chops(p, convex180T, &areCusps);
SkPoint chops[10];
SkChopCubicAt(p, chops, convex180T, convex180N);
float radsSum = 0;
@ -53,6 +58,9 @@ static void check_cubic_convex_180(skiatest::Reporter* r, const SkPoint p[4]) {
REPORTER_ASSERT(r, SkScalarNearlyEqual(
SkMeasureNonInflectCubicRotation(chops + 3), totalRotation - SK_ScalarPI));
}
REPORTER_ASSERT(r, !areCusps);
} else {
REPORTER_ASSERT(r, areCusps);
}
}
}
@ -81,6 +89,37 @@ DEF_TEST(GrPathUtils_findCubicConvex180Chops, r) {
SkPoint quad[4] = {{0,0}, {2,2}, {4,2}, {6,0}};
float T[2];
REPORTER_ASSERT(r, GrPathUtils::findCubicConvex180Chops(quad, T) == 0);
// Now test that cusps and near-cusps get flagged as cusps.
SkPoint cusp[4] = {{0,0}, {1,1}, {1,0}, {0,1}};
bool areCusps = false;
REPORTER_ASSERT(r, GrPathUtils::findCubicConvex180Chops(cusp, T, &areCusps) == 1);
REPORTER_ASSERT(r, areCusps == true);
// Find the height of the right side of "cusp" at which the distance between its inflection
// points is kEpsilon (in parametric space).
constexpr static double kEpsilon = 1.0 / (1 << 11);
constexpr static double kEpsilonSquared = kEpsilon * kEpsilon;
double h = (1 - kEpsilonSquared) / (3 * kEpsilonSquared + 1);
double dy = (1 - h) / 2;
cusp[1].fY = (float)(1 - dy);
cusp[2].fY = (float)(0 + dy);
REPORTER_ASSERT(r, SkFindCubicInflections(cusp, T) == 2);
REPORTER_ASSERT(r, SkScalarNearlyEqual(T[1] - T[0], (float)kEpsilon, (float)kEpsilonSquared));
// Ensure two inflection points barely more than kEpsilon apart do not get flagged as cusps.
cusp[1].fY = (float)(1 - 1.1 * dy);
cusp[2].fY = (float)(0 + 1.1 * dy);
areCusps = false;
REPORTER_ASSERT(r, GrPathUtils::findCubicConvex180Chops(cusp, T, &areCusps) == 2);
REPORTER_ASSERT(r, areCusps == false);
// Ensure two inflection points barely less than kEpsilon apart do get flagged as cusps.
cusp[1].fY = (float)(1 - .9 * dy);
cusp[2].fY = (float)(0 + .9 * dy);
areCusps = false;
REPORTER_ASSERT(r, GrPathUtils::findCubicConvex180Chops(cusp, T, &areCusps) == 1);
REPORTER_ASSERT(r, areCusps == true);
}
DEF_TEST(GrPathUtils_convertToCubic, r) {