diff --git a/editor/AGENTS.md b/editor/AGENTS.md index 2fae3c9a..2560c2c4 100644 --- a/editor/AGENTS.md +++ b/editor/AGENTS.md @@ -17,8 +17,9 @@ The primary product line is still runtime/product loop closure: the play-session runtime scene rather than the editable document scene - drive `EditorRuntimeCoordinator` from the app frame pump exactly once per outer frame; do not tick runtime from per-window shell/content update paths -- bind a real script assembly builder behind `scripts.*`; until then the - coordinator must keep the command honestly disabled +- keep `scripts.*` coordinator-owned and capability-driven. `scripts.rebuild` + must evaluate/dispatch through the bound scripting runtime service and expose + the real availability/failure message rather than a hardcoded stub - keep `Game`, `Scene`, `Inspector`, `Selection`, and `Console` coherent across runtime transitions @@ -74,17 +75,22 @@ Rules: scene document state. Scene viewport private state now lives in `SceneViewportSession`, owned by the scene viewport feature/panel instance, not by `EditorContext`, `EditorFrameServices`, or the shared scene runtime. - `EditorRuntimeCoordinator` owns scene document state: current path/name, - dirty flag, new/open/save routing, and runtime-mode transitions. + `EditorSceneRuntime` also owns the unified scene-edit transaction/history + layer: hierarchy edits, inspector mutations, and scene gizmo commits must all + flow through the same backend snapshot-based undo/redo path rather than + panel-local or transform-only history. `EditorRuntimeCoordinator` owns scene + document state: current path/name, dirty flag, new/open/save routing, and + runtime-mode transitions. - `EditorRuntimeCoordinator` time advancement is app-owned. It must tick once per outer application frame before window rendering, not once per workspace window or once per shell update. - `ProjectPanel` may identify an openable scene asset, but scene document loading must go through the bound typed scene-open request callback and the coordinator. Do not reintroduce `ProjectRuntime` pending-open queues. -- `scripts.rebuild` has a bound coordinator owner, but no in-process script - assembly builder is currently wired. Keep it disabled and explicit until that - builder exists. +- `scripts.rebuild` is coordinator-owned and routes through the bound + scripting-runtime service. Keep its evaluation/dispatch honest: expose the + live capability/failure message from that service rather than a hardcoded + placeholder. - the old shared `EditorPanelServices` dependency bag is gone. Workspace and utility panels now receive explicit bindings or panel-local contexts; do not recreate a catch-all mutable panel service bundle under a new name @@ -152,6 +158,9 @@ Rules: panel service request hook - no play-mode path that mutates the editable scene in place or skips `EditorScenePlaySession` +- no new transform-only or panel-local scene undo stack; all scene mutations + must record through `EditorSceneRuntime`'s unified snapshot transaction + history ## Good Entry Points diff --git a/editor/app/Core/Scene/EditorSceneBackend.h b/editor/app/Core/Scene/EditorSceneBackend.h index dcce0aad..5c1e466e 100644 --- a/editor/app/Core/Scene/EditorSceneBackend.h +++ b/editor/app/Core/Scene/EditorSceneBackend.h @@ -424,6 +424,13 @@ public: virtual bool OpenSceneAsset(const std::filesystem::path& scenePath) = 0; virtual bool SaveActiveScene(const std::filesystem::path& scenePath) = 0; virtual ::XCEngine::Components::Scene* GetActiveScene() const = 0; + virtual std::string CaptureActiveSceneSnapshot() const { + return {}; + } + virtual bool RestoreActiveSceneSnapshot(const std::string& snapshot) { + (void)snapshot; + return false; + } virtual std::unique_ptr BeginPlaySession() { return nullptr; } diff --git a/editor/app/Features/Scene/SceneEditCommandRoute.cpp b/editor/app/Features/Scene/SceneEditCommandRoute.cpp index 8b8826c1..757fb0ee 100644 --- a/editor/app/Features/Scene/SceneEditCommandRoute.cpp +++ b/editor/app/Features/Scene/SceneEditCommandRoute.cpp @@ -39,14 +39,14 @@ UIEditorHostCommandEvaluationResult SceneEditCommandRoute::EvaluateEditCommand( } if (commandId == "edit.undo") { - return m_sceneRuntime->CanUndoTransformEdit() - ? BuildEvaluationResult(true, "Undo the last scene transform edit.") - : BuildEvaluationResult(false, "Scene transform history is empty."); + return m_sceneRuntime->CanUndoSceneEdit() + ? BuildEvaluationResult(true, "Undo the last scene edit.") + : BuildEvaluationResult(false, "Scene edit history is empty."); } if (commandId == "edit.redo") { - return m_sceneRuntime->CanRedoTransformEdit() - ? BuildEvaluationResult(true, "Redo the last scene transform edit.") + return m_sceneRuntime->CanRedoSceneEdit() + ? BuildEvaluationResult(true, "Redo the last scene edit.") : BuildEvaluationResult(false, "Scene redo history is empty."); } @@ -101,14 +101,14 @@ UIEditorHostCommandDispatchResult SceneEditCommandRoute::DispatchEditCommand( } if (commandId == "edit.undo") { - return m_sceneRuntime->UndoTransformEdit() - ? BuildDispatchResult(true, "Undid the last scene transform edit.") - : BuildDispatchResult(false, "Scene transform history is empty."); + return m_sceneRuntime->UndoSceneEdit() + ? BuildDispatchResult(true, "Undid the last scene edit.") + : BuildDispatchResult(false, "Scene edit history is empty."); } if (commandId == "edit.redo") { - return m_sceneRuntime->RedoTransformEdit() - ? BuildDispatchResult(true, "Redid the last scene transform edit.") + return m_sceneRuntime->RedoSceneEdit() + ? BuildDispatchResult(true, "Redid the last scene edit.") : BuildDispatchResult(false, "Scene redo history is empty."); } diff --git a/editor/app/Features/Scene/SceneViewportTransformGizmo.cpp b/editor/app/Features/Scene/SceneViewportTransformGizmo.cpp index db8807b3..a5e141f3 100644 --- a/editor/app/Features/Scene/SceneViewportTransformGizmo.cpp +++ b/editor/app/Features/Scene/SceneViewportTransformGizmo.cpp @@ -218,21 +218,16 @@ public: } void BeginInteractiveChange(const std::string& label) override { - if (m_sceneRuntime == nullptr || HasPendingInteractiveChange()) { + if (m_sceneRuntime == nullptr) { return; } - SceneTransformSnapshot snapshot = {}; - if (!m_sceneRuntime->CaptureSelectedTransformSnapshot(snapshot)) { - return; - } - - m_pendingLabel = label; - m_beforeSnapshot = snapshot; + m_sceneRuntime->BeginSceneEditTransaction(label); } bool HasPendingInteractiveChange() const override { - return m_beforeSnapshot.IsValid(); + return m_sceneRuntime != nullptr && + m_sceneRuntime->HasPendingSceneEditTransaction(); } bool ApplyWorldTransformPreview( @@ -256,31 +251,23 @@ public: } void FinalizeInteractiveChange() override { - if (m_sceneRuntime == nullptr || !HasPendingInteractiveChange()) { + if (m_sceneRuntime == nullptr) { return; } - SceneTransformSnapshot afterSnapshot = {}; - m_sceneRuntime->CaptureSelectedTransformSnapshot(afterSnapshot); - m_sceneRuntime->RecordTransformEdit(m_beforeSnapshot, afterSnapshot); - m_pendingLabel.clear(); - m_beforeSnapshot = {}; + m_sceneRuntime->CommitSceneEditTransaction(); } void CancelInteractiveChange() override { - if (m_sceneRuntime == nullptr || !HasPendingInteractiveChange()) { + if (m_sceneRuntime == nullptr) { return; } - m_sceneRuntime->ApplyTransformSnapshot(m_beforeSnapshot); - m_pendingLabel.clear(); - m_beforeSnapshot = {}; + m_sceneRuntime->CancelSceneEditTransaction(); } private: EditorSceneRuntime* m_sceneRuntime = nullptr; - std::string m_pendingLabel = {}; - SceneTransformSnapshot m_beforeSnapshot = {}; }; } // namespace diff --git a/editor/app/Services/Scene/EditorSceneRuntime.cpp b/editor/app/Services/Scene/EditorSceneRuntime.cpp index c3964ddd..eac3d47a 100644 --- a/editor/app/Services/Scene/EditorSceneRuntime.cpp +++ b/editor/app/Services/Scene/EditorSceneRuntime.cpp @@ -2,8 +2,6 @@ #include -#include - namespace XCEngine::UI::Editor::App { namespace { @@ -11,29 +9,13 @@ namespace { using ::XCEngine::Math::Quaternion; using ::XCEngine::Math::Vector3; -bool NearlyEqual(float lhs, float rhs, float epsilon = 0.0001f) { - return std::abs(lhs - rhs) <= epsilon; -} - -bool NearlyEqual(const Vector3& lhs, const Vector3& rhs, float epsilon = 0.0001f) { - return NearlyEqual(lhs.x, rhs.x, epsilon) && - NearlyEqual(lhs.y, rhs.y, epsilon) && - NearlyEqual(lhs.z, rhs.z, epsilon); -} - -bool NearlyEqual(const Quaternion& lhs, const Quaternion& rhs, float epsilon = 0.0001f) { - return std::abs(lhs.Dot(rhs)) >= 1.0f - epsilon; -} - -bool TransformSnapshotsMatch( - const SceneTransformSnapshot& lhs, - const SceneTransformSnapshot& rhs) { - return lhs.IsValid() && - rhs.IsValid() && - lhs.targetId == rhs.targetId && - NearlyEqual(lhs.position, rhs.position) && - NearlyEqual(lhs.rotation, rhs.rotation) && - NearlyEqual(lhs.scale, rhs.scale); +bool SelectionStatesMatch( + const EditorSelectionState& lhs, + const EditorSelectionState& rhs) { + return lhs.kind == rhs.kind && + lhs.itemId == rhs.itemId && + lhs.absolutePath == rhs.absolutePath && + lhs.directory == rhs.directory; } } // namespace @@ -105,7 +87,7 @@ void EditorSceneRuntime::Reset() { m_projectRoot.clear(); m_ownedSelectionService.ClearSelection(); m_selectionService = &m_ownedSelectionService; - ResetTransformEditHistory(); + ResetSceneEditHistory(); m_inspectorRevision = 0u; m_sceneContentRevision = 0u; } @@ -281,7 +263,7 @@ bool EditorSceneRuntime::NewScene(std::string_view sceneName) { return false; } - ResetTransformEditHistory(); + ResetSceneEditHistory(); SelectionService().ClearSelection(); IncrementInspectorRevision(); IncrementSceneContentRevision(); @@ -295,7 +277,7 @@ bool EditorSceneRuntime::OpenSceneAsset(const std::filesystem::path& scenePath) return false; } - ResetTransformEditHistory(); + ResetSceneEditHistory(); SelectionService().ClearSelection(); IncrementInspectorRevision(); IncrementSceneContentRevision(); @@ -330,43 +312,80 @@ std::unique_ptr EditorSceneRuntime::BeginPlaySession() { bool EditorSceneRuntime::RenameGameObject( std::string_view itemId, std::string_view newName) { + if (!BeginSceneEditTransaction("Rename GameObject")) { + return false; + } + const bool renamed = m_backend != nullptr && m_backend->RenameGameObject(itemId, newName); - if (renamed) { + RefreshScene(); + if (!renamed) { + ClearPendingSceneEditTransaction(); + return false; + } + + bool changed = false; + if (!FinalizePendingSceneEditTransaction(changed)) { + return false; + } + if (changed) { IncrementInspectorRevision(); IncrementSceneContentRevision(); } - RefreshScene(); - return renamed; + return true; } bool EditorSceneRuntime::DeleteGameObject(std::string_view itemId) { - ResetTransformEditHistory(); + if (!BeginSceneEditTransaction("Delete GameObject")) { + return false; + } + const bool deleted = m_backend != nullptr && m_backend->DeleteGameObject(itemId); - if (deleted) { + RefreshScene(); + EnsureSceneSelection(); + if (!deleted) { + ClearPendingSceneEditTransaction(); + return false; + } + + bool changed = false; + if (!FinalizePendingSceneEditTransaction(changed)) { + return false; + } + if (changed) { IncrementInspectorRevision(); IncrementSceneContentRevision(); } - RefreshScene(); - EnsureSceneSelection(); - return deleted; + return true; } std::string EditorSceneRuntime::DuplicateGameObject(std::string_view itemId) { - ResetTransformEditHistory(); + if (!BeginSceneEditTransaction("Duplicate GameObject")) { + return {}; + } + const std::string duplicatedItemId = m_backend != nullptr ? m_backend->DuplicateGameObject(itemId) : std::string(); - if (!duplicatedItemId.empty()) { + if (duplicatedItemId.empty()) { + ClearPendingSceneEditTransaction(); + RefreshScene(); + return {}; + } + + SetSelection(duplicatedItemId); + RefreshScene(); + bool changed = false; + if (!FinalizePendingSceneEditTransaction(changed)) { + return {}; + } + if (changed) { IncrementInspectorRevision(); IncrementSceneContentRevision(); - SetSelection(duplicatedItemId); - } else { - RefreshScene(); } return duplicatedItemId; } @@ -374,59 +393,107 @@ std::string EditorSceneRuntime::DuplicateGameObject(std::string_view itemId) { bool EditorSceneRuntime::ReparentGameObject( std::string_view itemId, std::string_view parentItemId) { - ResetTransformEditHistory(); + if (!BeginSceneEditTransaction("Reparent GameObject")) { + return false; + } + const bool reparented = m_backend != nullptr && m_backend->ReparentGameObject(itemId, parentItemId); - if (reparented) { + RefreshScene(); + if (!reparented) { + ClearPendingSceneEditTransaction(); + return false; + } + + bool changed = false; + if (!FinalizePendingSceneEditTransaction(changed)) { + return false; + } + if (changed) { IncrementInspectorRevision(); IncrementSceneContentRevision(); } - RefreshScene(); - return reparented; + return true; } bool EditorSceneRuntime::MoveGameObjectBefore( std::string_view itemId, std::string_view targetItemId) { - ResetTransformEditHistory(); + if (!BeginSceneEditTransaction("Move GameObject Before")) { + return false; + } + const bool moved = m_backend != nullptr && m_backend->MoveGameObjectBefore(itemId, targetItemId); - if (moved) { + RefreshScene(); + if (!moved) { + ClearPendingSceneEditTransaction(); + return false; + } + + bool changed = false; + if (!FinalizePendingSceneEditTransaction(changed)) { + return false; + } + if (changed) { IncrementInspectorRevision(); IncrementSceneContentRevision(); } - RefreshScene(); - return moved; + return true; } bool EditorSceneRuntime::MoveGameObjectAfter( std::string_view itemId, std::string_view targetItemId) { - ResetTransformEditHistory(); + if (!BeginSceneEditTransaction("Move GameObject After")) { + return false; + } + const bool moved = m_backend != nullptr && m_backend->MoveGameObjectAfter(itemId, targetItemId); - if (moved) { + RefreshScene(); + if (!moved) { + ClearPendingSceneEditTransaction(); + return false; + } + + bool changed = false; + if (!FinalizePendingSceneEditTransaction(changed)) { + return false; + } + if (changed) { IncrementInspectorRevision(); IncrementSceneContentRevision(); } - RefreshScene(); - return moved; + return true; } bool EditorSceneRuntime::MoveGameObjectToRoot(std::string_view itemId) { - ResetTransformEditHistory(); + if (!BeginSceneEditTransaction("Move GameObject To Root")) { + return false; + } + const bool moved = m_backend != nullptr && m_backend->MoveGameObjectToRoot(itemId); - if (moved) { + RefreshScene(); + if (!moved) { + ClearPendingSceneEditTransaction(); + return false; + } + + bool changed = false; + if (!FinalizePendingSceneEditTransaction(changed)) { + return false; + } + if (changed) { IncrementInspectorRevision(); IncrementSceneContentRevision(); } - RefreshScene(); - return moved; + return true; } bool EditorSceneRuntime::AddComponentToSelectedGameObject( @@ -438,13 +505,24 @@ bool EditorSceneRuntime::AddComponentToSelectedGameObject( const std::string selectedItemId = GetSelectedItemId(); if (selectedItemId.empty() || m_backend == nullptr || - !m_backend->AddComponent(selectedItemId, componentTypeName)) { + !BeginSceneEditTransaction("Add Component")) { + return false; + } + + if (!m_backend->AddComponent(selectedItemId, componentTypeName)) { + ClearPendingSceneEditTransaction(); return false; } - IncrementInspectorRevision(); - IncrementSceneContentRevision(); RefreshScene(); + bool changed = false; + if (!FinalizePendingSceneEditTransaction(changed)) { + return false; + } + if (changed) { + IncrementInspectorRevision(); + IncrementSceneContentRevision(); + } return true; } @@ -462,16 +540,28 @@ bool EditorSceneRuntime::RemoveSelectedComponent(std::string_view componentId) { return false; } - ResetTransformEditHistory(); + if (!BeginSceneEditTransaction("Remove Component")) { + return false; + } + const bool removed = m_backend != nullptr && m_backend->RemoveComponent(GetSelectedItemId(), componentId); - if (removed) { + RefreshScene(); + if (!removed) { + ClearPendingSceneEditTransaction(); + return false; + } + + bool changed = false; + if (!FinalizePendingSceneEditTransaction(changed)) { + return false; + } + if (changed) { IncrementInspectorRevision(); IncrementSceneContentRevision(); } - RefreshScene(); - return removed; + return true; } bool EditorSceneRuntime::SetSelectedTransformLocalPosition( @@ -479,12 +569,9 @@ bool EditorSceneRuntime::SetSelectedTransformLocalPosition( const Vector3& position) { const EditorSceneComponentDescriptor descriptor = ResolveSelectedComponentDescriptor(componentId); - if (m_backend == nullptr || descriptor.typeName != "Transform") { - return false; - } - - SceneTransformSnapshot beforeSnapshot = {}; - if (!CaptureSelectedTransformSnapshot(beforeSnapshot)) { + if (m_backend == nullptr || + descriptor.typeName != "Transform" || + !BeginSceneEditTransaction("Set Transform Position")) { return false; } @@ -492,13 +579,16 @@ bool EditorSceneRuntime::SetSelectedTransformLocalPosition( GetSelectedItemId(), componentId, position)) { + ClearPendingSceneEditTransaction(); return false; } - SceneTransformSnapshot afterSnapshot = {}; - CaptureSelectedTransformSnapshot(afterSnapshot); - if (!TransformSnapshotsMatch(beforeSnapshot, afterSnapshot)) { - RecordTransformEdit(beforeSnapshot, afterSnapshot); + RefreshScene(); + bool changed = false; + if (!FinalizePendingSceneEditTransaction(changed)) { + return false; + } + if (changed) { IncrementInspectorRevision(); IncrementSceneContentRevision(); } @@ -510,12 +600,9 @@ bool EditorSceneRuntime::SetSelectedTransformLocalEulerAngles( const Vector3& eulerAngles) { const EditorSceneComponentDescriptor descriptor = ResolveSelectedComponentDescriptor(componentId); - if (m_backend == nullptr || descriptor.typeName != "Transform") { - return false; - } - - SceneTransformSnapshot beforeSnapshot = {}; - if (!CaptureSelectedTransformSnapshot(beforeSnapshot)) { + if (m_backend == nullptr || + descriptor.typeName != "Transform" || + !BeginSceneEditTransaction("Set Transform Rotation")) { return false; } @@ -523,13 +610,16 @@ bool EditorSceneRuntime::SetSelectedTransformLocalEulerAngles( GetSelectedItemId(), componentId, eulerAngles)) { + ClearPendingSceneEditTransaction(); return false; } - SceneTransformSnapshot afterSnapshot = {}; - CaptureSelectedTransformSnapshot(afterSnapshot); - if (!TransformSnapshotsMatch(beforeSnapshot, afterSnapshot)) { - RecordTransformEdit(beforeSnapshot, afterSnapshot); + RefreshScene(); + bool changed = false; + if (!FinalizePendingSceneEditTransaction(changed)) { + return false; + } + if (changed) { IncrementInspectorRevision(); IncrementSceneContentRevision(); } @@ -541,12 +631,9 @@ bool EditorSceneRuntime::SetSelectedTransformLocalScale( const Vector3& scale) { const EditorSceneComponentDescriptor descriptor = ResolveSelectedComponentDescriptor(componentId); - if (m_backend == nullptr || descriptor.typeName != "Transform") { - return false; - } - - SceneTransformSnapshot beforeSnapshot = {}; - if (!CaptureSelectedTransformSnapshot(beforeSnapshot)) { + if (m_backend == nullptr || + descriptor.typeName != "Transform" || + !BeginSceneEditTransaction("Set Transform Scale")) { return false; } @@ -554,13 +641,16 @@ bool EditorSceneRuntime::SetSelectedTransformLocalScale( GetSelectedItemId(), componentId, scale)) { + ClearPendingSceneEditTransaction(); return false; } - SceneTransformSnapshot afterSnapshot = {}; - CaptureSelectedTransformSnapshot(afterSnapshot); - if (!TransformSnapshotsMatch(beforeSnapshot, afterSnapshot)) { - RecordTransformEdit(beforeSnapshot, afterSnapshot); + RefreshScene(); + bool changed = false; + if (!FinalizePendingSceneEditTransaction(changed)) { + return false; + } + if (changed) { IncrementInspectorRevision(); IncrementSceneContentRevision(); } @@ -571,18 +661,27 @@ bool EditorSceneRuntime::ApplySelectedComponentMutation( const EditorSceneComponentMutation& mutation) { if (m_backend == nullptr || !mutation.IsValid() || - !ResolveSelectedComponentDescriptor(mutation.componentId).IsValid()) { + !ResolveSelectedComponentDescriptor(mutation.componentId).IsValid() || + !BeginSceneEditTransaction("Apply Component Mutation")) { return false; } if (!m_backend->ApplyComponentMutation( GetSelectedItemId(), mutation)) { + ClearPendingSceneEditTransaction(); return false; } - IncrementInspectorRevision(); - IncrementSceneContentRevision(); + RefreshScene(); + bool changed = false; + if (!FinalizePendingSceneEditTransaction(changed)) { + return false; + } + if (changed) { + IncrementInspectorRevision(); + IncrementSceneContentRevision(); + } return true; } @@ -608,46 +707,12 @@ bool EditorSceneRuntime::CaptureSelectedTransformSnapshot( return true; } -bool EditorSceneRuntime::ApplyTransformSnapshot( - const SceneTransformSnapshot& snapshot) { - if (!snapshot.IsValid()) { - return false; - } - - if (m_backend == nullptr || - !m_backend->SetWorldTransform( - snapshot.targetId, - snapshot.position, - snapshot.rotation, - snapshot.scale)) { - return false; - } - - IncrementInspectorRevision(); - IncrementSceneContentRevision(); - return true; -} - -bool EditorSceneRuntime::RecordTransformEdit( - const SceneTransformSnapshot& before, - const SceneTransformSnapshot& after) { - if (!before.IsValid() || - !after.IsValid() || - before.targetId != after.targetId || - TransformSnapshotsMatch(before, after)) { - return false; - } - - m_transformUndoStack.push_back({ before, after }); - m_transformRedoStack.clear(); - return true; -} - bool EditorSceneRuntime::ApplyTransformToolWorldPreview( EditorSceneObjectId targetId, const Vector3& position, const Quaternion& rotation) { - if (targetId == kInvalidEditorSceneObjectId) { + if (targetId == kInvalidEditorSceneObjectId || + !HasPendingSceneEditTransaction()) { return false; } @@ -671,7 +736,8 @@ bool EditorSceneRuntime::ApplyTransformToolWorldPreview( bool EditorSceneRuntime::ApplyTransformToolLocalScalePreview( EditorSceneObjectId targetId, const Vector3& localScale) { - if (targetId == kInvalidEditorSceneObjectId) { + if (targetId == kInvalidEditorSceneObjectId || + !HasPendingSceneEditTransaction()) { return false; } @@ -692,43 +758,89 @@ bool EditorSceneRuntime::ApplyTransformToolLocalScalePreview( return true; } -bool EditorSceneRuntime::CanUndoTransformEdit() const { - return !m_transformUndoStack.empty(); -} - -bool EditorSceneRuntime::CanRedoTransformEdit() const { - return !m_transformRedoStack.empty(); -} - -bool EditorSceneRuntime::UndoTransformEdit() { - if (m_transformUndoStack.empty()) { +bool EditorSceneRuntime::BeginSceneEditTransaction(std::string_view label) { + if (m_pendingSceneEditTransaction.has_value()) { return false; } - const TransformEditTransaction transaction = m_transformUndoStack.back(); - if (!ApplyTransformSnapshot(transaction.before)) { + SceneEditStateSnapshot before = {}; + if (!CaptureSceneEditState(before)) { return false; } - m_transformUndoStack.pop_back(); - m_transformRedoStack.push_back(transaction); - SetSelection(transaction.before.targetId); + PendingSceneEditTransaction pending = {}; + pending.label = std::string(label); + pending.before = before; + m_pendingSceneEditTransaction = pending; return true; } -bool EditorSceneRuntime::RedoTransformEdit() { - if (m_transformRedoStack.empty()) { +bool EditorSceneRuntime::HasPendingSceneEditTransaction() const { + return m_pendingSceneEditTransaction.has_value(); +} + +bool EditorSceneRuntime::CommitSceneEditTransaction() { + bool changed = false; + return FinalizePendingSceneEditTransaction(changed); +} + +bool EditorSceneRuntime::CancelSceneEditTransaction() { + if (!m_pendingSceneEditTransaction.has_value()) { return false; } - const TransformEditTransaction transaction = m_transformRedoStack.back(); - if (!ApplyTransformSnapshot(transaction.after)) { + const SceneEditStateSnapshot before = m_pendingSceneEditTransaction->before; + ClearPendingSceneEditTransaction(); + if (!RestoreSceneEditState(before)) { return false; } - m_transformRedoStack.pop_back(); - m_transformUndoStack.push_back(transaction); - SetSelection(transaction.after.targetId); + IncrementInspectorRevision(); + IncrementSceneContentRevision(); + return true; +} + +bool EditorSceneRuntime::CanUndoSceneEdit() const { + return !m_pendingSceneEditTransaction.has_value() && + !m_sceneUndoStack.empty(); +} + +bool EditorSceneRuntime::CanRedoSceneEdit() const { + return !m_pendingSceneEditTransaction.has_value() && + !m_sceneRedoStack.empty(); +} + +bool EditorSceneRuntime::UndoSceneEdit() { + if (!CanUndoSceneEdit()) { + return false; + } + + const SceneEditTransaction transaction = m_sceneUndoStack.back(); + if (!RestoreSceneEditState(transaction.before)) { + return false; + } + + m_sceneUndoStack.pop_back(); + m_sceneRedoStack.push_back(transaction); + IncrementInspectorRevision(); + IncrementSceneContentRevision(); + return true; +} + +bool EditorSceneRuntime::RedoSceneEdit() { + if (!CanRedoSceneEdit()) { + return false; + } + + const SceneEditTransaction transaction = m_sceneRedoStack.back(); + if (!RestoreSceneEditState(transaction.after)) { + return false; + } + + m_sceneRedoStack.pop_back(); + m_sceneUndoStack.push_back(transaction); + IncrementInspectorRevision(); + IncrementSceneContentRevision(); return true; } @@ -799,9 +911,96 @@ bool EditorSceneRuntime::SelectFirstAvailableGameObject() { return false; } -void EditorSceneRuntime::ResetTransformEditHistory() { - m_transformUndoStack.clear(); - m_transformRedoStack.clear(); +bool EditorSceneRuntime::CaptureSceneEditState( + SceneEditStateSnapshot& outSnapshot) const { + if (m_backend == nullptr) { + outSnapshot = {}; + return false; + } + + outSnapshot = {}; + outSnapshot.sceneSnapshot = m_backend->CaptureActiveSceneSnapshot(); + if (outSnapshot.sceneSnapshot.empty()) { + outSnapshot = {}; + return false; + } + + outSnapshot.selection = SelectionService().GetSelection(); + return true; +} + +bool EditorSceneRuntime::RestoreSceneEditState( + const SceneEditStateSnapshot& snapshot) { + if (m_backend == nullptr || + snapshot.sceneSnapshot.empty() || + !m_backend->RestoreActiveSceneSnapshot(snapshot.sceneSnapshot)) { + return false; + } + + switch (snapshot.selection.kind) { + case EditorSelectionKind::HierarchyNode: + if (snapshot.selection.itemId.empty() || + !SetSelection(snapshot.selection.itemId)) { + ClearSelection(); + } + break; + + case EditorSelectionKind::ProjectItem: + SelectionService().SetProjectSelection( + snapshot.selection.itemId, + snapshot.selection.displayName, + snapshot.selection.absolutePath, + snapshot.selection.directory); + break; + + case EditorSelectionKind::None: + default: + ClearSelection(); + break; + } + + RefreshScene(); + return true; +} + +void EditorSceneRuntime::ResetSceneEditHistory() { + m_sceneUndoStack.clear(); + m_sceneRedoStack.clear(); + ClearPendingSceneEditTransaction(); +} + +void EditorSceneRuntime::ClearPendingSceneEditTransaction() { + m_pendingSceneEditTransaction.reset(); +} + +bool EditorSceneRuntime::FinalizePendingSceneEditTransaction(bool& outChanged) { + outChanged = false; + if (!m_pendingSceneEditTransaction.has_value()) { + return false; + } + + const PendingSceneEditTransaction pending = *m_pendingSceneEditTransaction; + ClearPendingSceneEditTransaction(); + + SceneEditStateSnapshot after = {}; + if (!CaptureSceneEditState(after)) { + return false; + } + + outChanged = + pending.before.sceneSnapshot != after.sceneSnapshot || + !SelectionStatesMatch(pending.before.selection, after.selection); + if (!outChanged) { + return true; + } + + SceneEditTransaction transaction = {}; + transaction.label = pending.label; + transaction.before = pending.before; + transaction.after = after; + m_sceneUndoStack.push_back(transaction); + m_sceneRedoStack.clear(); + return true; } void EditorSceneRuntime::IncrementInspectorRevision() { diff --git a/editor/app/Services/Scene/EditorSceneRuntime.h b/editor/app/Services/Scene/EditorSceneRuntime.h index f8c7f8c4..3e292e76 100644 --- a/editor/app/Services/Scene/EditorSceneRuntime.h +++ b/editor/app/Services/Scene/EditorSceneRuntime.h @@ -100,10 +100,6 @@ public: const EditorSceneComponentMutation& mutation); bool CaptureSelectedTransformSnapshot(SceneTransformSnapshot& outSnapshot) const; - bool ApplyTransformSnapshot(const SceneTransformSnapshot& snapshot); - bool RecordTransformEdit( - const SceneTransformSnapshot& before, - const SceneTransformSnapshot& after); bool ApplyTransformToolWorldPreview( EditorSceneObjectId targetId, const ::XCEngine::Math::Vector3& position, @@ -112,16 +108,32 @@ public: EditorSceneObjectId targetId, const ::XCEngine::Math::Vector3& localScale); - bool CanUndoTransformEdit() const; - bool CanRedoTransformEdit() const; - bool UndoTransformEdit(); - bool RedoTransformEdit(); + bool BeginSceneEditTransaction(std::string_view label); + bool HasPendingSceneEditTransaction() const; + bool CommitSceneEditTransaction(); + bool CancelSceneEditTransaction(); + + bool CanUndoSceneEdit() const; + bool CanRedoSceneEdit() const; + bool UndoSceneEdit(); + bool RedoSceneEdit(); void NotifyExternalInspectorStateChanged(); private: - struct TransformEditTransaction { - SceneTransformSnapshot before = {}; - SceneTransformSnapshot after = {}; + struct SceneEditStateSnapshot { + std::string sceneSnapshot = {}; + EditorSelectionState selection = {}; + }; + + struct SceneEditTransaction { + std::string label = {}; + SceneEditStateSnapshot before = {}; + SceneEditStateSnapshot after = {}; + }; + + struct PendingSceneEditTransaction { + std::string label = {}; + SceneEditStateSnapshot before = {}; }; EditorSelectionService& SelectionService(); @@ -134,7 +146,11 @@ private: EditorSceneComponentDescriptor ResolveSelectedComponentDescriptor( std::string_view componentId) const; bool SelectFirstAvailableGameObject(); - void ResetTransformEditHistory(); + bool CaptureSceneEditState(SceneEditStateSnapshot& outSnapshot) const; + bool RestoreSceneEditState(const SceneEditStateSnapshot& snapshot); + void ResetSceneEditHistory(); + void ClearPendingSceneEditTransaction(); + bool FinalizePendingSceneEditTransaction(bool& outChanged); void IncrementInspectorRevision(); void IncrementSceneContentRevision(); @@ -142,8 +158,9 @@ private: std::unique_ptr m_backend = {}; EditorSelectionService m_ownedSelectionService = {}; EditorSelectionService* m_selectionService = &m_ownedSelectionService; - std::vector m_transformUndoStack = {}; - std::vector m_transformRedoStack = {}; + std::vector m_sceneUndoStack = {}; + std::vector m_sceneRedoStack = {}; + std::optional m_pendingSceneEditTransaction = {}; std::uint64_t m_inspectorRevision = 0u; std::uint64_t m_sceneContentRevision = 0u; }; diff --git a/editor/app/Services/Scene/EngineEditorSceneBackend.cpp b/editor/app/Services/Scene/EngineEditorSceneBackend.cpp index 9b79a9d6..26dbe060 100644 --- a/editor/app/Services/Scene/EngineEditorSceneBackend.cpp +++ b/editor/app/Services/Scene/EngineEditorSceneBackend.cpp @@ -2196,6 +2196,32 @@ public: return ResolvePrimaryScene(m_sceneManager); } + std::string CaptureActiveSceneSnapshot() const override { + Scene* scene = ResolvePrimaryScene(m_sceneManager); + return scene != nullptr + ? scene->SerializeToString() + : std::string(); + } + + bool RestoreActiveSceneSnapshot(const std::string& snapshot) override { + if (snapshot.empty()) { + return false; + } + + Scene* scene = ResolvePrimaryScene(m_sceneManager); + if (scene == nullptr) { + return false; + } + + { + ResourceManager::ScopedDeferredSceneLoad deferredSceneLoad( + m_resourceManager); + scene->DeserializeFromString(snapshot); + } + m_sceneManager.SetActiveScene(scene); + return true; + } + std::unique_ptr BeginPlaySession() override { Scene* editScene = ResolvePrimaryScene(m_sceneManager); if (editScene == nullptr) { diff --git a/tests/UI/Editor/unit/test_scene_viewport_runtime.cpp b/tests/UI/Editor/unit/test_scene_viewport_runtime.cpp index fe45dda1..d775e34b 100644 --- a/tests/UI/Editor/unit/test_scene_viewport_runtime.cpp +++ b/tests/UI/Editor/unit/test_scene_viewport_runtime.cpp @@ -451,7 +451,129 @@ TEST(SceneViewportRuntimeTests, TransformSetterApisWriteLocalValuesOnSelectedTra EXPECT_FLOAT_EQ(scale.x, 2.0f); EXPECT_FLOAT_EQ(scale.y, 3.0f); EXPECT_FLOAT_EQ(scale.z, 4.0f); - EXPECT_TRUE(runtime.CanUndoTransformEdit()); + EXPECT_TRUE(runtime.CanUndoSceneEdit()); + + ASSERT_TRUE(runtime.UndoSceneEdit()); + scene = GetLoadedActiveScene(); + ASSERT_NE(scene, nullptr); + target = scene->Find("Target"); + ASSERT_NE(target, nullptr); + transform = target->GetTransform(); + ASSERT_NE(transform, nullptr); + + const Math::Vector3 undonePosition = transform->GetLocalPosition(); + const Math::Quaternion undoneRotation = transform->GetLocalRotation(); + const Math::Vector3 undoneScale = transform->GetLocalScale(); + EXPECT_FLOAT_EQ(undonePosition.x, 8.0f); + EXPECT_FLOAT_EQ(undonePosition.y, 9.0f); + EXPECT_FLOAT_EQ(undonePosition.z, 10.0f); + EXPECT_GT(std::abs(undoneRotation.Dot(expectedRotation)), 0.9999f); + EXPECT_FLOAT_EQ(undoneScale.x, 1.0f); + EXPECT_FLOAT_EQ(undoneScale.y, 1.0f); + EXPECT_FLOAT_EQ(undoneScale.z, 1.0f); + EXPECT_TRUE(runtime.CanRedoSceneEdit()); + + ASSERT_TRUE(runtime.RedoSceneEdit()); + scene = GetLoadedActiveScene(); + ASSERT_NE(scene, nullptr); + target = scene->Find("Target"); + ASSERT_NE(target, nullptr); + transform = target->GetTransform(); + ASSERT_NE(transform, nullptr); + + const Math::Vector3 redoneScale = transform->GetLocalScale(); + EXPECT_FLOAT_EQ(redoneScale.x, 2.0f); + EXPECT_FLOAT_EQ(redoneScale.y, 3.0f); + EXPECT_FLOAT_EQ(redoneScale.z, 4.0f); +} + +TEST(SceneViewportRuntimeTests, DeleteGameObjectUndoRedoRestoresSceneAndSelection) { + ScopedSceneManagerReset reset = {}; + TemporaryProjectRoot projectRoot = {}; + SaveMainScene(projectRoot, Math::Vector3(0.0f, 0.0f, 0.0f)); + + EditorSceneRuntime runtime = {}; + BindEngineSceneBackend(runtime); + ASSERT_TRUE(runtime.Initialize(projectRoot.Root())); + + Scene* scene = GetLoadedActiveScene(); + ASSERT_NE(scene, nullptr); + GameObject* secondary = scene->CreateGameObject("Secondary"); + ASSERT_NE(secondary, nullptr); + ASSERT_TRUE(runtime.SetSelection(secondary->GetID())); + + const std::string secondaryItemId = + MakeEditorGameObjectItemId(secondary->GetID()); + ASSERT_TRUE(runtime.DeleteGameObject(secondaryItemId)); + EXPECT_EQ(scene->Find("Secondary"), nullptr); + EXPECT_EQ(runtime.GetSelectedDisplayName(), "Target"); + EXPECT_TRUE(runtime.CanUndoSceneEdit()); + + ASSERT_TRUE(runtime.UndoSceneEdit()); + scene = GetLoadedActiveScene(); + ASSERT_NE(scene, nullptr); + GameObject* restoredSecondary = scene->Find("Secondary"); + ASSERT_NE(restoredSecondary, nullptr); + ASSERT_TRUE(runtime.GetSelectedObjectId().has_value()); + EXPECT_EQ(runtime.GetSelectedObjectId().value(), restoredSecondary->GetID()); + EXPECT_EQ(runtime.GetSelectedDisplayName(), "Secondary"); + EXPECT_TRUE(runtime.CanRedoSceneEdit()); + + ASSERT_TRUE(runtime.RedoSceneEdit()); + scene = GetLoadedActiveScene(); + ASSERT_NE(scene, nullptr); + EXPECT_EQ(scene->Find("Secondary"), nullptr); + EXPECT_EQ(runtime.GetSelectedDisplayName(), "Target"); +} + +TEST(SceneViewportRuntimeTests, RemoveSelectedComponentUndoRedoRestoresComponentDescriptors) { + ScopedSceneManagerReset reset = {}; + TemporaryProjectRoot projectRoot = {}; + SaveMainScene(projectRoot, Math::Vector3(0.0f, 0.0f, 0.0f)); + + EditorSceneRuntime runtime = {}; + BindEngineSceneBackend(runtime); + ASSERT_TRUE(runtime.Initialize(projectRoot.Root())); + + Scene* scene = GetLoadedActiveScene(); + ASSERT_NE(scene, nullptr); + GameObject* target = scene->Find("Target"); + ASSERT_NE(target, nullptr); + ASSERT_NE(target->AddComponent<::XCEngine::Components::CameraComponent>(), nullptr); + ASSERT_TRUE(runtime.SetSelection(target->GetID())); + + ASSERT_TRUE(runtime.RemoveSelectedComponent("Camera#0")); + std::vector descriptors = + runtime.GetSelectedComponents(); + ASSERT_EQ(descriptors.size(), 1u); + EXPECT_EQ(descriptors[0].typeName, "Transform"); + EXPECT_TRUE(runtime.CanUndoSceneEdit()); + + ASSERT_TRUE(runtime.UndoSceneEdit()); + scene = GetLoadedActiveScene(); + ASSERT_NE(scene, nullptr); + target = scene->Find("Target"); + ASSERT_NE(target, nullptr); + ASSERT_TRUE(runtime.SetSelection(target->GetID()) || + (runtime.GetSelectedObjectId().has_value() && + runtime.GetSelectedObjectId().value() == target->GetID())); + descriptors = runtime.GetSelectedComponents(); + ASSERT_EQ(descriptors.size(), 2u); + EXPECT_NE(FindComponentDescriptor(descriptors, "Camera"), nullptr); + EXPECT_NE(target->GetComponent<::XCEngine::Components::CameraComponent>(), nullptr); + EXPECT_TRUE(runtime.CanRedoSceneEdit()); + + ASSERT_TRUE(runtime.RedoSceneEdit()); + scene = GetLoadedActiveScene(); + ASSERT_NE(scene, nullptr); + target = scene->Find("Target"); + ASSERT_NE(target, nullptr); + ASSERT_TRUE(runtime.SetSelection(target->GetID()) || + (runtime.GetSelectedObjectId().has_value() && + runtime.GetSelectedObjectId().value() == target->GetID())); + descriptors = runtime.GetSelectedComponents(); + ASSERT_EQ(descriptors.size(), 1u); + EXPECT_EQ(target->GetComponent<::XCEngine::Components::CameraComponent>(), nullptr); } TEST(SceneViewportRuntimeTests, SelectionStampAdvancesOnSceneSelectionChanges) { @@ -1027,27 +1149,31 @@ TEST(SceneViewportRuntimeTests, TranslateGizmoDragAppliesPreviewToSelectedObject auto* transform = target->GetTransform(); ASSERT_NE(transform, nullptr); - SceneViewportTransformGizmo gizmo = {}; const UIRect viewportRect(0.0f, 0.0f, 640.0f, 360.0f); - const std::optional hoverPoint = - FindHoveredTransformGizmoPoint(gizmo, runtime, session, viewportRect); - ASSERT_TRUE(hoverPoint.has_value()); - - const std::array dragTargets = { - UIPoint(hoverPoint->x + 40.0f, hoverPoint->y), - UIPoint(hoverPoint->x - 40.0f, hoverPoint->y), - UIPoint(hoverPoint->x, hoverPoint->y + 40.0f), - UIPoint(hoverPoint->x, hoverPoint->y - 40.0f), - UIPoint(hoverPoint->x + 32.0f, hoverPoint->y + 32.0f), - UIPoint(hoverPoint->x - 32.0f, hoverPoint->y - 32.0f) + const std::array dragOffsets = { + UIPoint(40.0f, 0.0f), + UIPoint(-40.0f, 0.0f), + UIPoint(0.0f, 40.0f), + UIPoint(0.0f, -40.0f), + UIPoint(32.0f, 32.0f), + UIPoint(-32.0f, -32.0f) }; bool previewApplied = false; - for (const UIPoint& dragPoint : dragTargets) { + for (const UIPoint& dragOffset : dragOffsets) { + SceneViewportTransformGizmo gizmo = {}; transform->SetPosition(Math::Vector3(0.0f, 0.0f, 0.0f)); + const std::optional hoverPoint = + FindHoveredTransformGizmoPoint(gizmo, runtime, session, viewportRect); + ASSERT_TRUE(hoverPoint.has_value()); + const UIPoint dragPoint( + hoverPoint->x + dragOffset.x, + hoverPoint->y + dragOffset.y); + gizmo.Refresh(runtime, session, viewportRect, hoverPoint.value(), true); ASSERT_TRUE(gizmo.IsHoveringHandle()); ASSERT_TRUE(gizmo.TryBeginDrag(runtime, session)); + EXPECT_TRUE(runtime.HasPendingSceneEditTransaction()); gizmo.Refresh(runtime, session, viewportRect, dragPoint, true); ASSERT_TRUE(gizmo.UpdateDrag(runtime)); @@ -1058,15 +1184,127 @@ TEST(SceneViewportRuntimeTests, TranslateGizmoDragAppliesPreviewToSelectedObject std::abs(previewPosition.z) > 0.0001f) { previewApplied = true; gizmo.CancelDrag(runtime); + EXPECT_FALSE(runtime.HasPendingSceneEditTransaction()); + scene = GetLoadedActiveScene(); + ASSERT_NE(scene, nullptr); + target = scene->Find("Target"); + ASSERT_NE(target, nullptr); + transform = target->GetTransform(); + ASSERT_NE(transform, nullptr); + ASSERT_TRUE(runtime.SetSelection(target->GetID()) || + (runtime.GetSelectedObjectId().has_value() && + runtime.GetSelectedObjectId().value() == target->GetID())); + const Math::Vector3 canceledPosition = transform->GetPosition(); + EXPECT_FLOAT_EQ(canceledPosition.x, 0.0f); + EXPECT_FLOAT_EQ(canceledPosition.y, 0.0f); + EXPECT_FLOAT_EQ(canceledPosition.z, 0.0f); + EXPECT_FALSE(runtime.CanUndoSceneEdit()); break; } gizmo.CancelDrag(runtime); + EXPECT_FALSE(runtime.HasPendingSceneEditTransaction()); + scene = GetLoadedActiveScene(); + ASSERT_NE(scene, nullptr); + target = scene->Find("Target"); + ASSERT_NE(target, nullptr); + transform = target->GetTransform(); + ASSERT_NE(transform, nullptr); + ASSERT_TRUE(runtime.SetSelection(target->GetID()) || + (runtime.GetSelectedObjectId().has_value() && + runtime.GetSelectedObjectId().value() == target->GetID())); } EXPECT_TRUE(previewApplied); } +TEST(SceneViewportRuntimeTests, TranslateGizmoEndDragRecordsUnifiedSceneEditHistory) { + ScopedSceneManagerReset reset = {}; + TemporaryProjectRoot projectRoot = {}; + SaveMainScene(projectRoot, Math::Vector3(0.0f, 0.0f, 0.0f)); + + EditorSceneRuntime runtime = {}; + BindEngineSceneBackend(runtime); + ASSERT_TRUE(runtime.Initialize(projectRoot.Root())); + SceneViewportSession session = {}; + session.SetToolMode(SceneToolMode::Translate); + runtime.EnsureSceneSelection(); + + Scene* scene = GetLoadedActiveScene(); + ASSERT_NE(scene, nullptr); + GameObject* target = scene->Find("Target"); + ASSERT_NE(target, nullptr); + auto* transform = target->GetTransform(); + ASSERT_NE(transform, nullptr); + + const UIRect viewportRect(0.0f, 0.0f, 640.0f, 360.0f); + const std::array dragOffsets = { + UIPoint(40.0f, 0.0f), + UIPoint(-40.0f, 0.0f), + UIPoint(0.0f, 40.0f), + UIPoint(0.0f, -40.0f), + UIPoint(32.0f, 32.0f), + UIPoint(-32.0f, -32.0f) + }; + + bool committed = false; + for (const UIPoint& dragOffset : dragOffsets) { + SceneViewportTransformGizmo gizmo = {}; + transform->SetPosition(Math::Vector3(0.0f, 0.0f, 0.0f)); + const std::optional hoverPoint = + FindHoveredTransformGizmoPoint(gizmo, runtime, session, viewportRect); + ASSERT_TRUE(hoverPoint.has_value()); + const UIPoint dragPoint( + hoverPoint->x + dragOffset.x, + hoverPoint->y + dragOffset.y); + + gizmo.Refresh(runtime, session, viewportRect, hoverPoint.value(), true); + ASSERT_TRUE(gizmo.IsHoveringHandle()); + ASSERT_TRUE(gizmo.TryBeginDrag(runtime, session)); + + gizmo.Refresh(runtime, session, viewportRect, dragPoint, true); + ASSERT_TRUE(gizmo.UpdateDrag(runtime)); + + const Math::Vector3 previewPosition = transform->GetPosition(); + if (std::abs(previewPosition.x) <= 0.0001f && + std::abs(previewPosition.y) <= 0.0001f && + std::abs(previewPosition.z) <= 0.0001f) { + gizmo.CancelDrag(runtime); + scene = GetLoadedActiveScene(); + ASSERT_NE(scene, nullptr); + target = scene->Find("Target"); + ASSERT_NE(target, nullptr); + transform = target->GetTransform(); + ASSERT_NE(transform, nullptr); + ASSERT_TRUE(runtime.SetSelection(target->GetID()) || + (runtime.GetSelectedObjectId().has_value() && + runtime.GetSelectedObjectId().value() == target->GetID())); + continue; + } + + ASSERT_TRUE(gizmo.EndDrag(runtime)); + committed = true; + break; + } + + ASSERT_TRUE(committed); + EXPECT_FALSE(runtime.HasPendingSceneEditTransaction()); + EXPECT_TRUE(runtime.CanUndoSceneEdit()); + + ASSERT_TRUE(runtime.UndoSceneEdit()); + scene = GetLoadedActiveScene(); + ASSERT_NE(scene, nullptr); + target = scene->Find("Target"); + ASSERT_NE(target, nullptr); + transform = target->GetTransform(); + ASSERT_NE(transform, nullptr); + const Math::Vector3 undonePosition = transform->GetPosition(); + EXPECT_FLOAT_EQ(undonePosition.x, 0.0f); + EXPECT_FLOAT_EQ(undonePosition.y, 0.0f); + EXPECT_FLOAT_EQ(undonePosition.z, 0.0f); + EXPECT_TRUE(runtime.CanRedoSceneEdit()); +} + TEST(SceneViewportRuntimeTests, SceneViewportRendererDeclaresExplicitAuxiliaryResourceRequirements) { const ViewportResourceRequirements requirements = SceneViewportRenderService::GetViewportResourceRequirements();