282 lines
11 KiB
C++
282 lines
11 KiB
C++
//
|
||
// HierarchyPanel.cpp:实体层级树实现(UE Outliner 语义)。
|
||
//
|
||
// 树形展示(根 = 无有效父链接的实体,子 = HierarchyComponent 挂载序)、
|
||
// 点击选中、拖拽改父/拖到空白区挂根(keepWorld = 挂载前后世界位姿
|
||
// 不变)、右键 Create Child / Detach / Delete(级联随葬,延迟销毁)。
|
||
//
|
||
|
||
#include "HierarchyPanel.h"
|
||
#include "AssetBrowserPanel.h" // kPrefabDragPayload(Content Browser 源)
|
||
#include "Engine/ECS/Components.h"
|
||
#include "Engine/ECS/EntityHandle.h" // CreateEntity 返回完整 EntityHandle
|
||
#include "Engine/Serialization/SceneSerializer.h"
|
||
#include "Engine/Core/Log.h"
|
||
|
||
#include <imgui.h>
|
||
|
||
#include <cstring>
|
||
#include <filesystem>
|
||
#include <string>
|
||
#include <vector>
|
||
|
||
using namespace Engine;
|
||
|
||
namespace {
|
||
|
||
// 拖拽 payload 类型名(Editor 内部约定;实体位模式 4 字节)
|
||
constexpr const char* kHierPayload = "HIER_ENTITY";
|
||
|
||
// 实体显示名
|
||
std::string DisplayName(const entt::registry& reg, entt::entity e)
|
||
{
|
||
if (const auto* n = reg.try_get<Engine::ECS::NameComponent>(e);
|
||
n && !n->name.empty()) {
|
||
return n->name;
|
||
}
|
||
return "(unnamed)";
|
||
}
|
||
|
||
// Prefab 拖入实例化(树节点/空白区落点共用)。根实体挂在场景根、
|
||
// 保留授权位姿;返回根实体(失败 entt::null)
|
||
entt::entity InstantiatePrefabDrop(const EditorContext& ctx,
|
||
const char* path)
|
||
{
|
||
if (!ctx.scene || !ctx.assets || !path) {
|
||
return entt::null;
|
||
}
|
||
return Engine::Serialization::SceneSerializer::InstantiatePrefab(
|
||
*ctx.scene, *ctx.assets, path);
|
||
}
|
||
|
||
// 实体名 → 合法文件名(路径分隔符/保留符净化;空名兜底)
|
||
std::string PrefabFileName(std::string name)
|
||
{
|
||
for (char& c : name) {
|
||
if (c == '/' || c == '\\' || c == ':' || c == '*' ||
|
||
c == '?' || c == '"' || c == '<' || c == '>' ||
|
||
c == '|') {
|
||
c = '_';
|
||
}
|
||
}
|
||
if (name.empty() || name == "(unnamed)") {
|
||
name = "prefab";
|
||
}
|
||
return name;
|
||
}
|
||
|
||
// 递归绘制单节点(含子树)。返回值无意义,选中写 selected
|
||
void DrawEntityNode(const EditorContext& ctx, entt::entity e,
|
||
entt::entity& selected)
|
||
{
|
||
auto& reg = ctx.scene->GetRegistry();
|
||
|
||
// 稳定行 ID(UUID):折叠态跨帧保持 + 与 Inspector PushID 隔离
|
||
u64 rowId = static_cast<u64>(static_cast<u32>(e));
|
||
if (const auto* id = reg.try_get<Engine::ECS::IdComponent>(e)) {
|
||
rowId = id->uuid.Value();
|
||
}
|
||
ImGui::PushID(reinterpret_cast<const void*>(rowId));
|
||
|
||
// 子表(过滤悬空 + 无 Name 的防御性兜底)
|
||
const auto* h = reg.try_get<Engine::ECS::HierarchyComponent>(e);
|
||
std::vector<entt::entity> kids;
|
||
if (h) {
|
||
for (entt::entity c : h->children) {
|
||
if (reg.valid(c) && reg.all_of<Engine::ECS::NameComponent>(c)) {
|
||
kids.push_back(c);
|
||
}
|
||
}
|
||
}
|
||
|
||
const std::string label = DisplayName(reg, e);
|
||
const ImGuiTreeNodeFlags flags =
|
||
ImGuiTreeNodeFlags_OpenOnArrow |
|
||
ImGuiTreeNodeFlags_SpanAvailWidth |
|
||
(kids.empty() ? ImGuiTreeNodeFlags_Leaf : 0) |
|
||
(selected == e ? ImGuiTreeNodeFlags_Selected : 0);
|
||
|
||
const bool opened = ImGui::TreeNodeEx(label.c_str(), flags);
|
||
const bool toggled = ImGui::IsItemToggledOpen();
|
||
if (ImGui::IsItemClicked(ImGuiMouseButton_Left) && !toggled) {
|
||
selected = e;
|
||
}
|
||
|
||
// ---- 拖拽源:拖动本实体去改父 ----
|
||
if (ImGui::BeginDragDropSource()) {
|
||
const u32 bits = static_cast<u32>(entt::to_integral(e));
|
||
ImGui::SetDragDropPayload(kHierPayload, &bits, sizeof(bits));
|
||
ImGui::TextUnformatted(label.c_str());
|
||
ImGui::EndDragDropSource();
|
||
}
|
||
|
||
// ---- 拖放目标:挂到本实体下(keepWorld,世界位姿不变);
|
||
// Prefab 拖入 = 实例化子树后挂到本实体下(keepWorld 保持
|
||
// 授权世界位姿)----
|
||
if (ImGui::BeginDragDropTarget()) {
|
||
if (const ImGuiPayload* pl =
|
||
ImGui::AcceptDragDropPayload(kHierPayload)) {
|
||
u32 bits = 0;
|
||
std::memcpy(&bits, pl->Data, sizeof(bits));
|
||
const entt::entity dragged = static_cast<entt::entity>(bits);
|
||
// 拒挂自身/自身后代(SetParent 内部同样防环,这里提前
|
||
// 静默拦截,避免"看起来挂上了"的错觉)
|
||
if (dragged != e && !ctx.scene->IsDescendantOf(dragged, e)) {
|
||
ctx.scene->SetParent(dragged, e);
|
||
}
|
||
}
|
||
if (const ImGuiPayload* pl =
|
||
ImGui::AcceptDragDropPayload(kPrefabDragPayload)) {
|
||
if (const entt::entity root =
|
||
InstantiatePrefabDrop(ctx,
|
||
static_cast<const char*>(
|
||
pl->Data));
|
||
root != entt::null) {
|
||
ctx.scene->SetParent(root, e); // keepWorld 默认 true
|
||
selected = root;
|
||
}
|
||
}
|
||
ImGui::EndDragDropTarget();
|
||
}
|
||
|
||
// ---- 右键菜单 ----
|
||
if (ImGui::BeginPopupContextItem("hier_ctx")) {
|
||
if (ImGui::MenuItem("Create Child")) {
|
||
// keepWorld=true:新实体出现在父的位置(UE attach 语义)
|
||
const auto child = ctx.scene->CreateEntity("child entity");
|
||
ctx.scene->SetParent(child, e);
|
||
selected = child;
|
||
}
|
||
if (ImGui::MenuItem("Save As Prefab")) {
|
||
// 固定目录 + 实体名派生文件名(无文件对话框库,与场景
|
||
// File 菜单同策略;重名覆盖,改名走文件系统)
|
||
const std::string path =
|
||
"assets/prefabs/" +
|
||
PrefabFileName(DisplayName(reg, e)) + ".prefab";
|
||
std::error_code ec;
|
||
std::filesystem::create_directories("assets/prefabs", ec);
|
||
if (Serialization::SceneSerializer::SerializePrefab(
|
||
*ctx.scene, *ctx.assets, e, path)) {
|
||
ENGINE_INFO("Hierarchy: prefab saved to '{}'", path);
|
||
}
|
||
}
|
||
if (ctx.scene->GetParent(e) != entt::null &&
|
||
ImGui::MenuItem("Detach to Root")) {
|
||
ctx.scene->SetParent(e, entt::null);
|
||
}
|
||
if (ImGui::MenuItem("Delete")) {
|
||
// 级联随葬:选中若是被删子树的一部分,一并清除
|
||
const bool selInSubtree = ctx.scene->IsDescendantOf(selected, e);
|
||
ctx.scene->DestroyEntity(e);
|
||
if (selInSubtree) {
|
||
selected = entt::null;
|
||
}
|
||
}
|
||
ImGui::EndPopup();
|
||
}
|
||
|
||
// ---- 组件徽标(与行内同级显示)----
|
||
std::string badge;
|
||
if (reg.all_of<Engine::ECS::MeshRendererComponent>(e)) badge += "[M]";
|
||
if (reg.all_of<Engine::ECS::AnimationComponent>(e)) badge += "[A]";
|
||
if (reg.all_of<Engine::ECS::CameraComponent>(e)) badge += "[C]";
|
||
if (reg.all_of<Engine::ECS::LightComponent>(e)) badge += "[L]";
|
||
if (reg.all_of<Engine::ECS::RigidBodyComponent>(e)) badge += "[P]";
|
||
if (!badge.empty()) {
|
||
ImGui::SameLine();
|
||
ImGui::TextDisabled("%s", badge.c_str());
|
||
if (ImGui::IsItemHovered()) {
|
||
ImGui::SetTooltip(
|
||
"M=Mesh A=Animation C=Camera L=Light P=Physics");
|
||
}
|
||
}
|
||
|
||
if (opened) {
|
||
for (entt::entity c : kids) {
|
||
DrawEntityNode(ctx, c, selected);
|
||
}
|
||
ImGui::TreePop();
|
||
}
|
||
ImGui::PopID();
|
||
}
|
||
|
||
} // namespace
|
||
|
||
void HierarchyPanel::Draw(const EditorContext& ctx, entt::entity& selected)
|
||
{
|
||
ImGui::Begin("Outliner");
|
||
|
||
// 工具栏:创建实体(在遍历之前,不修改遍历中的注册表结构)。
|
||
// Place Actors 简化语义:常用类型一键带组件,其余 Add Component 补
|
||
if (ImGui::Button("+ Create"))
|
||
{
|
||
ImGui::OpenPopup("create_entity");
|
||
}
|
||
if (ImGui::BeginPopup("create_entity"))
|
||
{
|
||
if (ImGui::MenuItem("Empty"))
|
||
{
|
||
selected = ctx.scene->CreateEntity("Entity");
|
||
}
|
||
if (ImGui::MenuItem("Light"))
|
||
{
|
||
auto e = ctx.scene->CreateEntity("Light");
|
||
e.AddComponent<Engine::ECS::LightComponent>();
|
||
selected = e;
|
||
}
|
||
if (ImGui::MenuItem("Camera"))
|
||
{
|
||
auto e = ctx.scene->CreateEntity("Camera");
|
||
e.AddComponent<Engine::ECS::CameraComponent>();
|
||
selected = e;
|
||
}
|
||
if (ImGui::MenuItem("Environment"))
|
||
{
|
||
// UE 新关卡环境集简化:Skybox(背景)+ SkyLight(环境光)一体
|
||
auto e = ctx.scene->CreateEntity("Environment");
|
||
e.AddComponent<Engine::ECS::SkyboxComponent>();
|
||
e.AddComponent<Engine::ECS::SkyLightComponent>();
|
||
selected = e;
|
||
}
|
||
ImGui::EndPopup();
|
||
}
|
||
ImGui::Separator();
|
||
|
||
auto& reg = ctx.scene->GetRegistry();
|
||
|
||
// 树形遍历:根 = 无有效父链接的实体(creation 序),子按挂载序递归
|
||
reg.view<Engine::ECS::NameComponent>().each(
|
||
[&](const entt::entity e, const Engine::ECS::NameComponent&)
|
||
{
|
||
if (ctx.scene->GetParent(e) != entt::null) return; // 非根
|
||
DrawEntityNode(ctx, e, selected);
|
||
});
|
||
|
||
// ---- 空白区 = 挂根落点(UE Outliner 拖到空白 = 顶层);
|
||
// Prefab 拖入 = 实例化到场景根(授权位姿)----
|
||
ImVec2 rem = ImGui::GetContentRegionAvail();
|
||
if (rem.y > 4.0f) {
|
||
ImGui::InvisibleButton("##hier_drop_root", rem);
|
||
if (ImGui::BeginDragDropTarget()) {
|
||
if (const ImGuiPayload* pl =
|
||
ImGui::AcceptDragDropPayload(kHierPayload)) {
|
||
u32 bits = 0;
|
||
std::memcpy(&bits, pl->Data, sizeof(bits));
|
||
ctx.scene->SetParent(static_cast<entt::entity>(bits),
|
||
entt::null);
|
||
}
|
||
if (const ImGuiPayload* pl =
|
||
ImGui::AcceptDragDropPayload(kPrefabDragPayload)) {
|
||
if (const entt::entity root = InstantiatePrefabDrop(
|
||
ctx, static_cast<const char*>(pl->Data));
|
||
root != entt::null) {
|
||
selected = root;
|
||
}
|
||
}
|
||
ImGui::EndDragDropTarget();
|
||
}
|
||
}
|
||
|
||
ImGui::End();
|
||
}
|