diff --git a/CMakeLists.txt b/CMakeLists.txt index 10cab2e..d1d2864 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -153,6 +153,7 @@ target_compile_options(MetaCoreRender PRIVATE ${METACORE_COMMON_WARNINGS}) set(METACORE_EDITOR_HEADERS Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorApp.h + Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorCommandService.h Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorContext.h Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorModule.h ) @@ -167,6 +168,7 @@ set(METACORE_EDITOR_SOURCES Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp Source/MetaCoreEditor/Private/MetaCoreEditorCameraController.cpp + Source/MetaCoreEditor/Private/MetaCoreEditorCommandService.cpp Source/MetaCoreEditor/Private/MetaCoreEditorContext.cpp Source/MetaCoreEditor/Private/MetaCoreEditorModule.cpp third_party/ImGuizmo/ImGuizmo.cpp diff --git a/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp b/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp index a09691b..076423d 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp @@ -2,11 +2,7 @@ #include "MetaCoreEditor/MetaCoreEditorContext.h" #include "MetaCoreEditorCameraController.h" -#include "MetaCorePlatform/MetaCoreInput.h" #include "MetaCorePlatform/MetaCoreWindow.h" -#include "MetaCoreRender/MetaCoreEditorViewportRenderer.h" -#include "MetaCoreRender/MetaCoreRenderTypes.h" -#include "MetaCoreScene/MetaCoreScene.h" #include @@ -15,40 +11,227 @@ #include #include #include +#include #include +#include #include namespace MetaCore { namespace { -void MetaCoreDrawHierarchyNode(MetaCoreEditorContext& editorContext, MetaCoreId objectId) { - MetaCoreScene& scene = editorContext.GetScene(); - MetaCoreGameObject* gameObject = scene.FindGameObject(objectId); - if (gameObject == nullptr) { +[[nodiscard]] bool MetaCoreRenameObject(MetaCoreEditorContext& editorContext, MetaCoreId objectId, const std::string& name) { + if (objectId == 0 || name.empty()) { + return false; + } + + const bool renamed = editorContext.ExecuteSnapshotCommand("重命名对象", [&]() { + return editorContext.GetScene().RenameGameObject(objectId, name); + }); + + if (renamed) { + editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "Hierarchy", "已重命名对象: " + name); + } + return renamed; +} + +[[nodiscard]] std::optional MetaCoreCreateObject( + MetaCoreEditorContext& editorContext, + const std::string& objectName, + bool withMeshRenderer, + std::optional forcedParentId +) { + MetaCoreId createdObjectId = 0; + const bool created = editorContext.ExecuteSnapshotCommand("创建对象", [&]() { + MetaCoreId parentId = forcedParentId.value_or(editorContext.GetActiveObjectId()); + if (parentId != 0 && editorContext.GetScene().FindGameObject(parentId) == nullptr) { + parentId = 0; + } + + MetaCoreGameObject& object = editorContext.GetScene().CreateGameObject(objectName, parentId); + if (withMeshRenderer) { + object.MeshRenderer = MetaCoreMeshRendererComponent{}; + object.Transform.Position = glm::vec3(0.0F, 0.5F, 0.0F); + } + + createdObjectId = object.Id; + editorContext.SelectOnly(createdObjectId); + editorContext.SetSelectionAnchorId(createdObjectId); + return true; + }); + + if (!created || createdObjectId == 0) { + return std::nullopt; + } + + editorContext.AddConsoleMessage( + MetaCoreLogLevel::Info, + "Scene", + withMeshRenderer ? "已创建立方体对象" : "已创建空对象" + ); + return createdObjectId; +} + +[[nodiscard]] bool MetaCoreDuplicateSelection(MetaCoreEditorContext& editorContext) { + const std::vector selectedIds = editorContext.GetSelectedObjectIds(); + if (selectedIds.empty()) { + return false; + } + + std::vector duplicatedRootIds; + const bool duplicated = editorContext.ExecuteSnapshotCommand("复制对象", [&]() { + duplicatedRootIds = editorContext.GetScene().DuplicateGameObjects(selectedIds); + if (duplicatedRootIds.empty()) { + return false; + } + + editorContext.SetSelection(duplicatedRootIds, duplicatedRootIds.back()); + return true; + }); + + if (duplicated) { + editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "Scene", "已复制对象"); + } + return duplicated; +} + +[[nodiscard]] bool MetaCoreDeleteSelection(MetaCoreEditorContext& editorContext) { + const std::vector selectedIds = editorContext.GetSelectedObjectIds(); + if (selectedIds.empty()) { + return false; + } + + std::vector deletedIds; + const bool deleted = editorContext.ExecuteSnapshotCommand("删除对象", [&]() { + deletedIds = editorContext.GetScene().DeleteGameObjects(selectedIds); + if (deletedIds.empty()) { + return false; + } + + editorContext.ClearSelection(); + return true; + }); + + if (deleted) { + editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "Scene", "已删除对象"); + } + return deleted; +} + +[[nodiscard]] bool MetaCoreReparentSelection(MetaCoreEditorContext& editorContext, MetaCoreId newParentId) { + const std::vector selectedIds = editorContext.GetSelectedObjectIds(); + if (selectedIds.empty()) { + return false; + } + + const bool keepWorldTransform = editorContext.GetReparentTransformRule() == MetaCoreReparentTransformRule::KeepWorld; + const bool reparented = editorContext.ExecuteSnapshotCommand("重挂接对象", [&]() { + return editorContext.GetScene().ReparentGameObjects(selectedIds, newParentId, keepWorldTransform); + }); + + if (reparented) { + editorContext.AddConsoleMessage( + MetaCoreLogLevel::Info, + "Hierarchy", + keepWorldTransform ? "已重挂接对象(保持世界变换)" : "已重挂接对象(保持局部变换)" + ); + } + return reparented; +} + +void MetaCoreHandleUndo(MetaCoreEditorContext& editorContext) { + if (editorContext.UndoCommand()) { + editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "Editor", "已撤销操作"); + } +} + +void MetaCoreHandleRedo(MetaCoreEditorContext& editorContext) { + if (editorContext.RedoCommand()) { + editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "Editor", "已重做操作"); + } +} + +void MetaCoreHandleGlobalEditorShortcuts(MetaCoreEditorContext& editorContext) { + const ImGuiIO& io = ImGui::GetIO(); + if (io.WantTextInput) { return; } - const std::vector childIds = scene.GetChildrenOf(objectId); - ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_SpanFullWidth; - if (childIds.empty()) { - flags |= ImGuiTreeNodeFlags_Leaf; - } - if (editorContext.GetSelectedObjectId() == objectId) { - flags |= ImGuiTreeNodeFlags_Selected; + const bool ctrlDown = io.KeyCtrl; + const bool shiftDown = io.KeyShift; + + if (ctrlDown && shiftDown && ImGui::IsKeyPressed(ImGuiKey_Z, false)) { + MetaCoreHandleRedo(editorContext); + return; } - const bool nodeOpened = ImGui::TreeNodeEx(reinterpret_cast(static_cast(objectId)), flags, "%s", gameObject->Name.c_str()); - if (ImGui::IsItemClicked()) { - editorContext.SetSelectedObjectId(objectId); - editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "Hierarchy", "已选中对象: " + gameObject->Name); + if (ctrlDown && ImGui::IsKeyPressed(ImGuiKey_Z, false)) { + MetaCoreHandleUndo(editorContext); + return; } - if (nodeOpened) { - for (MetaCoreId childId : childIds) { - MetaCoreDrawHierarchyNode(editorContext, childId); + if (ctrlDown && ImGui::IsKeyPressed(ImGuiKey_Y, false)) { + MetaCoreHandleRedo(editorContext); + return; + } + + if (ctrlDown && ImGui::IsKeyPressed(ImGuiKey_D, false)) { + (void)MetaCoreDuplicateSelection(editorContext); + return; + } + + if (ImGui::IsKeyPressed(ImGuiKey_Delete, false)) { + (void)MetaCoreDeleteSelection(editorContext); + return; + } + + if (ImGui::IsKeyPressed(ImGuiKey_F2, false)) { + editorContext.RequestRenameActiveObject(); + } +} + +void MetaCoreApplyHierarchySelection(MetaCoreEditorContext& editorContext, MetaCoreId objectId, bool allowToggle) { + if (objectId == 0 || editorContext.GetScene().FindGameObject(objectId) == nullptr) { + return; + } + + const ImGuiIO& io = ImGui::GetIO(); + if (io.KeyShift) { + editorContext.SelectRangeByOrderedIds(editorContext.GetScene().BuildHierarchyPreorder(), objectId, io.KeyCtrl); + return; + } + + if (allowToggle && io.KeyCtrl) { + editorContext.ToggleSelection(objectId); + return; + } + + editorContext.SelectOnly(objectId); +} + +std::unordered_map& MetaCoreGetInspectorEditSnapshots() { + static std::unordered_map snapshots; + return snapshots; +} + +void MetaCoreTrackInspectorEdit(MetaCoreEditorContext& editorContext, const char* commandLabel, bool allowMerge) { + const ImGuiID itemId = ImGui::GetItemID(); + if (itemId == 0) { + return; + } + + auto& snapshots = MetaCoreGetInspectorEditSnapshots(); + if (ImGui::IsItemActivated()) { + snapshots[itemId] = editorContext.CaptureStateSnapshot(); + } + + if (ImGui::IsItemDeactivatedAfterEdit()) { + const auto iterator = snapshots.find(itemId); + if (iterator != snapshots.end()) { + const MetaCoreEditorStateSnapshot afterSnapshot = editorContext.CaptureStateSnapshot(); + (void)editorContext.CommitStateTransition(commandLabel, iterator->second, afterSnapshot, allowMerge); + snapshots.erase(iterator); } - ImGui::TreePop(); } } @@ -57,6 +240,8 @@ public: std::string GetProviderId() const override { return "MetaCoreDefaultMenuProvider"; } void DrawMenuBar(MetaCoreEditorContext& editorContext) override { + MetaCoreHandleGlobalEditorShortcuts(editorContext); + if (ImGui::BeginMenu("文件")) { if (ImGui::MenuItem("重置布局")) { editorContext.SetDockLayoutBuilt(false); @@ -68,8 +253,22 @@ public: } if (ImGui::BeginMenu("编辑")) { - ImGui::MenuItem("撤销", "Ctrl+Z", false, false); - ImGui::MenuItem("重做", "Ctrl+Y", false, false); + if (ImGui::MenuItem("撤销", "Ctrl+Z", false, editorContext.GetCommandService().CanUndo())) { + MetaCoreHandleUndo(editorContext); + } + if (ImGui::MenuItem("重做", "Ctrl+Y / Ctrl+Shift+Z", false, editorContext.GetCommandService().CanRedo())) { + MetaCoreHandleRedo(editorContext); + } + ImGui::Separator(); + if (ImGui::MenuItem("复制", "Ctrl+D", false, !editorContext.GetSelectedObjectIds().empty())) { + (void)MetaCoreDuplicateSelection(editorContext); + } + if (ImGui::MenuItem("删除", "Delete", false, !editorContext.GetSelectedObjectIds().empty())) { + (void)MetaCoreDeleteSelection(editorContext); + } + if (ImGui::MenuItem("重命名", "F2", false, editorContext.GetActiveObjectId() != 0)) { + editorContext.RequestRenameActiveObject(); + } ImGui::EndMenu(); } @@ -80,16 +279,17 @@ public: if (ImGui::BeginMenu("游戏对象")) { if (ImGui::MenuItem("创建空对象")) { - MetaCoreGameObject& object = editorContext.GetScene().CreateGameObject("GameObject"); - editorContext.SetSelectedObjectId(object.Id); - editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "Scene", "已创建空对象"); + (void)MetaCoreCreateObject(editorContext, "GameObject", false, std::nullopt); } if (ImGui::MenuItem("创建立方体")) { - MetaCoreGameObject& cube = editorContext.GetScene().CreateGameObject("Cube"); - cube.MeshRenderer = MetaCoreMeshRendererComponent{}; - cube.Transform.Position = glm::vec3(0.0F, 0.5F, 0.0F); - editorContext.SetSelectedObjectId(cube.Id); - editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "Scene", "已创建立方体"); + (void)MetaCoreCreateObject(editorContext, "Cube", true, std::nullopt); + } + ImGui::Separator(); + if (ImGui::MenuItem("复制", "Ctrl+D", false, !editorContext.GetSelectedObjectIds().empty())) { + (void)MetaCoreDuplicateSelection(editorContext); + } + if (ImGui::MenuItem("删除", "Delete", false, !editorContext.GetSelectedObjectIds().empty())) { + (void)MetaCoreDeleteSelection(editorContext); } ImGui::EndMenu(); } @@ -121,13 +321,229 @@ public: bool IsOpenByDefault() const override { return true; } void DrawPanel(MetaCoreEditorContext& editorContext) override { + if (const MetaCoreId requestedRenameId = editorContext.ConsumeRenameRequestObjectId(); requestedRenameId != 0) { + BeginRename(editorContext, requestedRenameId); + } + + DrawToolbar(editorContext); ImGui::TextUnformatted("SampleScene"); ImGui::Separator(); for (MetaCoreId rootId : editorContext.GetScene().GetRootObjectIds()) { - MetaCoreDrawHierarchyNode(editorContext, rootId); + DrawHierarchyNode(editorContext, rootId); + } + + DrawRootDropTarget(editorContext); + DrawWindowContextMenu(editorContext); + DrawRenamePopup(editorContext); + } + +private: + void DrawToolbar(MetaCoreEditorContext& editorContext) { + const bool keepWorld = editorContext.GetReparentTransformRule() == MetaCoreReparentTransformRule::KeepWorld; + if (ImGui::Button("保持世界", ImVec2(84.0F, 0.0F))) { + editorContext.SetReparentTransformRule(MetaCoreReparentTransformRule::KeepWorld); + } + ImGui::SameLine(); + if (ImGui::Button("保持局部", ImVec2(84.0F, 0.0F))) { + editorContext.SetReparentTransformRule(MetaCoreReparentTransformRule::KeepLocal); + } + ImGui::SameLine(); + ImGui::TextDisabled(keepWorld ? "当前: 世界" : "当前: 局部"); + ImGui::Separator(); + } + + void DrawHierarchyNode(MetaCoreEditorContext& editorContext, MetaCoreId objectId) { + MetaCoreScene& scene = editorContext.GetScene(); + MetaCoreGameObject* gameObject = scene.FindGameObject(objectId); + if (gameObject == nullptr) { + return; + } + + const std::vector childIds = scene.GetChildrenOf(objectId); + ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_SpanFullWidth; + if (childIds.empty()) { + flags |= ImGuiTreeNodeFlags_Leaf; + } + if (editorContext.IsObjectSelected(objectId)) { + flags |= ImGuiTreeNodeFlags_Selected; + } + + const bool nodeOpened = ImGui::TreeNodeEx(reinterpret_cast(static_cast(objectId)), flags, "%s", gameObject->Name.c_str()); + const bool leftClicked = ImGui::IsItemClicked(ImGuiMouseButton_Left); + const bool rightClicked = ImGui::IsItemClicked(ImGuiMouseButton_Right); + if ((leftClicked || rightClicked) && !ImGui::IsItemToggledOpen()) { + if (leftClicked) { + MetaCoreApplyHierarchySelection(editorContext, objectId, true); + } else if (!editorContext.IsObjectSelected(objectId)) { + editorContext.SelectOnly(objectId); + } + } + + if (ImGui::BeginDragDropSource()) { + if (!editorContext.IsObjectSelected(objectId)) { + editorContext.SelectOnly(objectId); + } + MetaCoreId payloadId = objectId; + ImGui::SetDragDropPayload("MC_HIERARCHY_OBJECT", &payloadId, sizeof(payloadId)); + ImGui::Text("拖拽 %zu 个对象", editorContext.GetSelectedObjectIds().size()); + ImGui::EndDragDropSource(); + } + + if (ImGui::BeginDragDropTarget()) { + if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("MC_HIERARCHY_OBJECT"); payload != nullptr) { + if (payload->DataSize == sizeof(MetaCoreId)) { + (void)MetaCoreReparentSelection(editorContext, objectId); + } + } + ImGui::EndDragDropTarget(); + } + + if (ImGui::BeginPopupContextItem()) { + if (!editorContext.IsObjectSelected(objectId)) { + editorContext.SelectOnly(objectId); + } + DrawNodeContextMenu(editorContext, objectId); + ImGui::EndPopup(); + } + + if (nodeOpened) { + for (MetaCoreId childId : childIds) { + DrawHierarchyNode(editorContext, childId); + } + ImGui::TreePop(); } } + + void DrawNodeContextMenu(MetaCoreEditorContext& editorContext, MetaCoreId objectId) { + if (ImGui::MenuItem("创建空对象")) { + (void)MetaCoreCreateObject(editorContext, "GameObject", false, objectId); + } + if (ImGui::MenuItem("创建立方体")) { + (void)MetaCoreCreateObject(editorContext, "Cube", true, objectId); + } + ImGui::Separator(); + if (ImGui::MenuItem("重命名", "F2", false, objectId != 0)) { + BeginRename(editorContext, objectId); + } + if (ImGui::MenuItem("复制", "Ctrl+D", false, !editorContext.GetSelectedObjectIds().empty())) { + (void)MetaCoreDuplicateSelection(editorContext); + } + if (ImGui::MenuItem("删除", "Delete", false, !editorContext.GetSelectedObjectIds().empty())) { + (void)MetaCoreDeleteSelection(editorContext); + } + } + + void DrawWindowContextMenu(MetaCoreEditorContext& editorContext) { + if (!ImGui::BeginPopupContextWindow("MetaCoreHierarchyWindowContext", ImGuiPopupFlags_NoOpenOverItems | ImGuiPopupFlags_MouseButtonRight)) { + return; + } + + if (ImGui::MenuItem("创建空对象")) { + (void)MetaCoreCreateObject(editorContext, "GameObject", false, 0); + } + if (ImGui::MenuItem("创建立方体")) { + (void)MetaCoreCreateObject(editorContext, "Cube", true, 0); + } + ImGui::Separator(); + if (ImGui::MenuItem("重命名", "F2", false, editorContext.GetActiveObjectId() != 0)) { + BeginRename(editorContext, editorContext.GetActiveObjectId()); + } + if (ImGui::MenuItem("复制", "Ctrl+D", false, !editorContext.GetSelectedObjectIds().empty())) { + (void)MetaCoreDuplicateSelection(editorContext); + } + if (ImGui::MenuItem("删除", "Delete", false, !editorContext.GetSelectedObjectIds().empty())) { + (void)MetaCoreDeleteSelection(editorContext); + } + + ImGui::EndPopup(); + } + + void DrawRootDropTarget(MetaCoreEditorContext& editorContext) { + ImGui::Separator(); + ImGui::TextDisabled("拖拽到下方设为根节点"); + const ImVec2 dropAreaMin = ImGui::GetCursorScreenPos(); + const ImVec2 dropAreaSize = ImVec2(ImGui::GetContentRegionAvail().x, 24.0F); + ImGui::InvisibleButton("MetaCoreHierarchyRootDrop", dropAreaSize); + const ImVec2 dropAreaMax = ImVec2(dropAreaMin.x + dropAreaSize.x, dropAreaMin.y + dropAreaSize.y); + ImGui::GetWindowDrawList()->AddRect( + dropAreaMin, + dropAreaMax, + IM_COL32(96, 106, 122, 180), + 2.0F + ); + + if (ImGui::BeginDragDropTarget()) { + if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("MC_HIERARCHY_OBJECT"); payload != nullptr) { + if (payload->DataSize == sizeof(MetaCoreId)) { + (void)MetaCoreReparentSelection(editorContext, 0); + } + } + ImGui::EndDragDropTarget(); + } + } + + void BeginRename(MetaCoreEditorContext& editorContext, MetaCoreId objectId) { + if (objectId == 0) { + return; + } + + const MetaCoreGameObject* object = editorContext.GetScene().FindGameObject(objectId); + if (object == nullptr) { + return; + } + + RenameObjectId_ = objectId; + RenameBuffer_.fill('\0'); + std::snprintf(RenameBuffer_.data(), RenameBuffer_.size(), "%s", object->Name.c_str()); + OpenRenamePopup_ = true; + } + + void DrawRenamePopup(MetaCoreEditorContext& editorContext) { + if (RenameObjectId_ == 0) { + return; + } + + if (OpenRenamePopup_) { + ImGui::OpenPopup("重命名对象##MetaCoreHierarchy"); + OpenRenamePopup_ = false; + } + + if (!ImGui::BeginPopupModal("重命名对象##MetaCoreHierarchy", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { + return; + } + + if (ImGui::IsWindowAppearing()) { + ImGui::SetKeyboardFocusHere(); + } + + const bool submittedByEnter = ImGui::InputText( + "名称", + RenameBuffer_.data(), + RenameBuffer_.size(), + ImGuiInputTextFlags_AutoSelectAll | ImGuiInputTextFlags_EnterReturnsTrue + ); + + bool closePopup = false; + if (submittedByEnter || ImGui::Button("确定", ImVec2(90.0F, 0.0F))) { + closePopup = MetaCoreRenameObject(editorContext, RenameObjectId_, RenameBuffer_.data()); + } + ImGui::SameLine(); + if (ImGui::Button("取消", ImVec2(90.0F, 0.0F))) { + closePopup = true; + } + + if (closePopup) { + RenameObjectId_ = 0; + ImGui::CloseCurrentPopup(); + } + + ImGui::EndPopup(); + } + + MetaCoreId RenameObjectId_ = 0; + std::array RenameBuffer_{}; + bool OpenRenamePopup_ = false; }; class MetaCoreScenePanelProvider final : public MetaCoreIEditorPanelProvider { @@ -187,11 +603,16 @@ public: std::string GetDrawerId() const override { return "Transform"; } bool Supports(const MetaCoreGameObject&) const override { return true; } - void DrawInspector(MetaCoreEditorContext&, MetaCoreGameObject& gameObject) override { + void DrawInspector(MetaCoreEditorContext& editorContext, MetaCoreGameObject& gameObject) override { if (ImGui::CollapsingHeader("Transform", ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::DragFloat3("Position", glm::value_ptr(gameObject.Transform.Position), 0.05F); + MetaCoreTrackInspectorEdit(editorContext, "修改检查器属性", true); + ImGui::DragFloat3("Rotation", glm::value_ptr(gameObject.Transform.RotationEulerDegrees), 0.5F); + MetaCoreTrackInspectorEdit(editorContext, "修改检查器属性", true); + ImGui::DragFloat3("Scale", glm::value_ptr(gameObject.Transform.Scale), 0.05F, 0.05F, 100.0F); + MetaCoreTrackInspectorEdit(editorContext, "修改检查器属性", true); } } }; @@ -203,9 +624,15 @@ public: bool IsOpenByDefault() const override { return true; } void DrawPanel(MetaCoreEditorContext& editorContext) override { + const std::size_t selectedCount = editorContext.GetSelectedObjectIds().size(); + if (selectedCount == 0) { + ImGui::TextUnformatted("当前没有选中对象。"); + return; + } + MetaCoreGameObject* selectedObject = editorContext.GetSelectedGameObject(); if (selectedObject == nullptr) { - ImGui::TextUnformatted("当前没有选中对象。"); + ImGui::TextUnformatted("当前没有可编辑的主对象。"); return; } @@ -215,9 +642,26 @@ public: std::snprintf(NameBuffer_.data(), NameBuffer_.size(), "%s", selectedObject->Name.c_str()); } + if (selectedCount > 1) { + ImGui::Text("已选中 %zu 个对象(当前编辑 Active 对象)", selectedCount); + ImGui::Separator(); + } + ImGui::Text("对象 ID: %llu", static_cast(selectedObject->Id)); - if (ImGui::InputText("名称", NameBuffer_.data(), NameBuffer_.size())) { - selectedObject->Name = NameBuffer_.data(); + bool renameSubmitted = ImGui::InputText( + "名称", + NameBuffer_.data(), + NameBuffer_.size(), + ImGuiInputTextFlags_EnterReturnsTrue + ); + if (ImGui::IsItemDeactivatedAfterEdit()) { + renameSubmitted = true; + } + if (renameSubmitted) { + if (!MetaCoreRenameObject(editorContext, selectedObject->Id, NameBuffer_.data())) { + NameBuffer_.fill('\0'); + std::snprintf(NameBuffer_.data(), NameBuffer_.size(), "%s", selectedObject->Name.c_str()); + } } if (ImGui::Button("聚焦对象")) { @@ -242,13 +686,16 @@ public: ImGui::Separator(); ImGui::TextUnformatted("Light"); ImGui::DragFloat("Intensity", &selectedObject->Light->Intensity, 0.05F, 0.0F, 8.0F); + MetaCoreTrackInspectorEdit(editorContext, "修改检查器属性", true); } if (selectedObject->MeshRenderer.has_value()) { ImGui::Separator(); ImGui::TextUnformatted("MeshRenderer"); ImGui::ColorEdit3("BaseColor", glm::value_ptr(selectedObject->MeshRenderer->BaseColor)); + MetaCoreTrackInspectorEdit(editorContext, "修改检查器属性", true); ImGui::Checkbox("Visible", &selectedObject->MeshRenderer->Visible); + MetaCoreTrackInspectorEdit(editorContext, "修改检查器属性", false); } } diff --git a/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp b/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp index e2248da..f447330 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); @@ -221,6 +222,30 @@ MetaCoreId MetaCorePickGameObjectFromViewport( const glm::vec3 rayOrigin(rayStartWorld); const glm::vec3 rayDirection = glm::normalize(glm::vec3(rayEndWorld - rayStartWorld)); + std::unordered_map worldMatrixCache; + const auto buildWorldMatrix = [&](const auto& self, MetaCoreId objectId) -> glm::mat4 { + if (objectId == 0) { + return glm::mat4(1.0F); + } + + if (const auto cacheIterator = worldMatrixCache.find(objectId); cacheIterator != worldMatrixCache.end()) { + return cacheIterator->second; + } + + const MetaCoreGameObject* object = scene.FindGameObject(objectId); + if (object == nullptr) { + return glm::mat4(1.0F); + } + + glm::mat4 worldMatrix = MetaCoreBuildTransformMatrix(object->Transform); + if (object->ParentId != 0) { + worldMatrix = self(self, object->ParentId) * worldMatrix; + } + + worldMatrixCache.emplace(objectId, worldMatrix); + return worldMatrix; + }; + MetaCoreId bestObjectId = 0; float bestHitDistance = std::numeric_limits::max(); @@ -229,7 +254,7 @@ MetaCoreId MetaCorePickGameObjectFromViewport( continue; } - const glm::mat4 worldMatrix = MetaCoreBuildTransformMatrix(gameObject.Transform); + const glm::mat4 worldMatrix = buildWorldMatrix(buildWorldMatrix, gameObject.Id); const glm::mat4 inverseWorldMatrix = glm::inverse(worldMatrix); const glm::vec3 localRayOrigin = glm::vec3(inverseWorldMatrix * glm::vec4(rayOrigin, 1.0F)); const glm::vec3 localRayDirection = glm::normalize(glm::vec3(inverseWorldMatrix * glm::vec4(rayDirection, 0.0F))); @@ -410,6 +435,10 @@ void MetaCoreEditorApp::ShutdownImGui() { } void MetaCoreEditorApp::DrawEditorFrame() { + static bool gizmoWasUsing = false; + static bool hasGizmoBeforeSnapshot = false; + static MetaCoreEditorStateSnapshot gizmoBeforeSnapshot{}; + const ImGuiViewport* mainViewport = ImGui::GetMainViewport(); ImGui::SetNextWindowPos(mainViewport->WorkPos); ImGui::SetNextWindowSize(mainViewport->WorkSize); @@ -610,6 +639,10 @@ void MetaCoreEditorApp::DrawEditorFrame() { gizmoHovering = ImGuizmo::IsOver(); gizmoUsing = ImGuizmo::IsUsing(); + if (gizmoUsing && !gizmoWasUsing) { + gizmoBeforeSnapshot = EditorContext_->CaptureStateSnapshot(); + hasGizmoBeforeSnapshot = true; + } if (gizmoUsing) { // transformMatrix is in Panda space — apply directly. switch (EditorContext_->GetGizmoOperation()) { @@ -664,15 +697,33 @@ void MetaCoreEditorApp::DrawEditorFrame() { viewportState, EditorContext_->GetInput().GetCursorPosition() ); - if (pickedObjectId != 0 && pickedObjectId != EditorContext_->GetSelectedObjectId()) { - EditorContext_->SetSelectedObjectId(pickedObjectId); + const bool ctrlDown = ImGui::GetIO().KeyCtrl; + const bool shiftDown = ImGui::GetIO().KeyShift; + if (pickedObjectId != 0) { + if (shiftDown) { + EditorContext_->SelectRangeByOrderedIds(Scene_.BuildHierarchyPreorder(), pickedObjectId, ctrlDown); + } else if (ctrlDown) { + EditorContext_->ToggleSelection(pickedObjectId); + } else { + EditorContext_->SelectOnly(pickedObjectId); + } if (const MetaCoreGameObject* selectedObject = EditorContext_->GetSelectedGameObject(); selectedObject != nullptr) { EditorContext_->AddConsoleMessage(MetaCoreLogLevel::Info, "Scene", "视口已选中对象: " + selectedObject->Name); } - sceneView.SelectedObjectId = pickedObjectId; + sceneView.SelectedObjectId = EditorContext_->GetActiveObjectId(); + } else if (!ctrlDown && !shiftDown) { + EditorContext_->ClearSelection(); + sceneView.SelectedObjectId = 0; } } + if (!gizmoUsing && gizmoWasUsing && hasGizmoBeforeSnapshot) { + const MetaCoreEditorStateSnapshot gizmoAfterSnapshot = EditorContext_->CaptureStateSnapshot(); + (void)EditorContext_->CommitStateTransition("修改变换", gizmoBeforeSnapshot, gizmoAfterSnapshot, true); + hasGizmoBeforeSnapshot = false; + } + gizmoWasUsing = gizmoUsing; + ImGui::SetNextWindowPos(centralNode->Pos); ImGui::SetNextWindowSize(centralNode->Size); ImGui::SetNextWindowBgAlpha(0.0F); @@ -779,9 +830,13 @@ void MetaCoreEditorApp::DrawEditorFrame() { ImGui::PopStyleVar(3); } else { + gizmoWasUsing = false; + hasGizmoBeforeSnapshot = false; ViewportRenderer_.SetViewportRect(MetaCoreViewportRect{}); } } else { + gizmoWasUsing = false; + hasGizmoBeforeSnapshot = false; ViewportRenderer_.SetViewportRect(MetaCoreViewportRect{}); } } diff --git a/Source/MetaCoreEditor/Private/MetaCoreEditorCommandService.cpp b/Source/MetaCoreEditor/Private/MetaCoreEditorCommandService.cpp new file mode 100644 index 0000000..00b4d99 --- /dev/null +++ b/Source/MetaCoreEditor/Private/MetaCoreEditorCommandService.cpp @@ -0,0 +1,107 @@ +#include "MetaCoreEditor/MetaCoreEditorCommandService.h" + +#include "MetaCoreEditor/MetaCoreEditorContext.h" + +#include + +namespace MetaCore { + +bool MetaCoreIEditorCommand::CanMergeWith(const MetaCoreIEditorCommand& command) const { + (void)command; + return false; +} + +bool MetaCoreIEditorCommand::MergeWith(const MetaCoreIEditorCommand& command) { + (void)command; + return false; +} + +bool MetaCoreEditorCommandService::Execute(std::unique_ptr command, MetaCoreEditorContext& editorContext) { + if (command == nullptr || !command->Execute(editorContext)) { + return false; + } + + return PushExecuted(std::move(command)); +} + +bool MetaCoreEditorCommandService::PushExecuted(std::unique_ptr command) { + if (command == nullptr) { + return false; + } + + if (!UndoStack_.empty() && UndoStack_.back()->CanMergeWith(*command)) { + if (UndoStack_.back()->MergeWith(*command)) { + RedoStack_.clear(); + return true; + } + } + + UndoStack_.push_back(std::move(command)); + RedoStack_.clear(); + return true; +} + +bool MetaCoreEditorCommandService::Undo(MetaCoreEditorContext& editorContext) { + if (UndoStack_.empty()) { + return false; + } + + std::unique_ptr command = std::move(UndoStack_.back()); + UndoStack_.pop_back(); + command->Undo(editorContext); + RedoStack_.push_back(std::move(command)); + return true; +} + +bool MetaCoreEditorCommandService::Redo(MetaCoreEditorContext& editorContext) { + if (RedoStack_.empty()) { + return false; + } + + std::unique_ptr command = std::move(RedoStack_.back()); + RedoStack_.pop_back(); + if (!command->Execute(editorContext)) { + return false; + } + + UndoStack_.push_back(std::move(command)); + return true; +} + +bool MetaCoreEditorCommandService::CanUndo() const { + return !UndoStack_.empty(); +} + +bool MetaCoreEditorCommandService::CanRedo() const { + return !RedoStack_.empty(); +} + +std::size_t MetaCoreEditorCommandService::GetUndoCount() const { + return UndoStack_.size(); +} + +std::size_t MetaCoreEditorCommandService::GetRedoCount() const { + return RedoStack_.size(); +} + +void MetaCoreEditorCommandService::Clear() { + UndoStack_.clear(); + RedoStack_.clear(); +} + +void MetaCoreEditorCommandService::RegisterFactory(const std::string& commandType, MetaCoreEditorCommandFactory factory) { + if (commandType.empty() || !factory) { + return; + } + Factories_[commandType] = std::move(factory); +} + +std::unique_ptr MetaCoreEditorCommandService::CreateCommand(const std::string& commandType) const { + const auto iterator = Factories_.find(commandType); + if (iterator == Factories_.end()) { + return nullptr; + } + return iterator->second(); +} + +} // namespace MetaCore diff --git a/Source/MetaCoreEditor/Private/MetaCoreEditorContext.cpp b/Source/MetaCoreEditor/Private/MetaCoreEditorContext.cpp index ca04713..b7f14c8 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreEditorContext.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreEditorContext.cpp @@ -4,10 +4,137 @@ #include "MetaCorePlatform/MetaCoreWindow.h" #include "MetaCoreRender/MetaCoreEditorViewportRenderer.h" #include "MetaCoreRender/MetaCoreRenderDevice.h" -#include "MetaCoreScene/MetaCoreScene.h" + +#include +#include +#include namespace MetaCore { +namespace { + +class MetaCoreSnapshotEditorCommand final : public MetaCoreIEditorCommand { +public: + MetaCoreSnapshotEditorCommand( + std::string label, + MetaCoreEditorStateSnapshot beforeSnapshot, + MetaCoreEditorStateSnapshot afterSnapshot, + bool allowMerge + ) + : Label_(std::move(label)), + BeforeSnapshot_(std::move(beforeSnapshot)), + AfterSnapshot_(std::move(afterSnapshot)), + AllowMerge_(allowMerge) { + } + + [[nodiscard]] std::string GetLabel() const override { return Label_; } + + bool Execute(MetaCoreEditorContext& editorContext) override { + editorContext.RestoreStateSnapshot(AfterSnapshot_); + return true; + } + + void Undo(MetaCoreEditorContext& editorContext) override { + editorContext.RestoreStateSnapshot(BeforeSnapshot_); + } + + [[nodiscard]] bool CanMergeWith(const MetaCoreIEditorCommand& command) const override { + const auto* snapshotCommand = dynamic_cast(&command); + return AllowMerge_ && snapshotCommand != nullptr && snapshotCommand->AllowMerge_ && snapshotCommand->Label_ == Label_; + } + + bool MergeWith(const MetaCoreIEditorCommand& command) override { + const auto* snapshotCommand = dynamic_cast(&command); + if (snapshotCommand == nullptr || snapshotCommand->Label_ != Label_) { + return false; + } + + AfterSnapshot_ = snapshotCommand->AfterSnapshot_; + return true; + } + +private: + std::string Label_{}; + MetaCoreEditorStateSnapshot BeforeSnapshot_{}; + MetaCoreEditorStateSnapshot AfterSnapshot_{}; + bool AllowMerge_ = false; +}; + +[[nodiscard]] bool MetaCoreNearlyEqual(float lhs, float rhs) { + return std::abs(lhs - rhs) <= 0.0001F; +} + +[[nodiscard]] bool MetaCoreNearlyEqualVec3(const glm::vec3& lhs, const glm::vec3& rhs) { + return MetaCoreNearlyEqual(lhs.x, rhs.x) && + MetaCoreNearlyEqual(lhs.y, rhs.y) && + MetaCoreNearlyEqual(lhs.z, rhs.z); +} + +[[nodiscard]] bool MetaCoreTransformEqual(const MetaCoreTransformComponent& lhs, const MetaCoreTransformComponent& rhs) { + return MetaCoreNearlyEqualVec3(lhs.Position, rhs.Position) && + MetaCoreNearlyEqualVec3(lhs.RotationEulerDegrees, rhs.RotationEulerDegrees) && + MetaCoreNearlyEqualVec3(lhs.Scale, rhs.Scale); +} + +[[nodiscard]] bool MetaCoreGameObjectEqual(const MetaCoreGameObject& lhs, const MetaCoreGameObject& rhs) { + if (lhs.Id != rhs.Id || lhs.ParentId != rhs.ParentId || lhs.Name != rhs.Name || !MetaCoreTransformEqual(lhs.Transform, rhs.Transform)) { + return false; + } + + if (lhs.Camera.has_value() != rhs.Camera.has_value()) { + return false; + } + if (lhs.Camera.has_value()) { + if (!MetaCoreNearlyEqual(lhs.Camera->FieldOfViewDegrees, rhs.Camera->FieldOfViewDegrees) || + !MetaCoreNearlyEqual(lhs.Camera->NearClip, rhs.Camera->NearClip) || + !MetaCoreNearlyEqual(lhs.Camera->FarClip, rhs.Camera->FarClip) || + lhs.Camera->IsPrimary != rhs.Camera->IsPrimary) { + return false; + } + } + + if (lhs.MeshRenderer.has_value() != rhs.MeshRenderer.has_value()) { + return false; + } + if (lhs.MeshRenderer.has_value()) { + if (lhs.MeshRenderer->BuiltinMesh != rhs.MeshRenderer->BuiltinMesh || + !MetaCoreNearlyEqualVec3(lhs.MeshRenderer->BaseColor, rhs.MeshRenderer->BaseColor) || + lhs.MeshRenderer->Visible != rhs.MeshRenderer->Visible) { + return false; + } + } + + if (lhs.Light.has_value() != rhs.Light.has_value()) { + return false; + } + if (lhs.Light.has_value()) { + if (!MetaCoreNearlyEqualVec3(lhs.Light->Color, rhs.Light->Color) || + !MetaCoreNearlyEqual(lhs.Light->Intensity, rhs.Light->Intensity)) { + return false; + } + } + + return true; +} + +[[nodiscard]] bool MetaCoreStateSnapshotEqual(const MetaCoreEditorStateSnapshot& lhs, const MetaCoreEditorStateSnapshot& rhs) { + if (lhs.SceneSnapshot.GameObjects.size() != rhs.SceneSnapshot.GameObjects.size()) { + return false; + } + + for (std::size_t index = 0; index < lhs.SceneSnapshot.GameObjects.size(); ++index) { + if (!MetaCoreGameObjectEqual(lhs.SceneSnapshot.GameObjects[index], rhs.SceneSnapshot.GameObjects[index])) { + return false; + } + } + + return lhs.SelectionSnapshot.SelectedObjectIds == rhs.SelectionSnapshot.SelectedObjectIds && + lhs.SelectionSnapshot.ActiveObjectId == rhs.SelectionSnapshot.ActiveObjectId && + lhs.SelectionSnapshot.SelectionAnchorId == rhs.SelectionSnapshot.SelectionAnchorId; +} + +} // namespace + MetaCoreEditorContext::MetaCoreEditorContext( MetaCoreWindow& window, MetaCoreRenderDevice& renderDevice, @@ -42,14 +169,227 @@ MetaCoreSceneViewportState& MetaCoreEditorContext::GetSceneViewportState() { ret const MetaCoreSceneViewportState& MetaCoreEditorContext::GetSceneViewportState() const { return SceneViewportState_; } MetaCoreEditorCameraController& MetaCoreEditorContext::GetCameraController() { return *CameraController_; } const MetaCoreEditorCameraController& MetaCoreEditorContext::GetCameraController() const { return *CameraController_; } -MetaCoreId MetaCoreEditorContext::GetSelectedObjectId() const { return SelectedObjectId_; } -void MetaCoreEditorContext::SetSelectedObjectId(MetaCoreId objectId) { SelectedObjectId_ = objectId; } -MetaCoreGameObject* MetaCoreEditorContext::GetSelectedGameObject() { return Scene_.FindGameObject(SelectedObjectId_); } -const MetaCoreGameObject* MetaCoreEditorContext::GetSelectedGameObject() const { return Scene_.FindGameObject(SelectedObjectId_); } + +const std::vector& MetaCoreEditorContext::GetSelectedObjectIds() const { return SelectedObjectIds_; } +MetaCoreId MetaCoreEditorContext::GetActiveObjectId() const { return ActiveObjectId_; } + +bool MetaCoreEditorContext::IsObjectSelected(MetaCoreId objectId) const { + return std::find(SelectedObjectIds_.begin(), SelectedObjectIds_.end(), objectId) != SelectedObjectIds_.end(); +} + +void MetaCoreEditorContext::ClearSelection() { + SelectedObjectIds_.clear(); + ActiveObjectId_ = 0; + SelectionAnchorId_ = 0; +} + +void MetaCoreEditorContext::SelectOnly(MetaCoreId objectId) { + SetSelection(objectId == 0 ? std::vector{} : std::vector{objectId}, objectId); +} + +void MetaCoreEditorContext::ToggleSelection(MetaCoreId objectId) { + if (objectId == 0 || Scene_.FindGameObject(objectId) == nullptr) { + return; + } + + auto iterator = std::find(SelectedObjectIds_.begin(), SelectedObjectIds_.end(), objectId); + if (iterator == SelectedObjectIds_.end()) { + SelectedObjectIds_.push_back(objectId); + ActiveObjectId_ = objectId; + } else { + SelectedObjectIds_.erase(iterator); + if (ActiveObjectId_ == objectId) { + ActiveObjectId_ = SelectedObjectIds_.empty() ? 0 : SelectedObjectIds_.back(); + } + } + + SelectionAnchorId_ = ActiveObjectId_ != 0 ? ActiveObjectId_ : SelectionAnchorId_; + NormalizeSelection(); +} + +void MetaCoreEditorContext::AddToSelection(MetaCoreId objectId, bool makeActive) { + if (objectId == 0 || Scene_.FindGameObject(objectId) == nullptr) { + return; + } + + if (!IsObjectSelected(objectId)) { + SelectedObjectIds_.push_back(objectId); + } + if (makeActive) { + ActiveObjectId_ = objectId; + SelectionAnchorId_ = objectId; + } + NormalizeSelection(); +} + +void MetaCoreEditorContext::SetSelection(const std::vector& objectIds, MetaCoreId activeObjectId) { + SelectedObjectIds_.clear(); + SelectedObjectIds_.reserve(objectIds.size()); + + std::unordered_set deduplicatedIds; + for (MetaCoreId objectId : objectIds) { + if (objectId == 0 || Scene_.FindGameObject(objectId) == nullptr || deduplicatedIds.contains(objectId)) { + continue; + } + deduplicatedIds.insert(objectId); + SelectedObjectIds_.push_back(objectId); + } + + if (activeObjectId != 0 && deduplicatedIds.contains(activeObjectId)) { + ActiveObjectId_ = activeObjectId; + } else { + ActiveObjectId_ = SelectedObjectIds_.empty() ? 0 : SelectedObjectIds_.back(); + } + + SelectionAnchorId_ = ActiveObjectId_; + NormalizeSelection(); +} + +MetaCoreId MetaCoreEditorContext::GetSelectionAnchorId() const { return SelectionAnchorId_; } + +void MetaCoreEditorContext::SetSelectionAnchorId(MetaCoreId objectId) { + SelectionAnchorId_ = (objectId != 0 && Scene_.FindGameObject(objectId) != nullptr) ? objectId : 0; +} + +void MetaCoreEditorContext::SelectRangeByOrderedIds(const std::vector& orderedIds, MetaCoreId endId, bool additive) { + if (endId == 0 || Scene_.FindGameObject(endId) == nullptr) { + return; + } + + const MetaCoreId startId = SelectionAnchorId_ != 0 ? SelectionAnchorId_ : (ActiveObjectId_ != 0 ? ActiveObjectId_ : endId); + auto startIterator = std::find(orderedIds.begin(), orderedIds.end(), startId); + auto endIterator = std::find(orderedIds.begin(), orderedIds.end(), endId); + if (startIterator == orderedIds.end() || endIterator == orderedIds.end()) { + SelectOnly(endId); + return; + } + + const std::size_t startIndex = static_cast(std::distance(orderedIds.begin(), startIterator)); + const std::size_t endIndex = static_cast(std::distance(orderedIds.begin(), endIterator)); + const std::size_t minIndex = std::min(startIndex, endIndex); + const std::size_t maxIndex = std::max(startIndex, endIndex); + + std::unordered_set resultingIds; + if (additive) { + for (MetaCoreId objectId : SelectedObjectIds_) { + resultingIds.insert(objectId); + } + } + + for (std::size_t index = minIndex; index <= maxIndex; ++index) { + resultingIds.insert(orderedIds[index]); + } + + std::vector orderedSelection; + orderedSelection.reserve(resultingIds.size()); + for (MetaCoreId objectId : orderedIds) { + if (resultingIds.contains(objectId)) { + orderedSelection.push_back(objectId); + } + } + + SetSelection(orderedSelection, endId); + SelectionAnchorId_ = startId; +} + +MetaCoreId MetaCoreEditorContext::GetSelectedObjectId() const { return ActiveObjectId_; } +void MetaCoreEditorContext::SetSelectedObjectId(MetaCoreId objectId) { SelectOnly(objectId); } +MetaCoreGameObject* MetaCoreEditorContext::GetSelectedGameObject() { return Scene_.FindGameObject(ActiveObjectId_); } +const MetaCoreGameObject* MetaCoreEditorContext::GetSelectedGameObject() const { return Scene_.FindGameObject(ActiveObjectId_); } MetaCoreGizmoOperation MetaCoreEditorContext::GetGizmoOperation() const { return GizmoOperation_; } void MetaCoreEditorContext::SetGizmoOperation(MetaCoreGizmoOperation operation) { GizmoOperation_ = operation; } MetaCoreGizmoMode MetaCoreEditorContext::GetGizmoMode() const { return GizmoMode_; } void MetaCoreEditorContext::SetGizmoMode(MetaCoreGizmoMode mode) { GizmoMode_ = mode; } +MetaCoreReparentTransformRule MetaCoreEditorContext::GetReparentTransformRule() const { return ReparentTransformRule_; } +void MetaCoreEditorContext::SetReparentTransformRule(MetaCoreReparentTransformRule rule) { ReparentTransformRule_ = rule; } + +MetaCoreEditorCommandService& MetaCoreEditorContext::GetCommandService() { return CommandService_; } +const MetaCoreEditorCommandService& MetaCoreEditorContext::GetCommandService() const { return CommandService_; } + +bool MetaCoreEditorContext::ExecuteCommand(std::unique_ptr command) { + if (!CommandService_.Execute(std::move(command), *this)) { + return false; + } + + NormalizeSelection(); + return true; +} + +bool MetaCoreEditorContext::UndoCommand() { + if (!CommandService_.Undo(*this)) { + return false; + } + + PendingRenameObjectId_ = 0; + NormalizeSelection(); + return true; +} + +bool MetaCoreEditorContext::RedoCommand() { + if (!CommandService_.Redo(*this)) { + return false; + } + + PendingRenameObjectId_ = 0; + NormalizeSelection(); + return true; +} + +MetaCoreEditorStateSnapshot MetaCoreEditorContext::CaptureStateSnapshot() const { + MetaCoreEditorStateSnapshot snapshot; + snapshot.SceneSnapshot = Scene_.CaptureSnapshot(); + snapshot.SelectionSnapshot.SelectedObjectIds = SelectedObjectIds_; + snapshot.SelectionSnapshot.ActiveObjectId = ActiveObjectId_; + snapshot.SelectionSnapshot.SelectionAnchorId = SelectionAnchorId_; + return snapshot; +} + +void MetaCoreEditorContext::RestoreStateSnapshot(const MetaCoreEditorStateSnapshot& snapshot) { + Scene_.RestoreSnapshot(snapshot.SceneSnapshot); + SetSelection(snapshot.SelectionSnapshot.SelectedObjectIds, snapshot.SelectionSnapshot.ActiveObjectId); + if (snapshot.SelectionSnapshot.SelectionAnchorId != 0 && + Scene_.FindGameObject(snapshot.SelectionSnapshot.SelectionAnchorId) != nullptr) { + SelectionAnchorId_ = snapshot.SelectionSnapshot.SelectionAnchorId; + } + NormalizeSelection(); +} + +bool MetaCoreEditorContext::CommitStateTransition( + const std::string& label, + const MetaCoreEditorStateSnapshot& beforeSnapshot, + const MetaCoreEditorStateSnapshot& afterSnapshot, + bool allowMerge +) { + if (MetaCoreStateSnapshotEqual(beforeSnapshot, afterSnapshot)) { + return false; + } + + if (!CommandService_.PushExecuted(std::make_unique(label, beforeSnapshot, afterSnapshot, allowMerge))) { + return false; + } + NormalizeSelection(); + return true; +} + +bool MetaCoreEditorContext::ExecuteSnapshotCommand(const std::string& label, const std::function& mutator) { + const MetaCoreEditorStateSnapshot beforeSnapshot = CaptureStateSnapshot(); + if (!mutator()) { + return false; + } + + const MetaCoreEditorStateSnapshot afterSnapshot = CaptureStateSnapshot(); + return CommitStateTransition(label, beforeSnapshot, afterSnapshot); +} + +void MetaCoreEditorContext::RequestRenameActiveObject() { + PendingRenameObjectId_ = ActiveObjectId_; +} + +MetaCoreId MetaCoreEditorContext::ConsumeRenameRequestObjectId() { + const MetaCoreId objectId = PendingRenameObjectId_; + PendingRenameObjectId_ = 0; + return objectId; +} void MetaCoreEditorContext::AddConsoleMessage(MetaCoreLogLevel level, const std::string& category, const std::string& message) { LogService_.AddEntry(level, category, message); @@ -58,4 +398,26 @@ void MetaCoreEditorContext::AddConsoleMessage(MetaCoreLogLevel level, const std: void MetaCoreEditorContext::SetDockLayoutBuilt(bool built) { DockLayoutBuilt_ = built; } bool MetaCoreEditorContext::HasDockLayoutBuilt() const { return DockLayoutBuilt_; } +void MetaCoreEditorContext::NormalizeSelection() { + std::vector sanitizedSelection; + sanitizedSelection.reserve(SelectedObjectIds_.size()); + std::unordered_set deduplicatedIds; + for (MetaCoreId objectId : SelectedObjectIds_) { + if (objectId == 0 || Scene_.FindGameObject(objectId) == nullptr || deduplicatedIds.contains(objectId)) { + continue; + } + deduplicatedIds.insert(objectId); + sanitizedSelection.push_back(objectId); + } + + SelectedObjectIds_ = std::move(sanitizedSelection); + if (ActiveObjectId_ == 0 || !deduplicatedIds.contains(ActiveObjectId_)) { + ActiveObjectId_ = SelectedObjectIds_.empty() ? 0 : SelectedObjectIds_.back(); + } + + if (SelectionAnchorId_ == 0 || !deduplicatedIds.contains(SelectionAnchorId_)) { + SelectionAnchorId_ = ActiveObjectId_; + } +} + } // namespace MetaCore diff --git a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorCommandService.h b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorCommandService.h new file mode 100644 index 0000000..fa89c76 --- /dev/null +++ b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorCommandService.h @@ -0,0 +1,49 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace MetaCore { + +class MetaCoreEditorContext; + +class MetaCoreIEditorCommand { +public: + virtual ~MetaCoreIEditorCommand() = default; + + [[nodiscard]] virtual std::string GetLabel() const = 0; + virtual bool Execute(MetaCoreEditorContext& editorContext) = 0; + virtual void Undo(MetaCoreEditorContext& editorContext) = 0; + + [[nodiscard]] virtual bool CanMergeWith(const MetaCoreIEditorCommand& command) const; + virtual bool MergeWith(const MetaCoreIEditorCommand& command); +}; + +class MetaCoreEditorCommandService { +public: + using MetaCoreEditorCommandFactory = std::function()>; + + bool Execute(std::unique_ptr command, MetaCoreEditorContext& editorContext); + bool PushExecuted(std::unique_ptr command); + bool Undo(MetaCoreEditorContext& editorContext); + bool Redo(MetaCoreEditorContext& editorContext); + + [[nodiscard]] bool CanUndo() const; + [[nodiscard]] bool CanRedo() const; + [[nodiscard]] std::size_t GetUndoCount() const; + [[nodiscard]] std::size_t GetRedoCount() const; + + void Clear(); + void RegisterFactory(const std::string& commandType, MetaCoreEditorCommandFactory factory); + [[nodiscard]] std::unique_ptr CreateCommand(const std::string& commandType) const; + +private: + std::vector> UndoStack_{}; + std::vector> RedoStack_{}; + std::unordered_map Factories_{}; +}; + +} // namespace MetaCore diff --git a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorContext.h b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorContext.h index 5da2958..a84760a 100644 --- a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorContext.h +++ b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorContext.h @@ -2,9 +2,13 @@ #include "MetaCoreFoundation/MetaCoreId.h" #include "MetaCoreFoundation/MetaCoreLogService.h" +#include "MetaCoreEditor/MetaCoreEditorCommandService.h" +#include "MetaCoreScene/MetaCoreScene.h" +#include #include #include +#include namespace MetaCore { @@ -12,7 +16,6 @@ class MetaCoreWindow; class MetaCoreInput; class MetaCoreRenderDevice; class MetaCoreEditorViewportRenderer; -class MetaCoreScene; struct MetaCoreGameObject; class MetaCoreEditorModuleRegistry; class MetaCoreEditorCameraController; @@ -29,6 +32,22 @@ enum class MetaCoreGizmoMode { World }; +enum class MetaCoreReparentTransformRule { + KeepWorld = 0, + KeepLocal +}; + +struct MetaCoreEditorSelectionSnapshot { + std::vector SelectedObjectIds; + MetaCoreId ActiveObjectId = 0; + MetaCoreId SelectionAnchorId = 0; +}; + +struct MetaCoreEditorStateSnapshot { + MetaCoreSceneSnapshot SceneSnapshot{}; + MetaCoreEditorSelectionSnapshot SelectionSnapshot{}; +}; + struct MetaCoreSceneViewportState { float Left = 0.0F; float Top = 0.0F; @@ -65,6 +84,17 @@ public: [[nodiscard]] const MetaCoreSceneViewportState& GetSceneViewportState() const; [[nodiscard]] MetaCoreEditorCameraController& GetCameraController(); [[nodiscard]] const MetaCoreEditorCameraController& GetCameraController() const; + [[nodiscard]] const std::vector& GetSelectedObjectIds() const; + [[nodiscard]] MetaCoreId GetActiveObjectId() const; + [[nodiscard]] bool IsObjectSelected(MetaCoreId objectId) const; + void ClearSelection(); + void SelectOnly(MetaCoreId objectId); + void ToggleSelection(MetaCoreId objectId); + void AddToSelection(MetaCoreId objectId, bool makeActive); + void SetSelection(const std::vector& objectIds, MetaCoreId activeObjectId = 0); + [[nodiscard]] MetaCoreId GetSelectionAnchorId() const; + void SetSelectionAnchorId(MetaCoreId objectId); + void SelectRangeByOrderedIds(const std::vector& orderedIds, MetaCoreId endId, bool additive); [[nodiscard]] MetaCoreId GetSelectedObjectId() const; void SetSelectedObjectId(MetaCoreId objectId); [[nodiscard]] MetaCoreGameObject* GetSelectedGameObject(); @@ -73,11 +103,31 @@ public: void SetGizmoOperation(MetaCoreGizmoOperation operation); [[nodiscard]] MetaCoreGizmoMode GetGizmoMode() const; void SetGizmoMode(MetaCoreGizmoMode mode); + [[nodiscard]] MetaCoreReparentTransformRule GetReparentTransformRule() const; + void SetReparentTransformRule(MetaCoreReparentTransformRule rule); + [[nodiscard]] MetaCoreEditorCommandService& GetCommandService(); + [[nodiscard]] const MetaCoreEditorCommandService& GetCommandService() const; + bool ExecuteCommand(std::unique_ptr command); + bool UndoCommand(); + bool RedoCommand(); + bool ExecuteSnapshotCommand(const std::string& label, const std::function& mutator); + [[nodiscard]] MetaCoreEditorStateSnapshot CaptureStateSnapshot() const; + void RestoreStateSnapshot(const MetaCoreEditorStateSnapshot& snapshot); + bool CommitStateTransition( + const std::string& label, + const MetaCoreEditorStateSnapshot& beforeSnapshot, + const MetaCoreEditorStateSnapshot& afterSnapshot, + bool allowMerge = false + ); + void RequestRenameActiveObject(); + [[nodiscard]] MetaCoreId ConsumeRenameRequestObjectId(); void AddConsoleMessage(MetaCoreLogLevel level, const std::string& category, const std::string& message); void SetDockLayoutBuilt(bool built); [[nodiscard]] bool HasDockLayoutBuilt() const; private: + void NormalizeSelection(); + MetaCoreWindow& Window_; MetaCoreRenderDevice& RenderDevice_; MetaCoreEditorViewportRenderer& ViewportRenderer_; @@ -86,9 +136,14 @@ private: MetaCoreEditorModuleRegistry& ModuleRegistry_; std::unique_ptr CameraController_; MetaCoreSceneViewportState SceneViewportState_{}; - MetaCoreId SelectedObjectId_ = 0; + std::vector SelectedObjectIds_{}; + MetaCoreId ActiveObjectId_ = 0; + MetaCoreId SelectionAnchorId_ = 0; MetaCoreGizmoOperation GizmoOperation_ = MetaCoreGizmoOperation::Translate; MetaCoreGizmoMode GizmoMode_ = MetaCoreGizmoMode::Local; + MetaCoreReparentTransformRule ReparentTransformRule_ = MetaCoreReparentTransformRule::KeepWorld; + MetaCoreEditorCommandService CommandService_{}; + MetaCoreId PendingRenameObjectId_ = 0; bool DockLayoutBuilt_ = false; }; diff --git a/Source/MetaCoreFoundation/Private/MetaCoreId.cpp b/Source/MetaCoreFoundation/Private/MetaCoreId.cpp index d0a42b4..672d8d2 100644 --- a/Source/MetaCoreFoundation/Private/MetaCoreId.cpp +++ b/Source/MetaCoreFoundation/Private/MetaCoreId.cpp @@ -14,4 +14,11 @@ MetaCoreId MetaCoreIdGenerator::Generate() { return GMetaCoreNextId.fetch_add(1, std::memory_order_relaxed); } +void MetaCoreIdGenerator::EnsureAbove(MetaCoreId value) { + std::uint64_t desired = value + 1; + std::uint64_t current = GMetaCoreNextId.load(std::memory_order_relaxed); + while (current < desired && !GMetaCoreNextId.compare_exchange_weak(current, desired, std::memory_order_relaxed)) { + } +} + } // namespace MetaCore diff --git a/Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreId.h b/Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreId.h index 787a0c5..73d22ea 100644 --- a/Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreId.h +++ b/Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreId.h @@ -12,6 +12,9 @@ class MetaCoreIdGenerator { public: // 生成一个新的全局唯一标识。 static MetaCoreId Generate(); + + // 确保后续生成的标识严格大于给定值。 + static void EnsureAbove(MetaCoreId value); }; } // namespace MetaCore diff --git a/Source/MetaCoreScene/Private/MetaCoreScene.cpp b/Source/MetaCoreScene/Private/MetaCoreScene.cpp index 8d8557c..9a12696 100644 --- a/Source/MetaCoreScene/Private/MetaCoreScene.cpp +++ b/Source/MetaCoreScene/Private/MetaCoreScene.cpp @@ -2,8 +2,45 @@ #include "MetaCoreFoundation/MetaCoreId.h" +#define GLM_ENABLE_EXPERIMENTAL +#include +#include +#include + +#include +#include +#include +#include + namespace MetaCore { +namespace { + +glm::mat4 MetaCoreBuildTransformMatrix(const MetaCoreTransformComponent& transform) { + const glm::mat4 translation = glm::translate(glm::mat4(1.0F), transform.Position); + const glm::mat4 rotation = glm::yawPitchRoll( + glm::radians(transform.RotationEulerDegrees.y), + glm::radians(transform.RotationEulerDegrees.x), + glm::radians(transform.RotationEulerDegrees.z) + ); + const glm::mat4 scale = glm::scale(glm::mat4(1.0F), transform.Scale); + return translation * rotation * scale; +} + +void MetaCoreApplyMatrixToTransform(const glm::mat4& matrix, MetaCoreTransformComponent& transform) { + glm::vec3 skew{}; + glm::vec4 perspective{}; + glm::quat rotation{}; + glm::decompose(matrix, transform.Scale, rotation, transform.Position, skew, perspective); + transform.RotationEulerDegrees = glm::degrees(glm::eulerAngles(rotation)); +} + +std::string MetaCoreBuildCopyName(const std::string& name) { + return name + " Copy"; +} + +} // namespace + MetaCoreGameObject& MetaCoreScene::CreateGameObject(const std::string& name, MetaCoreId parentId) { GameObjects_.push_back(MetaCoreGameObject{ MetaCoreIdGenerator::Generate(), @@ -63,6 +100,296 @@ std::vector MetaCoreScene::GetChildrenOf(MetaCoreId parentId) const return childIds; } +void MetaCoreScene::CollectPreorder(MetaCoreId objectId, std::vector& output) const { + if (FindGameObject(objectId) == nullptr) { + return; + } + + output.push_back(objectId); + for (MetaCoreId childId : GetChildrenOf(objectId)) { + CollectPreorder(childId, output); + } +} + +std::vector MetaCoreScene::BuildHierarchyPreorder() const { + std::vector orderedIds; + for (MetaCoreId rootId : GetRootObjectIds()) { + CollectPreorder(rootId, orderedIds); + } + return orderedIds; +} + +std::vector MetaCoreScene::GetSubtreeObjectIds(MetaCoreId rootId) const { + std::vector subtreeIds; + CollectPreorder(rootId, subtreeIds); + return subtreeIds; +} + +bool MetaCoreScene::IsDescendantOf(MetaCoreId objectId, MetaCoreId ancestorId) const { + if (objectId == 0 || ancestorId == 0 || objectId == ancestorId) { + return false; + } + + const MetaCoreGameObject* currentObject = FindGameObject(objectId); + while (currentObject != nullptr && currentObject->ParentId != 0) { + if (currentObject->ParentId == ancestorId) { + return true; + } + currentObject = FindGameObject(currentObject->ParentId); + } + return false; +} + +bool MetaCoreScene::RenameGameObject(MetaCoreId objectId, const std::string& name) { + MetaCoreGameObject* object = FindGameObject(objectId); + if (object == nullptr || name.empty()) { + return false; + } + + object->Name = name; + return true; +} + +std::vector MetaCoreScene::DeleteGameObjects(const std::vector& objectIds) { + std::unordered_set selectedIds; + for (MetaCoreId objectId : objectIds) { + if (FindGameObject(objectId) != nullptr) { + selectedIds.insert(objectId); + } + } + + if (selectedIds.empty()) { + return {}; + } + + std::vector rootIds; + rootIds.reserve(selectedIds.size()); + for (MetaCoreId objectId : selectedIds) { + const MetaCoreGameObject* object = FindGameObject(objectId); + bool hasSelectedAncestor = false; + while (object != nullptr && object->ParentId != 0) { + if (selectedIds.contains(object->ParentId)) { + hasSelectedAncestor = true; + break; + } + object = FindGameObject(object->ParentId); + } + if (!hasSelectedAncestor) { + rootIds.push_back(objectId); + } + } + + std::unordered_set idsToDelete; + for (MetaCoreId rootId : rootIds) { + for (MetaCoreId subtreeId : GetSubtreeObjectIds(rootId)) { + idsToDelete.insert(subtreeId); + } + } + + std::vector deletedIds; + deletedIds.reserve(idsToDelete.size()); + std::erase_if(GameObjects_, [&](const MetaCoreGameObject& object) { + if (!idsToDelete.contains(object.Id)) { + return false; + } + deletedIds.push_back(object.Id); + return true; + }); + return deletedIds; +} + +std::vector MetaCoreScene::DuplicateGameObjects(const std::vector& objectIds) { + std::unordered_set selectedIds; + for (MetaCoreId objectId : objectIds) { + if (FindGameObject(objectId) != nullptr) { + selectedIds.insert(objectId); + } + } + + if (selectedIds.empty()) { + return {}; + } + + std::vector orderedIds = BuildHierarchyPreorder(); + std::vector rootIds; + for (MetaCoreId objectId : orderedIds) { + if (!selectedIds.contains(objectId)) { + continue; + } + + const MetaCoreGameObject* object = FindGameObject(objectId); + bool hasSelectedAncestor = false; + while (object != nullptr && object->ParentId != 0) { + if (selectedIds.contains(object->ParentId)) { + hasSelectedAncestor = true; + break; + } + object = FindGameObject(object->ParentId); + } + + if (!hasSelectedAncestor) { + rootIds.push_back(objectId); + } + } + + std::vector clonedObjects; + std::vector duplicatedRootIds; + + const auto cloneSubtree = [&](const auto& self, MetaCoreId sourceId, MetaCoreId duplicatedParentId, bool isRoot) -> void { + const MetaCoreGameObject* sourceObject = FindGameObject(sourceId); + if (sourceObject == nullptr) { + return; + } + + MetaCoreGameObject clonedObject = *sourceObject; + clonedObject.Id = MetaCoreIdGenerator::Generate(); + clonedObject.ParentId = duplicatedParentId; + if (isRoot) { + clonedObject.Name = MetaCoreBuildCopyName(sourceObject->Name); + duplicatedRootIds.push_back(clonedObject.Id); + } + + clonedObjects.push_back(std::move(clonedObject)); + const MetaCoreId newObjectId = clonedObjects.back().Id; + + for (MetaCoreId childId : GetChildrenOf(sourceId)) { + self(self, childId, newObjectId, false); + } + }; + + for (MetaCoreId rootId : rootIds) { + const MetaCoreGameObject* rootObject = FindGameObject(rootId); + if (rootObject == nullptr) { + continue; + } + + cloneSubtree(cloneSubtree, rootId, rootObject->ParentId, true); + } + + GameObjects_.insert(GameObjects_.end(), clonedObjects.begin(), clonedObjects.end()); + return duplicatedRootIds; +} + +bool MetaCoreScene::ReparentGameObjects(const std::vector& objectIds, MetaCoreId newParentId, bool keepWorldTransform) { + if (newParentId != 0 && FindGameObject(newParentId) == nullptr) { + return false; + } + + std::unordered_set selectedIds; + for (MetaCoreId objectId : objectIds) { + if (FindGameObject(objectId) != nullptr) { + selectedIds.insert(objectId); + } + } + + if (selectedIds.empty()) { + return false; + } + + std::vector orderedIds = BuildHierarchyPreorder(); + std::vector rootIds; + for (MetaCoreId objectId : orderedIds) { + if (!selectedIds.contains(objectId)) { + continue; + } + + const MetaCoreGameObject* object = FindGameObject(objectId); + bool hasSelectedAncestor = false; + while (object != nullptr && object->ParentId != 0) { + if (selectedIds.contains(object->ParentId)) { + hasSelectedAncestor = true; + break; + } + object = FindGameObject(object->ParentId); + } + + if (!hasSelectedAncestor) { + rootIds.push_back(objectId); + } + } + + if (rootIds.empty()) { + return false; + } + + for (MetaCoreId objectId : rootIds) { + if (objectId == newParentId || IsDescendantOf(newParentId, objectId)) { + return false; + } + } + + std::unordered_map worldMatrices; + if (keepWorldTransform) { + const auto buildWorldMatrix = [&](const auto& self, MetaCoreId objectId) -> glm::mat4 { + const MetaCoreGameObject* object = FindGameObject(objectId); + if (object == nullptr) { + return glm::mat4(1.0F); + } + + const glm::mat4 localMatrix = MetaCoreBuildTransformMatrix(object->Transform); + if (object->ParentId == 0) { + return localMatrix; + } + + return self(self, object->ParentId) * localMatrix; + }; + + for (MetaCoreId rootId : rootIds) { + worldMatrices.emplace(rootId, buildWorldMatrix(buildWorldMatrix, rootId)); + } + + if (newParentId != 0) { + worldMatrices.emplace(newParentId, buildWorldMatrix(buildWorldMatrix, newParentId)); + } + } + + for (MetaCoreId rootId : rootIds) { + MetaCoreGameObject* object = FindGameObject(rootId); + if (object != nullptr) { + object->ParentId = newParentId; + } + } + + if (keepWorldTransform) { + const glm::mat4 newParentWorldMatrix = + newParentId == 0 ? glm::mat4(1.0F) : worldMatrices[newParentId]; + const glm::mat4 inverseParentWorldMatrix = glm::inverse(newParentWorldMatrix); + + for (MetaCoreId rootId : rootIds) { + MetaCoreGameObject* object = FindGameObject(rootId); + if (object == nullptr) { + continue; + } + + const auto matrixIterator = worldMatrices.find(rootId); + if (matrixIterator == worldMatrices.end()) { + continue; + } + + const glm::mat4 newLocalMatrix = inverseParentWorldMatrix * matrixIterator->second; + MetaCoreApplyMatrixToTransform(newLocalMatrix, object->Transform); + } + } + + return true; +} + +MetaCoreSceneSnapshot MetaCoreScene::CaptureSnapshot() const { + MetaCoreSceneSnapshot snapshot; + snapshot.GameObjects = GameObjects_; + return snapshot; +} + +void MetaCoreScene::RestoreSnapshot(const MetaCoreSceneSnapshot& snapshot) { + GameObjects_ = snapshot.GameObjects; + + MetaCoreId maxId = 0; + for (const MetaCoreGameObject& object : GameObjects_) { + maxId = std::max(maxId, object.Id); + } + MetaCoreIdGenerator::EnsureAbove(maxId); +} + MetaCoreScene MetaCoreCreateDefaultScene() { MetaCoreScene scene; diff --git a/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreScene.h b/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreScene.h index a4c5aab..54dbd83 100644 --- a/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreScene.h +++ b/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreScene.h @@ -6,6 +6,10 @@ namespace MetaCore { +struct MetaCoreSceneSnapshot { + std::vector GameObjects; +}; + // 负责存储和查询场景中的全部对象。 class MetaCoreScene { public: @@ -26,7 +30,34 @@ public: // 返回某个对象的直接子对象列表。 [[nodiscard]] std::vector GetChildrenOf(MetaCoreId parentId) const; + // 按层级前序遍历顺序返回对象标识列表。 + [[nodiscard]] std::vector BuildHierarchyPreorder() const; + + // 返回指定根对象的完整子树标识(含自身)。 + [[nodiscard]] std::vector GetSubtreeObjectIds(MetaCoreId rootId) const; + + // 判断对象是否是另一个对象的后代。 + [[nodiscard]] bool IsDescendantOf(MetaCoreId objectId, MetaCoreId ancestorId) const; + + // 重命名对象。 + bool RenameGameObject(MetaCoreId objectId, const std::string& name); + + // 删除对象(包含其子树)。返回实际删除的全部对象标识。 + [[nodiscard]] std::vector DeleteGameObjects(const std::vector& objectIds); + + // 复制对象(包含其子树)。返回复制后顶层对象标识。 + [[nodiscard]] std::vector DuplicateGameObjects(const std::vector& objectIds); + + // 批量重挂接对象到新父节点。 + bool ReparentGameObjects(const std::vector& objectIds, MetaCoreId newParentId, bool keepWorldTransform); + + // 场景快照捕获与恢复。 + [[nodiscard]] MetaCoreSceneSnapshot CaptureSnapshot() const; + void RestoreSnapshot(const MetaCoreSceneSnapshot& snapshot); + private: + void CollectPreorder(MetaCoreId objectId, std::vector& output) const; + std::vector GameObjects_{}; }; diff --git a/tests/MetaCoreSmokeTests.cpp b/tests/MetaCoreSmokeTests.cpp index c49441c..0a15d12 100644 --- a/tests/MetaCoreSmokeTests.cpp +++ b/tests/MetaCoreSmokeTests.cpp @@ -1,5 +1,9 @@ +#include "MetaCoreEditor/MetaCoreEditorContext.h" #include "MetaCoreEditor/MetaCoreEditorModule.h" #include "MetaCoreFoundation/MetaCoreLogService.h" +#include "MetaCorePlatform/MetaCoreWindow.h" +#include "MetaCoreRender/MetaCoreEditorViewportRenderer.h" +#include "MetaCoreRender/MetaCoreRenderDevice.h" #include "MetaCoreScene/MetaCoreScene.h" #include @@ -22,6 +26,103 @@ public: void DrawPanel(MetaCore::MetaCoreEditorContext&) override {} }; +void MetaCoreTestSceneEditApi() { + MetaCore::MetaCoreScene scene = MetaCore::MetaCoreCreateDefaultScene(); + const std::size_t originalCount = scene.GetGameObjects().size(); + + const auto roots = scene.GetRootObjectIds(); + MetaCoreExpect(!roots.empty(), "默认场景应有根对象"); + + const std::vector duplicateRoots = scene.DuplicateGameObjects({roots.front()}); + MetaCoreExpect(!duplicateRoots.empty(), "复制应返回新根对象"); + MetaCoreExpect(scene.GetGameObjects().size() > originalCount, "复制后对象数量应增加"); + + const MetaCore::MetaCoreId duplicatedRootId = duplicateRoots.front(); + MetaCoreExpect(scene.FindGameObject(duplicatedRootId) != nullptr, "复制后的对象应可被查找到"); + + const bool reparented = scene.ReparentGameObjects({duplicatedRootId}, 0, true); + MetaCoreExpect(reparented, "重挂接操作应成功"); + + const std::vector deletedIds = scene.DeleteGameObjects({duplicatedRootId}); + MetaCoreExpect(!deletedIds.empty(), "删除应返回被删除对象"); + MetaCoreExpect(scene.FindGameObject(duplicatedRootId) == nullptr, "删除后对象不应存在"); +} + +void MetaCoreTestSelectionStateMachine() { + MetaCore::MetaCoreWindow window; + MetaCore::MetaCoreRenderDevice renderDevice; + MetaCore::MetaCoreEditorViewportRenderer viewportRenderer; + MetaCore::MetaCoreScene scene; + MetaCore::MetaCoreLogService logService; + MetaCore::MetaCoreEditorModuleRegistry moduleRegistry; + + const MetaCore::MetaCoreId objectAId = scene.CreateGameObject("A").Id; + const MetaCore::MetaCoreId objectBId = scene.CreateGameObject("B").Id; + const MetaCore::MetaCoreId objectCId = scene.CreateGameObject("C").Id; + + MetaCore::MetaCoreEditorContext editorContext( + window, + renderDevice, + viewportRenderer, + scene, + logService, + moduleRegistry + ); + + editorContext.SelectOnly(objectAId); + MetaCoreExpect(editorContext.GetActiveObjectId() == objectAId, "单选后 Active 应为 A"); + MetaCoreExpect(editorContext.GetSelectedObjectIds().size() == 1, "单选后应仅包含一个对象"); + + editorContext.ToggleSelection(objectBId); + MetaCoreExpect(editorContext.IsObjectSelected(objectAId), "Toggle 后应保留已选对象 A"); + MetaCoreExpect(editorContext.IsObjectSelected(objectBId), "Toggle 后应包含对象 B"); + MetaCoreExpect(editorContext.GetActiveObjectId() == objectBId, "Toggle 新增对象后 Active 应为 B"); + + editorContext.SetSelectionAnchorId(objectAId); + editorContext.SelectRangeByOrderedIds(scene.BuildHierarchyPreorder(), objectCId, false); + MetaCoreExpect(editorContext.IsObjectSelected(objectAId), "范围选择应包含起点 A"); + MetaCoreExpect(editorContext.IsObjectSelected(objectBId), "范围选择应包含中间对象 B"); + MetaCoreExpect(editorContext.IsObjectSelected(objectCId), "范围选择应包含终点 C"); + MetaCoreExpect(editorContext.GetActiveObjectId() == objectCId, "范围选择后 Active 应为终点 C"); +} + +void MetaCoreTestUndoRedo() { + MetaCore::MetaCoreWindow window; + MetaCore::MetaCoreRenderDevice renderDevice; + MetaCore::MetaCoreEditorViewportRenderer viewportRenderer; + MetaCore::MetaCoreScene scene = MetaCore::MetaCoreCreateDefaultScene(); + MetaCore::MetaCoreLogService logService; + MetaCore::MetaCoreEditorModuleRegistry moduleRegistry; + + MetaCore::MetaCoreEditorContext editorContext( + window, + renderDevice, + viewportRenderer, + scene, + logService, + moduleRegistry + ); + + const std::size_t beforeCreateCount = scene.GetGameObjects().size(); + const bool created = editorContext.ExecuteSnapshotCommand("创建对象", [&]() { + MetaCore::MetaCoreGameObject& object = scene.CreateGameObject("UndoRedoObject"); + editorContext.SelectOnly(object.Id); + return true; + }); + MetaCoreExpect(created, "创建快照命令应执行成功"); + MetaCoreExpect(scene.GetGameObjects().size() == beforeCreateCount + 1, "执行命令后对象数量应增加"); + MetaCoreExpect(editorContext.GetCommandService().CanUndo(), "执行命令后应可撤销"); + + const bool undoSucceeded = editorContext.UndoCommand(); + MetaCoreExpect(undoSucceeded, "Undo 应成功"); + MetaCoreExpect(scene.GetGameObjects().size() == beforeCreateCount, "Undo 后对象数量应恢复"); + MetaCoreExpect(editorContext.GetCommandService().CanRedo(), "Undo 后应可重做"); + + const bool redoSucceeded = editorContext.RedoCommand(); + MetaCoreExpect(redoSucceeded, "Redo 应成功"); + MetaCoreExpect(scene.GetGameObjects().size() == beforeCreateCount + 1, "Redo 后对象数量应再次增加"); +} + } // namespace int main() { @@ -49,6 +150,10 @@ int main() { logService.AddEntry(MetaCore::MetaCoreLogLevel::Info, "Test", "Smoke"); MetaCoreExpect(logService.GetEntries().size() == 1, "日志服务应记录一条日志"); + MetaCoreTestSceneEditApi(); + MetaCoreTestSelectionStateMachine(); + MetaCoreTestUndoRedo(); + std::cout << "MetaCoreSmokeTests passed\n"; return 0; }