blob: fe759c47f4fdd289dfb0f5dc96cdb566addd54a5 [file]
#include "rive/component_dirt.hpp"
#include "rive/assets/script_asset.hpp"
#include "rive/data_bind/data_bind.hpp"
#include "rive/data_bind/data_bind_container.hpp"
#include "rive/data_bind/data_context.hpp"
#include "rive/scripted/scripted_interpolator.hpp"
using namespace rive;
#ifdef WITH_RIVE_SCRIPTING
// Mirrors the Dart `_interpolatorBoilerplate` defaults: identity for
// `transform`, linear interpolation for `transformValue`. Used whenever the
// Lua side isn't ready (no VM, no `m_self`), the script doesn't define the
// method, or the pcall fails — so the runtime degrades to standard linear
// behavior instead of producing garbage.
static inline float defaultTransformValue(float from, float to, float factor)
{
return from + (to - from) * factor;
}
float ScriptedInterpolator::transform(float factor) const
{
if (m_vm == nullptr || !m_vm->valid() || m_self == 0)
{
return factor;
}
float result = factor;
// callNumberMethod wants a non-const ScriptedObject*; transform() is
// const because it doesn't mutate the C++ object — only the script side.
m_vm->callNumberMethod(
const_cast<ScriptedInterpolator*>(this),
m_self,
"transform",
&factor,
1,
&result);
return result;
}
float ScriptedInterpolator::transformValue(
float valueFrom,
float valueTo,
float factor)
{
if (m_vm == nullptr || !m_vm->valid() || m_self == 0)
{
return defaultTransformValue(valueFrom, valueTo, factor);
}
float args[3] = {valueFrom, valueTo, factor};
float result = 0.0f;
if (!m_vm->callNumberMethod(this, m_self, "transformValue", args, 3, &result))
{
return defaultTransformValue(valueFrom, valueTo, factor);
}
return result;
}
#else
float ScriptedInterpolator::transform(float factor) const { return factor; }
float ScriptedInterpolator::transformValue(
float valueFrom,
float valueTo,
float factor)
{
return valueFrom + (valueTo - valueFrom) * factor;
}
#endif
void ScriptedInterpolator::addProperty(CustomProperty* prop)
{
auto* scriptInput = ScriptInput::from(prop);
if (scriptInput != nullptr)
{
scriptInput->scriptedObject(this);
}
CustomPropertyContainer::addProperty(prop);
}
StatusCode ScriptedInterpolator::import(ImportStack& importStack)
{
auto result = registerReferencer(importStack);
if (result != StatusCode::Ok)
{
return result;
}
return Super::import(importStack);
}
// Mirrors ScriptedListenerAction::clone — preserve the file asset reference
// across the clone so the new instance vends the same script bytecode.
Core* ScriptedInterpolator::clone() const
{
auto* twin = ScriptedInterpolatorBase::clone()->as<ScriptedInterpolator>();
if (m_fileAsset != nullptr)
{
twin->setAsset(m_fileAsset);
}
return twin;
}
ScriptedInterpolator::~ScriptedInterpolator() { disposeScriptInputs(); }
// Mirrors ScriptedDataConverter / ScriptedListenerAction /
// ScriptedTransitionCondition: a ScriptedObject that is not a Component is not
// an artboard child, so ScriptInput*::import never routes its inputs to the
// ArtboardImporter and Artboard::m_Objects never frees them.
void ScriptedInterpolator::disposeScriptInputs()
{
auto props = m_customProperties;
ScriptedObject::disposeScriptInputs();
for (auto prop : props)
{
auto scriptInput = ScriptInput::from(prop);
if (scriptInput != nullptr)
{
#ifdef WITH_RIVE_EDITOR
if (prop->editorArena() != nullptr)
continue;
#endif
delete scriptInput;
}
}
}
// Used by LinearAnimationInstance::statefulInterpolator() to vend a per-(LAI,
// keyframe) Lua instance lazily. `dataBindContainer` is the
// ArtboardInstance — it owns any cloned data binds.
ScriptedObject* ScriptedInterpolator::cloneScriptedObject(
DataBindContainer* dataBindContainer) const
{
auto* cloned = clone()->as<ScriptedInterpolator>();
cloneProperties(cloned, dataBindContainer);
cloned->reinit();
return cloned;
}
namespace rive
{
// Exposes the bind plumbing a self-contained StatefulScriptedInterpolator
// forwards from its owner.
class StatefulScriptedInterpolatorBinds final : public DataBindContainer
{
public:
void bind(const rcp<DataContext>& dataContext)
{
bindDataBindsFromContext(dataContext);
}
void unbind() { unbindDataBinds(); }
};
} // namespace rive
StatefulScriptedInterpolator::StatefulScriptedInterpolator(
const ScriptedInterpolator* source,
DataBindContainer* container,
const rcp<DataContext>& dataContext)
: m_container(container)
{
cloneFrom(source, dataContext);
}
StatefulScriptedInterpolator::StatefulScriptedInterpolator(
const ScriptedInterpolator* source,
const rcp<DataContext>& dataContext)
: m_ownedContainer(new StatefulScriptedInterpolatorBinds())
, m_container(m_ownedContainer.get())
{
cloneFrom(source, dataContext);
}
void StatefulScriptedInterpolator::cloneFrom(
const ScriptedInterpolator* source,
const rcp<DataContext>& dataContext)
{
if (source == nullptr || m_container == nullptr)
{
return;
}
auto* cloned = source->cloneScriptedObject(m_container);
if (cloned == nullptr)
{
return;
}
m_clone.reset(static_cast<ScriptedInterpolator*>(cloned));
m_clone->dataContext(dataContext);
// cloneProperties() just parked a bind on `container` for every bound
// input, and wired the cloned input's back-pointer to it. Record them so
// the destructor can pull them out before the clone (their target) dies.
for (auto* prop : m_clone->customProperties())
{
if (auto* input = ScriptInput::from(prop))
{
if (auto* bind = input->dataBind())
{
m_dataBinds.push_back(bind);
}
}
}
// Late-binding safety net: reinit() inside cloneScriptedObject is a no-op
// when scriptAsset() is null at clone time. If the asset became available
// later (editor live-edit), init now.
if (m_clone->scriptAsset() != nullptr && !m_clone->userLuaInitDone())
{
m_clone->scriptAsset()->initScriptedObject(m_clone.get());
m_clone->hydrateScriptInputs();
}
}
StatefulScriptedInterpolator::~StatefulScriptedInterpolator()
{
for (auto* bind : m_dataBinds)
{
m_container->removeDataBind(bind);
delete bind;
}
}
void StatefulScriptedInterpolator::dataContext(
const rcp<DataContext>& dataContext)
{
if (m_clone == nullptr)
{
return;
}
m_clone->dataContext(dataContext);
initialize();
}
void StatefulScriptedInterpolator::bindFromContext(
const rcp<DataContext>& dataContext)
{
if (m_ownedContainer == nullptr || m_clone == nullptr)
{
return;
}
m_ownedContainer->bind(dataContext);
// Binding only marks the input binds dirty; apply them now so the inputs
// hold this context's values before init() reads them.
m_ownedContainer->updateDataBinds(false);
// The clone first ran init() in cloneFrom, before its inputs had a context
// to resolve against. Run it again now that they do, and on every rebind,
// as ScriptedDataConverter does in its own bindFromContext.
m_clone->resetLuaInit();
this->dataContext(dataContext);
}
void StatefulScriptedInterpolator::unbind()
{
if (m_ownedContainer != nullptr)
{
m_ownedContainer->unbind();
}
}
void StatefulScriptedInterpolator::updateDataBinds()
{
if (m_ownedContainer != nullptr)
{
m_ownedContainer->updateDataBinds(false);
}
}
void StatefulScriptedInterpolator::initialize()
{
if (m_clone == nullptr || m_clone->scriptAsset() == nullptr)
{
return;
}
if (!m_clone->userLuaInitDone())
{
m_clone->scriptAsset()->initScriptedObject(m_clone.get());
}
m_clone->hydrateScriptInputs();
}
#ifdef WITH_RIVE_EDITOR
void ScriptedInterpolator::editorPrepareForDestruction()
{
scriptDispose();
dataContext(nullptr);
}
#endif