diff --git a/.rive_head b/.rive_head index a6def145..cf570cd2 100644 --- a/.rive_head +++ b/.rive_head @@ -1 +1 @@ -9d605a1feb39dcad526ac9dda6b53b547921c58d +8bca56dcaffd0f563a91f628b0ed432eca71acb5 diff --git a/dev/defs/artboard.json b/dev/defs/artboard.json index 628ce9be..8b1f39a0 100644 --- a/dev/defs/artboard.json +++ b/dev/defs/artboard.json @@ -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." } } } \ No newline at end of file diff --git a/include/rive/animation/nested_state_machine.hpp b/include/rive/animation/nested_state_machine.hpp index a320c854..fe0cded9 100644 --- a/include/rive/animation/nested_state_machine.hpp +++ b/include/rive/animation/nested_state_machine.hpp @@ -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 @@ -23,9 +24,10 @@ class NestedStateMachine : public NestedStateMachineBase 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); }; diff --git a/include/rive/animation/state_machine_instance.hpp b/include/rive/animation/state_machine_instance.hpp index a9c5ad52..67369e69 100644 --- a/include/rive/animation/state_machine_instance.hpp +++ b/include/rive/animation/state_machine_instance.hpp @@ -6,6 +6,7 @@ #include #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 InstType* getNamedInput(const std::string& name) const; void notifyEventListeners(std::vector 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 @@ class StateMachineInstance : public Scene 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 @@ class StateMachineInstance : public Scene std::vector m_inputInstances; // we own each pointer size_t m_layerCount; StateMachineLayerInstance* m_layers; - std::vector> m_hitShapes; - std::vector m_hitNestedArtboards; + std::vector> m_hitComponents; StateMachineInstance* m_parentStateMachineInstance = nullptr; NestedArtboard* m_parentNestedArtboard = nullptr; }; diff --git a/include/rive/artboard.hpp b/include/rive/artboard.hpp index a110e0f7..8338a6d9 100644 --- a/include/rive/artboard.hpp +++ b/include/rive/artboard.hpp @@ -49,6 +49,7 @@ class Artboard : public ArtboardBase, public CoreContext, public ShapePaintConta std::vector m_NestedArtboards; std::vector m_Joysticks; bool m_JoysticksApplyBeforeUpdate = true; + bool m_HasChangedDrawOrderInLastUpdate = false; unsigned int m_DirtDepth = 0; rcp m_BackgroundPath; @@ -100,6 +101,8 @@ class Artboard : public ArtboardBase, public CoreContext, public ShapePaintConta void onDirty(ComponentDirt dirt) override; bool advance(double elapsedSeconds); + bool hasChangedDrawOrderInLastUpdate() { return m_HasChangedDrawOrderInLastUpdate; }; + Drawable* firstDrawable() { return m_FirstDrawable; }; enum class DrawOption { diff --git a/include/rive/drawable.hpp b/include/rive/drawable.hpp index 0e4c494c..547d6b54 100644 --- a/include/rive/drawable.hpp +++ b/include/rive/drawable.hpp @@ -4,6 +4,7 @@ #include "rive/hit_info.hpp" #include "rive/renderer.hpp" #include "rive/clip_result.hpp" +#include "rive/drawable_flag.hpp" #include namespace rive @@ -15,6 +16,7 @@ class DrawRules; class Drawable : public DrawableBase { friend class Artboard; + friend class StateMachineInstance; private: std::vector m_ClippingShapes; @@ -34,9 +36,15 @@ class Drawable : public DrawableBase 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(drawableFlags()) & DrawableFlag::Hidden) == + DrawableFlag::Hidden || + hasDirt(ComponentDirt::Collapsed); + } + + inline bool isTargetOpaque() const + { + return (static_cast(drawableFlags()) & DrawableFlag::Opaque) == + DrawableFlag::Opaque; } }; } // namespace rive diff --git a/include/rive/drawable_flag.hpp b/include/rive/drawable_flag.hpp new file mode 100644 index 00000000..9f79a152 --- /dev/null +++ b/include/rive/drawable_flag.hpp @@ -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 diff --git a/include/rive/hit_result.hpp b/include/rive/hit_result.hpp new file mode 100644 index 00000000..ec0c7fd1 --- /dev/null +++ b/include/rive/hit_result.hpp @@ -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 diff --git a/include/rive/scene.hpp b/include/rive/scene.hpp index 21cb6066..fd96d8e9 100644 --- a/include/rive/scene.hpp +++ b/include/rive/scene.hpp @@ -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 namespace rive @@ -46,9 +47,10 @@ class Scene : public KeyedCallbackReporter, public CallbackContext 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; diff --git a/src/animation/nested_state_machine.cpp b/src/animation/nested_state_machine.cpp index a686f14b..a6b5210b 100644 --- a/src/animation/nested_state_machine.cpp +++ b/src/animation/nested_state_machine.cpp @@ -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); } \ No newline at end of file diff --git a/src/animation/state_machine_instance.cpp b/src/animation/state_machine_instance.cpp index 3ba76eb3..fe7f27ab 100644 --- a/src/animation/state_machine_instance.cpp +++ b/src/animation/state_machine_instance.cpp @@ -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 @@ class StateMachineLayerInstance 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 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()); - } - - 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) + auto shape = m_component->as(); + 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; + + // // 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(); + 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()) { auto nestedStateMachine = nestedAnimation->as(); - 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) +{ + return updateListeners(position, ListenerType::move); +} +HitResult StateMachineInstance::pointerDown(Vec2D position) { - updateListeners(position, ListenerType::move); + return updateListeners(position, ListenerType::down); } -void StateMachineInstance::pointerDown(Vec2D position) +HitResult StateMachineInstance::pointerUp(Vec2D position) { - updateListeners(position, ListenerType::down); + return updateListeners(position, ListenerType::up); } -void StateMachineInstance::pointerUp(Vec2D position) +HitResult StateMachineInstance::pointerExit(Vec2D position) { - updateListeners(position, ListenerType::up); + 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()) { - auto hs = rivestd::make_unique(shape->as()); + auto hs = rivestd::make_unique(shape->as(), 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(nestedArtboard->as(), this); + m_hitComponents.push_back(std::move(hn)); + for (auto animation : nestedArtboard->nestedAnimations()) { if (animation->is()) @@ -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; diff --git a/src/artboard.cpp b/src/artboard.cpp index 6f18718b..b386bf77 100644 --- a/src/artboard.cpp +++ b/src/artboard.cpp @@ -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) diff --git a/src/scene.cpp b/src/scene.cpp index 2bf0a09a..a987dfec 100644 --- a/src/scene.cpp +++ b/src/scene.cpp @@ -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; } diff --git a/test/assets/opaque_hit_test.riv b/test/assets/opaque_hit_test.riv new file mode 100644 index 00000000..65c0d81d Binary files /dev/null and b/test/assets/opaque_hit_test.riv differ diff --git a/test/hittest_test.cpp b/test/hittest_test.cpp index fa870321..cb4f8f8c 100644 --- a/test/hittest_test.cpp +++ b/test/hittest_test.cpp @@ -4,6 +4,11 @@ #include #include +#include +#include +#include +#include +#include "rive_file_reader.hpp" #include #include @@ -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("second-nested"); + REQUIRE(nestedArtboard != nullptr); + auto nestedArtboardStateMachine = + nestedArtboard->nestedAnimations()[0]->as(); + 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; +} \ No newline at end of file