Unify editor scene edit history
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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<EditorScenePlaySession> BeginPlaySession() {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
#include <XCEngine/Scene/Scene.h>
|
||||
|
||||
#include <cmath>
|
||||
|
||||
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<EditorScenePlaySession> 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() {
|
||||
|
||||
@@ -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<EditorSceneBackend> m_backend = {};
|
||||
EditorSelectionService m_ownedSelectionService = {};
|
||||
EditorSelectionService* m_selectionService = &m_ownedSelectionService;
|
||||
std::vector<TransformEditTransaction> m_transformUndoStack = {};
|
||||
std::vector<TransformEditTransaction> m_transformRedoStack = {};
|
||||
std::vector<SceneEditTransaction> m_sceneUndoStack = {};
|
||||
std::vector<SceneEditTransaction> m_sceneRedoStack = {};
|
||||
std::optional<PendingSceneEditTransaction> m_pendingSceneEditTransaction = {};
|
||||
std::uint64_t m_inspectorRevision = 0u;
|
||||
std::uint64_t m_sceneContentRevision = 0u;
|
||||
};
|
||||
|
||||
@@ -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<EditorScenePlaySession> BeginPlaySession() override {
|
||||
Scene* editScene = ResolvePrimaryScene(m_sceneManager);
|
||||
if (editScene == nullptr) {
|
||||
|
||||
@@ -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<EditorSceneComponentDescriptor> 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<UIPoint> hoverPoint =
|
||||
FindHoveredTransformGizmoPoint(gizmo, runtime, session, viewportRect);
|
||||
ASSERT_TRUE(hoverPoint.has_value());
|
||||
|
||||
const std::array<UIPoint, 6u> 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<UIPoint, 6u> 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<UIPoint> 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<UIPoint, 6u> 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<UIPoint> 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();
|
||||
|
||||
Reference in New Issue
Block a user