Formalize scene viewport interaction resolver

This commit is contained in:
2026-04-03 17:16:16 +08:00
parent 27014e613e
commit 1ac2afb0bb
7 changed files with 345 additions and 156 deletions

View File

@@ -1,5 +1,22 @@
# SceneViewport Overlay/Gizmo Rework Checkpoint # SceneViewport Overlay/Gizmo Rework Checkpoint
## Update 2026-04-03 Phase 5A
### Interaction Resolver Completed
- Added `SceneViewportInteractionResolver.{h,cpp}` as the formal viewport-side interaction arbitration module.
- `SceneViewPanel` no longer owns overlay handle priority rules or HUD/world interaction winner selection.
- Overlay handle hit testing and HUD hit testing are now composed behind one resolver entry point.
- The panel now consumes a resolved interaction result instead of stitching together multiple hit systems inline.
### Verification
- `cmake --build build --config Debug --target editor_tests -- /p:BuildProjectReferences=false`
- `build/tests/Editor/Debug/editor_tests.exe --gtest_filter=SceneViewportInteractionResolverTest.*:SceneViewportOverlayRenderer_Test.*:SceneViewportOverlayProviderRegistryTest.*:ViewportRenderFlowUtilsTest.*`
- `cmake --build build --config Debug --target XCEditor`
All commands completed successfully in `Debug`.
## Update 2026-04-03 ## Update 2026-04-03
### Phase 4 Completed ### Phase 4 Completed

View File

@@ -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/SceneViewportInteractionResolver.cpp
src/Viewport/SceneViewportOrientationGizmo.cpp src/Viewport/SceneViewportOrientationGizmo.cpp
src/Viewport/SceneViewportOverlayBuilder.cpp src/Viewport/SceneViewportOverlayBuilder.cpp
src/Viewport/SceneViewportOverlayProviders.cpp src/Viewport/SceneViewportOverlayProviders.cpp

View File

