1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-08 01:13:21 +08:00

Merge pull request #31789 from bdach/distance-snap-fix

Fix distance snap grid not properly working due to multiple issues
This commit is contained in:
Dean Herbert 2025-02-05 18:41:46 +09:00 committed by GitHub
commit c37fa261c3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 126 additions and 175 deletions

View File

@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Catch.Edit
//
// The implementation below is probably correct but should be checked if/when exposed via controls.
float expectedDistance = DurationToDistance(before, after.StartTime - before.GetEndTime());
float expectedDistance = DurationToDistance(after.StartTime - before.GetEndTime(), before.StartTime);
float previousEndX = (before as JuiceStream)?.EndX ?? ((CatchHitObject)before).EffectiveX;
float actualDistance = Math.Abs(previousEndX - ((CatchHitObject)after).EffectiveX);

View File

@ -34,7 +34,7 @@ using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{
public partial class PathControlPointVisualiser<T> : CompositeDrawable, IKeyBindingHandler<PlatformAction>, IHasContextMenu
where T : OsuHitObject, IHasPath
where T : OsuHitObject, IHasPath, IHasSliderVelocity
{
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; // allow context menu to appear outside the playfield.

View File

@ -434,7 +434,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
if (state == SliderPlacementState.Drawing)
HitObject.Path.ExpectedDistance.Value = (float)HitObject.Path.CalculatedDistance;
else
HitObject.Path.ExpectedDistance.Value = distanceSnapProvider?.FindSnappedDistance(HitObject, (float)HitObject.Path.CalculatedDistance, DistanceSnapTarget.Start) ?? (float)HitObject.Path.CalculatedDistance;
HitObject.Path.ExpectedDistance.Value = distanceSnapProvider?.FindSnappedDistance((float)HitObject.Path.CalculatedDistance, HitObject.StartTime, HitObject) ?? (float)HitObject.Path.CalculatedDistance;
bodyPiece.UpdateFrom(HitObject);
headCirclePiece.UpdateFrom(HitObject.HeadCircle);

View File

@ -274,9 +274,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
}
else
{
double minDistance = distanceSnapProvider?.GetBeatSnapDistanceAt(HitObject, false) * oldVelocityMultiplier ?? 1;
double minDistance = distanceSnapProvider?.GetBeatSnapDistance() * oldVelocityMultiplier ?? 1;
// Add a small amount to the proposed distance to make it easier to snap to the full length of the slider.
proposedDistance = distanceSnapProvider?.FindSnappedDistance(HitObject, (float)proposedDistance + 1, DistanceSnapTarget.Start) ?? proposedDistance;
proposedDistance = distanceSnapProvider?.FindSnappedDistance((float)proposedDistance + 1, HitObject.StartTime, HitObject) ?? proposedDistance;
proposedDistance = MathHelper.Clamp(proposedDistance, minDistance, HitObject.Path.CalculatedDistance);
}

View File

@ -5,6 +5,7 @@
using JetBrains.Annotations;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit.Compose.Components;
@ -12,8 +13,8 @@ namespace osu.Game.Rulesets.Osu.Edit
{
public partial class OsuDistanceSnapGrid : CircularDistanceSnapGrid
{
public OsuDistanceSnapGrid(OsuHitObject hitObject, [CanBeNull] OsuHitObject nextHitObject = null)
: base(hitObject, hitObject.StackedEndPosition, hitObject.GetEndTime(), nextHitObject?.StartTime - 1)
public OsuDistanceSnapGrid(OsuHitObject hitObject, [CanBeNull] OsuHitObject nextHitObject = null, [CanBeNull] IHasSliderVelocity sliderVelocitySource = null)
: base(hitObject.StackedEndPosition, hitObject.GetEndTime(), nextHitObject?.StartTime - 1, sliderVelocitySource)
{
Masking = true;
}

View File

@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Edit
{
public override double ReadCurrentDistanceSnap(HitObject before, HitObject after)
{
float expectedDistance = DurationToDistance(before, after.StartTime - before.GetEndTime());
float expectedDistance = DurationToDistance(after.StartTime - before.GetEndTime(), before.StartTime);
float actualDistance = Vector2.Distance(((OsuHitObject)before).EndPosition, ((OsuHitObject)after).Position);
return actualDistance / expectedDistance;

View File

@ -23,6 +23,7 @@ using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.UI;
@ -406,22 +407,26 @@ namespace osu.Game.Rulesets.Osu.Edit
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(targetOffset);
int sourceIndex = -1;
int positionSourceObjectIndex = -1;
IHasSliderVelocity sliderVelocitySource = null;
for (int i = 0; i < EditorBeatmap.HitObjects.Count; i++)
{
if (!sourceSelector(EditorBeatmap.HitObjects[i]))
break;
sourceIndex = i;
positionSourceObjectIndex = i;
if (EditorBeatmap.HitObjects[i] is IHasSliderVelocity hasSliderVelocity)
sliderVelocitySource = hasSliderVelocity;
}
if (sourceIndex == -1)
if (positionSourceObjectIndex == -1)
return null;
HitObject sourceObject = EditorBeatmap.HitObjects[sourceIndex];
HitObject sourceObject = EditorBeatmap.HitObjects[positionSourceObjectIndex];
int targetIndex = sourceIndex + targetOffset;
int targetIndex = positionSourceObjectIndex + targetOffset;
HitObject targetObject = null;
// Keep advancing the target object while its start time falls before the end time of the source object
@ -442,7 +447,7 @@ namespace osu.Game.Rulesets.Osu.Edit
if (sourceObject is Spinner)
return null;
return new OsuDistanceSnapGrid((OsuHitObject)sourceObject, (OsuHitObject)targetObject);
return new OsuDistanceSnapGrid((OsuHitObject)sourceObject, (OsuHitObject)targetObject, sliderVelocitySource);
}
}
}

View File

@ -12,6 +12,7 @@ using osu.Framework.Utils;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Edit;
@ -67,17 +68,7 @@ namespace osu.Game.Tests.Editing
{
AddStep($"set slider multiplier = {multiplier}", () => composer.EditorBeatmap.Difficulty.SliderMultiplier = multiplier);
assertSnapDistance(100 * multiplier, null, true);
}
[TestCase(1)]
[TestCase(2)]
public void TestSpeedMultiplierDoesNotChangeDistanceSnap(float multiplier)
{
assertSnapDistance(100, new Slider
{
SliderVelocityMultiplier = multiplier
}, false);
assertSnapDistance(100 * multiplier);
}
[TestCase(1)]
@ -87,7 +78,7 @@ namespace osu.Game.Tests.Editing
assertSnapDistance(100 * multiplier, new Slider
{
SliderVelocityMultiplier = multiplier
}, true);
});
}
[TestCase(1)]
@ -96,7 +87,7 @@ namespace osu.Game.Tests.Editing
{
AddStep($"set divisor = {divisor}", () => BeatDivisor.Value = divisor);
assertSnapDistance(100f / divisor, null, true);
assertSnapDistance(100f / divisor);
}
/// <summary>
@ -114,9 +105,8 @@ namespace osu.Game.Tests.Editing
};
AddStep("add to beatmap", () => composer.EditorBeatmap.Add(referenceObject));
assertSnapDistance(base_distance * slider_velocity, referenceObject, true);
assertSnapDistance(base_distance * slider_velocity, referenceObject);
assertSnappedDistance(base_distance * slider_velocity + 10, base_distance * slider_velocity, referenceObject);
assertSnappedDuration(base_distance * slider_velocity + 10, 1000, referenceObject);
assertDistanceToDuration(base_distance * slider_velocity, 1000, referenceObject);
assertDurationToDistance(1000, base_distance * slider_velocity, referenceObject);
@ -164,39 +154,6 @@ namespace osu.Game.Tests.Editing
assertDistanceToDuration(400, 1000);
}
[Test]
public void TestGetSnappedDurationFromDistance()
{
assertSnappedDuration(0, 0);
assertSnappedDuration(50, 1000);
assertSnappedDuration(100, 1000);
assertSnappedDuration(150, 2000);
assertSnappedDuration(200, 2000);
assertSnappedDuration(250, 3000);
AddStep("set slider multiplier = 2", () => composer.EditorBeatmap.Difficulty.SliderMultiplier = 2);
assertSnappedDuration(0, 0);
assertSnappedDuration(50, 0);
assertSnappedDuration(100, 1000);
assertSnappedDuration(150, 1000);
assertSnappedDuration(200, 1000);
assertSnappedDuration(250, 1000);
AddStep("set beat length = 500", () =>
{
composer.EditorBeatmap.ControlPointInfo.Clear();
composer.EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 500 });
});
assertSnappedDuration(50, 0);
assertSnappedDuration(100, 500);
assertSnappedDuration(150, 500);
assertSnappedDuration(200, 500);
assertSnappedDuration(250, 500);
assertSnappedDuration(400, 1000);
}
[Test]
public void GetSnappedDistanceFromDistance()
{
@ -289,20 +246,17 @@ namespace osu.Game.Tests.Editing
AddUntilStep("use current snap not available", () => getCurrentSnapButton().Enabled.Value, () => Is.False);
}
private void assertSnapDistance(float expectedDistance, HitObject? referenceObject, bool includeSliderVelocity)
=> AddAssert($"distance is {expectedDistance}", () => composer.DistanceSnapProvider.GetBeatSnapDistanceAt(referenceObject ?? new HitObject(), includeSliderVelocity), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON));
private void assertSnapDistance(float expectedDistance, IHasSliderVelocity? hasSliderVelocity = null)
=> AddAssert($"distance is {expectedDistance}", () => composer.DistanceSnapProvider.GetBeatSnapDistance(hasSliderVelocity), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON));
private void assertDurationToDistance(double duration, float expectedDistance, HitObject? referenceObject = null)
=> AddAssert($"duration = {duration} -> distance = {expectedDistance}", () => composer.DistanceSnapProvider.DurationToDistance(referenceObject ?? new HitObject(), duration), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON));
=> AddAssert($"duration = {duration} -> distance = {expectedDistance}", () => composer.DistanceSnapProvider.DurationToDistance(duration, referenceObject?.StartTime ?? 0, referenceObject as IHasSliderVelocity), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON));
private void assertDistanceToDuration(float distance, double expectedDuration, HitObject? referenceObject = null)
=> 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.DistanceSnapProvider.FindSnappedDuration(referenceObject ?? new HitObject(), distance), () => Is.EqualTo(expectedDuration).Within(Precision.FLOAT_EPSILON));
=> AddAssert($"distance = {distance} -> duration = {expectedDuration}", () => composer.DistanceSnapProvider.DistanceToDuration(distance, referenceObject?.StartTime ?? 0, referenceObject as IHasSliderVelocity), () => Is.EqualTo(expectedDuration).Within(Precision.FLOAT_EPSILON));
private void assertSnappedDistance(float distance, float expectedDistance, HitObject? referenceObject = null)
=> AddAssert($"distance = {distance} -> distance = {expectedDistance} (snapped)", () => composer.DistanceSnapProvider.FindSnappedDistance(referenceObject ?? new HitObject(), distance, DistanceSnapTarget.End), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON));
=> AddAssert($"distance = {distance} -> distance = {expectedDistance} (snapped)", () => composer.DistanceSnapProvider.FindSnappedDistance(distance, referenceObject?.GetEndTime() ?? 0, referenceObject as IHasSliderVelocity), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON));
private partial class TestHitObjectComposer : OsuHitObjectComposer
{

View File

@ -10,7 +10,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Screens.Edit;
@ -115,7 +115,7 @@ namespace osu.Game.Tests.Visual.Editing
public new int MaxIntervals => base.MaxIntervals;
public TestDistanceSnapGrid(double? endTime = null)
: base(new HitObject(), grid_position, 0, endTime)
: base(grid_position, 0, endTime)
{
}
@ -191,15 +191,13 @@ namespace osu.Game.Tests.Visual.Editing
Bindable<double> IDistanceSnapProvider.DistanceSpacingMultiplier => DistanceSpacingMultiplier;
public float GetBeatSnapDistanceAt(HitObject referenceObject, bool useReferenceSliderVelocity = true) => beat_snap_distance;
public float GetBeatSnapDistance(IHasSliderVelocity withVelocity = null) => beat_snap_distance;
public float DurationToDistance(HitObject referenceObject, double duration) => (float)duration;
public float DurationToDistance(double duration, double timingReference, IHasSliderVelocity withVelocity = null) => (float)duration;
public double DistanceToDuration(HitObject referenceObject, float distance) => distance;
public double DistanceToDuration(float distance, double timingReference, IHasSliderVelocity withVelocity = null) => distance;
public double FindSnappedDuration(HitObject referenceObject, float distance) => 0;
public float FindSnappedDistance(HitObject referenceObject, float distance, DistanceSnapTarget target) => 0;
public float FindSnappedDistance(float distance, double snapReferenceTime, IHasSliderVelocity withVelocity = null) => 0;
}
}
}

