fix(runtime): Databind State machine transition duration (#11947) 98ac9c07a3 Since StateTransitions are shared across StateMachineInstances, databinding a transition's duration on any given state machine will cause that duration value to be applied across any StateMachineInstance using that transition (last binding applied wins). This PR injects BindablePropertyNumbers at runtime to apply the binding values in the correct databinding flow. Co-authored-by: Philip Chung <philterdesign@gmail.com>
diff --git a/.rive_head b/.rive_head index 0137464..f9c4863 100644 --- a/.rive_head +++ b/.rive_head
@@ -1 +1 @@ -fc1c3488ecdeac4a2b343ee4ce4418a05231062e +98ac9c07a3f1ad2c37639c1d55d5a98ad79acff3
diff --git a/include/rive/animation/state_machine_instance.hpp b/include/rive/animation/state_machine_instance.hpp index ce10afd..2b317ec 100644 --- a/include/rive/animation/state_machine_instance.hpp +++ b/include/rive/animation/state_machine_instance.hpp
@@ -14,6 +14,7 @@ #include "rive/listener_type.hpp" #include "rive/nested_animation.hpp" #include "rive/scene.hpp" +#include "rive/data_bind/bindable_property_number.hpp" #include "rive/data_bind/data_bind_container.hpp" #include "rive/input/focusable.hpp" #include "rive/input/focus_manager.hpp" @@ -207,6 +208,13 @@ BindableProperty* bindableProperty) const; DataBind* bindableDataBindToTarget( BindableProperty* bindableProperty) const; + + /// Find the per-instance BindablePropertyNumber for the given shared + /// StateTransition and property key (e.g. durationPropertyKey). + /// Returns nullptr if no binding exists. + BindablePropertyNumber* findTransitionPropertyInstance( + const StateTransition* transition, + uint32_t propertyKey) const; bool hasListeners() { return m_hitComponents.size() > 0; } void clearDataContext(); void relinkDataContext() override; @@ -284,6 +292,12 @@ m_bindableDataBindsToTarget; std::unordered_map<BindableProperty*, DataBind*> m_bindableDataBindsToSource; + /// Map from shared StateTransition* to per-instance BindablePropertyNumber + /// instances, keyed by original property key. Data binds write to these + /// instead of the shared StateTransition object. + std::unordered_map<const Core*, + std::unordered_map<uint32_t, BindablePropertyNumber*>> + m_transitionPropertyInstances; uint8_t m_drawOrderChangeCounter = 0; void unbind(); void removeEventListeners();
diff --git a/include/rive/animation/state_transition.hpp b/include/rive/animation/state_transition.hpp index 79b9fff..1be85e0 100644 --- a/include/rive/animation/state_transition.hpp +++ b/include/rive/animation/state_transition.hpp
@@ -96,6 +96,13 @@ StateTransitionFlags::EnableEarlyExit; } + bool durationIsPercentage() const + { + return (transitionFlags() & + StateTransitionFlags::DurationIsPercentage) == + StateTransitionFlags::DurationIsPercentage; + } + StatusCode import(ImportStack& importStack) override; size_t conditionCount() const { return m_Conditions.size(); }
diff --git a/src/animation/state_machine_instance.cpp b/src/animation/state_machine_instance.cpp index 0fed447..a04dca9 100644 --- a/src/animation/state_machine_instance.cpp +++ b/src/animation/state_machine_instance.cpp
@@ -34,6 +34,8 @@ #include "rive/constraints/draggable_constraint.hpp" #include "rive/data_bind/data_bind_context.hpp" #include "rive/data_bind/data_bind.hpp" +#include "rive/data_bind/context/context_value.hpp" +#include "rive/data_bind/data_values/data_value_number.hpp" #include "rive/data_bind_flags.hpp" #include "rive/event_report.hpp" #include "rive/hit_result.hpp" @@ -62,6 +64,7 @@ #include <unordered_map> #include <vector> #include <chrono> +#include <cmath> using namespace rive; namespace rive @@ -120,13 +123,18 @@ void updateMix(float seconds) { if (m_transition != nullptr && m_stateFrom != nullptr && - m_transition->duration() != 0) + resolvedDuration() != 0) { - m_mix = std::min( - 1.0f, - std::max(0.0f, - (m_mix + seconds / m_transition->mixTime( - m_stateFrom->state())))); + auto mixTime = resolvedMixTime(); + if (mixTime == 0.0f) + { + m_mix = 1.0f; + } + else + { + m_mix = + std::min(1.0f, std::max(0.0f, (m_mix + seconds / mixTime))); + } if (m_mix == 1.0f && !m_transitionCompleted) { m_transitionCompleted = true; @@ -184,10 +192,48 @@ (m_currentState != nullptr && m_currentState->keepGoing()); } + /// Returns the per-instance transition duration, resolving any data + /// binding override. Falls back to the shared definition value when + /// no binding exists. + uint32_t resolvedDuration() const + { + if (m_transitionDurationProperty != nullptr) + { + float val = m_transitionDurationProperty->propertyValue(); + return val < 0 ? 0 : static_cast<uint32_t>(std::round(val)); + } + return m_transition->duration(); + } + + /// Computes the mix time using the per-instance resolved duration. + float resolvedMixTime() const + { + auto dur = resolvedDuration(); + if (dur == 0) + { + return 0; + } + if (m_transition->durationIsPercentage()) + { + float animationDuration = 0.0f; + auto state = m_stateFrom->state(); + if (state->is<AnimationState>()) + { + auto animation = state->as<AnimationState>()->animation(); + if (animation != nullptr) + { + animationDuration = animation->durationSeconds(); + } + } + return (float)dur / 100.0f * animationDuration; + } + return (float)dur / 1000.0f; + } + bool isTransitioning() { return m_transition != nullptr && m_stateFrom != nullptr && - m_transition->duration() != 0 && m_mix < 1.0f; + resolvedDuration() != 0 && m_mix < 1.0f; } bool updateState() @@ -390,9 +436,13 @@ m_stateMachineChangedOnAdvance = true; // state actually has changed m_transition = transition; + m_transitionDurationProperty = + m_stateMachineInstance->findTransitionPropertyInstance( + transition, + StateTransitionBase::durationPropertyKey); fireEvents(StateMachineFireOccurance::atStart, transition->events()); - if (transition->duration() == 0) + if (resolvedDuration() == 0) { m_transitionCompleted = true; fireEvents(StateMachineFireOccurance::atEnd, @@ -526,6 +576,7 @@ StateInstance* m_stateFrom = nullptr; const StateTransition* m_transition = nullptr; + BindablePropertyNumber* m_transitionDurationProperty = nullptr; std::unique_ptr<AnimationReset> m_animationReset = nullptr; bool m_transitionCompleted = false; @@ -1551,7 +1602,22 @@ } else { - dataBindClone->target(dataBind->target()); + auto* originalTarget = dataBind->target(); + dataBindClone->target(originalTarget); + if (originalTarget->is<StateTransitionBase>()) + { + // Create a per-instance BindablePropertyNumber to + // receive the data-bound value instead of writing + // to the shared StateTransition. Swap the target + // and propertyKey so the normal apply() path writes + // to our instance-local property. + auto* prop = new BindablePropertyNumber(); + m_transitionPropertyInstances[originalTarget] + [dataBind->propertyKey()] = prop; + dataBindClone->target(prop); + dataBindClone->propertyKey( + BindablePropertyNumberBase::propertyValuePropertyKey); + } } } @@ -1804,6 +1870,14 @@ delete pair.second; pair.second = nullptr; } + for (auto& outer : m_transitionPropertyInstances) + { + for (auto& inner : outer.second) + { + delete inner.second; + } + } + m_transitionPropertyInstances.clear(); for (auto& listenerViewModel : m_listenerViewModels) { delete listenerViewModel; @@ -2535,6 +2609,22 @@ return dataBind->second; } +BindablePropertyNumber* StateMachineInstance::findTransitionPropertyInstance( + const StateTransition* transition, + uint32_t propertyKey) const +{ + auto it = m_transitionPropertyInstances.find(transition); + if (it != m_transitionPropertyInstances.end()) + { + auto propIt = it->second.find(propertyKey); + if (propIt != it->second.end()) + { + return propIt->second; + } + } + return nullptr; +} + bool StateMachineInstance::keyInput(Key value, KeyModifiers modifiers, bool isPressed,
diff --git a/tests/unit_tests/assets/transition_duration_bind_list.riv b/tests/unit_tests/assets/transition_duration_bind_list.riv new file mode 100644 index 0000000..5c1c4ec --- /dev/null +++ b/tests/unit_tests/assets/transition_duration_bind_list.riv Binary files differ
diff --git a/tests/unit_tests/assets/transition_duration_bind_nested.riv b/tests/unit_tests/assets/transition_duration_bind_nested.riv new file mode 100644 index 0000000..54c6c86 --- /dev/null +++ b/tests/unit_tests/assets/transition_duration_bind_nested.riv Binary files differ
diff --git a/tests/unit_tests/runtime/state_machine_test.cpp b/tests/unit_tests/runtime/state_machine_test.cpp index 8c7a065..cc41a4b 100644 --- a/tests/unit_tests/runtime/state_machine_test.cpp +++ b/tests/unit_tests/runtime/state_machine_test.cpp
@@ -711,4 +711,67 @@ } CHECK(silver.matches("multi_listeners-rebind")); -} \ No newline at end of file +} + +TEST_CASE("Transition duration in nested state machines is bindable", + "[silver]") +{ + SerializingFactory silver; + auto file = + ReadRiveFile("assets/transition_duration_bind_nested.riv", &silver); + auto artboard = file->artboardDefault(); + silver.frameSize(artboard->width(), artboard->height()); + + auto stateMachine = artboard->stateMachineAt(0); + int viewModelId = artboard.get()->viewModelId(); + + auto vmi = viewModelId == -1 + ? file->createViewModelInstance(artboard.get()) + : file->createViewModelInstance(viewModelId, 0); + + stateMachine->bindViewModelInstance(vmi); + stateMachine->advanceAndApply(0.0f); + auto renderer = silver.makeRenderer(); + + int frames = 86; + for (int i = 0; i < frames; i++) + { + silver.addFrame(); + stateMachine->advanceAndApply(0.016f); + artboard->draw(renderer.get()); + } + + CHECK(silver.matches("transition_duration_bind_nested")); +} + +TEST_CASE("Transition duration in artboard list state machines is bindable", + "[silver]") +{ + SerializingFactory silver; + auto file = + ReadRiveFile("assets/transition_duration_bind_list.riv", &silver); + + auto artboard = file->artboardDefault(); + silver.frameSize(artboard->width(), artboard->height()); + + auto stateMachine = artboard->stateMachineAt(0); + int viewModelId = artboard.get()->viewModelId(); + + auto vmi = viewModelId == -1 + ? file->createViewModelInstance(artboard.get()) + : file->createViewModelInstance(viewModelId, 0); + + stateMachine->bindViewModelInstance(vmi); + stateMachine->advanceAndApply(0.0f); + auto renderer = silver.makeRenderer(); + + int frames = 86; + for (int i = 0; i < frames; i++) + { + silver.addFrame(); + stateMachine->advanceAndApply(0.016f); + artboard->draw(renderer.get()); + } + + CHECK(silver.matches("transition_duration_bind_list")); +}
diff --git a/tests/unit_tests/silvers/transition_duration_bind_list.sriv b/tests/unit_tests/silvers/transition_duration_bind_list.sriv new file mode 100644 index 0000000..697dcca --- /dev/null +++ b/tests/unit_tests/silvers/transition_duration_bind_list.sriv Binary files differ
diff --git a/tests/unit_tests/silvers/transition_duration_bind_nested.sriv b/tests/unit_tests/silvers/transition_duration_bind_nested.sriv new file mode 100644 index 0000000..7c2beaf --- /dev/null +++ b/tests/unit_tests/silvers/transition_duration_bind_nested.sriv Binary files differ