Add scene transform toolbar and scale gizmo
@@ -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
|
||||
|
||||
BIN
editor/resources/Icons/move_tool.png
Normal file
|
After Width: | Height: | Size: 445 B |
BIN
editor/resources/Icons/move_tool_on.png
Normal file
|
After Width: | Height: | Size: 337 B |
BIN
editor/resources/Icons/rotate_tool.png
Normal file
|
After Width: | Height: | Size: 625 B |
BIN
editor/resources/Icons/rotate_tool_on.png
Normal file
|
After Width: | Height: | Size: 565 B |
BIN
editor/resources/Icons/scale_tool.png
Normal file
|
After Width: | Height: | Size: 298 B |
BIN
editor/resources/Icons/scale_tool_on.png
Normal file
|
After Width: | Height: | Size: 295 B |
BIN
editor/resources/Icons/transform_tool.png
Normal file
|
After Width: | Height: | Size: 847 B |
BIN
editor/resources/Icons/transform_tool_on.png
Normal file
|
After Width: | Height: | Size: 559 B |
BIN
editor/resources/Icons/view_move_tool.png
Normal file
|
After Width: | Height: | Size: 639 B |
BIN
editor/resources/Icons/view_move_tool_on.png
Normal file
|
After Width: | Height: | Size: 657 B |
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
502
editor/src/Viewport/SceneViewportScaleGizmo.cpp
Normal 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
|
||||
96
editor/src/Viewport/SceneViewportScaleGizmo.h
Normal 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
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
250
tests/editor/test_scene_viewport_scale_gizmo.cpp
Normal 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
|
||||