From 5c3566774bbed6aca9c64ae12a9edae6a115896a Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Thu, 26 Mar 2026 01:59:14 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E6=9B=B4=E6=96=B0=20containers=20?= =?UTF-8?q?=E5=92=8C=20threading=20=E6=A8=A1=E5=9D=97=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - containers: 更新 string 类的多个方法文档 - threading: 更新 mutex 和 task-group 方法文档 --- docs/api/containers/array/begin-end.md | 2 +- docs/api/containers/array/constructor.md | 2 +- docs/api/containers/string/clear.md | 2 +- docs/api/containers/string/constructor.md | 2 +- docs/api/containers/string/cstr.md | 2 +- docs/api/containers/string/destructor.md | 2 +- docs/api/containers/string/ends-with.md | 2 +- docs/api/containers/string/find.md | 2 +- docs/api/containers/string/operator-assign.md | 2 +- docs/api/containers/string/operator-equal.md | 2 +- .../containers/string/operator-plus-assign.md | 2 +- docs/api/containers/string/operator-plus.md | 2 +- .../containers/string/operator-subscript.md | 2 +- docs/api/core/filewriter/Write.md | 2 +- docs/api/resources/mesh/mesh.md | 2 +- docs/api/threading/mutex/lock-const.md | 2 +- docs/api/threading/mutex/try-lock-const.md | 2 +- docs/api/threading/mutex/unlock-const.md | 2 +- docs/api/threading/task-group/constructor.md | 2 +- docs/api/threading/task/release.md | 2 +- editor/CMakeLists.txt | 3 +- .../ComponentEditors/CameraComponentEditor.h | 26 ++- .../src/ComponentEditors/IComponentEditor.h | 4 +- .../ComponentEditors/LightComponentEditor.h | 20 ++- .../TransformComponentEditor.h | 11 +- editor/src/Core/EditorContext.h | 8 + editor/src/Core/IEditorContext.h | 2 + editor/src/Core/IUndoManager.h | 40 +++++ editor/src/Core/SceneSnapshot.h | 16 ++ editor/src/Core/UndoManager.cpp | 165 ++++++++++++++++++ editor/src/Core/UndoManager.h | 60 +++++++ editor/src/Layers/EditorLayer.cpp | 2 + editor/src/Managers/SceneManager.cpp | 48 +++++ editor/src/Managers/SceneManager.h | 3 + editor/src/Utils/UndoUtils.h | 34 ++++ editor/src/panels/HierarchyPanel.cpp | 141 ++++++++++----- editor/src/panels/InspectorPanel.cpp | 31 +++- editor/src/panels/MenuBar.cpp | 36 +++- editor/src/panels/ProjectPanel.cpp | 12 +- engine/include/XCEngine/Scene/Scene.h | 4 +- engine/src/Scene/Scene.cpp | 45 +++-- tests/Scene/test_scene.cpp | 59 +++++++ 42 files changed, 714 insertions(+), 96 deletions(-) create mode 100644 editor/src/Core/IUndoManager.h create mode 100644 editor/src/Core/SceneSnapshot.h create mode 100644 editor/src/Core/UndoManager.cpp create mode 100644 editor/src/Core/UndoManager.h create mode 100644 editor/src/Utils/UndoUtils.h diff --git a/docs/api/containers/array/begin-end.md b/docs/api/containers/array/begin-end.md index 0eb325b0..59f6264b 100644 --- a/docs/api/containers/array/begin-end.md +++ b/docs/api/containers/array/begin-end.md @@ -22,7 +22,7 @@ ConstIterator end() const; **示例:** ```cpp -#include +#include XCEngine::Containers::Array arr = {10, 20, 30, 40, 50}; diff --git a/docs/api/containers/array/constructor.md b/docs/api/containers/array/constructor.md index f559401e..2f922344 100644 --- a/docs/api/containers/array/constructor.md +++ b/docs/api/containers/array/constructor.md @@ -28,7 +28,7 @@ Array(std::initializer_list init); **示例:** ```cpp -#include +#include using namespace XCEngine::Containers; diff --git a/docs/api/containers/string/clear.md b/docs/api/containers/string/clear.md index af9c064c..5ae5bd5f 100644 --- a/docs/api/containers/string/clear.md +++ b/docs/api/containers/string/clear.md @@ -14,7 +14,7 @@ void Clear(); **示例:** ```cpp -#include "XCEngine/Containers/String.h" +#include "XCEngine/Core/Containers/String.h" #include int main() { diff --git a/docs/api/containers/string/constructor.md b/docs/api/containers/string/constructor.md index f1967eb4..559d9b4b 100644 --- a/docs/api/containers/string/constructor.md +++ b/docs/api/containers/string/constructor.md @@ -25,7 +25,7 @@ String(String&& other) noexcept; **示例:** ```cpp -#include "XCEngine/Containers/String.h" +#include "XCEngine/Core/Containers/String.h" #include int main() { diff --git a/docs/api/containers/string/cstr.md b/docs/api/containers/string/cstr.md index 70729622..416ca740 100644 --- a/docs/api/containers/string/cstr.md +++ b/docs/api/containers/string/cstr.md @@ -14,7 +14,7 @@ const char* CStr() const; **示例:** ```cpp -#include "XCEngine/Containers/String.h" +#include "XCEngine/Core/Containers/String.h" #include #include diff --git a/docs/api/containers/string/destructor.md b/docs/api/containers/string/destructor.md index 49375c9e..5e870244 100644 --- a/docs/api/containers/string/destructor.md +++ b/docs/api/containers/string/destructor.md @@ -14,7 +14,7 @@ **示例:** ```cpp -#include "XCEngine/Containers/String.h" +#include "XCEngine/Core/Containers/String.h" int main() { { diff --git a/docs/api/containers/string/ends-with.md b/docs/api/containers/string/ends-with.md index aabe335e..b38edea8 100644 --- a/docs/api/containers/string/ends-with.md +++ b/docs/api/containers/string/ends-with.md @@ -16,7 +16,7 @@ bool EndsWith(const char* suffix) const; **示例:** ```cpp -#include "XCEngine/Containers/String.h" +#include "XCEngine/Core/Containers/String.h" #include int main() { diff --git a/docs/api/containers/string/find.md b/docs/api/containers/string/find.md index 652e3d85..0f339a88 100644 --- a/docs/api/containers/string/find.md +++ b/docs/api/containers/string/find.md @@ -16,7 +16,7 @@ SizeType Find(const char* str, SizeType pos = 0) const; **示例:** ```cpp -#include "XCEngine/Containers/String.h" +#include "XCEngine/Core/Containers/String.h" #include int main() { diff --git a/docs/api/containers/string/operator-assign.md b/docs/api/containers/string/operator-assign.md index e6bd49a2..ca82d16f 100644 --- a/docs/api/containers/string/operator-assign.md +++ b/docs/api/containers/string/operator-assign.md @@ -21,7 +21,7 @@ String& operator=(const char* str); **示例:** ```cpp -#include "XCEngine/Containers/String.h" +#include "XCEngine/Core/Containers/String.h" #include int main() { diff --git a/docs/api/containers/string/operator-equal.md b/docs/api/containers/string/operator-equal.md index 5d998d50..6d2b4f5d 100644 --- a/docs/api/containers/string/operator-equal.md +++ b/docs/api/containers/string/operator-equal.md @@ -21,7 +21,7 @@ inline bool operator!=(const String& lhs, const String& rhs); **示例:** ```cpp -#include "XCEngine/Containers/String.h" +#include "XCEngine/Core/Containers/String.h" #include int main() { diff --git a/docs/api/containers/string/operator-plus-assign.md b/docs/api/containers/string/operator-plus-assign.md index 53c3377a..67bc7abc 100644 --- a/docs/api/containers/string/operator-plus-assign.md +++ b/docs/api/containers/string/operator-plus-assign.md @@ -19,7 +19,7 @@ String& operator+=(char c); **示例:** ```cpp -#include "XCEngine/Containers/String.h" +#include "XCEngine/Core/Containers/String.h" #include int main() { diff --git a/docs/api/containers/string/operator-plus.md b/docs/api/containers/string/operator-plus.md index 8e261efd..d2c1c401 100644 --- a/docs/api/containers/string/operator-plus.md +++ b/docs/api/containers/string/operator-plus.md @@ -16,7 +16,7 @@ inline String operator+(const String& lhs, const String& rhs); **示例:** ```cpp -#include "XCEngine/Containers/String.h" +#include "XCEngine/Core/Containers/String.h" #include int main() { diff --git a/docs/api/containers/string/operator-subscript.md b/docs/api/containers/string/operator-subscript.md index bb010214..4aff8ed7 100644 --- a/docs/api/containers/string/operator-subscript.md +++ b/docs/api/containers/string/operator-subscript.md @@ -18,7 +18,7 @@ const char& operator[](SizeType index) const; **示例:** ```cpp -#include "XCEngine/Containers/String.h" +#include "XCEngine/Core/Containers/String.h" #include int main() { diff --git a/docs/api/core/filewriter/Write.md b/docs/api/core/filewriter/Write.md index 25c33a05..0ce8ea75 100644 --- a/docs/api/core/filewriter/Write.md +++ b/docs/api/core/filewriter/Write.md @@ -26,7 +26,7 @@ bool Write(const Containers::String& str); ```cpp #include -#include +#include using namespace XCEngine::Core; diff --git a/docs/api/resources/mesh/mesh.md b/docs/api/resources/mesh/mesh.md index a3600dec..d40a9abf 100644 --- a/docs/api/resources/mesh/mesh.md +++ b/docs/api/resources/mesh/mesh.md @@ -78,7 +78,7 @@ struct MeshSection { ```cpp #include "XCEngine/Resources/Mesh.h" -#include "XCEngine/Containers/Array.h" +#include "XCEngine/Core/Containers/Array.h" using namespace XCEngine; using namespace Resources; diff --git a/docs/api/threading/mutex/lock-const.md b/docs/api/threading/mutex/lock-const.md index 773d4446..6cf5cab4 100644 --- a/docs/api/threading/mutex/lock-const.md +++ b/docs/api/threading/mutex/lock-const.md @@ -22,7 +22,7 @@ void lock() const; #include "XCEngine/Threading/Mutex.h" #include -XCEngine::XCEngine::Threading::Mutex mtx; +XCEngine::Threading::Mutex mtx; int counter = 0; void Increment() { diff --git a/docs/api/threading/mutex/try-lock-const.md b/docs/api/threading/mutex/try-lock-const.md index aa6baed9..dbb0342b 100644 --- a/docs/api/threading/mutex/try-lock-const.md +++ b/docs/api/threading/mutex/try-lock-const.md @@ -22,7 +22,7 @@ bool try_lock() const; #include "XCEngine/Threading/Mutex.h" #include -XCEngine::XCEngine::Threading::Mutex mtx; +XCEngine::Threading::Mutex mtx; volatile bool updated = false; void TryUpdate() { diff --git a/docs/api/threading/mutex/unlock-const.md b/docs/api/threading/mutex/unlock-const.md index 1c6c4d82..c5fbf31e 100644 --- a/docs/api/threading/mutex/unlock-const.md +++ b/docs/api/threading/mutex/unlock-const.md @@ -19,7 +19,7 @@ void unlock() const; ```cpp #include "XCEngine/Threading/Mutex.h" -XCEngine::XCEngine::Threading::Mutex mtx; +XCEngine::Threading::Mutex mtx; std::vector data; void SafePush(int value) { diff --git a/docs/api/threading/task-group/constructor.md b/docs/api/threading/task-group/constructor.md index caba323c..0222744c 100644 --- a/docs/api/threading/task-group/constructor.md +++ b/docs/api/threading/task-group/constructor.md @@ -16,7 +16,7 @@ TaskGroup(); **注意:** - 构造后的任务组不包含任何任务。 -- 任务组创建后需要通过 TaskSystem::CreateTaskGroup() 实际创建。 +- TaskGroup 可以独立使用,也可以通过 TaskSystem::CreateTaskGroup() 创建(后者会将任务组注册到任务系统管理)。 **示例:** diff --git a/docs/api/threading/task/release.md b/docs/api/threading/task/release.md index 09be82ba..458b26b4 100644 --- a/docs/api/threading/task/release.md +++ b/docs/api/threading/task/release.md @@ -29,4 +29,4 @@ task->Release(); // 引用计数 = 0,任务被 delete ## 相关文档 - [ITask 总览](task.md) - 返回类总览 -- [AddRef](../../core/refcounted/addref.md) - 增加引用计数 +- [AddRef](../../core/refcounted/AddRef.md) - 增加引用计数 diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 1cb269fb..1fea7f7a 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -32,6 +32,7 @@ add_executable(${PROJECT_NAME} WIN32 src/main.cpp src/Application.cpp src/Theme.cpp + src/Core/UndoManager.cpp src/Managers/SceneManager.cpp src/Managers/ProjectManager.cpp src/Core/EditorConsoleSink.cpp @@ -68,4 +69,4 @@ target_link_libraries(${PROJECT_NAME} PRIVATE set_target_properties(${PROJECT_NAME} PROPERTIES OUTPUT_NAME "XCEngine" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/bin" -) \ No newline at end of file +) diff --git a/editor/src/ComponentEditors/CameraComponentEditor.h b/editor/src/ComponentEditors/CameraComponentEditor.h index 0eddf81f..4f14c027 100644 --- a/editor/src/ComponentEditors/CameraComponentEditor.h +++ b/editor/src/ComponentEditors/CameraComponentEditor.h @@ -18,7 +18,7 @@ public: return dynamic_cast<::XCEngine::Components::CameraComponent*>(component) != nullptr; } - bool Render(::XCEngine::Components::Component* component) override { + bool Render(::XCEngine::Components::Component* component, IUndoManager* undoManager) override { auto* camera = dynamic_cast<::XCEngine::Components::CameraComponent*>(component); if (!camera) { return false; @@ -28,6 +28,9 @@ public: const char* projectionLabels[] = { "Perspective", "Orthographic" }; bool changed = false; if (ImGui::Combo("Projection", &projectionType, projectionLabels, 2)) { + if (undoManager) { + undoManager->BeginInteractiveChange("Modify Camera"); + } camera->SetProjectionType(static_cast<::XCEngine::Components::CameraProjectionType>(projectionType)); changed = true; } @@ -35,12 +38,18 @@ public: if (camera->GetProjectionType() == ::XCEngine::Components::CameraProjectionType::Perspective) { float fieldOfView = camera->GetFieldOfView(); if (UI::DrawSliderFloat("Field Of View", fieldOfView, 1.0f, 179.0f, 100.0f, "%.1f")) { + if (undoManager) { + undoManager->BeginInteractiveChange("Modify Camera"); + } camera->SetFieldOfView(fieldOfView); changed = true; } } else { float orthographicSize = camera->GetOrthographicSize(); if (UI::DrawFloat("Orthographic Size", orthographicSize, 100.0f, 0.1f, 0.001f)) { + if (undoManager) { + undoManager->BeginInteractiveChange("Modify Camera"); + } camera->SetOrthographicSize(orthographicSize); changed = true; } @@ -48,24 +57,36 @@ public: float nearClip = camera->GetNearClipPlane(); if (UI::DrawFloat("Near Clip", nearClip, 100.0f, 0.01f, 0.001f)) { + if (undoManager) { + undoManager->BeginInteractiveChange("Modify Camera"); + } camera->SetNearClipPlane(nearClip); changed = true; } float farClip = camera->GetFarClipPlane(); if (UI::DrawFloat("Far Clip", farClip, 100.0f, 0.1f, nearClip + 0.001f)) { + if (undoManager) { + undoManager->BeginInteractiveChange("Modify Camera"); + } camera->SetFarClipPlane(farClip); changed = true; } float depth = camera->GetDepth(); if (UI::DrawFloat("Depth", depth, 100.0f, 0.1f)) { + if (undoManager) { + undoManager->BeginInteractiveChange("Modify Camera"); + } camera->SetDepth(depth); changed = true; } bool primary = camera->IsPrimary(); if (UI::DrawBool("Primary", primary)) { + if (undoManager) { + undoManager->BeginInteractiveChange("Modify Camera"); + } camera->SetPrimary(primary); changed = true; } @@ -77,6 +98,9 @@ public: camera->GetClearColor().a }; if (UI::DrawColor4("Clear Color", clearColor)) { + if (undoManager) { + undoManager->BeginInteractiveChange("Modify Camera"); + } camera->SetClearColor(::XCEngine::Math::Color(clearColor[0], clearColor[1], clearColor[2], clearColor[3])); changed = true; } diff --git a/editor/src/ComponentEditors/IComponentEditor.h b/editor/src/ComponentEditors/IComponentEditor.h index 81954c2a..b79b8b60 100644 --- a/editor/src/ComponentEditors/IComponentEditor.h +++ b/editor/src/ComponentEditors/IComponentEditor.h @@ -6,13 +6,15 @@ namespace XCEngine { namespace Editor { +class IUndoManager; + class IComponentEditor { public: virtual ~IComponentEditor() = default; virtual const char* GetDisplayName() const = 0; virtual bool CanEdit(::XCEngine::Components::Component* component) const = 0; - virtual bool Render(::XCEngine::Components::Component* component) = 0; + virtual bool Render(::XCEngine::Components::Component* component, IUndoManager* undoManager) = 0; virtual bool ShowInAddComponentMenu() const { return true; } virtual bool CanAddTo(::XCEngine::Components::GameObject* gameObject) const { return false; } diff --git a/editor/src/ComponentEditors/LightComponentEditor.h b/editor/src/ComponentEditors/LightComponentEditor.h index cbff270f..7aab1dbf 100644 --- a/editor/src/ComponentEditors/LightComponentEditor.h +++ b/editor/src/ComponentEditors/LightComponentEditor.h @@ -18,7 +18,7 @@ public: return dynamic_cast<::XCEngine::Components::LightComponent*>(component) != nullptr; } - bool Render(::XCEngine::Components::Component* component) override { + bool Render(::XCEngine::Components::Component* component, IUndoManager* undoManager) override { auto* light = dynamic_cast<::XCEngine::Components::LightComponent*>(component); if (!light) { return false; @@ -28,6 +28,9 @@ public: const char* lightTypeLabels[] = { "Directional", "Point", "Spot" }; bool changed = false; if (ImGui::Combo("Type", &lightType, lightTypeLabels, 3)) { + if (undoManager) { + undoManager->BeginInteractiveChange("Modify Light"); + } light->SetLightType(static_cast<::XCEngine::Components::LightType>(lightType)); changed = true; } @@ -39,12 +42,18 @@ public: light->GetColor().a }; if (UI::DrawColor4("Color", color)) { + if (undoManager) { + undoManager->BeginInteractiveChange("Modify Light"); + } light->SetColor(::XCEngine::Math::Color(color[0], color[1], color[2], color[3])); changed = true; } float intensity = light->GetIntensity(); if (UI::DrawFloat("Intensity", intensity, 100.0f, 0.1f, 0.0f)) { + if (undoManager) { + undoManager->BeginInteractiveChange("Modify Light"); + } light->SetIntensity(intensity); changed = true; } @@ -52,6 +61,9 @@ public: if (light->GetLightType() != ::XCEngine::Components::LightType::Directional) { float range = light->GetRange(); if (UI::DrawFloat("Range", range, 100.0f, 0.1f, 0.001f)) { + if (undoManager) { + undoManager->BeginInteractiveChange("Modify Light"); + } light->SetRange(range); changed = true; } @@ -60,6 +72,9 @@ public: if (light->GetLightType() == ::XCEngine::Components::LightType::Spot) { float spotAngle = light->GetSpotAngle(); if (UI::DrawSliderFloat("Spot Angle", spotAngle, 1.0f, 179.0f, 100.0f, "%.1f")) { + if (undoManager) { + undoManager->BeginInteractiveChange("Modify Light"); + } light->SetSpotAngle(spotAngle); changed = true; } @@ -67,6 +82,9 @@ public: bool castsShadows = light->GetCastsShadows(); if (UI::DrawBool("Cast Shadows", castsShadows)) { + if (undoManager) { + undoManager->BeginInteractiveChange("Modify Light"); + } light->SetCastsShadows(castsShadows); changed = true; } diff --git a/editor/src/ComponentEditors/TransformComponentEditor.h b/editor/src/ComponentEditors/TransformComponentEditor.h index e94ab88f..c8c38439 100644 --- a/editor/src/ComponentEditors/TransformComponentEditor.h +++ b/editor/src/ComponentEditors/TransformComponentEditor.h @@ -18,7 +18,7 @@ public: return dynamic_cast<::XCEngine::Components::TransformComponent*>(component) != nullptr; } - bool Render(::XCEngine::Components::Component* component) override { + bool Render(::XCEngine::Components::Component* component, IUndoManager* undoManager) override { auto* transform = dynamic_cast<::XCEngine::Components::TransformComponent*>(component); if (!transform) { return false; @@ -30,16 +30,25 @@ public: bool changed = false; if (UI::DrawVec3("Position", position, 0.0f, 80.0f, 0.1f)) { + if (undoManager) { + undoManager->BeginInteractiveChange("Modify Transform"); + } transform->SetLocalPosition(position); changed = true; } if (UI::DrawVec3("Rotation", rotation, 0.0f, 80.0f, 1.0f)) { + if (undoManager) { + undoManager->BeginInteractiveChange("Modify Transform"); + } transform->SetLocalEulerAngles(rotation); changed = true; } if (UI::DrawVec3("Scale", scale, 1.0f, 80.0f, 0.1f)) { + if (undoManager) { + undoManager->BeginInteractiveChange("Modify Transform"); + } transform->SetLocalScale(scale); changed = true; } diff --git a/editor/src/Core/EditorContext.h b/editor/src/Core/EditorContext.h index f5df32be..55939e59 100644 --- a/editor/src/Core/EditorContext.h +++ b/editor/src/Core/EditorContext.h @@ -5,6 +5,8 @@ #include "SelectionManager.h" #include "IProjectManager.h" #include "ISceneManager.h" +#include "IUndoManager.h" +#include "UndoManager.h" #include "Managers/SceneManager.h" #include "Managers/ProjectManager.h" #include "EditorEvents.h" @@ -20,6 +22,7 @@ public: : m_eventBus(std::make_unique()) , m_selectionManager(std::make_unique(*m_eventBus)) , m_sceneManager(std::make_unique(m_eventBus.get())) + , m_undoManager(std::make_unique(*m_sceneManager, *m_selectionManager)) , m_projectManager(std::make_unique()) { m_entityDeletedHandlerId = m_eventBus->Subscribe([this](const EntityDeletedEvent& event) { @@ -48,6 +51,10 @@ public: IProjectManager& GetProjectManager() override { return *m_projectManager; } + + IUndoManager& GetUndoManager() override { + return *m_undoManager; + } void SetProjectPath(const std::string& path) override { m_projectPath = path; @@ -61,6 +68,7 @@ private: std::unique_ptr m_eventBus; std::unique_ptr m_selectionManager; std::unique_ptr m_sceneManager; + std::unique_ptr m_undoManager; std::unique_ptr m_projectManager; std::string m_projectPath; uint64_t m_entityDeletedHandlerId; diff --git a/editor/src/Core/IEditorContext.h b/editor/src/Core/IEditorContext.h index 37a69649..d4d4eeed 100644 --- a/editor/src/Core/IEditorContext.h +++ b/editor/src/Core/IEditorContext.h @@ -10,6 +10,7 @@ class EventBus; class ISelectionManager; class IProjectManager; class ISceneManager; +class IUndoManager; class IEditorContext { public: @@ -19,6 +20,7 @@ public: virtual ISelectionManager& GetSelectionManager() = 0; virtual ISceneManager& GetSceneManager() = 0; virtual IProjectManager& GetProjectManager() = 0; + virtual IUndoManager& GetUndoManager() = 0; virtual void SetProjectPath(const std::string& path) = 0; virtual const std::string& GetProjectPath() const = 0; diff --git a/editor/src/Core/IUndoManager.h b/editor/src/Core/IUndoManager.h new file mode 100644 index 00000000..edae255d --- /dev/null +++ b/editor/src/Core/IUndoManager.h @@ -0,0 +1,40 @@ +#pragma once + +#include "SceneSnapshot.h" + +#include +#include +#include + +namespace XCEngine { +namespace Editor { + +struct UndoStateSnapshot { + SceneSnapshot scene; + std::vector selectionIds; +}; + +class IUndoManager { +public: + virtual ~IUndoManager() = default; + + virtual void ClearHistory() = 0; + virtual bool CanUndo() const = 0; + virtual bool CanRedo() const = 0; + virtual const std::string& GetUndoLabel() const = 0; + virtual const std::string& GetRedoLabel() const = 0; + + virtual void Undo() = 0; + virtual void Redo() = 0; + + virtual UndoStateSnapshot CaptureCurrentState() const = 0; + virtual void PushCommand(const std::string& label, UndoStateSnapshot before, UndoStateSnapshot after) = 0; + + virtual void BeginInteractiveChange(const std::string& label) = 0; + virtual bool HasPendingInteractiveChange() const = 0; + virtual void FinalizeInteractiveChange() = 0; + virtual void CancelInteractiveChange() = 0; +}; + +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/Core/SceneSnapshot.h b/editor/src/Core/SceneSnapshot.h new file mode 100644 index 00000000..0376ee5a --- /dev/null +++ b/editor/src/Core/SceneSnapshot.h @@ -0,0 +1,16 @@ +#pragma once + +#include + +namespace XCEngine { +namespace Editor { + +struct SceneSnapshot { + bool hasScene = false; + std::string sceneData; + std::string scenePath; + bool dirty = false; +}; + +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/Core/UndoManager.cpp b/editor/src/Core/UndoManager.cpp new file mode 100644 index 00000000..fe946f9f --- /dev/null +++ b/editor/src/Core/UndoManager.cpp @@ -0,0 +1,165 @@ +#include "Core/UndoManager.h" + +#include "Core/ISelectionManager.h" +#include "Managers/SceneManager.h" + +#include + +namespace XCEngine { +namespace Editor { + +namespace { + +constexpr size_t kMaxUndoHistory = 128; + +} // namespace + +UndoManager::UndoManager(SceneManager& sceneManager, ISelectionManager& selectionManager) + : m_sceneManager(sceneManager) + , m_selectionManager(selectionManager) {} + +void UndoManager::ClearHistory() { + m_history.clear(); + m_nextIndex = 0; + m_pendingInteractiveChange.reset(); +} + +bool UndoManager::CanUndo() const { + return m_nextIndex > 0; +} + +bool UndoManager::CanRedo() const { + return m_nextIndex < m_history.size(); +} + +const std::string& UndoManager::GetUndoLabel() const { + return CanUndo() ? m_history[m_nextIndex - 1].label : m_emptyLabel; +} + +const std::string& UndoManager::GetRedoLabel() const { + return CanRedo() ? m_history[m_nextIndex].label : m_emptyLabel; +} + +void UndoManager::Undo() { + if (HasPendingInteractiveChange()) { + FinalizeInteractiveChange(); + } + + if (!CanUndo()) { + return; + } + + --m_nextIndex; + ApplyState(m_history[m_nextIndex].before); +} + +void UndoManager::Redo() { + if (HasPendingInteractiveChange()) { + FinalizeInteractiveChange(); + } + + if (!CanRedo()) { + return; + } + + ApplyState(m_history[m_nextIndex].after); + ++m_nextIndex; +} + +UndoStateSnapshot UndoManager::CaptureCurrentState() const { + UndoStateSnapshot snapshot; + snapshot.scene = m_sceneManager.CaptureSceneSnapshot(); + + if (!snapshot.scene.hasScene) { + return snapshot; + } + + for (uint64_t entityId : m_selectionManager.GetSelectedEntities()) { + if (m_sceneManager.GetEntity(entityId)) { + snapshot.selectionIds.push_back(entityId); + } + } + + return snapshot; +} + +void UndoManager::PushCommand(const std::string& label, UndoStateSnapshot before, UndoStateSnapshot after) { + if (AreStatesEqual(before, after)) { + return; + } + + if (m_nextIndex < m_history.size()) { + m_history.erase(m_history.begin() + static_cast(m_nextIndex), m_history.end()); + } + + m_history.push_back(CommandEntry{ label, std::move(before), std::move(after) }); + if (m_history.size() > kMaxUndoHistory) { + m_history.erase(m_history.begin()); + if (m_nextIndex > 0) { + --m_nextIndex; + } + } + + m_nextIndex = m_history.size(); +} + +void UndoManager::BeginInteractiveChange(const std::string& label) { + if (m_pendingInteractiveChange.has_value()) { + return; + } + + m_pendingInteractiveChange = PendingInteractiveChange{ label, CaptureCurrentState() }; +} + +bool UndoManager::HasPendingInteractiveChange() const { + return m_pendingInteractiveChange.has_value(); +} + +void UndoManager::FinalizeInteractiveChange() { + if (!m_pendingInteractiveChange.has_value()) { + return; + } + + PushCommand( + m_pendingInteractiveChange->label, + std::move(m_pendingInteractiveChange->before), + CaptureCurrentState()); + m_pendingInteractiveChange.reset(); +} + +void UndoManager::CancelInteractiveChange() { + m_pendingInteractiveChange.reset(); +} + +bool UndoManager::ApplyState(const UndoStateSnapshot& state) { + if (!m_sceneManager.RestoreSceneSnapshot(state.scene)) { + return false; + } + + std::vector validSelection; + validSelection.reserve(state.selectionIds.size()); + for (uint64_t entityId : state.selectionIds) { + if (m_sceneManager.GetEntity(entityId)) { + validSelection.push_back(entityId); + } + } + + if (validSelection.empty()) { + m_selectionManager.ClearSelection(); + } else { + m_selectionManager.SetSelectedEntities(validSelection); + } + + return true; +} + +bool UndoManager::AreStatesEqual(const UndoStateSnapshot& lhs, const UndoStateSnapshot& rhs) { + return lhs.scene.hasScene == rhs.scene.hasScene && + lhs.scene.scenePath == rhs.scene.scenePath && + lhs.scene.sceneData == rhs.scene.sceneData && + lhs.scene.dirty == rhs.scene.dirty && + lhs.selectionIds == rhs.selectionIds; +} + +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/Core/UndoManager.h b/editor/src/Core/UndoManager.h new file mode 100644 index 00000000..d35ad8d6 --- /dev/null +++ b/editor/src/Core/UndoManager.h @@ -0,0 +1,60 @@ +#pragma once + +#include "IUndoManager.h" + +#include +#include +#include + +namespace XCEngine { +namespace Editor { + +class ISelectionManager; +class SceneManager; + +class UndoManager : public IUndoManager { +public: + UndoManager(SceneManager& sceneManager, ISelectionManager& selectionManager); + + void ClearHistory() override; + bool CanUndo() const override; + bool CanRedo() const override; + const std::string& GetUndoLabel() const override; + const std::string& GetRedoLabel() const override; + + void Undo() override; + void Redo() override; + + UndoStateSnapshot CaptureCurrentState() const override; + void PushCommand(const std::string& label, UndoStateSnapshot before, UndoStateSnapshot after) override; + + void BeginInteractiveChange(const std::string& label) override; + bool HasPendingInteractiveChange() const override; + void FinalizeInteractiveChange() override; + void CancelInteractiveChange() override; + +private: + struct CommandEntry { + std::string label; + UndoStateSnapshot before; + UndoStateSnapshot after; + }; + + struct PendingInteractiveChange { + std::string label; + UndoStateSnapshot before; + }; + + bool ApplyState(const UndoStateSnapshot& state); + static bool AreStatesEqual(const UndoStateSnapshot& lhs, const UndoStateSnapshot& rhs); + + SceneManager& m_sceneManager; + ISelectionManager& m_selectionManager; + std::vector m_history; + size_t m_nextIndex = 0; + std::optional m_pendingInteractiveChange; + std::string m_emptyLabel; +}; + +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/Layers/EditorLayer.cpp b/editor/src/Layers/EditorLayer.cpp index 4b9d6bb9..a5b7c5f2 100644 --- a/editor/src/Layers/EditorLayer.cpp +++ b/editor/src/Layers/EditorLayer.cpp @@ -8,6 +8,7 @@ #include "panels/ProjectPanel.h" #include "Core/IEditorContext.h" #include "Core/EditorContext.h" +#include "Core/IUndoManager.h" #include #include #include @@ -45,6 +46,7 @@ void EditorLayer::onAttach() { m_projectPanel->Initialize(m_context->GetProjectPath()); m_context->GetSceneManager().LoadStartupScene(m_context->GetProjectPath()); m_context->GetProjectManager().RefreshCurrentFolder(); + m_context->GetUndoManager().ClearHistory(); m_menuBar->OnAttach(); m_hierarchyPanel->OnAttach(); diff --git a/editor/src/Managers/SceneManager.cpp b/editor/src/Managers/SceneManager.cpp index 9e47272f..29c1f8a3 100644 --- a/editor/src/Managers/SceneManager.cpp +++ b/editor/src/Managers/SceneManager.cpp @@ -172,6 +172,7 @@ void SceneManager::CopyEntity(::XCEngine::Components::GameObject::ID id) { const auto newEntityId = PasteEntityRecursive(*m_clipboard, parent); SyncRootEntities(); + SetSceneDirty(true); OnEntityCreated.Invoke(newEntityId); OnSceneChanged.Invoke(); @@ -184,6 +185,53 @@ void SceneManager::CopyEntity(::XCEngine::Components::GameObject::ID id) { return newEntityId; } +SceneSnapshot SceneManager::CaptureSceneSnapshot() const { + SceneSnapshot snapshot; + if (!m_scene) { + return snapshot; + } + + snapshot.hasScene = true; + snapshot.sceneData = m_scene->SerializeToString(); + snapshot.scenePath = m_currentScenePath; + snapshot.dirty = m_isSceneDirty; + return snapshot; +} + +bool SceneManager::RestoreSceneSnapshot(const SceneSnapshot& snapshot) { + if (!snapshot.hasScene) { + m_scene.reset(); + m_rootEntities.clear(); + m_clipboard.reset(); + m_currentScenePath.clear(); + m_currentSceneName = "Untitled Scene"; + SetSceneDirty(snapshot.dirty); + + OnSceneChanged.Invoke(); + if (m_eventBus) { + m_eventBus->Publish(SceneChangedEvent{}); + } + return true; + } + + auto scene = std::make_unique<::XCEngine::Components::Scene>(); + scene->DeserializeFromString(snapshot.sceneData); + + m_scene = std::move(scene); + m_clipboard.reset(); + SyncRootEntities(); + m_currentScenePath = snapshot.scenePath; + m_currentSceneName = m_scene ? m_scene->GetName() : "Untitled Scene"; + SetSceneDirty(snapshot.dirty); + + OnSceneChanged.Invoke(); + if (m_eventBus) { + m_eventBus->Publish(SceneChangedEvent{}); + } + + return true; +} + ::XCEngine::Components::GameObject::ID SceneManager::DuplicateEntity(::XCEngine::Components::GameObject::ID id) { if (!m_scene) return 0; diff --git a/editor/src/Managers/SceneManager.h b/editor/src/Managers/SceneManager.h index 9a7e486f..1b0a900a 100644 --- a/editor/src/Managers/SceneManager.h +++ b/editor/src/Managers/SceneManager.h @@ -14,6 +14,7 @@ #include #include +#include "Core/SceneSnapshot.h" #include "Core/ISceneManager.h" namespace XCEngine { @@ -61,6 +62,8 @@ public: void CreateDemoScene() override; bool HasClipboardData() const { return m_clipboard.has_value(); } + SceneSnapshot CaptureSceneSnapshot() const; + bool RestoreSceneSnapshot(const SceneSnapshot& snapshot); ::XCEngine::Core::Event<::XCEngine::Components::GameObject::ID> OnEntityCreated; ::XCEngine::Core::Event<::XCEngine::Components::GameObject::ID> OnEntityDeleted; diff --git a/editor/src/Utils/UndoUtils.h b/editor/src/Utils/UndoUtils.h new file mode 100644 index 00000000..2067e84f --- /dev/null +++ b/editor/src/Utils/UndoUtils.h @@ -0,0 +1,34 @@ +#pragma once + +#include "Core/IEditorContext.h" +#include "Core/IUndoManager.h" + +#include +#include + +namespace XCEngine { +namespace Editor { +namespace UndoUtils { + +template +auto ExecuteSceneCommand(IEditorContext& context, const std::string& label, Func&& func) { + auto& undoManager = context.GetUndoManager(); + if (undoManager.HasPendingInteractiveChange()) { + undoManager.FinalizeInteractiveChange(); + } + + auto before = undoManager.CaptureCurrentState(); + + if constexpr (std::is_void_v>) { + std::forward(func)(); + undoManager.PushCommand(label, std::move(before), undoManager.CaptureCurrentState()); + } else { + auto result = std::forward(func)(); + undoManager.PushCommand(label, std::move(before), undoManager.CaptureCurrentState()); + return result; + } +} + +} // namespace UndoUtils +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/panels/HierarchyPanel.cpp b/editor/src/panels/HierarchyPanel.cpp index fa1c9e0b..7d88d2c8 100644 --- a/editor/src/panels/HierarchyPanel.cpp +++ b/editor/src/panels/HierarchyPanel.cpp @@ -2,8 +2,10 @@ #include "Core/IEditorContext.h" #include "Core/ISceneManager.h" #include "Core/ISelectionManager.h" +#include "Core/IUndoManager.h" #include "Core/EditorEvents.h" #include "Core/EventBus.h" +#include "Utils/UndoUtils.h" #include #include #include @@ -73,16 +75,19 @@ void HierarchyPanel::Render() { ::XCEngine::Components::GameObject* sourceGameObject = *(::XCEngine::Components::GameObject**)payload->Data; if (sourceGameObject && sourceGameObject->GetParent() != nullptr) { auto& sceneManager = m_context->GetSceneManager(); - auto* srcTransform = sourceGameObject->GetTransform(); - Math::Vector3 worldPos = srcTransform->GetPosition(); - Math::Quaternion worldRot = srcTransform->GetRotation(); - Math::Vector3 worldScale = srcTransform->GetScale(); + UndoUtils::ExecuteSceneCommand(*m_context, "Reparent Entity", [&]() { + auto* srcTransform = sourceGameObject->GetTransform(); + Math::Vector3 worldPos = srcTransform->GetPosition(); + Math::Quaternion worldRot = srcTransform->GetRotation(); + Math::Vector3 worldScale = srcTransform->GetScale(); - sceneManager.MoveEntity(sourceGameObject->GetID(), 0); + sceneManager.MoveEntity(sourceGameObject->GetID(), 0); - srcTransform->SetPosition(worldPos); - srcTransform->SetRotation(worldRot); - srcTransform->SetScale(worldScale); + srcTransform->SetPosition(worldPos); + srcTransform->SetRotation(worldRot); + srcTransform->SetScale(worldScale); + sceneManager.MarkSceneDirty(); + }); } } ImGui::EndDragDropTarget(); @@ -133,7 +138,9 @@ void HierarchyPanel::RenderEntity(::XCEngine::Components::GameObject* gameObject ImGui::SetNextItemWidth(-1); if (ImGui::InputText("##Rename", m_renameBuffer, sizeof(m_renameBuffer), ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_AutoSelectAll)) { if (strlen(m_renameBuffer) > 0) { - m_context->GetSceneManager().RenameEntity(gameObject->GetID(), m_renameBuffer); + UndoUtils::ExecuteSceneCommand(*m_context, "Rename Entity", [&]() { + m_context->GetSceneManager().RenameEntity(gameObject->GetID(), m_renameBuffer); + }); } m_renaming = false; m_renamingEntity = nullptr; @@ -141,7 +148,9 @@ void HierarchyPanel::RenderEntity(::XCEngine::Components::GameObject* gameObject if (!ImGui::IsItemActive() && ImGui::IsMouseClicked(0)) { if (strlen(m_renameBuffer) > 0) { - m_context->GetSceneManager().RenameEntity(gameObject->GetID(), m_renameBuffer); + UndoUtils::ExecuteSceneCommand(*m_context, "Rename Entity", [&]() { + m_context->GetSceneManager().RenameEntity(gameObject->GetID(), m_renameBuffer); + }); } m_renaming = false; m_renamingEntity = nullptr; @@ -195,15 +204,19 @@ void HierarchyPanel::RenderContextMenu(::XCEngine::Components::GameObject* gameO } if (gameObject != nullptr && ImGui::MenuItem("Create Child")) { - auto* child = sceneManager.CreateEntity("GameObject", gameObject); - selectionManager.SetSelectedEntity(child->GetID()); + UndoUtils::ExecuteSceneCommand(*m_context, "Create Child", [&]() { + auto* child = sceneManager.CreateEntity("GameObject", gameObject); + selectionManager.SetSelectedEntity(child->GetID()); + }); } ImGui::Separator(); if (gameObject != nullptr && gameObject->GetParent() != nullptr) { if (ImGui::MenuItem("Detach from Parent")) { - sceneManager.MoveEntity(gameObject->GetID(), 0); + UndoUtils::ExecuteSceneCommand(*m_context, "Reparent Entity", [&]() { + sceneManager.MoveEntity(gameObject->GetID(), 0); + }); } } @@ -217,7 +230,9 @@ void HierarchyPanel::RenderContextMenu(::XCEngine::Components::GameObject* gameO } if (ImGui::MenuItem("Delete", "Delete")) { - sceneManager.DeleteEntity(gameObject->GetID()); + UndoUtils::ExecuteSceneCommand(*m_context, "Delete Entity", [&]() { + sceneManager.DeleteEntity(gameObject->GetID()); + }); } ImGui::Separator(); @@ -227,11 +242,22 @@ void HierarchyPanel::RenderContextMenu(::XCEngine::Components::GameObject* gameO } if (ImGui::MenuItem("Paste", "Ctrl+V", false, sceneManager.HasClipboardData())) { - sceneManager.PasteEntity(gameObject->GetID()); + UndoUtils::ExecuteSceneCommand(*m_context, "Paste Entity", [&]() { + const uint64_t newId = sceneManager.PasteEntity(gameObject->GetID()); + if (newId != 0) { + selectionManager.SetSelectedEntity(newId); + } + }); } if (ImGui::MenuItem("Duplicate", "Ctrl+D")) { - uint64_t newId = sceneManager.DuplicateEntity(gameObject->GetID()); + uint64_t newId = 0; + UndoUtils::ExecuteSceneCommand(*m_context, "Duplicate Entity", [&]() { + newId = sceneManager.DuplicateEntity(gameObject->GetID()); + if (newId != 0) { + selectionManager.SetSelectedEntity(newId); + } + }); if (newId != 0) { } } @@ -242,39 +268,53 @@ void HierarchyPanel::RenderCreateMenu(::XCEngine::Components::GameObject* parent auto& selectionManager = m_context->GetSelectionManager(); if (ImGui::MenuItem("Empty Object")) { - auto* newEntity = sceneManager.CreateEntity("GameObject", parent); - selectionManager.SetSelectedEntity(newEntity->GetID()); + UndoUtils::ExecuteSceneCommand(*m_context, "Create Entity", [&]() { + auto* newEntity = sceneManager.CreateEntity("GameObject", parent); + selectionManager.SetSelectedEntity(newEntity->GetID()); + }); } ImGui::Separator(); if (ImGui::MenuItem("Camera")) { - auto* newEntity = sceneManager.CreateEntity("Camera", parent); - newEntity->AddComponent<::XCEngine::Components::CameraComponent>(); - selectionManager.SetSelectedEntity(newEntity->GetID()); + UndoUtils::ExecuteSceneCommand(*m_context, "Create Camera", [&]() { + auto* newEntity = sceneManager.CreateEntity("Camera", parent); + newEntity->AddComponent<::XCEngine::Components::CameraComponent>(); + sceneManager.MarkSceneDirty(); + selectionManager.SetSelectedEntity(newEntity->GetID()); + }); } if (ImGui::MenuItem("Light")) { - auto* newEntity = sceneManager.CreateEntity("Light", parent); - newEntity->AddComponent<::XCEngine::Components::LightComponent>(); - selectionManager.SetSelectedEntity(newEntity->GetID()); + UndoUtils::ExecuteSceneCommand(*m_context, "Create Light", [&]() { + auto* newEntity = sceneManager.CreateEntity("Light", parent); + newEntity->AddComponent<::XCEngine::Components::LightComponent>(); + sceneManager.MarkSceneDirty(); + selectionManager.SetSelectedEntity(newEntity->GetID()); + }); } ImGui::Separator(); if (ImGui::MenuItem("Cube")) { - auto* newEntity = sceneManager.CreateEntity("Cube", parent); - selectionManager.SetSelectedEntity(newEntity->GetID()); + UndoUtils::ExecuteSceneCommand(*m_context, "Create Cube", [&]() { + auto* newEntity = sceneManager.CreateEntity("Cube", parent); + selectionManager.SetSelectedEntity(newEntity->GetID()); + }); } if (ImGui::MenuItem("Sphere")) { - auto* newEntity = sceneManager.CreateEntity("Sphere", parent); - selectionManager.SetSelectedEntity(newEntity->GetID()); + UndoUtils::ExecuteSceneCommand(*m_context, "Create Sphere", [&]() { + auto* newEntity = sceneManager.CreateEntity("Sphere", parent); + selectionManager.SetSelectedEntity(newEntity->GetID()); + }); } if (ImGui::MenuItem("Plane")) { - auto* newEntity = sceneManager.CreateEntity("Plane", parent); - selectionManager.SetSelectedEntity(newEntity->GetID()); + UndoUtils::ExecuteSceneCommand(*m_context, "Create Plane", [&]() { + auto* newEntity = sceneManager.CreateEntity("Plane", parent); + selectionManager.SetSelectedEntity(newEntity->GetID()); + }); } } @@ -302,16 +342,19 @@ void HierarchyPanel::HandleDragDrop(::XCEngine::Components::GameObject* gameObje } if (isValidMove) { - auto* srcTransform = sourceGameObject->GetTransform(); - Math::Vector3 worldPos = srcTransform->GetPosition(); - Math::Quaternion worldRot = srcTransform->GetRotation(); - Math::Vector3 worldScale = srcTransform->GetScale(); - - sceneManager.MoveEntity(sourceGameObject->GetID(), gameObject->GetID()); - - srcTransform->SetPosition(worldPos); - srcTransform->SetRotation(worldRot); - srcTransform->SetScale(worldScale); + UndoUtils::ExecuteSceneCommand(*m_context, "Reparent Entity", [&]() { + auto* srcTransform = sourceGameObject->GetTransform(); + Math::Vector3 worldPos = srcTransform->GetPosition(); + Math::Quaternion worldRot = srcTransform->GetRotation(); + Math::Vector3 worldScale = srcTransform->GetScale(); + + sceneManager.MoveEntity(sourceGameObject->GetID(), gameObject->GetID()); + + srcTransform->SetPosition(worldPos); + srcTransform->SetRotation(worldRot); + srcTransform->SetScale(worldScale); + sceneManager.MarkSceneDirty(); + }); } } } @@ -328,7 +371,9 @@ void HierarchyPanel::HandleKeyboardShortcuts() { if (ImGui::IsWindowFocused()) { if (ImGui::IsKeyPressed(ImGuiKey_Delete)) { if (selectedGameObject != nullptr) { - sceneManager.DeleteEntity(selectedGameObject->GetID()); + UndoUtils::ExecuteSceneCommand(*m_context, "Delete Entity", [&]() { + sceneManager.DeleteEntity(selectedGameObject->GetID()); + }); } } @@ -351,13 +396,23 @@ void HierarchyPanel::HandleKeyboardShortcuts() { if (ImGui::IsKeyPressed(ImGuiKey_V)) { if (sceneManager.HasClipboardData()) { - sceneManager.PasteEntity(selectedGameObject ? selectedGameObject->GetID() : 0); + UndoUtils::ExecuteSceneCommand(*m_context, "Paste Entity", [&]() { + const uint64_t newId = sceneManager.PasteEntity(selectedGameObject ? selectedGameObject->GetID() : 0); + if (newId != 0) { + selectionManager.SetSelectedEntity(newId); + } + }); } } if (ImGui::IsKeyPressed(ImGuiKey_D)) { if (selectedGameObject != nullptr) { - sceneManager.DuplicateEntity(selectedGameObject->GetID()); + UndoUtils::ExecuteSceneCommand(*m_context, "Duplicate Entity", [&]() { + const uint64_t newId = sceneManager.DuplicateEntity(selectedGameObject->GetID()); + if (newId != 0) { + selectionManager.SetSelectedEntity(newId); + } + }); } } } diff --git a/editor/src/panels/InspectorPanel.cpp b/editor/src/panels/InspectorPanel.cpp index 1214ab81..95becb03 100644 --- a/editor/src/panels/InspectorPanel.cpp +++ b/editor/src/panels/InspectorPanel.cpp @@ -2,12 +2,14 @@ #include "Core/IEditorContext.h" #include "Core/ISceneManager.h" #include "Core/ISelectionManager.h" +#include "Core/IUndoManager.h" #include "Core/EventBus.h" #include "Core/EditorEvents.h" #include "ComponentEditors/CameraComponentEditor.h" #include "ComponentEditors/IComponentEditor.h" #include "ComponentEditors/LightComponentEditor.h" #include "ComponentEditors/TransformComponentEditor.h" +#include "Utils/UndoUtils.h" #include #include @@ -25,6 +27,9 @@ InspectorPanel::~InspectorPanel() { } void InspectorPanel::OnSelectionChanged(const SelectionChangedEvent& event) { + if (m_context && m_context->GetUndoManager().HasPendingInteractiveChange()) { + m_context->GetUndoManager().FinalizeInteractiveChange(); + } m_selectedEntityId = event.primarySelection; } @@ -90,7 +95,9 @@ void InspectorPanel::RenderGameObject(::XCEngine::Components::GameObject* gameOb strcpy_s(nameBuffer, gameObject->GetName().c_str()); ImGui::InputText("##Name", nameBuffer, sizeof(nameBuffer)); if (ImGui::IsItemDeactivatedAfterEdit()) { - m_context->GetSceneManager().RenameEntity(gameObject->GetID(), nameBuffer); + UndoUtils::ExecuteSceneCommand(*m_context, "Rename Entity", [&]() { + m_context->GetSceneManager().RenameEntity(gameObject->GetID(), nameBuffer); + }); } ImGui::SameLine(); @@ -104,6 +111,10 @@ void InspectorPanel::RenderGameObject(::XCEngine::Components::GameObject* gameOb for (auto* component : components) { RenderComponent(component, gameObject); } + + if (m_context->GetUndoManager().HasPendingInteractiveChange() && !ImGui::IsAnyItemActive()) { + m_context->GetUndoManager().FinalizeInteractiveChange(); + } } void InspectorPanel::RenderAddComponentPopup(::XCEngine::Components::GameObject* gameObject) { @@ -133,8 +144,14 @@ void InspectorPanel::RenderAddComponentPopup(::XCEngine::Components::GameObject* } if (ImGui::MenuItem(label.c_str(), nullptr, false, canAdd)) { - if (editor->AddTo(gameObject)) { - m_context->GetSceneManager().MarkSceneDirty(); + bool added = false; + UndoUtils::ExecuteSceneCommand(*m_context, std::string("Add ") + editor->GetDisplayName() + " Component", [&]() { + added = editor->AddTo(gameObject) != nullptr; + if (added) { + m_context->GetSceneManager().MarkSceneDirty(); + } + }); + if (added) { ImGui::CloseCurrentPopup(); } } @@ -183,7 +200,7 @@ void InspectorPanel::RenderComponent(::XCEngine::Components::Component* componen if (open) { if (editor) { - if (editor->Render(component)) { + if (editor->Render(component, &m_context->GetUndoManager())) { m_context->GetSceneManager().MarkSceneDirty(); } } else { @@ -201,8 +218,10 @@ void InspectorPanel::RemoveComponentByType(::XCEngine::Components::Component* co return; } - gameObject->RemoveComponent(component); - m_context->GetSceneManager().MarkSceneDirty(); + UndoUtils::ExecuteSceneCommand(*m_context, std::string("Remove ") + component->GetName() + " Component", [&]() { + gameObject->RemoveComponent(component); + m_context->GetSceneManager().MarkSceneDirty(); + }); } } diff --git a/editor/src/panels/MenuBar.cpp b/editor/src/panels/MenuBar.cpp index 1c75b713..07f99636 100644 --- a/editor/src/panels/MenuBar.cpp +++ b/editor/src/panels/MenuBar.cpp @@ -1,6 +1,8 @@ #include "MenuBar.h" #include "Core/IEditorContext.h" #include "Core/ISceneManager.h" +#include "Core/IUndoManager.h" +#include "Core/ISelectionManager.h" #include "Utils/SceneEditorUtils.h" #include #include @@ -29,6 +31,8 @@ void MenuBar::NewScene() { } m_context->GetSceneManager().NewScene(); + m_context->GetSelectionManager().ClearSelection(); + m_context->GetUndoManager().ClearHistory(); } void MenuBar::OpenScene() { @@ -40,7 +44,10 @@ void MenuBar::OpenScene() { m_context->GetProjectPath(), m_context->GetSceneManager().GetCurrentScenePath()); if (!filePath.empty()) { - m_context->GetSceneManager().LoadScene(filePath); + if (m_context->GetSceneManager().LoadScene(filePath)) { + m_context->GetSelectionManager().ClearSelection(); + m_context->GetUndoManager().ClearHistory(); + } } } @@ -78,7 +85,7 @@ void MenuBar::HandleShortcuts() { return; } - auto& sceneManager = m_context->GetSceneManager(); + auto& undoManager = m_context->GetUndoManager(); if (ImGui::IsKeyPressed(ImGuiKey_N, false)) { NewScene(); } @@ -92,6 +99,20 @@ void MenuBar::HandleShortcuts() { SaveScene(); } } + if (!io.WantTextInput) { + if (ImGui::IsKeyPressed(ImGuiKey_Z, false)) { + if (io.KeyShift) { + if (undoManager.CanRedo()) { + undoManager.Redo(); + } + } else if (undoManager.CanUndo()) { + undoManager.Undo(); + } + } + if (ImGui::IsKeyPressed(ImGuiKey_Y, false) && undoManager.CanRedo()) { + undoManager.Redo(); + } + } } void MenuBar::ShowFileMenu() { @@ -116,8 +137,15 @@ void MenuBar::ShowFileMenu() { void MenuBar::ShowEditMenu() { if (ImGui::BeginMenu("Edit")) { - if (ImGui::MenuItem("Undo", "Ctrl+Z")) {} - if (ImGui::MenuItem("Redo", "Ctrl+Y")) {} + auto& undoManager = m_context->GetUndoManager(); + const std::string undoLabel = undoManager.CanUndo() ? "Undo " + undoManager.GetUndoLabel() : "Undo"; + const std::string redoLabel = undoManager.CanRedo() ? "Redo " + undoManager.GetRedoLabel() : "Redo"; + if (ImGui::MenuItem(undoLabel.c_str(), "Ctrl+Z", false, undoManager.CanUndo())) { + undoManager.Undo(); + } + if (ImGui::MenuItem(redoLabel.c_str(), "Ctrl+Y", false, undoManager.CanRedo())) { + undoManager.Redo(); + } ImGui::Separator(); if (ImGui::MenuItem("Cut", "Ctrl+X")) {} if (ImGui::MenuItem("Copy", "Ctrl+C")) {} diff --git a/editor/src/panels/ProjectPanel.cpp b/editor/src/panels/ProjectPanel.cpp index a7c51960..c123347c 100644 --- a/editor/src/panels/ProjectPanel.cpp +++ b/editor/src/panels/ProjectPanel.cpp @@ -2,6 +2,8 @@ #include "Core/IEditorContext.h" #include "Core/IProjectManager.h" #include "Core/ISceneManager.h" +#include "Core/IUndoManager.h" +#include "Core/ISelectionManager.h" #include "Core/AssetItem.h" #include "Utils/SceneEditorUtils.h" #include @@ -115,7 +117,10 @@ void ProjectPanel::Render() { if (item->isFolder) { manager.NavigateToFolder(item); } else if (SceneEditorUtils::ConfirmSceneSwitch(*m_context)) { - m_context->GetSceneManager().LoadScene(item->fullPath); + if (m_context->GetSceneManager().LoadScene(item->fullPath)) { + m_context->GetSelectionManager().ClearSelection(); + m_context->GetUndoManager().ClearHistory(); + } } } ImGui::Separator(); @@ -287,7 +292,10 @@ void ProjectPanel::RenderAssetItem(const AssetItemPtr& item, int index) { manager.NavigateToFolder(item); } else if (item->type == "Scene") { if (SceneEditorUtils::ConfirmSceneSwitch(*m_context)) { - m_context->GetSceneManager().LoadScene(item->fullPath); + if (m_context->GetSceneManager().LoadScene(item->fullPath)) { + m_context->GetSelectionManager().ClearSelection(); + m_context->GetUndoManager().ClearHistory(); + } } } } diff --git a/engine/include/XCEngine/Scene/Scene.h b/engine/include/XCEngine/Scene/Scene.h index 5110ac4d..f09ba80c 100644 --- a/engine/include/XCEngine/Scene/Scene.h +++ b/engine/include/XCEngine/Scene/Scene.h @@ -66,6 +66,8 @@ public: void Save(const std::string& filePath); void Load(const std::string& filePath); + std::string SerializeToString() const; + void DeserializeFromString(const std::string& data); Core::Event& OnGameObjectCreated() { return m_onGameObjectCreated; } Core::Event& OnGameObjectDestroyed() { return m_onGameObjectDestroyed; } @@ -112,4 +114,4 @@ private: }; } // namespace Components -} // namespace XCEngine \ No newline at end of file +} // namespace XCEngine diff --git a/engine/src/Scene/Scene.cpp b/engine/src/Scene/Scene.cpp index defd146c..bf9ac01a 100644 --- a/engine/src/Scene/Scene.cpp +++ b/engine/src/Scene/Scene.cpp @@ -264,22 +264,18 @@ void Scene::LateUpdate(float deltaTime) { } } -void Scene::Load(const std::string& filePath) { - std::ifstream file(filePath); - if (!file.is_open()) { - return; - } - +void Scene::DeserializeFromString(const std::string& data) { m_gameObjects.clear(); m_rootGameObjects.clear(); m_gameObjectIDs.clear(); std::vector pendingObjects; + std::istringstream input(data); std::string line; PendingGameObjectData* currentObject = nullptr; GameObject::ID maxId = 0; - while (std::getline(file, line)) { + while (std::getline(input, line)) { if (line.empty() || line[0] == '#') continue; if (line == "gameobject_begin") { @@ -389,20 +385,39 @@ void Scene::Load(const std::string& filePath) { } } +std::string Scene::SerializeToString() const { + std::ostringstream output; + + output << "# XCEngine Scene File\n"; + output << "scene=" << EscapeString(m_name) << "\n"; + output << "active=" << (m_active ? "1" : "0") << "\n\n"; + + for (auto* go : GetRootGameObjects()) { + SerializeGameObjectRecursive(output, go); + output << "\n"; + } + + return output.str(); +} + +void Scene::Load(const std::string& filePath) { + std::ifstream file(filePath); + if (!file.is_open()) { + return; + } + + std::stringstream buffer; + buffer << file.rdbuf(); + DeserializeFromString(buffer.str()); +} + void Scene::Save(const std::string& filePath) { std::ofstream file(filePath); if (!file.is_open()) { return; } - file << "# XCEngine Scene File\n"; - file << "scene=" << EscapeString(m_name) << "\n"; - file << "active=" << (m_active ? "1" : "0") << "\n\n"; - - for (auto* go : GetRootGameObjects()) { - SerializeGameObjectRecursive(file, go); - file << "\n"; - } + file << SerializeToString(); } } // namespace Components diff --git a/tests/Scene/test_scene.cpp b/tests/Scene/test_scene.cpp index 92652275..ecd719e2 100644 --- a/tests/Scene/test_scene.cpp +++ b/tests/Scene/test_scene.cpp @@ -330,6 +330,65 @@ TEST_F(SceneTest, Save_And_Load_PreservesHierarchyAndComponents) { std::filesystem::remove(scenePath); } +TEST_F(SceneTest, SerializeToString_And_DeserializeFromString_PreservesHierarchyAndComponents) { + testScene->SetName("Round Trip Scene"); + testScene->SetActive(false); + + GameObject* parent = testScene->CreateGameObject("Parent"); + parent->GetTransform()->SetLocalPosition(Vector3(2.0f, 4.0f, 6.0f)); + parent->GetTransform()->SetLocalScale(Vector3(1.5f, 1.5f, 1.5f)); + + auto* light = parent->AddComponent(); + light->SetLightType(LightType::Point); + light->SetIntensity(4.0f); + light->SetRange(20.0f); + light->SetCastsShadows(true); + + GameObject* child = testScene->CreateGameObject("Child Camera", parent); + child->GetTransform()->SetLocalPosition(Vector3(1.0f, 0.0f, -3.0f)); + + auto* camera = child->AddComponent(); + camera->SetProjectionType(CameraProjectionType::Perspective); + camera->SetFieldOfView(72.0f); + camera->SetNearClipPlane(0.2f); + camera->SetFarClipPlane(512.0f); + camera->SetPrimary(true); + + const std::string serialized = testScene->SerializeToString(); + + Scene loadedScene; + loadedScene.DeserializeFromString(serialized); + + EXPECT_EQ(loadedScene.GetName(), "Round Trip Scene"); + EXPECT_FALSE(loadedScene.IsActive()); + EXPECT_EQ(loadedScene.GetRootGameObjects().size(), 1u); + + GameObject* loadedParent = loadedScene.Find("Parent"); + GameObject* loadedChild = loadedScene.Find("Child Camera"); + ASSERT_NE(loadedParent, nullptr); + ASSERT_NE(loadedChild, nullptr); + + EXPECT_EQ(loadedChild->GetParent(), loadedParent); + EXPECT_EQ(loadedParent->GetTransform()->GetLocalPosition(), Vector3(2.0f, 4.0f, 6.0f)); + EXPECT_EQ(loadedParent->GetTransform()->GetLocalScale(), Vector3(1.5f, 1.5f, 1.5f)); + EXPECT_EQ(loadedChild->GetTransform()->GetPosition(), Vector3(3.5f, 4.0f, 1.5f)); + + auto* loadedLight = loadedParent->GetComponent(); + ASSERT_NE(loadedLight, nullptr); + EXPECT_EQ(loadedLight->GetLightType(), LightType::Point); + EXPECT_FLOAT_EQ(loadedLight->GetIntensity(), 4.0f); + EXPECT_FLOAT_EQ(loadedLight->GetRange(), 20.0f); + EXPECT_TRUE(loadedLight->GetCastsShadows()); + + auto* loadedCamera = loadedChild->GetComponent(); + ASSERT_NE(loadedCamera, nullptr); + EXPECT_EQ(loadedCamera->GetProjectionType(), CameraProjectionType::Perspective); + EXPECT_FLOAT_EQ(loadedCamera->GetFieldOfView(), 72.0f); + EXPECT_FLOAT_EQ(loadedCamera->GetNearClipPlane(), 0.2f); + EXPECT_FLOAT_EQ(loadedCamera->GetFarClipPlane(), 512.0f); + EXPECT_TRUE(loadedCamera->IsPrimary()); +} + TEST_F(SceneTest, Save_ContainsHierarchyAndComponentEntries) { GameObject* parent = testScene->CreateGameObject("Parent"); GameObject* child = testScene->CreateGameObject("Child", parent);