Add Unity-style scene rotate gizmo

This commit is contained in:
2026-03-31 23:45:08 +08:00
parent 7ff5cd4cf2
commit ac2b7c1fa2
7 changed files with 1119 additions and 37 deletions

View File

@@ -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();
}

View File

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

View 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

View 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