blob: 4d6b8c79c1c2e38e172e1b2ac67e85059126d7dc [file]
#include <rive/solo.hpp>
#include <rive/nested_artboard.hpp>
#include <rive/shapes/rectangle.hpp>
#include <rive/shapes/shape.hpp>
#include <rive/animation/state_machine_instance.hpp>
#include <rive/animation/nested_linear_animation.hpp>
#include <rive/animation/nested_state_machine.hpp>
#include <rive/viewmodel/viewmodel_instance_number.hpp>
#include "rive_file_reader.hpp"
#include "rive_testing.hpp"
#include "utils/serializing_factory.hpp"
#include <catch.hpp>
#include <cstdio>
#include <iostream>
TEST_CASE("collapsed nested artboards do not advance", "[solo]")
{
auto file = ReadRiveFile("assets/solos_with_nested_artboards.riv");
auto artboard = file->artboard("main-artboard")->instance();
artboard->advance(0.0f);
auto solos = artboard->find<rive::Solo>();
REQUIRE(solos.size() == 1);
auto stateMachine = artboard->stateMachineAt(0);
stateMachine->advanceAndApply(0.0f);
artboard->advance(0.75f);
// Testing whether the squares have moved in the artboard is an indirect way
// if checking whether the time of each artboard has advanced
// Unfortunately there is no way of accessing the time of the animations
// directly
auto redNestedArtboard =
stateMachine->artboard()->find<rive::NestedArtboard>("red-artboard");
auto redNestedArtboardArtboard = redNestedArtboard->artboardInstance();
auto movingShapes = redNestedArtboardArtboard->find<rive::Shape>();
auto redRect = movingShapes.at(0);
REQUIRE(redRect->x() > 50);
auto greenNestedArtboard =
stateMachine->artboard()->find<rive::NestedArtboard>("green-artboard");
auto greenNestedArtboardArtboard = greenNestedArtboard->artboardInstance();
auto greenMovingShapes = greenNestedArtboardArtboard->find<rive::Shape>();
auto greenRect = greenMovingShapes.at(0);
REQUIRE(greenRect->x() == 50);
}
TEST_CASE("nested artboards with looping animations will keep main "
"advanceAndApply advancing",
"[nested]")
{
auto file = ReadRiveFile("assets/ball_test.riv");
auto artboard = file->artboard("Artboard")->instance();
artboard->advance(0.0f);
auto stateMachine = artboard->stateMachineAt(0);
REQUIRE(stateMachine->advanceAndApply(0.0f) == true);
REQUIRE(stateMachine->advanceAndApply(1.0f) == true);
REQUIRE(stateMachine->advanceAndApply(1.0f) == true);
}
TEST_CASE("nested artboards with one shot animations will not main "
"advanceAndApply advancing",
"[nested]")
{
auto file = ReadRiveFile("assets/ball_test.riv");
auto artboard = file->artboard("Artboard 2")->instance();
artboard->advance(0.0f);
auto stateMachine = artboard->stateMachineAt(0);
REQUIRE(stateMachine->advanceAndApply(0.0f) == true);
REQUIRE(stateMachine->advanceAndApply(0.9f) == true);
REQUIRE(stateMachine->advanceAndApply(0.1f) == true);
// nested artboards animation is 1s long but has an event to complete
REQUIRE(stateMachine->advanceAndApply(0.1f) == true);
// animation has ended and no events to report
REQUIRE(stateMachine->advanceAndApply(0.1f) == false);
}
TEST_CASE(
"nested artboard remapped timelines controlled by a timeline controlled "
"itself by a joystick correctly apply the time after the joystick ran",
"[nested]")
{
auto file = ReadRiveFile("assets/joystick_nested_remap.riv");
auto artboard = file->artboard("parent")->instance();
artboard->advance(0.0f);
auto stateMachine = artboard->stateMachineAt(0);
REQUIRE(artboard->find<rive::NestedArtboard>("child") != nullptr);
auto child = artboard->find<rive::NestedArtboard>("child");
auto nestedArtboardArtboard = child->artboardInstance();
REQUIRE(nestedArtboardArtboard != nullptr);
auto rect = nestedArtboardArtboard->find<rive::Shape>("rect");
REQUIRE(rect != nullptr);
REQUIRE(rect->x() == 250.0f);
}
TEST_CASE("Nested events don't conflict with parent events.", "[silver]")
{
rive::SerializingFactory silver;
auto file = ReadRiveFile("assets/nested_events.riv", &silver);
auto artboard = file->artboardDefault();
silver.frameSize(artboard->width(), artboard->height());
REQUIRE(artboard != nullptr);
auto stateMachine = artboard->stateMachineAt(0);
int viewModelId = artboard.get()->viewModelId();
auto vmi = viewModelId == -1
? file->createViewModelInstance(artboard.get())
: file->createViewModelInstance(viewModelId, 0);
stateMachine->bindViewModelInstance(vmi);
stateMachine->advanceAndApply(0.1f);
auto renderer = silver.makeRenderer();
artboard->draw(renderer.get());
// Pointer down on left top square, triggers child event 1
silver.addFrame();
stateMachine->pointerDown(rive::Vec2D(50.0f, 50.0f));
// Advance and apply twice to take the transition and apply the next state.
stateMachine->advanceAndApply(0.1f);
stateMachine->advanceAndApply(1.0f);
artboard->draw(renderer.get());
// Pointer down on right top square, triggers child event 2
silver.addFrame();
stateMachine->pointerDown(rive::Vec2D(150.0f, 50.0f));
// Advance and apply twice to take the transition and apply the next state.
stateMachine->advanceAndApply(0.1f);
stateMachine->advanceAndApply(1.0f);
artboard->draw(renderer.get());
// Pointer down on left bottom square, triggers parent event 1
silver.addFrame();
stateMachine->pointerDown(rive::Vec2D(50.0f, 150.0f));
// Advance and apply twice to take the transition and apply the next state.
stateMachine->advanceAndApply(0.1f);
stateMachine->advanceAndApply(1.0f);
artboard->draw(renderer.get());
// Pointer down on right bottom square, triggers parent event 1
silver.addFrame();
stateMachine->pointerDown(rive::Vec2D(150.0f, 150.0f));
// Advance and apply twice to take the transition and apply the next state.
stateMachine->advanceAndApply(0.1f);
stateMachine->advanceAndApply(1.0f);
artboard->draw(renderer.get());
CHECK(silver.matches("nested_events"));
}
TEST_CASE("All nested artboard modes respect hug", "[silver]")
{
rive::SerializingFactory silver;
auto file = ReadRiveFile("assets/nested_hug.riv", &silver);
auto artboard = file->artboardDefault();
silver.frameSize(artboard->width(), artboard->height());
REQUIRE(artboard != nullptr);
auto stateMachine = artboard->stateMachineAt(0);
int viewModelId = artboard.get()->viewModelId();
auto vmi = viewModelId == -1
? file->createViewModelInstance(artboard.get())
: file->createViewModelInstance(viewModelId, 0);
stateMachine->bindViewModelInstance(vmi);
stateMachine->advanceAndApply(0.1f);
auto renderer = silver.makeRenderer();
artboard->draw(renderer.get());
silver.addFrame();
CHECK(silver.matches("nested_hug"));
}
TEST_CASE("Pause and resume nested artboards", "[silver]")
{
rive::SerializingFactory silver;
auto file = ReadRiveFile("assets/pause_nested_artboard.riv", &silver);
auto artboard = file->artboardDefault();
silver.frameSize(artboard->width(), artboard->height());
REQUIRE(artboard != nullptr);
auto stateMachine = artboard->stateMachineAt(0);
auto vmi = file->createViewModelInstance(artboard.get()->viewModelId(), 0);
stateMachine->bindViewModelInstance(vmi);
stateMachine->advanceAndApply(0.1f);
auto renderer = silver.makeRenderer();
artboard->draw(renderer.get());
int frames = (int)(0.25f / 0.016f);
// State machine advances normally
for (int i = 0; i < frames; i++)
{
silver.addFrame();
stateMachine->advanceAndApply(0.016f);
artboard->draw(renderer.get());
}
// Click toggles rectangle color
silver.addFrame();
stateMachine->pointerDown(rive::Vec2D(250.0f, 250.0f));
stateMachine->pointerUp(rive::Vec2D(250.0f, 250.0f));
stateMachine->advanceAndApply(0.016f);
artboard->draw(renderer.get());
// Toggle data bind pauses state machine
silver.addFrame();
stateMachine->pointerDown(rive::Vec2D(25.0f, 25.0f));
stateMachine->pointerUp(rive::Vec2D(25.0f, 25.0f));
stateMachine->advanceAndApply(0.016f);
artboard->draw(renderer.get());
// State machine does not advance
for (int i = 0; i < frames; i++)
{
silver.addFrame();
stateMachine->advanceAndApply(0.016f);
artboard->draw(renderer.get());
}
// Click does not toggle color back
silver.addFrame();
stateMachine->pointerDown(rive::Vec2D(250.0f, 250.0f));
stateMachine->pointerUp(rive::Vec2D(250.0f, 250.0f));
stateMachine->advanceAndApply(0.016f);
artboard->draw(renderer.get());
// Toggle data bind resumes state machine
silver.addFrame();
stateMachine->pointerDown(rive::Vec2D(25.0f, 25.0f));
stateMachine->pointerUp(rive::Vec2D(25.0f, 25.0f));
stateMachine->advanceAndApply(0.016f);
artboard->draw(renderer.get());
// Click toggles color back
silver.addFrame();
stateMachine->pointerDown(rive::Vec2D(250.0f, 250.0f));
stateMachine->pointerUp(rive::Vec2D(250.0f, 250.0f));
stateMachine->advanceAndApply(0.016f);
artboard->draw(renderer.get());
// State machine advances again
for (int i = 0; i < frames; i++)
{
silver.addFrame();
stateMachine->advanceAndApply(0.016f);
artboard->draw(renderer.get());
}
CHECK(silver.matches("pause_nested_artboard"));
}
TEST_CASE("Quantize and speed on nested artboards", "[silver]")
{
rive::SerializingFactory silver;
auto file =
ReadRiveFile("assets/nested_artboard_quantize_and_speed.riv", &silver);
auto artboard = file->artboardDefault();
silver.frameSize(artboard->width(), artboard->height());
REQUIRE(artboard != nullptr);
auto stateMachine = artboard->stateMachineAt(0);
auto vmi = file->createViewModelInstance(artboard.get()->viewModelId(), 0);
auto speedProp =
vmi->propertyValue("speed")->as<rive::ViewModelInstanceNumber>();
auto quantizeProp =
vmi->propertyValue("quant")->as<rive::ViewModelInstanceNumber>();
stateMachine->bindViewModelInstance(vmi);
stateMachine->advanceAndApply(0.1f);
auto renderer = silver.makeRenderer();
artboard->draw(renderer.get());
int frames = (int)(1.0f / 0.016f);
// Multiple state machines in different speed and quantize modes
for (int i = 0; i < frames; i++)
{
silver.addFrame();
stateMachine->advanceAndApply(0.016f);
artboard->draw(renderer.get());
}
// Update speed and quantize values
speedProp->propertyValue(4);
quantizeProp->propertyValue(7);
stateMachine->advanceAndApply(0.016f);
for (int i = 0; i < frames; i++)
{
silver.addFrame();
stateMachine->advanceAndApply(0.016f);
artboard->draw(renderer.get());
}
CHECK(silver.matches("nested_artboard_quantize_and_speed"));
}
TEST_CASE("Nested artboard needs advance", "[silver]")
{
rive::SerializingFactory silver;
auto file = ReadRiveFile("assets/nested_needs_advance.riv", &silver);
auto artboard = file->artboardNamed("node");
silver.frameSize(artboard->width(), artboard->height());
REQUIRE(artboard != nullptr);
auto stateMachine = artboard->stateMachineAt(0);
stateMachine->advanceAndApply(0.1f);
auto renderer = silver.makeRenderer();
artboard->draw(renderer.get());
int frames = 120;
for (int i = 0; i < frames; i++)
{
silver.addFrame();
stateMachine->advanceAndApply(0.016f);
artboard->draw(renderer.get());
}
CHECK(silver.matches("nested_needs_advance"));
}
// Regression for the EXC_BAD_ACCESS in ~LinearAnimationInstance (Sentry
// RIVE_NATIVE-11Q).
//
// ~NestedArtboard calls releaseDependencies() on every nested animation
// precisely so their instances die before m_Instance — the mounted
// ArtboardInstance — is freed at the end of that destructor. A
// NestedLinearAnimation's LinearAnimationInstance is built against that mounted
// instance, and ~LinearAnimationInstance dereferences it (removeDataBind for
// the scripted-interpolator and data-bound-keyframe clones it parked on the
// artboard). NestedLinearAnimation::releaseDependencies() used to be an empty
// no-op, so the instance survived until the parent artboard deleted the
// animation from its m_Objects — by which point the mounted artboard was freed
// and the destructor read through a dangling pointer.
TEST_CASE("nested linear animations release their animation instance",
"[nested]")
{
auto file = ReadRiveFile("assets/joystick_nested_remap.riv");
auto artboard = file->artboardNamed("parent");
REQUIRE(artboard != nullptr);
artboard->advance(0.0f);
auto nestedArtboards = artboard->find<rive::NestedArtboard>();
REQUIRE(!nestedArtboards.empty());
size_t checked = 0;
for (auto* nested : nestedArtboards)
{
for (auto* animation : nested->nestedAnimations())
{
if (!animation->is<rive::NestedLinearAnimation>())
{
continue;
}
auto* linear = animation->as<rive::NestedLinearAnimation>();
// Built by NestedArtboard::nest() against the mounted instance.
REQUIRE(linear->animationInstance() != nullptr);
animation->releaseDependencies();
// Must be gone before ~NestedArtboard frees the artboard instance
// this LinearAnimationInstance points at.
CHECK(linear->animationInstance() == nullptr);
checked++;
}
}
// Guard the fixture itself: a file without nested linear animations would
// make the loop above vacuously pass.
REQUIRE(checked > 0);
}
// The same bug, reproduced rather than asserted: this fixture's mounted "child"
// artboard has data-bound keyframe values, so applying its timeline through the
// parent's remap animation makes the LinearAnimationInstance clone those binds
// and park them on the mounted artboard. Those clones are exactly what
// ~LinearAnimationInstance reaches back through m_artboardInstance to remove,
// so destroying the parent exercises the ordering that used to read freed
// memory. Before the fix this trips AddressSanitizer with a
// heap-use-after-free.
TEST_CASE("nested remap animation with data-bound keyframes tears down cleanly",
"[nested]")
{
auto file = ReadRiveFile("assets/data_bound_keyframe_test.riv");
auto artboard = file->artboardNamed("parent");
REQUIRE(artboard != nullptr);
auto vmi = file->createDefaultViewModelInstance(artboard.get());
REQUIRE(vmi != nullptr);
auto stateMachine = artboard->stateMachineAt(0);
REQUIRE(stateMachine != nullptr);
stateMachine->bindViewModelInstance(vmi);
auto nestedArtboards = artboard->find<rive::NestedArtboard>();
REQUIRE(nestedArtboards.size() == 1);
auto* mounted = nestedArtboards[0]->artboardInstance();
REQUIRE(mounted != nullptr);
size_t bindsBeforeApply = mounted->dataBinds().size();
for (int i = 0; i < 4; i++)
{
stateMachine->advanceAndApply(0.1f);
}
// The keyframe value binds the destructor has to clean up are clones the
// instance appended to the mounted artboard. Without them the teardown
// below never dereferences m_artboardInstance and the test is vacuous.
REQUIRE(mounted->dataBinds().size() > bindsBeforeApply);
// ~NestedArtboard frees `mounted` here; the remap animation's
// LinearAnimationInstance must already be gone.
artboard.reset();
}