| /* |
| * Copyright 2022 Rive |
| */ |
| |
| #include <rive/math/contour_measure.hpp> |
| #include <rive/math/math_types.hpp> |
| #include <rive/math/raw_path.hpp> |
| #include <rive/math/vec2d.hpp> |
| |
| #include "rive_file_reader.hpp" |
| #include "rive/animation/state_machine_instance.hpp" |
| |
| #include <catch.hpp> |
| #include <cstdio> |
| |
| using namespace rive; |
| |
| static bool nearly_eq(float a, float b, float tolerance) |
| { |
| assert(tolerance >= 0); |
| const float diff = std::abs(a - b); |
| const float max = std::max(std::abs(a), std::abs(b)); |
| const float allowed = tolerance * max; |
| if (diff > allowed) |
| { |
| printf("%g %g delta %g allowed %g\n", a, b, diff, allowed); |
| return false; |
| } |
| return true; |
| } |
| |
| static bool nearly_eq(Vec2D a, Vec2D b, float tol) |
| { |
| return nearly_eq(a.x, b.x, tol) && nearly_eq(a.y, b.y, tol); |
| } |
| |
| TEST_CASE("contour-basics", "[contourmeasure]") |
| { |
| const float tol = 0.000001f; |
| |
| RawPath path; |
| ContourMeasureIter iter(&path); |
| REQUIRE(iter.next() == nullptr); |
| |
| path.moveTo(1, 2); |
| iter.rewind(&path); |
| REQUIRE(iter.next() == nullptr); |
| |
| path.lineTo(4, 6); |
| iter.rewind(&path); |
| auto cm = iter.next(); |
| REQUIRE(cm); |
| REQUIRE(nearly_eq(cm->length(), 5, tol)); |
| REQUIRE(iter.next() == nullptr); |
| |
| // check the mid-points of a rectangle |
| |
| path = RawPath(); |
| const float w = 4, h = 6; |
| path.addRect({0, 0, w, h}, PathDirection::cw); |
| iter.rewind(&path); |
| cm = iter.next(); |
| REQUIRE(cm); |
| REQUIRE(nearly_eq(cm->length(), 2 * (w + h), tol)); |
| const float midDistances[] = { |
| w / 2, |
| w + h / 2, |
| w + h + w / 2, |
| w + h + w + h / 2, |
| }; |
| const ContourMeasure::PosTan midPoints[] = { |
| {{w / 2, 0}, {1, 0}}, |
| {{w, h / 2}, {0, 1}}, |
| {{w / 2, h}, {-1, 0}}, |
| {{0, h / 2}, {0, -1}}, |
| }; |
| for (int i = 0; i < 4; ++i) |
| { |
| auto rec = cm->getPosTan(midDistances[i]); |
| REQUIRE(nearly_eq(rec.pos, midPoints[i].pos, tol)); |
| REQUIRE(nearly_eq(rec.tan, midPoints[i].tan, tol)); |
| } |
| REQUIRE(iter.next() == nullptr); |
| } |
| |
| TEST_CASE("multi-contours", "[contourmeasure]") |
| { |
| const Vec2D pts[] = { |
| {0, 0}, |
| {3, 0}, |
| {3, 4}, |
| }; |
| auto span = make_span(pts, sizeof(pts) / sizeof(pts[0])); |
| |
| // We expect 3 measurable contours out of this: 7, 16, 7 |
| // the others should be skipped since they are empty (len == 0) |
| |
| RawPath path; |
| path.addPoly(span, false); // len == 7 |
| |
| path.addPoly(span, true); // len == 12 |
| |
| // should be skipped (lengh == 0) |
| path.moveTo(0, 0); |
| |
| // should be skipped (lengh == 0) |
| path.moveTo(0, 0); |
| path.close(); |
| |
| // should be skipped (lengh == 0) |
| path.moveTo(0, 0); |
| path.lineTo(0, 0); |
| |
| // should be skipped (lengh == 0) |
| path.moveTo(0, 0); |
| path.lineTo(0, 0); |
| path.close(); |
| |
| path.addPoly(span, false); // len == 7 |
| |
| ContourMeasureIter iter(&path); |
| auto cm = iter.next(); |
| REQUIRE(cm->length() == 7); |
| cm = iter.next(); |
| REQUIRE(cm->length() == 12); |
| cm = iter.next(); |
| REQUIRE(cm->length() == 7); |
| cm = iter.next(); |
| REQUIRE(!cm); |
| } |
| |
| TEST_CASE("contour-oval", "[contourmeasure]") |
| { |
| const float tol = 0.0075f; |
| |
| const float r = 10; |
| RawPath path; |
| path.addOval({-r, -r, r, r}, PathDirection::cw); |
| ContourMeasureIter iter(&path, tol); |
| |
| auto cm = iter.next(); |
| REQUIRE(nearly_eq(cm->length(), 2 * r * math::PI, tol)); |
| REQUIRE(!iter.next()); |
| } |
| |
| TEST_CASE("bad contour", "[contourmeasure]") |
| { |
| auto file = ReadRiveFile("../../test/assets/zombie_skins.riv"); |
| |
| auto artboard = file->artboard()->instance(); |
| REQUIRE(artboard != nullptr); |
| auto machine = artboard->defaultStateMachine(); |
| machine->advanceAndApply(0.0f); |
| } |
| |
| // NaN paths don't return contours. |
| TEST_CASE("nan-path", "[contourmeasure]") |
| { |
| RawPath path; |
| path.lineTo(1, 2); |
| path.cubicTo(3, 4, 5, 6, 7, 8); |
| path.cubicTo(9, 10, 11, 12, 13, 14); |
| path.cubicTo(15, 16, 17, 18, 19, 20); |
| |
| { |
| ContourMeasureIter iter(&path); |
| auto cm = iter.next(); |
| CHECK(cm != nullptr); |
| CHECK(std::isfinite(cm->length())); |
| CHECK(iter.next() == nullptr); |
| } |
| |
| { |
| auto nan = std::numeric_limits<float>::quiet_NaN(); |
| RawPath path_ = path.transform(Mat2D(nan, nan, nan, nan, nan, nan)); |
| ContourMeasureIter iter(&path_); |
| CHECK(iter.next() == nullptr); |
| } |
| } |
| |
| // Regression test for a crash found by fuzzing. |
| TEST_CASE("fuzz_issue_7295", "[MetricsPath]") |
| { |
| NoOpFactory factory; |
| |
| RawPath innerPath; |
| innerPath.moveTo(.0f, -20.5f); |
| innerPath.cubicTo(11.3218384f, -20.5f, 20.5f, -11.3218384f, 20.5f, .0f); |
| innerPath.cubicTo(20.5f, 11.3218384f, 11.3218384f, 20.5f, .0f, 20.5f); |
| innerPath.cubicTo(-11.3218384f, 20.5f, -20.5f, 11.3218384f, -20.5f, .0f); |
| innerPath.cubicTo(-20.5f, -11.3218384f, -11.3218384f, -20.5f, .0f, -20.5f); |
| |
| RawPath outerPath; |
| Mat2D transform(1.f, .0f, .0f, 1.f, -134217728.f, -134217728.f); |
| outerPath.addPath(innerPath, &transform); |
| |
| auto contour = ContourMeasureIter(&outerPath).next(); |
| RawPath result; |
| contour->getSegment(.0f, 168.389008f, &result, true); |
| CHECK(math::nearly_equal(contour->length(), 168.389008f)); |
| } |