Formalize scene viewport interaction frame helpers

This commit is contained in:
2026-04-04 01:42:35 +08:00
parent a920ca7a6a
commit e636abb76d
5 changed files with 531 additions and 79 deletions

View File

@@ -1,5 +1,26 @@
# SceneViewport Overlay/Gizmo Rework Checkpoint # 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 ## Update 2026-04-04 Phase 5E
### Navigation/Input State Formalization Completed ### Navigation/Input State Formalization Completed

View File

@@ -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

View File

@@ -5,6 +5,7 @@
#include "SceneViewPanel.h" #include "SceneViewPanel.h"
#include "Viewport/SceneViewportEditorOverlayData.h" #include "Viewport/SceneViewportEditorOverlayData.h"
#include "Viewport/SceneViewportHudOverlay.h" #include "Viewport/SceneViewportHudOverlay.h"
#include "Viewport/SceneViewportInteractionFrame.h"
#include "Viewport/SceneViewportInteractionActions.h" #include "Viewport/SceneViewportInteractionActions.h"
#include "Viewport/SceneViewportInteractionResolver.h" #include "Viewport/SceneViewportInteractionResolver.h"
#include "Viewport/SceneViewportMath.h" #include "Viewport/SceneViewportMath.h"
@@ -292,91 +293,51 @@ void SceneViewPanel::Render() {
m_toolMode = toolShortcutAction.targetTool; m_toolMode = toolShortcutAction.targetTool;
} }
const bool usingViewMoveTool = m_toolMode == SceneViewportToolMode::ViewMove; const SceneViewportToolState toolState = BuildSceneViewportToolState(
const bool usingTransformTool = m_toolMode == SceneViewportToolMode::Transform; m_toolMode,
const bool showingMoveGizmo = m_toolMode == SceneViewportToolMode::Move || usingTransformTool; m_pivotMode,
const bool showingRotateGizmo = m_toolMode == SceneViewportToolMode::Rotate || usingTransformTool; m_transformSpaceMode);
const bool showingScaleGizmo = m_toolMode == SceneViewportToolMode::Scale || usingTransformTool; const SceneViewportFrameGeometry frameGeometry = BuildSceneViewportFrameGeometry(
const bool useCenterPivot = m_pivotMode == SceneViewportPivotMode::Center; content.availableSize,
const bool localSpace = m_transformSpaceMode == SceneViewportTransformSpaceMode::Local; content.itemMin,
const SceneViewportTransformGizmoFrameOptions gizmoFrameOptions = io.MousePos);
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 = {};
SceneViewportOverlayFrameData emptySceneOverlayFrameData = {}; SceneViewportOverlayFrameData emptySceneOverlayFrameData = {};
const SceneViewportInteractionFrameState interactionFrameState =
if (hasInteractiveViewport) { BuildSceneViewportInteractionFrameState(
overlay = viewportHostService->GetSceneViewOverlayData(); *m_context,
interactionGizmoFrame = RefreshAndSubmitSceneViewportTransformGizmoFrame(
*viewportHostService, *viewportHostService,
BuildSceneViewportTransformGizmoRefreshRequest( hasInteractiveViewport,
*m_context, frameGeometry,
overlay, toolState.gizmoFrameOptions,
viewportSize,
localMousePosition,
gizmoFrameOptions),
m_moveGizmo, m_moveGizmo,
m_rotateGizmo, m_rotateGizmo,
m_scaleGizmo); m_scaleGizmo,
gizmoFrameState = interactionGizmoFrame.frameState; emptySceneOverlayFrameData);
} else {
CancelSceneViewportTransformGizmoFrame(
*m_context,
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);
SceneViewportInteractionResult hoveredInteraction = {}; SceneViewportInteractionResult hoveredInteraction = {};
const bool canResolveViewportInteraction = CanResolveSceneViewportInteraction( const bool canResolveViewportInteraction = CanResolveSceneViewportInteraction(
hasInteractiveViewport, hasInteractiveViewport,
viewportContentHovered, viewportContentHovered,
usingViewMoveTool, toolState.usingViewMoveTool,
m_navigationState, m_navigationState,
gizmoActive); interactionFrameState.gizmoActive);
if (canResolveViewportInteraction) { if (canResolveViewportInteraction) {
SceneViewportInteractionResolveRequest interactionRequest = {}; hoveredInteraction = ResolveSceneViewportInteraction(
interactionRequest.overlayFrameData = &interactionOverlayFrameData; BuildSceneViewportInteractionResolveRequest(
interactionRequest.viewportSize = viewportSize; interactionFrameState,
interactionRequest.localMousePosition = localMousePosition; frameGeometry,
interactionRequest.hudOverlay = &interactionHudOverlay; content.itemMin,
interactionRequest.viewportMin = content.itemMin; content.itemMax,
interactionRequest.viewportMax = content.itemMax; io.MousePos));
interactionRequest.absoluteMousePosition = io.MousePos;
hoveredInteraction = ResolveSceneViewportInteraction(interactionRequest);
} }
ApplySceneViewportHoveredHandleState( ApplySceneViewportHoveredHandleState(
BuildSceneViewportHoveredHandleState(hoveredInteraction), BuildSceneViewportHoveredHandleState(hoveredInteraction),
gizmoActive, interactionFrameState.gizmoActive,
showingMoveGizmo, toolState.showingMoveGizmo,
m_moveGizmo, m_moveGizmo,
showingRotateGizmo, toolState.showingRotateGizmo,
m_rotateGizmo, m_rotateGizmo,
showingScaleGizmo, toolState.showingScaleGizmo,
m_scaleGizmo); m_scaleGizmo);
const SceneViewportInteractionActions interactionActions = const SceneViewportInteractionActions interactionActions =
@@ -389,8 +350,8 @@ void SceneViewPanel::Render() {
navigationRequest.state = m_navigationState; navigationRequest.state = m_navigationState;
navigationRequest.hasInteractiveViewport = hasInteractiveViewport; navigationRequest.hasInteractiveViewport = hasInteractiveViewport;
navigationRequest.viewportHovered = viewportContentHovered; navigationRequest.viewportHovered = viewportContentHovered;
navigationRequest.usingViewMoveTool = usingViewMoveTool; navigationRequest.usingViewMoveTool = toolState.usingViewMoveTool;
navigationRequest.gizmoActive = gizmoActive; navigationRequest.gizmoActive = interactionFrameState.gizmoActive;
navigationRequest.clickedLeft = content.clickedLeft; navigationRequest.clickedLeft = content.clickedLeft;
navigationRequest.clickedRight = content.clickedRight; navigationRequest.clickedRight = content.clickedRight;
navigationRequest.clickedMiddle = content.clickedMiddle; navigationRequest.clickedMiddle = content.clickedMiddle;
@@ -409,7 +370,7 @@ void SceneViewPanel::Render() {
ExecuteSceneViewportTransformGizmoLifecycleCommand( ExecuteSceneViewportTransformGizmoLifecycleCommand(
BuildBeginSceneViewportTransformGizmoLifecycleCommand(interactionActions), BuildBeginSceneViewportTransformGizmoLifecycleCommand(interactionActions),
m_context->GetUndoManager(), m_context->GetUndoManager(),
gizmoFrameState, interactionFrameState.gizmoFrameState,
m_moveGizmo, m_moveGizmo,
m_rotateGizmo, m_rotateGizmo,
m_scaleGizmo); m_scaleGizmo);
@@ -419,14 +380,14 @@ void SceneViewPanel::Render() {
*m_context, *m_context,
*viewportHostService, *viewportHostService,
content.availableSize, content.availableSize,
localMousePosition); frameGeometry.localMousePosition);
ExecuteSceneViewportTransformGizmoLifecycleCommand( ExecuteSceneViewportTransformGizmoLifecycleCommand(
BuildFrameSceneViewportTransformGizmoLifecycleCommand( BuildFrameSceneViewportTransformGizmoLifecycleCommand(
activeGizmoKind, interactionFrameState.activeGizmoKind,
ImGui::IsMouseDown(ImGuiMouseButton_Left)), ImGui::IsMouseDown(ImGuiMouseButton_Left)),
m_context->GetUndoManager(), m_context->GetUndoManager(),
gizmoFrameState, interactionFrameState.gizmoFrameState,
m_moveGizmo, m_moveGizmo,
m_rotateGizmo, m_rotateGizmo,
m_scaleGizmo); m_scaleGizmo);
@@ -466,15 +427,15 @@ void SceneViewPanel::Render() {
viewportHostService->UpdateSceneViewInput(*m_context, input); viewportHostService->UpdateSceneViewInput(*m_context, input);
if (content.hasViewportArea && content.frame.hasTexture) { if (content.hasViewportArea && content.frame.hasTexture) {
overlay = viewportHostService->GetSceneViewOverlayData(); const SceneViewportOverlayData overlay = viewportHostService->GetSceneViewOverlayData();
RefreshAndSubmitSceneViewportTransformGizmoFrame( RefreshAndSubmitSceneViewportTransformGizmoFrame(
*viewportHostService, *viewportHostService,
BuildSceneViewportTransformGizmoRefreshRequest( BuildSceneViewportTransformGizmoRefreshRequest(
*m_context, *m_context,
overlay, overlay,
viewportSize, frameGeometry.viewportSize,
localMousePosition, frameGeometry.localMousePosition,
gizmoFrameOptions), toolState.gizmoFrameOptions),
m_moveGizmo, m_moveGizmo,
m_rotateGizmo, m_rotateGizmo,
m_scaleGizmo); m_scaleGizmo);

View File

@@ -14,6 +14,7 @@ set(EDITOR_TEST_SOURCES
test_scene_viewport_picker.cpp test_scene_viewport_picker.cpp
test_scene_viewport_interaction_actions.cpp test_scene_viewport_interaction_actions.cpp
test_scene_viewport_interaction_resolver.cpp test_scene_viewport_interaction_resolver.cpp
test_scene_viewport_interaction_frame.cpp
test_scene_viewport_transform_gizmo_coordinator.cpp test_scene_viewport_transform_gizmo_coordinator.cpp
test_scene_viewport_shader_paths.cpp test_scene_viewport_shader_paths.cpp
test_scene_viewport_overlay_renderer.cpp test_scene_viewport_overlay_renderer.cpp

View File

@@ -0,0 +1,337 @@
#include <gtest/gtest.h>
#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 <algorithm>
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<uint64_t>&) override {}
void AddToSelection(uint64_t) override {}
void RemoveFromSelection(uint64_t) override {}
void ClearSelection() override {}
uint64_t GetSelectedEntity() const override { return 0; }
const std::vector<uint64_t>& 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<uint64_t> 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<XCEngine::Components::GameObject*>& 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<XCEngine::Components::GameObject*> rootEntities = {};
std::string empty;
};
class EmptyProjectManager : public IProjectManager {
public:
const std::vector<AssetItemPtr>& 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<AssetItemPtr> 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);
}