blob: 754b6572d07f6811504d2ba3067664ac6e3dc130 [file] [log] [blame]
#include "rive/animation/state_transition.hpp"
#include "rive/importers/import_stack.hpp"
#include "rive/importers/layer_state_importer.hpp"
#include "rive/animation/layer_state.hpp"
#include "rive/animation/transition_condition.hpp"
#include "rive/animation/animation_state.hpp"
#include "rive/animation/linear_animation.hpp"
#include "rive/animation/state_machine_input_instance.hpp"
#include "rive/animation/state_machine_trigger.hpp"
#include "rive/animation/animation_state_instance.hpp"
#include "rive/animation/transition_trigger_condition.hpp"
using namespace rive;
StateTransition::~StateTransition()
{
for (auto condition : m_Conditions)
{
delete condition;
}
}
StatusCode StateTransition::onAddedDirty(CoreContext* context)
{
StatusCode code;
for (auto condition : m_Conditions)
{
if ((code = condition->onAddedDirty(context)) != StatusCode::Ok)
{
return code;
}
}
return StatusCode::Ok;
}
StatusCode StateTransition::onAddedClean(CoreContext* context)
{
StatusCode code;
for (auto condition : m_Conditions)
{
if ((code = condition->onAddedClean(context)) != StatusCode::Ok)
{
return code;
}
}
return StatusCode::Ok;
}
StatusCode StateTransition::import(ImportStack& importStack)
{
auto stateImporter =
importStack.latest<LayerStateImporter>(LayerState::typeKey);
if (stateImporter == nullptr)
{
return StatusCode::MissingObject;
}
stateImporter->addTransition(this);
return Super::import(importStack);
}
void StateTransition::addCondition(TransitionCondition* condition)
{
m_Conditions.push_back(condition);
}
float StateTransition::mixTime(const LayerState* stateFrom) const
{
if (duration() == 0)
{
return 0;
}
if ((transitionFlags() & StateTransitionFlags::DurationIsPercentage) ==
StateTransitionFlags::DurationIsPercentage)
{
float animationDuration = 0.0f;
if (stateFrom->is<AnimationState>())
{
auto animation = stateFrom->as<AnimationState>()->animation();
if (animation != nullptr)
{
animationDuration = animation->durationSeconds();
}
}
return duration() / 100.0f * animationDuration;
}
else
{
return duration() / 1000.0f;
}
}
float StateTransition::exitTimeSeconds(const LayerState* stateFrom,
bool absolute) const
{
if ((transitionFlags() & StateTransitionFlags::ExitTimeIsPercentage) ==
StateTransitionFlags::ExitTimeIsPercentage)
{
float animationDuration = 0.0f;
float start = 0.0f;
auto exitAnimation = exitTimeAnimation(stateFrom);
if (exitAnimation != nullptr)
{
start = absolute ? exitAnimation->startSeconds() : 0.0f;
animationDuration = exitAnimation->durationSeconds();
}
return start + exitTime() / 100.0f * animationDuration;
}
return exitTime() / 1000.0f;
}
const LinearAnimationInstance*
StateTransition::exitTimeAnimationInstance(const StateInstance* from) const
{
return from != nullptr && from->state()->is<AnimationState>()
? static_cast<const AnimationStateInstance*>(from)
->animationInstance()
: nullptr;
}
const LinearAnimation*
StateTransition::exitTimeAnimation(const LayerState* from) const
{
return from != nullptr && from->is<AnimationState>()
? from->as<AnimationState>()->animation()
: nullptr;
}
AllowTransition StateTransition::allowed(StateInstance* stateFrom,
SMIInput** inputs,
bool ignoreTriggers) const
{
if (isDisabled())
{
return AllowTransition::no;
}
for (auto condition : m_Conditions)
{
// N.B. state machine instance sanitizes these for us...
auto input = inputs[condition->inputId()];
if ((ignoreTriggers && condition->is<TransitionTriggerCondition>()) ||
!condition->evaluate(input))
{
return AllowTransition::no;
}
}
if (enableExitTime())
{
auto exitAnimation = exitTimeAnimationInstance(stateFrom);
if (exitAnimation != nullptr)
{
// Exit time is specified in a value less than a single loop, so we
// want to allow exiting regardless of which loop we're on. To do
// that we bring the exit time up to the loop our lastTime is at.
auto lastTime = exitAnimation->lastTotalTime();
auto time = exitAnimation->totalTime();
auto exitTime = exitTimeSeconds(stateFrom->state());
auto animationFrom = exitAnimation->animation();
auto duration = animationFrom->durationSeconds();
// TODO: there are some considerations to have when exit time is combined with another condition (like trigger)
// - not sure how to get this to make sense with pingPing animations
// - also if exit time is, say 50% on a loop, this will be happy to fire
// - when time is anywhere in 50%-100%, 150%-200%. as opposed to just at 50%
// .... makes you wonder if we need some kind of exit after/exit before time
// .... but i suspect that will introduce some more issues?
// There's only one iteration in oneShot,
if (exitTime <= duration && animationFrom->loop() !=Loop::oneShot)
{
// Get exit time relative to the loop lastTime was in.
exitTime += std::floor(lastTime / duration) * duration;
}
if (time < exitTime)
{
return AllowTransition::waitingForExit;
}
}
}
return AllowTransition::yes;
}
bool StateTransition::applyExitCondition(StateInstance* from) const
{
// Hold exit time when the user has set to pauseOnExit on this condition
// (only valid when exiting from an Animation).
bool useExitTime = enableExitTime() &&
(from != nullptr && from->state()->is<AnimationState>());
if (pauseOnExit() && useExitTime)
{
static_cast<AnimationStateInstance*>(from)->animationInstance()->time(
exitTimeSeconds(from->state(), true));
return true;
}
return useExitTime;
}