Add animation controller system, material assets and skeleton visualization

- .animctrl state machine asset (params/transitions/blend trees, 1D+2D
  directional blending) with Unity Animator-style editor panel: state
  list, transitions, params, directional pad preview driving live
  CrossFade on the selected entity
- Split animation evaluation out of SceneRenderer into
  AnimationSystem/AnimationEvaluator; skin palettes with rest-pose
  fallback and blend-weight epsilon filtering
- MaterialAsset (.material) with reload-safe preview tile pool
  (DropMaterial clears dirty refs before instance destruction)
- EditorGizmoPass skeleton wires + viewport joint overlay; camera
  projection rework (baked Vulkan Y-flip, ortho support)
- Editor/game build split via HELLOVULKAN_EDITOR option
This commit is contained in:
2026-10-07 01:50:04 +08:00
parent a941207ae3
commit cc837bc605
71 changed files with 8198 additions and 548 deletions
+786
View File
@@ -0,0 +1,786 @@
//
// ControllerEditorPanel 实现:见头文件注释。
//
#include "ControllerEditorPanel.h"
#include <Engine/Animation/AnimationController.h>
#include <Engine/ECS/Components.h>
#include <Engine/Renderer/Model.h>
#include <imgui.h>
#include <algorithm>
#include <cstdio>
namespace {
ImVec4 WarnCol() { return ImVec4(1.0f, 0.75f, 0.2f, 1.0f); }
// 模型首个 clip 名(children/树新建时的默认引用;无模型 = 空)
std::string FirstClipName(const Engine::Model* model)
{
if (model == nullptr || model->GetClips().empty()) return "";
return model->GetClips()[0].name;
}
// clip 引用编辑:选中实体有模型 → combo(模型 clip 列表);
// 无模型 → InputText 手填(clip 缺失 = 该层 rest 兜底,不崩)
bool ClipRefEdit(const char* label, std::string& clip,
const Engine::Model* model)
{
if (model == nullptr || model->GetClips().empty())
{
char buf[128];
snprintf(buf, sizeof(buf), "%s", clip.c_str());
ImGui::SetNextItemWidth(-1.0f);
if (ImGui::InputText(label, buf, sizeof(buf)))
{
clip = buf;
return true;
}
return false;
}
std::vector<const char*> names;
names.reserve(model->GetClips().size());
int sel = -1;
for (const auto& c : model->GetClips())
{
names.push_back(c.name.c_str());
if (c.name == clip) sel = static_cast<int>(names.size()) - 1;
}
if (sel < 0) sel = 0;
if (ImGui::Combo(label, &sel, names.data(),
static_cast<int>(names.size())))
{
clip = model->GetClips()[static_cast<size_t>(sel)].name;
return true;
}
return false;
}
} // namespace
void ControllerEditorPanel::Draw(const EditorContext& ctx)
{
if (!m_Open || m_Path.empty()) return;
Engine::Animation::AnimationController* ctrl =
ctx.assets->FindController(m_Path);
if (!ImGui::Begin("Controller Editor", &m_Open))
{
ImGui::End();
return;
}
if (ctrl == nullptr)
{
ImGui::TextColored(WarnCol(), "'%s' failed to load",
m_Path.c_str());
if (ImGui::Button("Reload"))
{
ctx.assets->ReloadController(m_Path);
}
ImGui::End();
return;
}
// ---- 头部:路径 + Save(落盘 + Reload 重编译条件)----
ImGui::TextUnformatted(m_Path.c_str());
ImGui::SameLine();
if (ImGui::Button("Save"))
{
if (ctrl->Save(m_Path))
{
ctx.assets->ReloadController(m_Path);
ctrl = ctx.assets->FindController(m_Path);
}
}
ImGui::SameLine();
ImGui::TextDisabled("edits apply live; Save writes the file");
ImGui::Separator();
DrawPreview(ctx);
ImGui::Separator();
DrawStates(ctx, ctrl);
ImGui::Separator();
DrawTransitions(ctx, ctrl);
ImGui::Separator();
DrawParams(ctx, ctrl);
ImGui::End();
}
// ---- Preview:方向盘(拖动 = SetFloat 到选中实体,摇杆回中)----
void ControllerEditorPanel::DrawPreview(const EditorContext& ctx)
{
// 选中实体的 AnimationComponent(Controller 模式才驱动)
Engine::ECS::AnimationComponent* anim = nullptr;
const Engine::Model* model = nullptr;
if (ctx.selected != nullptr && ctx.scene != nullptr &&
ctx.scene->GetRegistry().valid(*ctx.selected))
{
auto& reg = ctx.scene->GetRegistry();
if (auto* a =
reg.try_get<Engine::ECS::AnimationComponent>(*ctx.selected))
{
anim = a;
if (auto* mr =
reg.try_get<Engine::ECS::MeshRendererComponent>(
*ctx.selected))
{
if (mr->model.Valid())
{
model = ctx.assets->GetModel(mr->model);
}
}
}
}
if (anim == nullptr)
{
ImGui::TextDisabled("select an entity with an Animation component "
"to preview");
return;
}
Engine::Animation::AnimationController* ctrl =
anim->controllerPath.empty()
? nullptr
: ctx.assets->FindController(anim->controllerPath);
if (ctrl == nullptr) return;
// 面板选中状态的 2D 树(预览读出;拖动写的是实体参数)
const Engine::Animation::ControllerState* padState = nullptr;
const auto& order = ctrl->GetStateOrder();
if (m_SelState >= 0 && m_SelState < static_cast<int>(order.size()))
{
const auto& s = ctrl->GetStates().at(order[static_cast<size_t>(
m_SelState)]);
if (s.tree.type ==
Engine::Animation::BlendTree::Type::Directional2D)
{
padState = &s;
}
}
if (padState == nullptr)
{
ImGui::TextDisabled("preview: select a 2DDirectional state");
return;
}
const float S = 190.0f;
const float rad = S * 0.5f - 14.0f;
const ImVec2 org = ImGui::GetCursorScreenPos();
const ImVec2 ctr(org.x + S * 0.5f, org.y + S * 0.5f);
ImDrawList* dl = ImGui::GetWindowDrawList();
dl->AddCircleFilled(ctr, rad, IM_COL32(30, 30, 34, 255));
dl->AddCircle(ctr, rad, IM_COL32(90, 90, 100, 255), 48, 1.5f);
dl->AddLine(ImVec2(ctr.x - rad, ctr.y), ImVec2(ctr.x + rad, ctr.y),
IM_COL32(60, 60, 68, 255));
dl->AddLine(ImVec2(ctr.x, ctr.y - rad), ImVec2(ctr.x, ctr.y + rad),
IM_COL32(60, 60, 68, 255));
const auto& ch = padState->tree.children;
for (const auto& c : ch)
{
const bool isOrigin =
c.pos.x * c.pos.x + c.pos.y * c.pos.y < 1e-8f;
const ImVec2 p(ctr.x + c.pos.x * rad, ctr.y - c.pos.y * rad);
dl->AddCircleFilled(p, isOrigin ? 5.0f : 4.0f,
isOrigin ? IM_COL32(90, 140, 255, 255)
: IM_COL32(220, 220, 225, 255));
}
// mode=Direct 的实体不跑状态机——参数写进去没有任何效果,
// 直接禁用方向盘(Unity:Animator 组件没挂 Controller 时
// 参数面板不可编辑,同语义)
const bool isCtrlMode =
anim->mode == Engine::ECS::AnimationComponent::AnimMode::Controller;
if (!isCtrlMode)
{
ImGui::TextColored(WarnCol(),
"entity is in Direct mode - switch Mode to "
"Controller to preview");
return;
}
ImGui::InvisibleButton("##dirpad", ImVec2(S, S));
const auto getF = [&](const std::string& n) -> float {
auto it = anim->floatParams.find(n);
return it != anim->floatParams.end() ? it->second : 0.0f;
};
float cx = getF(padState->tree.xParam);
float cz = getF(padState->tree.zParam);
if (ImGui::IsItemActivated())
{
// 预览闭环(Unity Animator 窗口语义:选中状态即可预览该
// 状态)——实体停在别的状态时拖盘自动切到面板选中的 2D
// 树状态,否则 SetFloat 对 Single 状态完全无效
if (anim->currentState != order[static_cast<size_t>(m_SelState)])
{
anim->CrossFade(order[static_cast<size_t>(m_SelState)], 0.1f);
}
}
if (ImGui::IsItemActive())
{
const ImVec2 m = ImGui::GetIO().MousePos;
float vx = (m.x - ctr.x) / rad;
float vz = -(m.y - ctr.y) / rad;
const float len = glm::length(glm::vec2(vx, vz));
if (len > 1.0f)
{
vx /= len;
vz /= len;
}
anim->SetFloat(padState->tree.xParam, vx);
anim->SetFloat(padState->tree.zParam, vz);
cx = vx;
cz = vz;
}
else if (ImGui::IsItemDeactivatedAfterEdit())
{
anim->SetFloat(padState->tree.xParam, 0.0f);
anim->SetFloat(padState->tree.zParam, 0.0f);
cx = cz = 0.0f;
}
if (ImGui::IsItemHovered())
{
const ImVec2 m = ImGui::GetIO().MousePos;
for (const auto& c : ch)
{
const ImVec2 p(ctr.x + c.pos.x * rad, ctr.y - c.pos.y * rad);
const float dx = m.x - p.x;
const float dy = m.y - p.y;
if (dx * dx + dy * dy < 64.0f)
{
ImGui::SetTooltip("%s", c.clip.c_str());
break;
}
}
}
const ImVec2 cur(ctr.x + cx * rad, ctr.y - cz * rad);
dl->AddCircleFilled(cur, 6.0f, IM_COL32(255, 170, 60, 255));
dl->AddCircle(cur, 9.0f, IM_COL32(255, 170, 60, 160), 0, 1.5f);
// 右侧读出:当前参数 + 选中状态树的权重(运行时同一数学)
ImGui::SameLine();
ImGui::BeginGroup();
ImGui::Text("preview: %s", anim->currentState.c_str());
ImGui::Text("%.2f / %.2f", cx, cz);
std::vector<std::pair<Engine::u32, float>> ws;
Engine::Animation::EvaluateBlendTree2D(padState->tree, cx, cz, ws);
std::sort(ws.begin(), ws.end(),
[](const auto& a, const auto& b)
{ return a.second > b.second; });
for (const auto& [idx, w] : ws)
{
ImGui::Text("%s %.2f", ch[idx].clip.c_str(), w);
}
ImGui::EndGroup();
}
// ---- States:列表(entry ★)+ 选中状态详情 + children 编辑 ----
void ControllerEditorPanel::DrawStates(
const EditorContext& ctx, Engine::Animation::AnimationController* ctrl)
{
if (!ImGui::TreeNodeEx("States", ImGuiTreeNodeFlags_DefaultOpen))
{
return;
}
auto& order = ctrl->StateOrderMut();
auto& states = ctrl->StatesMut();
// 选中实体模型(children 的 clip 下拉来源)
const Engine::Model* model = nullptr;
if (ctx.selected != nullptr && ctx.scene != nullptr &&
ctx.scene->GetRegistry().valid(*ctx.selected))
{
if (auto* mr =
ctx.scene->GetRegistry()
.try_get<Engine::ECS::MeshRendererComponent>(
*ctx.selected))
{
if (mr->model.Valid())
{
model = ctx.assets->GetModel(mr->model);
}
}
}
// 状态列表 + Add
int delIdx = -1;
for (int i = 0; i < static_cast<int>(order.size()); ++i)
{
const bool isEntry = order[static_cast<size_t>(i)] ==
ctrl->GetEntryState();
ImGui::PushID(i);
if (ImGui::Selectable(
(order[static_cast<size_t>(i)] +
std::string(isEntry ? " [entry]" : ""))
.c_str(),
i == m_SelState))
{
m_SelState = i;
}
ImGui::PopID();
}
{
static char s_NewName[64] = "";
ImGui::SetNextItemWidth(120.0f);
ImGui::InputText("##new_state", s_NewName, sizeof(s_NewName));
ImGui::SameLine();
if (ImGui::SmallButton("+ Add") && s_NewName[0] != '\0' &&
states.find(s_NewName) == states.end())
{
const std::string n = s_NewName;
states.emplace(n, Engine::Animation::ControllerState{n});
order.push_back(n);
m_SelState = static_cast<int>(order.size()) - 1;
s_NewName[0] = '\0';
}
}
if (m_SelState >= static_cast<int>(order.size()))
{
m_SelState = static_cast<int>(order.size()) - 1;
}
if (m_SelState < 0 || order.empty())
{
ImGui::TreePop();
return;
}
// ---- 选中状态详情 ----
const std::string selName = order[static_cast<size_t>(m_SelState)];
auto it = states.find(selName);
if (it == states.end())
{
ImGui::TreePop();
return;
}
auto& st = it->second;
ImGui::Indent();
ImGui::TextUnformatted("editing:");
ImGui::SameLine();
ImGui::TextUnformatted(selName.c_str());
if (selName != ctrl->GetEntryState() &&
ImGui::SmallButton("Set as Entry"))
{
ctrl->SetEntry(selName);
}
ImGui::SameLine();
ImGui::PushID("delstate");
if (ImGui::SmallButton("x Delete"))
{
delIdx = m_SelState;
}
ImGui::PopID();
// 重命名(级联:transitions / transitionsOnEnd / entry 引用同步)
{
static char s_Rename[64] = "";
ImGui::SetNextItemWidth(120.0f);
ImGui::InputText("##rename", s_Rename, sizeof(s_Rename));
ImGui::SameLine();
if (ImGui::SmallButton("Rename") && s_Rename[0] != '\0' &&
states.find(s_Rename) == states.end())
{
const std::string oldN = selName;
const std::string newN = s_Rename;
auto node = states.extract(oldN);
if (node)
{
node.key() = newN;
states.insert(std::move(node));
order[static_cast<size_t>(m_SelState)] = newN;
for (auto& t : ctrl->TransitionsMut())
{
if (t.from == oldN) t.from = newN;
if (t.to == oldN) t.to = newN;
}
for (auto& [n, s] : states)
{
for (auto& e : s.transitionsOnEnd)
{
if (e == oldN) e = newN;
}
}
if (ctrl->GetEntryState() == oldN)
{
ctrl->SetEntry(newN);
}
s_Rename[0] = '\0';
}
}
}
// ---- 播放分组(speed/loop/播完出口)----
if (ImGui::TreeNode("Playback"))
{
ImGui::DragFloat("speed", &st.speed, 0.05f, 0.1f, 10.0f, "%.2f");
ImGui::Checkbox("loop", &st.loop);
if (!st.loop)
{
// 播完出口列表(按序首个可用)+ 下拉追加
for (size_t ei = 0; ei < st.transitionsOnEnd.size(); ++ei)
{
ImGui::PushID(static_cast<int>(ei) + 1000);
ImGui::BulletText("%s",
st.transitionsOnEnd[ei].c_str());
ImGui::SameLine();
if (ImGui::SmallButton("x"))
{
st.transitionsOnEnd.erase(
st.transitionsOnEnd.begin() +
static_cast<long>(ei));
ImGui::PopID();
break;
}
ImGui::PopID();
}
if (!order.empty())
{
static int s_OnEndSel = 0;
s_OnEndSel = std::clamp(
s_OnEndSel, 0,
static_cast<int>(order.size()) - 1);
ImGui::SetNextItemWidth(120.0f);
ImGui::Combo(
"##onend", &s_OnEndSel,
[](void* d, int i) -> const char* {
return (*static_cast<std::vector<std::string>*>(
d))[static_cast<size_t>(i)]
.c_str();
},
&order, static_cast<int>(order.size()));
ImGui::SameLine();
if (ImGui::SmallButton("+ exit"))
{
st.transitionsOnEnd.push_back(
order[static_cast<size_t>(s_OnEndSel)]);
}
}
}
ImGui::TreePop();
}
// ---- 混合树分组 ----
if (ImGui::TreeNode("Blend Tree"))
{
using BTType = Engine::Animation::BlendTree::Type;
// 树类型:Single 单 clip / 1D 阈值 / 2D 平面方向
int tt = static_cast<int>(st.tree.type);
if (ImGui::Combo("type", &tt,
"Single\0Directional 1D\0Directional 2D\0"))
{
st.tree.type = static_cast<BTType>(tt);
st.tree.children.clear();
if (st.tree.type == BTType::Single)
{
if (st.tree.clip.empty())
{
st.tree.clip = FirstClipName(model);
}
}
else
{
// 默认一个原点 child(clip 取模型首个;无模型 = 手填)
st.tree.children.push_back(
{FirstClipName(model), glm::vec2{0.0f, 0.0f}});
}
}
// x/z 参数绑定(float 参数下拉)
{
std::vector<const char*> pnames;
for (const auto& p : ctrl->GetParams())
{
if (p.type ==
Engine::Animation::ControllerParam::Type::Float)
{
pnames.push_back(p.name.c_str());
}
}
if (!pnames.empty())
{
if (st.tree.type != BTType::Single)
{
int xi = 0;
for (int k = 0; k < static_cast<int>(pnames.size());
++k)
{
if (st.tree.xParam ==
pnames[static_cast<size_t>(k)])
xi = k;
}
ImGui::SetNextItemWidth(140.0f);
if (ImGui::Combo("x param", &xi, pnames.data(),
static_cast<int>(pnames.size())))
{
st.tree.xParam = pnames[static_cast<size_t>(xi)];
}
}
if (st.tree.type == BTType::Directional2D)
{
int zi = 0;
for (int k = 0; k < static_cast<int>(pnames.size());
++k)
{
if (st.tree.zParam ==
pnames[static_cast<size_t>(k)])
zi = k;
}
ImGui::SetNextItemWidth(140.0f);
if (ImGui::Combo("z param", &zi, pnames.data(),
static_cast<int>(pnames.size())))
{
st.tree.zParam = pnames[static_cast<size_t>(zi)];
}
}
}
else if (st.tree.type != BTType::Single)
{
ImGui::TextColored(
WarnCol(), "need a float param for the blend axis");
}
}
if (st.tree.type == BTType::Single)
{
ClipRefEdit("clip", st.tree.clip, model);
}
else
{
// 原点点缺失警告:v 回到中心时方向层权重全被最小阈值滤
// 掉,层空 → 引擎侧 fallback 到首个有效 child 单层(不是
// T-pose),但"中心=默认动作"的直觉语义要靠原点点承载
if (st.tree.type == BTType::Directional2D)
{
bool hasOrigin = false;
for (const auto& c : st.tree.children)
{
if (c.pos.x * c.pos.x + c.pos.y * c.pos.y < 1e-8f)
hasOrigin = true;
}
if (!hasOrigin)
{
ImGui::TextColored(
WarnCol(), "! no origin child (at 0,0) - center "
"falls back to first child");
}
}
// children 表:clip + 定位 + 删除(2D = (x,y) 平面定位点;
// 1D = x 即阈值)。编辑即时生效(System 下帧直读缓存实例)
int delChild = -1;
for (int ci = 0;
ci < static_cast<int>(st.tree.children.size()); ++ci)
{
auto& c = st.tree.children[static_cast<size_t>(ci)];
ImGui::PushID(ci);
ClipRefEdit("##clip", c.clip, model);
if (st.tree.type == BTType::Directional2D)
{
ImGui::SetNextItemWidth(70.0f);
ImGui::DragFloat("##atx", &c.pos.x, 0.01f, -1.0f,
1.0f, "x %.2f");
ImGui::SameLine();
ImGui::SetNextItemWidth(70.0f);
ImGui::DragFloat("##aty", &c.pos.y, 0.01f, -1.0f,
1.0f, "y %.2f");
}
else
{
ImGui::SetNextItemWidth(120.0f);
ImGui::DragFloat("##thr", &c.pos.x, 0.05f, -10.0f,
10.0f, "thr %.2f");
}
ImGui::SameLine();
if (ImGui::SmallButton("x")) delChild = ci;
ImGui::PopID();
}
if (delChild >= 0)
{
st.tree.children.erase(
st.tree.children.begin() + delChild);
}
if (ImGui::SmallButton("+ Add Child"))
{
st.tree.children.push_back(
{FirstClipName(model),
st.tree.type == BTType::Directional2D
? glm::vec2{1.0f, 0.0f}
: glm::vec2{0.0f, 0.0f}});
}
}
ImGui::TreePop(); // Blend Tree
}
ImGui::Unindent();
if (delIdx >= 0)
{
const std::string name = order[static_cast<size_t>(delIdx)];
states.erase(name);
order.erase(order.begin() + delIdx);
auto& trs = ctrl->TransitionsMut();
for (int i = static_cast<int>(trs.size()) - 1; i >= 0; --i)
{
if (trs[static_cast<size_t>(i)].from == name ||
trs[static_cast<size_t>(i)].to == name)
{
trs.erase(trs.begin() + i);
}
}
for (auto& [n, s] : states)
{
std::erase_if(s.transitionsOnEnd,
[&](const std::string& e)
{ return e == name; });
}
if (ctrl->GetEntryState() == name)
{
ctrl->SetEntry(order.empty() ? "" : order[0]);
}
m_SelState = 0;
}
ImGui::TreePop();
}
// ---- Transitions:行编辑(from/to 下拉 + cond 字符串 Enter 编译)----
void ControllerEditorPanel::DrawTransitions(
const EditorContext& ctx, Engine::Animation::AnimationController* ctrl)
{
(void)ctx;
if (!ImGui::TreeNode("Transitions")) return;
const auto& order = ctrl->GetStateOrder();
std::vector<const char*> snames;
snames.reserve(order.size() + 1);
snames.push_back("*"); // AnyState
for (const auto& n : order) snames.push_back(n.c_str());
auto stateCombo = [&](const char* label, std::string& cur) {
int sel = 0;
for (int k = 0; k < static_cast<int>(snames.size()); ++k)
{
if (cur == snames[static_cast<size_t>(k)]) sel = k;
}
ImGui::SetNextItemWidth(110.0f);
if (ImGui::Combo(label, &sel, snames.data(),
static_cast<int>(snames.size())))
{
cur = snames[static_cast<size_t>(sel)];
}
};
int delIdx = -1;
int ti = 0;
for (auto& t : ctrl->TransitionsMut())
{
ImGui::PushID(ti++);
// 行 1:来源 -> 目标(+ 删除)
stateCombo("##from", t.from);
ImGui::SameLine();
ImGui::TextUnformatted("->");
ImGui::SameLine();
stateCombo("##to", t.to);
ImGui::SameLine();
if (ImGui::SmallButton("x")) delIdx = ti - 1;
// 行 2:条件 + fade + exitTime(缩进——同一过渡的属性组)
ImGui::Indent(18.0f);
// cond:反推字符串 → Enter(DeactivatedAfterEdit)就地重编译
{
const std::string s =
t.cond.always ? "" :
Engine::Animation::AnimationController::CondToString(
t.cond);
char buf[96];
snprintf(buf, sizeof(buf), "%s", s.c_str());
ImGui::SetNextItemWidth(170.0f);
if (ImGui::InputText("##cond", buf, sizeof(buf),
ImGuiInputTextFlags_EnterReturnsTrue) ||
ImGui::IsItemDeactivatedAfterEdit())
{
t.cond = Engine::Animation::AnimationController::ParseCond(
buf);
}
if (!t.cond.always && !t.cond.valid)
{
ImGui::SameLine();
ImGui::TextColored(WarnCol(), "!");
}
}
ImGui::SameLine();
ImGui::SetNextItemWidth(60.0f);
ImGui::DragFloat("##fade", &t.fade, 0.02f, 0.0f, 5.0f, "%.2f");
ImGui::SameLine();
ImGui::Checkbox("##exit", &t.hasExitTime);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("exit time");
ImGui::Unindent(18.0f);
ImGui::PopID();
}
if (delIdx >= 0)
{
auto& trs = ctrl->TransitionsMut();
trs.erase(trs.begin() + delIdx);
}
if (ImGui::SmallButton("+ Add") && !order.empty())
{
Engine::Animation::Transition tr;
tr.from = order[0];
tr.to = order[order.size() > 1 ? 1 : 0];
ctrl->TransitionsMut().push_back(tr);
}
ImGui::TreePop();
}
// ---- Params:参数声明编辑(禁改名——被 tree/cond 引用,改了断链)----
void ControllerEditorPanel::DrawParams(
const EditorContext& ctx, Engine::Animation::AnimationController* ctrl)
{
(void)ctx;
if (!ImGui::TreeNode("Params")) return;
auto& ps = ctrl->ParamsMut();
int delIdx = -1;
int pi = 0;
for (auto& p : ps)
{
ImGui::PushID(pi++);
ImGui::TextUnformatted(p.name.c_str());
ImGui::SameLine();
int ty = static_cast<int>(p.type);
ImGui::SetNextItemWidth(80.0f);
if (ImGui::Combo("##ty", &ty, "float\0bool\0trigger\0"))
{
p.type =
static_cast<Engine::Animation::ControllerParam::Type>(ty);
}
ImGui::SameLine();
if (p.type ==
Engine::Animation::ControllerParam::Type::Float)
{
ImGui::SetNextItemWidth(90.0f);
ImGui::DragFloat("##def", &p.defFloat, 0.05f);
}
else if (p.type ==
Engine::Animation::ControllerParam::Type::Bool)
{
ImGui::Checkbox("##def", &p.defBool);
}
ImGui::SameLine();
if (ImGui::SmallButton("x")) delIdx = pi - 1;
ImGui::PopID();
}
if (delIdx >= 0) ps.erase(ps.begin() + delIdx);
{
static char s_NewName[64] = "";
ImGui::SetNextItemWidth(120.0f);
ImGui::InputText("##new_param", s_NewName, sizeof(s_NewName));
ImGui::SameLine();
if (ImGui::SmallButton("+ Add") && s_NewName[0] != '\0')
{
bool dup = false;
for (const auto& p : ps)
{
if (p.name == s_NewName) dup = true;
}
if (!dup)
{
Engine::Animation::ControllerParam d;
d.name = s_NewName;
ps.push_back(d);
s_NewName[0] = '\0';
}
}
}
ImGui::TreePop();
}