Formalize scene viewport interaction actions
This commit is contained in:
@@ -1,5 +1,22 @@
|
|||||||
# SceneViewport Overlay/Gizmo Rework Checkpoint
|
# SceneViewport Overlay/Gizmo Rework Checkpoint
|
||||||
|
|
||||||
|
## Update 2026-04-04 Phase 5B
|
||||||
|
|
||||||
|
### Interaction Actions Completed
|
||||||
|
|
||||||
|
- Added `SceneViewportInteractionActions.{h,cpp}` to formalize viewport-side hover state application and click action derivation.
|
||||||
|
- `SceneViewPanel` no longer assembles orientation click, scene icon click, scene pick fallback, or hovered gizmo handle state inline.
|
||||||
|
- Selection and orientation side effects now flow through `DispatchSceneViewportInteractionActions(...)`.
|
||||||
|
- The panel keeps navigation and active gizmo drag ownership; interaction semantics are now authored in the viewport module.
|
||||||
|
|
||||||
|
### Verification
|
||||||
|
|
||||||
|
- `cmake --build build --config Debug --target editor_tests -- /p:BuildProjectReferences=false`
|
||||||
|
- `build/tests/Editor/Debug/editor_tests.exe --gtest_filter=SceneViewportInteractionActionsTest.*:SceneViewportInteractionResolverTest.*:SceneViewportOverlayRenderer_Test.*:SceneViewportOverlayProviderRegistryTest.*:ViewportRenderFlowUtilsTest.*`
|
||||||
|
- `cmake --build build --config Debug --target XCEditor`
|
||||||
|
|
||||||
|
All commands completed successfully in `Debug`.
|
||||||
|
|
||||||
## Update 2026-04-03 Phase 5A
|
## Update 2026-04-03 Phase 5A
|
||||||
|
|
||||||
### Interaction Resolver Completed
|
### Interaction Resolver Completed
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ add_executable(${PROJECT_NAME} WIN32
|
|||||||
src/Viewport/SceneViewportRotateGizmo.cpp
|
src/Viewport/SceneViewportRotateGizmo.cpp
|
||||||
src/Viewport/SceneViewportScaleGizmo.cpp
|
src/Viewport/SceneViewportScaleGizmo.cpp
|
||||||
src/Viewport/SceneViewportHudOverlay.cpp
|
src/Viewport/SceneViewportHudOverlay.cpp
|
||||||
|
src/Viewport/SceneViewportInteractionActions.cpp
|
||||||
src/Viewport/SceneViewportInteractionResolver.cpp
|
src/Viewport/SceneViewportInteractionResolver.cpp
|
||||||
src/Viewport/SceneViewportOrientationGizmo.cpp
|
src/Viewport/SceneViewportOrientationGizmo.cpp
|
||||||
src/Viewport/SceneViewportOverlayBuilder.cpp
|
src/Viewport/SceneViewportOverlayBuilder.cpp
|
||||||
|
|||||||
128
editor/src/Viewport/SceneViewportInteractionActions.cpp
Normal file
128
editor/src/Viewport/SceneViewportInteractionActions.cpp
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
#include "SceneViewportInteractionActions.h"
|
||||||
|
|
||||||
|
#include "IViewportHostService.h"
|
||||||
|
|
||||||
|
namespace XCEngine {
|
||||||
|
namespace Editor {
|
||||||
|
|
||||||
|
SceneViewportActiveGizmoKind ToSceneViewportActiveGizmoKind(SceneViewportInteractionKind kind) {
|
||||||
|
switch (kind) {
|
||||||
|
case SceneViewportInteractionKind::MoveGizmo:
|
||||||
|
return SceneViewportActiveGizmoKind::Move;
|
||||||
|
case SceneViewportInteractionKind::RotateGizmo:
|
||||||
|
return SceneViewportActiveGizmoKind::Rotate;
|
||||||
|
case SceneViewportInteractionKind::ScaleGizmo:
|
||||||
|
return SceneViewportActiveGizmoKind::Scale;
|
||||||
|
case SceneViewportInteractionKind::OrientationGizmo:
|
||||||
|
case SceneViewportInteractionKind::SceneIcon:
|
||||||
|
case SceneViewportInteractionKind::None:
|
||||||
|
default:
|
||||||
|
return SceneViewportActiveGizmoKind::None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SceneViewportHoveredHandleState BuildSceneViewportHoveredHandleState(
|
||||||
|
const SceneViewportInteractionResult& interaction) {
|
||||||
|
SceneViewportHoveredHandleState state = {};
|
||||||
|
state.hoveredGizmoKind = ToSceneViewportActiveGizmoKind(interaction.kind);
|
||||||
|
if (interaction.kind == SceneViewportInteractionKind::MoveGizmo) {
|
||||||
|
state.moveAxis = interaction.moveAxis;
|
||||||
|
state.movePlane = interaction.movePlane;
|
||||||
|
} else if (interaction.kind == SceneViewportInteractionKind::RotateGizmo) {
|
||||||
|
state.rotateAxis = interaction.rotateAxis;
|
||||||
|
} else if (interaction.kind == SceneViewportInteractionKind::ScaleGizmo) {
|
||||||
|
state.scaleHandle = interaction.scaleHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ApplySceneViewportHoveredHandleState(
|
||||||
|
const SceneViewportHoveredHandleState& hoveredHandleState,
|
||||||
|
bool gizmoActive,
|
||||||
|
bool showingMoveGizmo,
|
||||||
|
SceneViewportMoveGizmo& moveGizmo,
|
||||||
|
bool showingRotateGizmo,
|
||||||
|
SceneViewportRotateGizmo& rotateGizmo,
|
||||||
|
bool showingScaleGizmo,
|
||||||
|
SceneViewportScaleGizmo& scaleGizmo) {
|
||||||
|
if (gizmoActive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showingMoveGizmo) {
|
||||||
|
moveGizmo.SetHoveredHandle(hoveredHandleState.moveAxis, hoveredHandleState.movePlane);
|
||||||
|
}
|
||||||
|
if (showingRotateGizmo) {
|
||||||
|
rotateGizmo.SetHoveredHandle(hoveredHandleState.rotateAxis);
|
||||||
|
}
|
||||||
|
if (showingScaleGizmo) {
|
||||||
|
scaleGizmo.SetHoveredHandle(hoveredHandleState.scaleHandle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SceneViewportInteractionActions BuildSceneViewportInteractionActions(
|
||||||
|
const SceneViewportInteractionResult& interaction,
|
||||||
|
bool hasInteractiveViewport,
|
||||||
|
bool clickedLeft,
|
||||||
|
bool canResolveViewportInteraction) {
|
||||||
|
SceneViewportInteractionActions actions = {};
|
||||||
|
actions.hoveredGizmoKind = ToSceneViewportActiveGizmoKind(interaction.kind);
|
||||||
|
actions.orientationAxis = interaction.kind == SceneViewportInteractionKind::OrientationGizmo
|
||||||
|
? interaction.orientationAxis
|
||||||
|
: SceneViewportOrientationAxis::None;
|
||||||
|
actions.sceneIconEntityId = interaction.kind == SceneViewportInteractionKind::SceneIcon
|
||||||
|
? interaction.entityId
|
||||||
|
: 0;
|
||||||
|
actions.beginTransformGizmo =
|
||||||
|
hasInteractiveViewport &&
|
||||||
|
clickedLeft &&
|
||||||
|
actions.hoveredGizmoKind != SceneViewportActiveGizmoKind::None;
|
||||||
|
actions.orientationGizmoClick =
|
||||||
|
hasInteractiveViewport &&
|
||||||
|
clickedLeft &&
|
||||||
|
actions.orientationAxis != SceneViewportOrientationAxis::None;
|
||||||
|
actions.sceneIconClick =
|
||||||
|
hasInteractiveViewport &&
|
||||||
|
clickedLeft &&
|
||||||
|
actions.sceneIconEntityId != 0;
|
||||||
|
actions.selectSceneClick =
|
||||||
|
hasInteractiveViewport &&
|
||||||
|
clickedLeft &&
|
||||||
|
canResolveViewportInteraction &&
|
||||||
|
!interaction.HasHit();
|
||||||
|
return actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DispatchSceneViewportInteractionActions(
|
||||||
|
const SceneViewportInteractionActions& actions,
|
||||||
|
IEditorContext& context,
|
||||||
|
IViewportHostService& viewportHostService,
|
||||||
|
const ImVec2& viewportSize,
|
||||||
|
const Math::Vector2& localMousePosition) {
|
||||||
|
if (actions.orientationGizmoClick) {
|
||||||
|
viewportHostService.AlignSceneViewToOrientationAxis(actions.orientationAxis);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actions.sceneIconClick) {
|
||||||
|
context.GetSelectionManager().SetSelectedEntity(actions.sceneIconEntityId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!actions.selectSceneClick) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint64_t selectedEntity = viewportHostService.PickSceneViewEntity(
|
||||||
|
context,
|
||||||
|
viewportSize,
|
||||||
|
ImVec2(localMousePosition.x, localMousePosition.y));
|
||||||
|
if (selectedEntity != 0) {
|
||||||
|
context.GetSelectionManager().SetSelectedEntity(selectedEntity);
|
||||||
|
} else {
|
||||||
|
context.GetSelectionManager().ClearSelection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Editor
|
||||||
|
} // namespace XCEngine
|
||||||
65
editor/src/Viewport/SceneViewportInteractionActions.h
Normal file
65
editor/src/Viewport/SceneViewportInteractionActions.h
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Core/IEditorContext.h"
|
||||||
|
#include "SceneViewportInteractionResolver.h"
|
||||||
|
#include "SceneViewportTransformGizmoFrameBuilder.h"
|
||||||
|
|
||||||
|
#include <imgui.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
namespace XCEngine {
|
||||||
|
namespace Editor {
|
||||||
|
|
||||||
|
struct SceneViewportHoveredHandleState {
|
||||||
|
SceneViewportActiveGizmoKind hoveredGizmoKind = SceneViewportActiveGizmoKind::None;
|
||||||
|
SceneViewportGizmoAxis moveAxis = SceneViewportGizmoAxis::None;
|
||||||
|
SceneViewportGizmoPlane movePlane = SceneViewportGizmoPlane::None;
|
||||||
|
SceneViewportRotateGizmoAxis rotateAxis = SceneViewportRotateGizmoAxis::None;
|
||||||
|
SceneViewportScaleGizmoHandle scaleHandle = SceneViewportScaleGizmoHandle::None;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SceneViewportInteractionActions {
|
||||||
|
SceneViewportActiveGizmoKind hoveredGizmoKind = SceneViewportActiveGizmoKind::None;
|
||||||
|
SceneViewportOrientationAxis orientationAxis = SceneViewportOrientationAxis::None;
|
||||||
|
uint64_t sceneIconEntityId = 0;
|
||||||
|
bool beginTransformGizmo = false;
|
||||||
|
bool orientationGizmoClick = false;
|
||||||
|
bool sceneIconClick = false;
|
||||||
|
bool selectSceneClick = false;
|
||||||
|
|
||||||
|
bool HasClickAction() const {
|
||||||
|
return beginTransformGizmo || orientationGizmoClick || sceneIconClick || selectSceneClick;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
SceneViewportActiveGizmoKind ToSceneViewportActiveGizmoKind(SceneViewportInteractionKind kind);
|
||||||
|
|
||||||
|
SceneViewportHoveredHandleState BuildSceneViewportHoveredHandleState(
|
||||||
|
const SceneViewportInteractionResult& interaction);
|
||||||
|
|
||||||
|
void ApplySceneViewportHoveredHandleState(
|
||||||
|
const SceneViewportHoveredHandleState& hoveredHandleState,
|
||||||
|
bool gizmoActive,
|
||||||
|
bool showingMoveGizmo,
|
||||||
|
SceneViewportMoveGizmo& moveGizmo,
|
||||||
|
bool showingRotateGizmo,
|
||||||
|
SceneViewportRotateGizmo& rotateGizmo,
|
||||||
|
bool showingScaleGizmo,
|
||||||
|
SceneViewportScaleGizmo& scaleGizmo);
|
||||||
|
|
||||||
|
SceneViewportInteractionActions BuildSceneViewportInteractionActions(
|
||||||
|
const SceneViewportInteractionResult& interaction,
|
||||||
|
bool hasInteractiveViewport,
|
||||||
|
bool clickedLeft,
|
||||||
|
bool canResolveViewportInteraction);
|
||||||
|
|
||||||
|
void DispatchSceneViewportInteractionActions(
|
||||||
|
const SceneViewportInteractionActions& actions,
|
||||||
|
IEditorContext& context,
|
||||||
|
IViewportHostService& viewportHostService,
|
||||||
|
const ImVec2& viewportSize,
|
||||||
|
const Math::Vector2& localMousePosition);
|
||||||
|
|
||||||
|
} // namespace Editor
|
||||||
|
} // namespace XCEngine
|
||||||
@@ -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/SceneViewportInteractionActions.h"
|
||||||
#include "Viewport/SceneViewportInteractionResolver.h"
|
#include "Viewport/SceneViewportInteractionResolver.h"
|
||||||
#include "Viewport/SceneViewportOverlayHandleBuilder.h"
|
#include "Viewport/SceneViewportOverlayHandleBuilder.h"
|
||||||
#include "Viewport/SceneViewportMath.h"
|
#include "Viewport/SceneViewportMath.h"
|
||||||
@@ -38,22 +39,6 @@ const char* GetSceneViewportTransformSpaceModeLabel(SceneViewportTransformSpaceM
|
|||||||
return mode == SceneViewportTransformSpaceMode::Global ? "Global" : "Local";
|
return mode == SceneViewportTransformSpaceMode::Global ? "Global" : "Local";
|
||||||
}
|
}
|
||||||
|
|
||||||
SceneViewportActiveGizmoKind ToActiveGizmoKind(SceneViewportInteractionKind kind) {
|
|
||||||
switch (kind) {
|
|
||||||
case SceneViewportInteractionKind::MoveGizmo:
|
|
||||||
return SceneViewportActiveGizmoKind::Move;
|
|
||||||
case SceneViewportInteractionKind::RotateGizmo:
|
|
||||||
return SceneViewportActiveGizmoKind::Rotate;
|
|
||||||
case SceneViewportInteractionKind::ScaleGizmo:
|
|
||||||
return SceneViewportActiveGizmoKind::Scale;
|
|
||||||
case SceneViewportInteractionKind::OrientationGizmo:
|
|
||||||
case SceneViewportInteractionKind::SceneIcon:
|
|
||||||
case SceneViewportInteractionKind::None:
|
|
||||||
default:
|
|
||||||
return SceneViewportActiveGizmoKind::None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
float GetSceneToolbarToggleWidth(const char* firstLabel, const char* secondLabel) {
|
float GetSceneToolbarToggleWidth(const char* firstLabel, const char* secondLabel) {
|
||||||
constexpr float kHorizontalPadding = 10.0f;
|
constexpr float kHorizontalPadding = 10.0f;
|
||||||
constexpr float kMinWidth = 68.0f;
|
constexpr float kMinWidth = 68.0f;
|
||||||
@@ -440,58 +425,22 @@ void SceneViewPanel::Render() {
|
|||||||
hoveredInteraction = ResolveSceneViewportInteraction(interactionRequest);
|
hoveredInteraction = ResolveSceneViewportInteraction(interactionRequest);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!gizmoActive) {
|
ApplySceneViewportHoveredHandleState(
|
||||||
if (showingMoveGizmo) {
|
BuildSceneViewportHoveredHandleState(hoveredInteraction),
|
||||||
m_moveGizmo.SetHoveredHandle(
|
gizmoActive,
|
||||||
hoveredInteraction.kind == SceneViewportInteractionKind::MoveGizmo
|
showingMoveGizmo,
|
||||||
? hoveredInteraction.moveAxis
|
m_moveGizmo,
|
||||||
: SceneViewportGizmoAxis::None,
|
showingRotateGizmo,
|
||||||
hoveredInteraction.kind == SceneViewportInteractionKind::MoveGizmo
|
m_rotateGizmo,
|
||||||
? hoveredInteraction.movePlane
|
showingScaleGizmo,
|
||||||
: SceneViewportGizmoPlane::None);
|
m_scaleGizmo);
|
||||||
}
|
|
||||||
if (showingRotateGizmo) {
|
|
||||||
m_rotateGizmo.SetHoveredHandle(
|
|
||||||
hoveredInteraction.kind == SceneViewportInteractionKind::RotateGizmo
|
|
||||||
? hoveredInteraction.rotateAxis
|
|
||||||
: SceneViewportRotateGizmoAxis::None);
|
|
||||||
}
|
|
||||||
if (showingScaleGizmo) {
|
|
||||||
m_scaleGizmo.SetHoveredHandle(
|
|
||||||
hoveredInteraction.kind == SceneViewportInteractionKind::ScaleGizmo
|
|
||||||
? hoveredInteraction.scaleHandle
|
|
||||||
: SceneViewportScaleGizmoHandle::None);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const SceneViewportActiveGizmoKind hoveredGizmoKind =
|
const SceneViewportInteractionActions interactionActions =
|
||||||
ToActiveGizmoKind(hoveredInteraction.kind);
|
BuildSceneViewportInteractionActions(
|
||||||
const bool gizmoHovering = hoveredGizmoKind != SceneViewportActiveGizmoKind::None;
|
hoveredInteraction,
|
||||||
const SceneViewportOrientationAxis orientationAxisHit =
|
hasInteractiveViewport,
|
||||||
hoveredInteraction.kind == SceneViewportInteractionKind::OrientationGizmo
|
content.clickedLeft,
|
||||||
? hoveredInteraction.orientationAxis
|
canResolveViewportInteraction);
|
||||||
: SceneViewportOrientationAxis::None;
|
|
||||||
const uint64_t clickedSceneIconEntity =
|
|
||||||
hoveredInteraction.kind == SceneViewportInteractionKind::SceneIcon
|
|
||||||
? hoveredInteraction.entityId
|
|
||||||
: 0;
|
|
||||||
const bool beginTransformGizmo =
|
|
||||||
hasInteractiveViewport &&
|
|
||||||
content.clickedLeft &&
|
|
||||||
gizmoHovering;
|
|
||||||
const bool orientationGizmoClick =
|
|
||||||
hasInteractiveViewport &&
|
|
||||||
content.clickedLeft &&
|
|
||||||
orientationAxisHit != SceneViewportOrientationAxis::None;
|
|
||||||
const bool sceneIconClick =
|
|
||||||
hasInteractiveViewport &&
|
|
||||||
content.clickedLeft &&
|
|
||||||
clickedSceneIconEntity != 0;
|
|
||||||
const bool selectClick =
|
|
||||||
hasInteractiveViewport &&
|
|
||||||
content.clickedLeft &&
|
|
||||||
canResolveViewportInteraction &&
|
|
||||||
!hoveredInteraction.HasHit();
|
|
||||||
const bool beginLeftPanDrag = usingViewMoveTool
|
const bool beginLeftPanDrag = usingViewMoveTool
|
||||||
? ShouldBeginSceneViewportNavigationDrag(
|
? ShouldBeginSceneViewportNavigationDrag(
|
||||||
hasInteractiveViewport,
|
hasInteractiveViewport,
|
||||||
@@ -517,38 +466,27 @@ void SceneViewPanel::Render() {
|
|||||||
ImGuiMouseButton_Middle);
|
ImGuiMouseButton_Middle);
|
||||||
const bool beginPanDrag = beginLeftPanDrag || beginMiddlePanDrag;
|
const bool beginPanDrag = beginLeftPanDrag || beginMiddlePanDrag;
|
||||||
|
|
||||||
if (toolOverlay.clicked || beginTransformGizmo || orientationGizmoClick || sceneIconClick || selectClick || beginLookDrag ||
|
if (toolOverlay.clicked || interactionActions.HasClickAction() || beginLookDrag ||
|
||||||
beginPanDrag) {
|
beginPanDrag) {
|
||||||
ImGui::SetWindowFocus();
|
ImGui::SetWindowFocus();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (beginTransformGizmo) {
|
if (interactionActions.beginTransformGizmo) {
|
||||||
if (hoveredGizmoKind == SceneViewportActiveGizmoKind::Scale) {
|
if (interactionActions.hoveredGizmoKind == SceneViewportActiveGizmoKind::Scale) {
|
||||||
m_scaleGizmo.TryBeginDrag(gizmoFrameState.scaleContext, m_context->GetUndoManager());
|
m_scaleGizmo.TryBeginDrag(gizmoFrameState.scaleContext, m_context->GetUndoManager());
|
||||||
} else if (hoveredGizmoKind == SceneViewportActiveGizmoKind::Move) {
|
} else if (interactionActions.hoveredGizmoKind == SceneViewportActiveGizmoKind::Move) {
|
||||||
m_moveGizmo.TryBeginDrag(gizmoFrameState.moveContext, m_context->GetUndoManager());
|
m_moveGizmo.TryBeginDrag(gizmoFrameState.moveContext, m_context->GetUndoManager());
|
||||||
} else if (hoveredGizmoKind == SceneViewportActiveGizmoKind::Rotate) {
|
} else if (interactionActions.hoveredGizmoKind == SceneViewportActiveGizmoKind::Rotate) {
|
||||||
m_rotateGizmo.TryBeginDrag(gizmoFrameState.rotateContext, m_context->GetUndoManager());
|
m_rotateGizmo.TryBeginDrag(gizmoFrameState.rotateContext, m_context->GetUndoManager());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (orientationGizmoClick) {
|
DispatchSceneViewportInteractionActions(
|
||||||
viewportHostService->AlignSceneViewToOrientationAxis(orientationAxisHit);
|
interactionActions,
|
||||||
}
|
*m_context,
|
||||||
|
*viewportHostService,
|
||||||
if (sceneIconClick) {
|
content.availableSize,
|
||||||
m_context->GetSelectionManager().SetSelectedEntity(clickedSceneIconEntity);
|
localMousePosition);
|
||||||
} else if (selectClick) {
|
|
||||||
const uint64_t selectedEntity = viewportHostService->PickSceneViewEntity(
|
|
||||||
*m_context,
|
|
||||||
content.availableSize,
|
|
||||||
ImVec2(localMousePosition.x, localMousePosition.y));
|
|
||||||
if (selectedEntity != 0) {
|
|
||||||
m_context->GetSelectionManager().SetSelectedEntity(selectedEntity);
|
|
||||||
} else {
|
|
||||||
m_context->GetSelectionManager().ClearSelection();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (gizmoActive) {
|
if (gizmoActive) {
|
||||||
if (ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
|
if (ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ set(EDITOR_TEST_SOURCES
|
|||||||
test_scene_viewport_rotate_gizmo.cpp
|
test_scene_viewport_rotate_gizmo.cpp
|
||||||
test_scene_viewport_scale_gizmo.cpp
|
test_scene_viewport_scale_gizmo.cpp
|
||||||
test_scene_viewport_picker.cpp
|
test_scene_viewport_picker.cpp
|
||||||
|
test_scene_viewport_interaction_actions.cpp
|
||||||
test_scene_viewport_interaction_resolver.cpp
|
test_scene_viewport_interaction_resolver.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
|
||||||
@@ -39,6 +40,7 @@ set(EDITOR_TEST_SOURCES
|
|||||||
${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportRotateGizmo.cpp
|
${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportRotateGizmo.cpp
|
||||||
${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportScaleGizmo.cpp
|
${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportScaleGizmo.cpp
|
||||||
${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportHudOverlay.cpp
|
${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportHudOverlay.cpp
|
||||||
|
${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportInteractionActions.cpp
|
||||||
${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportInteractionResolver.cpp
|
${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportInteractionResolver.cpp
|
||||||
${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportOrientationGizmo.cpp
|
${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportOrientationGizmo.cpp
|
||||||
${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportOverlayBuilder.cpp
|
${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportOverlayBuilder.cpp
|
||||||
|
|||||||
344
tests/editor/test_scene_viewport_interaction_actions.cpp
Normal file
344
tests/editor/test_scene_viewport_interaction_actions.cpp
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
#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/SceneViewportInteractionActions.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using XCEngine::Editor::AssetItemPtr;
|
||||||
|
using XCEngine::Editor::BuildSceneViewportHoveredHandleState;
|
||||||
|
using XCEngine::Editor::BuildSceneViewportInteractionActions;
|
||||||
|
using XCEngine::Editor::DispatchSceneViewportInteractionActions;
|
||||||
|
using XCEngine::Editor::EditorActionRoute;
|
||||||
|
using XCEngine::Editor::EditorRuntimeMode;
|
||||||
|
using XCEngine::Editor::EventBus;
|
||||||
|
using XCEngine::Editor::IEditorContext;
|
||||||
|
using XCEngine::Editor::IProjectManager;
|
||||||
|
using XCEngine::Editor::ISceneManager;
|
||||||
|
using XCEngine::Editor::ISelectionManager;
|
||||||
|
using XCEngine::Editor::IUndoManager;
|
||||||
|
using XCEngine::Editor::SceneSnapshot;
|
||||||
|
using XCEngine::Editor::SceneViewportActiveGizmoKind;
|
||||||
|
using XCEngine::Editor::SceneViewportGizmoAxis;
|
||||||
|
using XCEngine::Editor::SceneViewportInteractionActions;
|
||||||
|
using XCEngine::Editor::SceneViewportInteractionKind;
|
||||||
|
using XCEngine::Editor::SceneViewportInteractionResult;
|
||||||
|
using XCEngine::Editor::SceneViewportOrientationAxis;
|
||||||
|
using XCEngine::Editor::IViewportHostService;
|
||||||
|
using XCEngine::Math::Vector2;
|
||||||
|
|
||||||
|
class StubSelectionManager : public ISelectionManager {
|
||||||
|
public:
|
||||||
|
void SetSelectedEntity(uint64_t entityId) override {
|
||||||
|
selectedEntity = entityId;
|
||||||
|
selectedEntities = entityId == 0 ? std::vector<uint64_t>{} : std::vector<uint64_t>{entityId};
|
||||||
|
clearCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetSelectedEntities(const std::vector<uint64_t>& entityIds) override {
|
||||||
|
selectedEntities = entityIds;
|
||||||
|
selectedEntity = entityIds.empty() ? 0 : entityIds.front();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AddToSelection(uint64_t entityId) override {
|
||||||
|
selectedEntities.push_back(entityId);
|
||||||
|
selectedEntity = entityId;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RemoveFromSelection(uint64_t entityId) override {
|
||||||
|
selectedEntities.erase(
|
||||||
|
std::remove(selectedEntities.begin(), selectedEntities.end(), entityId),
|
||||||
|
selectedEntities.end());
|
||||||
|
if (selectedEntity == entityId) {
|
||||||
|
selectedEntity = selectedEntities.empty() ? 0 : selectedEntities.front();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClearSelection() override {
|
||||||
|
selectedEntity = 0;
|
||||||
|
selectedEntities.clear();
|
||||||
|
++clearCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t GetSelectedEntity() const override {
|
||||||
|
return selectedEntity;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<uint64_t>& GetSelectedEntities() const override {
|
||||||
|
return selectedEntities;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HasSelection() const override {
|
||||||
|
return !selectedEntities.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t GetSelectionCount() const override {
|
||||||
|
return selectedEntities.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsSelected(uint64_t entityId) const override {
|
||||||
|
return std::find(selectedEntities.begin(), selectedEntities.end(), entityId) != selectedEntities.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t selectedEntity = 0;
|
||||||
|
std::vector<uint64_t> selectedEntities = {};
|
||||||
|
int clearCount = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class StubSceneManager : public ISceneManager {
|
||||||
|
public:
|
||||||
|
XCEngine::Components::GameObject* CreateEntity(
|
||||||
|
const std::string&,
|
||||||
|
XCEngine::Components::GameObject* = nullptr) override { return nullptr; }
|
||||||
|
void DeleteEntity(XCEngine::Components::GameObject::ID) override {}
|
||||||
|
void RenameEntity(XCEngine::Components::GameObject::ID, const std::string&) override {}
|
||||||
|
XCEngine::Components::GameObject* GetEntity(XCEngine::Components::GameObject::ID) override { return nullptr; }
|
||||||
|
const std::vector<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 StubProjectManager : 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 StubUndoManager : public IUndoManager {
|
||||||
|
public:
|
||||||
|
void ClearHistory() override {}
|
||||||
|
bool CanUndo() const override { return false; }
|
||||||
|
bool CanRedo() const override { return false; }
|
||||||
|
const std::string& GetUndoLabel() const override { return empty; }
|
||||||
|
const std::string& GetRedoLabel() const override { return empty; }
|
||||||
|
void Undo() override {}
|
||||||
|
void Redo() override {}
|
||||||
|
XCEngine::Editor::UndoStateSnapshot CaptureCurrentState() const override { return {}; }
|
||||||
|
void PushCommand(const std::string&, XCEngine::Editor::UndoStateSnapshot, XCEngine::Editor::UndoStateSnapshot) override {}
|
||||||
|
void BeginInteractiveChange(const std::string&) override {}
|
||||||
|
bool HasPendingInteractiveChange() const override { return false; }
|
||||||
|
void FinalizeInteractiveChange() override {}
|
||||||
|
void CancelInteractiveChange() override {}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string empty;
|
||||||
|
};
|
||||||
|
|
||||||
|
class StubViewportHostService : public IViewportHostService {
|
||||||
|
public:
|
||||||
|
void BeginFrame() override {}
|
||||||
|
XCEngine::Editor::EditorViewportFrame RequestViewport(
|
||||||
|
XCEngine::Editor::EditorViewportKind,
|
||||||
|
const ImVec2&) override { return {}; }
|
||||||
|
void UpdateSceneViewInput(IEditorContext&, const XCEngine::Editor::SceneViewportInput&) override {}
|
||||||
|
uint64_t PickSceneViewEntity(IEditorContext&, const ImVec2&, const ImVec2&) override {
|
||||||
|
++pickCallCount;
|
||||||
|
return pickedEntity;
|
||||||
|
}
|
||||||
|
void AlignSceneViewToOrientationAxis(SceneViewportOrientationAxis axis) override {
|
||||||
|
alignedAxis = axis;
|
||||||
|
}
|
||||||
|
XCEngine::Editor::SceneViewportOverlayData GetSceneViewOverlayData() const override { return {}; }
|
||||||
|
const XCEngine::Editor::SceneViewportOverlayFrameData& GetSceneViewEditorOverlayFrameData(IEditorContext&) override {
|
||||||
|
return overlayFrameData;
|
||||||
|
}
|
||||||
|
void SetSceneViewTransformGizmoOverlayState(const XCEngine::Editor::SceneViewportTransformGizmoOverlayState&) override {}
|
||||||
|
void RenderRequestedViewports(IEditorContext&, const XCEngine::Rendering::RenderContext&) override {}
|
||||||
|
|
||||||
|
SceneViewportOrientationAxis alignedAxis = SceneViewportOrientationAxis::None;
|
||||||
|
uint64_t pickedEntity = 0;
|
||||||
|
int pickCallCount = 0;
|
||||||
|
|
||||||
|
private:
|
||||||
|
XCEngine::Editor::SceneViewportOverlayFrameData overlayFrameData = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
class StubEditorContext : public IEditorContext {
|
||||||
|
public:
|
||||||
|
EventBus& GetEventBus() override { return eventBus; }
|
||||||
|
ISelectionManager& GetSelectionManager() override { return selectionManager; }
|
||||||
|
ISceneManager& GetSceneManager() override { return sceneManager; }
|
||||||
|
IProjectManager& GetProjectManager() override { return projectManager; }
|
||||||
|
IUndoManager& GetUndoManager() override { return undoManager; }
|
||||||
|
IViewportHostService* GetViewportHostService() override { return viewportHostService; }
|
||||||
|
void SetActiveActionRoute(EditorActionRoute route) override { activeRoute = route; }
|
||||||
|
EditorActionRoute GetActiveActionRoute() const override { return activeRoute; }
|
||||||
|
void SetRuntimeMode(EditorRuntimeMode mode) override { runtimeMode = mode; }
|
||||||
|
EditorRuntimeMode GetRuntimeMode() const override { return runtimeMode; }
|
||||||
|
void SetProjectPath(const std::string& path) override { projectPath = path; }
|
||||||
|
const std::string& GetProjectPath() const override { return projectPath; }
|
||||||
|
|
||||||
|
EventBus eventBus = {};
|
||||||
|
StubSelectionManager selectionManager = {};
|
||||||
|
StubSceneManager sceneManager = {};
|
||||||
|
StubProjectManager projectManager = {};
|
||||||
|
StubUndoManager undoManager = {};
|
||||||
|
IViewportHostService* viewportHostService = nullptr;
|
||||||
|
EditorActionRoute activeRoute = EditorActionRoute::None;
|
||||||
|
EditorRuntimeMode runtimeMode = EditorRuntimeMode::Edit;
|
||||||
|
std::string projectPath;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST(SceneViewportInteractionActionsTest, BuildHoveredHandleStateMapsMoveInteraction) {
|
||||||
|
SceneViewportInteractionResult interaction = {};
|
||||||
|
interaction.kind = SceneViewportInteractionKind::MoveGizmo;
|
||||||
|
interaction.moveAxis = SceneViewportGizmoAxis::Y;
|
||||||
|
|
||||||
|
const auto hoverState = BuildSceneViewportHoveredHandleState(interaction);
|
||||||
|
|
||||||
|
EXPECT_EQ(hoverState.hoveredGizmoKind, SceneViewportActiveGizmoKind::Move);
|
||||||
|
EXPECT_EQ(hoverState.moveAxis, SceneViewportGizmoAxis::Y);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(SceneViewportInteractionActionsTest, BuildInteractionActionsMapsOrientationClick) {
|
||||||
|
SceneViewportInteractionResult interaction = {};
|
||||||
|
interaction.kind = SceneViewportInteractionKind::OrientationGizmo;
|
||||||
|
interaction.orientationAxis = SceneViewportOrientationAxis::PositiveZ;
|
||||||
|
|
||||||
|
const SceneViewportInteractionActions actions =
|
||||||
|
BuildSceneViewportInteractionActions(interaction, true, true, true);
|
||||||
|
|
||||||
|
EXPECT_TRUE(actions.orientationGizmoClick);
|
||||||
|
EXPECT_EQ(actions.orientationAxis, SceneViewportOrientationAxis::PositiveZ);
|
||||||
|
EXPECT_FALSE(actions.selectSceneClick);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(SceneViewportInteractionActionsTest, BuildInteractionActionsMapsScenePickFallback) {
|
||||||
|
const SceneViewportInteractionActions actions =
|
||||||
|
BuildSceneViewportInteractionActions({}, true, true, true);
|
||||||
|
|
||||||
|
EXPECT_TRUE(actions.selectSceneClick);
|
||||||
|
EXPECT_FALSE(actions.sceneIconClick);
|
||||||
|
EXPECT_FALSE(actions.orientationGizmoClick);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(SceneViewportInteractionActionsTest, DispatchOrientationActionAlignsViewport) {
|
||||||
|
StubViewportHostService viewportHostService = {};
|
||||||
|
StubEditorContext context = {};
|
||||||
|
context.viewportHostService = &viewportHostService;
|
||||||
|
|
||||||
|
SceneViewportInteractionActions actions = {};
|
||||||
|
actions.orientationGizmoClick = true;
|
||||||
|
actions.orientationAxis = SceneViewportOrientationAxis::PositiveY;
|
||||||
|
|
||||||
|
DispatchSceneViewportInteractionActions(
|
||||||
|
actions,
|
||||||
|
context,
|
||||||
|
viewportHostService,
|
||||||
|
ImVec2(200.0f, 100.0f),
|
||||||
|
Vector2(40.0f, 30.0f));
|
||||||
|
|
||||||
|
EXPECT_EQ(viewportHostService.alignedAxis, SceneViewportOrientationAxis::PositiveY);
|
||||||
|
EXPECT_EQ(viewportHostService.pickCallCount, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(SceneViewportInteractionActionsTest, DispatchSceneIconClickSelectsEntityWithoutPicking) {
|
||||||
|
StubViewportHostService viewportHostService = {};
|
||||||
|
StubEditorContext context = {};
|
||||||
|
context.viewportHostService = &viewportHostService;
|
||||||
|
|
||||||
|
SceneViewportInteractionActions actions = {};
|
||||||
|
actions.sceneIconClick = true;
|
||||||
|
actions.sceneIconEntityId = 42;
|
||||||
|
|
||||||
|
DispatchSceneViewportInteractionActions(
|
||||||
|
actions,
|
||||||
|
context,
|
||||||
|
viewportHostService,
|
||||||
|
ImVec2(200.0f, 100.0f),
|
||||||
|
Vector2(40.0f, 30.0f));
|
||||||
|
|
||||||
|
EXPECT_EQ(context.selectionManager.selectedEntity, 42u);
|
||||||
|
EXPECT_EQ(viewportHostService.pickCallCount, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(SceneViewportInteractionActionsTest, DispatchScenePickSelectsPickedEntityOrClearsSelection) {
|
||||||
|
StubViewportHostService viewportHostService = {};
|
||||||
|
StubEditorContext context = {};
|
||||||
|
context.viewportHostService = &viewportHostService;
|
||||||
|
|
||||||
|
SceneViewportInteractionActions actions = {};
|
||||||
|
actions.selectSceneClick = true;
|
||||||
|
viewportHostService.pickedEntity = 77;
|
||||||
|
|
||||||
|
DispatchSceneViewportInteractionActions(
|
||||||
|
actions,
|
||||||
|
context,
|
||||||
|
viewportHostService,
|
||||||
|
ImVec2(200.0f, 100.0f),
|
||||||
|
Vector2(40.0f, 30.0f));
|
||||||
|
|
||||||
|
EXPECT_EQ(context.selectionManager.selectedEntity, 77u);
|
||||||
|
EXPECT_EQ(viewportHostService.pickCallCount, 1);
|
||||||
|
|
||||||
|
viewportHostService.pickedEntity = 0;
|
||||||
|
DispatchSceneViewportInteractionActions(
|
||||||
|
actions,
|
||||||
|
context,
|
||||||
|
viewportHostService,
|
||||||
|
ImVec2(200.0f, 100.0f),
|
||||||
|
Vector2(40.0f, 30.0f));
|
||||||
|
|
||||||
|
EXPECT_EQ(context.selectionManager.selectedEntity, 0u);
|
||||||
|
EXPECT_GE(context.selectionManager.clearCount, 1);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user