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

186 lines
6.0 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "SmartSnap.h"
#include "Algo/AllOf.h"
#include "Algo/AnyOf.h"
#include "CurveEditor.h"
#include <cmath>
namespace UE::CurveEditor
{
bool CanSmartSnapSelection(const FCurveEditorSelection& InSelection)
{
return Algo::AnyOf(InSelection.GetAll(), [](const TPair<FCurveModelID, FKeyHandleSet>& Pair)
{
const FKeyHandleSet& KeySelection = Pair.Value;
return Algo::AnyOf(KeySelection.AsArray(), [&KeySelection](const FKeyHandle& InHandle)
{
return KeySelection.PointType(InHandle) == ECurvePointType::Key;
});
});
}
namespace SmartSnapDetail
{
static TArray<FKeyHandle> ReturnOnlyKeys(const FKeyHandleSet& InSelection)
{
TArray<FKeyHandle> Result;
Result.Reserve(InSelection.Num());
for (const FKeyHandle& KeyHandle : InSelection.AsArray())
{
if (InSelection.PointType(KeyHandle) == ECurvePointType::Key)
{
Result.Add(KeyHandle);
}
}
return Result;
}
static FFrameRate GetCurveEditorFrameRate(const FCurveEditor& InCurveEditor)
{
const TSharedPtr<ITimeSliderController> Controller = InCurveEditor.GetTimeSliderController();
return Controller ? Controller->GetDisplayRate() : FFrameRate{};
}
}
void EnumerateSmartSnappableKeys(
const FCurveEditor& InCurveEditor,
const TMap<FCurveModelID, FKeyHandleSet>& InKeysToOperateOn,
TMap<FCurveModelID, FKeyHandleSet>& OutKeysToSelect,
const TFunctionRef<void(const FCurveModelID& CurveModelId, FCurveModel& CurveModel, const FSmartSnapResult& SnapResult)>& InProcessSmartSnapping
)
{
const FFrameRate FrameRate = SmartSnapDetail::GetCurveEditorFrameRate(InCurveEditor);
for (const TPair<FCurveModelID, FKeyHandleSet>& Pair : InKeysToOperateOn)
{
FCurveModel* CurveModel = InCurveEditor.FindCurve(Pair.Key);
if (!CurveModel)
{
continue;
}
// Exclude tangent handles
const FKeyHandleSet& KeySelection = Pair.Value;
const bool bSelectionContainsOnlyKeys = Algo::AllOf(KeySelection.AsArray(), [&KeySelection](const FKeyHandle& InHandle)
{
return KeySelection.PointType(InHandle) == ECurvePointType::Key;
});
// Avoid TArray allocation when user has only selected key handles
const TArray<FKeyHandle> FilteredKeys = bSelectionContainsOnlyKeys ? TArray<FKeyHandle>{} : SmartSnapDetail::ReturnOnlyKeys(KeySelection);
const TConstArrayView<FKeyHandle> Keys = bSelectionContainsOnlyKeys ? KeySelection.AsArray() : FilteredKeys;
const int32 NumKeys = Keys.Num();
if (NumKeys == 0)
{
continue;
}
TArray<FKeyPosition> Positions;
Positions.SetNumUninitialized(NumKeys);
CurveModel->GetKeyPositions(Keys, Positions);
const FSmartSnapResult SnappingResult = ComputeSmartSnap(*CurveModel, Keys, Positions, FrameRate);
for (const FKeyHandle& InHandle : SnappingResult.UpdatedKeys)
{
OutKeysToSelect.FindOrAdd(Pair.Key).Add(InHandle, ECurvePointType::Key);
}
InProcessSmartSnapping(Pair.Key, *CurveModel, SnappingResult);
}
}
namespace SmartSnapDetail
{
struct FFrameData
{
FKeyHandle ClosestHandle = FKeyHandle::Invalid();
FFrameTime AbsDistToFrame;
};
static TMap<FFrameNumber, FFrameData> ComputeClosestFrames(
TConstArrayView<FKeyHandle> InHandles, TConstArrayView<FKeyPosition> InPositions, const FFrameRate& InFrameRate
)
{
TMap<FFrameNumber, FFrameData> FrameToData;
for (int32 Index = 0; Index < InHandles.Num(); ++Index)
{
const FKeyHandle& KeyHandle = InHandles[Index];
const FFrameTime SubFrame = InFrameRate.AsFrameTime(InPositions[Index].InputValue);
const FFrameTime FrameNumber = SubFrame.RoundToFrame();
const FFrameTime AbsDistToFrame = FMath::Max(SubFrame, FrameNumber) - FMath::Min(SubFrame, FrameNumber);
FFrameData& FrameData = FrameToData.FindOrAdd(FrameNumber.FrameNumber, FFrameData{ KeyHandle, AbsDistToFrame });
const bool bIsFirstKeyOnFrame = FrameData.ClosestHandle == KeyHandle;
if (bIsFirstKeyOnFrame)
{
continue;
}
const bool bIsCloserToFrame = AbsDistToFrame < FrameData.AbsDistToFrame;
if (bIsCloserToFrame)
{
FrameData.ClosestHandle = KeyHandle;
FrameData.AbsDistToFrame = AbsDistToFrame;
}
}
return FrameToData;
}
static FSmartSnapResult MoveKeysOntoFrames(
const FCurveModel& InModel, const TMap<FFrameNumber, FFrameData>& FrameToData, const FFrameRate& InFrameRate
)
{
FSmartSnapResult Result;
for (const TPair<FFrameNumber, FFrameData>& Frame : FrameToData)
{
const FKeyHandle& KeyHandle = Frame.Value.ClosestHandle;
const FFrameTime FrameNumber = Frame.Key;
FKeyPosition Position;
Position.InputValue = InFrameRate.AsSeconds(FrameNumber);
InModel.Evaluate(Position.InputValue, Position.OutputValue);
Result.NewPositions.Add(Position);
Result.UpdatedKeys.Add(KeyHandle);
}
return Result;
}
}
FSmartSnapResult ComputeSmartSnap(
const FCurveModel& InModel, TConstArrayView<FKeyHandle> InHandles, TConstArrayView<FKeyPosition> InPositions, const FFrameRate& InFrameRate
)
{
check(InHandles.Num() == InPositions.Num());
// 1. We'll compute all the frames covered by the keys, and the single key that is closest to it.
// Example: If key 1 is at 2.6 and key 2 at 2.7, we'd move key 2 to frame 3.0.
// This retains the shape of the curve a bit better (as opposed to taking a 'random' one without criteria).
const TMap<FFrameNumber, SmartSnapDetail::FFrameData> FrameToData = SmartSnapDetail::ComputeClosestFrames(InHandles, InPositions, InFrameRate);
// 2. The key closest to its assigned frame is moved there by evaluating the curve.
FSmartSnapResult Result = MoveKeysOntoFrames(InModel, FrameToData, InFrameRate);
// 3.1 All keys were moved? Done.
const int32 NumToRemove = InHandles.Num() - Result.NewPositions.Num();
if (!NumToRemove)
{
return Result;
}
// 3.2 All keys that were not moved are removed.
Result.RemovedKeys.Reserve(NumToRemove);
for (const FKeyHandle& Handle : InHandles)
{
if (!Result.UpdatedKeys.Contains(Handle))
{
Result.RemovedKeys.Add(Handle);
}
}
return Result;
}
void ApplySmartSnap(FCurveModel& InModel, const FSmartSnapResult& InSmartSnap, double InCurrentTime)
{
InModel.RemoveKeys(InSmartSnap.RemovedKeys, InCurrentTime);
InModel.SetKeyPositions(InSmartSnap.UpdatedKeys, InSmartSnap.NewPositions);
}
}