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 0ddbb252..4184eca4 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,28 @@ # SceneViewport Overlay/Gizmo Rework Checkpoint +## Update 2026-04-04 Phase 5E + +### Navigation/Input State Formalization Completed + +- Added `SceneViewportEditorModes.h` to move shared scene viewport editor mode enums out of `SceneViewPanel.h`. +- Added `SceneViewportNavigation.h` as the formal navigation/input helper layer for: + - tool shortcut resolution + - look/pan drag state transitions + - mouse/keyboard capture requests + - `SceneViewportInput` assembly + - interaction-resolution gating +- `SceneViewPanel` now owns one `SceneViewportNavigationState` instead of multiple raw drag booleans and no longer assembles scene viewport input inline. +- Removed the dead `m_loggedLookDelta` / `m_loggedPanDelta` panel state that no longer influenced runtime behavior. +- Added focused editor tests for shortcut mapping, drag transitions, capture flags, input assembly, and interaction gating. + +### Verification + +- `cmake --build build --config Debug --target editor_tests -- /p:BuildProjectReferences=false` +- `build/tests/Editor/Debug/editor_tests.exe --gtest_filter=SceneViewportNavigationTest.*:SceneViewportInteractionActionsTest.*:SceneViewportInteractionResolverTest.*:SceneViewportTransformGizmoCoordinatorTest.*:SceneViewportOverlayRenderer_Test.*:SceneViewportOverlayProviderRegistryTest.*:ViewportRenderFlowUtilsTest.*` +- `cmake --build build --config Debug --target XCEditor` + +All commands completed successfully in `Debug`. + ## Update 2026-04-04 Phase 5D ### Stage-Oriented Gizmo Frame API Completed diff --git a/editor/src/Viewport/SceneViewportEditorModes.h b/editor/src/Viewport/SceneViewportEditorModes.h new file mode 100644 index 00000000..ba61ecce --- /dev/null +++ b/editor/src/Viewport/SceneViewportEditorModes.h @@ -0,0 +1,27 @@ +#pragma once + +#include + +namespace XCEngine { +namespace Editor { + +enum class SceneViewportToolMode : uint8_t { + ViewMove = 0, + Move, + Rotate, + Scale, + Transform +}; + +enum class SceneViewportPivotMode : uint8_t { + Pivot = 0, + Center +}; + +enum class SceneViewportTransformSpaceMode : uint8_t { + Global = 0, + Local +}; + +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/Viewport/SceneViewportNavigation.h b/editor/src/Viewport/SceneViewportNavigation.h new file mode 100644 index 00000000..978b75f8 --- /dev/null +++ b/editor/src/Viewport/SceneViewportNavigation.h @@ -0,0 +1,279 @@ +#pragma once + +#include "IViewportHostService.h" +#include "SceneViewportEditorModes.h" + +#include + +namespace XCEngine { +namespace Editor { + +struct SceneViewportToolShortcutRequest { + bool wantTextInput = false; + bool lookDragging = false; + bool panDragging = false; + bool pressedViewMove = false; + bool pressedMove = false; + bool pressedRotate = false; + bool pressedScale = false; +}; + +struct SceneViewportToolShortcutAction { + SceneViewportToolMode targetTool = SceneViewportToolMode::Move; + bool triggered = false; + bool cancelMoveGizmo = false; + bool cancelRotateGizmo = false; + bool cancelScaleGizmo = false; + + bool HasAction() const { + return triggered; + } +}; + +inline bool CanUseSceneViewportToolShortcut(const SceneViewportToolShortcutRequest& request) { + return !request.wantTextInput && !request.lookDragging && !request.panDragging; +} + +inline SceneViewportToolShortcutAction BuildSceneViewportToolShortcutAction( + const SceneViewportToolShortcutRequest& request) { + SceneViewportToolShortcutAction action = {}; + if (!CanUseSceneViewportToolShortcut(request)) { + return action; + } + + if (request.pressedViewMove) { + action.targetTool = SceneViewportToolMode::ViewMove; + action.triggered = true; + action.cancelMoveGizmo = true; + action.cancelRotateGizmo = true; + action.cancelScaleGizmo = true; + } else if (request.pressedMove) { + action.targetTool = SceneViewportToolMode::Move; + action.triggered = true; + action.cancelRotateGizmo = true; + action.cancelScaleGizmo = true; + } else if (request.pressedRotate) { + action.targetTool = SceneViewportToolMode::Rotate; + action.triggered = true; + action.cancelMoveGizmo = true; + action.cancelScaleGizmo = true; + } else if (request.pressedScale) { + action.targetTool = SceneViewportToolMode::Scale; + action.triggered = true; + action.cancelMoveGizmo = true; + action.cancelRotateGizmo = true; + } + + return action; +} + +struct SceneViewportNavigationState { + bool lookDragging = false; + bool panDragging = false; + int panDragButton = ImGuiMouseButton_Middle; +}; + +struct SceneViewportNavigationRequest { + SceneViewportNavigationState state = {}; + bool hasInteractiveViewport = false; + bool viewportHovered = false; + bool usingViewMoveTool = false; + bool gizmoActive = false; + bool clickedLeft = false; + bool clickedRight = false; + bool clickedMiddle = false; + bool leftMouseDown = false; + bool rightMouseDown = false; + bool middleMouseDown = false; +}; + +struct SceneViewportNavigationUpdate { + SceneViewportNavigationState state = {}; + bool beginLookDrag = false; + bool beginPanDrag = false; + bool beginLeftPanDrag = false; + bool beginMiddlePanDrag = false; +}; + +inline bool ShouldBeginSceneViewportNavigationDrag( + bool hasInteractiveViewport, + bool hovered, + bool activeDrag, + bool otherDrag, + bool gizmoActive, + bool clicked) { + return hasInteractiveViewport && hovered && !activeDrag && !otherDrag && !gizmoActive && clicked; +} + +inline bool IsSceneViewportPanDragButtonDown( + const SceneViewportNavigationRequest& request, + int button) { + switch (button) { + case ImGuiMouseButton_Left: + return request.leftMouseDown; + case ImGuiMouseButton_Right: + return request.rightMouseDown; + case ImGuiMouseButton_Middle: + default: + return request.middleMouseDown; + } +} + +inline SceneViewportNavigationUpdate UpdateSceneViewportNavigationState( + const SceneViewportNavigationRequest& request) { + SceneViewportNavigationUpdate update = {}; + update.state = request.state; + update.beginLeftPanDrag = request.usingViewMoveTool + ? ShouldBeginSceneViewportNavigationDrag( + request.hasInteractiveViewport, + request.viewportHovered, + request.state.panDragging, + request.state.lookDragging, + request.gizmoActive, + request.clickedLeft) + : false; + update.beginLookDrag = ShouldBeginSceneViewportNavigationDrag( + request.hasInteractiveViewport, + request.viewportHovered, + request.state.lookDragging, + request.state.panDragging, + request.gizmoActive, + request.clickedRight); + update.beginMiddlePanDrag = ShouldBeginSceneViewportNavigationDrag( + request.hasInteractiveViewport, + request.viewportHovered, + request.state.panDragging, + request.state.lookDragging, + request.gizmoActive, + request.clickedMiddle); + update.beginPanDrag = update.beginLeftPanDrag || update.beginMiddlePanDrag; + + if (update.beginLookDrag) { + update.state.lookDragging = true; + update.state.panDragging = false; + } + if (update.beginPanDrag) { + update.state.panDragging = true; + update.state.lookDragging = false; + update.state.panDragButton = update.beginLeftPanDrag + ? ImGuiMouseButton_Left + : ImGuiMouseButton_Middle; + } + + if (update.state.lookDragging && !request.rightMouseDown) { + update.state.lookDragging = false; + } + if (update.state.panDragging && + !IsSceneViewportPanDragButtonDown(request, update.state.panDragButton)) { + update.state.panDragging = false; + update.state.panDragButton = ImGuiMouseButton_Middle; + } + + return update; +} + +struct SceneViewportCaptureRequest { + SceneViewportNavigationState state = {}; + bool moveGizmoActive = false; + bool rotateGizmoActive = false; + bool scaleGizmoActive = false; +}; + +struct SceneViewportCaptureFlags { + bool captureMouse = false; + bool captureKeyboard = false; +}; + +inline SceneViewportCaptureFlags BuildSceneViewportCaptureFlags( + const SceneViewportCaptureRequest& request) { + SceneViewportCaptureFlags flags = {}; + flags.captureMouse = + request.state.lookDragging || + request.state.panDragging || + request.moveGizmoActive || + request.rotateGizmoActive || + request.scaleGizmoActive; + flags.captureKeyboard = request.state.lookDragging; + return flags; +} + +inline bool CanResolveSceneViewportInteraction( + bool hasInteractiveViewport, + bool viewportHovered, + bool usingViewMoveTool, + const SceneViewportNavigationState& navigationState, + bool gizmoActive) { + return hasInteractiveViewport && + viewportHovered && + !usingViewMoveTool && + !navigationState.lookDragging && + !navigationState.panDragging && + !gizmoActive; +} + +struct SceneViewportInputBuildRequest { + SceneViewportNavigationState state = {}; + ImVec2 viewportSize = ImVec2(0.0f, 0.0f); + ImVec2 mouseDelta = ImVec2(0.0f, 0.0f); + float mouseWheel = 0.0f; + float deltaTime = 0.0f; + bool viewportHovered = false; + bool viewportFocused = false; + bool wantTextInput = false; + bool fastMove = false; + bool focusSelectionKeyPressed = false; + bool moveForwardKeyDown = false; + bool moveBackwardKeyDown = false; + bool moveRightKeyDown = false; + bool moveLeftKeyDown = false; + bool moveUpKeyDown = false; + bool moveDownKeyDown = false; +}; + +inline SceneViewportInput BuildSceneViewportInput(const SceneViewportInputBuildRequest& request) { + SceneViewportInput input = {}; + input.viewportSize = request.viewportSize; + input.deltaTime = request.deltaTime; + input.hovered = request.viewportHovered; + input.focused = + request.viewportFocused || + request.state.lookDragging || + request.state.panDragging; + input.mouseWheel = + (request.viewportHovered && !request.state.lookDragging) + ? request.mouseWheel + : 0.0f; + input.flySpeedDelta = + (request.viewportHovered && request.state.lookDragging) + ? request.mouseWheel + : 0.0f; + input.looking = request.state.lookDragging; + input.orbiting = false; + input.panning = request.state.panDragging; + input.fastMove = request.fastMove; + input.focusSelectionRequested = + input.focused && + !request.wantTextInput && + request.focusSelectionKeyPressed; + + if (request.state.lookDragging && !request.wantTextInput) { + input.moveForward = + (request.moveForwardKeyDown ? 1.0f : 0.0f) - + (request.moveBackwardKeyDown ? 1.0f : 0.0f); + input.moveRight = + (request.moveRightKeyDown ? 1.0f : 0.0f) - + (request.moveLeftKeyDown ? 1.0f : 0.0f); + input.moveUp = + (request.moveUpKeyDown ? 1.0f : 0.0f) - + (request.moveDownKeyDown ? 1.0f : 0.0f); + } + + if (request.state.lookDragging || request.state.panDragging) { + input.mouseDelta = request.mouseDelta; + } + + return input; +} + +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/panels/SceneViewPanel.cpp b/editor/src/panels/SceneViewPanel.cpp index da175046..4556a4a0 100644 --- a/editor/src/panels/SceneViewPanel.cpp +++ b/editor/src/panels/SceneViewPanel.cpp @@ -8,6 +8,7 @@ #include "Viewport/SceneViewportInteractionActions.h" #include "Viewport/SceneViewportInteractionResolver.h" #include "Viewport/SceneViewportMath.h" +#include "Viewport/SceneViewportNavigation.h" #include "Viewport/SceneViewportTransformGizmoCoordinator.h" #include "ViewportPanelContent.h" #include "Platform/Win32Utf8.h" @@ -243,21 +244,6 @@ SceneViewportToolOverlayResult RenderSceneViewportToolOverlay( return result; } -bool ShouldBeginSceneViewportNavigationDrag( - bool hasInteractiveViewport, - bool hovered, - bool activeDrag, - bool otherDrag, - bool gizmoActive, - ImGuiMouseButton button) { - return hasInteractiveViewport && - hovered && - !activeDrag && - !otherDrag && - !gizmoActive && - ImGui::IsMouseClicked(button); -} - } // namespace SceneViewPanel::SceneViewPanel() : Panel("Scene") {} @@ -279,56 +265,31 @@ void SceneViewPanel::Render() { const bool viewportContentHovered = content.hovered && !toolOverlay.hovered; if (toolOverlay.clicked) { - if (m_moveGizmo.IsActive()) { - m_moveGizmo.CancelDrag(&m_context->GetUndoManager()); - } - if (m_rotateGizmo.IsActive()) { - m_rotateGizmo.CancelDrag(&m_context->GetUndoManager()); - } - if (m_scaleGizmo.IsActive()) { - m_scaleGizmo.CancelDrag(&m_context->GetUndoManager()); - } + CancelSceneViewportTransformGizmoFrame(*m_context, m_moveGizmo, m_rotateGizmo, m_scaleGizmo); m_toolMode = toolOverlay.clickedTool; } - const bool allowToolShortcut = !io.WantTextInput && !m_lookDragging && !m_panDragging; - if (allowToolShortcut) { - if (ImGui::IsKeyPressed(ImGuiKey_Q, false)) { - if (m_moveGizmo.IsActive()) { - m_moveGizmo.CancelDrag(&m_context->GetUndoManager()); - } - if (m_rotateGizmo.IsActive()) { - m_rotateGizmo.CancelDrag(&m_context->GetUndoManager()); - } - if (m_scaleGizmo.IsActive()) { - m_scaleGizmo.CancelDrag(&m_context->GetUndoManager()); - } - m_toolMode = SceneViewportToolMode::ViewMove; - } else if (ImGui::IsKeyPressed(ImGuiKey_W, false)) { - if (m_rotateGizmo.IsActive()) { - m_rotateGizmo.CancelDrag(&m_context->GetUndoManager()); - } - if (m_scaleGizmo.IsActive()) { - m_scaleGizmo.CancelDrag(&m_context->GetUndoManager()); - } - m_toolMode = SceneViewportToolMode::Move; - } else if (ImGui::IsKeyPressed(ImGuiKey_E, false)) { - if (m_moveGizmo.IsActive()) { - m_moveGizmo.CancelDrag(&m_context->GetUndoManager()); - } - if (m_scaleGizmo.IsActive()) { - m_scaleGizmo.CancelDrag(&m_context->GetUndoManager()); - } - m_toolMode = SceneViewportToolMode::Rotate; - } else if (ImGui::IsKeyPressed(ImGuiKey_R, false)) { - if (m_moveGizmo.IsActive()) { - m_moveGizmo.CancelDrag(&m_context->GetUndoManager()); - } - if (m_rotateGizmo.IsActive()) { - m_rotateGizmo.CancelDrag(&m_context->GetUndoManager()); - } - m_toolMode = SceneViewportToolMode::Scale; + SceneViewportToolShortcutRequest toolShortcutRequest = {}; + toolShortcutRequest.wantTextInput = io.WantTextInput; + toolShortcutRequest.lookDragging = m_navigationState.lookDragging; + toolShortcutRequest.panDragging = m_navigationState.panDragging; + toolShortcutRequest.pressedViewMove = ImGui::IsKeyPressed(ImGuiKey_Q, false); + toolShortcutRequest.pressedMove = ImGui::IsKeyPressed(ImGuiKey_W, false); + toolShortcutRequest.pressedRotate = ImGui::IsKeyPressed(ImGuiKey_E, false); + toolShortcutRequest.pressedScale = ImGui::IsKeyPressed(ImGuiKey_R, false); + const SceneViewportToolShortcutAction toolShortcutAction = + BuildSceneViewportToolShortcutAction(toolShortcutRequest); + if (toolShortcutAction.HasAction()) { + if (toolShortcutAction.cancelMoveGizmo && m_moveGizmo.IsActive()) { + m_moveGizmo.CancelDrag(&m_context->GetUndoManager()); } + if (toolShortcutAction.cancelRotateGizmo && m_rotateGizmo.IsActive()) { + m_rotateGizmo.CancelDrag(&m_context->GetUndoManager()); + } + if (toolShortcutAction.cancelScaleGizmo && m_scaleGizmo.IsActive()) { + m_scaleGizmo.CancelDrag(&m_context->GetUndoManager()); + } + m_toolMode = toolShortcutAction.targetTool; } const bool usingViewMoveTool = m_toolMode == SceneViewportToolMode::ViewMove; @@ -390,14 +351,12 @@ void SceneViewPanel::Render() { const SceneViewportHudOverlayData interactionHudOverlay = BuildSceneViewportHudOverlayData(overlay); SceneViewportInteractionResult hoveredInteraction = {}; - const bool canResolveViewportInteraction = - hasInteractiveViewport && - viewportContentHovered && - !usingViewMoveTool && - !m_lookDragging && - !m_panDragging && - !toolOverlay.hovered && - !gizmoActive; + const bool canResolveViewportInteraction = CanResolveSceneViewportInteraction( + hasInteractiveViewport, + viewportContentHovered, + usingViewMoveTool, + m_navigationState, + gizmoActive); if (canResolveViewportInteraction) { SceneViewportInteractionResolveRequest interactionRequest = {}; interactionRequest.overlayFrameData = &interactionOverlayFrameData; @@ -426,33 +385,24 @@ void SceneViewPanel::Render() { hasInteractiveViewport, content.clickedLeft, canResolveViewportInteraction); - const bool beginLeftPanDrag = usingViewMoveTool - ? ShouldBeginSceneViewportNavigationDrag( - hasInteractiveViewport, - viewportContentHovered, - m_panDragging, - m_lookDragging, - gizmoActive, - ImGuiMouseButton_Left) - : false; - const bool beginLookDrag = ShouldBeginSceneViewportNavigationDrag( - hasInteractiveViewport, - viewportContentHovered, - m_lookDragging, - m_panDragging, - gizmoActive, - ImGuiMouseButton_Right); - const bool beginMiddlePanDrag = ShouldBeginSceneViewportNavigationDrag( - hasInteractiveViewport, - viewportContentHovered, - m_panDragging, - m_lookDragging, - gizmoActive, - ImGuiMouseButton_Middle); - const bool beginPanDrag = beginLeftPanDrag || beginMiddlePanDrag; + SceneViewportNavigationRequest navigationRequest = {}; + navigationRequest.state = m_navigationState; + navigationRequest.hasInteractiveViewport = hasInteractiveViewport; + navigationRequest.viewportHovered = viewportContentHovered; + navigationRequest.usingViewMoveTool = usingViewMoveTool; + navigationRequest.gizmoActive = gizmoActive; + navigationRequest.clickedLeft = content.clickedLeft; + navigationRequest.clickedRight = content.clickedRight; + navigationRequest.clickedMiddle = content.clickedMiddle; + navigationRequest.leftMouseDown = ImGui::IsMouseDown(ImGuiMouseButton_Left); + navigationRequest.rightMouseDown = ImGui::IsMouseDown(ImGuiMouseButton_Right); + navigationRequest.middleMouseDown = ImGui::IsMouseDown(ImGuiMouseButton_Middle); + const SceneViewportNavigationUpdate navigationUpdate = + UpdateSceneViewportNavigationState(navigationRequest); + m_navigationState = navigationUpdate.state; - if (toolOverlay.clicked || interactionActions.HasClickAction() || beginLookDrag || - beginPanDrag) { + if (toolOverlay.clicked || interactionActions.HasClickAction() || navigationUpdate.beginLookDrag || + navigationUpdate.beginPanDrag) { ImGui::SetWindowFocus(); } @@ -481,81 +431,37 @@ void SceneViewPanel::Render() { m_rotateGizmo, m_scaleGizmo); - if (beginLookDrag) { - m_lookDragging = true; - m_panDragging = false; - m_loggedLookDelta = false; - m_loggedPanDelta = false; - } - if (beginPanDrag) { - m_panDragging = true; - m_lookDragging = false; - m_panDragButton = beginLeftPanDrag ? ImGuiMouseButton_Left : ImGuiMouseButton_Middle; - m_loggedPanDelta = false; - m_loggedLookDelta = false; - } - - if (m_lookDragging && !ImGui::IsMouseDown(ImGuiMouseButton_Right)) { - m_lookDragging = false; - m_loggedLookDelta = false; - } - if (m_panDragging && !ImGui::IsMouseDown(m_panDragButton)) { - m_panDragging = false; - m_panDragButton = ImGuiMouseButton_Middle; - m_loggedPanDelta = false; - } - - if (m_lookDragging || m_panDragging || m_moveGizmo.IsActive() || m_rotateGizmo.IsActive() || - m_scaleGizmo.IsActive()) { + const SceneViewportCaptureFlags captureFlags = BuildSceneViewportCaptureFlags( + SceneViewportCaptureRequest{ + m_navigationState, + m_moveGizmo.IsActive(), + m_rotateGizmo.IsActive(), + m_scaleGizmo.IsActive()}); + if (captureFlags.captureMouse) { ImGui::SetNextFrameWantCaptureMouse(true); } - if (m_lookDragging) { + if (captureFlags.captureKeyboard) { ImGui::SetNextFrameWantCaptureKeyboard(true); } - SceneViewportInput input = {}; - input.viewportSize = content.availableSize; - input.deltaTime = io.DeltaTime; - input.hovered = viewportContentHovered; - input.focused = content.focused || m_lookDragging || m_panDragging; - input.mouseWheel = (viewportContentHovered && !m_lookDragging) ? io.MouseWheel : 0.0f; - input.flySpeedDelta = (viewportContentHovered && m_lookDragging) ? io.MouseWheel : 0.0f; - input.looking = m_lookDragging; - input.orbiting = false; - input.panning = m_panDragging; - input.fastMove = io.KeyShift; - input.focusSelectionRequested = - input.focused && !io.WantTextInput && ImGui::IsKeyPressed(ImGuiKey_F, false); - - if (m_lookDragging && !io.WantTextInput) { - input.moveForward = - (ImGui::IsKeyDown(ImGuiKey_W) ? 1.0f : 0.0f) - - (ImGui::IsKeyDown(ImGuiKey_S) ? 1.0f : 0.0f); - input.moveRight = - (ImGui::IsKeyDown(ImGuiKey_D) ? 1.0f : 0.0f) - - (ImGui::IsKeyDown(ImGuiKey_A) ? 1.0f : 0.0f); - input.moveUp = - (ImGui::IsKeyDown(ImGuiKey_E) ? 1.0f : 0.0f) - - (ImGui::IsKeyDown(ImGuiKey_Q) ? 1.0f : 0.0f); - } - - if (m_lookDragging || m_panDragging) { - if (m_lookDragging) { - input.mouseDelta = io.MouseDelta; - if (!m_loggedLookDelta && - (input.mouseDelta.x != 0.0f || input.mouseDelta.y != 0.0f)) { - m_loggedLookDelta = true; - } - } - - if (m_panDragging) { - input.mouseDelta = io.MouseDelta; - if (!m_loggedPanDelta && - (input.mouseDelta.x != 0.0f || input.mouseDelta.y != 0.0f)) { - m_loggedPanDelta = true; - } - } - } + SceneViewportInputBuildRequest inputRequest = {}; + inputRequest.state = m_navigationState; + inputRequest.viewportSize = content.availableSize; + inputRequest.mouseDelta = io.MouseDelta; + inputRequest.mouseWheel = io.MouseWheel; + inputRequest.deltaTime = io.DeltaTime; + inputRequest.viewportHovered = viewportContentHovered; + inputRequest.viewportFocused = content.focused; + inputRequest.wantTextInput = io.WantTextInput; + inputRequest.fastMove = io.KeyShift; + inputRequest.focusSelectionKeyPressed = ImGui::IsKeyPressed(ImGuiKey_F, false); + inputRequest.moveForwardKeyDown = ImGui::IsKeyDown(ImGuiKey_W); + inputRequest.moveBackwardKeyDown = ImGui::IsKeyDown(ImGuiKey_S); + inputRequest.moveRightKeyDown = ImGui::IsKeyDown(ImGuiKey_D); + inputRequest.moveLeftKeyDown = ImGui::IsKeyDown(ImGuiKey_A); + inputRequest.moveUpKeyDown = ImGui::IsKeyDown(ImGuiKey_E); + inputRequest.moveDownKeyDown = ImGui::IsKeyDown(ImGuiKey_Q); + const SceneViewportInput input = BuildSceneViewportInput(inputRequest); viewportHostService->UpdateSceneViewInput(*m_context, input); diff --git a/editor/src/panels/SceneViewPanel.h b/editor/src/panels/SceneViewPanel.h index 8170aa98..e2f95b94 100644 --- a/editor/src/panels/SceneViewPanel.h +++ b/editor/src/panels/SceneViewPanel.h @@ -1,7 +1,9 @@ #pragma once #include "Panel.h" +#include "Viewport/SceneViewportEditorModes.h" #include "Viewport/SceneViewportMoveGizmo.h" +#include "Viewport/SceneViewportNavigation.h" #include "Viewport/SceneViewportRotateGizmo.h" #include "Viewport/SceneViewportScaleGizmo.h" @@ -10,24 +12,6 @@ namespace XCEngine { namespace Editor { -enum class SceneViewportToolMode : uint8_t { - ViewMove = 0, - Move, - Rotate, - Scale, - Transform -}; - -enum class SceneViewportPivotMode : uint8_t { - Pivot = 0, - Center -}; - -enum class SceneViewportTransformSpaceMode : uint8_t { - Global = 0, - Local -}; - class SceneViewPanel : public Panel { public: SceneViewPanel(); @@ -37,11 +21,7 @@ private: SceneViewportToolMode m_toolMode = SceneViewportToolMode::Move; SceneViewportPivotMode m_pivotMode = SceneViewportPivotMode::Pivot; SceneViewportTransformSpaceMode m_transformSpaceMode = SceneViewportTransformSpaceMode::Global; - bool m_lookDragging = false; - bool m_panDragging = false; - int m_panDragButton = ImGuiMouseButton_Middle; - bool m_loggedLookDelta = false; - bool m_loggedPanDelta = false; + SceneViewportNavigationState m_navigationState = {}; SceneViewportMoveGizmo m_moveGizmo; SceneViewportRotateGizmo m_rotateGizmo; SceneViewportScaleGizmo m_scaleGizmo; diff --git a/tests/editor/CMakeLists.txt b/tests/editor/CMakeLists.txt index 44f4a4ba..cc123b63 100644 --- a/tests/editor/CMakeLists.txt +++ b/tests/editor/CMakeLists.txt @@ -7,6 +7,7 @@ set(EDITOR_TEST_SOURCES test_application_asset_cache_stub.cpp test_play_session_controller.cpp test_scene_viewport_camera_controller.cpp + test_scene_viewport_navigation.cpp test_scene_viewport_move_gizmo.cpp test_scene_viewport_rotate_gizmo.cpp test_scene_viewport_scale_gizmo.cpp diff --git a/tests/editor/test_scene_viewport_navigation.cpp b/tests/editor/test_scene_viewport_navigation.cpp new file mode 100644 index 00000000..8aaddf50 --- /dev/null +++ b/tests/editor/test_scene_viewport_navigation.cpp @@ -0,0 +1,154 @@ +#include + +#include "Viewport/SceneViewportNavigation.h" + +using XCEngine::Editor::BuildSceneViewportCaptureFlags; +using XCEngine::Editor::BuildSceneViewportInput; +using XCEngine::Editor::BuildSceneViewportToolShortcutAction; +using XCEngine::Editor::CanResolveSceneViewportInteraction; +using XCEngine::Editor::SceneViewportCaptureRequest; +using XCEngine::Editor::SceneViewportInputBuildRequest; +using XCEngine::Editor::SceneViewportNavigationRequest; +using XCEngine::Editor::SceneViewportNavigationState; +using XCEngine::Editor::SceneViewportToolMode; +using XCEngine::Editor::SceneViewportToolShortcutRequest; +using XCEngine::Editor::UpdateSceneViewportNavigationState; + +TEST(SceneViewportNavigationTest, ToolShortcutActionIgnoresShortcutsDuringTextInputOrDrag) { + SceneViewportToolShortcutRequest request = {}; + request.pressedMove = true; + request.wantTextInput = true; + EXPECT_FALSE(BuildSceneViewportToolShortcutAction(request).HasAction()); + + request.wantTextInput = false; + request.lookDragging = true; + EXPECT_FALSE(BuildSceneViewportToolShortcutAction(request).HasAction()); + + request.lookDragging = false; + request.panDragging = true; + EXPECT_FALSE(BuildSceneViewportToolShortcutAction(request).HasAction()); +} + +TEST(SceneViewportNavigationTest, ToolShortcutActionMapsShortcutTargetsAndCancelFlags) { + SceneViewportToolShortcutRequest request = {}; + request.pressedViewMove = true; + const auto viewMoveAction = BuildSceneViewportToolShortcutAction(request); + EXPECT_TRUE(viewMoveAction.HasAction()); + EXPECT_EQ(viewMoveAction.targetTool, SceneViewportToolMode::ViewMove); + EXPECT_TRUE(viewMoveAction.cancelMoveGizmo); + EXPECT_TRUE(viewMoveAction.cancelRotateGizmo); + EXPECT_TRUE(viewMoveAction.cancelScaleGizmo); + + request = {}; + request.pressedRotate = true; + const auto rotateAction = BuildSceneViewportToolShortcutAction(request); + EXPECT_TRUE(rotateAction.HasAction()); + EXPECT_EQ(rotateAction.targetTool, SceneViewportToolMode::Rotate); + EXPECT_TRUE(rotateAction.cancelMoveGizmo); + EXPECT_FALSE(rotateAction.cancelRotateGizmo); + EXPECT_TRUE(rotateAction.cancelScaleGizmo); +} + +TEST(SceneViewportNavigationTest, NavigationUpdateBeginsLookAndPanDrags) { + SceneViewportNavigationRequest lookRequest = {}; + lookRequest.hasInteractiveViewport = true; + lookRequest.viewportHovered = true; + lookRequest.clickedRight = true; + lookRequest.rightMouseDown = true; + const auto lookUpdate = UpdateSceneViewportNavigationState(lookRequest); + EXPECT_TRUE(lookUpdate.beginLookDrag); + EXPECT_FALSE(lookUpdate.beginPanDrag); + EXPECT_TRUE(lookUpdate.state.lookDragging); + EXPECT_FALSE(lookUpdate.state.panDragging); + + SceneViewportNavigationRequest panRequest = {}; + panRequest.hasInteractiveViewport = true; + panRequest.viewportHovered = true; + panRequest.usingViewMoveTool = true; + panRequest.clickedLeft = true; + panRequest.leftMouseDown = true; + const auto panUpdate = UpdateSceneViewportNavigationState(panRequest); + EXPECT_FALSE(panUpdate.beginLookDrag); + EXPECT_TRUE(panUpdate.beginPanDrag); + EXPECT_TRUE(panUpdate.beginLeftPanDrag); + EXPECT_TRUE(panUpdate.state.panDragging); + EXPECT_EQ(panUpdate.state.panDragButton, ImGuiMouseButton_Left); +} + +TEST(SceneViewportNavigationTest, NavigationUpdateEndsDragsWhenButtonsRelease) { + SceneViewportNavigationRequest lookRequest = {}; + lookRequest.state.lookDragging = true; + const auto lookUpdate = UpdateSceneViewportNavigationState(lookRequest); + EXPECT_FALSE(lookUpdate.state.lookDragging); + + SceneViewportNavigationRequest panRequest = {}; + panRequest.state.panDragging = true; + panRequest.state.panDragButton = ImGuiMouseButton_Left; + const auto panUpdate = UpdateSceneViewportNavigationState(panRequest); + EXPECT_FALSE(panUpdate.state.panDragging); + EXPECT_EQ(panUpdate.state.panDragButton, ImGuiMouseButton_Middle); +} + +TEST(SceneViewportNavigationTest, CaptureFlagsTrackNavigationAndActiveGizmos) { + SceneViewportCaptureRequest request = {}; + request.state.lookDragging = true; + const auto lookFlags = BuildSceneViewportCaptureFlags(request); + EXPECT_TRUE(lookFlags.captureMouse); + EXPECT_TRUE(lookFlags.captureKeyboard); + + request = {}; + request.rotateGizmoActive = true; + const auto gizmoFlags = BuildSceneViewportCaptureFlags(request); + EXPECT_TRUE(gizmoFlags.captureMouse); + EXPECT_FALSE(gizmoFlags.captureKeyboard); +} + +TEST(SceneViewportNavigationTest, BuildInputRoutesWheelFocusMovementAndMouseDelta) { + SceneViewportInputBuildRequest request = {}; + request.viewportSize = ImVec2(640.0f, 360.0f); + request.viewportHovered = true; + request.viewportFocused = true; + request.mouseWheel = 2.0f; + const auto zoomInput = BuildSceneViewportInput(request); + EXPECT_FLOAT_EQ(zoomInput.mouseWheel, 2.0f); + EXPECT_FLOAT_EQ(zoomInput.flySpeedDelta, 0.0f); + EXPECT_FALSE(zoomInput.looking); + EXPECT_TRUE(zoomInput.focused); + + request = {}; + request.state.lookDragging = true; + request.viewportSize = ImVec2(640.0f, 360.0f); + request.viewportHovered = true; + request.mouseWheel = 1.5f; + request.mouseDelta = ImVec2(5.0f, -3.0f); + request.fastMove = true; + request.focusSelectionKeyPressed = true; + request.moveForwardKeyDown = true; + request.moveRightKeyDown = true; + request.moveUpKeyDown = true; + const auto flyInput = BuildSceneViewportInput(request); + EXPECT_FLOAT_EQ(flyInput.mouseWheel, 0.0f); + EXPECT_FLOAT_EQ(flyInput.flySpeedDelta, 1.5f); + EXPECT_TRUE(flyInput.looking); + EXPECT_TRUE(flyInput.fastMove); + EXPECT_TRUE(flyInput.focused); + EXPECT_TRUE(flyInput.focusSelectionRequested); + EXPECT_FLOAT_EQ(flyInput.moveForward, 1.0f); + EXPECT_FLOAT_EQ(flyInput.moveRight, 1.0f); + EXPECT_FLOAT_EQ(flyInput.moveUp, 1.0f); + EXPECT_FLOAT_EQ(flyInput.mouseDelta.x, 5.0f); + EXPECT_FLOAT_EQ(flyInput.mouseDelta.y, -3.0f); +} + +TEST(SceneViewportNavigationTest, CanResolveInteractionRequiresHoverWithoutNavigationOrActiveGizmo) { + SceneViewportNavigationState state = {}; + EXPECT_TRUE(CanResolveSceneViewportInteraction(true, true, false, state, false)); + + state.lookDragging = true; + EXPECT_FALSE(CanResolveSceneViewportInteraction(true, true, false, state, false)); + + state = {}; + EXPECT_FALSE(CanResolveSceneViewportInteraction(true, true, true, state, false)); + EXPECT_FALSE(CanResolveSceneViewportInteraction(true, true, false, state, true)); + EXPECT_FALSE(CanResolveSceneViewportInteraction(false, true, false, state, false)); +}