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)
|
// 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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 ----
|
||||||
|
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user