blob: 24c4b3a3312f6fe01fa2c9d92a752c23c5f89596 [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, Span<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;
}