From e636abb76dcbad8981b8097054a3857668d6405b Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Sat, 4 Apr 2026 01:42:35 +0800 Subject: [PATCH] Formalize scene viewport interaction frame helpers --- ...rlay_Gizmo_Rework_Checkpoint_2026-04-02.md | 21 ++ .../Viewport/SceneViewportInteractionFrame.h | 132 +++++++ editor/src/panels/SceneViewPanel.cpp | 119 +++---- tests/editor/CMakeLists.txt | 1 + .../test_scene_viewport_interaction_frame.cpp | 337 ++++++++++++++++++ 5 files changed, 531 insertions(+), 79 deletions(-) create mode 100644 editor/src/Viewport/SceneViewportInteractionFrame.h create mode 100644 tests/editor/test_scene_viewport_interaction_frame.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 4184eca4..839389cc 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,26 @@ # SceneViewport Overlay/Gizmo Rework Checkpoint +## Update 2026-04-04 Phase 5F + +### Interaction Frame/Request Glue Formalization Completed + +- Added `SceneViewportInteractionFrame.h` to formalize: + - scene viewport tool visibility/state derivation + - frame geometry derivation from viewport rect and mouse position + - per-frame interaction/gizmo context assembly + - interaction resolve request construction +- `SceneViewPanel` no longer assembles tool visibility booleans, local mouse coordinates, overlay frame references, or interaction resolve requests inline. +- The panel now consumes `SceneViewportToolState`, `SceneViewportFrameGeometry`, and `SceneViewportInteractionFrameState` instead of stitching together those frame-level inputs itself. +- Added focused editor tests covering tool-state mapping, frame geometry, interaction frame state, and resolve request assembly. + +### Verification + +- `cmake --build build --config Debug --target editor_tests -- /p:BuildProjectReferences=false` +- `build/tests/Editor/Debug/editor_tests.exe --gtest_filter=SceneViewportNavigationTest.*:SceneViewportInteractionFrameTest.*: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 5E ### Navigation/Input State Formalization Completed diff --git a/editor/src/Viewport/SceneViewportInteractionFrame.h b/editor/src/Viewport/SceneViewportInteractionFrame.h new file mode 100644 index 00000000..097d8231 --- /dev/null +++ b/editor/src/Viewport/SceneViewportInteractionFrame.h @@ -0,0 +1,132 @@ +#pragma once + +#include "Core/IEditorContext.h" +#include "IViewportHostService.h" +#include "SceneViewportEditorModes.h" +#include "SceneViewportHudOverlay.h" +#include "SceneViewportInteractionResolver.h" +#include "SceneViewportTransformGizmoCoordinator.h" + +namespace XCEngine { +namespace Editor { + +struct SceneViewportToolState { + bool usingViewMoveTool = false; + bool usingTransformTool = false; + bool showingMoveGizmo = false; + bool showingRotateGizmo = false; + bool showingScaleGizmo = false; + bool useCenterPivot = false; + bool localSpace = false; + SceneViewportTransformGizmoFrameOptions gizmoFrameOptions = {}; +}; + +inline SceneViewportToolState BuildSceneViewportToolState( + SceneViewportToolMode toolMode, + SceneViewportPivotMode pivotMode, + SceneViewportTransformSpaceMode transformSpaceMode) { + SceneViewportToolState state = {}; + state.usingViewMoveTool = toolMode == SceneViewportToolMode::ViewMove; + state.usingTransformTool = toolMode == SceneViewportToolMode::Transform; + state.showingMoveGizmo = toolMode == SceneViewportToolMode::Move || state.usingTransformTool; + state.showingRotateGizmo = toolMode == SceneViewportToolMode::Rotate || state.usingTransformTool; + state.showingScaleGizmo = toolMode == SceneViewportToolMode::Scale || state.usingTransformTool; + state.useCenterPivot = pivotMode == SceneViewportPivotMode::Center; + state.localSpace = transformSpaceMode == SceneViewportTransformSpaceMode::Local; + state.gizmoFrameOptions = BuildSceneViewportTransformGizmoFrameOptions( + state.useCenterPivot, + state.localSpace, + state.usingTransformTool, + state.showingMoveGizmo, + state.showingRotateGizmo, + state.showingScaleGizmo); + return state; +} + +struct SceneViewportFrameGeometry { + Math::Vector2 viewportSize = Math::Vector2::Zero(); + Math::Vector2 localMousePosition = Math::Vector2::Zero(); +}; + +inline SceneViewportFrameGeometry BuildSceneViewportFrameGeometry( + const ImVec2& viewportSize, + const ImVec2& viewportMin, + const ImVec2& absoluteMousePosition) { + SceneViewportFrameGeometry geometry = {}; + geometry.viewportSize = Math::Vector2(viewportSize.x, viewportSize.y); + geometry.localMousePosition = Math::Vector2( + absoluteMousePosition.x - viewportMin.x, + absoluteMousePosition.y - viewportMin.y); + return geometry; +} + +struct SceneViewportInteractionFrameState { + bool hasInteractiveViewport = false; + SceneViewportOverlayData overlay = {}; + SceneViewportTransformGizmoFrameUpdate gizmoFrameUpdate = {}; + SceneViewportTransformGizmoFrameState gizmoFrameState = {}; + const SceneViewportOverlayFrameData* overlayFrameData = nullptr; + SceneViewportActiveGizmoKind activeGizmoKind = SceneViewportActiveGizmoKind::None; + bool gizmoActive = false; + SceneViewportHudOverlayData hudOverlay = {}; +}; + +inline SceneViewportInteractionFrameState BuildSceneViewportInteractionFrameState( + IEditorContext& context, + IViewportHostService& viewportHostService, + bool hasInteractiveViewport, + const SceneViewportFrameGeometry& geometry, + const SceneViewportTransformGizmoFrameOptions& gizmoFrameOptions, + SceneViewportMoveGizmo& moveGizmo, + SceneViewportRotateGizmo& rotateGizmo, + SceneViewportScaleGizmo& scaleGizmo, + const SceneViewportOverlayFrameData& emptyOverlayFrameData) { + SceneViewportInteractionFrameState state = {}; + state.hasInteractiveViewport = hasInteractiveViewport; + state.overlayFrameData = &emptyOverlayFrameData; + + if (!hasInteractiveViewport) { + CancelSceneViewportTransformGizmoFrame(context, moveGizmo, rotateGizmo, scaleGizmo); + state.hudOverlay = BuildSceneViewportHudOverlayData(state.overlay); + return state; + } + + state.overlay = viewportHostService.GetSceneViewOverlayData(); + state.gizmoFrameUpdate = RefreshAndSubmitSceneViewportTransformGizmoFrame( + viewportHostService, + BuildSceneViewportTransformGizmoRefreshRequest( + context, + state.overlay, + geometry.viewportSize, + geometry.localMousePosition, + gizmoFrameOptions), + moveGizmo, + rotateGizmo, + scaleGizmo); + state.gizmoFrameState = state.gizmoFrameUpdate.frameState; + state.overlayFrameData = &viewportHostService.GetSceneViewEditorOverlayFrameData(context); + state.activeGizmoKind = state.gizmoFrameUpdate.overlaySubmission.activeGizmoKind; + state.gizmoActive = state.gizmoFrameUpdate.overlaySubmission.GizmoActive(); + state.hudOverlay = BuildSceneViewportHudOverlayData(state.overlay); + return state; +} + +inline SceneViewportInteractionResolveRequest BuildSceneViewportInteractionResolveRequest( + const SceneViewportInteractionFrameState& frameState, + const SceneViewportFrameGeometry& geometry, + const ImVec2& viewportMin, + const ImVec2& viewportMax, + const ImVec2& absoluteMousePosition) { + SceneViewportInteractionResolveRequest request = {}; + request.overlayFrameData = frameState.overlayFrameData; + request.viewportSize = geometry.viewportSize; + request.localMousePosition = geometry.localMousePosition; + request.hudOverlay = &frameState.hudOverlay; + request.viewportMin = viewportMin; + request.viewportMax = viewportMax; + request.absoluteMousePosition = absoluteMousePosition; + return request; +} + +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/panels/SceneViewPanel.cpp b/editor/src/panels/SceneViewPanel.cpp index 4556a4a0..c0a8f06b 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/SceneViewportInteractionFrame.h" #include "Viewport/SceneViewportInteractionActions.h" #include "Viewport/SceneViewportInteractionResolver.h" #include "Viewport/SceneViewportMath.h" @@ -292,91 +293,51 @@ void SceneViewPanel::Render() { m_toolMode = toolShortcutAction.targetTool; } - const bool usingViewMoveTool = m_toolMode == SceneViewportToolMode::ViewMove; - const bool usingTransformTool = m_toolMode == SceneViewportToolMode::Transform; - const bool showingMoveGizmo = m_toolMode == SceneViewportToolMode::Move || usingTransformTool; - const bool showingRotateGizmo = m_toolMode == SceneViewportToolMode::Rotate || usingTransformTool; - const bool showingScaleGizmo = m_toolMode == SceneViewportToolMode::Scale || usingTransformTool; - const bool useCenterPivot = m_pivotMode == SceneViewportPivotMode::Center; - const bool localSpace = m_transformSpaceMode == SceneViewportTransformSpaceMode::Local; - const SceneViewportTransformGizmoFrameOptions gizmoFrameOptions = - BuildSceneViewportTransformGizmoFrameOptions( - useCenterPivot, - localSpace, - usingTransformTool, - showingMoveGizmo, - showingRotateGizmo, - showingScaleGizmo); - const Math::Vector2 viewportSize(content.availableSize.x, content.availableSize.y); - const Math::Vector2 localMousePosition( - io.MousePos.x - content.itemMin.x, - io.MousePos.y - content.itemMin.y); - SceneViewportOverlayData overlay = {}; - SceneViewportTransformGizmoFrameUpdate interactionGizmoFrame = {}; - SceneViewportTransformGizmoFrameState gizmoFrameState = {}; + const SceneViewportToolState toolState = BuildSceneViewportToolState( + m_toolMode, + m_pivotMode, + m_transformSpaceMode); + const SceneViewportFrameGeometry frameGeometry = BuildSceneViewportFrameGeometry( + content.availableSize, + content.itemMin, + io.MousePos); SceneViewportOverlayFrameData emptySceneOverlayFrameData = {}; - - if (hasInteractiveViewport) { - overlay = viewportHostService->GetSceneViewOverlayData(); - interactionGizmoFrame = RefreshAndSubmitSceneViewportTransformGizmoFrame( - *viewportHostService, - BuildSceneViewportTransformGizmoRefreshRequest( - *m_context, - overlay, - viewportSize, - localMousePosition, - gizmoFrameOptions), - m_moveGizmo, - m_rotateGizmo, - m_scaleGizmo); - gizmoFrameState = interactionGizmoFrame.frameState; - } else { - CancelSceneViewportTransformGizmoFrame( + const SceneViewportInteractionFrameState interactionFrameState = + BuildSceneViewportInteractionFrameState( *m_context, + *viewportHostService, + hasInteractiveViewport, + frameGeometry, + toolState.gizmoFrameOptions, m_moveGizmo, m_rotateGizmo, - m_scaleGizmo); - } - - const SceneViewportTransformGizmoOverlaySubmission interactionGizmoSubmission = - hasInteractiveViewport - ? interactionGizmoFrame.overlaySubmission - : SceneViewportTransformGizmoOverlaySubmission{}; - const SceneViewportOverlayFrameData& interactionOverlayFrameData = - hasInteractiveViewport - ? viewportHostService->GetSceneViewEditorOverlayFrameData(*m_context) - : emptySceneOverlayFrameData; - const SceneViewportActiveGizmoKind activeGizmoKind = interactionGizmoSubmission.activeGizmoKind; - const bool gizmoActive = interactionGizmoSubmission.GizmoActive(); - const SceneViewportHudOverlayData interactionHudOverlay = - BuildSceneViewportHudOverlayData(overlay); + m_scaleGizmo, + emptySceneOverlayFrameData); SceneViewportInteractionResult hoveredInteraction = {}; const bool canResolveViewportInteraction = CanResolveSceneViewportInteraction( hasInteractiveViewport, viewportContentHovered, - usingViewMoveTool, + toolState.usingViewMoveTool, m_navigationState, - gizmoActive); + interactionFrameState.gizmoActive); if (canResolveViewportInteraction) { - SceneViewportInteractionResolveRequest interactionRequest = {}; - interactionRequest.overlayFrameData = &interactionOverlayFrameData; - interactionRequest.viewportSize = viewportSize; - interactionRequest.localMousePosition = localMousePosition; - interactionRequest.hudOverlay = &interactionHudOverlay; - interactionRequest.viewportMin = content.itemMin; - interactionRequest.viewportMax = content.itemMax; - interactionRequest.absoluteMousePosition = io.MousePos; - hoveredInteraction = ResolveSceneViewportInteraction(interactionRequest); + hoveredInteraction = ResolveSceneViewportInteraction( + BuildSceneViewportInteractionResolveRequest( + interactionFrameState, + frameGeometry, + content.itemMin, + content.itemMax, + io.MousePos)); } ApplySceneViewportHoveredHandleState( BuildSceneViewportHoveredHandleState(hoveredInteraction), - gizmoActive, - showingMoveGizmo, + interactionFrameState.gizmoActive, + toolState.showingMoveGizmo, m_moveGizmo, - showingRotateGizmo, + toolState.showingRotateGizmo, m_rotateGizmo, - showingScaleGizmo, + toolState.showingScaleGizmo, m_scaleGizmo); const SceneViewportInteractionActions interactionActions = @@ -389,8 +350,8 @@ void SceneViewPanel::Render() { navigationRequest.state = m_navigationState; navigationRequest.hasInteractiveViewport = hasInteractiveViewport; navigationRequest.viewportHovered = viewportContentHovered; - navigationRequest.usingViewMoveTool = usingViewMoveTool; - navigationRequest.gizmoActive = gizmoActive; + navigationRequest.usingViewMoveTool = toolState.usingViewMoveTool; + navigationRequest.gizmoActive = interactionFrameState.gizmoActive; navigationRequest.clickedLeft = content.clickedLeft; navigationRequest.clickedRight = content.clickedRight; navigationRequest.clickedMiddle = content.clickedMiddle; @@ -409,7 +370,7 @@ void SceneViewPanel::Render() { ExecuteSceneViewportTransformGizmoLifecycleCommand( BuildBeginSceneViewportTransformGizmoLifecycleCommand(interactionActions), m_context->GetUndoManager(), - gizmoFrameState, + interactionFrameState.gizmoFrameState, m_moveGizmo, m_rotateGizmo, m_scaleGizmo); @@ -419,14 +380,14 @@ void SceneViewPanel::Render() { *m_context, *viewportHostService, content.availableSize, - localMousePosition); + frameGeometry.localMousePosition); ExecuteSceneViewportTransformGizmoLifecycleCommand( BuildFrameSceneViewportTransformGizmoLifecycleCommand( - activeGizmoKind, + interactionFrameState.activeGizmoKind, ImGui::IsMouseDown(ImGuiMouseButton_Left)), m_context->GetUndoManager(), - gizmoFrameState, + interactionFrameState.gizmoFrameState, m_moveGizmo, m_rotateGizmo, m_scaleGizmo); @@ -466,15 +427,15 @@ void SceneViewPanel::Render() { viewportHostService->UpdateSceneViewInput(*m_context, input); if (content.hasViewportArea && content.frame.hasTexture) { - overlay = viewportHostService->GetSceneViewOverlayData(); + const SceneViewportOverlayData overlay = viewportHostService->GetSceneViewOverlayData(); RefreshAndSubmitSceneViewportTransformGizmoFrame( *viewportHostService, BuildSceneViewportTransformGizmoRefreshRequest( *m_context, overlay, - viewportSize, - localMousePosition, - gizmoFrameOptions), + frameGeometry.viewportSize, + frameGeometry.localMousePosition, + toolState.gizmoFrameOptions), m_moveGizmo, m_rotateGizmo, m_scaleGizmo); diff --git a/tests/editor/CMakeLists.txt b/tests/editor/CMakeLists.txt index cc123b63..0dfd498c 100644 --- a/tests/editor/CMakeLists.txt +++ b/tests/editor/CMakeLists.txt @@ -14,6 +14,7 @@ set(EDITOR_TEST_SOURCES test_scene_viewport_picker.cpp test_scene_viewport_interaction_actions.cpp test_scene_viewport_interaction_resolver.cpp + test_scene_viewport_interaction_frame.cpp test_scene_viewport_transform_gizmo_coordinator.cpp test_scene_viewport_shader_paths.cpp test_scene_viewport_overlay_renderer.cpp diff --git a/tests/editor/test_scene_viewport_interaction_frame.cpp b/tests/editor/test_scene_viewport_interaction_frame.cpp new file mode 100644 index 00000000..58c459f0 --- /dev/null +++ b/tests/editor/test_scene_viewport_interaction_frame.cpp @@ -0,0 +1,337 @@ +#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); +}