diff --git a/osu.Game.Tests/Visual/TestCaseEditorSeekSnapping.cs b/osu.Game.Tests/Visual/TestCaseEditorSeekSnapping.cs index e9e966a826..b70519a7bf 100644 --- a/osu.Game.Tests/Visual/TestCaseEditorSeekSnapping.cs +++ b/osu.Game.Tests/Visual/TestCaseEditorSeekSnapping.cs @@ -3,23 +3,20 @@ using System; using System.Collections.Generic; -using System.Linq; using osu.Framework.Allocation; -using osu.Framework.Audio.Track; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Framework.Timing; +using osu.Framework.Input; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Tools; -using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Screens.Compose; -using osu.Game.Tests.Beatmaps; using OpenTK; using OpenTK.Graphics; @@ -29,26 +26,13 @@ namespace osu.Game.Tests.Visual { public override IReadOnlyList RequiredTypes => new[] { typeof(HitObjectComposer) }; - private Track track; - private HitObjectComposer composer; - private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor(4); - private DecoupleableInterpolatingFramedClock clock; - - private DependencyContainer dependencies; - - protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) - => dependencies = new DependencyContainer(parent); + private EditorClock clock; [BackgroundDependencyLoader] - private void load(OsuGameBase osuGame) + private void load() { - clock = new DecoupleableInterpolatingFramedClock { IsCoupled = false }; - dependencies.CacheAs(clock); - dependencies.CacheAs(clock); - dependencies.Cache(beatDivisor); - var testBeatmap = new Beatmap { ControlPointInfo = new ControlPointInfo @@ -70,23 +54,9 @@ namespace osu.Game.Tests.Visual } }; - osuGame.Beatmap.Value = new TestWorkingBeatmap(testBeatmap); - track = osuGame.Beatmap.Value.Track; + clock = new EditorClock(testBeatmap.ControlPointInfo, beatDivisor) { IsCoupled = false }; - Child = new GridContainer - { - RelativeSizeAxes = Axes.Both, - Content = new[] - { - new Drawable[] { composer = new TestHitObjectComposer(new OsuRuleset()) }, - new Drawable[] { new TimingPointVisualiser(testBeatmap, track) { Clock = clock } }, - }, - RowDimensions = new[] - { - new Dimension(GridSizeMode.Distributed), - new Dimension(GridSizeMode.AutoSize), - } - }; + Child = new TimingPointVisualiser(testBeatmap, 5000) { Clock = clock }; testSeekNoSnapping(); testSeekSnappingOnBeat(); @@ -99,6 +69,15 @@ namespace osu.Game.Tests.Visual testSeekingWithFloatingPointBeatLength(); } + protected override bool OnWheel(InputState state) + { + if (state.Mouse.WheelDelta > 0) + clock.SeekBackward(true); + else + clock.SeekForward(true); + return true; + } + /// /// Tests whether time is correctly seeked without snapping. /// @@ -107,17 +86,17 @@ namespace osu.Game.Tests.Visual reset(); // Forwards - AddStep("Seek(0)", () => composer.SeekTo(0)); + AddStep("Seek(0)", () => clock.Seek(0)); AddAssert("Time = 0", () => clock.CurrentTime == 0); - AddStep("Seek(33)", () => composer.SeekTo(33)); + AddStep("Seek(33)", () => clock.Seek(33)); AddAssert("Time = 33", () => clock.CurrentTime == 33); - AddStep("Seek(89)", () => composer.SeekTo(89)); + AddStep("Seek(89)", () => clock.Seek(89)); AddAssert("Time = 89", () => clock.CurrentTime == 89); // Backwards - AddStep("Seek(25)", () => composer.SeekTo(25)); + AddStep("Seek(25)", () => clock.Seek(25)); AddAssert("Time = 25", () => clock.CurrentTime == 25); - AddStep("Seek(0)", () => composer.SeekTo(0)); + AddStep("Seek(0)", () => clock.Seek(0)); AddAssert("Time = 0", () => clock.CurrentTime == 0); } @@ -129,19 +108,19 @@ namespace osu.Game.Tests.Visual { reset(); - AddStep("Seek(0), Snap", () => composer.SeekTo(0, true)); + AddStep("Seek(0), Snap", () => clock.Seek(0, true)); AddAssert("Time = 0", () => clock.CurrentTime == 0); - AddStep("Seek(50), Snap", () => composer.SeekTo(50, true)); + AddStep("Seek(50), Snap", () => clock.Seek(50, true)); AddAssert("Time = 50", () => clock.CurrentTime == 50); - AddStep("Seek(100), Snap", () => composer.SeekTo(100, true)); + AddStep("Seek(100), Snap", () => clock.Seek(100, true)); AddAssert("Time = 100", () => clock.CurrentTime == 100); - AddStep("Seek(175), Snap", () => composer.SeekTo(175, true)); + AddStep("Seek(175), Snap", () => clock.Seek(175, true)); AddAssert("Time = 175", () => clock.CurrentTime == 175); - AddStep("Seek(350), Snap", () => composer.SeekTo(350, true)); + AddStep("Seek(350), Snap", () => clock.Seek(350, true)); AddAssert("Time = 350", () => clock.CurrentTime == 350); - AddStep("Seek(400), Snap", () => composer.SeekTo(400, true)); + AddStep("Seek(400), Snap", () => clock.Seek(400, true)); AddAssert("Time = 400", () => clock.CurrentTime == 400); - AddStep("Seek(450), Snap", () => composer.SeekTo(450, true)); + AddStep("Seek(450), Snap", () => clock.Seek(450, true)); AddAssert("Time = 450", () => clock.CurrentTime == 450); } @@ -154,17 +133,17 @@ namespace osu.Game.Tests.Visual { reset(); - AddStep("Seek(24), Snap", () => composer.SeekTo(24, true)); + AddStep("Seek(24), Snap", () => clock.Seek(24, true)); AddAssert("Time = 0", () => clock.CurrentTime == 0); - AddStep("Seek(26), Snap", () => composer.SeekTo(26, true)); + AddStep("Seek(26), Snap", () => clock.Seek(26, true)); AddAssert("Time = 50", () => clock.CurrentTime == 50); - AddStep("Seek(150), Snap", () => composer.SeekTo(150, true)); + AddStep("Seek(150), Snap", () => clock.Seek(150, true)); AddAssert("Time = 100", () => clock.CurrentTime == 100); - AddStep("Seek(170), Snap", () => composer.SeekTo(170, true)); + AddStep("Seek(170), Snap", () => clock.Seek(170, true)); AddAssert("Time = 175", () => clock.CurrentTime == 175); - AddStep("Seek(274), Snap", () => composer.SeekTo(274, true)); + AddStep("Seek(274), Snap", () => clock.Seek(274, true)); AddAssert("Time = 175", () => clock.CurrentTime == 175); - AddStep("Seek(276), Snap", () => composer.SeekTo(276, true)); + AddStep("Seek(276), Snap", () => clock.Seek(276, true)); AddAssert("Time = 350", () => clock.CurrentTime == 350); } @@ -175,15 +154,15 @@ namespace osu.Game.Tests.Visual { reset(); - AddStep("SeekForward", () => composer.SeekForward()); + AddStep("SeekForward", () => clock.SeekForward()); AddAssert("Time = 50", () => clock.CurrentTime == 50); - AddStep("SeekForward", () => composer.SeekForward()); + AddStep("SeekForward", () => clock.SeekForward()); AddAssert("Time = 100", () => clock.CurrentTime == 100); - AddStep("SeekForward", () => composer.SeekForward()); + AddStep("SeekForward", () => clock.SeekForward()); AddAssert("Time = 200", () => clock.CurrentTime == 200); - AddStep("SeekForward", () => composer.SeekForward()); + AddStep("SeekForward", () => clock.SeekForward()); AddAssert("Time = 400", () => clock.CurrentTime == 400); - AddStep("SeekForward", () => composer.SeekForward()); + AddStep("SeekForward", () => clock.SeekForward()); AddAssert("Time = 450", () => clock.CurrentTime == 450); } @@ -194,17 +173,17 @@ namespace osu.Game.Tests.Visual { reset(); - AddStep("SeekForward, Snap", () => composer.SeekForward(true)); + AddStep("SeekForward, Snap", () => clock.SeekForward(true)); AddAssert("Time = 50", () => clock.CurrentTime == 50); - AddStep("SeekForward, Snap", () => composer.SeekForward(true)); + AddStep("SeekForward, Snap", () => clock.SeekForward(true)); AddAssert("Time = 100", () => clock.CurrentTime == 100); - AddStep("SeekForward, Snap", () => composer.SeekForward(true)); + AddStep("SeekForward, Snap", () => clock.SeekForward(true)); AddAssert("Time = 175", () => clock.CurrentTime == 175); - AddStep("SeekForward, Snap", () => composer.SeekForward(true)); + AddStep("SeekForward, Snap", () => clock.SeekForward(true)); AddAssert("Time = 350", () => clock.CurrentTime == 350); - AddStep("SeekForward, Snap", () => composer.SeekForward(true)); + AddStep("SeekForward, Snap", () => clock.SeekForward(true)); AddAssert("Time = 400", () => clock.CurrentTime == 400); - AddStep("SeekForward, Snap", () => composer.SeekForward(true)); + AddStep("SeekForward, Snap", () => clock.SeekForward(true)); AddAssert("Time = 450", () => clock.CurrentTime == 450); } @@ -216,29 +195,29 @@ namespace osu.Game.Tests.Visual { reset(); - AddStep("Seek(49)", () => composer.SeekTo(49)); - AddStep("SeekForward, Snap", () => composer.SeekForward(true)); + AddStep("Seek(49)", () => clock.Seek(49)); + AddStep("SeekForward, Snap", () => clock.SeekForward(true)); AddAssert("Time = 50", () => clock.CurrentTime == 50); - AddStep("Seek(49.999)", () => composer.SeekTo(49.999)); - AddStep("SeekForward, Snap", () => composer.SeekForward(true)); + AddStep("Seek(49.999)", () => clock.Seek(49.999)); + AddStep("SeekForward, Snap", () => clock.SeekForward(true)); AddAssert("Time = 50", () => clock.CurrentTime == 50); - AddStep("Seek(99)", () => composer.SeekTo(99)); - AddStep("SeekForward, Snap", () => composer.SeekForward(true)); + AddStep("Seek(99)", () => clock.Seek(99)); + AddStep("SeekForward, Snap", () => clock.SeekForward(true)); AddAssert("Time = 100", () => clock.CurrentTime == 100); - AddStep("Seek(99.999)", () => composer.SeekTo(99.999)); - AddStep("SeekForward, Snap", () => composer.SeekForward(true)); + AddStep("Seek(99.999)", () => clock.Seek(99.999)); + AddStep("SeekForward, Snap", () => clock.SeekForward(true)); AddAssert("Time = 100", () => clock.CurrentTime == 100); - AddStep("Seek(174)", () => composer.SeekTo(174)); - AddStep("SeekForward, Snap", () => composer.SeekForward(true)); + AddStep("Seek(174)", () => clock.Seek(174)); + AddStep("SeekForward, Snap", () => clock.SeekForward(true)); AddAssert("Time = 175", () => clock.CurrentTime == 175); - AddStep("Seek(349)", () => composer.SeekTo(349)); - AddStep("SeekForward, Snap", () => composer.SeekForward(true)); + AddStep("Seek(349)", () => clock.Seek(349)); + AddStep("SeekForward, Snap", () => clock.SeekForward(true)); AddAssert("Time = 350", () => clock.CurrentTime == 350); - AddStep("Seek(399)", () => composer.SeekTo(399)); - AddStep("SeekForward, Snap", () => composer.SeekForward(true)); + AddStep("Seek(399)", () => clock.Seek(399)); + AddStep("SeekForward, Snap", () => clock.SeekForward(true)); AddAssert("Time = 400", () => clock.CurrentTime == 400); - AddStep("Seek(449)", () => composer.SeekTo(449)); - AddStep("SeekForward, Snap", () => composer.SeekForward(true)); + AddStep("Seek(449)", () => clock.Seek(449)); + AddStep("SeekForward, Snap", () => clock.SeekForward(true)); AddAssert("Time = 450", () => clock.CurrentTime == 450); } @@ -249,18 +228,18 @@ namespace osu.Game.Tests.Visual { reset(); - AddStep("Seek(450)", () => composer.SeekTo(450)); - AddStep("SeekBackward", () => composer.SeekBackward()); + AddStep("Seek(450)", () => clock.Seek(450)); + AddStep("SeekBackward", () => clock.SeekBackward()); AddAssert("Time = 425", () => clock.CurrentTime == 425); - AddStep("SeekBackward", () => composer.SeekBackward()); + AddStep("SeekBackward", () => clock.SeekBackward()); AddAssert("Time = 375", () => clock.CurrentTime == 375); - AddStep("SeekBackward", () => composer.SeekBackward()); + AddStep("SeekBackward", () => clock.SeekBackward()); AddAssert("Time = 325", () => clock.CurrentTime == 325); - AddStep("SeekBackward", () => composer.SeekBackward()); + AddStep("SeekBackward", () => clock.SeekBackward()); AddAssert("Time = 125", () => clock.CurrentTime == 125); - AddStep("SeekBackward", () => composer.SeekBackward()); + AddStep("SeekBackward", () => clock.SeekBackward()); AddAssert("Time = 25", () => clock.CurrentTime == 25); - AddStep("SeekBackward", () => composer.SeekBackward()); + AddStep("SeekBackward", () => clock.SeekBackward()); AddAssert("Time = 0", () => clock.CurrentTime == 0); } @@ -271,18 +250,18 @@ namespace osu.Game.Tests.Visual { reset(); - AddStep("Seek(450)", () => composer.SeekTo(450)); - AddStep("SeekBackward, Snap", () => composer.SeekBackward(true)); + AddStep("Seek(450)", () => clock.Seek(450)); + AddStep("SeekBackward, Snap", () => clock.SeekBackward(true)); AddAssert("Time = 400", () => clock.CurrentTime == 400); - AddStep("SeekBackward, Snap", () => composer.SeekBackward(true)); + AddStep("SeekBackward, Snap", () => clock.SeekBackward(true)); AddAssert("Time = 350", () => clock.CurrentTime == 350); - AddStep("SeekBackward, Snap", () => composer.SeekBackward(true)); + AddStep("SeekBackward, Snap", () => clock.SeekBackward(true)); AddAssert("Time = 175", () => clock.CurrentTime == 175); - AddStep("SeekBackward, Snap", () => composer.SeekBackward(true)); + AddStep("SeekBackward, Snap", () => clock.SeekBackward(true)); AddAssert("Time = 100", () => clock.CurrentTime == 100); - AddStep("SeekBackward, Snap", () => composer.SeekBackward(true)); + AddStep("SeekBackward, Snap", () => clock.SeekBackward(true)); AddAssert("Time = 50", () => clock.CurrentTime == 50); - AddStep("SeekBackward, Snap", () => composer.SeekBackward(true)); + AddStep("SeekBackward, Snap", () => clock.SeekBackward(true)); AddAssert("Time = 0", () => clock.CurrentTime == 0); } @@ -294,17 +273,17 @@ namespace osu.Game.Tests.Visual { reset(); - AddStep("Seek(451)", () => composer.SeekTo(451)); - AddStep("SeekBackward, Snap", () => composer.SeekBackward(true)); + AddStep("Seek(451)", () => clock.Seek(451)); + AddStep("SeekBackward, Snap", () => clock.SeekBackward(true)); AddAssert("Time = 450", () => clock.CurrentTime == 450); - AddStep("Seek(450.999)", () => composer.SeekTo(450.999)); - AddStep("SeekBackward, Snap", () => composer.SeekBackward(true)); + AddStep("Seek(450.999)", () => clock.Seek(450.999)); + AddStep("SeekBackward, Snap", () => clock.SeekBackward(true)); AddAssert("Time = 450", () => clock.CurrentTime == 450); - AddStep("Seek(401)", () => composer.SeekTo(401)); - AddStep("SeekBackward, Snap", () => composer.SeekBackward(true)); + AddStep("Seek(401)", () => clock.Seek(401)); + AddStep("SeekBackward, Snap", () => clock.SeekBackward(true)); AddAssert("Time = 400", () => clock.CurrentTime == 400); - AddStep("Seek(401.999)", () => composer.SeekTo(401.999)); - AddStep("SeekBackward, Snap", () => composer.SeekBackward(true)); + AddStep("Seek(401.999)", () => clock.Seek(401.999)); + AddStep("SeekBackward, Snap", () => clock.SeekBackward(true)); AddAssert("Time = 400", () => clock.CurrentTime == 400); } @@ -317,14 +296,14 @@ namespace osu.Game.Tests.Visual double lastTime = 0; - AddStep("Seek(0)", () => composer.SeekTo(0)); + AddStep("Seek(0)", () => clock.Seek(0)); for (int i = 0; i < 20; i++) { AddStep("SeekForward, Snap", () => { lastTime = clock.CurrentTime; - composer.SeekForward(true); + clock.SeekForward(true); }); AddAssert("Time > lastTime", () => clock.CurrentTime > lastTime); } @@ -334,7 +313,7 @@ namespace osu.Game.Tests.Visual AddStep("SeekBackward, Snap", () => { lastTime = clock.CurrentTime; - composer.SeekBackward(true); + clock.SeekBackward(true); }); AddAssert("Time < lastTime", () => clock.CurrentTime < lastTime); } @@ -344,7 +323,7 @@ namespace osu.Game.Tests.Visual private void reset() { - AddStep("Reset", () => composer.SeekTo(0)); + AddStep("Reset", () => clock.Seek(0)); } private class TestHitObjectComposer : HitObjectComposer @@ -359,13 +338,13 @@ namespace osu.Game.Tests.Visual private class TimingPointVisualiser : CompositeDrawable { - private readonly Track track; + private readonly double length; private readonly Drawable tracker; - public TimingPointVisualiser(Beatmap beatmap, Track track) + public TimingPointVisualiser(Beatmap beatmap, double length) { - this.track = track; + this.length = length; Anchor = Anchor.Centre; Origin = Anchor.Centre; @@ -417,7 +396,7 @@ namespace osu.Game.Tests.Visual for (int i = 0; i < timingPoints.Count; i++) { TimingControlPoint next = i == timingPoints.Count - 1 ? null : timingPoints[i + 1]; - timelineContainer.Add(new TimingPointTimeline(timingPoints[i], next?.Time ?? beatmap.HitObjects.Last().StartTime, track.Length)); + timelineContainer.Add(new TimingPointTimeline(timingPoints[i], next?.Time ?? length, length)); } } @@ -425,7 +404,7 @@ namespace osu.Game.Tests.Visual { base.Update(); - tracker.X = (float)(Time.Current / track.Length); + tracker.X = (float)(Time.Current / length); } private class TimingPointTimeline : CompositeDrawable diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 24a0a7e643..79ab67fafd 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -9,9 +9,7 @@ using osu.Framework.Allocation; using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Input; using osu.Framework.Logging; -using osu.Framework.MathUtils; using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Rulesets.Edit.Tools; @@ -35,8 +33,6 @@ namespace osu.Game.Rulesets.Edit private readonly Bindable beatmap = new Bindable(); private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor(); - private IAdjustableClock adjustableClock; - protected HitObjectComposer(Ruleset ruleset) { this.ruleset = ruleset; @@ -45,10 +41,8 @@ namespace osu.Game.Rulesets.Edit } [BackgroundDependencyLoader(true)] - private void load([NotNull] OsuGameBase osuGame, [NotNull] IAdjustableClock adjustableClock, [NotNull] IFrameBasedClock framedClock, [CanBeNull] BindableBeatDivisor beatDivisor) + private void load([NotNull] OsuGameBase osuGame, [NotNull] IFrameBasedClock framedClock, [CanBeNull] BindableBeatDivisor beatDivisor) { - this.adjustableClock = adjustableClock; - if (beatDivisor != null) this.beatDivisor.BindTo(beatDivisor); @@ -148,106 +142,6 @@ namespace osu.Game.Rulesets.Edit }); } - protected override bool OnWheel(InputState state) - { - if (state.Mouse.WheelDelta > 0) - SeekBackward(true); - else - SeekForward(true); - return true; - } - - /// - /// Seeks the current time one beat-snapped beat-length backwards. - /// - /// Whether to snap to the closest beat. - public void SeekBackward(bool snapped = false) => seek(-1, snapped); - - /// - /// Seeks the current time one beat-snapped beat-length forwards. - /// - /// Whether to snap to the closest beat. - public void SeekForward(bool snapped = false) => seek(1, snapped); - - private void seek(int direction, bool snapped) - { - var cpi = beatmap.Value.Beatmap.ControlPointInfo; - - var timingPoint = cpi.TimingPointAt(adjustableClock.CurrentTime); - if (direction < 0 && timingPoint.Time == adjustableClock.CurrentTime) - { - // When going backwards and we're at the boundary of two timing points, we compute the seek distance with the timing point which we are seeking into - int activeIndex = cpi.TimingPoints.IndexOf(timingPoint); - while (activeIndex > 0 && adjustableClock.CurrentTime == timingPoint.Time) - timingPoint = cpi.TimingPoints[--activeIndex]; - } - - double seekAmount = timingPoint.BeatLength / beatDivisor; - double seekTime = adjustableClock.CurrentTime + seekAmount * direction; - - if (!snapped || cpi.TimingPoints.Count == 0) - { - adjustableClock.Seek(seekTime); - return; - } - - // We will be snapping to beats within timingPoint - seekTime -= timingPoint.Time; - - // Determine the index from timingPoint of the closest beat to seekTime, accounting for scrolling direction - int closestBeat; - if (direction > 0) - closestBeat = (int)Math.Floor(seekTime / seekAmount); - else - closestBeat = (int)Math.Ceiling(seekTime / seekAmount); - - seekTime = timingPoint.Time + closestBeat * seekAmount; - - // Due to the rounding above, we may end up on the current beat. This will effectively cause 0 seeking to happen, but we don't want this. - // Instead, we'll go to the next beat in the direction when this is the case - if (Precision.AlmostEquals(adjustableClock.CurrentTime, seekTime)) - { - closestBeat += direction > 0 ? 1 : -1; - seekTime = timingPoint.Time + closestBeat * seekAmount; - } - - if (seekTime < timingPoint.Time && timingPoint != cpi.TimingPoints.First()) - seekTime = timingPoint.Time; - - var nextTimingPoint = cpi.TimingPoints.FirstOrDefault(t => t.Time > timingPoint.Time); - if (seekTime > nextTimingPoint?.Time) - seekTime = nextTimingPoint.Time; - - adjustableClock.Seek(seekTime); - } - - public void SeekTo(double seekTime, bool snapped = false) - { - if (!snapped) - { - adjustableClock.Seek(seekTime); - return; - } - - var timingPoint = beatmap.Value.Beatmap.ControlPointInfo.TimingPointAt(seekTime); - double beatSnapLength = timingPoint.BeatLength / beatDivisor; - - // We will be snapping to beats within the timing point - seekTime -= timingPoint.Time; - - // Determine the index from the current timing point of the closest beat to seekTime - int closestBeat = (int)Math.Round(seekTime / beatSnapLength); - seekTime = timingPoint.Time + closestBeat * beatSnapLength; - - // Depending on beatSnapLength, we may snap to a beat that is beyond timingPoint's end time, but we want to instead snap to - // the next timing point's start time - var nextTimingPoint = beatmap.Value.Beatmap.ControlPointInfo.TimingPoints.FirstOrDefault(t => t.Time > timingPoint.Time); - if (seekTime > nextTimingPoint?.Time) - seekTime = nextTimingPoint.Time; - - adjustableClock.Seek(seekTime); - } - private void setCompositionTool(ICompositionTool tool) => CurrentTool = tool; protected virtual RulesetContainer CreateRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap) => ruleset.CreateRulesetContainerWith(beatmap, true); diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs index b249713581..9efe93c5a7 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs @@ -55,11 +55,9 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts if (Beatmap.Value.Track.Length == double.PositiveInfinity) return; float markerPos = MathHelper.Clamp(ToLocalSpace(screenPosition).X, 0, DrawWidth); - seekTo(markerPos / DrawWidth * Beatmap.Value.Track.Length); + adjustableClock.Seek(markerPos / DrawWidth * Beatmap.Value.Track.Length); } - private void seekTo(double time) => adjustableClock.Seek(time); - protected override void Update() { base.Update(); diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs index 0e80c13257..b368b92e42 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs @@ -17,8 +17,15 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary /// public class SummaryTimeline : BottomBarContainer { + private readonly IAdjustableClock adjustableClock; + + public SummaryTimeline(IAdjustableClock adjustableClock) + { + this.adjustableClock = adjustableClock; + } + [BackgroundDependencyLoader] - private void load(OsuColour colours, IAdjustableClock adjustableClock) + private void load(OsuColour colours) { TimelinePart markerPart, controlPointPart, bookmarkPart, breakPart; diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 8b651000fd..8a8932970c 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -12,6 +12,7 @@ using osu.Game.Screens.Edit.Menus; using osu.Game.Screens.Edit.Components.Timelines.Summary; using osu.Framework.Allocation; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input; using osu.Framework.Timing; using osu.Game.Graphics.UserInterface; using osu.Game.Screens.Edit.Screens; @@ -32,6 +33,10 @@ namespace osu.Game.Screens.Edit private EditorScreen currentScreen; + private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor(); + + private EditorClock clock; + private DependencyContainer dependencies; protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) @@ -42,11 +47,11 @@ namespace osu.Game.Screens.Edit { // TODO: should probably be done at a RulesetContainer level to share logic with Player. var sourceClock = (IAdjustableClock)Beatmap.Value.Track ?? new StopwatchClock(); - var adjustableClock = new DecoupleableInterpolatingFramedClock { IsCoupled = false }; - adjustableClock.ChangeSource(sourceClock); + clock = new EditorClock(Beatmap.Value.Beatmap.ControlPointInfo, beatDivisor) { IsCoupled = false }; + clock.ChangeSource(sourceClock); - dependencies.CacheAs(adjustableClock); - dependencies.CacheAs(adjustableClock); + dependencies.CacheAs(clock); + dependencies.CacheAs(clock); EditorMenuBar menuBar; TimeInfoContainer timeInfo; @@ -123,7 +128,7 @@ namespace osu.Game.Screens.Edit Padding = new MarginPadding { Right = 10 }, Child = timeInfo = new TimeInfoContainer { RelativeSizeAxes = Axes.Both }, }, - timeline = new SummaryTimeline + timeline = new SummaryTimeline(clock) { RelativeSizeAxes = Axes.Both, }, @@ -147,7 +152,6 @@ namespace osu.Game.Screens.Edit menuBar.Mode.ValueChanged += onModeChanged; bottomBackground.Colour = colours.Gray2; - } private void exportBeatmap() @@ -176,6 +180,15 @@ namespace osu.Game.Screens.Edit screenContainer.Add(currentScreen); } + protected override bool OnWheel(InputState state) + { + if (state.Mouse.WheelDelta > 0) + clock.SeekBackward(true); + else + clock.SeekForward(true); + return true; + } + protected override void OnResuming(Screen last) { Beatmap.Value.Track?.Stop(); diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs new file mode 100644 index 0000000000..ddde819757 --- /dev/null +++ b/osu.Game/Screens/Edit/EditorClock.cs @@ -0,0 +1,107 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Linq; +using osu.Framework.MathUtils; +using osu.Framework.Timing; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Screens.Edit.Screens.Compose; + +namespace osu.Game.Screens.Edit +{ + public class EditorClock : DecoupleableInterpolatingFramedClock + { + private readonly ControlPointInfo controlPointInfo; + private readonly BindableBeatDivisor beatDivisor; + + public EditorClock(ControlPointInfo controlPointInfo, BindableBeatDivisor beatDivisor) + { + this.controlPointInfo = controlPointInfo; + this.beatDivisor = beatDivisor; + } + + public bool SeekSnapped(double position) + { + var timingPoint = controlPointInfo.TimingPointAt(position); + double beatSnapLength = timingPoint.BeatLength / beatDivisor; + + // We will be snapping to beats within the timing point + position -= timingPoint.Time; + + // Determine the index from the current timing point of the closest beat to position + int closestBeat = (int)Math.Round(position / beatSnapLength); + position = timingPoint.Time + closestBeat * beatSnapLength; + + // Depending on beatSnapLength, we may snap to a beat that is beyond timingPoint's end time, but we want to instead snap to + // the next timing point's start time + var nextTimingPoint = controlPointInfo.TimingPoints.FirstOrDefault(t => t.Time > timingPoint.Time); + if (position > nextTimingPoint?.Time) + position = nextTimingPoint.Time; + + return Seek(position); + } + + /// + /// Seeks the current time one beat-snapped beat-length backwards. + /// + /// Whether to snap to the closest beat. + public void SeekBackward(bool snapped = false) => seek(-1, snapped); + + /// + /// Seeks the current time one beat-snapped beat-length forwards. + /// + /// Whether to snap to the closest beat. + public void SeekForward(bool snapped = false) => seek(1, snapped); + + private void seek(int direction, bool snapped) + { + var timingPoint = controlPointInfo.TimingPointAt(CurrentTime); + if (direction < 0 && timingPoint.Time == CurrentTime) + { + // When going backwards and we're at the boundary of two timing points, we compute the seek distance with the timing point which we are seeking into + int activeIndex = controlPointInfo.TimingPoints.IndexOf(timingPoint); + while (activeIndex > 0 && CurrentTime == timingPoint.Time) + timingPoint = controlPointInfo.TimingPoints[--activeIndex]; + } + + double seekAmount = timingPoint.BeatLength / beatDivisor; + double seekTime = CurrentTime + seekAmount * direction; + + if (!snapped || controlPointInfo.TimingPoints.Count == 0) + { + Seek(seekTime); + return; + } + + // We will be snapping to beats within timingPoint + seekTime -= timingPoint.Time; + + // Determine the index from timingPoint of the closest beat to seekTime, accounting for scrolling direction + int closestBeat; + if (direction > 0) + closestBeat = (int)Math.Floor(seekTime / seekAmount); + else + closestBeat = (int)Math.Ceiling(seekTime / seekAmount); + + seekTime = timingPoint.Time + closestBeat * seekAmount; + + // Due to the rounding above, we may end up on the current beat. This will effectively cause 0 seeking to happen, but we don't want this. + // Instead, we'll go to the next beat in the direction when this is the case + if (Precision.AlmostEquals(CurrentTime, seekTime)) + { + closestBeat += direction > 0 ? 1 : -1; + seekTime = timingPoint.Time + closestBeat * seekAmount; + } + + if (seekTime < timingPoint.Time && timingPoint != controlPointInfo.TimingPoints.First()) + seekTime = timingPoint.Time; + + var nextTimingPoint = controlPointInfo.TimingPoints.FirstOrDefault(t => t.Time > timingPoint.Time); + if (seekTime > nextTimingPoint?.Time) + seekTime = nextTimingPoint.Time; + + Seek(seekTime); + } + } +}