blob: b75642a85e035a04ce8f3370732b1c40d6731a39 [file] [log] [blame]
#include "rive/animation/animation_state_instance.hpp"
#include "rive/animation/animation_state.hpp"
#include "rive/animation/any_state.hpp"
#include "rive/animation/cubic_interpolator.hpp"
#include "rive/animation/entry_state.hpp"
#include "rive/animation/nested_state_machine.hpp"
#include "rive/animation/state_instance.hpp"
#include "rive/animation/state_machine_bool.hpp"
#include "rive/animation/state_machine_input_instance.hpp"
#include "rive/animation/state_machine_input.hpp"
#include "rive/animation/state_machine_instance.hpp"
#include "rive/animation/state_machine_layer.hpp"
#include "rive/animation/state_machine_listener.hpp"
#include "rive/animation/state_machine_number.hpp"
#include "rive/animation/state_machine_trigger.hpp"
#include "rive/animation/state_machine.hpp"
#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"
#include "rive/nested_artboard.hpp"
#include "rive/shapes/shape.hpp"
#include "rive/math/math_types.hpp"
#include "rive/audio_event.hpp"
#include <unordered_map>
using namespace rive;
namespace rive
{
class StateMachineLayerInstance
{
public:
~StateMachineLayerInstance()
{
delete m_anyStateInstance;
delete m_currentState;
delete m_stateFrom;
}
void init(StateMachineInstance* stateMachineInstance,
const StateMachineLayer* layer,
ArtboardInstance* instance)
{
m_stateMachineInstance = stateMachineInstance;
m_artboardInstance = instance;
assert(m_layer == nullptr);
m_anyStateInstance = layer->anyState()->makeInstance(instance).release();
m_layer = layer;
changeState(m_layer->entryState());
}
void updateMix(float seconds)
{
if (m_transition != nullptr && m_stateFrom != nullptr && m_transition->duration() != 0)
{
m_mix = std::min(
1.0f,
std::max(0.0f, (m_mix + seconds / m_transition->mixTime(m_stateFrom->state()))));
if (m_mix == 1.0f && !m_transitionCompleted)
{
m_transitionCompleted = true;
fireEvents(StateMachineFireOccurance::atEnd, m_transition->events());
}
}
else
{
m_mix = 1.0f;
}
}
bool advance(float seconds)
{
m_stateMachineChangedOnAdvance = false;
m_currentState->advance(seconds, m_stateMachineInstance);
updateMix(seconds);
if (m_stateFrom != nullptr && m_mix < 1.0f && !m_holdAnimationFrom)
{
// This didn't advance during our updateState, but it should now
// that we realize we need to mix it in.
m_stateFrom->advance(seconds, m_stateMachineInstance);
}
apply();
for (int i = 0; updateState(i != 0); i++)
{
apply();
if (i == maxIterations)
{
fprintf(stderr, "StateMachine exceeded max iterations.\n");
return false;
}
}
m_currentState->clearSpilledTime();
return m_mix != 1.0f || m_waitingForExit ||
(m_currentState != nullptr && m_currentState->keepGoing());
}
bool isTransitioning()
{
return m_transition != nullptr && m_stateFrom != nullptr && m_transition->duration() != 0 &&
m_mix < 1.0f;
}
bool updateState(bool ignoreTriggers)
{
// Don't allow changing state while a transition is taking place
// (we're mixing one state onto another) if enableEarlyExit is not true.
if (isTransitioning() && !m_transition->enableEarlyExit())
{
return false;
}
m_waitingForExit = false;
if (tryChangeState(m_anyStateInstance, ignoreTriggers))
{
return true;
}
return tryChangeState(m_currentState, ignoreTriggers);
}
void fireEvents(StateMachineFireOccurance occurs,
const std::vector<StateMachineFireEvent*>& fireEvents)
{
for (auto event : fireEvents)
{
if (event->occurs() == occurs)
{
event->perform(m_stateMachineInstance);
}
}
}
bool changeState(const LayerState* stateTo)
{
if ((m_currentState == nullptr ? nullptr : m_currentState->state()) == stateTo)
{
return false;
}
// Fire end events for the state we're changing from.
if (m_currentState != nullptr)
{
fireEvents(StateMachineFireOccurance::atEnd, m_currentState->state()->events());
}
m_currentState =
stateTo == nullptr ? nullptr : stateTo->makeInstance(m_artboardInstance).release();
// Fire start events for the state we're changing to.
if (m_currentState != nullptr)
{
fireEvents(StateMachineFireOccurance::atStart, m_currentState->state()->events());
}
return true;
}
bool tryChangeState(StateInstance* stateFromInstance, bool ignoreTriggers)
{
if (stateFromInstance == nullptr)
{
return false;
}
auto stateFrom = stateFromInstance->state();
auto outState = m_currentState;
for (size_t i = 0, length = stateFrom->transitionCount(); i < length; i++)
{
auto transition = stateFrom->transition(i);
auto allowed =
transition->allowed(stateFromInstance, m_stateMachineInstance, ignoreTriggers);
if (allowed == AllowTransition::yes && changeState(transition->stateTo()))
{
m_stateMachineChangedOnAdvance = true;
// state actually has changed
m_transition = transition;
fireEvents(StateMachineFireOccurance::atStart, transition->events());
if (transition->duration() == 0)
{
m_transitionCompleted = true;
fireEvents(StateMachineFireOccurance::atEnd, transition->events());
}
else
{
m_transitionCompleted = false;
}
if (m_stateFrom != m_anyStateInstance)
{
// Old state from is done.
delete m_stateFrom;
}
m_stateFrom = outState;
// If we had an exit time and wanted to pause on exit, make
// sure to hold the exit time. Delegate this to the
// transition by telling it that it was completed.
if (outState != nullptr && transition->applyExitCondition(outState))
{
// Make sure we apply this state. This only returns true
// when it's an animation state instance.
auto instance =
static_cast<AnimationStateInstance*>(m_stateFrom)->animationInstance();
m_holdAnimation = instance->animation();
m_holdTime = instance->time();
}
m_mixFrom = m_mix;
// Keep mixing last animation that was mixed in.
if (m_mix != 0.0f)
{
m_holdAnimationFrom = transition->pauseOnExit();
}
if (m_stateFrom != nullptr && m_stateFrom->state()->is<AnimationState>() &&
m_currentState != nullptr)
{
auto instance =
static_cast<AnimationStateInstance*>(m_stateFrom)->animationInstance();
auto spilledTime = instance->spilledTime();
m_currentState->advance(spilledTime, m_stateMachineInstance);
}
m_mix = 0.0f;
updateMix(0.0f);
m_waitingForExit = false;
return true;
}
else if (allowed == AllowTransition::waitingForExit)
{
m_waitingForExit = true;
}
}
return false;
}
void apply(/*Artboard* artboard*/)
{
if (m_holdAnimation != nullptr)
{
m_holdAnimation->apply(m_artboardInstance, m_holdTime, m_mixFrom);
m_holdAnimation = nullptr;
}
CubicInterpolator* cubic = nullptr;
if (m_transition != nullptr && m_transition->interpolator() != nullptr)
{
cubic = m_transition->interpolator();
}
if (m_stateFrom != nullptr && m_mix < 1.0f)
{
auto fromMix = cubic != nullptr ? cubic->transform(m_mixFrom) : m_mixFrom;
m_stateFrom->apply(fromMix);
}
if (m_currentState != nullptr)
{
auto mix = cubic != nullptr ? cubic->transform(m_mix) : m_mix;
m_currentState->apply(mix);
}
}
bool stateChangedOnAdvance() const { return m_stateMachineChangedOnAdvance; }
const LayerState* currentState()
{
return m_currentState == nullptr ? nullptr : m_currentState->state();
}
const LinearAnimationInstance* currentAnimation() const
{
if (m_currentState == nullptr || !m_currentState->state()->is<AnimationState>())
{
return nullptr;
}
return static_cast<AnimationStateInstance*>(m_currentState)->animationInstance();
}
private:
static const int maxIterations = 100;
StateMachineInstance* m_stateMachineInstance = nullptr;
const StateMachineLayer* m_layer = nullptr;
ArtboardInstance* m_artboardInstance = nullptr;
StateInstance* m_anyStateInstance = nullptr;
StateInstance* m_currentState = nullptr;
StateInstance* m_stateFrom = nullptr;
const StateTransition* m_transition = nullptr;
bool m_transitionCompleted = false;
bool m_holdAnimationFrom = false;
float m_mix = 1.0f;
float m_mixFrom = 1.0f;
bool m_stateMachineChangedOnAdvance = false;
bool m_waitingForExit = false;
/// Used to ensure a specific animation is applied on the next apply.
const LinearAnimation* m_holdAnimation = nullptr;
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 : public HitComponent
{
public:
HitShape(Component* shape, StateMachineInstance* stateMachineInstance) :
HitComponent(shape, stateMachineInstance)
{}
~HitShape() {}
bool isHovered = false;
float hitRadius = 2;
std::vector<const StateMachineListener*> listeners;
HitResult processEvent(Vec2D position, ListenerType hitType, bool canHit) override
{
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;
// // 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.
if (hoverChange)
{
if (isOver && listener->listenerType() == ListenerType::enter)
{
listener->performChanges(m_stateMachineInstance, position);
m_stateMachineInstance->markNeedsAdvance();
}
else if (!isOver && listener->listenerType() == ListenerType::exit)
{
listener->performChanges(m_stateMachineInstance, position);
m_stateMachineInstance->markNeedsAdvance();
}
}
if (isOver && hitType == listener->listenerType())
{
listener->performChanges(m_stateMachineInstance, position);
m_stateMachineInstance->markNeedsAdvance();
}
}
return isOver ? shape->isTargetOpaque() ? HitResult::hitOpaque : HitResult::hit
: HitResult::none;
}
};
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())
{
return hitResult;
}
Vec2D nestedPosition;
if (!nestedArtboard->worldToLocal(position, &nestedPosition))
{
// Mounted artboard isn't ready or has a 0 scale transform.
return hitResult;
}
for (auto nestedAnimation : nestedArtboard->nestedAnimations())
{
if (nestedAnimation->is<NestedStateMachine>())
{
auto nestedStateMachine = nestedAnimation->as<NestedStateMachine>();
if (canHit)
{
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;
}
HitResult StateMachineInstance::pointerMove(Vec2D position)
{
return updateListeners(position, ListenerType::move);
}
HitResult StateMachineInstance::pointerDown(Vec2D position)
{
return updateListeners(position, ListenerType::down);
}
HitResult StateMachineInstance::pointerUp(Vec2D position)
{
return updateListeners(position, ListenerType::up);
}
HitResult StateMachineInstance::pointerExit(Vec2D position)
{
return updateListeners(position, ListenerType::exit);
}
StateMachineInstance::StateMachineInstance(const StateMachine* machine,
ArtboardInstance* instance) :
Scene(instance), m_machine(machine)
{
const auto count = machine->inputCount();
m_inputInstances.resize(count);
for (size_t i = 0; i < count; i++)
{
auto input = machine->input(i);
if (input == nullptr)
{
continue;
}
switch (input->coreType())
{
case StateMachineBool::typeKey:
m_inputInstances[i] = new SMIBool(input->as<StateMachineBool>(), this);
break;
case StateMachineNumber::typeKey:
m_inputInstances[i] = new SMINumber(input->as<StateMachineNumber>(), this);
break;
case StateMachineTrigger::typeKey:
m_inputInstances[i] = new SMITrigger(input->as<StateMachineTrigger>(), this);
break;
default:
// Sanity check.
break;
}
}
m_layerCount = machine->layerCount();
m_layers = new StateMachineLayerInstance[m_layerCount];
for (size_t i = 0; i < m_layerCount; i++)
{
m_layers[i].init(this, machine->layer(i), m_artboardInstance);
}
// Initialize listeners. Store a lookup table of shape id to hit shape
// representation (an object that stores all the listeners triggered by the
// shape producing a listener).
std::unordered_map<uint32_t, HitShape*> hitShapeLookup;
for (std::size_t i = 0; i < machine->listenerCount(); i++)
{
auto listener = machine->listener(i);
// Iterate actual leaf hittable shapes tied to this listener and resolve
// corresponding ones in the artboard instance.
for (auto id : listener->hitShapeIds())
{
HitShape* hitShape;
auto itr = hitShapeLookup.find(id);
if (itr == hitShapeLookup.end())
{
auto shape = m_artboardInstance->resolve(id);
if (shape != nullptr && shape->is<Shape>())
{
auto hs = rivestd::make_unique<HitShape>(shape->as<Component>(), this);
hitShapeLookup[id] = hitShape = hs.get();
m_hitComponents.push_back(std::move(hs));
}
else
{
// No object or not a shape...
continue;
}
}
else
{
hitShape = itr->second;
}
hitShape->listeners.push_back(listener);
}
}
for (auto nestedArtboard : instance->nestedArtboards())
{
if (nestedArtboard->hasNestedStateMachines())
{
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>())
{
animation->as<NestedStateMachine>()
->stateMachineInstance()
->setParentNestedArtboard(nestedArtboard);
animation->as<NestedStateMachine>()
->stateMachineInstance()
->setParentStateMachineInstance(this);
}
}
}
}
sortHitComponents();
}
StateMachineInstance::~StateMachineInstance()
{
for (auto inst : m_inputInstances)
{
delete inst;
}
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;
for (size_t i = 0; i < m_layerCount; i++)
{
if (m_layers[i].advance(seconds))
{
m_needsAdvance = true;
}
}
for (auto inst : m_inputInstances)
{
inst->advanced();
}
return m_needsAdvance;
}
bool StateMachineInstance::advanceAndApply(float seconds)
{
bool more = this->advance(seconds);
m_artboardInstance->advance(seconds);
return more;
}
void StateMachineInstance::markNeedsAdvance() { m_needsAdvance = true; }
bool StateMachineInstance::needsAdvance() const { return m_needsAdvance; }
std::string StateMachineInstance::name() const { return m_machine->name(); }
SMIInput* StateMachineInstance::input(size_t index) const
{
if (index < m_inputInstances.size())
{
return m_inputInstances[index];
}
return nullptr;
}
template <typename SMType, typename InstType>
InstType* StateMachineInstance::getNamedInput(const std::string& name) const
{
for (const auto inst : m_inputInstances)
{
auto input = inst->input();
if (input->is<SMType>() && input->name() == name)
{
return static_cast<InstType*>(inst);
}
}
return nullptr;
}
SMIBool* StateMachineInstance::getBool(const std::string& name) const
{
return getNamedInput<StateMachineBool, SMIBool>(name);
}
SMINumber* StateMachineInstance::getNumber(const std::string& name) const
{
return getNamedInput<StateMachineNumber, SMINumber>(name);
}
SMITrigger* StateMachineInstance::getTrigger(const std::string& name) const
{
return getNamedInput<StateMachineTrigger, SMITrigger>(name);
}
size_t StateMachineInstance::stateChangedCount() const
{
size_t count = 0;
for (size_t i = 0; i < m_layerCount; i++)
{
if (m_layers[i].stateChangedOnAdvance())
{
count++;
}
}
return count;
}
const LayerState* StateMachineInstance::stateChangedByIndex(size_t index) const
{
size_t count = 0;
for (size_t i = 0; i < m_layerCount; i++)
{
if (m_layers[i].stateChangedOnAdvance())
{
if (count == index)
{
return m_layers[i].currentState();
}
count++;
}
}
return nullptr;
}
size_t StateMachineInstance::currentAnimationCount() const
{
size_t count = 0;
for (size_t i = 0; i < m_layerCount; i++)
{
if (m_layers[i].currentAnimation() != nullptr)
{
count++;
}
}
return count;
}
const LinearAnimationInstance* StateMachineInstance::currentAnimationByIndex(size_t index) const
{
size_t count = 0;
for (size_t i = 0; i < m_layerCount; i++)
{
if (m_layers[i].currentAnimation() != nullptr)
{
if (count == index)
{
return m_layers[i].currentAnimation();
}
count++;
}
}
return nullptr;
}
void StateMachineInstance::reportEvent(Event* event, float delaySeconds)
{
m_reportedEvents.push_back(EventReport(event, delaySeconds));
}
std::size_t StateMachineInstance::reportedEventCount() const { return m_reportedEvents.size(); }
const EventReport StateMachineInstance::reportedEventAt(std::size_t index) const
{
if (index >= m_reportedEvents.size())
{
return EventReport(nullptr, 0.0f);
}
return m_reportedEvents[index];
}
void StateMachineInstance::notifyEventListeners(const std::vector<EventReport>& events,
NestedArtboard* source)
{
if (events.size() > 0)
{
// We trigger the listeners in order
for (size_t i = 0; i < m_machine->listenerCount(); i++)
{
auto listener = m_machine->listener(i);
auto target = artboard()->resolve(listener->targetId());
if (listener != nullptr && listener->listenerType() == ListenerType::event &&
(source == nullptr || source == target))
{
for (const auto event : events)
{
auto sourceArtboard = source == nullptr ? artboard() : source->artboard();
// listener->eventId() can point to an id from an event in the context of this
// artboard or the context of a nested artboard. Because those ids belong to
// different contexts, they can have the same value. So when the eventId is
// resolved within one context, but actually pointing to the other, it can
// return the wrong event object. If, by chance, that event exists in the other
// context, and is being reported, it will trigger the wrong set of actions.
// This validation makes sure that a listener must be targetting the current
// artboard to disambiguate between external and internal events.
if (source == nullptr &&
sourceArtboard->resolve(listener->targetId()) != artboard())
{
continue;
}
auto listenerEvent = sourceArtboard->resolve(listener->eventId());
if (listenerEvent == event.event())
{
listener->performChanges(this, Vec2D());
break;
}
}
}
}
// Bubble the event up to parent artboard state machines immediately
if (m_parentStateMachineInstance != nullptr)
{
m_parentStateMachineInstance->notifyEventListeners(events, m_parentNestedArtboard);
}
for (auto report : events)
{
auto event = report.event();
if (event->is<AudioEvent>())
{
event->as<AudioEvent>()->play();
}
}
}
}