blob: 7b77337c7e1e300fbf1bfb72c87d040a01a7b0b6 [file] [log] [blame]
/*
* Copyright 2022 Rive
*/
#include <rive/math/aabb.hpp>
#include <rive/math/raw_path.hpp>
#include <catch.hpp>
#include <cstdio>
#include <limits>
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 bool is_move(const RawPath::Iter::Rec& rec) {
if (rec.verb == PathVerb::move) {
REQUIRE(rec.count == 1);
return true;
}
return false;
}
static bool is_line(const RawPath::Iter::Rec& rec) {
if (rec.verb == PathVerb::line) {
REQUIRE(rec.count == 1);
return true;
}
return false;
}
static bool is_quad(const RawPath::Iter::Rec& rec) {
if (rec.verb == PathVerb::quad) {
REQUIRE(rec.count == 2);
return true;
}
return false;
}
static bool is_cubic(const RawPath::Iter::Rec& rec) {
if (rec.verb == PathVerb::cubic) {
REQUIRE(rec.count == 3);
return true;
}
return false;
}
static bool is_close(const RawPath::Iter::Rec& rec) {
if (rec.verb == PathVerb::close) {
REQUIRE(rec.count == 0);
return true;
}
return false;
}
// clang-format off
static inline bool eq(Vec2D p, float x, float y) {
return p.x == x && p.y == y;
}
// clang-format on
TEST_CASE("rawpath-iter", "[rawpath]") {
{
RawPath rp;
RawPath::Iter iter(rp);
REQUIRE(iter.next() == false);
REQUIRE(iter.next() == false); // should be safe to call again
}
{
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();
RawPath::Iter iter(rp);
auto rec = iter.next();
REQUIRE((rec && is_move(rec) && eq(rec.pts[0], 1, 2)));
rec = iter.next();
REQUIRE((rec && is_line(rec) && eq(rec.pts[0], 3, 4)));
rec = iter.next();
REQUIRE((rec && is_quad(rec) && eq(rec.pts[0], 5, 6) && eq(rec.pts[1], 7, 8)));
rec = iter.next();
REQUIRE((rec && is_cubic(rec) && eq(rec.pts[0], 9, 10) && eq(rec.pts[1], 11, 12) &&
eq(rec.pts[2], 13, 14)));
rec = iter.next();
REQUIRE((rec && is_close(rec)));
rec = iter.next();
REQUIRE(rec == false);
REQUIRE(iter.next() == false); // should be safe to call again
}
}
TEST_CASE("isDone", "[rawpath::iter]") {
RawPath rp;
rp.moveTo(1, 2);
rp.lineTo(3, 4);
RawPath::Iter iter(rp);
REQUIRE(!iter.isDone()); // moveTo
REQUIRE(iter.next());
REQUIRE(!iter.isDone()); // lineTo
REQUIRE(iter.next());
REQUIRE(iter.isDone()); // now we're done
REQUIRE(!iter.next());
REQUIRE(iter.isDone()); // ensure we 'still' think we're done
}
TEST_CASE("reset", "[rawpath]") {
RawPath path;
path.moveTo(1, 2);
path.lineTo(3, 4);
RawPath::Iter iter(path);
auto rec = iter.next();
REQUIRE((rec && is_move(rec) && eq(rec.pts[0], 1, 2)));
rec = iter.next();
REQUIRE((rec && is_line(rec) && eq(rec.pts[0], 3, 4)));
REQUIRE(!iter.next());
// now change the path (not required for the test per-se)
path = RawPath();
path.moveTo(0, 0);
path.close();
iter.reset(path);
rec = iter.next();
REQUIRE((rec && is_move(rec) && eq(rec.pts[0], 0, 0)));
rec = iter.next();
REQUIRE((rec && is_close(rec)));
REQUIRE(!iter.next());
}
TEST_CASE("backup", "[rawpath]") {
RawPath rp;
rp.moveTo(1, 2);
rp.lineTo(3, 4);
rp.close();
RawPath::Iter iter(rp);
auto rec = iter.next();
REQUIRE((rec && is_move(rec) && eq(rec.pts[0], 1, 2)));
const Vec2D* move_pts = rec.pts;
rec = iter.next();
REQUIRE((rec && is_line(rec) && eq(rec.pts[0], 3, 4)));
const Vec2D* line_pts = rec.pts;
rec = iter.next();
REQUIRE((rec && is_close(rec)));
rec = iter.next();
REQUIRE(!rec);
// Now try backing up
iter.backUp(); // go back to 'close'
rec = iter.next();
REQUIRE((rec && is_close(rec)));
iter.backUp(); // go back to 'close'
iter.backUp(); // go back to 'line'
rec = iter.next();
REQUIRE((rec && is_line(rec) && eq(rec.pts[0], 3, 4)));
REQUIRE(rec.pts == line_pts);
iter.backUp(); // go back to 'line'
iter.backUp(); // go back to 'move'
rec = iter.next();
REQUIRE((rec && is_move(rec) && eq(rec.pts[0], 1, 2)));
REQUIRE(rec.pts == move_pts);
}
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: path.line(randPt()); break;
case 2: path.quad(randPt(), randPt()); break;
case 3: 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);
}
}