From 8dba2ab0fcbacb451781dfc0acbe10ff54de8310 Mon Sep 17 00:00:00 2001 From: "j.foucher" Date: Sat, 11 Apr 2026 12:45:12 +0200 Subject: [PATCH] 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) --- .../PS_Editor/GameMode/PS_Editor_Pawn.cpp | 74 ++++++++++++--- .../PS_Editor/GameMode/PS_Editor_Pawn.h | 1 + .../PS_Editor/Gizmo/PS_Editor_Gizmo.cpp | 83 +++++++++-------- .../Source/PS_Editor/Gizmo/PS_Editor_Gizmo.h | 7 +- .../Selection/PS_Editor_SelectionManager.cpp | 81 +++++++++++++++-- .../Selection/PS_Editor_SelectionManager.h | 22 +++++ .../UI/Widgets/PS_Editor_MainWidget.cpp | 90 +++++++++++++++++-- .../UI/Widgets/PS_Editor_MainWidget.h | 5 ++ 8 files changed, 294 insertions(+), 69 deletions(-) diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.cpp index 5742b85..e4dc834 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.cpp @@ -160,15 +160,15 @@ void APS_Editor_Pawn::CreateInputActions() // Transform mode hotkeys (E/R/T) IA_TranslateMode = NewObject(this, TEXT("IA_TranslateMode")); IA_TranslateMode->ValueType = EInputActionValueType::Boolean; - EditorMappingContext->MapKey(IA_TranslateMode, EKeys::E); + EditorMappingContext->MapKey(IA_TranslateMode, EKeys::Z); IA_RotateMode = NewObject(this, TEXT("IA_RotateMode")); IA_RotateMode->ValueType = EInputActionValueType::Boolean; - EditorMappingContext->MapKey(IA_RotateMode, EKeys::R); + EditorMappingContext->MapKey(IA_RotateMode, EKeys::E); IA_ScaleMode = NewObject(this, TEXT("IA_ScaleMode")); IA_ScaleMode->ValueType = EInputActionValueType::Boolean; - EditorMappingContext->MapKey(IA_ScaleMode, EKeys::T); + EditorMappingContext->MapKey(IA_ScaleMode, EKeys::R); IA_Delete = NewObject(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>& 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(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; diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.h index 5980099..4535fcf 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.h @@ -185,6 +185,7 @@ private: FVector GizmoDragInitialIntersection = FVector::ZeroVector; TArray GizmoDragInitialTransforms; float GizmoDragAccumulator = 0.0f; + FVector GizmoDragAxisDir = FVector::ZeroVector; // Fixed axis direction for the entire drag // ---- Input Handlers ---- void HandleMove(const FInputActionValue& Value); diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Gizmo/PS_Editor_Gizmo.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Gizmo/PS_Editor_Gizmo.cpp index bfdbb0b..36883c4 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Gizmo/PS_Editor_Gizmo.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Gizmo/PS_Editor_Gizmo.cpp @@ -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() diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Gizmo/PS_Editor_Gizmo.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Gizmo/PS_Editor_Gizmo.h index 0fdb6a5..558c5fa 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Gizmo/PS_Editor_Gizmo.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Gizmo/PS_Editor_Gizmo.h @@ -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: diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_SelectionManager.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_SelectionManager.cpp index d2bf088..ecf17a3 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_SelectionManager.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_SelectionManager.cpp @@ -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 diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_SelectionManager.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_SelectionManager.h index 3276ac5..60c65aa 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_SelectionManager.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_SelectionManager.h @@ -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 ---- diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.cpp index 0c09984..1cf11c8 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.cpp @@ -63,7 +63,7 @@ TSharedRef 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 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 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 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()) diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.h index 3a0e396..4329a09 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.h @@ -44,6 +44,11 @@ private: TSharedPtr ModeText; TSharedPtr SelectionInfoText; + // Toolbar refs + TSharedPtr SpaceText; + TSharedPtr SnapText; + TSharedPtr SnapSizeField; + // Spawn catalog TSharedPtr SpawnListContainer;