feature: add scripted nodes! (#10949) 6a61e45ac0

Co-authored-by: Luigi Rosso <luigi-rosso@users.noreply.github.com>
This commit is contained in:
luigi-rosso
2025-10-31 05:31:33 +00:00
parent 0130018219
commit e225f224e4
6 changed files with 516 additions and 43 deletions

View File

@@ -1 +1 @@
45400d839868605fe8df6b794ce4e481affb1881
6a61e45ac0cbfe32bf249d904c42b698a9cd91b0

View File

@@ -150,8 +150,18 @@ enum class LuaAtoms : int16_t
// inputs
hit,
id,
position
position,
// nodes
rotation,
scale,
worldTransform,
scaleX,
scaleY,
decompose,
children,
parent,
node
};
struct ScriptedMat2D
@@ -352,40 +362,58 @@ private:
uint32_t m_saveCount = 0;
};
class ScriptedArtboard
class ScriptReffedArtboard : public RefCnt<ScriptReffedArtboard>
{
public:
ScriptedArtboard(rcp<File> file,
std::unique_ptr<ArtboardInstance>&& artboardInstance);
~ScriptedArtboard()
{
// Make sure artboard is deleted before file.
m_artboard = nullptr;
m_file = nullptr;
}
static constexpr uint8_t luaTag = LUA_T_COUNT + 10;
static constexpr const char* luaName = "Artboard";
static constexpr bool hasMetatable = true;
ScriptReffedArtboard(rcp<File> file,
std::unique_ptr<ArtboardInstance>&& artboardInstance);
~ScriptReffedArtboard();
rive::rcp<rive::File> file() { return m_file; }
Artboard* artboard() { return m_artboard.get(); }
int pushData(lua_State* L);
int instance(lua_State* L);
bool advance(float seconds);
StateMachineInstance* stateMachine() { return m_stateMachine.get(); }
rcp<ViewModelInstance> viewModelInstance() { return m_viewModelInstance; }
private:
rcp<File> m_file;
std::unique_ptr<ArtboardInstance> m_artboard;
std::unique_ptr<StateMachineInstance> m_stateMachine;
rcp<ViewModelInstance> m_viewModelInstance;
int m_dataRef = 0;
// std::vector<WrappedDataBind*> m_dataBinds;
};
// for parent data context
// internalDataContext()
// bindViewModelInstance on state machine
class ScriptedArtboard
{
public:
ScriptedArtboard(rcp<File> file,
std::unique_ptr<ArtboardInstance>&& artboardInstance);
static constexpr uint8_t luaTag = LUA_T_COUNT + 10;
static constexpr const char* luaName = "Artboard";
static constexpr bool hasMetatable = true;
Artboard* artboard() { return m_scriptReffedArtboard->artboard(); }
StateMachineInstance* stateMachine()
{
return m_scriptReffedArtboard->stateMachine();
}
rcp<ViewModelInstance> viewModelInstance()
{
return m_scriptReffedArtboard->viewModelInstance();
}
rcp<ScriptReffedArtboard> scriptReffedArtboard()
{
return m_scriptReffedArtboard;
}
int pushData(lua_State* L);
int instance(lua_State* L);
bool advance(float seconds);
private:
rcp<ScriptReffedArtboard> m_scriptReffedArtboard;
int m_dataRef = 0;
};
struct ScriptedListener
@@ -755,6 +783,24 @@ public:
HitResult m_hitResult = HitResult::none;
};
class ScriptedNode
{
public:
ScriptedNode(rcp<ScriptReffedArtboard> artboard,
TransformComponent* component);
static constexpr uint8_t luaTag = LUA_T_COUNT + 25;
static constexpr const char* luaName = "Node";
static constexpr bool hasMetatable = true;
TransformComponent* component() { return m_component; }
rcp<ScriptReffedArtboard> artboard() { return m_artboard; }
private:
rcp<ScriptReffedArtboard> m_artboard;
TransformComponent* m_component;
};
static void interruptCPP(lua_State* L, int gc);
class CPPRuntimeScriptingContext : public ScriptingContext

View File

