#include "Scene/EditorSceneRuntime.h" #include namespace XCEngine::UI::Editor::App { namespace { using ::XCEngine::Math::Quaternion; using ::XCEngine::Math::Vector3; 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 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"; } } void EditorSceneRuntime::Reset() { m_projectRoot.clear(); m_ownedSelectionService.ClearSelection(); m_selectionService = &m_ownedSelectionService; ResetSceneEditHistory(); m_inspectorRevision = 0u; m_sceneContentRevision = 0u; } void EditorSceneRuntime::SetBackend(std::unique_ptr backend) { m_backend = std::move(backend); Reset(); } EditorStartupSceneResult EditorSceneRuntime::Initialize( const std::filesystem::path& projectRoot) { Reset(); if (m_backend == nullptr) { return {}; } m_projectRoot = projectRoot; const EditorStartupSceneResult startupScene = m_backend->EnsureStartupScene(projectRoot); RefreshScene(); return startupScene; } void EditorSceneRuntime::BindSelectionService( EditorSelectionService* selectionService) { m_selectionService = selectionService != nullptr ? selectionService : &m_ownedSelectionService; } void EditorSceneRuntime::RefreshScene() { if (HasHierarchySelection()) { if (!HasValidSelection()) { ClearSelection(); } else { RevalidateSelection(); } } } void EditorSceneRuntime::EnsureSceneSelection() { if (HasValidSelection() || SelectionService().HasSelectionKind(EditorSelectionKind::ProjectItem)) { return; } SelectFirstAvailableGameObject(); } EditorSceneHierarchySnapshot EditorSceneRuntime::BuildHierarchySnapshot() const { return m_backend != nullptr ? m_backend->BuildHierarchySnapshot() : EditorSceneHierarchySnapshot{}; } std::vector EditorSceneRuntime::BuildSceneViewportIconSnapshots() const { return m_backend != nullptr ? m_backend->BuildViewportIconSnapshots() : std::vector{}; } std::optional EditorSceneRuntime::BuildSceneViewportSelectionSnapshot() const { const std::optional selectedId = GetSelectedObjectId(); if (!selectedId.has_value() || m_backend == nullptr) { return std::nullopt; } return m_backend->BuildViewportSelectionSnapshot(selectedId.value()); } std::vector EditorSceneRuntime::BuildSelectedViewportHelperSnapshots() const { const std::optional selectedId = GetSelectedObjectId(); if (!selectedId.has_value() || m_backend == nullptr) { return {}; } return m_backend->BuildViewportHelperSnapshots(selectedId.value()); } bool EditorSceneRuntime::HasSceneSelection() const { return HasValidSelection(); } std::optional EditorSceneRuntime::GetSelectedObjectId() const { if (!HasHierarchySelection()) { return std::nullopt; } return ParseEditorGameObjectItemId(SelectionService().GetSelection().itemId); } std::string EditorSceneRuntime::GetSelectedItemId() const { const std::optional selectedId = GetSelectedObjectId(); return selectedId.has_value() ? MakeEditorGameObjectItemId(selectedId.value()) : std::string(); } std::string EditorSceneRuntime::GetSelectedDisplayName() const { const std::optional snapshot = GetSelectedObjectSnapshot(); return snapshot.has_value() ? snapshot->displayName : std::string(); } std::optional EditorSceneRuntime::GetSelectedObjectSnapshot() const { const std::string itemId = GetSelectedItemId(); return itemId.empty() ? std::nullopt : GetObjectSnapshot(itemId); } std::vector EditorSceneRuntime::GetSelectedComponents() const { const std::string selectedItemId = GetSelectedItemId(); return m_backend != nullptr && !selectedItemId.empty() ? m_backend->GetComponents(selectedItemId) : std::vector{}; } std::uint64_t EditorSceneRuntime::GetSelectionStamp() const { return SelectionService().GetStamp(); } std::uint64_t EditorSceneRuntime::GetInspectorRevision() const { return m_inspectorRevision; } std::uint64_t EditorSceneRuntime::GetSceneContentRevision() const { return m_sceneContentRevision; } 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(EditorSceneObjectId id) { if (id == kInvalidEditorSceneObjectId) { return false; } const std::optional snapshot = GetObjectSnapshot(MakeEditorGameObjectItemId(id)); if (!snapshot.has_value()) { return false; } const std::optional previousId = GetSelectedObjectId(); const bool changed = !previousId.has_value() || previousId.value() != id || !HasHierarchySelection(); SelectionService().SetHierarchySelection( MakeEditorGameObjectItemId(id), snapshot->displayName); return changed; } void EditorSceneRuntime::ClearSelection() { SelectionService().ClearSelection(); } bool EditorSceneRuntime::NewScene(std::string_view sceneName) { if (m_backend == nullptr || !m_backend->NewScene(sceneName)) { return false; } ResetSceneEditHistory(); SelectionService().ClearSelection(); IncrementInspectorRevision(); IncrementSceneContentRevision(); RefreshScene(); EnsureSceneSelection(); return true; } bool EditorSceneRuntime::OpenSceneAsset(const std::filesystem::path& scenePath) { if (m_backend == nullptr || !m_backend->OpenSceneAsset(scenePath)) { return false; } ResetSceneEditHistory(); SelectionService().ClearSelection(); IncrementInspectorRevision(); IncrementSceneContentRevision(); RefreshScene(); EnsureSceneSelection(); return true; } bool EditorSceneRuntime::SaveScene(const std::filesystem::path& scenePath) { return m_backend != nullptr && m_backend->SaveActiveScene(scenePath); } ::XCEngine::Components::Scene* EditorSceneRuntime::GetActiveScene() const { return m_backend != nullptr ? m_backend->GetActiveScene() : nullptr; } std::string EditorSceneRuntime::GetActiveSceneName() const { if (const ::XCEngine::Components::Scene* activeScene = GetActiveScene(); activeScene != nullptr) { return activeScene->GetName(); } return {}; } std::unique_ptr EditorSceneRuntime::BeginPlaySession() { return m_backend != nullptr ? m_backend->BeginPlaySession() : nullptr; } 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); RefreshScene(); if (!renamed) { ClearPendingSceneEditTransaction(); return false; } bool changed = false; if (!FinalizePendingSceneEditTransaction(changed)) { return false; } if (changed) { IncrementInspectorRevision(); IncrementSceneContentRevision(); } return true; } bool EditorSceneRuntime::DeleteGameObject(std::string_view itemId) { if (!BeginSceneEditTransaction("Delete GameObject")) { return false; } const bool deleted = m_backend != nullptr && m_backend->DeleteGameObject(itemId); RefreshScene(); EnsureSceneSelection(); if (!deleted) { ClearPendingSceneEditTransaction(); return false; } bool changed = false; if (!FinalizePendingSceneEditTransaction(changed)) { return false; } if (changed) { IncrementInspectorRevision(); IncrementSceneContentRevision(); } return true; } std::string EditorSceneRuntime::DuplicateGameObject(std::string_view itemId) { if (!BeginSceneEditTransaction("Duplicate GameObject")) { return {}; } const std::string duplicatedItemId = m_backend != nullptr ? m_backend->DuplicateGameObject(itemId) : std::string(); if (duplicatedItemId.empty()) { ClearPendingSceneEditTransaction(); RefreshScene(); return {}; } SetSelection(duplicatedItemId); RefreshScene(); bool changed = false; if (!FinalizePendingSceneEditTransaction(changed)) { return {}; } if (changed) { IncrementInspectorRevision(); IncrementSceneContentRevision(); } return duplicatedItemId; } bool EditorSceneRuntime::ReparentGameObject( std::string_view itemId, std::string_view parentItemId) { if (!BeginSceneEditTransaction("Reparent GameObject")) { return false; } const bool reparented = m_backend != nullptr && m_backend->ReparentGameObject(itemId, parentItemId); RefreshScene(); if (!reparented) { ClearPendingSceneEditTransaction(); return false; } bool changed = false; if (!FinalizePendingSceneEditTransaction(changed)) { return false; } if (changed) { IncrementInspectorRevision(); IncrementSceneContentRevision(); } return true; } bool EditorSceneRuntime::MoveGameObjectBefore( std::string_view itemId, std::string_view targetItemId) { if (!BeginSceneEditTransaction("Move GameObject Before")) { return false; } const bool moved = m_backend != nullptr && m_backend->MoveGameObjectBefore(itemId, targetItemId); RefreshScene(); if (!moved) { ClearPendingSceneEditTransaction(); return false; } bool changed = false; if (!FinalizePendingSceneEditTransaction(changed)) { return false; } if (changed) { IncrementInspectorRevision(); IncrementSceneContentRevision(); } return true; } bool EditorSceneRuntime::MoveGameObjectAfter( std::string_view itemId, std::string_view targetItemId) { if (!BeginSceneEditTransaction("Move GameObject After")) { return false; } const bool moved = m_backend != nullptr && m_backend->MoveGameObjectAfter(itemId, targetItemId); RefreshScene(); if (!moved) { ClearPendingSceneEditTransaction(); return false; } bool changed = false; if (!FinalizePendingSceneEditTransaction(changed)) { return false; } if (changed) { IncrementInspectorRevision(); IncrementSceneContentRevision(); } return true; } bool EditorSceneRuntime::MoveGameObjectToRoot(std::string_view itemId) { if (!BeginSceneEditTransaction("Move GameObject To Root")) { return false; } const bool moved = m_backend != nullptr && m_backend->MoveGameObjectToRoot(itemId); RefreshScene(); if (!moved) { ClearPendingSceneEditTransaction(); return false; } bool changed = false; if (!FinalizePendingSceneEditTransaction(changed)) { return false; } if (changed) { IncrementInspectorRevision(); IncrementSceneContentRevision(); } return true; } bool EditorSceneRuntime::AddComponentToSelectedGameObject( std::string_view componentTypeName) { if (componentTypeName.empty()) { return false; } const std::string selectedItemId = GetSelectedItemId(); if (selectedItemId.empty() || m_backend == nullptr || !BeginSceneEditTransaction("Add Component")) { return false; } if (!m_backend->AddComponent(selectedItemId, componentTypeName)) { ClearPendingSceneEditTransaction(); return false; } RefreshScene(); bool changed = false; if (!FinalizePendingSceneEditTransaction(changed)) { return false; } if (changed) { IncrementInspectorRevision(); IncrementSceneContentRevision(); } return true; } 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; } if (!BeginSceneEditTransaction("Remove Component")) { return false; } const bool removed = m_backend != nullptr && m_backend->RemoveComponent(GetSelectedItemId(), componentId); RefreshScene(); if (!removed) { ClearPendingSceneEditTransaction(); return false; } bool changed = false; if (!FinalizePendingSceneEditTransaction(changed)) { return false; } if (changed) { IncrementInspectorRevision(); IncrementSceneContentRevision(); } return true; } bool EditorSceneRuntime::SetSelectedTransformLocalPosition( std::string_view componentId, const Vector3& position) { const EditorSceneComponentDescriptor descriptor = ResolveSelectedComponentDescriptor(componentId); if (m_backend == nullptr || descriptor.typeName != "Transform" || !BeginSceneEditTransaction("Set Transform Position")) { return false; } if (!m_backend->SetTransformLocalPosition( GetSelectedItemId(), componentId, position)) { ClearPendingSceneEditTransaction(); return false; } RefreshScene(); bool changed = false; if (!FinalizePendingSceneEditTransaction(changed)) { return false; } if (changed) { IncrementInspectorRevision(); IncrementSceneContentRevision(); } return true; } bool EditorSceneRuntime::SetSelectedTransformLocalEulerAngles( std::string_view componentId, const Vector3& eulerAngles) { const EditorSceneComponentDescriptor descriptor = ResolveSelectedComponentDescriptor(componentId); if (m_backend == nullptr || descriptor.typeName != "Transform" || !BeginSceneEditTransaction("Set Transform Rotation")) { return false; } if (!m_backend->SetTransformLocalEulerAngles( GetSelectedItemId(), componentId, eulerAngles)) { ClearPendingSceneEditTransaction(); return false; } RefreshScene(); bool changed = false; if (!FinalizePendingSceneEditTransaction(changed)) { return false; } if (changed) { IncrementInspectorRevision(); IncrementSceneContentRevision(); } return true; } bool EditorSceneRuntime::SetSelectedTransformLocalScale( std::string_view componentId, const Vector3& scale) { const EditorSceneComponentDescriptor descriptor = ResolveSelectedComponentDescriptor(componentId); if (m_backend == nullptr || descriptor.typeName != "Transform" || !BeginSceneEditTransaction("Set Transform Scale")) { return false; } if (!m_backend->SetTransformLocalScale( GetSelectedItemId(), componentId, scale)) { ClearPendingSceneEditTransaction(); return false; } RefreshScene(); bool changed = false; if (!FinalizePendingSceneEditTransaction(changed)) { return false; } if (changed) { IncrementInspectorRevision(); IncrementSceneContentRevision(); } return true; } bool EditorSceneRuntime::ApplySelectedComponentMutation( const EditorSceneComponentMutation& mutation) { if (m_backend == nullptr || !mutation.IsValid() || !ResolveSelectedComponentDescriptor(mutation.componentId).IsValid() || !BeginSceneEditTransaction("Apply Component Mutation")) { return false; } if (!m_backend->ApplyComponentMutation( GetSelectedItemId(), mutation)) { ClearPendingSceneEditTransaction(); return false; } RefreshScene(); bool changed = false; if (!FinalizePendingSceneEditTransaction(changed)) { return false; } if (changed) { IncrementInspectorRevision(); IncrementSceneContentRevision(); } return true; } bool EditorSceneRuntime::CaptureSelectedTransformSnapshot( SceneTransformSnapshot& outSnapshot) const { const std::optional selectedId = GetSelectedObjectId(); if (!selectedId.has_value() || m_backend == nullptr) { outSnapshot = {}; return false; } outSnapshot = {}; outSnapshot.targetId = selectedId.value(); if (!m_backend->QueryWorldTransform( selectedId.value(), outSnapshot.position, outSnapshot.rotation, outSnapshot.scale)) { outSnapshot = {}; return false; } outSnapshot.valid = true; return true; } bool EditorSceneRuntime::ApplyTransformToolWorldPreview( EditorSceneObjectId targetId, const Vector3& position, const Quaternion& rotation) { if (targetId == kInvalidEditorSceneObjectId || !HasPendingSceneEditTransaction()) { return false; } const EditorSceneObjectId activeTargetId = GetSelectedObjectId().value_or(kInvalidEditorSceneObjectId); if (activeTargetId == kInvalidEditorSceneObjectId || targetId != activeTargetId) { return false; } if (m_backend == nullptr || !m_backend->SetWorldPositionRotation(targetId, position, rotation)) { return false; } IncrementInspectorRevision(); IncrementSceneContentRevision(); return true; } bool EditorSceneRuntime::ApplyTransformToolLocalScalePreview( EditorSceneObjectId targetId, const Vector3& localScale) { if (targetId == kInvalidEditorSceneObjectId || !HasPendingSceneEditTransaction()) { return false; } const EditorSceneObjectId activeTargetId = GetSelectedObjectId().value_or(kInvalidEditorSceneObjectId); if (activeTargetId == kInvalidEditorSceneObjectId || targetId != activeTargetId) { return false; } if (m_backend == nullptr || !m_backend->SetObjectLocalScale(targetId, localScale)) { return false; } IncrementInspectorRevision(); IncrementSceneContentRevision(); return true; } bool EditorSceneRuntime::BeginSceneEditTransaction(std::string_view label) { if (m_pendingSceneEditTransaction.has_value()) { return false; } SceneEditStateSnapshot before = {}; if (!CaptureSceneEditState(before)) { return false; } PendingSceneEditTransaction pending = {}; pending.label = std::string(label); pending.before = before; m_pendingSceneEditTransaction = pending; return true; } 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 SceneEditStateSnapshot before = m_pendingSceneEditTransaction->before; ClearPendingSceneEditTransaction(); if (!RestoreSceneEditState(before)) { return false; } 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; } void EditorSceneRuntime::NotifyExternalInspectorStateChanged() { IncrementInspectorRevision(); } 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 std::optional snapshot = GetSelectedObjectSnapshot(); if (!snapshot.has_value()) { return; } SelectionService().SetHierarchySelection( snapshot->itemId, snapshot->displayName); } bool EditorSceneRuntime::HasValidSelection() const { return GetSelectedObjectSnapshot().has_value(); } std::optional EditorSceneRuntime::GetObjectSnapshot( std::string_view itemId) const { return m_backend != nullptr ? m_backend->GetObjectSnapshot(itemId) : std::nullopt; } EditorSceneComponentDescriptor EditorSceneRuntime::ResolveSelectedComponentDescriptor( std::string_view componentId) const { for (const EditorSceneComponentDescriptor& descriptor : GetSelectedComponents()) { if (descriptor.componentId == componentId) { return descriptor; } } return {}; } bool EditorSceneRuntime::SelectFirstAvailableGameObject() { const EditorSceneHierarchySnapshot snapshot = BuildHierarchySnapshot(); for (const EditorSceneHierarchyNode& root : snapshot.roots) { if (root.itemId.empty()) { continue; } return SetSelection(root.itemId); } if (HasHierarchySelection()) { ClearSelection(); } return false; } 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() { ++m_inspectorRevision; } void EditorSceneRuntime::IncrementSceneContentRevision() { ++m_sceneContentRevision; } } // namespace XCEngine::UI::Editor::App