fix(runtime): keep coincident control points exact when trimming paths (#14474) dcd60ea7f5 Co-authored-by: Philip Chung <philterdesign@gmail.com>
diff --git a/.rive_head b/.rive_head index 53b26e1..4e00c48 100644 --- a/.rive_head +++ b/.rive_head
@@ -1 +1 @@ -0b5fd6293b7655e31839cbdb4cf6319fe898bdd8 +dcd60ea7f53e84aafd1b10001667d8946a2b8c85
diff --git a/include/rive/math/raw_path_utils.hpp b/include/rive/math/raw_path_utils.hpp index 56aab47..f838786 100644 --- a/include/rive/math/raw_path_utils.hpp +++ b/include/rive/math/raw_path_utils.hpp
@@ -46,9 +46,6 @@ // Extract a subcurve from the curve (given start and end t-values) -extern void quad_subdivide(const Vec2D src[3], float t, Vec2D dst[5]); -extern void cubic_subdivide(const Vec2D src[4], float t, Vec2D dst[7]); - extern void line_extract(const Vec2D src[2], float startT, float endT,
diff --git a/src/math/raw_path_utils.cpp b/src/math/raw_path_utils.cpp index 3d11908..b36158c 100644 --- a/src/math/raw_path_utils.cpp +++ b/src/math/raw_path_utils.cpp
@@ -2,41 +2,28 @@ * Copyright 2022 Rive */ -#include "rive/math/math_types.hpp" #include "rive/math/raw_path_utils.hpp" -#include <cmath> +#include "rive/math/bezier_utils.hpp" +#include <algorithm> -// Extract subsets - -void rive::quad_subdivide(const rive::Vec2D src[3], float t, rive::Vec2D dst[5]) +// Exact at t=0, t=1 and when a == b, so coincident points survive a chop. +static rive::Vec2D exact_lerp(rive::Vec2D a, rive::Vec2D b, float t) { - assert(t >= 0 && t <= 1); - auto ab = lerp(src[0], src[1], t); - auto bc = lerp(src[1], src[2], t); - dst[0] = src[0]; - dst[1] = ab; - dst[2] = lerp(ab, bc, t); - dst[3] = bc; - dst[4] = src[2]; + return t < 0.5f ? a + (b - a) * t : b - (b - a) * (1 - t); } -void rive::cubic_subdivide(const rive::Vec2D src[4], +static void quad_subdivide(const rive::Vec2D src[3], float t, - rive::Vec2D dst[7]) + rive::Vec2D dst[5]) { assert(t >= 0 && t <= 1); - auto ab = lerp(src[0], src[1], t); - auto bc = lerp(src[1], src[2], t); - auto cd = lerp(src[2], src[3], t); - auto abc = lerp(ab, bc, t); - auto bcd = lerp(bc, cd, t); + auto ab = exact_lerp(src[0], src[1], t); + auto bc = exact_lerp(src[1], src[2], t); dst[0] = src[0]; dst[1] = ab; - dst[2] = abc; - dst[3] = lerp(abc, bcd, t); - dst[4] = bcd; - dst[5] = cd; - dst[6] = src[3]; + dst[2] = exact_lerp(ab, bc, t); + dst[3] = bc; + dst[4] = src[2]; } void rive::line_extract(const rive::Vec2D src[2], @@ -47,8 +34,8 @@ assert(startT <= endT); assert(startT >= 0 && endT <= 1); - dst[0] = lerp(src[0], src[1], startT); - dst[1] = lerp(src[0], src[1], endT); + dst[0] = exact_lerp(src[0], src[1], startT); + dst[1] = exact_lerp(src[0], src[1], endT); } void rive::quad_extract(const rive::Vec2D src[3], @@ -66,20 +53,20 @@ } else if (startT == 0) { - rive::quad_subdivide(src, endT, tmp); + quad_subdivide(src, endT, tmp); std::copy(tmp, tmp + 3, dst); } else if (endT == 1) { - rive::quad_subdivide(src, startT, tmp); + quad_subdivide(src, startT, tmp); std::copy(tmp + 2, tmp + 5, dst); } else { assert(endT > 0); - rive::quad_subdivide(src, endT, tmp); + quad_subdivide(src, endT, tmp); rive::Vec2D tmp2[5]; - rive::quad_subdivide(tmp, startT / endT, tmp2); + quad_subdivide(tmp, startT / endT, tmp2); std::copy(tmp2 + 2, tmp2 + 5, dst); } } @@ -89,30 +76,7 @@ float endT, rive::Vec2D dst[4]) { - assert(startT <= endT); - assert(startT >= 0 && endT <= 1); - - rive::Vec2D tmp[7]; - if (startT == 0 && endT == 1) - { - std::copy(src, src + 4, dst); - } - else if (startT == 0) - { - rive::cubic_subdivide(src, endT, tmp); - std::copy(tmp, tmp + 4, dst); - } - else if (endT == 1) - { - rive::cubic_subdivide(src, startT, tmp); - std::copy(tmp + 3, tmp + 7, dst); - } - else - { - assert(endT > 0); - rive::cubic_subdivide(src, endT, tmp); - rive::Vec2D tmp2[7]; - rive::cubic_subdivide(tmp, startT / endT, tmp2); - std::copy(tmp2 + 3, tmp2 + 7, dst); - } + rive::Vec2D tmp[10]; + rive::math::chop_cubic_at(src, tmp, startT, endT); + std::copy(tmp + 3, tmp + 7, dst); }
diff --git a/tests/unit_tests/runtime/contour_measure_test.cpp b/tests/unit_tests/runtime/contour_measure_test.cpp index 7788b25..9c2c7a2 100644 --- a/tests/unit_tests/runtime/contour_measure_test.cpp +++ b/tests/unit_tests/runtime/contour_measure_test.cpp
@@ -199,3 +199,40 @@ contour->getSegment(.0f, 168.389008f, &result, true); CHECK(math::nearly_equal(contour->length(), 168.389008f)); } + +// A trim chop must keep P1 == P0 exact, or the stroker reads the fp32 residue +// as the start tangent (fan + detached cap on a straight-vertex shoulder). +TEST_CASE("trim keeps coincident control points", "[contourmeasure]") +{ + const Vec2D p0 = {244.19449f, 186.5583f}; + const Vec2D p2 = {160.f, 90.f}; + const Vec2D p3 = {330.f, 280.f}; + for (int i = 1; i < 1000; ++i) + { + RawPath path; + path.move(p0); + path.cubic(p0, p2, p3); + ContourMeasureIter iter(&path); + auto cm = iter.next(); + REQUIRE(cm != nullptr); + + const float d = cm->length() * i / 1000.f; + RawPath head; + cm->getSegment(0, d, &head, true); + const Vec2D* hp = head.points().data(); + REQUIRE(head.points().size() == 4); + CHECK(hp[1] == hp[0]); + + // Mirror case: coincident end control points on the tail. + RawPath rpath; + rpath.move(p3); + rpath.cubic(p2, p0, p0); + ContourMeasureIter riter(&rpath); + auto rcm = riter.next(); + RawPath tail; + rcm->getSegment(rcm->length() - d, rcm->length(), &tail, true); + const Vec2D* tp = tail.points().data(); + REQUIRE(tail.points().size() == 4); + CHECK(tp[2] == tp[3]); + } +}