blob: 69ec0422218bd5b0fb0b119bb37e729bb313397b [file] [log] [blame]
#include "rive/file.hpp"
#include "rive/node.hpp"
#include "rive/shapes/rectangle.hpp"
#include "rive/shapes/shape.hpp"
#include "rive/assets/image_asset.hpp"
#include "rive/shapes/points_path.hpp"
#include "rive/shapes/mesh.hpp"
#include "utils/no_op_renderer.hpp"
#include "rive_file_reader.hpp"
#include <catch.hpp>
#include <cstdio>
#include <cstring>
TEST_CASE("transform order is as expected", "[transform]")
{
auto translation = rive::Mat2D::fromTranslate(10.0f, 20.0f);
auto rotation = rive::Mat2D::fromRotation(3.14f / 2.0f);
auto scale = rive::Mat2D::fromScale(2.0f, 3.0f);
auto xform = translation * rotation * scale;
auto xform2 = rive::Mat2D::fromRotation(3.14f / 2.0f);
xform2[0] *= 2.0f;
xform2[1] *= 2.0f;
xform2[2] *= 3.0f;
xform2[3] *= 3.0f;
xform2[4] = 10.0f;
xform2[5] = 20.0f;
REQUIRE(xform2 == xform);
}
TEST_CASE("file can be read", "[file]")
{
auto file = ReadRiveFile("../../test/assets/two_artboards.riv");
// Default artboard should be named Two.
REQUIRE(file->artboard()->name() == "Two");
// There should be a second artboard named One.
REQUIRE(file->artboard("One") != nullptr);
}
TEST_CASE("file with bad blend mode fails to load", "[file]")
{
std::vector<uint8_t> bytes = ReadFile("../../test/assets/solar-system.riv");
rive::ImportResult result;
auto file = rive::File::import(bytes, &gNoOpFactory, &result, nullptr);
CHECK(result == rive::ImportResult::malformed);
}
TEST_CASE("file with animation can be read", "[file]")
{
auto file = ReadRiveFile("../../test/assets/juice.riv");
auto artboard = file->artboard();
REQUIRE(artboard->name() == "New Artboard");
auto shin = artboard->find("shin_right");
REQUIRE(shin != nullptr);
REQUIRE(shin->is<rive::Node>());
auto shinNode = shin->as<rive::Node>();
REQUIRE(shinNode->parent() != nullptr);
REQUIRE(shinNode->parent()->name() == "leg_right");
REQUIRE(shinNode->parent()->parent() != nullptr);
REQUIRE(shinNode->parent()->parent()->name() == "root");
REQUIRE(shinNode->parent()->parent() != nullptr);
REQUIRE(shinNode->parent()->parent()->parent() != nullptr);
REQUIRE(shinNode->parent()->parent()->parent() == artboard);
auto walkAnimation = artboard->animation("walk");
REQUIRE(walkAnimation != nullptr);
REQUIRE(walkAnimation->numKeyedObjects() == 22);
}
TEST_CASE("artboards can be counted and accessed via index or name", "[file]")
{
auto file = ReadRiveFile("../../test/assets/dependency_test.riv");
// The artboards caqn be counted
REQUIRE(file->artboardCount() == 1);
// Artboards can be access by index
REQUIRE(file->artboard(0) != nullptr);
// Artboards can be accessed by name
REQUIRE(file->artboard("Blue") != nullptr);
}
TEST_CASE("dependencies are as expected", "[file]")
{
// ┌────┐
// │Blue│
// └────┘
// │ ┌───┐
// └▶│ A │
// └───┘
// │ ┌───┐
// └▶│ B │
// └───┘
// │ ┌───┐
// ├▶│ C │
// │ └───┘
// │ ┌─────────┐
// └▶│Rectangle│
// └─────────┘
// │ ┌──────────────┐
// └▶│Rectangle Path│
// └──────────────┘
auto file = ReadRiveFile("../../test/assets/dependency_test.riv");
auto artboard = file->artboard();
REQUIRE(artboard->name() == "Blue");
auto nodeA = artboard->find<rive::Node>("A");
auto nodeB = artboard->find<rive::Node>("B");
auto nodeC = artboard->find<rive::Node>("C");
auto shape = artboard->find<rive::Shape>("Rectangle");
auto path = artboard->find<rive::Path>("Rectangle Path");
REQUIRE(nodeA != nullptr);
REQUIRE(nodeB != nullptr);
REQUIRE(nodeC != nullptr);
REQUIRE(shape != nullptr);
REQUIRE(path != nullptr);
REQUIRE(nodeA->parent() == artboard);
REQUIRE(nodeB->parent() == nodeA);
REQUIRE(nodeC->parent() == nodeB);
REQUIRE(shape->parent() == nodeB);
REQUIRE(path->parent() == shape);
REQUIRE(nodeB->dependents().size() == 2);
REQUIRE(artboard->graphOrder() == 0);
REQUIRE(nodeA->graphOrder() > artboard->graphOrder());
REQUIRE(nodeB->graphOrder() > nodeA->graphOrder());
REQUIRE(nodeC->graphOrder() > nodeB->graphOrder());
REQUIRE(shape->graphOrder() > nodeB->graphOrder());
REQUIRE(path->graphOrder() > shape->graphOrder());
artboard->advance(0.0f);
auto world = shape->worldTransform();
REQUIRE(world[4] == 39.203125f);
REQUIRE(world[5] == 29.535156f);
}
TEST_CASE("long name in object is parsed correctly", "[file]")
{
auto file = ReadRiveFile("../../test/assets/long_name.riv");
auto artboard = file->artboard();
// Expect all object in file to be loaded, in this case 7
REQUIRE(artboard->objects().size() == 7);
}
TEST_CASE("file with in-band images can have the stripped", "[file]")
{
FILE* fp = fopen("../../test/assets/jellyfish_test.riv", "rb");
REQUIRE(fp != nullptr);
fseek(fp, 0, SEEK_END);
const size_t length = ftell(fp);
fseek(fp, 0, SEEK_SET);
std::vector<uint8_t> bytes(length);
REQUIRE(fread(bytes.data(), 1, length, fp) == length);
fclose(fp);
rive::ImportResult result;
auto file = rive::File::import(bytes, &gNoOpFactory, &result);
REQUIRE(result == rive::ImportResult::success);
REQUIRE(file.get() != nullptr);
REQUIRE(file->artboard() != nullptr);
// Default artboard should be named Two.
REQUIRE(file->artboard()->name() == "Jellyfish");
// Strip nothing should result in the same file.
{
rive::ImportResult stripResult;
auto strippedBytes = rive::File::stripAssets(bytes, {}, &stripResult);
REQUIRE(stripResult == rive::ImportResult::success);
REQUIRE(bytes.size() == strippedBytes.size());
REQUIRE(std::memcmp(bytes.data(), strippedBytes.data(), bytes.size()) == 0);
}
// Strip image assets should result in a smaller file.
{
rive::ImportResult stripResult;
auto strippedBytes =
rive::File::stripAssets(bytes, {rive::ImageAsset::typeKey}, &stripResult);
REQUIRE(stripResult == rive::ImportResult::success);
REQUIRE(strippedBytes.size() < bytes.size());
}
}
TEST_CASE("file a bad skin (no parent skinnable) doesn't crash", "[file]")
{
FILE* fp = fopen("../../test/assets/bad_skin.riv", "rb");
REQUIRE(fp != nullptr);
fseek(fp, 0, SEEK_END);
const size_t length = ftell(fp);
fseek(fp, 0, SEEK_SET);
std::vector<uint8_t> bytes(length);
REQUIRE(fread(bytes.data(), 1, length, fp) == length);
fclose(fp);
rive::ImportResult result;
auto file = rive::File::import(bytes, &gNoOpFactory, &result);
REQUIRE(result == rive::ImportResult::success);
REQUIRE(file.get() != nullptr);
REQUIRE(file->artboard() != nullptr);
REQUIRE(file->artboard()->name() == "Illustration WOman.svg");
auto artboard = file->artboardDefault();
artboard->updateComponents();
auto paths = artboard->find<rive::PointsPath>();
for (auto path : paths)
{
path->markPathDirty();
}
artboard->updateComponents();
}
// TODO:
// ShapePaint (fill/stroke) needs to be implemented in WASM (jsFill/jsStroke) in
// order to create Paint objects as necessary.
// Mutators need to be implemented in WASM (solid/linear/radial) and get access
// to their ShapePaint so they can mutate any extra objects they create on it
// (like a paint object for skia).
// Paths need to be implemented in WASM but not so much as a core path (like
// parametric/pointspath, etc) but more as a general rendering path. Handed
// their commands so they can generate/store a re-usable path. This would be a
// Path2D in context2D and a SkPath in CanvasKit.
// PathComposer is the factory for the Paths. But they do need to surive so they
// can be reset/reused as available by the rendering lib.
// PathComposer needs to be implemented in WASM to compose the paths together
// and be accessible from the Shape (jsShape) which will need a call
// setupFill/restoreFill and setupStroke/restoreStroke.
// Draw will be called by C++ on the Shape, the Shape will call draw on the
// fill/stroke (propagates to jsFill/jsStroke)