@@ -0,0 +1,167 @@
#include "SceneViewportInteractionResolver.h"
namespace XCEngine {
namespace Editor {
namespace {
struct SceneViewportInteractionCandidate {
SceneViewportInteractionResult interaction = {};
int priority = 0;
int secondaryPriority = 0;
float distanceSq = Math::FLOAT_MAX;
float depth = Math::FLOAT_MAX;
bool HasHit() const {
return interaction.HasHit();
}
};
bool IsBetterSceneViewportInteractionCandidate(
const SceneViewportInteractionCandidate& candidate,
const SceneViewportInteractionCandidate& current) {
constexpr float kMetricEpsilon = 0.001f;
if (!candidate.HasHit()) {
return false;
}
if (!current.HasHit()) {
return true;
}
if (candidate.priority != current.priority) {
return candidate.priority > current.priority;
}
if (candidate.distanceSq + kMetricEpsilon < current.distanceSq) {
return true;
}
if (current.distanceSq + kMetricEpsilon < candidate.distanceSq) {
return false;
}
if (candidate.depth + kMetricEpsilon < current.depth) {
return true;
}
if (current.depth + kMetricEpsilon < candidate.depth) {
return false;
}
return candidate.secondaryPriority > current.secondaryPriority;
}
void AccumulateSceneViewportInteractionCandidate(
const SceneViewportInteractionCandidate& candidate,
SceneViewportInteractionCandidate& bestCandidate) {
if (IsBetterSceneViewportInteractionCandidate(candidate, bestCandidate)) {
bestCandidate = candidate;
}
}
SceneViewportInteractionCandidate BuildHudOverlayInteractionCandidate(
const SceneViewportHudOverlayHitResult& hitResult) {
SceneViewportInteractionCandidate candidate = {};
switch (hitResult.kind) {
case SceneViewportHudOverlayHitKind::OrientationAxis:
if (hitResult.orientationAxis == SceneViewportOrientationAxis::None) {
return candidate;
}
candidate.interaction.kind = SceneViewportInteractionKind::OrientationGizmo;
candidate.interaction.orientationAxis = hitResult.orientationAxis;
candidate.priority = 200;
candidate.distanceSq = 0.0f;
candidate.depth = 0.0f;
return candidate;
case SceneViewportHudOverlayHitKind::None:
default:
return candidate;
}
}
SceneViewportInteractionCandidate BuildOverlayHandleInteractionCandidate(
const SceneViewportOverlayHandleHitResult& hitResult) {
SceneViewportInteractionCandidate candidate = {};
if (!hitResult.HasHit()) {
return candidate;
}
candidate.priority = hitResult.priority;
candidate.distanceSq = hitResult.distanceSq;
candidate.depth = hitResult.depth;
candidate.interaction.entityId = hitResult.entityId;
switch (hitResult.kind) {
case SceneViewportOverlayHandleKind::SceneIcon:
candidate.interaction.kind = SceneViewportInteractionKind::SceneIcon;
return candidate;
case SceneViewportOverlayHandleKind::MoveAxis:
candidate.interaction.kind = SceneViewportInteractionKind::MoveGizmo;
candidate.interaction.moveAxis = static_cast<SceneViewportGizmoAxis>(hitResult.handleId);
return candidate;
case SceneViewportOverlayHandleKind::MovePlane:
candidate.interaction.kind = SceneViewportInteractionKind::MoveGizmo;
candidate.interaction.movePlane = static_cast<SceneViewportGizmoPlane>(hitResult.handleId);
return candidate;
case SceneViewportOverlayHandleKind::RotateAxis:
candidate.interaction.kind = SceneViewportInteractionKind::RotateGizmo;
candidate.interaction.rotateAxis = static_cast<SceneViewportRotateGizmoAxis>(hitResult.handleId);
return candidate;
case SceneViewportOverlayHandleKind::ScaleAxis:
case SceneViewportOverlayHandleKind::ScaleUniform:
candidate.interaction.kind = SceneViewportInteractionKind::ScaleGizmo;
candidate.interaction.scaleHandle = static_cast<SceneViewportScaleGizmoHandle>(hitResult.handleId);
return candidate;
case SceneViewportOverlayHandleKind::None:
default:
return candidate;
}
}
SceneViewportOverlayHandleHitResult HitTestSceneViewportOverlayInteractionHandles(
const SceneViewportInteractionResolveRequest& request) {
if (request.overlayFrameData == nullptr) {
return {};
}
return HitTestSceneViewportOverlayHandles(
*request.overlayFrameData,
request.viewportSize,
request.localMousePosition);
}
SceneViewportHudOverlayHitResult HitTestSceneViewportHudInteraction(
const SceneViewportInteractionResolveRequest& request) {
if (request.hudOverlay == nullptr) {
return {};
}
return HitTestSceneViewportHudOverlay(
*request.hudOverlay,
request.viewportMin,
request.viewportMax,
request.absoluteMousePosition);
}
} // namespace
SceneViewportInteractionResult ResolveSceneViewportInteraction(
const SceneViewportOverlayHandleHitResult& overlayHandleHit,
const SceneViewportHudOverlayHitResult& hudOverlayHit) {
SceneViewportInteractionCandidate bestCandidate = {};
AccumulateSceneViewportInteractionCandidate(
BuildOverlayHandleInteractionCandidate(overlayHandleHit),
bestCandidate);
AccumulateSceneViewportInteractionCandidate(
BuildHudOverlayInteractionCandidate(hudOverlayHit),
bestCandidate);
return bestCandidate.interaction;
}
SceneViewportInteractionResult ResolveSceneViewportInteraction(
const SceneViewportInteractionResolveRequest& request) {
return ResolveSceneViewportInteraction(
HitTestSceneViewportOverlayInteractionHandles(request),
HitTestSceneViewportHudInteraction(request));
}
} // namespace Editor
} // namespace XCEngine

View File

