#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/SceneViewportInteractionActions.h" #include namespace { using XCEngine::Editor::AssetItemPtr; using XCEngine::Editor::BuildSceneViewportHoveredHandleState; using XCEngine::Editor::BuildSceneViewportInteractionActions; using XCEngine::Editor::DispatchSceneViewportInteractionActions; using XCEngine::Editor::EditorActionRoute; using XCEngine::Editor::EditorRuntimeMode; using XCEngine::Editor::EventBus; using XCEngine::Editor::IEditorContext; using XCEngine::Editor::IProjectManager; using XCEngine::Editor::ISceneManager; using XCEngine::Editor::ISelectionManager; using XCEngine::Editor::IUndoManager; using XCEngine::Editor::SceneSnapshot; using XCEngine::Editor::SceneViewportActiveGizmoKind; using XCEngine::Editor::SceneViewportGizmoAxis; using XCEngine::Editor::SceneViewportInteractionActions; using XCEngine::Editor::SceneViewportInteractionKind; using XCEngine::Editor::SceneViewportInteractionResult; using XCEngine::Editor::SceneViewportOrientationAxis; using XCEngine::Editor::IViewportHostService; using XCEngine::Math::Vector2; using XCEngine::UI::UIPoint; using XCEngine::UI::UISize; class StubSelectionManager : public ISelectionManager { public: void SetSelectedEntity(uint64_t entityId) override { selectedEntity = entityId; selectedEntities = entityId == 0 ? std::vector{} : std::vector{entityId}; clearCount = 0; } void SetSelectedEntities(const std::vector& entityIds) override { selectedEntities = entityIds; selectedEntity = entityIds.empty() ? 0 : entityIds.front(); } void AddToSelection(uint64_t entityId) override { selectedEntities.push_back(entityId); selectedEntity = entityId; } void RemoveFromSelection(uint64_t entityId) override { selectedEntities.erase( std::remove(selectedEntities.begin(), selectedEntities.end(), entityId), selectedEntities.end()); if (selectedEntity == entityId) { selectedEntity = selectedEntities.empty() ? 0 : selectedEntities.front(); } } void ClearSelection() override { selectedEntity = 0; selectedEntities.clear(); ++clearCount; } uint64_t GetSelectedEntity() const override { return selectedEntity; } const std::vector& GetSelectedEntities() const override { return selectedEntities; } bool HasSelection() const override { return !selectedEntities.empty(); } size_t GetSelectionCount() const override { return selectedEntities.size(); } bool IsSelected(uint64_t entityId) const override { return std::find(selectedEntities.begin(), selectedEntities.end(), entityId) != selectedEntities.end(); } uint64_t selectedEntity = 0; std::vector selectedEntities = {}; int clearCount = 0; }; class StubSceneManager : 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; } XCEngine::Editor::SceneLoadProgressSnapshot GetSceneLoadProgress() const override { return {}; } void NotifySceneViewportFramePresented(std::uint32_t) override {} SceneSnapshot CaptureSceneSnapshot() const override { return {}; } bool RestoreSceneSnapshot(const SceneSnapshot&) override { return false; } void CreateDemoScene() override {} private: std::vector rootEntities = {}; std::string empty; }; class StubProjectManager : 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 StubUndoManager : public 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 StubViewportHostService : public IViewportHostService { public: void BeginFrame() override {} XCEngine::Editor::EditorViewportFrame RequestViewport( XCEngine::Editor::EditorViewportKind, const UISize&) override { return {}; } void UpdateSceneViewInput(IEditorContext&, const XCEngine::Editor::SceneViewportInput&) override {} uint64_t PickSceneViewEntity(IEditorContext&, const UISize&, const UIPoint&) override { ++pickCallCount; return pickedEntity; } void AlignSceneViewToOrientationAxis(SceneViewportOrientationAxis axis) override { alignedAxis = axis; } XCEngine::Editor::SceneViewportOverlayData GetSceneViewOverlayData() const override { return {}; } const XCEngine::Editor::SceneViewportOverlayFrameData& GetSceneViewEditorOverlayFrameData(IEditorContext&) override { return overlayFrameData; } void SetSceneViewTransformGizmoOverlayState(const XCEngine::Editor::SceneViewportTransformGizmoOverlayState&) override {} void RenderRequestedViewports(IEditorContext&, const XCEngine::Rendering::RenderContext&) override {} SceneViewportOrientationAxis alignedAxis = SceneViewportOrientationAxis::None; uint64_t pickedEntity = 0; int pickCallCount = 0; private: XCEngine::Editor::SceneViewportOverlayFrameData overlayFrameData = {}; }; 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; } 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 = {}; StubSelectionManager selectionManager = {}; StubSceneManager sceneManager = {}; StubProjectManager projectManager = {}; StubUndoManager undoManager = {}; IViewportHostService* viewportHostService = nullptr; EditorActionRoute activeRoute = EditorActionRoute::None; EditorRuntimeMode runtimeMode = EditorRuntimeMode::Edit; std::string projectPath; }; } // namespace TEST(SceneViewportInteractionActionsTest, BuildHoveredHandleStateMapsMoveInteraction) { SceneViewportInteractionResult interaction = {}; interaction.kind = SceneViewportInteractionKind::MoveGizmo; interaction.moveAxis = SceneViewportGizmoAxis::Y; const auto hoverState = BuildSceneViewportHoveredHandleState(interaction); EXPECT_EQ(hoverState.hoveredGizmoKind, SceneViewportActiveGizmoKind::Move); EXPECT_EQ(hoverState.moveAxis, SceneViewportGizmoAxis::Y); } TEST(SceneViewportInteractionActionsTest, BuildInteractionActionsMapsOrientationClick) { SceneViewportInteractionResult interaction = {}; interaction.kind = SceneViewportInteractionKind::OrientationGizmo; interaction.orientationAxis = SceneViewportOrientationAxis::PositiveZ; const SceneViewportInteractionActions actions = BuildSceneViewportInteractionActions(interaction, true, true, true); EXPECT_TRUE(actions.orientationGizmoClick); EXPECT_EQ(actions.orientationAxis, SceneViewportOrientationAxis::PositiveZ); EXPECT_FALSE(actions.selectSceneClick); } TEST(SceneViewportInteractionActionsTest, BuildInteractionActionsMapsScenePickFallback) { const SceneViewportInteractionActions actions = BuildSceneViewportInteractionActions({}, true, true, true); EXPECT_TRUE(actions.selectSceneClick); EXPECT_FALSE(actions.sceneIconClick); EXPECT_FALSE(actions.orientationGizmoClick); } TEST(SceneViewportInteractionActionsTest, DispatchOrientationActionAlignsViewport) { StubViewportHostService viewportHostService = {}; StubEditorContext context = {}; context.viewportHostService = &viewportHostService; SceneViewportInteractionActions actions = {}; actions.orientationGizmoClick = true; actions.orientationAxis = SceneViewportOrientationAxis::PositiveY; DispatchSceneViewportInteractionActions( actions, context, viewportHostService, UISize(200.0f, 100.0f), Vector2(40.0f, 30.0f)); EXPECT_EQ(viewportHostService.alignedAxis, SceneViewportOrientationAxis::PositiveY); EXPECT_EQ(viewportHostService.pickCallCount, 0); } TEST(SceneViewportInteractionActionsTest, DispatchSceneIconClickSelectsEntityWithoutPicking) { StubViewportHostService viewportHostService = {}; StubEditorContext context = {}; context.viewportHostService = &viewportHostService; SceneViewportInteractionActions actions = {}; actions.sceneIconClick = true; actions.sceneIconEntityId = 42; DispatchSceneViewportInteractionActions( actions, context, viewportHostService, UISize(200.0f, 100.0f), Vector2(40.0f, 30.0f)); EXPECT_EQ(context.selectionManager.selectedEntity, 42u); EXPECT_EQ(viewportHostService.pickCallCount, 0); } TEST(SceneViewportInteractionActionsTest, DispatchScenePickSelectsPickedEntityOrClearsSelection) { StubViewportHostService viewportHostService = {}; StubEditorContext context = {}; context.viewportHostService = &viewportHostService; SceneViewportInteractionActions actions = {}; actions.selectSceneClick = true; viewportHostService.pickedEntity = 77; DispatchSceneViewportInteractionActions( actions, context, viewportHostService, UISize(200.0f, 100.0f), Vector2(40.0f, 30.0f)); EXPECT_EQ(context.selectionManager.selectedEntity, 77u); EXPECT_EQ(viewportHostService.pickCallCount, 1); viewportHostService.pickedEntity = 0; DispatchSceneViewportInteractionActions( actions, context, viewportHostService, UISize(200.0f, 100.0f), Vector2(40.0f, 30.0f)); EXPECT_EQ(context.selectionManager.selectedEntity, 0u); EXPECT_GE(context.selectionManager.clearCount, 1); }