diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
index 6ffe27dc13..cb57c8e6e0 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
@@ -401,7 +401,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) ?? (float)HitObject.Path.CalculatedDistance;
+ HitObject.Path.ExpectedDistance.Value = distanceSnapProvider?.FindSnappedDistance(HitObject, (float)HitObject.Path.CalculatedDistance, DistanceSnapTarget.Start) ?? (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 1debb09099..cd66f8d796 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
@@ -269,7 +269,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{
double minDistance = distanceSnapProvider?.GetBeatSnapDistanceAt(HitObject, false) * 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) ?? proposedDistance;
+ proposedDistance = distanceSnapProvider?.FindSnappedDistance(HitObject, (float)proposedDistance + 1, DistanceSnapTarget.Start) ?? proposedDistance;
proposedDistance = MathHelper.Clamp(proposedDistance, minDistance, HitObject.Path.CalculatedDistance);
}
diff --git a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs
index d16199b0f5..4f4556b820 100644
--- a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs
+++ b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs
@@ -228,6 +228,42 @@ namespace osu.Game.Tests.Editing
assertSnappedDistance(400, 400);
}
+ [Test]
+ public void TestUnsnappedObject()
+ {
+ var slider = new Slider
+ {
+ StartTime = 0,
+ Path = new SliderPath
+ {
+ ControlPoints =
+ {
+ new PathControlPoint(),
+ // simulate object snapped to 1/3rds
+ // this object's end time will be 2000 / 3 = 666.66... ms
+ new PathControlPoint(new Vector2(200 / 3f, 0)),
+ }
+ }
+ };
+
+ AddStep("add slider", () => composer.EditorBeatmap.Add(slider));
+ AddStep("set snap to 1/4", () => BeatDivisor.Value = 4);
+
+ // with default beat length of 1000ms and snap at 1/4, the valid snap times are 500ms, 750ms, and 1000ms
+ // with default settings, the snapped distance will be a tenth of the difference of the time delta
+
+ // (500 - 666.66...) / 10 = -16.66... = -100 / 6
+ assertSnappedDistance(0, -100 / 6f, slider);
+ assertSnappedDistance(7, -100 / 6f, slider);
+
+ // (750 - 666.66...) / 10 = 8.33... = 100 / 12
+ assertSnappedDistance(9, 100 / 12f, slider);
+ assertSnappedDistance(33, 100 / 12f, slider);
+
+ // (1000 - 666.66...) / 10 = 33.33... = 100 / 3
+ assertSnappedDistance(34, 100 / 3f, slider);
+ }
+
[Test]
public void TestUseCurrentSnap()
{
@@ -263,7 +299,7 @@ namespace osu.Game.Tests.Editing
=> 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.DistanceSnapProvider.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, DistanceSnapTarget.End), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON));
private partial class TestHitObjectComposer : OsuHitObjectComposer
{
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs
index f2a015402a..c1a788cd22 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs
@@ -199,7 +199,7 @@ namespace osu.Game.Tests.Visual.Editing
public double FindSnappedDuration(HitObject referenceObject, float distance) => 0;
- public float FindSnappedDistance(HitObject referenceObject, float distance) => 0;
+ public float FindSnappedDistance(HitObject referenceObject, float distance, DistanceSnapTarget target) => 0;
}
}
}
diff --git a/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs b/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs
index 979492fd8b..7ed692ad3d 100644
--- a/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs
+++ b/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs
@@ -280,22 +280,36 @@ namespace osu.Game.Rulesets.Edit
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)
+ public virtual float FindSnappedDistance(HitObject referenceObject, float distance, DistanceSnapTarget target)
{
- double startTime = referenceObject.StartTime;
+ double referenceTime;
- double actualDuration = startTime + DistanceToDuration(referenceObject, distance);
+ switch (target)
+ {
+ case DistanceSnapTarget.Start:
+ referenceTime = referenceObject.StartTime;
+ break;
- double snappedEndTime = beatSnapProvider.SnapTime(actualDuration, startTime);
+ case DistanceSnapTarget.End:
+ referenceTime = referenceObject.GetEndTime();
+ break;
- double beatLength = beatSnapProvider.GetBeatLengthAtTime(startTime);
+ 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.
// 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;
+ if (snappedTime > actualDuration + 1)
+ snappedTime -= beatLength;
- return DurationToDistance(referenceObject, snappedEndTime - startTime);
+ return DurationToDistance(referenceObject, snappedTime - referenceTime);
}
#endregion
diff --git a/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs b/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs
index 380038eadf..17fae9e8b2 100644
--- a/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs
+++ b/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs
@@ -58,10 +58,17 @@ namespace osu.Game.Rulesets.Edit
///
/// An object to be used as a reference point for this operation.
/// The distance to convert.
+ /// Whether the distance measured should be from the start or the end of .
///
/// A value that represents snapped to the closest beat of the timing point.
/// The distance will always be less than or equal to the provided .
///
- float FindSnappedDistance(HitObject referenceObject, float distance);
+ float FindSnappedDistance(HitObject referenceObject, float distance, DistanceSnapTarget target);
+ }
+
+ public enum DistanceSnapTarget
+ {
+ Start,
+ End,
}
}
diff --git a/osu.Game/Rulesets/Objects/SliderPathExtensions.cs b/osu.Game/Rulesets/Objects/SliderPathExtensions.cs
index c03d3646da..a631274f74 100644
--- a/osu.Game/Rulesets/Objects/SliderPathExtensions.cs
+++ b/osu.Game/Rulesets/Objects/SliderPathExtensions.cs
@@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Objects
public static void SnapTo(this THitObject hitObject, IDistanceSnapProvider? snapProvider)
where THitObject : HitObject, IHasPath
{
- hitObject.Path.ExpectedDistance.Value = snapProvider?.FindSnappedDistance(hitObject, (float)hitObject.Path.CalculatedDistance) ?? hitObject.Path.CalculatedDistance;
+ hitObject.Path.ExpectedDistance.Value = snapProvider?.FindSnappedDistance(hitObject, (float)hitObject.Path.CalculatedDistance, DistanceSnapTarget.Start) ?? hitObject.Path.CalculatedDistance;
}
///
diff --git a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs
index 92fe52148c..bd750dac76 100644
--- a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs
@@ -59,7 +59,7 @@ 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);
+ float offset = SnapProvider.FindSnappedDistance(ReferenceObject, 0, DistanceSnapTarget.End);
for (int i = 0; i < requiredCircles; i++)
{
@@ -104,7 +104,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
? SnapProvider.DurationToDistance(ReferenceObject, editorClock.CurrentTime - ReferenceObject.GetEndTime())
// 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);
+ : SnapProvider.FindSnappedDistance(ReferenceObject, travelLength / distanceSpacingMultiplier, DistanceSnapTarget.End);
double snappedTime = StartTime + SnapProvider.DistanceToDuration(ReferenceObject, snappedDistance);
diff --git a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs
index 8aa2fa9f45..7003d632ca 100644
--- a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs
@@ -155,7 +155,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
var timingPoint = Beatmap.ControlPointInfo.TimingPointAt(StartTime);
double beatLength = timingPoint.BeatLength / beatDivisor.Value;
- int beatIndex = (int)Math.Round((StartTime - timingPoint.Time) / beatLength);
+ int beatIndex = (int)Math.Floor((StartTime - timingPoint.Time) / beatLength);
var colour = BindableBeatDivisor.GetColourFor(BindableBeatDivisor.GetDivisorForBeatIndex(beatIndex + placementIndex + 1, beatDivisor.Value), Colours);