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

@@ -76,6 +76,7 @@ add_executable(${PROJECT_NAME} WIN32
src/Viewport/SceneViewportPicker.cpp
src/Viewport/SceneViewportMoveGizmo.cpp
src/Viewport/SceneViewportRotateGizmo.cpp
src/Viewport/SceneViewportScaleGizmo.cpp
src/Viewport/SceneViewportGrid.cpp
src/Viewport/SceneViewportInfiniteGridPass.cpp
src/Viewport/SceneViewportSelectionMaskPass.cpp

Binary file not shown.

After

Width:  |  Height:  |  Size: 445 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 337 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 625 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 565 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 847 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 559 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 639 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 657 B

View File

@@ -5,16 +5,13 @@
#include "SceneViewportPicker.h"
#include <XCEngine/Components/GameObject.h>
#include <XCEngine/Components/MeshFilterComponent.h>
#include <XCEngine/Components/MeshRendererComponent.h>
#include <XCEngine/Core/Math/Bounds.h>
namespace XCEngine {
namespace Editor {
namespace {
constexpr float kMoveGizmoHandleLengthPixels = 88.0f;
constexpr float kMoveGizmoHandleLengthPixels = 100.0f;
constexpr float kMoveGizmoHoverThresholdPixels = 10.0f;
Math::Vector3 GetAxisVector(SceneViewportGizmoAxis axis) {
@@ -166,63 +163,6 @@ bool IsMouseInsideViewport(const SceneViewportMoveGizmoContext& context) {
context.mousePosition.y <= context.viewportSize.y;
}
void EncapsulateTransformedBounds(
const Math::Bounds& localBounds,
const Components::TransformComponent& transform,
Math::Bounds& inOutBounds,
bool& inOutHasBounds) {
const Math::Vector3 localMin = localBounds.GetMin();
const Math::Vector3 localMax = localBounds.GetMax();
const Math::Vector3 corners[] = {
Math::Vector3(localMin.x, localMin.y, localMin.z),
Math::Vector3(localMax.x, localMin.y, localMin.z),
Math::Vector3(localMin.x, localMax.y, localMin.z),
Math::Vector3(localMin.x, localMin.y, localMax.z),
Math::Vector3(localMax.x, localMax.y, localMin.z),
Math::Vector3(localMin.x, localMax.y, localMax.z),
Math::Vector3(localMax.x, localMin.y, localMax.z),
Math::Vector3(localMax.x, localMax.y, localMax.z)
};
for (const Math::Vector3& localCorner : corners) {
const Math::Vector3 worldCorner = transform.TransformPoint(localCorner);
if (!inOutHasBounds) {
inOutBounds = Math::Bounds(worldCorner, Math::Vector3::Zero());
inOutHasBounds = true;
} else {
inOutBounds.Encapsulate(worldCorner);
}
}
}
void CollectRenderableWorldBoundsRecursive(
const Components::GameObject& gameObject,
Math::Bounds& inOutBounds,
bool& inOutHasBounds) {
if (!gameObject.IsActiveInHierarchy()) {
return;
}
const auto* meshFilter = gameObject.GetComponent<Components::MeshFilterComponent>();
const auto* meshRenderer = gameObject.GetComponent<Components::MeshRendererComponent>();
if (meshFilter != nullptr &&
meshRenderer != nullptr &&
meshFilter->IsEnabled() &&
meshRenderer->IsEnabled()) {
const auto* mesh = meshFilter->GetMesh();
if (mesh != nullptr) {
EncapsulateTransformedBounds(mesh->GetBounds(), *gameObject.GetTransform(), inOutBounds, inOutHasBounds);
}
}
for (size_t childIndex = 0; childIndex < gameObject.GetChildCount(); ++childIndex) {
const Components::GameObject* child = gameObject.GetChild(childIndex);
if (child != nullptr) {
CollectRenderableWorldBoundsRecursive(*child, inOutBounds, inOutHasBounds);
}
}
}
float ComputeWorldUnitsPerPixel(
const SceneViewportOverlayData& overlay,
const Math::Vector3& worldPoint,
@@ -240,13 +180,6 @@ float ComputeWorldUnitsPerPixel(
return 2.0f * depth * std::tan(overlay.verticalFovDegrees * Math::DEG_TO_RAD * 0.5f) / viewportHeight;
}
Math::Vector3 GetGizmoWorldOrigin(const Components::GameObject& gameObject) {
Math::Bounds worldBounds = {};
bool hasBounds = false;
CollectRenderableWorldBoundsRecursive(gameObject, worldBounds, hasBounds);
return hasBounds ? worldBounds.center : gameObject.GetTransform()->GetPosition();
}
} // namespace
void SceneViewportMoveGizmo::Update(const SceneViewportMoveGizmoContext& context) {
@@ -289,7 +222,7 @@ bool SceneViewportMoveGizmo::TryBeginDrag(const SceneViewportMoveGizmoContext& c
}
const Math::Vector3 objectWorldPosition = context.selectedObject->GetTransform()->GetPosition();
const Math::Vector3 pivotWorldPosition = GetGizmoWorldOrigin(*context.selectedObject);
const Math::Vector3 pivotWorldPosition = context.selectedObject->GetTransform()->GetPosition();
Math::Vector3 dragPlaneNormal = Math::Vector3::Zero();
Math::Vector3 worldAxis = Math::Vector3::Zero();
@@ -442,7 +375,7 @@ void SceneViewportMoveGizmo::BuildDrawData(const SceneViewportMoveGizmoContext&
return;
}
const Math::Vector3 gizmoWorldOrigin = GetGizmoWorldOrigin(*selectedObject);
const Math::Vector3 gizmoWorldOrigin = selectedObject->GetTransform()->GetPosition();
const SceneViewportProjectedPoint projectedPivot = ProjectSceneViewportWorldPoint(
context.overlay,
context.viewportSize.x,

View File

@@ -135,6 +135,99 @@ void DrawSceneRotateGizmoHandle(
}
}
void DrawSceneRotateGizmoAngleFill(
ImDrawList* drawList,
const ImVec2& viewportMin,
const SceneViewportRotateGizmoAngleFillDrawData& angleFill) {
if (drawList == nullptr || !angleFill.visible || angleFill.arcPointCount < 2) {
return;
}
const ImVec2 pivot(viewportMin.x + angleFill.pivot.x, viewportMin.y + angleFill.pivot.y);
const ImU32 fillColor = ToImGuiColor(angleFill.fillColor);
const ImU32 outlineColor = ToImGuiColor(angleFill.outlineColor);
ImVec2 fillPoints[kSceneViewportRotateGizmoAngleFillPointCount + 1] = {};
fillPoints[0] = pivot;
for (size_t index = 0; index < angleFill.arcPointCount; ++index) {
fillPoints[index + 1] = ImVec2(
viewportMin.x + angleFill.arcPoints[index].x,
viewportMin.y + angleFill.arcPoints[index].y);
}
drawList->AddConvexPolyFilled(
fillPoints,
static_cast<int>(angleFill.arcPointCount + 1),
fillColor);
for (size_t index = 0; index + 1 < angleFill.arcPointCount; ++index) {
drawList->AddLine(
ImVec2(viewportMin.x + angleFill.arcPoints[index].x, viewportMin.y + angleFill.arcPoints[index].y),
ImVec2(
viewportMin.x + angleFill.arcPoints[index + 1].x,
viewportMin.y + angleFill.arcPoints[index + 1].y),
outlineColor,
2.0f);
}
drawList->AddLine(
pivot,
ImVec2(viewportMin.x + angleFill.arcPoints.front().x, viewportMin.y + angleFill.arcPoints.front().y),
outlineColor,
1.6f);
drawList->AddLine(
pivot,
ImVec2(
viewportMin.x + angleFill.arcPoints[angleFill.arcPointCount - 1].x,
viewportMin.y + angleFill.arcPoints[angleFill.arcPointCount - 1].y),
outlineColor,
1.6f);
}
void DrawSceneScaleGizmoAxis(
ImDrawList* drawList,
const ImVec2& viewportMin,
const SceneViewportScaleGizmoAxisHandleDrawData& handle) {
if (drawList == nullptr || !handle.visible) {
return;
}
const ImU32 color = ToImGuiColor(handle.color);
const float thickness = handle.active ? 4.0f : (handle.hovered ? 3.0f : 2.2f);
const ImVec2 start(viewportMin.x + handle.start.x, viewportMin.y + handle.start.y);
const ImVec2 capCenter(viewportMin.x + handle.capCenter.x, viewportMin.y + handle.capCenter.y);
const ImVec2 direction = NormalizeImVec2(ImVec2(capCenter.x - start.x, capCenter.y - start.y));
const ImVec2 lineEnd(
capCenter.x - direction.x * handle.capHalfSize,
capCenter.y - direction.y * handle.capHalfSize);
const ImVec2 capMin(capCenter.x - handle.capHalfSize, capCenter.y - handle.capHalfSize);
const ImVec2 capMax(capCenter.x + handle.capHalfSize, capCenter.y + handle.capHalfSize);
drawList->AddLine(start, lineEnd, color, thickness);
drawList->AddRectFilled(capMin, capMax, color, 1.2f);
drawList->AddRect(capMin, capMax, IM_COL32(24, 24, 24, 220), 1.2f, 0, handle.active ? 2.0f : 1.0f);
}
void DrawSceneScaleGizmoCenterHandle(
ImDrawList* drawList,
const ImVec2& viewportMin,
const SceneViewportScaleGizmoCenterHandleDrawData& handle) {
if (drawList == nullptr || !handle.visible) {
return;
}
const ImVec2 center(viewportMin.x + handle.center.x, viewportMin.y + handle.center.y);
const ImVec2 handleMin(center.x - handle.halfSize, center.y - handle.halfSize);
const ImVec2 handleMax(center.x + handle.halfSize, center.y + handle.halfSize);
drawList->AddRectFilled(handleMin, handleMax, ToImGuiColor(handle.fillColor), 1.2f);
drawList->AddRect(
handleMin,
handleMax,
ToImGuiColor(handle.outlineColor),
1.2f,
0,
handle.active ? 2.0f : 1.0f);
}
void DrawSceneMoveGizmo(
ImDrawList* drawList,
const ImVec2& viewportMin,
@@ -143,7 +236,6 @@ void DrawSceneMoveGizmo(
return;
}
const ImVec2 pivot(viewportMin.x + moveGizmo.pivot.x, viewportMin.y + moveGizmo.pivot.y);
for (const SceneViewportMoveGizmoPlaneDrawData& plane : moveGizmo.planes) {
DrawSceneMoveGizmoPlane(drawList, viewportMin, plane);
}
@@ -151,9 +243,6 @@ void DrawSceneMoveGizmo(
for (const SceneViewportMoveGizmoHandleDrawData& handle : moveGizmo.handles) {
DrawSceneMoveGizmoAxis(drawList, viewportMin, handle);
}
drawList->AddCircleFilled(pivot, moveGizmo.pivotRadius + 1.0f, IM_COL32(20, 22, 24, 220), 20);
drawList->AddCircle(pivot, moveGizmo.pivotRadius + 1.0f, IM_COL32(255, 255, 255, 48), 20, 1.0f);
}
void DrawSceneRotateGizmo(
@@ -176,6 +265,8 @@ void DrawSceneRotateGizmo(
}
}
DrawSceneRotateGizmoAngleFill(drawList, viewportMin, rotateGizmo.angleFill);
for (const SceneViewportRotateGizmoHandleDrawData& handle : rotateGizmo.handles) {
if (handle.axis != SceneViewportRotateGizmoAxis::View) {
DrawSceneRotateGizmoHandle(drawList, viewportMin, handle, true);
@@ -183,6 +274,21 @@ void DrawSceneRotateGizmo(
}
}
void DrawSceneScaleGizmo(
ImDrawList* drawList,
const ImVec2& viewportMin,
const SceneViewportScaleGizmoDrawData& scaleGizmo) {
if (drawList == nullptr || !scaleGizmo.visible) {
return;
}
for (const SceneViewportScaleGizmoAxisHandleDrawData& handle : scaleGizmo.axisHandles) {
DrawSceneScaleGizmoAxis(drawList, viewportMin, handle);
}
DrawSceneScaleGizmoCenterHandle(drawList, viewportMin, scaleGizmo.centerHandle);
}
} // namespace
void DrawSceneViewportOverlay(
@@ -192,7 +298,8 @@ void DrawSceneViewportOverlay(
const ImVec2& viewportMax,
const ImVec2& viewportSize,
const SceneViewportMoveGizmoDrawData* moveGizmo,
const SceneViewportRotateGizmoDrawData* rotateGizmo) {
const SceneViewportRotateGizmoDrawData* rotateGizmo,
const SceneViewportScaleGizmoDrawData* scaleGizmo) {
if (drawList == nullptr || viewportSize.x <= 1.0f || viewportSize.y <= 1.0f) {
return;
}
@@ -207,6 +314,9 @@ void DrawSceneViewportOverlay(
if (rotateGizmo != nullptr) {
DrawSceneRotateGizmo(drawList, viewportMin, *rotateGizmo);
}
if (scaleGizmo != nullptr) {
DrawSceneScaleGizmo(drawList, viewportMin, *scaleGizmo);
}
drawList->PopClipRect();
}

View File

@@ -3,6 +3,7 @@
#include "IViewportHostService.h"
#include "SceneViewportMoveGizmo.h"
#include "SceneViewportRotateGizmo.h"
#include "SceneViewportScaleGizmo.h"
#include <imgui.h>
@@ -16,7 +17,8 @@ void DrawSceneViewportOverlay(
const ImVec2& viewportMax,
const ImVec2& viewportSize,
const SceneViewportMoveGizmoDrawData* moveGizmo = nullptr,
const SceneViewportRotateGizmoDrawData* rotateGizmo = nullptr);
const SceneViewportRotateGizmoDrawData* rotateGizmo = nullptr,
const SceneViewportScaleGizmoDrawData* scaleGizmo = nullptr);
} // namespace Editor
} // namespace XCEngine

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

View File

@@ -29,6 +29,7 @@ enum class SceneViewportRotateGizmoAxis : uint8_t {
};
constexpr size_t kSceneViewportRotateGizmoSegmentCount = 96;
constexpr size_t kSceneViewportRotateGizmoAngleFillPointCount = kSceneViewportRotateGizmoSegmentCount + 1;
struct SceneViewportRotateGizmoSegmentDrawData {
Math::Vector2 start = Math::Vector2::Zero();
@@ -48,10 +49,21 @@ struct SceneViewportRotateGizmoHandleDrawData {
bool active = false;
};
struct SceneViewportRotateGizmoAngleFillDrawData {
SceneViewportRotateGizmoAxis axis = SceneViewportRotateGizmoAxis::None;
Math::Vector2 pivot = Math::Vector2::Zero();
std::array<Math::Vector2, kSceneViewportRotateGizmoAngleFillPointCount> arcPoints = {};
size_t arcPointCount = 0;
Math::Color fillColor = Math::Color::White();
Math::Color outlineColor = Math::Color::White();
bool visible = false;
};
struct SceneViewportRotateGizmoDrawData {
bool visible = false;
Math::Vector2 pivot = Math::Vector2::Zero();
std::array<SceneViewportRotateGizmoHandleDrawData, 4> handles = {};
SceneViewportRotateGizmoAngleFillDrawData angleFill = {};
};
struct SceneViewportRotateGizmoContext {
@@ -92,8 +104,8 @@ private:
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;
float m_dragCurrentDeltaRadians = 0.0f;
};
} // namespace Editor

View File

@@ -0,0 +1,502 @@
#include "SceneViewportScaleGizmo.h"
#include "Core/IUndoManager.h"
#include "SceneViewportMath.h"
#include <XCEngine/Components/GameObject.h>
#include <XCEngine/Components/TransformComponent.h>
#include <algorithm>
#include <cmath>
namespace XCEngine {
namespace Editor {
namespace {
constexpr float kScaleGizmoAxisLengthPixels = 110.0f;
constexpr float kScaleGizmoCapHalfSizePixels = 6.5f;
constexpr float kScaleGizmoCenterHalfSizePixels = 7.5f;
constexpr float kScaleGizmoHoverThresholdPixels = 10.0f;
constexpr float kScaleGizmoAxisScalePerPixel = 0.015f;
constexpr float kScaleGizmoUniformScalePerPixel = 0.0125f;
constexpr float kScaleGizmoMinScale = 0.001f;
constexpr float kScaleGizmoVisualScaleMin = 0.4f;
constexpr float kScaleGizmoVisualScaleMax = 2.25f;
Math::Vector3 NormalizeVector3(const Math::Vector3& value, const Math::Vector3& fallback) {
return value.SqrMagnitude() <= Math::EPSILON ? fallback : value.Normalized();
}
bool IsMouseInsideViewport(const SceneViewportScaleGizmoContext& 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 WithAlpha(const Math::Color& color, float alpha) {
return Math::Color(color.r, color.g, color.b, alpha);
}
Math::Color GetScaleHandleBaseColor(SceneViewportScaleGizmoHandle handle) {
switch (handle) {
case SceneViewportScaleGizmoHandle::X:
return Math::Color(0.91f, 0.09f, 0.05f, 1.0f);
case SceneViewportScaleGizmoHandle::Y:
return Math::Color(0.45f, 1.0f, 0.12f, 1.0f);
case SceneViewportScaleGizmoHandle::Z:
return Math::Color(0.11f, 0.29f, 1.0f, 1.0f);
case SceneViewportScaleGizmoHandle::Uniform:
return Math::Color(0.78f, 0.78f, 0.78f, 1.0f);
case SceneViewportScaleGizmoHandle::None:
default:
return Math::Color::White();
}
}
SceneViewportScaleGizmoHandle GetHandleForIndex(size_t index) {
switch (index) {
case 0:
return SceneViewportScaleGizmoHandle::X;
case 1:
return SceneViewportScaleGizmoHandle::Y;
case 2:
return SceneViewportScaleGizmoHandle::Z;
default:
return SceneViewportScaleGizmoHandle::None;
}
}
Math::Vector3 GetHandleWorldAxis(
SceneViewportScaleGizmoHandle handle,
const Components::TransformComponent& transform) {
switch (handle) {
case SceneViewportScaleGizmoHandle::X:
return NormalizeVector3(transform.GetRight(), Math::Vector3::Right());
case SceneViewportScaleGizmoHandle::Y:
return NormalizeVector3(transform.GetUp(), Math::Vector3::Up());
case SceneViewportScaleGizmoHandle::Z:
return NormalizeVector3(transform.GetForward(), Math::Vector3::Forward());
case SceneViewportScaleGizmoHandle::Uniform:
case SceneViewportScaleGizmoHandle::None:
default:
return Math::Vector3::Zero();
}
}
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;
}
bool IsPointInsideSquare(
const Math::Vector2& point,
const Math::Vector2& center,
float halfSize) {
return std::abs(point.x - center.x) <= halfSize &&
std::abs(point.y - center.y) <= halfSize;
}
float ClampPositiveScale(float value) {
return std::max(value, kScaleGizmoMinScale);
}
float ComputeVisualScaleFactor(float current, float start) {
if (std::abs(start) <= Math::EPSILON) {
return 1.0f;
}
return std::clamp(
current / start,
kScaleGizmoVisualScaleMin,
kScaleGizmoVisualScaleMax);
}
} // namespace
void SceneViewportScaleGizmo::Update(const SceneViewportScaleGizmoContext& context) {
BuildDrawData(context);
if (m_activeHandle == SceneViewportScaleGizmoHandle::None && IsMouseInsideViewport(context)) {
m_hoveredHandle = HitTestHandle(context.mousePosition);
} else if (m_activeHandle == SceneViewportScaleGizmoHandle::None) {
m_hoveredHandle = SceneViewportScaleGizmoHandle::None;
} else {
m_hoveredHandle = m_activeHandle;
}
RefreshHandleState();
}
bool SceneViewportScaleGizmo::TryBeginDrag(const SceneViewportScaleGizmoContext& context, IUndoManager& undoManager) {
if (m_activeHandle != SceneViewportScaleGizmoHandle::None ||
m_hoveredHandle == SceneViewportScaleGizmoHandle::None ||
context.selectedObject == nullptr ||
!m_drawData.visible ||
undoManager.HasPendingInteractiveChange()) {
return false;
}
Math::Vector2 activeScreenDirection = Math::Vector2::Zero();
if (m_hoveredHandle != SceneViewportScaleGizmoHandle::Uniform) {
const SceneViewportScaleGizmoAxisHandleDrawData* handle = FindAxisHandleDrawData(m_hoveredHandle);
if (handle == nullptr || !handle->visible) {
return false;
}
activeScreenDirection = handle->end - handle->start;
if (activeScreenDirection.SqrMagnitude() <= Math::EPSILON) {
const Math::Vector3 pivotWorldPosition = context.selectedObject->GetTransform()->GetPosition();
if (!ProjectSceneViewportAxisDirectionAtPoint(
context.overlay,
context.viewportSize.x,
context.viewportSize.y,
pivotWorldPosition,
GetHandleWorldAxis(m_hoveredHandle, *context.selectedObject->GetTransform()),
activeScreenDirection)) {
return false;
}
} else {
activeScreenDirection = activeScreenDirection.Normalized();
}
}
undoManager.BeginInteractiveChange("Scale Gizmo");
if (!undoManager.HasPendingInteractiveChange()) {
return false;
}
m_activeHandle = m_hoveredHandle;
m_activeEntityId = context.selectedObject->GetID();
m_dragStartLocalScale = context.selectedObject->GetTransform()->GetLocalScale();
m_dragCurrentVisualScale = Math::Vector3::One();
m_dragStartMousePosition = context.mousePosition;
m_activeScreenDirection = activeScreenDirection;
RefreshHandleState();
return true;
}
void SceneViewportScaleGizmo::UpdateDrag(const SceneViewportScaleGizmoContext& context) {
if (m_activeHandle == SceneViewportScaleGizmoHandle::None ||
context.selectedObject == nullptr ||
context.selectedObject->GetID() != m_activeEntityId) {
return;
}
const Math::Vector2 mouseDelta = context.mousePosition - m_dragStartMousePosition;
Math::Vector3 localScale = m_dragStartLocalScale;
if (m_activeHandle == SceneViewportScaleGizmoHandle::Uniform) {
const float signedPixels = mouseDelta.x - mouseDelta.y;
const float factor = std::max(1.0f + signedPixels * kScaleGizmoUniformScalePerPixel, kScaleGizmoMinScale);
localScale.x = ClampPositiveScale(m_dragStartLocalScale.x * factor);
localScale.y = ClampPositiveScale(m_dragStartLocalScale.y * factor);
localScale.z = ClampPositiveScale(m_dragStartLocalScale.z * factor);
} else {
if (m_activeScreenDirection.SqrMagnitude() <= Math::EPSILON) {
return;
}
const float signedPixels = Math::Vector2::Dot(mouseDelta, m_activeScreenDirection);
const float factor = std::max(1.0f + signedPixels * kScaleGizmoAxisScalePerPixel, kScaleGizmoMinScale);
switch (m_activeHandle) {
case SceneViewportScaleGizmoHandle::X:
localScale.x = ClampPositiveScale(m_dragStartLocalScale.x * factor);
break;
case SceneViewportScaleGizmoHandle::Y:
localScale.y = ClampPositiveScale(m_dragStartLocalScale.y * factor);
break;
case SceneViewportScaleGizmoHandle::Z:
localScale.z = ClampPositiveScale(m_dragStartLocalScale.z * factor);
break;
case SceneViewportScaleGizmoHandle::Uniform:
case SceneViewportScaleGizmoHandle::None:
default:
break;
}
}
context.selectedObject->GetTransform()->SetLocalScale(localScale);
switch (m_activeHandle) {
case SceneViewportScaleGizmoHandle::X:
m_dragCurrentVisualScale = Math::Vector3(
ComputeVisualScaleFactor(localScale.x, m_dragStartLocalScale.x),
1.0f,
1.0f);
break;
case SceneViewportScaleGizmoHandle::Y:
m_dragCurrentVisualScale = Math::Vector3(
1.0f,
ComputeVisualScaleFactor(localScale.y, m_dragStartLocalScale.y),
1.0f);
break;
case SceneViewportScaleGizmoHandle::Z:
m_dragCurrentVisualScale = Math::Vector3(
1.0f,
1.0f,
ComputeVisualScaleFactor(localScale.z, m_dragStartLocalScale.z));
break;
case SceneViewportScaleGizmoHandle::Uniform:
m_dragCurrentVisualScale = Math::Vector3(
ComputeVisualScaleFactor(localScale.x, m_dragStartLocalScale.x),
ComputeVisualScaleFactor(localScale.y, m_dragStartLocalScale.y),
ComputeVisualScaleFactor(localScale.z, m_dragStartLocalScale.z));
break;
case SceneViewportScaleGizmoHandle::None:
default:
m_dragCurrentVisualScale = Math::Vector3::One();
break;
}
}
void SceneViewportScaleGizmo::EndDrag(IUndoManager& undoManager) {
if (m_activeHandle == SceneViewportScaleGizmoHandle::None) {
return;
}
if (undoManager.HasPendingInteractiveChange()) {
undoManager.FinalizeInteractiveChange();
}
m_activeHandle = SceneViewportScaleGizmoHandle::None;
m_activeEntityId = 0;
m_dragStartLocalScale = Math::Vector3::Zero();
m_dragCurrentVisualScale = Math::Vector3::One();
m_dragStartMousePosition = Math::Vector2::Zero();
m_activeScreenDirection = Math::Vector2::Zero();
RefreshHandleState();
}
void SceneViewportScaleGizmo::CancelDrag(IUndoManager* undoManager) {
if (undoManager != nullptr && undoManager->HasPendingInteractiveChange()) {
undoManager->CancelInteractiveChange();
}
m_hoveredHandle = SceneViewportScaleGizmoHandle::None;
m_activeHandle = SceneViewportScaleGizmoHandle::None;
m_activeEntityId = 0;
m_dragStartLocalScale = Math::Vector3::Zero();
m_dragCurrentVisualScale = Math::Vector3::One();
m_dragStartMousePosition = Math::Vector2::Zero();
m_activeScreenDirection = Math::Vector2::Zero();
RefreshHandleState();
}
bool SceneViewportScaleGizmo::IsHoveringHandle() const {
return m_hoveredHandle != SceneViewportScaleGizmoHandle::None;
}
bool SceneViewportScaleGizmo::IsActive() const {
return m_activeHandle != SceneViewportScaleGizmoHandle::None;
}
uint64_t SceneViewportScaleGizmo::GetActiveEntityId() const {
return m_activeEntityId;
}
const SceneViewportScaleGizmoDrawData& SceneViewportScaleGizmo::GetDrawData() const {
return m_drawData;
}
void SceneViewportScaleGizmo::BuildDrawData(const SceneViewportScaleGizmoContext& 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.centerHandle.visible = true;
m_drawData.centerHandle.center = projectedPivot.screenPosition;
m_drawData.centerHandle.halfSize = kScaleGizmoCenterHalfSizePixels;
if (context.uniformOnly) {
return;
}
const bool hasVisualDragFeedback =
m_activeHandle != SceneViewportScaleGizmoHandle::None &&
selectedObject->GetID() == m_activeEntityId;
for (size_t index = 0; index < m_drawData.axisHandles.size(); ++index) {
SceneViewportScaleGizmoAxisHandleDrawData& handle = m_drawData.axisHandles[index];
handle.handle = GetHandleForIndex(index);
handle.start = projectedPivot.screenPosition;
handle.capHalfSize = kScaleGizmoCapHalfSizePixels;
handle.color = GetScaleHandleBaseColor(handle.handle);
const Math::Vector3 axisWorld =
GetHandleWorldAxis(handle.handle, *selectedObject->GetTransform());
if (axisWorld.SqrMagnitude() <= Math::EPSILON) {
continue;
}
float axisVisualScale = 1.0f;
if (hasVisualDragFeedback) {
switch (handle.handle) {
case SceneViewportScaleGizmoHandle::X:
axisVisualScale = m_dragCurrentVisualScale.x;
break;
case SceneViewportScaleGizmoHandle::Y:
axisVisualScale = m_dragCurrentVisualScale.y;
break;
case SceneViewportScaleGizmoHandle::Z:
axisVisualScale = m_dragCurrentVisualScale.z;
break;
case SceneViewportScaleGizmoHandle::Uniform:
case SceneViewportScaleGizmoHandle::None:
default:
break;
}
}
const float axisLengthWorld =
worldUnitsPerPixel * kScaleGizmoAxisLengthPixels * axisVisualScale;
const SceneViewportProjectedPoint projectedEnd = ProjectSceneViewportWorldPoint(
context.overlay,
context.viewportSize.x,
context.viewportSize.y,
pivotWorldPosition + axisWorld * axisLengthWorld);
if (projectedEnd.ndcDepth < 0.0f || projectedEnd.ndcDepth > 1.0f) {
continue;
}
if ((projectedEnd.screenPosition - projectedPivot.screenPosition).SqrMagnitude() <= Math::EPSILON) {
continue;
}
handle.visible = true;
handle.end = projectedEnd.screenPosition;
handle.capCenter = projectedEnd.screenPosition;
}
}
void SceneViewportScaleGizmo::RefreshHandleState() {
for (SceneViewportScaleGizmoAxisHandleDrawData& handle : m_drawData.axisHandles) {
if (!handle.visible) {
continue;
}
handle.hovered = handle.handle == m_hoveredHandle;
handle.active = handle.handle == m_activeHandle;
handle.color = (handle.hovered || handle.active)
? Math::Color::Yellow()
: GetScaleHandleBaseColor(handle.handle);
}
if (!m_drawData.centerHandle.visible) {
return;
}
m_drawData.centerHandle.hovered = m_hoveredHandle == SceneViewportScaleGizmoHandle::Uniform;
m_drawData.centerHandle.active = m_activeHandle == SceneViewportScaleGizmoHandle::Uniform;
const Math::Color baseColor =
(m_drawData.centerHandle.hovered || m_drawData.centerHandle.active)
? Math::Color::Yellow()
: GetScaleHandleBaseColor(SceneViewportScaleGizmoHandle::Uniform);
m_drawData.centerHandle.fillColor = WithAlpha(
baseColor,
m_drawData.centerHandle.active ? 0.96f : (m_drawData.centerHandle.hovered ? 0.9f : 0.82f));
m_drawData.centerHandle.outlineColor = WithAlpha(
baseColor,
m_drawData.centerHandle.active ? 1.0f : (m_drawData.centerHandle.hovered ? 0.96f : 0.88f));
}
SceneViewportScaleGizmoHandle SceneViewportScaleGizmo::HitTestHandle(const Math::Vector2& mousePosition) const {
if (!m_drawData.visible) {
return SceneViewportScaleGizmoHandle::None;
}
if (m_drawData.centerHandle.visible &&
IsPointInsideSquare(
mousePosition,
m_drawData.centerHandle.center,
m_drawData.centerHandle.halfSize + 2.0f)) {
return SceneViewportScaleGizmoHandle::Uniform;
}
SceneViewportScaleGizmoHandle bestHandle = SceneViewportScaleGizmoHandle::None;
float bestDistanceSq = Math::FLOAT_MAX;
for (const SceneViewportScaleGizmoAxisHandleDrawData& handle : m_drawData.axisHandles) {
if (!handle.visible ||
!IsPointInsideSquare(mousePosition, handle.capCenter, handle.capHalfSize + 2.0f)) {
continue;
}
const float distanceSq = (handle.capCenter - mousePosition).SqrMagnitude();
if (distanceSq >= bestDistanceSq) {
continue;
}
bestDistanceSq = distanceSq;
bestHandle = handle.handle;
}
if (bestHandle != SceneViewportScaleGizmoHandle::None) {
return bestHandle;
}
bestDistanceSq = kScaleGizmoHoverThresholdPixels * kScaleGizmoHoverThresholdPixels;
for (const SceneViewportScaleGizmoAxisHandleDrawData& handle : m_drawData.axisHandles) {
if (!handle.visible) {
continue;
}
const float distanceSq = DistanceToSegmentSquared(mousePosition, handle.start, handle.end);
if (distanceSq > bestDistanceSq) {
continue;
}
bestDistanceSq = distanceSq;
bestHandle = handle.handle;
}
return bestHandle;
}
const SceneViewportScaleGizmoAxisHandleDrawData* SceneViewportScaleGizmo::FindAxisHandleDrawData(
SceneViewportScaleGizmoHandle handle) const {
for (const SceneViewportScaleGizmoAxisHandleDrawData& drawHandle : m_drawData.axisHandles) {
if (drawHandle.handle == handle) {
return &drawHandle;
}
}
return nullptr;
}
} // namespace Editor
} // namespace XCEngine

View File

@@ -0,0 +1,96 @@
#pragma once
#include "IViewportHostService.h"
#include <XCEngine/Core/Math/Color.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 SceneViewportScaleGizmoHandle : uint8_t {
None = 0,
X,
Y,
Z,
Uniform
};
struct SceneViewportScaleGizmoAxisHandleDrawData {
SceneViewportScaleGizmoHandle handle = SceneViewportScaleGizmoHandle::None;
Math::Vector2 start = Math::Vector2::Zero();
Math::Vector2 end = Math::Vector2::Zero();
Math::Vector2 capCenter = Math::Vector2::Zero();
float capHalfSize = 0.0f;
Math::Color color = Math::Color::White();
bool visible = false;
bool hovered = false;
bool active = false;
};
struct SceneViewportScaleGizmoCenterHandleDrawData {
Math::Vector2 center = Math::Vector2::Zero();
float halfSize = 0.0f;
Math::Color fillColor = Math::Color::White();
Math::Color outlineColor = Math::Color::White();
bool visible = false;
bool hovered = false;
bool active = false;
};
struct SceneViewportScaleGizmoDrawData {
bool visible = false;
std::array<SceneViewportScaleGizmoAxisHandleDrawData, 3> axisHandles = {};
SceneViewportScaleGizmoCenterHandleDrawData centerHandle = {};
};
struct SceneViewportScaleGizmoContext {
SceneViewportOverlayData overlay = {};
Math::Vector2 viewportSize = Math::Vector2::Zero();
Math::Vector2 mousePosition = Math::Vector2::Zero();
Components::GameObject* selectedObject = nullptr;
bool uniformOnly = false;
};
class SceneViewportScaleGizmo {
public:
void Update(const SceneViewportScaleGizmoContext& context);
bool TryBeginDrag(const SceneViewportScaleGizmoContext& context, IUndoManager& undoManager);
void UpdateDrag(const SceneViewportScaleGizmoContext& context);
void EndDrag(IUndoManager& undoManager);
void CancelDrag(IUndoManager* undoManager = nullptr);
bool IsHoveringHandle() const;
bool IsActive() const;
uint64_t GetActiveEntityId() const;
const SceneViewportScaleGizmoDrawData& GetDrawData() const;
private:
void BuildDrawData(const SceneViewportScaleGizmoContext& context);
void RefreshHandleState();
SceneViewportScaleGizmoHandle HitTestHandle(const Math::Vector2& mousePosition) const;
const SceneViewportScaleGizmoAxisHandleDrawData* FindAxisHandleDrawData(
SceneViewportScaleGizmoHandle handle) const;
SceneViewportScaleGizmoDrawData m_drawData = {};
SceneViewportScaleGizmoHandle m_hoveredHandle = SceneViewportScaleGizmoHandle::None;
SceneViewportScaleGizmoHandle m_activeHandle = SceneViewportScaleGizmoHandle::None;
uint64_t m_activeEntityId = 0;
Math::Vector3 m_dragStartLocalScale = Math::Vector3::Zero();
Math::Vector3 m_dragCurrentVisualScale = Math::Vector3::One();
Math::Vector2 m_dragStartMousePosition = Math::Vector2::Zero();
Math::Vector2 m_activeScreenDirection = Math::Vector2::Zero();
};
} // namespace Editor
} // namespace XCEngine

View File

@@ -6,15 +6,205 @@
#include "Viewport/SceneViewportOrientationGizmo.h"
#include "Viewport/SceneViewportOverlayRenderer.h"
#include "ViewportPanelContent.h"
#include "Platform/Win32Utf8.h"
#include "UI/UI.h"
#include <XCEngine/Debug/Logger.h>
#include <imgui.h>
#include <cstdarg>
#include <cstdio>
#include <filesystem>
namespace XCEngine {
namespace Editor {
namespace {
struct SceneViewportToolOverlayResult {
bool hovered = false;
bool clicked = false;
SceneViewportToolMode clickedTool = SceneViewportToolMode::Move;
};
enum class SceneViewportActiveGizmoKind : uint8_t {
None = 0,
Move,
Rotate,
Scale
};
const char* GetSceneViewportToolTooltip(SceneViewportToolMode toolMode) {
switch (toolMode) {
case SceneViewportToolMode::ViewMove:
return "View Move";
case SceneViewportToolMode::Move:
return "Move";
case SceneViewportToolMode::Rotate:
return "Rotate";
case SceneViewportToolMode::Scale:
return "Scale";
case SceneViewportToolMode::Transform:
return "Transform";
default:
return "";
}
}
const char* GetSceneViewportToolIconBaseName(SceneViewportToolMode toolMode) {
switch (toolMode) {
case SceneViewportToolMode::ViewMove:
return "view_move_tool";
case SceneViewportToolMode::Move:
return "move_tool";
case SceneViewportToolMode::Rotate:
return "rotate_tool";
case SceneViewportToolMode::Scale:
return "scale_tool";
case SceneViewportToolMode::Transform:
return "transform_tool";
default:
return "";
}
}
const std::string& GetSceneViewportToolIconPath(SceneViewportToolMode toolMode, bool active) {
static std::string cachedPaths[5][2] = {};
const size_t toolIndex = static_cast<size_t>(toolMode);
const size_t stateIndex = active ? 1u : 0u;
std::string& cachedPath = cachedPaths[toolIndex][stateIndex];
if (!cachedPath.empty()) {
return cachedPath;
}
const std::filesystem::path exeDir(
XCEngine::Editor::Platform::Utf8ToWide(XCEngine::Editor::Platform::GetExecutableDirectoryUtf8()));
std::filesystem::path iconPath =
(exeDir / L".." / L".." / L"resources" / L"Icons" /
std::filesystem::path(XCEngine::Editor::Platform::Utf8ToWide(GetSceneViewportToolIconBaseName(toolMode))));
if (active) {
iconPath += L"_on";
}
iconPath += L".png";
cachedPath = XCEngine::Editor::Platform::WideToUtf8(iconPath.lexically_normal().wstring());
return cachedPath;
}
SceneViewportToolOverlayResult RenderSceneViewportToolOverlay(
const ViewportPanelContentResult& content,
SceneViewportToolMode activeTool) {
SceneViewportToolOverlayResult result = {};
if (!content.hasViewportArea) {
return result;
}
constexpr float kButtonExtent = 30.0f;
constexpr float kButtonSpacing = 6.0f;
constexpr float kPanelPadding = 6.0f;
constexpr float kViewportInset = 10.0f;
constexpr float kIconInset = 4.0f;
constexpr size_t kToolCount = 5;
ImDrawList* drawList = ImGui::GetWindowDrawList();
if (drawList == nullptr) {
return result;
}
const ImVec2 panelMin(
content.itemMin.x + kViewportInset,
content.itemMin.y + kViewportInset);
const ImVec2 panelMax(
panelMin.x + kPanelPadding * 2.0f + kButtonExtent,
panelMin.y + kPanelPadding * 2.0f + kToolCount * kButtonExtent + (kToolCount - 1) * kButtonSpacing);
drawList->AddRectFilled(panelMin, panelMax, IM_COL32(24, 26, 29, 220), 7.0f);
drawList->AddRect(panelMin, panelMax, IM_COL32(255, 255, 255, 28), 7.0f, 0, 1.0f);
const SceneViewportToolMode toolModes[kToolCount] = {
SceneViewportToolMode::ViewMove,
SceneViewportToolMode::Move,
SceneViewportToolMode::Rotate,
SceneViewportToolMode::Scale,
SceneViewportToolMode::Transform
};
for (size_t index = 0; index < kToolCount; ++index) {
const SceneViewportToolMode toolMode = toolModes[index];
const bool active = toolMode == activeTool;
const ImVec2 buttonMin(
panelMin.x + kPanelPadding,
panelMin.y + kPanelPadding + index * (kButtonExtent + kButtonSpacing));
const ImVec2 buttonMax(buttonMin.x + kButtonExtent, buttonMin.y + kButtonExtent);
ImGui::SetCursorScreenPos(buttonMin);
const std::string buttonId = std::string("##SceneToolButton") + std::to_string(static_cast<int>(toolMode));
const bool clicked = ImGui::InvisibleButton(
buttonId.c_str(),
ImVec2(kButtonExtent, kButtonExtent),
ImGuiButtonFlags_MouseButtonLeft |
ImGuiButtonFlags_PressedOnClick |
ImGuiButtonFlags_AllowOverlap);
const bool hovered = ImGui::IsItemHovered();
const bool held = ImGui::IsItemActive();
result.hovered = result.hovered || hovered;
result.clicked = result.clicked || clicked;
if (clicked) {
result.clickedTool = toolMode;
}
const ImU32 backgroundColor = ImGui::GetColorU32(
held ? UI::ToolbarButtonActiveColor()
: hovered ? UI::ToolbarButtonHoveredColor(active)
: UI::ToolbarButtonColor(active));
drawList->AddRectFilled(buttonMin, buttonMax, backgroundColor, 5.0f);
drawList->AddRect(buttonMin, buttonMax, IM_COL32(255, 255, 255, active ? 48 : 24), 5.0f, 0, 1.0f);
const ImVec2 iconMin(buttonMin.x + kIconInset, buttonMin.y + kIconInset);
const ImVec2 iconMax(buttonMax.x - kIconInset, buttonMax.y - kIconInset);
if (!UI::DrawTextureAssetPreview(
drawList,
iconMin,
iconMax,
GetSceneViewportToolIconPath(toolMode, active))) {
drawList->AddText(
ImVec2(buttonMin.x + 8.0f, buttonMin.y + 7.0f),
IM_COL32(230, 230, 230, 255),
GetSceneViewportToolTooltip(toolMode));
}
if (hovered) {
ImGui::SetTooltip("%s", GetSceneViewportToolTooltip(toolMode));
}
}
return result;
}
void LogSceneViewNavigation(Debug::Logger& logger, const char* format, ...) {
char buffer[512] = {};
va_list args;
va_start(args, format);
std::vsnprintf(buffer, sizeof(buffer), format, args);
va_end(args);
logger.Info(Debug::LogCategory::General, buffer);
}
bool ShouldBeginSceneViewportNavigationDrag(
bool hasInteractiveViewport,
bool hovered,
bool activeDrag,
bool otherDrag,
bool gizmoActive,
ImGuiMouseButton button) {
return hasInteractiveViewport &&
hovered &&
!activeDrag &&
!otherDrag &&
!gizmoActive &&
ImGui::IsMouseClicked(button);
}
SceneViewportMoveGizmoContext BuildMoveGizmoContext(
IEditorContext& context,
const SceneViewportOverlayData& overlay,
@@ -59,6 +249,28 @@ SceneViewportRotateGizmoContext BuildRotateGizmoContext(
return gizmoContext;
}
SceneViewportScaleGizmoContext BuildScaleGizmoContext(
IEditorContext& context,
const SceneViewportOverlayData& overlay,
const ViewportPanelContentResult& content,
const ImVec2& mousePosition) {
SceneViewportScaleGizmoContext 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") {}
@@ -74,53 +286,130 @@ void SceneViewPanel::Render() {
const ViewportPanelContentResult content = RenderViewportPanelContent(*m_context, EditorViewportKind::Scene);
if (IViewportHostService* viewportHostService = m_context->GetViewportHostService()) {
const ImGuiIO& io = ImGui::GetIO();
auto& logger = Debug::Logger::Get();
const bool hasInteractiveViewport = content.hasViewportArea && content.frame.hasTexture;
const SceneViewportToolOverlayResult toolOverlay = RenderSceneViewportToolOverlay(content, m_toolMode);
const bool viewportContentHovered = content.hovered && !toolOverlay.hovered;
if (toolOverlay.clicked) {
if (m_moveGizmo.IsActive()) {
m_moveGizmo.CancelDrag(&m_context->GetUndoManager());
}
if (m_rotateGizmo.IsActive()) {
m_rotateGizmo.CancelDrag(&m_context->GetUndoManager());
}
if (m_scaleGizmo.IsActive()) {
m_scaleGizmo.CancelDrag(&m_context->GetUndoManager());
}
m_toolMode = toolOverlay.clickedTool;
}
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;
if (m_scaleGizmo.IsActive()) {
m_scaleGizmo.CancelDrag(&m_context->GetUndoManager());
}
m_toolMode = SceneViewportToolMode::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();
if (usingMoveGizmo) {
if (m_scaleGizmo.IsActive()) {
m_scaleGizmo.CancelDrag(&m_context->GetUndoManager());
}
m_toolMode = SceneViewportToolMode::Rotate;
} else if (ImGui::IsKeyPressed(ImGuiKey_R, false)) {
if (m_moveGizmo.IsActive()) {
m_moveGizmo.CancelDrag(&m_context->GetUndoManager());
}
if (m_rotateGizmo.IsActive()) {
m_rotateGizmo.CancelDrag(&m_context->GetUndoManager());
}
m_toolMode = SceneViewportToolMode::Scale;
}
}
const bool usingViewMoveTool = m_toolMode == SceneViewportToolMode::ViewMove;
const bool usingTransformTool = m_toolMode == SceneViewportToolMode::Transform;
const bool showingMoveGizmo = m_toolMode == SceneViewportToolMode::Move || usingTransformTool;
const bool showingRotateGizmo = m_toolMode == SceneViewportToolMode::Rotate || usingTransformTool;
const bool showingScaleGizmo = m_toolMode == SceneViewportToolMode::Scale || usingTransformTool;
SceneViewportOverlayData overlay = {};
SceneViewportMoveGizmoContext moveGizmoContext = {};
SceneViewportRotateGizmoContext rotateGizmoContext = {};
SceneViewportScaleGizmoContext scaleGizmoContext = {};
SceneViewportActiveGizmoKind activeGizmoKind = SceneViewportActiveGizmoKind::None;
if (hasInteractiveViewport) {
overlay = viewportHostService->GetSceneViewOverlayData();
if (showingMoveGizmo) {
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());
}
} else if (m_moveGizmo.IsActive()) {
m_moveGizmo.CancelDrag(&m_context->GetUndoManager());
}
if (showingRotateGizmo) {
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_rotateGizmo.IsActive()) {
m_rotateGizmo.CancelDrag(&m_context->GetUndoManager());
}
if (showingScaleGizmo) {
scaleGizmoContext = BuildScaleGizmoContext(*m_context, overlay, content, io.MousePos);
scaleGizmoContext.uniformOnly = usingTransformTool;
if (m_scaleGizmo.IsActive() &&
(scaleGizmoContext.selectedObject == nullptr ||
m_context->GetSelectionManager().GetSelectedEntity() != m_scaleGizmo.GetActiveEntityId())) {
m_scaleGizmo.CancelDrag(&m_context->GetUndoManager());
}
} else if (m_scaleGizmo.IsActive()) {
m_scaleGizmo.CancelDrag(&m_context->GetUndoManager());
}
if (m_moveGizmo.IsActive()) {
activeGizmoKind = SceneViewportActiveGizmoKind::Move;
} else if (m_rotateGizmo.IsActive()) {
activeGizmoKind = SceneViewportActiveGizmoKind::Rotate;
} else if (m_scaleGizmo.IsActive()) {
activeGizmoKind = SceneViewportActiveGizmoKind::Scale;
}
if (showingMoveGizmo) {
SceneViewportMoveGizmoContext updateContext = moveGizmoContext;
if (activeGizmoKind != SceneViewportActiveGizmoKind::None &&
activeGizmoKind != SceneViewportActiveGizmoKind::Move) {
updateContext.mousePosition = Math::Vector2(-1.0f, -1.0f);
}
m_moveGizmo.Update(updateContext);
}
if (showingRotateGizmo) {
SceneViewportRotateGizmoContext updateContext = rotateGizmoContext;
if (activeGizmoKind != SceneViewportActiveGizmoKind::None &&
activeGizmoKind != SceneViewportActiveGizmoKind::Rotate) {
updateContext.mousePosition = Math::Vector2(-1.0f, -1.0f);
}
m_rotateGizmo.Update(updateContext);
}
if (showingScaleGizmo) {
SceneViewportScaleGizmoContext updateContext = scaleGizmoContext;
if (activeGizmoKind != SceneViewportActiveGizmoKind::None &&
activeGizmoKind != SceneViewportActiveGizmoKind::Scale) {
updateContext.mousePosition = Math::Vector2(-1.0f, -1.0f);
}
m_scaleGizmo.Update(updateContext);
}
} else {
if (m_moveGizmo.IsActive()) {
@@ -129,20 +418,45 @@ void SceneViewPanel::Render() {
if (m_rotateGizmo.IsActive()) {
m_rotateGizmo.CancelDrag(&m_context->GetUndoManager());
}
if (m_scaleGizmo.IsActive()) {
m_scaleGizmo.CancelDrag(&m_context->GetUndoManager());
}
}
const bool gizmoHovering = usingMoveGizmo ? m_moveGizmo.IsHoveringHandle() : m_rotateGizmo.IsHoveringHandle();
const bool gizmoActive = usingMoveGizmo ? m_moveGizmo.IsActive() : m_rotateGizmo.IsActive();
const bool moveGizmoHovering = showingMoveGizmo && m_moveGizmo.IsHoveringHandle();
const bool rotateGizmoHovering = showingRotateGizmo && m_rotateGizmo.IsHoveringHandle();
const bool scaleGizmoHovering = showingScaleGizmo && m_scaleGizmo.IsHoveringHandle();
const bool moveGizmoActive = showingMoveGizmo && m_moveGizmo.IsActive();
const bool rotateGizmoActive = showingRotateGizmo && m_rotateGizmo.IsActive();
const bool scaleGizmoActive = showingScaleGizmo && m_scaleGizmo.IsActive();
const SceneViewportActiveGizmoKind hoveredGizmoKind = scaleGizmoHovering
? SceneViewportActiveGizmoKind::Scale
: (moveGizmoHovering ? SceneViewportActiveGizmoKind::Move
: (rotateGizmoHovering ? SceneViewportActiveGizmoKind::Rotate
: SceneViewportActiveGizmoKind::None));
if (moveGizmoActive) {
activeGizmoKind = SceneViewportActiveGizmoKind::Move;
} else if (rotateGizmoActive) {
activeGizmoKind = SceneViewportActiveGizmoKind::Rotate;
} else if (scaleGizmoActive) {
activeGizmoKind = SceneViewportActiveGizmoKind::Scale;
} else {
activeGizmoKind = SceneViewportActiveGizmoKind::None;
}
const bool gizmoHovering = hoveredGizmoKind != SceneViewportActiveGizmoKind::None;
const bool gizmoActive = activeGizmoKind != SceneViewportActiveGizmoKind::None;
const bool beginTransformGizmo =
hasInteractiveViewport &&
content.clickedLeft &&
!m_lookDragging &&
!m_panDragging &&
!toolOverlay.hovered &&
gizmoHovering;
const SceneViewportOrientationAxis orientationAxisHit =
hasInteractiveViewport &&
content.hovered &&
viewportContentHovered &&
!usingViewMoveTool &&
!m_lookDragging &&
!m_panDragging &&
!gizmoHovering &&
@@ -160,33 +474,68 @@ void SceneViewPanel::Render() {
const bool selectClick =
hasInteractiveViewport &&
content.clickedLeft &&
viewportContentHovered &&
!usingViewMoveTool &&
!m_lookDragging &&
!m_panDragging &&
!orientationGizmoClick &&
!gizmoHovering &&
!gizmoActive;
const bool beginLookDrag =
hasInteractiveViewport &&
content.hovered &&
!m_lookDragging &&
!gizmoActive &&
ImGui::IsMouseClicked(ImGuiMouseButton_Right);
const bool beginPanDrag =
hasInteractiveViewport &&
content.hovered &&
!m_panDragging &&
!gizmoActive &&
!m_lookDragging &&
ImGui::IsMouseClicked(ImGuiMouseButton_Middle);
const bool beginLeftPanDrag = usingViewMoveTool
? ShouldBeginSceneViewportNavigationDrag(
hasInteractiveViewport,
viewportContentHovered,
m_panDragging,
m_lookDragging,
gizmoActive,
ImGuiMouseButton_Left)
: false;
const bool beginLookDrag = ShouldBeginSceneViewportNavigationDrag(
hasInteractiveViewport,
viewportContentHovered,
m_lookDragging,
m_panDragging,
gizmoActive,
ImGuiMouseButton_Right);
const bool beginMiddlePanDrag = ShouldBeginSceneViewportNavigationDrag(
hasInteractiveViewport,
viewportContentHovered,
m_panDragging,
m_lookDragging,
gizmoActive,
ImGuiMouseButton_Middle);
const bool beginPanDrag = beginLeftPanDrag || beginMiddlePanDrag;
if (beginTransformGizmo || orientationGizmoClick || selectClick || beginLookDrag || beginPanDrag) {
if (ImGui::IsMouseClicked(ImGuiMouseButton_Right) || ImGui::IsMouseClicked(ImGuiMouseButton_Middle)) {
LogSceneViewNavigation(
logger,
"SceneView nav click hovered=%d focused=%d hasViewport=%d clickedR=%d clickedM=%d downR=%d downM=%d "
"look=%d pan=%d gizmoActive=%d ioDelta=(%.2f, %.2f)",
content.hovered ? 1 : 0,
content.focused ? 1 : 0,
hasInteractiveViewport ? 1 : 0,
ImGui::IsMouseClicked(ImGuiMouseButton_Right) ? 1 : 0,
ImGui::IsMouseClicked(ImGuiMouseButton_Middle) ? 1 : 0,
ImGui::IsMouseDown(ImGuiMouseButton_Right) ? 1 : 0,
ImGui::IsMouseDown(ImGuiMouseButton_Middle) ? 1 : 0,
m_lookDragging ? 1 : 0,
m_panDragging ? 1 : 0,
gizmoActive ? 1 : 0,
io.MouseDelta.x,
io.MouseDelta.y);
}
if (toolOverlay.clicked || beginTransformGizmo || orientationGizmoClick || selectClick || beginLookDrag ||
beginPanDrag) {
ImGui::SetWindowFocus();
}
if (beginTransformGizmo) {
if (usingMoveGizmo) {
if (hoveredGizmoKind == SceneViewportActiveGizmoKind::Scale) {
m_scaleGizmo.TryBeginDrag(scaleGizmoContext, m_context->GetUndoManager());
} else if (hoveredGizmoKind == SceneViewportActiveGizmoKind::Move) {
m_moveGizmo.TryBeginDrag(moveGizmoContext, m_context->GetUndoManager());
} else {
} else if (hoveredGizmoKind == SceneViewportActiveGizmoKind::Rotate) {
m_rotateGizmo.TryBeginDrag(rotateGizmoContext, m_context->GetUndoManager());
}
}
@@ -194,13 +543,19 @@ void SceneViewPanel::Render() {
if (orientationGizmoClick) {
viewportHostService->AlignSceneViewToOrientationAxis(orientationAxisHit);
overlay = viewportHostService->GetSceneViewOverlayData();
if (usingMoveGizmo) {
if (showingMoveGizmo) {
moveGizmoContext = BuildMoveGizmoContext(*m_context, overlay, content, io.MousePos);
m_moveGizmo.Update(moveGizmoContext);
} else {
}
if (showingRotateGizmo) {
rotateGizmoContext = BuildRotateGizmoContext(*m_context, overlay, content, io.MousePos);
m_rotateGizmo.Update(rotateGizmoContext);
}
if (showingScaleGizmo) {
scaleGizmoContext = BuildScaleGizmoContext(*m_context, overlay, content, io.MousePos);
scaleGizmoContext.uniformOnly = usingTransformTool;
m_scaleGizmo.Update(scaleGizmoContext);
}
}
if (selectClick) {
@@ -220,16 +575,20 @@ void SceneViewPanel::Render() {
if (gizmoActive) {
if (ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
if (usingMoveGizmo) {
if (activeGizmoKind == SceneViewportActiveGizmoKind::Move) {
m_moveGizmo.UpdateDrag(moveGizmoContext);
} else {
} else if (activeGizmoKind == SceneViewportActiveGizmoKind::Rotate) {
m_rotateGizmo.UpdateDrag(rotateGizmoContext);
} else if (activeGizmoKind == SceneViewportActiveGizmoKind::Scale) {
m_scaleGizmo.UpdateDrag(scaleGizmoContext);
}
} else {
if (usingMoveGizmo) {
if (activeGizmoKind == SceneViewportActiveGizmoKind::Move) {
m_moveGizmo.EndDrag(m_context->GetUndoManager());
} else {
} else if (activeGizmoKind == SceneViewportActiveGizmoKind::Rotate) {
m_rotateGizmo.EndDrag(m_context->GetUndoManager());
} else if (activeGizmoKind == SceneViewportActiveGizmoKind::Scale) {
m_scaleGizmo.EndDrag(m_context->GetUndoManager());
}
}
}
@@ -237,26 +596,49 @@ void SceneViewPanel::Render() {
if (beginLookDrag) {
m_lookDragging = true;
m_panDragging = false;
m_lastLookDragDelta = ImVec2(0.0f, 0.0f);
m_lastPanDragDelta = ImVec2(0.0f, 0.0f);
m_loggedLookDelta = false;
m_loggedPanDelta = false;
LogSceneViewNavigation(
logger,
"SceneView begin look drag hovered=%d focused=%d downR=%d downM=%d gizmoActive=%d",
content.hovered ? 1 : 0,
content.focused ? 1 : 0,
ImGui::IsMouseDown(ImGuiMouseButton_Right) ? 1 : 0,
ImGui::IsMouseDown(ImGuiMouseButton_Middle) ? 1 : 0,
gizmoActive ? 1 : 0);
}
if (beginPanDrag) {
m_panDragging = true;
m_lookDragging = false;
m_lastPanDragDelta = ImVec2(0.0f, 0.0f);
m_lastLookDragDelta = ImVec2(0.0f, 0.0f);
m_panDragButton = beginLeftPanDrag ? ImGuiMouseButton_Left : ImGuiMouseButton_Middle;
m_loggedPanDelta = false;
m_loggedLookDelta = false;
LogSceneViewNavigation(
logger,
"SceneView begin pan drag hovered=%d focused=%d button=%d downR=%d downM=%d downL=%d gizmoActive=%d",
content.hovered ? 1 : 0,
content.focused ? 1 : 0,
m_panDragButton,
ImGui::IsMouseDown(ImGuiMouseButton_Right) ? 1 : 0,
ImGui::IsMouseDown(ImGuiMouseButton_Middle) ? 1 : 0,
ImGui::IsMouseDown(ImGuiMouseButton_Left) ? 1 : 0,
gizmoActive ? 1 : 0);
}
if (m_lookDragging && !ImGui::IsMouseDown(ImGuiMouseButton_Right)) {
LogSceneViewNavigation(logger, "SceneView end look drag");
m_lookDragging = false;
m_lastLookDragDelta = ImVec2(0.0f, 0.0f);
m_loggedLookDelta = false;
}
if (m_panDragging && !ImGui::IsMouseDown(ImGuiMouseButton_Middle)) {
if (m_panDragging && !ImGui::IsMouseDown(m_panDragButton)) {
LogSceneViewNavigation(logger, "SceneView end pan drag");
m_panDragging = false;
m_lastPanDragDelta = ImVec2(0.0f, 0.0f);
m_panDragButton = ImGuiMouseButton_Middle;
m_loggedPanDelta = false;
}
if (m_lookDragging || m_panDragging || m_moveGizmo.IsActive() || m_rotateGizmo.IsActive()) {
if (m_lookDragging || m_panDragging || m_moveGizmo.IsActive() || m_rotateGizmo.IsActive() ||
m_scaleGizmo.IsActive()) {
ImGui::SetNextFrameWantCaptureMouse(true);
}
if (m_lookDragging) {
@@ -266,10 +648,10 @@ void SceneViewPanel::Render() {
SceneViewportInput input = {};
input.viewportSize = content.availableSize;
input.deltaTime = io.DeltaTime;
input.hovered = content.hovered;
input.hovered = viewportContentHovered;
input.focused = content.focused || m_lookDragging || m_panDragging;
input.mouseWheel = (content.hovered && !m_lookDragging) ? io.MouseWheel : 0.0f;
input.flySpeedDelta = (content.hovered && m_lookDragging) ? io.MouseWheel : 0.0f;
input.mouseWheel = (viewportContentHovered && !m_lookDragging) ? io.MouseWheel : 0.0f;
input.flySpeedDelta = (viewportContentHovered && m_lookDragging) ? io.MouseWheel : 0.0f;
input.looking = m_lookDragging;
input.orbiting = false;
input.panning = m_panDragging;
@@ -291,29 +673,33 @@ void SceneViewPanel::Render() {
if (m_lookDragging || m_panDragging) {
if (m_lookDragging) {
const ImVec2 lookDragDelta = ImGui::GetMouseDragDelta(ImGuiMouseButton_Right, 0.0f);
input.mouseDelta.x += lookDragDelta.x - m_lastLookDragDelta.x;
input.mouseDelta.y += lookDragDelta.y - m_lastLookDragDelta.y;
m_lastLookDragDelta = lookDragDelta;
} else {
m_lastLookDragDelta = ImVec2(0.0f, 0.0f);
input.mouseDelta = io.MouseDelta;
if (!m_loggedLookDelta &&
(input.mouseDelta.x != 0.0f || input.mouseDelta.y != 0.0f)) {
LogSceneViewNavigation(
logger,
"SceneView look delta=(%.2f, %.2f) hovered=%d downR=%d",
input.mouseDelta.x,
input.mouseDelta.y,
content.hovered ? 1 : 0,
ImGui::IsMouseDown(ImGuiMouseButton_Right) ? 1 : 0);
m_loggedLookDelta = true;
}
}
if (m_panDragging) {
const ImVec2 panDragDelta = ImGui::GetMouseDragDelta(ImGuiMouseButton_Middle, 0.0f);
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 = io.MouseDelta;
if (!m_loggedPanDelta &&
(input.mouseDelta.x != 0.0f || input.mouseDelta.y != 0.0f)) {
LogSceneViewNavigation(
logger,
"SceneView pan delta=(%.2f, %.2f) hovered=%d downM=%d",
input.mouseDelta.x,
input.mouseDelta.y,
content.hovered ? 1 : 0,
ImGui::IsMouseDown(ImGuiMouseButton_Middle) ? 1 : 0);
m_loggedPanDelta = true;
}
input.mouseDelta.x += framePanDelta.x;
input.mouseDelta.y += framePanDelta.y;
m_lastPanDragDelta = panDragDelta;
} else {
m_lastPanDragDelta = ImVec2(0.0f, 0.0f);
}
}
@@ -321,12 +707,41 @@ void SceneViewPanel::Render() {
if (content.hasViewportArea && content.frame.hasTexture) {
overlay = viewportHostService->GetSceneViewOverlayData();
if (usingMoveGizmo) {
SceneViewportActiveGizmoKind drawActiveGizmoKind = SceneViewportActiveGizmoKind::None;
if (m_moveGizmo.IsActive()) {
drawActiveGizmoKind = SceneViewportActiveGizmoKind::Move;
} else if (m_rotateGizmo.IsActive()) {
drawActiveGizmoKind = SceneViewportActiveGizmoKind::Rotate;
} else if (m_scaleGizmo.IsActive()) {
drawActiveGizmoKind = SceneViewportActiveGizmoKind::Scale;
}
if (showingMoveGizmo) {
moveGizmoContext = BuildMoveGizmoContext(*m_context, overlay, content, io.MousePos);
m_moveGizmo.Update(moveGizmoContext);
} else {
SceneViewportMoveGizmoContext updateContext = moveGizmoContext;
if (drawActiveGizmoKind != SceneViewportActiveGizmoKind::None &&
drawActiveGizmoKind != SceneViewportActiveGizmoKind::Move) {
updateContext.mousePosition = Math::Vector2(-1.0f, -1.0f);
}
m_moveGizmo.Update(updateContext);
}
if (showingRotateGizmo) {
rotateGizmoContext = BuildRotateGizmoContext(*m_context, overlay, content, io.MousePos);
m_rotateGizmo.Update(rotateGizmoContext);
SceneViewportRotateGizmoContext updateContext = rotateGizmoContext;
if (drawActiveGizmoKind != SceneViewportActiveGizmoKind::None &&
drawActiveGizmoKind != SceneViewportActiveGizmoKind::Rotate) {
updateContext.mousePosition = Math::Vector2(-1.0f, -1.0f);
}
m_rotateGizmo.Update(updateContext);
}
if (showingScaleGizmo) {
scaleGizmoContext = BuildScaleGizmoContext(*m_context, overlay, content, io.MousePos);
scaleGizmoContext.uniformOnly = usingTransformTool;
SceneViewportScaleGizmoContext updateContext = scaleGizmoContext;
if (drawActiveGizmoKind != SceneViewportActiveGizmoKind::None &&
drawActiveGizmoKind != SceneViewportActiveGizmoKind::Scale) {
updateContext.mousePosition = Math::Vector2(-1.0f, -1.0f);
}
m_scaleGizmo.Update(updateContext);
}
DrawSceneViewportOverlay(
@@ -335,8 +750,9 @@ void SceneViewPanel::Render() {
content.itemMin,
content.itemMax,
content.availableSize,
usingMoveGizmo ? &m_moveGizmo.GetDrawData() : nullptr,
usingMoveGizmo ? nullptr : &m_rotateGizmo.GetDrawData());
showingMoveGizmo ? &m_moveGizmo.GetDrawData() : nullptr,
showingRotateGizmo ? &m_rotateGizmo.GetDrawData() : nullptr,
showingScaleGizmo ? &m_scaleGizmo.GetDrawData() : nullptr);
}
}

View File

@@ -3,15 +3,19 @@
#include "Panel.h"
#include "Viewport/SceneViewportMoveGizmo.h"
#include "Viewport/SceneViewportRotateGizmo.h"
#include "Viewport/SceneViewportScaleGizmo.h"
#include <imgui.h>
namespace XCEngine {
namespace Editor {
enum class SceneViewportTransformTool : uint8_t {
Move = 0,
Rotate
enum class SceneViewportToolMode : uint8_t {
ViewMove = 0,
Move,
Rotate,
Scale,
Transform
};
class SceneViewPanel : public Panel {
@@ -20,13 +24,15 @@ public:
void Render() override;
private:
SceneViewportTransformTool m_transformTool = SceneViewportTransformTool::Move;
SceneViewportToolMode m_toolMode = SceneViewportToolMode::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);
int m_panDragButton = ImGuiMouseButton_Middle;
bool m_loggedLookDelta = false;
bool m_loggedPanDelta = false;
SceneViewportMoveGizmo m_moveGizmo;
SceneViewportRotateGizmo m_rotateGizmo;
SceneViewportScaleGizmo m_scaleGizmo;
};
}

View File

@@ -7,6 +7,7 @@ set(EDITOR_TEST_SOURCES
test_scene_viewport_camera_controller.cpp
test_scene_viewport_move_gizmo.cpp
test_scene_viewport_rotate_gizmo.cpp
test_scene_viewport_scale_gizmo.cpp
test_scene_viewport_post_pass_plan.cpp
test_scene_viewport_picker.cpp
test_scene_viewport_overlay_renderer.cpp
@@ -17,6 +18,7 @@ set(EDITOR_TEST_SOURCES
${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportPicker.cpp
${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportMoveGizmo.cpp
${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportRotateGizmo.cpp
${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportScaleGizmo.cpp
${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportGrid.cpp
)

View File

@@ -221,6 +221,62 @@ TEST_F(SceneViewportRotateGizmoTest, DraggingXAxisRotatesAroundWorldXAndCreatesU
EXPECT_NEAR(restoredForward.z, 1.0f, 1e-4f);
}
TEST_F(SceneViewportRotateGizmoTest, DraggingXAxisShowsAngleFillAndTemporarilyRotatesOtherRings) {
Components::GameObject* target = GetSceneManager().CreateEntity("Target");
ASSERT_NE(target, nullptr);
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);
const SceneViewportRotateGizmoHandleDrawData* yHandle =
FindHandle(gizmo.GetDrawData(), SceneViewportRotateGizmoAxis::Y);
ASSERT_NE(xHandle, nullptr);
ASSERT_NE(yHandle, nullptr);
const SceneViewportRotateGizmoSegmentDrawData* xStartSegment = FindLongestVisibleSegment(*xHandle, true);
const SceneViewportRotateGizmoSegmentDrawData* xEndSegment =
FindFarthestVisibleSegment(*xHandle, SegmentMidpoint(*xStartSegment), true);
const SceneViewportRotateGizmoSegmentDrawData* yInitialSegment = FindLongestVisibleSegment(*yHandle, false);
ASSERT_NE(xStartSegment, nullptr);
ASSERT_NE(xEndSegment, nullptr);
ASSERT_NE(yInitialSegment, nullptr);
const Math::Vector2 initialYMidpoint = SegmentMidpoint(*yInitialSegment);
const auto startContext = MakeContext(target, SegmentMidpoint(*xStartSegment), overlay);
gizmo.Update(startContext);
ASSERT_TRUE(gizmo.TryBeginDrag(startContext, m_context.GetUndoManager()));
const auto dragContext = MakeContext(target, SegmentMidpoint(*xEndSegment), overlay);
gizmo.Update(dragContext);
gizmo.UpdateDrag(dragContext);
ASSERT_TRUE(gizmo.GetDrawData().angleFill.visible);
EXPECT_GT(gizmo.GetDrawData().angleFill.arcPointCount, 2u);
const SceneViewportRotateGizmoHandleDrawData* yHandleDuring =
FindHandle(gizmo.GetDrawData(), SceneViewportRotateGizmoAxis::Y);
ASSERT_NE(yHandleDuring, nullptr);
const SceneViewportRotateGizmoSegmentDrawData* yDuringSegment = FindLongestVisibleSegment(*yHandleDuring, false);
ASSERT_NE(yDuringSegment, nullptr);
const Math::Vector2 duringYMidpoint = SegmentMidpoint(*yDuringSegment);
EXPECT_GT((duringYMidpoint - initialYMidpoint).Magnitude(), 2.0f);
gizmo.EndDrag(m_context.GetUndoManager());
gizmo.Update(dragContext);
EXPECT_FALSE(gizmo.GetDrawData().angleFill.visible);
const SceneViewportRotateGizmoHandleDrawData* yHandleAfter =
FindHandle(gizmo.GetDrawData(), SceneViewportRotateGizmoAxis::Y);
ASSERT_NE(yHandleAfter, nullptr);
const SceneViewportRotateGizmoSegmentDrawData* yAfterSegment = FindLongestVisibleSegment(*yHandleAfter, false);
ASSERT_NE(yAfterSegment, nullptr);
const Math::Vector2 afterYMidpoint = SegmentMidpoint(*yAfterSegment);
EXPECT_LT((afterYMidpoint - initialYMidpoint).Magnitude(), 1.0f);
}
TEST_F(SceneViewportRotateGizmoTest, DraggingEdgeOnXAxisFallsBackToScreenSpaceRotation) {
Components::GameObject* target = GetSceneManager().CreateEntity("Target");
ASSERT_NE(target, nullptr);

View File

@@ -0,0 +1,250 @@
#include <gtest/gtest.h>
#include <XCEngine/Core/Math/Quaternion.h>
#include "Core/EditorContext.h"
#include "Managers/SceneManager.h"
#include "Viewport/SceneViewportMath.h"
#include "Viewport/SceneViewportScaleGizmo.h"
namespace XCEngine::Editor {
namespace {
float HandleLength(const SceneViewportScaleGizmoAxisHandleDrawData& handle) {
return (handle.end - handle.start).Magnitude();
}
Math::Vector2 HandleDirection(const SceneViewportScaleGizmoAxisHandleDrawData& handle) {
return (handle.end - handle.start).Normalized();
}
const SceneViewportScaleGizmoAxisHandleDrawData* FindAxisHandle(
const SceneViewportScaleGizmoDrawData& drawData,
SceneViewportScaleGizmoHandle handleKind) {
for (const SceneViewportScaleGizmoAxisHandleDrawData& handle : drawData.axisHandles) {
if (handle.handle == handleKind) {
return &handle;
}
}
return nullptr;
}
class SceneViewportScaleGizmoTest : public ::testing::Test {
protected:
void SetUp() override {
m_context.GetSceneManager().NewScene("Scale 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 SceneViewportScaleGizmoContext MakeContext(
Components::GameObject* selectedObject,
const Math::Vector2& mousePosition) {
SceneViewportScaleGizmoContext context = {};
context.overlay = MakeOverlay();
context.viewportSize = Math::Vector2(800.0f, 600.0f);
context.mousePosition = mousePosition;
context.selectedObject = selectedObject;
return context;
}
static SceneViewportScaleGizmoContext MakeContext(
Components::GameObject* selectedObject,
const Math::Vector2& mousePosition,
const SceneViewportOverlayData& overlay) {
SceneViewportScaleGizmoContext 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(SceneViewportScaleGizmoTest, UpdateHighlightsXAxisWhenMouseIsNearXAxisHandle) {
Components::GameObject* target = GetSceneManager().CreateEntity("Target");
ASSERT_NE(target, nullptr);
SceneViewportScaleGizmo gizmo;
gizmo.Update(MakeContext(target, Math::Vector2(400.0f, 300.0f)));
ASSERT_TRUE(gizmo.GetDrawData().visible);
const SceneViewportScaleGizmoAxisHandleDrawData* xHandle =
FindAxisHandle(gizmo.GetDrawData(), SceneViewportScaleGizmoHandle::X);
ASSERT_NE(xHandle, nullptr);
ASSERT_TRUE(xHandle->visible);
gizmo.Update(MakeContext(target, xHandle->capCenter));
EXPECT_TRUE(gizmo.IsHoveringHandle());
EXPECT_TRUE(FindAxisHandle(gizmo.GetDrawData(), SceneViewportScaleGizmoHandle::X)->hovered);
}
TEST_F(SceneViewportScaleGizmoTest, DraggingXAxisOnChildWithScaledParentChangesOnlyLocalXScale) {
Components::GameObject* parent = GetSceneManager().CreateEntity("Parent");
ASSERT_NE(parent, nullptr);
parent->GetTransform()->SetLocalScale(Math::Vector3(0.5f, 2.0f, 1.5f));
parent->GetTransform()->SetLocalRotation(Math::Quaternion::FromAxisAngle(Math::Vector3::Up(), Math::PI * 0.25f));
Components::GameObject* target = GetSceneManager().CreateEntity("Target", parent);
ASSERT_NE(target, nullptr);
target->GetTransform()->SetLocalScale(Math::Vector3(1.0f, 2.0f, 3.0f));
const SceneViewportOverlayData overlay = MakeIsometricOverlay();
SceneViewportScaleGizmo gizmo;
gizmo.Update(MakeContext(target, Math::Vector2(400.0f, 300.0f), overlay));
const SceneViewportScaleGizmoAxisHandleDrawData* xHandle =
FindAxisHandle(gizmo.GetDrawData(), SceneViewportScaleGizmoHandle::X);
ASSERT_NE(xHandle, nullptr);
ASSERT_TRUE(xHandle->visible);
const Math::Vector2 startMouse = xHandle->capCenter;
const Math::Vector2 dragMouse = startMouse + HandleDirection(*xHandle) * 48.0f;
const auto startContext = MakeContext(target, startMouse, overlay);
gizmo.Update(startContext);
ASSERT_TRUE(gizmo.TryBeginDrag(startContext, m_context.GetUndoManager()));
const auto dragContext = MakeContext(target, dragMouse, overlay);
gizmo.Update(dragContext);
gizmo.UpdateDrag(dragContext);
gizmo.EndDrag(m_context.GetUndoManager());
const Math::Vector3 localScale = target->GetTransform()->GetLocalScale();
EXPECT_GT(localScale.x, 1.1f);
EXPECT_NEAR(localScale.y, 2.0f, 1e-4f);
EXPECT_NEAR(localScale.z, 3.0f, 1e-4f);
}
TEST_F(SceneViewportScaleGizmoTest, DraggingXAxisTemporarilyChangesHandleLengthAndResetsAfterRelease) {
Components::GameObject* target = GetSceneManager().CreateEntity("Target");
ASSERT_NE(target, nullptr);
const SceneViewportOverlayData overlay = MakeIsometricOverlay();
SceneViewportScaleGizmo gizmo;
gizmo.Update(MakeContext(target, Math::Vector2(400.0f, 300.0f), overlay));
const SceneViewportScaleGizmoAxisHandleDrawData* xInitial =
FindAxisHandle(gizmo.GetDrawData(), SceneViewportScaleGizmoHandle::X);
ASSERT_NE(xInitial, nullptr);
ASSERT_TRUE(xInitial->visible);
const float initialLength = HandleLength(*xInitial);
const Math::Vector2 startMouse = xInitial->capCenter;
const Math::Vector2 dragMouse = startMouse + HandleDirection(*xInitial) * 48.0f;
const auto startContext = MakeContext(target, startMouse, overlay);
gizmo.Update(startContext);
ASSERT_TRUE(gizmo.TryBeginDrag(startContext, m_context.GetUndoManager()));
const auto dragContext = MakeContext(target, dragMouse, overlay);
gizmo.Update(dragContext);
gizmo.UpdateDrag(dragContext);
gizmo.Update(dragContext);
const SceneViewportScaleGizmoAxisHandleDrawData* xDuring =
FindAxisHandle(gizmo.GetDrawData(), SceneViewportScaleGizmoHandle::X);
ASSERT_NE(xDuring, nullptr);
EXPECT_GT(HandleLength(*xDuring), initialLength + 6.0f);
gizmo.EndDrag(m_context.GetUndoManager());
gizmo.Update(dragContext);
const SceneViewportScaleGizmoAxisHandleDrawData* xAfter =
FindAxisHandle(gizmo.GetDrawData(), SceneViewportScaleGizmoHandle::X);
ASSERT_NE(xAfter, nullptr);
EXPECT_NEAR(HandleLength(*xAfter), initialLength, 1.0f);
}
TEST_F(SceneViewportScaleGizmoTest, DraggingCenterHandleScalesUniformlyAndCreatesUndoStep) {
Components::GameObject* target = GetSceneManager().CreateEntity("Target");
ASSERT_NE(target, nullptr);
const uint64_t targetId = target->GetID();
SceneViewportScaleGizmo gizmo;
const SceneViewportOverlayData overlay = MakeIsometricOverlay();
gizmo.Update(MakeContext(target, Math::Vector2(400.0f, 300.0f), overlay));
ASSERT_TRUE(gizmo.GetDrawData().centerHandle.visible);
const Math::Vector2 startMouse = gizmo.GetDrawData().centerHandle.center;
const auto startContext = MakeContext(target, startMouse, overlay);
gizmo.Update(startContext);
ASSERT_TRUE(gizmo.TryBeginDrag(startContext, m_context.GetUndoManager()));
const auto dragContext = MakeContext(target, startMouse + Math::Vector2(28.0f, -28.0f), overlay);
gizmo.Update(dragContext);
gizmo.UpdateDrag(dragContext);
gizmo.EndDrag(m_context.GetUndoManager());
const Math::Vector3 localScale = target->GetTransform()->GetLocalScale();
EXPECT_GT(localScale.x, 1.1f);
EXPECT_NEAR(localScale.x, localScale.y, 1e-4f);
EXPECT_NEAR(localScale.y, localScale.z, 1e-4f);
EXPECT_TRUE(m_context.GetUndoManager().CanUndo());
m_context.GetUndoManager().Undo();
Components::GameObject* restoredTarget = GetSceneManager().GetEntity(targetId);
ASSERT_NE(restoredTarget, nullptr);
EXPECT_NEAR(restoredTarget->GetTransform()->GetLocalScale().x, 1.0f, 1e-4f);
EXPECT_NEAR(restoredTarget->GetTransform()->GetLocalScale().y, 1.0f, 1e-4f);
EXPECT_NEAR(restoredTarget->GetTransform()->GetLocalScale().z, 1.0f, 1e-4f);
}
TEST_F(SceneViewportScaleGizmoTest, RotatedObjectPlacesXAxisHandleAlongProjectedLocalRight) {
Components::GameObject* target = GetSceneManager().CreateEntity("Target");
ASSERT_NE(target, nullptr);
target->GetTransform()->SetLocalRotation(Math::Quaternion::FromAxisAngle(Math::Vector3::Up(), Math::PI * 0.33f));
const SceneViewportOverlayData overlay = MakeIsometricOverlay();
SceneViewportScaleGizmo gizmo;
gizmo.Update(MakeContext(target, Math::Vector2(400.0f, 300.0f), overlay));
const SceneViewportScaleGizmoAxisHandleDrawData* xHandle =
FindAxisHandle(gizmo.GetDrawData(), SceneViewportScaleGizmoHandle::X);
ASSERT_NE(xHandle, nullptr);
ASSERT_TRUE(xHandle->visible);
Math::Vector2 expectedDirection = Math::Vector2::Zero();
ASSERT_TRUE(ProjectSceneViewportAxisDirectionAtPoint(
overlay,
800.0f,
600.0f,
target->GetTransform()->GetPosition(),
target->GetTransform()->GetRight(),
expectedDirection));
EXPECT_GT(Math::Vector2::Dot(HandleDirection(*xHandle), expectedDirection), 0.99f);
}
} // namespace
} // namespace XCEngine::Editor