View File

@ -265,57 +265,38 @@ namespace osu.Game.Rulesets.Edit
#region IDistanceSnapProvider
public virtual float GetBeatSnapDistanceAt(HitObject referenceObject, bool useReferenceSliderVelocity = true)
public virtual float GetBeatSnapDistance(IHasSliderVelocity? withVelocity = null)
{
return (float)(100 * (useReferenceSliderVelocity && referenceObject is IHasSliderVelocity hasSliderVelocity ? hasSliderVelocity.SliderVelocityMultiplier : 1) * editorBeatmap.Difficulty.SliderMultiplier * 1
return (float)(100 * (withVelocity?.SliderVelocityMultiplier ?? 1) * editorBeatmap.Difficulty.SliderMultiplier * 1
/ beatSnapProvider.BeatDivisor);
}
public virtual float DurationToDistance(HitObject referenceObject, double duration)
public virtual float DurationToDistance(double duration, double timingReference, IHasSliderVelocity? withVelocity = null)
{
double beatLength = beatSnapProvider.GetBeatLengthAtTime(referenceObject.StartTime);
return (float)(duration / beatLength * GetBeatSnapDistanceAt(referenceObject));
double beatLength = beatSnapProvider.GetBeatLengthAtTime(timingReference);
return (float)(duration / beatLength * GetBeatSnapDistance(withVelocity));
}
public virtual double DistanceToDuration(HitObject referenceObject, float distance)
public virtual double DistanceToDuration(float distance, double timingReference, IHasSliderVelocity? withVelocity = null)
{
double beatLength = beatSnapProvider.GetBeatLengthAtTime(referenceObject.StartTime);
return distance / GetBeatSnapDistanceAt(referenceObject) * beatLength;
double beatLength = beatSnapProvider.GetBeatLengthAtTime(timingReference);
return distance / GetBeatSnapDistance(withVelocity) * 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, DistanceSnapTarget target)
public virtual float FindSnappedDistance(float distance, double snapReferenceTime, IHasSliderVelocity? withVelocity = null)
{
double referenceTime;
double actualDuration = snapReferenceTime + DistanceToDuration(distance, snapReferenceTime, withVelocity);
switch (target)
{
case DistanceSnapTarget.Start:
referenceTime = referenceObject.StartTime;
break;
double snappedTime = beatSnapProvider.SnapTime(actualDuration, snapReferenceTime);
case DistanceSnapTarget.End:
referenceTime = referenceObject.GetEndTime();
break;
default:
throw new ArgumentOutOfRangeException(nameof(target), target, $"Unknown {nameof(DistanceSnapTarget)} value");
}
double actualDuration = referenceTime + DistanceToDuration(referenceObject, distance);
double snappedTime = beatSnapProvider.SnapTime(actualDuration, referenceTime);
double beatLength = beatSnapProvider.GetBeatLengthAtTime(referenceTime);
double beatLength = beatSnapProvider.GetBeatLengthAtTime(snapReferenceTime);
// 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 (snappedTime > actualDuration + 1)
snappedTime -= beatLength;
return DurationToDistance(referenceObject, snappedTime - referenceTime);
return DurationToDistance(snappedTime - snapReferenceTime, snapReferenceTime, withVelocity);
}
#endregion

View File

@ -4,7 +4,7 @@
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Edit
{
@ -22,53 +22,63 @@ namespace osu.Game.Rulesets.Edit
Bindable<double> DistanceSpacingMultiplier { get; }
/// <summary>
/// Retrieves the distance between two points within a timing point that are one beat length apart.
/// Returns the spatial distance between objects which are temporally one beat apart.
/// Depends on:
/// <list type="bullet">
/// <item>the slider velocity taken from <paramref name="withVelocity"/>,</item>
/// <item>the beatmap's <see cref="IBeatmapDifficultyInfo.SliderMultiplier"/>,</item>,
/// <item>the current beat divisor.</item>
/// </list>
/// Note that the returned value does <b>NOT</b> depend on <see cref="DistanceSpacingMultiplier"/>;
/// consumers are expected to include that multiplier as they see fit.
/// </summary>
/// <param name="referenceObject">An object to be used as a reference point for this operation.</param>
/// <param name="useReferenceSliderVelocity">Whether the <paramref name="referenceObject"/>'s slider velocity should be factored into the returned distance.</param>
/// <returns>The distance between two points residing in the timing point that are one beat length apart.</returns>
float GetBeatSnapDistanceAt(HitObject referenceObject, bool useReferenceSliderVelocity = true);
float GetBeatSnapDistance(IHasSliderVelocity? withVelocity = null);
/// <summary>
/// Converts a duration to a distance without applying any snapping.
/// Converts a temporal duration into a spatial distance.
/// Does not perform any snapping.
/// Depends on:
/// <list type="bullet">
/// <item>the <paramref name="duration"/> provided,</item>
/// <item>a <paramref name="timingReference"/> used to retrieve the beat length of the beatmap at that time,</item>
/// <item>the slider velocity taken from <paramref name="withVelocity"/>,</item>
/// <item>the beatmap's <see cref="IBeatmapDifficultyInfo.SliderMultiplier"/>,</item>,
/// <item>the current beat divisor.</item>
/// </list>
/// Note that the returned value does <b>NOT</b> depend on <see cref="DistanceSpacingMultiplier"/>;
/// consumers are expected to include that multiplier as they see fit.
/// </summary>
/// <param name="referenceObject">An object to be used as a reference point for this operation.</param>
/// <param name="duration">The duration to convert.</param>
/// <returns>A value that represents <paramref name="duration"/> as a distance in the timing point.</returns>
float DurationToDistance(HitObject referenceObject, double duration);
float DurationToDistance(double duration, double timingReference, IHasSliderVelocity? withVelocity = null);
/// <summary>
/// Converts a distance to a duration without applying any snapping.
/// Converts a spatial distance into a temporal duration.
/// Does not perform any snapping.
/// Depends on:
/// <list type="bullet">
/// <item>the <paramref name="distance"/> provided,</item>
/// <item>a <paramref name="timingReference"/> used to retrieve the beat length of the beatmap at that time,</item>
/// <item>the slider velocity taken from <paramref name="withVelocity"/>,</item>
/// <item>the beatmap's <see cref="IBeatmapDifficultyInfo.SliderMultiplier"/>,</item>,
/// <item>the current beat divisor.</item>
/// </list>
/// Note that the returned value does <b>NOT</b> depend on <see cref="DistanceSpacingMultiplier"/>;
/// consumers are expected to include that multiplier as they see fit.
/// </summary>
/// <param name="referenceObject">An object to be used as a reference point for this operation.</param>
/// <param name="distance">The distance to convert.</param>
/// <returns>A value that represents <paramref name="distance"/> as a duration in the timing point.</returns>
double DistanceToDuration(HitObject referenceObject, float distance);
double DistanceToDuration(float distance, double timingReference, IHasSliderVelocity? withVelocity = null);
/// <summary>
/// Given a distance from the provided hit object, find the valid snapped duration.
/// Snaps a spatial distance to the beat, relative to <paramref name="snapReferenceTime"/>.
/// Depends on:
/// <list type="bullet">
/// <item>the <paramref name="distance"/> provided,</item>
/// <item>a <paramref name="snapReferenceTime"/> used to retrieve the beat length of the beatmap at that time,</item>
/// <item>the slider velocity taken from <paramref name="withVelocity"/>,</item>
/// <item>the beatmap's <see cref="IBeatmapDifficultyInfo.SliderMultiplier"/>,</item>,
/// <item>the current beat divisor.</item>
/// </list>
/// Note that the returned value does <b>NOT</b> depend on <see cref="DistanceSpacingMultiplier"/>;
/// consumers are expected to include that multiplier as they see fit.
/// </summary>
/// <param name="referenceObject">An object to be used as a reference point for this operation.</param>
/// <param name="distance">The distance to convert.</param>
/// <returns>A value that represents <paramref name="distance"/> as a duration snapped to the closest beat of the timing point.</returns>
double FindSnappedDuration(HitObject referenceObject, float distance);
/// <summary>
/// Given a distance from the provided hit object, find the valid snapped distance.
/// </summary>
/// <param name="referenceObject">An object to be used as a reference point for this operation.</param>
/// <param name="distance">The distance to convert.</param>
/// <param name="target">Whether the distance measured should be from the start or the end of <paramref name="referenceObject"/>.</param>
/// <returns>
/// A value that represents <paramref name="distance"/> snapped to the closest beat of the timing point.
/// The distance will always be less than or equal to the provided <paramref name="distance"/>.
/// </returns>
float FindSnappedDistance(HitObject referenceObject, float distance, DistanceSnapTarget target);
}
public enum DistanceSnapTarget
{
Start,
End,
float FindSnappedDistance(float distance, double snapReferenceTime, IHasSliderVelocity? withVelocity = null);
}
}