@@ -2,12 +2,37 @@
#include "rive/lua/rive_lua_libs.hpp"
#include "rive/viewmodel/viewmodel_property_number.hpp"
#include "rive/viewmodel/viewmodel_property_trigger.hpp"
#include "rive/node.hpp"
#include "rive/bones/root_bone.hpp"
#include "rive/constraints/constraint.hpp"
#include "rive/math/transform_components.hpp"
#include <math.h>
#include <stdio.h>
using namespace rive;
ScriptReffedArtboard::ScriptReffedArtboard(
rcp<File> file,
std::unique_ptr<ArtboardInstance>&& artboardInstance) :
m_file(file),
m_artboard(std::move(artboardInstance)),
m_stateMachine(m_artboard->defaultStateMachine())
{
m_viewModelInstance = m_file->createViewModelInstance(m_artboard.get());
if (m_stateMachine && m_viewModelInstance)
{
m_stateMachine->bindViewModelInstance(m_viewModelInstance);
}
}
ScriptReffedArtboard::~ScriptReffedArtboard()
{
// Make sure artboard is deleted before file.
m_artboard = nullptr;
m_file = nullptr;
}
static int artboard_draw(lua_State* L)
{
auto scriptedArtboard = lua_torive<ScriptedArtboard>(L, 1);
@@ -21,13 +46,14 @@ static int artboard_draw(lua_State* L)
bool ScriptedArtboard::advance(float seconds)
{
if (m_stateMachine)
auto machine = stateMachine();
if (machine)
{
return m_stateMachine->advanceAndApply(seconds);
return machine->advanceAndApply(seconds);
}
else
{
return m_artboard->advance(seconds);
return artboard()->advance(seconds);
}
}
@@ -67,6 +93,24 @@ static int artboard_namecall(lua_State* L)
lua_pushvec2d(L, bounds.max());
return 2;
}
case (int)LuaAtoms::node:
{
auto scriptedArtboard = lua_torive<ScriptedArtboard>(L, 1);
const char* name = luaL_checkstring(L, 2);
auto component =
scriptedArtboard->artboard()->find<TransformComponent>(
name);
if (component == nullptr)
{
lua_pushnil(L);
return 1;
}
lua_newrive<ScriptedNode>(
L,
scriptedArtboard->scriptReffedArtboard(),
component);
return 1;
}
}
}
@@ -84,17 +128,14 @@ int ScriptedArtboard::pushData(lua_State* L)
lua_rawgeti(L, LUA_REGISTRYINDEX, m_dataRef);
return 1;
}
if (m_viewModelInstance == nullptr)
auto vm = viewModelInstance();
if (!vm)
{
lua_pushnil(L);
}
else
{
lua_newrive<ScriptedViewModel>(
L,
L,
ref_rcp(m_viewModelInstance->viewModel()),
m_viewModelInstance);
lua_newrive<ScriptedViewModel>(L, L, ref_rcp(vm->viewModel()), vm);
}
m_dataRef = lua_ref(L, -1);
@@ -103,10 +144,17 @@ int ScriptedArtboard::pushData(lua_State* L)
int ScriptedArtboard::instance(lua_State* L)
{
lua_newrive<ScriptedArtboard>(L, m_file, m_artboard->instance());
lua_newrive<ScriptedArtboard>(L,
m_scriptReffedArtboard->file(),
artboard()->instance());
return 1;
}
ScriptedNode::ScriptedNode(rcp<ScriptReffedArtboard> artboard,
TransformComponent* component) :
m_artboard(artboard), m_component(component)
{}
static int artboard_index(lua_State* L)
{
int atom;
@@ -136,8 +184,14 @@ static int artboard_index(lua_State* L)
return scriptedArtboard->pushData(L);
default:
return 0;
break;
}
luaL_error(L,
"'%s' is not a valid index of %s",
key,
ScriptedArtboard::luaName);
return 0;
}
static int artboard_newindex(lua_State* L)
@@ -167,21 +221,259 @@ static int artboard_newindex(lua_State* L)
break;
}
luaL_error(L,
"'%s' is not a valid index of %s",
key,
ScriptedArtboard::luaName);
return 0;
}
ScriptedArtboard::ScriptedArtboard(
rcp<File> file,
std::unique_ptr<ArtboardInstance>&& artboardInstance) :
m_file(file),
m_artboard(std::move(artboardInstance)),
m_stateMachine(m_artboard->defaultStateMachine())
m_scriptReffedArtboard(
make_rcp<ScriptReffedArtboard>(file, std::move(artboardInstance)))
{}
static int node_index(lua_State* L)
{
m_viewModelInstance = m_file->createViewModelInstance(m_artboard.get());
if (m_stateMachine && m_viewModelInstance)
int atom;
const char* key = lua_tostringatom(L, 2, &atom);
if (!key)
{
m_stateMachine->bindViewModelInstance(m_viewModelInstance);
luaL_typeerrorL(L, 2, lua_typename(L, LUA_TSTRING));
return 0;
}
auto scriptedNode = lua_torive<ScriptedNode>(L, 1);
switch (atom)
{
case (int)LuaAtoms::position:
lua_pushvector2(L,
scriptedNode->component()->x(),
scriptedNode->component()->y());
return 1;
case (int)LuaAtoms::rotation:
lua_pushnumber(L, scriptedNode->component()->rotation());
return 1;
case (int)LuaAtoms::scale:
lua_pushvector2(L,
scriptedNode->component()->scaleX(),
scriptedNode->component()->scaleY());
return 1;
case (int)LuaAtoms::scaleX:
lua_pushnumber(L, scriptedNode->component()->scaleX());
return 1;
case (int)LuaAtoms::scaleY:
lua_pushnumber(L, scriptedNode->component()->scaleY());
return 1;
case (int)LuaAtoms::worldTransform:
lua_newrive<ScriptedMat2D>(
L,
scriptedNode->component()->worldTransform());
return 1;
case (int)LuaAtoms::children:
{
auto component = scriptedNode->component();
if (component->is<ContainerComponent>())
{
auto container = component->as<ContainerComponent>();
// speculative pre-alloc the table
auto& children = container->children();
lua_createtable(L, (int)children.size(), 0);
int index = 1;
for (auto child : children)
{
if (child->is<TransformComponent>())
{
lua_newrive<ScriptedNode>(
L,
scriptedNode->artboard(),
child->as<TransformComponent>());
// Set table[i] = value, pops the value
lua_rawseti(L, -2, index++);
}
}
}
return 1;
}
case (int)LuaAtoms::parent:
{
auto parent = scriptedNode->component()->parent();
if (parent != nullptr && parent->is<TransformComponent>())
{
lua_newrive<ScriptedNode>(L,
scriptedNode->artboard(),
parent->as<TransformComponent>());
}
else
{
lua_pushnil(L);
}
return 1;
}
default:
switch (key[0])
{
case 'x':
lua_pushnumber(L, scriptedNode->component()->x());
return 1;
case 'y':
lua_pushnumber(L, scriptedNode->component()->y());
return 1;
}
}
luaL_error(L,
"'%s' is not a valid index of %s",
key,
ScriptedNode::luaName);
return 0;
}
static int node_newindex(lua_State* L)
{
int atom;
const char* key = lua_tostringatom(L, 2, &atom);
if (!key)
{
luaL_typeerrorL(L, 2, lua_typename(L, LUA_TSTRING));
return 0;
}
auto scriptedNode = lua_torive<ScriptedNode>(L, 1);
auto component = scriptedNode->component();
switch (atom)
{
case (int)LuaAtoms::position:
{
const float* vec = luaL_checkvector(L, 3);
if (component->is<Node>())
{
Node* node = component->as<Node>();
node->x(vec[0]);
node->y(vec[1]);
}
else if (component->is<RootBone>())
{
RootBone* bone = component->as<RootBone>();
bone->x(vec[0]);
bone->y(vec[1]);
}
return 0;
}
case (int)LuaAtoms::rotation:
component->rotation(float(luaL_checknumber(L, 3)));
return 0;
case (int)LuaAtoms::scale:
{
const float* vec = luaL_checkvector(L, 3);
component->scaleX(vec[0]);
component->scaleY(vec[1]);
return 0;
}
case (int)LuaAtoms::scaleX:
component->scaleX(float(luaL_checknumber(L, 3)));
return 0;
case (int)LuaAtoms::scaleY:
component->scaleY(float(luaL_checknumber(L, 3)));
return 0;
case (int)LuaAtoms::worldTransform:
{
Mat2D& transform = component->mutableWorldTransform();
transform = *(Mat2D*)lua_torive<ScriptedMat2D>(L, 3);
return 0;
}
default:
switch (key[0])
{
case 'x':
if (component->is<Node>())
{
Node* node = component->as<Node>();
node->x(float(luaL_checknumber(L, 3)));
}
else if (component->is<RootBone>())
{
RootBone* bone = component->as<RootBone>();
bone->x(float(luaL_checknumber(L, 3)));
}
return 0;
case 'y':
if (component->is<Node>())
{
Node* node = component->as<Node>();
node->y(float(luaL_checknumber(L, 3)));
}
else if (component->is<RootBone>())
{
RootBone* bone = component->as<RootBone>();
bone->y(float(luaL_checknumber(L, 3)));
}
return 0;
}
}
luaL_error(L,
"'%s' is not a valid index of %s",
key,
ScriptedNode::luaName);
return 0;
}
static int node_namecall(lua_State* L)
{
int atom;
const char* str = lua_namecallatom(L, &atom);
if (str != nullptr)
{
switch (atom)
{
case (int)LuaAtoms::decompose:
{
auto scriptedNode = lua_torive<ScriptedNode>(L, 1);
auto component = scriptedNode->component();
Mat2D world = getParentWorld(*component).invertOrIdentity() *
(*(Mat2D*)lua_torive<ScriptedMat2D>(L, 2));
TransformComponents components = world.decompose();
if (component->is<Node>())
{
Node* node = component->as<Node>();
node->x(components.x());
node->y(components.y());
}
else if (component->is<RootBone>())
{
RootBone* bone = component->as<RootBone>();
bone->x(components.x());
bone->y(components.y());
}
component->scaleX(components.scaleX());
component->scaleY(components.scaleY());
component->rotation(components.rotation());
return 0;
}
}
}
luaL_error(L, "%s is not a valid method of %s", str, ScriptedNode::luaName);
return 0;
}
int luaopen_rive_artboards(lua_State* L)
@@ -200,6 +492,20 @@ int luaopen_rive_artboards(lua_State* L)
lua_setreadonly(L, -1, true);
lua_pop(L, 1); // pop the metatable
return 1;
lua_register_rive<ScriptedNode>(L);
lua_pushcfunction(L, node_index, nullptr);
lua_setfield(L, -2, "__index");
lua_pushcfunction(L, node_newindex, nullptr);
lua_setfield(L, -2, "__newindex");
lua_pushcfunction(L, node_namecall, nullptr);
lua_setfield(L, -2, "__namecall");
lua_setreadonly(L, -1, true);
lua_pop(L, 1); // pop the metatable
return 2;
}
#endif

