1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-12 19:32:55 +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. // 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 previousEndX = (before as JuiceStream)?.EndX ?? ((CatchHitObject)before).EffectiveX;
float actualDistance = Math.Abs(previousEndX - ((CatchHitObject)after).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 namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{ {
public partial class PathControlPointVisualiser<T> : CompositeDrawable, IKeyBindingHandler<PlatformAction>, IHasContextMenu 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. 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) if (state == SliderPlacementState.Drawing)
HitObject.Path.ExpectedDistance.Value = (float)HitObject.Path.CalculatedDistance; HitObject.Path.ExpectedDistance.Value = (float)HitObject.Path.CalculatedDistance;
else 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); bodyPiece.UpdateFrom(HitObject);
headCirclePiece.UpdateFrom(HitObject.HeadCircle); headCirclePiece.UpdateFrom(HitObject.HeadCircle);

View File

@ -274,9 +274,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
} }
else 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. // 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); proposedDistance = MathHelper.Clamp(proposedDistance, minDistance, HitObject.Path.CalculatedDistance);
} }

View File

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

View File

@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Edit
{ {
public override double ReadCurrentDistanceSnap(HitObject before, HitObject after) 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); float actualDistance = Vector2.Distance(((OsuHitObject)before).EndPosition, ((OsuHitObject)after).Position);
return actualDistance / expectedDistance; return actualDistance / expectedDistance;

View File

@ -23,6 +23,7 @@ using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
@ -406,22 +407,26 @@ namespace osu.Game.Rulesets.Osu.Edit
{ {
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(targetOffset); ArgumentOutOfRangeException.ThrowIfNegativeOrZero(targetOffset);
int sourceIndex = -1; int positionSourceObjectIndex = -1;
IHasSliderVelocity sliderVelocitySource = null;
for (int i = 0; i < EditorBeatmap.HitObjects.Count; i++) for (int i = 0; i < EditorBeatmap.HitObjects.Count; i++)
{ {
if (!sourceSelector(EditorBeatmap.HitObjects[i])) if (!sourceSelector(EditorBeatmap.HitObjects[i]))
break; break;
sourceIndex = i; positionSourceObjectIndex = i;
if (EditorBeatmap.HitObjects[i] is IHasSliderVelocity hasSliderVelocity)
sliderVelocitySource = hasSliderVelocity;
} }
if (sourceIndex == -1) if (positionSourceObjectIndex == -1)
return null; return null;
HitObject sourceObject = EditorBeatmap.HitObjects[sourceIndex]; HitObject sourceObject = EditorBeatmap.HitObjects[positionSourceObjectIndex];
int targetIndex = sourceIndex + targetOffset; int targetIndex = positionSourceObjectIndex + targetOffset;
HitObject targetObject = null; HitObject targetObject = null;
// Keep advancing the target object while its start time falls before the end time of the source object // 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) if (sourceObject is Spinner)
return null; 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.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Edit; 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); AddStep($"set slider multiplier = {multiplier}", () => composer.EditorBeatmap.Difficulty.SliderMultiplier = multiplier);
assertSnapDistance(100 * multiplier, null, true); assertSnapDistance(100 * multiplier);
}
[TestCase(1)]
[TestCase(2)]
public void TestSpeedMultiplierDoesNotChangeDistanceSnap(float multiplier)
{
assertSnapDistance(100, new Slider
{
SliderVelocityMultiplier = multiplier
}, false);
} }
[TestCase(1)] [TestCase(1)]
@ -87,7 +78,7 @@ namespace osu.Game.Tests.Editing
assertSnapDistance(100 * multiplier, new Slider assertSnapDistance(100 * multiplier, new Slider
{ {
SliderVelocityMultiplier = multiplier SliderVelocityMultiplier = multiplier
}, true); });
} }
[TestCase(1)] [TestCase(1)]
@ -96,7 +87,7 @@ namespace osu.Game.Tests.Editing
{ {
AddStep($"set divisor = {divisor}", () => BeatDivisor.Value = divisor); AddStep($"set divisor = {divisor}", () => BeatDivisor.Value = divisor);
assertSnapDistance(100f / divisor, null, true); assertSnapDistance(100f / divisor);
} }
/// <summary> /// <summary>
@ -114,9 +105,8 @@ namespace osu.Game.Tests.Editing
}; };
AddStep("add to beatmap", () => composer.EditorBeatmap.Add(referenceObject)); 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); 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); assertDistanceToDuration(base_distance * slider_velocity, 1000, referenceObject);
assertDurationToDistance(1000, base_distance * slider_velocity, referenceObject); assertDurationToDistance(1000, base_distance * slider_velocity, referenceObject);
@ -164,39 +154,6 @@ namespace osu.Game.Tests.Editing
assertDistanceToDuration(400, 1000); 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] [Test]
public void GetSnappedDistanceFromDistance() public void GetSnappedDistanceFromDistance()
{ {
@ -289,20 +246,17 @@ namespace osu.Game.Tests.Editing
AddUntilStep("use current snap not available", () => getCurrentSnapButton().Enabled.Value, () => Is.False); AddUntilStep("use current snap not available", () => getCurrentSnapButton().Enabled.Value, () => Is.False);
} }
private void assertSnapDistance(float expectedDistance, HitObject? referenceObject, bool includeSliderVelocity) private void assertSnapDistance(float expectedDistance, IHasSliderVelocity? hasSliderVelocity = null)
=> AddAssert($"distance is {expectedDistance}", () => composer.DistanceSnapProvider.GetBeatSnapDistanceAt(referenceObject ?? new HitObject(), includeSliderVelocity), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON)); => 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) 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) 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)); => AddAssert($"distance = {distance} -> duration = {expectedDuration}", () => composer.DistanceSnapProvider.DistanceToDuration(distance, referenceObject?.StartTime ?? 0, referenceObject as IHasSliderVelocity), () => 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));
private void assertSnappedDistance(float distance, float expectedDistance, HitObject? referenceObject = null) 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 private partial class TestHitObjectComposer : OsuHitObjectComposer
{ {

View File

@ -10,7 +10,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Edit; 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;
using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
@ -115,7 +115,7 @@ namespace osu.Game.Tests.Visual.Editing
public new int MaxIntervals => base.MaxIntervals; public new int MaxIntervals => base.MaxIntervals;
public TestDistanceSnapGrid(double? endTime = null) 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; 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(float distance, double snapReferenceTime, IHasSliderVelocity withVelocity = null) => 0;
public float FindSnappedDistance(HitObject referenceObject, float distance, DistanceSnapTarget target) => 0;
} }
} }
} }

