Add World/Local space toggle, snap system, and gizmo polish
World/Local: - Toggle button in toolbar (World/Local) - Local mode: translate/rotate gizmo aligns with actor's local axes - Rotation arcs dynamically follow local orientation during drag - Scale always in local space - RefreshGizmoOrientation after every drag completion Snap System: - Toggle button + configurable value field in toolbar - Translation: snaps displacement along axis (no world-grid staircase) - Rotation: snaps accumulated angle to nearest increment - Scale: snaps scale values Rotation improvements: - Screen-space direction with magnitude-based speed (all mouse axes contribute) - Fixed axis direction stored at drag start (prevents feedback loop) - Smooth fallback when camera looks along rotation axis - Keyboard shortcuts: Z/E/R (AZERTY convention) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -160,15 +160,15 @@ void APS_Editor_Pawn::CreateInputActions()
|
||||
// Transform mode hotkeys (E/R/T)
|
||||
IA_TranslateMode = NewObject<UInputAction>(this, TEXT("IA_TranslateMode"));
|
||||
IA_TranslateMode->ValueType = EInputActionValueType::Boolean;
|
||||
EditorMappingContext->MapKey(IA_TranslateMode, EKeys::E);
|
||||
EditorMappingContext->MapKey(IA_TranslateMode, EKeys::Z);
|
||||
|
||||
IA_RotateMode = NewObject<UInputAction>(this, TEXT("IA_RotateMode"));
|
||||
IA_RotateMode->ValueType = EInputActionValueType::Boolean;
|
||||
EditorMappingContext->MapKey(IA_RotateMode, EKeys::R);
|
||||
EditorMappingContext->MapKey(IA_RotateMode, EKeys::E);
|
||||
|
||||
IA_ScaleMode = NewObject<UInputAction>(this, TEXT("IA_ScaleMode"));
|
||||
IA_ScaleMode->ValueType = EInputActionValueType::Boolean;
|
||||
EditorMappingContext->MapKey(IA_ScaleMode, EKeys::T);
|
||||
EditorMappingContext->MapKey(IA_ScaleMode, EKeys::R);
|
||||
|
||||
IA_Delete = NewObject<UInputAction>(this, TEXT("IA_Delete"));
|
||||
IA_Delete->ValueType = EInputActionValueType::Boolean;
|
||||
@@ -318,7 +318,8 @@ void APS_Editor_Pawn::HandleLook(const FInputActionValue& Value)
|
||||
APS_Editor_Gizmo* Gizmo = SM->GetGizmo();
|
||||
if (!Gizmo) return;
|
||||
|
||||
const FVector AxisDir = Gizmo->GetAxisDirection(ActiveGizmoAxis);
|
||||
// Use fixed axis direction from drag start (avoid feedback loop with local orientation updates)
|
||||
const FVector AxisDir = GizmoDragAxisDir;
|
||||
const EPS_Editor_TransformMode Mode = SM->GetTransformMode();
|
||||
const TArray<TWeakObjectPtr<AActor>>& Selected = SM->GetSelectedActors();
|
||||
|
||||
@@ -331,7 +332,14 @@ void APS_Editor_Pawn::HandleLook(const FInputActionValue& Value)
|
||||
if (!RayPlaneIntersection(RayOrigin, RayDir, GizmoDragPlaneOrigin, GizmoDragPlaneNormal, CurrentIntersection)) return;
|
||||
|
||||
const FVector TotalDelta = CurrentIntersection - GizmoDragInitialIntersection;
|
||||
const FVector ConstrainedDelta = FVector::DotProduct(TotalDelta, AxisDir) * AxisDir;
|
||||
float DisplacementAlongAxis = FVector::DotProduct(TotalDelta, AxisDir);
|
||||
|
||||
// Snap displacement along axis, not world position
|
||||
if (SM->bSnapTranslation && SM->TranslationSnapSize > 0.0f)
|
||||
{
|
||||
DisplacementAlongAxis = FMath::RoundToFloat(DisplacementAlongAxis / SM->TranslationSnapSize) * SM->TranslationSnapSize;
|
||||
}
|
||||
const FVector ConstrainedDelta = DisplacementAlongAxis * AxisDir;
|
||||
|
||||
for (int32 i = 0; i < Selected.Num() && i < GizmoDragInitialTransforms.Num(); ++i)
|
||||
{
|
||||
@@ -355,16 +363,35 @@ void APS_Editor_Pawn::HandleLook(const FInputActionValue& Value)
|
||||
DragPC->ProjectWorldLocationToScreen(GizmoDragPlaneOrigin, ScreenStart);
|
||||
DragPC->ProjectWorldLocationToScreen(GizmoDragPlaneOrigin + AxisDir * 100.0f, ScreenEnd);
|
||||
|
||||
FVector2D ScreenAxisDir = (ScreenEnd - ScreenStart).GetSafeNormal();
|
||||
// Mouse movement perpendicular to the projected axis drives rotation
|
||||
FVector2D ScreenPerp(-ScreenAxisDir.Y, ScreenAxisDir.X);
|
||||
const float ScreenLen = (ScreenEnd - ScreenStart).Size();
|
||||
|
||||
AngleDelta = -(LookInput.X * ScreenPerp.X - LookInput.Y * ScreenPerp.Y) * RotationSpeed;
|
||||
// Use perpendicular for DIRECTION (sign), total mouse for SPEED
|
||||
// This way both X and Y always contribute, no dead zones
|
||||
float PerpDot = 0.0f;
|
||||
if (ScreenLen > 1.0f)
|
||||
{
|
||||
FVector2D ScreenAxisDir = (ScreenEnd - ScreenStart) / ScreenLen;
|
||||
FVector2D ScreenPerp(-ScreenAxisDir.Y, ScreenAxisDir.X);
|
||||
PerpDot = LookInput.X * ScreenPerp.X - LookInput.Y * ScreenPerp.Y;
|
||||
}
|
||||
else
|
||||
{
|
||||
PerpDot = LookInput.X + LookInput.Y;
|
||||
}
|
||||
|
||||
const float MouseMagnitude = FMath::Abs(LookInput.X) + FMath::Abs(LookInput.Y);
|
||||
AngleDelta = -FMath::Sign(PerpDot) * MouseMagnitude * RotationSpeed;
|
||||
}
|
||||
|
||||
GizmoDragAccumulator += AngleDelta;
|
||||
|
||||
const FQuat RotationQuat(AxisDir, FMath::DegreesToRadians(GizmoDragAccumulator));
|
||||
float SnapAngle = GizmoDragAccumulator;
|
||||
if (SM->bSnapRotation && SM->RotationSnapSize > 0.0f)
|
||||
{
|
||||
SnapAngle = FMath::RoundToFloat(SnapAngle / SM->RotationSnapSize) * SM->RotationSnapSize;
|
||||
}
|
||||
|
||||
const FQuat RotationQuat(AxisDir, FMath::DegreesToRadians(SnapAngle));
|
||||
const FVector GizmoLoc = GizmoDragPlaneOrigin;
|
||||
|
||||
for (int32 i = 0; i < Selected.Num() && i < GizmoDragInitialTransforms.Num(); ++i)
|
||||
@@ -399,16 +426,25 @@ void APS_Editor_Pawn::HandleLook(const FInputActionValue& Value)
|
||||
else 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;
|
||||
Actor->SetActorScale3D(NewScale);
|
||||
Actor->SetActorScale3D(SM->SnapScale(NewScale));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// In translate mode, gizmo follows the selection. In rotate/scale, stay at initial position to avoid jitter.
|
||||
// In translate mode, gizmo follows the selection. In rotate/scale, stay at initial position.
|
||||
if (Mode == EPS_Editor_TransformMode::Translate)
|
||||
{
|
||||
Gizmo->SetActorLocation(SM->GetSelectionPivot());
|
||||
}
|
||||
|
||||
// Update local orientation in real-time during rotation drag
|
||||
if (Mode == EPS_Editor_TransformMode::Rotate && SM->GetTransformSpace() == EPS_Editor_Space::Local)
|
||||
{
|
||||
if (AActor* FirstActor = Selected.Num() > 0 ? Selected[0].Get() : nullptr)
|
||||
{
|
||||
Gizmo->SetLocalOrientation(FirstActor->GetActorRotation());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -517,6 +553,16 @@ void APS_Editor_Pawn::HandleLeftClickCompleted(const FInputActionValue& Value)
|
||||
}
|
||||
}
|
||||
ActiveGizmoAxis = EPS_Editor_GizmoAxis::None;
|
||||
|
||||
// Refresh gizmo orientation (local mode needs to track actor's new rotation)
|
||||
if (APS_Editor_PlayerController* PC2 = Cast<APS_Editor_PlayerController>(GetController()))
|
||||
{
|
||||
if (UPS_Editor_SelectionManager* SM2 = PC2->GetSelectionManager())
|
||||
{
|
||||
SM2->RefreshGizmoOrientation();
|
||||
}
|
||||
}
|
||||
|
||||
SetCameraMode(EPS_Editor_CameraMode::Idle);
|
||||
}
|
||||
else if (CameraMode == EPS_Editor_CameraMode::Pan || CameraMode == EPS_Editor_CameraMode::Orbit)
|
||||
@@ -813,7 +859,9 @@ void APS_Editor_Pawn::BeginGizmoDrag(EPS_Editor_GizmoAxis Axis)
|
||||
|
||||
ActiveGizmoAxis = Axis;
|
||||
|
||||
const FVector AxisDir = Gizmo->GetAxisDirection(Axis);
|
||||
// Store axis direction at drag start - used for entire drag duration
|
||||
GizmoDragAxisDir = Gizmo->GetAxisDirection(Axis);
|
||||
const FVector AxisDir = GizmoDragAxisDir;
|
||||
const EPS_Editor_TransformMode Mode = SM->GetTransformMode();
|
||||
|
||||
FVector PlaneNormal;
|
||||
|
||||
@@ -185,6 +185,7 @@ private:
|
||||
FVector GizmoDragInitialIntersection = FVector::ZeroVector;
|
||||
TArray<FTransform> GizmoDragInitialTransforms;
|
||||
float GizmoDragAccumulator = 0.0f;
|
||||
FVector GizmoDragAxisDir = FVector::ZeroVector; // Fixed axis direction for the entire drag
|
||||
|
||||
// ---- Input Handlers ----
|
||||
void HandleMove(const FInputActionValue& Value);
|
||||
|
||||
@@ -455,58 +455,62 @@ void APS_Editor_Gizmo::UpdateArcFacing()
|
||||
FRotator CameraRotation;
|
||||
PC->GetPlayerViewPoint(CameraLocation, CameraRotation);
|
||||
|
||||
// Direction from gizmo to camera
|
||||
// Compute local axes (world axes rotated by LocalOrientation)
|
||||
const FVector LocalX = LocalOrientation.RotateVector(FVector::ForwardVector);
|
||||
const FVector LocalY = LocalOrientation.RotateVector(FVector::RightVector);
|
||||
const FVector LocalZ = LocalOrientation.RotateVector(FVector::UpVector);
|
||||
|
||||
// Direction from gizmo to camera in world space
|
||||
const FVector CamDir = (CameraLocation - GetActorLocation()).GetSafeNormal();
|
||||
|
||||
// Determine which octant the camera is in (sign of each component)
|
||||
// Project camera onto local axes to determine octant
|
||||
const FVector NewOctant(
|
||||
CamDir.X >= 0 ? 1.0f : -1.0f,
|
||||
CamDir.Y >= 0 ? 1.0f : -1.0f,
|
||||
CamDir.Z >= 0 ? 1.0f : -1.0f
|
||||
FVector::DotProduct(CamDir, LocalX) >= 0 ? 1.0f : -1.0f,
|
||||
FVector::DotProduct(CamDir, LocalY) >= 0 ? 1.0f : -1.0f,
|
||||
FVector::DotProduct(CamDir, LocalZ) >= 0 ? 1.0f : -1.0f
|
||||
);
|
||||
|
||||
// Only regenerate when the camera crosses an octant boundary
|
||||
if (NewOctant.Equals(LastCameraOctant))
|
||||
{
|
||||
return;
|
||||
}
|
||||
LastCameraOctant = NewOctant;
|
||||
|
||||
// For each axis, show the quarter-arc facing the camera.
|
||||
// The arc start/end directions are chosen based on camera octant.
|
||||
// Arc directions in world space using local axes
|
||||
const FVector ArcX_Start = LocalY * NewOctant.Y;
|
||||
const FVector ArcX_End = LocalZ * NewOctant.Z;
|
||||
|
||||
// X-axis (red, arc in YZ plane): choose the Y and Z quadrant facing camera
|
||||
const FVector ArcX_Start = FVector::RightVector * NewOctant.Y; // +Y or -Y
|
||||
const FVector ArcX_End = FVector::UpVector * NewOctant.Z; // +Z or -Z
|
||||
const FVector ArcY_Start = LocalX * NewOctant.X;
|
||||
const FVector ArcY_End = LocalZ * NewOctant.Z;
|
||||
|
||||
// Y-axis (green, arc in XZ plane): choose the X and Z quadrant facing camera
|
||||
const FVector ArcY_Start = FVector::ForwardVector * NewOctant.X; // +X or -X
|
||||
const FVector ArcY_End = FVector::UpVector * NewOctant.Z; // +Z or -Z
|
||||
const FVector ArcZ_Start = LocalX * NewOctant.X;
|
||||
const FVector ArcZ_End = LocalY * NewOctant.Y;
|
||||
|
||||
// Z-axis (blue, arc in XY plane): choose the X and Y quadrant facing camera
|
||||
const FVector ArcZ_Start = FVector::ForwardVector * NewOctant.X; // +X or -X
|
||||
const FVector ArcZ_End = FVector::RightVector * NewOctant.Y; // +Y or -Y
|
||||
CreateArcRing(Arc_X, LocalX, ArcX_Start, ArcX_End, ArcRadius, ArcWidth, ArcSegments);
|
||||
CreateArcRing(Arc_Y, LocalY, ArcY_Start, ArcY_End, ArcRadius, ArcWidth, ArcSegments);
|
||||
CreateArcRing(Arc_Z, LocalZ, ArcZ_Start, ArcZ_End, ArcRadius, ArcWidth, ArcSegments);
|
||||
|
||||
CreateArcRing(Arc_X, FVector::ForwardVector, ArcX_Start, ArcX_End, ArcRadius, ArcWidth, ArcSegments);
|
||||
CreateArcRing(Arc_Y, FVector::RightVector, ArcY_Start, ArcY_End, ArcRadius, ArcWidth, ArcSegments);
|
||||
CreateArcRing(Arc_Z, FVector::UpVector, ArcZ_Start, ArcZ_End, ArcRadius, ArcWidth, ArcSegments);
|
||||
// Re-apply materials
|
||||
if (Mat_X) Arc_X->SetMaterial(0, Mat_X);
|
||||
if (Mat_Y) Arc_Y->SetMaterial(0, Mat_Y);
|
||||
if (Mat_Z) Arc_Z->SetMaterial(0, Mat_Z);
|
||||
|
||||
// Position guide lines from center toward each arc connection point
|
||||
const FVector GuideDir_X = FVector::ForwardVector * NewOctant.X;
|
||||
const FVector GuideDir_Y = FVector::RightVector * NewOctant.Y;
|
||||
const FVector GuideDir_Z = FVector::UpVector * NewOctant.Z;
|
||||
if (HighlightedAxis != EPS_Editor_GizmoAxis::None)
|
||||
{
|
||||
SetAxisMaterial(HighlightedAxis, Mat_Highlight);
|
||||
}
|
||||
|
||||
// Guide lines toward arc connection points (world space)
|
||||
auto PositionGuideLine = [](UStaticMeshComponent* Line, const FVector& Dir)
|
||||
{
|
||||
if (!Line) return;
|
||||
FRotator Rot = FRotationMatrix::MakeFromZ(Dir).Rotator();
|
||||
Line->SetRelativeRotation(Rot);
|
||||
Line->SetRelativeRotation(FRotationMatrix::MakeFromZ(Dir).Rotator());
|
||||
Line->SetRelativeLocation(Dir * 27.5f);
|
||||
};
|
||||
|
||||
PositionGuideLine(GuideLine_A, GuideDir_X);
|
||||
PositionGuideLine(GuideLine_B, GuideDir_Y);
|
||||
PositionGuideLine(GuideLine_C, GuideDir_Z);
|
||||
PositionGuideLine(GuideLine_A, LocalX * NewOctant.X);
|
||||
PositionGuideLine(GuideLine_B, LocalY * NewOctant.Y);
|
||||
PositionGuideLine(GuideLine_C, LocalZ * NewOctant.Z);
|
||||
|
||||
// Re-apply materials (mesh sections are recreated)
|
||||
if (Mat_X) Arc_X->SetMaterial(0, Mat_X);
|
||||
@@ -531,21 +535,26 @@ FVector APS_Editor_Gizmo::GetAxisDirection(EPS_Editor_GizmoAxis Axis) const
|
||||
default: return FVector::ZeroVector;
|
||||
}
|
||||
|
||||
// In scale mode, return the axis in the object's local space
|
||||
if (CurrentMode == EPS_Editor_TransformMode::Scale && ScaleRoot)
|
||||
// Return axis in local space if orientation is set
|
||||
if (!LocalOrientation.IsNearlyZero())
|
||||
{
|
||||
return ScaleRoot->GetRelativeRotation().RotateVector(LocalDir);
|
||||
return LocalOrientation.RotateVector(LocalDir);
|
||||
}
|
||||
|
||||
return LocalDir;
|
||||
}
|
||||
|
||||
void APS_Editor_Gizmo::SetScaleOrientation(FRotator ActorRotation)
|
||||
void APS_Editor_Gizmo::SetLocalOrientation(FRotator ActorRotation)
|
||||
{
|
||||
if (ScaleRoot)
|
||||
{
|
||||
ScaleRoot->SetRelativeRotation(ActorRotation);
|
||||
}
|
||||
LocalOrientation = ActorRotation;
|
||||
|
||||
if (TranslateRoot) TranslateRoot->SetRelativeRotation(ActorRotation);
|
||||
// RotateRoot stays at identity - arcs are generated in world space using LocalOrientation
|
||||
if (RotateRoot) RotateRoot->SetRelativeRotation(FRotator::ZeroRotator);
|
||||
if (ScaleRoot) ScaleRoot->SetRelativeRotation(ActorRotation);
|
||||
|
||||
// Force arc regeneration
|
||||
LastCameraOctant = FVector::ZeroVector;
|
||||
}
|
||||
|
||||
void APS_Editor_Gizmo::UpdateConstantScreenSize()
|
||||
|
||||
@@ -31,8 +31,11 @@ public:
|
||||
void SetHighlightedAxis(EPS_Editor_GizmoAxis Axis);
|
||||
FVector GetAxisDirection(EPS_Editor_GizmoAxis Axis) const;
|
||||
|
||||
/** Set the orientation for scale mode (matches selected actor's rotation). */
|
||||
void SetScaleOrientation(FRotator ActorRotation);
|
||||
/** Set the orientation for local space mode (matches selected actor's rotation). */
|
||||
void SetLocalOrientation(FRotator ActorRotation);
|
||||
|
||||
/** Stored local orientation for axis direction computation. */
|
||||
FRotator LocalOrientation = FRotator::ZeroRotator;
|
||||
EPS_Editor_TransformMode GetTransformMode() const { return CurrentMode; }
|
||||
|
||||
protected:
|
||||
|
||||
@@ -198,12 +198,13 @@ void UPS_Editor_SelectionManager::SetTransformMode(EPS_Editor_TransformMode NewM
|
||||
{
|
||||
Gizmo->SetTransformMode(NewMode);
|
||||
|
||||
// In scale mode, orient the gizmo to match the first selected actor's rotation
|
||||
if (NewMode == EPS_Editor_TransformMode::Scale && IsAnythingSelected())
|
||||
// Update gizmo orientation: Scale always local, others depend on space setting
|
||||
if (IsAnythingSelected())
|
||||
{
|
||||
if (AActor* Actor = SelectedActors[0].Get())
|
||||
{
|
||||
Gizmo->SetScaleOrientation(Actor->GetActorRotation());
|
||||
bool bUseLocal = (NewMode == EPS_Editor_TransformMode::Scale) || (CurrentSpace == EPS_Editor_Space::Local);
|
||||
Gizmo->SetLocalOrientation(bUseLocal ? Actor->GetActorRotation() : FRotator::ZeroRotator);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -213,9 +214,73 @@ void UPS_Editor_SelectionManager::SetTransformMode(EPS_Editor_TransformMode NewM
|
||||
void UPS_Editor_SelectionManager::SetTransformSpace(EPS_Editor_Space NewSpace)
|
||||
{
|
||||
CurrentSpace = NewSpace;
|
||||
|
||||
// Update gizmo orientation based on space
|
||||
if (Gizmo && IsAnythingSelected())
|
||||
{
|
||||
if (NewSpace == EPS_Editor_Space::Local)
|
||||
{
|
||||
if (AActor* Actor = SelectedActors[0].Get())
|
||||
{
|
||||
Gizmo->SetLocalOrientation(Actor->GetActorRotation());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Gizmo->SetLocalOrientation(FRotator::ZeroRotator);
|
||||
}
|
||||
}
|
||||
|
||||
OnSelectionChanged.Broadcast();
|
||||
}
|
||||
|
||||
void UPS_Editor_SelectionManager::RefreshGizmoOrientation()
|
||||
{
|
||||
if (!Gizmo || !IsAnythingSelected()) return;
|
||||
|
||||
if (AActor* Actor = SelectedActors[0].Get())
|
||||
{
|
||||
bool bUseLocal = (CurrentTransformMode == EPS_Editor_TransformMode::Scale) || (CurrentSpace == EPS_Editor_Space::Local);
|
||||
Gizmo->SetLocalOrientation(bUseLocal ? Actor->GetActorRotation() : FRotator::ZeroRotator);
|
||||
Gizmo->SetActorLocation(GetSelectionPivot());
|
||||
}
|
||||
}
|
||||
|
||||
void UPS_Editor_SelectionManager::ToggleTransformSpace()
|
||||
{
|
||||
SetTransformSpace(CurrentSpace == EPS_Editor_Space::World ? EPS_Editor_Space::Local : EPS_Editor_Space::World);
|
||||
}
|
||||
|
||||
FVector UPS_Editor_SelectionManager::SnapPosition(const FVector& Position) const
|
||||
{
|
||||
if (!bSnapTranslation || TranslationSnapSize <= 0.0f) return Position;
|
||||
return FVector(
|
||||
FMath::RoundToFloat(Position.X / TranslationSnapSize) * TranslationSnapSize,
|
||||
FMath::RoundToFloat(Position.Y / TranslationSnapSize) * TranslationSnapSize,
|
||||
FMath::RoundToFloat(Position.Z / TranslationSnapSize) * TranslationSnapSize
|
||||
);
|
||||
}
|
||||
|
||||
FRotator UPS_Editor_SelectionManager::SnapRotation(const FRotator& Rotation) const
|
||||
{
|
||||
if (!bSnapRotation || RotationSnapSize <= 0.0f) return Rotation;
|
||||
return FRotator(
|
||||
FMath::RoundToFloat(Rotation.Pitch / RotationSnapSize) * RotationSnapSize,
|
||||
FMath::RoundToFloat(Rotation.Yaw / RotationSnapSize) * RotationSnapSize,
|
||||
FMath::RoundToFloat(Rotation.Roll / RotationSnapSize) * RotationSnapSize
|
||||
);
|
||||
}
|
||||
|
||||
FVector UPS_Editor_SelectionManager::SnapScale(const FVector& Scale) const
|
||||
{
|
||||
if (!bSnapScale || ScaleSnapSize <= 0.0f) return Scale;
|
||||
return FVector(
|
||||
FMath::RoundToFloat(Scale.X / ScaleSnapSize) * ScaleSnapSize,
|
||||
FMath::RoundToFloat(Scale.Y / ScaleSnapSize) * ScaleSnapSize,
|
||||
FMath::RoundToFloat(Scale.Z / ScaleSnapSize) * ScaleSnapSize
|
||||
);
|
||||
}
|
||||
|
||||
void UPS_Editor_SelectionManager::SetActorHighlight(AActor* Actor, bool bHighlight)
|
||||
{
|
||||
if (!Actor)
|
||||
@@ -299,13 +364,11 @@ void UPS_Editor_SelectionManager::UpdateGizmo()
|
||||
{
|
||||
Gizmo->SetGizmoActive(true, GetSelectionPivot());
|
||||
|
||||
// Update scale orientation to match first selected actor
|
||||
if (CurrentTransformMode == EPS_Editor_TransformMode::Scale)
|
||||
{
|
||||
// Update gizmo orientation
|
||||
if (AActor* Actor = SelectedActors[0].Get())
|
||||
{
|
||||
Gizmo->SetScaleOrientation(Actor->GetActorRotation());
|
||||
}
|
||||
bool bUseLocal = (CurrentTransformMode == EPS_Editor_TransformMode::Scale) || (CurrentSpace == EPS_Editor_Space::Local);
|
||||
Gizmo->SetLocalOrientation(bUseLocal ? Actor->GetActorRotation() : FRotator::ZeroRotator);
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
@@ -55,6 +55,28 @@ public:
|
||||
|
||||
EPS_Editor_Space GetTransformSpace() const { return CurrentSpace; }
|
||||
void SetTransformSpace(EPS_Editor_Space NewSpace);
|
||||
void ToggleTransformSpace();
|
||||
|
||||
/** Refresh gizmo orientation to match current selection (call after transforms change). */
|
||||
void RefreshGizmoOrientation();
|
||||
|
||||
// ---- Snap Settings ----
|
||||
|
||||
bool bSnapTranslation = false;
|
||||
float TranslationSnapSize = 10.0f; // cm
|
||||
|
||||
bool bSnapRotation = false;
|
||||
float RotationSnapSize = 10.0f; // degrees
|
||||
|
||||
bool bSnapScale = false;
|
||||
float ScaleSnapSize = 0.1f;
|
||||
|
||||
/** Apply snap to a position value. */
|
||||
FVector SnapPosition(const FVector& Position) const;
|
||||
/** Apply snap to a rotation value. */
|
||||
FRotator SnapRotation(const FRotator& Rotation) const;
|
||||
/** Apply snap to a scale value. */
|
||||
FVector SnapScale(const FVector& Scale) const;
|
||||
|
||||
// ---- Delegates ----
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::RebuildWidget()
|
||||
.Padding(12.0f, 0.0f, 0.0f, 0.0f)
|
||||
[
|
||||
SAssignNew(ModeText, STextBlock)
|
||||
.Text(FText::FromString(TEXT("Translate (E)")))
|
||||
.Text(FText::FromString(TEXT("Translate (Z)")))
|
||||
.ColorAndOpacity(FLinearColor(0.7f, 0.7f, 0.7f, 1.0f))
|
||||
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 11))
|
||||
]
|
||||
@@ -111,7 +111,7 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::RebuildWidget()
|
||||
.Padding(FMargin(16.0f, 6.0f))
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(FText::FromString(TEXT("LMB: Select/Pan | RMB: Fly | Alt+LMB/MMB: Orbit | E/R/T: Translate/Rotate/Scale | End: Snap | Del: Delete")))
|
||||
.Text(FText::FromString(TEXT("LMB: Select/Pan | RMB: Fly | Alt+LMB/MMB: Orbit | Z/E/R: Translate/Rotate/Scale | End: Snap | Del: Delete")))
|
||||
.ColorAndOpacity(FLinearColor(0.6f, 0.6f, 0.6f, 1.0f))
|
||||
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 9))
|
||||
]
|
||||
@@ -142,11 +142,11 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildToolbar()
|
||||
[
|
||||
SNew(SHorizontalBox)
|
||||
+ SHorizontalBox::Slot().AutoWidth().Padding(2.0f, 0.0f)
|
||||
[ MakeButton(FText::FromString(TEXT("Translate (E)")), EPS_Editor_TransformMode::Translate) ]
|
||||
[ MakeButton(FText::FromString(TEXT("Translate (Z)")), EPS_Editor_TransformMode::Translate) ]
|
||||
+ SHorizontalBox::Slot().AutoWidth().Padding(2.0f, 0.0f)
|
||||
[ MakeButton(FText::FromString(TEXT("Rotate (R)")), EPS_Editor_TransformMode::Rotate) ]
|
||||
[ MakeButton(FText::FromString(TEXT("Rotate (E)")), EPS_Editor_TransformMode::Rotate) ]
|
||||
+ SHorizontalBox::Slot().AutoWidth().Padding(2.0f, 0.0f)
|
||||
[ MakeButton(FText::FromString(TEXT("Scale (T)")), EPS_Editor_TransformMode::Scale) ]
|
||||
[ MakeButton(FText::FromString(TEXT("Scale (R)")), EPS_Editor_TransformMode::Scale) ]
|
||||
+ SHorizontalBox::Slot().AutoWidth().Padding(8.0f, 0.0f, 2.0f, 0.0f)
|
||||
[
|
||||
SNew(SButton)
|
||||
@@ -164,6 +164,66 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildToolbar()
|
||||
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 10))
|
||||
]
|
||||
]
|
||||
// World/Local toggle
|
||||
+ SHorizontalBox::Slot().AutoWidth().Padding(8.0f, 0.0f, 2.0f, 0.0f)
|
||||
[
|
||||
SNew(SButton)
|
||||
.OnClicked_Lambda([this]() -> FReply
|
||||
{
|
||||
if (UPS_Editor_SelectionManager* SM = SelectionManager.Get())
|
||||
{
|
||||
SM->ToggleTransformSpace();
|
||||
}
|
||||
return FReply::Handled();
|
||||
})
|
||||
[
|
||||
SAssignNew(SpaceText, STextBlock)
|
||||
.Text(FText::FromString(TEXT("World")))
|
||||
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 10))
|
||||
]
|
||||
]
|
||||
// Snap toggle + value
|
||||
+ SHorizontalBox::Slot().AutoWidth().Padding(8.0f, 0.0f, 2.0f, 0.0f)
|
||||
[
|
||||
SNew(SButton)
|
||||
.OnClicked_Lambda([this]() -> FReply
|
||||
{
|
||||
if (UPS_Editor_SelectionManager* SM = SelectionManager.Get())
|
||||
{
|
||||
SM->bSnapTranslation = !SM->bSnapTranslation;
|
||||
SM->bSnapRotation = SM->bSnapTranslation;
|
||||
SM->bSnapScale = SM->bSnapTranslation;
|
||||
}
|
||||
return FReply::Handled();
|
||||
})
|
||||
[
|
||||
SAssignNew(SnapText, STextBlock)
|
||||
.Text(FText::FromString(TEXT("Snap: Off")))
|
||||
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 10))
|
||||
]
|
||||
]
|
||||
+ SHorizontalBox::Slot().AutoWidth().Padding(2.0f, 0.0f).VAlign(VAlign_Center)
|
||||
[
|
||||
SNew(SBox).MinDesiredWidth(40.0f)
|
||||
[
|
||||
SAssignNew(SnapSizeField, SEditableTextBox)
|
||||
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 9))
|
||||
.HintText(FText::FromString(TEXT("10")))
|
||||
.OnTextCommitted_Lambda([this](const FText& Text, ETextCommit::Type)
|
||||
{
|
||||
if (UPS_Editor_SelectionManager* SM = SelectionManager.Get())
|
||||
{
|
||||
float Val = FCString::Atof(*Text.ToString());
|
||||
if (Val > 0.0f)
|
||||
{
|
||||
SM->TranslationSnapSize = Val;
|
||||
SM->RotationSnapSize = Val;
|
||||
SM->ScaleSnapSize = Val * 0.01f;
|
||||
}
|
||||
}
|
||||
})
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
@@ -608,13 +668,27 @@ void UPS_Editor_MainWidget::RefreshPropertiesFromSelection()
|
||||
FString ModeStr;
|
||||
switch (SM->GetTransformMode())
|
||||
{
|
||||
case EPS_Editor_TransformMode::Translate: ModeStr = TEXT("Translate (E)"); break;
|
||||
case EPS_Editor_TransformMode::Rotate: ModeStr = TEXT("Rotate (R)"); break;
|
||||
case EPS_Editor_TransformMode::Scale: ModeStr = TEXT("Scale (T)"); break;
|
||||
case EPS_Editor_TransformMode::Translate: ModeStr = TEXT("Translate (Z)"); break;
|
||||
case EPS_Editor_TransformMode::Rotate: ModeStr = TEXT("Rotate (E)"); break;
|
||||
case EPS_Editor_TransformMode::Scale: ModeStr = TEXT("Scale (R)"); break;
|
||||
}
|
||||
ModeText->SetText(FText::FromString(ModeStr));
|
||||
}
|
||||
|
||||
// Update space button text
|
||||
if (SpaceText.IsValid())
|
||||
{
|
||||
SpaceText->SetText(FText::FromString(
|
||||
SM->GetTransformSpace() == EPS_Editor_Space::World ? TEXT("World") : TEXT("Local")));
|
||||
}
|
||||
|
||||
// Update snap button text
|
||||
if (SnapText.IsValid())
|
||||
{
|
||||
SnapText->SetText(FText::FromString(
|
||||
SM->bSnapTranslation ? TEXT("Snap: On") : TEXT("Snap: Off")));
|
||||
}
|
||||
|
||||
if (!SM->IsAnythingSelected())
|
||||
{
|
||||
if (SelectionInfoText.IsValid())
|
||||
|
||||
@@ -44,6 +44,11 @@ private:
|
||||
TSharedPtr<STextBlock> ModeText;
|
||||
TSharedPtr<STextBlock> SelectionInfoText;
|
||||
|
||||
// Toolbar refs
|
||||
TSharedPtr<STextBlock> SpaceText;
|
||||
TSharedPtr<STextBlock> SnapText;
|
||||
TSharedPtr<SEditableTextBox> SnapSizeField;
|
||||
|
||||
// Spawn catalog
|
||||
TSharedPtr<SVerticalBox> SpawnListContainer;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user