2675 lines
92 KiB
C++
2675 lines
92 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "CurveEditor.h"
|
|
|
|
#include "Algo/Transform.h"
|
|
#include "Containers/SparseArray.h"
|
|
#include "CoreGlobals.h"
|
|
#include "CurveEditorCommands.h"
|
|
#include "CurveEditorCopyBuffer.h"
|
|
#include "CurveEditorSettings.h"
|
|
#include "CurveEditorSnapMetrics.h"
|
|
#include "CurveEditorAxis.h"
|
|
#include "CurveEditorZoomScaleConfig.h"
|
|
#include "CurveModel.h"
|
|
#include "Curves/KeyHandle.h"
|
|
#include "Curves/RichCurve.h"
|
|
#include "Editor.h"
|
|
#include "Editor/EditorEngine.h"
|
|
#include "Exporters/Exporter.h"
|
|
#include "Factories.h"
|
|
#include "Framework/Commands/GenericCommands.h"
|
|
#include "Framework/Commands/UIAction.h"
|
|
#include "Framework/Commands/UICommandList.h"
|
|
#include "Framework/Notifications/NotificationManager.h"
|
|
#include "Framework/SlateDelegates.h"
|
|
#include "HAL/IConsoleManager.h"
|
|
#include "HAL/PlatformApplicationMisc.h"
|
|
#include "ICurveEditorExtension.h"
|
|
#include "ICurveEditorModule.h"
|
|
#include "ICurveEditorToolExtension.h"
|
|
#include "ITimeSlider.h"
|
|
#include "Internationalization/Internationalization.h"
|
|
#include "Layout/Geometry.h"
|
|
#include "Logging/LogCategory.h"
|
|
#include "Logging/LogMacros.h"
|
|
#include "Math/Color.h"
|
|
#include "Math/NumericLimits.h"
|
|
#include "Math/Range.h"
|
|
#include "Math/UnrealMathUtility.h"
|
|
#include "Misc/FrameNumber.h"
|
|
#include "Misc/FrameTime.h"
|
|
#include "Misc/KeyPasteArgs.h"
|
|
#include "Modules/ModuleManager.h"
|
|
#include "SCurveEditor.h" // for access to LogCurveEditor
|
|
#include "SCurveEditorPanel.h"
|
|
#include "SCurveEditorView.h"
|
|
#include "ScopedTransaction.h"
|
|
#include "Templates/Casts.h"
|
|
#include "Templates/Tuple.h"
|
|
#include "Templates/UnrealTemplate.h"
|
|
#include "Trace/Detail/Channel.h"
|
|
#include "UObject/Class.h"
|
|
#include "UObject/Object.h"
|
|
#include "UObject/ObjectMacros.h"
|
|
#include "UObject/ObjectPtr.h"
|
|
#include "UObject/Package.h"
|
|
#include "UObject/PropertyPortFlags.h"
|
|
#include "UObject/UObjectGlobals.h"
|
|
#include "UObject/UnrealNames.h"
|
|
#include "UnrealExporter.h"
|
|
#include "Misc/SmartSnap.h"
|
|
#include "Modification/Utils/ScopedSelectionTransaction.h"
|
|
#include "Widgets/Colors/SColorPicker.h"
|
|
#include "Widgets/Notifications/SNotificationList.h"
|
|
|
|
#define LOCTEXT_NAMESPACE "CurveEditor"
|
|
|
|
FCurveModelID FCurveModelID::Unique()
|
|
{
|
|
static uint32 CurrentID = 1;
|
|
|
|
FCurveModelID ID;
|
|
ID.ID = CurrentID++;
|
|
return ID;
|
|
}
|
|
|
|
FCurveEditor::FCurveEditor()
|
|
: Bounds(new FStaticCurveEditorBounds)
|
|
, bBoundTransformUpdatesSuppressed(false)
|
|
, ActiveCurvesSerialNumber(0)
|
|
, SuspendBroadcastCount(0)
|
|
{
|
|
Settings = GetMutableDefault<UCurveEditorSettings>();
|
|
CommandList = MakeShared<FUICommandList>();
|
|
|
|
OutputSnapEnabledAttribute = true;
|
|
InputSnapEnabledAttribute = true;
|
|
InputSnapRateAttribute = FFrameRate(10, 1);
|
|
|
|
GridLineLabelFormatXAttribute = LOCTEXT("GridXLabelFormat", "{0}s");
|
|
GridLineLabelFormatYAttribute = LOCTEXT("GridYLabelFormat", "{0}");
|
|
|
|
Settings->GetOnCustomColorsChanged().AddRaw(this, &FCurveEditor::OnCustomColorsChanged);
|
|
Settings->GetOnAxisSnappingChanged().AddRaw(this, &FCurveEditor::OnAxisSnappingChanged);
|
|
}
|
|
|
|
FCurveEditor::~FCurveEditor()
|
|
{
|
|
if (!IsEngineExitRequested() && Settings)
|
|
{
|
|
Settings->GetOnCustomColorsChanged().RemoveAll(this);
|
|
Settings->GetOnAxisSnappingChanged().RemoveAll(this);
|
|
}
|
|
}
|
|
|
|
void FCurveEditor::InitCurveEditor(const FCurveEditorInitParams& InInitParams)
|
|
{
|
|
ICurveEditorModule& CurveEditorModule = FModuleManager::LoadModuleChecked<ICurveEditorModule>("CurveEditor");
|
|
|
|
Selection = FCurveEditorSelection(SharedThis(this));
|
|
ZoomScalingAttr = InInitParams.ZoomScalingAttr;
|
|
|
|
// Editor Extensions can be registered in the Curve Editor module. To allow users to derive from FCurveEditor
|
|
// we have to manually reach out to the module and get a list of extensions to create an instance of them.
|
|
// If none of your extensions are showing up, it's because you forgot to call this function after construction
|
|
// We're not allowed to use SharedThis(...) in a Constructor so it must exist as a separate function call.
|
|
EditorExtensions.Append(InInitParams.AdditionalEditorExtensions);
|
|
TArrayView<const FOnCreateCurveEditorExtension> Extensions = CurveEditorModule.GetEditorExtensions();
|
|
for (int32 DelegateIndex = 0; DelegateIndex < Extensions.Num(); ++DelegateIndex)
|
|
{
|
|
check(Extensions[DelegateIndex].IsBound());
|
|
|
|
// We call a delegate and have the delegate create the instance to cover cross-module
|
|
TSharedRef<ICurveEditorExtension> NewExtension = Extensions[DelegateIndex].Execute(SharedThis(this));
|
|
EditorExtensions.Add(NewExtension);
|
|
}
|
|
|
|
|
|
TArrayView<const FOnCreateCurveEditorToolExtension> Tools = CurveEditorModule.GetToolExtensions();
|
|
for (int32 DelegateIndex = 0; DelegateIndex < Tools.Num(); ++DelegateIndex)
|
|
{
|
|
check(Tools[DelegateIndex].IsBound());
|
|
|
|
// We call a delegate and have the delegate create the instance to cover cross-module
|
|
AddTool(Tools[DelegateIndex].Execute(SharedThis(this)));
|
|
}
|
|
|
|
SuspendBroadcastCount = 0;
|
|
// Listen to global undo so we can fix up our selection state for keys that no longer exist.
|
|
GEditor->RegisterForUndo(this);
|
|
TransactionManager = MakeUnique<UE::CurveEditor::FTransactionManager>(SharedThis(this));
|
|
}
|
|
|
|
int32 FCurveEditor::GetSupportedTangentTypes()
|
|
{
|
|
return ((int32)ECurveEditorTangentTypes::InterpolationConstant |
|
|
(int32)ECurveEditorTangentTypes::InterpolationLinear |
|
|
(int32)ECurveEditorTangentTypes::InterpolationCubicAuto |
|
|
(int32)ECurveEditorTangentTypes::InterpolationCubicUser |
|
|
(int32)ECurveEditorTangentTypes::InterpolationCubicBreak |
|
|
(int32)ECurveEditorTangentTypes::InterpolationCubicWeighted);
|
|
//nope we don't support smart auto by default, FRichCurve doesn't support i
|
|
}
|
|
|
|
void FCurveEditor::SetPanel(TSharedPtr<SCurveEditorPanel> InPanel)
|
|
{
|
|
WeakPanel = InPanel;
|
|
}
|
|
|
|
TSharedPtr<SCurveEditorPanel> FCurveEditor::GetPanel() const
|
|
{
|
|
return WeakPanel.Pin();
|
|
}
|
|
|
|
void FCurveEditor::SetView(TSharedPtr<SCurveEditorView> InView)
|
|
{
|
|
WeakView = InView;
|
|
}
|
|
|
|
TSharedPtr<SCurveEditorView> FCurveEditor::GetView() const
|
|
{
|
|
return WeakView.Pin();
|
|
}
|
|
|
|
FCurveModel* FCurveEditor::FindCurve(FCurveModelID CurveID) const
|
|
{
|
|
const TUniquePtr<FCurveModel>* Ptr = CurveData.Find(CurveID);
|
|
return Ptr ? Ptr->Get() : nullptr;
|
|
}
|
|
|
|
const TMap<FCurveModelID, TUniquePtr<FCurveModel>>& FCurveEditor::GetCurves() const
|
|
{
|
|
return CurveData;
|
|
}
|
|
|
|
FCurveEditorToolID FCurveEditor::AddTool(TUniquePtr<ICurveEditorToolExtension>&& InTool)
|
|
{
|
|
FCurveEditorToolID NewID = FCurveEditorToolID::Unique();
|
|
ToolExtensions.Add(NewID, MoveTemp(InTool));
|
|
ToolExtensions[NewID]->SetToolID(NewID);
|
|
return NewID;
|
|
}
|
|
|
|
void FCurveEditor::AddAxis(const FName& InIdentifier, TSharedPtr<FCurveEditorAxis> InAxis)
|
|
{
|
|
// Allow overwrites
|
|
CustomAxes.Add(InIdentifier, InAxis);
|
|
}
|
|
|
|
TSharedPtr<FCurveEditorAxis> FCurveEditor::FindAxis(const FName& InIdentifier) const
|
|
{
|
|
return CustomAxes.FindRef(InIdentifier);
|
|
}
|
|
|
|
void FCurveEditor::RemoveAxis(const FName& InIdentifier)
|
|
{
|
|
CustomAxes.Remove(InIdentifier);
|
|
}
|
|
|
|
void FCurveEditor::ClearAxes()
|
|
{
|
|
CustomAxes.Empty();
|
|
}
|
|
|
|
FCurveModelID FCurveEditor::AddCurve(TUniquePtr<FCurveModel>&& InCurve)
|
|
{
|
|
check(InCurve);
|
|
|
|
// The curve ID is relevant e.g. for undo / redo.
|
|
// You can undo / redo selecting keys: if undo past a transaction that called FCurveEditor::AddCurve, redoing that transaction needs to add back
|
|
// the same curve ID so redoing the key selection also works.
|
|
// If InCurve has no ID set, GetOrInitId will set it here. If the caller actually specifies an ID and that ID is added already, they have a
|
|
// mistake in their business logic.
|
|
const FCurveModelID& CurveId = InCurve->GetOrInitId();
|
|
if (!ensureMsgf(!CurveData.Contains(CurveId), TEXT("Investigate what caused the double-addition and fix it!")))
|
|
{
|
|
return FCurveModelID();
|
|
}
|
|
|
|
FCurveModel *Curve = InCurve.Get();
|
|
|
|
CurveData.Add(CurveId, MoveTemp(InCurve));
|
|
|
|
// Add child curves
|
|
TArray<TUniquePtr<FCurveModel>> ChildCurvesArray;
|
|
Curve->MakeChildCurves(ChildCurvesArray);
|
|
for (TUniquePtr<FCurveModel>& Child : ChildCurvesArray)
|
|
{
|
|
ChildCurves.Add(CurveId, AddCurve(MoveTemp(Child)));
|
|
}
|
|
|
|
++ActiveCurvesSerialNumber;
|
|
if (IsBroadcasting())
|
|
{
|
|
OnCurveArrayChanged.Broadcast(Curve, true, this);
|
|
}
|
|
return CurveId;
|
|
}
|
|
|
|
void FCurveEditor::BroadcastCurveChanged(FCurveModel* InCurve)
|
|
{
|
|
if (IsBroadcasting())
|
|
{
|
|
OnCurveArrayChanged.Broadcast(InCurve, true, this);
|
|
}
|
|
}
|
|
|
|
FCurveModelID FCurveEditor::AddCurveForTreeItem(TUniquePtr<FCurveModel>&& InCurve, FCurveEditorTreeItemID TreeItemID)
|
|
{
|
|
FCurveModelID NewID = AddCurve(MoveTemp(InCurve));
|
|
TreeIDByCurveID.Add(NewID, TreeItemID);
|
|
return NewID;
|
|
}
|
|
|
|
void FCurveEditor::ResetMinMaxes()
|
|
{
|
|
TSharedPtr<SCurveEditorPanel> Panel = WeakPanel.Pin();
|
|
if (Panel.IsValid())
|
|
{
|
|
Panel->ResetMinMaxes();
|
|
}
|
|
}
|
|
void FCurveEditor::RemoveCurve(FCurveModelID InCurveID)
|
|
{
|
|
for (auto ChildID = ChildCurves.CreateConstKeyIterator(InCurveID); ChildID; ++ChildID)
|
|
{
|
|
RemoveCurve(ChildID.Value());
|
|
}
|
|
ChildCurves.Remove(InCurveID);
|
|
|
|
TSharedPtr<SCurveEditorPanel> Panel = WeakPanel.Pin();
|
|
if (Panel.IsValid())
|
|
{
|
|
Panel->RemoveCurveFromViews(InCurveID);
|
|
}
|
|
|
|
if(IsBroadcasting())
|
|
{
|
|
OnCurveArrayChanged.Broadcast(FindCurve(InCurveID), false,this);
|
|
}
|
|
|
|
|
|
CurveData.Remove(InCurveID);
|
|
Selection.Remove(InCurveID);
|
|
PinnedCurves.Remove(InCurveID);
|
|
|
|
|
|
++ActiveCurvesSerialNumber;
|
|
}
|
|
|
|
void FCurveEditor::RemoveAllCurves()
|
|
{
|
|
TSharedPtr<SCurveEditorPanel> Panel = WeakPanel.Pin();
|
|
if (Panel.IsValid())
|
|
{
|
|
for (TPair<FCurveModelID, TUniquePtr<FCurveModel>>& CurvePair : CurveData)
|
|
{
|
|
Panel->RemoveCurveFromViews(CurvePair.Key);
|
|
}
|
|
}
|
|
|
|
CurveData.Empty();
|
|
Selection.Clear();
|
|
PinnedCurves.Empty();
|
|
ChildCurves.Empty();
|
|
|
|
++ActiveCurvesSerialNumber;
|
|
}
|
|
|
|
bool FCurveEditor::IsCurvePinned(FCurveModelID InCurveID) const
|
|
{
|
|
return PinnedCurves.Contains(InCurveID);
|
|
}
|
|
|
|
void FCurveEditor::PinCurve(FCurveModelID InCurveID)
|
|
{
|
|
PinnedCurves.Add(InCurveID);
|
|
++ActiveCurvesSerialNumber;
|
|
}
|
|
|
|
void FCurveEditor::UnpinCurve(FCurveModelID InCurveID)
|
|
{
|
|
PinnedCurves.Remove(InCurveID);
|
|
++ActiveCurvesSerialNumber;
|
|
}
|
|
|
|
const SCurveEditorView* FCurveEditor::FindFirstInteractiveView(FCurveModelID InCurveID) const
|
|
{
|
|
TSharedPtr<SCurveEditorPanel> Panel = WeakPanel.Pin();
|
|
if (Panel.IsValid())
|
|
{
|
|
for (auto ViewIt = Panel->FindViews(InCurveID); ViewIt; ++ViewIt)
|
|
{
|
|
if (ViewIt.Value()->IsInteractive())
|
|
{
|
|
return &ViewIt.Value().Get();
|
|
}
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
FCurveEditorTreeItem& FCurveEditor::GetTreeItem(FCurveEditorTreeItemID ItemID)
|
|
{
|
|
return Tree.GetItem(ItemID);
|
|
}
|
|
|
|
const FCurveEditorTreeItem& FCurveEditor::GetTreeItem(FCurveEditorTreeItemID ItemID) const
|
|
{
|
|
return Tree.GetItem(ItemID);
|
|
}
|
|
|
|
FCurveEditorTreeItem* FCurveEditor::FindTreeItem(FCurveEditorTreeItemID ItemID)
|
|
{
|
|
return Tree.FindItem(ItemID);
|
|
}
|
|
|
|
const FCurveEditorTreeItem* FCurveEditor::FindTreeItem(FCurveEditorTreeItemID ItemID) const
|
|
{
|
|
return Tree.FindItem(ItemID);
|
|
}
|
|
|
|
const TArray<FCurveEditorTreeItemID>& FCurveEditor::GetRootTreeItems() const
|
|
{
|
|
return Tree.GetRootItems();
|
|
}
|
|
|
|
FCurveEditorTreeItemID FCurveEditor::GetTreeIDFromCurveID(FCurveModelID CurveID) const
|
|
{
|
|
if (TreeIDByCurveID.Contains(CurveID))
|
|
{
|
|
return TreeIDByCurveID[CurveID];
|
|
}
|
|
|
|
return FCurveEditorTreeItemID();
|
|
}
|
|
|
|
FCurveEditorTreeItem* FCurveEditor::AddTreeItem(FCurveEditorTreeItemID ParentID)
|
|
{
|
|
return Tree.AddItem(ParentID);
|
|
}
|
|
|
|
void FCurveEditor::RemoveTreeItem(FCurveEditorTreeItemID ItemID)
|
|
{
|
|
FCurveEditorTreeItem* Item = Tree.FindItem(ItemID);
|
|
if (!Item)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Tree.RemoveItem(ItemID, this);
|
|
++ActiveCurvesSerialNumber;
|
|
}
|
|
|
|
void FCurveEditor::RemoveAllTreeItems()
|
|
{
|
|
TArray<FCurveEditorTreeItemID> RootItems = Tree.GetRootItems();
|
|
for(FCurveEditorTreeItemID ItemID : RootItems)
|
|
{
|
|
Tree.RemoveItem(ItemID, this);
|
|
}
|
|
++ActiveCurvesSerialNumber;
|
|
}
|
|
|
|
void FCurveEditor::SetTreeSelection(TArray<FCurveEditorTreeItemID>&& TreeItems)
|
|
{
|
|
Tree.SetDirectSelection(MoveTemp(TreeItems), this);
|
|
}
|
|
|
|
void FCurveEditor::RemoveFromTreeSelection(TArrayView<const FCurveEditorTreeItemID> TreeItems)
|
|
{
|
|
Tree.RemoveFromSelection(TreeItems, this);
|
|
}
|
|
|
|
ECurveEditorTreeSelectionState FCurveEditor::GetTreeSelectionState(FCurveEditorTreeItemID InTreeItemID) const
|
|
{
|
|
return Tree.GetSelectionState(InTreeItemID);
|
|
}
|
|
|
|
const TMap<FCurveEditorTreeItemID, ECurveEditorTreeSelectionState>& FCurveEditor::GetTreeSelection() const
|
|
{
|
|
return Tree.GetSelection();
|
|
}
|
|
|
|
void FCurveEditor::SetBounds(TUniquePtr<ICurveEditorBounds>&& InBounds)
|
|
{
|
|
check(InBounds.IsValid());
|
|
Bounds = MoveTemp(InBounds);
|
|
}
|
|
|
|
bool FCurveEditor::ShouldAutoFrame() const
|
|
{
|
|
return Settings->GetAutoFrameCurveEditor();
|
|
}
|
|
|
|
|
|
void FCurveEditor::BindCommands()
|
|
{
|
|
using namespace UE::CurveEditor;
|
|
UCurveEditorSettings* CurveSettings = Settings;
|
|
|
|
CommandList->MapAction(FGenericCommands::Get().Delete, FExecuteAction::CreateSP(this, &FCurveEditor::DeleteSelection));
|
|
|
|
CommandList->MapAction(FGenericCommands::Get().Cut, FExecuteAction::CreateSP(this, &FCurveEditor::CutSelection));
|
|
CommandList->MapAction(FGenericCommands::Get().Copy, FExecuteAction::CreateSP(this, &FCurveEditor::CopySelection));
|
|
CommandList->MapAction(FGenericCommands::Get().Paste, FExecuteAction::CreateSP(this, &FCurveEditor::PasteKeys,
|
|
FKeyPasteArgs{ .Mode = ECurveEditorPasteMode::OverwriteRange, .Flags = ECurveEditorPasteFlags::Default }
|
|
));
|
|
CommandList->MapAction(FCurveEditorCommands::Get().PasteAndMerge, FExecuteAction::CreateSP(this, &FCurveEditor::PasteKeys,
|
|
FKeyPasteArgs{ .Mode = ECurveEditorPasteMode::Merge, .Flags = ECurveEditorPasteFlags::Default }
|
|
));
|
|
CommandList->MapAction(FCurveEditorCommands::Get().PasteRelative, FExecuteAction::CreateSP(this, &FCurveEditor::PasteKeys,
|
|
FKeyPasteArgs{ .Mode = ECurveEditorPasteMode::OverwriteRange, .Flags = ECurveEditorPasteFlags::Default | ECurveEditorPasteFlags::Relative }
|
|
));
|
|
|
|
CommandList->MapAction(FCurveEditorCommands::Get().ZoomToFit, FExecuteAction::CreateSP(this, &FCurveEditor::ZoomToFit, EAxisList::All));
|
|
CommandList->MapAction(FCurveEditorCommands::Get().ZoomToFitHorizontal, FExecuteAction::CreateSP(this, &FCurveEditor::ZoomToFit, EAxisList::X));
|
|
CommandList->MapAction(FCurveEditorCommands::Get().ZoomToFitVertical, FExecuteAction::CreateSP(this, &FCurveEditor::ZoomToFit, EAxisList::Y));
|
|
CommandList->MapAction(FCurveEditorCommands::Get().ZoomToFitAll, FExecuteAction::CreateSP(this, &FCurveEditor::ZoomToFitAll, EAxisList::All));
|
|
|
|
CommandList->MapAction(FCurveEditorCommands::Get().ToggleExpandCollapseNodes, FExecuteAction::CreateSP(this, &FCurveEditor::ToggleExpandCollapseNodes, false));
|
|
CommandList->MapAction(FCurveEditorCommands::Get().ToggleExpandCollapseNodesAndDescendants, FExecuteAction::CreateSP(this, &FCurveEditor::ToggleExpandCollapseNodes, true));
|
|
|
|
CommandList->MapAction(FCurveEditorCommands::Get().TranslateSelectedKeysLeft, FExecuteAction::CreateSP(this, &FCurveEditor::TranslateSelectedKeysLeft));
|
|
CommandList->MapAction(FCurveEditorCommands::Get().TranslateSelectedKeysRight, FExecuteAction::CreateSP(this, &FCurveEditor::TranslateSelectedKeysRight));
|
|
|
|
CommandList->MapAction(FCurveEditorCommands::Get().SetSelectionRangeStart, FExecuteAction::CreateSP(this, &FCurveEditor::SetSelectionRangeStart));
|
|
CommandList->MapAction(FCurveEditorCommands::Get().SetSelectionRangeEnd, FExecuteAction::CreateSP(this, &FCurveEditor::SetSelectionRangeEnd));
|
|
CommandList->MapAction(FCurveEditorCommands::Get().ClearSelectionRange, FExecuteAction::CreateSP(this, &FCurveEditor::ClearSelectionRange));
|
|
|
|
CommandList->MapAction(FCurveEditorCommands::Get().SelectAllKeys, FExecuteAction::CreateSP(this, &FCurveEditor::SelectAllKeys));
|
|
CommandList->MapAction(FCurveEditorCommands::Get().SelectForward, FExecuteAction::CreateSP(this, &FCurveEditor::SelectForward));
|
|
CommandList->MapAction(FCurveEditorCommands::Get().SelectBackward, FExecuteAction::CreateSP(this, &FCurveEditor::SelectBackward));
|
|
CommandList->MapAction(FCurveEditorCommands::Get().SelectNone, FExecuteAction::CreateSP(this, &FCurveEditor::SelectNone));
|
|
CommandList->MapAction(FCurveEditorCommands::Get().InvertSelection, FExecuteAction::CreateSP(this, &FCurveEditor::InvertSelection));
|
|
|
|
CommandList->MapAction(FCurveEditorCommands::Get().MatchLastTangentToFirst, FExecuteAction::CreateSP(this, &FCurveEditor::MatchLastTangentToFirst,true));
|
|
CommandList->MapAction(FCurveEditorCommands::Get().MatchFirstTangentToLast, FExecuteAction::CreateSP(this, &FCurveEditor::MatchLastTangentToFirst,false));
|
|
|
|
{
|
|
FExecuteAction ToggleInputSnapping = FExecuteAction::CreateSP(this, &FCurveEditor::ToggleInputSnapping);
|
|
FIsActionChecked IsInputSnappingEnabled = FIsActionChecked::CreateSP(this, &FCurveEditor::IsInputSnappingEnabled);
|
|
FExecuteAction ToggleOutputSnapping = FExecuteAction::CreateSP(this, &FCurveEditor::ToggleOutputSnapping);
|
|
FIsActionChecked IsOutputSnappingEnabled = FIsActionChecked::CreateSP(this, &FCurveEditor::IsOutputSnappingEnabled);
|
|
|
|
CommandList->MapAction(FCurveEditorCommands::Get().ToggleInputSnapping, ToggleInputSnapping, FCanExecuteAction(), IsInputSnappingEnabled);
|
|
CommandList->MapAction(FCurveEditorCommands::Get().ToggleOutputSnapping, ToggleOutputSnapping, FCanExecuteAction(), IsOutputSnappingEnabled);
|
|
}
|
|
|
|
// Flip Curve
|
|
CommandList->MapAction(FCurveEditorCommands::Get().FlipCurveHorizontal, FExecuteAction::CreateSP(this, &FCurveEditor::FlipCurve, ECurveFlipDirection::Horizontal));
|
|
CommandList->MapAction(FCurveEditorCommands::Get().FlipCurveVertical, FExecuteAction::CreateSP(this, &FCurveEditor::FlipCurve, ECurveFlipDirection::Vertical));
|
|
|
|
// Flatten and Straighten Tangents
|
|
{
|
|
CommandList->MapAction(FCurveEditorCommands::Get().FlattenTangents, FExecuteAction::CreateSP(this, &FCurveEditor::FlattenSelection), FCanExecuteAction::CreateSP(this, &FCurveEditor::CanFlattenOrStraightenSelection) );
|
|
CommandList->MapAction(FCurveEditorCommands::Get().StraightenTangents, FExecuteAction::CreateSP(this, &FCurveEditor::StraightenSelection), FCanExecuteAction::CreateSP(this, &FCurveEditor::CanFlattenOrStraightenSelection) );
|
|
}
|
|
|
|
CommandList->MapAction(FCurveEditorCommands::Get().SmartSnapKeys, FExecuteAction::CreateSP(this, &FCurveEditor::SmartSnapSelection), FCanExecuteAction::CreateSP(this, &FCurveEditor::CanSmartSnapSelection));
|
|
|
|
// Curve Colors
|
|
{
|
|
CommandList->MapAction(FCurveEditorCommands::Get().SetRandomCurveColorsForSelected, FExecuteAction::CreateSP(this, &FCurveEditor::SetRandomCurveColorsForSelected), FCanExecuteAction());
|
|
CommandList->MapAction(FCurveEditorCommands::Get().SetCurveColorsForSelected, FExecuteAction::CreateSP(this, &FCurveEditor::SetCurveColorsForSelected), FCanExecuteAction());
|
|
}
|
|
|
|
// Tangent Visibility
|
|
{
|
|
FExecuteAction SetAllTangents = FExecuteAction::CreateUObject(Settings, &UCurveEditorSettings::SetTangentVisibility, ECurveEditorTangentVisibility::AllTangents);
|
|
FExecuteAction SetSelectedKeyTangents = FExecuteAction::CreateUObject(Settings, &UCurveEditorSettings::SetTangentVisibility, ECurveEditorTangentVisibility::SelectedKeys);
|
|
FExecuteAction SetNoTangents = FExecuteAction::CreateUObject(Settings, &UCurveEditorSettings::SetTangentVisibility, ECurveEditorTangentVisibility::NoTangents);
|
|
|
|
FIsActionChecked IsAllTangents = FIsActionChecked::CreateLambda( [CurveSettings]{ return CurveSettings->GetTangentVisibility() == ECurveEditorTangentVisibility::AllTangents; } );
|
|
FIsActionChecked IsSelectedKeyTangents = FIsActionChecked::CreateLambda( [CurveSettings]{ return CurveSettings->GetTangentVisibility() == ECurveEditorTangentVisibility::SelectedKeys; } );
|
|
FIsActionChecked IsNoTangents = FIsActionChecked::CreateLambda( [CurveSettings]{ return CurveSettings->GetTangentVisibility() == ECurveEditorTangentVisibility::NoTangents; } );
|
|
|
|
CommandList->MapAction(FCurveEditorCommands::Get().SetAllTangentsVisibility, SetAllTangents, FCanExecuteAction(), IsAllTangents);
|
|
CommandList->MapAction(FCurveEditorCommands::Get().SetSelectedKeysTangentVisibility, SetSelectedKeyTangents, FCanExecuteAction(), IsSelectedKeyTangents);
|
|
CommandList->MapAction(FCurveEditorCommands::Get().SetNoTangentsVisibility, SetNoTangents, FCanExecuteAction(), IsNoTangents);
|
|
}
|
|
|
|
CommandList->MapAction(FCurveEditorCommands::Get().ToggleAutoFrameCurveEditor,
|
|
FExecuteAction::CreateLambda( [CurveSettings]{ CurveSettings->SetAutoFrameCurveEditor( !CurveSettings->GetAutoFrameCurveEditor() ); } ),
|
|
FCanExecuteAction(),
|
|
FIsActionChecked::CreateLambda( [CurveSettings]{ return CurveSettings->GetAutoFrameCurveEditor(); } )
|
|
);
|
|
|
|
CommandList->MapAction(FCurveEditorCommands::Get().ToggleShowBars,
|
|
FExecuteAction::CreateLambda([this, CurveSettings] { CurveSettings->SetShowBars(!CurveSettings->GetShowBars()); Tree.RecreateModelsFromExistingSelection(this); }),
|
|
FCanExecuteAction(),
|
|
FIsActionChecked::CreateLambda([CurveSettings] { return CurveSettings->GetShowBars(); })
|
|
);
|
|
|
|
CommandList->MapAction(FCurveEditorCommands::Get().ToggleSnapTimeToSelection,
|
|
FExecuteAction::CreateLambda( [CurveSettings]{ CurveSettings->SetSnapTimeToSelection( !CurveSettings->GetSnapTimeToSelection() ); } ),
|
|
FCanExecuteAction(),
|
|
FIsActionChecked::CreateLambda( [CurveSettings]{ return CurveSettings->GetSnapTimeToSelection(); } )
|
|
);
|
|
|
|
CommandList->MapAction(FCurveEditorCommands::Get().ToggleShowBufferedCurves,
|
|
FExecuteAction::CreateLambda( [CurveSettings]{ CurveSettings->SetShowBufferedCurves( !CurveSettings->GetShowBufferedCurves() ); } ),
|
|
FCanExecuteAction(),
|
|
FIsActionChecked::CreateLambda( [CurveSettings]{ return CurveSettings->GetShowBufferedCurves(); } )
|
|
);
|
|
|
|
CommandList->MapAction(FCurveEditorCommands::Get().ToggleShowCurveEditorCurveToolTips,
|
|
FExecuteAction::CreateLambda( [CurveSettings]{ CurveSettings->SetShowCurveEditorCurveToolTips( !CurveSettings->GetShowCurveEditorCurveToolTips() ); } ),
|
|
FCanExecuteAction(),
|
|
FIsActionChecked::CreateLambda( [CurveSettings]{ return CurveSettings->GetShowCurveEditorCurveToolTips(); } )
|
|
);
|
|
CommandList->MapAction(FCurveEditorCommands::Get().ToggleShowValueIndicatorLines,
|
|
FExecuteAction::CreateLambda( [CurveSettings]{ CurveSettings->SetShowValueIndicators( !CurveSettings->GetShowValueIndicators() ); } ),
|
|
FCanExecuteAction(),
|
|
FIsActionChecked::CreateLambda( [CurveSettings]{ return CurveSettings->GetShowValueIndicators(); } )
|
|
);
|
|
|
|
// Deactivate Current Tool
|
|
CommandList->MapAction(FCurveEditorCommands::Get().DeactivateCurrentTool,
|
|
FExecuteAction::CreateSP(this, &FCurveEditor::MakeToolActive, FCurveEditorToolID::Unset()),
|
|
FCanExecuteAction(),
|
|
FIsActionChecked::CreateLambda( [this]{ return ActiveTool.IsSet() == false; } ) );
|
|
|
|
// Bind commands for Editor Extensions
|
|
for (TSharedRef<ICurveEditorExtension> Extension : EditorExtensions)
|
|
{
|
|
Extension->BindCommands(CommandList.ToSharedRef());
|
|
}
|
|
|
|
// Bind Commands for Tool Extensions
|
|
for (TPair<FCurveEditorToolID, TUniquePtr<ICurveEditorToolExtension>>& Pair : ToolExtensions)
|
|
{
|
|
Pair.Value->BindCommands(CommandList.ToSharedRef());
|
|
}
|
|
}
|
|
|
|
TSharedPtr<UE::CurveEditor::FPromotedFilterContainer> FCurveEditor::GetToolbarPromotedFilters() const
|
|
{
|
|
TSharedPtr<UE::CurveEditor::FPromotedFilterContainer> Result = ICurveEditorModule::Get().GetGlobalToolbarPromotedFilters();
|
|
checkf(Result, TEXT("Should be valid for the lifetime of the module"));
|
|
|
|
// In the future, we could extend FCurveEditor to have its own override for the globally promoted filters.
|
|
return Result;
|
|
}
|
|
|
|
FCurveSnapMetrics FCurveEditor::GetCurveSnapMetrics(FCurveModelID CurveModel) const
|
|
{
|
|
FCurveSnapMetrics CurveMetrics;
|
|
|
|
const SCurveEditorView* View = FindFirstInteractiveView(CurveModel);
|
|
if (!View)
|
|
{
|
|
return CurveMetrics;
|
|
}
|
|
|
|
// get the grid lines in view space
|
|
TArray<float> ViewSpaceGridLines;
|
|
View->GetGridLinesY(SharedThis(this), ViewSpaceGridLines, ViewSpaceGridLines);
|
|
|
|
// convert the grid lines from view space
|
|
TArray<double> CurveSpaceGridLines;
|
|
ViewSpaceGridLines.Reserve(ViewSpaceGridLines.Num());
|
|
FCurveEditorScreenSpace CurveSpace = View->GetCurveSpace(CurveModel);
|
|
Algo::Transform(ViewSpaceGridLines, CurveSpaceGridLines, [&CurveSpace](float VSVal) { return CurveSpace.ScreenToValue(VSVal); });
|
|
|
|
// create metrics struct;
|
|
CurveMetrics.bSnapOutputValues = OutputSnapEnabledAttribute.Get();
|
|
CurveMetrics.bSnapInputValues = InputSnapEnabledAttribute.Get();
|
|
CurveMetrics.AllGridLines = CurveSpaceGridLines;
|
|
CurveMetrics.InputSnapRate = InputSnapRateAttribute.Get();
|
|
|
|
return CurveMetrics;
|
|
}
|
|
|
|
void FCurveEditor::ZoomToFit(EAxisList::Type Axes)
|
|
{
|
|
// If they have keys selected, we fit the specific keys.
|
|
if (Selection.Count() > 0)
|
|
{
|
|
ZoomToFitSelection(Axes);
|
|
}
|
|
else
|
|
{
|
|
ZoomToFitAll(Axes);
|
|
}
|
|
}
|
|
|
|
void FCurveEditor::ZoomToFitAll(EAxisList::Type Axes)
|
|
{
|
|
TMap<FCurveModelID, FKeyHandleSet> AllCurves;
|
|
for (FCurveModelID ID : GetEditedCurves())
|
|
{
|
|
AllCurves.Add(ID);
|
|
}
|
|
ZoomToFitInternal(Axes, AllCurves);
|
|
}
|
|
|
|
void FCurveEditor::ZoomToFitCurves(TArrayView<const FCurveModelID> CurveModelIDs, EAxisList::Type Axes)
|
|
{
|
|
TMap<FCurveModelID, FKeyHandleSet> AllCurves;
|
|
for (FCurveModelID ID : CurveModelIDs)
|
|
{
|
|
AllCurves.Add(ID);
|
|
}
|
|
ZoomToFitInternal(Axes, AllCurves);
|
|
}
|
|
|
|
void FCurveEditor::ZoomToFitSelection(EAxisList::Type Axes)
|
|
{
|
|
ZoomToFitInternal(Axes, Selection.GetAll());
|
|
}
|
|
|
|
const FCurveEditorZoomScaleConfig& FCurveEditor::GetZoomScaleConfig() const
|
|
{
|
|
const bool bCanCallGet = ZoomScalingAttr.IsBound() || ZoomScalingAttr.IsSet();
|
|
const FCurveEditorZoomScaleConfig* OverrideConfig = bCanCallGet ? ZoomScalingAttr.Get() : nullptr;
|
|
|
|
static FCurveEditorZoomScaleConfig Default;
|
|
return OverrideConfig ? *OverrideConfig : Default;
|
|
}
|
|
|
|
void FCurveEditor::ZoomToFitInternal(EAxisList::Type Axes, const TMap<FCurveModelID, FKeyHandleSet>& CurveKeySet)
|
|
{
|
|
TArray<FKeyPosition> KeyPositionsScratch;
|
|
|
|
TMap<TTuple<TSharedRef<SCurveEditorView>, FCurveEditorViewAxisID>, TTuple<double, double>> ViewAndAxisToInputBounds;
|
|
TMap<TTuple<TSharedRef<SCurveEditorView>, FCurveEditorViewAxisID>, TTuple<double, double>> ViewAndAxisToOutputBounds;
|
|
|
|
auto TrackHorizontalBoundsForView = [&ViewAndAxisToInputBounds, Axes](const TSharedRef<SCurveEditorView>& View, FCurveModelID InCurveID, double InputMin, double InputMax)
|
|
{
|
|
if (Axes & EAxisList::X)
|
|
{
|
|
FCurveEditorViewAxisID HorizontalAxis = View->GetAxisForCurve(InCurveID, ECurveEditorAxisOrientation::Horizontal);
|
|
if (HorizontalAxis) // Only track horizontal axis zoom for custom axes since every view is implicitly linked to the global curve editor bounds
|
|
{
|
|
TTuple<double, double>* ViewBounds = ViewAndAxisToInputBounds.Find(MakeTuple(View, HorizontalAxis));
|
|
if (ViewBounds)
|
|
{
|
|
ViewBounds->Get<0>() = FMath::Min(ViewBounds->Get<0>(), InputMin);
|
|
ViewBounds->Get<1>() = FMath::Max(ViewBounds->Get<1>(), InputMax);
|
|
}
|
|
else
|
|
{
|
|
ViewAndAxisToInputBounds.Add(MakeTuple(View, HorizontalAxis), MakeTuple(InputMin, InputMax));
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
auto TrackVerticalBoundsForView = [&ViewAndAxisToOutputBounds, Axes](const TSharedRef<SCurveEditorView>& View, FCurveModelID InCurveID, double OutputMin, double OutputMax)
|
|
{
|
|
if (Axes & EAxisList::Y)
|
|
{
|
|
FCurveEditorViewAxisID VerticalAxis = View->GetAxisForCurve(InCurveID, ECurveEditorAxisOrientation::Vertical);
|
|
|
|
TTuple<double, double>* ViewBounds = ViewAndAxisToOutputBounds.Find(MakeTuple(View, VerticalAxis));
|
|
if (ViewBounds)
|
|
{
|
|
ViewBounds->Get<0>() = FMath::Min(ViewBounds->Get<0>(), OutputMin);
|
|
ViewBounds->Get<1>() = FMath::Max(ViewBounds->Get<1>(), OutputMax);
|
|
}
|
|
else
|
|
{
|
|
ViewAndAxisToOutputBounds.Add(MakeTuple(View, VerticalAxis), MakeTuple(OutputMin, OutputMax));
|
|
}
|
|
}
|
|
};
|
|
|
|
double AllInputMin = TNumericLimits<double>::Max(), AllInputMax = TNumericLimits<double>::Lowest();
|
|
|
|
TSharedPtr<SCurveEditorPanel> Panel = WeakPanel.Pin();
|
|
TSharedPtr<SCurveEditorView> View = WeakView.Pin();
|
|
|
|
for (const TTuple<FCurveModelID, FKeyHandleSet>& Pair : CurveKeySet)
|
|
{
|
|
FCurveModelID CurveID = Pair.Key;
|
|
const FCurveModel* Curve = FindCurve(CurveID);
|
|
if (!Curve)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
double InputMin = TNumericLimits<double>::Max(), InputMax = TNumericLimits<double>::Lowest();
|
|
double OutputMin = TNumericLimits<double>::Max(), OutputMax = TNumericLimits<double>::Lowest();
|
|
|
|
int32 NumKeys = Pair.Value.AsArray().Num();
|
|
if (NumKeys == 0)
|
|
{
|
|
double LocalMin = 0.0, LocalMax = 1.0;
|
|
|
|
// Zoom to the entire curve range if no specific keys are specified
|
|
if (Curve->GetNumKeys())
|
|
{
|
|
// Only zoom time range if there are keys on the curve (otherwise where do we zoom *to* on an infinite timeline?)
|
|
Curve->GetTimeRange(LocalMin, LocalMax);
|
|
InputMin = FMath::Min(InputMin, LocalMin);
|
|
InputMax = FMath::Max(InputMax, LocalMax);
|
|
}
|
|
|
|
// Most curve types we know about support default values, so we can zoom to that even if there are no keys
|
|
Curve->GetValueRange(LocalMin, LocalMax);
|
|
OutputMin = FMath::Min(OutputMin, LocalMin);
|
|
OutputMax = FMath::Max(OutputMax, LocalMax);
|
|
}
|
|
else
|
|
{
|
|
// Zoom to the min/max of the specified key set
|
|
KeyPositionsScratch.SetNum(NumKeys, EAllowShrinking::No);
|
|
Curve->GetKeyPositions(Pair.Value.AsArray(), KeyPositionsScratch);
|
|
for (const FKeyPosition& Key : KeyPositionsScratch)
|
|
{
|
|
InputMin = FMath::Min(InputMin, Key.InputValue);
|
|
InputMax = FMath::Max(InputMax, Key.InputValue);
|
|
OutputMin = FMath::Min(OutputMin, Key.OutputValue);
|
|
OutputMax = FMath::Max(OutputMax, Key.OutputValue);
|
|
}
|
|
}
|
|
|
|
AllInputMin = FMath::Min(InputMin, AllInputMin);
|
|
AllInputMax = FMath::Max(InputMax, AllInputMax);
|
|
|
|
if (Panel)
|
|
{
|
|
// Store the min max for each view
|
|
for (auto ViewIt = Panel->FindViews(CurveID); ViewIt; ++ViewIt)
|
|
{
|
|
TrackHorizontalBoundsForView(ViewIt.Value(), CurveID, InputMin, InputMax);
|
|
TrackVerticalBoundsForView(ViewIt.Value(), CurveID, OutputMin, OutputMax);
|
|
}
|
|
}
|
|
else if(View.IsValid())
|
|
{
|
|
TrackHorizontalBoundsForView(View.ToSharedRef(), CurveID, InputMin, InputMax);
|
|
TrackVerticalBoundsForView(View.ToSharedRef(), CurveID, OutputMin, OutputMax);
|
|
}
|
|
}
|
|
|
|
auto AdjustHorizontalBounds = [this, Panel, View](TSharedPtr<SCurveEditorView> InView, double CurrentInputMin, double CurrentInputMax, double& NewInputMin, double& NewInputMax)
|
|
{
|
|
// If zooming to the same (or invalid) min/max, keep the same zoom scale and center within the timeline
|
|
if (NewInputMin >= NewInputMax)
|
|
{
|
|
const double HalfInputScale = (CurrentInputMax - CurrentInputMin) * 0.5;
|
|
NewInputMin -= HalfInputScale;
|
|
NewInputMax += HalfInputScale;
|
|
}
|
|
else
|
|
{
|
|
double PanelHeight = 0;
|
|
if (Panel)
|
|
{
|
|
PanelHeight = Panel->GetViewContainerGeometry().GetLocalSize().Y;
|
|
}
|
|
else
|
|
{
|
|
PanelHeight = InView->GetViewSpace().GetPhysicalHeight();
|
|
}
|
|
|
|
double InputPercentage = PanelHeight != 0 ? FMath::Min(Settings->GetFrameInputPadding() / PanelHeight, 0.5) : 0.1; // Cannot pad more than half the height
|
|
|
|
constexpr double MinInputZoom = 0.00001;
|
|
const double InputPadding = FMath::Max((NewInputMax - NewInputMin) * InputPercentage, MinInputZoom);
|
|
|
|
NewInputMin -= InputPadding;
|
|
NewInputMax = FMath::Max(NewInputMin + MinInputZoom, NewInputMax) + InputPadding;
|
|
}
|
|
};
|
|
|
|
// Perform per-view input zoom for custom axes
|
|
for (const TPair<TTuple<TSharedRef<SCurveEditorView>, FCurveEditorViewAxisID>, TTuple<double, double>>& ViewAndAxisToBounds : ViewAndAxisToInputBounds)
|
|
{
|
|
FCurveEditorViewAxisID AxisID = ViewAndAxisToBounds.Key.Value;
|
|
TSharedRef<SCurveEditorView> AxisView = ViewAndAxisToBounds.Key.Key;
|
|
|
|
check(AxisID);
|
|
|
|
FCurveEditorScreenSpaceH AxisSpace = AxisView->GetHorizontalAxisSpace(AxisID);
|
|
|
|
double InputMin = ViewAndAxisToBounds.Value.Get<0>();
|
|
double InputMax = ViewAndAxisToBounds.Value.Get<1>();
|
|
|
|
AdjustHorizontalBounds(AxisView, AxisSpace.GetInputMin(), AxisSpace.GetInputMax(), InputMin, InputMax);
|
|
|
|
AxisView->FrameHorizontal(InputMin, InputMax, AxisID);
|
|
}
|
|
|
|
if (Axes & EAxisList::X && AllInputMin != TNumericLimits<double>::Max() && AllInputMax != TNumericLimits<double>::Lowest())
|
|
{
|
|
double CurrentInputMin = 0.0, CurrentInputMax = 1.0;
|
|
Bounds->GetInputBounds(CurrentInputMin, CurrentInputMax);
|
|
|
|
AdjustHorizontalBounds(View, CurrentInputMin, CurrentInputMax, AllInputMin, AllInputMax);
|
|
|
|
Bounds->SetInputBounds(AllInputMin, AllInputMax);
|
|
}
|
|
|
|
// Perform per-view output zoom for any computed ranges
|
|
for (const TPair<TTuple<TSharedRef<SCurveEditorView>, FCurveEditorViewAxisID>, TTuple<double, double>>& ViewAndAxisToBounds : ViewAndAxisToOutputBounds)
|
|
{
|
|
FCurveEditorViewAxisID AxisID = ViewAndAxisToBounds.Key.Value;
|
|
TSharedRef<SCurveEditorView> AxisView = ViewAndAxisToBounds.Key.Key;
|
|
|
|
double OutputMin = ViewAndAxisToBounds.Value.Get<0>();
|
|
double OutputMax = ViewAndAxisToBounds.Value.Get<1>();
|
|
|
|
// If zooming to the same (or invalid) min/max, keep the same zoom scale and center within the timeline
|
|
if (OutputMin >= OutputMax)
|
|
{
|
|
const double HalfOutputScale = (AxisView->GetOutputMax() - AxisView->GetOutputMin()) * 0.5;
|
|
OutputMin -= HalfOutputScale;
|
|
OutputMax += HalfOutputScale;
|
|
}
|
|
else
|
|
{
|
|
double PanelHeight = 0;
|
|
if (Panel)
|
|
{
|
|
PanelHeight = Panel->GetViewContainerGeometry().GetLocalSize().Y;
|
|
}
|
|
else
|
|
{
|
|
PanelHeight = AxisView->GetViewSpace().GetPhysicalHeight();
|
|
}
|
|
|
|
double OutputPercentage = PanelHeight != 0 ? FMath::Min(Settings->GetFrameOutputPadding() / PanelHeight, 0.5) : 0.1; // Cannot pad more than half the height
|
|
|
|
constexpr double MinOutputZoom = 0.00001;
|
|
const double OutputPadding = FMath::Max((OutputMax - OutputMin) * OutputPercentage, MinOutputZoom);
|
|
|
|
OutputMin -= OutputPadding;
|
|
OutputMax = FMath::Max(OutputMin + MinOutputZoom, OutputMax) + OutputPadding;
|
|
}
|
|
|
|
AxisView->FrameVertical(OutputMin, OutputMax, AxisID);
|
|
}
|
|
}
|
|
|
|
void FCurveEditor::TranslateSelectedKeys(double SecondsToAdd)
|
|
{
|
|
if (Selection.Count() > 0)
|
|
{
|
|
for (const TTuple<FCurveModelID, FKeyHandleSet>& Pair : Selection.GetAll())
|
|
{
|
|
if (FCurveModel* Curve = FindCurve(Pair.Key))
|
|
{
|
|
int32 NumKeys = Pair.Value.Num();
|
|
|
|
if (NumKeys > 0)
|
|
{
|
|
TArrayView<const FKeyHandle> KeyHandles = Pair.Value.AsArray();
|
|
TArray<FKeyPosition> KeyPositions;
|
|
KeyPositions.SetNum(KeyHandles.Num());
|
|
|
|
Curve->GetKeyPositions(KeyHandles, KeyPositions);
|
|
|
|
for (int KeyIndex = 0; KeyIndex < KeyPositions.Num(); ++KeyIndex)
|
|
{
|
|
KeyPositions[KeyIndex].InputValue += SecondsToAdd;
|
|
}
|
|
Curve->SetKeyPositions(KeyHandles, KeyPositions);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void FCurveEditor::TranslateSelectedKeysLeft()
|
|
{
|
|
TSharedPtr<ITimeSliderController> TimeSliderController = WeakTimeSliderController.Pin();
|
|
if (!TimeSliderController.IsValid())
|
|
{
|
|
return;
|
|
}
|
|
|
|
FScopedTransaction Transaction(LOCTEXT("TranslateKeysLeft", "Translate Keys Left"));
|
|
FFrameRate FrameRate = TimeSliderController->GetDisplayRate();
|
|
double SecondsToAdd = -FrameRate.AsInterval();
|
|
TranslateSelectedKeys(SecondsToAdd);
|
|
}
|
|
|
|
void FCurveEditor::TranslateSelectedKeysRight()
|
|
{
|
|
TSharedPtr<ITimeSliderController> TimeSliderController = WeakTimeSliderController.Pin();
|
|
if (!TimeSliderController.IsValid())
|
|
{
|
|
return;
|
|
}
|
|
|
|
FScopedTransaction Transaction(LOCTEXT("TranslateKeyRight", "Translate Keys Right"));
|
|
FFrameRate FrameRate = TimeSliderController->GetDisplayRate();
|
|
double SecondsToAdd = FrameRate.AsInterval();
|
|
|
|
TranslateSelectedKeys(SecondsToAdd);
|
|
}
|
|
|
|
void FCurveEditor::SnapToSelectedKey()
|
|
{
|
|
TSharedPtr<ITimeSliderController> TimeSliderController = WeakTimeSliderController.Pin();
|
|
if (!TimeSliderController.IsValid())
|
|
{
|
|
return;
|
|
}
|
|
|
|
FFrameRate TickResolution = TimeSliderController->GetTickResolution();
|
|
|
|
TOptional<double> MinTime;
|
|
|
|
for (const TTuple<FCurveModelID, FKeyHandleSet>& Pair : Selection.GetAll())
|
|
{
|
|
if (FCurveModel* Curve = FindCurve(Pair.Key))
|
|
{
|
|
int32 NumKeys = Pair.Value.Num();
|
|
|
|
if (NumKeys > 0)
|
|
{
|
|
TArrayView<const FKeyHandle> KeyHandles = Pair.Value.AsArray();
|
|
TArray<FKeyPosition> KeyPositions;
|
|
KeyPositions.SetNum(KeyHandles.Num());
|
|
|
|
Curve->GetKeyPositions(KeyHandles, KeyPositions);
|
|
|
|
for (const FKeyPosition& KeyPosition : KeyPositions)
|
|
{
|
|
if (MinTime.IsSet())
|
|
{
|
|
MinTime = FMath::Min(KeyPosition.InputValue, MinTime.GetValue());
|
|
}
|
|
else
|
|
{
|
|
MinTime = KeyPosition.InputValue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (MinTime.IsSet())
|
|
{
|
|
TimeSliderController->SetScrubPosition(MinTime.GetValue() * TickResolution,/*bEvaluate*/ true);
|
|
}
|
|
}
|
|
|
|
void FCurveEditor::SetSelectionRangeStart()
|
|
{
|
|
TSharedPtr<ITimeSliderController> TimeSliderController = WeakTimeSliderController.Pin();
|
|
if (!TimeSliderController.IsValid())
|
|
{
|
|
return;
|
|
}
|
|
|
|
FFrameNumber LocalTime = TimeSliderController->GetScrubPosition().FrameNumber;
|
|
FFrameNumber UpperBound = TimeSliderController->GetSelectionRange().GetUpperBoundValue();
|
|
if (UpperBound <= LocalTime)
|
|
{
|
|
TimeSliderController->SetSelectionRange(TRange<FFrameNumber>(LocalTime, LocalTime + 1));
|
|
}
|
|
else
|
|
{
|
|
TimeSliderController->SetSelectionRange(TRange<FFrameNumber>(LocalTime, UpperBound));
|
|
}
|
|
}
|
|
|
|
void FCurveEditor::SetSelectionRangeEnd()
|
|
{
|
|
TSharedPtr<ITimeSliderController> TimeSliderController = WeakTimeSliderController.Pin();
|
|
if (!TimeSliderController.IsValid())
|
|
{
|
|
return;
|
|
}
|
|
|
|
FFrameNumber LocalTime = TimeSliderController->GetScrubPosition().FrameNumber;
|
|
FFrameNumber LowerBound = TimeSliderController->GetSelectionRange().GetLowerBoundValue();
|
|
if (LowerBound >= LocalTime)
|
|
{
|
|
TimeSliderController->SetSelectionRange(TRange<FFrameNumber>(LocalTime - 1, LocalTime));
|
|
}
|
|
else
|
|
{
|
|
TimeSliderController->SetSelectionRange(TRange<FFrameNumber>(LowerBound, LocalTime));
|
|
}
|
|
}
|
|
|
|
void FCurveEditor::ClearSelectionRange()
|
|
{
|
|
TSharedPtr<ITimeSliderController> TimeSliderController = WeakTimeSliderController.Pin();
|
|
if (!TimeSliderController.IsValid())
|
|
{
|
|
return;
|
|
}
|
|
|
|
TimeSliderController->SetSelectionRange(TRange<FFrameNumber>::Empty());
|
|
}
|
|
|
|
void FCurveEditor::SelectAllKeys()
|
|
{
|
|
const UE::CurveEditor::FScopedSelectionTransaction Transaction(SharedThis(this), LOCTEXT("SelectAllKeys", "Select all keys"));
|
|
for (FCurveModelID ID : GetEditedCurves())
|
|
{
|
|
if (FCurveModel* Curve = FindCurve(ID))
|
|
{
|
|
TArray<FKeyHandle> KeyHandles;
|
|
Curve->GetKeys(TNumericLimits<double>::Lowest(), TNumericLimits<double>::Max(), TNumericLimits<double>::Lowest(), TNumericLimits<double>::Max(), KeyHandles);
|
|
Selection.Add(ID, ECurvePointType::Key, KeyHandles);
|
|
}
|
|
}
|
|
}
|
|
|
|
void FCurveEditor::SelectForward()
|
|
{
|
|
const UE::CurveEditor::FScopedSelectionTransaction Transaction(SharedThis(this), LOCTEXT("SelectForward", "Select forward"));
|
|
Selection.Clear();
|
|
|
|
TSharedPtr<ITimeSliderController> TimeSliderController = WeakTimeSliderController.Pin();
|
|
if (!TimeSliderController.IsValid())
|
|
{
|
|
return;
|
|
}
|
|
|
|
FFrameRate TickResolution = TimeSliderController->GetTickResolution();
|
|
|
|
double CurrentTime = TickResolution.AsSeconds(TimeSliderController->GetScrubPosition());
|
|
|
|
for (FCurveModelID ID : GetEditedCurves())
|
|
{
|
|
if (FCurveModel* Curve = FindCurve(ID))
|
|
{
|
|
TArray<FKeyHandle> KeyHandles;
|
|
Curve->GetKeys(CurrentTime, TNumericLimits<double>::Max(), TNumericLimits<double>::Lowest(), TNumericLimits<double>::Max(), KeyHandles);
|
|
Selection.Add(ID, ECurvePointType::Key, KeyHandles);
|
|
}
|
|
}
|
|
}
|
|
|
|
void FCurveEditor::SelectBackward()
|
|
{
|
|
const UE::CurveEditor::FScopedSelectionTransaction Transaction(SharedThis(this), LOCTEXT("SelectBackward", "Select backward"));
|
|
Selection.Clear();
|
|
|
|
TSharedPtr<ITimeSliderController> TimeSliderController = WeakTimeSliderController.Pin();
|
|
if (!TimeSliderController.IsValid())
|
|
{
|
|
return;
|
|
}
|
|
|
|
FFrameRate TickResolution = TimeSliderController->GetTickResolution();
|
|
|
|
double CurrentTime = TickResolution.AsSeconds(TimeSliderController->GetScrubPosition());
|
|
|
|
for (FCurveModelID ID : GetEditedCurves())
|
|
{
|
|
if (FCurveModel* Curve = FindCurve(ID))
|
|
{
|
|
TArray<FKeyHandle> KeyHandles;
|
|
Curve->GetKeys(TNumericLimits<double>::Min(), CurrentTime, TNumericLimits<double>::Lowest(), TNumericLimits<double>::Max(), KeyHandles);
|
|
Selection.Add(ID, ECurvePointType::Key, KeyHandles);
|
|
}
|
|
}
|
|
}
|
|
|
|
void FCurveEditor::SelectNone()
|
|
{
|
|
const UE::CurveEditor::FScopedSelectionTransaction Transaction(SharedThis(this));
|
|
Selection.Clear();
|
|
}
|
|
|
|
void FCurveEditor::InvertSelection()
|
|
{
|
|
const UE::CurveEditor::FScopedSelectionTransaction Transaction(SharedThis(this), LOCTEXT("InvertSelection", "Invert selection"));
|
|
for (const TTuple<FCurveModelID, FKeyHandleSet>& Pair : Selection.GetAll())
|
|
{
|
|
FCurveModelID CurveModelID = Pair.Key;
|
|
if (FCurveModel* Curve = FindCurve(CurveModelID))
|
|
{
|
|
TArray<FKeyHandle> KeyHandles;
|
|
Curve->GetKeys(TNumericLimits<double>::Lowest(), TNumericLimits<double>::Max(), TNumericLimits<double>::Lowest(), TNumericLimits<double>::Max(), KeyHandles);
|
|
|
|
TArrayView<const FKeyHandle> SelectedKeyHandles = Pair.Value.AsArray();
|
|
|
|
if (SelectedKeyHandles.Num() > 0)
|
|
{
|
|
for (const FKeyHandle& SelectedKeyHandle : SelectedKeyHandles)
|
|
{
|
|
KeyHandles.Remove(SelectedKeyHandle);
|
|
}
|
|
|
|
Selection.Remove(CurveModelID);
|
|
Selection.Add(CurveModelID, ECurvePointType::Key, KeyHandles);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool FCurveEditor::IsInputSnappingEnabled() const
|
|
{
|
|
return InputSnapEnabledAttribute.Get();
|
|
}
|
|
|
|
void FCurveEditor::ToggleInputSnapping()
|
|
{
|
|
bool NewValue = !InputSnapEnabledAttribute.Get();
|
|
|
|
if (!InputSnapEnabledAttribute.IsBound())
|
|
{
|
|
InputSnapEnabledAttribute = NewValue;
|
|
}
|
|
else
|
|
{
|
|
OnInputSnapEnabledChanged.ExecuteIfBound(NewValue);
|
|
}
|
|
}
|
|
|
|
bool FCurveEditor::IsOutputSnappingEnabled() const
|
|
{
|
|
return OutputSnapEnabledAttribute.Get();
|
|
}
|
|
|
|
void FCurveEditor::ToggleOutputSnapping()
|
|
{
|
|
bool NewValue = !OutputSnapEnabledAttribute.Get();
|
|
|
|
if (!OutputSnapEnabledAttribute.IsBound())
|
|
{
|
|
OutputSnapEnabledAttribute = NewValue;
|
|
}
|
|
else
|
|
{
|
|
OnOutputSnapEnabledChanged.ExecuteIfBound(NewValue);
|
|
}
|
|
}
|
|
void FCurveEditor::FlipCurveHorizontal(TArray<FKeyPosition>& AllKeyPositions, TArray<FKeyAttributes>& AllKeyAttributes, ECurveFlipRangeType RangeType,
|
|
float InRangeMin, float InRangeMax, double CurveMinTime, double CurveMaxTime)
|
|
{
|
|
float RangeMin = FLT_MAX;
|
|
float RangeMax = -FLT_MAX;
|
|
|
|
if (RangeType == ECurveFlipRangeType::CurveRange)
|
|
{
|
|
RangeMin = FMath::Min(CurveMinTime, RangeMin);
|
|
RangeMax = FMath::Max(CurveMaxTime, RangeMax);
|
|
}
|
|
else
|
|
{
|
|
RangeMin = InRangeMin;
|
|
RangeMax = InRangeMax;
|
|
}
|
|
|
|
// Loop through all keys to adjust positions and tangents
|
|
for (int32 Index = AllKeyPositions.Num() - 1; Index >= 0; --Index)
|
|
{
|
|
FKeyPosition& Position = AllKeyPositions[Index];
|
|
FKeyAttributes& Attributes = AllKeyAttributes[Index];
|
|
|
|
// Mirror x value
|
|
Position.InputValue = RangeMax - Position.InputValue + RangeMin;
|
|
|
|
// Mirror tangent
|
|
if (Attributes.HasArriveTangent() && Attributes.HasLeaveTangent())
|
|
{
|
|
float ArriveTemp = Attributes.GetArriveTangent();
|
|
float LeaveTemp = Attributes.GetLeaveTangent();
|
|
Attributes.SetArriveTangent(-LeaveTemp);
|
|
Attributes.SetLeaveTangent(-ArriveTemp);
|
|
}
|
|
|
|
if (Attributes.HasTangentMode())
|
|
{
|
|
// Mirror tangent weight
|
|
if (Attributes.HasArriveTangentWeight() && Attributes.HasLeaveTangentWeight())
|
|
{
|
|
float ArriveWeightTemp = Attributes.GetArriveTangentWeight();
|
|
float LeaveWeightTemp = Attributes.GetLeaveTangentWeight();
|
|
Attributes.SetArriveTangentWeight(LeaveWeightTemp);
|
|
Attributes.SetLeaveTangentWeight(ArriveWeightTemp);
|
|
}
|
|
ERichCurveTangentMode TangentMode = Attributes.GetTangentMode();
|
|
Attributes.SetTangentMode(TangentMode);
|
|
}
|
|
}
|
|
}
|
|
|
|
void FCurveEditor::FlipCurveVertical(TArray<FKeyPosition>& AllKeyPositions, TArray<FKeyAttributes>& AllKeyAttributes, ECurveFlipRangeType RangeType,
|
|
float InRangeMin, float InRangeMax, double CurveMinVal, double CurveMaxVal)
|
|
{
|
|
float RangeMin = FLT_MAX;
|
|
float RangeMax = -FLT_MAX;
|
|
|
|
if (RangeType == ECurveFlipRangeType::CurveRange)
|
|
{
|
|
RangeMin = FMath::Min(CurveMinVal, RangeMin);
|
|
RangeMax = FMath::Max(CurveMaxVal, RangeMax);
|
|
}
|
|
else if (RangeType == ECurveFlipRangeType::KeyRange)
|
|
{
|
|
for (int32 Index = AllKeyPositions.Num() - 1; Index >= 0; --Index)
|
|
{
|
|
FKeyPosition& Position = AllKeyPositions[Index];
|
|
|
|
RangeMin = FMath::Min(Position.OutputValue, RangeMin);
|
|
RangeMax = FMath::Max(Position.OutputValue, RangeMax);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
RangeMin = InRangeMin;
|
|
RangeMax = InRangeMax;
|
|
}
|
|
|
|
// Loop through all keys to adjust positions and tangents
|
|
for (int32 Index = AllKeyPositions.Num() - 1; Index >= 0; --Index)
|
|
{
|
|
FKeyPosition& Position = AllKeyPositions[Index];
|
|
FKeyAttributes& Attributes = AllKeyAttributes[Index];
|
|
|
|
// Mirror y value
|
|
Position.OutputValue = RangeMax - Position.OutputValue + RangeMin;
|
|
|
|
// Mirror tangent
|
|
if (Attributes.HasArriveTangent())
|
|
{
|
|
float ArriveTemp = Attributes.GetArriveTangent();
|
|
Attributes.SetArriveTangent(-ArriveTemp);
|
|
}
|
|
if (Attributes.HasLeaveTangent())
|
|
{
|
|
float LeaveTemp = Attributes.GetLeaveTangent();
|
|
Attributes.SetLeaveTangent(-LeaveTemp);
|
|
}
|
|
if (Attributes.HasTangentMode())
|
|
{
|
|
ERichCurveTangentMode TangentMode = Attributes.GetTangentMode();
|
|
Attributes.SetTangentMode(TangentMode);
|
|
}
|
|
}
|
|
}
|
|
|
|
void FCurveEditor::FlipCurve(ECurveFlipDirection Direction)
|
|
{
|
|
FScopedTransaction Transaction(LOCTEXT("FlipCurve", "Flip Curve"));
|
|
for (FCurveModelID ID : GetEditedCurves())
|
|
{
|
|
if (FCurveModel* Curve = FindCurve(ID))
|
|
{
|
|
// Init key handles
|
|
TArray<FKeyHandle> KeyHandles;
|
|
Curve->GetKeys(TNumericLimits<double>::Lowest(), TNumericLimits<double>::Max(), TNumericLimits<double>::Lowest(), TNumericLimits<double>::Max(), KeyHandles);
|
|
|
|
// Init key positions
|
|
TArray<FKeyPosition> AllKeyPositions;
|
|
AllKeyPositions.SetNum(KeyHandles.Num());
|
|
Curve->GetKeyPositions(KeyHandles, AllKeyPositions);
|
|
|
|
// Init key attributes
|
|
TArray<FKeyAttributes> AllKeyAttributes;
|
|
AllKeyAttributes.SetNum(KeyHandles.Num());
|
|
Curve->GetKeyAttributes(KeyHandles, AllKeyAttributes);
|
|
|
|
// If flipping horizontally
|
|
if (Direction == ECurveFlipDirection::Horizontal)
|
|
{
|
|
double MinTime, MaxTime;
|
|
Curve->GetTimeRange(MinTime, MaxTime);
|
|
FlipCurveHorizontal(AllKeyPositions, AllKeyAttributes, HorizontalCurveFlipRangeSettings.RangeType,
|
|
HorizontalCurveFlipRangeSettings.MinRange, HorizontalCurveFlipRangeSettings.MaxRange, MinTime, MaxTime);
|
|
}
|
|
|
|
// If flipping vertically
|
|
if (Direction == ECurveFlipDirection::Vertical)
|
|
{
|
|
double MinVal, MaxVal;
|
|
Curve->GetValueRange(MinVal, MaxVal);
|
|
FlipCurveVertical(AllKeyPositions, AllKeyAttributes, VerticalCurveFlipRangeSettings.RangeType,
|
|
VerticalCurveFlipRangeSettings.MinRange, VerticalCurveFlipRangeSettings.MaxRange, MinVal, MaxVal);
|
|
}
|
|
|
|
if (AllKeyPositions.Num() > 0)
|
|
{
|
|
Curve->Modify();
|
|
Curve->SetKeyPositions(KeyHandles, AllKeyPositions);
|
|
Curve->SetKeyAttributes(KeyHandles, AllKeyAttributes);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
FCurveEditor::ToggleExpandCollapseNodes(bool bRecursive)
|
|
{
|
|
Tree.ToggleExpansionState(bRecursive);
|
|
}
|
|
|
|
FCurveEditorScreenSpaceH FCurveEditor::GetPanelInputSpace() const
|
|
{
|
|
const float PanelWidth = FMath::Max(1.f, WeakPanel.Pin()->GetViewContainerGeometry().GetLocalSize().X);
|
|
|
|
double InputMin = 0.0, InputMax = 1.0;
|
|
Bounds->GetInputBounds(InputMin, InputMax);
|
|
|
|
InputMax = FMath::Max(InputMax, InputMin + 1e-10);
|
|
return FCurveEditorScreenSpaceH(PanelWidth, InputMin, InputMax);
|
|
}
|
|
|
|
void FCurveEditor::ConstructXGridLines(TArray<float>& MajorGridLines, TArray<float>& MinorGridLines, TArray<FText>* MajorGridLabels) const
|
|
{
|
|
FCurveEditorScreenSpaceH InputSpace = GetPanelInputSpace();
|
|
|
|
double MajorGridStep = 0.0;
|
|
int32 MinorDivisions = 0;
|
|
if (InputSnapRateAttribute.Get().ComputeGridSpacing(InputSpace.PixelsPerInput(), MajorGridStep, MinorDivisions))
|
|
{
|
|
FText GridLineLabelFormatX = GridLineLabelFormatXAttribute.Get();
|
|
const double FirstMajorLine = FMath::FloorToDouble(InputSpace.GetInputMin() / MajorGridStep) * MajorGridStep;
|
|
const double LastMajorLine = FMath::CeilToDouble(InputSpace.GetInputMax() / MajorGridStep) * MajorGridStep;
|
|
|
|
for (double CurrentMajorLine = FirstMajorLine; CurrentMajorLine < LastMajorLine; CurrentMajorLine += MajorGridStep)
|
|
{
|
|
MajorGridLines.Add( (CurrentMajorLine - InputSpace.GetInputMin()) * InputSpace.PixelsPerInput() );
|
|
if (MajorGridLabels)
|
|
{
|
|
MajorGridLabels->Add(FText::Format(GridLineLabelFormatX, FText::AsNumber(CurrentMajorLine)));
|
|
}
|
|
|
|
for (int32 Step = 1; Step < MinorDivisions; ++Step)
|
|
{
|
|
float MinorLine = CurrentMajorLine + Step*MajorGridStep/MinorDivisions;
|
|
MinorGridLines.Add( (MinorLine - InputSpace.GetInputMin()) * InputSpace.PixelsPerInput() );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void FCurveEditor::CutSelection()
|
|
{
|
|
FScopedTransaction Transaction(LOCTEXT("CutKeys", "Cut Keys"));
|
|
|
|
CopySelection();
|
|
DeleteSelection();
|
|
}
|
|
|
|
void FCurveEditor::GetChildCurveModelIDs(const FCurveEditorTreeItemID TreeItemID, TSet<FCurveModelID>& OutCurveModelIDs) const
|
|
{
|
|
const FCurveEditorTreeItem& TreeItem = GetTreeItem(TreeItemID);
|
|
for (const FCurveModelID& CurveModelID : TreeItem.GetCurves())
|
|
{
|
|
OutCurveModelIDs.Add(CurveModelID);
|
|
}
|
|
|
|
for (const FCurveEditorTreeItemID& ChildTreeItem : TreeItem.GetChildren())
|
|
{
|
|
GetChildCurveModelIDs(ChildTreeItem, OutCurveModelIDs);
|
|
}
|
|
}
|
|
|
|
void FCurveEditor::CopySelection() const
|
|
{
|
|
FStringOutputDevice Archive;
|
|
const FExportObjectInnerContext Context;
|
|
|
|
TOptional<double> KeyOffset;
|
|
|
|
UCurveEditorCopyBuffer* CopyableBuffer = NewObject<UCurveEditorCopyBuffer>(GetTransientPackage(), UCurveEditorCopyBuffer::StaticClass(), NAME_None, RF_Transient);
|
|
|
|
if (Selection.Count() > 0)
|
|
{
|
|
for (const TTuple<FCurveModelID, FKeyHandleSet>& Pair : Selection.GetAll())
|
|
{
|
|
if (FCurveModel* Curve = FindCurve(Pair.Key))
|
|
{
|
|
int32 NumKeys = Pair.Value.Num();
|
|
|
|
if (NumKeys > 0)
|
|
{
|
|
UCurveEditorCopyableCurveKeys *CopyableCurveKeys = NewObject<UCurveEditorCopyableCurveKeys>(CopyableBuffer, UCurveEditorCopyableCurveKeys::StaticClass(), NAME_None, RF_Transient);
|
|
|
|
CopyableCurveKeys->ShortDisplayName = Curve->GetShortDisplayName().ToString();
|
|
CopyableCurveKeys->LongDisplayName = Curve->GetLongDisplayName().ToString();
|
|
CopyableCurveKeys->LongIntentionName = Curve->GetLongIntentionName();
|
|
CopyableCurveKeys->IntentionName = Curve->GetIntentionName();
|
|
CopyableCurveKeys->KeyPositions.SetNum(NumKeys, EAllowShrinking::No);
|
|
CopyableCurveKeys->KeyAttributes.SetNum(NumKeys, EAllowShrinking::No);
|
|
|
|
TArrayView<const FKeyHandle> KeyHandles = Pair.Value.AsArray();
|
|
|
|
Curve->GetKeyPositions(KeyHandles, CopyableCurveKeys->KeyPositions);
|
|
// We need to get the attributes as they were specified by the user: so call the version that skips auto-computed values.
|
|
Curve->GetKeyAttributesExcludingAutoComputed(KeyHandles, CopyableCurveKeys->KeyAttributes);
|
|
|
|
for (int KeyIndex = 0; KeyIndex < CopyableCurveKeys->KeyPositions.Num(); ++KeyIndex)
|
|
{
|
|
if (!KeyOffset.IsSet() || CopyableCurveKeys->KeyPositions[KeyIndex].InputValue < KeyOffset.GetValue())
|
|
{
|
|
KeyOffset = CopyableCurveKeys->KeyPositions[KeyIndex].InputValue;
|
|
}
|
|
}
|
|
|
|
CopyableBuffer->Curves.Add(CopyableCurveKeys);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
TSet<FCurveModelID> CurveModelIDs;
|
|
|
|
for (const TTuple<FCurveEditorTreeItemID, ECurveEditorTreeSelectionState>& Pair : GetTreeSelection())
|
|
{
|
|
if (Pair.Value == ECurveEditorTreeSelectionState::Explicit)
|
|
{
|
|
GetChildCurveModelIDs(Pair.Key, CurveModelIDs);
|
|
}
|
|
}
|
|
|
|
for(const FCurveModelID& CurveModelID : CurveModelIDs)
|
|
{
|
|
if (FCurveModel* Curve = FindCurve(CurveModelID))
|
|
{
|
|
TUniquePtr<IBufferedCurveModel> CurveModelCopy = Curve->CreateBufferedCurveCopy();
|
|
if (CurveModelCopy)
|
|
{
|
|
TArray<FKeyPosition> KeyPositions;
|
|
CurveModelCopy->GetKeyPositions(KeyPositions);
|
|
if (KeyPositions.Num() > 0)
|
|
{
|
|
UCurveEditorCopyableCurveKeys *CopyableCurveKeys = NewObject<UCurveEditorCopyableCurveKeys>(CopyableBuffer, UCurveEditorCopyableCurveKeys::StaticClass(), NAME_None, RF_Transient);
|
|
|
|
CopyableCurveKeys->ShortDisplayName = Curve->GetShortDisplayName().ToString();
|
|
CopyableCurveKeys->LongDisplayName = Curve->GetLongDisplayName().ToString();
|
|
CopyableCurveKeys->IntentionName = Curve->GetIntentionName();
|
|
|
|
CopyableCurveKeys->KeyPositions = KeyPositions;
|
|
CurveModelCopy->GetKeyAttributes(CopyableCurveKeys->KeyAttributes);
|
|
|
|
CopyableBuffer->Curves.Add(CopyableCurveKeys);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// When copying entire curve objects we want absolute positions, so reset the detected offset
|
|
KeyOffset.Reset();
|
|
}
|
|
|
|
if (KeyOffset.IsSet())
|
|
{
|
|
for (UCurveEditorCopyableCurveKeys* Curve : CopyableBuffer->Curves)
|
|
{
|
|
for (int Index = 0; Index < Curve->KeyPositions.Num(); ++Index)
|
|
{
|
|
Curve->KeyPositions[Index].InputValue -= KeyOffset.GetValue();
|
|
}
|
|
}
|
|
|
|
CopyableBuffer->TimeOffset = KeyOffset.GetValue();
|
|
}
|
|
else
|
|
{
|
|
CopyableBuffer->bAbsolutePosition = true;
|
|
}
|
|
|
|
|
|
UExporter::ExportToOutputDevice(&Context, CopyableBuffer, nullptr, Archive, TEXT("copy"), 0, PPF_ExportsNotFullyQualified | PPF_Copy | PPF_Delimited, false, CopyableBuffer);
|
|
FPlatformApplicationMisc::ClipboardCopy(*Archive);
|
|
}
|
|
|
|
class FCurveEditorCopyableCurveKeysObjectTextFactory : public FCustomizableTextObjectFactory
|
|
{
|
|
public:
|
|
FCurveEditorCopyableCurveKeysObjectTextFactory()
|
|
: FCustomizableTextObjectFactory(GWarn)
|
|
{
|
|
}
|
|
|
|
// FCustomizableTextObjectFactory implementation
|
|
virtual bool CanCreateClass(UClass* InObjectClass, bool& bOmitSubObjs) const override
|
|
{
|
|
if (InObjectClass->IsChildOf(UCurveEditorCopyBuffer::StaticClass()))
|
|
{
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
virtual void ProcessConstructedObject(UObject* NewObject) override
|
|
{
|
|
check(NewObject);
|
|
|
|
NewCopyBuffers.Add(Cast<UCurveEditorCopyBuffer>(NewObject));
|
|
}
|
|
|
|
public:
|
|
TArray<UCurveEditorCopyBuffer*> NewCopyBuffers;
|
|
};
|
|
|
|
void FCurveEditor::MatchLastTangentToFirst(bool bMatchLastToFirst)
|
|
{
|
|
FScopedTransaction Transaction(LOCTEXT("MatchTangents", "Match Tangents"));
|
|
bool bFoundAnyTangents = false;
|
|
|
|
TArray<FKeyHandle> KeyHandles;
|
|
KeyHandles.SetNum(2);
|
|
TArray<FKeyAttributes> KeyAttributes;
|
|
KeyAttributes.SetNum(2);
|
|
TArray<FKeyHandle> AllKeyHandles;
|
|
|
|
for (const TTuple<FCurveModelID, TUniquePtr<FCurveModel>>& Pair : CurveData)
|
|
{
|
|
FCurveModel* CurveModel = Pair.Value.Get();
|
|
|
|
if (CurveModel)
|
|
{
|
|
// Get all of the key handles from this curve.
|
|
CurveModel->GetKeys(TNumericLimits<double>::Lowest(), TNumericLimits<double>::Max(), TNumericLimits<double>::Lowest(), TNumericLimits<double>::Max(), AllKeyHandles);
|
|
//need 2
|
|
if (AllKeyHandles.Num() < 2)
|
|
{
|
|
continue;
|
|
}
|
|
bFoundAnyTangents = true;
|
|
KeyHandles[0] = AllKeyHandles[0];
|
|
KeyHandles[1] = AllKeyHandles[AllKeyHandles.Num() - 1];
|
|
|
|
CurveModel->GetKeyAttributes(KeyHandles, KeyAttributes);
|
|
if (bMatchLastToFirst)
|
|
{
|
|
KeyAttributes[1] = KeyAttributes[0];
|
|
}
|
|
else
|
|
{
|
|
KeyAttributes[0] = KeyAttributes[1];
|
|
}
|
|
CurveModel->SetKeyAttributes(KeyHandles, KeyAttributes);
|
|
}
|
|
}
|
|
if (!bFoundAnyTangents)
|
|
{
|
|
Transaction.Cancel();
|
|
}
|
|
}
|
|
|
|
bool FCurveEditor::CanPaste(const FString& TextToImport) const
|
|
{
|
|
FCurveEditorCopyableCurveKeysObjectTextFactory CopyableCurveKeysFactory;
|
|
if (CopyableCurveKeysFactory.CanCreateObjectsFromText(TextToImport))
|
|
{
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void FCurveEditor::ImportCopyBufferFromText(const FString& TextToImport, /*out*/ TArray<UCurveEditorCopyBuffer*>& ImportedCopyBuffers) const
|
|
{
|
|
UPackage* TempPackage = NewObject<UPackage>(nullptr, TEXT("/Engine/Editor/CurveEditor/Transient"), RF_Transient);
|
|
TempPackage->AddToRoot();
|
|
|
|
// Turn the text buffer into objects
|
|
FCurveEditorCopyableCurveKeysObjectTextFactory Factory;
|
|
Factory.ProcessBuffer(TempPackage, RF_Transactional, TextToImport);
|
|
|
|
ImportedCopyBuffers = Factory.NewCopyBuffers;
|
|
|
|
// Remove the temp package from the root now that it has served its purpose
|
|
TempPackage->RemoveFromRoot();
|
|
}
|
|
|
|
TSet<FCurveModelID> FCurveEditor::GetTargetCurvesForPaste() const
|
|
{
|
|
TSet<FCurveModelID> TargetCurves;
|
|
|
|
TArray<FCurveEditorTreeItemID> NodesToSearch;
|
|
|
|
// Try nodes with selected keys
|
|
for (const TTuple<FCurveModelID, FKeyHandleSet>& Pair : Selection.GetAll())
|
|
{
|
|
TargetCurves.Add(Pair.Key);
|
|
}
|
|
|
|
// Try selected nodes
|
|
if (TargetCurves.Num() == 0)
|
|
{
|
|
for (const TTuple<FCurveEditorTreeItemID, ECurveEditorTreeSelectionState>& Pair : GetTreeSelection())
|
|
{
|
|
NodesToSearch.Add(Pair.Key);
|
|
}
|
|
}
|
|
|
|
for (const FCurveEditorTreeItemID& TreeItemID : NodesToSearch)
|
|
{
|
|
const FCurveEditorTreeItem& TreeItem = GetTreeItem(TreeItemID);
|
|
for (const FCurveModelID& CurveModelID : TreeItem.GetCurves())
|
|
{
|
|
TargetCurves.Add(CurveModelID);
|
|
}
|
|
}
|
|
|
|
return TargetCurves;
|
|
}
|
|
|
|
bool FCurveEditor::CopyBufferCurveToCurveID(const UCurveEditorCopyableCurveKeys* InSourceCurve, const FCurveModelID InTargetCurve, TOptional<double> InTimeOffset, const bool bInAddToSelection, const bool bInOverwriteRange)
|
|
{
|
|
using namespace UE::CurveEditor;
|
|
return CopyBufferCurveToCurveID(InSourceCurve, InTargetCurve, InTimeOffset,
|
|
bInOverwriteRange ? ECurveEditorPasteMode::OverwriteRange : ECurveEditorPasteMode::Merge,
|
|
bInAddToSelection ? ECurveEditorPasteFlags::Default | ECurveEditorPasteFlags::SetSelection : ECurveEditorPasteFlags::Default
|
|
);
|
|
}
|
|
|
|
namespace UE::CurveEditor::PasteDetail
|
|
{
|
|
static void RemovePastedKeysInRange(
|
|
const UCurveEditorCopyableCurveKeys* InSourceCurve, FCurveModel* InTargetCurveModel, const TOptional<double>& InTimeOffset,
|
|
double InCurrentTime
|
|
)
|
|
{
|
|
TArray<FKeyHandle> KeysToRemove;
|
|
double MinKeyTime = TNumericLimits<double>::Max();
|
|
double MaxKeyTime = TNumericLimits<double>::Lowest();
|
|
for (int32 Index = 0; Index < InSourceCurve->KeyPositions.Num(); ++Index)
|
|
{
|
|
FKeyPosition KeyPosition = InSourceCurve->KeyPositions[Index];
|
|
if (InTimeOffset.IsSet())
|
|
{
|
|
KeyPosition.InputValue += InTimeOffset.GetValue();
|
|
}
|
|
if (KeyPosition.InputValue < MinKeyTime)
|
|
{
|
|
MinKeyTime = KeyPosition.InputValue;
|
|
}
|
|
if (KeyPosition.InputValue > MaxKeyTime)
|
|
{
|
|
MaxKeyTime = KeyPosition.InputValue;
|
|
}
|
|
}
|
|
|
|
// Just double checking we actually set a Min/Max time so we don't wipe out every key to infinity.
|
|
if (InSourceCurve->KeyPositions.Num() > 0)
|
|
{
|
|
InTargetCurveModel->GetKeys(MinKeyTime, MaxKeyTime, TNumericLimits<double>::Lowest(), TNumericLimits<double>::Max(), KeysToRemove);
|
|
}
|
|
|
|
InTargetCurveModel->RemoveKeys(KeysToRemove, InCurrentTime);
|
|
}
|
|
/** @return Height to add to each pasted key to make the range relative to the closest key to the left of the pasted range. */
|
|
static double FindRelativeKeyPasteInset(const UCurveEditorCopyableCurveKeys* InSourceCurve, FCurveModel* InTargetCurveModel, double InCurrentTime)
|
|
{
|
|
const FKeyPosition* MinElement = Algo::MinElementBy(InSourceCurve->KeyPositions, [](const FKeyPosition& Position)
|
|
{
|
|
return Position.InputValue;
|
|
});
|
|
if (!MinElement)
|
|
{
|
|
// Nothing to paste
|
|
return 0.0;
|
|
}
|
|
|
|
TOptional<FKeyHandle> ClosestPrevious, ClosestNext;
|
|
InTargetCurveModel->GetClosestKeysTo(InCurrentTime, ClosestPrevious, ClosestNext);
|
|
if (!ClosestPrevious)
|
|
{
|
|
// No previous key -> no inset to apply
|
|
return 0.0;
|
|
}
|
|
|
|
FKeyPosition Position{};
|
|
InTargetCurveModel->GetKeyPositions(TConstArrayView<FKeyHandle>(&ClosestPrevious.GetValue(), 1), TArrayView<FKeyPosition>(&Position, 1));
|
|
return Position.OutputValue - MinElement->OutputValue; // Bring all values down to the closest value to the left.
|
|
}
|
|
}
|
|
|
|
bool FCurveEditor::CopyBufferCurveToCurveID(
|
|
const UCurveEditorCopyableCurveKeys* InSourceCurve, const FCurveModelID InTargetCurve,
|
|
TOptional<double> InTimeOffset, UE::CurveEditor::ECurveEditorPasteMode InMode, UE::CurveEditor::ECurveEditorPasteFlags InFlags
|
|
)
|
|
{
|
|
using namespace UE::CurveEditor;
|
|
|
|
FCurveModel* TargetCurveModel = FindCurve(InTargetCurve);
|
|
if (!InSourceCurve || !TargetCurveModel)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
double CurrentTime = 0.0;
|
|
if (TSharedPtr<ITimeSliderController> TimeSliderController = WeakTimeSliderController.Pin())
|
|
{
|
|
FFrameRate TickResolution = TimeSliderController->GetTickResolution();
|
|
CurrentTime = TickResolution.AsSeconds(TimeSliderController->GetScrubPosition());
|
|
}
|
|
|
|
// Sometimes when you paste you want to delete any keys that already exist in the timerange you'll be replacing
|
|
// because mixing the pasted results with the original results wouldn't make any sense.
|
|
if (InMode == ECurveEditorPasteMode::OverwriteRange)
|
|
{
|
|
PasteDetail::RemovePastedKeysInRange(InSourceCurve, TargetCurveModel, InTimeOffset, CurrentTime);
|
|
}
|
|
|
|
// If pasting relative, bring all pasted values down to the first key to the left of the scrubber.
|
|
const double ValueInset = EnumHasAnyFlags(InFlags, ECurveEditorPasteFlags::Relative)
|
|
? PasteDetail::FindRelativeKeyPasteInset(InSourceCurve, TargetCurveModel, CurrentTime) : 0.0;
|
|
|
|
for (int32 Index = 0; Index < InSourceCurve->KeyPositions.Num(); ++Index)
|
|
{
|
|
FKeyPosition KeyPosition = InSourceCurve->KeyPositions[Index];
|
|
if (InTimeOffset.IsSet())
|
|
{
|
|
KeyPosition.InputValue += InTimeOffset.GetValue();
|
|
}
|
|
|
|
KeyPosition.OutputValue += ValueInset;
|
|
|
|
TOptional<FKeyHandle> KeyHandle = TargetCurveModel->AddKey(KeyPosition, InSourceCurve->KeyAttributes[Index]);
|
|
if (KeyHandle.IsSet() && EnumHasAnyFlags(InFlags, ECurveEditorPasteFlags::SetSelection))
|
|
{
|
|
Selection.Add(FCurvePointHandle(InTargetCurve, ECurvePointType::Key, KeyHandle.GetValue()));
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void FCurveEditor::PasteKeys(TSet<FCurveModelID> CurveModelIDs, const bool bInOverwriteRange)
|
|
{
|
|
using namespace UE::CurveEditor;
|
|
|
|
FKeyPasteArgs Args;
|
|
Args.CurveModelIds = CurveModelIDs;
|
|
Args.Mode = bInOverwriteRange ? ECurveEditorPasteMode::OverwriteRange : ECurveEditorPasteMode::Merge;
|
|
PasteKeys(Args);
|
|
}
|
|
|
|
void FCurveEditor::PasteKeys(UE::CurveEditor::FKeyPasteArgs InArgs)
|
|
{
|
|
// Grab the text to paste from the clipboard
|
|
FString TextToImport;
|
|
FPlatformApplicationMisc::ClipboardPaste(TextToImport);
|
|
|
|
TArray<UCurveEditorCopyBuffer*> ImportedCopyBuffers;
|
|
ImportCopyBufferFromText(TextToImport, ImportedCopyBuffers);
|
|
|
|
if (ImportedCopyBuffers.Num() == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// There are numerous scenarios that Copy/Paste needs to handle.
|
|
// 1:1 - Copying a single curve to another single curve should always work.
|
|
// 1:Multiple - Copying a single curve with multiple target curves should always work, the value will just be written into each one.
|
|
// Multiple (Related): Multiple (Related)
|
|
// - Copying multiple curves between related controls, ie: fk_foot_l and fk_foot_r from one rig to another.
|
|
// - If their long intent name matches, we consider them to be related controls. If their intent name doesn't match
|
|
// - then we consider them unrelated controls.
|
|
// Multiple (Unrelated):Multiple (Unrelated)
|
|
// - If the long name doesn't match then we fall back to just the intent name. We want to handle copying both from one
|
|
// - group of controls to multiple groups of controls, matching each by short intent name. This lets you copy fk_foot_l
|
|
// - onto fk_foot_r and fk_spine_1 at the same time. We also handle trying to copy from multiple groups of controls
|
|
// - onto multiple groups of controls - this falls back to a index-in-array order based copy and tries to ensure that
|
|
// - the intent for each one (ie: transform.x) copies onto the first target transform.x, and then the next source that
|
|
// - has a transform.x intent gets copied onto the *second* target transform.x.
|
|
// Multiple (Unrelated):1
|
|
// - This one is mostly an unhandled case and the last source intent will win on the target group, so fk_foot_l and fk_foot_r
|
|
// - pasted onto fk_spine_1, fk_spine_1 will just get the intents from fk_foot_r and fk_foot_l is ignored. This order isn't
|
|
// - guranteed though because it's using the order the curves are in the internal arrays.
|
|
|
|
// There should only be one copy buffer, but the way the import works returns an array.
|
|
ensureMsgf(ImportedCopyBuffers.Num() == 1, TEXT("Multiple copy buffers pasted at one time, only the first one will be used!"));
|
|
UCurveEditorCopyBuffer* SourceBuffer = ImportedCopyBuffers[0];
|
|
|
|
// Figure out which CurveModelIDs we're trying to paste to. If they're not already specified, we try to find hovered curves,
|
|
// and failing that we try to find all curves.
|
|
TSet<FCurveModelID> TargetCurves = InArgs.CurveModelIds.Num() > 0 ? InArgs.CurveModelIds : GetTargetCurvesForPaste();
|
|
|
|
if (TargetCurves.Num() == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// When we're pasting keys, we want the first key to paste where the timeslider is
|
|
TOptional<double> TimeOffset;
|
|
bool bApplyOffset = !SourceBuffer->bAbsolutePosition;
|
|
|
|
if (bApplyOffset)
|
|
{
|
|
TSharedPtr<ITimeSliderController> TimeSliderController = WeakTimeSliderController.Pin();
|
|
if (TimeSliderController.IsValid())
|
|
{
|
|
FFrameRate TickResolution = TimeSliderController->GetTickResolution();
|
|
|
|
TimeOffset = TimeSliderController->GetScrubPosition() / TickResolution;
|
|
}
|
|
else
|
|
{
|
|
TimeOffset = SourceBuffer->TimeOffset;
|
|
}
|
|
}
|
|
|
|
const UE::CurveEditor::FScopedSelectionTransaction KeyChange(SharedThis(this), LOCTEXT("PasteKeys", "Paste Keys"));
|
|
Selection.Clear();
|
|
|
|
|
|
// Two simple cases, 1 to 1 and 1 to many.
|
|
TArray<TPair<UCurveEditorCopyableCurveKeys*, FCurveModelID>> CopyPairs;
|
|
|
|
if (SourceBuffer->Curves.Num() == 1)
|
|
{
|
|
for (FCurveModelID TargetCurveID : TargetCurves)
|
|
{
|
|
CopyPairs.Add(TPair<UCurveEditorCopyableCurveKeys*, FCurveModelID>(SourceBuffer->Curves[0], TargetCurveID));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// The more complicated is the Multiple:Multiple / Multiple:1 (which is really just the same). We want to
|
|
// prioritize matching up longer names if possible - this allows us to copy multiple controls to multiple
|
|
// controls, such as starting with fk_foot_l and fk_foot_r and pasting to fk_foot_l, fk_foot_r, fk_neck_01.
|
|
// We will match up the transform/scale/rotation for the fk_foot_l/fk_foot_r and don't touch fk_neck_01 in this
|
|
// example. If no matches are made, then we fall back to the shorter intent string - where we just copy
|
|
// transform.xyz to transform.xyz even though the source may be fk_foot_l and the target is fk_foot_r.
|
|
|
|
// If any of the long names match (ie: fk_foot_l.transform.x) then we'll use long name matching for all.
|
|
bool bUseLongNameForMatches = false;
|
|
for (const UCurveEditorCopyableCurveKeys* SourceCurveKeys : SourceBuffer->Curves)
|
|
{
|
|
for (const FCurveModelID& TargetCurveID : TargetCurves)
|
|
{
|
|
FCurveModel* TargetCurve = FindCurve(TargetCurveID);
|
|
if (TargetCurve)
|
|
{
|
|
if (SourceCurveKeys->LongIntentionName == TargetCurve->GetLongIntentionName())
|
|
{
|
|
bUseLongNameForMatches = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Exit out of the outer loop too if we've got a match.
|
|
if (bUseLongNameForMatches)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Multiple to Multiple curve copying can get complicated when we only have the short intent name to deal with it, so
|
|
// this creates an edge case where you're copying one set of intents (ie: transform.x, transform.y, transform.z) onto
|
|
// multiple objects with those intents... we want to support this, but we don't support copying from multiple objects
|
|
// onto multiple objects unless their LongIntentionName matches as it gets too confusing to match up.
|
|
bool bOnlyOneSetOfSourceIntentions = true;
|
|
{
|
|
TMap<FString, int32> IntentionUseCounts;
|
|
for (UCurveEditorCopyableCurveKeys* SourceCurveKeys : SourceBuffer->Curves)
|
|
{
|
|
IntentionUseCounts.FindOrAdd(SourceCurveKeys->IntentionName)++;
|
|
}
|
|
|
|
for (TPair<FString, int32>& Pair : IntentionUseCounts)
|
|
{
|
|
if (Pair.Value > 1)
|
|
{
|
|
bOnlyOneSetOfSourceIntentions = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
TSet<FCurveModelID> CurvesToMatchTo = TargetCurves;
|
|
for (UCurveEditorCopyableCurveKeys* SourceCurveKeys : SourceBuffer->Curves)
|
|
{
|
|
TArray<FCurveModelID> CurvesToRemove;
|
|
for (const FCurveModelID& TargetCurveID : CurvesToMatchTo)
|
|
{
|
|
FCurveModel* TargetCurve = FindCurve(TargetCurveID);
|
|
if (TargetCurve)
|
|
{
|
|
const bool bNameMatches = bUseLongNameForMatches ?
|
|
SourceCurveKeys->LongIntentionName == TargetCurve->GetLongIntentionName() :
|
|
SourceCurveKeys->IntentionName == TargetCurve->GetIntentionName();
|
|
|
|
if (bNameMatches)
|
|
{
|
|
CopyPairs.Add(TPair<UCurveEditorCopyableCurveKeys*, FCurveModelID>(SourceCurveKeys, TargetCurveID));
|
|
|
|
// Don't try to match to this curve again. This lets us try to handle the case where we have
|
|
// multiple source objects (fk_foot_l, fk_foot_r) trying to copy to unrelated objects (cube1, cube2).
|
|
// They will fail the LongDisplayName check but get the IntentionName check, but we need to remove
|
|
// cube1 after the first time we match it so that fk_foot_r has a chance to paste into cube2 instead of cube1.
|
|
CurvesToRemove.Add(TargetCurveID);
|
|
|
|
// If we're copying from one object with multiple curves (ie: fk_foot_l) but we have multiple destination
|
|
// objects, we loop through all of the target curves and apply them using the IntentionName matches check.
|
|
// This only happens when using short intention names (as it's the more vague logic case), and we only
|
|
// do this when you have multiple source curves, but only one of each kind. If you have multiple source
|
|
// curves with multiple copies of the same intention, then we only apply it once to the first curve
|
|
// who's intention matches and then remove it from the pool so that the next source with the same
|
|
// intention (such as the second foot in the above example) gets a chance to write to the second
|
|
// target curve with the same destination.
|
|
bool bCopyToMultipleDestCurves = bOnlyOneSetOfSourceIntentions && !bUseLongNameForMatches;
|
|
if (!bCopyToMultipleDestCurves)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for (FCurveModelID Curve : CurvesToRemove)
|
|
{
|
|
CurvesToMatchTo.Remove(Curve);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Now that we've calculated the source curve for each destination curve, copy them over.
|
|
for (const TPair<UCurveEditorCopyableCurveKeys*, FCurveModelID>& Pair : CopyPairs)
|
|
{
|
|
CopyBufferCurveToCurveID(Pair.Key, Pair.Value, TimeOffset, InArgs.Mode, InArgs.Flags);
|
|
}
|
|
|
|
if (ShouldAutoFrame())
|
|
{
|
|
ZoomToFitSelection();
|
|
}
|
|
}
|
|
|
|
void FCurveEditor::DeleteSelection()
|
|
{
|
|
const UE::CurveEditor::FScopedSelectionTransaction Transaction(SharedThis(this), LOCTEXT("DeleteKeys", "Delete Keys"));
|
|
|
|
double CurrentTime = 0.0;
|
|
if (TSharedPtr<ITimeSliderController> TimeSliderController = WeakTimeSliderController.Pin())
|
|
{
|
|
FFrameRate TickResolution = TimeSliderController->GetTickResolution();
|
|
CurrentTime = TickResolution.AsSeconds(TimeSliderController->GetScrubPosition());
|
|
}
|
|
|
|
for (const TTuple<FCurveModelID, FKeyHandleSet>& Pair : Selection.GetAll())
|
|
{
|
|
if (FCurveModel* Curve = FindCurve(Pair.Key))
|
|
{
|
|
Curve->Modify();
|
|
Curve->RemoveKeys(Pair.Value.AsArray(), CurrentTime);
|
|
}
|
|
}
|
|
|
|
Selection.Clear();
|
|
}
|
|
|
|
void FCurveEditor::FlattenSelection()
|
|
{
|
|
FScopedTransaction Transaction(LOCTEXT("FlattenTangents", "Flatten Tangents"));
|
|
bool bFoundAnyTangents = false;
|
|
|
|
TArray<FKeyHandle> KeyHandles;
|
|
TArray<FKeyAttributes> AllKeyPositions;
|
|
//Since we don't have access here to the Section to get Tick Resolution if we flatten a weighted tangent we
|
|
//do so by converting it to non-weighted and then back again.
|
|
TArray<FKeyHandle> KeyHandlesWeighted;
|
|
TArray<FKeyAttributes> KeyAttributesWeighted;
|
|
for (const TTuple<FCurveModelID, FKeyHandleSet>& Pair : Selection.GetAll())
|
|
{
|
|
if (FCurveModel* Curve = FindCurve(Pair.Key))
|
|
{
|
|
KeyHandles.Reset(Pair.Value.Num());
|
|
KeyHandles.Append(Pair.Value.AsArray().GetData(), Pair.Value.Num());
|
|
|
|
AllKeyPositions.SetNum(KeyHandles.Num());
|
|
Curve->GetKeyAttributes(KeyHandles, AllKeyPositions);
|
|
|
|
KeyHandlesWeighted.Reset(Pair.Value.Num());
|
|
KeyHandlesWeighted.Append(Pair.Value.AsArray().GetData(), Pair.Value.Num());
|
|
|
|
KeyAttributesWeighted.SetNum(KeyHandlesWeighted.Num());
|
|
Curve->GetKeyAttributes(KeyHandlesWeighted, KeyAttributesWeighted);
|
|
|
|
|
|
// Straighten tangents, ignoring any keys that we can't set tangents on
|
|
for (int32 Index = AllKeyPositions.Num()-1 ; Index >= 0; --Index)
|
|
{
|
|
FKeyAttributes& Attributes = AllKeyPositions[Index];
|
|
if (Attributes.HasTangentMode() && (Attributes.HasArriveTangent() || Attributes.HasLeaveTangent()))
|
|
{
|
|
Attributes.SetArriveTangent(0.f).SetLeaveTangent(0.f);
|
|
if (Attributes.GetTangentMode() == RCTM_Auto || Attributes.GetTangentMode() == RCTM_SmartAuto)
|
|
{
|
|
Attributes.SetTangentMode(RCTM_User);
|
|
}
|
|
//if any weighted convert and convert back to both (which is what only support other modes are not really used).,
|
|
if (Attributes.GetTangentWeightMode() == RCTWM_WeightedBoth || Attributes.GetTangentWeightMode() == RCTWM_WeightedArrive
|
|
|| Attributes.GetTangentWeightMode() == RCTWM_WeightedLeave)
|
|
{
|
|
Attributes.SetTangentWeightMode(RCTWM_WeightedNone);
|
|
FKeyAttributes& WeightedAttributes = KeyAttributesWeighted[Index];
|
|
WeightedAttributes.UnsetArriveTangent();
|
|
WeightedAttributes.UnsetLeaveTangent();
|
|
WeightedAttributes.UnsetArriveTangentWeight();
|
|
WeightedAttributes.UnsetLeaveTangentWeight();
|
|
WeightedAttributes.SetTangentWeightMode(RCTWM_WeightedBoth);
|
|
|
|
}
|
|
else
|
|
{
|
|
KeyAttributesWeighted.RemoveAtSwap(Index, EAllowShrinking::No);
|
|
KeyHandlesWeighted.RemoveAtSwap(Index, EAllowShrinking::No);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
AllKeyPositions.RemoveAtSwap(Index, EAllowShrinking::No);
|
|
KeyHandles.RemoveAtSwap(Index, EAllowShrinking::No);
|
|
KeyAttributesWeighted.RemoveAtSwap(Index, EAllowShrinking::No);
|
|
KeyHandlesWeighted.RemoveAtSwap(Index, EAllowShrinking::No);
|
|
}
|
|
}
|
|
|
|
if (AllKeyPositions.Num() > 0)
|
|
{
|
|
Curve->Modify();
|
|
Curve->SetKeyAttributes(KeyHandles, AllKeyPositions);
|
|
if (KeyAttributesWeighted.Num() > 0)
|
|
{
|
|
Curve->SetKeyAttributes(KeyHandlesWeighted, KeyAttributesWeighted);
|
|
}
|
|
bFoundAnyTangents = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!bFoundAnyTangents)
|
|
{
|
|
Transaction.Cancel();
|
|
}
|
|
}
|
|
|
|
void FCurveEditor::StraightenSelection()
|
|
{
|
|
FScopedTransaction Transaction(LOCTEXT("StraightenTangents", "Straighten Tangents"));
|
|
bool bFoundAnyTangents = false;
|
|
|
|
TArray<FKeyHandle> KeyHandles;
|
|
TArray<FKeyAttributes> AllKeyPositions;
|
|
|
|
for (const TTuple<FCurveModelID, FKeyHandleSet>& Pair : Selection.GetAll())
|
|
{
|
|
if (FCurveModel* Curve = FindCurve(Pair.Key))
|
|
{
|
|
KeyHandles.Reset(Pair.Value.Num());
|
|
KeyHandles.Append(Pair.Value.AsArray().GetData(), Pair.Value.Num());
|
|
|
|
AllKeyPositions.SetNum(KeyHandles.Num());
|
|
Curve->GetKeyAttributes(KeyHandles, AllKeyPositions);
|
|
|
|
// Straighten tangents, ignoring any keys that we can't set tangents on
|
|
for (int32 Index = AllKeyPositions.Num()-1 ; Index >= 0; --Index)
|
|
{
|
|
FKeyAttributes& Attributes = AllKeyPositions[Index];
|
|
if (Attributes.HasTangentMode() && Attributes.HasArriveTangent() && Attributes.HasLeaveTangent())
|
|
{
|
|
float NewTangent = (Attributes.GetLeaveTangent() + Attributes.GetArriveTangent()) * 0.5f;
|
|
Attributes.SetArriveTangent(NewTangent).SetLeaveTangent(NewTangent);
|
|
if (Attributes.GetTangentMode() == RCTM_Auto || Attributes.GetTangentMode() == RCTM_SmartAuto)
|
|
{
|
|
Attributes.SetTangentMode(RCTM_User);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
AllKeyPositions.RemoveAtSwap(Index, EAllowShrinking::No);
|
|
KeyHandles.RemoveAtSwap(Index, EAllowShrinking::No);
|
|
}
|
|
}
|
|
|
|
if (AllKeyPositions.Num() > 0)
|
|
{
|
|
Curve->Modify();
|
|
Curve->SetKeyAttributes(KeyHandles, AllKeyPositions);
|
|
bFoundAnyTangents = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!bFoundAnyTangents)
|
|
{
|
|
Transaction.Cancel();
|
|
}
|
|
}
|
|
|
|
bool FCurveEditor::CanFlattenOrStraightenSelection() const
|
|
{
|
|
return Selection.Count() > 0;
|
|
}
|
|
|
|
void FCurveEditor::SmartSnapSelection()
|
|
{
|
|
const UE::CurveEditor::FScopedSelectionTransaction Transaction(SharedThis(this), LOCTEXT("SmartSnapKeys", "Smart Snap"));
|
|
|
|
TMap<FCurveModelID, FKeyHandleSet> OutKeysToSelect;
|
|
UE::CurveEditor::EnumerateSmartSnappableKeys(*this, Selection.GetAll(), OutKeysToSelect,
|
|
[](const FCurveModelID&, FCurveModel& CurveModel, const UE::CurveEditor::FSmartSnapResult& SnapResult)
|
|
{
|
|
CurveModel.Modify();
|
|
ApplySmartSnap(CurveModel, SnapResult);
|
|
});
|
|
|
|
// Some keys might have been removed. Clean up the selection.
|
|
Selection.Clear();
|
|
for (const TTuple<FCurveModelID, FKeyHandleSet>& OutSet : OutKeysToSelect)
|
|
{
|
|
Selection.Add(OutSet.Key, ECurvePointType::Key, OutSet.Value.AsArray());
|
|
}
|
|
}
|
|
|
|
bool FCurveEditor::CanSmartSnapSelection() const
|
|
{
|
|
return UE::CurveEditor::CanSmartSnapSelection(Selection);
|
|
}
|
|
|
|
void FCurveEditor::UpdateGeometry(const FGeometry& CurrentGeometry)
|
|
{
|
|
}
|
|
|
|
void FCurveEditor::SetRandomCurveColorsForSelected()
|
|
{
|
|
TSet<FCurveModelID> CurveModelIDs = GetSelectionFromTreeAndKeys();
|
|
if (CurveModelIDs.Num() == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
for (const FCurveModelID& CurveModelID : CurveModelIDs)
|
|
{
|
|
if (FCurveModel* Curve = FindCurve(CurveModelID))
|
|
{
|
|
UObject* Object = nullptr;
|
|
FString Name;
|
|
Curve->GetCurveColorObjectAndName(&Object, Name);
|
|
if (Object)
|
|
{
|
|
FLinearColor Color = UCurveEditorSettings::GetNextRandomColor();
|
|
Settings->SetCustomColor(Object->GetClass(), Name, Color);
|
|
Curve->SetColor(Color);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void FCurveEditor::SetCurveColorsForSelected()
|
|
{
|
|
TSet<FCurveModelID> CurveModelIDs = GetSelectionFromTreeAndKeys();
|
|
if (CurveModelIDs.Num() == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
TWeakPtr<FCurveEditor> WeakSelf = AsShared();
|
|
|
|
FColorPickerArgs PickerArgs;
|
|
PickerArgs.bUseAlpha = false;
|
|
PickerArgs.InitialColor = FindCurve(*CurveModelIDs.CreateIterator())->GetColor();
|
|
PickerArgs.OnColorCommitted.BindLambda([WeakSelf, CurveModelIDs](FLinearColor NewColor)
|
|
{
|
|
if (TSharedPtr<FCurveEditor> Self = WeakSelf.Pin())
|
|
{
|
|
for (const FCurveModelID& CurveModelID : CurveModelIDs)
|
|
{
|
|
if (FCurveModel* Curve = Self->FindCurve(CurveModelID))
|
|
{
|
|
UObject* Object = nullptr;
|
|
FString Name;
|
|
Curve->GetCurveColorObjectAndName(&Object, Name);
|
|
if (Object)
|
|
{
|
|
Self->Settings->SetCustomColor(Object->GetClass(), Name, NewColor);
|
|
Curve->SetColor(NewColor);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
OpenColorPicker(PickerArgs);
|
|
}
|
|
|
|
bool FCurveEditor::IsToolActive(const FCurveEditorToolID InToolID) const
|
|
{
|
|
if (ActiveTool.IsSet())
|
|
{
|
|
return ActiveTool == InToolID;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void FCurveEditor::MakeToolActive(const FCurveEditorToolID InToolID)
|
|
{
|
|
if (ActiveTool.IsSet())
|
|
{
|
|
// Early out in the event that they're trying to switch to the same tool. This avoids
|
|
// unwanted activation/deactivation calls.
|
|
if (ActiveTool == InToolID)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Deactivate the current tool before we activate the new one.
|
|
ToolExtensions[ActiveTool.GetValue()]->OnToolDeactivated();
|
|
}
|
|
|
|
ActiveTool.Reset();
|
|
|
|
// Notify anyone listening that we've switched tools (possibly to an inactive one)
|
|
OnActiveToolChangedDelegate.Broadcast(InToolID);
|
|
|
|
if (InToolID != FCurveEditorToolID::Unset())
|
|
{
|
|
ActiveTool = InToolID;
|
|
ToolExtensions[ActiveTool.GetValue()]->OnToolActivated();
|
|
}
|
|
}
|
|
|
|
ICurveEditorToolExtension* FCurveEditor::GetCurrentTool() const
|
|
{
|
|
if (ActiveTool.IsSet())
|
|
{
|
|
return ToolExtensions[ActiveTool.GetValue()].Get();
|
|
}
|
|
|
|
// If there is no active tool we return nullptr.
|
|
return nullptr;
|
|
}
|
|
|
|
TSet<FCurveModelID> FCurveEditor::GetEditedCurves() const
|
|
{
|
|
TArray<FCurveModelID> AllCurves;
|
|
GetCurves().GenerateKeyArray(AllCurves);
|
|
return TSet<FCurveModelID>(AllCurves);
|
|
}
|
|
|
|
void FCurveEditor::AddBufferedCurves(const TSet<FCurveModelID>& InCurves)
|
|
{
|
|
// We make a copy of the curve data and store it.
|
|
for (FCurveModelID CurveID : InCurves)
|
|
{
|
|
FCurveModel* CurveModel = FindCurve(CurveID);
|
|
check(CurveModel);
|
|
|
|
// Add a buffered curve copy if the curve model supports buffered curves
|
|
TUniquePtr<IBufferedCurveModel> CurveModelCopy = CurveModel->CreateBufferedCurveCopy();
|
|
if (CurveModelCopy)
|
|
{
|
|
// Remove any existing buffered curves
|
|
for (int32 BufferedCurveIndex = 0; BufferedCurveIndex < BufferedCurves.Num(); )
|
|
{
|
|
if (BufferedCurves[BufferedCurveIndex]->GetLongDisplayName() == CurveModel->GetLongDisplayName().ToString())
|
|
{
|
|
BufferedCurves.RemoveAt(BufferedCurveIndex);
|
|
}
|
|
else
|
|
{
|
|
++BufferedCurveIndex;
|
|
}
|
|
}
|
|
|
|
BufferedCurves.Add(MoveTemp(CurveModelCopy));
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogCurveEditor, Warning, TEXT("Failed to buffer curve, curve model did not provide a copy."))
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void FCurveEditor::ApplyBufferedCurveToTarget(const IBufferedCurveModel* BufferedCurve, FCurveModel* TargetCurve)
|
|
{
|
|
check(TargetCurve);
|
|
check(BufferedCurve);
|
|
|
|
TArray<FKeyPosition> KeyPositions;
|
|
TArray<FKeyAttributes> KeyAttributes;
|
|
BufferedCurve->GetKeyPositions(KeyPositions);
|
|
BufferedCurve->GetKeyAttributes(KeyAttributes);
|
|
|
|
|
|
// Copy the data from the Buffered curve into the target curve. This just does wholesale replacement.
|
|
TArray<FKeyHandle> TargetKeyHandles;
|
|
TargetCurve->GetKeys(TNumericLimits<double>::Lowest(), TNumericLimits<double>::Max(), TNumericLimits<double>::Lowest(), TNumericLimits<double>::Max(), TargetKeyHandles);
|
|
|
|
double CurrentTime = 0.0;
|
|
if (TSharedPtr<ITimeSliderController> TimeSliderController = WeakTimeSliderController.Pin())
|
|
{
|
|
FFrameRate TickResolution = TimeSliderController->GetTickResolution();
|
|
CurrentTime = TickResolution.AsSeconds(TimeSliderController->GetScrubPosition());
|
|
}
|
|
// Clear our current keys from the target curve
|
|
TargetCurve->RemoveKeys(TargetKeyHandles, CurrentTime);
|
|
|
|
// Now put our buffered keys into the target curve
|
|
TargetCurve->AddKeys(KeyPositions, KeyAttributes);
|
|
}
|
|
|
|
bool FCurveEditor::ApplyBufferedCurves(const TSet<FCurveModelID>& InCurvesToApplyTo, const bool bSwapBufferCurves)
|
|
{
|
|
FScopedTransaction Transaction(bSwapBufferCurves ? LOCTEXT("SwapBufferedCurves", "Swap Buffered Curves") : LOCTEXT("ApplyBufferedCurves", "Apply Buffered Curves"));
|
|
|
|
// Each curve can specify an "Intention" name. This gives a little bit of context about how the curve is intended to be used,
|
|
// without locking anyone into a specific set of intentions. When you go to apply the buffered curves, for each curve that you
|
|
// want to apply it to, we can look in our stored curves to see if someone has the same intention. If there isn't a matching intention
|
|
// then we skip and consider a fallback method (such as 1:1 copy). There is a lot of guessing still involved as there are complex
|
|
// situations that users may try to use it in (such as buffering two sets of transform curves and applying it to two destination transform curves)
|
|
// or trying to copy something with a name like "Focal Length" and pasting it onto a different track. We don't handle these cases for now,
|
|
// but attempt to communicate it to the user via toast notification when pasting fails for whatever reason.
|
|
int32 NumCurvesMatchedByIntent = 0;
|
|
int32 NumCurvesNoMatchedIntent = 0;
|
|
bool bFoundAnyMatchedIntent = false;
|
|
|
|
TMap<FString, int32> IntentMatchIndexes;
|
|
|
|
for (const FCurveModelID& CurveModelID : InCurvesToApplyTo)
|
|
{
|
|
FCurveModel* TargetCurve = FindCurve(CurveModelID);
|
|
check(TargetCurve);
|
|
|
|
// Figure out what our destination thinks it's supposed to be used for, ie "Location.X"
|
|
FString TargetIntent = TargetCurve->GetLongDisplayName().ToString();
|
|
if (TargetIntent.IsEmpty())
|
|
{
|
|
// We don't try to match curves with no intent as that's just chaos.
|
|
NumCurvesNoMatchedIntent++;
|
|
continue;
|
|
}
|
|
|
|
TargetCurve->Modify();
|
|
|
|
// In an attempt to support buffering multiple curves with the same intention, we'll try to match them up in pairs. This means
|
|
// for the first curve that we're trying to apply to, if the intention is "Location.X" we will search the buffered curves for a
|
|
// "Location.X". Upon finding one, we store the index that it was found at, so the next time we try to find a curve with the same
|
|
// intention, we look for the second "Location.X" and so forth. If we don't find a second "Location.X" in our buffered curves we'll
|
|
// fall back to the first buffered one so you can 1:Many copy a curve.
|
|
int32 BufferedCurveSearchIndexStart = 0;
|
|
const int32* PreviouslyFoundIntent = IntentMatchIndexes.Find(TargetIntent);
|
|
if (PreviouslyFoundIntent)
|
|
{
|
|
// Start our search on the next item in the array. If we don't find one, we'll fall back to the last one.
|
|
BufferedCurveSearchIndexStart = IntentMatchIndexes[TargetIntent] + 1;
|
|
}
|
|
|
|
int32 MatchedBufferedCurveIndex = -1;
|
|
for (int32 BufferedCurveIndex = BufferedCurveSearchIndexStart; BufferedCurveIndex < BufferedCurves.Num(); BufferedCurveIndex++)
|
|
{
|
|
if (BufferedCurves[BufferedCurveIndex]->GetLongDisplayName() == TargetIntent)
|
|
{
|
|
MatchedBufferedCurveIndex = BufferedCurveIndex;
|
|
|
|
// Update our previously found intent to the latest one.
|
|
IntentMatchIndexes.FindOrAdd(TargetIntent) = MatchedBufferedCurveIndex;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// The Intent Match Indexes stores the latest index to find a valid curve, or the last one if no new valid one was found.
|
|
// If there is an entry in the match indexes now, we can use that to figure out which buffered curve we'll pull from.
|
|
// If we didn't find any more with the same intention, we fall back to the existing one (if it exists!)
|
|
if (IntentMatchIndexes.Find(TargetIntent))
|
|
{
|
|
MatchedBufferedCurveIndex = IntentMatchIndexes[TargetIntent];
|
|
}
|
|
|
|
// Finally, we can try to use the matched curve if one was found.
|
|
if (MatchedBufferedCurveIndex >= 0)
|
|
{
|
|
// We successfully matched, so count that one up!
|
|
NumCurvesMatchedByIntent++;
|
|
bFoundAnyMatchedIntent = true;
|
|
|
|
const IBufferedCurveModel* BufferedCurve = BufferedCurves[MatchedBufferedCurveIndex].Get();
|
|
|
|
TUniquePtr<IBufferedCurveModel> CurveModelCopy;
|
|
if (bSwapBufferCurves)
|
|
{
|
|
CurveModelCopy = TargetCurve->CreateBufferedCurveCopy();
|
|
}
|
|
|
|
ApplyBufferedCurveToTarget(BufferedCurve, TargetCurve);
|
|
|
|
if (bSwapBufferCurves)
|
|
{
|
|
BufferedCurves[MatchedBufferedCurveIndex] = MoveTemp(CurveModelCopy);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// We couldn't find a match despite our best efforts
|
|
NumCurvesNoMatchedIntent++;
|
|
}
|
|
}
|
|
|
|
// If we managed to match any by intent, we're going to early out and assume that's what their intent was.
|
|
if (bFoundAnyMatchedIntent)
|
|
{
|
|
const FText NotificationText = FText::Format(LOCTEXT("MatchedBufferedCurvesByIntent", "Applied {0}/{1} buffered curves to {2}/{3} target curves."),
|
|
FText::AsNumber(IntentMatchIndexes.Num()), FText::AsNumber(BufferedCurves.Num()), // We used X of Y total buffered curves
|
|
FText::AsNumber(NumCurvesMatchedByIntent), FText::AsNumber(InCurvesToApplyTo.Num())); // To apply to Z of W target curves,
|
|
|
|
FNotificationInfo Info(NotificationText);
|
|
Info.ExpireDuration = 6.f;
|
|
Info.bUseLargeFont = false;
|
|
Info.bUseSuccessFailIcons = false;
|
|
FSlateNotificationManager::Get().AddNotification(Info);
|
|
|
|
if (NumCurvesNoMatchedIntent > 0)
|
|
{
|
|
const FText FailedNotificationText = FText::Format(LOCTEXT("NumCurvesNotMatchedByIntent", "Failed to find a buffered curve with the same intent for {0} target curves, skipping..."),
|
|
FText::AsNumber(NumCurvesNoMatchedIntent)); // Leaving V many target curves unaffected due to no intent match.
|
|
|
|
FNotificationInfo FailInfo(FailedNotificationText);
|
|
FailInfo.ExpireDuration = 6.f;
|
|
FailInfo.bUseLargeFont = false;
|
|
FailInfo.bUseSuccessFailIcons = true;
|
|
FSlateNotificationManager::Get().AddNotification(FailInfo);
|
|
}
|
|
|
|
// Early out
|
|
return true;
|
|
}
|
|
|
|
// If we got this far, it means that the buffered curves have no recognizable relation to the target curves.
|
|
// If the number of curves match, we'll just do a 1:1 mapping. This works for most cases where you're trying
|
|
// to paste an unrelated curve onto another as it's likely that there's only one curve. We don't limit it to
|
|
// one curve though, we'll just warn...
|
|
if (InCurvesToApplyTo.Num() == BufferedCurves.Num())
|
|
{
|
|
// This will work great in the case there's only one curve. It'll guess if there's more than one, relying on
|
|
// sets with no guaranteed order.
|
|
TArray<FCurveModelID> CurvesToApplyTo = InCurvesToApplyTo.Array();
|
|
|
|
for (int32 CurveIndex = 0; CurveIndex < InCurvesToApplyTo.Num(); CurveIndex++)
|
|
{
|
|
FCurveModel* TargetCurve = FindCurve(CurvesToApplyTo[CurveIndex]);
|
|
|
|
TUniquePtr<IBufferedCurveModel> CurveModelCopy;
|
|
if (bSwapBufferCurves)
|
|
{
|
|
CurveModelCopy = TargetCurve->CreateBufferedCurveCopy();
|
|
}
|
|
|
|
ApplyBufferedCurveToTarget(BufferedCurves[CurveIndex].Get(), TargetCurve);
|
|
|
|
if (bSwapBufferCurves)
|
|
{
|
|
BufferedCurves[CurveIndex] = MoveTemp(CurveModelCopy);
|
|
}
|
|
}
|
|
|
|
FText NotificationText;
|
|
if (InCurvesToApplyTo.Num() == 1)
|
|
{
|
|
NotificationText = LOCTEXT("MatchedBufferedCurvesBySolo", "Applied buffered curve to target curve with no intention matching.");
|
|
}
|
|
else
|
|
{
|
|
NotificationText = LOCTEXT("MatchedBufferedCurvesByIndex", "Applied buffered curves with no intention matching. Order not guranteed.");
|
|
}
|
|
|
|
FNotificationInfo Info(NotificationText);
|
|
Info.ExpireDuration = 6.f;
|
|
Info.bUseLargeFont = false;
|
|
Info.bUseSuccessFailIcons = false;
|
|
FSlateNotificationManager::Get().AddNotification(Info);
|
|
|
|
// Early out
|
|
return true;
|
|
}
|
|
|
|
// If we got this far, we have no idea what to do. They're trying to match a bunch of curves with no intention and different amounts.
|
|
// Warn of failure and give up.
|
|
{
|
|
const FText FailedNotificationText = LOCTEXT("NoBufferedCurvesMatched", "Failed to apply buffered curves, apply them one at a time instead.");
|
|
|
|
FNotificationInfo FailInfo(FailedNotificationText);
|
|
FailInfo.ExpireDuration = 6.f;
|
|
FailInfo.bUseLargeFont = false;
|
|
FailInfo.bUseSuccessFailIcons = true;
|
|
FSlateNotificationManager::Get().AddNotification(FailInfo);
|
|
}
|
|
|
|
// No need to make a entry in the Undo/Redo buffer if it didn't apply anything.
|
|
Transaction.Cancel();
|
|
return false;
|
|
}
|
|
|
|
TSet<FCurveModelID> FCurveEditor::GetSelectionFromTreeAndKeys() const
|
|
{
|
|
TSet<FCurveModelID> CurveModelIDs;
|
|
|
|
// Buffer curves operates on the selected curves (tree selection or key selection)
|
|
for (const TTuple<FCurveEditorTreeItemID, ECurveEditorTreeSelectionState>& Pair : GetTreeSelection())
|
|
{
|
|
if (Pair.Value == ECurveEditorTreeSelectionState::Explicit)
|
|
{
|
|
const FCurveEditorTreeItem& TreeItem = GetTreeItem(Pair.Key);
|
|
for (const FCurveModelID& CurveModelID : TreeItem.GetCurves())
|
|
{
|
|
CurveModelIDs.Add(CurveModelID);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const TTuple<FCurveModelID, FKeyHandleSet>& Pair : Selection.GetAll())
|
|
{
|
|
CurveModelIDs.Add(Pair.Key);
|
|
}
|
|
|
|
return CurveModelIDs;
|
|
}
|
|
|
|
bool FCurveEditor::IsActiveBufferedCurve(const TUniquePtr<IBufferedCurveModel>& BufferedCurve) const
|
|
{
|
|
TSet<FCurveModelID> CurveModelIDs = GetSelectionFromTreeAndKeys();
|
|
for (const FCurveModelID& CurveModelID : CurveModelIDs)
|
|
{
|
|
if (FCurveModel* Curve = FindCurve(CurveModelID))
|
|
{
|
|
if (Curve->GetLongDisplayName().ToString() == BufferedCurve.Get()->GetLongDisplayName())
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static TAutoConsoleVariable<bool> CVarDisableKeyCleansing(
|
|
TEXT("CurveEditor.EnableCurveCleansing"), false, TEXT("After undo operations, whether to remove invalid keys from the key selection.")
|
|
);
|
|
|
|
void FCurveEditor::PostUndo(bool bSuccess)
|
|
{
|
|
if (WeakPanel.IsValid())
|
|
{
|
|
WeakPanel.Pin()->PostUndo();
|
|
}
|
|
|
|
// Temporary hack while we're testing the new undo / redo system for key selection.
|
|
// Disabled by default - if animators notice problems, they can enable this CVar. We don't expect issues with this off by default.
|
|
// If no issues are found in 2 weeks, we'll just remove the entirety of the below code.
|
|
// 19th of Feb 2025.
|
|
if (!CVarDisableKeyCleansing.GetValueOnGameThread())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// If you create keys and then undo them the selection set still thinks there's keys selected.
|
|
// This presents issues with context menus and other things that are activated when there is a selection set.
|
|
// To fix this, we have to loop through all of our curve models, and re-select only the key handles that were
|
|
// previously selected that still exist. Ugly, but reasonably functional.
|
|
TMap<FCurveModelID, FKeyHandleSet> SelectionSet = Selection.GetAll();
|
|
for (const TPair<FCurveModelID, FKeyHandleSet>& Set : SelectionSet)
|
|
{
|
|
FCurveModel* CurveModel = FindCurve(Set.Key);
|
|
|
|
// If the entire curve was removed, just dump that out of the selection set.
|
|
if (!CurveModel)
|
|
{
|
|
Selection.Remove(Set.Key);
|
|
continue;
|
|
}
|
|
// Get all of the key handles from this curve.
|
|
TArray<FKeyHandle> KeyHandles;
|
|
CurveModel->GetKeys(TNumericLimits<double>::Lowest(), TNumericLimits<double>::Max(), TNumericLimits<double>::Lowest(), TNumericLimits<double>::Max(), KeyHandles);
|
|
|
|
// The set handles will be mutated as we remove things so we need a copy that we can iterate through.
|
|
TArrayView<const FKeyHandle> SelectedHandles = Set.Value.AsArray();
|
|
TArray<FKeyHandle> NonMutableArray = TArray<FKeyHandle>(SelectedHandles.GetData(), SelectedHandles.Num());
|
|
|
|
for (const FKeyHandle& Handle : NonMutableArray)
|
|
{
|
|
// Check to see if our curve model contains this handle still.
|
|
if (!KeyHandles.Contains(Handle))
|
|
{
|
|
Selection.Remove(Set.Key, ECurvePointType::Key, Handle);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void FCurveEditor::PostRedo(bool bSuccess)
|
|
{
|
|
PostUndo(bSuccess);
|
|
}
|
|
|
|
void FCurveEditor::OnCustomColorsChanged()
|
|
{
|
|
for (TPair<FCurveModelID, TUniquePtr<FCurveModel>>& CurvePair : CurveData)
|
|
{
|
|
if (FCurveModel* Curve = CurvePair.Value.Get())
|
|
{
|
|
UObject* Object = nullptr;
|
|
FString Name;
|
|
Curve->GetCurveColorObjectAndName(&Object, Name);
|
|
|
|
TOptional<FLinearColor> Color = Settings->GetCustomColor(Object->GetClass(), Name);
|
|
if (Color.IsSet())
|
|
{
|
|
Curve->SetColor(Color.GetValue());
|
|
}
|
|
else
|
|
{
|
|
// Note: If the color is no longer defined, there's no way to update with the previously defined
|
|
// default color. The curve models would need to be rebuilt, but would cause selection/framing and
|
|
// other things to change. So, this is intentionally not implemented.
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void FCurveEditor::OnAxisSnappingChanged()
|
|
{
|
|
TSharedPtr<SCurveEditorPanel> Panel = WeakPanel.Pin();
|
|
if (Panel.IsValid())
|
|
{
|
|
Panel->UpdateAxisSnapping();
|
|
}
|
|
}
|
|
|
|
#undef LOCTEXT_NAMESPACE
|