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

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

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 "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<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) {
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) {