blob: e0a7132c335ad44d6c69c47d568033c80d11081e [file] [log] [blame]
#include "rive/animation/state_machine_instance.hpp"
#include "rive/animation/state_machine_input.hpp"
#include "rive/animation/state_machine_bool.hpp"
#include "rive/animation/state_machine_number.hpp"
#include "rive/animation/state_machine_trigger.hpp"
#include "rive/animation/state_machine_input_instance.hpp"
#include "rive/animation/state_machine.hpp"
#include "rive/animation/state_machine_layer.hpp"
#include "rive/animation/any_state.hpp"
#include "rive/animation/entry_state.hpp"
#include "rive/animation/state_transition.hpp"
#include "rive/animation/transition_condition.hpp"
#include "rive/animation/animation_state.hpp"
#include "rive/animation/state_instance.hpp"
#include "rive/animation/animation_state_instance.hpp"
#include "rive/animation/state_machine_listener.hpp"
#include "rive/shapes/shape.hpp"
#include "rive/math/aabb.hpp"
#include "rive/math/hit_test.hpp"
#include "rive/nested_artboard.hpp"
#include "rive/nested_animation.hpp"
#include "rive/animation/nested_state_machine.hpp"
#include "rive/rive_counter.hpp"
#include <unordered_map>
using namespace rive;
namespace rive
{
class StateMachineLayerInstance
{
private:
static const int maxIterations = 100;
const StateMachineLayer* m_Layer = nullptr;
ArtboardInstance* m_ArtboardInstance = nullptr;
StateInstance* m_AnyStateInstance = nullptr;
StateInstance* m_CurrentState = nullptr;
StateInstance* m_StateFrom = nullptr;
// const LayerState* m_CurrentState = nullptr;
// const LayerState* m_StateFrom = nullptr;
const StateTransition* m_Transition = nullptr;
bool m_HoldAnimationFrom = false;
// LinearAnimationInstance* m_AnimationInstance = nullptr;
// LinearAnimationInstance* m_AnimationInstanceFrom = nullptr;
float m_Mix = 1.0f;
float m_MixFrom = 1.0f;
bool m_StateChangedOnAdvance = 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;
public:
~StateMachineLayerInstance()
{
delete m_AnyStateInstance;
delete m_CurrentState;
delete m_StateFrom;
}
void init(const StateMachineLayer* layer, ArtboardInstance* instance)
{
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()))));
}
else
{
m_Mix = 1.0f;
}
}
bool advance(/*Artboard* artboard, */ float seconds, Span<SMIInput*> inputs)
{
m_StateChangedOnAdvance = false;
if (m_CurrentState != nullptr)
{
m_CurrentState->advance(seconds, inputs);
}
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, inputs);
}
for (int i = 0; updateState(inputs, i != 0); i++)
{
apply();
if (i == maxIterations)
{
fprintf(stderr, "StateMachine exceeded max iterations.\n");
return false;
}
}
apply();
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(Span<SMIInput*> inputs, bool ignoreTriggers)
{
// Don't allow changing state while a transition is taking place
// (we're mixing one state onto another).
if (isTransitioning())
{
return false;
}
m_WaitingForExit = false;
if (tryChangeState(m_AnyStateInstance, inputs, ignoreTriggers))
{
return true;
}
return tryChangeState(m_CurrentState, inputs, ignoreTriggers);
}
bool changeState(const LayerState* stateTo)
{
if ((m_CurrentState == nullptr ? nullptr : m_CurrentState->state()) == stateTo)
{
return false;
}
m_CurrentState =
stateTo == nullptr ? nullptr : stateTo->makeInstance(m_ArtboardInstance).release();
return true;
}
bool tryChangeState(StateInstance* stateFromInstance,
Span<SMIInput*> inputs,
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, inputs, ignoreTriggers);
if (allowed == AllowTransition::yes && changeState(transition->stateTo()))
{
m_StateChangedOnAdvance = true;
// state actually has changed
m_Transition = transition;
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, inputs);
}
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;
}
if (m_StateFrom != nullptr && m_Mix < 1.0f)
{
m_StateFrom->apply(m_MixFrom);
}
if (m_CurrentState != nullptr)
{
m_CurrentState->apply(m_Mix);
}
}
bool stateChangedOnAdvance() const { return m_StateChangedOnAdvance; }
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();
}
};
/// 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
{
private:
Shape* m_Shape;
public:
Shape* shape() const { return m_Shape; }
HitShape(Shape* shape) : m_Shape(shape) {}
bool isHovered = false;
std::vector<const StateMachineListener*> listeners;
};
} // namespace rive
void StateMachineInstance::updateListeners(Vec2D position, ListenerType hitType)
{
if (m_ArtboardInstance->frameOrigin())
{
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)
{
// 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(this, position);
markNeedsAdvance();
}
else if (!isOver && listener->listenerType() == ListenerType::exit)
{
listener->performChanges(this, position);
markNeedsAdvance();
}
}
if (isOver && hitType == listener->listenerType())
{
listener->performChanges(this, position);
markNeedsAdvance();
}
}
}
// 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)
{
Vec2D nestedPosition;
if (!nestedArtboard->worldToLocal(position, &nestedPosition))
{
// Mounted artboard isn't ready or has a 0 scale transform.
continue;
}
for (auto nestedAnimation : nestedArtboard->nestedAnimations())
{
if (nestedAnimation->is<NestedStateMachine>())
{
auto nestedStateMachine = nestedAnimation->as<NestedStateMachine>();
switch (hitType)
{
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:
break;
}
}
}
}
}
void StateMachineInstance::pointerMove(Vec2D position)
{
updateListeners(position, ListenerType::move);
}
void StateMachineInstance::pointerDown(Vec2D position)
{
updateListeners(position, ListenerType::down);
}
void StateMachineInstance::pointerUp(Vec2D position)
{
updateListeners(position, ListenerType::up);
}
StateMachineInstance::StateMachineInstance(const StateMachine* machine,
ArtboardInstance* instance) :
Scene(instance), m_Machine(machine)
{
Counter::update(Counter::kStateMachineInstance, +1);
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(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<Shape>());
hitShapeLookup[id] = hitShape = hs.get();
m_HitShapes.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())
{
m_HitNestedArtboards.push_back(nestedArtboard);
}
}
}
StateMachineInstance::~StateMachineInstance()
{
for (auto inst : m_InputInstances)
{
delete inst;
}
delete[] m_Layers;
Counter::update(Counter::kStateMachineInstance, -1);
}
bool StateMachineInstance::advance(float seconds)
{
m_NeedsAdvance = false;
for (size_t i = 0; i < m_LayerCount; i++)
{
if (m_Layers[i].advance(seconds, m_InputInstances))
{
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;
}