From 468dbfa7ac9e1b39e74d11712270bc96a2147e42 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Sat, 4 Apr 2026 00:21:25 +0800 Subject: [PATCH] Formalize scene viewport interaction actions --- ...rlay_Gizmo_Rework_Checkpoint_2026-04-02.md | 17 + editor/CMakeLists.txt | 1 + .../SceneViewportInteractionActions.cpp | 128 +++++++ .../SceneViewportInteractionActions.h | 65 ++++ editor/src/panels/SceneViewPanel.cpp | 116 ++---- tests/editor/CMakeLists.txt | 2 + ...est_scene_viewport_interaction_actions.cpp | 344 ++++++++++++++++++ 7 files changed, 584 insertions(+), 89 deletions(-) create mode 100644 editor/src/Viewport/SceneViewportInteractionActions.cpp create mode 100644 editor/src/Viewport/SceneViewportInteractionActions.h create mode 100644 tests/editor/test_scene_viewport_interaction_actions.cpp diff --git a/docs/plan/SceneViewport_Overlay_Gizmo_Rework_Checkpoint_2026-04-02.md b/docs/plan/SceneViewport_Overlay_Gizmo_Rework_Checkpoint_2026-04-02.md index c48e7085..fbb22c50 100644 --- a/docs/plan/SceneViewport_Overlay_Gizmo_Rework_Checkpoint_2026-04-02.md +++ b/docs/plan/SceneViewport_Overlay_Gizmo_Rework_Checkpoint_2026-04-02.md @@ -1,5 +1,22 @@ # SceneViewport Overlay/Gizmo Rework Checkpoint +## Update 2026-04-04 Phase 5B + +### Interaction Actions Completed + +- Added `SceneViewportInteractionActions.{h,cpp}` to formalize viewport-side hover state application and click action derivation. +- `SceneViewPanel` no longer assembles orientation click, scene icon click, scene pick fallback, or hovered gizmo handle state inline. +- Selection and orientation side effects now flow through `DispatchSceneViewportInteractionActions(...)`. +- The panel keeps navigation and active gizmo drag ownership; interaction semantics are now authored in the viewport module. + +### Verification + +- `cmake --build build --config Debug --target editor_tests -- /p:BuildProjectReferences=false` +- `build/tests/Editor/Debug/editor_tests.exe --gtest_filter=SceneViewportInteractionActionsTest.*:SceneViewportInteractionResolverTest.*:SceneViewportOverlayRenderer_Test.*:SceneViewportOverlayProviderRegistryTest.*:ViewportRenderFlowUtilsTest.*` +- `cmake --build build --config Debug --target XCEditor` + +All commands completed successfully in `Debug`. + ## Update 2026-04-03 Phase 5A ### Interaction Resolver Completed diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index ee94374f..2dd71a25 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -83,6 +83,7 @@ add_executable(${PROJECT_NAME} WIN32 src/Viewport/SceneViewportRotateGizmo.cpp src/Viewport/SceneViewportScaleGizmo.cpp src/Viewport/SceneViewportHudOverlay.cpp + src/Viewport/SceneViewportInteractionActions.cpp src/Viewport/SceneViewportInteractionResolver.cpp src/Viewport/SceneViewportOrientationGizmo.cpp src/Viewport/SceneViewportOverlayBuilder.cpp diff --git a/editor/src/Viewport/SceneViewportInteractionActions.cpp b/editor/src/Viewport/SceneViewportInteractionActions.cpp new file mode 100644 index 00000000..d2816435 --- /dev/null +++ b/editor/src/Viewport/SceneViewportInteractionActions.cpp @@ -0,0 +1,128 @@ +#include "SceneViewportInteractionActions.h" + +#include "IViewportHostService.h" + +namespace XCEngine { +namespace Editor { + +SceneViewportActiveGizmoKind ToSceneViewportActiveGizmoKind(SceneViewportInteractionKind kind) { + switch (kind) { + case SceneViewportInteractionKind::MoveGizmo: + return SceneViewportActiveGizmoKind::Move; + case SceneViewportInteractionKind::RotateGizmo: + return SceneViewportActiveGizmoKind::Rotate; + case SceneViewportInteractionKind::ScaleGizmo: + return SceneViewportActiveGizmoKind::Scale; + case SceneViewportInteractionKind::OrientationGizmo: + case SceneViewportInteractionKind::SceneIcon: + case SceneViewportInteractionKind::None: + default: + return SceneViewportActiveGizmoKind::None; + } +} + +SceneViewportHoveredHandleState BuildSceneViewportHoveredHandleState( + const SceneViewportInteractionResult& interaction) { + SceneViewportHoveredHandleState state = {}; + state.hoveredGizmoKind = ToSceneViewportActiveGizmoKind(interaction.kind); + if (interaction.kind == SceneViewportInteractionKind::MoveGizmo) { + state.moveAxis = interaction.moveAxis; + state.movePlane = interaction.movePlane; + } else if (interaction.kind == SceneViewportInteractionKind::RotateGizmo) { + state.rotateAxis = interaction.rotateAxis; + } else if (interaction.kind == SceneViewportInteractionKind::ScaleGizmo) { + state.scaleHandle = interaction.scaleHandle; + } + + return state; +} + +void ApplySceneViewportHoveredHandleState( + const SceneViewportHoveredHandleState& hoveredHandleState, + bool gizmoActive, + bool showingMoveGizmo, + SceneViewportMoveGizmo& moveGizmo, + bool showingRotateGizmo, + SceneViewportRotateGizmo& rotateGizmo, + bool showingScaleGizmo, + SceneViewportScaleGizmo& scaleGizmo) { + if (gizmoActive) { + return; + } + + if (showingMoveGizmo) { + moveGizmo.SetHoveredHandle(hoveredHandleState.moveAxis, hoveredHandleState.movePlane); + } + if (showingRotateGizmo) { + rotateGizmo.SetHoveredHandle(hoveredHandleState.rotateAxis); + } + if (showingScaleGizmo) { + scaleGizmo.SetHoveredHandle(hoveredHandleState.scaleHandle); + } +} + +SceneViewportInteractionActions BuildSceneViewportInteractionActions( + const SceneViewportInteractionResult& interaction, + bool hasInteractiveViewport, + bool clickedLeft, + bool canResolveViewportInteraction) { + SceneViewportInteractionActions actions = {}; + actions.hoveredGizmoKind = ToSceneViewportActiveGizmoKind(interaction.kind); + actions.orientationAxis = interaction.kind == SceneViewportInteractionKind::OrientationGizmo + ? interaction.orientationAxis + : SceneViewportOrientationAxis::None; + actions.sceneIconEntityId = interaction.kind == SceneViewportInteractionKind::SceneIcon + ? interaction.entityId + : 0; + actions.beginTransformGizmo = + hasInteractiveViewport && + clickedLeft && + actions.hoveredGizmoKind != SceneViewportActiveGizmoKind::None; + actions.orientationGizmoClick = + hasInteractiveViewport && + clickedLeft && + actions.orientationAxis != SceneViewportOrientationAxis::None; + actions.sceneIconClick = + hasInteractiveViewport && + clickedLeft && + actions.sceneIconEntityId != 0; + actions.selectSceneClick = + hasInteractiveViewport && + clickedLeft && + canResolveViewportInteraction && + !interaction.HasHit(); + return actions; +} + +void DispatchSceneViewportInteractionActions( + const SceneViewportInteractionActions& actions, + IEditorContext& context, + IViewportHostService& viewportHostService, + const ImVec2& viewportSize, + const Math::Vector2& localMousePosition) { + if (actions.orientationGizmoClick) { + viewportHostService.AlignSceneViewToOrientationAxis(actions.orientationAxis); + } + + if (actions.sceneIconClick) { + context.GetSelectionManager().SetSelectedEntity(actions.sceneIconEntityId); + return; + } + + if (!actions.selectSceneClick) { + return; + } + + const uint64_t selectedEntity = viewportHostService.PickSceneViewEntity( + context, + viewportSize, + ImVec2(localMousePosition.x, localMousePosition.y)); + if (selectedEntity != 0) { + context.GetSelectionManager().SetSelectedEntity(selectedEntity); + } else { + context.GetSelectionManager().ClearSelection(); + } +} + +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/Viewport/SceneViewportInteractionActions.h b/editor/src/Viewport/SceneViewportInteractionActions.h new file mode 100644 index 00000000..0eb088d4 --- /dev/null +++ b/editor/src/Viewport/SceneViewportInteractionActions.h @@ -0,0 +1,65 @@ +#pragma once + +#include "Core/IEditorContext.h" +#include "SceneViewportInteractionResolver.h" +#include "SceneViewportTransformGizmoFrameBuilder.h" + +#include + +#include + +namespace XCEngine { +namespace Editor { + +struct SceneViewportHoveredHandleState { + SceneViewportActiveGizmoKind hoveredGizmoKind = SceneViewportActiveGizmoKind::None; + SceneViewportGizmoAxis moveAxis = SceneViewportGizmoAxis::None; + SceneViewportGizmoPlane movePlane = SceneViewportGizmoPlane::None; + SceneViewportRotateGizmoAxis rotateAxis = SceneViewportRotateGizmoAxis::None; + SceneViewportScaleGizmoHandle scaleHandle = SceneViewportScaleGizmoHandle::None; +}; + +struct SceneViewportInteractionActions { + SceneViewportActiveGizmoKind hoveredGizmoKind = SceneViewportActiveGizmoKind::None; + SceneViewportOrientationAxis orientationAxis = SceneViewportOrientationAxis::None; + uint64_t sceneIconEntityId = 0; + bool beginTransformGizmo = false; + bool orientationGizmoClick = false; + bool sceneIconClick = false; + bool selectSceneClick = false; + + bool HasClickAction() const { + return beginTransformGizmo || orientationGizmoClick || sceneIconClick || selectSceneClick; + } +}; + +SceneViewportActiveGizmoKind ToSceneViewportActiveGizmoKind(SceneViewportInteractionKind kind); + +SceneViewportHoveredHandleState BuildSceneViewportHoveredHandleState( + const SceneViewportInteractionResult& interaction); + +void ApplySceneViewportHoveredHandleState( + const SceneViewportHoveredHandleState& hoveredHandleState, + bool gizmoActive, + bool showingMoveGizmo, + SceneViewportMoveGizmo& moveGizmo, + bool showingRotateGizmo, + SceneViewportRotateGizmo& rotateGizmo, + bool showingScaleGizmo, + SceneViewportScaleGizmo& scaleGizmo); + +SceneViewportInteractionActions BuildSceneViewportInteractionActions( + const SceneViewportInteractionResult& interaction, + bool hasInteractiveViewport, + bool clickedLeft, + bool canResolveViewportInteraction); + +void DispatchSceneViewportInteractionActions( + const SceneViewportInteractionActions& actions, + IEditorContext& context, + IViewportHostService& viewportHostService, + const ImVec2& viewportSize, + const Math::Vector2& localMousePosition); + +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/panels/SceneViewPanel.cpp b/editor/src/panels/SceneViewPanel.cpp index 6d9162e1..9fe691bb 100644 --- a/editor/src/panels/SceneViewPanel.cpp +++ b/editor/src/panels/SceneViewPanel.cpp @@ -5,6 +5,7 @@ #include "SceneViewPanel.h" #include "Viewport/SceneViewportEditorOverlayData.h" #include "Viewport/SceneViewportHudOverlay.h" +#include "Viewport/SceneViewportInteractionActions.h" #include "Viewport/SceneViewportInteractionResolver.h" #include "Viewport/SceneViewportOverlayHandleBuilder.h" #include "Viewport/SceneViewportMath.h" @@ -38,22 +39,6 @@ const char* GetSceneViewportTransformSpaceModeLabel(SceneViewportTransformSpaceM return mode == SceneViewportTransformSpaceMode::Global ? "Global" : "Local"; } -SceneViewportActiveGizmoKind ToActiveGizmoKind(SceneViewportInteractionKind kind) { - switch (kind) { - case SceneViewportInteractionKind::MoveGizmo: - return SceneViewportActiveGizmoKind::Move; - case SceneViewportInteractionKind::RotateGizmo: - return SceneViewportActiveGizmoKind::Rotate; - case SceneViewportInteractionKind::ScaleGizmo: - return SceneViewportActiveGizmoKind::Scale; - case SceneViewportInteractionKind::OrientationGizmo: - case SceneViewportInteractionKind::SceneIcon: - case SceneViewportInteractionKind::None: - default: - return SceneViewportActiveGizmoKind::None; - } -} - float GetSceneToolbarToggleWidth(const char* firstLabel, const char* secondLabel) { constexpr float kHorizontalPadding = 10.0f; constexpr float kMinWidth = 68.0f; @@ -440,58 +425,22 @@ void SceneViewPanel::Render() { hoveredInteraction = ResolveSceneViewportInteraction(interactionRequest); } - if (!gizmoActive) { - if (showingMoveGizmo) { - m_moveGizmo.SetHoveredHandle( - hoveredInteraction.kind == SceneViewportInteractionKind::MoveGizmo - ? hoveredInteraction.moveAxis - : SceneViewportGizmoAxis::None, - hoveredInteraction.kind == SceneViewportInteractionKind::MoveGizmo - ? hoveredInteraction.movePlane - : SceneViewportGizmoPlane::None); - } - if (showingRotateGizmo) { - m_rotateGizmo.SetHoveredHandle( - hoveredInteraction.kind == SceneViewportInteractionKind::RotateGizmo - ? hoveredInteraction.rotateAxis - : SceneViewportRotateGizmoAxis::None); - } - if (showingScaleGizmo) { - m_scaleGizmo.SetHoveredHandle( - hoveredInteraction.kind == SceneViewportInteractionKind::ScaleGizmo - ? hoveredInteraction.scaleHandle - : SceneViewportScaleGizmoHandle::None); - } - } + ApplySceneViewportHoveredHandleState( + BuildSceneViewportHoveredHandleState(hoveredInteraction), + gizmoActive, + showingMoveGizmo, + m_moveGizmo, + showingRotateGizmo, + m_rotateGizmo, + showingScaleGizmo, + m_scaleGizmo); - const SceneViewportActiveGizmoKind hoveredGizmoKind = - ToActiveGizmoKind(hoveredInteraction.kind); - const bool gizmoHovering = hoveredGizmoKind != SceneViewportActiveGizmoKind::None; - const SceneViewportOrientationAxis orientationAxisHit = - hoveredInteraction.kind == SceneViewportInteractionKind::OrientationGizmo - ? hoveredInteraction.orientationAxis - : SceneViewportOrientationAxis::None; - const uint64_t clickedSceneIconEntity = - hoveredInteraction.kind == SceneViewportInteractionKind::SceneIcon - ? hoveredInteraction.entityId - : 0; - const bool beginTransformGizmo = - hasInteractiveViewport && - content.clickedLeft && - gizmoHovering; - const bool orientationGizmoClick = - hasInteractiveViewport && - content.clickedLeft && - orientationAxisHit != SceneViewportOrientationAxis::None; - const bool sceneIconClick = - hasInteractiveViewport && - content.clickedLeft && - clickedSceneIconEntity != 0; - const bool selectClick = - hasInteractiveViewport && - content.clickedLeft && - canResolveViewportInteraction && - !hoveredInteraction.HasHit(); + const SceneViewportInteractionActions interactionActions = + BuildSceneViewportInteractionActions( + hoveredInteraction, + hasInteractiveViewport, + content.clickedLeft, + canResolveViewportInteraction); const bool beginLeftPanDrag = usingViewMoveTool ? ShouldBeginSceneViewportNavigationDrag( hasInteractiveViewport, @@ -517,38 +466,27 @@ void SceneViewPanel::Render() { ImGuiMouseButton_Middle); const bool beginPanDrag = beginLeftPanDrag || beginMiddlePanDrag; - if (toolOverlay.clicked || beginTransformGizmo || orientationGizmoClick || sceneIconClick || selectClick || beginLookDrag || + if (toolOverlay.clicked || interactionActions.HasClickAction() || beginLookDrag || beginPanDrag) { ImGui::SetWindowFocus(); } - if (beginTransformGizmo) { - if (hoveredGizmoKind == SceneViewportActiveGizmoKind::Scale) { + if (interactionActions.beginTransformGizmo) { + if (interactionActions.hoveredGizmoKind == SceneViewportActiveGizmoKind::Scale) { m_scaleGizmo.TryBeginDrag(gizmoFrameState.scaleContext, m_context->GetUndoManager()); - } else if (hoveredGizmoKind == SceneViewportActiveGizmoKind::Move) { + } else if (interactionActions.hoveredGizmoKind == SceneViewportActiveGizmoKind::Move) { m_moveGizmo.TryBeginDrag(gizmoFrameState.moveContext, m_context->GetUndoManager()); - } else if (hoveredGizmoKind == SceneViewportActiveGizmoKind::Rotate) { + } else if (interactionActions.hoveredGizmoKind == SceneViewportActiveGizmoKind::Rotate) { m_rotateGizmo.TryBeginDrag(gizmoFrameState.rotateContext, m_context->GetUndoManager()); } } - if (orientationGizmoClick) { - viewportHostService->AlignSceneViewToOrientationAxis(orientationAxisHit); - } - - if (sceneIconClick) { - m_context->GetSelectionManager().SetSelectedEntity(clickedSceneIconEntity); - } else if (selectClick) { - const uint64_t selectedEntity = viewportHostService->PickSceneViewEntity( - *m_context, - content.availableSize, - ImVec2(localMousePosition.x, localMousePosition.y)); - if (selectedEntity != 0) { - m_context->GetSelectionManager().SetSelectedEntity(selectedEntity); - } else { - m_context->GetSelectionManager().ClearSelection(); - } - } + DispatchSceneViewportInteractionActions( + interactionActions, + *m_context, + *viewportHostService, + content.availableSize, + localMousePosition); if (gizmoActive) { if (ImGui::IsMouseDown(ImGuiMouseButton_Left)) { diff --git a/tests/editor/CMakeLists.txt b/tests/editor/CMakeLists.txt index 1b058b39..2a177aff 100644 --- a/tests/editor/CMakeLists.txt +++ b/tests/editor/CMakeLists.txt @@ -11,6 +11,7 @@ set(EDITOR_TEST_SOURCES test_scene_viewport_rotate_gizmo.cpp test_scene_viewport_scale_gizmo.cpp test_scene_viewport_picker.cpp + test_scene_viewport_interaction_actions.cpp test_scene_viewport_interaction_resolver.cpp test_scene_viewport_shader_paths.cpp test_scene_viewport_overlay_renderer.cpp @@ -39,6 +40,7 @@ set(EDITOR_TEST_SOURCES ${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportRotateGizmo.cpp ${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportScaleGizmo.cpp ${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportHudOverlay.cpp + ${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportInteractionActions.cpp ${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportInteractionResolver.cpp ${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportOrientationGizmo.cpp ${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportOverlayBuilder.cpp diff --git a/tests/editor/test_scene_viewport_interaction_actions.cpp b/tests/editor/test_scene_viewport_interaction_actions.cpp new file mode 100644 index 00000000..b8a1abdb --- /dev/null +++ b/tests/editor/test_scene_viewport_interaction_actions.cpp @@ -0,0 +1,344 @@ +#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; + +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; } + 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 ImVec2&) override { return {}; } + void UpdateSceneViewInput(IEditorContext&, const XCEngine::Editor::SceneViewportInput&) override {} + uint64_t PickSceneViewEntity(IEditorContext&, const ImVec2&, const ImVec2&) 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, + ImVec2(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, + ImVec2(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, + ImVec2(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, + ImVec2(200.0f, 100.0f), + Vector2(40.0f, 30.0f)); + + EXPECT_EQ(context.selectionManager.selectedEntity, 0u); + EXPECT_GE(context.selectionManager.clearCount, 1); +}