Add gizmo drag interaction and hover highlighting

- Click+drag on gizmo arrows to translate selected actors along axis
- Drag plane computation based on axis direction and camera orientation
- Axis-constrained movement with initial transform preservation
- Hover highlighting: gizmo arrows turn yellow on mouse-over
- Sphere sweep for easier gizmo clicking (generous hit radius)
- UE editor-style gizmo colors (dark red/green/blue)
- Fixed gizmo visibility toggle (component-level instead of actor-level)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-10 15:31:40 +02:00
parent 8b2609669f
commit 7884d1de59
3 changed files with 196 additions and 10 deletions

View File

@@ -10,6 +10,7 @@
#include "GameFramework/PlayerController.h"
#include "PS_Editor_SelectionManager.h"
#include "PS_Editor_PlayerController.h"
#include "PS_Editor_Gizmo.h"
#include "DrawDebugHelpers.h"
APS_Editor_Pawn::APS_Editor_Pawn()
@@ -44,6 +45,12 @@ void APS_Editor_Pawn::Tick(float DeltaTime)
bAltHeld = PC->IsInputKeyDown(EKeys::LeftAlt) || PC->IsInputKeyDown(EKeys::RightAlt);
bCtrlHeld = PC->IsInputKeyDown(EKeys::LeftControl) || PC->IsInputKeyDown(EKeys::RightControl);
// Gizmo hover highlighting in Idle mode
if (CameraMode == EPS_Editor_CameraMode::Idle)
{
UpdateGizmoHover();
}
// Draw debug bounding box around selected actors
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(PC))
{
@@ -264,6 +271,45 @@ void APS_Editor_Pawn::HandleLook(const FInputActionValue& Value)
SetActorLocation(OrbitFocalPoint + Offset);
SetActorRotation((OrbitFocalPoint - GetActorLocation()).Rotation());
}
else if (CameraMode == EPS_Editor_CameraMode::GizmoDrag)
{
// Intersect mouse ray with drag plane, constrain delta to axis, apply to actors
FVector RayOrigin, RayDir;
if (!GetMouseWorldRay(RayOrigin, RayDir))
{
return;
}
FVector CurrentIntersection;
if (!RayPlaneIntersection(RayOrigin, RayDir, GizmoDragPlaneOrigin, GizmoDragPlaneNormal, CurrentIntersection))
{
return;
}
APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetController());
if (!EditorPC) return;
UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager();
if (!SM) return;
APS_Editor_Gizmo* Gizmo = SM->GetGizmo();
if (!Gizmo) return;
const FVector AxisDir = Gizmo->GetAxisDirection(ActiveGizmoAxis);
const FVector TotalDelta = CurrentIntersection - GizmoDragInitialIntersection;
const FVector ConstrainedDelta = FVector::DotProduct(TotalDelta, AxisDir) * AxisDir;
// Apply delta to all selected actors from their initial transforms
const TArray<TWeakObjectPtr<AActor>>& Selected = SM->GetSelectedActors();
for (int32 i = 0; i < Selected.Num() && i < GizmoDragInitialTransforms.Num(); ++i)
{
if (AActor* Actor = Selected[i].Get())
{
Actor->SetActorLocation(GizmoDragInitialTransforms[i].GetLocation() + ConstrainedDelta);
}
}
// Update gizmo position
Gizmo->SetActorLocation(SM->GetSelectionPivot());
}
}
void APS_Editor_Pawn::HandleScroll(const FInputActionValue& Value)
@@ -293,16 +339,35 @@ void APS_Editor_Pawn::HandleLeftClickStarted(const FInputActionValue& Value)
{
if (bAltHeld)
{
// Alt + left-click = orbit immediately (no pending)
ComputeOrbitFocalPoint();
SetCameraMode(EPS_Editor_CameraMode::Orbit);
return;
}
else
// Check if clicking on a gizmo handle
FVector RayOrigin, RayDir;
if (GetMouseWorldRay(RayOrigin, RayDir))
{
// Enter PendingClick: wait to see if this is a click or a drag
PendingClickScreenPos = GetMouseScreenPosition();
SetCameraMode(EPS_Editor_CameraMode::PendingClick);
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetController()))
{
if (UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager())
{
if (APS_Editor_Gizmo* Gizmo = SM->GetGizmo())
{
EPS_Editor_GizmoAxis Axis = Gizmo->GetAxisUnderCursor(RayOrigin, RayDir);
if (Axis != EPS_Editor_GizmoAxis::None)
{
BeginGizmoDrag(Axis);
return;
}
}
}
}
}
// No gizmo hit: enter PendingClick
PendingClickScreenPos = GetMouseScreenPosition();
SetCameraMode(EPS_Editor_CameraMode::PendingClick);
}
void APS_Editor_Pawn::HandleLeftClickCompleted(const FInputActionValue& Value)
@@ -419,3 +484,106 @@ FVector2D APS_Editor_Pawn::GetMouseScreenPosition() const
PC->GetMousePosition(MouseX, MouseY);
return FVector2D(MouseX, MouseY);
}
bool APS_Editor_Pawn::GetMouseWorldRay(FVector& OutOrigin, FVector& OutDirection) const
{
APlayerController* PC = Cast<APlayerController>(GetController());
if (!PC)
{
return false;
}
const FVector2D ScreenPos = GetMouseScreenPosition();
return PC->DeprojectScreenPositionToWorld(ScreenPos.X, ScreenPos.Y, OutOrigin, OutDirection);
}
bool APS_Editor_Pawn::RayPlaneIntersection(const FVector& RayOrigin, const FVector& RayDir,
const FVector& PlaneOrigin, const FVector& PlaneNormal, FVector& OutIntersection) const
{
const float Denom = FVector::DotProduct(RayDir, PlaneNormal);
if (FMath::Abs(Denom) < KINDA_SMALL_NUMBER)
{
return false; // Ray is parallel to plane
}
const float T = FVector::DotProduct(PlaneOrigin - RayOrigin, PlaneNormal) / Denom;
if (T < 0.0f)
{
return false; // Intersection is behind the ray
}
OutIntersection = RayOrigin + RayDir * T;
return true;
}
void APS_Editor_Pawn::UpdateGizmoHover()
{
APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetController());
if (!EditorPC) return;
UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager();
if (!SM) return;
APS_Editor_Gizmo* Gizmo = SM->GetGizmo();
if (!Gizmo) return;
FVector RayOrigin, RayDir;
if (GetMouseWorldRay(RayOrigin, RayDir))
{
EPS_Editor_GizmoAxis Axis = Gizmo->GetAxisUnderCursor(RayOrigin, RayDir);
Gizmo->SetHighlightedAxis(Axis);
}
else
{
Gizmo->SetHighlightedAxis(EPS_Editor_GizmoAxis::None);
}
}
void APS_Editor_Pawn::BeginGizmoDrag(EPS_Editor_GizmoAxis Axis)
{
APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetController());
if (!EditorPC) return;
UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager();
if (!SM) return;
APS_Editor_Gizmo* Gizmo = SM->GetGizmo();
if (!Gizmo) return;
ActiveGizmoAxis = Axis;
// Get the axis direction in world space
const FVector AxisDir = Gizmo->GetAxisDirection(Axis);
// Compute the drag plane: contains the axis and faces the camera as much as possible
const FVector CameraForward = GetActorForwardVector();
FVector PlaneNormal = FVector::CrossProduct(AxisDir, CameraForward).GetSafeNormal();
if (PlaneNormal.IsNearlyZero())
{
// Camera looking along the axis - use camera up instead
PlaneNormal = FVector::CrossProduct(AxisDir, FVector::UpVector).GetSafeNormal();
}
PlaneNormal = FVector::CrossProduct(AxisDir, PlaneNormal).GetSafeNormal();
GizmoDragPlaneNormal = PlaneNormal;
GizmoDragPlaneOrigin = Gizmo->GetActorLocation();
// Get initial mouse-plane intersection
FVector RayOrigin, RayDir;
if (GetMouseWorldRay(RayOrigin, RayDir))
{
RayPlaneIntersection(RayOrigin, RayDir, GizmoDragPlaneOrigin, GizmoDragPlaneNormal, GizmoDragInitialIntersection);
}
// Store initial transforms of all selected actors
GizmoDragInitialTransforms.Empty();
for (const TWeakObjectPtr<AActor>& Weak : SM->GetSelectedActors())
{
if (AActor* Actor = Weak.Get())
{
GizmoDragInitialTransforms.Add(Actor->GetActorTransform());
}
else
{
GizmoDragInitialTransforms.Add(FTransform::Identity);
}
}
SetCameraMode(EPS_Editor_CameraMode::GizmoDrag);
}

