sort hit shapes when draw order changes and stop propagation on hit s…

sort hit shapes when draw order changes and stop propagation on hit success

Diffs=
8bca56dca sort hit shapes when draw order changes and stop propagation on hit s… (#6624)

Co-authored-by: hernan <hernan@rive.app>
This commit is contained in:
bodymovin
2024-02-21 14:38:13 +00:00
parent 03d7c9c587
commit 71bf9e3105
15 changed files with 409 additions and 99 deletions

View File

@@ -1 +1 @@
9d605a1feb39dcad526ac9dda6b53b547921c58d
8bca56dcaffd0f563a91f628b0ed432eca71acb5

View File

@@ -123,6 +123,17 @@
"description": "List of selected animations",
"runtime": false,
"coop": false
},
"viewModelId": {
"type": "Id",
"typeRuntime": "uint",
"initialValue": "Core.missingId",
"initialValueRuntime": "-1",
"key": {
"int": 434,
"string": "viewmodelid"
},
"description": "The view model attached to this artboard data context."
}
}
}

View File

@@ -2,6 +2,7 @@
#define _RIVE_NESTED_STATE_MACHINE_HPP_
#include "rive/animation/state_machine_instance.hpp"
#include "rive/generated/animation/nested_state_machine_base.hpp"
#include "rive/hit_result.hpp"
#include "rive/math/vec2d.hpp"
#include <memory>
@@ -23,9 +24,10 @@ public:
void initializeAnimation(ArtboardInstance*) override;
StateMachineInstance* stateMachineInstance();
void pointerMove(Vec2D position);
void pointerDown(Vec2D position);
void pointerUp(Vec2D position);
HitResult pointerMove(Vec2D position);
HitResult pointerDown(Vec2D position);
HitResult pointerUp(Vec2D position);
HitResult pointerExit(Vec2D position);
void addNestedInput(NestedInput* input);
};

View File

@@ -6,6 +6,7 @@
#include <vector>
#include "rive/animation/linear_animation_instance.hpp"
#include "rive/core/field_types/core_callback_type.hpp"
#include "rive/hit_result.hpp"
#include "rive/listener_type.hpp"
#include "rive/scene.hpp"
@@ -20,7 +21,7 @@ class SMINumber;
class SMITrigger;
class Shape;
class StateMachineLayerInstance;
class HitShape;
class HitComponent;
class NestedArtboard;
class Event;
class KeyedProperty;
@@ -41,23 +42,24 @@ class StateMachineInstance : public Scene
{
friend class SMIInput;
friend class KeyedProperty;
friend class HitComponent;
private:
void markNeedsAdvance();
/// Provide a hitListener if you want to process a down or an up for the pointer position
/// too.
void updateListeners(Vec2D position, ListenerType hitListener);
HitResult updateListeners(Vec2D position, ListenerType hitListener);
template <typename SMType, typename InstType>
InstType* getNamedInput(const std::string& name) const;
void notifyEventListeners(std::vector<EventReport> events, NestedArtboard* source);
void sortHitComponents();
public:
StateMachineInstance(const StateMachine* machine, ArtboardInstance* instance);
StateMachineInstance(StateMachineInstance const&) = delete;
~StateMachineInstance() override;
void markNeedsAdvance();
// Advance the state machine by the specified time. Returns true if the
// state machine will continue to animate after this advance.
bool advance(float seconds);
@@ -88,9 +90,10 @@ public:
bool advanceAndApply(float secs) override;
std::string name() const override;
void pointerMove(Vec2D position) override;
void pointerDown(Vec2D position) override;
void pointerUp(Vec2D position) override;
HitResult pointerMove(Vec2D position) override;
HitResult pointerDown(Vec2D position) override;
HitResult pointerUp(Vec2D position) override;
HitResult pointerExit(Vec2D position) override;
float durationSeconds() const override { return -1; }
Loop loop() const override { return Loop::oneShot; }
@@ -125,8 +128,7 @@ private:
std::vector<SMIInput*> m_inputInstances; // we own each pointer
size_t m_layerCount;
StateMachineLayerInstance* m_layers;
std::vector<std::unique_ptr<HitShape>> m_hitShapes;
std::vector<NestedArtboard*> m_hitNestedArtboards;
std::vector<std::unique_ptr<HitComponent>> m_hitComponents;
StateMachineInstance* m_parentStateMachineInstance = nullptr;
NestedArtboard* m_parentNestedArtboard = nullptr;
};

