#include "Scene/EditorSceneRuntime.h" #include #include #include #include #include #include #include #include namespace XCEngine::UI::Editor::App { namespace { using ::XCEngine::Components::GameObject; using ::XCEngine::Components::Scene; using ::XCEngine::Components::TransformComponent; using ::XCEngine::Components::Component; using ::XCEngine::Math::Quaternion; using ::XCEngine::Math::Vector3; constexpr char kComponentIdSeparator = '#'; std::string ResolveGameObjectDisplayName(const GameObject& gameObject) { return gameObject.GetName().empty() ? std::string("GameObject") : gameObject.GetName(); } 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); } std::string BuildEditorComponentId( std::string_view typeName, std::size_t ordinal) { std::string componentId(typeName); componentId.push_back(kComponentIdSeparator); componentId += std::to_string(ordinal); return componentId; } bool ParseEditorComponentId( std::string_view componentId, std::string& outTypeName, std::size_t& outOrdinal) { const std::size_t separatorIndex = componentId.find(kComponentIdSeparator); if (separatorIndex == std::string_view::npos || separatorIndex == 0u || separatorIndex + 1u >= componentId.size()) { return false; } outTypeName = std::string(componentId.substr(0u, separatorIndex)); std::size_t ordinal = 0u; const std::string_view ordinalText = componentId.substr(separatorIndex + 1u); const char* first = ordinalText.data(); const char* last = ordinalText.data() + ordinalText.size(); const std::from_chars_result result = std::from_chars(first, last, ordinal); if (result.ec != std::errc() || result.ptr != last) { outTypeName.clear(); return false; } outOrdinal = ordinal; return true; } EditorSceneComponentDescriptor BuildComponentDescriptor( const Component& component, std::size_t ordinal) { EditorSceneComponentDescriptor descriptor = {}; descriptor.typeName = component.GetName(); descriptor.componentId = BuildEditorComponentId(descriptor.typeName, ordinal); descriptor.component = &component; descriptor.removable = component.GetGameObject() != nullptr && component.GetGameObject()->GetTransform() != &component; return descriptor; } } // namespace std::string_view GetSceneToolModeName(SceneToolMode mode) { switch (mode) { case SceneToolMode::View: return "View"; case SceneToolMode::Translate: return "Move"; case SceneToolMode::Rotate: return "Rotate"; case SceneToolMode::Scale: return "Scale"; case SceneToolMode::Transform: return "Transform"; default: return "Unknown"; } } std::string_view GetSceneToolSpaceModeName(SceneToolSpaceMode mode) { switch (mode) { case SceneToolSpaceMode::World: return "World"; case SceneToolSpaceMode::Local: return "Local"; default: return "Unknown"; } } std::string_view GetSceneToolPivotModeName(SceneToolPivotMode mode) { switch (mode) { case SceneToolPivotMode::Pivot: return "Pivot"; case SceneToolPivotMode::Center: return "Center"; default: return "Unknown"; } } std::string_view GetSceneToolHandleName(SceneToolHandle handle) { switch (handle) { case SceneToolHandle::AxisX: return "AxisX"; case SceneToolHandle::AxisY: return "AxisY"; case SceneToolHandle::AxisZ: return "AxisZ"; case SceneToolHandle::None: default: return "None"; } } std::string_view GetSceneToolInteractionLockName(SceneToolInteractionLock lock) { switch (lock) { case SceneToolInteractionLock::TransformDrag: return "TransformDrag"; case SceneToolInteractionLock::None: default: return "None"; } } bool EditorSceneRuntime::Initialize(const std::filesystem::path& projectRoot) { m_projectRoot = projectRoot; m_ownedSelectionService = {}; m_selectionService = &m_ownedSelectionService; m_startupSceneResult = EnsureEditorStartupScene(projectRoot); EnsureSceneViewCamera(); ResetTransformEditHistory(); m_toolState = {}; RefreshScene(); return m_startupSceneResult.ready; } void EditorSceneRuntime::BindSelectionService( EditorSelectionService* selectionService) { m_selectionService = selectionService != nullptr ? selectionService : &m_ownedSelectionService; } void EditorSceneRuntime::RefreshScene() { if (HasHierarchySelection()) { if (!HasValidSelection()) { ClearSelection(); } else { RevalidateSelection(); } } else if (m_toolState.dragState.active) { ResetToolInteractionState(); } ClearInvalidToolInteractionState(); } void EditorSceneRuntime::EnsureSceneSelection() { if (HasValidSelection() || SelectionService().HasSelectionKind(EditorSelectionKind::ProjectItem)) { return; } SelectFirstAvailableGameObject(); } const EditorStartupSceneResult& EditorSceneRuntime::GetStartupResult() const { return m_startupSceneResult; } Scene* EditorSceneRuntime::GetActiveScene() const { return GetActiveEditorScene(); } ::XCEngine::Components::CameraComponent* EditorSceneRuntime::GetSceneViewCamera() { return EnsureSceneViewCamera() ? m_sceneViewCamera.camera : nullptr; } const ::XCEngine::Components::CameraComponent* EditorSceneRuntime::GetSceneViewCamera() const { return m_sceneViewCamera.camera; } float EditorSceneRuntime::GetSceneViewOrbitDistance() const { return m_sceneViewCamera.controller.GetDistance(); } SceneViewportRenderRequest EditorSceneRuntime::BuildSceneViewportRenderRequest() { SceneViewportRenderRequest request = {}; request.scene = GetActiveScene(); request.camera = GetSceneViewCamera(); request.orbitDistance = GetSceneViewOrbitDistance(); if (const std::optional selectedId = GetSelectedGameObjectId(); selectedId.has_value()) { request.selectedObjectIds.push_back(selectedId.value()); } return request; } void EditorSceneRuntime::ApplySceneViewportCameraInput( const SceneViewportCameraInputState& input) { if (!EnsureSceneViewCamera()) { return; } m_sceneViewCamera.controller.ApplyInput(input); ApplySceneViewCameraController(); } bool EditorSceneRuntime::FocusSceneSelection() { const GameObject* selectedGameObject = GetSelectedGameObject(); if (selectedGameObject == nullptr || !EnsureSceneViewCamera()) { return false; } const auto* transform = selectedGameObject->GetTransform(); if (transform == nullptr) { return false; } m_sceneViewCamera.controller.Focus(transform->GetPosition()); ApplySceneViewCameraController(); return true; } bool EditorSceneRuntime::HasSceneSelection() const { return HasValidSelection(); } std::optional EditorSceneRuntime::GetSelectedGameObjectId() const { if (!HasHierarchySelection()) { return std::nullopt; } return ParseEditorGameObjectItemId(SelectionService().GetSelection().itemId); } std::string EditorSceneRuntime::GetSelectedItemId() const { const std::optional selectedId = GetSelectedGameObjectId(); return selectedId.has_value() ? MakeEditorGameObjectItemId(selectedId.value()) : std::string(); } std::string EditorSceneRuntime::GetSelectedDisplayName() const { const GameObject* gameObject = GetSelectedGameObject(); return gameObject != nullptr ? ResolveGameObjectDisplayName(*gameObject) : std::string(); } const GameObject* EditorSceneRuntime::GetSelectedGameObject() const { const std::optional selectedId = GetSelectedGameObjectId(); if (!selectedId.has_value()) { return nullptr; } Scene* scene = GetActiveScene(); return scene != nullptr ? scene->FindByID(selectedId.value()) : nullptr; } std::vector EditorSceneRuntime::GetSelectedComponents() const { std::vector descriptors = {}; const GameObject* gameObject = GetSelectedGameObject(); if (gameObject == nullptr) { return descriptors; } const std::vector components = gameObject->GetComponents(); descriptors.reserve(components.size()); std::unordered_map ordinalsByType = {}; for (const Component* component : components) { if (component == nullptr) { continue; } const std::string typeName = component->GetName(); const std::size_t ordinal = ordinalsByType[typeName]; descriptors.push_back(BuildComponentDescriptor(*component, ordinal)); ordinalsByType[typeName] = ordinal + 1u; } return descriptors; } std::uint64_t EditorSceneRuntime::GetSelectionStamp() const { return SelectionService().GetStamp(); } bool EditorSceneRuntime::SetSelection(std::string_view itemId) { const std::optional gameObjectId = ParseEditorGameObjectItemId(itemId); if (!gameObjectId.has_value()) { return false; } return SetSelection(gameObjectId.value()); } bool EditorSceneRuntime::SetSelection(GameObject::ID id) { if (id == GameObject::INVALID_ID) { return false; } Scene* scene = GetActiveScene(); GameObject* gameObject = scene != nullptr ? scene->FindByID(id) : nullptr; if (gameObject == nullptr) { return false; } const std::optional previousId = GetSelectedGameObjectId(); const bool changed = !previousId.has_value() || previousId.value() != id || !HasHierarchySelection(); if (changed) { ResetToolInteractionState(); } SelectionService().SetHierarchySelection( MakeEditorGameObjectItemId(id), ResolveGameObjectDisplayName(*gameObject)); ClearInvalidToolInteractionState(); return changed; } void EditorSceneRuntime::ClearSelection() { ResetToolInteractionState(); SelectionService().ClearSelection(); } bool EditorSceneRuntime::OpenSceneAsset(const std::filesystem::path& scenePath) { if (!OpenEditorSceneAsset(scenePath)) { return false; } m_startupSceneResult.ready = true; m_startupSceneResult.loadedFromDisk = true; m_startupSceneResult.scenePath = scenePath; if (Scene* activeScene = GetActiveScene(); activeScene != nullptr) { m_startupSceneResult.sceneName = activeScene->GetName(); } else { m_startupSceneResult.sceneName = scenePath.stem().string(); } ResetTransformEditHistory(); ResetToolInteractionState(); SelectionService().ClearSelection(); RefreshScene(); EnsureSceneSelection(); return true; } GameObject* EditorSceneRuntime::FindGameObject(std::string_view itemId) const { return FindEditorGameObject(itemId); } bool EditorSceneRuntime::RenameGameObject( std::string_view itemId, std::string_view newName) { const bool renamed = RenameEditorGameObject(itemId, newName); RefreshScene(); return renamed; } bool EditorSceneRuntime::DeleteGameObject(std::string_view itemId) { ResetTransformEditHistory(); const bool deleted = DeleteEditorGameObject(itemId); RefreshScene(); EnsureSceneSelection(); return deleted; } std::string EditorSceneRuntime::DuplicateGameObject(std::string_view itemId) { ResetTransformEditHistory(); const std::string duplicatedItemId = DuplicateEditorGameObject(itemId); if (!duplicatedItemId.empty()) { SetSelection(duplicatedItemId); } else { RefreshScene(); } return duplicatedItemId; } bool EditorSceneRuntime::ReparentGameObject( std::string_view itemId, std::string_view parentItemId) { ResetTransformEditHistory(); const bool reparented = ReparentEditorGameObject(itemId, parentItemId); RefreshScene(); return reparented; } bool EditorSceneRuntime::MoveGameObjectToRoot(std::string_view itemId) { ResetTransformEditHistory(); const bool moved = MoveEditorGameObjectToRoot(itemId); RefreshScene(); return moved; } bool EditorSceneRuntime::CanRemoveSelectedComponent( std::string_view componentId) const { const EditorSceneComponentDescriptor descriptor = ResolveSelectedComponentDescriptor(componentId); return descriptor.IsValid() && descriptor.removable; } bool EditorSceneRuntime::RemoveSelectedComponent(std::string_view componentId) { const EditorSceneComponentDescriptor descriptor = ResolveSelectedComponentDescriptor(componentId); if (!descriptor.IsValid() || !descriptor.removable) { return false; } const std::optional selectedId = GetSelectedGameObjectId(); Scene* scene = GetActiveScene(); if (!selectedId.has_value() || scene == nullptr) { return false; } GameObject* gameObject = scene->FindByID(selectedId.value()); Component* component = const_cast(descriptor.component); if (gameObject == nullptr || component == nullptr) { return false; } ResetTransformEditHistory(); const bool removed = gameObject->RemoveComponent(component); RefreshScene(); return removed; } bool EditorSceneRuntime::SetSelectedTransformLocalPosition( std::string_view componentId, const Vector3& position) { const EditorSceneComponentDescriptor descriptor = ResolveSelectedComponentDescriptor(componentId); auto* transform = const_cast( dynamic_cast(descriptor.component)); if (transform == nullptr) { return false; } SceneTransformSnapshot beforeSnapshot = {}; if (!CaptureSelectedTransformSnapshot(beforeSnapshot)) { return false; } transform->SetLocalPosition(position); SceneTransformSnapshot afterSnapshot = {}; CaptureSelectedTransformSnapshot(afterSnapshot); RecordTransformEdit(beforeSnapshot, afterSnapshot); return true; } bool EditorSceneRuntime::SetSelectedTransformLocalEulerAngles( std::string_view componentId, const Vector3& eulerAngles) { const EditorSceneComponentDescriptor descriptor = ResolveSelectedComponentDescriptor(componentId); auto* transform = const_cast( dynamic_cast(descriptor.component)); if (transform == nullptr) { return false; } SceneTransformSnapshot beforeSnapshot = {}; if (!CaptureSelectedTransformSnapshot(beforeSnapshot)) { return false; } transform->SetLocalEulerAngles(eulerAngles); SceneTransformSnapshot afterSnapshot = {}; CaptureSelectedTransformSnapshot(afterSnapshot); RecordTransformEdit(beforeSnapshot, afterSnapshot); return true; } bool EditorSceneRuntime::SetSelectedTransformLocalScale( std::string_view componentId, const Vector3& scale) { const EditorSceneComponentDescriptor descriptor = ResolveSelectedComponentDescriptor(componentId); auto* transform = const_cast( dynamic_cast(descriptor.component)); if (transform == nullptr) { return false; } SceneTransformSnapshot beforeSnapshot = {}; if (!CaptureSelectedTransformSnapshot(beforeSnapshot)) { return false; } transform->SetLocalScale(scale); SceneTransformSnapshot afterSnapshot = {}; CaptureSelectedTransformSnapshot(afterSnapshot); RecordTransformEdit(beforeSnapshot, afterSnapshot); return true; } bool EditorSceneRuntime::ApplySelectedComponentMutation( std::string_view componentId, const std::function& mutation) { if (!mutation) { return false; } const EditorSceneComponentDescriptor descriptor = ResolveSelectedComponentDescriptor(componentId); Component* component = const_cast(descriptor.component); if (!descriptor.IsValid() || component == nullptr) { return false; } return mutation(*component); } const SceneToolState& EditorSceneRuntime::GetToolState() const { return m_toolState; } SceneToolMode EditorSceneRuntime::GetToolMode() const { return m_toolState.mode; } SceneToolSpaceMode EditorSceneRuntime::GetToolSpaceMode() const { return m_toolState.spaceMode; } SceneToolPivotMode EditorSceneRuntime::GetToolPivotMode() const { return m_toolState.pivotMode; } void EditorSceneRuntime::SetToolMode(SceneToolMode mode) { if (m_toolState.mode == mode) { return; } ResetToolInteractionState(); m_toolState.mode = mode; } void EditorSceneRuntime::SetToolSpaceMode(SceneToolSpaceMode mode) { if (m_toolState.spaceMode == mode) { return; } ResetToolInteractionState(); m_toolState.spaceMode = mode; } void EditorSceneRuntime::SetToolPivotMode(SceneToolPivotMode mode) { if (m_toolState.pivotMode == mode) { return; } ResetToolInteractionState(); m_toolState.pivotMode = mode; } void EditorSceneRuntime::SetHoveredToolHandle(SceneToolHandle handle) { if (m_toolState.dragState.active) { return; } m_toolState.hoveredHandle = handle; } void EditorSceneRuntime::SetToolInteractionLock(SceneToolInteractionLock lock) { m_toolState.interactionLock = lock; } void EditorSceneRuntime::ClearToolInteractionLock() { m_toolState.interactionLock = SceneToolInteractionLock::None; } void EditorSceneRuntime::ResetToolInteractionState() { CancelTransformToolDrag(); ResetToolInteractionTransientState(); } bool EditorSceneRuntime::CaptureSelectedTransformSnapshot( SceneTransformSnapshot& outSnapshot) const { const GameObject* selectedGameObject = GetSelectedGameObject(); if (selectedGameObject == nullptr) { outSnapshot = {}; return false; } const TransformComponent* transform = selectedGameObject->GetTransform(); if (transform == nullptr) { outSnapshot = {}; return false; } outSnapshot = {}; outSnapshot.targetId = selectedGameObject->GetID(); outSnapshot.position = transform->GetPosition(); outSnapshot.rotation = transform->GetRotation(); outSnapshot.scale = transform->GetScale(); outSnapshot.valid = true; return true; } bool EditorSceneRuntime::ApplyTransformSnapshot( const SceneTransformSnapshot& snapshot) { if (!snapshot.IsValid()) { return false; } Scene* scene = GetActiveScene(); if (scene == nullptr) { return false; } GameObject* gameObject = scene->FindByID(snapshot.targetId); if (gameObject == nullptr) { return false; } TransformComponent* transform = gameObject->GetTransform(); if (transform == nullptr) { return false; } transform->SetPosition(snapshot.position); transform->SetRotation(snapshot.rotation); transform->SetScale(snapshot.scale); 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::BeginTransformToolDrag( SceneToolHandle handle, const ::XCEngine::UI::UIPoint& startPointerPosition) { if (handle == SceneToolHandle::None || m_toolState.mode == SceneToolMode::View) { return false; } SceneTransformSnapshot snapshot = {}; if (!CaptureSelectedTransformSnapshot(snapshot)) { return false; } CancelTransformToolDrag(); m_toolState.dragState = {}; m_toolState.dragState.active = true; m_toolState.dragState.mode = m_toolState.mode; m_toolState.dragState.handle = handle; m_toolState.dragState.startPointerPosition = startPointerPosition; m_toolState.dragState.initialTransform = snapshot; m_toolState.hoveredHandle = handle; m_toolState.activeHandle = handle; SetToolInteractionLock(SceneToolInteractionLock::TransformDrag); return true; } bool EditorSceneRuntime::HasActiveTransformToolDrag() const { return m_toolState.dragState.active; } const SceneToolDragState* EditorSceneRuntime::GetActiveTransformToolDrag() const { return m_toolState.dragState.active ? &m_toolState.dragState : nullptr; } bool EditorSceneRuntime::ApplyTransformToolPreview( const SceneTransformSnapshot& snapshot) { if (!m_toolState.dragState.active || !snapshot.IsValid() || snapshot.targetId != m_toolState.dragState.initialTransform.targetId) { return false; } return ApplyTransformSnapshot(snapshot); } bool EditorSceneRuntime::CommitTransformToolDrag() { if (!m_toolState.dragState.active) { return false; } SceneTransformSnapshot afterSnapshot = {}; const bool captured = CaptureSelectedTransformSnapshot(afterSnapshot); const SceneTransformSnapshot beforeSnapshot = m_toolState.dragState.initialTransform; ResetToolInteractionTransientState(); if (!captured || !afterSnapshot.IsValid() || !beforeSnapshot.IsValid()) { return false; } return RecordTransformEdit(beforeSnapshot, afterSnapshot); } void EditorSceneRuntime::CancelTransformToolDrag() { if (!m_toolState.dragState.active) { return; } const SceneTransformSnapshot snapshot = m_toolState.dragState.initialTransform; if (snapshot.IsValid()) { ApplyTransformSnapshot(snapshot); } ResetToolInteractionTransientState(); } bool EditorSceneRuntime::CanUndoTransformEdit() const { return !m_transformUndoStack.empty(); } bool EditorSceneRuntime::CanRedoTransformEdit() const { return !m_transformRedoStack.empty(); } bool EditorSceneRuntime::UndoTransformEdit() { if (m_transformUndoStack.empty()) { return false; } const TransformEditTransaction transaction = m_transformUndoStack.back(); if (!ApplyTransformSnapshot(transaction.before)) { return false; } m_transformUndoStack.pop_back(); m_transformRedoStack.push_back(transaction); SetSelection(transaction.before.targetId); return true; } bool EditorSceneRuntime::RedoTransformEdit() { if (m_transformRedoStack.empty()) { return false; } const TransformEditTransaction transaction = m_transformRedoStack.back(); if (!ApplyTransformSnapshot(transaction.after)) { return false; } m_transformRedoStack.pop_back(); m_transformUndoStack.push_back(transaction); SetSelection(transaction.after.targetId); return true; } bool EditorSceneRuntime::EnsureSceneViewCamera() { if (m_sceneViewCamera.gameObject != nullptr && m_sceneViewCamera.camera != nullptr) { return true; } m_sceneViewCamera = {}; m_sceneViewCamera.gameObject = std::make_unique("EditorSceneCamera"); m_sceneViewCamera.camera = m_sceneViewCamera.gameObject->AddComponent< ::XCEngine::Components::CameraComponent>(); if (m_sceneViewCamera.camera == nullptr) { m_sceneViewCamera.gameObject.reset(); return false; } m_sceneViewCamera.camera->SetPrimary(false); m_sceneViewCamera.camera->SetProjectionType( ::XCEngine::Components::CameraProjectionType::Perspective); m_sceneViewCamera.camera->SetFieldOfView(60.0f); m_sceneViewCamera.camera->SetNearClipPlane(0.03f); m_sceneViewCamera.camera->SetFarClipPlane(2000.0f); m_sceneViewCamera.controller.Reset(); ApplySceneViewCameraController(); return true; } void EditorSceneRuntime::ApplySceneViewCameraController() { if (m_sceneViewCamera.gameObject == nullptr) { return; } if (auto* transform = m_sceneViewCamera.gameObject->GetTransform(); transform != nullptr) { m_sceneViewCamera.controller.ApplyTo(*transform); } } EditorSelectionService& EditorSceneRuntime::SelectionService() { return *m_selectionService; } const EditorSelectionService& EditorSceneRuntime::SelectionService() const { return *m_selectionService; } bool EditorSceneRuntime::HasHierarchySelection() const { return SelectionService().HasSelectionKind(EditorSelectionKind::HierarchyNode); } void EditorSceneRuntime::RevalidateSelection() { const GameObject* gameObject = GetSelectedGameObject(); if (gameObject == nullptr) { return; } SelectionService().SetHierarchySelection( MakeEditorGameObjectItemId(gameObject->GetID()), ResolveGameObjectDisplayName(*gameObject)); } bool EditorSceneRuntime::HasValidSelection() const { return GetSelectedGameObject() != nullptr; } EditorSceneComponentDescriptor EditorSceneRuntime::ResolveSelectedComponentDescriptor( std::string_view componentId) const { std::string typeName = {}; std::size_t ordinal = 0u; if (!ParseEditorComponentId(componentId, typeName, ordinal)) { return {}; } const GameObject* gameObject = GetSelectedGameObject(); if (gameObject == nullptr) { return {}; } std::size_t currentOrdinal = 0u; for (const Component* component : gameObject->GetComponents()) { if (component == nullptr || component->GetName() != typeName) { continue; } if (currentOrdinal == ordinal) { return BuildComponentDescriptor(*component, currentOrdinal); } ++currentOrdinal; } return {}; } bool EditorSceneRuntime::SelectFirstAvailableGameObject() { Scene* scene = GetActiveScene(); if (scene == nullptr) { if (HasHierarchySelection()) { ClearSelection(); } return false; } for (GameObject* root : scene->GetRootGameObjects()) { if (root == nullptr) { continue; } return SetSelection(root->GetID()); } if (HasHierarchySelection()) { ClearSelection(); } return false; } void EditorSceneRuntime::ResetTransformEditHistory() { m_transformUndoStack.clear(); m_transformRedoStack.clear(); } void EditorSceneRuntime::ResetToolInteractionTransientState() { m_toolState.hoveredHandle = SceneToolHandle::None; m_toolState.activeHandle = SceneToolHandle::None; ClearToolInteractionLock(); m_toolState.dragState = {}; } void EditorSceneRuntime::ClearInvalidToolInteractionState() { if (!HasSceneSelection()) { ResetToolInteractionTransientState(); return; } if (!m_toolState.dragState.active) { return; } const SceneTransformSnapshot snapshot = m_toolState.dragState.initialTransform; Scene* scene = GetActiveScene(); if (scene == nullptr || !snapshot.IsValid() || scene->FindByID(snapshot.targetId) == nullptr) { ResetToolInteractionTransientState(); } } } // namespace XCEngine::UI::Editor::App