- 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
787 lines
27 KiB
C++
787 lines
27 KiB
C++
//
|
||
// 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();
|
||
}
|