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) // Transform mode hotkeys (E/R/T)
IA_TranslateMode = NewObject<UInputAction>(this, TEXT("IA_TranslateMode")); IA_TranslateMode = NewObject<UInputAction>(this, TEXT("IA_TranslateMode"));
IA_TranslateMode->ValueType = EInputActionValueType::Boolean; 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 = NewObject<UInputAction>(this, TEXT("IA_RotateMode"));
IA_RotateMode->ValueType = EInputActionValueType::Boolean; 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 = NewObject<UInputAction>(this, TEXT("IA_ScaleMode"));
IA_ScaleMode->ValueType = EInputActionValueType::Boolean; 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 = NewObject<UInputAction>(this, TEXT("IA_Delete"));
IA_Delete->ValueType = EInputActionValueType::Boolean; IA_Delete->ValueType = EInputActionValueType::Boolean;
@@ -318,7 +318,8 @@ void APS_Editor_Pawn::HandleLook(const FInputActionValue& Value)
APS_Editor_Gizmo* Gizmo = SM->GetGizmo(); APS_Editor_Gizmo* Gizmo = SM->GetGizmo();
if (!Gizmo) return; 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 EPS_Editor_TransformMode Mode = SM->GetTransformMode();
const TArray<TWeakObjectPtr<AActor>>& Selected = SM->GetSelectedActors(); 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; if (!RayPlaneIntersection(RayOrigin, RayDir, GizmoDragPlaneOrigin, GizmoDragPlaneNormal, CurrentIntersection)) return;
const FVector TotalDelta = CurrentIntersection - GizmoDragInitialIntersection; 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) 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, ScreenStart);
DragPC->ProjectWorldLocationToScreen(GizmoDragPlaneOrigin + AxisDir * 100.0f, ScreenEnd); DragPC->ProjectWorldLocationToScreen(GizmoDragPlaneOrigin + AxisDir * 100.0f, ScreenEnd);
FVector2D ScreenAxisDir = (ScreenEnd - ScreenStart).GetSafeNormal(); const float ScreenLen = (ScreenEnd - ScreenStart).Size();
// Mouse movement perpendicular to the projected axis drives rotation
FVector2D ScreenPerp(-ScreenAxisDir.Y, ScreenAxisDir.X);
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; 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; const FVector GizmoLoc = GizmoDragPlaneOrigin;
for (int32 i = 0; i < Selected.Num() && i < GizmoDragInitialTransforms.Num(); ++i) 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::X) NewScale.X *= ScaleFactor;
else if (ActiveGizmoAxis == EPS_Editor_GizmoAxis::Y) NewScale.Y *= ScaleFactor; else if (ActiveGizmoAxis == EPS_Editor_GizmoAxis::Y) NewScale.Y *= ScaleFactor;
else if (ActiveGizmoAxis == EPS_Editor_GizmoAxis::Z) NewScale.Z *= 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) if (Mode == EPS_Editor_TransformMode::Translate)
{ {
Gizmo->SetActorLocation(SM->GetSelectionPivot()); 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; 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); SetCameraMode(EPS_Editor_CameraMode::Idle);
} }
else if (CameraMode == EPS_Editor_CameraMode::Pan || CameraMode == EPS_Editor_CameraMode::Orbit) 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; 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(); const EPS_Editor_TransformMode Mode = SM->GetTransformMode();
FVector PlaneNormal; FVector PlaneNormal;

View File

@@ -185,6 +185,7 @@ private:
FVector GizmoDragInitialIntersection = FVector::ZeroVector; FVector GizmoDragInitialIntersection = FVector::ZeroVector;
TArray<FTransform> GizmoDragInitialTransforms; TArray<FTransform> GizmoDragInitialTransforms;
float GizmoDragAccumulator = 0.0f; float GizmoDragAccumulator = 0.0f;
FVector GizmoDragAxisDir = FVector::ZeroVector; // Fixed axis direction for the entire drag
// ---- Input Handlers ---- // ---- Input Handlers ----
void HandleMove(const FInputActionValue& Value); void HandleMove(const FInputActionValue& Value);

View File

