// Copyright Epic Games, Inc. All Rights Reserved. #include "SGeometryCacheTimeline.h" #include "Fonts/FontMeasure.h" #include "FrameNumberNumericInterface.h" #include "FrameNumberTimeEvaluator.h" #include "Framework/Application/SlateApplication.h" #include "GeometryCache.h" #include "GeometryCacheComponent.h" #include "GeometryCacheHelpers.h" #include "GeometryCacheTimelineBindingAsset.h" #include "GeometryCacheTimeSliderController.h" #include "ISequencerWidgetsModule.h" #include "Modules/ModuleManager.h" #include "Widgets/Layout/SGridPanel.h" #include "Widgets/Layout/SScrollBorder.h" #include "Widgets/Layout/SScrollBox.h" #include "Widgets/Layout/SSpacer.h" #include "Widgets/SBoxPanel.h" #include "Widgets/SGeometryCacheTimelineOverlay.h" #include "Widgets/SGeometryCacheTimelineSplitterOverlay.h" #include "Widgets/SGeometryCacheTimelineTransportControls.h" #include "Widgets/Text/STextBlock.h" #include "Widgets/Views/SListView.h" #define LOCTEXT_NAMESPACE "SGeometryCacheTimeline" void SGeometryCacheTimeline::Construct(const FArguments& InArgs, const TSharedRef& InBindingAsset) { TWeakPtr WeakBindingAsset = InBindingAsset; BindingAsset = InBindingAsset; const EFrameNumberDisplayFormats DisplayFormat = EFrameNumberDisplayFormats::Frames; const TAttribute TickResolution = MakeAttributeLambda([WeakBindingAsset]() { return FFrameRate(WeakBindingAsset.Pin()->GetTickResolution(), 1); }); ViewRange = MakeAttributeLambda([WeakBindingAsset]() { return WeakBindingAsset.IsValid() ? WeakBindingAsset.Pin()->GetViewRange() : FAnimatedRange(0.0, 0.0); }); const TAttribute DisplayRate = MakeAttributeLambda([WeakBindingAsset]() { return WeakBindingAsset.Pin()->GetFrameRate(); }); ColumnFillCoefficients[0] = 0.3f; ColumnFillCoefficients[1] = 0.7f; TAttribute FillCoefficient_0, FillCoefficient_1; { FillCoefficient_0.Bind(TAttribute::FGetter::CreateSP(this, &SGeometryCacheTimeline::GetColumnFillCoefficient, 0)); FillCoefficient_1.Bind(TAttribute::FGetter::CreateSP(this, &SGeometryCacheTimeline::GetColumnFillCoefficient, 1)); }; NumericTypeInterface = MakeShareable(new FFrameNumberInterface(DisplayFormat, 0, TickResolution, DisplayRate)); FTimeSliderArgs TimeSliderArgs; { TimeSliderArgs.ScrubPosition = MakeAttributeLambda([WeakBindingAsset]() { return WeakBindingAsset.IsValid() ? WeakBindingAsset.Pin()->GetScrubPosition() : FFrameTime(0); }); TimeSliderArgs.ViewRange = ViewRange; TimeSliderArgs.PlaybackRange = MakeAttributeLambda([WeakBindingAsset]() { return WeakBindingAsset.IsValid() ? WeakBindingAsset.Pin()->GetPlaybackRange() : TRange(0, 0); }); TimeSliderArgs.ClampRange = MakeAttributeLambda([WeakBindingAsset]() { return WeakBindingAsset.IsValid() ? WeakBindingAsset.Pin()->GetWorkingRange() : FAnimatedRange(0.0, 0.0); }); TimeSliderArgs.DisplayRate = DisplayRate; TimeSliderArgs.TickResolution = TickResolution; TimeSliderArgs.OnViewRangeChanged = FOnViewRangeChanged::CreateSP(&InBindingAsset.Get(), &FGeometryCacheTimelineBindingAsset::HandleViewRangeChanged); TimeSliderArgs.OnClampRangeChanged = FOnTimeRangeChanged::CreateSP(&InBindingAsset.Get(), &FGeometryCacheTimelineBindingAsset::HandleWorkingRangeChanged); TimeSliderArgs.IsPlaybackRangeLocked = true; TimeSliderArgs.PlaybackStatus = EMovieScenePlayerStatus::Stopped; TimeSliderArgs.NumericTypeInterface = NumericTypeInterface; TimeSliderArgs.OnScrubPositionChanged = FOnScrubPositionChanged::CreateSP(this, &SGeometryCacheTimeline::HandleScrubPositionChanged); } TimeSliderController = MakeShareable(new FGeometryCacheTimeSlideController(TimeSliderArgs, SharedThis(this))); TSharedRef TimeSliderControllerRef = TimeSliderController.ToSharedRef(); // Create the top slider constexpr bool bMirrorLabels = false; ISequencerWidgetsModule& SequencerWidgets = FModuleManager::Get().LoadModuleChecked("SequencerWidgets"); TimeSlider = SequencerWidgets.CreateTimeSlider(TimeSliderControllerRef, bMirrorLabels); // Create bottom time range slider TSharedRef BottomTimeRange = SequencerWidgets.CreateTimeRange( FTimeRangeArgs( EShowRange::ViewRange | EShowRange::WorkingRange | EShowRange::PlaybackRange, EShowRange::ViewRange | EShowRange::WorkingRange, TimeSliderControllerRef, EVisibility::Visible, NumericTypeInterface.ToSharedRef() ), SequencerWidgets.CreateTimeRangeSlider(TimeSliderControllerRef) ); TSharedRef ScrollBar = SNew(SScrollBar) .Thickness(FVector2D(5.0f, 5.0f)); InitTrackNames(); TracksListView = SNew(SListView< TSharedPtr >) .ExternalScrollbar(ScrollBar) .ListItemsSource(&TrackNames) .SelectionMode(ESelectionMode::Single) .OnGenerateRow(this, &SGeometryCacheTimeline::HandleTimelineListViewGenerateRow); // Grid Panel Constants const int32 Column0 = 0, Column1 = 1; const int32 Row0 = 0, Row1 = 1, Row2 = 2, Row3 = 3, Row4 = 4; const float CommonPadding = 3.f; const FMargin ResizeBarPadding(4.f, 0, 0, 0); ChildSlot [ SNew(SOverlay) + SOverlay::Slot() [ SNew(SGridPanel) .FillRow(1, 1.0f) .FillColumn(0, FillCoefficient_0) .FillColumn(1, FillCoefficient_1) + SGridPanel::Slot(Column1, Row0, SGridPanel::Layer(10)) .Padding(ResizeBarPadding) [ SNew(SBorder) .BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder")) .BorderBackgroundColor(FLinearColor(.50f, .50f, .50f, 1.0f)) .Padding(0.f) .Clipping(EWidgetClipping::ClipToBounds) [ TimeSlider.ToSharedRef() ] ] + SGridPanel::Slot(Column1, Row1, SGridPanel::Layer(10)) .Padding(ResizeBarPadding) [ SNew(SGeometryCacheTimelineOverlay, TimeSliderControllerRef) .Visibility(EVisibility::HitTestInvisible) .DisplayScrubPosition(false) .DisplayTickLines(true) .Clipping(EWidgetClipping::ClipToBounds) .PaintPlaybackRangeArgs(FPaintPlaybackRangeArgs(FAppStyle::GetBrush("Sequencer.Timeline.PlayRange_L"), FAppStyle::GetBrush("Sequencer.Timeline.PlayRange_R"), 6.f)) ] // Overlay that draws the scrub position + SGridPanel::Slot(Column1, Row1, SGridPanel::Layer(20)) .Padding(ResizeBarPadding) [ SNew(SGeometryCacheTimelineOverlay, TimeSliderControllerRef) .Visibility(EVisibility::HitTestInvisible) .DisplayScrubPosition(true) .DisplayTickLines(false) .Clipping(EWidgetClipping::ClipToBounds) ] + SGridPanel::Slot(Column1, Row3, SGridPanel::Layer(10)) .Padding(ResizeBarPadding) [ SNew(SBorder) .BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder")) .BorderBackgroundColor(FLinearColor(0.5f, 0.5f, 0.5f, 1.0f)) .Clipping(EWidgetClipping::ClipToBounds) .Padding(0) [ BottomTimeRange ] ] + SGridPanel::Slot(Column0, Row3, SGridPanel::Layer(10)) .VAlign(VAlign_Center) .HAlign(HAlign_Center) [ SNew(SGeometryCacheTimelineTransportControls, InBindingAsset) ] + SGridPanel::Slot(Column0, Row1, SGridPanel::Layer(5)) .ColumnSpan(2) [ SNew(SHorizontalBox) + SHorizontalBox::Slot() [ SNew(SOverlay) + SOverlay::Slot() [ SNew(SVerticalBox) + SVerticalBox::Slot() .FillHeight(1.f) [ SNew(SScrollBorder, TracksListView.ToSharedRef()) [ SNew(SHorizontalBox) +SHorizontalBox::Slot() .FillWidth(FillCoefficient_0) [ SNew(SBox) [ TracksListView.ToSharedRef() ] ] + SHorizontalBox::Slot() .FillWidth(FillCoefficient_1) [ SNew(SBox) .Padding(ResizeBarPadding) .Clipping(EWidgetClipping::ClipToBounds) ] ] ] ] + SOverlay::Slot() .HAlign(HAlign_Right) [ ScrollBar ] ] ] ] + SOverlay::Slot() [ // track area virtual splitter overlay SNew(SGeometryCacheTimelineSplitterOverlay) .Style(FAppStyle::Get(), "AnimTimeline.Outliner.Splitter") .Visibility(EVisibility::SelfHitTestInvisible) + SSplitter::Slot() .Value(FillCoefficient_0) .OnSlotResized(SSplitter::FOnSlotResized::CreateSP(this, &SGeometryCacheTimeline::OnColumnFillCoefficientChanged, 0)) [ SNew(SSpacer) ] + SSplitter::Slot() .Value(FillCoefficient_1) .OnSlotResized(SSplitter::FOnSlotResized::CreateSP(this, &SGeometryCacheTimeline::OnColumnFillCoefficientChanged, 1)) [ SNew(SSpacer) ] ] ]; } void SGeometryCacheTimeline::HandleScrubPositionChanged(FFrameTime NewScrubPosition, bool bIsScrubbing, bool bEvaluate) const { if (BindingAsset.IsValid()) { TWeakObjectPtr GeometryCacheComponent = BindingAsset.Pin()->GetPreviewComponent(); if (GeometryCacheComponent.IsValid() && GeometryCacheComponent->IsPlaying()) { GeometryCacheComponent->Stop(); } BindingAsset.Pin()->SetScrubPosition(NewScrubPosition); } } void SGeometryCacheTimeline::InitTrackNames() { TrackNames.Empty(); if (BindingAsset.IsValid()) { const TArray Names = BindingAsset.Pin()->GetPreviewComponent()->GetTrackNames(); for (const FString& Name : Names) { TrackNames.Add(MakeShareable(new FString(Name))); } } } void SGeometryCacheTimeline::OnColumnFillCoefficientChanged(float FillCoefficient, int32 ColumnIndex) { ColumnFillCoefficients[ColumnIndex] = FillCoefficient; } TSharedRef SGeometryCacheTimeline::HandleTimelineListViewGenerateRow(TSharedPtr Text, const TSharedRef& OwnerTable) { return SNew(STableRow >, OwnerTable) [ SNew(SBox) [ SNew(STextBlock) .Margin(FMargin{10,5}) .Text(FText::FromString(*Text)) ] ]; } // FFrameRate::ComputeGridSpacing doesnt deal well with prime numbers, so we have a custom impl here static bool ComputeGridSpacing(const FFrameRate& InFrameRate, float PixelsPerSecond, double& OutMajorInterval, int32& OutMinorDivisions, float MinTickPx, float DesiredMajorTickPx) { // First try built-in spacing const bool bResult = InFrameRate.ComputeGridSpacing(PixelsPerSecond, OutMajorInterval, OutMinorDivisions, MinTickPx, DesiredMajorTickPx); if (!bResult || OutMajorInterval == 1.0) { if (PixelsPerSecond <= 0.f) { return false; } const int32 RoundedFPS = static_cast(FMath::RoundToInt(InFrameRate.AsDecimal())); if (RoundedFPS > 0) { // Showing frames TArray> CommonBases; // Divide the rounded frame rate by 2s, 3s or 5s recursively { const int32 Denominators[] = { 2, 3, 5 }; int32 LowestBase = RoundedFPS; for (;;) { CommonBases.Add(LowestBase); if (LowestBase % 2 == 0) { LowestBase = LowestBase / 2; } else if (LowestBase % 3 == 0) { LowestBase = LowestBase / 3; } else if (LowestBase % 5 == 0) { LowestBase = LowestBase / 5; } else { int32 LowestResult = LowestBase; for (int32 Denominator : Denominators) { int32 Result = LowestBase / Denominator; if (Result > 0 && Result < LowestResult) { LowestResult = Result; } } if (LowestResult < LowestBase) { LowestBase = LowestResult; } else { break; } } } } Algo::Reverse(CommonBases); const int32 Scale = static_cast(FMath::CeilToInt(DesiredMajorTickPx / PixelsPerSecond * InFrameRate.AsDecimal())); const int32 BaseIndex = FMath::Min(Algo::LowerBound(CommonBases, Scale), CommonBases.Num() - 1); const int32 Base = CommonBases[BaseIndex]; const int32 MajorIntervalFrames = FMath::CeilToInt((float)Scale / static_cast(Base)) * Base; OutMajorInterval = MajorIntervalFrames * InFrameRate.AsInterval(); // Find the lowest number of divisions we can show that's larger than the minimum tick size OutMinorDivisions = 0; for (int32 DivIndex = 0; DivIndex < BaseIndex; ++DivIndex) { if (Base % CommonBases[DivIndex] == 0) { const int32 MinorDivisions = MajorIntervalFrames / CommonBases[DivIndex]; if (OutMajorInterval / MinorDivisions * PixelsPerSecond >= MinTickPx) { OutMinorDivisions = MinorDivisions; break; } } } } } return OutMajorInterval != 0; } bool SGeometryCacheTimeline::GetGridMetrics(float PhysicalWidth, double& OutMajorInterval, int32& OutMinorDivisions) const { const FSlateFontInfo SmallLayoutFont = FCoreStyle::GetDefaultFontStyle("Regular", 8); const TSharedRef FontMeasureService = FSlateApplication::Get().GetRenderer()->GetFontMeasureService(); const FFrameRate DisplayRate = BindingAsset.Pin()->GetFrameRate(); const double BiggestTime = ViewRange.Get().GetUpperBoundValue(); const FString TickString = NumericTypeInterface->ToString((BiggestTime * DisplayRate).FrameNumber.Value); const FVector2D MaxTextSize = FontMeasureService->Measure(TickString, SmallLayoutFont); constexpr float MajorTickMultiplier = 2.f; const float MinTickPx = static_cast(MaxTextSize.X) + 5.f; const float DesiredMajorTickPx = static_cast(MaxTextSize.X) * MajorTickMultiplier; if (PhysicalWidth > 0 && DisplayRate.AsDecimal() > 0) { return ComputeGridSpacing( DisplayRate, static_cast(PhysicalWidth / ViewRange.Get().Size()), OutMajorInterval, OutMinorDivisions, MinTickPx, DesiredMajorTickPx); } return false; } #undef LOCTEXT_NAMESPACE