blob: b5add32c1bf47679dd5ae6223630d6ec84aa50d8 [file]
/*
* Copyright 2024 Rive
*/
#include "rive/math/math_types.hpp"
#include "../src/rive_render_path.hpp"
#include <catch.hpp>
namespace rive::gpu
{
class PLSTestPath : public RiveRenderPath
{
public:
void addRect(float x, float y, float width, float height, PathDirection wind)
{
RawPath rawPath;
rawPath.addRect({x, y, x + width, y + height}, wind);
RiveRenderPath path2(FillRule::nonZero, rawPath);
addRenderPath(&path2, Mat2D());
}
void addCircle(float x, float y, float radius, PathDirection wind)
{
RawPath rawPath;
rawPath.addOval({x - radius, y - radius, x + radius, y + radius}, wind);
RiveRenderPath path2(FillRule::nonZero, rawPath);
addRenderPath(&path2, Mat2D());
}
};
// Check that RiveRenderPath::getCoarseArea() is positive for clockwise paths
// and negative for counterclockwise. Also check that it's within a margin of
// error of the true area for curves.
TEST_CASE("getCoarseArea", "[RiveRenderPath]")
{
PLSTestPath path;
CHECK(path.getCoarseArea() == 0);
path.addRect(10, 20, 10, 200, PathDirection::clockwise); // 2000 px
CHECK(path.getCoarseArea() == 2000);
path.addRect(15, 30, 2, 20, PathDirection::counterclockwise); // -40 px
CHECK(path.getCoarseArea() == (2000 - 40));
path.addRect(0, 0, 1, 1, PathDirection::clockwise); // +1 px
CHECK(path.getCoarseArea() == (2000 - 40 + 1));
path.addRect(-1, -1, 300, 100,
PathDirection::counterclockwise); // -30000 px
CHECK(path.getCoarseArea() == (2000 - 40 + 1 - 30000));
CHECK(path.getCoarseArea() == (2000 - 40 + 1 - 30000));
path.rewind();
CHECK(path.getCoarseArea() == 0);
CHECK(path.getCoarseArea() == 0);
path.addCircle(0, 0, 1000, PathDirection::clockwise);
CHECK(
path.getCoarseArea() / (math::PI * 1000 * 1000) ==
Approx(1).margin(1e-2f));
path.rewind();
path.addCircle(30, -100, 900, PathDirection::counterclockwise);
CHECK(
path.getCoarseArea() / (math::PI * 900 * 900) ==
Approx(-1).margin(1e-2f));
path.addCircle(0, 0, 1000, PathDirection::clockwise);
CHECK(
path.getCoarseArea() / (math::PI * 1000 * 1000 - math::PI * 900 * 900) ==
Approx(1).margin(1e-2f));
}
TEST_CASE("addRawPath invalidates derived state", "[RiveRenderPath]")
{
RiveRenderPath path;
RawPath first;
first.addRect({0, 0, 10, 10}, PathDirection::clockwise);
path.addRawPath(first);
// Warm the caches so a missing invalidation shows up below.
CHECK(path.getBounds().right() == 10);
CHECK(path.getCoarseArea() == 100);
uint64_t firstMutationID = path.getRawPathMutationID();
RawPath second;
second.addRect({20, 20, 40, 40}, PathDirection::clockwise);
path.addRawPath(second);
CHECK(path.getBounds().right() == 40);
CHECK(path.getCoarseArea() == 100 + 400);
CHECK(path.getRawPathMutationID() != firstMutationID);
}
// A script can hand us a contour of coincident points, which the tessellator
// cannot find a join tangent in.
TEST_CASE("addUntrustedRawPath prunes empty segments", "[RiveRenderPath]")
{
RawPath degenerate;
degenerate.move({304, 160});
degenerate.line({304, 160});
degenerate.line({304, 160});
degenerate.line({304, 160});
degenerate.close();
RiveRenderPath path;
path.addUntrustedRawPath(degenerate);
const RawPath& pruned = path.getRawPath();
REQUIRE(pruned.verbs().size() == 2);
CHECK(pruned.verbs()[0] == PathVerb::move);
CHECK(pruned.verbs()[1] == PathVerb::close);
CHECK(pruned.points().size() == 1);
// The trusted entry point stays a plain append.
RiveRenderPath trusted;
trusted.addRawPath(degenerate);
CHECK(trusted.getRawPath().verbs().size() == 5);
}
// Scale/translate matrices take a shortcut through the cached bounds. It has to
// match mapping every point exactly.
TEST_CASE(
"calculatePixelBounds matches mapping every point",
"[RiveRenderPath]")
{
srand(0);
auto randf = [](float lo, float hi) {
return lo + (hi - lo) * (rand() / static_cast<float>(RAND_MAX));
};
const Mat2D matrices[] = {
Mat2D(),
Mat2D(1, 0, 0, 1, 10.25f, -7.5f),
Mat2D(3.7f, 0, 0, 0.31f, 100.1f, 20.9f),
Mat2D(-2.5f, 0, 0, 1.75f, 512.3f, 3.3f),
Mat2D(0.4f, 0, 0, -6.1f, -40.6f, 900.7f),
Mat2D(-1, 0, 0, -1, 0.5f, 0.5f),
Mat2D(0.8f, 0.6f, -0.6f, 0.8f, 30, 40), // Rotation takes the slow path.
};
for (int i = 0; i < 200; ++i)
{
RawPath rawPath;
rawPath.move({randf(-500, 500), randf(-500, 500)});
for (int j = rand() % 8; j >= 0; --j)
{
rawPath.cubic(
{randf(-500, 500), randf(-500, 500)},
{randf(-500, 500), randf(-500, 500)},
{randf(-500, 500), randf(-500, 500)});
}
RiveRenderPath path(FillRule::nonZero, rawPath);
for (const Mat2D& m : matrices)
{
IAABB expected = m.mapBoundingBox(path.getRawPath().points()).roundOut();
CHECK(path.calculatePixelBounds(m, std::nullopt, 0) == expected);
}
}
}
} // namespace rive::gpu