Files
XCEngine/editor/app/Features/Scene/SceneViewportTransformGizmo.cpp

619 lines
21 KiB
C++
Raw Normal View History

2026-04-27 19:16:08 +08:00
#include "Scene/SceneViewportTransformGizmo.h"
2026-04-27 19:16:08 +08:00
#include "Scene/SceneViewportTransformGizmoSupport.h"
#include "Scene/EditorSceneRuntime.h"
#include "Scene/SceneViewportSession.h"
#include "Scene/SceneToolState.h"
#include <utility>
2026-04-29 01:24:21 +08:00
#include <optional>
namespace XCEngine::UI::Editor::App {
namespace {
using ::XCEngine::Math::Color;
using ::XCEngine::Math::Quaternion;
using ::XCEngine::Math::Vector2;
using ::XCEngine::Math::Vector3;
using ::XCEngine::UI::UIColor;
using ::XCEngine::UI::UIPoint;
using ::XCEngine::UI::UIRect;
namespace SceneViewportGizmoSupport = ::XCEngine::UI::Editor::App::SceneViewportGizmoSupport;
using SceneViewportGizmoSupport::BuildSceneViewportTransformGizmoOverlayFrameData;
using SceneViewportGizmoSupport::IUndoManager;
using SceneViewportGizmoSupport::SceneViewportMoveGizmo;
using SceneViewportGizmoSupport::SceneViewportMoveGizmoContext;
using SceneViewportGizmoSupport::SceneViewportOverlayData;
using SceneViewportGizmoSupport::SceneViewportOverlayFrameData;
using SceneViewportGizmoSupport::SceneViewportRotateGizmo;
using SceneViewportGizmoSupport::SceneViewportRotateGizmoContext;
using SceneViewportGizmoSupport::SceneViewportScaleGizmo;
using SceneViewportGizmoSupport::SceneViewportScaleGizmoContext;
using SceneViewportGizmoSupport::SceneViewportTransformGizmoHandleBuildInputs;
enum class ActiveTransformGizmoKind : std::uint8_t {
None = 0,
Move,
Rotate,
Scale
};
struct TransformGizmoSelectionState {
2026-04-29 01:24:21 +08:00
EditorSceneViewportSelectionSnapshot primaryObject = {};
std::vector<EditorSceneViewportSelectionSnapshot> selectedObjects = {};
Vector3 pivotWorldPosition = Vector3::Zero();
Quaternion primaryWorldRotation = Quaternion::Identity();
};
UIColor ToUIColor(const Color& color) {
return UIColor(color.r, color.g, color.b, color.a);
}
UIPoint ToScreenPoint(const Vector2& point, const UIRect& viewportRect) {
return UIPoint(viewportRect.x + point.x, viewportRect.y + point.y);
}
Vector2 ToLocalPoint(const UIRect& viewportRect, const UIPoint& point) {
return Vector2(point.x - viewportRect.x, point.y - viewportRect.y);
}
2026-04-29 01:24:21 +08:00
Quaternion ComputeStableWorldRotation(
const EditorSceneViewportSelectionSnapshot& selection) {
return selection.IsValid()
? selection.worldRotation.Normalized()
: Quaternion::Identity();
}
TransformGizmoSelectionState BuildSelectionState(
EditorSceneRuntime& sceneRuntime,
bool useCenterPivot) {
TransformGizmoSelectionState state = {};
2026-04-29 01:24:21 +08:00
const std::optional<EditorSceneViewportSelectionSnapshot> selectedObject =
sceneRuntime.BuildSceneViewportSelectionSnapshot();
if (!selectedObject.has_value() || !selectedObject->IsValid()) {
return state;
}
2026-04-29 01:24:21 +08:00
state.primaryObject = selectedObject.value();
state.selectedObjects.push_back(selectedObject.value());
state.primaryWorldRotation = ComputeStableWorldRotation(state.primaryObject);
state.pivotWorldPosition = useCenterPivot
2026-04-29 01:24:21 +08:00
? state.primaryObject.centerWorldPosition
: state.primaryObject.worldPosition;
return state;
}
SceneViewportOverlayData BuildOverlayData(const SceneViewportSession& session) {
SceneViewportOverlayData overlay = {};
const EditorSceneCameraSnapshot snapshot = session.BuildCameraSnapshot();
2026-04-29 01:24:21 +08:00
if (!snapshot.valid) {
return overlay;
}
overlay.valid = true;
2026-04-29 01:24:21 +08:00
overlay.cameraPosition = snapshot.position;
overlay.cameraForward = snapshot.forward;
overlay.cameraRight = snapshot.right;
overlay.cameraUp = snapshot.up;
overlay.verticalFovDegrees = snapshot.verticalFovDegrees;
overlay.nearClipPlane = snapshot.nearClipPlane;
overlay.farClipPlane = snapshot.farClipPlane;
overlay.orbitDistance = snapshot.orbitDistance;
return overlay;
}
SceneViewportMoveGizmoContext BuildMoveContext(
const TransformGizmoSelectionState& selection,
const SceneViewportOverlayData& overlay,
const UIRect& viewportRect,
const Vector2& localPointer,
bool localSpace) {
SceneViewportMoveGizmoContext context = {};
context.overlay = overlay;
context.viewportSize = Vector2(viewportRect.width, viewportRect.height);
context.mousePosition = localPointer;
context.selectedObject = selection.primaryObject;
context.selectedObjects = selection.selectedObjects;
context.pivotWorldPosition = selection.pivotWorldPosition;
context.axisOrientation = localSpace
? selection.primaryWorldRotation
: Quaternion::Identity();
return context;
}
SceneViewportRotateGizmoContext BuildRotateContext(
const TransformGizmoSelectionState& selection,
const SceneViewportOverlayData& overlay,
const UIRect& viewportRect,
const Vector2& localPointer,
bool localSpace,
bool useCenterPivot) {
SceneViewportRotateGizmoContext context = {};
context.overlay = overlay;
context.viewportSize = Vector2(viewportRect.width, viewportRect.height);
context.mousePosition = localPointer;
context.selectedObject = selection.primaryObject;
context.selectedObjects = selection.selectedObjects;
context.pivotWorldPosition = selection.pivotWorldPosition;
context.axisOrientation = localSpace
? selection.primaryWorldRotation
: Quaternion::Identity();
context.localSpace = localSpace;
context.rotateAroundSharedPivot = useCenterPivot;
return context;
}
SceneViewportScaleGizmoContext BuildScaleContext(
const TransformGizmoSelectionState& selection,
const SceneViewportOverlayData& overlay,
const UIRect& viewportRect,
const Vector2& localPointer,
bool localSpace) {
SceneViewportScaleGizmoContext context = {};
context.overlay = overlay;
context.viewportSize = Vector2(viewportRect.width, viewportRect.height);
context.mousePosition = localPointer;
context.selectedObject = selection.primaryObject;
context.pivotWorldPosition = selection.pivotWorldPosition;
context.axisOrientation = localSpace
? selection.primaryWorldRotation
: Quaternion::Identity();
return context;
}
ActiveTransformGizmoKind ResolveActiveGizmoKind(
const SceneViewportMoveGizmo& moveGizmo,
const SceneViewportRotateGizmo& rotateGizmo,
const SceneViewportScaleGizmo& scaleGizmo) {
if (moveGizmo.IsActive()) {
return ActiveTransformGizmoKind::Move;
}
if (rotateGizmo.IsActive()) {
return ActiveTransformGizmoKind::Rotate;
}
if (scaleGizmo.IsActive()) {
return ActiveTransformGizmoKind::Scale;
}
return ActiveTransformGizmoKind::None;
}
2026-04-18 23:56:17 +08:00
bool IsTransformToolMode(SceneToolMode mode) {
return mode == SceneToolMode::Transform;
}
bool ShouldShowMoveGizmo(SceneToolMode mode) {
return mode == SceneToolMode::Translate || IsTransformToolMode(mode);
}
bool ShouldShowRotateGizmo(SceneToolMode mode) {
return mode == SceneToolMode::Rotate || IsTransformToolMode(mode);
}
bool ShouldShowScaleGizmo(SceneToolMode mode) {
return mode == SceneToolMode::Scale || IsTransformToolMode(mode);
}
Vector2 ResolveUpdatePointerPosition(
const Vector2& pointerPosition,
bool hoverEnabled,
ActiveTransformGizmoKind activeKind,
ActiveTransformGizmoKind gizmoKind) {
if (!hoverEnabled && activeKind == ActiveTransformGizmoKind::None) {
2026-04-18 23:56:17 +08:00
return Vector2(-1.0f, -1.0f);
}
if (activeKind != ActiveTransformGizmoKind::None && activeKind != gizmoKind) {
2026-04-18 23:56:17 +08:00
return Vector2(-1.0f, -1.0f);
}
return pointerPosition;
}
class SceneGizmoUndoBridge final : public IUndoManager {
public:
void Bind(EditorSceneRuntime& sceneRuntime) {
m_sceneRuntime = &sceneRuntime;
}
void BeginInteractiveChange(const std::string& label) override {
if (m_sceneRuntime == nullptr || HasPendingInteractiveChange()) {
return;
}
SceneTransformSnapshot snapshot = {};
if (!m_sceneRuntime->CaptureSelectedTransformSnapshot(snapshot)) {
return;
}
m_pendingLabel = label;
m_beforeSnapshot = snapshot;
}
bool HasPendingInteractiveChange() const override {
return m_beforeSnapshot.IsValid();
}
2026-04-28 17:53:36 +08:00
bool ApplyWorldTransformPreview(
std::uint64_t entityId,
const Vector3& position,
const Quaternion& rotation) override {
return m_sceneRuntime != nullptr &&
m_sceneRuntime->ApplyTransformToolWorldPreview(
entityId,
position,
rotation);
}
bool ApplyLocalScalePreview(
std::uint64_t entityId,
const Vector3& localScale) override {
return m_sceneRuntime != nullptr &&
m_sceneRuntime->ApplyTransformToolLocalScalePreview(
entityId,
localScale);
}
void FinalizeInteractiveChange() override {
if (m_sceneRuntime == nullptr || !HasPendingInteractiveChange()) {
return;
}
SceneTransformSnapshot afterSnapshot = {};
m_sceneRuntime->CaptureSelectedTransformSnapshot(afterSnapshot);
m_sceneRuntime->RecordTransformEdit(m_beforeSnapshot, afterSnapshot);
m_pendingLabel.clear();
m_beforeSnapshot = {};
}
void CancelInteractiveChange() override {
if (m_sceneRuntime == nullptr || !HasPendingInteractiveChange()) {
return;
}
m_sceneRuntime->ApplyTransformSnapshot(m_beforeSnapshot);
m_pendingLabel.clear();
m_beforeSnapshot = {};
}
private:
EditorSceneRuntime* m_sceneRuntime = nullptr;
std::string m_pendingLabel = {};
SceneTransformSnapshot m_beforeSnapshot = {};
};
} // namespace
struct SceneViewportTransformGizmo::State {
SceneGizmoUndoBridge undoBridge = {};
SceneViewportMoveGizmo moveGizmo = {};
SceneViewportRotateGizmo rotateGizmo = {};
SceneViewportScaleGizmo scaleGizmo = {};
SceneViewportMoveGizmoContext moveContext = {};
SceneViewportRotateGizmoContext rotateContext = {};
SceneViewportScaleGizmoContext scaleContext = {};
SceneViewportTransformGizmoFrame frame = {};
};
SceneViewportTransformGizmo::SceneViewportTransformGizmo()
: m_state(std::make_unique<State>()) {
}
SceneViewportTransformGizmo::~SceneViewportTransformGizmo() = default;
SceneViewportTransformGizmo::SceneViewportTransformGizmo(SceneViewportTransformGizmo&&) noexcept =
default;
SceneViewportTransformGizmo& SceneViewportTransformGizmo::operator=(
SceneViewportTransformGizmo&&) noexcept = default;
void SceneViewportTransformGizmo::Refresh(
EditorSceneRuntime& sceneRuntime,
const SceneViewportSession& session,
const UIRect& viewportRect,
const UIPoint& pointerScreen,
bool hoverEnabled) {
State& state = *m_state;
state.undoBridge.Bind(sceneRuntime);
state.frame = {};
state.frame.clipRect = viewportRect;
if (viewportRect.width <= 1.0f || viewportRect.height <= 1.0f) {
return;
}
const SceneViewportOverlayData overlay = BuildOverlayData(session);
if (!overlay.valid) {
CancelDrag(sceneRuntime);
return;
}
const bool useCenterPivot =
session.GetToolPivotMode() == SceneToolPivotMode::Center;
const bool localSpace =
session.GetToolSpaceMode() == SceneToolSpaceMode::Local;
const TransformGizmoSelectionState selection =
BuildSelectionState(sceneRuntime, useCenterPivot);
2026-04-29 01:24:21 +08:00
if (!selection.primaryObject.IsValid()) {
CancelDrag(sceneRuntime);
return;
}
const SceneToolMode toolMode = session.GetToolMode();
if (toolMode == SceneToolMode::View) {
CancelDrag(sceneRuntime);
return;
}
Vector2 localPointer = ToLocalPoint(viewportRect, pointerScreen);
2026-04-18 23:56:17 +08:00
const bool usingTransformTool = IsTransformToolMode(toolMode);
const bool showingMoveGizmo = ShouldShowMoveGizmo(toolMode);
const bool showingRotateGizmo = ShouldShowRotateGizmo(toolMode);
const bool showingScaleGizmo = ShouldShowScaleGizmo(toolMode);
SceneViewportTransformGizmoHandleBuildInputs inputs = {};
2026-04-18 23:56:17 +08:00
if (showingMoveGizmo) {
state.moveContext = BuildMoveContext(
selection,
overlay,
viewportRect,
localPointer,
localSpace);
if (state.moveGizmo.IsActive() &&
2026-04-29 01:24:21 +08:00
state.moveContext.selectedObject.IsValid() &&
state.moveContext.selectedObject.objectId !=
state.moveGizmo.GetActiveEntityId()) {
state.moveGizmo.CancelDrag(&state.undoBridge);
}
2026-04-18 23:56:17 +08:00
} else if (state.moveGizmo.IsActive()) {
state.moveGizmo.CancelDrag(&state.undoBridge);
}
2026-04-18 23:56:17 +08:00
if (showingRotateGizmo) {
state.rotateContext = BuildRotateContext(
selection,
overlay,
viewportRect,
localPointer,
localSpace,
useCenterPivot);
if (state.rotateGizmo.IsActive() &&
2026-04-29 01:24:21 +08:00
state.rotateContext.selectedObject.IsValid() &&
state.rotateContext.selectedObject.objectId !=
state.rotateGizmo.GetActiveEntityId()) {
state.rotateGizmo.CancelDrag(&state.undoBridge);
}
2026-04-18 23:56:17 +08:00
} else if (state.rotateGizmo.IsActive()) {
state.rotateGizmo.CancelDrag(&state.undoBridge);
}
2026-04-18 23:56:17 +08:00
if (showingScaleGizmo) {
state.scaleContext = BuildScaleContext(
selection,
overlay,
viewportRect,
localPointer,
localSpace);
2026-04-18 23:56:17 +08:00
state.scaleContext.uniformOnly = usingTransformTool;
if (state.scaleGizmo.IsActive() &&
2026-04-29 01:24:21 +08:00
state.scaleContext.selectedObject.IsValid() &&
state.scaleContext.selectedObject.objectId !=
state.scaleGizmo.GetActiveEntityId()) {
state.scaleGizmo.CancelDrag(&state.undoBridge);
}
2026-04-18 23:56:17 +08:00
} else if (state.scaleGizmo.IsActive()) {
state.scaleGizmo.CancelDrag(&state.undoBridge);
}
const ActiveTransformGizmoKind activeKind = ResolveActiveGizmoKind(
2026-04-18 23:56:17 +08:00
state.moveGizmo,
state.rotateGizmo,
state.scaleGizmo);
if (showingMoveGizmo) {
SceneViewportMoveGizmoContext updateContext = state.moveContext;
updateContext.mousePosition = ResolveUpdatePointerPosition(
state.moveContext.mousePosition,
hoverEnabled,
activeKind,
ActiveTransformGizmoKind::Move);
2026-04-18 23:56:17 +08:00
state.moveGizmo.Update(updateContext);
inputs.moveGizmo = &state.moveGizmo.GetDrawData();
2026-04-29 01:24:21 +08:00
inputs.moveEntityId = selection.primaryObject.objectId;
2026-04-18 23:56:17 +08:00
}
if (showingRotateGizmo) {
SceneViewportRotateGizmoContext updateContext = state.rotateContext;
updateContext.mousePosition = ResolveUpdatePointerPosition(
state.rotateContext.mousePosition,
hoverEnabled,
activeKind,
ActiveTransformGizmoKind::Rotate);
2026-04-18 23:56:17 +08:00
state.rotateGizmo.Update(updateContext);
inputs.rotateGizmo = &state.rotateGizmo.GetDrawData();
2026-04-29 01:24:21 +08:00
inputs.rotateEntityId = selection.primaryObject.objectId;
2026-04-18 23:56:17 +08:00
}
if (showingScaleGizmo) {
SceneViewportScaleGizmoContext updateContext = state.scaleContext;
updateContext.mousePosition = ResolveUpdatePointerPosition(
state.scaleContext.mousePosition,
hoverEnabled,
activeKind,
ActiveTransformGizmoKind::Scale);
2026-04-18 23:56:17 +08:00
state.scaleGizmo.Update(updateContext);
inputs.scaleGizmo = &state.scaleGizmo.GetDrawData();
2026-04-29 01:24:21 +08:00
inputs.scaleEntityId = selection.primaryObject.objectId;
}
const SceneViewportOverlayFrameData overlayFrame =
BuildSceneViewportTransformGizmoOverlayFrameData(overlay, inputs);
if (overlayFrame.screenTriangles.empty()) {
return;
}
state.frame.visible = true;
state.frame.triangles.reserve(overlayFrame.screenTriangles.size());
for (const auto& triangle : overlayFrame.screenTriangles) {
SceneViewportTransformGizmoTriangle triangleFrame = {};
triangleFrame.a =
ToScreenPoint(triangle.vertices[0].screenPosition, viewportRect);
triangleFrame.b =
ToScreenPoint(triangle.vertices[1].screenPosition, viewportRect);
triangleFrame.c =
ToScreenPoint(triangle.vertices[2].screenPosition, viewportRect);
triangleFrame.color = ToUIColor(triangle.vertices[0].color);
state.frame.triangles.push_back(std::move(triangleFrame));
}
}
bool SceneViewportTransformGizmo::TryBeginDrag(
EditorSceneRuntime& sceneRuntime,
const SceneViewportSession& session) {
State& state = *m_state;
state.undoBridge.Bind(sceneRuntime);
switch (session.GetToolMode()) {
case SceneToolMode::Translate:
return state.moveGizmo.TryBeginDrag(state.moveContext, state.undoBridge);
case SceneToolMode::Rotate:
return state.rotateGizmo.TryBeginDrag(state.rotateContext, state.undoBridge);
case SceneToolMode::Scale:
return state.scaleGizmo.TryBeginDrag(state.scaleContext, state.undoBridge);
2026-04-18 23:56:17 +08:00
case SceneToolMode::Transform:
if (state.scaleGizmo.EvaluateHit(state.scaleContext.mousePosition).HasHit()) {
return state.scaleGizmo.TryBeginDrag(
state.scaleContext,
state.undoBridge);
}
if (state.moveGizmo.EvaluateHit(state.moveContext.mousePosition).HasHit()) {
return state.moveGizmo.TryBeginDrag(
state.moveContext,
state.undoBridge);
}
if (state.rotateGizmo.EvaluateHit(state.rotateContext.mousePosition).HasHit()) {
return state.rotateGizmo.TryBeginDrag(
state.rotateContext,
state.undoBridge);
}
return false;
case SceneToolMode::View:
default:
return false;
}
}
bool SceneViewportTransformGizmo::UpdateDrag(EditorSceneRuntime& sceneRuntime) {
State& state = *m_state;
state.undoBridge.Bind(sceneRuntime);
switch (ResolveActiveGizmoKind(
state.moveGizmo,
state.rotateGizmo,
state.scaleGizmo)) {
case ActiveTransformGizmoKind::Move:
2026-04-28 17:53:36 +08:00
state.moveGizmo.UpdateDrag(state.moveContext, state.undoBridge);
return true;
case ActiveTransformGizmoKind::Rotate:
2026-04-28 17:53:36 +08:00
state.rotateGizmo.UpdateDrag(state.rotateContext, state.undoBridge);
return true;
case ActiveTransformGizmoKind::Scale:
2026-04-28 17:53:36 +08:00
state.scaleGizmo.UpdateDrag(state.scaleContext, state.undoBridge);
return true;
case ActiveTransformGizmoKind::None:
default:
return false;
}
}
bool SceneViewportTransformGizmo::EndDrag(EditorSceneRuntime& sceneRuntime) {
State& state = *m_state;
state.undoBridge.Bind(sceneRuntime);
switch (ResolveActiveGizmoKind(
state.moveGizmo,
state.rotateGizmo,
state.scaleGizmo)) {
case ActiveTransformGizmoKind::Move:
state.moveGizmo.EndDrag(state.undoBridge);
return true;
case ActiveTransformGizmoKind::Rotate:
state.rotateGizmo.EndDrag(state.undoBridge);
return true;
case ActiveTransformGizmoKind::Scale:
state.scaleGizmo.EndDrag(state.undoBridge);
return true;
case ActiveTransformGizmoKind::None:
default:
return false;
}
}
void SceneViewportTransformGizmo::CancelDrag(EditorSceneRuntime& sceneRuntime) {
State& state = *m_state;
state.undoBridge.Bind(sceneRuntime);
if (state.moveGizmo.IsActive()) {
state.moveGizmo.CancelDrag(&state.undoBridge);
}
if (state.rotateGizmo.IsActive()) {
state.rotateGizmo.CancelDrag(&state.undoBridge);
}
if (state.scaleGizmo.IsActive()) {
state.scaleGizmo.CancelDrag(&state.undoBridge);
}
}
void SceneViewportTransformGizmo::ResetVisualState() {
if (m_state == nullptr) {
return;
}
m_state->frame = {};
}
bool SceneViewportTransformGizmo::IsActive() const {
if (m_state == nullptr) {
return false;
}
return ResolveActiveGizmoKind(
m_state->moveGizmo,
m_state->rotateGizmo,
m_state->scaleGizmo) != ActiveTransformGizmoKind::None;
}
bool SceneViewportTransformGizmo::IsHoveringHandle() const {
if (m_state == nullptr) {
return false;
}
return m_state->moveGizmo.IsHoveringHandle() ||
m_state->rotateGizmo.IsHoveringHandle() ||
m_state->scaleGizmo.IsHoveringHandle();
}
const SceneViewportTransformGizmoFrame& SceneViewportTransformGizmo::GetFrame() const {
return m_state->frame;
}
void AppendSceneViewportTransformGizmo(
::XCEngine::UI::UIDrawList& drawList,
const SceneViewportTransformGizmoFrame& frame) {
if (!frame.visible || frame.triangles.empty()) {
return;
}
drawList.PushClipRect(frame.clipRect);
for (const SceneViewportTransformGizmoTriangle& triangle : frame.triangles) {
drawList.AddFilledTriangle(
triangle.a,
triangle.b,
triangle.c,
triangle.color);
}
drawList.PopClipRect();
}
} // namespace XCEngine::UI::Editor::App