View File

@@ -49,6 +49,7 @@ private:
std::vector<NestedArtboard*> m_NestedArtboards;
std::vector<Joystick*> m_Joysticks;
bool m_JoysticksApplyBeforeUpdate = true;
bool m_HasChangedDrawOrderInLastUpdate = false;
unsigned int m_DirtDepth = 0;
rcp<RenderPath> m_BackgroundPath;
@@ -100,6 +101,8 @@ public:
void onDirty(ComponentDirt dirt) override;
bool advance(double elapsedSeconds);
bool hasChangedDrawOrderInLastUpdate() { return m_HasChangedDrawOrderInLastUpdate; };
Drawable* firstDrawable() { return m_FirstDrawable; };
enum class DrawOption
{

View File

@@ -4,6 +4,7 @@
#include "rive/hit_info.hpp"
#include "rive/renderer.hpp"
#include "rive/clip_result.hpp"
#include "rive/drawable_flag.hpp"
#include <vector>
namespace rive
@@ -15,6 +16,7 @@ class DrawRules;
class Drawable : public DrawableBase
{
friend class Artboard;
friend class StateMachineInstance;
private:
std::vector<ClippingShape*> m_ClippingShapes;
@@ -34,9 +36,15 @@ public:
inline bool isHidden() const
{
// For now we have a single drawable flag, when we have more we can
// make an actual enum for this.
return (drawableFlags() & 0x1) == 0x1 || hasDirt(ComponentDirt::Collapsed);
return (static_cast<DrawableFlag>(drawableFlags()) & DrawableFlag::Hidden) ==
DrawableFlag::Hidden ||
hasDirt(ComponentDirt::Collapsed);
}
inline bool isTargetOpaque() const
{
return (static_cast<DrawableFlag>(drawableFlags()) & DrawableFlag::Opaque) ==
DrawableFlag::Opaque;
}
};
} // namespace rive

View File

@@ -0,0 +1,26 @@
#ifndef _RIVE_DRAWABLE_FLAGS_HPP_
#define _RIVE_DRAWABLE_FLAGS_HPP_
#include "rive/enum_bitset.hpp"
namespace rive
{
enum class DrawableFlag : unsigned short
{
None = 0,
/// Whether the component should be drawn
Hidden = 1 << 0,
/// Editor only
Locked = 1 << 1,
/// Editor only
Disconnected = 1 << 2,
/// Whether this Component lets hit events pass through to components behind it
Opaque = 1 << 3,
};
RIVE_MAKE_ENUM_BITSET(DrawableFlag)
} // namespace rive
#endif

View File

@@ -0,0 +1,13 @@
#ifndef _RIVE_HIT_RESULT_HPP_
#define _RIVE_HIT_RESULT_HPP_
namespace rive
{
enum class HitResult : uint8_t
{
none,
hit,
hitOpaque,
};
} // namespace rive
#endif

View File

@@ -6,6 +6,7 @@
#include "rive/math/vec2d.hpp"
#include "rive/animation/keyed_callback_reporter.hpp"
#include "rive/core/field_types/core_callback_type.hpp"
#include "rive/hit_result.hpp"
#include <string>
namespace rive
@@ -46,9 +47,10 @@ public:
void draw(Renderer*);
virtual void pointerDown(Vec2D);
virtual void pointerMove(Vec2D);
virtual void pointerUp(Vec2D);
virtual HitResult pointerDown(Vec2D);
virtual HitResult pointerMove(Vec2D);
virtual HitResult pointerUp(Vec2D);
virtual HitResult pointerExit(Vec2D);
virtual size_t inputCount() const;
virtual SMIInput* input(size_t index) const;

