blob: 6c12c9f8b23ef5201875ef93a9d8929ff751ec76 [file] [log] [blame]
/*
* Copyright 2022 Rive
*/
#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/animation/animation_state.hpp>
#include "rive_file_reader.hpp"
#include <catch.hpp>
#include <cstdio>
using namespace rive;
TEST_CASE("hittest-basics", "[hittest]")
{
HitTester tester;
tester.reset({10, 10, 12, 12});
tester.move({0, 0});
tester.line({20, 0});
tester.line({20, 20});
tester.line({0, 20});
tester.close();
REQUIRE(tester.test());
IAABB area = {81, 156, 84, 159};
Vec2D pts[] = {
{29.9785f, 32.5261f},
{231.102f, 32.5261f},
{231.102f, 269.898f},
{29.9785f, 269.898f},
};
tester.reset(area);
tester.move(pts[0]);
for (int i = 1; i < 4; ++i)
{
tester.line(pts[i]);
}
tester.close();
REQUIRE(tester.test());
}
TEST_CASE("hittest-mesh", "[hittest]")
{
const IAABB area{10, 10, 12, 12};
Vec2D verts[] = {
{0, 0},
{20, 10},
{0, 20},
};
uint16_t indices[] = {
0,
1,
2,
};
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(301.0f, 50.0f));
// toggle does not change because it is beyond the area of the square by 1 pixel
// And the 2px padding is unly used after the coarse grained test passes
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;
}
TEST_CASE("early out on listeners", "[hittest]")
{
auto file = ReadRiveFile("../../test/assets/pointer_events.riv");
auto artboard = file->artboard("art-1");
auto artboardInstance = artboard->instance();
auto stateMachine = artboard->stateMachine("sm-1");
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);
REQUIRE(stateMachineInstance->hitComponentsCount() == 4);
// Hit component with only pointer down and pointer up listeners
auto hitComponentWithEarlyOut = stateMachineInstance->hitComponent(0);
// Hit component that can't early out because it has a pointer enter event
auto hitComponentWithNoEarlyOut = stateMachineInstance->hitComponent(1);
// Hit component that can't early out because it is an opaque target
auto hitComponentOpaque = stateMachineInstance->hitComponent(2);
// Hit component that can early out on all and pointer up
auto hitComponentOnlyPointerDown = stateMachineInstance->hitComponent(3);
REQUIRE(hitComponentWithEarlyOut->earlyOutCount == 0);
REQUIRE(hitComponentWithNoEarlyOut->earlyOutCount == 0);
REQUIRE(hitComponentOpaque->earlyOutCount == 0);
REQUIRE(hitComponentOnlyPointerDown->earlyOutCount == 0);
stateMachineInstance->pointerMove(rive::Vec2D(100.0f, 250.0f));
REQUIRE(hitComponentWithEarlyOut->earlyOutCount == 1);
REQUIRE(hitComponentWithNoEarlyOut->earlyOutCount == 0);
REQUIRE(hitComponentOpaque->earlyOutCount == 0);
REQUIRE(hitComponentOnlyPointerDown->earlyOutCount == 1);
stateMachineInstance->pointerExit(rive::Vec2D(100.0f, 250.0f));
REQUIRE(hitComponentWithEarlyOut->earlyOutCount == 2);
REQUIRE(hitComponentWithNoEarlyOut->earlyOutCount == 0);
REQUIRE(hitComponentOnlyPointerDown->earlyOutCount == 2);
REQUIRE(hitComponentOpaque->earlyOutCount == 0);
stateMachineInstance->pointerDown(rive::Vec2D(100.0f, 250.0f));
REQUIRE(hitComponentWithEarlyOut->earlyOutCount == 2);
REQUIRE(hitComponentWithNoEarlyOut->earlyOutCount == 0);
REQUIRE(hitComponentOpaque->earlyOutCount == 0);
REQUIRE(hitComponentOnlyPointerDown->earlyOutCount == 2);
stateMachineInstance->pointerUp(rive::Vec2D(100.0f, 250.0f));
REQUIRE(hitComponentWithEarlyOut->earlyOutCount == 2);
REQUIRE(hitComponentWithNoEarlyOut->earlyOutCount == 0);
REQUIRE(hitComponentOpaque->earlyOutCount == 0);
REQUIRE(hitComponentOnlyPointerDown->earlyOutCount == 3);
stateMachineInstance->pointerMove(rive::Vec2D(105.0f, 205.0f));
REQUIRE(hitComponentWithEarlyOut->earlyOutCount == 3);
REQUIRE(hitComponentWithNoEarlyOut->earlyOutCount == 0);
REQUIRE(hitComponentOpaque->earlyOutCount == 0);
REQUIRE(hitComponentOnlyPointerDown->earlyOutCount == 4);
delete stateMachineInstance;
}
TEST_CASE("click event", "[hittest]")
{
// This test has two rectangles of size [200, 200]
// positioned at [100,100] and [200, 200]
// they overlap between coordinates [100,100]-[200, 200]
// they are inside a group that has a listener attached to it
// that listener should fire an event on "Click"
auto file = ReadRiveFile("../../test/assets/click_event.riv");
auto artboard = file->artboard("art-1");
auto artboardInstance = artboard->instance();
auto stateMachine = artboard->stateMachine("sm-1");
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);
// There is a single listener with two shapes in it
REQUIRE(stateMachineInstance->hitComponentsCount() == 2);
auto layerCount = stateMachine->layerCount();
REQUIRE(layerCount == 1);
REQUIRE(stateMachineInstance->reportedEventCount() == 0);
// Click in place should trigger a click event
stateMachineInstance->pointerDown(rive::Vec2D(75.0f, 75.0f));
stateMachineInstance->pointerUp(rive::Vec2D(75.0f, 75.0f));
REQUIRE(stateMachineInstance->reportedEventCount() == 1);
// Pointer down inside shape but Pointer up outside the shape
// should not trigger a click event
stateMachineInstance->pointerDown(rive::Vec2D(75.0f, 75.0f));
stateMachineInstance->pointerUp(rive::Vec2D(300.0f, 75.0f));
REQUIRE(stateMachineInstance->reportedEventCount() == 1);
// Pointer down outside shape but Pointer up inside the shape
// should not trigger a click event
stateMachineInstance->pointerDown(rive::Vec2D(300.0f, 75.0f));
stateMachineInstance->pointerUp(rive::Vec2D(75.0f, 75.0f));
REQUIRE(stateMachineInstance->reportedEventCount() == 1);
// Pointer down in shape 1 Pointer up in shape 2 of the same group
// should trigger a click event
stateMachineInstance->pointerDown(rive::Vec2D(75.0f, 75.0f));
stateMachineInstance->pointerUp(rive::Vec2D(225.0f, 225.0f));
REQUIRE(stateMachineInstance->reportedEventCount() == 2);
// Pointer down and up in area where both shapes overlap
// should trigger a single click event
stateMachineInstance->pointerDown(rive::Vec2D(150.0f, 150.0f));
stateMachineInstance->pointerUp(rive::Vec2D(150.0f, 150.0f));
REQUIRE(stateMachineInstance->reportedEventCount() == 3);
delete stateMachineInstance;
}
TEST_CASE("multiple shapes with mouse movement behavior", "[hittest]")
{
// This test has two rectangles of size [200, 200]
// positioned at [100,100] and [100, 200]
// they overlap between coordinates [100,0]-[200, 200]
// they are inside a group that has a Pointer enter and a Pointer out
// listeners that toggle between two states (red and green)
// starting at "red"
auto file = ReadRiveFile("../../test/assets/click_event.riv");
auto artboard = file->artboard("art-2");
auto artboardInstance = artboard->instance();
auto stateMachine = artboard->stateMachine("sm-1");
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);
// There is a single listener with two shapes in it
REQUIRE(stateMachineInstance->hitComponentsCount() == 2);
auto layerCount = stateMachine->layerCount();
REQUIRE(layerCount == 1);
// Move over the first shape
stateMachineInstance->pointerMove(rive::Vec2D(75.0f, 75.0f));
artboardInstance->advance(0.0f);
stateMachineInstance->advanceAndApply(0.0f);
{
auto state = stateMachineInstance->layerState(0);
REQUIRE(state->is<rive::AnimationState>());
auto animation = state->as<rive::AnimationState>()->animation();
REQUIRE(animation->name() == "green");
}
// Move over the second shape, nothing should change
stateMachineInstance->pointerMove(rive::Vec2D(200.0f, 75.0f));
artboardInstance->advance(0.0f);
stateMachineInstance->advanceAndApply(0.0f);
{
auto state = stateMachineInstance->layerState(0);
REQUIRE(state->is<rive::AnimationState>());
auto animation = state->as<rive::AnimationState>()->animation();
REQUIRE(animation->name() == "green");
}
// Move out of the second shape, should go back to red
stateMachineInstance->pointerMove(rive::Vec2D(400.0f, 75.0f));
artboardInstance->advance(0.0f);
stateMachineInstance->advanceAndApply(0.0f);
{
auto state = stateMachineInstance->layerState(0);
REQUIRE(state->is<rive::AnimationState>());
auto animation = state->as<rive::AnimationState>()->animation();
REQUIRE(animation->name() == "red");
}
// Move back into the second shape, should go to green
stateMachineInstance->pointerMove(rive::Vec2D(200.0f, 75.0f));
artboardInstance->advance(0.0f);
stateMachineInstance->advanceAndApply(0.0f);
{
auto state = stateMachineInstance->layerState(0);
REQUIRE(state->is<rive::AnimationState>());
auto animation = state->as<rive::AnimationState>()->animation();
REQUIRE(animation->name() == "green");
}
delete stateMachineInstance;
}