View File

@@ -3,6 +3,7 @@
#include "CoreMinimal.h"
#include "GameFramework/Pawn.h"
#include "InputActionValue.h"
#include "PS_Editor_SelectionTypes.h"
#include "PS_Editor_Pawn.generated.h"
class UCameraComponent;
@@ -152,6 +153,13 @@ private:
// ---- Click vs drag state ----
FVector2D PendingClickScreenPos = FVector2D::ZeroVector;
// ---- Gizmo drag state ----
EPS_Editor_GizmoAxis ActiveGizmoAxis = EPS_Editor_GizmoAxis::None;
FVector GizmoDragPlaneNormal = FVector::ZeroVector;
FVector GizmoDragPlaneOrigin = FVector::ZeroVector;
FVector GizmoDragInitialIntersection = FVector::ZeroVector;
TArray<FTransform> GizmoDragInitialTransforms;
// ---- Input Handlers ----
void HandleMove(const FInputActionValue& Value);
void HandleLook(const FInputActionValue& Value);
@@ -169,6 +177,10 @@ private:
void UpdateCursorVisibility();
void ComputeOrbitFocalPoint();
FVector2D GetMouseScreenPosition() const;
bool GetMouseWorldRay(FVector& OutOrigin, FVector& OutDirection) const;
bool RayPlaneIntersection(const FVector& RayOrigin, const FVector& RayDir, const FVector& PlaneOrigin, const FVector& PlaneNormal, FVector& OutIntersection) const;
void UpdateGizmoHover();
void BeginGizmoDrag(EPS_Editor_GizmoAxis Axis);
float OrbitPitch = 0.0f;
float OrbitYaw = 0.0f;

