Add F key to frame the current selection (Maya/Blender style)
Pressing F computes the combined bounding box of every selected actor, then animates the camera over 0.25s (smoothstep ease) to a distance that fits the box in view while keeping the current viewing direction. The orbit focal point and distance are synced so a subsequent Alt+drag pivots around the new center. No-ops cleanly when nothing is selected. Falls back to actor pivots when no component reports valid bounds, and clamps the radius to a minimum so point-like actors don't pull the camera onto them. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -77,6 +77,26 @@ void APS_Editor_Pawn::Tick(float DeltaTime)
|
||||
{
|
||||
Super::Tick(DeltaTime);
|
||||
|
||||
// Focus animation: smoothly interpolate camera location/rotation toward the framed
|
||||
// selection. Uses ease-in-out (smoothstep) so the camera doesn't snap at the boundaries.
|
||||
if (bIsFocusing)
|
||||
{
|
||||
FocusElapsed = FMath::Min(FocusElapsed + DeltaTime, FocusDuration);
|
||||
const float Linear = FocusElapsed / FocusDuration;
|
||||
const float Alpha = Linear * Linear * (3.0f - 2.0f * Linear); // smoothstep
|
||||
const FVector NewLoc = FMath::Lerp(FocusStartLocation, FocusTargetLocation, Alpha);
|
||||
const FRotator NewRot = FMath::Lerp(FocusStartRotation, FocusTargetRotation, Alpha);
|
||||
SetActorLocation(NewLoc);
|
||||
SetActorRotation(NewRot);
|
||||
if (FocusElapsed >= FocusDuration)
|
||||
{
|
||||
bIsFocusing = false;
|
||||
// Sync orbit state so a subsequent Alt+drag pivots around the new focal point.
|
||||
OrbitYaw = NewRot.Yaw;
|
||||
OrbitPitch = NewRot.Pitch;
|
||||
}
|
||||
}
|
||||
|
||||
if (APlayerController* PC = Cast<APlayerController>(GetController()))
|
||||
{
|
||||
bAltHeld = PC->IsInputKeyDown(EKeys::LeftAlt) || PC->IsInputKeyDown(EKeys::RightAlt);
|
||||
@@ -229,6 +249,11 @@ void APS_Editor_Pawn::CreateInputActions()
|
||||
IA_Cancel = NewObject<UInputAction>(this, TEXT("IA_Cancel"));
|
||||
IA_Cancel->ValueType = EInputActionValueType::Boolean;
|
||||
EditorMappingContext->MapKey(IA_Cancel, EKeys::Escape);
|
||||
|
||||
// Focus selection (Maya/Blender style)
|
||||
IA_Focus = NewObject<UInputAction>(this, TEXT("IA_Focus"));
|
||||
IA_Focus->ValueType = EInputActionValueType::Boolean;
|
||||
EditorMappingContext->MapKey(IA_Focus, EKeys::F);
|
||||
}
|
||||
|
||||
void APS_Editor_Pawn::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
|
||||
@@ -275,6 +300,7 @@ void APS_Editor_Pawn::SetupPlayerInputComponent(UInputComponent* PlayerInputComp
|
||||
EIC->BindAction(IA_SplineMode, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleSplineMode);
|
||||
EIC->BindAction(IA_Confirm, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleConfirm);
|
||||
EIC->BindAction(IA_Cancel, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleCancel);
|
||||
EIC->BindAction(IA_Focus, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleFocus);
|
||||
}
|
||||
|
||||
// ---- Input Handlers ----
|
||||
@@ -1362,6 +1388,78 @@ void APS_Editor_Pawn::HandleCancel(const FInputActionValue& Value)
|
||||
}
|
||||
}
|
||||
|
||||
void APS_Editor_Pawn::HandleFocus(const FInputActionValue& Value)
|
||||
{
|
||||
// Frame the current selection (Maya/Blender F-key behaviour). Computes the combined
|
||||
// world-space bounding box of every selected actor, then animates the camera to a
|
||||
// distance that fits the box in view while keeping the current viewing direction.
|
||||
APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetController());
|
||||
if (!EditorPC) return;
|
||||
|
||||
UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager();
|
||||
if (!SM || !SM->IsAnythingSelected()) return;
|
||||
|
||||
FBox CombinedBox(EForceInit::ForceInit);
|
||||
for (const TWeakObjectPtr<AActor>& Weak : SM->GetSelectedActors())
|
||||
{
|
||||
if (AActor* Actor = Weak.Get())
|
||||
{
|
||||
const FBox ActorBox = Actor->GetComponentsBoundingBox(true);
|
||||
if (ActorBox.IsValid)
|
||||
{
|
||||
CombinedBox += ActorBox;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!CombinedBox.IsValid)
|
||||
{
|
||||
// Fallback: use actor pivots if no component reports valid bounds (rare — empty actors)
|
||||
for (const TWeakObjectPtr<AActor>& Weak : SM->GetSelectedActors())
|
||||
{
|
||||
if (AActor* Actor = Weak.Get())
|
||||
{
|
||||
CombinedBox += FBox(Actor->GetActorLocation(), Actor->GetActorLocation());
|
||||
}
|
||||
}
|
||||
if (!CombinedBox.IsValid) return;
|
||||
}
|
||||
|
||||
const FVector Center = CombinedBox.GetCenter();
|
||||
const FVector Extent = CombinedBox.GetExtent();
|
||||
// Sphere radius enclosing the box; clamp to a minimum so picking a point-like actor
|
||||
// (zero-size bounds) still leaves the camera at a usable distance.
|
||||
const float Radius = FMath::Max(Extent.Size(), 50.0f);
|
||||
|
||||
// Distance to fit a sphere of given radius given the camera's vertical FOV.
|
||||
float FOV = 90.0f;
|
||||
if (APlayerController* PC = Cast<APlayerController>(GetController()))
|
||||
{
|
||||
if (PC->PlayerCameraManager)
|
||||
{
|
||||
FOV = PC->PlayerCameraManager->GetFOVAngle();
|
||||
}
|
||||
}
|
||||
const float HalfFOVRad = FMath::DegreesToRadians(FOV * 0.5f);
|
||||
const float FitDistance = Radius / FMath::Max(FMath::Tan(HalfFOVRad), 0.01f);
|
||||
const float TargetDistance = FMath::Max(FitDistance * 1.4f, 100.0f); // 40% margin
|
||||
|
||||
// Keep current viewing direction; just slide the camera so Center sits TargetDistance
|
||||
// in front along that direction. Then force the rotation to look squarely at Center —
|
||||
// guards against accumulated rounding so the pivot for the next orbit is exact.
|
||||
const FVector Forward = GetActorForwardVector();
|
||||
FocusStartLocation = GetActorLocation();
|
||||
FocusStartRotation = GetActorRotation();
|
||||
FocusTargetLocation = Center - Forward * TargetDistance;
|
||||
FocusTargetRotation = (Center - FocusTargetLocation).Rotation();
|
||||
|
||||
FocusElapsed = 0.0f;
|
||||
bIsFocusing = true;
|
||||
|
||||
// Update orbit state so Alt+drag immediately after F pivots around the new center.
|
||||
OrbitFocalPoint = Center;
|
||||
OrbitDistance = TargetDistance;
|
||||
}
|
||||
|
||||
// ---- Helpers ----
|
||||
|
||||
void APS_Editor_Pawn::SetCameraMode(EPS_Editor_CameraMode NewMode)
|
||||
|
||||
@@ -209,6 +209,9 @@ private:
|
||||
UPROPERTY()
|
||||
TObjectPtr<UInputAction> IA_Cancel;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<UInputAction> IA_Focus;
|
||||
|
||||
// ---- Spline point / ChildMovable drag state ----
|
||||
TWeakObjectPtr<AActor> DraggedSplineActor;
|
||||
TArray<FVector> SplinePointDragInitialState;
|
||||
@@ -256,6 +259,16 @@ private:
|
||||
void HandleSplineMode(const FInputActionValue& Value);
|
||||
void HandleConfirm(const FInputActionValue& Value);
|
||||
void HandleCancel(const FInputActionValue& Value);
|
||||
void HandleFocus(const FInputActionValue& Value);
|
||||
|
||||
// ---- Focus animation state (F key, frame selection) ----
|
||||
bool bIsFocusing = false;
|
||||
float FocusElapsed = 0.0f;
|
||||
float FocusDuration = 0.25f;
|
||||
FVector FocusStartLocation = FVector::ZeroVector;
|
||||
FVector FocusTargetLocation = FVector::ZeroVector;
|
||||
FRotator FocusStartRotation = FRotator::ZeroRotator;
|
||||
FRotator FocusTargetRotation = FRotator::ZeroRotator;
|
||||
|
||||
// ---- Helpers ----
|
||||
void CreateInputActions();
|
||||
|
||||
Reference in New Issue
Block a user