@@ -455,58 +455,62 @@ void APS_Editor_Gizmo::UpdateArcFacing()
FRotator CameraRotation; FRotator CameraRotation;
PC->GetPlayerViewPoint(CameraLocation, 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(); 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( const FVector NewOctant(
CamDir.X >= 0 ? 1.0f : -1.0f, FVector::DotProduct(CamDir, LocalX) >= 0 ? 1.0f : -1.0f,
CamDir.Y >= 0 ? 1.0f : -1.0f, FVector::DotProduct(CamDir, LocalY) >= 0 ? 1.0f : -1.0f,
CamDir.Z >= 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)) if (NewOctant.Equals(LastCameraOctant))
{ {
return; return;
} }
LastCameraOctant = NewOctant; LastCameraOctant = NewOctant;
// For each axis, show the quarter-arc facing the camera. // Arc directions in world space using local axes
// The arc start/end directions are chosen based on camera octant. 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 ArcY_Start = LocalX * NewOctant.X;
const FVector ArcX_Start = FVector::RightVector * NewOctant.Y; // +Y or -Y const FVector ArcY_End = LocalZ * NewOctant.Z;
const FVector ArcX_End = FVector::UpVector * NewOctant.Z; // +Z or -Z
// Y-axis (green, arc in XZ plane): choose the X and Z quadrant facing camera const FVector ArcZ_Start = LocalX * NewOctant.X;
const FVector ArcY_Start = FVector::ForwardVector * NewOctant.X; // +X or -X const FVector ArcZ_End = LocalY * NewOctant.Y;
const FVector ArcY_End = FVector::UpVector * NewOctant.Z; // +Z or -Z
// Z-axis (blue, arc in XY plane): choose the X and Y quadrant facing camera CreateArcRing(Arc_X, LocalX, ArcX_Start, ArcX_End, ArcRadius, ArcWidth, ArcSegments);
const FVector ArcZ_Start = FVector::ForwardVector * NewOctant.X; // +X or -X CreateArcRing(Arc_Y, LocalY, ArcY_Start, ArcY_End, ArcRadius, ArcWidth, ArcSegments);
const FVector ArcZ_End = FVector::RightVector * NewOctant.Y; // +Y or -Y CreateArcRing(Arc_Z, LocalZ, ArcZ_Start, ArcZ_End, ArcRadius, ArcWidth, ArcSegments);
CreateArcRing(Arc_X, FVector::ForwardVector, ArcX_Start, ArcX_End, ArcRadius, ArcWidth, ArcSegments); // Re-apply materials
CreateArcRing(Arc_Y, FVector::RightVector, ArcY_Start, ArcY_End, ArcRadius, ArcWidth, ArcSegments); if (Mat_X) Arc_X->SetMaterial(0, Mat_X);
CreateArcRing(Arc_Z, FVector::UpVector, ArcZ_Start, ArcZ_End, ArcRadius, ArcWidth, ArcSegments); 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 if (HighlightedAxis != EPS_Editor_GizmoAxis::None)
const FVector GuideDir_X = FVector::ForwardVector * NewOctant.X; {
const FVector GuideDir_Y = FVector::RightVector * NewOctant.Y; SetAxisMaterial(HighlightedAxis, Mat_Highlight);
const FVector GuideDir_Z = FVector::UpVector * NewOctant.Z; }
// Guide lines toward arc connection points (world space)
auto PositionGuideLine = [](UStaticMeshComponent* Line, const FVector& Dir) auto PositionGuideLine = [](UStaticMeshComponent* Line, const FVector& Dir)
{ {
if (!Line) return; if (!Line) return;
FRotator Rot = FRotationMatrix::MakeFromZ(Dir).Rotator(); Line->SetRelativeRotation(FRotationMatrix::MakeFromZ(Dir).Rotator());
Line->SetRelativeRotation(Rot);
Line->SetRelativeLocation(Dir * 27.5f); Line->SetRelativeLocation(Dir * 27.5f);
}; };
PositionGuideLine(GuideLine_A, GuideDir_X); PositionGuideLine(GuideLine_A, LocalX * NewOctant.X);
PositionGuideLine(GuideLine_B, GuideDir_Y); PositionGuideLine(GuideLine_B, LocalY * NewOctant.Y);
PositionGuideLine(GuideLine_C, GuideDir_Z); PositionGuideLine(GuideLine_C, LocalZ * NewOctant.Z);
// Re-apply materials (mesh sections are recreated) // Re-apply materials (mesh sections are recreated)
if (Mat_X) Arc_X->SetMaterial(0, Mat_X); 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; default: return FVector::ZeroVector;
} }
// In scale mode, return the axis in the object's local space // Return axis in local space if orientation is set
if (CurrentMode == EPS_Editor_TransformMode::Scale && ScaleRoot) if (!LocalOrientation.IsNearlyZero())
{ {
return ScaleRoot->GetRelativeRotation().RotateVector(LocalDir); return LocalOrientation.RotateVector(LocalDir);
} }
return LocalDir; return LocalDir;
} }
void APS_Editor_Gizmo::SetScaleOrientation(FRotator ActorRotation) void APS_Editor_Gizmo::SetLocalOrientation(FRotator ActorRotation)
{ {
if (ScaleRoot) LocalOrientation = ActorRotation;
{
ScaleRoot->SetRelativeRotation(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() void APS_Editor_Gizmo::UpdateConstantScreenSize()

View File

@@ -31,8 +31,11 @@ public:
void SetHighlightedAxis(EPS_Editor_GizmoAxis Axis); void SetHighlightedAxis(EPS_Editor_GizmoAxis Axis);
FVector GetAxisDirection(EPS_Editor_GizmoAxis Axis) const; FVector GetAxisDirection(EPS_Editor_GizmoAxis Axis) const;
/** Set the orientation for scale mode (matches selected actor's rotation). */ /** Set the orientation for local space mode (matches selected actor's rotation). */
void SetScaleOrientation(FRotator ActorRotation); void SetLocalOrientation(FRotator ActorRotation);
/** Stored local orientation for axis direction computation. */
FRotator LocalOrientation = FRotator::ZeroRotator;
EPS_Editor_TransformMode GetTransformMode() const { return CurrentMode; } EPS_Editor_TransformMode GetTransformMode() const { return CurrentMode; }
protected: protected:

View File

@@ -198,12 +198,13 @@ void UPS_Editor_SelectionManager::SetTransformMode(EPS_Editor_TransformMode NewM
{ {
Gizmo->SetTransformMode(NewMode); Gizmo->SetTransformMode(NewMode);
// In scale mode, orient the gizmo to match the first selected actor's rotation // Update gizmo orientation: Scale always local, others depend on space setting
if (NewMode == EPS_Editor_TransformMode::Scale && IsAnythingSelected()) if (IsAnythingSelected())
{ {
if (AActor* Actor = SelectedActors[0].Get()) 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) void UPS_Editor_SelectionManager::SetTransformSpace(EPS_Editor_Space NewSpace)
{ {
CurrentSpace = 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(); 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) void UPS_Editor_SelectionManager::SetActorHighlight(AActor* Actor, bool bHighlight)
{ {
if (!Actor) if (!Actor)
@@ -299,13 +364,11 @@ void UPS_Editor_SelectionManager::UpdateGizmo()
{ {
Gizmo->SetGizmoActive(true, GetSelectionPivot()); Gizmo->SetGizmoActive(true, GetSelectionPivot());
// Update scale orientation to match first selected actor // Update gizmo orientation
if (CurrentTransformMode == EPS_Editor_TransformMode::Scale) if (AActor* Actor = SelectedActors[0].Get())
{ {
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->SetScaleOrientation(Actor->GetActorRotation());
}
} }
} }
else else

View File

@@ -55,6 +55,28 @@ public:
EPS_Editor_Space GetTransformSpace() const { return CurrentSpace; } EPS_Editor_Space GetTransformSpace() const { return CurrentSpace; }
void SetTransformSpace(EPS_Editor_Space NewSpace); 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 ---- // ---- Delegates ----

View File

@@ -63,7 +63,7 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::RebuildWidget()
.Padding(12.0f, 0.0f, 0.0f, 0.0f) .Padding(12.0f, 0.0f, 0.0f, 0.0f)
[ [
SAssignNew(ModeText, STextBlock) 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)) .ColorAndOpacity(FLinearColor(0.7f, 0.7f, 0.7f, 1.0f))
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 11)) .Font(FCoreStyle::GetDefaultFontStyle("Regular", 11))
] ]
@@ -111,7 +111,7 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::RebuildWidget()
.Padding(FMargin(16.0f, 6.0f)) .Padding(FMargin(16.0f, 6.0f))
[ [
SNew(STextBlock) 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)) .ColorAndOpacity(FLinearColor(0.6f, 0.6f, 0.6f, 1.0f))
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 9)) .Font(FCoreStyle::GetDefaultFontStyle("Regular", 9))
] ]
@@ -142,11 +142,11 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildToolbar()
[ [
SNew(SHorizontalBox) SNew(SHorizontalBox)
+ SHorizontalBox::Slot().AutoWidth().Padding(2.0f, 0.0f) + 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) + 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) + 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) + SHorizontalBox::Slot().AutoWidth().Padding(8.0f, 0.0f, 2.0f, 0.0f)
[ [
SNew(SButton) SNew(SButton)
@@ -164,6 +164,66 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildToolbar()
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 10)) .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; FString ModeStr;
switch (SM->GetTransformMode()) switch (SM->GetTransformMode())
{ {
case EPS_Editor_TransformMode::Translate: ModeStr = TEXT("Translate (E)"); break; case EPS_Editor_TransformMode::Translate: ModeStr = TEXT("Translate (Z)"); break;
case EPS_Editor_TransformMode::Rotate: ModeStr = TEXT("Rotate (R)"); break; case EPS_Editor_TransformMode::Rotate: ModeStr = TEXT("Rotate (E)"); break;
case EPS_Editor_TransformMode::Scale: ModeStr = TEXT("Scale (T)"); break; case EPS_Editor_TransformMode::Scale: ModeStr = TEXT("Scale (R)"); break;
} }
ModeText->SetText(FText::FromString(ModeStr)); 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 (!SM->IsAnythingSelected())
{ {
if (SelectionInfoText.IsValid()) if (SelectionInfoText.IsValid())

View File

@@ -44,6 +44,11 @@ private:
TSharedPtr<STextBlock> ModeText; TSharedPtr<STextBlock> ModeText;
TSharedPtr<STextBlock> SelectionInfoText; TSharedPtr<STextBlock> SelectionInfoText;
// Toolbar refs
TSharedPtr<STextBlock> SpaceText;
TSharedPtr<STextBlock> SnapText;
TSharedPtr<SEditableTextBox> SnapSizeField;
// Spawn catalog // Spawn catalog
TSharedPtr<SVerticalBox> SpawnListContainer; TSharedPtr<SVerticalBox> SpawnListContainer;