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:
2026-04-29 18:00:54 +02:00
parent 7f2600f0b8
commit 49cb480e15
2 changed files with 111 additions and 0 deletions

View File

@@ -77,6 +77,26 @@ void APS_Editor_Pawn::Tick(float DeltaTime)
{ {
Super::Tick(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())) if (APlayerController* PC = Cast<APlayerController>(GetController()))
{ {
bAltHeld = PC->IsInputKeyDown(EKeys::LeftAlt) || PC->IsInputKeyDown(EKeys::RightAlt); 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 = NewObject<UInputAction>(this, TEXT("IA_Cancel"));
IA_Cancel->ValueType = EInputActionValueType::Boolean; IA_Cancel->ValueType = EInputActionValueType::Boolean;
EditorMappingContext->MapKey(IA_Cancel, EKeys::Escape); 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) 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_SplineMode, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleSplineMode);
EIC->BindAction(IA_Confirm, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleConfirm); 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_Cancel, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleCancel);
EIC->BindAction(IA_Focus, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleFocus);
} }
// ---- Input Handlers ---- // ---- 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 ---- // ---- Helpers ----
void APS_Editor_Pawn::SetCameraMode(EPS_Editor_CameraMode NewMode) void APS_Editor_Pawn::SetCameraMode(EPS_Editor_CameraMode NewMode)

View File

@@ -209,6 +209,9 @@ private:
UPROPERTY() UPROPERTY()
TObjectPtr<UInputAction> IA_Cancel; TObjectPtr<UInputAction> IA_Cancel;
UPROPERTY()
TObjectPtr<UInputAction> IA_Focus;
// ---- Spline point / ChildMovable drag state ---- // ---- Spline point / ChildMovable drag state ----
TWeakObjectPtr<AActor> DraggedSplineActor; TWeakObjectPtr<AActor> DraggedSplineActor;
TArray<FVector> SplinePointDragInitialState; TArray<FVector> SplinePointDragInitialState;
@@ -256,6 +259,16 @@ private:
void HandleSplineMode(const FInputActionValue& Value); void HandleSplineMode(const FInputActionValue& Value);
void HandleConfirm(const FInputActionValue& Value); void HandleConfirm(const FInputActionValue& Value);
void HandleCancel(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 ---- // ---- Helpers ----
void CreateInputActions(); void CreateInputActions();