diff --git a/src/features/editScene/AGENTS.md b/src/features/editScene/AGENTS.md index 6c91651..779e272 100644 --- a/src/features/editScene/AGENTS.md +++ b/src/features/editScene/AGENTS.md @@ -147,6 +147,14 @@ to avoid stuck keys caused by missed `keyReleased` events. One-shot action flags ### AnimationTreeSystem & Root Motion +- Animation trees are stored in the global `AnimationTreeRegistry`, persisted to + `animation_tree.json`. Each `AnimationTreeComponent` references a registry + tree by name only. +- Trees are authored in **Tools -> Animation Tree Registry**. Each registry + entry can select a skeleton source mesh from the `Characters` resource group; + this is used only by the editor to populate animation-name dropdowns. +- `AnimationTreeSystem` resolves the tree from the registry each frame and + evaluates it. - Selects a root bone (`Root`, `mixamorig:Hips`, then `Spineroot`), freezes it, and excludes it from animation via a blend mask. - Computes `CharacterComponent::linearVelocity` from root-bone displacement when diff --git a/src/features/editScene/CMakeLists.txt b/src/features/editScene/CMakeLists.txt index d8f9f83..89bd5a4 100644 --- a/src/features/editScene/CMakeLists.txt +++ b/src/features/editScene/CMakeLists.txt @@ -37,6 +37,7 @@ set(EDITSCENE_SOURCES systems/StartupMenuSystem.cpp systems/PauseMenuSystem.cpp systems/ItemRegistry.cpp + systems/AnimationTreeRegistry.cpp systems/ContainerStateRegistry.cpp systems/ItemStateRegistry.cpp systems/SaveLoadSystem.cpp @@ -102,7 +103,8 @@ set(EDITSCENE_SOURCES ui/CharacterSpawnerEditor.cpp ui/CharacterIdentityEditor.cpp ui/AnimationTreeEditor.cpp - ui/AnimationTreeTemplateEditor.cpp + ui/AnimationTreeNodeEditor.cpp + ui/AnimationTreeRegistryEditor.cpp ui/CharacterEditor.cpp ui/CellGridEditor.cpp ui/LotEditor.cpp @@ -144,7 +146,6 @@ set(EDITSCENE_SOURCES components/CharacterSlotsModule.cpp components/CharacterSpawnerModule.cpp components/AnimationTreeModule.cpp - components/AnimationTreeTemplateModule.cpp components/AnimationTree.cpp components/CharacterModule.cpp components/CellGridModule.cpp @@ -171,6 +172,7 @@ set(EDITSCENE_SOURCES lua/LuaState.cpp lua/LuaEntityApi.cpp lua/LuaComponentApi.cpp + lua/LuaAnimationTreeApi.cpp lua/LuaEventApi.cpp lua/LuaActionApi.cpp lua/LuaBehaviorTreeApi.cpp @@ -222,6 +224,7 @@ set(EDITSCENE_HEADERS systems/StartupMenuSystem.hpp systems/PauseMenuSystem.hpp systems/ItemRegistry.hpp + systems/AnimationTreeRegistry.hpp systems/ContainerStateRegistry.hpp systems/ItemStateRegistry.hpp systems/PlayerControllerSystem.hpp @@ -305,7 +308,8 @@ set(EDITSCENE_HEADERS ui/CharacterSpawnerEditor.hpp ui/CharacterIdentityEditor.hpp ui/AnimationTreeEditor.hpp - ui/AnimationTreeTemplateEditor.hpp + ui/AnimationTreeNodeEditor.hpp + ui/AnimationTreeRegistryEditor.hpp ui/CharacterEditor.hpp ui/CellGridEditor.hpp ui/LotEditor.hpp @@ -344,6 +348,7 @@ set(EDITSCENE_HEADERS lua/LuaState.hpp lua/LuaEntityApi.hpp lua/LuaComponentApi.hpp + lua/LuaAnimationTreeApi.hpp lua/LuaEventApi.hpp lua/LuaActionApi.hpp lua/LuaBehaviorTreeApi.hpp diff --git a/src/features/editScene/EditorApp.cpp b/src/features/editScene/EditorApp.cpp index 92740af..2c1f3d9 100644 --- a/src/features/editScene/EditorApp.cpp +++ b/src/features/editScene/EditorApp.cpp @@ -35,6 +35,7 @@ #include "systems/PauseMenuSystem.hpp" #include "systems/DialogueSystem.hpp" #include "systems/ItemRegistry.hpp" +#include "systems/AnimationTreeRegistry.hpp" #include "systems/ContainerStateRegistry.hpp" #include "systems/ItemStateRegistry.hpp" #include "systems/CharacterClassSystem.hpp" @@ -75,7 +76,6 @@ #include "components/CharacterIdentity.hpp" #include "systems/CharacterRegistry.hpp" #include "components/AnimationTree.hpp" -#include "components/AnimationTreeTemplate.hpp" #include "components/Character.hpp" #include "components/StartupMenu.hpp" #include "components/PlayerController.hpp" @@ -115,6 +115,7 @@ #include #include "lua/LuaEntityApi.hpp" #include "lua/LuaComponentApi.hpp" +#include "lua/LuaAnimationTreeApi.hpp" #include "lua/LuaEventApi.hpp" #include "lua/LuaActionApi.hpp" #include "lua/LuaBehaviorTreeApi.hpp" @@ -519,6 +520,7 @@ void EditorApp::setup() PauseMenuSystem::getInstance().init(this); static ItemRegistry s_itemRegistry; ItemRegistry::getSingleton().initialize(); + AnimationTreeRegistry::getSingleton().initialize(); ItemStateRegistry::getInstance().loadFromFile( "item_state.json"); ContainerStateRegistry::getInstance().loadFromFile( @@ -600,6 +602,7 @@ void EditorApp::setup() // Component and Event APIs add to it. editScene::registerLuaEntityApi(L); editScene::registerLuaComponentApi(L); + editScene::registerLuaAnimationTreeApi(L); editScene::registerLuaEventApi(L); editScene::registerLuaActionApi(L); editScene::registerLuaBehaviorTreeApi(L); @@ -1224,9 +1227,6 @@ void EditorApp::setupECS() // Register AnimationTree component m_world.component(); - // Register AnimationTreeTemplate component - m_world.component(); - // Register Character component m_world.component(); diff --git a/src/features/editScene/components/AnimationTree.cpp b/src/features/editScene/components/AnimationTree.cpp index 8ac684f..20fa022 100644 --- a/src/features/editScene/components/AnimationTree.cpp +++ b/src/features/editScene/components/AnimationTree.cpp @@ -1,9 +1,61 @@ #include "AnimationTree.hpp" AnimationTreeComponent::AnimationTreeComponent() - : root() + : treeName() , enabled(true) , useRootMotion(false) , dirty(true) + , treeVersion(0) { } + +nlohmann::json animationTreeNodeToJson(const AnimationTreeNode &node) +{ + nlohmann::json json; + json["type"] = node.type; + if (!node.name.empty()) + json["name"] = node.name; + if (!node.animationName.empty()) + json["animationName"] = node.animationName; + if (node.speed != 1.0f) + json["speed"] = node.speed; + if (node.fadeSpeed != 7.5f) + json["fadeSpeed"] = node.fadeSpeed; + if (!node.endTransitions.empty()) { + nlohmann::json trans = nlohmann::json::object(); + for (const auto &pair : node.endTransitions) + trans[pair.first] = pair.second; + json["endTransitions"] = trans; + } + if (!node.children.empty()) { + json["children"] = nlohmann::json::array(); + for (const auto &child : node.children) + json["children"].push_back(animationTreeNodeToJson(child)); + } + return json; +} + +void animationTreeNodeFromJson(AnimationTreeNode &node, + const nlohmann::json &json) +{ + node.type = json.value("type", "animation"); + node.name = json.value("name", ""); + node.animationName = json.value("animationName", ""); + node.speed = json.value("speed", 1.0f); + node.fadeSpeed = json.value("fadeSpeed", 7.5f); + node.endTransitions.clear(); + if (json.contains("endTransitions") && + json["endTransitions"].is_object()) { + for (auto it = json["endTransitions"].begin(); + it != json["endTransitions"].end(); ++it) + node.endTransitions[it.key()] = it.value(); + } + node.children.clear(); + if (json.contains("children") && json["children"].is_array()) { + for (const auto &childJson : json["children"]) { + AnimationTreeNode child; + animationTreeNodeFromJson(child, childJson); + node.children.push_back(child); + } + } +} diff --git a/src/features/editScene/components/AnimationTree.hpp b/src/features/editScene/components/AnimationTree.hpp index d3b4f2d..60ae68d 100644 --- a/src/features/editScene/components/AnimationTree.hpp +++ b/src/features/editScene/components/AnimationTree.hpp @@ -2,6 +2,7 @@ #define EDITSCENE_ANIMATIONTREE_HPP #pragma once #include +#include #include #include @@ -138,6 +139,11 @@ struct AnimationTreeNode { } }; +/* Serialization helpers shared between registry and scene serializer. */ +nlohmann::json animationTreeNodeToJson(const AnimationTreeNode &node); +void animationTreeNodeFromJson(AnimationTreeNode &node, + const nlohmann::json &json); + /** * Animation tree component for hierarchical state-machine-based animation. * @@ -145,16 +151,15 @@ struct AnimationTreeNode { * which Ogre AnimationStates are active and their blend weights. */ struct AnimationTreeComponent { - AnimationTreeNode root; + /* Name of the tree in AnimationTreeRegistry. */ + Ogre::String treeName; + bool enabled = true; bool useRootMotion = false; bool dirty = true; - /* If set, the tree root is copied from the named template */ - Ogre::String templateName; - - /* Runtime: last copied template version (not serialized) */ - uint64_t templateVersion = 0; + /* Runtime: last resolved registry version (not serialized) */ + uint64_t treeVersion = 0; /* Runtime: current state of each state machine (not serialized) */ std::unordered_map currentStates; diff --git a/src/features/editScene/components/AnimationTreeModule.cpp b/src/features/editScene/components/AnimationTreeModule.cpp index 88300bf..32a243e 100644 --- a/src/features/editScene/components/AnimationTreeModule.cpp +++ b/src/features/editScene/components/AnimationTreeModule.cpp @@ -1,14 +1,14 @@ #include "AnimationTree.hpp" #include "../ui/ComponentRegistration.hpp" #include "../ui/AnimationTreeEditor.hpp" +#include "../systems/AnimationTreeRegistry.hpp" #include "Transform.hpp" -static AnimationTreeComponent createDefaultTree() +static Ogre::String createDefaultTree() { - AnimationTreeComponent at; - - at.root.type = "output"; - at.root.speed = 1.0f; + AnimationTreeNode root; + root.type = "output"; + root.speed = 1.0f; AnimationTreeNode sm; sm.type = "stateMachine"; @@ -25,9 +25,17 @@ static AnimationTreeComponent createDefaultTree() state.children.push_back(anim); sm.children.push_back(state); - at.root.children.push_back(sm); + root.children.push_back(sm); - return at; + AnimationTreeRegistry ® = AnimationTreeRegistry::getSingleton(); + Ogre::String name = reg.generateUniqueName("default"); + + AnimationTreeRegistry::AnimationTreeDefinition def; + def.name = name; + def.root = root; + reg.registerTree(def); + + return name; } REGISTER_COMPONENT_GROUP("Animation Tree", "Rendering", @@ -45,7 +53,8 @@ REGISTER_COMPONENT_GROUP("Animation Tree", "Rendering", ->createChildSceneNode(); e.set(transform); } - AnimationTreeComponent at = createDefaultTree(); + AnimationTreeComponent at; + at.treeName = createDefaultTree(); at.dirty = true; e.set(at); }, diff --git a/src/features/editScene/components/AnimationTreeTemplate.hpp b/src/features/editScene/components/AnimationTreeTemplate.hpp deleted file mode 100644 index 964d3a0..0000000 --- a/src/features/editScene/components/AnimationTreeTemplate.hpp +++ /dev/null @@ -1,20 +0,0 @@ -#ifndef EDITSCENE_ANIMATIONTREETEMPLATE_HPP -#define EDITSCENE_ANIMATIONTREETEMPLATE_HPP -#pragma once - -#include - -/** - * Template marker for reusable animation trees. - * - * Entities with this component serve as shared animation tree templates. - * They should also have an AnimationTreeComponent for editing the tree. - * Other entities reference the template by name via - * AnimationTreeComponent::templateName. - */ -struct AnimationTreeTemplate { - Ogre::String name; - uint64_t version = 1; -}; - -#endif // EDITSCENE_ANIMATIONTREETEMPLATE_HPP diff --git a/src/features/editScene/components/AnimationTreeTemplateModule.cpp b/src/features/editScene/components/AnimationTreeTemplateModule.cpp deleted file mode 100644 index e8a8a5a..0000000 --- a/src/features/editScene/components/AnimationTreeTemplateModule.cpp +++ /dev/null @@ -1,23 +0,0 @@ -#include "AnimationTreeTemplate.hpp" -#include "../ui/ComponentRegistration.hpp" -#include "../ui/AnimationTreeTemplateEditor.hpp" - -REGISTER_COMPONENT_GROUP("Animation Tree Template", "Animation", - AnimationTreeTemplate, AnimationTreeTemplateEditor) -{ - registry.registerComponent( - AnimationTreeTemplate_name, AnimationTreeTemplate_group, - std::make_unique(), - // Adder - [](flecs::entity e) { - if (!e.has()) { - e.set({}); - } - }, - // Remover - [](flecs::entity e) { - if (e.has()) { - e.remove(); - } - }); -} diff --git a/src/features/editScene/lua-examples/animation_tree_registry_example.lua b/src/features/editScene/lua-examples/animation_tree_registry_example.lua new file mode 100644 index 0000000..038dac6 --- /dev/null +++ b/src/features/editScene/lua-examples/animation_tree_registry_example.lua @@ -0,0 +1,79 @@ +-- ============================================================================= +-- Animation Tree Registry Lua API Examples +-- ============================================================================= +-- Animation trees are stored in the Animation Tree Registry +-- (animation_tree.json). They are referenced from entities by name and edited +-- in the registry editor (Tools -> Animation Tree Registry). +-- +-- The Lua API lets you create, query and remove registry entries. +-- ============================================================================= + +-- Register a simple humanoid locomotion tree. +-- skeletonSource is optional; when set, the registry editor loads that mesh +-- from the Characters resource group to populate animation-name dropdowns. +ecs.animation_trees.register("humanoid", { + skeletonSource = "male_Face.mesh", + root = { + type = "output", + speed = 1.0, + children = { + { + type = "stateMachine", + name = "main", + fadeSpeed = 7.5, + children = { + { + type = "state", + name = "idle", + children = { + { type = "animation", animationName = "idle" } + } + }, + { + type = "state", + name = "walk", + children = { + { type = "animation", animationName = "walk" } + } + }, + { + type = "state", + name = "run", + children = { + { type = "animation", animationName = "run" } + } + } + }, + endTransitions = { + -- auto-transition when the source state's animation ends + idle = "walk" + } + } + } + } +}) + +-- List all registered trees +local trees = ecs.animation_trees.list() +for _, name in ipairs(trees) do + print("Registered animation tree: " .. name) +end + +-- Query a tree definition +local def = ecs.animation_trees.find("humanoid") +if def then + print("Tree: " .. def.name) + print("Skeleton source: " .. def.skeletonSource) + print("Root type: " .. def.root.type) +end + +-- Attach the tree to an entity +local entity = ecs.create_entity() +ecs.set_component(entity, "AnimationTree", { + treeName = "humanoid", + enabled = true, + useRootMotion = false +}) + +-- Persist the registry to disk +ecs.animation_trees.save() diff --git a/src/features/editScene/lua-examples/component_example.lua b/src/features/editScene/lua-examples/component_example.lua index bee1faf..72631d1 100644 --- a/src/features/editScene/lua-examples/component_example.lua +++ b/src/features/editScene/lua-examples/component_example.lua @@ -551,15 +551,44 @@ local chair = create_furniture("Wooden Chair", "chair.mesh", "seating") -- Working with Animation Components -- ============================================================================= +-- Animation trees are authored in the Animation Tree Registry +-- (Tools -> Animation Tree Registry). Entities reference a registry tree by +-- name. The registry can also be manipulated from Lua: +ecs.animation_trees.register("humanoid", { + skeletonSource = "male_Face.mesh", + root = { + type = "output", + children = { + { + type = "stateMachine", + name = "main", + fadeSpeed = 7.5, + children = { + { + type = "state", + name = "idle", + children = { + { type = "animation", animationName = "idle" } + } + }, + { + type = "state", + name = "walk", + children = { + { type = "animation", animationName = "walk" } + } + } + } + } + } + } +}) + -- Add animation tree to a character: ecs.set_component(npc, "AnimationTree", { treeName = "humanoid", - enabled = true -}) - -ecs.set_component(npc, "AnimationTreeTemplate", { - templateName = "humanoid_base", - blendTime = 0.2 + enabled = true, + useRootMotion = false }) -- ============================================================================= diff --git a/src/features/editScene/lua/LuaAnimationTreeApi.cpp b/src/features/editScene/lua/LuaAnimationTreeApi.cpp new file mode 100644 index 0000000..f0a23c9 --- /dev/null +++ b/src/features/editScene/lua/LuaAnimationTreeApi.cpp @@ -0,0 +1,217 @@ +#include "LuaAnimationTreeApi.hpp" +#include "../systems/AnimationTreeRegistry.hpp" +#include "../components/AnimationTree.hpp" +#include + +namespace editScene +{ + +static void pushAnimationTreeNode(lua_State *L, + const AnimationTreeNode &node); +static bool readAnimationTreeNode(lua_State *L, int idx, + AnimationTreeNode &node); + +static void pushAnimationTreeNode(lua_State *L, + const AnimationTreeNode &node) +{ + lua_newtable(L); + lua_pushstring(L, node.type.c_str()); + lua_setfield(L, -2, "type"); + if (!node.name.empty()) { + lua_pushstring(L, node.name.c_str()); + lua_setfield(L, -2, "name"); + } + if (!node.animationName.empty()) { + lua_pushstring(L, node.animationName.c_str()); + lua_setfield(L, -2, "animationName"); + } + if (node.speed != 1.0f) { + lua_pushnumber(L, node.speed); + lua_setfield(L, -2, "speed"); + } + if (node.fadeSpeed != 7.5f) { + lua_pushnumber(L, node.fadeSpeed); + lua_setfield(L, -2, "fadeSpeed"); + } + if (!node.endTransitions.empty()) { + lua_newtable(L); + for (const auto &pair : node.endTransitions) { + lua_pushstring(L, pair.second.c_str()); + lua_setfield(L, -2, pair.first.c_str()); + } + lua_setfield(L, -2, "endTransitions"); + } + if (!node.children.empty()) { + lua_newtable(L); + for (size_t i = 0; i < node.children.size(); ++i) { + pushAnimationTreeNode(L, node.children[i]); + lua_rawseti(L, -2, (int)i + 1); + } + lua_setfield(L, -2, "children"); + } +} + +static bool readAnimationTreeNode(lua_State *L, int idx, + AnimationTreeNode &node) +{ + if (!lua_istable(L, idx)) + return false; + + node.type = "animation"; + if (lua_getfield(L, idx, "type"), lua_isstring(L, -1)) + node.type = lua_tostring(L, -1); + lua_pop(L, 1); + + node.name = ""; + if (lua_getfield(L, idx, "name"), lua_isstring(L, -1)) + node.name = lua_tostring(L, -1); + lua_pop(L, 1); + + node.animationName = ""; + if (lua_getfield(L, idx, "animationName"), lua_isstring(L, -1)) + node.animationName = lua_tostring(L, -1); + lua_pop(L, 1); + + node.speed = 1.0f; + if (lua_getfield(L, idx, "speed"), lua_isnumber(L, -1)) + node.speed = (float)lua_tonumber(L, -1); + lua_pop(L, 1); + + node.fadeSpeed = 7.5f; + if (lua_getfield(L, idx, "fadeSpeed"), lua_isnumber(L, -1)) + node.fadeSpeed = (float)lua_tonumber(L, -1); + lua_pop(L, 1); + + node.endTransitions.clear(); + if (lua_getfield(L, idx, "endTransitions"), lua_istable(L, -1)) { + lua_pushnil(L); + while (lua_next(L, -2) != 0) { + if (lua_isstring(L, -2) && lua_isstring(L, -1)) { + node.endTransitions[lua_tostring(L, -2)] = + lua_tostring(L, -1); + } + lua_pop(L, 1); + } + } + lua_pop(L, 1); + + node.children.clear(); + if (lua_getfield(L, idx, "children"), lua_istable(L, -1)) { + int childIdx = 1; + while (true) { + lua_rawgeti(L, -1, childIdx++); + if (lua_isnil(L, -1)) { + lua_pop(L, 1); + break; + } + AnimationTreeNode child; + if (readAnimationTreeNode(L, lua_gettop(L), child)) + node.children.push_back(child); + lua_pop(L, 1); + } + } + lua_pop(L, 1); + + return true; +} + +static int luaAnimationTreeRegister(lua_State *L) +{ + if (lua_gettop(L) < 2 || !lua_isstring(L, 1) || !lua_istable(L, 2)) + return 0; + + Ogre::String name = lua_tostring(L, 1); + AnimationTreeRegistry::AnimationTreeDefinition def; + def.name = name; + + if (lua_getfield(L, 2, "skeletonSource"), lua_isstring(L, -1)) + def.skeletonSource = lua_tostring(L, -1); + lua_pop(L, 1); + + if (lua_getfield(L, 2, "root"), lua_istable(L, -1)) + readAnimationTreeNode(L, lua_gettop(L), def.root); + lua_pop(L, 1); + + AnimationTreeRegistry::getSingleton().registerTree(def); + return 0; +} + +static int luaAnimationTreeFind(lua_State *L) +{ + if (lua_gettop(L) < 1 || !lua_isstring(L, 1)) + return 0; + + Ogre::String name = lua_tostring(L, 1); + const AnimationTreeRegistry::AnimationTreeDefinition *def = + AnimationTreeRegistry::getSingleton().findTree(name); + if (!def) + return 0; + + lua_newtable(L); + lua_pushstring(L, def->name.c_str()); + lua_setfield(L, -2, "name"); + lua_pushstring(L, def->skeletonSource.c_str()); + lua_setfield(L, -2, "skeletonSource"); + pushAnimationTreeNode(L, def->root); + lua_setfield(L, -2, "root"); + return 1; +} + +static int luaAnimationTreeList(lua_State *L) +{ + lua_newtable(L); + std::vector names = + AnimationTreeRegistry::getSingleton().getTreeNames(); + std::sort(names.begin(), names.end()); + for (size_t i = 0; i < names.size(); ++i) { + lua_pushstring(L, names[i].c_str()); + lua_rawseti(L, -2, (int)i + 1); + } + return 1; +} + +static int luaAnimationTreeRemove(lua_State *L) +{ + if (lua_gettop(L) < 1 || !lua_isstring(L, 1)) + return 0; + AnimationTreeRegistry::getSingleton().removeTree(lua_tostring(L, 1)); + return 0; +} + +static int luaAnimationTreeSave(lua_State *L) +{ + (void)L; + bool ok = AnimationTreeRegistry::getSingleton().saveToFile( + AnimationTreeRegistry::defaultPath()); + lua_pushboolean(L, ok ? 1 : 0); + return 1; +} + +static int luaAnimationTreeLoad(lua_State *L) +{ + (void)L; + bool ok = AnimationTreeRegistry::getSingleton().loadFromFile( + AnimationTreeRegistry::defaultPath()); + lua_pushboolean(L, ok ? 1 : 0); + return 1; +} + +void registerLuaAnimationTreeApi(lua_State *L) +{ + lua_newtable(L); + lua_pushcfunction(L, luaAnimationTreeRegister); + lua_setfield(L, -2, "register"); + lua_pushcfunction(L, luaAnimationTreeFind); + lua_setfield(L, -2, "find"); + lua_pushcfunction(L, luaAnimationTreeList); + lua_setfield(L, -2, "list"); + lua_pushcfunction(L, luaAnimationTreeRemove); + lua_setfield(L, -2, "remove"); + lua_pushcfunction(L, luaAnimationTreeSave); + lua_setfield(L, -2, "save"); + lua_pushcfunction(L, luaAnimationTreeLoad); + lua_setfield(L, -2, "load"); + lua_setfield(L, -2, "animation_trees"); +} + +} // namespace editScene diff --git a/src/features/editScene/lua/LuaAnimationTreeApi.hpp b/src/features/editScene/lua/LuaAnimationTreeApi.hpp new file mode 100644 index 0000000..250cede --- /dev/null +++ b/src/features/editScene/lua/LuaAnimationTreeApi.hpp @@ -0,0 +1,14 @@ +#ifndef EDITSCENE_LUA_ANIMATIONTREE_API_HPP +#define EDITSCENE_LUA_ANIMATIONTREE_API_HPP +#pragma once + +#include + +namespace editScene +{ + +void registerLuaAnimationTreeApi(lua_State *L); + +} // namespace editScene + +#endif // EDITSCENE_LUA_ANIMATIONTREE_API_HPP diff --git a/src/features/editScene/lua/LuaComponentApi.cpp b/src/features/editScene/lua/LuaComponentApi.cpp index ad1203b..4acf6bc 100644 --- a/src/features/editScene/lua/LuaComponentApi.cpp +++ b/src/features/editScene/lua/LuaComponentApi.cpp @@ -37,7 +37,6 @@ #include "components/CharacterIdentity.hpp" #include "systems/CharacterRegistry.hpp" #include "components/AnimationTree.hpp" -#include "components/AnimationTreeTemplate.hpp" #include "components/Character.hpp" #include "components/StartupMenu.hpp" #include "components/PlayerController.hpp" @@ -595,31 +594,22 @@ static void registerAllComponents() // --- AnimationTree --- REGISTER_COMPONENT( AnimationTreeComponent, "AnimationTree", + lua_pushstring(L, c.treeName.c_str()); + lua_setfield(L, -2, "treeName"); lua_pushboolean(L, c.enabled ? 1 : 0); lua_setfield(L, -2, "enabled"); lua_pushboolean(L, c.useRootMotion ? 1 : 0); lua_setfield(L, -2, "useRootMotion"); - lua_pushstring(L, c.templateName.c_str()); - lua_setfield(L, -2, "templateName"); - , if (lua_getfield(L, idx, "enabled"), lua_isboolean(L, -1)) - c.enabled = lua_toboolean(L, -1) != 0; + , if (lua_getfield(L, idx, "treeName"), lua_isstring(L, -1)) + c.treeName = lua_tostring(L, -1); + lua_pop(L, 1); + if (lua_getfield(L, idx, "enabled"), lua_isboolean(L, -1)) + c.enabled = lua_toboolean(L, -1) != 0; lua_pop(L, 1); if (lua_getfield(L, idx, "useRootMotion"), lua_isboolean(L, -1)) c.useRootMotion = lua_toboolean(L, -1) != 0; - lua_pop(L, 1); - if (lua_getfield(L, idx, "templateName"), lua_isstring(L, -1)) - c.templateName = lua_tostring(L, -1); lua_pop(L, 1);); - // --- AnimationTreeTemplate --- - REGISTER_COMPONENT(AnimationTreeTemplate, "AnimationTreeTemplate", - lua_pushstring(L, c.name.c_str()); - lua_setfield(L, -2, "name"); - , if (lua_getfield(L, idx, "name"), - lua_isstring(L, -1)) - c.name = lua_tostring(L, -1); - lua_pop(L, 1);); - // --- BehaviorTree --- REGISTER_COMPONENT( BehaviorTreeComponent, "BehaviorTree", diff --git a/src/features/editScene/lua/LuaComponentApi.hpp b/src/features/editScene/lua/LuaComponentApi.hpp index 46f9f06..b2b87ed 100644 --- a/src/features/editScene/lua/LuaComponentApi.hpp +++ b/src/features/editScene/lua/LuaComponentApi.hpp @@ -24,7 +24,7 @@ * Supported component names (case-sensitive): * "EntityName", "Transform", "Renderable", "Light", "Camera", * "RigidBody", "PhysicsCollider", "Character", "CharacterSlots", - * "AnimationTree", "AnimationTreeTemplate", "BehaviorTree", + * "AnimationTree", "BehaviorTree", * "GoapBlackboard", "GoapAction", "GoapGoal", "ActionDatabase", * "ActionDebug", "SmartObject", "Actuator", "EventHandler", * "GoapPlanner", "GoapRunner", "PathFollowing", "NavMesh", diff --git a/src/features/editScene/systems/AnimationTreeRegistry.cpp b/src/features/editScene/systems/AnimationTreeRegistry.cpp new file mode 100644 index 0000000..c3b1f43 --- /dev/null +++ b/src/features/editScene/systems/AnimationTreeRegistry.cpp @@ -0,0 +1,237 @@ +#include "AnimationTreeRegistry.hpp" +#include +#include +#include +#include +#include +#include +#include + +AnimationTreeRegistry *AnimationTreeRegistry::ms_singleton = nullptr; + +AnimationTreeRegistry &AnimationTreeRegistry::getSingleton() +{ + static AnimationTreeRegistry instance; + return instance; +} + +AnimationTreeRegistry *AnimationTreeRegistry::getSingletonPtr() +{ + return &getSingleton(); +} + +AnimationTreeRegistry::AnimationTreeRegistry() + : m_autoSavePath(defaultPath()) +{ + ms_singleton = this; +} + +void AnimationTreeRegistry::initialize() +{ + if (!std::filesystem::exists(m_autoSavePath)) + return; + if (!loadFromFile(m_autoSavePath)) { + Ogre::LogManager::getSingleton().logMessage( + "AnimationTreeRegistry: auto-load failed: " + + m_lastError); + } +} + +bool AnimationTreeRegistry::registerTree(const AnimationTreeDefinition &def) +{ + bool wasNew = m_trees.find(def.name) == m_trees.end(); + m_trees[def.name] = def; + if (wasNew) + Ogre::LogManager::getSingleton().logMessage( + "AnimationTreeRegistry: registered tree '" + def.name + + "'"); + autoSave(); + return wasNew; +} + +bool AnimationTreeRegistry::removeTree(const Ogre::String &name) +{ + if (m_trees.erase(name) == 0) + return false; + autoSave(); + return true; +} + +AnimationTreeRegistry::AnimationTreeDefinition * +AnimationTreeRegistry::findTree(const Ogre::String &name) +{ + auto it = m_trees.find(name); + if (it != m_trees.end()) + return &it->second; + return nullptr; +} + +const AnimationTreeRegistry::AnimationTreeDefinition * +AnimationTreeRegistry::findTree(const Ogre::String &name) const +{ + auto it = m_trees.find(name); + if (it != m_trees.end()) + return &it->second; + return nullptr; +} + +std::vector AnimationTreeRegistry::getTreeNames() const +{ + std::vector names; + names.reserve(m_trees.size()); + for (const auto &pair : m_trees) + names.push_back(pair.first); + return names; +} + +Ogre::String AnimationTreeRegistry::generateUniqueName(const Ogre::String &base) + const +{ + if (m_trees.find(base) == m_trees.end()) + return base; + for (int i = 1; i < 10000; ++i) { + Ogre::String candidate = base + "_" + Ogre::StringConverter::toString(i); + if (m_trees.find(candidate) == m_trees.end()) + return candidate; + } + return base + "_" + Ogre::StringConverter::toString( + (unsigned long long)this); +} + +void AnimationTreeRegistry::markModified(const Ogre::String &name) +{ + auto it = m_trees.find(name); + if (it != m_trees.end()) { + ++it->second.version; + autoSave(); + } +} + +std::vector +AnimationTreeRegistry::getAvailableSkeletonSources() const +{ + std::vector result; + Ogre::ResourceGroupManager &rgm = + Ogre::ResourceGroupManager::getSingleton(); + try { + Ogre::StringVectorPtr names = + rgm.findResourceNames("Characters", "*.mesh"); + if (names) + result.assign(names->begin(), names->end()); + } catch (...) { + } + std::sort(result.begin(), result.end()); + return result; +} + +std::vector +AnimationTreeRegistry::getAnimationsForSource(const Ogre::String &source) const +{ + std::vector result; + if (source.empty()) + return result; + + try { + Ogre::MeshPtr mesh = Ogre::MeshManager::getSingleton().load( + source, "Characters"); + if (!mesh || !mesh->getSkeleton()) + return result; + + Ogre::SkeletonPtr skel = mesh->getSkeleton(); + unsigned short numAnims = skel->getNumAnimations(); + for (unsigned short i = 0; i < numAnims; ++i) { + Ogre::Animation *anim = skel->getAnimation(i); + if (anim) + result.push_back(anim->getName()); + } + } catch (const std::exception &e) { + Ogre::LogManager::getSingleton().logMessage( + "AnimationTreeRegistry: failed to enumerate animations for '" + + source + "': " + e.what()); + } + + std::sort(result.begin(), result.end()); + return result; +} + +nlohmann::json AnimationTreeRegistry::serialize() const +{ + nlohmann::json j; + j["version"] = "1.0"; + j["trees"] = nlohmann::json::array(); + for (const auto &pair : m_trees) { + const AnimationTreeDefinition &def = pair.second; + nlohmann::json treeJson; + treeJson["name"] = def.name; + treeJson["skeletonSource"] = def.skeletonSource; + treeJson["version"] = def.version; + treeJson["root"] = animationTreeNodeToJson(def.root); + j["trees"].push_back(treeJson); + } + return j; +} + +void AnimationTreeRegistry::deserialize(const nlohmann::json &j) +{ + m_trees.clear(); + if (!j.contains("trees") || !j["trees"].is_array()) + return; + for (const auto &treeJson : j["trees"]) { + if (!treeJson.contains("name")) + continue; + AnimationTreeDefinition def; + def.name = treeJson.value("name", ""); + def.skeletonSource = treeJson.value("skeletonSource", ""); + def.version = treeJson.value("version", 1); + if (treeJson.contains("root")) + animationTreeNodeFromJson(def.root, treeJson["root"]); + if (!def.name.empty()) + m_trees[def.name] = def; + } +} + +bool AnimationTreeRegistry::saveToFile(const std::string &filepath) +{ + try { + std::ofstream file(filepath); + if (!file.is_open()) { + m_lastError = "Failed to open file for writing: " + filepath; + return false; + } + file << serialize().dump(4); + file.close(); + return true; + } catch (const std::exception &e) { + m_lastError = std::string("Save error: ") + e.what(); + return false; + } +} + +bool AnimationTreeRegistry::loadFromFile(const std::string &filepath) +{ + try { + if (!std::filesystem::exists(filepath)) { + m_lastError = "File not found: " + filepath; + return false; + } + std::ifstream file(filepath); + if (!file.is_open()) { + m_lastError = "Failed to open file for reading: " + filepath; + return false; + } + nlohmann::json j; + file >> j; + file.close(); + deserialize(j); + return true; + } catch (const std::exception &e) { + m_lastError = std::string("Load error: ") + e.what(); + return false; + } +} + +void AnimationTreeRegistry::autoSave() +{ + if (!m_autoSavePath.empty()) + saveToFile(m_autoSavePath); +} diff --git a/src/features/editScene/systems/AnimationTreeRegistry.hpp b/src/features/editScene/systems/AnimationTreeRegistry.hpp new file mode 100644 index 0000000..34c4d4c --- /dev/null +++ b/src/features/editScene/systems/AnimationTreeRegistry.hpp @@ -0,0 +1,108 @@ +#ifndef EDITSCENE_ANIMATIONTREEREGISTRY_HPP +#define EDITSCENE_ANIMATIONTREEREGISTRY_HPP +#pragma once + +#include "../components/AnimationTree.hpp" +#include +#include +#include +#include +#include + +/** + * Global animation tree registry. + * + * Holds named animation tree definitions. Each tree is referenced by name + * from AnimationTreeComponent. Trees are edited in the Animation Tree + * Registry window (Tools menu) and persisted to animation_tree.json. + */ +class AnimationTreeRegistry { +public: + static AnimationTreeRegistry &getSingleton(); + static AnimationTreeRegistry *getSingletonPtr(); + +private: + static AnimationTreeRegistry *ms_singleton; + +public: + struct AnimationTreeDefinition { + Ogre::String name; + Ogre::String skeletonSource; + AnimationTreeNode root; + uint64_t version = 1; + }; + + AnimationTreeRegistry(); + ~AnimationTreeRegistry() = default; + + AnimationTreeRegistry(const AnimationTreeRegistry &) = delete; + AnimationTreeRegistry &operator=(const AnimationTreeRegistry &) = delete; + + /* ------------------------------------------------------------------ */ + /* Life-cycle */ + /* ------------------------------------------------------------------ */ + void initialize(); + + /* ------------------------------------------------------------------ */ + /* Definitions */ + /* ------------------------------------------------------------------ */ + bool registerTree(const AnimationTreeDefinition &def); + bool removeTree(const Ogre::String &name); + + AnimationTreeDefinition *findTree(const Ogre::String &name); + const AnimationTreeDefinition *findTree(const Ogre::String &name) const; + + const std::unordered_map & + getTrees() const + { + return m_trees; + } + + std::vector getTreeNames() const; + + bool hasTree(const Ogre::String &name) const + { + return m_trees.find(name) != m_trees.end(); + } + + /* Generate a unique name from a base prefix. */ + Ogre::String generateUniqueName(const Ogre::String &base) const; + + /* Bump version and auto-save. */ + void markModified(const Ogre::String &name); + + /* ------------------------------------------------------------------ */ + /* Skeleton source helpers */ + /* ------------------------------------------------------------------ */ + std::vector getAvailableSkeletonSources() const; + std::vector getAnimationsForSource( + const Ogre::String &source) const; + + /* ------------------------------------------------------------------ */ + /* Persistence */ + /* ------------------------------------------------------------------ */ + nlohmann::json serialize() const; + void deserialize(const nlohmann::json &j); + + bool saveToFile(const std::string &filepath); + bool loadFromFile(const std::string &filepath); + const std::string &getLastError() const + { + return m_lastError; + } + + void autoSave(); + + /* Default file path used by initialize()/autoSave(). */ + static const char *defaultPath() + { + return "animation_tree.json"; + } + +private: + std::unordered_map m_trees; + std::string m_autoSavePath; + std::string m_lastError; +}; + +#endif // EDITSCENE_ANIMATIONTREEREGISTRY_HPP diff --git a/src/features/editScene/systems/AnimationTreeSystem.cpp b/src/features/editScene/systems/AnimationTreeSystem.cpp index 807dd17..238663d 100644 --- a/src/features/editScene/systems/AnimationTreeSystem.cpp +++ b/src/features/editScene/systems/AnimationTreeSystem.cpp @@ -1,4 +1,5 @@ #include "AnimationTreeSystem.hpp" +#include "AnimationTreeRegistry.hpp" #include "CharacterSlotSystem.hpp" #include "../components/Transform.hpp" #include "../components/Renderable.hpp" @@ -99,7 +100,8 @@ Ogre::Entity *AnimationTreeSystem::findAnimatedEntity(flecs::entity e) } bool AnimationTreeSystem::setupEntity(flecs::entity e, - AnimationTreeComponent &at) + AnimationTreeComponent &at, + EntityAnimTreeState &state) { Ogre::Entity *ent = findAnimatedEntity(e); std::cout << "AnimationTreeSystem::setupEntity: entity=" << e.id() @@ -111,7 +113,6 @@ bool AnimationTreeSystem::setupEntity(flecs::entity e, return false; } - EntityAnimTreeState &state = m_states[e.id()]; /* Preserve root-motion tracking state across rebuilds so * setupEntity() does not produce a one-frame zero-delta * stutter when the mesh is recreated by CharacterSlotSystem. @@ -239,7 +240,8 @@ bool AnimationTreeSystem::setupEntity(flecs::entity e, } /* Initialize default state machine states */ - initializeTreeStates(at.root, at); + if (state.resolvedRoot) + initializeTreeStates(*state.resolvedRoot, at); std::cout << " setupEntity done: animations=" << state.animations.size() << " rootBone=" @@ -326,35 +328,29 @@ void AnimationTreeSystem::initializeTreeStates(const AnimationTreeNode &node, initializeTreeStates(child, at); } -void AnimationTreeSystem::resolveTemplate(AnimationTreeComponent &at) +const AnimationTreeNode * +AnimationTreeSystem::getResolvedRoot(const AnimationTreeComponent &at) const { - if (at.templateName.empty()) + if (at.treeName.empty()) + return nullptr; + const AnimationTreeRegistry::AnimationTreeDefinition *def = + AnimationTreeRegistry::getSingleton().findTree(at.treeName); + if (!def) + return nullptr; + return &def->root; +} + +void AnimationTreeSystem::resolveTree(AnimationTreeComponent &at, + EntityAnimTreeState &state) +{ + state.resolvedRoot = getResolvedRoot(at); + if (!state.resolvedRoot) return; - AnimationTreeTemplate *templ = nullptr; - AnimationTreeComponent *templAt = nullptr; - - m_world.query().each( - [&](flecs::entity, AnimationTreeTemplate &t, - AnimationTreeComponent &ta) { - if (t.name == at.templateName) { - templ = &t; - templAt = &ta; - } - }); - - if (!templ) { - m_world.query().each( - [&](flecs::entity, AnimationTreeTemplate &t) { - if (t.name == at.templateName) - templ = &t; - }); - } - - if (templ && at.templateVersion != templ->version) { - if (templAt) - at.root = templAt->root; - at.templateVersion = templ->version; + const AnimationTreeRegistry::AnimationTreeDefinition *def = + AnimationTreeRegistry::getSingleton().findTree(at.treeName); + if (def && at.treeVersion != def->version) { + at.treeVersion = def->version; at.dirty = true; } } @@ -374,31 +370,38 @@ void AnimationTreeSystem::update(float deltaTime) if (e.has()) e.get_mut().linearVelocity = Ogre::Vector3::ZERO; - resolveTemplate(at); + + auto stateIt = m_states.find(e.id()); + if (stateIt == m_states.end()) { + EntityAnimTreeState newState; + stateIt = m_states.emplace(e.id(), newState).first; + } + EntityAnimTreeState &state = stateIt->second; + + resolveTree(at, state); + + if (!state.resolvedRoot) { + /* No registry entry for this tree name. */ + return; + } if (at.dirty) { - if (setupEntity(e, at)) + if (setupEntity(e, at, state)) at.dirty = false; } - auto it = m_states.find(e.id()); - if (it == m_states.end()) + if (!state.ogreEntity) return; /* Validate cached entity pointer - * CharacterSlotSystem rebuilds can destroy and * recreate the Ogre::Entity */ Ogre::Entity *currentEnt = findAnimatedEntity(e); - if (currentEnt != it->second.ogreEntity) { - if (!setupEntity(e, at)) { - return; - } - it = m_states.find(e.id()); - if (it == m_states.end()) + if (currentEnt != state.ogreEntity) { + if (!setupEntity(e, at, state)) return; } - EntityAnimTreeState &state = it->second; if (!state.ogreEntity) return; @@ -431,7 +434,7 @@ void AnimationTreeSystem::update(float deltaTime) /* Evaluate tree */ EvalContext ctx; ctx.deltaTime = deltaTime; - evaluateNode(at.root, 1.0f, 1.0f, at, state, ctx); + evaluateNode(*state.resolvedRoot, 1.0f, 1.0f, at, state, ctx); /* Apply animation weights and advance */ Ogre::Vector3 totalRootMotion = Ogre::Vector3::ZERO; @@ -758,8 +761,11 @@ void AnimationTreeSystem::checkEndTransitions(flecs::entity e, const Ogre::String &smName = pair.first; const Ogre::String &stateName = pair.second; + const AnimationTreeNode *root = getResolvedRoot(at); + if (!root) + continue; const AnimationTreeNode *smNode = - findStateMachineNode(at.root, smName); + findStateMachineNode(*root, smName); if (!smNode) continue; @@ -837,8 +843,11 @@ void AnimationTreeSystem::setStateInternal(flecs::entity e, if (reset) { /* Reset the new state's animation */ - const AnimationTreeNode *smNode = - findStateMachineNode(at.root, stateMachineName); + const AnimationTreeNode *root = getResolvedRoot(at); + const AnimationTreeNode *smNode = root ? + findStateMachineNode(*root, + stateMachineName) : + nullptr; if (smNode) { const AnimationTreeNode *stNode = findStateNode(*smNode, stateName); diff --git a/src/features/editScene/systems/AnimationTreeSystem.hpp b/src/features/editScene/systems/AnimationTreeSystem.hpp index 7e867a4..7dbe254 100644 --- a/src/features/editScene/systems/AnimationTreeSystem.hpp +++ b/src/features/editScene/systems/AnimationTreeSystem.hpp @@ -9,7 +9,7 @@ #include #include "../components/AnimationTree.hpp" -#include "../components/AnimationTreeTemplate.hpp" +#include "AnimationTreeRegistry.hpp" /** @@ -89,6 +89,9 @@ private: std::unordered_map fadeStates; /* Track previous currentStates to detect external changes */ std::unordered_map prevStates; + + /* Cached root from the registry. Not owned. */ + const AnimationTreeNode *resolvedRoot = nullptr; }; struct AnimEvalData { @@ -102,7 +105,8 @@ private: std::vector > endChecks; }; - bool setupEntity(flecs::entity e, AnimationTreeComponent &at); + bool setupEntity(flecs::entity e, AnimationTreeComponent &at, + EntityAnimTreeState &state); void teardownEntity(flecs::entity e); void disableAllAnimations(EntityAnimTreeState &state); void initializeTreeStates(const AnimationTreeNode &node, @@ -119,8 +123,11 @@ private: const Ogre::String &stateMachineName, const Ogre::String &stateName, bool reset); - /* Resolve template reference and copy tree if template changed */ - void resolveTemplate(AnimationTreeComponent &at); + /* Resolve tree reference from registry and copy tree if version changed */ + void resolveTree(AnimationTreeComponent &at, EntityAnimTreeState &state); + + const AnimationTreeNode *getResolvedRoot( + const AnimationTreeComponent &at) const; const AnimationTreeNode * findStateMachineNode(const AnimationTreeNode &root, diff --git a/src/features/editScene/systems/BehaviorTreeSystem.cpp b/src/features/editScene/systems/BehaviorTreeSystem.cpp index 2934b66..f5223b6 100644 --- a/src/features/editScene/systems/BehaviorTreeSystem.cpp +++ b/src/features/editScene/systems/BehaviorTreeSystem.cpp @@ -428,8 +428,12 @@ BehaviorTreeSystem::evaluateNode(const BehaviorTreeNode &node, flecs::entity e, * lookup on the same entity. */ if (e.has()) { auto &at = e.get(); + const AnimationTreeRegistry::AnimationTreeDefinition *def = + AnimationTreeRegistry::getSingleton().findTree( + at.treeName); const AnimationTreeNode *smNode = nullptr; - for (const auto &child : at.root.children) { + if (def) { + for (const auto &child : def->root.children) { if (child.type == "stateMachine" && child.name == node.name) { smNode = &child; @@ -442,6 +446,7 @@ BehaviorTreeSystem::evaluateNode(const BehaviorTreeNode &node, flecs::entity e, break; } } + } if (smNode) { for (const auto &st : smNode->children) { if (st.type == "state" && diff --git a/src/features/editScene/systems/EditorUISystem.cpp b/src/features/editScene/systems/EditorUISystem.cpp index 61c2938..3a92e57 100644 --- a/src/features/editScene/systems/EditorUISystem.cpp +++ b/src/features/editScene/systems/EditorUISystem.cpp @@ -30,7 +30,6 @@ #include "../components/CharacterSpawner.hpp" #include "../components/Character.hpp" #include "../components/AnimationTree.hpp" -#include "../components/AnimationTreeTemplate.hpp" #include "../components/StartupMenu.hpp" #include "../components/PlayerController.hpp" #include "../components/CellGrid.hpp" @@ -382,6 +381,12 @@ void EditorUISystem::update(float deltaTime) ItemRegistry::getSingleton().drawEditor(&m_showItemRegistry); } + // Render Animation Tree Registry window + if (m_showAnimationTreeRegistry) { + m_animationTreeRegistryEditor.render( + &m_showAnimationTreeRegistry); + } + // Render Inventory Dialog Config window if (m_showInventoryConfig) { renderInventoryConfigWindow(); @@ -490,6 +495,9 @@ void EditorUISystem::renderHierarchyWindow() if (ImGui::MenuItem("Item Registry")) { m_showItemRegistry = true; } + if (ImGui::MenuItem("Animation Tree Registry")) { + m_showAnimationTreeRegistry = true; + } ImGui::Separator(); if (ImGui::MenuItem( "Inventory Dialog Config")) { @@ -1098,14 +1106,6 @@ void EditorUISystem::renderComponentList(flecs::entity entity) componentCount++; } - // Render AnimationTreeTemplate if present - if (entity.has()) { - auto &templ = entity.get_mut(); - m_componentRegistry.render(entity, - templ); - componentCount++; - } - // Render StartupMenu if present if (entity.has()) { auto &sm = entity.get_mut(); diff --git a/src/features/editScene/systems/EditorUISystem.hpp b/src/features/editScene/systems/EditorUISystem.hpp index cee381a..f213691 100644 --- a/src/features/editScene/systems/EditorUISystem.hpp +++ b/src/features/editScene/systems/EditorUISystem.hpp @@ -10,6 +10,7 @@ #include "../ui/ComponentRegistry.hpp" #include "../ui/ActionDatabaseSingletonEditor.hpp" #include "../ui/CharacterClassDatabaseEditor.hpp" +#include "../ui/AnimationTreeRegistryEditor.hpp" #include "../components/EntityName.hpp" #include "../gizmo/Gizmo.hpp" #include "../gizmo/Cursor3D.hpp" @@ -347,6 +348,10 @@ private: // Item registry bool m_showItemRegistry = false; + // Animation tree registry + bool m_showAnimationTreeRegistry = false; + AnimationTreeRegistryEditor m_animationTreeRegistryEditor; + // Queries flecs::query m_nameQuery; diff --git a/src/features/editScene/systems/SceneSerializer.cpp b/src/features/editScene/systems/SceneSerializer.cpp index 3c6a60a..60316fe 100644 --- a/src/features/editScene/systems/SceneSerializer.cpp +++ b/src/features/editScene/systems/SceneSerializer.cpp @@ -24,7 +24,8 @@ #include "../components/CharacterIdentity.hpp" #include "CharacterRegistry.hpp" #include "../components/AnimationTree.hpp" -#include "../components/AnimationTreeTemplate.hpp" +#include "AnimationTreeRegistry.hpp" +#include "AnimationTreeRegistry.hpp" #include "../components/StartupMenu.hpp" #include "../components/PlayerController.hpp" #include "../components/CellGrid.hpp" @@ -263,11 +264,6 @@ nlohmann::json SceneSerializer::serializeEntity(flecs::entity entity) json["animationTree"] = serializeAnimationTree(entity); } - if (entity.has()) { - json["animationTreeTemplate"] = - serializeAnimationTreeTemplate(entity); - } - if (entity.has()) { json["startupMenu"] = serializeStartupMenu(entity); } @@ -2294,11 +2290,6 @@ void SceneSerializer::deserializeCharacterSpawner(flecs::entity entity, entity.set(spawner); } -/* Forward declarations for AnimationTreeNode serialization */ -static nlohmann::json serializeAnimationTreeNode(const AnimationTreeNode &node); -static void deserializeAnimationTreeNode(AnimationTreeNode &node, - const nlohmann::json &json); - nlohmann::json SceneSerializer::serializeCharacterSlots(flecs::entity entity) { /* CharacterSlotsComponent is deprecated and no longer saved. */ @@ -2379,65 +2370,36 @@ void SceneSerializer::deserializeCharacterSlots(flecs::entity entity, CharacterRegistry::getSingleton().markCharacterDirty(registryId); } -static nlohmann::json serializeAnimationTreeNode(const AnimationTreeNode &node) +/* Migrate a legacy inline animation tree into the registry and return + * the registry name to use. */ +static Ogre::String migrateInlineAnimationTree( + const nlohmann::json &json, const Ogre::String &preferredName) { - nlohmann::json json; - json["type"] = node.type; - if (!node.name.empty()) - json["name"] = node.name; - if (!node.animationName.empty()) - json["animationName"] = node.animationName; - if (node.speed != 1.0f) - json["speed"] = node.speed; - if (node.fadeSpeed != 7.5f) - json["fadeSpeed"] = node.fadeSpeed; - if (!node.endTransitions.empty()) { - json["endTransitions"] = nlohmann::json::object(); - for (const auto &pair : node.endTransitions) - json["endTransitions"][pair.first] = pair.second; - } - if (!node.children.empty()) { - json["children"] = nlohmann::json::array(); - for (const auto &child : node.children) - json["children"].push_back( - serializeAnimationTreeNode(child)); - } - return json; -} + AnimationTreeNode root; + if (json.contains("root")) + animationTreeNodeFromJson(root, json["root"]); -static void deserializeAnimationTreeNode(AnimationTreeNode &node, - const nlohmann::json &json) -{ - node.type = json.value("type", "animation"); - node.name = json.value("name", ""); - node.animationName = json.value("animationName", ""); - node.speed = json.value("speed", 1.0f); - node.fadeSpeed = json.value("fadeSpeed", 7.5f); - node.endTransitions.clear(); - if (json.contains("endTransitions") && - json["endTransitions"].is_object()) { - for (auto &[key, val] : json["endTransitions"].items()) - node.endTransitions[key] = val.get(); - } - node.children.clear(); - if (json.contains("children") && json["children"].is_array()) { - for (const auto &childJson : json["children"]) { - AnimationTreeNode child; - deserializeAnimationTreeNode(child, childJson); - node.children.push_back(child); - } - } + AnimationTreeRegistry ® = AnimationTreeRegistry::getSingleton(); + Ogre::String name = preferredName; + if (name.empty()) + name = reg.generateUniqueName("migrated"); + else if (reg.hasTree(name)) + name = reg.generateUniqueName(name); + + AnimationTreeRegistry::AnimationTreeDefinition def; + def.name = name; + def.root = root; + reg.registerTree(def); + return name; } nlohmann::json SceneSerializer::serializeAnimationTree(flecs::entity entity) { auto &at = entity.get(); nlohmann::json json; + json["treeName"] = at.treeName; json["enabled"] = at.enabled; json["useRootMotion"] = at.useRootMotion; - if (!at.templateName.empty()) - json["templateName"] = at.templateName; - json["root"] = serializeAnimationTreeNode(at.root); return json; } @@ -2447,30 +2409,70 @@ void SceneSerializer::deserializeAnimationTree(flecs::entity entity, AnimationTreeComponent at; at.enabled = json.value("enabled", true); at.useRootMotion = json.value("useRootMotion", false); - at.templateName = json.value("templateName", ""); - if (json.contains("root")) - deserializeAnimationTreeNode(at.root, json["root"]); at.dirty = true; + + if (json.contains("treeName")) { + /* Current format. */ + at.treeName = json.value("treeName", ""); + } else if (json.contains("root")) { + /* Legacy inline tree: register in registry. */ + Ogre::String preferred = json.value("templateName", ""); + at.treeName = migrateInlineAnimationTree(json, preferred); + } else if (json.contains("templateName")) { + /* Legacy template reference: ensure registry entry exists. */ + at.treeName = json.value("templateName", ""); + AnimationTreeRegistry ® = + AnimationTreeRegistry::getSingleton(); + if (!at.treeName.empty() && !reg.hasTree(at.treeName)) { + AnimationTreeRegistry::AnimationTreeDefinition def; + def.name = at.treeName; + reg.registerTree(def); + } + } + entity.set(at); } nlohmann::json SceneSerializer::serializeAnimationTreeTemplate(flecs::entity entity) { - auto &templ = entity.get(); - nlohmann::json json; - json["name"] = templ.name; - json["version"] = templ.version; - return json; + /* AnimationTreeTemplate is no longer saved. */ + (void)entity; + return nlohmann::json::object(); } void SceneSerializer::deserializeAnimationTreeTemplate( flecs::entity entity, const nlohmann::json &json) { - AnimationTreeTemplate templ; - templ.name = json.value("name", ""); - templ.version = json.value("version", 1); - entity.set(templ); + /* AnimationTreeTemplate has been migrated into AnimationTreeRegistry. + * If the entity already has an AnimationTreeComponent, update its + * treeName to the template name and copy the tree data into the + * registry. Otherwise just ensure a registry placeholder exists. */ + Ogre::String templName = json.value("name", ""); + if (templName.empty()) + return; + + AnimationTreeRegistry ® = AnimationTreeRegistry::getSingleton(); + + if (entity.has()) { + auto &at = entity.get_mut(); + const AnimationTreeRegistry::AnimationTreeDefinition *def = + reg.findTree(at.treeName); + if (def) { + AnimationTreeRegistry::AnimationTreeDefinition renamed = + *def; + renamed.name = templName; + reg.removeTree(at.treeName); + reg.registerTree(renamed); + } + at.treeName = templName; + } else { + if (!reg.hasTree(templName)) { + AnimationTreeRegistry::AnimationTreeDefinition def; + def.name = templName; + reg.registerTree(def); + } + } } // ============================================================================ diff --git a/src/features/editScene/tests/component_lua_test.cpp b/src/features/editScene/tests/component_lua_test.cpp index 0122930..3be7e01 100644 --- a/src/features/editScene/tests/component_lua_test.cpp +++ b/src/features/editScene/tests/component_lua_test.cpp @@ -133,6 +133,7 @@ private: namespace editScene { void registerLuaComponentApi(lua_State *L); +void registerLuaAnimationTreeApi(lua_State *L); void registerLuaEntityApi(lua_State *L); void registerLuaDialogueApi(lua_State *L); } @@ -1420,12 +1421,14 @@ static int testAnimationTreeComponent(lua_State *L) "local id = ecs.create_entity();" "ecs.set_component(id, 'AnimationTree', {" " treeName = 'humanoid'," - " enabled = true" + " enabled = true," + " useRootMotion = false" "});" "local a = ecs.get_component(id, 'AnimationTree');" "assert(a ~= nil, 'AnimationTree should exist');" "assert(a.treeName == 'humanoid', 'wrong treeName');" - "assert(a.enabled == true, 'wrong enabled')"); + "assert(a.enabled == true, 'wrong enabled');" + "assert(a.useRootMotion == false, 'wrong useRootMotion')"); if (!ok) FAIL("AnimationTree component assertion failed"); @@ -1434,26 +1437,47 @@ static int testAnimationTreeComponent(lua_State *L) } // --------------------------------------------------------------------------- -// Test 48: AnimationTreeTemplate component +// Test 48: Animation Tree Registry Lua API // --------------------------------------------------------------------------- -static int testAnimationTreeTemplateComponent(lua_State *L) +static int testAnimationTreeRegistryApi(lua_State *L) { - TEST("AnimationTreeTemplate component"); + TEST("Animation Tree Registry API"); - bool ok = runLua( - L, - "local id = ecs.create_entity();" - "ecs.set_component(id, 'AnimationTreeTemplate', {" - " templateName = 'humanoid_base'," - " blendTime = 0.2" - "});" - "local a = ecs.get_component(id, 'AnimationTreeTemplate');" - "assert(a ~= nil, 'AnimationTreeTemplate should exist');" - "assert(a.templateName == 'humanoid_base', 'wrong templateName');" - "assert(a.blendTime == 0.2, 'wrong blendTime')"); + bool ok = runLua(L, + "ecs.animation_trees.register('test_humanoid', {" + " skeletonSource = 'male_Face.mesh'," + " root = {" + " type = 'output'," + " children = {" + " { type = 'stateMachine', name = 'main', fadeSpeed = 7.5," + " children = {" + " { type = 'state', name = 'idle'," + " children = {" + " { type = 'animation', animationName = 'idle' }" + " }" + " }" + " }" + " }" + " }" + " }" + "}); " + "local trees = ecs.animation_trees.list(); " + "assert(#trees >= 1, 'list should contain at least one tree'); " + "local found = false; " + "for _, name in ipairs(trees) do " + " if name == 'test_humanoid' then found = true end; " + "end; " + "assert(found, 'test_humanoid not found in list'); " + "local def = ecs.animation_trees.find('test_humanoid'); " + "assert(def ~= nil, 'find should return definition'); " + "assert(def.name == 'test_humanoid', 'wrong def name'); " + "assert(def.skeletonSource == 'male_Face.mesh', 'wrong skeletonSource'); " + "assert(def.root.type == 'output', 'wrong root type'); " + "ecs.animation_trees.remove('test_humanoid'); " + "assert(ecs.animation_trees.find('test_humanoid') == nil, 'remove failed')"); if (!ok) - FAIL("AnimationTreeTemplate component assertion failed"); + FAIL("Animation Tree Registry API assertion failed"); PASS(); return 0; @@ -1830,6 +1854,7 @@ int main() // Register the entity and component APIs editScene::registerLuaEntityApi(L); editScene::registerLuaComponentApi(L); + editScene::registerLuaAnimationTreeApi(L); editScene::registerLuaDialogueApi(L); // Run tests @@ -1881,7 +1906,7 @@ int main() failures += testTriangleBufferComponent(L); failures += testCharacterSpawnerComponent(L); failures += testAnimationTreeComponent(L); - failures += testAnimationTreeTemplateComponent(L); + failures += testAnimationTreeRegistryApi(L); failures += testBehaviorTreeComponent(L); failures += testGoapActionComponent(L); failures += testGoapGoalComponent(L); diff --git a/src/features/editScene/tests/lua_test_stubs.cpp b/src/features/editScene/tests/lua_test_stubs.cpp index 0afdbeb..7b36afa 100644 --- a/src/features/editScene/tests/lua_test_stubs.cpp +++ b/src/features/editScene/tests/lua_test_stubs.cpp @@ -705,6 +705,106 @@ void registerLuaItemApi(lua_State *L) lua_setglobal(L, "ecs"); } +// --------------------------------------------------------------------------- +// Stub: LuaAnimationTreeApi +// --------------------------------------------------------------------------- + +struct StubAnimTreeDef { + std::string name; + std::string skeletonSource; + std::string rootJson; +}; + +static std::unordered_map s_stubAnimTrees; + +static void pushStubAnimTreeNode(lua_State *L, const std::string &json) +{ + /* Simplified stub: return a minimal root table. */ + lua_newtable(L); + lua_pushstring(L, "output"); + lua_setfield(L, -2, "type"); +} + +void registerLuaAnimationTreeApi(lua_State *L) +{ + lua_getglobal(L, "ecs"); + if (lua_isnil(L, -1)) { + lua_pop(L, 1); + lua_newtable(L); + } + + lua_newtable(L); + + lua_pushcfunction(L, [](lua_State *L) -> int { + if (lua_gettop(L) < 2 || !lua_isstring(L, 1) || + !lua_istable(L, 2)) + return 0; + std::string name = lua_tostring(L, 1); + StubAnimTreeDef def; + def.name = name; + if (lua_getfield(L, 2, "skeletonSource"), lua_isstring(L, -1)) + def.skeletonSource = lua_tostring(L, -1); + lua_pop(L, 1); + s_stubAnimTrees[name] = def; + return 0; + }); + lua_setfield(L, -2, "register"); + + lua_pushcfunction(L, [](lua_State *L) -> int { + if (lua_gettop(L) < 1 || !lua_isstring(L, 1)) + return 0; + auto it = s_stubAnimTrees.find(lua_tostring(L, 1)); + if (it == s_stubAnimTrees.end()) + return 0; + lua_newtable(L); + lua_pushstring(L, it->second.name.c_str()); + lua_setfield(L, -2, "name"); + lua_pushstring(L, it->second.skeletonSource.c_str()); + lua_setfield(L, -2, "skeletonSource"); + pushStubAnimTreeNode(L, it->second.rootJson); + lua_setfield(L, -2, "root"); + return 1; + }); + lua_setfield(L, -2, "find"); + + lua_pushcfunction(L, [](lua_State *L) -> int { + lua_newtable(L); + int idx = 1; + for (const auto &pair : s_stubAnimTrees) { + lua_pushstring(L, pair.first.c_str()); + lua_rawseti(L, -2, idx++); + } + return 1; + }); + lua_setfield(L, -2, "list"); + + lua_pushcfunction(L, [](lua_State *L) -> int { + if (lua_gettop(L) < 1 || !lua_isstring(L, 1)) + return 0; + s_stubAnimTrees.erase(lua_tostring(L, 1)); + return 0; + }); + lua_setfield(L, -2, "remove"); + + lua_pushcfunction(L, [](lua_State *L) -> int { + (void)L; + lua_pushboolean(L, 1); + return 1; + }); + lua_setfield(L, -2, "save"); + + lua_pushcfunction(L, [](lua_State *L) -> int { + (void)L; + lua_pushboolean(L, 1); + return 1; + }); + lua_setfield(L, -2, "load"); + + lua_setfield(L, -2, "animation_trees"); + + lua_setglobal(L, "ecs"); +} + } // namespace editScene // --------------------------------------------------------------------------- diff --git a/src/features/editScene/ui/AnimationTreeEditor.cpp b/src/features/editScene/ui/AnimationTreeEditor.cpp index c1da9a2..7accf23 100644 --- a/src/features/editScene/ui/AnimationTreeEditor.cpp +++ b/src/features/editScene/ui/AnimationTreeEditor.cpp @@ -1,6 +1,6 @@ #include "AnimationTreeEditor.hpp" #include "../systems/AnimationTreeSystem.hpp" -#include "../components/AnimationTreeTemplate.hpp" +#include "../systems/AnimationTreeRegistry.hpp" #include AnimationTreeEditor::AnimationTreeEditor(Ogre::SceneManager *sceneMgr) @@ -8,433 +8,11 @@ AnimationTreeEditor::AnimationTreeEditor(Ogre::SceneManager *sceneMgr) { } -std::vector AnimationTreeEditor::getAvailableAnimations( - flecs::entity entity) +static void renderStatePreview(AnimationTreeComponent &at, + const AnimationTreeNode &root) { - std::vector result; - Ogre::Entity *ent = - AnimationTreeSystem::findAnimatedEntity(entity); - if (ent && ent->hasSkeleton()) { - Ogre::AnimationStateSet *states = - ent->getAllAnimationStates(); - if (states) { - for (const auto &pair : states->getAnimationStates()) - result.push_back(pair.first); - } - } - return result; -} - -static ImU32 nodeTypeColorU32(const Ogre::String &type) -{ - if (type == "output") - return IM_COL32(0xFF, 0x88, 0x88, 0xFF); - if (type == "stateMachine") - return IM_COL32(0x88, 0xBB, 0xFF, 0xFF); - if (type == "state") - return IM_COL32(0x88, 0xFF, 0x88, 0xFF); - if (type == "speed") - return IM_COL32(0xFF, 0xEE, 0x88, 0xFF); - return IM_COL32(0xFF, 0xFF, 0xFF, 0xFF); -} - -static void makeLabel(const AnimationTreeNode &node, char *buf, - size_t bufSize) -{ - if (node.type == "animation") { - snprintf(buf, bufSize, "[%s] %s", node.type.c_str(), - node.animationName.empty() ? - "(none)" : - node.animationName.c_str()); - } else if (node.type == "speed") { - snprintf(buf, bufSize, "[%s] %.2fx %s", node.type.c_str(), - node.speed, - node.name.empty() ? "" : node.name.c_str()); - } else if (node.type == "stateMachine") { - snprintf(buf, bufSize, "[%s] %s (fade %.1f)", - node.type.c_str(), node.name.c_str(), - node.fadeSpeed); - } else { - snprintf(buf, bufSize, "[%s] %s", node.type.c_str(), - node.name.empty() ? "" : node.name.c_str()); - } -} - -static int countChildrenOfType(const AnimationTreeNode &node, - const Ogre::String &type) -{ - int n = 0; - for (const auto &c : node.children) - if (c.type == type) - n++; - return n; -} - -static size_t findChildIndex(const AnimationTreeNode &parent, - const AnimationTreeNode *child) -{ - for (size_t i = 0; i < parent.children.size(); i++) - if (&parent.children[i] == child) - return i; - return (size_t)-1; -} - -void AnimationTreeEditor::queueAddChild(AnimationTreeNode *parent, - const Ogre::String &type) -{ - TreeEditOp op; - op.type = TreeEditOp::AddChild; - op.targetNode = parent; - op.childType = type; - m_editOps.push_back(op); -} - -void AnimationTreeEditor::queueRemoveNode(AnimationTreeNode *parent, - size_t childIndex) -{ - TreeEditOp op; - op.type = TreeEditOp::RemoveNode; - op.targetNode = parent; - op.childIndex = childIndex; - m_editOps.push_back(op); -} - -void AnimationTreeEditor::queueMoveNode(AnimationTreeNode *parent, - size_t childIndex, - int delta) -{ - TreeEditOp op; - op.type = TreeEditOp::MoveNode; - op.targetNode = parent; - op.childIndex = childIndex; - op.moveDelta = delta; - m_editOps.push_back(op); -} - -void AnimationTreeEditor::applyEditOps(AnimationTreeComponent &at) -{ - bool changed = false; - for (auto &op : m_editOps) { - if (!op.targetNode) - continue; - auto &vec = op.targetNode->children; - switch (op.type) { - case TreeEditOp::AddChild: { - AnimationTreeNode child; - child.type = op.childType; - if (op.childType == "stateMachine") { - child.name = "sm" + - std::to_string(countChildrenOfType( - *op.targetNode, - "stateMachine")); - child.fadeSpeed = 7.5f; - } else if (op.childType == "state") { - child.name = "state" + - std::to_string(countChildrenOfType( - *op.targetNode, "state")); - } else if (op.childType == "animation") { - child.animationName = ""; - } else if (op.childType == "speed") { - child.speed = 1.0f; - } - vec.push_back(child); - changed = true; - break; - } - case TreeEditOp::RemoveNode: { - if (op.childIndex < vec.size()) { - vec.erase(vec.begin() + op.childIndex); - changed = true; - } - break; - } - case TreeEditOp::MoveNode: { - if (op.childIndex < vec.size()) { - int newIdx = (int)op.childIndex + op.moveDelta; - if (newIdx >= 0 && - newIdx < (int)vec.size()) { - std::swap(vec[op.childIndex], - vec[newIdx]); - changed = true; - } - } - break; - } - } - } - m_editOps.clear(); - if (changed) - at.dirty = true; -} - -static bool canHaveChildren(const Ogre::String &type) -{ - return type == "output" || type == "stateMachine" || - type == "state" || type == "speed"; -} - -void AnimationTreeEditor::renderTree(AnimationTreeNode &node, - AnimationTreeNode *parent, - AnimationTreeNode *&selectedNode, - AnimationTreeNode *&selectedParent, - int &id) -{ - int myId = id++; - bool isLeaf = node.children.empty(); - ImGuiTreeNodeFlags flags = - ImGuiTreeNodeFlags_OpenOnArrow | - ImGuiTreeNodeFlags_OpenOnDoubleClick; - if (isLeaf) - flags |= ImGuiTreeNodeFlags_Leaf | - ImGuiTreeNodeFlags_NoTreePushOnOpen; - if (selectedNode == &node) - flags |= ImGuiTreeNodeFlags_Selected; - - char label[256]; - makeLabel(node, label, sizeof(label)); - - /* Color the whole tree node label */ - ImGui::PushStyleColor(ImGuiCol_Text, - ImGui::ColorConvertU32ToFloat4( - nodeTypeColorU32(node.type))); - bool open = ImGui::TreeNodeEx( - ("##animnode" + std::to_string(myId)).c_str(), flags, - "%s", label); - ImGui::PopStyleColor(); - - if (ImGui::IsItemClicked()) { - selectedNode = &node; - selectedParent = parent; - } - - /* Right-click context menu — must be attached to TreeNodeEx - * (the last item), before we add buttons on the same line */ - if (ImGui::BeginPopupContextItem()) { - if (canHaveChildren(node.type)) { - if (node.type == "output" || - node.type == "stateMachine" || - node.type == "state") { - if (ImGui::MenuItem("Add State Machine")) - queueAddChild(&node, "stateMachine"); - } - if (node.type == "stateMachine" || - node.type == "output" || - node.type == "state") { - if (ImGui::MenuItem("Add State")) - queueAddChild(&node, "state"); - } - if (node.type == "output" || - node.type == "state" || - node.type == "speed") { - if (ImGui::MenuItem("Add Animation")) - queueAddChild(&node, "animation"); - } - if (node.type == "output" || - node.type == "state") { - if (ImGui::MenuItem("Add Speed")) - queueAddChild(&node, "speed"); - } - } - if (parent) { - size_t idx = findChildIndex(*parent, &node); - if (ImGui::MenuItem("Remove")) { - queueRemoveNode(parent, idx); - if (selectedNode == &node) { - selectedNode = nullptr; - selectedParent = nullptr; - } - } - if (ImGui::MenuItem("Move Up")) - queueMoveNode(parent, idx, -1); - if (ImGui::MenuItem("Move Down")) - queueMoveNode(parent, idx, +1); - } - ImGui::EndPopup(); - } - - /* Inline buttons on the same line as the tree node label */ - ImGui::PushID(myId); - if (canHaveChildren(node.type)) { - ImGui::SameLine(); - if (ImGui::SmallButton("+")) { - ImGui::OpenPopup("AddChild"); - } - if (ImGui::BeginPopup("AddChild")) { - if (node.type == "output" || - node.type == "stateMachine" || - node.type == "state") { - if (ImGui::MenuItem("State Machine")) - queueAddChild(&node, "stateMachine"); - } - if (node.type == "stateMachine" || - node.type == "output" || - node.type == "state") { - if (ImGui::MenuItem("State")) - queueAddChild(&node, "state"); - } - if (node.type == "output" || - node.type == "state" || - node.type == "speed") { - if (ImGui::MenuItem("Animation")) - queueAddChild(&node, "animation"); - } - if (node.type == "output" || - node.type == "state") { - if (ImGui::MenuItem("Speed")) - queueAddChild(&node, "speed"); - } - ImGui::EndPopup(); - } - } - if (parent) { - size_t myIndex = findChildIndex(*parent, &node); - - ImGui::SameLine(); - if (ImGui::SmallButton("-")) - queueRemoveNode(parent, myIndex); - - if (myIndex > 0) { - ImGui::SameLine(); - if (ImGui::SmallButton("^")) - queueMoveNode(parent, myIndex, -1); - } - if (myIndex + 1 < parent->children.size()) { - ImGui::SameLine(); - if (ImGui::SmallButton("v")) - queueMoveNode(parent, myIndex, +1); - } - } - ImGui::PopID(); - - /* Render children and TreePop (only when node is open) */ - if (!isLeaf && open) { - for (auto &child : node.children) - renderTree(child, &node, selectedNode, - selectedParent, id); - ImGui::TreePop(); - } -} - -void AnimationTreeEditor::renderProperties( - AnimationTreeNode *node, AnimationTreeComponent &at, - flecs::entity entity) -{ - if (!node) - return; - - ImGui::Separator(); - ImGui::Text("Node Properties"); - - bool modified = false; - - /* Type selector */ - const char *types[] = {"output", "stateMachine", "state", - "speed", "animation"}; - int typeIdx = 0; - for (int i = 0; i < IM_ARRAYSIZE(types); i++) { - if (node->type == types[i]) { - typeIdx = i; - break; - } - } - if (ImGui::Combo("Type", &typeIdx, types, - IM_ARRAYSIZE(types))) { - node->type = types[typeIdx]; - modified = true; - } - - /* Name */ - if (node->type == "stateMachine" || node->type == "state") { - char buf[256]; - strncpy(buf, node->name.c_str(), sizeof(buf) - 1); - buf[sizeof(buf) - 1] = '\0'; - if (ImGui::InputText("Name", buf, sizeof(buf))) { - node->name = buf; - modified = true; - } - } - - /* Animation name */ - if (node->type == "animation") { - std::vector anims = - getAvailableAnimations(entity); - Ogre::String current = node->animationName; - Ogre::String preview = current.empty() ? "(none)" : - current; - if (ImGui::BeginCombo("Animation", preview.c_str())) { - bool noneSel = current.empty(); - if (ImGui::Selectable("(none)", noneSel)) { - node->animationName = ""; - modified = true; - } - for (const auto &name : anims) { - bool sel = (current == name); - if (ImGui::Selectable(name.c_str(), sel)) { - node->animationName = name; - modified = true; - } - } - ImGui::EndCombo(); - } - } - - /* Speed */ - if (node->type == "output" || node->type == "speed") { - if (ImGui::SliderFloat("Speed", &node->speed, -2.0f, - 10.0f, "%.2f")) - modified = true; - } - - /* Fade speed */ - if (node->type == "stateMachine") { - if (ImGui::SliderFloat("Fade Speed", &node->fadeSpeed, - 0.1f, 30.0f, "%.1f")) - modified = true; - } - - /* End transitions (for state machines) */ - if (node->type == "stateMachine" && !node->children.empty()) { - ImGui::Separator(); - ImGui::Text("End Transitions"); - ImGui::TextDisabled( - "Auto-switch state when animation ends"); - - for (const auto &child : node->children) { - if (child.type != "state") - continue; - char buf[256]; - auto it = node->endTransitions.find(child.name); - if (it != node->endTransitions.end()) - strncpy(buf, it->second.c_str(), - sizeof(buf) - 1); - else - buf[0] = '\0'; - buf[sizeof(buf) - 1] = '\0'; - - Ogre::String lbl = "On '" + child.name + - "' end →"; - if (ImGui::InputText(lbl.c_str(), buf, - sizeof(buf))) { - if (buf[0] == '\0') - node->endTransitions.erase( - child.name); - else - node->endTransitions[child.name] = - buf; - modified = true; - } - } - } - - if (modified) - at.dirty = true; -} - -void AnimationTreeEditor::renderStatePreview( - AnimationTreeComponent &at) -{ - std::vector stateMachines; - at.root.collectStateMachines(stateMachines); + std::vector stateMachines; + root.collectStateMachines(stateMachines); if (stateMachines.empty()) return; @@ -473,8 +51,8 @@ void AnimationTreeEditor::renderStatePreview( } } -bool AnimationTreeEditor::renderComponent( - flecs::entity entity, AnimationTreeComponent &at) +bool AnimationTreeEditor::renderComponent(flecs::entity entity, + AnimationTreeComponent &at) { ImGui::PushID("AnimTree"); bool modified = false; @@ -483,33 +61,35 @@ bool AnimationTreeEditor::renderComponent( ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::Indent(); - /* Template reference */ - char templBuf[256]; - snprintf(templBuf, sizeof(templBuf), "%s", - at.templateName.c_str()); - if (ImGui::InputText("Template Name", templBuf, - sizeof(templBuf))) { - at.templateName = templBuf; - modified = true; - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip( - "Leave empty for local tree. Set to template name to copy from a template entity."); + AnimationTreeRegistry ® = + AnimationTreeRegistry::getSingleton(); + std::vector names = reg.getTreeNames(); + + Ogre::String current = at.treeName; + Ogre::String preview = current.empty() ? "(none)" : current; + if (ImGui::BeginCombo("Tree", preview.c_str())) { + bool noneSel = current.empty(); + if (ImGui::Selectable("(none)", noneSel)) { + at.treeName = ""; + modified = true; + at.dirty = true; + } + for (const auto &name : names) { + bool sel = (current == name); + if (ImGui::Selectable(name.c_str(), sel)) { + at.treeName = name; + modified = true; + at.dirty = true; + } + } + ImGui::EndCombo(); } - bool isTemplateEntity = entity.has(); - if (isTemplateEntity) { - auto &templ = entity.get_mut(); - char nameBuf[256]; - snprintf(nameBuf, sizeof(nameBuf), "%s", - templ.name.c_str()); - if (ImGui::InputText("Template Publish Name", nameBuf, - sizeof(nameBuf))) { - templ.name = nameBuf; - modified = true; - } - ImGui::TextDisabled("Template version: %llu", - (unsigned long long)templ.version); + if (!at.treeName.empty() && + ImGui::Button("Edit in Registry")) { + /* TODO: signal EditorUISystem to open the registry + * editor on this tree. For now the user can open + * Tools -> Animation Tree Registry manually. */ } ImGui::Separator(); @@ -517,45 +97,20 @@ bool AnimationTreeEditor::renderComponent( /* Global toggles */ if (ImGui::Checkbox("Enabled", &at.enabled)) modified = true; - if (ImGui::Checkbox("Use Root Motion", - &at.useRootMotion)) { + if (ImGui::Checkbox("Use Root Motion", &at.useRootMotion)) { modified = true; at.dirty = true; } - ImGui::Separator(); - - /* Tree view */ - static AnimationTreeNode *selectedNode = nullptr; - static AnimationTreeNode *selectedParent = nullptr; - int id = 0; - renderTree(at.root, nullptr, selectedNode, - selectedParent, id); - - /* Apply all deferred edits. Vector reallocations may - * invalidate pointers, so clear selection afterwards. */ - bool hadEdits = !m_editOps.empty(); - applyEditOps(at); - if (hadEdits) { - selectedNode = nullptr; - selectedParent = nullptr; - } - - /* Node properties */ - renderProperties(selectedNode, at, entity); - - /* State preview */ - renderStatePreview(at); + /* State preview from registry tree */ + const AnimationTreeRegistry::AnimationTreeDefinition *def = + reg.findTree(at.treeName); + if (def) + renderStatePreview(at, def->root); ImGui::Unindent(); } - /* Auto-publish template changes */ - if (modified && entity.has()) { - auto &templ = entity.get_mut(); - templ.version++; - } - ImGui::PopID(); return modified; } diff --git a/src/features/editScene/ui/AnimationTreeEditor.hpp b/src/features/editScene/ui/AnimationTreeEditor.hpp index 5b2a340..d844559 100644 --- a/src/features/editScene/ui/AnimationTreeEditor.hpp +++ b/src/features/editScene/ui/AnimationTreeEditor.hpp @@ -4,13 +4,14 @@ #include "ComponentEditor.hpp" #include "../components/AnimationTree.hpp" #include -#include /** * Editor for AnimationTreeComponent. * - * Provides a recursive tree view for editing the animation hierarchy - * and state-machine current-state selectors for preview. + * In the new registry-based design this editor is only a picker: it + * selects the tree name from AnimationTreeRegistry and exposes the + * per-instance toggles. Tree authoring happens in + * AnimationTreeRegistryEditor (Tools -> Animation Tree Registry). */ class AnimationTreeEditor : public ComponentEditor { public: @@ -18,42 +19,12 @@ public: const char *getName() const override { return "Animation Tree"; } - /* Deferred edit operations — safe to call during UI rendering - * since they are applied only after the tree is fully drawn. */ - struct TreeEditOp { - enum OpType { AddChild, RemoveNode, MoveNode } type; - AnimationTreeNode *targetNode = nullptr; - size_t childIndex = 0; - Ogre::String childType; - int moveDelta = 0; - }; - - void queueAddChild(AnimationTreeNode *parent, - const Ogre::String &type); - void queueRemoveNode(AnimationTreeNode *parent, size_t childIndex); - void queueMoveNode(AnimationTreeNode *parent, size_t childIndex, - int delta); - protected: bool renderComponent(flecs::entity entity, AnimationTreeComponent &at) override; private: - void renderTree(AnimationTreeNode &node, - AnimationTreeNode *parent, - AnimationTreeNode *&selectedNode, - AnimationTreeNode *&selectedParent, - int &id); - void renderProperties(AnimationTreeNode *node, - AnimationTreeComponent &at, - flecs::entity entity); - void renderStatePreview(AnimationTreeComponent &at); - void applyEditOps(AnimationTreeComponent &at); - std::vector getAvailableAnimations( - flecs::entity entity); - Ogre::SceneManager *m_sceneMgr; - std::vector m_editOps; }; #endif // EDITSCENE_ANIMATIONTREEEDITOR_HPP diff --git a/src/features/editScene/ui/AnimationTreeNodeEditor.cpp b/src/features/editScene/ui/AnimationTreeNodeEditor.cpp new file mode 100644 index 0000000..3b37c09 --- /dev/null +++ b/src/features/editScene/ui/AnimationTreeNodeEditor.cpp @@ -0,0 +1,440 @@ +#include "AnimationTreeNodeEditor.hpp" +#include +#include + +AnimationTreeNodeEditor::AnimationTreeNodeEditor() +{ +} + +void AnimationTreeNodeEditor::reset() +{ + m_editOps.clear(); + m_selectedNode = nullptr; + m_selectedParent = nullptr; + m_treeId = 0; + m_modified = false; +} + +static ImU32 nodeTypeColorU32(const Ogre::String &type) +{ + if (type == "output") + return IM_COL32(0xFF, 0x88, 0x88, 0xFF); + if (type == "stateMachine") + return IM_COL32(0x88, 0xBB, 0xFF, 0xFF); + if (type == "state") + return IM_COL32(0x88, 0xFF, 0x88, 0xFF); + if (type == "speed") + return IM_COL32(0xFF, 0xEE, 0x88, 0xFF); + return IM_COL32(0xFF, 0xFF, 0xFF, 0xFF); +} + +static void makeLabel(const AnimationTreeNode &node, char *buf, + size_t bufSize) +{ + if (node.type == "animation") { + snprintf(buf, bufSize, "[%s] %s", node.type.c_str(), + node.animationName.empty() ? + "(none)" : + node.animationName.c_str()); + } else if (node.type == "speed") { + snprintf(buf, bufSize, "[%s] %.2fx %s", node.type.c_str(), + node.speed, + node.name.empty() ? "" : node.name.c_str()); + } else if (node.type == "stateMachine") { + snprintf(buf, bufSize, "[%s] %s (fade %.1f)", + node.type.c_str(), node.name.c_str(), + node.fadeSpeed); + } else { + snprintf(buf, bufSize, "[%s] %s", node.type.c_str(), + node.name.empty() ? "" : node.name.c_str()); + } +} + +static int countChildrenOfType(const AnimationTreeNode &node, + const Ogre::String &type) +{ + int n = 0; + for (const auto &c : node.children) + if (c.type == type) + n++; + return n; +} + +static size_t findChildIndex(const AnimationTreeNode &parent, + const AnimationTreeNode *child) +{ + for (size_t i = 0; i < parent.children.size(); i++) + if (&parent.children[i] == child) + return i; + return (size_t)-1; +} + +static bool canHaveChildren(const Ogre::String &type) +{ + return type == "output" || type == "stateMachine" || + type == "state" || type == "speed"; +} + +void AnimationTreeNodeEditor::queueAddChild(AnimationTreeNode *parent, + const Ogre::String &type) +{ + TreeEditOp op; + op.type = TreeEditOp::AddChild; + op.targetNode = parent; + op.childType = type; + m_editOps.push_back(op); +} + +void AnimationTreeNodeEditor::queueRemoveNode(AnimationTreeNode *parent, + size_t childIndex) +{ + TreeEditOp op; + op.type = TreeEditOp::RemoveNode; + op.targetNode = parent; + op.childIndex = childIndex; + m_editOps.push_back(op); +} + +void AnimationTreeNodeEditor::queueMoveNode(AnimationTreeNode *parent, + size_t childIndex, + int delta) +{ + TreeEditOp op; + op.type = TreeEditOp::MoveNode; + op.targetNode = parent; + op.childIndex = childIndex; + op.moveDelta = delta; + m_editOps.push_back(op); +} + +void AnimationTreeNodeEditor::applyEditOps(AnimationTreeNode &root) +{ + bool changed = false; + for (auto &op : m_editOps) { + if (!op.targetNode) + continue; + auto &vec = op.targetNode->children; + switch (op.type) { + case TreeEditOp::AddChild: { + AnimationTreeNode child; + child.type = op.childType; + if (op.childType == "stateMachine") { + child.name = "sm" + + std::to_string(countChildrenOfType( + *op.targetNode, + "stateMachine")); + child.fadeSpeed = 7.5f; + } else if (op.childType == "state") { + child.name = "state" + + std::to_string(countChildrenOfType( + *op.targetNode, "state")); + } else if (op.childType == "animation") { + child.animationName = ""; + } else if (op.childType == "speed") { + child.speed = 1.0f; + } + vec.push_back(child); + changed = true; + break; + } + case TreeEditOp::RemoveNode: { + if (op.childIndex < vec.size()) { + vec.erase(vec.begin() + op.childIndex); + changed = true; + } + break; + } + case TreeEditOp::MoveNode: { + if (op.childIndex < vec.size()) { + int newIdx = (int)op.childIndex + op.moveDelta; + if (newIdx >= 0 && + newIdx < (int)vec.size()) { + std::swap(vec[op.childIndex], + vec[newIdx]); + changed = true; + } + } + break; + } + } + } + m_editOps.clear(); + if (changed) + m_modified = true; + if (changed) { + /* Selection may have been invalidated by vector reallocation. */ + m_selectedNode = nullptr; + m_selectedParent = nullptr; + } +} + +void AnimationTreeNodeEditor::renderTree(AnimationTreeNode &node, + AnimationTreeNode *parent, + AnimationTreeNode *&selectedNode, + AnimationTreeNode *&selectedParent, + int &id) +{ + int myId = id++; + bool isLeaf = node.children.empty(); + ImGuiTreeNodeFlags flags = + ImGuiTreeNodeFlags_OpenOnArrow | + ImGuiTreeNodeFlags_OpenOnDoubleClick; + if (isLeaf) + flags |= ImGuiTreeNodeFlags_Leaf | + ImGuiTreeNodeFlags_NoTreePushOnOpen; + if (selectedNode == &node) + flags |= ImGuiTreeNodeFlags_Selected; + + char label[256]; + makeLabel(node, label, sizeof(label)); + + /* Color the whole tree node label */ + ImGui::PushStyleColor(ImGuiCol_Text, + ImGui::ColorConvertU32ToFloat4( + nodeTypeColorU32(node.type))); + bool open = ImGui::TreeNodeEx( + ("##animnode" + std::to_string(myId)).c_str(), flags, + "%s", label); + ImGui::PopStyleColor(); + + if (ImGui::IsItemClicked()) { + selectedNode = &node; + selectedParent = parent; + } + + /* Right-click context menu */ + if (ImGui::BeginPopupContextItem()) { + if (canHaveChildren(node.type)) { + if (node.type == "output" || + node.type == "stateMachine" || + node.type == "state") { + if (ImGui::MenuItem("Add State Machine")) + queueAddChild(&node, "stateMachine"); + } + if (node.type == "stateMachine" || + node.type == "output" || + node.type == "state") { + if (ImGui::MenuItem("Add State")) + queueAddChild(&node, "state"); + } + if (node.type == "output" || + node.type == "state" || + node.type == "speed") { + if (ImGui::MenuItem("Add Animation")) + queueAddChild(&node, "animation"); + } + if (node.type == "output" || + node.type == "state") { + if (ImGui::MenuItem("Add Speed")) + queueAddChild(&node, "speed"); + } + } + if (parent) { + size_t idx = findChildIndex(*parent, &node); + if (ImGui::MenuItem("Remove")) { + queueRemoveNode(parent, idx); + if (selectedNode == &node) { + selectedNode = nullptr; + selectedParent = nullptr; + } + } + if (ImGui::MenuItem("Move Up")) + queueMoveNode(parent, idx, -1); + if (ImGui::MenuItem("Move Down")) + queueMoveNode(parent, idx, +1); + } + ImGui::EndPopup(); + } + + /* Inline buttons on the same line as the tree node label */ + ImGui::PushID(myId); + if (canHaveChildren(node.type)) { + ImGui::SameLine(); + if (ImGui::SmallButton("+")) { + ImGui::OpenPopup("AddChild"); + } + if (ImGui::BeginPopup("AddChild")) { + if (node.type == "output" || + node.type == "stateMachine" || + node.type == "state") { + if (ImGui::MenuItem("State Machine")) + queueAddChild(&node, "stateMachine"); + } + if (node.type == "stateMachine" || + node.type == "output" || + node.type == "state") { + if (ImGui::MenuItem("State")) + queueAddChild(&node, "state"); + } + if (node.type == "output" || + node.type == "state" || + node.type == "speed") { + if (ImGui::MenuItem("Animation")) + queueAddChild(&node, "animation"); + } + if (node.type == "output" || + node.type == "state") { + if (ImGui::MenuItem("Speed")) + queueAddChild(&node, "speed"); + } + ImGui::EndPopup(); + } + } + if (parent) { + size_t myIndex = findChildIndex(*parent, &node); + + ImGui::SameLine(); + if (ImGui::SmallButton("-")) + queueRemoveNode(parent, myIndex); + + if (myIndex > 0) { + ImGui::SameLine(); + if (ImGui::SmallButton("^")) + queueMoveNode(parent, myIndex, -1); + } + if (myIndex + 1 < parent->children.size()) { + ImGui::SameLine(); + if (ImGui::SmallButton("v")) + queueMoveNode(parent, myIndex, +1); + } + } + ImGui::PopID(); + + /* Render children and TreePop (only when node is open) */ + if (!isLeaf && open) { + for (auto &child : node.children) + renderTree(child, &node, selectedNode, + selectedParent, id); + ImGui::TreePop(); + } +} + +void AnimationTreeNodeEditor::renderProperties( + AnimationTreeNode *node, + const std::vector &animNames) +{ + if (!node) + return; + + ImGui::Separator(); + ImGui::Text("Node Properties"); + + /* Type selector */ + const char *types[] = { "output", "stateMachine", "state", + "speed", "animation" }; + int typeIdx = 0; + for (int i = 0; i < IM_ARRAYSIZE(types); i++) { + if (node->type == types[i]) { + typeIdx = i; + break; + } + } + if (ImGui::Combo("Type", &typeIdx, types, + IM_ARRAYSIZE(types))) { + node->type = types[typeIdx]; + m_modified = true; + } + + /* Name */ + if (node->type == "stateMachine" || node->type == "state") { + char buf[256]; + strncpy(buf, node->name.c_str(), sizeof(buf) - 1); + buf[sizeof(buf) - 1] = '\0'; + if (ImGui::InputText("Name", buf, sizeof(buf))) { + node->name = buf; + m_modified = true; + } + } + + /* Animation name */ + if (node->type == "animation") { + Ogre::String current = node->animationName; + Ogre::String preview = current.empty() ? "(none)" : current; + if (ImGui::BeginCombo("Animation", preview.c_str())) { + bool noneSel = current.empty(); + if (ImGui::Selectable("(none)", noneSel)) { + node->animationName = ""; + m_modified = true; + } + for (const auto &name : animNames) { + bool sel = (current == name); + if (ImGui::Selectable(name.c_str(), sel)) { + node->animationName = name; + m_modified = true; + } + } + ImGui::EndCombo(); + } + } + + /* Speed */ + if (node->type == "output" || node->type == "speed") { + if (ImGui::SliderFloat("Speed", &node->speed, -2.0f, 10.0f, + "%.2f")) + m_modified = true; + } + + /* Fade speed */ + if (node->type == "stateMachine") { + if (ImGui::SliderFloat("Fade Speed", &node->fadeSpeed, + 0.1f, 30.0f, "%.1f")) + m_modified = true; + } + + /* End transitions (for state machines) */ + if (node->type == "stateMachine" && !node->children.empty()) { + ImGui::Separator(); + ImGui::Text("End Transitions"); + ImGui::TextDisabled( + "Auto-switch state when animation ends"); + + for (const auto &child : node->children) { + if (child.type != "state") + continue; + char buf[256]; + auto it = node->endTransitions.find(child.name); + if (it != node->endTransitions.end()) + strncpy(buf, it->second.c_str(), + sizeof(buf) - 1); + else + buf[0] = '\0'; + buf[sizeof(buf) - 1] = '\0'; + + Ogre::String lbl = "On '" + child.name + + "' end →"; + if (ImGui::InputText(lbl.c_str(), buf, + sizeof(buf))) { + if (buf[0] == '\0') + node->endTransitions.erase( + child.name); + else + node->endTransitions[child.name] = + buf; + m_modified = true; + } + } + } +} + +bool AnimationTreeNodeEditor::render(AnimationTreeNode &root, + const std::vector &animNames) +{ + ImGui::PushID("AnimTreeNodeEditor"); + + m_treeId = 0; + renderTree(root, nullptr, m_selectedNode, m_selectedParent, + m_treeId); + + /* Apply deferred edits before rendering properties so the + * property panel reflects the current selection. */ + applyEditOps(root); + + /* Node properties */ + renderProperties(m_selectedNode, animNames); + + ImGui::PopID(); + + bool wasModified = m_modified; + m_modified = false; + return wasModified; +} diff --git a/src/features/editScene/ui/AnimationTreeNodeEditor.hpp b/src/features/editScene/ui/AnimationTreeNodeEditor.hpp new file mode 100644 index 0000000..660e830 --- /dev/null +++ b/src/features/editScene/ui/AnimationTreeNodeEditor.hpp @@ -0,0 +1,65 @@ +#ifndef EDITSCENE_ANIMATIONTREENODEEDITOR_HPP +#define EDITSCENE_ANIMATIONTREENODEEDITOR_HPP +#pragma once + +#include "../components/AnimationTree.hpp" +#include +#include + +/** + * Reusable editor for an AnimationTreeNode hierarchy. + * + * This editor does not know about registries or components; it simply + * edits the provided root node in place. It is used by the Animation Tree + * Registry editor and can be used anywhere a tree needs to be authored. + */ +class AnimationTreeNodeEditor { +public: + AnimationTreeNodeEditor(); + + /** + * Render the tree editor. + * + * @param root Root node to edit. + * @param animNames Optional list of animation names for dropdowns. + * @return true if the tree was modified. + */ + bool render(AnimationTreeNode &root, + const std::vector &animNames); + + /** Clear selection and pending edit operations. */ + void reset(); + +private: + struct TreeEditOp { + enum OpType { AddChild, RemoveNode, MoveNode } type; + AnimationTreeNode *targetNode = nullptr; + size_t childIndex = 0; + Ogre::String childType; + int moveDelta = 0; + }; + + void queueAddChild(AnimationTreeNode *parent, + const Ogre::String &type); + void queueRemoveNode(AnimationTreeNode *parent, size_t childIndex); + void queueMoveNode(AnimationTreeNode *parent, size_t childIndex, + int delta); + void applyEditOps(AnimationTreeNode &root); + + void renderTree(AnimationTreeNode &node, + AnimationTreeNode *parent, + AnimationTreeNode *&selectedNode, + AnimationTreeNode *&selectedParent, + int &id); + void renderProperties(AnimationTreeNode *node, + const std::vector &animNames); + void renderStatePreview(AnimationTreeNode &root); + + std::vector m_editOps; + AnimationTreeNode *m_selectedNode = nullptr; + AnimationTreeNode *m_selectedParent = nullptr; + int m_treeId = 0; + bool m_modified = false; +}; + +#endif // EDITSCENE_ANIMATIONTREENODEEDITOR_HPP diff --git a/src/features/editScene/ui/AnimationTreeRegistryEditor.cpp b/src/features/editScene/ui/AnimationTreeRegistryEditor.cpp new file mode 100644 index 0000000..66f9934 --- /dev/null +++ b/src/features/editScene/ui/AnimationTreeRegistryEditor.cpp @@ -0,0 +1,165 @@ +#include "AnimationTreeRegistryEditor.hpp" +#include +#include + +AnimationTreeRegistryEditor::AnimationTreeRegistryEditor() +{ + m_newNameBuf[0] = '\0'; +} + +void AnimationTreeRegistryEditor::selectTree(const Ogre::String &name) +{ + m_pendingSelect = true; + m_pendingSelectName = name; + m_nodeEditor.reset(); +} + +void AnimationTreeRegistryEditor::renderMenuBar() +{ + if (!ImGui::BeginMenuBar()) + return; + + if (ImGui::BeginMenu("File")) { + if (ImGui::MenuItem("Save", nullptr)) + AnimationTreeRegistry::getSingleton().saveToFile( + AnimationTreeRegistry::defaultPath()); + if (ImGui::MenuItem("Load", nullptr)) + AnimationTreeRegistry::getSingleton().loadFromFile( + AnimationTreeRegistry::defaultPath()); + ImGui::EndMenu(); + } + ImGui::EndMenuBar(); +} + +void AnimationTreeRegistryEditor::renderListPane() +{ + ImGui::BeginChild("TreeList", ImVec2(250, 0), true); + + if (ImGui::Button("Add Tree")) { + AnimationTreeRegistry ® = + AnimationTreeRegistry::getSingleton(); + Ogre::String name = reg.generateUniqueName("new_tree"); + AnimationTreeRegistry::AnimationTreeDefinition def; + def.name = name; + def.root.type = "output"; + def.root.speed = 1.0f; + reg.registerTree(def); + m_selectedTreeName = name; + } + + AnimationTreeRegistry ® = AnimationTreeRegistry::getSingleton(); + std::vector names = reg.getTreeNames(); + std::sort(names.begin(), names.end()); + + for (const auto &name : names) { + bool selected = (m_selectedTreeName == name); + if (ImGui::Selectable(name.c_str(), selected)) { + if (m_selectedTreeName != name) + m_nodeEditor.reset(); + m_selectedTreeName = name; + } + } + + ImGui::EndChild(); +} + +void AnimationTreeRegistryEditor::renderEditPane() +{ + ImGui::SameLine(); + ImGui::BeginChild("TreeEdit", ImVec2(0, 0), true); + + AnimationTreeRegistry ® = AnimationTreeRegistry::getSingleton(); + AnimationTreeRegistry::AnimationTreeDefinition *def = + reg.findTree(m_selectedTreeName); + + if (!def) { + ImGui::Text("Select a tree from the list to edit."); + ImGui::EndChild(); + return; + } + + /* Name */ + char nameBuf[256]; + snprintf(nameBuf, sizeof(nameBuf), "%s", def->name.c_str()); + if (ImGui::InputText("Name", nameBuf, sizeof(nameBuf))) { + Ogre::String newName = nameBuf; + if (!newName.empty() && newName != def->name && + !reg.hasTree(newName)) { + AnimationTreeRegistry::AnimationTreeDefinition renamed = + *def; + renamed.name = newName; + reg.removeTree(def->name); + reg.registerTree(renamed); + m_selectedTreeName = newName; + def = reg.findTree(newName); + } + } + + /* Skeleton source */ + std::vector sources = reg.getAvailableSkeletonSources(); + Ogre::String currentSource = def->skeletonSource; + Ogre::String preview = currentSource.empty() ? "(none)" : currentSource; + if (ImGui::BeginCombo("Skeleton Source", preview.c_str())) { + bool noneSel = currentSource.empty(); + if (ImGui::Selectable("(none)", noneSel)) { + def->skeletonSource = ""; + reg.markModified(def->name); + } + for (const auto &src : sources) { + bool sel = (currentSource == src); + if (ImGui::Selectable(src.c_str(), sel)) { + def->skeletonSource = src; + reg.markModified(def->name); + } + } + ImGui::EndCombo(); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip( + "Mesh from the Characters resource group used to " + "populate animation-name dropdowns."); + } + + ImGui::Separator(); + + /* Tree node editor */ + std::vector animNames = + reg.getAnimationsForSource(def->skeletonSource); + AnimationTreeNode *rootPtr = def ? &def->root : nullptr; + if (rootPtr) { + bool edited = m_nodeEditor.render(*rootPtr, animNames); + if (edited) + reg.markModified(def->name); + } + + ImGui::Separator(); + + if (ImGui::Button("Delete Tree")) { + reg.removeTree(def->name); + m_selectedTreeName.clear(); + } + + ImGui::EndChild(); +} + +void AnimationTreeRegistryEditor::render(bool *open) +{ + if (m_pendingSelect) { + m_selectedTreeName = m_pendingSelectName; + m_pendingSelect = false; + } + + ImGui::SetNextWindowSize(ImVec2(900, 600), ImGuiCond_FirstUseEver); + if (!ImGui::Begin("Animation Tree Registry", open, + ImGuiWindowFlags_MenuBar)) { + ImGui::End(); + return; + } + + renderMenuBar(); + + renderListPane(); + renderEditPane(); + + ImGui::End(); +} diff --git a/src/features/editScene/ui/AnimationTreeRegistryEditor.hpp b/src/features/editScene/ui/AnimationTreeRegistryEditor.hpp new file mode 100644 index 0000000..0daf1be --- /dev/null +++ b/src/features/editScene/ui/AnimationTreeRegistryEditor.hpp @@ -0,0 +1,39 @@ +#ifndef EDITSCENE_ANIMATIONTREEREGISTRYEDITOR_HPP +#define EDITSCENE_ANIMATIONTREEREGISTRYEDITOR_HPP +#pragma once + +#include "../systems/AnimationTreeRegistry.hpp" +#include "AnimationTreeNodeEditor.hpp" +#include +#include + +/** + * Editor window for the global Animation Tree Registry. + * + * Accessible from Tools -> Animation Tree Registry. Allows creating, + * renaming, deleting and editing named animation trees. Each tree can + * optionally select a skeleton source from the Characters resource group + * to populate animation-name dropdowns. + */ +class AnimationTreeRegistryEditor { +public: + AnimationTreeRegistryEditor(); + + void render(bool *open); + + /* Ask the editor to select a specific tree on next render. */ + void selectTree(const Ogre::String &name); + +private: + void renderMenuBar(); + void renderListPane(); + void renderEditPane(); + + AnimationTreeNodeEditor m_nodeEditor; + Ogre::String m_selectedTreeName; + char m_newNameBuf[256]; + bool m_pendingSelect = false; + Ogre::String m_pendingSelectName; +}; + +#endif // EDITSCENE_ANIMATIONTREEREGISTRYEDITOR_HPP diff --git a/src/features/editScene/ui/AnimationTreeTemplateEditor.cpp b/src/features/editScene/ui/AnimationTreeTemplateEditor.cpp deleted file mode 100644 index fbad9fc..0000000 --- a/src/features/editScene/ui/AnimationTreeTemplateEditor.cpp +++ /dev/null @@ -1,37 +0,0 @@ -#include "AnimationTreeTemplateEditor.hpp" -#include - -bool AnimationTreeTemplateEditor::renderComponent( - flecs::entity entity, AnimationTreeTemplate &templ) -{ - ImGui::PushID("AnimTreeTempl"); - (void)entity; - bool modified = false; - - if (ImGui::CollapsingHeader("Animation Tree Template", - ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::Indent(); - - char nameBuf[256]; - snprintf(nameBuf, sizeof(nameBuf), "%s", templ.name.c_str()); - if (ImGui::InputText("Template Name", nameBuf, - sizeof(nameBuf))) { - templ.name = nameBuf; - modified = true; - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip( - "Other entities reference this template by name in their Animation Tree component."); - } - - ImGui::TextDisabled("Version: %llu", - (unsigned long long)templ.version); - ImGui::TextDisabled( - "Edit the Animation Tree component on this entity to modify the template."); - - ImGui::Unindent(); - } - - ImGui::PopID(); - return modified; -} diff --git a/src/features/editScene/ui/AnimationTreeTemplateEditor.hpp b/src/features/editScene/ui/AnimationTreeTemplateEditor.hpp deleted file mode 100644 index eae8d01..0000000 --- a/src/features/editScene/ui/AnimationTreeTemplateEditor.hpp +++ /dev/null @@ -1,24 +0,0 @@ -#ifndef EDITSCENE_ANIMATIONTREETEMPLATEEDITOR_HPP -#define EDITSCENE_ANIMATIONTREETEMPLATEEDITOR_HPP -#pragma once - -#include "ComponentEditor.hpp" -#include "../components/AnimationTreeTemplate.hpp" - -/** - * Editor for AnimationTreeTemplate component. - * The actual tree editing is done via the co-located AnimationTreeComponent. - * This editor only exposes the template name and version. - */ -class AnimationTreeTemplateEditor - : public ComponentEditor { -public: - bool renderComponent(flecs::entity entity, - AnimationTreeTemplate &templ) override; - const char *getName() const override - { - return "Animation Tree Template"; - } -}; - -#endif // EDITSCENE_ANIMATIONTREETEMPLATEEDITOR_HPP