Files
UnrealEngine/Engine/Source/Editor/DetailCustomizations/Private/ComponentTransformDetails.cpp
2025-05-18 13:04:45 +08:00

1933 lines
68 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "ComponentTransformDetails.h"
#include "Algo/Transform.h"
#include "Components/SceneComponent.h"
#include "Containers/ArrayView.h"
#include "Containers/Set.h"
#include "Containers/UnrealString.h"
#include "CoreGlobals.h"
#include "DetailCategoryBuilder.h"
#include "DetailLayoutBuilder.h"
#include "DetailWidgetRow.h"
#include "Editor.h"
#include "Editor/EditorEngine.h"
#include "Editor/UnrealEdEngine.h"
#include "Fonts/SlateFontInfo.h"
#include "Framework/Application/SlateApplication.h"
#include "Framework/Commands/UICommandInfo.h"
#include "Framework/MultiBox/MultiBoxBuilder.h"
#include "GameFramework/Actor.h"
#include "HAL/PlatformApplicationMisc.h"
#include "IDetailChildrenBuilder.h"
#include "IDetailPropertyRow.h"
#include "Input/Events.h"
#include "Internationalization/Internationalization.h"
#include "IPropertyUtilities.h"
#include "PropertyEditorArchetypePolicy.h"
#include "Kismet2/ComponentEditorUtils.h"
#include "Layout/Margin.h"
#include "Math/Quat.h"
#include "Math/Transform.h"
#include "Math/UnitConversion.h"
#include "Misc/AssertionMacros.h"
#include "Misc/Attribute.h"
#include "Misc/AxisDisplayInfo.h"
#include "Misc/ConfigCacheIni.h"
#include "Misc/NotifyHook.h"
#include "PropertyEditorCopyPaste.h"
#include "PropertyHandle.h"
#include "ScopedTransaction.h"
#include "Settings/EditorProjectSettings.h"
#include "SlateOptMacros.h"
#include "SlotBase.h"
#include "Styling/AppStyle.h"
#include "Styling/SlateColor.h"
#include "Templates/Casts.h"
#include "Templates/UnrealTemplate.h"
#include "Textures/SlateIcon.h"
#include "Types/SlateStructs.h"
#include "UnrealEdGlobals.h"
#include "UObject/Class.h"
#include "UObject/Object.h"
#include "UObject/ObjectMacros.h"
#include "UObject/ObjectPtr.h"
#include "UObject/Package.h"
#include "UObject/UnrealNames.h"
#include "UObject/UnrealType.h"
#include "UObject/UObjectGlobals.h"
#include "Widgets/DeclarativeSyntaxSupport.h"
#include "Widgets/Images/SImage.h"
#include "Widgets/Input/NumericUnitTypeInterface.inl"
#include "Widgets/Input/SCheckBox.h"
#include "Widgets/Input/SComboButton.h"
#include "Widgets/Input/SRotatorInputBox.h"
#include "Widgets/Input/SVectorInputBox.h"
#include "Widgets/Layout/SBox.h"
#include "Widgets/SBoxPanel.h"
#include "Widgets/Text/STextBlock.h"
#include <utility>
class SWidget;
class UWorld;
struct FSlateBrush;
#define LOCTEXT_NAMESPACE "FComponentTransformDetails"
namespace UE::DetailsCustomizations::Internal
{
/** Lookup to get the property name for the given TransformField. */
static TMap<ETransformField::Type, FString> TransformFieldToPropertyNameString = {
{ ETransformField::Location, USceneComponent::GetRelativeLocationPropertyName().ToString() },
{ ETransformField::Rotation, USceneComponent::GetRelativeRotationPropertyName().ToString() },
{ ETransformField::Scale, USceneComponent::GetRelativeScale3DPropertyName().ToString() }
};
}
class FScopedSwitchWorldForObject
{
public:
FScopedSwitchWorldForObject( UObject* Object )
: PrevWorld( NULL )
{
bool bRequiresPlayWorld = false;
if( GUnrealEd->PlayWorld && !GIsPlayInEditorWorld )
{
UPackage* ObjectPackage = Object->GetOutermost();
bRequiresPlayWorld = ObjectPackage->HasAnyPackageFlags(PKG_PlayInEditor);
}
if( bRequiresPlayWorld )
{
PrevWorld = SetPlayInEditorWorld( GUnrealEd->PlayWorld );
}
}
~FScopedSwitchWorldForObject()
{
if( PrevWorld )
{
RestoreEditorWorld( PrevWorld );
}
}
private:
UWorld* PrevWorld;
};
static USceneComponent* GetSceneComponentFromDetailsObject(UObject* InObject)
{
AActor* Actor = Cast<AActor>(InObject);
if(Actor)
{
return Actor->GetRootComponent();
}
return Cast<USceneComponent>(InObject);
}
namespace ComponentTransformDetails::Private
{
bool AreRotationsEqual(const FVector& Lhs, const FVector& Rhs)
{
constexpr double RotationEpsilon = 1.e-4;
const double AbsDiffX = FMath::Abs(Lhs.X - Rhs.X);
const double AbsDiffY = FMath::Abs(Lhs.Y - Rhs.Y);
const double AbsDiffZ = FMath::Abs(Lhs.Z - Rhs.Z);
return AbsDiffX < RotationEpsilon && AbsDiffY < RotationEpsilon && AbsDiffZ < RotationEpsilon;
}
}
FComponentTransformDetails::FComponentTransformDetails( const TArray< TWeakObjectPtr<UObject> >& InSelectedObjects, const FSelectedActorInfo& InSelectedActorInfo, IDetailLayoutBuilder& DetailBuilder )
: TNumericUnitTypeInterface(GetDefault<UEditorProjectAppearanceSettings>()->bDisplayUnitsOnComponentTransforms ? EUnit::Centimeters : EUnit::Unspecified)
, SelectedActorInfo( InSelectedActorInfo )
, SelectedObjects( InSelectedObjects )
, NotifyHook( DetailBuilder.GetPropertyUtilities()->GetNotifyHook() )
, bPreserveScaleRatio( false )
, bEditingRotationInUI( false )
, bIsSliderTransaction( false )
, HiddenFieldMask( 0 )
, bIsEnabledCache( false )
, bIsAxisDisplayLeftUpForward( AxisDisplayInfo::GetAxisDisplayCoordinateSystem() == EAxisList::LeftUpForward )
{
GConfig->GetBool(TEXT("SelectionDetails"), TEXT("PreserveScaleRatio"), bPreserveScaleRatio, GEditorPerProjectIni);
FCoreUObjectDelegates::OnObjectsReplaced.AddRaw(this, &FComponentTransformDetails::OnObjectsReplaced);
}
FComponentTransformDetails::~FComponentTransformDetails()
{
FCoreUObjectDelegates::OnObjectsReplaced.RemoveAll(this);
}
TSharedRef<SWidget> FComponentTransformDetails::BuildTransformFieldLabel( ETransformField::Type TransformField )
{
FText Label;
switch( TransformField )
{
case ETransformField::Rotation:
Label = LOCTEXT( "RotationLabel", "Rotation");
break;
case ETransformField::Scale:
Label = LOCTEXT( "ScaleLabel", "Scale" );
break;
case ETransformField::Location:
default:
Label = LOCTEXT("LocationLabel", "Location");
break;
}
FMenuBuilder MenuBuilder( true, NULL, NULL );
FUIAction SetRelativeLocationAction
(
FExecuteAction::CreateSP( this, &FComponentTransformDetails::OnSetAbsoluteTransform, TransformField, false ),
FCanExecuteAction(),
FIsActionChecked::CreateSP( this, &FComponentTransformDetails::IsAbsoluteTransformChecked, TransformField, false )
);
FUIAction SetWorldLocationAction
(
FExecuteAction::CreateSP( this, &FComponentTransformDetails::OnSetAbsoluteTransform, TransformField, true ),
FCanExecuteAction(),
FIsActionChecked::CreateSP( this, &FComponentTransformDetails::IsAbsoluteTransformChecked, TransformField, true )
);
MenuBuilder.BeginSection( TEXT("TransformType"), FText::Format( LOCTEXT("TransformType", "{0} Type"), Label ) );
MenuBuilder.AddMenuEntry
(
FText::Format( LOCTEXT( "RelativeLabel", "Relative"), Label ),
FText::Format( LOCTEXT( "RelativeLabel_ToolTip", "{0} is relative to its parent"), Label ),
FSlateIcon(),
SetRelativeLocationAction,
NAME_None,
EUserInterfaceActionType::RadioButton
);
MenuBuilder.AddMenuEntry
(
FText::Format( LOCTEXT( "WorldLabel", "World"), Label ),
FText::Format( LOCTEXT( "WorldLabel_ToolTip", "{0} is relative to the world"), Label ),
FSlateIcon(),
SetWorldLocationAction,
NAME_None,
EUserInterfaceActionType::RadioButton
);
MenuBuilder.EndSection();
TSharedRef<SHorizontalBox> NameContent =
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.VAlign(VAlign_Center)
[
SNew(SComboButton)
.ContentPadding(0.f)
.IsEnabled(this, &FComponentTransformDetails::CanChangeAbsoluteFlag, TransformField)
.MenuContent()
[
MenuBuilder.MakeWidget()
]
.ButtonContent()
[
SNew( SBox )
.Padding( FMargin( 0.0f, 0.0f, 2.0f, 0.0f ) )
.MinDesiredWidth(50.f)
[
SNew(STextBlock)
.Text(this, &FComponentTransformDetails::GetTransformFieldText, TransformField)
.Font(IDetailLayoutBuilder::GetDetailFont())
]
]
];
if (TransformField == ETransformField::Scale)
{
NameContent->AddSlot()
.AutoWidth()
.VAlign(VAlign_Center)
.Padding(FMargin(4.0f, 0.0f, 0.0f, 0.0f))
[
// Add a checkbox to toggle between preserving the ratio of x,y,z components of scale when a value is entered
SNew(SCheckBox)
.IsChecked(this, &FComponentTransformDetails::IsPreserveScaleRatioChecked)
.IsEnabled(this, &FComponentTransformDetails::GetIsScaleEnabled)
.OnCheckStateChanged(this, &FComponentTransformDetails::OnPreserveScaleRatioToggled)
.Style(FAppStyle::Get(), "TransparentCheckBox")
.ToolTipText(LOCTEXT("PreserveScaleToolTip", "When locked, all axis values scale together so the object maintains its proportions in all directions."))
[
SNew(SImage)
.Image(this, &FComponentTransformDetails::GetPreserveScaleRatioImage)
.ColorAndOpacity(FSlateColor::UseForeground())
]
];
}
return NameContent;
}
FText FComponentTransformDetails::GetTransformFieldText( ETransformField::Type TransformField ) const
{
switch (TransformField)
{
case ETransformField::Location:
return GetLocationText();
case ETransformField::Rotation:
return GetRotationText();
case ETransformField::Scale:
return GetScaleText();
default:
return FText::GetEmpty();
}
}
bool FComponentTransformDetails::OnCanCopy( ETransformField::Type TransformField ) const
{
// We can only copy values if the whole field is set. If multiple values are defined we do not copy since we are unable to determine the value
switch (TransformField)
{
case ETransformField::Location:
return CachedLocation.IsSet();
case ETransformField::Rotation:
return CachedRotation.IsSet();
case ETransformField::Scale:
return CachedScale.IsSet();
default:
return false;
}
}
void FComponentTransformDetails::OnCopy( ETransformField::Type TransformField )
{
CacheDetails();
FString CopyStr;
switch (TransformField)
{
case ETransformField::Location:
CopyStr = FString::Printf(TEXT("(X=%f,Y=%f,Z=%f)"), GetLocationX().GetValue(), GetLocationY().GetValue(), GetLocationZ().GetValue());
break;
case ETransformField::Rotation:
CopyStr = FString::Printf(TEXT("(Pitch=%f,Yaw=%f,Roll=%f)"), CachedRotation.Y.GetValue(), CachedRotation.Z.GetValue(), CachedRotation.X.GetValue());
break;
case ETransformField::Scale:
CopyStr = FString::Printf(TEXT("(X=%f,Y=%f,Z=%f)"), CachedScale.X.GetValue(), CachedScale.Y.GetValue(), CachedScale.Z.GetValue());
break;
default:
break;
}
if( !CopyStr.IsEmpty() )
{
FPlatformApplicationMisc::ClipboardCopy( *CopyStr );
}
}
void FComponentTransformDetails::OnPaste( ETransformField::Type TransformField )
{
FString PastedText;
FPlatformApplicationMisc::ClipboardPaste(PastedText);
PasteFromText(TEXT(""), PastedText, TransformField);
}
void FComponentTransformDetails::OnPasteFromText(
const FString& InTag,
const FString& InText,
const TOptional<FGuid>& InOperationId,
ETransformField::Type InTransformField)
{
PasteFromText(InTag, InText, InTransformField);
}
void FComponentTransformDetails::PasteFromText(
const FString& InTag,
const FString& InText,
ETransformField::Type InTransformField)
{
if (InText.IsEmpty())
{
return;
}
FString Text = InText;
if (!InTag.IsEmpty())
{
const FString PropertyPath = UE::PropertyEditor::GetPropertyPath(GetPropertyHandle());
// ensure that if tag is specified, that it matches the subscriber
if (!InTag.Equals(UE::DetailsCustomizations::Internal::TransformFieldToPropertyNameString[InTransformField]))
{
return;
}
}
switch (InTransformField)
{
case ETransformField::Location:
{
FVector Location;
if (Location.InitFromString(Text))
{
FScopedTransaction Transaction(LOCTEXT("PasteLocation", "Paste Location"));
OnSetTransform(ETransformField::Location, EAxisList::All, Location, false, true);
}
}
break;
case ETransformField::Rotation:
{
FRotator Rotation;
Text.ReplaceInline(TEXT("Pitch="), TEXT("P="));
Text.ReplaceInline(TEXT("Yaw="), TEXT("Y="));
Text.ReplaceInline(TEXT("Roll="), TEXT("R="));
if (Rotation.InitFromString(Text))
{
FScopedTransaction Transaction(LOCTEXT("PasteRotation", "Paste Rotation"));
OnSetTransform(ETransformField::Rotation, EAxisList::All, Rotation.Euler(), false, true);
}
}
break;
case ETransformField::Scale:
{
FVector Scale;
if (Scale.InitFromString(Text))
{
FScopedTransaction Transaction(LOCTEXT("PasteScale", "Paste Scale"));
OnSetTransform(ETransformField::Scale, EAxisList::All, Scale, false, true);
}
}
break;
default:
break;
}
}
FUIAction FComponentTransformDetails::CreateCopyAction( ETransformField::Type TransformField ) const
{
return
FUIAction
(
FExecuteAction::CreateSP(const_cast<FComponentTransformDetails*>(this), &FComponentTransformDetails::OnCopy, TransformField ),
FCanExecuteAction::CreateSP(const_cast<FComponentTransformDetails*>(this), &FComponentTransformDetails::OnCanCopy, TransformField )
);
}
FUIAction FComponentTransformDetails::CreatePasteAction( ETransformField::Type TransformField ) const
{
return
FUIAction( FExecuteAction::CreateSP(const_cast<FComponentTransformDetails*>(this), &FComponentTransformDetails::OnPaste, TransformField ) );
}
BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION
void FComponentTransformDetails::GenerateChildContent( IDetailChildrenBuilder& ChildrenBuilder )
{
UClass* SceneComponentClass = USceneComponent::StaticClass();
FSlateFontInfo FontInfo = IDetailLayoutBuilder::GetDetailFont();
const bool bHideLocationField = ( HiddenFieldMask & ( 1 << ETransformField::Location ) ) != 0;
const bool bHideRotationField = ( HiddenFieldMask & ( 1 << ETransformField::Rotation ) ) != 0;
const bool bHideScaleField = ( HiddenFieldMask & ( 1 << ETransformField::Scale ) ) != 0;
IDetailCategoryBuilder& ParentCategory = ChildrenBuilder.GetParentCategory();
IDetailLayoutBuilder& LayoutBuilder = ParentCategory.GetParentLayout();
TSharedPtr<IPropertyHandle> LocationPropertyHandle = LayoutBuilder.GetProperty(USceneComponent::GetRelativeLocationPropertyName(), USceneComponent::StaticClass());
TSharedPtr<IPropertyHandle> RotationPropertyHandle = LayoutBuilder.GetProperty(USceneComponent::GetRelativeRotationPropertyName(), USceneComponent::StaticClass());
TSharedPtr<IPropertyHandle> ScalePropertyHandle = LayoutBuilder.GetProperty(USceneComponent::GetRelativeScale3DPropertyName(), USceneComponent::StaticClass());
const FString& MetaLocationDeltaString = LocationPropertyHandle->GetMetaData("Delta");
const FString& MetaRotationDeltaString = RotationPropertyHandle->GetMetaData("Delta");
const FString& MetaRotationMinString = RotationPropertyHandle->GetMetaData("UIMin");
const FString& MetaRotationMaxString = RotationPropertyHandle->GetMetaData("UIMax");
const FString& MetaScaleDeltaString = ScalePropertyHandle->GetMetaData("Delta");
float LocationSpinDelta = !MetaLocationDeltaString.IsEmpty() ? FCString::Atof(*MetaLocationDeltaString) : 1.f;
float RotationSpinDelta = !MetaRotationDeltaString.IsEmpty() ? FCString::Atof(*MetaRotationDeltaString) : 1.f;
TOptional<FRotator::FReal> RotationMin = !MetaRotationMinString.IsEmpty() ? FCString::Atof(*MetaRotationMinString) : TOptional<FRotator::FReal>();
TOptional<FRotator::FReal> RotationMax = !MetaRotationMaxString.IsEmpty() ? FCString::Atof(*MetaRotationMaxString) : TOptional<FRotator::FReal>();
float ScaleSpinDelta = !MetaScaleDeltaString.IsEmpty() ? FCString::Atof(*MetaScaleDeltaString) : 0.0025f;
// Location
if(!bHideLocationField)
{
TSharedPtr<INumericTypeInterface<FVector::FReal>> TypeInterface;
if( FUnitConversion::Settings().ShouldDisplayUnits() )
{
TypeInterface = SharedThis(this);
}
ParentCategory.OnPasteFromText()->AddSP(this, &FComponentTransformDetails::OnPasteFromText, ETransformField::Location);
FindOrCreatePropertyHandle(USceneComponent::GetAbsoluteLocationPropertyName(), ChildrenBuilder);
TSharedPtr<IPropertyHandle> PropertyHandle = FindOrCreatePropertyHandle(USceneComponent::GetRelativeLocationPropertyName(), ChildrenBuilder);
ChildrenBuilder.AddCustomRow( LOCTEXT("LocationFilter", "Location") )
.RowTag("Location")
.CopyAction( CreateCopyAction( ETransformField::Location ) )
.PasteAction( CreatePasteAction( ETransformField::Location ) )
.OverrideResetToDefault(FResetToDefaultOverride::Create(TAttribute<bool>(this, &FComponentTransformDetails::GetLocationResetVisibility), FSimpleDelegate::CreateSP(this, &FComponentTransformDetails::OnLocationResetClicked)))
.PropertyHandleList({ PropertyHandle })
.IsEnabled(TAttribute<bool>(this, &FComponentTransformDetails::GetIsEnabled))
.NameContent()
.VAlign(VAlign_Center)
[
BuildTransformFieldLabel( ETransformField::Location )
]
.ValueContent()
.MinDesiredWidth(125.0f * 3.0f)
.MaxDesiredWidth(125.0f * 3.0f)
.VAlign(VAlign_Center)
[
SNew(SNumericVectorInputBox<FVector::FReal>)
.X(this, &FComponentTransformDetails::GetLocationX)
.Y(this, &FComponentTransformDetails::GetLocationY)
.Z(this, &FComponentTransformDetails::GetLocationZ)
.XDisplayName(AxisDisplayInfo::GetAxisDisplayName(EAxisList::Forward))
.YDisplayName(AxisDisplayInfo::GetAxisDisplayName(EAxisList::Left))
.ZDisplayName(AxisDisplayInfo::GetAxisDisplayName(EAxisList::Up))
.bColorAxisLabels(true)
.Swizzle(AxisDisplayInfo::GetTransformAxisSwizzle())
.IsEnabled(this, &FComponentTransformDetails::GetIsLocationEnabled)
.OnXChanged(this, &FComponentTransformDetails::OnSetTransformAxis, ETextCommit::Default, ETransformField::Location, EAxisList::X, false)
.OnYChanged(this, &FComponentTransformDetails::OnSetTransformAxis, ETextCommit::Default, ETransformField::Location, EAxisList::Y, false)
.OnZChanged(this, &FComponentTransformDetails::OnSetTransformAxis, ETextCommit::Default, ETransformField::Location, EAxisList::Z, false)
.OnXCommitted(this, &FComponentTransformDetails::OnSetTransformAxis, ETransformField::Location, EAxisList::X, true)
.OnYCommitted(this, &FComponentTransformDetails::OnSetTransformAxis, ETransformField::Location, EAxisList::Y, true)
.OnZCommitted(this, &FComponentTransformDetails::OnSetTransformAxis, ETransformField::Location, EAxisList::Z, true)
.Font(FontInfo)
.TypeInterface(TypeInterface)
.AllowSpin(SelectedObjects.Num() == 1)
.SpinDelta(LocationSpinDelta)
.OnBeginSliderMovement(this, &FComponentTransformDetails::OnBeginLocationSlider)
.OnEndSliderMovement(this, &FComponentTransformDetails::OnEndLocationSlider)
.PreventThrottling(true)
];
}
// Rotation
if(!bHideRotationField)
{
TSharedPtr<INumericTypeInterface<FRotator::FReal>> TypeInterface;
if( FUnitConversion::Settings().ShouldDisplayUnits() )
{
TypeInterface = MakeShareable( new TNumericUnitTypeInterface<FRotator::FReal>(EUnit::Degrees) );
if (bIsAxisDisplayLeftUpForward)
{
TypeInterface->SetMaxFractionalDigits(3);
TypeInterface->SetIndicateNearlyInteger(false);
}
}
ParentCategory.OnPasteFromText()->AddSP(this, &FComponentTransformDetails::OnPasteFromText, ETransformField::Rotation);
FindOrCreatePropertyHandle(USceneComponent::GetAbsoluteRotationPropertyName(), ChildrenBuilder);
TSharedPtr<IPropertyHandle> PropertyHandle = FindOrCreatePropertyHandle(USceneComponent::GetRelativeRotationPropertyName(), ChildrenBuilder);
ChildrenBuilder.AddCustomRow( LOCTEXT("RotationFilter", "Rotation") )
.RowTag("Rotation")
.CopyAction( CreateCopyAction(ETransformField::Rotation) )
.PasteAction( CreatePasteAction(ETransformField::Rotation) )
.OverrideResetToDefault(FResetToDefaultOverride::Create(TAttribute<bool>(this, &FComponentTransformDetails::GetRotationResetVisibility), FSimpleDelegate::CreateSP(this, &FComponentTransformDetails::OnRotationResetClicked)))
.PropertyHandleList({ PropertyHandle })
.IsEnabled(TAttribute<bool>(this, &FComponentTransformDetails::GetIsEnabled))
.NameContent()
.VAlign(VAlign_Center)
[
BuildTransformFieldLabel(ETransformField::Rotation)
]
.ValueContent()
.MinDesiredWidth(125.0f * 3.0f)
.MaxDesiredWidth(125.0f * 3.0f)
.VAlign(VAlign_Center)
[
SNew( SNumericRotatorInputBox<FRotator::FReal> )
.AllowSpin( SelectedObjects.Num() == 1 )
.SpinDelta(RotationSpinDelta)
.MinSliderValue(RotationMin)
.MaxSliderValue(RotationMax)
.Roll( this, &FComponentTransformDetails::GetRotationX )
.Pitch( this, &FComponentTransformDetails::GetRotationY )
.Yaw( this, &FComponentTransformDetails::GetRotationZ )
.RollDisplayName(AxisDisplayInfo::GetAxisDisplayName(EAxisList::Forward))
.PitchDisplayName(AxisDisplayInfo::GetAxisDisplayName(EAxisList::Left))
.YawDisplayName(AxisDisplayInfo::GetAxisDisplayName(EAxisList::Up))
.bColorAxisLabels( true )
.Swizzle(AxisDisplayInfo::GetTransformAxisSwizzle())
.IsEnabled( this, &FComponentTransformDetails::GetIsRotationEnabled )
.OnPitchBeginSliderMovement( this, &FComponentTransformDetails::OnBeginRotationSlider )
.OnYawBeginSliderMovement( this, &FComponentTransformDetails::OnBeginRotationSlider )
.OnRollBeginSliderMovement( this, &FComponentTransformDetails::OnBeginRotationSlider )
.OnPitchEndSliderMovement( this, &FComponentTransformDetails::OnEndRotationSlider )
.OnYawEndSliderMovement( this, &FComponentTransformDetails::OnEndRotationSlider )
.OnRollEndSliderMovement( this, &FComponentTransformDetails::OnEndRotationSlider )
.OnRollChanged( this, &FComponentTransformDetails::OnSetTransformAxis, ETextCommit::Default, ETransformField::Rotation, EAxisList::X, false )
.OnPitchChanged( this, &FComponentTransformDetails::OnSetTransformAxis, ETextCommit::Default, ETransformField::Rotation, EAxisList::Y, false )
.OnYawChanged( this, &FComponentTransformDetails::OnSetTransformAxis, ETextCommit::Default, ETransformField::Rotation, EAxisList::Z, false )
.OnRollCommitted( this, &FComponentTransformDetails::OnSetTransformAxis, ETransformField::Rotation, EAxisList::X, true )
.OnPitchCommitted( this, &FComponentTransformDetails::OnSetTransformAxis, ETransformField::Rotation, EAxisList::Y, true )
.OnYawCommitted( this, &FComponentTransformDetails::OnSetTransformAxis, ETransformField::Rotation, EAxisList::Z, true )
.TypeInterface( TypeInterface )
.Font( FontInfo )
.PreventThrottling(true)
];
}
// Scale
if(!bHideScaleField)
{
ParentCategory.OnPasteFromText()->AddSP(this, &FComponentTransformDetails::OnPasteFromText, ETransformField::Scale);
FindOrCreatePropertyHandle(USceneComponent::GetAbsoluteScalePropertyName(), ChildrenBuilder);
TSharedPtr<IPropertyHandle> PropertyHandle = FindOrCreatePropertyHandle(USceneComponent::GetRelativeScale3DPropertyName(), ChildrenBuilder);
ChildrenBuilder.AddCustomRow( LOCTEXT("ScaleFilter", "Scale") )
.RowTag("Scale")
.CopyAction( CreateCopyAction(ETransformField::Scale) )
.PasteAction( CreatePasteAction(ETransformField::Scale) )
.OverrideResetToDefault(FResetToDefaultOverride::Create(TAttribute<bool>(this, &FComponentTransformDetails::GetScaleResetVisibility), FSimpleDelegate::CreateSP(this, &FComponentTransformDetails::OnScaleResetClicked)))
.PropertyHandleList({ PropertyHandle })
.IsEnabled(TAttribute<bool>(this, &FComponentTransformDetails::GetIsEnabled))
.NameContent()
.VAlign(VAlign_Center)
[
BuildTransformFieldLabel(ETransformField::Scale)
]
.ValueContent()
.MinDesiredWidth(125.0f * 3.0f)
.MaxDesiredWidth(125.0f * 3.0f)
.VAlign(VAlign_Center)
[
SNew( SNumericVectorInputBox<FVector::FReal> )
.X( this, &FComponentTransformDetails::GetScaleX )
.Y( this, &FComponentTransformDetails::GetScaleY )
.Z( this, &FComponentTransformDetails::GetScaleZ )
.XDisplayName(AxisDisplayInfo::GetAxisDisplayName(EAxisList::Forward))
.YDisplayName(AxisDisplayInfo::GetAxisDisplayName(EAxisList::Left))
.ZDisplayName(AxisDisplayInfo::GetAxisDisplayName(EAxisList::Up))
.bColorAxisLabels( true )
.Swizzle(AxisDisplayInfo::GetTransformAxisSwizzle())
.IsEnabled( this, &FComponentTransformDetails::GetIsScaleEnabled )
.OnXChanged( this, &FComponentTransformDetails::OnSetTransformAxis, ETextCommit::Default, ETransformField::Scale, EAxisList::X, false )
.OnYChanged( this, &FComponentTransformDetails::OnSetTransformAxis, ETextCommit::Default, ETransformField::Scale, EAxisList::Y, false )
.OnZChanged( this, &FComponentTransformDetails::OnSetTransformAxis, ETextCommit::Default, ETransformField::Scale, EAxisList::Z, false )
.OnXCommitted( this, &FComponentTransformDetails::OnSetTransformAxis, ETransformField::Scale, EAxisList::X, true )
.OnYCommitted( this, &FComponentTransformDetails::OnSetTransformAxis, ETransformField::Scale, EAxisList::Y, true )
.OnZCommitted( this, &FComponentTransformDetails::OnSetTransformAxis, ETransformField::Scale, EAxisList::Z, true )
.ContextMenuExtenderX( this, &FComponentTransformDetails::ExtendXScaleContextMenu )
.ContextMenuExtenderY( this, &FComponentTransformDetails::ExtendYScaleContextMenu )
.ContextMenuExtenderZ( this, &FComponentTransformDetails::ExtendZScaleContextMenu )
.Font( FontInfo )
.AllowSpin( SelectedObjects.Num() == 1 )
.SpinDelta( ScaleSpinDelta )
.OnBeginSliderMovement( this, &FComponentTransformDetails::OnBeginScaleSlider )
.OnEndSliderMovement(this, &FComponentTransformDetails::OnEndScaleSlider)
.PreventThrottling(true)
];
}
}
END_SLATE_FUNCTION_BUILD_OPTIMIZATION
void FComponentTransformDetails::Tick( float DeltaTime )
{
CacheDetails();
if (!FixedDisplayUnits.IsSet())
{
CacheCommonLocationUnits();
}
}
void FComponentTransformDetails::CacheCommonLocationUnits()
{
const TOptional<FVector::FReal> LocationX = GetLocationX();
const TOptional<FVector::FReal> LocationY = GetLocationY();
const TOptional<FVector::FReal> LocationZ = GetLocationZ();
FVector::FReal LargestValue = 0.0;
if (LocationX.IsSet() && LocationX.GetValue() > LargestValue)
{
LargestValue = LocationX.GetValue();
}
if (LocationY.IsSet() && LocationY.GetValue() > LargestValue)
{
LargestValue = LocationY.GetValue();
}
if (LocationZ.IsSet() && LocationZ.GetValue() > LargestValue)
{
LargestValue = LocationZ.GetValue();
}
SetupFixedDisplay(LargestValue);
}
TSharedPtr<IPropertyHandle> FComponentTransformDetails::FindOrCreatePropertyHandle(FName PropertyName, IDetailChildrenBuilder& ChildrenBuilder)
{
if (TSharedPtr<IPropertyHandle>* HandlePtr = PropertyHandles.Find(PropertyName))
{
return *HandlePtr;
}
// Try finding the property handle in the details panel's property map first.
IDetailLayoutBuilder& LayoutBuilder = ChildrenBuilder.GetParentCategory().GetParentLayout();
TSharedPtr<IPropertyHandle> PropertyHandle = LayoutBuilder.GetProperty(PropertyName, USceneComponent::StaticClass());
if (!PropertyHandle || !PropertyHandle->IsValidHandle())
{
// If it wasn't found, add a collapsed row which contains the property node.
TArray<UObject*> SceneComponents;
Algo::Transform(SelectedObjects, SceneComponents, [](TWeakObjectPtr<UObject> Obj) { return GetSceneComponentFromDetailsObject(Obj.Get()); });
PropertyHandle = LayoutBuilder.AddObjectPropertyData(SceneComponents, PropertyName);
CachedHandlesObjects.Append(SceneComponents);
}
if (PropertyHandle && PropertyHandle->IsValidHandle())
{
PropertyHandles.Add(PropertyName, PropertyHandle);
}
return PropertyHandle;
}
void FComponentTransformDetails::UpdatePropertyHandlesObjects(const TArray<UObject*> NewSceneComponents)
{
// Cached the old handles objects.
CachedHandlesObjects.Reset(NewSceneComponents.Num());
Algo::Transform(NewSceneComponents, CachedHandlesObjects, [](UObject* Obj) { return TWeakObjectPtr<UObject>(Obj); });
for (TMap<FName, TSharedPtr<IPropertyHandle>>::TIterator It(PropertyHandles); It; ++It)
{
TSharedPtr<IPropertyHandle> PropertyHandle = It.Value();
if (PropertyHandle && PropertyHandle->IsValidHandle())
{
PropertyHandle->ReplaceOuterObjects(NewSceneComponents);
}
}
}
bool FComponentTransformDetails::GetIsEnabled() const
{
return bIsEnabledCache;
}
bool FComponentTransformDetails::GetIsLocationEnabled() const
{
return GetIsTransformComponentEnabled(USceneComponent::GetRelativeLocationPropertyName());
}
bool FComponentTransformDetails::GetIsRotationEnabled() const
{
return GetIsTransformComponentEnabled(USceneComponent::GetRelativeRotationPropertyName());
}
bool FComponentTransformDetails::GetIsScaleEnabled() const
{
return GetIsTransformComponentEnabled(USceneComponent::GetRelativeScale3DPropertyName());
}
bool FComponentTransformDetails::GetIsTransformComponentEnabled(FName ComponentName) const
{
if (GetIsEnabled())
{
if (const TSharedPtr<IPropertyHandle>* PropertyHandle = PropertyHandles.Find(ComponentName))
{
return (*PropertyHandle)->IsEditable();
}
}
return false;
}
const FSlateBrush* FComponentTransformDetails::GetPreserveScaleRatioImage() const
{
return bPreserveScaleRatio ? FAppStyle::GetBrush( TEXT("Icons.Lock") ) : FAppStyle::GetBrush( TEXT("Icons.Unlock") ) ;
}
ECheckBoxState FComponentTransformDetails::IsPreserveScaleRatioChecked() const
{
return bPreserveScaleRatio ? ECheckBoxState::Checked : ECheckBoxState::Unchecked;
}
void FComponentTransformDetails::OnPreserveScaleRatioToggled( ECheckBoxState NewState )
{
bPreserveScaleRatio = (NewState == ECheckBoxState::Checked) ? true : false;
GConfig->SetBool(TEXT("SelectionDetails"), TEXT("PreserveScaleRatio"), bPreserveScaleRatio, GEditorPerProjectIni);
}
FText FComponentTransformDetails::GetLocationText() const
{
return bAbsoluteLocation ? LOCTEXT( "AbsoluteLocation", "Absolute Location" ) : LOCTEXT( "Location", "Location" );
}
FText FComponentTransformDetails::GetRotationText() const
{
return bAbsoluteRotation ? LOCTEXT( "AbsoluteRotation", "Absolute Rotation" ) : LOCTEXT( "Rotation", "Rotation" );
}
FText FComponentTransformDetails::GetScaleText() const
{
return bAbsoluteScale ? LOCTEXT( "AbsoluteScale", "Absolute Scale" ) : LOCTEXT( "Scale", "Scale" );
}
void FComponentTransformDetails::OnSetAbsoluteTransform(ETransformField::Type TransformField, bool bAbsoluteEnabled)
{
FBoolProperty* AbsoluteProperty = nullptr;
FText TransactionText;
switch (TransformField)
{
case ETransformField::Location:
AbsoluteProperty = FindFProperty<FBoolProperty>(USceneComponent::StaticClass(), USceneComponent::GetAbsoluteLocationPropertyName());
TransactionText = LOCTEXT("ToggleAbsoluteLocation", "Toggle Absolute Location");
break;
case ETransformField::Rotation:
AbsoluteProperty = FindFProperty<FBoolProperty>(USceneComponent::StaticClass(), USceneComponent::GetAbsoluteRotationPropertyName());
TransactionText = LOCTEXT("ToggleAbsoluteRotation", "Toggle Absolute Rotation");
break;
case ETransformField::Scale:
AbsoluteProperty = FindFProperty<FBoolProperty>(USceneComponent::StaticClass(), USceneComponent::GetAbsoluteScalePropertyName());
TransactionText = LOCTEXT("ToggleAbsoluteScale", "Toggle Absolute Scale");
break;
default:
return;
}
bool bBeganTransaction = false;
TArray<UObject*> ModifiedObjects;
for (int32 ObjectIndex = 0; ObjectIndex < SelectedObjects.Num(); ++ObjectIndex)
{
TWeakObjectPtr<UObject> ObjectPtr = SelectedObjects[ObjectIndex];
if (ObjectPtr.IsValid())
{
UObject* Object = ObjectPtr.Get();
USceneComponent* SceneComponent = GetSceneComponentFromDetailsObject(Object);
if (SceneComponent)
{
bool bOldValue = TransformField == ETransformField::Location ? SceneComponent->IsUsingAbsoluteLocation() : (TransformField == ETransformField::Rotation ? SceneComponent->IsUsingAbsoluteRotation() : SceneComponent->IsUsingAbsoluteScale());
if (bOldValue == bAbsoluteEnabled)
{
// Already the desired value
continue;
}
if (!bBeganTransaction)
{
// NOTE: One transaction per change, not per actor
GEditor->BeginTransaction(TransactionText);
bBeganTransaction = true;
}
FScopedSwitchWorldForObject WorldSwitcher(Object);
if (SceneComponent->HasAnyFlags(RF_DefaultSubObject))
{
// Default subobjects must be included in any undo/redo operations
SceneComponent->SetFlags(RF_Transactional);
}
SceneComponent->PreEditChange(AbsoluteProperty);
if (NotifyHook)
{
NotifyHook->NotifyPreChange(AbsoluteProperty);
}
TOptional<FTransform> TransformToPreserve;
if (SceneComponent->GetAttachParent())
{
if (bAbsoluteEnabled)
{
TransformToPreserve = SceneComponent->GetComponentTransform();
}
else
{
FTransform ParentToWorld = SceneComponent->GetAttachParent()->GetSocketTransform(SceneComponent->GetAttachSocketName());
TransformToPreserve = SceneComponent->GetComponentTransform().GetRelativeTransform(ParentToWorld);
}
}
switch (TransformField)
{
case ETransformField::Location:
SceneComponent->SetUsingAbsoluteLocation(bAbsoluteEnabled);
if (TransformToPreserve.IsSet())
{
SceneComponent->SetRelativeLocation_Direct(TransformToPreserve->GetTranslation());
}
break;
case ETransformField::Rotation:
SceneComponent->SetUsingAbsoluteRotation(bAbsoluteEnabled);
if (TransformToPreserve.IsSet())
{
SceneComponent->SetRelativeRotation_Direct(FRotator(TransformToPreserve->GetRotation()));
}
break;
case ETransformField::Scale:
SceneComponent->SetUsingAbsoluteScale(bAbsoluteEnabled);
if (TransformToPreserve.IsSet())
{
SceneComponent->SetRelativeScale3D_Direct(TransformToPreserve->GetScale3D());
}
break;
}
ModifiedObjects.Add(Object);
}
}
}
if (bBeganTransaction)
{
FPropertyChangedEvent PropertyChangedEvent(AbsoluteProperty, EPropertyChangeType::ValueSet, MakeArrayView(ModifiedObjects));
for (UObject* Object : ModifiedObjects)
{
USceneComponent* SceneComponent = GetSceneComponentFromDetailsObject(Object);
if (SceneComponent)
{
SceneComponent->PostEditChangeProperty(PropertyChangedEvent);
// If it's a template, propagate the change out to any current instances of the object
if (SceneComponent->IsTemplate())
{
bool NewValue = bAbsoluteEnabled;
bool OldValue = !NewValue;
TSet<USceneComponent*> UpdatedInstances;
FComponentEditorUtils::PropagateDefaultValueChange(SceneComponent, AbsoluteProperty, OldValue, NewValue, UpdatedInstances);
}
}
}
if (NotifyHook)
{
NotifyHook->NotifyPostChange(PropertyChangedEvent, AbsoluteProperty);
}
GEditor->EndTransaction();
GUnrealEd->RedrawLevelEditingViewports();
}
}
bool FComponentTransformDetails::IsAbsoluteTransformChecked(ETransformField::Type TransformField, bool bAbsoluteEnabled) const
{
switch (TransformField)
{
case ETransformField::Location:
return bAbsoluteLocation == bAbsoluteEnabled;
case ETransformField::Rotation:
return bAbsoluteRotation == bAbsoluteEnabled;
case ETransformField::Scale:
return bAbsoluteScale == bAbsoluteEnabled;
default:
return false;
}
}
bool FComponentTransformDetails::CanChangeAbsoluteFlag(ETransformField::Type TransformField) const
{
FName PropertyName;
switch (TransformField)
{
case ETransformField::Location:
PropertyName = USceneComponent::GetAbsoluteLocationPropertyName();
break;
case ETransformField::Rotation:
PropertyName = USceneComponent::GetAbsoluteRotationPropertyName();
break;
case ETransformField::Scale:
PropertyName = USceneComponent::GetAbsoluteScalePropertyName();
break;
default:
break;
}
if (!PropertyName.IsNone())
{
if (const TSharedPtr<IPropertyHandle>* HandlePtr = PropertyHandles.Find(PropertyName))
{
return (*HandlePtr)->IsEditable();
}
}
return false;
}
struct FGetRootComponentArchetype
{
static USceneComponent* Get(UObject* Object)
{
auto RootComponent = Object ? GetSceneComponentFromDetailsObject(Object) : nullptr;
return RootComponent ? Cast<USceneComponent>(PropertyEditorPolicy::GetArchetype(RootComponent)) : nullptr;
}
};
TOptional<FVector::FReal> FComponentTransformDetails::GetLocationX() const
{
return CachedLocation.X;
}
TOptional<FVector::FReal> FComponentTransformDetails::GetLocationY() const
{
if (bIsAxisDisplayLeftUpForward && CachedLocation.Y.IsSet())
{
return TOptional<FVector::FReal>(-CachedLocation.Y.GetValue());
}
return CachedLocation.Y;
}
TOptional<FVector::FReal> FComponentTransformDetails::GetLocationZ() const
{
return CachedLocation.Z;
}
TOptional<FRotator::FReal> FComponentTransformDetails::GetRotationX() const
{
return CachedRotation.X;
}
TOptional<FRotator::FReal> FComponentTransformDetails::GetRotationY() const
{
return CachedRotation.Y;
}
TOptional<FRotator::FReal> FComponentTransformDetails::GetRotationZ() const
{
return CachedRotation.Z;
}
TOptional<FVector::FReal> FComponentTransformDetails::GetScaleX() const
{
return CachedScale.X;
}
TOptional<FVector::FReal> FComponentTransformDetails::GetScaleY() const
{
return CachedScale.Y;
}
TOptional<FVector::FReal> FComponentTransformDetails::GetScaleZ() const
{
return CachedScale.Z;
}
bool FComponentTransformDetails::GetLocationResetVisibility() const
{
const USceneComponent* Archetype = FGetRootComponentArchetype::Get(SelectedObjects[0].Get());
const FVector Data = Archetype ? Archetype->GetRelativeLocation() : FVector::ZeroVector;
// unset means multiple differing values, so show "Reset to Default" in that case
return CachedLocation.IsSet() && CachedLocation.X.GetValue() == Data.X && CachedLocation.Y.GetValue() == Data.Y && CachedLocation.Z.GetValue() == Data.Z ? false : true;
}
void FComponentTransformDetails::OnLocationResetClicked()
{
if (GetIsLocationEnabled())
{
const FText TransactionName = LOCTEXT("ResetLocation", "Reset Location");
FScopedTransaction Transaction(TransactionName);
const USceneComponent* Archetype = FGetRootComponentArchetype::Get(SelectedObjects[0].Get());
const FVector Data = Archetype ? Archetype->GetRelativeLocation() : FVector::ZeroVector;
OnSetTransform(ETransformField::Location, EAxisList::All, Data, false, true);
}
}
bool FComponentTransformDetails::GetRotationResetVisibility() const
{
const USceneComponent* Archetype = FGetRootComponentArchetype::Get(SelectedObjects[0].Get());
// unset means multiple differing values, so show "Reset to Default" in that case
if (!CachedRotation.IsSet())
{
return true;
}
if (!bIsAxisDisplayLeftUpForward)
{
const FVector Data = Archetype ? Archetype->GetRelativeRotation().Euler() : FVector::ZeroVector;
// unset means multiple differing values, so show "Reset to Default" in that case
return CachedRotation.X.GetValue() != Data.X || CachedRotation.Y.GetValue() != Data.Y || CachedRotation.Z.GetValue() != Data.Z;
}
else
{
const FVector Data = Archetype ? ConvertFromUnrealSpace_EulerDeg(Archetype->GetRelativeRotation()) : FVector::ZeroVector;
return ComponentTransformDetails::Private::AreRotationsEqual(Data, CachedRotation.ToVector());
}
}
void FComponentTransformDetails::OnRotationResetClicked()
{
if (GetIsRotationEnabled())
{
const FText TransactionName = LOCTEXT("ResetRotation", "Reset Rotation");
FScopedTransaction Transaction(TransactionName);
const USceneComponent* Archetype = FGetRootComponentArchetype::Get(SelectedObjects[0].Get());
const FVector Data = Archetype ? ConvertFromUnrealSpace_EulerDeg(Archetype->GetRelativeRotation()) : FVector::ZeroVector;
OnSetTransform(ETransformField::Rotation, EAxisList::All, Data, false, true);
}
}
bool FComponentTransformDetails::GetScaleResetVisibility() const
{
const USceneComponent* Archetype = FGetRootComponentArchetype::Get(SelectedObjects[0].Get());
const FVector Data = Archetype ? Archetype->GetRelativeScale3D() : FVector(1.0f);
// unset means multiple differing values, so show "Reset to Default" in that case
return CachedScale.IsSet() && CachedScale.X.GetValue() == Data.X && CachedScale.Y.GetValue() == Data.Y && CachedScale.Z.GetValue() == Data.Z ? false : true;
}
void FComponentTransformDetails::OnScaleResetClicked()
{
if (GetIsScaleEnabled())
{
const FText TransactionName = LOCTEXT("ResetScale", "Reset Scale");
FScopedTransaction Transaction(TransactionName);
const USceneComponent* Archetype = FGetRootComponentArchetype::Get(SelectedObjects[0].Get());
const FVector Data = Archetype ? Archetype->GetRelativeScale3D() : FVector(1.0f);
OnSetTransform(ETransformField::Scale, EAxisList::All, Data, false, true);
}
}
void FComponentTransformDetails::ExtendXScaleContextMenu( FMenuBuilder& MenuBuilder )
{
MenuBuilder.BeginSection( "ScaleOperations", LOCTEXT( "ScaleOperations", "Scale Operations" ) );
MenuBuilder.AddMenuEntry(
FText::Format(LOCTEXT("MirrorValue", "Mirror {0} Axis"), AxisDisplayInfo::GetAxisDisplayName(EAxisList::Forward)),
FText::Format(LOCTEXT("MirrorValue_Tooltip", "Mirror scale value on the {0} axis"), AxisDisplayInfo::GetAxisDisplayName(EAxisList::Forward)),
FSlateIcon(),
FUIAction( FExecuteAction::CreateSP( this, &FComponentTransformDetails::OnXScaleMirrored ), FCanExecuteAction::CreateSP( this, &FComponentTransformDetails::GetIsScaleEnabled ) )
);
MenuBuilder.EndSection();
}
void FComponentTransformDetails::ExtendYScaleContextMenu( FMenuBuilder& MenuBuilder )
{
MenuBuilder.BeginSection( "ScaleOperations", LOCTEXT( "ScaleOperations", "Scale Operations" ) );
MenuBuilder.AddMenuEntry(
FText::Format(LOCTEXT("MirrorValue", "Mirror {0} Axis"), AxisDisplayInfo::GetAxisDisplayName(EAxisList::Left)),
FText::Format(LOCTEXT("MirrorValue_Tooltip", "Mirror scale value on the {0} axis"), AxisDisplayInfo::GetAxisDisplayName(EAxisList::Left)),
FSlateIcon(),
FUIAction( FExecuteAction::CreateSP( this, &FComponentTransformDetails::OnYScaleMirrored ), FCanExecuteAction::CreateSP( this, &FComponentTransformDetails::GetIsScaleEnabled ) )
);
MenuBuilder.EndSection();
}
void FComponentTransformDetails::ExtendZScaleContextMenu( FMenuBuilder& MenuBuilder )
{
MenuBuilder.BeginSection( "ScaleOperations", LOCTEXT( "ScaleOperations", "Scale Operations" ) );
MenuBuilder.AddMenuEntry(
FText::Format(LOCTEXT("MirrorValue", "Mirror {0} Axis"), AxisDisplayInfo::GetAxisDisplayName(EAxisList::Up)),
FText::Format(LOCTEXT("MirrorValue_Tooltip", "Mirror scale value on the {0} axis"), AxisDisplayInfo::GetAxisDisplayName(EAxisList::Up)),
FSlateIcon(),
FUIAction( FExecuteAction::CreateSP( this, &FComponentTransformDetails::OnZScaleMirrored ), FCanExecuteAction::CreateSP( this, &FComponentTransformDetails::GetIsScaleEnabled ) )
);
MenuBuilder.EndSection();
}
void FComponentTransformDetails::OnXScaleMirrored()
{
FSlateApplication::Get().ClearKeyboardFocus(EFocusCause::Mouse);
FScopedTransaction Transaction(FText::Format(LOCTEXT("MirrorScaleTransaction", "Scale - Mirror {0} Axis"), AxisDisplayInfo::GetAxisDisplayName(EAxisList::Forward)));
OnSetTransform(ETransformField::Scale, EAxisList::X, FVector(1.0f), true, true);
}
void FComponentTransformDetails::OnYScaleMirrored()
{
FSlateApplication::Get().ClearKeyboardFocus(EFocusCause::Mouse);
FScopedTransaction Transaction(FText::Format(LOCTEXT("MirrorScaleTransaction", "Scale - Mirror {0} Axis"), AxisDisplayInfo::GetAxisDisplayName(EAxisList::Left)));
OnSetTransform(ETransformField::Scale, EAxisList::Y, FVector(1.0f), true, true);
}
void FComponentTransformDetails::OnZScaleMirrored()
{
FSlateApplication::Get().ClearKeyboardFocus(EFocusCause::Mouse);
FScopedTransaction Transaction(FText::Format(LOCTEXT("MirrorScaleTransaction", "Scale - Mirror {0} Axis"), AxisDisplayInfo::GetAxisDisplayName(EAxisList::Up)));
OnSetTransform(ETransformField::Scale, EAxisList::Z, FVector(1.0f), true, true);
}
void FComponentTransformDetails::CacheDetails()
{
FVector CurLoc = FVector::ZeroVector;
FRotator CurRot = FRotator::ZeroRotator;
FVector CurScale = FVector::ZeroVector;
bIsEnabledCache = true;
for( int32 ObjectIndex = 0; ObjectIndex < SelectedObjects.Num(); ++ObjectIndex )
{
TWeakObjectPtr<UObject> ObjectPtr = SelectedObjects[ObjectIndex];
if( ObjectPtr.IsValid() )
{
UObject* Object = ObjectPtr.Get();
USceneComponent* SceneComponent = GetSceneComponentFromDetailsObject( Object );
FVector Loc;
FRotator Rot;
FVector Scale;
if( SceneComponent )
{
if (AActor* Owner = SceneComponent->GetOwner(); Owner && Owner->GetRootComponent() == SceneComponent)
{
bIsEnabledCache &= !Owner->IsLockLocation();
}
Loc = SceneComponent->GetRelativeLocation();
FRotator* FoundRotator = ObjectToRelativeRotationMap.Find(SceneComponent);
Rot = (bEditingRotationInUI && !Object->IsTemplate() && FoundRotator) ? *FoundRotator : SceneComponent->GetRelativeRotation();
if (bIsAxisDisplayLeftUpForward)
{
FVector Euler = ConvertFromUnrealSpace_EulerDeg(Rot);
Rot = FRotator(Euler.X, Euler.Y, Euler.Z);
}
Scale = SceneComponent->GetRelativeScale3D();
if( ObjectIndex == 0 )
{
// Cache the current values from the first actor to see if any values differ among other actors
CurLoc = Loc;
CurRot = Rot;
CurScale = Scale;
CachedLocation.Set( Loc );
CachedRotation.Set( Rot );
CachedScale.Set( Scale );
bAbsoluteLocation = SceneComponent->IsUsingAbsoluteLocation();
bAbsoluteScale = SceneComponent->IsUsingAbsoluteScale();
bAbsoluteRotation = SceneComponent->IsUsingAbsoluteRotation();
}
else if( CurLoc != Loc || CurRot != Rot || CurScale != Scale )
{
// Check which values differ and unset the different values
CachedLocation.X = Loc.X == CurLoc.X && CachedLocation.X.IsSet() ? Loc.X : TOptional<FVector::FReal>();
CachedLocation.Y = Loc.Y == CurLoc.Y && CachedLocation.Y.IsSet() ? Loc.Y : TOptional<FVector::FReal>();
CachedLocation.Z = Loc.Z == CurLoc.Z && CachedLocation.Z.IsSet() ? Loc.Z : TOptional<FVector::FReal>();
CachedRotation.X = Rot.Roll == CurRot.Roll && CachedRotation.X.IsSet() ? Rot.Roll : TOptional<FRotator::FReal>();
CachedRotation.Y = Rot.Pitch == CurRot.Pitch && CachedRotation.Y.IsSet() ? Rot.Pitch : TOptional<FRotator::FReal>();
CachedRotation.Z = Rot.Yaw == CurRot.Yaw && CachedRotation.Z.IsSet() ? Rot.Yaw : TOptional<FRotator::FReal>();
CachedScale.X = Scale.X == CurScale.X && CachedScale.X.IsSet() ? Scale.X : TOptional<FVector::FReal>();
CachedScale.Y = Scale.Y == CurScale.Y && CachedScale.Y.IsSet() ? Scale.Y : TOptional<FVector::FReal>();
CachedScale.Z = Scale.Z == CurScale.Z && CachedScale.Z.IsSet() ? Scale.Z : TOptional<FVector::FReal>();
// If all values are unset all values are different and we can stop looking
const bool bAllValuesDiffer = !CachedLocation.IsSet() && !CachedRotation.IsSet() && !CachedScale.IsSet();
if( bAllValuesDiffer )
{
break;
}
}
}
}
}
}
FVector FComponentTransformDetails::GetAxisFilteredVector(EAxisList::Type Axis, const FVector& NewValue, const FVector& OldValue)
{
return FVector((Axis & EAxisList::X) ? NewValue.X : OldValue.X,
(Axis & EAxisList::Y) ? NewValue.Y : OldValue.Y,
(Axis & EAxisList::Z) ? NewValue.Z : OldValue.Z);
}
void FComponentTransformDetails::OnSetTransform(ETransformField::Type TransformField, EAxisList::Type Axis, FVector NewValue, bool bMirror, bool bCommitted)
{
if (!bCommitted && SelectedObjects.Num() > 1)
{
// Ignore interactive changes when we have more than one selected object
return;
}
FText TransactionText;
FProperty* ValueProperty = nullptr;
FProperty* AxisProperty = nullptr;
switch (TransformField)
{
case ETransformField::Location:
TransactionText = LOCTEXT("OnSetLocation", "Set Location");
ValueProperty = FindFProperty<FProperty>(USceneComponent::StaticClass(), USceneComponent::GetRelativeLocationPropertyName());
// Only set axis property for single axis set
if (Axis == EAxisList::X)
{
AxisProperty = FindFProperty<FDoubleProperty>(TBaseStructure<FVector>::Get(), GET_MEMBER_NAME_CHECKED(FVector, X));
check(AxisProperty != nullptr);
}
else if (Axis == EAxisList::Y)
{
AxisProperty = FindFProperty<FDoubleProperty>(TBaseStructure<FVector>::Get(), GET_MEMBER_NAME_CHECKED(FVector, Y));
check(AxisProperty != nullptr);
}
else if (Axis == EAxisList::Z)
{
AxisProperty = FindFProperty<FDoubleProperty>(TBaseStructure<FVector>::Get(), GET_MEMBER_NAME_CHECKED(FVector, Z));
check(AxisProperty != nullptr);
}
break;
case ETransformField::Rotation:
TransactionText = LOCTEXT("OnSetRotation", "Set Rotation");
ValueProperty = FindFProperty<FProperty>(USceneComponent::StaticClass(), USceneComponent::GetRelativeRotationPropertyName());
// Only set axis property for single axis set
if (Axis == EAxisList::X)
{
AxisProperty = FindFProperty<FDoubleProperty>(TBaseStructure<FRotator>::Get(), GET_MEMBER_NAME_CHECKED(FRotator, Roll));
check(AxisProperty != nullptr);
}
else if (Axis == EAxisList::Y)
{
AxisProperty = FindFProperty<FDoubleProperty>(TBaseStructure<FRotator>::Get(), GET_MEMBER_NAME_CHECKED(FRotator, Pitch));
check(AxisProperty != nullptr);
}
else if (Axis == EAxisList::Z)
{
AxisProperty = FindFProperty<FDoubleProperty>(TBaseStructure<FRotator>::Get(), GET_MEMBER_NAME_CHECKED(FRotator, Yaw));
check(AxisProperty != nullptr);
}
break;
case ETransformField::Scale:
TransactionText = LOCTEXT("OnSetScale", "Set Scale");
ValueProperty = FindFProperty<FProperty>(USceneComponent::StaticClass(), USceneComponent::GetRelativeScale3DPropertyName());
// If keep scale is set, don't set axis property
if (!bPreserveScaleRatio && Axis == EAxisList::X)
{
AxisProperty = FindFProperty<FDoubleProperty>(TBaseStructure<FVector>::Get(), GET_MEMBER_NAME_CHECKED(FVector, X));
check(AxisProperty != nullptr);
}
else if (!bPreserveScaleRatio && Axis == EAxisList::Y)
{
AxisProperty = FindFProperty<FDoubleProperty>(TBaseStructure<FVector>::Get(), GET_MEMBER_NAME_CHECKED(FVector, Y));
check(AxisProperty != nullptr);
}
else if (!bPreserveScaleRatio && Axis == EAxisList::Z)
{
AxisProperty = FindFProperty<FDoubleProperty>(TBaseStructure<FVector>::Get(), GET_MEMBER_NAME_CHECKED(FVector, Z));
check(AxisProperty != nullptr);
}
break;
default:
return;
}
bool bBeganTransaction = false;
TArray<UObject*> ModifiedObjects;
FPropertyChangedEvent PropertyChangedEvent(ValueProperty, !bCommitted ? EPropertyChangeType::Interactive : EPropertyChangeType::ValueSet, MakeArrayView(ModifiedObjects));
FEditPropertyChain PropertyChain;
if (AxisProperty)
{
PropertyChain.AddHead(AxisProperty);
}
PropertyChain.AddHead(ValueProperty);
FPropertyChangedChainEvent PropertyChangedChainEvent(PropertyChain, PropertyChangedEvent);
EAxisList::Type RemappedAxis = Axis;
FVector SwizzledNewValue = NewValue;
if (bIsAxisDisplayLeftUpForward)
{
if (TransformField == ETransformField::Location)
{
SwizzledNewValue.Y = -NewValue.Y;
}
if (TransformField == ETransformField::Rotation)
{
// Need to convert from Right-handed Y-Up to UE's Left-handed Z-Up however...
// NewValue is not the full set of Euler values to be applied, it will only contain
// the single value that was changed as specified by Axis
// Therefore it is not yet safe to convert it over.
// However we do need to swizzle the NewValue since the rotation widgets Axis values are set assuming
// normal Unreal rotations
RemappedAxis = [&]()
{
switch (Axis)
{
case EAxisList::X: return EAxisList::Z;
case EAxisList::Y: return EAxisList::X;
case EAxisList::Z: return EAxisList::Y;
case EAxisList::All: return EAxisList::All;
default: return EAxisList::X;
}
}();
SwizzledNewValue = FVector(NewValue.Y, NewValue.Z, NewValue.X);
// Next step is to run SwizzledNewValue through GetAxisFilteredValue() to compose it
// with the converted rotator to right-hand coordinate space and get the full set of euler angles.
// Finally, these euler angles will be converted back to Unreal Rotator space and applied.
// ObjectToRelativeRotationMap stores the rotations in Unreal Rotator space always - this may need to change though
// CachedRotation is stored in Right handed Y-up space as this is used to read back the values into the widget for display purposes
// See GetRotationY, GetRotationZ
}
}
for (int32 ObjectIndex = 0; ObjectIndex < SelectedObjects.Num(); ++ObjectIndex)
{
TWeakObjectPtr<UObject> ObjectPtr = SelectedObjects[ObjectIndex];
if (ObjectPtr.IsValid())
{
UObject* Object = ObjectPtr.Get();
USceneComponent* SceneComponent = GetSceneComponentFromDetailsObject(Object);
if (SceneComponent)
{
AActor* EditedActor = SceneComponent->GetOwner();
const bool bIsEditingTemplateObject = Object->IsTemplate();
FRotator OldComponentRotator;
FVector OldComponentValue;
FVector NewComponentValue;
switch (TransformField)
{
case ETransformField::Location:
OldComponentValue = SceneComponent->GetRelativeLocation();
break;
case ETransformField::Rotation:
// Pull from the actual component or from the cache
if (bEditingRotationInUI && !bIsEditingTemplateObject && ObjectToRelativeRotationMap.Find(SceneComponent))
{
OldComponentRotator = *ObjectToRelativeRotationMap.Find(SceneComponent);
}
else
{
OldComponentRotator = SceneComponent->GetRelativeRotation();
}
OldComponentValue = ConvertFromUnrealSpace_EulerDeg(OldComponentRotator);
break;
case ETransformField::Scale:
OldComponentValue = SceneComponent->GetRelativeScale3D();
break;
}
// Set the incoming value
if (bMirror)
{
NewComponentValue = GetAxisFilteredVector(RemappedAxis, -OldComponentValue, OldComponentValue);
}
else
{
NewComponentValue = GetAxisFilteredVector(RemappedAxis, SwizzledNewValue, OldComponentValue);
}
auto AreValuesEqual = [this, TransformField](const FVector& NewComponentValue_, const FVector& OldComponentValue_)
{
if (!bIsAxisDisplayLeftUpForward || TransformField != ETransformField::Rotation)
{
// Bit-wise identical check
return NewComponentValue_ == OldComponentValue_;
}
else
{
// LeftUpForward uses alternative XYZ intrinsic rotation but rotation is stored
// still as FRotator in Yaw-Pitch-Roll intrinsic convention. The conversion between
// these two conventions prevents bit-exact comparisons. If values set are close enough
// to what exists on the component, then skip the setting rotation
// This prevents accidental small errors accumulating due to automatic conversion from the cached
// euler rotation representations and the underlying data
return ComponentTransformDetails::Private::AreRotationsEqual(NewComponentValue_, OldComponentValue_);
}
};
// If we're committing during a slider transaction then we need to force it, in order that PostEditChangeChainProperty be called.
// Note: this will even happen if the slider hasn't changed the value.
if (!AreValuesEqual(NewComponentValue, OldComponentValue) || (bCommitted && bIsSliderTransaction))
{
if (!bBeganTransaction && bCommitted)
{
// NOTE: One transaction per change, not per actor
GEditor->BeginTransaction(TransactionText);
bBeganTransaction = true;
}
FScopedSwitchWorldForObject WorldSwitcher(Object);
if (bCommitted)
{
if (!bIsEditingTemplateObject)
{
// Broadcast the first time an actor is about to move
GEditor->BroadcastBeginObjectMovement(*SceneComponent);
if (EditedActor && EditedActor->GetRootComponent() == SceneComponent)
{
GEditor->BroadcastBeginObjectMovement(*EditedActor);
}
}
if (SceneComponent->HasAnyFlags(RF_DefaultSubObject))
{
// Default subobjects must be included in any undo/redo operations
SceneComponent->SetFlags(RF_Transactional);
}
}
// Have to downcast here because of function overloading and inheritance not playing nicely
((UObject*)SceneComponent)->PreEditChange(PropertyChain);
if (EditedActor && EditedActor->GetRootComponent() == SceneComponent)
{
((UObject*)EditedActor)->PreEditChange(PropertyChain);
}
if (NotifyHook)
{
NotifyHook->NotifyPreChange(ValueProperty);
}
switch (TransformField)
{
case ETransformField::Location:
{
if (!bIsEditingTemplateObject)
{
// Update local cache for restoring later
ObjectToRelativeRotationMap.FindOrAdd(SceneComponent) = SceneComponent->GetRelativeRotation();
}
SceneComponent->SetRelativeLocation(NewComponentValue);
// Also forcibly set it as the cache may have changed it slightly
SceneComponent->SetRelativeLocation_Direct(NewComponentValue);
// If it's a template, propagate the change out to any current instances of the object
if (bIsEditingTemplateObject)
{
TSet<USceneComponent*> UpdatedInstances;
FComponentEditorUtils::PropagateDefaultValueChange(SceneComponent, ValueProperty, OldComponentValue, NewComponentValue, UpdatedInstances);
}
break;
}
case ETransformField::Rotation:
{
FRotator NewRotation = ConvertToUnrealSpace_EulerDeg(NewComponentValue);
if (!bIsEditingTemplateObject)
{
// Update local cache for restoring later
ObjectToRelativeRotationMap.FindOrAdd(SceneComponent) = NewRotation;
}
SceneComponent->SetRelativeRotationExact(NewRotation);
// If it's a template, propagate the change out to any current instances of the object
if (bIsEditingTemplateObject)
{
TSet<USceneComponent*> UpdatedInstances;
FComponentEditorUtils::PropagateDefaultValueChange(SceneComponent, ValueProperty, ConvertToUnrealSpace_EulerDeg(OldComponentValue), NewRotation, UpdatedInstances);
}
break;
}
case ETransformField::Scale:
{
if (bPreserveScaleRatio)
{
// If we set a single axis, scale the others
FVector::FReal Ratio = 0.0f;
switch (Axis)
{
case EAxisList::X:
if (bIsSliderTransaction)
{
Ratio = SliderScaleRatio.X == 0.0f ? SliderScaleRatio.Y : (SliderScaleRatio.Y / SliderScaleRatio.X);
NewComponentValue.Y = NewComponentValue.X * Ratio;
Ratio = SliderScaleRatio.X == 0.0f ? SliderScaleRatio.Z : (SliderScaleRatio.Z / SliderScaleRatio.X);
NewComponentValue.Z = NewComponentValue.X * Ratio;
}
else
{
Ratio = OldComponentValue.X == 0.0f ? NewComponentValue.Z : NewComponentValue.X / OldComponentValue.X;
NewComponentValue.Y *= Ratio;
NewComponentValue.Z *= Ratio;
}
break;
case EAxisList::Y:
if (bIsSliderTransaction)
{
Ratio = SliderScaleRatio.Y == 0.0f ? SliderScaleRatio.X : (SliderScaleRatio.X / SliderScaleRatio.Y);
NewComponentValue.X = NewComponentValue.Y * Ratio;
Ratio = SliderScaleRatio.Y == 0.0f ? SliderScaleRatio.Z : (SliderScaleRatio.Z / SliderScaleRatio.Y);
NewComponentValue.Z = NewComponentValue.Y * Ratio;
}
else
{
Ratio = OldComponentValue.Y == 0.0f ? NewComponentValue.Z : NewComponentValue.Y / OldComponentValue.Y;
NewComponentValue.X *= Ratio;
NewComponentValue.Z *= Ratio;
}
break;
case EAxisList::Z:
if (bIsSliderTransaction)
{
Ratio = SliderScaleRatio.Z == 0.0f ? SliderScaleRatio.X : (SliderScaleRatio.X / SliderScaleRatio.Z);
NewComponentValue.X = NewComponentValue.Z * Ratio;
Ratio = SliderScaleRatio.Z == 0.0f ? SliderScaleRatio.Y : (SliderScaleRatio.Y / SliderScaleRatio.Z);
NewComponentValue.Y = NewComponentValue.Z * Ratio;
}
else
{
Ratio = OldComponentValue.Z == 0.0f ? NewComponentValue.Z : NewComponentValue.Z / OldComponentValue.Z;
NewComponentValue.X *= Ratio;
NewComponentValue.Y *= Ratio;
}
break;
default:
// Do nothing, this set multiple axis at once
break;
}
}
SceneComponent->SetRelativeScale3D(NewComponentValue);
// If it's a template, propagate the change out to any current instances of the object
if (bIsEditingTemplateObject)
{
TSet<USceneComponent*> UpdatedInstances;
FComponentEditorUtils::PropagateDefaultValueChange(SceneComponent, ValueProperty, OldComponentValue, NewComponentValue, UpdatedInstances);
}
break;
}
}
ModifiedObjects.Add(Object);
}
}
}
}
if (ModifiedObjects.Num())
{
for (UObject* Object : ModifiedObjects)
{
USceneComponent* SceneComponent = GetSceneComponentFromDetailsObject(Object);
USceneComponent* OldSceneComponent = SceneComponent;
if (SceneComponent)
{
AActor* EditedActor = SceneComponent->GetOwner();
FString SceneComponentPath = SceneComponent->GetPathName(EditedActor);
// This can invalidate OldSceneComponent
OldSceneComponent->PostEditChangeChainProperty(PropertyChangedChainEvent);
if (!bCommitted)
{
const FProperty* ConstValueProperty = ValueProperty;
SnapshotTransactionBuffer(OldSceneComponent, MakeArrayView(&ConstValueProperty, 1));
}
SceneComponent = FindObject<USceneComponent>(EditedActor, *SceneComponentPath);
if (EditedActor && EditedActor->GetRootComponent() == SceneComponent)
{
EditedActor->PostEditChangeChainProperty(PropertyChangedChainEvent);
SceneComponent = FindObject<USceneComponent>(EditedActor, *SceneComponentPath);
if (!bCommitted && OldSceneComponent != SceneComponent)
{
const FProperty* ConstValueProperty = ValueProperty;
SnapshotTransactionBuffer(SceneComponent, MakeArrayView(&ConstValueProperty, 1));
}
}
if (!Object->IsTemplate())
{
if (TransformField == ETransformField::Rotation || TransformField == ETransformField::Location)
{
FRotator* FoundRotator = ObjectToRelativeRotationMap.Find(OldSceneComponent);
if (FoundRotator)
{
FQuat OldQuat = FoundRotator->GetDenormalized().Quaternion();
FQuat NewQuat = SceneComponent->GetRelativeRotation().GetDenormalized().Quaternion();
if (OldQuat.Equals(NewQuat))
{
// Need to restore the manually set rotation as it was modified by quat conversion
SceneComponent->SetRelativeRotation_Direct(*FoundRotator);
}
}
}
if (bCommitted)
{
// Broadcast when the actor is done moving
GEditor->BroadcastEndObjectMovement(*SceneComponent);
if (EditedActor && EditedActor->GetRootComponent() == SceneComponent)
{
GEditor->BroadcastEndObjectMovement(*EditedActor);
}
}
}
}
}
if (NotifyHook)
{
NotifyHook->NotifyPostChange(PropertyChangedEvent, ValueProperty);
}
}
if (bCommitted && bBeganTransaction)
{
GEditor->EndTransaction();
CacheDetails();
}
GUnrealEd->UpdatePivotLocationForSelection();
GUnrealEd->SetPivotMovedIndependently(false);
// Redraw
GUnrealEd->RedrawLevelEditingViewports();
}
void FComponentTransformDetails::OnSetTransformAxis(FVector::FReal NewValue, ETextCommit::Type CommitInfo, ETransformField::Type TransformField, EAxisList::Type Axis, bool bCommitted)
{
FVector NewVector = GetAxisFilteredVector(Axis, FVector(NewValue), FVector::ZeroVector);
OnSetTransform(TransformField, Axis, NewVector, false, bCommitted);
}
void FComponentTransformDetails::BeginSliderTransaction(FText ActorTransaction, FText ComponentTransaction) const
{
bool bBeganTransaction = false;
for (TWeakObjectPtr<UObject> ObjectPtr : SelectedObjects)
{
if (ObjectPtr.IsValid())
{
UObject* Object = ObjectPtr.Get();
// Start a new transaction when a slider begins to change
// We'll end it when the slider is released
// NOTE: One transaction per change, not per actor
if (!bBeganTransaction)
{
if (Object->IsA<AActor>())
{
GEditor->BeginTransaction(ActorTransaction);
}
else
{
GEditor->BeginTransaction(ComponentTransaction);
}
bBeganTransaction = true;
}
USceneComponent* SceneComponent = GetSceneComponentFromDetailsObject(Object);
if (SceneComponent)
{
FScopedSwitchWorldForObject WorldSwitcher(Object);
if (SceneComponent->HasAnyFlags(RF_DefaultSubObject))
{
// Default subobjects must be included in any undo/redo operations
SceneComponent->SetFlags(RF_Transactional);
}
// Call modify but not PreEdit, we don't do the proper "Edit" until it's committed
SceneComponent->Modify();
}
}
}
// Just in case we couldn't start a new transaction for some reason
if (!bBeganTransaction)
{
GEditor->BeginTransaction(ActorTransaction);
}
}
void FComponentTransformDetails::OnBeginRotationSlider()
{
FText ActorTransaction = LOCTEXT("OnSetRotation", "Set Rotation");
FText ComponentTransaction = LOCTEXT("OnSetRotation_ComponentDirect", "Modify Component(s)");
BeginSliderTransaction(ActorTransaction, ComponentTransaction);
bEditingRotationInUI = true;
bIsSliderTransaction = true;
for (TWeakObjectPtr<UObject> ObjectPtr : SelectedObjects)
{
if (ObjectPtr.IsValid())
{
UObject* Object = ObjectPtr.Get();
USceneComponent* SceneComponent = GetSceneComponentFromDetailsObject(Object);
if (SceneComponent)
{
FScopedSwitchWorldForObject WorldSwitcher(Object);
// Add/update cached rotation value prior to slider interaction
ObjectToRelativeRotationMap.FindOrAdd(SceneComponent) = SceneComponent->GetRelativeRotation();
}
}
}
}
void FComponentTransformDetails::OnEndRotationSlider(FRotator::FReal NewValue)
{
// Commit gets called right before this, only need to end the transaction
bEditingRotationInUI = false;
bIsSliderTransaction = false;
GEditor->EndTransaction();
}
void FComponentTransformDetails::OnBeginLocationSlider()
{
bIsSliderTransaction = true;
FText ActorTransaction = LOCTEXT("OnSetLocation", "Set Location");
FText ComponentTransaction = LOCTEXT("OnSetLocation_ComponentDirect", "Modify Component Location");
BeginSliderTransaction(ActorTransaction, ComponentTransaction);
}
void FComponentTransformDetails::OnEndLocationSlider(FVector::FReal NewValue)
{
bIsSliderTransaction = false;
GEditor->EndTransaction();
}
void FComponentTransformDetails::OnBeginScaleSlider()
{
// Assumption: slider isn't usable if multiple objects are selected
SliderScaleRatio.X = CachedScale.X.GetValue();
SliderScaleRatio.Y = CachedScale.Y.GetValue();
SliderScaleRatio.Z = CachedScale.Z.GetValue();
bIsSliderTransaction = true;
FText ActorTransaction = LOCTEXT("OnSetScale", "Set Scale");
FText ComponentTransaction = LOCTEXT("OnSetScale_ComponentDirect", "Modify Component Scale");
BeginSliderTransaction(ActorTransaction, ComponentTransaction);
}
void FComponentTransformDetails::OnEndScaleSlider(FVector::FReal NewValue)
{
bIsSliderTransaction = false;
GEditor->EndTransaction();
}
void FComponentTransformDetails::OnObjectsReplaced(const TMap<UObject*, UObject*>& ReplacementMap)
{
TArray<UObject*> NewSceneComponents;
for (const TWeakObjectPtr<UObject> Obj : CachedHandlesObjects)
{
if (UObject* Replacement = ReplacementMap.FindRef(Obj.GetEvenIfUnreachable()))
{
NewSceneComponents.Add(Replacement);
}
}
if (NewSceneComponents.Num())
{
UpdatePropertyHandlesObjects(NewSceneComponents);
}
}
namespace ComponentTransformDetailsPrivate
{
TTuple<FQuat::FReal, FQuat::FReal, FQuat::FReal> RadiansToDegrees(const TTuple<FQuat::FReal, FQuat::FReal, FQuat::FReal>& Rads)
{
constexpr FQuat::FReal RadsToDegrees = (180.f / UE_PI);
TTuple<FQuat::FReal, FQuat::FReal, FQuat::FReal> Output
{
Rads.Get<0>() * RadsToDegrees,
Rads.Get<1>() * RadsToDegrees,
Rads.Get<2>() * RadsToDegrees
};
return Output;
}
TTuple<FQuat::FReal, FQuat::FReal, FQuat::FReal> DegreesToRadians(const TTuple<FQuat::FReal, FQuat::FReal, FQuat::FReal>& Rads)
{
constexpr FQuat::FReal DegreesToRadians = (UE_PI / 180.f);
TTuple<FQuat::FReal, FQuat::FReal, FQuat::FReal> Output
{
Rads.Get<0>() * DegreesToRadians,
Rads.Get<1>() * DegreesToRadians,
Rads.Get<2>() * DegreesToRadians
};
return Output;
}
}
FVector FComponentTransformDetails::ConvertFromUnrealSpace_EulerDeg(const FRotator& Rotator) const
{
using namespace ComponentTransformDetailsPrivate;
if (!bIsAxisDisplayLeftUpForward)
{
return Rotator.Euler();
}
FQuat Q = Rotator.Quaternion().GetNormalized();
TTuple<FQuat::FReal, FQuat::FReal, FQuat::FReal> VerseEulerRads = Q.ToLUFEuler();
// Since the value is converted from quaternion, will likely have denormals. Clamp those values.
auto SanitizeFloat = [](FQuat::FReal Val)->FQuat::FReal
{
if (FMath::IsNearlyZero(Val))
{
return 0.0;
}
return Val;
};
FVector VerseEulerRadsV
{
SanitizeFloat(VerseEulerRads.Get<0>()),
SanitizeFloat(VerseEulerRads.Get<1>()),
SanitizeFloat(VerseEulerRads.Get<2>())
};
FVector VerseEulerDegrees = FMath::RadiansToDegrees(VerseEulerRadsV);
return VerseEulerDegrees;
}
FRotator FComponentTransformDetails::ConvertToUnrealSpace_EulerDeg(const FVector& Rotation) const
{
using namespace ComponentTransformDetailsPrivate;
if (!bIsAxisDisplayLeftUpForward)
{
return FRotator::MakeFromEuler(Rotation);
}
const FVector RotationRads = FMath::DegreesToRadians(Rotation);
TTuple<FQuat::FReal, FQuat::FReal, FQuat::FReal> RotationRadsT =
{
RotationRads.X,
RotationRads.Y,
RotationRads.Z
};
FQuat Quat = FQuat::MakeFromLUFEuler(RotationRadsT);
Quat.Normalize();
FRotator Result(Quat);
return Result;
}
#undef LOCTEXT_NAMESPACE