View File

@@ -3,6 +3,7 @@
#include "rive/animation/nested_number.hpp"
#include "rive/animation/nested_state_machine.hpp"
#include "rive/animation/state_machine_instance.hpp"
#include "rive/hit_result.hpp"
using namespace rive;
@@ -37,28 +38,40 @@ StateMachineInstance* NestedStateMachine::stateMachineInstance()
return m_StateMachineInstance.get();
}
void NestedStateMachine::pointerMove(Vec2D position)
HitResult NestedStateMachine::pointerMove(Vec2D position)
{
if (m_StateMachineInstance != nullptr)
{
m_StateMachineInstance->pointerMove(position);
return m_StateMachineInstance->pointerMove(position);
}
return HitResult::none;
}
void NestedStateMachine::pointerDown(Vec2D position)
HitResult NestedStateMachine::pointerDown(Vec2D position)
{
if (m_StateMachineInstance != nullptr)
{
m_StateMachineInstance->pointerDown(position);
return m_StateMachineInstance->pointerDown(position);
}
return HitResult::none;
}
void NestedStateMachine::pointerUp(Vec2D position)
HitResult NestedStateMachine::pointerUp(Vec2D position)
{
if (m_StateMachineInstance != nullptr)
{
m_StateMachineInstance->pointerUp(position);
return m_StateMachineInstance->pointerUp(position);
}
return HitResult::none;
}
HitResult NestedStateMachine::pointerExit(Vec2D position)
{
if (m_StateMachineInstance != nullptr)
{
return m_StateMachineInstance->pointerExit(position);
}
return HitResult::none;
}
void NestedStateMachine::addNestedInput(NestedInput* input) { m_nestedInputs.push_back(input); }

View File

