Add Unity-style scene rotate gizmo
This commit is contained in:
@@ -24,6 +24,18 @@ ImU32 ToImGuiColor(const Math::Color& color) {
|
||||
toChannel(color.a));
|
||||
}
|
||||
|
||||
Math::Color WithAlpha(const Math::Color& color, float alpha) {
|
||||
return Math::Color(color.r, color.g, color.b, alpha);
|
||||
}
|
||||
|
||||
Math::Color LerpColor(const Math::Color& a, const Math::Color& b, float t) {
|
||||
return Math::Color(
|
||||
a.r + (b.r - a.r) * t,
|
||||
a.g + (b.g - a.g) * t,
|
||||
a.b + (b.b - a.b) * t,
|
||||
a.a + (b.a - a.a) * t);
|
||||
}
|
||||
|
||||
ImVec2 NormalizeImVec2(const ImVec2& value, const ImVec2& fallback = ImVec2(1.0f, 0.0f)) {
|
||||
const float lengthSq = value.x * value.x + value.y * value.y;
|
||||
if (lengthSq <= 1e-6f) {
|
||||
@@ -87,6 +99,42 @@ void DrawSceneMoveGizmoAxis(
|
||||
drawList->AddConvexPolyFilled(triangle, 3, color);
|
||||
}
|
||||
|
||||
void DrawSceneRotateGizmoHandle(
|
||||
ImDrawList* drawList,
|
||||
const ImVec2& viewportMin,
|
||||
const SceneViewportRotateGizmoHandleDrawData& handle,
|
||||
bool frontPass) {
|
||||
if (drawList == nullptr || !handle.visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool isViewHandle = handle.axis == SceneViewportRotateGizmoAxis::View;
|
||||
if (isViewHandle && !frontPass) {
|
||||
return;
|
||||
}
|
||||
|
||||
const float thickness = handle.active ? 3.6f : (handle.hovered ? 3.0f : 2.1f);
|
||||
for (const SceneViewportRotateGizmoSegmentDrawData& segment : handle.segments) {
|
||||
if (!segment.visible || (!isViewHandle && segment.frontFacing != frontPass)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Math::Color drawColor = handle.color;
|
||||
if (!isViewHandle && !frontPass) {
|
||||
drawColor = LerpColor(handle.color, Math::Color(0.72f, 0.72f, 0.72f, 1.0f), 0.78f);
|
||||
drawColor = WithAlpha(drawColor, handle.active ? 0.55f : 0.38f);
|
||||
} else if (isViewHandle) {
|
||||
drawColor = WithAlpha(drawColor, handle.active ? 0.95f : (handle.hovered ? 0.88f : 0.78f));
|
||||
}
|
||||
|
||||
drawList->AddLine(
|
||||
ImVec2(viewportMin.x + segment.start.x, viewportMin.y + segment.start.y),
|
||||
ImVec2(viewportMin.x + segment.end.x, viewportMin.y + segment.end.y),
|
||||
ToImGuiColor(drawColor),
|
||||
thickness);
|
||||
}
|
||||
}
|
||||
|
||||
void DrawSceneMoveGizmo(
|
||||
ImDrawList* drawList,
|
||||
const ImVec2& viewportMin,
|
||||
@@ -108,6 +156,33 @@ void DrawSceneMoveGizmo(
|
||||
drawList->AddCircle(pivot, moveGizmo.pivotRadius + 1.0f, IM_COL32(255, 255, 255, 48), 20, 1.0f);
|
||||
}
|
||||
|
||||
void DrawSceneRotateGizmo(
|
||||
ImDrawList* drawList,
|
||||
const ImVec2& viewportMin,
|
||||
const SceneViewportRotateGizmoDrawData& rotateGizmo) {
|
||||
if (drawList == nullptr || !rotateGizmo.visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const SceneViewportRotateGizmoHandleDrawData& handle : rotateGizmo.handles) {
|
||||
if (handle.axis == SceneViewportRotateGizmoAxis::View) {
|
||||
DrawSceneRotateGizmoHandle(drawList, viewportMin, handle, true);
|
||||
}
|
||||
}
|
||||
|
||||
for (const SceneViewportRotateGizmoHandleDrawData& handle : rotateGizmo.handles) {
|
||||
if (handle.axis != SceneViewportRotateGizmoAxis::View) {
|
||||
DrawSceneRotateGizmoHandle(drawList, viewportMin, handle, false);
|
||||
}
|
||||
}
|
||||
|
||||
for (const SceneViewportRotateGizmoHandleDrawData& handle : rotateGizmo.handles) {
|
||||
if (handle.axis != SceneViewportRotateGizmoAxis::View) {
|
||||
DrawSceneRotateGizmoHandle(drawList, viewportMin, handle, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void DrawSceneViewportOverlay(
|
||||
@@ -116,7 +191,8 @@ void DrawSceneViewportOverlay(
|
||||
const ImVec2& viewportMin,
|
||||
const ImVec2& viewportMax,
|
||||
const ImVec2& viewportSize,
|
||||
const SceneViewportMoveGizmoDrawData* moveGizmo) {
|
||||
const SceneViewportMoveGizmoDrawData* moveGizmo,
|
||||
const SceneViewportRotateGizmoDrawData* rotateGizmo) {
|
||||
if (drawList == nullptr || viewportSize.x <= 1.0f || viewportSize.y <= 1.0f) {
|
||||
return;
|
||||
}
|
||||
@@ -128,6 +204,9 @@ void DrawSceneViewportOverlay(
|
||||
if (moveGizmo != nullptr) {
|
||||
DrawSceneMoveGizmo(drawList, viewportMin, *moveGizmo);
|
||||
}
|
||||
if (rotateGizmo != nullptr) {
|
||||
DrawSceneRotateGizmo(drawList, viewportMin, *rotateGizmo);
|
||||
}
|
||||
drawList->PopClipRect();
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "IViewportHostService.h"
|
||||
#include "SceneViewportMoveGizmo.h"
|
||||
#include "SceneViewportRotateGizmo.h"
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
@@ -14,7 +15,8 @@ void DrawSceneViewportOverlay(
|
||||
const ImVec2& viewportMin,
|
||||
const ImVec2& viewportMax,
|
||||
const ImVec2& viewportSize,
|
||||
const SceneViewportMoveGizmoDrawData* moveGizmo = nullptr);
|
||||
const SceneViewportMoveGizmoDrawData* moveGizmo = nullptr,
|
||||
const SceneViewportRotateGizmoDrawData* rotateGizmo = nullptr);
|
||||
|
||||
} // namespace Editor
|
||||
} // namespace XCEngine
|
||||
|
||||
522
editor/src/Viewport/SceneViewportRotateGizmo.cpp
Normal file
522
editor/src/Viewport/SceneViewportRotateGizmo.cpp
Normal file
@@ -0,0 +1,522 @@
|
||||
#include "SceneViewportRotateGizmo.h"
|
||||
|
||||
#include "Core/IUndoManager.h"
|
||||
#include "SceneViewportMath.h"
|
||||
#include "SceneViewportPicker.h"
|
||||
|
||||
#include <XCEngine/Components/GameObject.h>
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Editor {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr float kRotateGizmoAxisRadiusPixels = 84.0f;
|
||||
constexpr float kRotateGizmoViewRadiusPixels = 92.0f;
|
||||
constexpr float kRotateGizmoHoverThresholdPixels = 9.0f;
|
||||
|
||||
Math::Vector3 NormalizeVector3(const Math::Vector3& value, const Math::Vector3& fallback) {
|
||||
return value.SqrMagnitude() <= Math::EPSILON ? fallback : value.Normalized();
|
||||
}
|
||||
|
||||
bool IsMouseInsideViewport(const SceneViewportRotateGizmoContext& context) {
|
||||
return context.mousePosition.x >= 0.0f &&
|
||||
context.mousePosition.y >= 0.0f &&
|
||||
context.mousePosition.x <= context.viewportSize.x &&
|
||||
context.mousePosition.y <= context.viewportSize.y;
|
||||
}
|
||||
|
||||
Math::Color GetRotateAxisBaseColor(SceneViewportRotateGizmoAxis axis) {
|
||||
switch (axis) {
|
||||
case SceneViewportRotateGizmoAxis::X:
|
||||
return Math::Color(0.91f, 0.09f, 0.05f, 1.0f);
|
||||
case SceneViewportRotateGizmoAxis::Y:
|
||||
return Math::Color(0.45f, 1.0f, 0.12f, 1.0f);
|
||||
case SceneViewportRotateGizmoAxis::Z:
|
||||
return Math::Color(0.11f, 0.29f, 1.0f, 1.0f);
|
||||
case SceneViewportRotateGizmoAxis::View:
|
||||
return Math::Color(0.78f, 0.78f, 0.78f, 0.9f);
|
||||
case SceneViewportRotateGizmoAxis::None:
|
||||
default:
|
||||
return Math::Color::White();
|
||||
}
|
||||
}
|
||||
|
||||
Math::Vector3 GetRotateAxisVector(
|
||||
SceneViewportRotateGizmoAxis axis,
|
||||
const SceneViewportOverlayData& overlay) {
|
||||
switch (axis) {
|
||||
case SceneViewportRotateGizmoAxis::X:
|
||||
return Math::Vector3::Right();
|
||||
case SceneViewportRotateGizmoAxis::Y:
|
||||
return Math::Vector3::Up();
|
||||
case SceneViewportRotateGizmoAxis::Z:
|
||||
return Math::Vector3::Forward();
|
||||
case SceneViewportRotateGizmoAxis::View:
|
||||
return NormalizeVector3(overlay.cameraForward, Math::Vector3::Forward());
|
||||
case SceneViewportRotateGizmoAxis::None:
|
||||
default:
|
||||
return Math::Vector3::Zero();
|
||||
}
|
||||
}
|
||||
|
||||
bool GetRotateRingBasis(
|
||||
SceneViewportRotateGizmoAxis axis,
|
||||
const SceneViewportOverlayData& overlay,
|
||||
Math::Vector3& outBasisA,
|
||||
Math::Vector3& outBasisB) {
|
||||
switch (axis) {
|
||||
case SceneViewportRotateGizmoAxis::X:
|
||||
outBasisA = Math::Vector3::Up();
|
||||
outBasisB = Math::Vector3::Forward();
|
||||
return true;
|
||||
case SceneViewportRotateGizmoAxis::Y:
|
||||
outBasisA = Math::Vector3::Forward();
|
||||
outBasisB = Math::Vector3::Right();
|
||||
return true;
|
||||
case SceneViewportRotateGizmoAxis::Z:
|
||||
outBasisA = Math::Vector3::Right();
|
||||
outBasisB = Math::Vector3::Up();
|
||||
return true;
|
||||
case SceneViewportRotateGizmoAxis::View:
|
||||
outBasisA = NormalizeVector3(overlay.cameraRight, Math::Vector3::Right());
|
||||
outBasisB = NormalizeVector3(overlay.cameraUp, Math::Vector3::Up());
|
||||
return outBasisA.SqrMagnitude() > Math::EPSILON && outBasisB.SqrMagnitude() > Math::EPSILON;
|
||||
case SceneViewportRotateGizmoAxis::None:
|
||||
default:
|
||||
outBasisA = Math::Vector3::Zero();
|
||||
outBasisB = Math::Vector3::Zero();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
float ComputeWorldUnitsPerPixel(
|
||||
const SceneViewportOverlayData& overlay,
|
||||
const Math::Vector3& worldPoint,
|
||||
float viewportHeight) {
|
||||
if (!overlay.valid || viewportHeight <= 1.0f) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
const Math::Vector3 cameraForward = NormalizeVector3(overlay.cameraForward, Math::Vector3::Forward());
|
||||
const float depth = Math::Vector3::Dot(worldPoint - overlay.cameraPosition, cameraForward);
|
||||
if (depth <= Math::EPSILON) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
return 2.0f * depth * std::tan(overlay.verticalFovDegrees * Math::DEG_TO_RAD * 0.5f) / viewportHeight;
|
||||
}
|
||||
|
||||
float SignedAngleRadiansAroundAxis(
|
||||
const Math::Vector3& from,
|
||||
const Math::Vector3& to,
|
||||
const Math::Vector3& axis) {
|
||||
const Math::Vector3 normalizedFrom = from.Normalized();
|
||||
const Math::Vector3 normalizedTo = to.Normalized();
|
||||
const float dot = std::clamp(
|
||||
Math::Vector3::Dot(normalizedFrom, normalizedTo),
|
||||
-1.0f,
|
||||
1.0f);
|
||||
const float sine = Math::Vector3::Dot(axis.Normalized(), Math::Vector3::Cross(normalizedFrom, normalizedTo));
|
||||
return std::atan2(sine, dot);
|
||||
}
|
||||
|
||||
float NormalizeSignedAngleRadians(float radians) {
|
||||
while (radians > Math::PI) {
|
||||
radians -= Math::PI * 2.0f;
|
||||
}
|
||||
|
||||
while (radians < -Math::PI) {
|
||||
radians += Math::PI * 2.0f;
|
||||
}
|
||||
|
||||
return radians;
|
||||
}
|
||||
|
||||
SceneViewportRotateGizmoAxis GetRotateAxisForIndex(size_t index) {
|
||||
switch (index) {
|
||||
case 0:
|
||||
return SceneViewportRotateGizmoAxis::X;
|
||||
case 1:
|
||||
return SceneViewportRotateGizmoAxis::Y;
|
||||
case 2:
|
||||
return SceneViewportRotateGizmoAxis::Z;
|
||||
case 3:
|
||||
return SceneViewportRotateGizmoAxis::View;
|
||||
default:
|
||||
return SceneViewportRotateGizmoAxis::None;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void SceneViewportRotateGizmo::Update(const SceneViewportRotateGizmoContext& context) {
|
||||
BuildDrawData(context);
|
||||
if (m_activeAxis == SceneViewportRotateGizmoAxis::None && IsMouseInsideViewport(context)) {
|
||||
m_hoveredAxis = HitTestAxis(context.mousePosition);
|
||||
} else if (m_activeAxis == SceneViewportRotateGizmoAxis::None) {
|
||||
m_hoveredAxis = SceneViewportRotateGizmoAxis::None;
|
||||
} else {
|
||||
m_hoveredAxis = m_activeAxis;
|
||||
}
|
||||
|
||||
RefreshHandleState();
|
||||
}
|
||||
|
||||
bool SceneViewportRotateGizmo::TryBeginDrag(const SceneViewportRotateGizmoContext& context, IUndoManager& undoManager) {
|
||||
if (m_activeAxis != SceneViewportRotateGizmoAxis::None ||
|
||||
m_hoveredAxis == SceneViewportRotateGizmoAxis::None ||
|
||||
context.selectedObject == nullptr ||
|
||||
!m_drawData.visible ||
|
||||
undoManager.HasPendingInteractiveChange()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const Math::Vector3 pivotWorldPosition = context.selectedObject->GetTransform()->GetPosition();
|
||||
const Math::Vector3 worldAxis = GetRotateAxisVector(m_hoveredAxis, context.overlay);
|
||||
if (worldAxis.SqrMagnitude() <= Math::EPSILON) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const Math::Plane dragPlane = BuildSceneViewportPlaneFromPointNormal(pivotWorldPosition, worldAxis);
|
||||
Math::Vector3 startDirection = Math::Vector3::Zero();
|
||||
bool useScreenSpaceDrag = true;
|
||||
|
||||
Math::Ray worldRay;
|
||||
if (BuildSceneViewportRay(
|
||||
context.overlay,
|
||||
context.viewportSize,
|
||||
context.mousePosition,
|
||||
worldRay)) {
|
||||
float hitDistance = 0.0f;
|
||||
if (worldRay.Intersects(dragPlane, hitDistance)) {
|
||||
const Math::Vector3 hitPoint = worldRay.GetPoint(hitDistance);
|
||||
startDirection = Math::Vector3::ProjectOnPlane(hitPoint - pivotWorldPosition, worldAxis);
|
||||
if (startDirection.SqrMagnitude() > Math::EPSILON) {
|
||||
useScreenSpaceDrag = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
float startRingAngle = 0.0f;
|
||||
if (useScreenSpaceDrag &&
|
||||
!TryGetClosestRingAngle(m_hoveredAxis, context.mousePosition, false, startRingAngle)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
undoManager.BeginInteractiveChange("Rotate Gizmo");
|
||||
if (!undoManager.HasPendingInteractiveChange()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_activeAxis = m_hoveredAxis;
|
||||
m_activeEntityId = context.selectedObject->GetID();
|
||||
m_activeWorldAxis = worldAxis.Normalized();
|
||||
m_screenSpaceDrag = useScreenSpaceDrag;
|
||||
m_dragPlane = dragPlane;
|
||||
m_dragStartWorldRotation = context.selectedObject->GetTransform()->GetRotation();
|
||||
m_dragStartDirectionWorld = useScreenSpaceDrag ? Math::Vector3::Zero() : startDirection.Normalized();
|
||||
m_dragStartRingAngle = useScreenSpaceDrag ? startRingAngle : 0.0f;
|
||||
RefreshHandleState();
|
||||
return true;
|
||||
}
|
||||
|
||||
void SceneViewportRotateGizmo::UpdateDrag(const SceneViewportRotateGizmoContext& context) {
|
||||
if (m_activeAxis == SceneViewportRotateGizmoAxis::None ||
|
||||
context.selectedObject == nullptr ||
|
||||
context.selectedObject->GetID() != m_activeEntityId) {
|
||||
return;
|
||||
}
|
||||
|
||||
float deltaRadians = 0.0f;
|
||||
if (m_screenSpaceDrag) {
|
||||
float currentRingAngle = 0.0f;
|
||||
if (!TryGetClosestRingAngle(m_activeAxis, context.mousePosition, false, currentRingAngle)) {
|
||||
return;
|
||||
}
|
||||
|
||||
deltaRadians = NormalizeSignedAngleRadians(currentRingAngle - m_dragStartRingAngle);
|
||||
} else {
|
||||
Math::Ray worldRay;
|
||||
if (!BuildSceneViewportRay(
|
||||
context.overlay,
|
||||
context.viewportSize,
|
||||
context.mousePosition,
|
||||
worldRay)) {
|
||||
return;
|
||||
}
|
||||
|
||||
float hitDistance = 0.0f;
|
||||
if (!worldRay.Intersects(m_dragPlane, hitDistance)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Math::Vector3 pivotWorldPosition = context.selectedObject->GetTransform()->GetPosition();
|
||||
const Math::Vector3 hitPoint = worldRay.GetPoint(hitDistance);
|
||||
const Math::Vector3 currentDirection = Math::Vector3::ProjectOnPlane(hitPoint - pivotWorldPosition, m_activeWorldAxis);
|
||||
if (currentDirection.SqrMagnitude() <= Math::EPSILON) {
|
||||
return;
|
||||
}
|
||||
|
||||
deltaRadians = SignedAngleRadiansAroundAxis(
|
||||
m_dragStartDirectionWorld,
|
||||
currentDirection,
|
||||
m_activeWorldAxis);
|
||||
}
|
||||
|
||||
const Math::Quaternion deltaRotation = Math::Quaternion::FromAxisAngle(m_activeWorldAxis, deltaRadians);
|
||||
context.selectedObject->GetTransform()->SetRotation(deltaRotation * m_dragStartWorldRotation);
|
||||
}
|
||||
|
||||
void SceneViewportRotateGizmo::EndDrag(IUndoManager& undoManager) {
|
||||
if (m_activeAxis == SceneViewportRotateGizmoAxis::None) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (undoManager.HasPendingInteractiveChange()) {
|
||||
undoManager.FinalizeInteractiveChange();
|
||||
}
|
||||
|
||||
m_activeAxis = SceneViewportRotateGizmoAxis::None;
|
||||
m_activeEntityId = 0;
|
||||
m_screenSpaceDrag = false;
|
||||
m_activeWorldAxis = Math::Vector3::Zero();
|
||||
m_dragStartWorldRotation = Math::Quaternion::Identity();
|
||||
m_dragStartDirectionWorld = Math::Vector3::Zero();
|
||||
m_dragStartRingAngle = 0.0f;
|
||||
RefreshHandleState();
|
||||
}
|
||||
|
||||
void SceneViewportRotateGizmo::CancelDrag(IUndoManager* undoManager) {
|
||||
if (undoManager != nullptr && undoManager->HasPendingInteractiveChange()) {
|
||||
undoManager->CancelInteractiveChange();
|
||||
}
|
||||
|
||||
m_activeAxis = SceneViewportRotateGizmoAxis::None;
|
||||
m_activeEntityId = 0;
|
||||
m_screenSpaceDrag = false;
|
||||
m_activeWorldAxis = Math::Vector3::Zero();
|
||||
m_dragStartWorldRotation = Math::Quaternion::Identity();
|
||||
m_dragStartDirectionWorld = Math::Vector3::Zero();
|
||||
m_dragStartRingAngle = 0.0f;
|
||||
m_hoveredAxis = SceneViewportRotateGizmoAxis::None;
|
||||
RefreshHandleState();
|
||||
}
|
||||
|
||||
bool SceneViewportRotateGizmo::IsHoveringHandle() const {
|
||||
return m_hoveredAxis != SceneViewportRotateGizmoAxis::None;
|
||||
}
|
||||
|
||||
bool SceneViewportRotateGizmo::IsActive() const {
|
||||
return m_activeAxis != SceneViewportRotateGizmoAxis::None;
|
||||
}
|
||||
|
||||
uint64_t SceneViewportRotateGizmo::GetActiveEntityId() const {
|
||||
return m_activeEntityId;
|
||||
}
|
||||
|
||||
const SceneViewportRotateGizmoDrawData& SceneViewportRotateGizmo::GetDrawData() const {
|
||||
return m_drawData;
|
||||
}
|
||||
|
||||
void SceneViewportRotateGizmo::BuildDrawData(const SceneViewportRotateGizmoContext& context) {
|
||||
m_drawData = {};
|
||||
|
||||
const Components::GameObject* selectedObject = context.selectedObject;
|
||||
if (selectedObject == nullptr ||
|
||||
!context.overlay.valid ||
|
||||
context.viewportSize.x <= 1.0f ||
|
||||
context.viewportSize.y <= 1.0f) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Math::Vector3 pivotWorldPosition = selectedObject->GetTransform()->GetPosition();
|
||||
const SceneViewportProjectedPoint projectedPivot = ProjectSceneViewportWorldPoint(
|
||||
context.overlay,
|
||||
context.viewportSize.x,
|
||||
context.viewportSize.y,
|
||||
pivotWorldPosition);
|
||||
if (!projectedPivot.visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
const float worldUnitsPerPixel = ComputeWorldUnitsPerPixel(
|
||||
context.overlay,
|
||||
pivotWorldPosition,
|
||||
context.viewportSize.y);
|
||||
if (worldUnitsPerPixel <= Math::EPSILON) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_drawData.visible = true;
|
||||
m_drawData.pivot = projectedPivot.screenPosition;
|
||||
|
||||
for (size_t handleIndex = 0; handleIndex < m_drawData.handles.size(); ++handleIndex) {
|
||||
SceneViewportRotateGizmoHandleDrawData& handle = m_drawData.handles[handleIndex];
|
||||
handle.axis = GetRotateAxisForIndex(handleIndex);
|
||||
handle.color = GetRotateAxisBaseColor(handle.axis);
|
||||
const float ringRadiusWorld = worldUnitsPerPixel *
|
||||
(handle.axis == SceneViewportRotateGizmoAxis::View
|
||||
? kRotateGizmoViewRadiusPixels
|
||||
: kRotateGizmoAxisRadiusPixels);
|
||||
|
||||
Math::Vector3 basisA = Math::Vector3::Zero();
|
||||
Math::Vector3 basisB = Math::Vector3::Zero();
|
||||
if (!GetRotateRingBasis(handle.axis, context.overlay, basisA, basisB)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
bool anyVisibleSegment = false;
|
||||
for (size_t segmentIndex = 0; segmentIndex < handle.segments.size(); ++segmentIndex) {
|
||||
const float angle0 = static_cast<float>(segmentIndex) / static_cast<float>(handle.segments.size()) * Math::PI * 2.0f;
|
||||
const float angle1 = static_cast<float>(segmentIndex + 1) / static_cast<float>(handle.segments.size()) * Math::PI * 2.0f;
|
||||
const float midAngle = (angle0 + angle1) * 0.5f;
|
||||
|
||||
const Math::Vector3 startWorld =
|
||||
pivotWorldPosition + (basisA * std::cos(angle0) + basisB * std::sin(angle0)) * ringRadiusWorld;
|
||||
const Math::Vector3 endWorld =
|
||||
pivotWorldPosition + (basisA * std::cos(angle1) + basisB * std::sin(angle1)) * ringRadiusWorld;
|
||||
const Math::Vector3 midWorld =
|
||||
pivotWorldPosition + (basisA * std::cos(midAngle) + basisB * std::sin(midAngle)) * ringRadiusWorld;
|
||||
|
||||
const SceneViewportProjectedPoint projectedStart = ProjectSceneViewportWorldPoint(
|
||||
context.overlay,
|
||||
context.viewportSize.x,
|
||||
context.viewportSize.y,
|
||||
startWorld);
|
||||
const SceneViewportProjectedPoint projectedEnd = ProjectSceneViewportWorldPoint(
|
||||
context.overlay,
|
||||
context.viewportSize.x,
|
||||
context.viewportSize.y,
|
||||
endWorld);
|
||||
if (projectedStart.ndcDepth < 0.0f || projectedStart.ndcDepth > 1.0f ||
|
||||
projectedEnd.ndcDepth < 0.0f || projectedEnd.ndcDepth > 1.0f) {
|
||||
continue;
|
||||
}
|
||||
|
||||
SceneViewportRotateGizmoSegmentDrawData& segment = handle.segments[segmentIndex];
|
||||
segment.start = projectedStart.screenPosition;
|
||||
segment.end = projectedEnd.screenPosition;
|
||||
segment.startAngle = angle0;
|
||||
segment.endAngle = angle1;
|
||||
segment.visible = (segment.end - segment.start).SqrMagnitude() > Math::EPSILON;
|
||||
if (!segment.visible) {
|
||||
continue;
|
||||
}
|
||||
|
||||
anyVisibleSegment = true;
|
||||
if (handle.axis == SceneViewportRotateGizmoAxis::View) {
|
||||
segment.frontFacing = true;
|
||||
} else {
|
||||
const Math::Vector3 radial = (midWorld - pivotWorldPosition).Normalized();
|
||||
segment.frontFacing = Math::Vector3::Dot(
|
||||
radial,
|
||||
NormalizeVector3(context.overlay.cameraForward, Math::Vector3::Forward())) < 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
handle.visible = anyVisibleSegment;
|
||||
}
|
||||
}
|
||||
|
||||
void SceneViewportRotateGizmo::RefreshHandleState() {
|
||||
for (SceneViewportRotateGizmoHandleDrawData& handle : m_drawData.handles) {
|
||||
if (!handle.visible) {
|
||||
continue;
|
||||
}
|
||||
|
||||
handle.hovered = handle.axis == m_hoveredAxis;
|
||||
handle.active = handle.axis == m_activeAxis;
|
||||
handle.color = (handle.hovered || handle.active)
|
||||
? Math::Color::Yellow()
|
||||
: GetRotateAxisBaseColor(handle.axis);
|
||||
}
|
||||
}
|
||||
|
||||
SceneViewportRotateGizmoAxis SceneViewportRotateGizmo::HitTestAxis(const Math::Vector2& mousePosition) const {
|
||||
if (!m_drawData.visible) {
|
||||
return SceneViewportRotateGizmoAxis::None;
|
||||
}
|
||||
|
||||
const float hoverThresholdSq = kRotateGizmoHoverThresholdPixels * kRotateGizmoHoverThresholdPixels;
|
||||
SceneViewportRotateGizmoAxis bestAxis = SceneViewportRotateGizmoAxis::None;
|
||||
float bestDistanceSq = hoverThresholdSq;
|
||||
|
||||
for (const SceneViewportRotateGizmoHandleDrawData& handle : m_drawData.handles) {
|
||||
if (!handle.visible) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const SceneViewportRotateGizmoSegmentDrawData& segment : handle.segments) {
|
||||
if (!segment.visible ||
|
||||
(handle.axis != SceneViewportRotateGizmoAxis::View && !segment.frontFacing)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const float distanceSq = DistanceToSegmentSquared(mousePosition, segment.start, segment.end);
|
||||
if (distanceSq > bestDistanceSq) {
|
||||
continue;
|
||||
}
|
||||
|
||||
bestDistanceSq = distanceSq;
|
||||
bestAxis = handle.axis;
|
||||
}
|
||||
}
|
||||
|
||||
return bestAxis;
|
||||
}
|
||||
|
||||
bool SceneViewportRotateGizmo::TryGetClosestRingAngle(
|
||||
SceneViewportRotateGizmoAxis axis,
|
||||
const Math::Vector2& mousePosition,
|
||||
bool allowBackFacing,
|
||||
float& outAngle) const {
|
||||
if (!m_drawData.visible || axis == SceneViewportRotateGizmoAxis::None) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const SceneViewportRotateGizmoHandleDrawData* targetHandle = nullptr;
|
||||
for (const SceneViewportRotateGizmoHandleDrawData& handle : m_drawData.handles) {
|
||||
if (handle.axis == axis && handle.visible) {
|
||||
targetHandle = &handle;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (targetHandle == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool isViewHandle = axis == SceneViewportRotateGizmoAxis::View;
|
||||
const SceneViewportRotateGizmoSegmentDrawData* bestSegment = nullptr;
|
||||
float bestSegmentT = 0.0f;
|
||||
float bestDistanceSq = Math::FLOAT_MAX;
|
||||
|
||||
for (const SceneViewportRotateGizmoSegmentDrawData& segment : targetHandle->segments) {
|
||||
if (!segment.visible || (!isViewHandle && !allowBackFacing && !segment.frontFacing)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
float segmentT = 0.0f;
|
||||
const float distanceSq = DistanceToSegmentSquared(mousePosition, segment.start, segment.end, &segmentT);
|
||||
if (distanceSq >= bestDistanceSq) {
|
||||
continue;
|
||||
}
|
||||
|
||||
bestDistanceSq = distanceSq;
|
||||
bestSegment = &segment;
|
||||
bestSegmentT = segmentT;
|
||||
}
|
||||
|
||||
if (bestSegment == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
outAngle = bestSegment->startAngle + (bestSegment->endAngle - bestSegment->startAngle) * bestSegmentT;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace Editor
|
||||
} // namespace XCEngine
|
||||
100
editor/src/Viewport/SceneViewportRotateGizmo.h
Normal file
100
editor/src/Viewport/SceneViewportRotateGizmo.h
Normal file
@@ -0,0 +1,100 @@
|
||||
#pragma once
|
||||
|
||||
#include "IViewportHostService.h"
|
||||
|
||||
#include <XCEngine/Core/Math/Color.h>
|
||||
#include <XCEngine/Core/Math/Plane.h>
|
||||
#include <XCEngine/Core/Math/Quaternion.h>
|
||||
#include <XCEngine/Core/Math/Vector2.h>
|
||||
#include <XCEngine/Core/Math/Vector3.h>
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Components {
|
||||
class GameObject;
|
||||
} // namespace Components
|
||||
|
||||
namespace Editor {
|
||||
|
||||
class IUndoManager;
|
||||
|
||||
enum class SceneViewportRotateGizmoAxis : uint8_t {
|
||||
None = 0,
|
||||
X,
|
||||
Y,
|
||||
Z,
|
||||
View
|
||||
};
|
||||
|
||||
constexpr size_t kSceneViewportRotateGizmoSegmentCount = 96;
|
||||
|
||||
struct SceneViewportRotateGizmoSegmentDrawData {
|
||||
Math::Vector2 start = Math::Vector2::Zero();
|
||||
Math::Vector2 end = Math::Vector2::Zero();
|
||||
float startAngle = 0.0f;
|
||||
float endAngle = 0.0f;
|
||||
bool visible = false;
|
||||
bool frontFacing = true;
|
||||
};
|
||||
|
||||
struct SceneViewportRotateGizmoHandleDrawData {
|
||||
SceneViewportRotateGizmoAxis axis = SceneViewportRotateGizmoAxis::None;
|
||||
std::array<SceneViewportRotateGizmoSegmentDrawData, kSceneViewportRotateGizmoSegmentCount> segments = {};
|
||||
Math::Color color = Math::Color::White();
|
||||
bool visible = false;
|
||||
bool hovered = false;
|
||||
bool active = false;
|
||||
};
|
||||
|
||||
struct SceneViewportRotateGizmoDrawData {
|
||||
bool visible = false;
|
||||
Math::Vector2 pivot = Math::Vector2::Zero();
|
||||
std::array<SceneViewportRotateGizmoHandleDrawData, 4> handles = {};
|
||||
};
|
||||
|
||||
struct SceneViewportRotateGizmoContext {
|
||||
SceneViewportOverlayData overlay = {};
|
||||
Math::Vector2 viewportSize = Math::Vector2::Zero();
|
||||
Math::Vector2 mousePosition = Math::Vector2::Zero();
|
||||
Components::GameObject* selectedObject = nullptr;
|
||||
};
|
||||
|
||||
class SceneViewportRotateGizmo {
|
||||
public:
|
||||
void Update(const SceneViewportRotateGizmoContext& context);
|
||||
bool TryBeginDrag(const SceneViewportRotateGizmoContext& context, IUndoManager& undoManager);
|
||||
void UpdateDrag(const SceneViewportRotateGizmoContext& context);
|
||||
void EndDrag(IUndoManager& undoManager);
|
||||
void CancelDrag(IUndoManager* undoManager = nullptr);
|
||||
|
||||
bool IsHoveringHandle() const;
|
||||
bool IsActive() const;
|
||||
uint64_t GetActiveEntityId() const;
|
||||
const SceneViewportRotateGizmoDrawData& GetDrawData() const;
|
||||
|
||||
private:
|
||||
void BuildDrawData(const SceneViewportRotateGizmoContext& context);
|
||||
void RefreshHandleState();
|
||||
SceneViewportRotateGizmoAxis HitTestAxis(const Math::Vector2& mousePosition) const;
|
||||
bool TryGetClosestRingAngle(
|
||||
SceneViewportRotateGizmoAxis axis,
|
||||
const Math::Vector2& mousePosition,
|
||||
bool allowBackFacing,
|
||||
float& outAngle) const;
|
||||
|
||||
SceneViewportRotateGizmoDrawData m_drawData = {};
|
||||
SceneViewportRotateGizmoAxis m_hoveredAxis = SceneViewportRotateGizmoAxis::None;
|
||||
SceneViewportRotateGizmoAxis m_activeAxis = SceneViewportRotateGizmoAxis::None;
|
||||
uint64_t m_activeEntityId = 0;
|
||||
bool m_screenSpaceDrag = false;
|
||||
Math::Vector3 m_activeWorldAxis = Math::Vector3::Zero();
|
||||
Math::Plane m_dragPlane = {};
|
||||
Math::Quaternion m_dragStartWorldRotation = Math::Quaternion::Identity();
|
||||
Math::Vector3 m_dragStartDirectionWorld = Math::Vector3::Zero();
|
||||
float m_dragStartRingAngle = 0.0f;
|
||||
};
|
||||
|
||||
} // namespace Editor
|
||||
} // namespace XCEngine
|
||||
@@ -37,6 +37,28 @@ SceneViewportMoveGizmoContext BuildMoveGizmoContext(
|
||||
return gizmoContext;
|
||||
}
|
||||
|
||||
SceneViewportRotateGizmoContext BuildRotateGizmoContext(
|
||||
IEditorContext& context,
|
||||
const SceneViewportOverlayData& overlay,
|
||||
const ViewportPanelContentResult& content,
|
||||
const ImVec2& mousePosition) {
|
||||
SceneViewportRotateGizmoContext gizmoContext = {};
|
||||
gizmoContext.overlay = overlay;
|
||||
gizmoContext.viewportSize = Math::Vector2(content.availableSize.x, content.availableSize.y);
|
||||
gizmoContext.mousePosition = Math::Vector2(
|
||||
mousePosition.x - content.itemMin.x,
|
||||
mousePosition.y - content.itemMin.y);
|
||||
|
||||
if (context.GetSelectionManager().GetSelectionCount() == 1) {
|
||||
const uint64_t selectedEntity = context.GetSelectionManager().GetSelectedEntity();
|
||||
if (selectedEntity != 0) {
|
||||
gizmoContext.selectedObject = context.GetSceneManager().GetEntity(selectedEntity);
|
||||
}
|
||||
}
|
||||
|
||||
return gizmoContext;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
SceneViewPanel::SceneViewPanel() : Panel("Scene") {}
|
||||
@@ -53,36 +75,78 @@ void SceneViewPanel::Render() {
|
||||
if (IViewportHostService* viewportHostService = m_context->GetViewportHostService()) {
|
||||
const ImGuiIO& io = ImGui::GetIO();
|
||||
const bool hasInteractiveViewport = content.hasViewportArea && content.frame.hasTexture;
|
||||
|
||||
if (content.focused && !io.WantTextInput && !m_lookDragging && !m_panDragging) {
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_W, false)) {
|
||||
if (m_rotateGizmo.IsActive()) {
|
||||
m_rotateGizmo.CancelDrag(&m_context->GetUndoManager());
|
||||
}
|
||||
m_transformTool = SceneViewportTransformTool::Move;
|
||||
} else if (ImGui::IsKeyPressed(ImGuiKey_E, false)) {
|
||||
if (m_moveGizmo.IsActive()) {
|
||||
m_moveGizmo.CancelDrag(&m_context->GetUndoManager());
|
||||
}
|
||||
m_transformTool = SceneViewportTransformTool::Rotate;
|
||||
}
|
||||
}
|
||||
|
||||
const bool usingMoveGizmo = m_transformTool == SceneViewportTransformTool::Move;
|
||||
SceneViewportOverlayData overlay = {};
|
||||
SceneViewportMoveGizmoContext moveGizmoContext = {};
|
||||
SceneViewportRotateGizmoContext rotateGizmoContext = {};
|
||||
|
||||
if (hasInteractiveViewport) {
|
||||
overlay = viewportHostService->GetSceneViewOverlayData();
|
||||
moveGizmoContext = BuildMoveGizmoContext(*m_context, overlay, content, io.MousePos);
|
||||
if (m_moveGizmo.IsActive() &&
|
||||
(moveGizmoContext.selectedObject == nullptr ||
|
||||
m_context->GetSelectionManager().GetSelectedEntity() != m_moveGizmo.GetActiveEntityId())) {
|
||||
if (usingMoveGizmo) {
|
||||
if (m_rotateGizmo.IsActive()) {
|
||||
m_rotateGizmo.CancelDrag(&m_context->GetUndoManager());
|
||||
}
|
||||
|
||||
moveGizmoContext = BuildMoveGizmoContext(*m_context, overlay, content, io.MousePos);
|
||||
if (m_moveGizmo.IsActive() &&
|
||||
(moveGizmoContext.selectedObject == nullptr ||
|
||||
m_context->GetSelectionManager().GetSelectedEntity() != m_moveGizmo.GetActiveEntityId())) {
|
||||
m_moveGizmo.CancelDrag(&m_context->GetUndoManager());
|
||||
}
|
||||
m_moveGizmo.Update(moveGizmoContext);
|
||||
} else {
|
||||
if (m_moveGizmo.IsActive()) {
|
||||
m_moveGizmo.CancelDrag(&m_context->GetUndoManager());
|
||||
}
|
||||
|
||||
rotateGizmoContext = BuildRotateGizmoContext(*m_context, overlay, content, io.MousePos);
|
||||
if (m_rotateGizmo.IsActive() &&
|
||||
(rotateGizmoContext.selectedObject == nullptr ||
|
||||
m_context->GetSelectionManager().GetSelectedEntity() != m_rotateGizmo.GetActiveEntityId())) {
|
||||
m_rotateGizmo.CancelDrag(&m_context->GetUndoManager());
|
||||
}
|
||||
m_rotateGizmo.Update(rotateGizmoContext);
|
||||
}
|
||||
} else {
|
||||
if (m_moveGizmo.IsActive()) {
|
||||
m_moveGizmo.CancelDrag(&m_context->GetUndoManager());
|
||||
}
|
||||
m_moveGizmo.Update(moveGizmoContext);
|
||||
} else if (m_moveGizmo.IsActive()) {
|
||||
m_moveGizmo.CancelDrag(&m_context->GetUndoManager());
|
||||
if (m_rotateGizmo.IsActive()) {
|
||||
m_rotateGizmo.CancelDrag(&m_context->GetUndoManager());
|
||||
}
|
||||
}
|
||||
|
||||
const bool beginMoveGizmo =
|
||||
const bool gizmoHovering = usingMoveGizmo ? m_moveGizmo.IsHoveringHandle() : m_rotateGizmo.IsHoveringHandle();
|
||||
const bool gizmoActive = usingMoveGizmo ? m_moveGizmo.IsActive() : m_rotateGizmo.IsActive();
|
||||
|
||||
const bool beginTransformGizmo =
|
||||
hasInteractiveViewport &&
|
||||
content.hovered &&
|
||||
ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
|
||||
content.clickedLeft &&
|
||||
!m_lookDragging &&
|
||||
!m_panDragging &&
|
||||
m_moveGizmo.IsHoveringHandle();
|
||||
gizmoHovering;
|
||||
const SceneViewportOrientationAxis orientationAxisHit =
|
||||
hasInteractiveViewport &&
|
||||
content.hovered &&
|
||||
!m_lookDragging &&
|
||||
!m_panDragging &&
|
||||
!m_moveGizmo.IsHoveringHandle() &&
|
||||
!m_moveGizmo.IsActive()
|
||||
!gizmoHovering &&
|
||||
!gizmoActive
|
||||
? HitTestSceneViewportOrientationGizmo(
|
||||
overlay,
|
||||
content.itemMin,
|
||||
@@ -91,45 +155,52 @@ void SceneViewPanel::Render() {
|
||||
: SceneViewportOrientationAxis::None;
|
||||
const bool orientationGizmoClick =
|
||||
hasInteractiveViewport &&
|
||||
content.hovered &&
|
||||
ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
|
||||
content.clickedLeft &&
|
||||
orientationAxisHit != SceneViewportOrientationAxis::None;
|
||||
const bool selectClick =
|
||||
hasInteractiveViewport &&
|
||||
content.hovered &&
|
||||
ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
|
||||
content.clickedLeft &&
|
||||
!m_lookDragging &&
|
||||
!m_panDragging &&
|
||||
!orientationGizmoClick &&
|
||||
!m_moveGizmo.IsHoveringHandle() &&
|
||||
!m_moveGizmo.IsActive();
|
||||
!gizmoHovering &&
|
||||
!gizmoActive;
|
||||
const bool beginLookDrag =
|
||||
hasInteractiveViewport &&
|
||||
content.hovered &&
|
||||
!m_lookDragging &&
|
||||
!m_moveGizmo.IsActive() &&
|
||||
!gizmoActive &&
|
||||
ImGui::IsMouseClicked(ImGuiMouseButton_Right);
|
||||
const bool beginPanDrag =
|
||||
hasInteractiveViewport &&
|
||||
content.hovered &&
|
||||
!m_panDragging &&
|
||||
!m_moveGizmo.IsActive() &&
|
||||
!gizmoActive &&
|
||||
!m_lookDragging &&
|
||||
ImGui::IsMouseClicked(ImGuiMouseButton_Middle);
|
||||
|
||||
if (beginMoveGizmo || orientationGizmoClick || selectClick || beginLookDrag || beginPanDrag) {
|
||||
if (beginTransformGizmo || orientationGizmoClick || selectClick || beginLookDrag || beginPanDrag) {
|
||||
ImGui::SetWindowFocus();
|
||||
}
|
||||
|
||||
if (beginMoveGizmo) {
|
||||
m_moveGizmo.TryBeginDrag(moveGizmoContext, m_context->GetUndoManager());
|
||||
if (beginTransformGizmo) {
|
||||
if (usingMoveGizmo) {
|
||||
m_moveGizmo.TryBeginDrag(moveGizmoContext, m_context->GetUndoManager());
|
||||
} else {
|
||||
m_rotateGizmo.TryBeginDrag(rotateGizmoContext, m_context->GetUndoManager());
|
||||
}
|
||||
}
|
||||
|
||||
if (orientationGizmoClick) {
|
||||
viewportHostService->AlignSceneViewToOrientationAxis(orientationAxisHit);
|
||||
overlay = viewportHostService->GetSceneViewOverlayData();
|
||||
moveGizmoContext = BuildMoveGizmoContext(*m_context, overlay, content, io.MousePos);
|
||||
m_moveGizmo.Update(moveGizmoContext);
|
||||
if (usingMoveGizmo) {
|
||||
moveGizmoContext = BuildMoveGizmoContext(*m_context, overlay, content, io.MousePos);
|
||||
m_moveGizmo.Update(moveGizmoContext);
|
||||
} else {
|
||||
rotateGizmoContext = BuildRotateGizmoContext(*m_context, overlay, content, io.MousePos);
|
||||
m_rotateGizmo.Update(rotateGizmoContext);
|
||||
}
|
||||
}
|
||||
|
||||
if (selectClick) {
|
||||
@@ -147,11 +218,19 @@ void SceneViewPanel::Render() {
|
||||
}
|
||||
}
|
||||
|
||||
if (m_moveGizmo.IsActive()) {
|
||||
if (gizmoActive) {
|
||||
if (ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
|
||||
m_moveGizmo.UpdateDrag(moveGizmoContext);
|
||||
if (usingMoveGizmo) {
|
||||
m_moveGizmo.UpdateDrag(moveGizmoContext);
|
||||
} else {
|
||||
m_rotateGizmo.UpdateDrag(rotateGizmoContext);
|
||||
}
|
||||
} else {
|
||||
m_moveGizmo.EndDrag(m_context->GetUndoManager());
|
||||
if (usingMoveGizmo) {
|
||||
m_moveGizmo.EndDrag(m_context->GetUndoManager());
|
||||
} else {
|
||||
m_rotateGizmo.EndDrag(m_context->GetUndoManager());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,7 +256,7 @@ void SceneViewPanel::Render() {
|
||||
m_lastPanDragDelta = ImVec2(0.0f, 0.0f);
|
||||
}
|
||||
|
||||
if (m_lookDragging || m_panDragging || m_moveGizmo.IsActive()) {
|
||||
if (m_lookDragging || m_panDragging || m_moveGizmo.IsActive() || m_rotateGizmo.IsActive()) {
|
||||
ImGui::SetNextFrameWantCaptureMouse(true);
|
||||
}
|
||||
if (m_lookDragging) {
|
||||
@@ -222,8 +301,16 @@ void SceneViewPanel::Render() {
|
||||
|
||||
if (m_panDragging) {
|
||||
const ImVec2 panDragDelta = ImGui::GetMouseDragDelta(ImGuiMouseButton_Middle, 0.0f);
|
||||
input.mouseDelta.x += panDragDelta.x - m_lastPanDragDelta.x;
|
||||
input.mouseDelta.y += panDragDelta.y - m_lastPanDragDelta.y;
|
||||
ImVec2 framePanDelta(
|
||||
panDragDelta.x - m_lastPanDragDelta.x,
|
||||
panDragDelta.y - m_lastPanDragDelta.y);
|
||||
// Some middle-button drags report a zero drag delta on the interaction surface.
|
||||
if ((framePanDelta.x == 0.0f && framePanDelta.y == 0.0f) &&
|
||||
(io.MouseDelta.x != 0.0f || io.MouseDelta.y != 0.0f)) {
|
||||
framePanDelta = io.MouseDelta;
|
||||
}
|
||||
input.mouseDelta.x += framePanDelta.x;
|
||||
input.mouseDelta.y += framePanDelta.y;
|
||||
m_lastPanDragDelta = panDragDelta;
|
||||
} else {
|
||||
m_lastPanDragDelta = ImVec2(0.0f, 0.0f);
|
||||
@@ -234,15 +321,22 @@ void SceneViewPanel::Render() {
|
||||
|
||||
if (content.hasViewportArea && content.frame.hasTexture) {
|
||||
overlay = viewportHostService->GetSceneViewOverlayData();
|
||||
moveGizmoContext = BuildMoveGizmoContext(*m_context, overlay, content, io.MousePos);
|
||||
m_moveGizmo.Update(moveGizmoContext);
|
||||
if (usingMoveGizmo) {
|
||||
moveGizmoContext = BuildMoveGizmoContext(*m_context, overlay, content, io.MousePos);
|
||||
m_moveGizmo.Update(moveGizmoContext);
|
||||
} else {
|
||||
rotateGizmoContext = BuildRotateGizmoContext(*m_context, overlay, content, io.MousePos);
|
||||
m_rotateGizmo.Update(rotateGizmoContext);
|
||||
}
|
||||
|
||||
DrawSceneViewportOverlay(
|
||||
ImGui::GetWindowDrawList(),
|
||||
overlay,
|
||||
content.itemMin,
|
||||
content.itemMax,
|
||||
content.availableSize,
|
||||
&m_moveGizmo.GetDrawData());
|
||||
usingMoveGizmo ? &m_moveGizmo.GetDrawData() : nullptr,
|
||||
usingMoveGizmo ? nullptr : &m_rotateGizmo.GetDrawData());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,23 +2,31 @@
|
||||
|
||||
#include "Panel.h"
|
||||
#include "Viewport/SceneViewportMoveGizmo.h"
|
||||
#include "Viewport/SceneViewportRotateGizmo.h"
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Editor {
|
||||
|
||||
enum class SceneViewportTransformTool : uint8_t {
|
||||
Move = 0,
|
||||
Rotate
|
||||
};
|
||||
|
||||
class SceneViewPanel : public Panel {
|
||||
public:
|
||||
SceneViewPanel();
|
||||
void Render() override;
|
||||
|
||||
private:
|
||||
SceneViewportTransformTool m_transformTool = SceneViewportTransformTool::Move;
|
||||
bool m_lookDragging = false;
|
||||
bool m_panDragging = false;
|
||||
ImVec2 m_lastLookDragDelta = ImVec2(0.0f, 0.0f);
|
||||
ImVec2 m_lastPanDragDelta = ImVec2(0.0f, 0.0f);
|
||||
SceneViewportMoveGizmo m_moveGizmo;
|
||||
SceneViewportRotateGizmo m_rotateGizmo;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
277
tests/editor/test_scene_viewport_rotate_gizmo.cpp
Normal file
277
tests/editor/test_scene_viewport_rotate_gizmo.cpp
Normal file
@@ -0,0 +1,277 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#include "Core/EditorContext.h"
|
||||
#include "Managers/SceneManager.h"
|
||||
#include "Viewport/SceneViewportRotateGizmo.h"
|
||||
|
||||
namespace XCEngine::Editor {
|
||||
namespace {
|
||||
|
||||
Math::Vector2 SegmentMidpoint(const SceneViewportRotateGizmoSegmentDrawData& segment) {
|
||||
return (segment.start + segment.end) * 0.5f;
|
||||
}
|
||||
|
||||
const SceneViewportRotateGizmoHandleDrawData* FindHandle(
|
||||
const SceneViewportRotateGizmoDrawData& drawData,
|
||||
SceneViewportRotateGizmoAxis axis) {
|
||||
for (const SceneViewportRotateGizmoHandleDrawData& handle : drawData.handles) {
|
||||
if (handle.axis == axis) {
|
||||
return &handle;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const SceneViewportRotateGizmoSegmentDrawData* FindVisibleSegment(
|
||||
const SceneViewportRotateGizmoHandleDrawData& handle,
|
||||
bool frontOnly) {
|
||||
for (const SceneViewportRotateGizmoSegmentDrawData& segment : handle.segments) {
|
||||
if (!segment.visible) {
|
||||
continue;
|
||||
}
|
||||
if (frontOnly && !segment.frontFacing) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return &segment;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const SceneViewportRotateGizmoSegmentDrawData* FindLongestVisibleSegment(
|
||||
const SceneViewportRotateGizmoHandleDrawData& handle,
|
||||
bool frontOnly) {
|
||||
const SceneViewportRotateGizmoSegmentDrawData* bestSegment = nullptr;
|
||||
float bestLengthSq = -1.0f;
|
||||
for (const SceneViewportRotateGizmoSegmentDrawData& segment : handle.segments) {
|
||||
if (!segment.visible) {
|
||||
continue;
|
||||
}
|
||||
if (frontOnly && !segment.frontFacing) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const float lengthSq = (segment.end - segment.start).SqrMagnitude();
|
||||
if (lengthSq <= bestLengthSq) {
|
||||
continue;
|
||||
}
|
||||
|
||||
bestLengthSq = lengthSq;
|
||||
bestSegment = &segment;
|
||||
}
|
||||
|
||||
return bestSegment;
|
||||
}
|
||||
|
||||
const SceneViewportRotateGizmoSegmentDrawData* FindFarthestVisibleSegment(
|
||||
const SceneViewportRotateGizmoHandleDrawData& handle,
|
||||
const Math::Vector2& fromPoint,
|
||||
bool frontOnly) {
|
||||
const SceneViewportRotateGizmoSegmentDrawData* bestSegment = nullptr;
|
||||
float bestDistanceSq = -1.0f;
|
||||
for (const SceneViewportRotateGizmoSegmentDrawData& segment : handle.segments) {
|
||||
if (!segment.visible) {
|
||||
continue;
|
||||
}
|
||||
if (frontOnly && !segment.frontFacing) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const float distanceSq = (SegmentMidpoint(segment) - fromPoint).SqrMagnitude();
|
||||
if (distanceSq <= bestDistanceSq) {
|
||||
continue;
|
||||
}
|
||||
|
||||
bestDistanceSq = distanceSq;
|
||||
bestSegment = &segment;
|
||||
}
|
||||
|
||||
return bestSegment;
|
||||
}
|
||||
|
||||
class SceneViewportRotateGizmoTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
m_context.GetSceneManager().NewScene("Rotate Gizmo Test Scene");
|
||||
}
|
||||
|
||||
static SceneViewportOverlayData MakeOverlay() {
|
||||
SceneViewportOverlayData overlay = {};
|
||||
overlay.valid = true;
|
||||
overlay.cameraPosition = Math::Vector3(0.0f, 0.0f, -5.0f);
|
||||
overlay.cameraForward = Math::Vector3::Forward();
|
||||
overlay.cameraRight = Math::Vector3::Right();
|
||||
overlay.cameraUp = Math::Vector3::Up();
|
||||
overlay.verticalFovDegrees = 60.0f;
|
||||
overlay.nearClipPlane = 0.03f;
|
||||
overlay.farClipPlane = 2000.0f;
|
||||
return overlay;
|
||||
}
|
||||
|
||||
static SceneViewportOverlayData MakeIsometricOverlay() {
|
||||
SceneViewportOverlayData overlay = {};
|
||||
overlay.valid = true;
|
||||
overlay.cameraPosition = Math::Vector3(-5.0f, 5.0f, -5.0f);
|
||||
overlay.cameraForward = (Math::Vector3::Zero() - overlay.cameraPosition).Normalized();
|
||||
overlay.cameraRight = Math::Vector3::Cross(Math::Vector3::Up(), overlay.cameraForward).Normalized();
|
||||
overlay.cameraUp = Math::Vector3::Cross(overlay.cameraForward, overlay.cameraRight).Normalized();
|
||||
overlay.verticalFovDegrees = 60.0f;
|
||||
overlay.nearClipPlane = 0.03f;
|
||||
overlay.farClipPlane = 2000.0f;
|
||||
return overlay;
|
||||
}
|
||||
|
||||
static SceneViewportRotateGizmoContext MakeContext(
|
||||
Components::GameObject* selectedObject,
|
||||
const Math::Vector2& mousePosition) {
|
||||
SceneViewportRotateGizmoContext context = {};
|
||||
context.overlay = MakeOverlay();
|
||||
context.viewportSize = Math::Vector2(800.0f, 600.0f);
|
||||
context.mousePosition = mousePosition;
|
||||
context.selectedObject = selectedObject;
|
||||
return context;
|
||||
}
|
||||
|
||||
static SceneViewportRotateGizmoContext MakeContext(
|
||||
Components::GameObject* selectedObject,
|
||||
const Math::Vector2& mousePosition,
|
||||
const SceneViewportOverlayData& overlay) {
|
||||
SceneViewportRotateGizmoContext context = {};
|
||||
context.overlay = overlay;
|
||||
context.viewportSize = Math::Vector2(800.0f, 600.0f);
|
||||
context.mousePosition = mousePosition;
|
||||
context.selectedObject = selectedObject;
|
||||
return context;
|
||||
}
|
||||
|
||||
SceneManager& GetSceneManager() {
|
||||
return dynamic_cast<SceneManager&>(m_context.GetSceneManager());
|
||||
}
|
||||
|
||||
EditorContext m_context;
|
||||
};
|
||||
|
||||
TEST_F(SceneViewportRotateGizmoTest, UpdateHighlightsXAxisWhenMouseIsNearVisibleXAxisRing) {
|
||||
Components::GameObject* target = GetSceneManager().CreateEntity("Target");
|
||||
ASSERT_NE(target, nullptr);
|
||||
|
||||
SceneViewportRotateGizmo gizmo;
|
||||
gizmo.Update(MakeContext(target, Math::Vector2(400.0f, 300.0f)));
|
||||
|
||||
ASSERT_TRUE(gizmo.GetDrawData().visible);
|
||||
const SceneViewportRotateGizmoHandleDrawData* xHandle =
|
||||
FindHandle(gizmo.GetDrawData(), SceneViewportRotateGizmoAxis::X);
|
||||
ASSERT_NE(xHandle, nullptr);
|
||||
const SceneViewportRotateGizmoSegmentDrawData* xSegment = FindLongestVisibleSegment(*xHandle, true);
|
||||
ASSERT_NE(xSegment, nullptr);
|
||||
|
||||
gizmo.Update(MakeContext(target, SegmentMidpoint(*xSegment)));
|
||||
|
||||
EXPECT_TRUE(gizmo.IsHoveringHandle());
|
||||
EXPECT_TRUE(FindHandle(gizmo.GetDrawData(), SceneViewportRotateGizmoAxis::X)->hovered);
|
||||
}
|
||||
|
||||
TEST_F(SceneViewportRotateGizmoTest, DraggingXAxisRotatesAroundWorldXAndCreatesUndoStep) {
|
||||
Components::GameObject* target = GetSceneManager().CreateEntity("Target");
|
||||
ASSERT_NE(target, nullptr);
|
||||
const uint64_t targetId = target->GetID();
|
||||
|
||||
SceneViewportRotateGizmo gizmo;
|
||||
const SceneViewportOverlayData overlay = MakeIsometricOverlay();
|
||||
gizmo.Update(MakeContext(target, Math::Vector2(400.0f, 300.0f), overlay));
|
||||
|
||||
const SceneViewportRotateGizmoHandleDrawData* xHandle =
|
||||
FindHandle(gizmo.GetDrawData(), SceneViewportRotateGizmoAxis::X);
|
||||
ASSERT_NE(xHandle, nullptr);
|
||||
const SceneViewportRotateGizmoSegmentDrawData* startSegment = FindLongestVisibleSegment(*xHandle, true);
|
||||
ASSERT_NE(startSegment, nullptr);
|
||||
const Math::Vector2 startMouse = SegmentMidpoint(*startSegment);
|
||||
const SceneViewportRotateGizmoSegmentDrawData* endSegment = FindFarthestVisibleSegment(*xHandle, startMouse, true);
|
||||
ASSERT_NE(endSegment, nullptr);
|
||||
|
||||
const auto startContext = MakeContext(target, startMouse, overlay);
|
||||
gizmo.Update(startContext);
|
||||
ASSERT_TRUE(gizmo.IsHoveringHandle());
|
||||
ASSERT_TRUE(gizmo.TryBeginDrag(startContext, m_context.GetUndoManager()));
|
||||
ASSERT_TRUE(gizmo.IsActive());
|
||||
|
||||
const auto dragContext = MakeContext(target, SegmentMidpoint(*endSegment), overlay);
|
||||
gizmo.Update(dragContext);
|
||||
gizmo.UpdateDrag(dragContext);
|
||||
gizmo.EndDrag(m_context.GetUndoManager());
|
||||
|
||||
const Math::Vector3 rotatedRight = target->GetTransform()->GetRight();
|
||||
const Math::Vector3 rotatedForward = target->GetTransform()->GetForward();
|
||||
EXPECT_NEAR(rotatedRight.x, 1.0f, 1e-3f);
|
||||
EXPECT_NEAR(rotatedRight.y, 0.0f, 1e-3f);
|
||||
EXPECT_NEAR(rotatedRight.z, 0.0f, 1e-3f);
|
||||
EXPECT_GT(std::abs(rotatedForward.y), 0.05f);
|
||||
EXPECT_TRUE(m_context.GetUndoManager().CanUndo());
|
||||
|
||||
m_context.GetUndoManager().Undo();
|
||||
Components::GameObject* restoredTarget = GetSceneManager().GetEntity(targetId);
|
||||
ASSERT_NE(restoredTarget, nullptr);
|
||||
const Math::Vector3 restoredForward = restoredTarget->GetTransform()->GetForward();
|
||||
EXPECT_NEAR(restoredForward.x, 0.0f, 1e-4f);
|
||||
EXPECT_NEAR(restoredForward.y, 0.0f, 1e-4f);
|
||||
EXPECT_NEAR(restoredForward.z, 1.0f, 1e-4f);
|
||||
}
|
||||
|
||||
TEST_F(SceneViewportRotateGizmoTest, DraggingEdgeOnXAxisFallsBackToScreenSpaceRotation) {
|
||||
Components::GameObject* target = GetSceneManager().CreateEntity("Target");
|
||||
ASSERT_NE(target, nullptr);
|
||||
|
||||
SceneViewportRotateGizmo gizmo;
|
||||
gizmo.Update(MakeContext(target, Math::Vector2(400.0f, 300.0f)));
|
||||
|
||||
const SceneViewportRotateGizmoHandleDrawData* xHandle =
|
||||
FindHandle(gizmo.GetDrawData(), SceneViewportRotateGizmoAxis::X);
|
||||
ASSERT_NE(xHandle, nullptr);
|
||||
const SceneViewportRotateGizmoSegmentDrawData* startSegment = FindLongestVisibleSegment(*xHandle, true);
|
||||
ASSERT_NE(startSegment, nullptr);
|
||||
const Math::Vector2 startMouse = SegmentMidpoint(*startSegment);
|
||||
const SceneViewportRotateGizmoSegmentDrawData* endSegment = FindFarthestVisibleSegment(*xHandle, startMouse, true);
|
||||
ASSERT_NE(endSegment, nullptr);
|
||||
|
||||
const auto startContext = MakeContext(target, startMouse);
|
||||
gizmo.Update(startContext);
|
||||
ASSERT_TRUE(gizmo.IsHoveringHandle());
|
||||
ASSERT_TRUE(gizmo.TryBeginDrag(startContext, m_context.GetUndoManager()));
|
||||
ASSERT_TRUE(gizmo.IsActive());
|
||||
|
||||
const auto dragContext = MakeContext(target, SegmentMidpoint(*endSegment));
|
||||
gizmo.Update(dragContext);
|
||||
gizmo.UpdateDrag(dragContext);
|
||||
gizmo.EndDrag(m_context.GetUndoManager());
|
||||
|
||||
const Math::Vector3 rotatedForward = target->GetTransform()->GetForward();
|
||||
EXPECT_GT(std::abs(rotatedForward.y), 0.05f);
|
||||
EXPECT_TRUE(m_context.GetUndoManager().CanUndo());
|
||||
}
|
||||
|
||||
TEST_F(SceneViewportRotateGizmoTest, ViewRingIsVisibleAndHoverable) {
|
||||
Components::GameObject* target = GetSceneManager().CreateEntity("Target");
|
||||
ASSERT_NE(target, nullptr);
|
||||
|
||||
SceneViewportRotateGizmo gizmo;
|
||||
gizmo.Update(MakeContext(target, Math::Vector2(400.0f, 300.0f)));
|
||||
|
||||
ASSERT_TRUE(gizmo.GetDrawData().visible);
|
||||
const SceneViewportRotateGizmoHandleDrawData* viewHandle =
|
||||
FindHandle(gizmo.GetDrawData(), SceneViewportRotateGizmoAxis::View);
|
||||
ASSERT_NE(viewHandle, nullptr);
|
||||
const SceneViewportRotateGizmoSegmentDrawData* viewSegment = FindLongestVisibleSegment(*viewHandle, false);
|
||||
ASSERT_NE(viewSegment, nullptr);
|
||||
|
||||
gizmo.Update(MakeContext(target, SegmentMidpoint(*viewSegment)));
|
||||
|
||||
EXPECT_TRUE(gizmo.IsHoveringHandle());
|
||||
EXPECT_TRUE(FindHandle(gizmo.GetDrawData(), SceneViewportRotateGizmoAxis::View)->hovered);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace XCEngine::Editor
|
||||
Reference in New Issue
Block a user