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 6336911b..c48e7085 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,22 @@ # 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 ### Phase 4 Completed diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 02e6962b..ee94374f 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -83,6 +83,7 @@ add_executable(${PROJECT_NAME} WIN32 src/Viewport/SceneViewportRotateGizmo.cpp src/Viewport/SceneViewportScaleGizmo.cpp src/Viewport/SceneViewportHudOverlay.cpp + src/Viewport/SceneViewportInteractionResolver.cpp src/Viewport/SceneViewportOrientationGizmo.cpp src/Viewport/SceneViewportOverlayBuilder.cpp src/Viewport/SceneViewportOverlayProviders.cpp diff --git a/editor/src/Viewport/SceneViewportInteractionResolver.cpp b/editor/src/Viewport/SceneViewportInteractionResolver.cpp new file mode 100644 index 00000000..5eb234de --- /dev/null +++ b/editor/src/Viewport/SceneViewportInteractionResolver.cpp @@ -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(hitResult.handleId); + return candidate; + case SceneViewportOverlayHandleKind::MovePlane: + candidate.interaction.kind = SceneViewportInteractionKind::MoveGizmo; + candidate.interaction.movePlane = static_cast(hitResult.handleId); + return candidate; + case SceneViewportOverlayHandleKind::RotateAxis: + candidate.interaction.kind = SceneViewportInteractionKind::RotateGizmo; + candidate.interaction.rotateAxis = static_cast(hitResult.handleId); + return candidate; + case SceneViewportOverlayHandleKind::ScaleAxis: + case SceneViewportOverlayHandleKind::ScaleUniform: + candidate.interaction.kind = SceneViewportInteractionKind::ScaleGizmo; + candidate.interaction.scaleHandle = static_cast(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 diff --git a/editor/src/Viewport/SceneViewportInteractionResolver.h b/editor/src/Viewport/SceneViewportInteractionResolver.h new file mode 100644 index 00000000..a08080c3 --- /dev/null +++ b/editor/src/Viewport/SceneViewportInteractionResolver.h @@ -0,0 +1,57 @@ +#pragma once + +#include "SceneViewportHudOverlay.h" +#include "SceneViewportMoveGizmo.h" +#include "SceneViewportOverlayHitTester.h" +#include "SceneViewportRotateGizmo.h" +#include "SceneViewportScaleGizmo.h" + +#include + +#include + +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 diff --git a/editor/src/panels/SceneViewPanel.cpp b/editor/src/panels/SceneViewPanel.cpp index 8d8803cd..6d9162e1 100644 --- a/editor/src/panels/SceneViewPanel.cpp +++ b/editor/src/panels/SceneViewPanel.cpp @@ -5,8 +5,8 @@ #include "SceneViewPanel.h" #include "Viewport/SceneViewportEditorOverlayData.h" #include "Viewport/SceneViewportHudOverlay.h" +#include "Viewport/SceneViewportInteractionResolver.h" #include "Viewport/SceneViewportOverlayHandleBuilder.h" -#include "Viewport/SceneViewportOverlayHitTester.h" #include "Viewport/SceneViewportMath.h" #include "Viewport/SceneViewportTransformGizmoFrameBuilder.h" #include "ViewportPanelContent.h" @@ -30,33 +30,6 @@ struct SceneViewportToolOverlayResult { 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) { 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(hitResult.handleId); - return candidate; - case SceneViewportOverlayHandleKind::MovePlane: - candidate.kind = SceneViewportInteractionKind::MoveGizmo; - candidate.movePlane = static_cast(hitResult.handleId); - return candidate; - case SceneViewportOverlayHandleKind::RotateAxis: - candidate.kind = SceneViewportInteractionKind::RotateGizmo; - candidate.rotateAxis = static_cast(hitResult.handleId); - return candidate; - case SceneViewportOverlayHandleKind::ScaleAxis: - case SceneViewportOverlayHandleKind::ScaleUniform: - candidate.kind = SceneViewportInteractionKind::ScaleGizmo; - candidate.scaleHandle = static_cast(hitResult.handleId); - return candidate; - case SceneViewportOverlayHandleKind::None: - default: - return SceneViewportInteractionCandidate{}; - } - - return candidate; -} - float GetSceneToolbarToggleWidth(const char* firstLabel, const char* secondLabel) { constexpr float kHorizontalPadding = 10.0f; constexpr float kMinWidth = 68.0f; @@ -539,13 +404,6 @@ void SceneViewPanel::Render() { hasInteractiveViewport ? viewportHostService->GetSceneViewEditorOverlayFrameData(*m_context) : 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 rotateGizmoActive = showingRotateGizmo && m_rotateGizmo.IsActive(); const bool scaleGizmoActive = showingScaleGizmo && m_scaleGizmo.IsActive(); @@ -561,7 +419,7 @@ void SceneViewPanel::Render() { const bool gizmoActive = activeGizmoKind != SceneViewportActiveGizmoKind::None; const SceneViewportHudOverlayData interactionHudOverlay = BuildSceneViewportHudOverlayData(overlay); - SceneViewportInteractionCandidate hoveredInteraction = {}; + SceneViewportInteractionResult hoveredInteraction = {}; const bool canResolveViewportInteraction = hasInteractiveViewport && viewportContentHovered && @@ -571,18 +429,15 @@ void SceneViewPanel::Render() { !toolOverlay.hovered && !gizmoActive; if (canResolveViewportInteraction) { - AccumulateSceneViewportInteractionCandidate( - BuildOverlayHandleInteractionCandidate(overlayHandleHit), - hoveredInteraction); - - AccumulateSceneViewportInteractionCandidate( - BuildHudOverlayInteractionCandidate( - HitTestSceneViewportHudOverlay( - interactionHudOverlay, - content.itemMin, - content.itemMax, - io.MousePos)), - hoveredInteraction); + 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); } if (!gizmoActive) { diff --git a/tests/editor/CMakeLists.txt b/tests/editor/CMakeLists.txt index cda00c08..1b058b39 100644 --- a/tests/editor/CMakeLists.txt +++ b/tests/editor/CMakeLists.txt @@ -11,6 +11,7 @@ set(EDITOR_TEST_SOURCES test_scene_viewport_rotate_gizmo.cpp test_scene_viewport_scale_gizmo.cpp test_scene_viewport_picker.cpp + test_scene_viewport_interaction_resolver.cpp test_scene_viewport_shader_paths.cpp test_scene_viewport_overlay_renderer.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/SceneViewportScaleGizmo.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/SceneViewportOverlayBuilder.cpp ${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportOverlayProviders.cpp diff --git a/tests/editor/test_scene_viewport_interaction_resolver.cpp b/tests/editor/test_scene_viewport_interaction_resolver.cpp new file mode 100644 index 00000000..8ec062c6 --- /dev/null +++ b/tests/editor/test_scene_viewport_interaction_resolver.cpp @@ -0,0 +1,90 @@ +#include + +#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(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); +}