@@ -17,6 +17,7 @@
#include "rive/animation/state_transition.hpp"
#include "rive/animation/transition_condition.hpp"
#include "rive/animation/state_machine_fire_event.hpp"
#include "rive/hit_result.hpp"
#include "rive/math/aabb.hpp"
#include "rive/math/hit_test.hpp"
#include "rive/nested_animation.hpp"
@@ -318,49 +319,48 @@ private:
float m_holdTime = 0.0f;
};
class HitComponent
{
public:
Component* component() const { return m_component; }
HitComponent(Component* component, StateMachineInstance* stateMachineInstance) :
m_component(component), m_stateMachineInstance(stateMachineInstance)
{}
virtual ~HitComponent(){};
virtual HitResult processEvent(Vec2D position, ListenerType hitType, bool canHit) = 0;
protected:
Component* m_component;
StateMachineInstance* m_stateMachineInstance;
};
/// Representation of a Shape from the Artboard Instance and all the listeners it
/// triggers. Allows tracking hover and performing hit detection only once on
/// shapes that trigger multiple listeners.
class HitShape
class HitShape : public HitComponent
{
public:
Shape* shape() const { return m_shape; }
HitShape(Shape* shape) : m_shape(shape) {}
HitShape(Component* shape, StateMachineInstance* stateMachineInstance) :
HitComponent(shape, stateMachineInstance)
{}
~HitShape() {}
bool isHovered = false;
float hitRadius = 2;
std::vector<const StateMachineListener*> listeners;
private:
Shape* m_shape;
};
} // namespace rive
void StateMachineInstance::updateListeners(Vec2D position, ListenerType hitType)
{
if (m_artboardInstance->frameOrigin())
HitResult processEvent(Vec2D position, ListenerType hitType, bool canHit) override
{
position -= Vec2D(m_artboardInstance->originX() * m_artboardInstance->width(),
m_artboardInstance->originY() * m_artboardInstance->height());
}
auto shape = m_component->as<Shape>();
auto hitArea = AABB(position.x - hitRadius,
position.y - hitRadius,
position.x + hitRadius,
position.y + hitRadius)
.round();
bool isOver = canHit ? shape->hitTest(hitArea) : false;
bool hoverChange = isHovered != isOver;
isHovered = isOver;
const float hitRadius = 2;
auto hitArea = AABB(position.x - hitRadius,
position.y - hitRadius,
position.x + hitRadius,
position.y + hitRadius)
.round();
for (const auto& hitShape : m_hitShapes)
{
// TODO: quick reject.
bool isOver = hitShape->shape()->hitTest(hitArea);
bool hoverChange = hitShape->isHovered != isOver;
hitShape->isHovered = isOver;
// iterate all listeners associated with this hit shape
for (auto listener : hitShape->listeners)
// // iterate all listeners associated with this hit shape
for (auto listener : listeners)
{
// Always update hover states regardless of which specific listener type
// we're trying to trigger.
@@ -368,38 +368,45 @@ void StateMachineInstance::updateListeners(Vec2D position, ListenerType hitType)
{
if (isOver && listener->listenerType() == ListenerType::enter)
{
listener->performChanges(this, position);
markNeedsAdvance();
listener->performChanges(m_stateMachineInstance, position);
m_stateMachineInstance->markNeedsAdvance();
}
else if (!isOver && listener->listenerType() == ListenerType::exit)
{
listener->performChanges(this, position);
markNeedsAdvance();
listener->performChanges(m_stateMachineInstance, position);
m_stateMachineInstance->markNeedsAdvance();
}
}
if (isOver && hitType == listener->listenerType())
{
listener->performChanges(this, position);
markNeedsAdvance();
listener->performChanges(m_stateMachineInstance, position);
m_stateMachineInstance->markNeedsAdvance();
}
}
return isOver ? shape->isTargetOpaque() ? HitResult::hitOpaque : HitResult::hit
: HitResult::none;
}
// TODO: store a hittable abstraction for HitShape and NestedArtboard that
// can be sorted by drawOrder so they can be iterated in one loop and early
// out if any hit stops propagation (also require the ability to mark a hit
// as able to stop propagation)
for (auto nestedArtboard : m_hitNestedArtboards)
};
class HitNestedArtboard : public HitComponent
{
public:
HitNestedArtboard(Component* nestedArtboard, StateMachineInstance* stateMachineInstance) :
HitComponent(nestedArtboard, stateMachineInstance)
{}
~HitNestedArtboard() {}
HitResult processEvent(Vec2D position, ListenerType hitType, bool canHit) override
{
auto nestedArtboard = m_component->as<NestedArtboard>();
HitResult hitResult = HitResult::none;
if (nestedArtboard->isCollapsed())
{
continue;
return hitResult;
}
Vec2D nestedPosition;
if (!nestedArtboard->worldToLocal(position, &nestedPosition))
{
// Mounted artboard isn't ready or has a 0 scale transform.
continue;
return hitResult;
}
for (auto nestedAnimation : nestedArtboard->nestedAnimations())
@@ -407,38 +414,90 @@ void StateMachineInstance::updateListeners(Vec2D position, ListenerType hitType)
if (nestedAnimation->is<NestedStateMachine>())
{
auto nestedStateMachine = nestedAnimation->as<NestedStateMachine>();
switch (hitType)
if (canHit)
{
case ListenerType::down:
nestedStateMachine->pointerDown(nestedPosition);
break;
case ListenerType::up:
nestedStateMachine->pointerUp(nestedPosition);
break;
case ListenerType::move:
nestedStateMachine->pointerMove(nestedPosition);
break;
case ListenerType::enter:
case ListenerType::exit:
case ListenerType::event:
break;
switch (hitType)
{
case ListenerType::down:
hitResult = nestedStateMachine->pointerDown(nestedPosition);
break;
case ListenerType::up:
hitResult = nestedStateMachine->pointerUp(nestedPosition);
break;
case ListenerType::move:
hitResult = nestedStateMachine->pointerMove(nestedPosition);
break;
case ListenerType::enter:
case ListenerType::exit:
case ListenerType::event:
break;
}
}
else
{
switch (hitType)
{
case ListenerType::down:
case ListenerType::up:
case ListenerType::move:
nestedStateMachine->pointerExit(nestedPosition);
break;
case ListenerType::enter:
case ListenerType::exit:
case ListenerType::event:
break;
}
}
}
}
return hitResult;
}
};
} // namespace rive
HitResult StateMachineInstance::updateListeners(Vec2D position, ListenerType hitType)
{
if (m_artboardInstance->frameOrigin())
{
position -= Vec2D(m_artboardInstance->originX() * m_artboardInstance->width(),
m_artboardInstance->originY() * m_artboardInstance->height());
}
bool hitSomething = false;
bool hitOpaque = false;
for (const auto& hitShape : m_hitComponents)
{
// TODO: quick reject.
HitResult hitResult = hitShape->processEvent(position, hitType, !hitOpaque);
if (hitResult != HitResult::none)
{
hitSomething = true;
if (hitResult == HitResult::hitOpaque)
{
hitOpaque = true;
}
}
}
return hitSomething ? hitOpaque ? HitResult::hitOpaque : HitResult::hit : HitResult::none;
}
void StateMachineInstance::pointerMove(Vec2D position)
HitResult StateMachineInstance::pointerMove(Vec2D position)
{
updateListeners(position, ListenerType::move);
return updateListeners(position, ListenerType::move);
}
void StateMachineInstance::pointerDown(Vec2D position)
HitResult StateMachineInstance::pointerDown(Vec2D position)
{
updateListeners(position, ListenerType::down);
return updateListeners(position, ListenerType::down);
}
void StateMachineInstance::pointerUp(Vec2D position)
HitResult StateMachineInstance::pointerUp(Vec2D position)
{
updateListeners(position, ListenerType::up);
return updateListeners(position, ListenerType::up);
}
HitResult StateMachineInstance::pointerExit(Vec2D position)
{
return updateListeners(position, ListenerType::exit);
}
StateMachineInstance::StateMachineInstance(const StateMachine* machine,
@@ -497,9 +556,9 @@ StateMachineInstance::StateMachineInstance(const StateMachine* machine,
auto shape = m_artboardInstance->resolve(id);
if (shape != nullptr && shape->is<Shape>())
{
auto hs = rivestd::make_unique<HitShape>(shape->as<Shape>());
auto hs = rivestd::make_unique<HitShape>(shape->as<Component>(), this);
hitShapeLookup[id] = hitShape = hs.get();
m_hitShapes.push_back(std::move(hs));
m_hitComponents.push_back(std::move(hs));
}
else
{
@@ -519,7 +578,11 @@ StateMachineInstance::StateMachineInstance(const StateMachine* machine,
{
if (nestedArtboard->hasNestedStateMachines())
{
m_hitNestedArtboards.push_back(nestedArtboard);
auto hn =
rivestd::make_unique<HitNestedArtboard>(nestedArtboard->as<Component>(), this);
m_hitComponents.push_back(std::move(hn));
for (auto animation : nestedArtboard->nestedAnimations())
{
if (animation->is<NestedStateMachine>())
@@ -534,6 +597,7 @@ StateMachineInstance::StateMachineInstance(const StateMachine* machine,
}
}
}
sortHitComponents();
}
StateMachineInstance::~StateMachineInstance()
@@ -545,8 +609,47 @@ StateMachineInstance::~StateMachineInstance()
delete[] m_layers;
}
void StateMachineInstance::sortHitComponents()
{
Drawable* last = m_artboardInstance->firstDrawable();
if (last)
{
// walk to the end, so we can visit in reverse-order
while (last->prev)
{
last = last->prev;
}
}
auto hitShapesCount = m_hitComponents.size();
auto currentSortedIndex = 0;
for (auto drawable = last; drawable; drawable = drawable->next)
{
for (size_t i = currentSortedIndex; i < hitShapesCount; i++)
{
if (m_hitComponents[i]->component() == drawable)
{
if (currentSortedIndex != i)
{
std::iter_swap(m_hitComponents.begin() + currentSortedIndex,
m_hitComponents.begin() + i);
}
currentSortedIndex++;
break;
}
}
if (currentSortedIndex == hitShapesCount)
{
break;
}
}
}
bool StateMachineInstance::advance(float seconds)
{
if (m_artboardInstance->hasChangedDrawOrderInLastUpdate())
{
sortHitComponents();
}
this->notifyEventListeners(m_reportedEvents, nullptr);
m_reportedEvents.clear();
m_needsAdvance = false;

View File

@@ -272,6 +272,7 @@ StatusCode Artboard::initialize()
void Artboard::sortDrawOrder()
{
m_HasChangedDrawOrderInLastUpdate = true;
for (auto target : m_DrawTargets)
{
target->first = target->last = nullptr;
@@ -486,6 +487,7 @@ bool Artboard::updateComponents()
bool Artboard::advance(double elapsedSeconds)
{
m_HasChangedDrawOrderInLastUpdate = false;
if (m_JoysticksApplyBeforeUpdate)
{
for (auto joystick : m_Joysticks)

View File

@@ -1,4 +1,5 @@
#include "rive/artboard.hpp"
#include "rive/hit_result.hpp"
#include "rive/scene.hpp"
#include "rive/generated/core_registry.hpp"
using namespace rive;
@@ -14,9 +15,10 @@ float Scene::height() const { return m_artboardInstance->height(); }
void Scene::draw(Renderer* renderer) { m_artboardInstance->draw(renderer); }
void Scene::pointerDown(Vec2D) {}
void Scene::pointerMove(Vec2D) {}
void Scene::pointerUp(Vec2D) {}
HitResult Scene::pointerDown(Vec2D) { return HitResult::none; }
HitResult Scene::pointerMove(Vec2D) { return HitResult::none; }
HitResult Scene::pointerUp(Vec2D) { return HitResult::none; }
HitResult Scene::pointerExit(Vec2D) { return HitResult::none; }
size_t Scene::inputCount() const { return 0; }
SMIInput* Scene::input(size_t index) const { return nullptr; }

Binary file not shown.

View File

@@ -4,6 +4,11 @@
#include <rive/math/aabb.hpp>
#include <rive/math/hit_test.hpp>
#include <rive/nested_artboard.hpp>
#include <rive/animation/state_machine_instance.hpp>
#include <rive/animation/state_machine_input_instance.hpp>
#include <rive/animation/nested_state_machine.hpp>
#include "rive_file_reader.hpp"
#include <catch.hpp>
#include <cstdio>
@@ -57,3 +62,121 @@ TEST_CASE("hittest-mesh", "[hittest]")
};
REQUIRE(HitTester::testMesh(area, make_span(verts, 3), make_span(indices, 3)));
}
TEST_CASE("hit test on opaque target", "[hittest]")
{
// This artboard has two rects of size 200 x 200, "red-activate" at [0, 0, 200, 200]
// and "green-activate" at [0, 100, 200, 300]
// "red-activate" is above "green-activate" in drawing order
// Both targets are set as opaque for its listeners
// "red-activate" sets "toGreen" to false
// "green-activate" sets "toGreen" to true
// There is also a "gray-activate" above the other 2 that is not opaque so events should
// traverse through the other targets
auto file = ReadRiveFile("../../test/assets/opaque_hit_test.riv");
auto artboard = file->artboard("main");
auto artboardInstance = artboard->instance();
auto stateMachine = artboard->stateMachine("main-state-machine");
REQUIRE(artboardInstance != nullptr);
REQUIRE(artboardInstance->stateMachineCount() == 1);
REQUIRE(stateMachine != nullptr);
rive::StateMachineInstance* stateMachineInstance =
new rive::StateMachineInstance(stateMachine, artboardInstance.get());
stateMachineInstance->advance(0.0f);
artboardInstance->advance(0.0f);
REQUIRE(stateMachineInstance->needsAdvance() == true);
stateMachineInstance->advance(0.0f);
auto toGreenToggle = stateMachineInstance->getBool("toGreen");
REQUIRE(toGreenToggle != nullptr);
auto grayToggle = stateMachineInstance->getBool("grayToggle");
REQUIRE(grayToggle != nullptr);
stateMachineInstance->pointerDown(rive::Vec2D(100.0f, 50.0f));
// "gray-activate" is clicked
REQUIRE(grayToggle->value() == true);
// Pointer only over "red-activate"
REQUIRE(toGreenToggle->value() == false);
stateMachineInstance->pointerDown(rive::Vec2D(100.0f, 250.0f));
// "gray-activate" is clicked
REQUIRE(grayToggle->value() == false);
// Pointer over "green-activate"
REQUIRE(toGreenToggle->value() == true);
stateMachineInstance->pointerDown(rive::Vec2D(100.0f, 110.0f));
// "gray-activate" is clicked
REQUIRE(grayToggle->value() == true);
// Pointer over "red-activate" and "green-activate", but "red-activate" is opaque and above
// so green activate does not trigger
REQUIRE(toGreenToggle->value() == false);
delete stateMachineInstance;
}
TEST_CASE("hit test on opaque nested artboard", "[hittest]")
{
// This artboard (300x300) has a main rect at [0, 0, 300, 300]
// this rect has a listener that toggles "second-gray-toggle"
// and a nested artboard at [0, 0, 150, 150]
// the nested artboard and the rect have opaque targets
auto file = ReadRiveFile("../../test/assets/opaque_hit_test.riv");
auto artboard = file->artboard("second");
auto artboardInstance = artboard->instance();
auto stateMachine = artboard->stateMachine("second-state-machine");
REQUIRE(artboardInstance != nullptr);
REQUIRE(artboardInstance->stateMachineCount() == 1);
REQUIRE(stateMachine != nullptr);
rive::StateMachineInstance* stateMachineInstance =
new rive::StateMachineInstance(stateMachine, artboardInstance.get());
auto nestedArtboard =
stateMachineInstance->artboard()->find<rive::NestedArtboard>("second-nested");
REQUIRE(nestedArtboard != nullptr);
auto nestedArtboardStateMachine =
nestedArtboard->nestedAnimations()[0]->as<NestedStateMachine>();
REQUIRE(nestedArtboardStateMachine != nullptr);
auto nestedArtboardStateMachineInstance = nestedArtboardStateMachine->stateMachineInstance();
auto secondNestedBoolTarget = nestedArtboardStateMachineInstance->getBool("bool-target");
REQUIRE(secondNestedBoolTarget != nullptr);
artboardInstance->advance(0.0f);
stateMachineInstance->advanceAndApply(0.0f);
REQUIRE(secondNestedBoolTarget->value() == false);
auto secondGrayToggle = stateMachineInstance->getBool("second-gray-toggle");
REQUIRE(secondGrayToggle != nullptr);
stateMachineInstance->pointerDown(rive::Vec2D(100.0f, 250.0f));
// toggle changes value because it is not under an opaque nested artboard
REQUIRE(secondGrayToggle->value() == true);
stateMachineInstance->pointerDown(rive::Vec2D(100.0f, 50.0f));
// toggle does not change because it is under an opaque nested artboard
REQUIRE(secondGrayToggle->value() == true);
// nested toggle changes because it's on top of shape
REQUIRE(secondNestedBoolTarget->value() == true);
// A timeline switches draw order and the nested artboard is now below the rect
stateMachineInstance->advanceAndApply(1.0f);
stateMachineInstance->advance(0.0f);
stateMachineInstance->pointerDown(rive::Vec2D(100.0f, 50.0f));
// So now the pointer down is captured by the rect
REQUIRE(secondGrayToggle->value() == false);
// nested toggle does not change because it's below shape
REQUIRE(secondNestedBoolTarget->value() == true);
delete stateMachineInstance;
}