View File

@@ -103,6 +103,15 @@ std::unordered_map<std::string, int16_t> atoms = {
{"hit", (int16_t)LuaAtoms::hit},
{"id", (int16_t)LuaAtoms::id},
{"position", (int16_t)LuaAtoms::position},
{"rotation", (int16_t)LuaAtoms::rotation},
{"scale", (int16_t)LuaAtoms::scale},
{"worldTransform", (int16_t)LuaAtoms::worldTransform},
{"scaleX", (int16_t)LuaAtoms::scaleX},
{"scaleY", (int16_t)LuaAtoms::scaleY},
{"decompose", (int16_t)LuaAtoms::decompose},
{"children", (int16_t)LuaAtoms::children},
{"parent", (int16_t)LuaAtoms::parent},
{"node", (int16_t)LuaAtoms::node},
};
static const luaL_Reg lualibs[] = {

Binary file not shown.

View File

@@ -149,4 +149,116 @@ TEST_CASE("can detect hit test from PointerEvent", "[scripting]")
CHECK(lua_pcall(L, 1, 1, 0) == LUA_OK);
CHECK(pointerEvent->m_hitResult == HitResult::hitOpaque);
CHECK(lua_tonumber(L, -1) == 22.0f);
}
TEST_CASE("can access nodes from artboards", "[scripting]")
{
ScriptingTest vm("function getNode(artboard:Artboard):Node?\n"
" return artboard:node('muzzle')\n"
"end\n"
"function getNodeX(artboard:Artboard):number\n"
" return artboard:node('muzzle').x\n"
"end\n"
"function getNodeY(artboard:Artboard):number\n"
" return artboard:node('muzzle').y\n"
"end\n"
"function getScaleX(artboard:Artboard):number\n"
" return artboard:node('muzzle').scaleX\n"
"end\n"
"function getScaleY(artboard:Artboard):number\n"
" return artboard:node('muzzle').scaleY\n"
"end\n"
"function decompose(artboard:Artboard)\n"
" artboard:node('muzzle'):decompose(Mat2D.identity())\n"
"end\n"
"function getChildren(artboard:Artboard):number\n"
" return #(artboard:node('muzzle').children)\n"
"end\n"
"function getWeaponChildren(artboard:Artboard):number\n"
" return #(artboard:node('Weapon').children)\n"
"end\n"
"function getParent(artboard:Artboard):Node?\n"
" return artboard:node('muzzle').parent\n"
"end\n");
lua_State* L = vm.state();
auto file = ReadRiveFile("assets/joel_v3.riv", vm.serializer());
auto artboard = file->artboard("Character");
REQUIRE(artboard != nullptr);
lua_newrive<ScriptedArtboard>(L, file, artboard->instance());
lua_getglobal(L, "getNode");
lua_pushvalue(L, -2);
CHECK(lua_pcall(L, 1, 1, 0) == LUA_OK);
CHECK(lua_isuserdata(L, -1));
lua_pop(L, 1);
lua_getglobal(L, "getNodeX");
lua_pushvalue(L, -2);
CHECK(lua_pcall(L, 1, 1, 0) == LUA_OK);
CHECK(lua_tonumber(L, -1) == 203.0);
lua_pop(L, 1);
lua_getglobal(L, "getNodeY");
lua_pushvalue(L, -2);
CHECK(lua_pcall(L, 1, 1, 0) == LUA_OK);
CHECK(lua_tonumber(L, -1) == 0.0);
lua_pop(L, 1);
lua_getglobal(L, "getScaleX");
lua_pushvalue(L, -2);
CHECK(lua_pcall(L, 1, 1, 0) == LUA_OK);
CHECK(lua_tonumber(L, -1) == Approx(1.2500029802));
lua_pop(L, 1);
lua_getglobal(L, "getScaleY");
lua_pushvalue(L, -2);
CHECK(lua_pcall(L, 1, 1, 0) == LUA_OK);
CHECK(lua_tonumber(L, -1) == Approx(1.2500029802));
lua_pop(L, 1);
lua_getglobal(L, "decompose");
lua_pushvalue(L, -2);
CHECK(lua_pcall(L, 1, 0, 0) == LUA_OK);
lua_getglobal(L, "getNodeX");
lua_pushvalue(L, -2);
CHECK(lua_pcall(L, 1, 1, 0) == LUA_OK);
CHECK(lua_tonumber(L, -1) == 0.0);
lua_pop(L, 1);
lua_getglobal(L, "getNodeY");
lua_pushvalue(L, -2);
CHECK(lua_pcall(L, 1, 1, 0) == LUA_OK);
CHECK(lua_tonumber(L, -1) == 0.0);
lua_pop(L, 1);
lua_getglobal(L, "getScaleX");
lua_pushvalue(L, -2);
CHECK(lua_pcall(L, 1, 1, 0) == LUA_OK);
CHECK(lua_tonumber(L, -1) == 1);
lua_pop(L, 1);
lua_getglobal(L, "getScaleY");
lua_pushvalue(L, -2);
CHECK(lua_pcall(L, 1, 1, 0) == LUA_OK);
CHECK(lua_tonumber(L, -1) == 1);
lua_pop(L, 1);
lua_getglobal(L, "getChildren");
lua_pushvalue(L, -2);
CHECK(lua_pcall(L, 1, 1, 0) == LUA_OK);
CHECK(lua_tonumber(L, -1) == 0);
lua_pop(L, 1);
lua_getglobal(L, "getParent");
lua_pushvalue(L, -2);
CHECK(lua_pcall(L, 1, 1, 0) == LUA_OK);
CHECK(lua_isuserdata(L, -1));
lua_pop(L, 1);
lua_getglobal(L, "getWeaponChildren");
lua_pushvalue(L, -2);
CHECK(lua_pcall(L, 1, 1, 0) == LUA_OK);
CHECK(lua_tonumber(L, -1) == 9);
lua_pop(L, 1);
}