#include #include "Core/EventBus.h" #include "Core/IEditorContext.h" #include "Core/IProjectManager.h" #include "Core/ISceneManager.h" #include "Core/ISelectionManager.h" #include "Core/IUndoManager.h" #include "Viewport/IViewportHostService.h" #include "Viewport/SceneViewportInteractionFrame.h" #include namespace { using XCEngine::Editor::AssetItemPtr; using XCEngine::Editor::BuildSceneViewportFrameGeometry; using XCEngine::Editor::BuildSceneViewportInteractionFrameState; using XCEngine::Editor::BuildSceneViewportInteractionResolveRequest; using XCEngine::Editor::BuildSceneViewportToolState; using XCEngine::Editor::EditorActionRoute; using XCEngine::Editor::EditorRuntimeMode; using XCEngine::Editor::EditorViewportFrame; using XCEngine::Editor::EditorViewportKind; using XCEngine::Editor::EventBus; using XCEngine::Editor::IEditorContext; using XCEngine::Editor::IProjectManager; using XCEngine::Editor::ISceneManager; using XCEngine::Editor::ISelectionManager; using XCEngine::Editor::IViewportHostService; using XCEngine::Editor::SceneSnapshot; using XCEngine::Editor::SceneViewportInput; using XCEngine::Editor::SceneViewportInteractionFrameState; using XCEngine::Editor::SceneViewportOrientationAxis; using XCEngine::Editor::SceneViewportOverlayData; using XCEngine::Editor::SceneViewportOverlayFrameData; using XCEngine::Editor::SceneViewportPivotMode; using XCEngine::Editor::SceneViewportToolMode; using XCEngine::Editor::SceneViewportTransformGizmoOverlayState; using XCEngine::Editor::SceneViewportTransformSpaceMode; using XCEngine::Rendering::RenderContext; class EmptySelectionManager : public ISelectionManager { public: void SetSelectedEntity(uint64_t) override {} void SetSelectedEntities(const std::vector&) override {} void AddToSelection(uint64_t) override {} void RemoveFromSelection(uint64_t) override {} void ClearSelection() override {} uint64_t GetSelectedEntity() const override { return 0; } const std::vector& GetSelectedEntities() const override { return selectedEntities; } bool HasSelection() const override { return false; } size_t GetSelectionCount() const override { return 0; } bool IsSelected(uint64_t) const override { return false; } private: std::vector selectedEntities = {}; }; class EmptySceneManager : public ISceneManager { public: XCEngine::Components::GameObject* CreateEntity( const std::string&, XCEngine::Components::GameObject* = nullptr) override { return nullptr; } void DeleteEntity(XCEngine::Components::GameObject::ID) override {} void RenameEntity(XCEngine::Components::GameObject::ID, const std::string&) override {} XCEngine::Components::GameObject* GetEntity(XCEngine::Components::GameObject::ID) override { return nullptr; } const std::vector& GetRootEntities() const override { return rootEntities; } void CopyEntity(XCEngine::Components::GameObject::ID) override {} XCEngine::Components::GameObject::ID PasteEntity(XCEngine::Components::GameObject::ID = 0) override { return 0; } XCEngine::Components::GameObject::ID DuplicateEntity(XCEngine::Components::GameObject::ID) override { return 0; } void MoveEntity(XCEngine::Components::GameObject::ID, XCEngine::Components::GameObject::ID) override {} bool HasClipboardData() const override { return false; } void NewScene(const std::string& = "Untitled Scene") override {} bool LoadScene(const std::string&) override { return false; } bool SaveScene() override { return false; } bool SaveSceneAs(const std::string&) override { return false; } bool LoadStartupScene(const std::string&) override { return false; } bool HasActiveScene() const override { return false; } bool IsSceneDirty() const override { return false; } void MarkSceneDirty() override {} void SetSceneDocumentDirtyTrackingEnabled(bool) override {} bool IsSceneDocumentDirtyTrackingEnabled() const override { return false; } const std::string& GetCurrentScenePath() const override { return empty; } const std::string& GetCurrentSceneName() const override { return empty; } XCEngine::Components::Scene* GetScene() override { return nullptr; } const XCEngine::Components::Scene* GetScene() const override { return nullptr; } SceneSnapshot CaptureSceneSnapshot() const override { return {}; } bool RestoreSceneSnapshot(const SceneSnapshot&) override { return false; } void CreateDemoScene() override {} private: std::vector rootEntities = {}; std::string empty; }; class EmptyProjectManager : public IProjectManager { public: const std::vector& GetCurrentItems() const override { return items; } AssetItemPtr GetRootFolder() const override { return {}; } AssetItemPtr GetCurrentFolder() const override { return {}; } AssetItemPtr GetSelectedItem() const override { return {}; } const std::string& GetSelectedItemPath() const override { return empty; } int GetSelectedIndex() const override { return -1; } void SetSelectedIndex(int) override {} void SetSelectedItem(const AssetItemPtr&) override {} void ClearSelection() override {} int FindCurrentItemIndex(const std::string&) const override { return -1; } void NavigateToFolder(const AssetItemPtr&) override {} void NavigateBack() override {} void NavigateToIndex(size_t) override {} bool CanNavigateBack() const override { return false; } std::string GetCurrentPath() const override { return empty; } size_t GetPathDepth() const override { return 0; } std::string GetPathName(size_t) const override { return {}; } void Initialize(const std::string&) override {} void RefreshCurrentFolder() override {} AssetItemPtr CreateFolder(const std::string&) override { return {}; } bool DeleteItem(const std::string&) override { return false; } bool MoveItem(const std::string&, const std::string&) override { return false; } bool RenameItem(const std::string&, const std::string&) override { return false; } const std::string& GetProjectPath() const override { return empty; } private: std::vector items = {}; std::string empty; }; class EmptyUndoManager : public XCEngine::Editor::IUndoManager { public: void ClearHistory() override {} bool CanUndo() const override { return false; } bool CanRedo() const override { return false; } const std::string& GetUndoLabel() const override { return empty; } const std::string& GetRedoLabel() const override { return empty; } void Undo() override {} void Redo() override {} XCEngine::Editor::UndoStateSnapshot CaptureCurrentState() const override { return {}; } void PushCommand(const std::string&, XCEngine::Editor::UndoStateSnapshot, XCEngine::Editor::UndoStateSnapshot) override {} void BeginInteractiveChange(const std::string&) override {} bool HasPendingInteractiveChange() const override { return false; } void FinalizeInteractiveChange() override {} void CancelInteractiveChange() override {} private: std::string empty; }; class StubEditorContext : public IEditorContext { public: EventBus& GetEventBus() override { return eventBus; } ISelectionManager& GetSelectionManager() override { return selectionManager; } ISceneManager& GetSceneManager() override { return sceneManager; } IProjectManager& GetProjectManager() override { return projectManager; } XCEngine::Editor::IUndoManager& GetUndoManager() override { return undoManager; } IViewportHostService* GetViewportHostService() override { return viewportHostService; } void SetActiveActionRoute(EditorActionRoute route) override { activeRoute = route; } EditorActionRoute GetActiveActionRoute() const override { return activeRoute; } void SetRuntimeMode(EditorRuntimeMode mode) override { runtimeMode = mode; } EditorRuntimeMode GetRuntimeMode() const override { return runtimeMode; } void SetProjectPath(const std::string& path) override { projectPath = path; } const std::string& GetProjectPath() const override { return projectPath; } EventBus eventBus = {}; EmptySelectionManager selectionManager = {}; EmptySceneManager sceneManager = {}; EmptyProjectManager projectManager = {}; EmptyUndoManager undoManager = {}; IViewportHostService* viewportHostService = nullptr; EditorActionRoute activeRoute = EditorActionRoute::None; EditorRuntimeMode runtimeMode = EditorRuntimeMode::Edit; std::string projectPath; }; class StubViewportHostService : public IViewportHostService { public: void BeginFrame() override {} EditorViewportFrame RequestViewport(EditorViewportKind, const ImVec2&) override { return {}; } void UpdateSceneViewInput(IEditorContext&, const SceneViewportInput&) override {} uint64_t PickSceneViewEntity(IEditorContext&, const ImVec2&, const ImVec2&) override { return 0; } void AlignSceneViewToOrientationAxis(SceneViewportOrientationAxis) override {} SceneViewportOverlayData GetSceneViewOverlayData() const override { return overlay; } const SceneViewportOverlayFrameData& GetSceneViewEditorOverlayFrameData(IEditorContext&) override { return overlayFrameData; } void SetSceneViewTransformGizmoOverlayState(const SceneViewportTransformGizmoOverlayState& state) override { ++submissionCount; lastOverlayState = state; } void RenderRequestedViewports(IEditorContext&, const RenderContext&) override {} SceneViewportOverlayData overlay = {}; SceneViewportOverlayFrameData overlayFrameData = {}; SceneViewportTransformGizmoOverlayState lastOverlayState = {}; int submissionCount = 0; }; } // namespace TEST(SceneViewportInteractionFrameTest, ToolStateMapsModesAndTransformSpace) { const auto moveState = BuildSceneViewportToolState( SceneViewportToolMode::Move, SceneViewportPivotMode::Center, SceneViewportTransformSpaceMode::Local); EXPECT_FALSE(moveState.usingViewMoveTool); EXPECT_FALSE(moveState.usingTransformTool); EXPECT_TRUE(moveState.showingMoveGizmo); EXPECT_FALSE(moveState.showingRotateGizmo); EXPECT_FALSE(moveState.showingScaleGizmo); EXPECT_TRUE(moveState.useCenterPivot); EXPECT_TRUE(moveState.localSpace); EXPECT_TRUE(moveState.gizmoFrameOptions.localSpace); const auto transformState = BuildSceneViewportToolState( SceneViewportToolMode::Transform, SceneViewportPivotMode::Pivot, SceneViewportTransformSpaceMode::Global); EXPECT_FALSE(transformState.usingViewMoveTool); EXPECT_TRUE(transformState.usingTransformTool); EXPECT_TRUE(transformState.showingMoveGizmo); EXPECT_TRUE(transformState.showingRotateGizmo); EXPECT_TRUE(transformState.showingScaleGizmo); EXPECT_FALSE(transformState.localSpace); EXPECT_TRUE(transformState.gizmoFrameOptions.usingTransformTool); } TEST(SceneViewportInteractionFrameTest, FrameGeometryBuildsViewportAndLocalMouseCoordinates) { const auto geometry = BuildSceneViewportFrameGeometry( ImVec2(640.0f, 360.0f), ImVec2(100.0f, 40.0f), ImVec2(148.0f, 92.0f)); EXPECT_FLOAT_EQ(geometry.viewportSize.x, 640.0f); EXPECT_FLOAT_EQ(geometry.viewportSize.y, 360.0f); EXPECT_FLOAT_EQ(geometry.localMousePosition.x, 48.0f); EXPECT_FLOAT_EQ(geometry.localMousePosition.y, 52.0f); } TEST(SceneViewportInteractionFrameTest, FrameStateUsesEmptyOverlayWhenViewportIsNotInteractive) { StubEditorContext context = {}; StubViewportHostService viewportHostService = {}; context.viewportHostService = &viewportHostService; const SceneViewportOverlayFrameData emptyOverlayFrameData = {}; XCEngine::Editor::SceneViewportMoveGizmo moveGizmo = {}; XCEngine::Editor::SceneViewportRotateGizmo rotateGizmo = {}; XCEngine::Editor::SceneViewportScaleGizmo scaleGizmo = {}; const SceneViewportInteractionFrameState frameState = BuildSceneViewportInteractionFrameState( context, viewportHostService, false, BuildSceneViewportFrameGeometry( ImVec2(320.0f, 180.0f), ImVec2(10.0f, 20.0f), ImVec2(30.0f, 60.0f)), BuildSceneViewportToolState( SceneViewportToolMode::Move, SceneViewportPivotMode::Pivot, SceneViewportTransformSpaceMode::Global) .gizmoFrameOptions, moveGizmo, rotateGizmo, scaleGizmo, emptyOverlayFrameData); EXPECT_FALSE(frameState.hasInteractiveViewport); EXPECT_EQ(frameState.overlayFrameData, &emptyOverlayFrameData); EXPECT_FALSE(frameState.overlay.valid); EXPECT_EQ(frameState.activeGizmoKind, XCEngine::Editor::SceneViewportActiveGizmoKind::None); EXPECT_FALSE(frameState.gizmoActive); EXPECT_EQ(viewportHostService.submissionCount, 0); } TEST(SceneViewportInteractionFrameTest, FrameStateConsumesViewportHostOverlayAndSubmission) { StubEditorContext context = {}; StubViewportHostService viewportHostService = {}; context.viewportHostService = &viewportHostService; viewportHostService.overlay.valid = true; viewportHostService.overlay.verticalFovDegrees = 55.0f; const SceneViewportOverlayFrameData emptyOverlayFrameData = {}; XCEngine::Editor::SceneViewportMoveGizmo moveGizmo = {}; XCEngine::Editor::SceneViewportRotateGizmo rotateGizmo = {}; XCEngine::Editor::SceneViewportScaleGizmo scaleGizmo = {}; const SceneViewportInteractionFrameState frameState = BuildSceneViewportInteractionFrameState( context, viewportHostService, true, BuildSceneViewportFrameGeometry( ImVec2(320.0f, 180.0f), ImVec2(10.0f, 20.0f), ImVec2(30.0f, 60.0f)), BuildSceneViewportToolState( SceneViewportToolMode::Move, SceneViewportPivotMode::Pivot, SceneViewportTransformSpaceMode::Global) .gizmoFrameOptions, moveGizmo, rotateGizmo, scaleGizmo, emptyOverlayFrameData); EXPECT_TRUE(frameState.hasInteractiveViewport); EXPECT_TRUE(frameState.overlay.valid); EXPECT_FLOAT_EQ(frameState.overlay.verticalFovDegrees, 55.0f); EXPECT_EQ(frameState.overlayFrameData, &viewportHostService.overlayFrameData); EXPECT_TRUE(frameState.hudOverlay.sceneOverlay.valid); EXPECT_EQ(viewportHostService.submissionCount, 1); } TEST(SceneViewportInteractionFrameTest, ResolveRequestCopiesFrameStateAndViewportGeometry) { SceneViewportInteractionFrameState frameState = {}; SceneViewportOverlayFrameData overlayFrameData = {}; frameState.overlayFrameData = &overlayFrameData; frameState.hudOverlay = XCEngine::Editor::BuildSceneViewportHudOverlayData( SceneViewportOverlayData{true}); const auto geometry = BuildSceneViewportFrameGeometry( ImVec2(800.0f, 600.0f), ImVec2(100.0f, 120.0f), ImVec2(180.0f, 260.0f)); const auto request = BuildSceneViewportInteractionResolveRequest( frameState, geometry, ImVec2(100.0f, 120.0f), ImVec2(900.0f, 720.0f), ImVec2(180.0f, 260.0f)); EXPECT_EQ(request.overlayFrameData, &overlayFrameData); EXPECT_EQ(request.hudOverlay, &frameState.hudOverlay); EXPECT_FLOAT_EQ(request.viewportSize.x, 800.0f); EXPECT_FLOAT_EQ(request.viewportSize.y, 600.0f); EXPECT_FLOAT_EQ(request.localMousePosition.x, 80.0f); EXPECT_FLOAT_EQ(request.localMousePosition.y, 140.0f); EXPECT_FLOAT_EQ(request.absoluteMousePosition.x, 180.0f); EXPECT_FLOAT_EQ(request.absoluteMousePosition.y, 260.0f); }