Files
HelloVulkan/App/editor/panels/ControllerEditorPanel.cpp
T
atdunbg f73e52926f Reorganize directory layout: editor panels/, grouped shaders, drop stale header
- App/editor: move 7 panels into panels/ (Editor core stays at root)
- shaders/: group into pbr/ post/ debug/ env/ (runtime load paths updated)
- Delete outdated duplicate Engine/src/Renderer/VulkanAssetUploader.h
2026-10-07 09:52:17 +08:00

787 lines
27 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// 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();
}