View File

@ -265,57 +265,38 @@ namespace osu.Game.Rulesets.Edit
#region IDistanceSnapProvider #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); / 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); double beatLength = beatSnapProvider.GetBeatLengthAtTime(timingReference);
return (float)(duration / beatLength * GetBeatSnapDistanceAt(referenceObject)); 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); double beatLength = beatSnapProvider.GetBeatLengthAtTime(timingReference);
return distance / GetBeatSnapDistanceAt(referenceObject) * beatLength; return distance / GetBeatSnapDistance(withVelocity) * beatLength;
} }
public virtual double FindSnappedDuration(HitObject referenceObject, float distance) public virtual float FindSnappedDistance(float distance, double snapReferenceTime, IHasSliderVelocity? withVelocity = null)
=> beatSnapProvider.SnapTime(referenceObject.StartTime + DistanceToDuration(referenceObject, distance), referenceObject.StartTime) - referenceObject.StartTime;
public virtual float FindSnappedDistance(HitObject referenceObject, float distance, DistanceSnapTarget target)
{ {
double referenceTime; double actualDuration = snapReferenceTime + DistanceToDuration(distance, snapReferenceTime, withVelocity);
switch (target) double snappedTime = beatSnapProvider.SnapTime(actualDuration, snapReferenceTime);
{
case DistanceSnapTarget.Start:
referenceTime = referenceObject.StartTime;
break;
case DistanceSnapTarget.End: double beatLength = beatSnapProvider.GetBeatLengthAtTime(snapReferenceTime);
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);
// we don't want to exceed the actual duration and snap to a point in the future. // 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. // 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) if (snappedTime > actualDuration + 1)
snappedTime -= beatLength; snappedTime -= beatLength;
return DurationToDistance(referenceObject, snappedTime - referenceTime); return DurationToDistance(snappedTime - snapReferenceTime, snapReferenceTime, withVelocity);
} }
#endregion #endregion

View File