View File

@@ -53,9 +53,10 @@ void APS_Editor_Gizmo::BeginPlay()
return MID;
};
Mat_X = MakeColorMat(FLinearColor::Red);
Mat_Y = MakeColorMat(FLinearColor::Green);
Mat_Z = MakeColorMat(FLinearColor(0.0f, 0.0f, 1.0f));
// UE editor gizmo colors
Mat_X = MakeColorMat(FLinearColor(0.594f, 0.0197f, 0.0f)); // Dark red
Mat_Y = MakeColorMat(FLinearColor(0.1255f, 0.372f, 0.0f)); // Dark green
Mat_Z = MakeColorMat(FLinearColor(0.0392f, 0.0784f, 0.594f)); // Dark blue
Mat_Highlight = MakeColorMat(FLinearColor::Yellow);
// Apply materials to shafts and cones
@@ -182,13 +183,18 @@ EPS_Editor_GizmoAxis APS_Editor_Gizmo::GetAxisUnderCursor(const FVector& RayOrig
float BestDistance = TNumericLimits<float>::Max();
EPS_Editor_GizmoAxis BestAxis = EPS_Editor_GizmoAxis::None;
// Use a sphere sweep with a generous radius for easier clicking
const float SweepRadius = 8.0f * GetActorScale3D().X;
auto TestComponent = [&](UStaticMeshComponent* Comp, EPS_Editor_GizmoAxis Axis)
{
if (!Comp || !Comp->IsVisible()) return;
if (!Comp) return;
FHitResult Hit;
const FVector RayEnd = RayOrigin + RayDirection * 100000.0f;
if (Comp->LineTraceComponent(Hit, RayOrigin, RayEnd, FCollisionQueryParams::DefaultQueryParam))
const FCollisionShape Sphere = FCollisionShape::MakeSphere(SweepRadius);
if (Comp->SweepComponent(Hit, RayOrigin, RayEnd, FQuat::Identity, Sphere, false))
{
if (Hit.Distance < BestDistance)
{