| #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(); | |
| } |