From 4bcd1055dd1fca224d0b4f18b3d17d072f16e407 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Thu, 12 Mar 2026 17:54:59 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84HierarchyPanel=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=EF=BC=9A=E5=8F=B3=E9=94=AE=E8=8F=9C=E5=8D=95=E3=80=81?= =?UTF-8?q?=E6=8B=96=E6=8B=BD=E6=8E=92=E5=BA=8F=E3=80=81=E6=90=9C=E7=B4=A2?= =?UTF-8?q?=E8=BF=87=E6=BB=A4=E3=80=81=E9=87=8D=E5=91=BD=E5=90=8D=E3=80=81?= =?UTF-8?q?=E5=BF=AB=E6=8D=B7=E9=94=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 右键菜单: - Create: Empty Object, Camera, Light, Cube, Sphere, Plane - Rename (F2), Delete (Del) - Copy (Ctrl+C), Paste (Ctrl+V), Duplicate (Ctrl+D) 拖拽排序: - 支持拖拽实体到另一个实体下成为子节点 - 自动防止循环父子关系 搜索过滤: - 顶部搜索框实时过滤实体 双击重命名: - 双击实体名称进入编辑模式 键盘快捷键: - Delete: 删除选中实体 - F2: 重命名 - Ctrl+C/V/D: 复制/粘贴/复制 --- ui/src/Managers/SceneManager.cpp | 180 +++++++++++++++++++ ui/src/Managers/SceneManager.h | 97 ++++------ ui/src/panels/HierarchyPanel.cpp | 297 +++++++++++++++++++++++++++++-- ui/src/panels/HierarchyPanel.h | 15 +- 4 files changed, 511 insertions(+), 78 deletions(-) diff --git a/ui/src/Managers/SceneManager.cpp b/ui/src/Managers/SceneManager.cpp index 47fdadf7..ba9d419d 100644 --- a/ui/src/Managers/SceneManager.cpp +++ b/ui/src/Managers/SceneManager.cpp @@ -1,5 +1,185 @@ #include "SceneManager.h" +#include namespace UI { +EntityID SceneManager::CreateEntity(const std::string& name, EntityID parent) { + EntityID id = m_nextEntityId++; + Entity entity; + entity.id = id; + entity.name = name; + entity.parent = parent; + m_entities[id] = std::move(entity); + + if (parent != INVALID_ENTITY) { + m_entities[parent].children.push_back(id); + } else { + m_rootEntities.push_back(id); + } + + OnEntityCreated.Invoke(id); + return id; +} + +void SceneManager::DeleteEntity(EntityID id) { + auto it = m_entities.find(id); + if (it == m_entities.end()) return; + + Entity& entity = it->second; + + std::vector childrenToDelete = entity.children; + for (EntityID childId : childrenToDelete) { + DeleteEntity(childId); + } + + if (entity.parent != INVALID_ENTITY) { + auto* parent = GetEntity(entity.parent); + if (parent) { + auto& siblings = parent->children; + siblings.erase(std::remove(siblings.begin(), siblings.end(), id), siblings.end()); + } + } else { + m_rootEntities.erase(std::remove(m_rootEntities.begin(), m_rootEntities.end(), id), m_rootEntities.end()); + } + + if (SelectionManager::Get().GetSelectedEntity() == id) { + SelectionManager::Get().ClearSelection(); + } + + m_entities.erase(it); + OnEntityDeleted.Invoke(id); +} + +ClipboardData SceneManager::CopyEntityRecursive(const Entity* entity) { + ClipboardData data; + data.name = entity->name; + + for (const auto& comp : entity->components) { + if (auto* transform = dynamic_cast(comp.get())) { + auto newComp = std::make_unique(); + memcpy(newComp->position, transform->position, sizeof(transform->position)); + memcpy(newComp->rotation, transform->rotation, sizeof(transform->rotation)); + memcpy(newComp->scale, transform->scale, sizeof(transform->scale)); + data.components.push_back(std::move(newComp)); + } + else if (auto* meshRenderer = dynamic_cast(comp.get())) { + auto newComp = std::make_unique(); + newComp->materialName = meshRenderer->materialName; + newComp->meshName = meshRenderer->meshName; + data.components.push_back(std::move(newComp)); + } + } + + for (EntityID childId : entity->children) { + const Entity* child = GetEntity(childId); + if (child) { + data.children.push_back(CopyEntityRecursive(child)); + } + } + + return data; +} + +void SceneManager::CopyEntity(EntityID id) { + const Entity* entity = GetEntity(id); + if (!entity) return; + + m_clipboard = CopyEntityRecursive(entity); +} + +EntityID SceneManager::PasteEntityRecursive(const ClipboardData& data, EntityID parent) { + EntityID newId = CreateEntity(data.name, parent); + Entity* newEntity = GetEntity(newId); + + if (newEntity) { + newEntity->components.clear(); + for (const auto& comp : data.components) { + if (auto* transform = dynamic_cast(comp.get())) { + auto newComp = std::make_unique(); + memcpy(newComp->position, transform->position, sizeof(transform->position)); + memcpy(newComp->rotation, transform->rotation, sizeof(transform->rotation)); + memcpy(newComp->scale, transform->scale, sizeof(transform->scale)); + newEntity->components.push_back(std::move(newComp)); + } + else if (auto* meshRenderer = dynamic_cast(comp.get())) { + auto newComp = std::make_unique(); + newComp->materialName = meshRenderer->materialName; + newComp->meshName = meshRenderer->meshName; + newEntity->components.push_back(std::move(newComp)); + } + } + } + + for (const auto& childData : data.children) { + PasteEntityRecursive(childData, newId); + } + + return newId; +} + +EntityID SceneManager::PasteEntity(EntityID parent) { + if (!m_clipboard) return INVALID_ENTITY; + return PasteEntityRecursive(*m_clipboard, parent); +} + +EntityID SceneManager::DuplicateEntity(EntityID id) { + CopyEntity(id); + const Entity* entity = GetEntity(id); + if (!entity) return INVALID_ENTITY; + return PasteEntity(entity->parent); +} + +void SceneManager::MoveEntity(EntityID id, EntityID newParent) { + Entity* entity = GetEntity(id); + if (!entity || id == newParent) return; + + if (entity->parent != INVALID_ENTITY) { + Entity* oldParent = GetEntity(entity->parent); + if (oldParent) { + auto& siblings = oldParent->children; + siblings.erase(std::remove(siblings.begin(), siblings.end(), id), siblings.end()); + } + } else { + m_rootEntities.erase(std::remove(m_rootEntities.begin(), m_rootEntities.end(), id), m_rootEntities.end()); + } + + entity->parent = newParent; + + if (newParent != INVALID_ENTITY) { + Entity* newParentEntity = GetEntity(newParent); + if (newParentEntity) { + newParentEntity->children.push_back(id); + } + } else { + m_rootEntities.push_back(id); + } + + OnEntityChanged.Invoke(id); +} + +void SceneManager::CreateDemoScene() { + m_entities.clear(); + m_rootEntities.clear(); + m_nextEntityId = 1; + m_clipboard.reset(); + + EntityID camera = CreateEntity("Main Camera"); + GetEntity(camera)->AddComponent(); + + EntityID light = CreateEntity("Directional Light"); + + EntityID cube = CreateEntity("Cube"); + GetEntity(cube)->AddComponent(); + GetEntity(cube)->AddComponent()->meshName = "Cube Mesh"; + + EntityID sphere = CreateEntity("Sphere"); + GetEntity(sphere)->AddComponent(); + GetEntity(sphere)->AddComponent()->meshName = "Sphere Mesh"; + + EntityID player = CreateEntity("Player"); + EntityID weapon = CreateEntity("Weapon", player); + + OnSceneChanged.Invoke(); +} + } \ No newline at end of file diff --git a/ui/src/Managers/SceneManager.h b/ui/src/Managers/SceneManager.h index 2c19bed0..49986e59 100644 --- a/ui/src/Managers/SceneManager.h +++ b/ui/src/Managers/SceneManager.h @@ -4,9 +4,17 @@ #include "SelectionManager.h" #include #include +#include +#include namespace UI { +struct ClipboardData { + std::string name; + std::vector> components; + std::vector children; +}; + class SceneManager { public: static SceneManager& Get() { @@ -14,23 +22,7 @@ public: return instance; } - EntityID CreateEntity(const std::string& name, EntityID parent = INVALID_ENTITY) { - EntityID id = m_nextEntityId++; - Entity entity; - entity.id = id; - entity.name = name; - entity.parent = parent; - m_entities[id] = std::move(entity); - - if (parent != INVALID_ENTITY) { - m_entities[parent].children.push_back(id); - } else { - m_rootEntities.push_back(id); - } - - OnEntityCreated.Invoke(id); - return id; - } + EntityID CreateEntity(const std::string& name, EntityID parent = INVALID_ENTITY); Entity* GetEntity(EntityID id) { auto it = m_entities.find(id); @@ -52,68 +44,43 @@ public: return m_rootEntities; } - void DeleteEntity(EntityID id) { - auto it = m_entities.find(id); - if (it == m_entities.end()) return; - - Entity& entity = it->second; - - for (EntityID childId : entity.children) { - DeleteEntity(childId); + void DeleteEntity(EntityID id); + + void RenameEntity(EntityID id, const std::string& newName) { + auto* entity = GetEntity(id); + if (entity) { + entity->name = newName; + OnEntityChanged.Invoke(id); } - - if (entity.parent != INVALID_ENTITY) { - auto* parent = GetEntity(entity.parent); - if (parent) { - auto& siblings = parent->children; - siblings.erase(std::remove(siblings.begin(), siblings.end(), id), siblings.end()); - } - } else { - m_rootEntities.erase(std::remove(m_rootEntities.begin(), m_rootEntities.end(), id), m_rootEntities.end()); - } - - if (SelectionManager::Get().GetSelectedEntity() == id) { - SelectionManager::Get().ClearSelection(); - } - - m_entities.erase(it); - OnEntityDeleted.Invoke(id); } - void CreateDemoScene() { - m_entities.clear(); - m_rootEntities.clear(); - m_nextEntityId = 1; - - EntityID camera = CreateEntity("Main Camera"); - GetEntity(camera)->AddComponent(); - - EntityID light = CreateEntity("Directional Light"); - - EntityID cube = CreateEntity("Cube"); - GetEntity(cube)->AddComponent(); - GetEntity(cube)->AddComponent()->meshName = "Cube Mesh"; - - EntityID sphere = CreateEntity("Sphere"); - GetEntity(sphere)->AddComponent(); - GetEntity(sphere)->AddComponent()->meshName = "Sphere Mesh"; - - EntityID player = CreateEntity("Player"); - EntityID weapon = CreateEntity("Weapon", player); - - OnSceneChanged.Invoke(); - } + void CopyEntity(EntityID id); + + EntityID PasteEntity(EntityID parent = INVALID_ENTITY); + + EntityID DuplicateEntity(EntityID id); + + void MoveEntity(EntityID id, EntityID newParent); + + void CreateDemoScene(); + + bool HasClipboardData() const { return m_clipboard.has_value(); } Event OnEntityCreated; Event OnEntityDeleted; + Event OnEntityChanged; Event<> OnSceneChanged; private: SceneManager() = default; + ClipboardData CopyEntityRecursive(const Entity* entity); + EntityID PasteEntityRecursive(const ClipboardData& data, EntityID parent); + EntityID m_nextEntityId = 1; std::unordered_map m_entities; std::vector m_rootEntities; + std::optional m_clipboard; }; } \ No newline at end of file diff --git a/ui/src/panels/HierarchyPanel.cpp b/ui/src/panels/HierarchyPanel.cpp index 53dbd61f..5cb2b05d 100644 --- a/ui/src/panels/HierarchyPanel.cpp +++ b/ui/src/panels/HierarchyPanel.cpp @@ -2,6 +2,7 @@ #include "Managers/SceneManager.h" #include "Managers/SelectionManager.h" #include +#include namespace UI { @@ -19,18 +20,52 @@ HierarchyPanel::~HierarchyPanel() { void HierarchyPanel::Render() { ImGui::Begin(m_name.c_str(), &m_isOpen, ImGuiWindowFlags_None); - for (EntityID id : SceneManager::Get().GetRootEntities()) { - RenderEntity(id); + RenderSearchBar(); + + ImGui::Separator(); + + HandleKeyboardShortcuts(); + + std::string filter = m_searchBuffer; + + if (ImGui::BeginPopupContextWindow("HierarchyContextMenu", ImGuiPopupFlags_MouseButtonRight | ImGuiPopupFlags_NoOpenOverItems)) { + RenderCreateMenu(INVALID_ENTITY); + ImGui::EndPopup(); } + ImGui::BeginChild("EntityList"); + + for (EntityID id : SceneManager::Get().GetRootEntities()) { + RenderEntity(id, filter); + } + + if (ImGui::IsWindowHovered() && ImGui::IsMouseDown(0) && !ImGui::IsAnyItemHovered()) { + if (!m_renaming) { + SelectionManager::Get().ClearSelection(); + } + } + + ImGui::EndChild(); + ImGui::End(); } -void HierarchyPanel::RenderEntity(EntityID id) { +void HierarchyPanel::RenderSearchBar() { + ImGui::SetNextItemWidth(-1); + ImGui::InputTextWithHint("##Search", "Search...", m_searchBuffer, sizeof(m_searchBuffer)); +} + +void HierarchyPanel::RenderEntity(EntityID id, const std::string& filter) { auto& sceneManager = SceneManager::Get(); - const Entity* entity = sceneManager.GetEntity(id); + Entity* entity = sceneManager.GetEntity(id); if (!entity) return; + if (!filter.empty() && !PassesFilter(id, filter)) { + return; + } + + ImGui::PushID(static_cast(id)); + ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_SpanAvailWidth; if (entity->children.empty()) { @@ -41,18 +76,256 @@ void HierarchyPanel::RenderEntity(EntityID id) { flags |= ImGuiTreeNodeFlags_Selected; } - bool isOpen = ImGui::TreeNodeEx(entity->name.c_str(), flags); - - if (ImGui::IsItemClicked() && !ImGui::IsItemToggledOpen()) { - SelectionManager::Get().SetSelectedEntity(id); + if (m_renaming && m_renamingEntity == id) { + if (m_renameJustStarted) { + ImGui::SetKeyboardFocusHere(); + m_renameJustStarted = false; + } + + ImGui::SetNextItemWidth(-1); + if (ImGui::InputText("##Rename", m_renameBuffer, sizeof(m_renameBuffer), ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_AutoSelectAll)) { + if (strlen(m_renameBuffer) > 0) { + sceneManager.RenameEntity(id, m_renameBuffer); + } + m_renaming = false; + m_renamingEntity = INVALID_ENTITY; + } + + if (!ImGui::IsItemActive() && ImGui::IsMouseClicked(0)) { + if (strlen(m_renameBuffer) > 0) { + sceneManager.RenameEntity(id, m_renameBuffer); + } + m_renaming = false; + m_renamingEntity = INVALID_ENTITY; + } + } else { + bool isOpen = ImGui::TreeNodeEx(entity->name.c_str(), flags); + + if (ImGui::IsItemClicked() && !ImGui::IsItemToggledOpen()) { + SelectionManager::Get().SetSelectedEntity(id); + } + + if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0)) { + m_renaming = true; + m_renamingEntity = id; + strcpy_s(m_renameBuffer, entity->name.c_str()); + m_renameJustStarted = true; + } + + HandleDragDrop(id); + + if (ImGui::BeginPopupContextItem("EntityContextMenu")) { + RenderContextMenu(id); + ImGui::EndPopup(); + } + + if (isOpen) { + for (EntityID childId : entity->children) { + RenderEntity(childId, filter); + } + ImGui::TreePop(); + } } - if (isOpen) { - for (EntityID childId : entity->children) { - RenderEntity(childId); + ImGui::PopID(); +} + +void HierarchyPanel::RenderContextMenu(EntityID id) { + auto& sceneManager = SceneManager::Get(); + auto& selectionManager = SelectionManager::Get(); + + if (ImGui::BeginMenu("Create")) { + RenderCreateMenu(id); + ImGui::EndMenu(); + } + + ImGui::Separator(); + + if (ImGui::MenuItem("Rename", "F2")) { + const Entity* entity = sceneManager.GetEntity(id); + if (entity) { + m_renaming = true; + m_renamingEntity = id; + strcpy_s(m_renameBuffer, entity->name.c_str()); + m_renameJustStarted = true; + } + } + + if (ImGui::MenuItem("Delete", "Delete")) { + sceneManager.DeleteEntity(id); + } + + ImGui::Separator(); + + if (ImGui::MenuItem("Copy", "Ctrl+C")) { + sceneManager.CopyEntity(id); + } + + if (ImGui::MenuItem("Paste", "Ctrl+V", false, sceneManager.HasClipboardData())) { + sceneManager.PasteEntity(id); + } + + if (ImGui::MenuItem("Duplicate", "Ctrl+D")) { + EntityID newId = sceneManager.DuplicateEntity(id); + if (newId != INVALID_ENTITY) { + selectionManager.SetSelectedEntity(newId); } - ImGui::TreePop(); } } +void HierarchyPanel::RenderCreateMenu(EntityID parent) { + auto& sceneManager = SceneManager::Get(); + auto& selectionManager = SelectionManager::Get(); + + if (ImGui::MenuItem("Empty Object")) { + EntityID newId = sceneManager.CreateEntity("GameObject", parent); + selectionManager.SetSelectedEntity(newId); + } + + ImGui::Separator(); + + if (ImGui::MenuItem("Camera")) { + EntityID newId = sceneManager.CreateEntity("Camera", parent); + sceneManager.GetEntity(newId)->AddComponent(); + selectionManager.SetSelectedEntity(newId); + } + + if (ImGui::MenuItem("Light")) { + EntityID newId = sceneManager.CreateEntity("Light", parent); + selectionManager.SetSelectedEntity(newId); + } + + ImGui::Separator(); + + if (ImGui::MenuItem("Cube")) { + EntityID newId = sceneManager.CreateEntity("Cube", parent); + sceneManager.GetEntity(newId)->AddComponent(); + sceneManager.GetEntity(newId)->AddComponent()->meshName = "Cube"; + selectionManager.SetSelectedEntity(newId); + } + + if (ImGui::MenuItem("Sphere")) { + EntityID newId = sceneManager.CreateEntity("Sphere", parent); + sceneManager.GetEntity(newId)->AddComponent(); + sceneManager.GetEntity(newId)->AddComponent()->meshName = "Sphere"; + selectionManager.SetSelectedEntity(newId); + } + + if (ImGui::MenuItem("Plane")) { + EntityID newId = sceneManager.CreateEntity("Plane", parent); + sceneManager.GetEntity(newId)->AddComponent(); + sceneManager.GetEntity(newId)->AddComponent()->meshName = "Plane"; + selectionManager.SetSelectedEntity(newId); + } +} + +void HierarchyPanel::HandleDragDrop(EntityID id) { + auto& sceneManager = SceneManager::Get(); + + if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_None)) { + m_dragSource = id; + ImGui::SetDragDropPayload("ENTITY_ID", &id, sizeof(EntityID)); + const Entity* entity = sceneManager.GetEntity(id); + if (entity) { + ImGui::Text("%s", entity->name.c_str()); + } + ImGui::EndDragDropSource(); + } + + if (ImGui::BeginDragDropTarget()) { + if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("ENTITY_ID")) { + EntityID sourceId = *(const EntityID*)payload->Data; + if (sourceId != id && sourceId != INVALID_ENTITY) { + const Entity* targetEntity = sceneManager.GetEntity(id); + const Entity* sourceEntity = sceneManager.GetEntity(sourceId); + + bool isValidMove = true; + EntityID checkParent = targetEntity ? targetEntity->parent : INVALID_ENTITY; + while (checkParent != INVALID_ENTITY) { + if (checkParent == sourceId) { + isValidMove = false; + break; + } + const Entity* parentEntity = sceneManager.GetEntity(checkParent); + checkParent = parentEntity ? parentEntity->parent : INVALID_ENTITY; + } + + if (isValidMove && sourceEntity && sourceEntity->parent != id) { + sceneManager.MoveEntity(sourceId, id); + } + } + } + ImGui::EndDragDropTarget(); + } +} + +void HierarchyPanel::HandleKeyboardShortcuts() { + auto& sceneManager = SceneManager::Get(); + auto& selectionManager = SelectionManager::Get(); + + EntityID selectedId = selectionManager.GetSelectedEntity(); + + if (ImGui::IsWindowFocused()) { + if (ImGui::IsKeyPressed(ImGuiKey_Delete)) { + if (selectedId != INVALID_ENTITY) { + sceneManager.DeleteEntity(selectedId); + } + } + + if (ImGui::IsKeyPressed(ImGuiKey_F2)) { + if (selectedId != INVALID_ENTITY) { + const Entity* entity = sceneManager.GetEntity(selectedId); + if (entity) { + m_renaming = true; + m_renamingEntity = selectedId; + strcpy_s(m_renameBuffer, entity->name.c_str()); + m_renameJustStarted = true; + } + } + } + + ImGuiIO& io = ImGui::GetIO(); + if (io.KeyCtrl) { + if (ImGui::IsKeyPressed(ImGuiKey_C)) { + if (selectedId != INVALID_ENTITY) { + sceneManager.CopyEntity(selectedId); + } + } + + if (ImGui::IsKeyPressed(ImGuiKey_V)) { + if (sceneManager.HasClipboardData()) { + sceneManager.PasteEntity(selectedId); + } + } + + if (ImGui::IsKeyPressed(ImGuiKey_D)) { + if (selectedId != INVALID_ENTITY) { + EntityID newId = sceneManager.DuplicateEntity(selectedId); + if (newId != INVALID_ENTITY) { + selectionManager.SetSelectedEntity(newId); + } + } + } + } + } +} + +bool HierarchyPanel::PassesFilter(EntityID id, const std::string& filter) { + auto& sceneManager = SceneManager::Get(); + const Entity* entity = sceneManager.GetEntity(id); + if (!entity) return false; + + if (entity->name.find(filter) != std::string::npos) { + return true; + } + + for (EntityID childId : entity->children) { + if (PassesFilter(childId, filter)) { + return true; + } + } + + return false; +} + } \ No newline at end of file diff --git a/ui/src/panels/HierarchyPanel.h b/ui/src/panels/HierarchyPanel.h index 3c9c6a0d..2b1ab3ed 100644 --- a/ui/src/panels/HierarchyPanel.h +++ b/ui/src/panels/HierarchyPanel.h @@ -14,9 +14,22 @@ public: void Render() override; private: - void RenderEntity(EntityID id); + void RenderSearchBar(); + void RenderEntity(EntityID id, const std::string& filter); + void RenderContextMenu(EntityID id); + void RenderCreateMenu(EntityID parent); + void HandleDragDrop(EntityID id); + void HandleKeyboardShortcuts(); + bool PassesFilter(EntityID id, const std::string& filter); Event::HandlerID m_selectionHandlerId = 0; + + char m_searchBuffer[256] = ""; + bool m_renaming = false; + EntityID m_renamingEntity = INVALID_ENTITY; + char m_renameBuffer[256] = ""; + bool m_renameJustStarted = false; + EntityID m_dragSource = INVALID_ENTITY; }; } \ No newline at end of file