From 3ab083b696906a03a6c6cd575ab81b8fe5541c39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 19 Oct 2023 09:56:40 +0200 Subject: [PATCH 1/5] Split `IPositionSnapProvider` from `IDistanceSnapProvider` In preparation to remove `DistancedHitObjectComposer`, split off `IPositionSnapProvider` from `IDistanceSnapProvider`. `DistancedHitObjectComposer` was not touching `IPositionSnapProvider`'s only interface member at all, it was just forwarding it for subclasses to override to their own leisure. --- .../Components/PathControlPointVisualiser.cs | 13 ++++++++----- .../Blueprints/Sliders/SliderPlacementBlueprint.cs | 9 ++++++--- .../Blueprints/Sliders/SliderSelectionBlueprint.cs | 11 +++++++---- osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs | 2 +- 4 files changed, 22 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index f624ebc8b5..f891d23bbd 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -47,7 +47,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components public Action> SplitControlPointsRequested; [Resolved(CanBeNull = true)] - private IDistanceSnapProvider snapProvider { get; set; } + private IPositionSnapProvider positionSnapProvider { get; set; } + + [Resolved(CanBeNull = true)] + private IDistanceSnapProvider distanceSnapProvider { get; set; } public PathControlPointVisualiser(T hitObject, bool allowSelection) { @@ -289,7 +292,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { // Special handling for selections containing head control point - the position of the hit object changes which means the snapped position and time have to be taken into account Vector2 newHeadPosition = Parent!.ToScreenSpace(e.MousePosition + (dragStartPositions[0] - dragStartPositions[draggedControlPointIndex])); - var result = snapProvider?.FindSnappedPositionAndTime(newHeadPosition); + var result = positionSnapProvider?.FindSnappedPositionAndTime(newHeadPosition); Vector2 movementDelta = Parent!.ToLocalSpace(result?.ScreenSpacePosition ?? newHeadPosition) - hitObject.Position; @@ -309,7 +312,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components } else { - var result = snapProvider?.FindSnappedPositionAndTime(Parent!.ToScreenSpace(e.MousePosition), SnapType.GlobalGrids); + var result = positionSnapProvider?.FindSnappedPositionAndTime(Parent!.ToScreenSpace(e.MousePosition), SnapType.GlobalGrids); Vector2 movementDelta = Parent!.ToLocalSpace(result?.ScreenSpacePosition ?? Parent!.ToScreenSpace(e.MousePosition)) - dragStartPositions[draggedControlPointIndex] - hitObject.Position; @@ -322,7 +325,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components } // Snap the path to the current beat divisor before checking length validity. - hitObject.SnapTo(snapProvider); + hitObject.SnapTo(distanceSnapProvider); if (!hitObject.Path.HasValidLength) { @@ -332,7 +335,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components hitObject.Position = oldPosition; hitObject.StartTime = oldStartTime; // Snap the path length again to undo the invalid length. - hitObject.SnapTo(snapProvider); + hitObject.SnapTo(distanceSnapProvider); return; } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index ba7b855511..9b6adc04cf 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -39,7 +39,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders private int currentSegmentLength; [Resolved(CanBeNull = true)] - private IDistanceSnapProvider snapProvider { get; set; } + private IPositionSnapProvider positionSnapProvider { get; set; } + + [Resolved(CanBeNull = true)] + private IDistanceSnapProvider distanceSnapProvider { get; set; } protected override bool IsValidForPlacement => HitObject.Path.HasValidLength; @@ -198,7 +201,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders } // Update the cursor position. - var result = snapProvider?.FindSnappedPositionAndTime(inputManager.CurrentState.Mouse.Position, state == SliderPlacementState.Body ? SnapType.GlobalGrids : SnapType.All); + var result = positionSnapProvider?.FindSnappedPositionAndTime(inputManager.CurrentState.Mouse.Position, state == SliderPlacementState.Body ? SnapType.GlobalGrids : SnapType.All); cursor.Position = ToLocalSpace(result?.ScreenSpacePosition ?? inputManager.CurrentState.Mouse.Position) - HitObject.Position; } else if (cursor != null) @@ -230,7 +233,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders private void updateSlider() { - HitObject.Path.ExpectedDistance.Value = snapProvider?.FindSnappedDistance(HitObject, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance; + HitObject.Path.ExpectedDistance.Value = distanceSnapProvider?.FindSnappedDistance(HitObject, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance; bodyPiece.UpdateFrom(HitObject); headCirclePiece.UpdateFrom(HitObject.HeadCircle); diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 02023decd6..80c4cee7f2 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -40,7 +40,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders protected PathControlPointVisualiser ControlPointVisualiser { get; private set; } [Resolved(CanBeNull = true)] - private IDistanceSnapProvider snapProvider { get; set; } + private IPositionSnapProvider positionSnapProvider { get; set; } + + [Resolved(CanBeNull = true)] + private IDistanceSnapProvider distanceSnapProvider { get; set; } [Resolved(CanBeNull = true)] private IPlacementHandler placementHandler { get; set; } @@ -194,7 +197,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders { if (placementControlPoint != null) { - var result = snapProvider?.FindSnappedPositionAndTime(ToScreenSpace(e.MousePosition)); + var result = positionSnapProvider?.FindSnappedPositionAndTime(ToScreenSpace(e.MousePosition)); placementControlPoint.Position = ToLocalSpace(result?.ScreenSpacePosition ?? ToScreenSpace(e.MousePosition)) - HitObject.Position; } } @@ -245,7 +248,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders // Move the control points from the insertion index onwards to make room for the insertion controlPoints.Insert(insertionIndex, pathControlPoint); - HitObject.SnapTo(snapProvider); + HitObject.SnapTo(distanceSnapProvider); return pathControlPoint; } @@ -267,7 +270,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders } // Snap the slider to the current beat divisor before checking length validity. - HitObject.SnapTo(snapProvider); + HitObject.SnapTo(distanceSnapProvider); // If there are 0 or 1 remaining control points, or the slider has an invalid length, it is in a degenerate form and should be deleted if (controlPoints.Count <= 1 || !HitObject.Path.HasValidLength) diff --git a/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs b/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs index da44b42831..af9d02f5c4 100644 --- a/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs +++ b/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs @@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Edit /// A snap provider which given a reference hit object and proposed distance from it, offers a more correct duration or distance value. /// [Cached] - public interface IDistanceSnapProvider : IPositionSnapProvider + public interface IDistanceSnapProvider { /// /// A multiplier which changes the ratio of distance travelled per time unit. From dcfd6a0a8a9ad298fe63ea988dbe6f70dae3cfad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 19 Oct 2023 11:20:10 +0200 Subject: [PATCH 2/5] Remove `DistancedHitObjectComposer` inheritance from osu! composer --- .../Editor/TestSceneOsuDistanceSnapGrid.cs | 28 +- .../Edit/OsuDistanceSnapProvider.cs | 31 ++ .../Edit/OsuHitObjectComposer.cs | 44 +-- ...tSceneHitObjectComposerDistanceSnapping.cs | 13 +- .../Editing/TestSceneDistanceSnapGrid.cs | 4 +- .../Edit/ComposerDistanceSnapProvider.cs | 311 ++++++++++++++++++ .../Edit/DistancedHitObjectComposer.cs | 4 +- .../Rulesets/Edit/IDistanceSnapProvider.cs | 2 +- 8 files changed, 392 insertions(+), 45 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapProvider.cs create mode 100644 osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs index 9338d5453d..b70f932913 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs @@ -47,8 +47,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor [Cached] private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor(); - [Cached(typeof(IDistanceSnapProvider))] - private readonly OsuHitObjectComposer snapProvider = new OsuHitObjectComposer(new OsuRuleset()) + private readonly TestHitObjectComposer composer = new TestHitObjectComposer { // Just used for the snap implementation, so let's hide from vision. AlwaysPresent = true, @@ -71,11 +70,18 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor base.Content.Children = new Drawable[] { editorClock = new EditorClock(editorBeatmap), - new PopoverContainer { Child = snapProvider }, + new PopoverContainer { Child = composer }, Content }; } + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) + { + var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); + dependencies.CacheAs(composer.DistanceSnapProvider); + return dependencies; + } + protected override Container Content { get; } = new PopoverContainer { RelativeSizeAxes = Axes.Both }; [SetUp] @@ -84,7 +90,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor editorBeatmap.Difficulty.SliderMultiplier = 1; editorBeatmap.ControlPointInfo.Clear(); editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = beat_length }); - snapProvider.DistanceSpacingMultiplier.Value = 1; + composer.DistanceSnapProvider.DistanceSpacingMultiplier.Value = 1; Children = new Drawable[] { @@ -116,7 +122,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor [TestCase(0.5f)] public void TestDistanceSpacing(float multiplier) { - AddStep($"set distance spacing = {multiplier}", () => snapProvider.DistanceSpacingMultiplier.Value = multiplier); + AddStep($"set distance spacing = {multiplier}", () => composer.DistanceSnapProvider.DistanceSpacingMultiplier.Value = multiplier); } [Test] @@ -153,7 +159,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor [TestCase(2f, beat_length * 2)] public void TestDistanceSpacingAdjust(float multiplier, float expectedDistance) { - AddStep($"Set distance spacing to {multiplier}", () => snapProvider.DistanceSpacingMultiplier.Value = multiplier); + AddStep($"Set distance spacing to {multiplier}", () => composer.DistanceSnapProvider.DistanceSpacingMultiplier.Value = multiplier); AddStep("move mouse to point", () => InputManager.MoveMouseTo(grid.ToScreenSpace(grid_position + new Vector2(beat_length, 0) * 2))); assertSnappedDistance(expectedDistance); @@ -266,5 +272,15 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor cursor.Position = LastSnappedPosition = GetSnapPosition.Invoke(inputManager.CurrentState.Mouse.Position); } } + + private partial class TestHitObjectComposer : OsuHitObjectComposer + { + public new IDistanceSnapProvider DistanceSnapProvider => base.DistanceSnapProvider; + + public TestHitObjectComposer() + : base(new OsuRuleset()) + { + } + } } } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapProvider.cs b/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapProvider.cs new file mode 100644 index 0000000000..522943df7d --- /dev/null +++ b/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapProvider.cs @@ -0,0 +1,31 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Graphics.UserInterface; +using osu.Game.Input.Bindings; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Objects; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Edit +{ + public partial class OsuDistanceSnapProvider : ComposerDistanceSnapProvider + { + protected override double ReadCurrentDistanceSnap(HitObject before, HitObject after) + { + float expectedDistance = DurationToDistance(before, after.StartTime - before.GetEndTime()); + float actualDistance = Vector2.Distance(((OsuHitObject)before).EndPosition, ((OsuHitObject)after).Position); + + return actualDistance / expectedDistance; + } + + protected override bool AdjustDistanceSpacing(GlobalAction action, float amount) + { + // To allow better visualisation, ensure that the spacing grid is visible before adjusting. + DistanceSnapToggle.Value = TernaryState.True; + + return base.AdjustDistanceSpacing(action, amount); + } + } +} diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index fdc11be42c..0f8c960b65 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -17,7 +17,6 @@ using osu.Framework.Input.Events; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; -using osu.Game.Input.Bindings; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Mods; @@ -30,7 +29,7 @@ using osuTK; namespace osu.Game.Rulesets.Osu.Edit { - public partial class OsuHitObjectComposer : DistancedHitObjectComposer + public partial class OsuHitObjectComposer : HitObjectComposer { public OsuHitObjectComposer(Ruleset ruleset) : base(ruleset) @@ -49,18 +48,27 @@ namespace osu.Game.Rulesets.Osu.Edit private readonly Bindable rectangularGridSnapToggle = new Bindable(); - protected override IEnumerable CreateTernaryButtons() => base.CreateTernaryButtons().Concat(new[] - { - new TernaryButton(rectangularGridSnapToggle, "Grid Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Th }) - }); + protected override IEnumerable CreateTernaryButtons() + => base.CreateTernaryButtons() + .Concat(DistanceSnapProvider.CreateTernaryButtons()) + .Concat(new[] + { + new TernaryButton(rectangularGridSnapToggle, "Grid Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Th }) + }); private BindableList selectedHitObjects; private Bindable placementObject; + [Cached(typeof(IDistanceSnapProvider))] + protected readonly OsuDistanceSnapProvider DistanceSnapProvider = new OsuDistanceSnapProvider(); + [BackgroundDependencyLoader] private void load() { + AddInternal(DistanceSnapProvider); + DistanceSnapProvider.AttachToToolbox(RightToolbox); + // Give a bit of breathing room around the playfield content. PlayfieldContentContainer.Padding = new MarginPadding(10); @@ -81,7 +89,7 @@ namespace osu.Game.Rulesets.Osu.Edit placementObject = EditorBeatmap.PlacementObject.GetBoundCopy(); placementObject.ValueChanged += _ => updateDistanceSnapGrid(); - DistanceSnapToggle.ValueChanged += _ => updateDistanceSnapGrid(); + DistanceSnapProvider.DistanceSnapToggle.ValueChanged += _ => updateDistanceSnapGrid(); // we may be entering the screen with a selection already active updateDistanceSnapGrid(); @@ -106,14 +114,6 @@ namespace osu.Game.Rulesets.Osu.Edit private RectangularPositionSnapGrid rectangularPositionSnapGrid; - protected override double ReadCurrentDistanceSnap(HitObject before, HitObject after) - { - float expectedDistance = DurationToDistance(before, after.StartTime - before.GetEndTime()); - float actualDistance = Vector2.Distance(((OsuHitObject)before).EndPosition, ((OsuHitObject)after).Position); - - return actualDistance / expectedDistance; - } - protected override void Update() { base.Update(); @@ -143,7 +143,7 @@ namespace osu.Game.Rulesets.Osu.Edit // We want to ensure that in this particular case, the time-snapping component of distance snap is still applied. // The easiest way to ensure this is to attempt application of distance snap after a nearby object is found, and copy over // the time value if the proposed positions are roughly the same. - if (snapType.HasFlagFast(SnapType.RelativeGrids) && DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null) + if (snapType.HasFlagFast(SnapType.RelativeGrids) && DistanceSnapProvider.DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null) { (Vector2 distanceSnappedPosition, double distanceSnappedTime) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(snapResult.ScreenSpacePosition)); if (Precision.AlmostEquals(distanceSnapGrid.ToScreenSpace(distanceSnappedPosition), snapResult.ScreenSpacePosition, 1)) @@ -157,7 +157,7 @@ namespace osu.Game.Rulesets.Osu.Edit if (snapType.HasFlagFast(SnapType.RelativeGrids)) { - if (DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null) + if (DistanceSnapProvider.DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null) { (Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition)); @@ -220,7 +220,7 @@ namespace osu.Game.Rulesets.Osu.Edit distanceSnapGridCache.Invalidate(); distanceSnapGrid = null; - if (DistanceSnapToggle.Value != TernaryState.True) + if (DistanceSnapProvider.DistanceSnapToggle.Value != TernaryState.True) return; switch (BlueprintContainer.CurrentTool) @@ -262,14 +262,6 @@ namespace osu.Game.Rulesets.Osu.Edit base.OnKeyUp(e); } - protected override bool AdjustDistanceSpacing(GlobalAction action, float amount) - { - // To allow better visualisation, ensure that the spacing grid is visible before adjusting. - DistanceSnapToggle.Value = TernaryState.True; - - return base.AdjustDistanceSpacing(action, amount); - } - private bool gridSnapMomentary; private void handleToggleViaKey(KeyboardEvent key) diff --git a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs index 6b78eea009..463287fb35 100644 --- a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs +++ b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs @@ -3,7 +3,6 @@ using NUnit.Framework; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; @@ -230,25 +229,25 @@ namespace osu.Game.Tests.Editing } private void assertSnapDistance(float expectedDistance, HitObject? referenceObject, bool includeSliderVelocity) - => AddAssert($"distance is {expectedDistance}", () => composer.GetBeatSnapDistanceAt(referenceObject ?? new HitObject(), includeSliderVelocity), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON)); + => AddAssert($"distance is {expectedDistance}", () => composer.DistanceSnapProvider.GetBeatSnapDistanceAt(referenceObject ?? new HitObject(), includeSliderVelocity), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON)); private void assertDurationToDistance(double duration, float expectedDistance, HitObject? referenceObject = null) - => AddAssert($"duration = {duration} -> distance = {expectedDistance}", () => composer.DurationToDistance(referenceObject ?? new HitObject(), duration), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON)); + => AddAssert($"duration = {duration} -> distance = {expectedDistance}", () => composer.DistanceSnapProvider.DurationToDistance(referenceObject ?? new HitObject(), duration), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON)); private void assertDistanceToDuration(float distance, double expectedDuration, HitObject? referenceObject = null) - => AddAssert($"distance = {distance} -> duration = {expectedDuration}", () => composer.DistanceToDuration(referenceObject ?? new HitObject(), distance), () => Is.EqualTo(expectedDuration).Within(Precision.FLOAT_EPSILON)); + => AddAssert($"distance = {distance} -> duration = {expectedDuration}", () => composer.DistanceSnapProvider.DistanceToDuration(referenceObject ?? new HitObject(), distance), () => Is.EqualTo(expectedDuration).Within(Precision.FLOAT_EPSILON)); private void assertSnappedDuration(float distance, double expectedDuration, HitObject? referenceObject = null) - => AddAssert($"distance = {distance} -> duration = {expectedDuration} (snapped)", () => composer.FindSnappedDuration(referenceObject ?? new HitObject(), distance), () => Is.EqualTo(expectedDuration).Within(Precision.FLOAT_EPSILON)); + => AddAssert($"distance = {distance} -> duration = {expectedDuration} (snapped)", () => composer.DistanceSnapProvider.FindSnappedDuration(referenceObject ?? new HitObject(), distance), () => Is.EqualTo(expectedDuration).Within(Precision.FLOAT_EPSILON)); private void assertSnappedDistance(float distance, float expectedDistance, HitObject? referenceObject = null) - => AddAssert($"distance = {distance} -> distance = {expectedDistance} (snapped)", () => composer.FindSnappedDistance(referenceObject ?? new HitObject(), distance), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON)); + => AddAssert($"distance = {distance} -> distance = {expectedDistance} (snapped)", () => composer.DistanceSnapProvider.FindSnappedDistance(referenceObject ?? new HitObject(), distance), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON)); private partial class TestHitObjectComposer : OsuHitObjectComposer { public new EditorBeatmap EditorBeatmap => base.EditorBeatmap; - public new Bindable DistanceSpacingMultiplier => base.DistanceSpacingMultiplier; + public new IDistanceSnapProvider DistanceSnapProvider => base.DistanceSnapProvider; public TestHitObjectComposer() : base(new OsuRuleset()) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs index 70e4420a45..f2a015402a 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs @@ -187,11 +187,9 @@ namespace osu.Game.Tests.Visual.Editing private class SnapProvider : IDistanceSnapProvider { - public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.AllGrids) => new SnapResult(screenSpacePosition, 0); - public Bindable DistanceSpacingMultiplier { get; } = new BindableDouble(1); - IBindable IDistanceSnapProvider.DistanceSpacingMultiplier => DistanceSpacingMultiplier; + Bindable IDistanceSnapProvider.DistanceSpacingMultiplier => DistanceSpacingMultiplier; public float GetBeatSnapDistanceAt(HitObject referenceObject, bool useReferenceSliderVelocity = true) => beat_snap_distance; diff --git a/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs b/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs new file mode 100644 index 0000000000..0b1809e7d9 --- /dev/null +++ b/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs @@ -0,0 +1,311 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions; +using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; +using osu.Framework.Localisation; +using osu.Framework.Utils; +using osu.Game.Configuration; +using osu.Game.Graphics.UserInterface; +using osu.Game.Input.Bindings; +using osu.Game.Overlays; +using osu.Game.Overlays.OSD; +using osu.Game.Overlays.Settings.Sections; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.UI; +using osu.Game.Screens.Edit; +using osu.Game.Screens.Edit.Components.TernaryButtons; + +namespace osu.Game.Rulesets.Edit +{ + public abstract partial class ComposerDistanceSnapProvider : Component, IDistanceSnapProvider, IScrollBindingHandler + { + private const float adjust_step = 0.1f; + + public BindableDouble DistanceSpacingMultiplier { get; } = new BindableDouble(1.0) + { + MinValue = 0.1, + MaxValue = 6.0, + Precision = 0.01, + }; + + Bindable IDistanceSnapProvider.DistanceSpacingMultiplier => DistanceSpacingMultiplier; + + private ExpandableSlider> distanceSpacingSlider = null!; + private ExpandableButton currentDistanceSpacingButton = null!; + + [Resolved] + private Playfield playfield { get; set; } = null!; + + [Resolved] + private EditorClock editorClock { get; set; } = null!; + + [Resolved] + private EditorBeatmap editorBeatmap { get; set; } = null!; + + [Resolved] + private IBeatSnapProvider beatSnapProvider { get; set; } = null!; + + [Resolved] + private OnScreenDisplay? onScreenDisplay { get; set; } + + public readonly Bindable DistanceSnapToggle = new Bindable(); + + private bool distanceSnapMomentary; + + private EditorToolboxGroup? toolboxGroup; + + public void AttachToToolbox(ExpandingToolboxContainer toolboxContainer) + { + if (toolboxGroup != null) + throw new InvalidOperationException($"{nameof(AttachToToolbox)} may be called only once for a single {nameof(ComposerDistanceSnapProvider)} instance."); + + toolboxContainer.Add(toolboxGroup = new EditorToolboxGroup("snapping") + { + Alpha = DistanceSpacingMultiplier.Disabled ? 0 : 1, + Children = new Drawable[] + { + distanceSpacingSlider = new ExpandableSlider> + { + KeyboardStep = adjust_step, + // Manual binding in LoadComplete to handle one-way event flow. + Current = DistanceSpacingMultiplier.GetUnboundCopy(), + }, + currentDistanceSpacingButton = new ExpandableButton + { + Action = () => + { + (HitObject before, HitObject after)? objects = getObjectsOnEitherSideOfCurrentTime(); + + Debug.Assert(objects != null); + + DistanceSpacingMultiplier.Value = ReadCurrentDistanceSnap(objects.Value.before, objects.Value.after); + DistanceSnapToggle.Value = TernaryState.True; + }, + RelativeSizeAxes = Axes.X, + } + } + }); + + if (DistanceSpacingMultiplier.Disabled) + { + distanceSpacingSlider.Hide(); + return; + } + + DistanceSpacingMultiplier.Value = editorBeatmap.BeatmapInfo.DistanceSpacing; + DistanceSpacingMultiplier.BindValueChanged(multiplier => + { + distanceSpacingSlider.ContractedLabelText = $"D. S. ({multiplier.NewValue:0.##x})"; + distanceSpacingSlider.ExpandedLabelText = $"Distance Spacing ({multiplier.NewValue:0.##x})"; + + if (multiplier.NewValue != multiplier.OldValue) + onScreenDisplay?.Display(new DistanceSpacingToast(multiplier.NewValue.ToLocalisableString(@"0.##x"), multiplier)); + + editorBeatmap.BeatmapInfo.DistanceSpacing = multiplier.NewValue; + }, true); + + // Manual binding to handle enabling distance spacing when the slider is interacted with. + distanceSpacingSlider.Current.BindValueChanged(spacing => + { + DistanceSpacingMultiplier.Value = spacing.NewValue; + DistanceSnapToggle.Value = TernaryState.True; + }); + DistanceSpacingMultiplier.BindValueChanged(spacing => distanceSpacingSlider.Current.Value = spacing.NewValue); + } + + private (HitObject before, HitObject after)? getObjectsOnEitherSideOfCurrentTime() + { + HitObject? lastBefore = playfield.HitObjectContainer.AliveObjects.LastOrDefault(h => h.HitObject.StartTime < editorClock.CurrentTime)?.HitObject; + + if (lastBefore == null) + return null; + + HitObject? firstAfter = playfield.HitObjectContainer.AliveObjects.FirstOrDefault(h => h.HitObject.StartTime >= editorClock.CurrentTime)?.HitObject; + + if (firstAfter == null) + return null; + + if (lastBefore == firstAfter) + return null; + + return (lastBefore, firstAfter); + } + + protected abstract double ReadCurrentDistanceSnap(HitObject before, HitObject after); + + protected override void Update() + { + base.Update(); + + (HitObject before, HitObject after)? objects = getObjectsOnEitherSideOfCurrentTime(); + + double currentSnap = objects == null + ? 0 + : ReadCurrentDistanceSnap(objects.Value.before, objects.Value.after); + + if (currentSnap > DistanceSpacingMultiplier.MinValue) + { + currentDistanceSpacingButton.Enabled.Value = currentDistanceSpacingButton.Expanded.Value + && !DistanceSpacingMultiplier.Disabled + && !Precision.AlmostEquals(currentSnap, DistanceSpacingMultiplier.Value, DistanceSpacingMultiplier.Precision / 2); + currentDistanceSpacingButton.ContractedLabelText = $"current {currentSnap:N2}x"; + currentDistanceSpacingButton.ExpandedLabelText = $"Use current ({currentSnap:N2}x)"; + } + else + { + currentDistanceSpacingButton.Enabled.Value = false; + currentDistanceSpacingButton.ContractedLabelText = string.Empty; + currentDistanceSpacingButton.ExpandedLabelText = "Use current (unavailable)"; + } + } + + public IEnumerable CreateTernaryButtons() => new[] + { + new TernaryButton(DistanceSnapToggle, "Distance Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Ruler }) + }; + + protected override bool OnKeyDown(KeyDownEvent e) + { + if (e.Repeat) + return false; + + handleToggleViaKey(e); + return base.OnKeyDown(e); + } + + protected override void OnKeyUp(KeyUpEvent e) + { + handleToggleViaKey(e); + base.OnKeyUp(e); + } + + private void handleToggleViaKey(KeyboardEvent key) + { + bool altPressed = key.AltPressed; + + if (altPressed != distanceSnapMomentary) + { + distanceSnapMomentary = altPressed; + DistanceSnapToggle.Value = DistanceSnapToggle.Value == TernaryState.False ? TernaryState.True : TernaryState.False; + } + } + + public virtual bool OnPressed(KeyBindingPressEvent e) + { + switch (e.Action) + { + case GlobalAction.EditorIncreaseDistanceSpacing: + case GlobalAction.EditorDecreaseDistanceSpacing: + return AdjustDistanceSpacing(e.Action, adjust_step); + } + + return false; + } + + public virtual void OnReleased(KeyBindingReleaseEvent e) + { + } + + public bool OnScroll(KeyBindingScrollEvent e) + { + switch (e.Action) + { + case GlobalAction.EditorIncreaseDistanceSpacing: + case GlobalAction.EditorDecreaseDistanceSpacing: + return AdjustDistanceSpacing(e.Action, e.ScrollAmount * adjust_step); + } + + return false; + } + + protected virtual bool AdjustDistanceSpacing(GlobalAction action, float amount) + { + if (DistanceSpacingMultiplier.Disabled) + return false; + + if (action == GlobalAction.EditorIncreaseDistanceSpacing) + DistanceSpacingMultiplier.Value += amount; + else if (action == GlobalAction.EditorDecreaseDistanceSpacing) + DistanceSpacingMultiplier.Value -= amount; + + DistanceSnapToggle.Value = TernaryState.True; + return true; + } + + #region IDistanceSnapProvider + + public virtual float GetBeatSnapDistanceAt(HitObject referenceObject, bool useReferenceSliderVelocity = true) + { + return (float)(100 * (useReferenceSliderVelocity && referenceObject is IHasSliderVelocity hasSliderVelocity ? hasSliderVelocity.SliderVelocityMultiplier : 1) * editorBeatmap.Difficulty.SliderMultiplier * 1 + / beatSnapProvider.BeatDivisor); + } + + public virtual float DurationToDistance(HitObject referenceObject, double duration) + { + double beatLength = beatSnapProvider.GetBeatLengthAtTime(referenceObject.StartTime); + return (float)(duration / beatLength * GetBeatSnapDistanceAt(referenceObject)); + } + + public virtual double DistanceToDuration(HitObject referenceObject, float distance) + { + double beatLength = beatSnapProvider.GetBeatLengthAtTime(referenceObject.StartTime); + return distance / GetBeatSnapDistanceAt(referenceObject) * beatLength; + } + + public virtual double FindSnappedDuration(HitObject referenceObject, float distance) + => beatSnapProvider.SnapTime(referenceObject.StartTime + DistanceToDuration(referenceObject, distance), referenceObject.StartTime) - referenceObject.StartTime; + + public virtual float FindSnappedDistance(HitObject referenceObject, float distance) + { + double startTime = referenceObject.StartTime; + + double actualDuration = startTime + DistanceToDuration(referenceObject, distance); + + double snappedEndTime = beatSnapProvider.SnapTime(actualDuration, startTime); + + double beatLength = beatSnapProvider.GetBeatLengthAtTime(startTime); + + // we don't want to exceed the actual duration and snap to a point in the future. + // as we are snapping to beat length via SnapTime (which will round-to-nearest), check for snapping in the forward direction and reverse it. + if (snappedEndTime > actualDuration + 1) + snappedEndTime -= beatLength; + + return DurationToDistance(referenceObject, snappedEndTime - startTime); + } + + #endregion + + private partial class DistanceSpacingToast : Toast + { + private readonly ValueChangedEvent change; + + public DistanceSpacingToast(LocalisableString value, ValueChangedEvent change) + : base(getAction(change).GetLocalisableDescription(), value, string.Empty) + { + this.change = change; + } + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + ShortcutText.Text = config.LookupKeyBindings(getAction(change)).ToUpper(); + } + + private static GlobalAction getAction(ValueChangedEvent change) => change.NewValue - change.OldValue > 0 + ? GlobalAction.EditorIncreaseDistanceSpacing + : GlobalAction.EditorDecreaseDistanceSpacing; + } + } +} diff --git a/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs b/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs index 70b79c30f0..938e2d9011 100644 --- a/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs @@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Edit Precision = 0.01, }; - IBindable IDistanceSnapProvider.DistanceSpacingMultiplier => DistanceSpacingMultiplier; + Bindable IDistanceSnapProvider.DistanceSpacingMultiplier => DistanceSpacingMultiplier; private ExpandableSlider> distanceSpacingSlider; private ExpandableButton currentDistanceSpacingButton; @@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Edit [Resolved(canBeNull: true)] private OnScreenDisplay onScreenDisplay { get; set; } - protected readonly Bindable DistanceSnapToggle = new Bindable(); + public readonly Bindable DistanceSnapToggle = new Bindable(); private bool distanceSnapMomentary; diff --git a/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs b/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs index af9d02f5c4..380038eadf 100644 --- a/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs +++ b/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Edit /// Importantly, this is provided for manual usage, and not multiplied into any of the methods exposed by this interface. /// /// - IBindable DistanceSpacingMultiplier { get; } + Bindable DistanceSpacingMultiplier { get; } /// /// Retrieves the distance between two points within a timing point that are one beat length apart. From 31849192c39f555de03d34f1dc95cd24dce2834b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 19 Oct 2023 11:42:40 +0200 Subject: [PATCH 3/5] Remove `DistancedHitObjectComposer` inheritance from catch composer --- .../Edit/CatchDistanceSnapProvider.cs | 26 +++++++++++ .../Edit/CatchHitObjectComposer.cs | 44 ++++++++++--------- 2 files changed, 49 insertions(+), 21 deletions(-) create mode 100644 osu.Game.Rulesets.Catch/Edit/CatchDistanceSnapProvider.cs diff --git a/osu.Game.Rulesets.Catch/Edit/CatchDistanceSnapProvider.cs b/osu.Game.Rulesets.Catch/Edit/CatchDistanceSnapProvider.cs new file mode 100644 index 0000000000..c3103bd204 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Edit/CatchDistanceSnapProvider.cs @@ -0,0 +1,26 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Objects; + +namespace osu.Game.Rulesets.Catch.Edit +{ + public partial class CatchDistanceSnapProvider : ComposerDistanceSnapProvider + { + protected override double ReadCurrentDistanceSnap(HitObject before, HitObject after) + { + // osu!catch's distance snap implementation is limited, in that a custom spacing cannot be specified. + // Therefore this functionality is not currently used. + // + // The implementation below is probably correct but should be checked if/when exposed via controls. + + float expectedDistance = DurationToDistance(before, after.StartTime - before.GetEndTime()); + float actualDistance = Math.Abs(((CatchHitObject)before).EffectiveX - ((CatchHitObject)after).EffectiveX); + + return actualDistance / expectedDistance; + } + } +} diff --git a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs index dc3a4416a5..50bc9ec157 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; @@ -9,6 +8,7 @@ using osu.Framework.Bindables; using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Input; +using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; @@ -20,13 +20,14 @@ using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.UI; +using osu.Game.Screens.Edit.Components.TernaryButtons; using osu.Game.Screens.Edit.Compose.Components; using osuTK; namespace osu.Game.Rulesets.Catch.Edit { // we're also a ScrollingHitObjectComposer candidate, but can't be everything can we? - public partial class CatchHitObjectComposer : DistancedHitObjectComposer + public partial class CatchHitObjectComposer : HitObjectComposer, IKeyBindingHandler { private const float distance_snap_radius = 50; @@ -42,6 +43,9 @@ namespace osu.Game.Rulesets.Catch.Edit MaxValue = 10, }; + [Cached(typeof(IDistanceSnapProvider))] + protected readonly CatchDistanceSnapProvider DistanceSnapProvider = new CatchDistanceSnapProvider(); + public CatchHitObjectComposer(CatchRuleset ruleset) : base(ruleset) { @@ -50,8 +54,11 @@ namespace osu.Game.Rulesets.Catch.Edit [BackgroundDependencyLoader] private void load() { + AddInternal(DistanceSnapProvider); + DistanceSnapProvider.AttachToToolbox(RightToolbox); + // todo: enable distance spacing once catch supports applying it to its existing distance snap grid implementation. - DistanceSpacingMultiplier.Disabled = true; + DistanceSnapProvider.DistanceSpacingMultiplier.Disabled = true; LayerBelowRuleset.Add(new PlayfieldBorder { @@ -72,6 +79,10 @@ namespace osu.Game.Rulesets.Catch.Edit AddInternal(beatSnapGrid = new CatchBeatSnapGrid()); } + protected override IEnumerable CreateTernaryButtons() + => base.CreateTernaryButtons() + .Concat(DistanceSnapProvider.CreateTernaryButtons()); + protected override void LoadComplete() { base.LoadComplete(); @@ -102,19 +113,6 @@ namespace osu.Game.Rulesets.Catch.Edit } } - protected override double ReadCurrentDistanceSnap(HitObject before, HitObject after) - { - // osu!catch's distance snap implementation is limited, in that a custom spacing cannot be specified. - // Therefore this functionality is not currently used. - // - // The implementation below is probably correct but should be checked if/when exposed via controls. - - float expectedDistance = DurationToDistance(before, after.StartTime - before.GetEndTime()); - float actualDistance = Math.Abs(((CatchHitObject)before).EffectiveX - ((CatchHitObject)after).EffectiveX); - - return actualDistance / expectedDistance; - } - protected override void Update() { base.Update(); @@ -122,7 +120,7 @@ namespace osu.Game.Rulesets.Catch.Edit updateDistanceSnapGrid(); } - public override bool OnPressed(KeyBindingPressEvent e) + public bool OnPressed(KeyBindingPressEvent e) { switch (e.Action) { @@ -131,14 +129,18 @@ namespace osu.Game.Rulesets.Catch.Edit // May be worth considering standardising "zoom" behaviour with what the timeline uses (ie. alt-wheel) but that may cause new conflicts. case GlobalAction.IncreaseScrollSpeed: this.TransformBindableTo(timeRangeMultiplier, timeRangeMultiplier.Value - 1, 200, Easing.OutQuint); - break; + return true; case GlobalAction.DecreaseScrollSpeed: this.TransformBindableTo(timeRangeMultiplier, timeRangeMultiplier.Value + 1, 200, Easing.OutQuint); - break; + return true; } - return base.OnPressed(e); + return false; + } + + public void OnReleased(KeyBindingReleaseEvent e) + { } protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods) => @@ -224,7 +226,7 @@ namespace osu.Game.Rulesets.Catch.Edit private void updateDistanceSnapGrid() { - if (DistanceSnapToggle.Value != TernaryState.True) + if (DistanceSnapProvider.DistanceSnapToggle.Value != TernaryState.True) { distanceSnapGrid.Hide(); return; From 144ef5a87c2b12d4fe525d15945b6ee0d844ff75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 19 Oct 2023 11:43:11 +0200 Subject: [PATCH 4/5] Remove `DistancedHitObjectComposer` --- .../Edit/DistancedHitObjectComposer.cs | 305 ------------------ 1 file changed, 305 deletions(-) delete mode 100644 osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs diff --git a/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs b/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs deleted file mode 100644 index 938e2d9011..0000000000 --- a/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs +++ /dev/null @@ -1,305 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -#nullable disable - -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Extensions; -using osu.Framework.Extensions.LocalisationExtensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Input.Bindings; -using osu.Framework.Input.Events; -using osu.Framework.Localisation; -using osu.Framework.Utils; -using osu.Game.Configuration; -using osu.Game.Graphics.UserInterface; -using osu.Game.Input.Bindings; -using osu.Game.Overlays; -using osu.Game.Overlays.OSD; -using osu.Game.Overlays.Settings.Sections; -using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Objects.Types; -using osu.Game.Screens.Edit.Components.TernaryButtons; - -namespace osu.Game.Rulesets.Edit -{ - /// - /// Represents a for rulesets with the concept of distances between objects. - /// - /// The base type of supported objects. - public abstract partial class DistancedHitObjectComposer : HitObjectComposer, IDistanceSnapProvider, IScrollBindingHandler - where TObject : HitObject - { - private const float adjust_step = 0.1f; - - public BindableDouble DistanceSpacingMultiplier { get; } = new BindableDouble(1.0) - { - MinValue = 0.1, - MaxValue = 6.0, - Precision = 0.01, - }; - - Bindable IDistanceSnapProvider.DistanceSpacingMultiplier => DistanceSpacingMultiplier; - - private ExpandableSlider> distanceSpacingSlider; - private ExpandableButton currentDistanceSpacingButton; - - [Resolved(canBeNull: true)] - private OnScreenDisplay onScreenDisplay { get; set; } - - public readonly Bindable DistanceSnapToggle = new Bindable(); - - private bool distanceSnapMomentary; - - protected DistancedHitObjectComposer(Ruleset ruleset) - : base(ruleset) - { - } - - [BackgroundDependencyLoader] - private void load() - { - RightToolbox.Add(new EditorToolboxGroup("snapping") - { - Alpha = DistanceSpacingMultiplier.Disabled ? 0 : 1, - Children = new Drawable[] - { - distanceSpacingSlider = new ExpandableSlider> - { - KeyboardStep = adjust_step, - // Manual binding in LoadComplete to handle one-way event flow. - Current = DistanceSpacingMultiplier.GetUnboundCopy(), - }, - currentDistanceSpacingButton = new ExpandableButton - { - Action = () => - { - (HitObject before, HitObject after)? objects = getObjectsOnEitherSideOfCurrentTime(); - - Debug.Assert(objects != null); - - DistanceSpacingMultiplier.Value = ReadCurrentDistanceSnap(objects.Value.before, objects.Value.after); - DistanceSnapToggle.Value = TernaryState.True; - }, - RelativeSizeAxes = Axes.X, - } - } - }); - } - - private (HitObject before, HitObject after)? getObjectsOnEitherSideOfCurrentTime() - { - HitObject lastBefore = Playfield.HitObjectContainer.AliveObjects.LastOrDefault(h => h.HitObject.StartTime < EditorClock.CurrentTime)?.HitObject; - - if (lastBefore == null) - return null; - - HitObject firstAfter = Playfield.HitObjectContainer.AliveObjects.FirstOrDefault(h => h.HitObject.StartTime >= EditorClock.CurrentTime)?.HitObject; - - if (firstAfter == null) - return null; - - if (lastBefore == firstAfter) - return null; - - return (lastBefore, firstAfter); - } - - protected abstract double ReadCurrentDistanceSnap(HitObject before, HitObject after); - - protected override void Update() - { - base.Update(); - - (HitObject before, HitObject after)? objects = getObjectsOnEitherSideOfCurrentTime(); - - double currentSnap = objects == null - ? 0 - : ReadCurrentDistanceSnap(objects.Value.before, objects.Value.after); - - if (currentSnap > DistanceSpacingMultiplier.MinValue) - { - currentDistanceSpacingButton.Enabled.Value = currentDistanceSpacingButton.Expanded.Value - && !DistanceSpacingMultiplier.Disabled - && !Precision.AlmostEquals(currentSnap, DistanceSpacingMultiplier.Value, DistanceSpacingMultiplier.Precision / 2); - currentDistanceSpacingButton.ContractedLabelText = $"current {currentSnap:N2}x"; - currentDistanceSpacingButton.ExpandedLabelText = $"Use current ({currentSnap:N2}x)"; - } - else - { - currentDistanceSpacingButton.Enabled.Value = false; - currentDistanceSpacingButton.ContractedLabelText = string.Empty; - currentDistanceSpacingButton.ExpandedLabelText = "Use current (unavailable)"; - } - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - if (DistanceSpacingMultiplier.Disabled) - { - distanceSpacingSlider.Hide(); - return; - } - - DistanceSpacingMultiplier.Value = EditorBeatmap.BeatmapInfo.DistanceSpacing; - DistanceSpacingMultiplier.BindValueChanged(multiplier => - { - distanceSpacingSlider.ContractedLabelText = $"D. S. ({multiplier.NewValue:0.##x})"; - distanceSpacingSlider.ExpandedLabelText = $"Distance Spacing ({multiplier.NewValue:0.##x})"; - - if (multiplier.NewValue != multiplier.OldValue) - onScreenDisplay?.Display(new DistanceSpacingToast(multiplier.NewValue.ToLocalisableString(@"0.##x"), multiplier)); - - EditorBeatmap.BeatmapInfo.DistanceSpacing = multiplier.NewValue; - }, true); - - // Manual binding to handle enabling distance spacing when the slider is interacted with. - distanceSpacingSlider.Current.BindValueChanged(spacing => - { - DistanceSpacingMultiplier.Value = spacing.NewValue; - DistanceSnapToggle.Value = TernaryState.True; - }); - DistanceSpacingMultiplier.BindValueChanged(spacing => distanceSpacingSlider.Current.Value = spacing.NewValue); - } - - protected override IEnumerable CreateTernaryButtons() => base.CreateTernaryButtons().Concat(new[] - { - new TernaryButton(DistanceSnapToggle, "Distance Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Ruler }) - }); - - protected override bool OnKeyDown(KeyDownEvent e) - { - if (e.Repeat) - return false; - - handleToggleViaKey(e); - return base.OnKeyDown(e); - } - - protected override void OnKeyUp(KeyUpEvent e) - { - handleToggleViaKey(e); - base.OnKeyUp(e); - } - - private void handleToggleViaKey(KeyboardEvent key) - { - bool altPressed = key.AltPressed; - - if (altPressed != distanceSnapMomentary) - { - distanceSnapMomentary = altPressed; - DistanceSnapToggle.Value = DistanceSnapToggle.Value == TernaryState.False ? TernaryState.True : TernaryState.False; - } - } - - public virtual bool OnPressed(KeyBindingPressEvent e) - { - switch (e.Action) - { - case GlobalAction.EditorIncreaseDistanceSpacing: - case GlobalAction.EditorDecreaseDistanceSpacing: - return AdjustDistanceSpacing(e.Action, adjust_step); - } - - return false; - } - - public virtual void OnReleased(KeyBindingReleaseEvent e) - { - } - - public bool OnScroll(KeyBindingScrollEvent e) - { - switch (e.Action) - { - case GlobalAction.EditorIncreaseDistanceSpacing: - case GlobalAction.EditorDecreaseDistanceSpacing: - return AdjustDistanceSpacing(e.Action, e.ScrollAmount * adjust_step); - } - - return false; - } - - protected virtual bool AdjustDistanceSpacing(GlobalAction action, float amount) - { - if (DistanceSpacingMultiplier.Disabled) - return false; - - if (action == GlobalAction.EditorIncreaseDistanceSpacing) - DistanceSpacingMultiplier.Value += amount; - else if (action == GlobalAction.EditorDecreaseDistanceSpacing) - DistanceSpacingMultiplier.Value -= amount; - - DistanceSnapToggle.Value = TernaryState.True; - return true; - } - - public virtual float GetBeatSnapDistanceAt(HitObject referenceObject, bool useReferenceSliderVelocity = true) - { - return (float)(100 * (useReferenceSliderVelocity && referenceObject is IHasSliderVelocity hasSliderVelocity ? hasSliderVelocity.SliderVelocityMultiplier : 1) * EditorBeatmap.Difficulty.SliderMultiplier * 1 - / BeatSnapProvider.BeatDivisor); - } - - public virtual float DurationToDistance(HitObject referenceObject, double duration) - { - double beatLength = BeatSnapProvider.GetBeatLengthAtTime(referenceObject.StartTime); - return (float)(duration / beatLength * GetBeatSnapDistanceAt(referenceObject)); - } - - public virtual double DistanceToDuration(HitObject referenceObject, float distance) - { - double beatLength = BeatSnapProvider.GetBeatLengthAtTime(referenceObject.StartTime); - return distance / GetBeatSnapDistanceAt(referenceObject) * beatLength; - } - - public virtual double FindSnappedDuration(HitObject referenceObject, float distance) - => BeatSnapProvider.SnapTime(referenceObject.StartTime + DistanceToDuration(referenceObject, distance), referenceObject.StartTime) - referenceObject.StartTime; - - public virtual float FindSnappedDistance(HitObject referenceObject, float distance) - { - double startTime = referenceObject.StartTime; - - double actualDuration = startTime + DistanceToDuration(referenceObject, distance); - - double snappedEndTime = BeatSnapProvider.SnapTime(actualDuration, startTime); - - double beatLength = BeatSnapProvider.GetBeatLengthAtTime(startTime); - - // we don't want to exceed the actual duration and snap to a point in the future. - // as we are snapping to beat length via SnapTime (which will round-to-nearest), check for snapping in the forward direction and reverse it. - if (snappedEndTime > actualDuration + 1) - snappedEndTime -= beatLength; - - return DurationToDistance(referenceObject, snappedEndTime - startTime); - } - - private partial class DistanceSpacingToast : Toast - { - private readonly ValueChangedEvent change; - - public DistanceSpacingToast(LocalisableString value, ValueChangedEvent change) - : base(getAction(change).GetLocalisableDescription(), value, string.Empty) - { - this.change = change; - } - - [BackgroundDependencyLoader] - private void load(OsuConfigManager config) - { - ShortcutText.Text = config.LookupKeyBindings(getAction(change)).ToUpper(); - } - - private static GlobalAction getAction(ValueChangedEvent change) => change.NewValue - change.OldValue > 0 - ? GlobalAction.EditorIncreaseDistanceSpacing - : GlobalAction.EditorDecreaseDistanceSpacing; - } - } -} From 0c4e74c82dbc3423cb461cb73c2e1bd15867257c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 19 Oct 2023 11:43:31 +0200 Subject: [PATCH 5/5] Inherit `ScrollingHitObjectComposer` in catch --- osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs index 50bc9ec157..04dd4827b6 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs @@ -26,8 +26,7 @@ using osuTK; namespace osu.Game.Rulesets.Catch.Edit { - // we're also a ScrollingHitObjectComposer candidate, but can't be everything can we? - public partial class CatchHitObjectComposer : HitObjectComposer, IKeyBindingHandler + public partial class CatchHitObjectComposer : ScrollingHitObjectComposer, IKeyBindingHandler { private const float distance_snap_radius = 50;