diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index 1076255b6aa2..ded7c6605853 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -42,7 +42,7 @@ public partial class SamplePointPiece : HitObjectPointPiece, IHasPopover private Editor? editor { get; set; } [Resolved] - private Timeline? timeline { get; set; } + private TimelineBlueprintContainer? timelineBlueprintContainer { get; set; } private Bindable samplesVisible = null!; @@ -72,9 +72,7 @@ private void load(OsuConfigManager config) samplesVisible = config.GetBindable(OsuSetting.EditorTimelineShowSamples); } - private BindableNumber? timelineZoom; - - private bool contracted; + private readonly Bindable contracted = new Bindable(); protected override void LoadComplete() { @@ -83,21 +81,19 @@ protected override void LoadComplete() samplesVisible.BindValueChanged(visible => this.FadeTo(visible.NewValue ? 1 : 0, 200, Easing.OutQuint)); this.FadeTo(samplesVisible.Value ? 1 : 0); - timelineZoom = timeline?.CurrentZoom.GetBoundCopy(); - timelineZoom?.BindValueChanged(zoom => - { - const float zoom_threshold = 40f; + if (timelineBlueprintContainer != null) + contracted.BindTo(timelineBlueprintContainer.SamplePointContracted); - if (zoom.NewValue < zoom_threshold) + contracted.BindValueChanged(v => + { + if (v.NewValue) { - contracted = true; Label.FadeOut(200, Easing.OutQuint); LabelContainer.ResizeTo(new Vector2(12), 200, Easing.OutQuint); LabelContainer.CornerRadius = 6; } else { - contracted = false; Label.FadeIn(200, Easing.OutQuint); LabelContainer.ResizeTo(new Vector2(Label.Width, 16), 200, Easing.OutQuint); LabelContainer.CornerRadius = 8; @@ -131,7 +127,7 @@ private void updateText() { Label.Text = $"{abbreviateBank(GetBankValue(GetSamples()))} {GetVolumeValue(GetSamples())}"; - if (!contracted) + if (!contracted.Value) LabelContainer.ResizeWidthTo(Label.Width, 200, Easing.OutQuint); } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index 740f0b6aac24..b50739c80fe9 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -15,16 +15,19 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; +using osu.Framework.Layout; using osu.Framework.Utils; using osu.Game.Graphics; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts; using osuTK; using osuTK.Graphics; namespace osu.Game.Screens.Edit.Compose.Components.Timeline { + [Cached] internal partial class TimelineBlueprintContainer : EditorBlueprintContainer { [Resolved(CanBeNull = true)] @@ -35,6 +38,8 @@ internal partial class TimelineBlueprintContainer : EditorBlueprintContainer private bool hitObjectDragged; + private readonly LayoutValue samplePointContractedStateCache = new LayoutValue(Invalidation.DrawSize); + /// /// Positional input must be received outside the container's bounds, /// in order to handle timeline blueprints which are stacked offscreen. @@ -49,6 +54,8 @@ public TimelineBlueprintContainer(HitObjectComposer composer) Origin = Anchor.Centre; Height = 0.6f; + + AddLayout(samplePointContractedStateCache); } [BackgroundDependencyLoader] @@ -116,11 +123,43 @@ protected override void Update() Composer.Playfield.FutureLifetimeExtension = timeline.VisibleRange / 2; } + updateSamplePointContractedState(); + base.Update(); updateStacking(); } + public Bindable SamplePointContracted = new Bindable(); + + private void updateSamplePointContractedState() + { + if (samplePointContractedStateCache.IsValid) + return; + + const double minimum_gap = 28; + + // Find the smallest time gap between any two sample point pieces + double smallestTimeGap = double.PositiveInfinity; + double lastTime = double.PositiveInfinity; + + // The blueprints are ordered in reverse chronological order + foreach (var selectionBlueprint in SelectionBlueprints) + { + var hitObject = selectionBlueprint.Item; + + if (hitObject is IHasRepeats hasRepeats) + smallestTimeGap = Math.Min(smallestTimeGap, hasRepeats.Duration / hasRepeats.SpanCount() / 2); + + smallestTimeGap = Math.Min(smallestTimeGap, lastTime - hitObject.GetEndTime()); + lastTime = hitObject.StartTime; + } + + double smallestAbsoluteGap = ((TimelineSelectionBlueprintContainer)SelectionBlueprints).ContentRelativeToAbsoluteFactor.X * smallestTimeGap; + SamplePointContracted.Value = smallestAbsoluteGap < minimum_gap; + samplePointContractedStateCache.Validate(); + } + private readonly Stack currentConcurrentObjects = new Stack(); private void updateStacking() @@ -288,6 +327,8 @@ protected partial class TimelineSelectionBlueprintContainer : Container> Content { get; } + public Vector2 ContentRelativeToAbsoluteFactor => Content.RelativeToAbsoluteFactor; + public TimelineSelectionBlueprintContainer() { AddInternal(new TimelinePart>(Content = new HitObjectOrderedSelectionContainer { RelativeSizeAxes = Axes.Both }) { RelativeSizeAxes = Axes.Both });