blob: fcb779ddb98c65cfafd6c66b6475f6516f0c7fb5 [file] [log] [blame]
/*
* Copyright 2020 Google Inc.
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include "rive/math/math_types.hpp"
#include "rive/math/bezier_utils.hpp"
#include "rive/math/simd.hpp"
#include "common/rand.hpp"
#include <catch.hpp>
// glsl cross-compiling requires the clang/gcc vector extension.
#ifndef _MSC_VER
#include "../renderer/cpp.glsl"
namespace glsl_cross
{
#include "generated/shaders/bezier_utils.minified.glsl"
}
#endif
namespace rive
{
namespace math
{
constexpr static float kEpsilon = 1.f / (1 << 12);
static bool fuzzy_equal(float a, float b, float tolerance = kEpsilon)
{
assert(tolerance >= 0);
return fabsf(a - b) <= tolerance;
}
static float frand() { return rand() / static_cast<float>(RAND_MAX); }
TEST_CASE("chop_cubic_at", "[bezier_utils]")
{
// Make sure src and dst can be the same pointer.
{
Vec2D pts[7];
for (int i = 0; i < 7; ++i)
{
pts[i] = {(float)i, (float)i};
}
math::chop_cubic_at(pts, pts, .5f);
for (int i = 0; i < 7; ++i)
{
CHECK(pts[i].x == pts[i].y);
CHECK(pts[i].x == i * .5f);
}
}
static const float chopTs[] = {
0, 3 / 83.f, 3 / 79.f, 3 / 73.f, 3 / 71.f, 3 / 67.f,
3 / 61.f, 3 / 59.f, 3 / 53.f, 3 / 47.f, 3 / 43.f, 3 / 41.f,
3 / 37.f, 3 / 31.f, 3 / 29.f, 3 / 23.f, 3 / 19.f, 3 / 17.f,
3 / 13.f, 3 / 11.f, 3 / 7.f, 3 / 5.f, 1,
};
float ones[] = {1, 1, 1, 1, 1};
// Ensure an odd number of T values so we exercise the single chop code at
// the end of SkChopCubicAt form multiple T.
static_assert(std::size(chopTs) % 2 == 1);
static_assert(std::size(ones) % 2 == 1);
for (int iterIdx = 0; iterIdx < 5; ++iterIdx)
{
Vec2D pts[4] = {{frand(), frand()},
{frand(), frand()},
{frand(), frand()},
{frand(), frand()}};
Vec2D allChops[4 + std::size(chopTs) * 3];
math::chop_cubic_at(pts, allChops, chopTs, std::size(chopTs));
int i = 3;
for (float chopT : chopTs)
{
// Ensure we chop at approximately the correct points when we chop
// an entire list.
Vec2D expectedPt = math::eval_cubic_at(pts, chopT);
CHECK(fuzzy_equal(allChops[i].x, expectedPt.x));
CHECK(fuzzy_equal(allChops[i].y, expectedPt.y));
if (chopT == 0)
{
CHECK(allChops[i] == pts[0]);
}
if (chopT == 1)
{
CHECK(allChops[i] == pts[3]);
}
i += 3;
// Ensure the middle is exactly degenerate when we chop at two equal
// points.
Vec2D localChops[10];
math::chop_cubic_at(pts, localChops, chopT, chopT);
CHECK(localChops[3] == localChops[4]);
CHECK(localChops[3] == localChops[5]);
CHECK(localChops[3] == localChops[6]);
if (chopT == 0)
{
// Also ensure the first curve is exactly p0 when we chop at
// T=0.
CHECK(localChops[0] == pts[0]);
CHECK(localChops[1] == pts[0]);
CHECK(localChops[2] == pts[0]);
CHECK(localChops[3] == pts[0]);
}
if (chopT == 1)
{
// Also ensure the last curve is exactly p3 when we chop at T=1.
CHECK(localChops[6] == pts[3]);
CHECK(localChops[7] == pts[3]);
CHECK(localChops[8] == pts[3]);
CHECK(localChops[9] == pts[3]);
}
}
// Now test what happens when SkChopCubicAt does 0/0 and gets NaN
// values.
Vec2D oneChops[4 + std::size(ones) * 3];
math::chop_cubic_at(pts, oneChops, ones, std::size(ones));
CHECK(oneChops[0] == pts[0]);
CHECK(oneChops[1] == pts[1]);
CHECK(oneChops[2] == pts[2]);
for (size_t index = 3; index < std::size(oneChops); ++index)
{
CHECK(oneChops[index] == pts[3]);
}
}
}
TEST_CASE("chop_cubic_at(tValues=null)", "[bezier_utils]")
{
constexpr int kMaxNumChops = 20;
float chopT[kMaxNumChops];
Vec2D chopTChops[(kMaxNumChops + 1) * 3 + 1];
Vec2D nullTChops[(kMaxNumChops + 1) * 3 + 1];
for (int numChops = 1; numChops <= kMaxNumChops; ++numChops)
{
Vec2D pts[4] = {{frand(), frand()},
{frand(), frand()},
{frand(), frand()},
{frand(), frand()}};
float stepT = 1.f / (numChops + 1);
float t = 0;
for (size_t i = 0; i < numChops; ++i)
{
chopT[i] = t += stepT;
}
math::chop_cubic_at(pts, chopTChops, chopT, numChops);
math::chop_cubic_at(pts, nullTChops, nullptr, numChops);
size_t numChoptPts = numChops * 3 + 1;
for (size_t i = 0; i <= numChoptPts; ++i)
{
CHECK(nullTChops[i].x == Approx(chopTChops[i].x));
CHECK(nullTChops[i].y == Approx(chopTChops[i].y));
}
}
}
static float measure_non_inflect_cubic_rotation(const Vec2D pts[4])
{
Vec2D a = pts[1] - pts[0];
Vec2D b = pts[2] - pts[1];
Vec2D c = pts[3] - pts[2];
float lengthA = a.length();
float lengthB = b.length();
float lengthC = c.length();
float lengthMax = std::max(std::max(lengthA, lengthB), lengthC);
float zeroThreshold = std::max(lengthMax, 1.f) * 1e-4f;
if (lengthA <= zeroThreshold)
{
return math::measure_angle_between_vectors(b, c);
}
if (lengthB <= zeroThreshold)
{
return math::measure_angle_between_vectors(a, c);
}
if (lengthC <= zeroThreshold)
{
return math::measure_angle_between_vectors(a, b);
}
// Postulate: When no points are colocated and there are no inflection
// points in T=0..1, the rotation is: 360 degrees, minus the angle
// [p0,p1,p2], minus the angle [p1,p2,p3].
return 2 * math::PI - math::measure_angle_between_vectors(a, -b) -
math::measure_angle_between_vectors(b, -c);
}
TEST_CASE("measure_non_inflect_cubic_rotation", "[bezier_utils]")
{
static Vec2D kFlatCubic[4] = {{0, 0}, {0, 1}, {0, 2}, {0, 3}};
CHECK(fuzzy_equal(measure_non_inflect_cubic_rotation(kFlatCubic), 0));
static Vec2D kFlatCubic180_1[4] = {{0, 0}, {1, 0}, {3, 0}, {2, 0}};
CHECK(fuzzy_equal(measure_non_inflect_cubic_rotation(kFlatCubic180_1),
math::PI));
static Vec2D kFlatCubic180_2[4] = {{0, 1}, {0, 0}, {0, 2}, {0, 3}};
CHECK(fuzzy_equal(measure_non_inflect_cubic_rotation(kFlatCubic180_2),
math::PI));
static Vec2D kFlatCubic360[4] = {{0, 1}, {0, 0}, {0, 3}, {0, 2}};
CHECK(fuzzy_equal(measure_non_inflect_cubic_rotation(kFlatCubic360),
2 * math::PI));
static Vec2D kSquare180[4] = {{0, 0}, {0, 1}, {1, 1}, {1, 0}};
CHECK(
fuzzy_equal(measure_non_inflect_cubic_rotation(kSquare180), math::PI));
auto checkQuadRotation = [](const Vec2D pts[3], float expectedRotation) {
#if 0
float r = SkMeasureQuadRotation(pts);
CHECK(fuzzy_equal(r, expectedRotation));
#endif
Vec2D cubic1[4] = {pts[0], pts[0], pts[1], pts[2]};
CHECK(fuzzy_equal(measure_non_inflect_cubic_rotation(cubic1),
expectedRotation));
Vec2D cubic2[4] = {pts[0], pts[1], pts[1], pts[2]};
CHECK(fuzzy_equal(measure_non_inflect_cubic_rotation(cubic2),
expectedRotation));
Vec2D cubic3[4] = {pts[0], pts[1], pts[2], pts[2]};
CHECK(fuzzy_equal(measure_non_inflect_cubic_rotation(cubic3),
expectedRotation));
};
static Vec2D kFlatQuad[4] = {{0, 0}, {0, 1}, {0, 2}};
checkQuadRotation(kFlatQuad, 0);
static Vec2D kFlatQuad180_1[4] = {{1, 0}, {0, 0}, {2, 0}};
checkQuadRotation(kFlatQuad180_1, math::PI);
static Vec2D kFlatQuad180_2[4] = {{0, 0}, {0, 2}, {0, 1}};
checkQuadRotation(kFlatQuad180_2, math::PI);
static Vec2D kTri120[3] = {{0, 0}, {.5f, std::sqrt(3.f) / 2}, {1, 0}};
checkQuadRotation(kTri120, 2 * math::PI / 3);
}
static bool is_linear(Vec2D p0, Vec2D p1, Vec2D p2)
{
return fuzzy_equal(Vec2D::cross(p0 - p1, p2 - p1), 0);
}
static bool is_linear(const Vec2D p[4])
{
return is_linear(p[0], p[1], p[2]) && is_linear(p[0], p[2], p[3]) &&
is_linear(p[1], p[2], p[3]);
}
static int valid_unit_divide(float numer, float denom, float* ratio)
{
assert(ratio);
if (numer < 0)
{
numer = -numer;
denom = -denom;
}
if (denom == 0 || numer == 0 || numer >= denom)
{
return 0;
}
float r = numer / denom;
if (std::isnan(r))
{
return 0;
}
assert(r >= 0 && r < 1.f);
if (r == 0)
{ // catch underflow if numer <<<< denom
return 0;
}
*ratio = r;
return 1;
}
// Just returns its argument, but makes it easy to set a break-point to know
// when SkFindUnitQuadRoots is going to return 0 (an error).
static int return_check_zero(int value)
{
if (value == 0)
{
return 0;
}
return value;
}
static int SkFindUnitQuadRoots(float A, float B, float C, float roots[2])
{
assert(roots);
if (A == 0)
{
return return_check_zero(valid_unit_divide(-C, B, roots));
}
float* r = roots;
// use doubles so we don't overflow temporarily trying to compute R
double dr = (double)B * B - 4 * (double)A * C;
if (dr < 0)
{
return return_check_zero(0);
}
dr = sqrt(dr);
float R = static_cast<float>(dr);
if (std::isnan(R) || std::isinf(R))
{
return return_check_zero(0);
}
float Q = (B < 0) ? -(B - R) / 2 : -(B + R) / 2;
r += valid_unit_divide(Q, A, r);
r += valid_unit_divide(C, Q, r);
if (r - roots == 2)
{
if (roots[0] > roots[1])
{
using std::swap;
swap(roots[0], roots[1]);
}
else if (roots[0] == roots[1])
{ // nearly-equal?
r -= 1; // skip the double root
}
}
return return_check_zero((int)(r - roots));
}
/** http://www.faculty.idc.ac.il/arik/quality/appendixA.html
Inflection means that curvature is zero.
Curvature is [F' x F''] / [F'^3]
So we solve F'x X F''y - F'y X F''y == 0
After some canceling of the cubic term, we get
A = b - a
B = c - 2b + a
C = d - 3c + 3b - a
(BxCy - ByCx)t^2 + (AxCy - AyCx)t + AxBy - AyBx == 0
*/
static int SkFindCubicInflections(const Vec2D src[4], float tValues[2])
{
float Ax = src[1].x - src[0].x;
float Ay = src[1].y - src[0].y;
float Bx = src[2].x - 2 * src[1].x + src[0].x;
float By = src[2].y - 2 * src[1].y + src[0].y;
float Cx = src[3].x + 3 * (src[1].x - src[2].x) - src[0].x;
float Cy = src[3].y + 3 * (src[1].y - src[2].y) - src[0].y;
return SkFindUnitQuadRoots(Bx * Cy - By * Cx,
Ax * Cy - Ay * Cx,
Ax * By - Ay * Bx,
tValues);
}
static void check_cubic_convex_180(const Vec2D 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 =
math::find_cubic_convex_180_chops(p, convex180T, &areCusps);
REQUIRE(inflectN == convex180N);
if (!areCusps)
{
REQUIRE((inflectN == 1 ||
fabsf(inflectT[0] - inflectT[1]) >= kEpsilon));
}
for (int i = 0; i < convex180N; ++i)
{
REQUIRE(fuzzy_equal(inflectT[i], convex180T[i]));
}
}
else
{
float totalRotation = measure_non_inflect_cubic_rotation(p);
int convex180N =
math::find_cubic_convex_180_chops(p, convex180T, &areCusps);
Vec2D chops[10];
math::chop_cubic_at(p, chops, convex180T, convex180N);
float radsSum = 0;
if (areCusps)
{
if (convex180N == 1)
{
// The cusp itself accounts for 180 degrees of rotation.
radsSum = math::PI;
// Rechop, this time straddling the cusp, to avoid fp32
// precision issues from crossover.
Vec2D straddleChops[10];
math::chop_cubic_at(p,
straddleChops,
std::max(convex180T[0] - math::PI, 0.f),
std::min(convex180T[0] + math::PI, 1.f));
memcpy(chops + 1, straddleChops + 1, 2 * sizeof(Vec2D));
memcpy(chops + 4, straddleChops + 7, 2 * sizeof(Vec2D));
}
else if (convex180N == 2)
{
// The only way a cubic can have two cusps is if it's flat with
// two 180 degree turnarounds.
radsSum = math::PI * 2;
chops[1] = chops[0];
chops[2] = chops[4] = chops[3];
chops[5] = chops[7] = chops[6];
chops[8] = chops[9];
}
}
for (int i = 0; i <= convex180N; ++i)
{
float rads = measure_non_inflect_cubic_rotation(chops + i * 3);
assert(rads < math::PI + kEpsilon);
radsSum += rads;
}
if (totalRotation < math::PI - kEpsilon)
{
// The curve should never chop if rotation is <180 degrees.
REQUIRE(convex180N == 0);
}
else if (!is_linear(p))
{
REQUIRE(fuzzy_equal(radsSum, totalRotation));
if (totalRotation > math::PI + kEpsilon)
{
REQUIRE(convex180N == 1);
// This works because cusps take the "inflection" path above, so
// we don't get non-lilnear curves that lose rotation when
// chopped.
REQUIRE(fuzzy_equal(measure_non_inflect_cubic_rotation(chops),
math::PI));
REQUIRE(
fuzzy_equal(measure_non_inflect_cubic_rotation(chops + 3),
totalRotation - math::PI));
}
REQUIRE(!areCusps);
}
else
{
REQUIRE(areCusps);
}
}
}
TEST_CASE("find_cubic_convex_180_chops", "[bezier_utils]")
{
// Test all combinations of corners from the square [0,0,1,1]. This covers
// every cubic type as well as a wide variety of special cases for cusps,
// lines, loops, and inflections.
for (int i = 0; i < (1 << 8); ++i)
{
Vec2D p[4] = {Vec2D((i >> 0) & 1, (i >> 1) & 1),
Vec2D((i >> 2) & 1, (i >> 3) & 1),
Vec2D((i >> 4) & 1, (i >> 5) & 1),
Vec2D((i >> 6) & 1, (i >> 7) & 1)};
check_cubic_convex_180(p);
}
{
// This cubic has a convex-180 chop at T=1-"epsilon"
static const uint32_t hexPts[] = {0x3ee0ac74,
0x3f1e061a,
0x3e0fc408,
0x3f457230,
0x3f42ac7c,
0x3f70d76c,
0x3f4e6520,
0x3f6acafa};
Vec2D p[4];
memcpy(p, hexPts, sizeof(p));
check_cubic_convex_180(p);
}
// Now test an exact quadratic.
Vec2D quad[4] = {{0, 0}, {2, 2}, {4, 2}, {6, 0}};
float T[2];
bool areCusps;
CHECK(math::find_cubic_convex_180_chops(quad, T, &areCusps) == 0);
// Now test that cusps and near-cusps get flagged as cusps.
Vec2D cusp[4] = {{0, 0}, {1, 1}, {1, 0}, {0, 1}};
CHECK(math::find_cubic_convex_180_chops(cusp, T, &areCusps) == 1);
CHECK(areCusps == true);
// Find the height of the right side of "cusp" at which the distance between
// its inflection points is epsilon (in parametric space).
constexpr double epsilon = 1.0 / (1 << 11);
constexpr double epsilonPow2 = epsilon * epsilon;
double h = (1 - epsilonPow2) / (3 * epsilonPow2 + 1);
double dy = (1 - h) / 2;
cusp[1].y = (float)(1 - dy);
cusp[2].y = (float)(0 + dy);
CHECK(SkFindCubicInflections(cusp, T) == 2);
CHECK(fuzzy_equal(T[1] - T[0], (float)epsilon, (float)epsilonPow2));
// Ensure two inflection points barely more than epsilon apart do not get
// flagged as cusps.
cusp[1].y = (float)(1 - 4 * dy);
cusp[2].y = (float)(0 + 4 * dy);
CHECK(math::find_cubic_convex_180_chops(cusp, T, &areCusps) == 2);
CHECK(areCusps == false);
// Ensure two inflection points barely less than epsilon apart do get
// flagged as cusps.
cusp[1].y = (float)(1 - .9 * dy);
cusp[2].y = (float)(0 + .9 * dy);
CHECK(math::find_cubic_convex_180_chops(cusp, T, &areCusps) == 1);
CHECK(areCusps == true);
// Ensure fast floatingpoint is not enbled.
float2 p[] = {{460, 1060},
{774, 526},
{60, 660},
{460, 460},
{667, 460},
{1060, 460},
{686, 460},
{686, 660},
{1042, 1020}};
float2 c0 = simd::mix(p[3], p[4], float2(2 / 3.f));
float2 c1 = simd::mix(p[5], p[4], float2(2 / 3.f));
Vec2D cubic[] = {math::bit_cast<Vec2D>(p[3]),
math::bit_cast<Vec2D>(c0),
math::bit_cast<Vec2D>(c1),
math::bit_cast<Vec2D>(p[5])};
CHECK(math::find_cubic_convex_180_chops(cubic, T, &areCusps) == 0);
}
// Check that flat lines with ordered points never get detected as cusps.
TEST_CASE("find_cubic_convex_180_chops_lines", "[bezier_utils]")
{
Vec2D p0 = {123, 200}, p3 = {223, 432};
for (float t0 = 1e-3f; t0 < 1; t0 += .12f)
{
for (float t1 = t0 + .097f; t1 < 1; t1 += .097f)
{
Vec2D line[4] = {p0, lerp(p0, p3, t0), lerp(p0, p3, t1), p3};
float _[2];
bool areCusps = true;
math::find_cubic_convex_180_chops(line, _, &areCusps);
CHECK_FALSE(areCusps);
}
}
}
Vec2D eval_cubic(const Vec2D* p, float t)
{
return math::pow3(1 - t) * p[0] + 3 * math::pow2(1 - t) * t * p[1] +
3 * (1 - t) * math::pow2(t) * p[2] + math::pow3(t) * p[3];
}
constexpr static Vec2D TEST_CUBICS[][4] = {
{{199, 1225}, {197, 943}, {349, 607}, {549, 427}},
{{549, 427}, {349, 607}, {197, 943}, {199, 1225}},
{{460, 1060}, {403, -320}, {60, 660}, {1181, 634}},
{{1181, 634}, {60, 660}, {403, -320}, {460, 1060}},
{{0, 0}, {0, 0}, {0, 0}, {0, 0}},
{{0, 0}, {0, 0}, {0, 0}, {100, 100}},
{{0, 0}, {0, 0}, {100, 100}, {100, 100}},
{{0, 0}, {100, 100}, {100, 100}, {0, 0}},
{{-100, -100}, {100, 100}, {100, -100}, {-100, 100}}, // Cusp
{{0, 0}, {0, 0}, {100, 100}, {100, 100}}, // Line
{{0, 0}, {-100, -100}, {200, 200}, {100, 100}}, // Line w/ 2 cusps
{{0, 0},
{50 * 2.f / 3.f, 100 * 2.f / 3.f},
{100 - 50 * 2.f / 3.f, 100 * 2.f / 3.f},
{100, 0}}, // Quadratic
// The remaining cubics had some sort of issue during development. They're
// in the list to make sure they don't regress.
{{0, 0},
{50 * 2.f / 3.f, 100 * 2.f / 3.f},
{100 - 50 * 2.f / 3.f, 100 * 2.f / 3.f},
{100, 100}},
{{100, 0}, {0, 0}, {0, 0}, {0, 0}},
};
static void check_eval_cubic(const EvalCubic& evalCubic,
const Vec2D* cubic,
float t0,
float t1)
{
float4 pp = evalCubic(float4{t0, t0, t1, t1});
Vec2D p0ref = eval_cubic(cubic, t0);
Vec2D p1ref = eval_cubic(cubic, t1);
CHECK(pp.x == Approx(p0ref.x).margin(1e-3f));
CHECK(pp.y == Approx(p0ref.y).margin(1e-3f));
CHECK(pp.z == Approx(p1ref.x).margin(1e-3f));
CHECK(pp.w == Approx(p1ref.y).margin(1e-3f));
float2 p0 = evalCubic(t0);
float2 p1 = evalCubic(t1);
CHECK(p0.x == Approx(p0ref.x).margin(1e-3f));
CHECK(p0.y == Approx(p0ref.y).margin(1e-3f));
CHECK(p1.x == Approx(p1ref.x).margin(1e-3f));
CHECK(p1.y == Approx(p1ref.y).margin(1e-3f));
}
TEST_CASE("EvalCubic", "[bezier_utils]")
{
for (auto cubic : TEST_CUBICS)
{
math::EvalCubic evalCubic(cubic);
check_eval_cubic(evalCubic, cubic, 0, 1);
for (float t = 0; t <= 1; t += 1e-2f)
{
check_eval_cubic(evalCubic, cubic, t, t + 3e-3f);
}
}
}
// glsl cross-compiling requires the clang/gcc vector extension.
#ifndef _MSC_VER
static void check_cubic_coeffs_tangents_glsl(const Vec2D* pts)
{
// Check cubic coefficients.
math::CubicCoeffs coeffs(pts);
float2 p0 = simd::load2f(pts + 0);
float2 p1 = simd::load2f(pts + 1);
float2 p2 = simd::load2f(pts + 2);
float2 p3 = simd::load2f(pts + 3);
float2 A, B, C;
glsl_cross::find_cubic_coeffs(p0, p1, p2, p3, A, B, C);
CHECK(simd::all(A == coeffs.A));
CHECK(simd::all(B == coeffs.B));
CHECK(simd::all(C == coeffs.C));
// Check cubic tangents.
Vec2D tangents[2];
math::find_cubic_tangents(pts, tangents);
std::array<float2, 2> glslTangents =
glsl_cross::find_cubic_tangents(p0, p1, p2, p3);
CHECK(simd::all(simd::load2f(&tangents[0]) == glslTangents[0]));
CHECK(simd::all(simd::load2f(&tangents[1]) == glslTangents[1]));
}
TEST_CASE("find_cubic_coeffs_tangents_glsl", "[bezier_utils]")
{
for (auto pts : TEST_CUBICS)
{
check_cubic_coeffs_tangents_glsl(pts);
}
// Test all combinations of corners from the square [0,0,1,1]. This covers
// every cubic type as well as a wide variety of special cases for cusps,
// lines, loops, and inflections.
for (int i = 0; i < (1 << 8); ++i)
{
Vec2D pts[4] = {Vec2D((i >> 0) & 1, (i >> 1) & 1) * 100,
Vec2D((i >> 2) & 1, (i >> 3) & 1) * 100,
Vec2D((i >> 4) & 1, (i >> 5) & 1) * 100,
Vec2D((i >> 6) & 1, (i >> 7) & 1) * 100};
check_cubic_coeffs_tangents_glsl(pts);
}
Rand rando;
rando.seed(0);
for (int i = 0; i < 100; ++i)
{
Vec2D randos[] = {
{rando.f32(-100, 100), rando.f32(-100, 100)},
{rando.f32(-100, 100), rando.f32(-100, 100)},
{rando.f32(-100, 100), rando.f32(-100, 100)},
{rando.f32(-100, 100), rando.f32(-100, 100)},
};
check_cubic_coeffs_tangents_glsl(randos);
}
}
TEST_CASE("clamped_divide_glsl", "[bezier_utils]")
{
constexpr static float INF = std::numeric_limits<float>::infinity();
CHECK(glsl_cross::clamped_divide(1, 2) == .5f);
CHECK(glsl_cross::clamped_divide(-2, 0) == 0);
CHECK(glsl_cross::clamped_divide(1, 0) == 1);
CHECK(glsl_cross::clamped_divide(-INF, 1) == 0);
CHECK(glsl_cross::clamped_divide(INF, 1) == 1);
CHECK(glsl_cross::clamped_divide(INF, INF) == 1);
CHECK(glsl_cross::clamped_divide(-INF, -INF) == 1);
CHECK(glsl_cross::clamped_divide(-INF, INF) == 0);
CHECK(glsl_cross::clamped_divide(INF, -INF) == 0);
CHECK(glsl_cross::clamped_divide(0, 0) == 0);
CHECK(glsl_cross::clamped_divide(1, NAN) == 1);
CHECK(glsl_cross::clamped_divide(-1, NAN) == 0);
CHECK(glsl_cross::clamped_divide(NAN, 1) == 0);
CHECK(glsl_cross::clamped_divide(NAN, -1) == 0);
CHECK(glsl_cross::clamped_divide(NAN, NAN) == 0);
}
static void check_cubic_max_height_glsl(const Vec2D* pts)
{
float t, h = glsl_cross::find_cubic_max_height(simd::load2f(pts + 0),
simd::load2f(pts + 1),
simd::load2f(pts + 2),
simd::load2f(pts + 3),
t);
CHECK(h >= 0);
CHECK(t >= 0);
CHECK(t <= 1);
Vec2D norm = (pts[3] - pts[0]).normalized();
norm = {-norm.y, norm.x};
float k = -Vec2D::dot(norm, pts[0]);
auto heightAt = [=](float t) {
return fabsf(Vec2D::dot(norm, math::eval_cubic_at(pts, t)) + k);
};
CHECK(heightAt(t) == Approx(h).margin(1e-4f));
float epsilon = std::max(h, 1.f) * 1e-3f;
CHECK(h + epsilon > heightAt(0));
CHECK(h + epsilon > heightAt(1));
for (float t2 = 0; t2 <= 1; t2 += .005137f)
{
CHECK(h + epsilon > heightAt(t2));
}
}
TEST_CASE("find_cubic_max_height_glsl", "[bezier_utils]")
{
for (auto pts : TEST_CUBICS)
{
check_cubic_max_height_glsl(pts);
}
// Test all combinations of corners from the square [0,0,1,1]. This covers
// every cubic type as well as a wide variety of special cases for cusps,
// lines, loops, and inflections.
for (int i = 0; i < (1 << 8); ++i)
{
Vec2D pts[4] = {Vec2D((i >> 0) & 1, (i >> 1) & 1) * 100,
Vec2D((i >> 2) & 1, (i >> 3) & 1) * 100,
Vec2D((i >> 4) & 1, (i >> 5) & 1) * 100,
Vec2D((i >> 6) & 1, (i >> 7) & 1) * 100};
check_cubic_max_height_glsl(pts);
}
Rand rando;
rando.seed(0);
for (int i = 0; i < 100; ++i)
{
Vec2D randos[] = {
{rando.f32(-100, 100), rando.f32(-100, 100)},
{rando.f32(-100, 100), rando.f32(-100, 100)},
{rando.f32(-100, 100), rando.f32(-100, 100)},
{rando.f32(-100, 100), rando.f32(-100, 100)},
};
check_cubic_max_height_glsl(randos);
}
}
static float guess_cubic_local_curvature(const Vec2D* p,
const CubicCoeffs& coeffs,
float t,
float desiredSpread)
{
// Iteratively guess a spread and compare with the computed result.
float maxDT = fminf(t, 1 - t) * .9999f;
float2 tan = 3.f * ((coeffs.A * t + 2.f * coeffs.B) * t + coeffs.C);
float lengthTan = sqrtf(simd::dot(tan, tan));
tan /= lengthTan;
float dt = 0;
math::EvalCubic evalCubic(coeffs, p[0]);
// This would converge faster if we used the derivative of the Spread(dt)
// function from measure_cubic_local_curvature, but since the whole point of
// this test is to validate that function, use a binary search instead.
for (int i = 0; i < 24; ++i)
{
float guessDT = (dt + maxDT) * .5f;
float guessT0 = t - guessDT;
float guessT1 = t + guessDT;
float4 endpts = evalCubic(float4{guessT0, guessT0, guessT1, guessT1});
float spread = fabsf(simd::dot(endpts.zw - endpts.xy, tan));
if (spread <= desiredSpread)
dt = guessDT;
else
maxDT = guessDT;
}
Vec2D chops[10];
math::chop_cubic_at(p, chops, t - dt, t + dt);
return measure_non_inflect_cubic_rotation(chops + 3);
}
constexpr static float FEATHERING_CUSP_PADDING = 1e-3f;
static int find_cubic_convex_90_chops(const Vec2D pts[],
float outT[4],
float cuspPadding,
bool* areCusps)
{
assert(pts);
assert(outT);
assert(areCusps);
// Now find the cubic's inflection function.
// There are inflections where F' x F'' == 0.
//
// We formulate this as a quadratic equation:
//
// F' x F'' == a * T^2 + b * T + c == 0.
//
// See:
// https://www.microsoft.com/en-us/research/wp-content/uploads/2005/01/p1000-loop.pdf
// NOTE: We only need the roots, so a uniform scale factor does not affect
// the solution.
CubicCoeffs coeffs(pts);
float a = simd::cross(coeffs.A, coeffs.B);
float b_over_2 = simd::cross(coeffs.A, coeffs.C) * .5f;
float c = simd::cross(coeffs.B, coeffs.C);
float discr_over_4 = b_over_2 * b_over_2 - a * c;
// If -cuspThreshold <= discr_over_4 <= cuspThreshold, it means the two
// roots are within TESS_EPSILON of one another (in parametric space). This
// is close enough for our purposes to consider them a single cusp.
constexpr static float TESS_EPSILON = 1.f / (1 << 10);
float cuspThreshold = a * (TESS_EPSILON / 2);
cuspThreshold *= cuspThreshold;
// Find the first two chops, based on curve classification. Also fill in
// "tan90", which will define the second pair of chops as the two points
// perpendicular to "tan90".
float4 T;
float2 tan90;
if (discr_over_4 < -cuspThreshold ||
// Check if it's quadratic.
std::max(fabs(a), fabs(b_over_2)) < fabs(c) * TESS_EPSILON)
{
// The curve is a loop or quadratic.
// One chop is where rotation == 180 deg (which happens at infinity if
// the curve is quadratic).
// (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 + 2cT + 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
// can only rotate 180 degrees if the endpoints are colocated, and this
// gets handled next.
T.xy = {-c / b_over_2, 1};
// Next chop 90 degrees from the starting tangent of the curve.
tan90 = simd::any(coeffs.C != 0.f)
? coeffs.C
: math::bit_cast<float2>(pts[2] - pts[0]);
*areCusps = false;
}
else if (discr_over_4 > cuspThreshold)
{
// The curve is serpentine. Solve for the two inflection points.
float q = sqrtf(discr_over_4);
q = -b_over_2 - copysignf(q, b_over_2);
T.xy = float2{q, c} / float2{a, q};
// Next chop 90 degrees from the whichever inflection point is closest
// to the middle.
float t = fabsf(T.x - .5f) < fabsf(T.y - .5f) ? T.x : T.y;
tan90 = (coeffs.A * t + 2.f * coeffs.B) * t + coeffs.C;
*areCusps = false;
}
else
{
// The curve is a cusp. A proper cusp is at T=-b/2a, but just solving
// for 90 degrees from the starting tangent will also find it, in
// addition to finding cusps from degenerate flat lines reversing
// direction. Since 180 degrees of rotation is lost to the cusp, we only
// need to find 2 roots max.
T.xy = 1;
tan90 = simd::any(coeffs.C != 0.f)
? coeffs.C
: math::bit_cast<float2>(pts[2] - pts[0]);
*areCusps = true;
}
// Find a second set of chops where the curve is perpendicular to tan90.
//
// Tangent_Direction(T) dot tan90 == 0
// (A dot tan90) * T^2 + (2B dot tan90) * T + (C dot tan90) == 0
//
a = simd::dot(coeffs.A, tan90);
b_over_2 = simd::dot(coeffs.B, tan90);
c = simd::dot(coeffs.C, tan90);
discr_over_4 = b_over_2 * b_over_2 - a * c;
float q = sqrtf(discr_over_4);
q = -b_over_2 - copysignf(q, b_over_2);
T.zw = float2{q, c} / float2{a, q};
// Throw out T <= epsilon and T >= epsilon by converting them to 1.
// (Use logic such that NaN also converts to 1.)
T = simd::if_then_else((T > 0) & (T < 1), T, float4(1));
assert(simd::all(T > 0));
assert(simd::all(T <= 1));
// Sort the roots.
T = simd::if_then_else((float2{T.x, T.z} < float2{T.y, T.w}).xxyy,
T,
T.yxwz);
T = simd::if_then_else((float2{T.x, T.y} < float2{T.z, T.w}).xyxy,
T,
T.zwxy);
T = T.y < T.z ? T : T.xzyw;
// Count the number of roots that != 1 and store T.
int4 n4 = (T != 1) & 1;
n4.xy += n4.zw;
int n = n4.x + n4.y;
simd::store(outT, T);
if (*areCusps && n > 0)
{
// Generate padding around cusp points. Odd numbered chops are always
// padding sections that pass through a cusp.
assert(n <= 2);
for (int i = n - 1; i >= 0; --i)
{
float maxT = i == n - 1 ? 1 : outT[i * 2 + 1];
float minT = i == 0 ? 0 : (outT[i - 1] + outT[i]) * .5f;
outT[i * 2 + 1] = std::min(outT[i] + cuspPadding, maxT);
outT[i * 2 + 0] = std::max(outT[i] - cuspPadding, minT);
}
n *= 2;
// Re-clip and re-sort n after adding cusp padding. This is a hack, but
// we leave it here for now becase we're about to remove this entire
// method in favor of something more robust.
if (outT[n - 1] == 1)
{
--n;
}
std::sort(outT, outT + n);
}
return n;
}
static void check_cubic_convex_90_chops(const Vec2D* pts)
{
float T[4];
bool areCusps;
int n =
find_cubic_convex_90_chops(pts, T, FEATHERING_CUSP_PADDING, &areCusps);
CHECK(n <= 4);
Vec2D chops[16];
assert(n * 3 + 1 <= std::size(chops));
math::chop_cubic_at(pts, chops, T, n);
Vec2D* p = chops;
for (int i = 0; i <= n; ++i, p += 3)
{
if (areCusps && (i & 1))
{
// If the chops are around a cusp, odd-numbered chops are a padding
// section that passes through a cusp.
continue;
}
CHECK(measure_non_inflect_cubic_rotation(p) <=
Approx(math::PI / 2).margin(1e-2f));
}
}
TEST_CASE("find_cubic_convex_90_chops", "[bezier_utils]")
{
for (auto pts : TEST_CUBICS)
{
check_cubic_convex_90_chops(pts);
}
// Test all combinations of corners from the square [0,0,1,1]. This covers
// every cubic type as well as a wide variety of special cases for cusps,
// lines, loops, and inflections.
for (int i = 0; i < (1 << 8); ++i)
{
Vec2D pts[4] = {Vec2D((i >> 0) & 1, (i >> 1) & 1) * 100,
Vec2D((i >> 2) & 1, (i >> 3) & 1) * 100,
Vec2D((i >> 4) & 1, (i >> 5) & 1) * 100,
Vec2D((i >> 6) & 1, (i >> 7) & 1) * 100};
check_cubic_convex_90_chops(pts);
}
Rand rando;
rando.seed(0);
for (int i = 0; i < 100; ++i)
{
Vec2D randos[] = {
{rando.f32(-100, 100), rando.f32(-100, 100)},
{rando.f32(-100, 100), rando.f32(-100, 100)},
{rando.f32(-100, 100), rando.f32(-100, 100)},
{rando.f32(-100, 100), rando.f32(-100, 100)},
};
check_cubic_convex_90_chops(randos);
}
}
static void check_cubic_local_curvature_glsl(const Vec2D* pts)
{
float T[4];
bool areCusps;
int n =
find_cubic_convex_90_chops(pts, T, FEATHERING_CUSP_PADDING, &areCusps);
CHECK(n <= 4);
Vec2D chops[16];
assert(n * 3 + 1 <= std::size(chops));
math::chop_cubic_at(pts, chops, T, n);
Vec2D* p = chops;
for (int i = 0; i <= n; ++i, p += 3)
{
if (areCusps && (i & 1))
{
// If the chops are around a cusp, odd-numbered chops are a padding
// section that passes through a cusp.
continue;
}
float2 p0 = simd::load2f(p + 0);
float2 p1 = simd::load2f(p + 1);
float2 p2 = simd::load2f(p + 2);
float2 p3 = simd::load2f(p + 3);
float maxHeightT;
CHECK(measure_non_inflect_cubic_rotation(p) <=
Approx(math::PI / 2).margin(2.6e-3f));
glsl_cross::find_cubic_max_height(p0, p1, p2, p3, maxHeightT);
CubicCoeffs coeffs(p);
for (float desiredSpread : {1.f, 10.f, 100.f})
{
for (float t : {maxHeightT, .5f})
{
float theta =
glsl_cross::measure_cubic_local_curvature(p0,
p1,
p2,
p3,
t,
desiredSpread);
CHECK(theta >= 0);
CHECK(theta <= math::PI);
float guess =
guess_cubic_local_curvature(p, coeffs, t, desiredSpread);
CHECK(theta == Approx(guess).margin(1e-2f));
}
}
}
}
TEST_CASE("measure_cubic_local_curvature_glsl", "[bezier_utils]")
{
for (auto pts : TEST_CUBICS)
{
check_cubic_local_curvature_glsl(pts);
}
// Test all combinations of corners from the square [0,0,1,1]. This covers
// every cubic type as well as a wide variety of special cases for cusps,
// lines, loops, and inflections.
for (int i = 0; i < (1 << 8); ++i)
{
Vec2D pts[4] = {Vec2D((i >> 0) & 1, (i >> 1) & 1) * 100,
Vec2D((i >> 2) & 1, (i >> 3) & 1) * 100,
Vec2D((i >> 4) & 1, (i >> 5) & 1) * 100,
Vec2D((i >> 6) & 1, (i >> 7) & 1) * 100};
check_cubic_local_curvature_glsl(pts);
}
Rand rando;
rando.seed(0);
for (int i = 0; i < 100; ++i)
{
Vec2D randos[] = {
{rando.f32(-100, 100), rando.f32(-100, 100)},
{rando.f32(-100, 100), rando.f32(-100, 100)},
{rando.f32(-100, 100), rando.f32(-100, 100)},
{rando.f32(-100, 100), rando.f32(-100, 100)},
};
check_cubic_local_curvature_glsl(randos);
}
}
#endif
} // namespace math
} // namespace rive