@ -4,7 +4,7 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Edit namespace osu.Game.Rulesets.Edit
{ {
@ -22,53 +22,63 @@ namespace osu.Game.Rulesets.Edit
Bindable<double> DistanceSpacingMultiplier { get; } Bindable<double> DistanceSpacingMultiplier { get; }
/// <summary> /// <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> /// </summary>
/// <param name="referenceObject">An object to be used as a reference point for this operation.</param> float GetBeatSnapDistance(IHasSliderVelocity? withVelocity = null);
/// <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);
/// <summary> /// <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> /// </summary>
/// <param name="referenceObject">An object to be used as a reference point for this operation.</param> float DurationToDistance(double duration, double timingReference, IHasSliderVelocity? withVelocity = null);
/// <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);
/// <summary> /// <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> /// </summary>
/// <param name="referenceObject">An object to be used as a reference point for this operation.</param> double DistanceToDuration(float distance, double timingReference, IHasSliderVelocity? withVelocity = null);
/// <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);
/// <summary> /// <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> /// </summary>
/// <param name="referenceObject">An object to be used as a reference point for this operation.</param> float FindSnappedDistance(float distance, double snapReferenceTime, IHasSliderVelocity? withVelocity = null);
/// <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,
} }
} }

View File

@ -15,9 +15,9 @@ namespace osu.Game.Rulesets.Objects
/// Snaps the provided <paramref name="hitObject"/>'s duration using the <paramref name="snapProvider"/>. /// Snaps the provided <paramref name="hitObject"/>'s duration using the <paramref name="snapProvider"/>.
/// </summary> /// </summary>
public static void SnapTo<THitObject>(this THitObject hitObject, IDistanceSnapProvider? snapProvider) 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> /// <summary>

View File

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

View File

@ -4,6 +4,7 @@
#nullable disable #nullable disable
using System; using System;
using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
@ -12,7 +13,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Layout; using osu.Framework.Layout;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -48,6 +49,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected readonly double? LatestEndTime; protected readonly double? LatestEndTime;
[CanBeNull]
protected readonly IHasSliderVelocity SliderVelocitySource;
[Resolved] [Resolved]
protected OsuColour Colours { get; private set; } 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); private readonly LayoutValue gridCache = new LayoutValue(Invalidation.RequiredParentSizeToFit);
protected readonly HitObject ReferenceObject;
/// <summary> /// <summary>
/// Creates a new <see cref="DistanceSnapGrid"/>. /// Creates a new <see cref="DistanceSnapGrid"/>.
/// </summary> /// </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="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="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> /// <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; LatestEndTime = endTime;
SliderVelocitySource = sliderVelocitySource;
StartPosition = startPosition; StartPosition = startPosition;
StartTime = startTime; StartTime = startTime;
@ -97,14 +99,14 @@ namespace osu.Game.Screens.Edit.Compose.Components
private void updateSpacing() private void updateSpacing()
{ {
float distanceSpacingMultiplier = (float)DistanceSpacingMultiplier.Value; float distanceSpacingMultiplier = (float)DistanceSpacingMultiplier.Value;
float beatSnapDistance = SnapProvider.GetBeatSnapDistanceAt(ReferenceObject, false); float beatSnapDistance = SnapProvider.GetBeatSnapDistance(SliderVelocitySource);
DistanceBetweenTicks = beatSnapDistance * distanceSpacingMultiplier; DistanceBetweenTicks = beatSnapDistance * distanceSpacingMultiplier;
if (LatestEndTime == null) if (LatestEndTime == null)
MaxIntervals = int.MaxValue; MaxIntervals = int.MaxValue;
else else
MaxIntervals = (int)((LatestEndTime.Value - StartTime) / SnapProvider.DistanceToDuration(ReferenceObject, beatSnapDistance)); MaxIntervals = (int)((LatestEndTime.Value - StartTime) / SnapProvider.DistanceToDuration(beatSnapDistance, StartTime, SliderVelocitySource));
gridCache.Invalidate(); 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="position">The original position in coordinate space local to this <see cref="DistanceSnapGrid"/>.</param>
/// <param name="fixedTime"> /// <param name="fixedTime">
/// Whether the snap operation should be temporally constrained to a particular time instant, /// 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> /// </param>
/// <returns>A tuple containing the snapped position in coordinate space local to this <see cref="DistanceSnapGrid"/> and the respective time value.</returns> /// <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); public abstract (Vector2 position, double time) GetSnappedPosition(Vector2 position, double? fixedTime = null);