Nnnnn add data metatable and viewmodels (#11269) f23eb70aec

* feature(scripting): add support for accessing view models from context and creating view model from global data

Co-authored-by: hernan <hernan@rive.app>
This commit is contained in:
bodymovin
2025-12-14 14:47:49 +00:00
parent 69b17fe3a8
commit f0b7370df7
13 changed files with 267 additions and 1 deletions

View File

@@ -1 +1 @@
332a226197975255adecf4f80f2aca99c39fbdd5
f23eb70aecf373d9413b14074f00c9607259093e

View File

@@ -4,9 +4,11 @@
#ifdef WITH_RIVE_SCRIPTING
#include "lua.h"
#endif
#include <vector>
namespace rive
{
class ViewModel;
class LuaState
{
public:
@@ -14,6 +16,7 @@ public:
LuaState(lua_State* state) : state(state) {}
~LuaState() { state = nullptr; }
void initializeData(std::vector<ViewModel*>&);
lua_State* state;
#endif

View File

@@ -162,6 +162,7 @@ enum class LuaAtoms : int16_t
frameOrigin,
data,
instance,
newAtom,
bounds,
pointerDown,
pointerMove,
@@ -201,6 +202,7 @@ enum class LuaAtoms : int16_t
// Scripted Context
markNeedsUpdate,
viewModel,
};
struct ScriptedMat2D

View File

@@ -620,6 +620,7 @@ void File::makeScriptingVM()
rivestd::make_unique<CPPRuntimeScriptingContext>(m_factory);
m_scriptingVM = rivestd::make_unique<ScriptingVM>(m_scriptingContext.get());
m_luaState = new LuaState(m_scriptingVM->state());
m_luaState->initializeData(m_ViewModels);
}
void File::cleanupScriptingVM()

View File

@@ -537,6 +537,7 @@ static int vm_namecall(lua_State* L)
return vm->pushValue(name, ViewModelInstanceListBase::typeKey);
}
case (int)LuaAtoms::instance:
case (int)LuaAtoms::newAtom:
{
assert(vm->state() == L);
return vm->instance(L);

View File

@@ -26,6 +26,22 @@ static int context_namecall(lua_State* L)
scriptedObject->markNeedsUpdate();
return 0;
}
case (int)LuaAtoms::viewModel:
{
auto scriptedObject = scriptedContext->scriptedObject();
auto dataContext = scriptedObject->dataContext();
if (dataContext && dataContext->viewModelInstance())
{
auto viewModelInstance = dataContext->viewModelInstance();
lua_newrive<ScriptedViewModel>(
L,
L,
ref_rcp(viewModelInstance->viewModel()),
viewModelInstance);
return 1;
}
return 0;
}
default:
break;
}

66
src/lua/lua_state.cpp Normal file
View File

@@ -0,0 +1,66 @@
#include "rive/lua/lua_state.hpp"
#ifdef WITH_RIVE_SCRIPTING
#include "rive/lua/rive_lua_libs.hpp"
#endif
#include "rive/viewmodel/viewmodel.hpp"
#include <stdio.h>
using namespace rive;
#ifdef WITH_RIVE_SCRIPTING
static int viewmodel_new(lua_State* L)
{
// Get the viewModel pointer from the upvalue
ViewModel* viewModel = (ViewModel*)lua_touserdata(L, lua_upvalueindex(1));
if (viewModel)
{
auto instance = viewModel->createInstance();
lua_newrive<ScriptedViewModel>(L, L, ref_rcp(viewModel), instance);
return 1;
}
lua_pushnil(L);
return 1;
}
void LuaState::initializeData(std::vector<ViewModel*>& viewModels)
{
if (state)
{
// create metatable for global Data
luaL_newmetatable(state, "Data");
// push it again as lua_setuserdatametatable pops it
lua_pushvalue(state, -1);
lua_setuserdatametatable(state, LUA_T_COUNT + 31);
lua_pop(state, 1);
lua_getfield(state, luaRegistryIndex, "Data");
lua_setglobal(state, "Data");
// Get the Data metatable back on the stack
lua_getfield(state, luaRegistryIndex, "Data");
for (auto& viewModel : viewModels)
{
// Create a new table for this viewModel
lua_createtable(state, 0, 1);
// Push the viewModel pointer as a light userdata (upvalue)
lua_pushlightuserdata(state, viewModel);
// Create a closure with the viewModel as an upvalue
lua_pushcclosurek(state, viewmodel_new, "new", 1, nullptr);
// Set the function as "new" in the table
lua_setfield(state, -2, "new");
// Set the table as a field in Data metatable using the viewModel
// name
lua_setfield(state, -2, viewModel->name().c_str());
}
// Pop the Data metatable from the stack
lua_pop(state, 1);
}
}
#endif

View File