View File

@ -15,9 +15,9 @@ namespace osu.Game.Rulesets.Objects
/// Snaps the provided <paramref name="hitObject"/>'s duration using the <paramref name="snapProvider"/>.
/// </summary>
public static void SnapTo<THitObject>(this THitObject hitObject, IDistanceSnapProvider? snapProvider)
where THitObject : HitObject, IHasPath
where THitObject : HitObject, IHasPath, IHasSliderVelocity
{
hitObject.Path.ExpectedDistance.Value = snapProvider?.FindSnappedDistance(hitObject, (float)hitObject.Path.CalculatedDistance, DistanceSnapTarget.Start) ?? hitObject.Path.CalculatedDistance;
hitObject.Path.ExpectedDistance.Value = snapProvider?.FindSnappedDistance((float)hitObject.Path.CalculatedDistance, hitObject.StartTime, hitObject) ?? hitObject.Path.CalculatedDistance;
}
/// <summary>

View File

@ -8,7 +8,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osuTK;
using osuTK.Graphics;
@ -16,8 +16,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
public abstract partial class CircularDistanceSnapGrid : DistanceSnapGrid
{
protected CircularDistanceSnapGrid(HitObject referenceObject, Vector2 startPosition, double startTime, double? endTime = null)
: base(referenceObject, startPosition, startTime, endTime)
protected CircularDistanceSnapGrid(Vector2 startPosition, double startTime, double? endTime = null, IHasSliderVelocity? sliderVelocitySource = null)
: base(startPosition, startTime, endTime, sliderVelocitySource)
{
}
@ -56,14 +56,14 @@ namespace osu.Game.Screens.Edit.Compose.Components
// Picture the scenario where the user has just placed an object on a 1/2 snap, then changes to
// 1/3 snap and expects to be able to place the next object on a valid 1/3 snap, regardless of the
// fact that the 1/2 snap reference object is not valid for 1/3 snapping.
float offset = SnapProvider.FindSnappedDistance(ReferenceObject, 0, DistanceSnapTarget.End);
float offset = (float)(SnapProvider.FindSnappedDistance(0, StartTime, SliderVelocitySource) * DistanceSpacingMultiplier.Value);
for (int i = 0; i < requiredCircles; i++)
{
const float thickness = 4;
float diameter = (offset + (i + 1) * DistanceBetweenTicks + thickness / 2) * 2;
AddInternal(new Ring(ReferenceObject, GetColourForIndexFromPlacement(i))
AddInternal(new Ring(StartTime, GetColourForIndexFromPlacement(i))
{
Position = StartPosition,
Origin = Anchor.Centre,
@ -98,19 +98,19 @@ namespace osu.Game.Screens.Edit.Compose.Components
travelLength = DistanceBetweenTicks;
float snappedDistance = fixedTime != null
? SnapProvider.DurationToDistance(ReferenceObject, fixedTime.Value - ReferenceObject.GetEndTime())
? SnapProvider.DurationToDistance(fixedTime.Value - StartTime, StartTime, SliderVelocitySource)
// When interacting with the resolved snap provider, the distance spacing multiplier should first be removed
// to allow for snapping at a non-multiplied ratio.
: SnapProvider.FindSnappedDistance(ReferenceObject, travelLength / distanceSpacingMultiplier, DistanceSnapTarget.End);
: SnapProvider.FindSnappedDistance(travelLength / distanceSpacingMultiplier, StartTime, SliderVelocitySource);
double snappedTime = StartTime + SnapProvider.DistanceToDuration(ReferenceObject, snappedDistance);
double snappedTime = StartTime + SnapProvider.DistanceToDuration(snappedDistance, StartTime, SliderVelocitySource);
if (snappedTime > LatestEndTime)
{
double tickLength = Beatmap.GetBeatLengthAtTime(StartTime);
snappedDistance = SnapProvider.DurationToDistance(ReferenceObject, MaxIntervals * tickLength);
snappedTime = StartTime + SnapProvider.DistanceToDuration(ReferenceObject, snappedDistance);
snappedDistance = SnapProvider.DurationToDistance(MaxIntervals * tickLength, StartTime, SliderVelocitySource);
snappedTime = StartTime + SnapProvider.DistanceToDuration(snappedDistance, StartTime, SliderVelocitySource);
}
// The multiplier can then be reapplied to the final position.
@ -127,13 +127,13 @@ namespace osu.Game.Screens.Edit.Compose.Components
[Resolved]
private EditorClock? editorClock { get; set; }
private readonly HitObject referenceObject;
private readonly double startTime;
private readonly Color4 baseColour;
public Ring(HitObject referenceObject, Color4 baseColour)
public Ring(double startTime, Color4 baseColour)
{
this.referenceObject = referenceObject;
this.startTime = startTime;
Colour = this.baseColour = baseColour;
@ -148,9 +148,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
return;
float distanceSpacingMultiplier = (float)snapProvider.DistanceSpacingMultiplier.Value;
double timeFromReferencePoint = editorClock.CurrentTime - referenceObject.GetEndTime();
double timeFromReferencePoint = editorClock.CurrentTime - startTime;
float distanceForCurrentTime = snapProvider.DurationToDistance(referenceObject, timeFromReferencePoint)
float distanceForCurrentTime = snapProvider.DurationToDistance(timeFromReferencePoint, startTime)
* distanceSpacingMultiplier;
float timeBasedAlpha = 1 - Math.Clamp(Math.Abs(distanceForCurrentTime - Size.X / 2) / 30, 0, 1);

View File

@ -4,6 +4,7 @@
#nullable disable
using System;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
@ -12,7 +13,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Layout;
using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osuTK;
using osuTK.Graphics;
@ -48,6 +49,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected readonly double? LatestEndTime;
[CanBeNull]
protected readonly IHasSliderVelocity SliderVelocitySource;
[Resolved]
protected OsuColour Colours { get; private set; }
@ -62,19 +66,17 @@ namespace osu.Game.Screens.Edit.Compose.Components
private readonly LayoutValue gridCache = new LayoutValue(Invalidation.RequiredParentSizeToFit);
protected readonly HitObject ReferenceObject;
/// <summary>
/// Creates a new <see cref="DistanceSnapGrid"/>.
/// </summary>
/// <param name="referenceObject">A reference object to gather relevant difficulty values from.</param>
/// <param name="startPosition">The position at which the grid should start. The first tick is located one distance spacing length away from this point.</param>
/// <param name="startTime">The snapping time at <see cref="StartPosition"/>.</param>
/// <param name="endTime">The time at which the snapping grid should end. If null, the grid will continue until the bounds of the screen are exceeded.</param>
protected DistanceSnapGrid(HitObject referenceObject, Vector2 startPosition, double startTime, double? endTime = null)
/// <param name="sliderVelocitySource">The reference object with slider velocity to include in the calculations for distance snapping.</param>
protected DistanceSnapGrid(Vector2 startPosition, double startTime, double? endTime = null, [CanBeNull] IHasSliderVelocity sliderVelocitySource = null)
{
ReferenceObject = referenceObject;
LatestEndTime = endTime;
SliderVelocitySource = sliderVelocitySource;
StartPosition = startPosition;
StartTime = startTime;
@ -97,14 +99,14 @@ namespace osu.Game.Screens.Edit.Compose.Components
private void updateSpacing()
{
float distanceSpacingMultiplier = (float)DistanceSpacingMultiplier.Value;
float beatSnapDistance = SnapProvider.GetBeatSnapDistanceAt(ReferenceObject, false);
float beatSnapDistance = SnapProvider.GetBeatSnapDistance(SliderVelocitySource);
DistanceBetweenTicks = beatSnapDistance * distanceSpacingMultiplier;
if (LatestEndTime == null)
MaxIntervals = int.MaxValue;
else
MaxIntervals = (int)((LatestEndTime.Value - StartTime) / SnapProvider.DistanceToDuration(ReferenceObject, beatSnapDistance));
MaxIntervals = (int)((LatestEndTime.Value - StartTime) / SnapProvider.DistanceToDuration(beatSnapDistance, StartTime, SliderVelocitySource));
gridCache.Invalidate();
}
@ -132,7 +134,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// <param name="position">The original position in coordinate space local to this <see cref="DistanceSnapGrid"/>.</param>
/// <param name="fixedTime">
/// Whether the snap operation should be temporally constrained to a particular time instant,
/// thus fixing the possible positions to a set distance from the <see cref="ReferenceObject"/>.
/// thus fixing the possible positions to a set distance relative from the <see cref="StartTime"/>.
/// </param>
/// <returns>A tuple containing the snapped position in coordinate space local to this <see cref="DistanceSnapGrid"/> and the respective time value.</returns>
public abstract (Vector2 position, double time) GetSnappedPosition(Vector2 position, double? fixedTime = null);