Add snap to ground, screen-space rotate/scale, and input refinements

- End key snaps selected actors to ground (downward trace + bounds offset)
- Rotate/Scale now use screen-space mouse delta (X+Y combined) for intuitive control
- Accumulator-based drag for smooth incremental rotation and scaling
- Fixed rotation drag plane (perpendicular to axis instead of parallel)
- Movement: arrows + A (up) / Q (down), letter keys freed for hotkeys

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-10 18:49:04 +02:00
parent 30c0078a1d
commit 20017ff3fd
4 changed files with 102 additions and 48 deletions

View File

@@ -168,6 +168,10 @@ void APS_Editor_Pawn::CreateInputActions()
IA_Delete = NewObject<UInputAction>(this, TEXT("IA_Delete"));
IA_Delete->ValueType = EInputActionValueType::Boolean;
EditorMappingContext->MapKey(IA_Delete, EKeys::Delete);
IA_SnapToGround = NewObject<UInputAction>(this, TEXT("IA_SnapToGround"));
IA_SnapToGround->ValueType = EInputActionValueType::Boolean;
EditorMappingContext->MapKey(IA_SnapToGround, EKeys::End);
}
void APS_Editor_Pawn::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
@@ -205,6 +209,7 @@ void APS_Editor_Pawn::SetupPlayerInputComponent(UInputComponent* PlayerInputComp
EIC->BindAction(IA_RotateMode, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleRotateMode);
EIC->BindAction(IA_ScaleMode, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleScaleMode);
EIC->BindAction(IA_Delete, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleDelete);
EIC->BindAction(IA_SnapToGround, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleSnapToGround);
}
// ---- Input Handlers ----
@@ -290,19 +295,6 @@ void APS_Editor_Pawn::HandleLook(const FInputActionValue& Value)
}
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();
@@ -311,14 +303,20 @@ void APS_Editor_Pawn::HandleLook(const FInputActionValue& Value)
if (!Gizmo) return;
const FVector AxisDir = Gizmo->GetAxisDirection(ActiveGizmoAxis);
const FVector TotalDelta = CurrentIntersection - GizmoDragInitialIntersection;
const EPS_Editor_TransformMode Mode = SM->GetTransformMode();
const TArray<TWeakObjectPtr<AActor>>& Selected = SM->GetSelectedActors();
if (Mode == EPS_Editor_TransformMode::Translate)
{
// Ray-plane intersection for precise axis-constrained translation
FVector RayOrigin, RayDir;
if (!GetMouseWorldRay(RayOrigin, RayDir)) return;
FVector CurrentIntersection;
if (!RayPlaneIntersection(RayOrigin, RayDir, GizmoDragPlaneOrigin, GizmoDragPlaneNormal, CurrentIntersection)) return;
const FVector TotalDelta = CurrentIntersection - GizmoDragInitialIntersection;
const FVector ConstrainedDelta = FVector::DotProduct(TotalDelta, AxisDir) * AxisDir;
for (int32 i = 0; i < Selected.Num() && i < GizmoDragInitialTransforms.Num(); ++i)
{
if (AActor* Actor = Selected[i].Get())
@@ -329,29 +327,23 @@ void APS_Editor_Pawn::HandleLook(const FInputActionValue& Value)
}
else if (Mode == EPS_Editor_TransformMode::Rotate)
{
// Compute rotation angle from initial to current intersection around the axis
// Use combined mouse X+Y screen movement to drive rotation angle
const float RotationSpeed = 1.0f;
const float AngleDelta = (LookInput.X + LookInput.Y) * RotationSpeed;
// Accumulate total angle since drag start
GizmoDragAccumulator += AngleDelta;
const FQuat RotationQuat(AxisDir, FMath::DegreesToRadians(GizmoDragAccumulator));
const FVector GizmoLoc = GizmoDragPlaneOrigin;
const FVector InitDir = (GizmoDragInitialIntersection - GizmoLoc).GetSafeNormal();
const FVector CurrDir = (CurrentIntersection - GizmoLoc).GetSafeNormal();
// Signed angle via cross product
float Angle = FMath::Atan2(
FVector::DotProduct(FVector::CrossProduct(InitDir, CurrDir), AxisDir),
FVector::DotProduct(InitDir, CurrDir));
Angle = FMath::RadiansToDegrees(Angle);
const FQuat RotationQuat(AxisDir, FMath::DegreesToRadians(Angle));
for (int32 i = 0; i < Selected.Num() && i < GizmoDragInitialTransforms.Num(); ++i)
{
if (AActor* Actor = Selected[i].Get())
{
// Rotate around gizmo pivot
const FVector Offset = GizmoDragInitialTransforms[i].GetLocation() - GizmoLoc;
const FVector RotatedOffset = RotationQuat.RotateVector(Offset);
Actor->SetActorLocation(GizmoLoc + RotatedOffset);
Actor->SetActorLocation(GizmoLoc + RotationQuat.RotateVector(Offset));
// Also rotate the actor itself
const FQuat OrigRot = GizmoDragInitialTransforms[i].GetRotation();
Actor->SetActorRotation((RotationQuat * OrigRot).Rotator());
}
@@ -359,22 +351,17 @@ void APS_Editor_Pawn::HandleLook(const FInputActionValue& Value)
}
else if (Mode == EPS_Editor_TransformMode::Scale)
{
// Scale factor from mouse movement along the axis
const float InitialDist = FVector::DotProduct(GizmoDragInitialIntersection - GizmoDragPlaneOrigin, AxisDir);
const float CurrentDist = FVector::DotProduct(CurrentIntersection - GizmoDragPlaneOrigin, AxisDir);
// Use combined mouse X+Y screen movement to drive scale
const float ScaleSpeed = 0.01f;
GizmoDragAccumulator += (LookInput.X + LookInput.Y) * ScaleSpeed;
float ScaleFactor = 1.0f;
if (FMath::Abs(InitialDist) > KINDA_SMALL_NUMBER)
{
ScaleFactor = FMath::Clamp(CurrentDist / InitialDist, 0.01f, 100.0f);
}
const float ScaleFactor = FMath::Clamp(1.0f + GizmoDragAccumulator, 0.01f, 100.0f);
for (int32 i = 0; i < Selected.Num() && i < GizmoDragInitialTransforms.Num(); ++i)
{
if (AActor* Actor = Selected[i].Get())
{
FVector NewScale = GizmoDragInitialTransforms[i].GetScale3D();
// Scale only on the dragged axis
if (ActiveGizmoAxis == EPS_Editor_GizmoAxis::X) NewScale.X *= ScaleFactor;
else if (ActiveGizmoAxis == EPS_Editor_GizmoAxis::Y) NewScale.Y *= ScaleFactor;
else if (ActiveGizmoAxis == EPS_Editor_GizmoAxis::Z) NewScale.Z *= ScaleFactor;
@@ -383,7 +370,6 @@ void APS_Editor_Pawn::HandleLook(const FInputActionValue& Value)
}
}
// Update gizmo position
Gizmo->SetActorLocation(SM->GetSelectionPivot());
}
}
@@ -548,6 +534,18 @@ void APS_Editor_Pawn::HandleDelete(const FInputActionValue& Value)
}
}
void APS_Editor_Pawn::HandleSnapToGround(const FInputActionValue& Value)
{
if (CameraMode != EPS_Editor_CameraMode::Idle) return;
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetController()))
{
if (UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager())
{
SM->SnapSelectionToGround();
}
}
}
// ---- Helpers ----
void APS_Editor_Pawn::SetCameraMode(EPS_Editor_CameraMode NewMode)
@@ -684,21 +682,30 @@ void APS_Editor_Pawn::BeginGizmoDrag(EPS_Editor_GizmoAxis Axis)
ActiveGizmoAxis = Axis;
// Get the axis direction in world space
const FVector AxisDir = Gizmo->GetAxisDirection(Axis);
const EPS_Editor_TransformMode Mode = SM->GetTransformMode();
// 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())
FVector PlaneNormal;
if (Mode == EPS_Editor_TransformMode::Rotate)
{
// Camera looking along the axis - use camera up instead
PlaneNormal = FVector::CrossProduct(AxisDir, FVector::UpVector).GetSafeNormal();
// For rotation: plane perpendicular to the rotation axis
PlaneNormal = AxisDir;
}
else
{
// For translate/scale: plane containing the axis, facing the camera
const FVector CameraForward = GetActorForwardVector();
PlaneNormal = FVector::CrossProduct(AxisDir, CameraForward).GetSafeNormal();
if (PlaneNormal.IsNearlyZero())
{
PlaneNormal = FVector::CrossProduct(AxisDir, FVector::UpVector).GetSafeNormal();
}
PlaneNormal = FVector::CrossProduct(AxisDir, PlaneNormal).GetSafeNormal();
}
PlaneNormal = FVector::CrossProduct(AxisDir, PlaneNormal).GetSafeNormal();
GizmoDragPlaneNormal = PlaneNormal;
GizmoDragPlaneOrigin = Gizmo->GetActorLocation();
GizmoDragAccumulator = 0.0f;
// Get initial mouse-plane intersection
FVector RayOrigin, RayDir;