@@ -106,6 +106,7 @@ std::unordered_map<std::string, int16_t> atoms = {
{"frameOrigin", (int16_t)LuaAtoms::frameOrigin},
{"data", (int16_t)LuaAtoms::data},
{"instance", (int16_t)LuaAtoms::instance},
{"new", (int16_t)LuaAtoms::newAtom},
{"bounds", (int16_t)LuaAtoms::bounds},
{"pointerDown", (int16_t)LuaAtoms::pointerDown},
{"pointerUp", (int16_t)LuaAtoms::pointerUp},
@@ -134,6 +135,7 @@ std::unordered_map<std::string, int16_t> atoms = {
{"next", (int16_t)LuaAtoms::next},
{"isClosed", (int16_t)LuaAtoms::isClosed},
{"markNeedsUpdate", (int16_t)LuaAtoms::markNeedsUpdate},
{"viewModel", (int16_t)LuaAtoms::viewModel},
};
static const luaL_Reg lualibs[] = {

Binary file not shown.

View File

@@ -2,6 +2,8 @@
#include "catch.hpp"
#include "scripting_test_utilities.hpp"
#include "rive/lua/rive_lua_libs.hpp"
#include "rive/animation/state_machine_instance.hpp"
#include "rive_file_reader.hpp"
using namespace rive;
@@ -30,3 +32,176 @@ end
CHECK(scriptedObjectTest.needsUpdate());
}
}
TEST_CASE("script has access to user created view models via Data", "[silver]")
{
rive::SerializingFactory silver;
auto file =
ReadRiveFile("assets/script_create_viewmodel_instance.riv", &silver);
auto artboard = file->artboardNamed("main");
silver.frameSize(artboard->width(), artboard->height());
REQUIRE(artboard != nullptr);
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.1f);
auto renderer = silver.makeRenderer();
artboard->draw(renderer.get());
// Push element
{
silver.addFrame();
rive::ViewModelInstanceViewModel* button =
vmi->propertyValue("newButton")
->as<rive::ViewModelInstanceViewModel>();
REQUIRE(button != nullptr);
auto instance = button->referenceViewModelInstance();
rive::ViewModelInstanceTrigger* trigger =
instance->propertyValue("onClick")
->as<rive::ViewModelInstanceTrigger>();
REQUIRE(trigger != nullptr);
trigger->trigger();
stateMachine->advanceAndApply(0.1f);
artboard->draw(renderer.get());
}
// Push element at specific index
{
silver.addFrame();
rive::ViewModelInstanceViewModel* button =
vmi->propertyValue("newAtButton")
->as<rive::ViewModelInstanceViewModel>();
REQUIRE(button != nullptr);
auto instance = button->referenceViewModelInstance();
rive::ViewModelInstanceTrigger* trigger =
instance->propertyValue("onClick")
->as<rive::ViewModelInstanceTrigger>();
REQUIRE(trigger != nullptr);
trigger->trigger();
stateMachine->advanceAndApply(0.1f);
artboard->draw(renderer.get());
}
// Swap elements from indexes
{
silver.addFrame();
rive::ViewModelInstanceViewModel* button =
vmi->propertyValue("swapButton")
->as<rive::ViewModelInstanceViewModel>();
REQUIRE(button != nullptr);
auto instance = button->referenceViewModelInstance();
rive::ViewModelInstanceTrigger* trigger =
instance->propertyValue("onClick")
->as<rive::ViewModelInstanceTrigger>();
REQUIRE(trigger != nullptr);
trigger->trigger();
stateMachine->advanceAndApply(0.1f);
artboard->draw(renderer.get());
}
// Shift first element
{
silver.addFrame();
rive::ViewModelInstanceViewModel* button =
vmi->propertyValue("shiftButton")
->as<rive::ViewModelInstanceViewModel>();
REQUIRE(button != nullptr);
auto instance = button->referenceViewModelInstance();
rive::ViewModelInstanceTrigger* trigger =
instance->propertyValue("onClick")
->as<rive::ViewModelInstanceTrigger>();
REQUIRE(trigger != nullptr);
trigger->trigger();
stateMachine->advanceAndApply(0.1f);
artboard->draw(renderer.get());
}
// Pop last element
{
silver.addFrame();
rive::ViewModelInstanceViewModel* button =
vmi->propertyValue("popButton")
->as<rive::ViewModelInstanceViewModel>();
REQUIRE(button != nullptr);
auto instance = button->referenceViewModelInstance();
rive::ViewModelInstanceTrigger* trigger =
instance->propertyValue("onClick")
->as<rive::ViewModelInstanceTrigger>();
REQUIRE(trigger != nullptr);
trigger->trigger();
stateMachine->advanceAndApply(0.1f);
artboard->draw(renderer.get());
}
// Pop all elements and pop beyond empty list
{
silver.addFrame();
rive::ViewModelInstanceViewModel* button =
vmi->propertyValue("popButton")
->as<rive::ViewModelInstanceViewModel>();
REQUIRE(button != nullptr);
auto instance = button->referenceViewModelInstance();
rive::ViewModelInstanceTrigger* trigger =
instance->propertyValue("onClick")
->as<rive::ViewModelInstanceTrigger>();
REQUIRE(trigger != nullptr);
trigger->trigger();
trigger->trigger();
trigger->trigger();
trigger->trigger();
stateMachine->advanceAndApply(0.1f);
artboard->draw(renderer.get());
}
// Push 2 elements
{
silver.addFrame();
rive::ViewModelInstanceViewModel* button =
vmi->propertyValue("newButton")
->as<rive::ViewModelInstanceViewModel>();
REQUIRE(button != nullptr);
auto instance = button->referenceViewModelInstance();
rive::ViewModelInstanceTrigger* trigger =
instance->propertyValue("onClick")
->as<rive::ViewModelInstanceTrigger>();
REQUIRE(trigger != nullptr);
trigger->trigger();
trigger->trigger();
stateMachine->advanceAndApply(0.1f);
artboard->draw(renderer.get());
}
CHECK(silver.matches("script_create_viewmodel_instance"));
}
TEST_CASE("script has access to the data bound view model", "[silver]")
{
rive::SerializingFactory silver;
auto file = ReadRiveFile("assets/viewmodel_from_context.riv", &silver);
auto artboard = file->artboardNamed("main");
silver.frameSize(artboard->width(), artboard->height());
REQUIRE(artboard != nullptr);
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.1f);
auto renderer = silver.makeRenderer();
artboard->draw(renderer.get());
silver.addFrame();
stateMachine->advanceAndApply(0.1f);
artboard->draw(renderer.get());
CHECK(silver.matches("viewmodel_from_context"));
}

Binary file not shown.