@@ -0,0 +1,57 @@
#pragma once
#include "SceneViewportHudOverlay.h"
#include "SceneViewportMoveGizmo.h"
#include "SceneViewportOverlayHitTester.h"
#include "SceneViewportRotateGizmo.h"
#include "SceneViewportScaleGizmo.h"
#include <imgui.h>
#include <cstdint>
namespace XCEngine {
namespace Editor {
enum class SceneViewportInteractionKind : uint8_t {
None = 0,
MoveGizmo,
RotateGizmo,
ScaleGizmo,
OrientationGizmo,
SceneIcon
};
struct SceneViewportInteractionResult {
SceneViewportInteractionKind kind = SceneViewportInteractionKind::None;
uint64_t entityId = 0;
SceneViewportGizmoAxis moveAxis = SceneViewportGizmoAxis::None;
SceneViewportGizmoPlane movePlane = SceneViewportGizmoPlane::None;
SceneViewportRotateGizmoAxis rotateAxis = SceneViewportRotateGizmoAxis::None;
SceneViewportScaleGizmoHandle scaleHandle = SceneViewportScaleGizmoHandle::None;
SceneViewportOrientationAxis orientationAxis = SceneViewportOrientationAxis::None;
bool HasHit() const {
return kind != SceneViewportInteractionKind::None;
}
};
struct SceneViewportInteractionResolveRequest {
const SceneViewportOverlayFrameData* overlayFrameData = nullptr;
Math::Vector2 viewportSize = Math::Vector2::Zero();
Math::Vector2 localMousePosition = Math::Vector2::Zero();
const SceneViewportHudOverlayData* hudOverlay = nullptr;
ImVec2 viewportMin = ImVec2(0.0f, 0.0f);
ImVec2 viewportMax = ImVec2(0.0f, 0.0f);
ImVec2 absoluteMousePosition = ImVec2(0.0f, 0.0f);
};
SceneViewportInteractionResult ResolveSceneViewportInteraction(
const SceneViewportOverlayHandleHitResult& overlayHandleHit,
const SceneViewportHudOverlayHitResult& hudOverlayHit);
SceneViewportInteractionResult ResolveSceneViewportInteraction(
const SceneViewportInteractionResolveRequest& request);
} // namespace Editor
} // namespace XCEngine

View File

