// Copyright Epic Games, Inc. All Rights Reserved. #include "Tools/SequencerSnapField.h" #include "Containers/PagedArray.h" #include "MovieScene.h" #include "SSequencer.h" #include "MovieSceneTimeHelpers.h" #include "MovieSceneSequence.h" #include "ISequencerSection.h" #include "SequencerSettings.h" #include "IKeyArea.h" #include "MVVM/ViewModels/ViewModel.h" #include "MVVM/Views/SOutlinerView.h" #include "MVVM/Extensions/ISnappableExtension.h" #include "MVVM/Extensions/IClockExtension.h" #include "IKeyArea.h" struct FSnapGridVisitor : ISequencerEntityVisitor, UE::Sequencer::ISnapField { FSnapGridVisitor(UE::Sequencer::ISnapCandidate& InCandidate, uint32 EntityMask) : ISequencerEntityVisitor(EntityMask) , Candidate(InCandidate) { InCandidate.GetCapabilities(Applicability, Capabilities); } virtual void VisitKeys(const UE::Sequencer::TViewModelPtr& Channel, const TRange& VisitRangeFrames) const override { using namespace UE::Sequencer; const bool bCallFunction = EnumHasAnyFlags(Capabilities, ISnapCandidate::ESnapCapabilities::IsKeyApplicable); const bool bDefaultState = EnumHasAnyFlags(Applicability, ISnapCandidate::EDefaultApplicability::Keys); if (!bCallFunction) { if (bDefaultState) { // Faster implementation if we need to add all the keys. // Only need to allocate space for the times in this case TimesScratch.Reset(); Channel->GetKeyArea()->GetKeyInfo(nullptr, &TimesScratch, VisitRangeFrames); const int32 StartNum = Snaps.Num(); const int32 NumKeys = TimesScratch.Num(); Snaps.Reserve(StartNum + NumKeys); for (int32 Index = 0; Index < NumKeys; ++Index) { Snaps.Emplace(FSnapPoint::Key, TimesScratch[Index]); } } } else { // Call the function for each key individually. Much slower. // Need to allocate handles as well HandlesScratch.Reset(); TimesScratch.Reset(); Channel->GetKeyArea()->GetKeyInfo(&HandlesScratch, &TimesScratch, VisitRangeFrames); for (int32 Index = 0; Index < HandlesScratch.Num(); ++Index) { FKeyHandle KeyHandle = HandlesScratch[Index]; if (Candidate.IsKeyApplicable(KeyHandle, Channel)) { Snaps.Add(FSnapPoint{ FSnapPoint::Key, TimesScratch[Index] }); } } } } virtual void VisitDataModel(UE::Sequencer::FViewModel* DataModel) const { using namespace UE::Sequencer; if (ISnappableExtension* Snappable = DataModel->CastThis()) { Snappable->AddToSnapField(Candidate, *const_cast(this)); } } virtual void AddSnapPoint(const UE::Sequencer::FSnapPoint& SnapPoint) override { Snaps.Emplace(SnapPoint); } UE::Sequencer::ISnapCandidate& Candidate; /** utilize a chunked array to reduce the number of resize/grow memcpies for very large data sets (ie, > 1million keys) */ mutable TPagedArray Snaps; mutable TArray HandlesScratch; mutable TArray TimesScratch; UE::Sequencer::ISnapCandidate::ESnapCapabilities Capabilities; UE::Sequencer::ISnapCandidate::EDefaultApplicability Applicability; }; FSequencerSnapField::FSequencerSnapField(const FSequencer& InSequencer, UE::Sequencer::ISnapCandidate& Candidate, uint32 EntityMask) { Initialize(InSequencer, Candidate, EntityMask); Finalize(); } void FSequencerSnapField::AddExplicitSnap(UE::Sequencer::FSnapPoint InSnap) { if (InSnap.Weighting == 1.f && InSnap.Type != UE::Sequencer::FSnapPoint::Key) { InSnap.Weighting = 10.f; } SortedSnaps.Add(InSnap); } void FSequencerSnapField::Initialize(const FSequencer& InSequencer, UE::Sequencer::ISnapCandidate& Candidate, uint32 EntityMask) { using namespace UE::Sequencer; if (InSequencer.GetSequencerSettings()->GetIsSnapEnabled()) { TViewModelPtr Extension = InSequencer.GetViewModel()->GetRootSequenceModel().ImplicitCast(); if (Extension.IsValid() && Extension->SupportsSnapping() && Extension->ShouldSnapFrameTime()) { ClockExtension = Extension; } } TSharedPtr TreeView = StaticCastSharedRef(InSequencer.GetSequencerWidget())->GetTreeView(); TArray> VisibleItems; TreeView->GetVisibleItems(VisibleItems); TRange ViewRange = InSequencer.GetViewRange(); FSequencerEntityWalker Walker( FSequencerEntityRange(ViewRange, InSequencer.GetFocusedTickResolution()), FVector2D(SequencerSectionConstants::KeySize)); // Traverse the visible space, collecting snapping times as we go FSnapGridVisitor Visitor(Candidate, EntityMask); for (const TViewModelPtr& Item : VisibleItems) { Walker.Traverse(Visitor, Item); } // Add the playback range start/end bounds as potential snap candidates TRange PlaybackRange = InSequencer.GetFocusedMovieSceneSequence()->GetMovieScene()->GetPlaybackRange(); if(UE::MovieScene::DiscreteSize(PlaybackRange) > 0) { Visitor.Snaps.Emplace(FSnapPoint{ FSnapPoint::PlaybackRange, UE::MovieScene::DiscreteInclusiveLower(PlaybackRange)}); Visitor.Snaps.Emplace(FSnapPoint{ FSnapPoint::PlaybackRange, UE::MovieScene::DiscreteExclusiveUpper(PlaybackRange)}); } // Add the current time as a potential snap candidate Visitor.Snaps.Emplace(FSnapPoint{ FSnapPoint::CurrentTime, InSequencer.GetLocalTime().Time.FrameNumber }); // Add the selection range bounds as a potential snap candidate TRange SelectionRange = InSequencer.GetFocusedMovieSceneSequence()->GetMovieScene()->GetSelectionRange(); if (UE::MovieScene::DiscreteSize(SelectionRange) > 0) { Visitor.Snaps.Emplace(FSnapPoint{ FSnapPoint::InOutRange, UE::MovieScene::DiscreteInclusiveLower(SelectionRange)}); Visitor.Snaps.Emplace(FSnapPoint{ FSnapPoint::InOutRange, UE::MovieScene::DiscreteExclusiveUpper(SelectionRange) - 1}); } if (InSequencer.GetSequencerSettings()->GetShowMarkedFrames()) { // Add in the marked frames for (const FMovieSceneMarkedFrame& MarkedFrame : InSequencer.GetFocusedMovieSceneSequence()->GetMovieScene()->GetMarkedFrames()) { Visitor.Snaps.Emplace( FSnapPoint{ FSnapPoint::Mark, MarkedFrame.FrameNumber } ); } // Add in the global marked frames for (const FMovieSceneMarkedFrame& MarkedFrame : InSequencer.GetGlobalMarkedFrames()) { Visitor.Snaps.Emplace(FSnapPoint{ FSnapPoint::Mark, MarkedFrame.FrameNumber }); } } // Copy the paged array to our linear array ready for final sorting Visitor.Snaps.ToArray(SortedSnaps); TickResolution = InSequencer.GetFocusedTickResolution(); DisplayRate = InSequencer.GetFocusedDisplayRate(); ScrubStyle = InSequencer.GetScrubStyle(); } void FSequencerSnapField::Finalize() { using namespace UE::Sequencer; // Sort SortedSnaps.Sort([](const FSnapPoint& A, const FSnapPoint& B){ return A.Time < B.Time; }); const int32 NumSnaps = SortedSnaps.Num(); TArray FinalSnaps; FinalSnaps.Reserve(NumSnaps); const FSnapPoint* const RESTRICT Snaps = SortedSnaps.GetData(); // Remove duplicates for (int32 Index = 0; Index < NumSnaps; /* incremented inside inner loop */) { const FFrameNumber CurrentTime = SortedSnaps[Index].Time; FSnapPoint FinalSnap = Snaps[Index]; FinalSnap.Weighting = 0.f; // Add up all weights of the same time for ( ; Index < NumSnaps && Snaps[Index].Time == CurrentTime; ++Index) { FinalSnap.Weighting += Snaps[Index].Weighting; } FinalSnaps.Add(FinalSnap); } FinalSnaps.Shrink(); Swap(FinalSnaps, SortedSnaps); } TOptional FSequencerSnapField::Snap(const FFrameTime& InTime, const FFrameTime& Threshold) const { int32 Min = 0; int32 Max = SortedSnaps.Num(); // Binary search, then linearly search a range for ( ; Min != Max ; ) { int32 SearchIndex = Min + (Max - Min) / 2; UE::Sequencer::FSnapPoint ProspectiveSnap = SortedSnaps[SearchIndex]; if (ProspectiveSnap.Time > InTime + Threshold) { Max = SearchIndex; } else if (ProspectiveSnap.Time < InTime - Threshold) { Min = SearchIndex + 1; } else { // Linearly search forwards and backwards to find the closest or heaviest snap float SnapWeight = 0.f; FFrameTime SnapDelta = ProspectiveSnap.Time - InTime; // Search forwards while we're in the threshold for (int32 FwdIndex = SearchIndex; FwdIndex < Max && SortedSnaps[FwdIndex].Time < InTime + Threshold; ++FwdIndex) { FFrameTime ThisSnapDelta = InTime - SortedSnaps[FwdIndex].Time; float ThisSnapWeight = SortedSnaps[FwdIndex].Weighting; if (ThisSnapWeight > SnapWeight || (ThisSnapWeight == SnapWeight && FMath::Abs(ThisSnapDelta) < FMath::Abs(SnapDelta))) { SnapDelta = ThisSnapDelta; SnapWeight = ThisSnapWeight; ProspectiveSnap = SortedSnaps[FwdIndex]; } } // Search backwards while we're in the threshold for (int32 BckIndex = SearchIndex-1; BckIndex >= Min && SortedSnaps[BckIndex].Time > InTime - Threshold; --BckIndex) { FFrameTime ThisSnapDelta = InTime - SortedSnaps[BckIndex].Time; float ThisSnapWeight = SortedSnaps[BckIndex].Weighting; if (ThisSnapWeight > SnapWeight || (ThisSnapWeight == SnapWeight && FMath::Abs(ThisSnapDelta) < FMath::Abs(SnapDelta))) { SnapDelta = ThisSnapDelta; SnapWeight = ThisSnapWeight; ProspectiveSnap = SortedSnaps[BckIndex]; } } if (SnapWeight != 0.f) { FSnapResult Result = { InTime, ProspectiveSnap.Time, ProspectiveSnap.Weighting }; return Result; } break; } } return TOptional(); } TOptional FSequencerSnapField::Snap(const TArray& InTimes, const FFrameTime& Threshold) const { TOptional ProspectiveSnap; const FFrameTime IntervalSnapThreshold = FFrameTime::FromDecimal((TickResolution / DisplayRate).AsDecimal()); int32 NumSnaps = 0; float MaxSnapWeight = 0.f; for (FFrameTime Time : InTimes) { TOptional ThisSnap; if (bSnapToLikeTypes) { ThisSnap = Snap(Time, FMath::Max(IntervalSnapThreshold, Threshold)); } if (ClockExtension.IsValid()) { FFrameNumber ClockSnapTime = ClockExtension->SnapFrameTime(Time).FrameNumber; if (ClockSnapTime != Time) { if (ThisSnap.IsSet()) { ThisSnap->SnappedTime = ClockSnapTime; } else { ThisSnap = { Time, ClockSnapTime, 1.f }; } } } const bool bShouldTrySnappingToInterval = !bSnapToLikeTypes || IntervalSnapThreshold > Threshold; if (bSnapToInterval || bShouldTrySnappingToInterval) { FFrameTime ThisTime = ThisSnap.IsSet() ? ThisSnap->SnappedTime : Time; FFrameTime IntervalTime = FFrameRate::TransformTime(ThisTime, TickResolution, DisplayRate); FFrameNumber PlayIntervalTime = IntervalTime.RoundToFrame(); FFrameNumber IntervalSnap = FFrameRate::TransformTime(PlayIntervalTime, DisplayRate, TickResolution).FloorToFrame(); FFrameTime ThisSnapAmount = IntervalSnap - ThisTime; bool bSnapToLikeTypesHasPriority = ThisSnap.IsSet() && FMath::Abs(ThisTime - Time) <= FMath::Abs(IntervalSnap - Time); if (!bSnapToLikeTypesHasPriority && FMath::Abs(ThisSnapAmount) <= IntervalSnapThreshold) { ThisSnap = { Time, IntervalSnap, 1.f }; } } if (!ThisSnap.IsSet()) { continue; } if (!ProspectiveSnap.IsSet() || ThisSnap->SnappedWeight > MaxSnapWeight) { ProspectiveSnap = ThisSnap; MaxSnapWeight = ThisSnap->SnappedWeight; } } return ProspectiveSnap; }