View File

@@ -158,6 +158,9 @@ private:
UPROPERTY()
TObjectPtr<UInputAction> IA_Delete;
UPROPERTY()
TObjectPtr<UInputAction> IA_SnapToGround;
// ---- Modifier key state ----
bool bAltHeld = false;
bool bCtrlHeld = false;
@@ -171,6 +174,7 @@ private:
FVector GizmoDragPlaneOrigin = FVector::ZeroVector;
FVector GizmoDragInitialIntersection = FVector::ZeroVector;
TArray<FTransform> GizmoDragInitialTransforms;
float GizmoDragAccumulator = 0.0f;
// ---- Input Handlers ----
void HandleMove(const FInputActionValue& Value);
@@ -186,6 +190,7 @@ private:
void HandleRotateMode(const FInputActionValue& Value);
void HandleScaleMode(const FInputActionValue& Value);
void HandleDelete(const FInputActionValue& Value);
void HandleSnapToGround(const FInputActionValue& Value);
// ---- Helpers ----
void CreateInputActions();

View File

@@ -218,6 +218,45 @@ void UPS_Editor_SelectionManager::SetActorHighlight(AActor* Actor, bool bHighlig
}
}
void UPS_Editor_SelectionManager::SnapSelectionToGround()
{
APlayerController* PC = OwnerPC.Get();
if (!PC) return;
for (const TWeakObjectPtr<AActor>& Weak : SelectedActors)
{
AActor* Actor = Weak.Get();
if (!Actor) continue;
// Get actor bounds to compute the bottom of the actor
FVector Origin, Extent;
Actor->GetActorBounds(false, Origin, Extent);
const float BottomOffset = Extent.Z; // Distance from actor origin to bottom
// Trace downward from actor location
const FVector Start = Actor->GetActorLocation();
const FVector End = Start - FVector::UpVector * 100000.0f;
FHitResult Hit;
FCollisionQueryParams Params;
Params.AddIgnoredActor(Actor);
Params.AddIgnoredActor(PC->GetPawn());
if (Gizmo) Params.AddIgnoredActor(Gizmo);
if (Actor->GetWorld()->LineTraceSingleByChannel(Hit, Start, End, ECC_Visibility, Params))
{
// Place actor so its bottom sits on the ground
FVector NewLocation = Hit.ImpactPoint;
NewLocation.Z += BottomOffset;
Actor->SetActorLocation(NewLocation);
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Snapped %s to ground"), *Actor->GetName());
}
}
UpdateGizmo();
}
void UPS_Editor_SelectionManager::UpdateGizmo()
{
if (!Gizmo)

View File

@@ -36,6 +36,9 @@ public:
void ToggleActor(AActor* Actor);
void ClearSelection();
/** Snap all selected actors to the ground below them. */
void SnapSelectionToGround();
// ---- Queries ----
const TArray<TWeakObjectPtr<AActor>>& GetSelectedActors() const { return SelectedActors; }