@@ -5,8 +5,8 @@
#include "SceneViewPanel.h" #include "SceneViewPanel.h"
#include "Viewport/SceneViewportEditorOverlayData.h" #include "Viewport/SceneViewportEditorOverlayData.h"
#include "Viewport/SceneViewportHudOverlay.h" #include "Viewport/SceneViewportHudOverlay.h"
#include "Viewport/SceneViewportInteractionResolver.h"
#include "Viewport/SceneViewportOverlayHandleBuilder.h" #include "Viewport/SceneViewportOverlayHandleBuilder.h"
#include "Viewport/SceneViewportOverlayHitTester.h"
#include "Viewport/SceneViewportMath.h" #include "Viewport/SceneViewportMath.h"
#include "Viewport/SceneViewportTransformGizmoFrameBuilder.h" #include "Viewport/SceneViewportTransformGizmoFrameBuilder.h"
#include "ViewportPanelContent.h" #include "ViewportPanelContent.h"
@@ -30,33 +30,6 @@ struct SceneViewportToolOverlayResult {
SceneViewportToolMode clickedTool = SceneViewportToolMode::Move; SceneViewportToolMode clickedTool = SceneViewportToolMode::Move;
}; };
enum class SceneViewportInteractionKind : uint8_t {
None = 0,
MoveGizmo,
RotateGizmo,
ScaleGizmo,
OrientationGizmo,
SceneIcon
};
struct SceneViewportInteractionCandidate {
SceneViewportInteractionKind kind = SceneViewportInteractionKind::None;
int priority = 0;
int secondaryPriority = 0;
float distanceSq = Math::FLOAT_MAX;
float depth = Math::FLOAT_MAX;
uint64_t entityId = 0;
SceneViewportGizmoAxis moveAxis = SceneViewportGizmoAxis::None;
SceneViewportGizmoPlane movePlane = SceneViewportGizmoPlane::None;
SceneViewportRotateGizmoAxis rotateAxis = SceneViewportRotateGizmoAxis::None;
SceneViewportScaleGizmoHandle scaleHandle = SceneViewportScaleGizmoHandle::None;
SceneViewportOrientationAxis orientationAxis = SceneViewportOrientationAxis::None;
bool HasHit() const {
return kind != SceneViewportInteractionKind::None;
}
};
const char* GetSceneViewportPivotModeLabel(SceneViewportPivotMode mode) { const char* GetSceneViewportPivotModeLabel(SceneViewportPivotMode mode) {
return mode == SceneViewportPivotMode::Pivot ? "Pivot" : "Center"; return mode == SceneViewportPivotMode::Pivot ? "Pivot" : "Center";
} }
@@ -81,114 +54,6 @@ SceneViewportActiveGizmoKind ToActiveGizmoKind(SceneViewportInteractionKind kind
} }
} }
bool IsBetterSceneViewportInteractionCandidate(
const SceneViewportInteractionCandidate& candidate,
const SceneViewportInteractionCandidate& current) {
constexpr float kMetricEpsilon = 0.001f;
if (!candidate.HasHit()) {
return false;
}
if (!current.HasHit()) {
return true;
}
if (candidate.priority != current.priority) {
return candidate.priority > current.priority;
}
if (candidate.distanceSq + kMetricEpsilon < current.distanceSq) {
return true;
}
if (current.distanceSq + kMetricEpsilon < candidate.distanceSq) {
return false;
}
if (candidate.depth + kMetricEpsilon < current.depth) {
return true;
}
if (current.depth + kMetricEpsilon < candidate.depth) {
return false;
}
return candidate.secondaryPriority > current.secondaryPriority;
}
void AccumulateSceneViewportInteractionCandidate(
const SceneViewportInteractionCandidate& candidate,
SceneViewportInteractionCandidate& bestCandidate) {
if (IsBetterSceneViewportInteractionCandidate(candidate, bestCandidate)) {
bestCandidate = candidate;
}
}
SceneViewportInteractionCandidate BuildOrientationGizmoInteractionCandidate(
SceneViewportOrientationAxis axis) {
SceneViewportInteractionCandidate candidate = {};
if (axis == SceneViewportOrientationAxis::None) {
return candidate;
}
candidate.kind = SceneViewportInteractionKind::OrientationGizmo;
candidate.priority = 200;
candidate.distanceSq = 0.0f;
candidate.depth = 0.0f;
candidate.orientationAxis = axis;
return candidate;
}
SceneViewportInteractionCandidate BuildHudOverlayInteractionCandidate(
const SceneViewportHudOverlayHitResult& hitResult) {
switch (hitResult.kind) {
case SceneViewportHudOverlayHitKind::OrientationAxis:
return BuildOrientationGizmoInteractionCandidate(hitResult.orientationAxis);
case SceneViewportHudOverlayHitKind::None:
default:
return {};
}
}
SceneViewportInteractionCandidate BuildOverlayHandleInteractionCandidate(
const SceneViewportOverlayHandleHitResult& hitResult) {
SceneViewportInteractionCandidate candidate = {};
if (!hitResult.HasHit()) {
return candidate;
}
candidate.priority = hitResult.priority;
candidate.distanceSq = hitResult.distanceSq;
candidate.depth = hitResult.depth;
candidate.entityId = hitResult.entityId;
switch (hitResult.kind) {
case SceneViewportOverlayHandleKind::SceneIcon:
candidate.kind = SceneViewportInteractionKind::SceneIcon;
return candidate;
case SceneViewportOverlayHandleKind::MoveAxis:
candidate.kind = SceneViewportInteractionKind::MoveGizmo;
candidate.moveAxis = static_cast<SceneViewportGizmoAxis>(hitResult.handleId);
return candidate;
case SceneViewportOverlayHandleKind::MovePlane:
candidate.kind = SceneViewportInteractionKind::MoveGizmo;
candidate.movePlane = static_cast<SceneViewportGizmoPlane>(hitResult.handleId);
return candidate;
case SceneViewportOverlayHandleKind::RotateAxis:
candidate.kind = SceneViewportInteractionKind::RotateGizmo;
candidate.rotateAxis = static_cast<SceneViewportRotateGizmoAxis>(hitResult.handleId);
return candidate;
case SceneViewportOverlayHandleKind::ScaleAxis:
case SceneViewportOverlayHandleKind::ScaleUniform:
candidate.kind = SceneViewportInteractionKind::ScaleGizmo;
candidate.scaleHandle = static_cast<SceneViewportScaleGizmoHandle>(hitResult.handleId);
return candidate;
case SceneViewportOverlayHandleKind::None:
default:
return SceneViewportInteractionCandidate{};
}
return candidate;
}
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;
@@ -539,13 +404,6 @@ void SceneViewPanel::Render() {
hasInteractiveViewport hasInteractiveViewport
? viewportHostService->GetSceneViewEditorOverlayFrameData(*m_context) ? viewportHostService->GetSceneViewEditorOverlayFrameData(*m_context)
: emptySceneOverlayFrameData; : emptySceneOverlayFrameData;
const SceneViewportOverlayHandleHitResult overlayHandleHit =
hasInteractiveViewport
? HitTestSceneViewportOverlayHandles(
interactionOverlayFrameData,
Math::Vector2(content.availableSize.x, content.availableSize.y),
localMousePosition)
: SceneViewportOverlayHandleHitResult{};
const bool moveGizmoActive = showingMoveGizmo && m_moveGizmo.IsActive(); const bool moveGizmoActive = showingMoveGizmo && m_moveGizmo.IsActive();
const bool rotateGizmoActive = showingRotateGizmo && m_rotateGizmo.IsActive(); const bool rotateGizmoActive = showingRotateGizmo && m_rotateGizmo.IsActive();
const bool scaleGizmoActive = showingScaleGizmo && m_scaleGizmo.IsActive(); const bool scaleGizmoActive = showingScaleGizmo && m_scaleGizmo.IsActive();
@@ -561,7 +419,7 @@ void SceneViewPanel::Render() {
const bool gizmoActive = activeGizmoKind != SceneViewportActiveGizmoKind::None; const bool gizmoActive = activeGizmoKind != SceneViewportActiveGizmoKind::None;
const SceneViewportHudOverlayData interactionHudOverlay = const SceneViewportHudOverlayData interactionHudOverlay =
BuildSceneViewportHudOverlayData(overlay); BuildSceneViewportHudOverlayData(overlay);
SceneViewportInteractionCandidate hoveredInteraction = {}; SceneViewportInteractionResult hoveredInteraction = {};
const bool canResolveViewportInteraction = const bool canResolveViewportInteraction =
hasInteractiveViewport && hasInteractiveViewport &&
viewportContentHovered && viewportContentHovered &&
@@ -571,18 +429,15 @@ void SceneViewPanel::Render() {
!toolOverlay.hovered && !toolOverlay.hovered &&
!gizmoActive; !gizmoActive;
if (canResolveViewportInteraction) { if (canResolveViewportInteraction) {
AccumulateSceneViewportInteractionCandidate( SceneViewportInteractionResolveRequest interactionRequest = {};
BuildOverlayHandleInteractionCandidate(overlayHandleHit), interactionRequest.overlayFrameData = &interactionOverlayFrameData;
hoveredInteraction); interactionRequest.viewportSize = viewportSize;
interactionRequest.localMousePosition = localMousePosition;
AccumulateSceneViewportInteractionCandidate( interactionRequest.hudOverlay = &interactionHudOverlay;
BuildHudOverlayInteractionCandidate( interactionRequest.viewportMin = content.itemMin;
HitTestSceneViewportHudOverlay( interactionRequest.viewportMax = content.itemMax;
interactionHudOverlay, interactionRequest.absoluteMousePosition = io.MousePos;
content.itemMin, hoveredInteraction = ResolveSceneViewportInteraction(interactionRequest);
content.itemMax,
io.MousePos)),
hoveredInteraction);
} }
if (!gizmoActive) { if (!gizmoActive) {

View File

@@ -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_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
test_scene_viewport_overlay_providers.cpp test_scene_viewport_overlay_providers.cpp
@@ -38,6 +39,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/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
${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportOverlayProviders.cpp ${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportOverlayProviders.cpp

View File

@@ -0,0 +1,90 @@
#include <gtest/gtest.h>
#include "Viewport/SceneViewportInteractionResolver.h"
using XCEngine::Editor::ResolveSceneViewportInteraction;
using XCEngine::Editor::SceneViewportGizmoAxis;
using XCEngine::Editor::SceneViewportHudOverlayHitKind;
using XCEngine::Editor::SceneViewportHudOverlayHitResult;
using XCEngine::Editor::SceneViewportInteractionKind;
using XCEngine::Editor::SceneViewportInteractionResolveRequest;
using XCEngine::Editor::SceneViewportOrientationAxis;
using XCEngine::Editor::SceneViewportOverlayFrameData;
using XCEngine::Editor::SceneViewportOverlayHandleHitResult;
using XCEngine::Editor::SceneViewportOverlayHandleKind;
using XCEngine::Editor::SceneViewportOverlayHandleRecord;
using XCEngine::Editor::SceneViewportOverlayHandleShape;
using XCEngine::Math::Vector2;
TEST(SceneViewportInteractionResolverTest, ReturnsNoHitWhenBothSourcesAreEmpty) {
const auto interaction = ResolveSceneViewportInteraction(
SceneViewportOverlayHandleHitResult{},
SceneViewportHudOverlayHitResult{});
EXPECT_FALSE(interaction.HasHit());
EXPECT_EQ(interaction.kind, SceneViewportInteractionKind::None);
}
TEST(SceneViewportInteractionResolverTest, PrefersTransformHandleOverHudOrientationByPriority) {
SceneViewportOverlayHandleHitResult overlayHandleHit = {};
overlayHandleHit.kind = SceneViewportOverlayHandleKind::MoveAxis;
overlayHandleHit.handleId = static_cast<uint64_t>(SceneViewportGizmoAxis::X);
overlayHandleHit.priority = 322;
overlayHandleHit.distanceSq = 4.0f;
overlayHandleHit.depth = 3.0f;
SceneViewportHudOverlayHitResult hudHit = {};
hudHit.kind = SceneViewportHudOverlayHitKind::OrientationAxis;
hudHit.orientationAxis = SceneViewportOrientationAxis::PositiveX;
const auto interaction = ResolveSceneViewportInteraction(overlayHandleHit, hudHit);
ASSERT_TRUE(interaction.HasHit());
EXPECT_EQ(interaction.kind, SceneViewportInteractionKind::MoveGizmo);
EXPECT_EQ(interaction.moveAxis, SceneViewportGizmoAxis::X);
EXPECT_EQ(interaction.orientationAxis, SceneViewportOrientationAxis::None);
}
TEST(SceneViewportInteractionResolverTest, PrefersHudOrientationOverSceneIconByPriority) {
SceneViewportOverlayHandleHitResult overlayHandleHit = {};
overlayHandleHit.kind = SceneViewportOverlayHandleKind::SceneIcon;
overlayHandleHit.entityId = 99;
overlayHandleHit.priority = 100;
overlayHandleHit.distanceSq = 0.0f;
overlayHandleHit.depth = 0.0f;
SceneViewportHudOverlayHitResult hudHit = {};
hudHit.kind = SceneViewportHudOverlayHitKind::OrientationAxis;
hudHit.orientationAxis = SceneViewportOrientationAxis::PositiveY;
const auto interaction = ResolveSceneViewportInteraction(overlayHandleHit, hudHit);
ASSERT_TRUE(interaction.HasHit());
EXPECT_EQ(interaction.kind, SceneViewportInteractionKind::OrientationGizmo);
EXPECT_EQ(interaction.orientationAxis, SceneViewportOrientationAxis::PositiveY);
EXPECT_EQ(interaction.entityId, 0u);
}
TEST(SceneViewportInteractionResolverTest, RequestPathDelegatesToOverlayHandleHitTesting) {
SceneViewportOverlayFrameData frameData = {};
frameData.overlay.valid = true;
SceneViewportOverlayHandleRecord& record = frameData.handleRecords.emplace_back();
record.kind = SceneViewportOverlayHandleKind::SceneIcon;
record.entityId = 77;
record.shape = SceneViewportOverlayHandleShape::ScreenRect;
record.priority = 100;
record.screenCenter = Vector2(80.0f, 60.0f);
record.screenHalfSize = Vector2(12.0f, 10.0f);
SceneViewportInteractionResolveRequest request = {};
request.overlayFrameData = &frameData;
request.viewportSize = Vector2(200.0f, 150.0f);
request.localMousePosition = Vector2(80.0f, 60.0f);
const auto interaction = ResolveSceneViewportInteraction(request);
ASSERT_TRUE(interaction.HasHit());
EXPECT_EQ(interaction.kind, SceneViewportInteractionKind::SceneIcon);
EXPECT_EQ(interaction.entityId, 77u);
}