Unify editor scene edit history

This commit is contained in:
2026-04-29 17:30:44 +08:00
parent 2e50c90167
commit 4a125cbe7f
8 changed files with 708 additions and 225 deletions

View File

@@ -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() {