Add scene transform toolbar and scale gizmo

This commit is contained in:
2026-04-01 16:42:57 +08:00
parent 4e8ad9a706
commit 3f18530396
23 changed files with 1668 additions and 198 deletions

View File

@@ -6,6 +6,7 @@
#include <XCEngine/Components/GameObject.h>
#include <algorithm>
#include <cmath>
namespace XCEngine {
@@ -13,9 +14,10 @@ namespace Editor {
namespace {
constexpr float kRotateGizmoAxisRadiusPixels = 84.0f;
constexpr float kRotateGizmoViewRadiusPixels = 92.0f;
constexpr float kRotateGizmoAxisRadiusPixels = 96.0f;
constexpr float kRotateGizmoViewRadiusPixels = 106.0f;
constexpr float kRotateGizmoHoverThresholdPixels = 9.0f;
constexpr float kRotateGizmoAngleFillMinRadians = 0.01f;
Math::Vector3 NormalizeVector3(const Math::Vector3& value, const Math::Vector3& fallback) {
return value.SqrMagnitude() <= Math::EPSILON ? fallback : value.Normalized();
@@ -92,6 +94,12 @@ bool GetRotateRingBasis(
}
}
float GetRotateRingRadiusPixels(SceneViewportRotateGizmoAxis axis) {
return axis == SceneViewportRotateGizmoAxis::View
? kRotateGizmoViewRadiusPixels
: kRotateGizmoAxisRadiusPixels;
}
float ComputeWorldUnitsPerPixel(
const SceneViewportOverlayData& overlay,
const Math::Vector3& worldPoint,
@@ -109,20 +117,6 @@ float ComputeWorldUnitsPerPixel(
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;
@@ -150,6 +144,28 @@ SceneViewportRotateGizmoAxis GetRotateAxisForIndex(size_t index) {
}
}
bool TryComputeRingAngleFromWorldDirection(
SceneViewportRotateGizmoAxis axis,
const SceneViewportOverlayData& overlay,
const Math::Vector3& directionWorld,
float& outAngle) {
Math::Vector3 basisA = Math::Vector3::Zero();
Math::Vector3 basisB = Math::Vector3::Zero();
if (!GetRotateRingBasis(axis, overlay, basisA, basisB)) {
return false;
}
const Math::Vector3 direction = directionWorld.Normalized();
const float projectedX = Math::Vector3::Dot(direction, basisA);
const float projectedY = Math::Vector3::Dot(direction, basisB);
if (projectedX * projectedX + projectedY * projectedY <= Math::EPSILON) {
return false;
}
outAngle = std::atan2(projectedY, projectedX);
return true;
}
} // namespace
void SceneViewportRotateGizmo::Update(const SceneViewportRotateGizmoContext& context) {
@@ -201,9 +217,18 @@ bool SceneViewportRotateGizmo::TryBeginDrag(const SceneViewportRotateGizmoContex
}
float startRingAngle = 0.0f;
if (useScreenSpaceDrag &&
!TryGetClosestRingAngle(m_hoveredAxis, context.mousePosition, false, startRingAngle)) {
return false;
if (useScreenSpaceDrag) {
if (!TryGetClosestRingAngle(m_hoveredAxis, context.mousePosition, false, startRingAngle)) {
return false;
}
} else {
if (!TryComputeRingAngleFromWorldDirection(
m_hoveredAxis,
context.overlay,
startDirection,
startRingAngle)) {
return false;
}
}
undoManager.BeginInteractiveChange("Rotate Gizmo");
@@ -217,8 +242,8 @@ bool SceneViewportRotateGizmo::TryBeginDrag(const SceneViewportRotateGizmoContex
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;
m_dragStartRingAngle = startRingAngle;
m_dragCurrentDeltaRadians = 0.0f;
RefreshHandleState();
return true;
}
@@ -230,14 +255,11 @@ void SceneViewportRotateGizmo::UpdateDrag(const SceneViewportRotateGizmoContext&
return;
}
float deltaRadians = 0.0f;
float currentRingAngle = 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(
@@ -260,14 +282,22 @@ void SceneViewportRotateGizmo::UpdateDrag(const SceneViewportRotateGizmoContext&
return;
}
deltaRadians = SignedAngleRadiansAroundAxis(
m_dragStartDirectionWorld,
currentDirection,
m_activeWorldAxis);
if (!TryComputeRingAngleFromWorldDirection(
m_activeAxis,
context.overlay,
currentDirection,
currentRingAngle)) {
return;
}
}
const float deltaRadians = NormalizeSignedAngleRadians(currentRingAngle - m_dragStartRingAngle);
m_dragCurrentDeltaRadians = deltaRadians;
const Math::Quaternion deltaRotation = Math::Quaternion::FromAxisAngle(m_activeWorldAxis, deltaRadians);
context.selectedObject->GetTransform()->SetRotation(deltaRotation * m_dragStartWorldRotation);
BuildDrawData(context);
m_hoveredAxis = m_activeAxis;
RefreshHandleState();
}
void SceneViewportRotateGizmo::EndDrag(IUndoManager& undoManager) {
@@ -284,8 +314,8 @@ void SceneViewportRotateGizmo::EndDrag(IUndoManager& undoManager) {
m_screenSpaceDrag = false;
m_activeWorldAxis = Math::Vector3::Zero();
m_dragStartWorldRotation = Math::Quaternion::Identity();
m_dragStartDirectionWorld = Math::Vector3::Zero();
m_dragStartRingAngle = 0.0f;
m_dragCurrentDeltaRadians = 0.0f;
RefreshHandleState();
}
@@ -299,8 +329,8 @@ void SceneViewportRotateGizmo::CancelDrag(IUndoManager* undoManager) {
m_screenSpaceDrag = false;
m_activeWorldAxis = Math::Vector3::Zero();
m_dragStartWorldRotation = Math::Quaternion::Identity();
m_dragStartDirectionWorld = Math::Vector3::Zero();
m_dragStartRingAngle = 0.0f;
m_dragCurrentDeltaRadians = 0.0f;
m_hoveredAxis = SceneViewportRotateGizmoAxis::None;
RefreshHandleState();
}
@@ -352,21 +382,29 @@ void SceneViewportRotateGizmo::BuildDrawData(const SceneViewportRotateGizmoConte
m_drawData.visible = true;
m_drawData.pivot = projectedPivot.screenPosition;
const bool hasActiveDragFeedback =
m_activeAxis != SceneViewportRotateGizmoAxis::None &&
m_activeAxis != SceneViewportRotateGizmoAxis::View &&
std::abs(m_dragCurrentDeltaRadians) > Math::EPSILON;
const Math::Quaternion dragFeedbackRotation = hasActiveDragFeedback
? Math::Quaternion::FromAxisAngle(m_activeWorldAxis, m_dragCurrentDeltaRadians)
: Math::Quaternion::Identity();
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);
const float ringRadiusWorld = worldUnitsPerPixel * GetRotateRingRadiusPixels(handle.axis);
Math::Vector3 basisA = Math::Vector3::Zero();
Math::Vector3 basisB = Math::Vector3::Zero();
if (!GetRotateRingBasis(handle.axis, context.overlay, basisA, basisB)) {
continue;
}
if (hasActiveDragFeedback && handle.axis != SceneViewportRotateGizmoAxis::View) {
basisA = dragFeedbackRotation * basisA;
basisB = dragFeedbackRotation * basisB;
}
bool anyVisibleSegment = false;
for (size_t segmentIndex = 0; segmentIndex < handle.segments.size(); ++segmentIndex) {
@@ -419,6 +457,52 @@ void SceneViewportRotateGizmo::BuildDrawData(const SceneViewportRotateGizmoConte
handle.visible = anyVisibleSegment;
}
if (m_activeAxis != SceneViewportRotateGizmoAxis::None &&
std::abs(m_dragCurrentDeltaRadians) >= kRotateGizmoAngleFillMinRadians) {
SceneViewportRotateGizmoAngleFillDrawData& angleFill = m_drawData.angleFill;
angleFill.axis = m_activeAxis;
angleFill.pivot = projectedPivot.screenPosition;
angleFill.fillColor = Math::Color(1.0f, 0.92f, 0.12f, 0.22f);
angleFill.outlineColor = Math::Color(1.0f, 0.92f, 0.12f, 0.95f);
Math::Vector3 basisA = Math::Vector3::Zero();
Math::Vector3 basisB = Math::Vector3::Zero();
if (GetRotateRingBasis(m_activeAxis, context.overlay, basisA, basisB)) {
const float ringRadiusWorld = worldUnitsPerPixel * GetRotateRingRadiusPixels(m_activeAxis);
const float sweepRadians = NormalizeSignedAngleRadians(m_dragCurrentDeltaRadians);
const float sweepAbs = std::abs(sweepRadians);
const size_t stepCount = std::clamp(
static_cast<size_t>(std::ceil(
sweepAbs / (Math::PI * 2.0f) * static_cast<float>(kSceneViewportRotateGizmoSegmentCount))),
static_cast<size_t>(1),
kSceneViewportRotateGizmoAngleFillPointCount - 1);
bool valid = true;
for (size_t pointIndex = 0; pointIndex <= stepCount; ++pointIndex) {
const float t = static_cast<float>(pointIndex) / static_cast<float>(stepCount);
const float angle = m_dragStartRingAngle + sweepRadians * t;
const Math::Vector3 worldPoint =
pivotWorldPosition + (basisA * std::cos(angle) + basisB * std::sin(angle)) * ringRadiusWorld;
const SceneViewportProjectedPoint projectedPoint = ProjectSceneViewportWorldPoint(
context.overlay,
context.viewportSize.x,
context.viewportSize.y,
worldPoint);
if (projectedPoint.ndcDepth < 0.0f || projectedPoint.ndcDepth > 1.0f) {
valid = false;
break;
}
angleFill.arcPoints[pointIndex] = projectedPoint.screenPosition;
}
if (valid) {
angleFill.arcPointCount = stepCount + 1;
angleFill.visible = true;
}
}
}
}
void SceneViewportRotateGizmo::RefreshHandleState() {