blob: 4e5b41a778dbb7e5adecc7d94ccee4eb95e12580 [file]
/*
* Copyright 2022 Rive
*/
#include <rive/math/aabb.hpp>
#include <rive/math/raw_path.hpp>
#include <catch.hpp>
#include <cstdio>
#include <limits>
#include <tuple>
using namespace rive;
TEST_CASE("rawpath-basics", "[rawpath]")
{
RawPath path;
REQUIRE(path.empty());
REQUIRE(path.bounds() == AABB{0, 0, 0, 0});
path.move({1, 2});
REQUIRE(!path.empty());
REQUIRE(path.bounds() == AABB{1, 2, 1, 2});
path = RawPath();
REQUIRE(path.empty());
REQUIRE(path.bounds() == AABB{0, 0, 0, 0});
path.move({1, -2});
path.line({3, 4});
path.line({-1, 5});
REQUIRE(!path.empty());
REQUIRE(path.bounds() == AABB{-1, -2, 3, 5});
}
TEST_CASE("rawpath-add-helpers", "[rawpath]")
{
RawPath path;
path.addRect({1, 1, 5, 6});
REQUIRE(!path.empty());
REQUIRE(path.bounds() == AABB{1, 1, 5, 6});
REQUIRE(path.points().size() == 4);
REQUIRE(path.verbs().size() == 5); // move, line, line, line, close
path = RawPath();
path.addOval({0, 0, 3, 6});
REQUIRE(!path.empty());
REQUIRE(path.bounds() == AABB{0, 0, 3, 6});
REQUIRE(path.points().size() == 13);
REQUIRE(path.verbs().size() == 6); // move, cubic, cubic, cubic, cubic, close
const Vec2D pts[] = {
{1, 2},
{4, 5},
{3, 2},
{100, -100},
};
constexpr auto size = sizeof(pts) / sizeof(pts[0]);
for (auto isClosed : {false, true})
{
path = RawPath();
path.addPoly({pts, size}, isClosed);
REQUIRE(path.bounds() == AABB{1, -100, 100, 5});
REQUIRE(path.points().size() == size);
REQUIRE(path.verbs().size() == size + isClosed);
for (size_t i = 0; i < size; ++i)
{
REQUIRE(path.points()[i] == pts[i]);
}
REQUIRE(path.verbs()[0] == PathVerb::move);
for (size_t i = 1; i < size; ++i)
{
REQUIRE(path.verbs()[i] == PathVerb::line);
}
if (isClosed)
{
REQUIRE(path.verbs()[size] == PathVerb::close);
}
}
}
//////////////////////////////////////////////////////////////////////////
static void check_iter(
RawPath::Iter& iter,
const RawPath::Iter& end,
PathVerb expectedVerb,
std::vector<Vec2D> expectedPts)
{
REQUIRE(iter != end);
PathVerb verb = iter.verb();
const Vec2D* pts = iter.pts();
REQUIRE(verb == expectedVerb);
switch (verb)
{
case PathVerb::move:
CHECK(expectedPts.size() == 1);
CHECK(pts[0] == iter.movePt());
break;
case PathVerb::line:
CHECK(expectedPts.size() == 2);
CHECK(pts == iter.linePts());
break;
case PathVerb::quad:
CHECK(expectedPts.size() == 3);
CHECK(pts == iter.quadPts());
break;
case PathVerb::cubic:
CHECK(expectedPts.size() == 4);
CHECK(pts == iter.cubicPts());
break;
case PathVerb::close:
CHECK(expectedPts.size() == 0);
CHECK(pts == iter.rawPtsPtr() - 1);
break;
}
for (size_t i = 0; i < expectedPts.size(); ++i)
{
CHECK(pts[i] == expectedPts[i]);
}
++iter;
}
TEST_CASE("rawpath-iter", "[rawpath]")
{
{
RawPath rp;
REQUIRE(rp.begin() == rp.end());
}
{
RawPath rp;
rp.moveTo(1, 2);
rp.lineTo(3, 4);
rp.quadTo(5, 6, 7, 8);
rp.cubicTo(9, 10, 11, 12, 13, 14);
rp.close();
auto iter = rp.begin();
auto end = rp.end();
check_iter(iter, end, PathVerb::move, {{1, 2}});
check_iter(iter, end, PathVerb::line, {{1, 2}, {3, 4}});
check_iter(iter, end, PathVerb::quad, {{3, 4}, {5, 6}, {7, 8}});
check_iter(
iter,
end,
PathVerb::cubic,
{{7, 8}, {9, 10}, {11, 12}, {13, 14}});
check_iter(iter, end, PathVerb::close, {});
REQUIRE(iter == end);
// Moves are never discarded.
rp.reset();
rp.moveTo(1, 2);
rp.moveTo(3, 4);
rp.moveTo(5, 6);
rp.close();
std::tie(iter, end) = std::make_tuple(rp.begin(), rp.end());
check_iter(iter, end, PathVerb::move, {{1, 2}});
check_iter(iter, end, PathVerb::move, {{3, 4}});
check_iter(iter, end, PathVerb::move, {{5, 6}});
check_iter(iter, end, PathVerb::close, {});
REQUIRE(iter == end);
// lineTo, quadTo, and cubicTo can inject implicit moveTos.
rp.rewind();
rp.close(); // discarded
rp.close(); // discarded
rp.close(); // discarded
rp.close(); // discarded
rp.lineTo(1, 2); // injects moveTo(0, 0)
rp.close(); // kept
rp.close(); // discarded
rp.cubicTo(3, 4, 5, 6, 7, 8); // injects moveTo(0, 0)
rp.moveTo(9, 10);
rp.moveTo(11, 12);
rp.quadTo(13, 14, 15, 16);
rp.close(); // kept
rp.lineTo(17, 18); // injects moveTo(11, 12)
std::tie(iter, end) = std::make_tuple(rp.begin(), rp.end());
check_iter(iter, end, PathVerb::move, {{0, 0}});
check_iter(iter, end, PathVerb::line, {{0, 0}, {1, 2}});
check_iter(iter, end, PathVerb::close, {});
check_iter(iter, end, PathVerb::move, {{0, 0}});
check_iter(iter, end, PathVerb::cubic, {{0, 0}, {3, 4}, {5, 6}, {7, 8}});
check_iter(iter, end, PathVerb::move, {{9, 10}});
check_iter(iter, end, PathVerb::move, {{11, 12}});
check_iter(iter, end, PathVerb::quad, {{11, 12}, {13, 14}, {15, 16}});
check_iter(iter, end, PathVerb::close, {});
check_iter(iter, end, PathVerb::move, {{11, 12}});
check_iter(iter, end, PathVerb::line, {{11, 12}, {17, 18}});
REQUIRE(iter == end);
}
}
TEST_CASE("addPath", "[rawpath]")
{
using PathMaker = void (*)(RawPath* sink);
const PathMaker makers[] = {
[](RawPath* sink) {},
[](RawPath* sink) {
sink->moveTo(1, 2);
sink->lineTo(3, 4);
},
[](RawPath* sink) {
sink->moveTo(1, 2);
sink->lineTo(3, 4);
sink->close();
},
[](RawPath* sink) {
sink->moveTo(1, 2);
sink->lineTo(3, 4);
sink->quadTo(5, 6, 7, 8);
sink->cubicTo(9, 10, 11, 12, 13, 14);
sink->close();
},
};
constexpr size_t N = sizeof(makers) / sizeof(makers[0]);
auto direct = [](PathMaker m0, PathMaker m1, const Mat2D* mx) {
RawPath p;
m0(&p);
m1(&p);
if (mx)
{
p.transformInPlace(*mx);
}
return p;
};
auto useadd = [](PathMaker m0, PathMaker m1, const Mat2D* mx) {
RawPath p;
RawPath tmp;
m0(&tmp);
p.addPath(tmp, mx);
tmp.reset();
m1(&tmp);
p.addPath(tmp, mx);
return p;
};
for (auto i = 0; i < N; ++i)
{
for (auto j = 0; j < N; ++j)
{
RawPath p0, p1;
p0 = direct(makers[i], makers[j], nullptr);
p1 = useadd(makers[i], makers[j], nullptr);
REQUIRE(p0 == p1);
auto mx = Mat2D::fromScale(2, 3);
p0 = direct(makers[i], makers[j], &mx);
p1 = useadd(makers[i], makers[j], &mx);
REQUIRE(p0 == p1);
}
}
}
TEST_CASE("bounds", "[rawpath]")
{
RawPath path;
AABB bounds;
srand(0);
const auto randPt = [&] {
Vec2D pt = Vec2D(float(rand()), float(rand())) / (float(RAND_MAX) * .5f) -
Vec2D(1, 1);
bounds.minX = std::min(bounds.minX, pt.x);
bounds.minY = std::min(bounds.minY, pt.y);
bounds.maxX = std::max(bounds.maxX, pt.x);
bounds.maxY = std::max(bounds.maxY, pt.y);
return pt;
};
for (int numVerbs = 1; numVerbs < 1 << 16; numVerbs <<= 1)
{
path.rewind();
bounds.minX = bounds.minY = std::numeric_limits<float>::infinity();
bounds.maxX = bounds.maxY = -std::numeric_limits<float>::infinity();
for (int i = 0; i < numVerbs; ++i)
{
switch (rand() % 5)
{
case 0:
path.move(randPt());
break;
case 1:
if (path.empty())
{ // Account for the implicit moveTo(0).
bounds = {};
}
path.line(randPt());
break;
case 2:
if (path.empty())
{ // Account for the implicit moveTo(0).
bounds = {};
}
path.quad(randPt(), randPt());
break;
case 3:
if (path.empty())
{ // Account for the implicit moveTo(0).
bounds = {};
}
path.cubic(randPt(), randPt(), randPt());
break;
case 4:
path.close();
break;
}
}
AABB pathBounds = path.bounds();
REQUIRE(pathBounds.minX == bounds.minX);
REQUIRE(pathBounds.minY == bounds.minY);
REQUIRE(pathBounds.maxX == bounds.maxX);
REQUIRE(pathBounds.maxY == bounds.maxY);
}
}
TEST_CASE("prune-empty-segments", "[rawpath]")
{
{
RawPath p;
p.pruneEmptySegments();
CHECK(p.begin() == p.end());
}
{
RawPath p;
p.lineTo(0, 0);
p.pruneEmptySegments();
auto iter = p.begin();
auto end = p.end();
check_iter(iter, end, PathVerb::move, {{0, 0}});
CHECK(iter == end);
}
{
RawPath p;
p.quadTo(0, 0, 0, 0);
p.pruneEmptySegments();
auto iter = p.begin();
auto end = p.end();
check_iter(iter, end, PathVerb::move, {{0, 0}});
CHECK(iter == end);
}
{
RawPath p;
p.cubicTo(0, 0, 0, 0, 0, 0);
p.pruneEmptySegments();
auto iter = p.begin();
auto end = p.end();
check_iter(iter, end, PathVerb::move, {{0, 0}});
CHECK(iter == end);
}
{
RawPath p;
p.moveTo(1, 2);
p.lineTo(3, 4);
p.lineTo(3, 4);
p.quadTo(5, 6, 7, 8);
p.quadTo(7, 8, 7, 8);
p.quadTo(7, 8, 7, 9);
p.quadTo(7, 9, 7, 9);
p.quadTo(7, 9, 7, 8);
p.quadTo(7, 8, 7, 8);
p.cubicTo(9, 10, 11, 12, 13, 14);
p.cubicTo(13, 14, 13, 14, 13, 14);
p.cubicTo(13, 14, 13, 14, 13, 15);
p.cubicTo(13, 15, 13, 15, 13, 15);
p.cubicTo(13, 16, 13, 15, 13, 15);
p.cubicTo(13, 15, 13, 15, 13, 15);
p.cubicTo(13, 15, 13, 16, 13, 15);
p.cubicTo(13, 15, 13, 15, 13, 15);
p.cubicTo(13, 15, 13, 15, 13, 16);
p.close();
p.pruneEmptySegments();
auto iter = p.begin();
auto end = p.end();
check_iter(iter, end, PathVerb::move, {{1, 2}});
check_iter(iter, end, PathVerb::line, {{1, 2}, {3, 4}});
check_iter(iter, end, PathVerb::quad, {{3, 4}, {5, 6}, {7, 8}});
check_iter(iter, end, PathVerb::quad, {{7, 8}, {7, 8}, {7, 9}});
check_iter(iter, end, PathVerb::quad, {{7, 9}, {7, 9}, {7, 8}});
check_iter(
iter,
end,
PathVerb::cubic,
{{7, 8}, {9, 10}, {11, 12}, {13, 14}});
check_iter(
iter,
end,
PathVerb::cubic,
{{13, 14}, {13, 14}, {13, 14}, {13, 15}});
check_iter(
iter,
end,
PathVerb::cubic,
{{13, 15}, {13, 16}, {13, 15}, {13, 15}});
check_iter(
iter,
end,
PathVerb::cubic,
{{13, 15}, {13, 15}, {13, 16}, {13, 15}});
check_iter(
iter,
end,
PathVerb::cubic,
{{13, 15}, {13, 15}, {13, 15}, {13, 16}});
check_iter(iter, end, PathVerb::close, {});
CHECK(iter == end);
}
{
RawPath p;
p.moveTo(1, 2);
p.lineTo(1, 2);
p.lineTo(3, 4);
RawPath p2;
p2.moveTo(5, 6);
p2.quadTo(7, 8, 9, 10);
p2.close();
p2.moveTo(11, 12);
p2.cubicTo(13, 14, 15, 16, 17, 18);
Mat2D matZero = Mat2D(0, 0, 0, 0, 19, 20);
auto p2it = p.addPath(p2, &matZero);
// Pruning at the end does nothing.
p.pruneEmptySegments(p.end());
{
auto iter = p.begin();
auto end = p.end();
check_iter(iter, end, PathVerb::move, {{1, 2}});
check_iter(iter, end, PathVerb::line, {{1, 2}, {1, 2}});
check_iter(iter, end, PathVerb::line, {{1, 2}, {3, 4}});
check_iter(iter, end, PathVerb::move, {{19, 20}});
check_iter(iter, end, PathVerb::quad, {{19, 20}, {19, 20}, {19, 20}});
check_iter(iter, end, PathVerb::close, {});
check_iter(iter, end, PathVerb::move, {{19, 20}});
check_iter(
iter,
end,
PathVerb::cubic,
{{19, 20}, {19, 20}, {19, 20}, {19, 20}});
CHECK(iter == end);
}
// Pruning just at the beginning of the added p2 won't remove the
// pre-existing empty segment.
p.pruneEmptySegments(p2it);
{
auto iter = p.begin();
auto end = p.end();
check_iter(iter, end, PathVerb::move, {{1, 2}});
check_iter(iter, end, PathVerb::line, {{1, 2}, {1, 2}});
check_iter(iter, end, PathVerb::line, {{1, 2}, {3, 4}});
check_iter(iter, end, PathVerb::move, {{19, 20}});
check_iter(iter, end, PathVerb::close, {});
check_iter(iter, end, PathVerb::move, {{19, 20}});
CHECK(iter == end);
}
// Now remove the pre-existing one.
p.pruneEmptySegments(p.begin());
{
auto iter = p.begin();
auto end = p.end();
check_iter(iter, end, PathVerb::move, {{1, 2}});
check_iter(iter, end, PathVerb::line, {{1, 2}, {3, 4}});
check_iter(iter, end, PathVerb::move, {{19, 20}});
check_iter(iter, end, PathVerb::close, {});
check_iter(iter, end, PathVerb::move, {{19, 20}});
CHECK(iter == end);
}
}
}
TEST_CASE("addPathBackwards", "[rawpath]")
{
const auto validatePathVerbPointCounts = [](const RawPath& path) {
size_t ptCount = 0;
for (auto [verb, pts] : path)
{
switch (verb)
{
case PathVerb::move:
++ptCount;
break;
case PathVerb::line:
++ptCount;
break;
case PathVerb::quad:
ptCount += 2;
break;
case PathVerb::cubic:
ptCount += 3;
break;
case PathVerb::close:
break;
}
}
return ptCount == path.points().count();
};
const auto checkPathReversal = [=](const RawPath& path) {
RawPath forwards;
forwards.addPath(path);
validatePathVerbPointCounts(forwards);
CHECK(forwards == path);
RawPath backwards;
backwards.addPathBackwards(path);
CHECK(backwards.verbs().size() == path.verbs().size());
CHECK(backwards.points().size() == path.points().size());
validatePathVerbPointCounts(backwards);
RawPath backwardsBackwards;
backwardsBackwards.addPathBackwards(backwards);
validatePathVerbPointCounts(backwardsBackwards);
CHECK(backwardsBackwards == path);
CHECK(backwardsBackwards == forwards);
};
{
// Empty path.
RawPath path;
checkPathReversal(path);
path.moveTo(0, 0);
checkPathReversal(path);
path.moveTo(0, 0);
checkPathReversal(path);
path.moveTo(10, 10);
checkPathReversal(path);
path.close();
checkPathReversal(path);
}
{
// Implicit moveTo.
RawPath path;
path.lineTo(1, 2);
checkPathReversal(path);
path.close();
path.lineTo(3, 4);
checkPathReversal(path);
}
{
// Double close.
RawPath path;
path.lineTo(1, 2);
path.close();
path.close();
path.close();
path.close();
path.close();
path.lineTo(3, 4);
path.close();
path.close();
path.close();
path.close();
checkPathReversal(path);
}
{
// Complex path.
RawPath path;
path.moveTo(0, 0);
path.lineTo(32, 84);
path.lineTo(36, 76);
path.close();
path.moveTo(0, 0);
path.cubicTo(1, 57, 32, 10, 33, 86);
path.lineTo(20, 99);
// Double moveTo tests an empty contour!
path.close();
path.moveTo(22, 59);
path.moveTo(62, 76);
path.cubicTo(74, 39, 50, 35, 60, 26);
path.moveTo(26, 46);
path.lineTo(58, 76);
path.lineTo(93, 76);
path.close();
path.moveTo(36, 74);
path.lineTo(3, 22);
path.moveTo(48, 47);
path.moveTo(47, 48);
path.lineTo(18, 94);
path.cubicTo(35, 87, 73, 1, 74, 7);
path.moveTo(6, 50);
// Double close!
path.close();
path.close();
// Ensure an implicit moveTo will be inserted here, which is what
// makes the reversal work.
path.lineTo(13, 97);
path.cubicTo(31, 26, 72, 94, 69, 32);
path.moveTo(80, 77);
path.cubicTo(40, 23, 34, 34, 77, 41);
path.cubicTo(58, 64, 26, 42, 28, 4);
path.close();
path.moveTo(80, 77);
path.lineTo(20, 22);
path.lineTo(75, 26);
path.lineTo(88, 92);
path.cubicTo(27, 7, 23, 84, 7, 89);
path.close();
checkPathReversal(path);
}
}
TEST_CASE(
"coarse area measured from a nearby origin ignores position",
"[rawpath]")
{
float curvedAtOrigin = 0;
for (float offset : {0.0f, 1e3f, 1e6f})
{
RawPath path;
path.moveTo(offset, offset);
path.lineTo(offset + 100, offset);
path.lineTo(offset + 100, offset + 100);
path.lineTo(offset, offset + 100);
path.close();
CHECK(path.computeCoarseArea(path.bounds().center()) == Approx(10000.0f));
RawPath curved;
curved.moveTo(offset, offset);
curved.cubicTo(
offset + 100,
offset,
offset + 100,
offset + 100,
offset,
offset + 100);
curved.close();
float area = curved.computeCoarseArea(curved.bounds().center());
if (offset == 0)
{
curvedAtOrigin = area;
CHECK(curvedAtOrigin > 5000.0f);
// The renderer's goldens depend on the default staying as it was.
CHECK(curved.computeCoarseArea() == Approx(curvedAtOrigin));
}
CHECK(area == Approx(curvedAtOrigin));
}
}
TEST_CASE(
"coarse area under a transform matches transforming the path",
"[rawpath]")
{
// A dot in em units, counterclockwise, drawn with just two cubics. This
// small, each cubic flattens to its chord, and the two chords enclose
// nothing.
RawPath dot;
dot.moveTo(0.25f, 0);
dot.cubicTo(0.25f, -0.33f, -0.25f, -0.33f, -0.25f, 0);
dot.cubicTo(-0.25f, 0.33f, 0.25f, 0.33f, 0.25f, 0);
dot.close();
CHECK(dot.computeCoarseArea() == 0.0f);
const Mat2D transforms[] = {
Mat2D::fromScaleAndTranslation(100, 100, 300, 200),
Mat2D::fromScaleAndTranslation(-100, 100, 300, 200),
Mat2D::fromRotation(1.0f) * Mat2D::fromScale(120, 80),
};
for (const Mat2D& transform : transforms)
{
RawPath transformed = dot.transform(transform);
float area = transformed.computeCoarseArea(transformed.bounds().center());
CHECK(area != 0.0f);
CHECK(dot.computeCoarseArea(transform) == Approx(area));
}
CHECK(dot.computeCoarseArea(transforms[0]) < 0.0f);
CHECK(dot.computeCoarseArea(transforms[1]) > 0.0f);
}
TEST_CASE("RawPath appends itself with and without a transform", "[rawpath]")
{
RawPath path;
path.moveTo(1, 2);
path.lineTo(3, 4);
path.close();
path.addPath(path, nullptr);
REQUIRE(path.verbs().size() == 6);
REQUIRE(path.points().size() == 4);
CHECK(path.points()[2] == Vec2D(1, 2));
CHECK(path.points()[3] == Vec2D(3, 4));
CHECK(path.verbs()[3] == PathVerb::move);
Mat2D shift = Mat2D::fromTranslate(10, 20);
path.addPath(path, &shift);
REQUIRE(path.verbs().size() == 12);
REQUIRE(path.points().size() == 8);
CHECK(path.points()[4] == Vec2D(11, 22));
CHECK(path.points()[7] == Vec2D(13, 24));
CHECK(path.verbs()[9] == PathVerb::move);
}
// Verb counts are tracked incrementally, so every mutation has to keep them in
// sync with the verbs themselves.
static void checkVerbCounts(const RawPath& path)
{
for (PathVerb verb :
{PathVerb::move,
PathVerb::line,
PathVerb::quad,
PathVerb::cubic,
PathVerb::close})
{
size_t expected = 0;
for (PathVerb v : path.verbs())
{
expected += v == verb;
}
CHECK(path.verbCount(verb) == expected);
}
}
TEST_CASE("verb counts track every mutation", "[rawpath]")
{
RawPath path;
checkVerbCounts(path);
path.line({1, 1}); // Injects an implicit move.
path.quad({2, 2}, {3, 0});
path.cubic({4, 4}, {5, 4}, {6, 0});
path.close();
path.close(); // No-op, the contour is already closed.
path.addRect({0, 0, 10, 10});
path.addOval({0, 0, 10, 10});
const Vec2D poly[] = {{0, 0}, {5, 0}, {5, 5}};
path.addPoly(poly, true);
path.addPoly(poly, false);
path.quadToCubic(7, 7, 8, 0);
checkVerbCounts(path);
CHECK(path.verbCount(PathVerb::move) == 5);
RawPath copy = path;
checkVerbCounts(copy);
checkVerbCounts(path.transform(Mat2D(2, 0, 0, 2, 1, 1)));
checkVerbCounts(RawPath(path.verbs(), path.points()));
copy.addPath(path);
checkVerbCounts(copy);
copy.addPath(copy, nullptr); // Self append.
checkVerbCounts(copy);
copy.addPathBackwards(path);
checkVerbCounts(copy);
RawPath degenerate;
degenerate.move({1, 1});
degenerate.line({1, 1});
degenerate.cubic({1, 1}, {1, 1}, {1, 1});
degenerate.line({2, 2});
degenerate.close();
// A matrix makes addPathBackwards prune empty segments.
Mat2D identity;
copy.addPathBackwards(degenerate, &identity);
checkVerbCounts(copy);
degenerate.pruneEmptySegments();
checkVerbCounts(degenerate);
CHECK(degenerate.verbCount(PathVerb::cubic) == 0);
copy.swap(degenerate);
checkVerbCounts(copy);
checkVerbCounts(degenerate);
// A moved-from path is empty, with counts to match.
RawPath moved = std::move(degenerate);
checkVerbCounts(moved);
checkVerbCounts(degenerate);
CHECK(degenerate.verbCount(PathVerb::move) == 0);
degenerate = std::move(moved);
checkVerbCounts(degenerate);
checkVerbCounts(moved);
path.rewind();
checkVerbCounts(path);
copy.reset();
checkVerbCounts(copy);
}