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:
2026-04-11 12:45:12 +02:00
parent 48505bfb82
commit 8dba2ab0fc
8 changed files with 294 additions and 69 deletions

View File

@@ -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;

View File

@@ -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);

View File

@@ -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()

View File

@@ -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:

View File

@@ -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())
{
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

View File

@@ -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 ----

View File

@@ -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())

View File

@@ -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;