| /* |
| * 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 |