Files
HelloVulkan/App/editor/panels/HierarchyPanel.cpp
T

323 lines
13 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.
//
// 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);
}
// 模型拖入落点:创建带 MeshRenderer 的单实体(名字 = 文件名
// stem,默认位姿;模型句柄 = LoadModel 磁盘路径)。拖到节点下 =
// SetParent keepWorld;空白区 = 场景根。加载失败不建空实体
entt::entity SpawnModelDrop(const EditorContext& ctx, const char* path)
{
if (!ctx.scene || !ctx.assets || !path) {
return entt::null;
}
const auto model = ctx.assets->LoadModel(path);
if (!model.Valid()) {
return entt::null;
}
std::string name =
std::filesystem::path(path).stem().string();
if (name.empty()) {
name = "model entity";
}
auto e = ctx.scene->CreateEntity(name);
e.AddComponent<Engine::ECS::MeshRendererComponent>().model = model;
ENGINE_INFO("Hierarchy: model entity '{}' created from '{}'",
name, path);
return static_cast<entt::entity>(e);
}
// 实体名 → 合法文件名(路径分隔符/保留符净化;空名兜底)
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;
}
}
if (const ImGuiPayload* pl =
ImGui::AcceptDragDropPayload(kModelDragPayload)) {
if (const entt::entity me = SpawnModelDrop(
ctx, static_cast<const char*>(pl->Data));
me != entt::null) {
ctx.scene->SetParent(me, e); // keepWorld 默认 true
selected = me;
}
}
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;
}
}
if (const ImGuiPayload* pl =
ImGui::AcceptDragDropPayload(kModelDragPayload)) {
if (const entt::entity me = SpawnModelDrop(
ctx, static_cast<const char*>(pl->Data));
me != entt::null) {
selected = me;
}
}
ImGui::EndDragDropTarget();
}
}
ImGui::End();
}