#include "Scene/EditorSceneRuntime.h" #include #include namespace XCEngine::UI::Editor::App { 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); } } // 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_startupSceneResult = {}; m_ownedSelectionService.ClearSelection(); m_selectionService = &m_ownedSelectionService; ResetTransformEditHistory(); m_inspectorRevision = 0u; m_sceneContentRevision = 0u; } void EditorSceneRuntime::SetBackend(std::unique_ptr backend) { m_backend = std::move(backend); Reset(); } bool EditorSceneRuntime::Initialize(const std::filesystem::path& projectRoot) { Reset(); if (m_backend == nullptr) { return false; } m_projectRoot = projectRoot; m_startupSceneResult = m_backend->EnsureStartupScene(projectRoot); 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(); } } } void EditorSceneRuntime::EnsureSceneSelection() { if (HasValidSelection() || SelectionService().HasSelectionKind(EditorSelectionKind::ProjectItem)) { return; } SelectFirstAvailableGameObject(); } const EditorStartupSceneResult& EditorSceneRuntime::GetStartupResult() const { return m_startupSceneResult; } 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; } m_startupSceneResult.ready = true; m_startupSceneResult.loadedFromDisk = false; m_startupSceneResult.scenePath = std::filesystem::path(); if (::XCEngine::Components::Scene* activeScene = m_backend->GetActiveScene(); activeScene != nullptr) { m_startupSceneResult.sceneName = activeScene->GetName(); } else { m_startupSceneResult.sceneName = sceneName.empty() ? std::string("Untitled") : std::string(sceneName); } ResetTransformEditHistory(); 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; } m_startupSceneResult.ready = true; m_startupSceneResult.loadedFromDisk = true; m_startupSceneResult.scenePath = scenePath; m_startupSceneResult.sceneName = scenePath.stem().string(); ResetTransformEditHistory(); 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::unique_ptr EditorSceneRuntime::BeginPlaySession() { return m_backend != nullptr ? m_backend->BeginPlaySession() : nullptr; } bool EditorSceneRuntime::RenameGameObject( std::string_view itemId, std::string_view newName) { const bool renamed = m_backend != nullptr && m_backend->RenameGameObject(itemId, newName); if (renamed) { IncrementInspectorRevision(); IncrementSceneContentRevision(); } RefreshScene(); return renamed; } bool EditorSceneRuntime::DeleteGameObject(std::string_view itemId) { ResetTransformEditHistory(); const bool deleted = m_backend != nullptr && m_backend->DeleteGameObject(itemId); if (deleted) { IncrementInspectorRevision(); IncrementSceneContentRevision(); } RefreshScene(); EnsureSceneSelection(); return deleted; } std::string EditorSceneRuntime::DuplicateGameObject(std::string_view itemId) { ResetTransformEditHistory(); const std::string duplicatedItemId = m_backend != nullptr ? m_backend->DuplicateGameObject(itemId) : std::string(); if (!duplicatedItemId.empty()) { IncrementInspectorRevision(); IncrementSceneContentRevision(); SetSelection(duplicatedItemId); } else { RefreshScene(); } return duplicatedItemId; } bool EditorSceneRuntime::ReparentGameObject( std::string_view itemId, std::string_view parentItemId) { ResetTransformEditHistory(); const bool reparented = m_backend != nullptr && m_backend->ReparentGameObject(itemId, parentItemId); if (reparented) { IncrementInspectorRevision(); IncrementSceneContentRevision(); } RefreshScene(); return reparented; } bool EditorSceneRuntime::MoveGameObjectBefore( std::string_view itemId, std::string_view targetItemId) { ResetTransformEditHistory(); const bool moved = m_backend != nullptr && m_backend->MoveGameObjectBefore(itemId, targetItemId); if (moved) { IncrementInspectorRevision(); IncrementSceneContentRevision(); } RefreshScene(); return moved; } bool EditorSceneRuntime::MoveGameObjectAfter( std::string_view itemId, std::string_view targetItemId) { ResetTransformEditHistory(); const bool moved = m_backend != nullptr && m_backend->MoveGameObjectAfter(itemId, targetItemId); if (moved) { IncrementInspectorRevision(); IncrementSceneContentRevision(); } RefreshScene(); return moved; } bool EditorSceneRuntime::MoveGameObjectToRoot(std::string_view itemId) { ResetTransformEditHistory(); const bool moved = m_backend != nullptr && m_backend->MoveGameObjectToRoot(itemId); if (moved) { IncrementInspectorRevision(); IncrementSceneContentRevision(); } RefreshScene(); return moved; } bool EditorSceneRuntime::AddComponentToSelectedGameObject( std::string_view componentTypeName) { if (componentTypeName.empty()) { return false; } const std::string selectedItemId = GetSelectedItemId(); if (selectedItemId.empty() || m_backend == nullptr || !m_backend->AddComponent(selectedItemId, componentTypeName)) { return false; } IncrementInspectorRevision(); IncrementSceneContentRevision(); RefreshScene(); 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; } ResetTransformEditHistory(); const bool removed = m_backend != nullptr && m_backend->RemoveComponent(GetSelectedItemId(), componentId); if (removed) { IncrementInspectorRevision(); IncrementSceneContentRevision(); } RefreshScene(); return removed; } bool EditorSceneRuntime::SetSelectedTransformLocalPosition( std::string_view componentId, const Vector3& position) { const EditorSceneComponentDescriptor descriptor = ResolveSelectedComponentDescriptor(componentId); if (m_backend == nullptr || descriptor.typeName != "Transform") { return false; } SceneTransformSnapshot beforeSnapshot = {}; if (!CaptureSelectedTransformSnapshot(beforeSnapshot)) { return false; } if (!m_backend->SetTransformLocalPosition( GetSelectedItemId(), componentId, position)) { return false; } SceneTransformSnapshot afterSnapshot = {}; CaptureSelectedTransformSnapshot(afterSnapshot); if (!TransformSnapshotsMatch(beforeSnapshot, afterSnapshot)) { RecordTransformEdit(beforeSnapshot, afterSnapshot); 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") { return false; } SceneTransformSnapshot beforeSnapshot = {}; if (!CaptureSelectedTransformSnapshot(beforeSnapshot)) { return false; } if (!m_backend->SetTransformLocalEulerAngles( GetSelectedItemId(), componentId, eulerAngles)) { return false; } SceneTransformSnapshot afterSnapshot = {}; CaptureSelectedTransformSnapshot(afterSnapshot); if (!TransformSnapshotsMatch(beforeSnapshot, afterSnapshot)) { RecordTransformEdit(beforeSnapshot, afterSnapshot); 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") { return false; } SceneTransformSnapshot beforeSnapshot = {}; if (!CaptureSelectedTransformSnapshot(beforeSnapshot)) { return false; } if (!m_backend->SetTransformLocalScale( GetSelectedItemId(), componentId, scale)) { return false; } SceneTransformSnapshot afterSnapshot = {}; CaptureSelectedTransformSnapshot(afterSnapshot); if (!TransformSnapshotsMatch(beforeSnapshot, afterSnapshot)) { RecordTransformEdit(beforeSnapshot, afterSnapshot); IncrementInspectorRevision(); IncrementSceneContentRevision(); } return true; } bool EditorSceneRuntime::ApplySelectedComponentMutation( const EditorSceneComponentMutation& mutation) { if (m_backend == nullptr || !mutation.IsValid() || !ResolveSelectedComponentDescriptor(mutation.componentId).IsValid()) { return false; } if (!m_backend->ApplyComponentMutation( GetSelectedItemId(), mutation)) { return false; } 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::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) { 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) { 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::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; } 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; } void EditorSceneRuntime::ResetTransformEditHistory() { m_transformUndoStack.clear(); m_transformRedoStack.clear(); } void EditorSceneRuntime::IncrementInspectorRevision() { ++m_inspectorRevision; } void EditorSceneRuntime::IncrementSceneContentRevision() { ++m_sceneContentRevision; } } // namespace XCEngine::UI::Editor::App