Skip to content

Fix SliderPath.GetSegmentEnds #24581

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Aug 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions osu.Game.Tests/Visual/Gameplay/TestSceneSliderPath.cs
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,37 @@ public void TestShortenToNegativeLength()
AddStep("shorten to -10 length", () => path.ExpectedDistance.Value = -10);
}

[Test]
public void TestGetSegmentEnds()
{
var positions = new[]
{
Vector2.Zero,
new Vector2(100, 0),
new Vector2(100),
new Vector2(200, 100),
};
double[] distances = { 100d, 200d, 300d };

AddStep("create path", () => path.ControlPoints.AddRange(positions.Select(p => new PathControlPoint(p, PathType.Linear))));
AddAssert("segment ends are correct", () => path.GetSegmentEnds(), () => Is.EqualTo(distances.Select(d => d / 300)));
AddAssert("segment end positions recovered", () => path.GetSegmentEnds().Select(p => path.PositionAt(p)), () => Is.EqualTo(positions.Skip(1)));

AddStep("lengthen last segment", () => path.ExpectedDistance.Value = 400);
AddAssert("segment ends are correct", () => path.GetSegmentEnds(), () => Is.EqualTo(distances.Select(d => d / 400)));
AddAssert("segment end positions recovered", () => path.GetSegmentEnds().Select(p => path.PositionAt(p)), () => Is.EqualTo(positions.Skip(1)));

AddStep("shorten last segment", () => path.ExpectedDistance.Value = 150);
AddAssert("segment ends are correct", () => path.GetSegmentEnds(), () => Is.EqualTo(distances.Select(d => d / 150)));
// see remarks in `GetSegmentEnds()` xmldoc (`SliderPath.PositionAt()` clamps progress to [0,1]).
AddAssert("segment end positions not recovered", () => path.GetSegmentEnds().Select(p => path.PositionAt(p)), () => Is.EqualTo(new[]
{
positions[1],
new Vector2(100, 50),
new Vector2(100, 50),
}));
}

private List<PathControlPoint> createSegment(PathType type, params Vector2[] controlPoints)
{
var points = controlPoints.Select(p => new PathControlPoint { Position = p }).ToList();
Expand Down
42 changes: 33 additions & 9 deletions osu.Game/Rulesets/Objects/SliderPath.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,13 @@ public class SliderPath

private readonly List<Vector2> calculatedPath = new List<Vector2>();
private readonly List<double> cumulativeLength = new List<double>();
private readonly List<int> segmentEnds = new List<int>();
private readonly Cached pathCache = new Cached();

private double calculatedLength;

private readonly List<int> segmentEnds = new List<int>();
private double[] segmentEndDistances = Array.Empty<double>();

/// <summary>
/// Creates a new <see cref="SliderPath"/>.
/// </summary>
Expand Down Expand Up @@ -196,13 +198,28 @@ public List<PathControlPoint> PointsInSegment(PathControlPoint controlPoint)
}

/// <summary>
/// Returns the progress values at which segments of the path end.
/// Returns the progress values at which (control point) segments of the path end.
/// Ranges from 0 (beginning of the path) to 1 (end of the path) to infinity (beyond the end of the path).
/// </summary>
/// <remarks>
/// <see cref="PositionAt"/> truncates the progression values to [0,1],
/// so you can't use this method in conjunction with that one to retrieve the positions of segment ends beyond the end of the path.
/// </remarks>
/// <example>
/// <para>
/// In case <see cref="Distance"/> is less than <see cref="CalculatedDistance"/>,
/// the last segment ends after the end of the path, hence it returns a value greater than 1.
/// </para>
/// <para>
/// In case <see cref="Distance"/> is greater than <see cref="CalculatedDistance"/>,
/// the last segment ends before the end of the path, hence it returns a value less than 1.
/// </para>
/// </example>
public IEnumerable<double> GetSegmentEnds()
{
ensureValid();

return segmentEnds.Select(i => cumulativeLength[i] / calculatedLength);
return segmentEndDistances.Select(d => d / Distance);
}

private void invalidate()
Expand Down Expand Up @@ -251,8 +268,11 @@ private void calculatePath()
calculatedPath.Add(t);
}

// Remember the index of the segment end
segmentEnds.Add(calculatedPath.Count - 1);
if (i > 0)
{
// Remember the index of the segment end
segmentEnds.Add(calculatedPath.Count - 1);
}

// Start the new segment at the current vertex
start = i;
Expand Down Expand Up @@ -298,6 +318,14 @@ private void calculateLength()
cumulativeLength.Add(calculatedLength);
}

// Store the distances of the segment ends now, because after shortening the indices may be out of range
segmentEndDistances = new double[segmentEnds.Count];

for (int i = 0; i < segmentEnds.Count; i++)
{
segmentEndDistances[i] = cumulativeLength[segmentEnds[i]];
}

if (ExpectedDistance.Value is double expectedDistance && calculatedLength != expectedDistance)
{
// In osu-stable, if the last two control points of a slider are equal, extension is not performed.
Expand All @@ -319,10 +347,6 @@ private void calculateLength()
{
cumulativeLength.RemoveAt(cumulativeLength.Count - 1);
calculatedPath.RemoveAt(pathEndIndex--);

// Shorten the last segment to the expected distance
if (segmentEnds.Count > 0)
segmentEnds[^1]--;
}
}

Expand Down