mirror of
https://github.com/ppy/osu.git
synced 2024-11-09 06:17:24 +08:00
233 lines
8.0 KiB
C#
233 lines
8.0 KiB
C#
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
|
|
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using osu.Framework.MathUtils;
|
|
using osu.Game.Rulesets.Objects.Types;
|
|
using OpenTK;
|
|
|
|
namespace osu.Game.Rulesets.Objects
|
|
{
|
|
public class SliderPath
|
|
{
|
|
public double Distance;
|
|
|
|
public Vector2[] ControlPoints = Array.Empty<Vector2>();
|
|
|
|
public PathType PathType = PathType.PerfectCurve;
|
|
|
|
public Vector2 Offset;
|
|
|
|
private readonly List<Vector2> calculatedPath = new List<Vector2>();
|
|
private readonly List<double> cumulativeLength = new List<double>();
|
|
|
|
private List<Vector2> calculateSubpath(ReadOnlySpan<Vector2> subControlPoints)
|
|
{
|
|
switch (PathType)
|
|
{
|
|
case PathType.Linear:
|
|
return PathApproximator.ApproximateLinear(subControlPoints);
|
|
case PathType.PerfectCurve:
|
|
//we can only use CircularArc iff we have exactly three control points and no dissection.
|
|
if (ControlPoints.Length != 3 || subControlPoints.Length != 3)
|
|
break;
|
|
|
|
// Here we have exactly 3 control points. Attempt to fit a circular arc.
|
|
List<Vector2> subpath = PathApproximator.ApproximateCircularArc(subControlPoints);
|
|
|
|
// If for some reason a circular arc could not be fit to the 3 given points, fall back to a numerically stable bezier approximation.
|
|
if (subpath.Count == 0)
|
|
break;
|
|
|
|
return subpath;
|
|
case PathType.Catmull:
|
|
return PathApproximator.ApproximateCatmull(subControlPoints);
|
|
}
|
|
|
|
return PathApproximator.ApproximateBezier(subControlPoints);
|
|
}
|
|
|
|
private void calculatePath()
|
|
{
|
|
calculatedPath.Clear();
|
|
|
|
// Sliders may consist of various subpaths separated by two consecutive vertices
|
|
// with the same position. The following loop parses these subpaths and computes
|
|
// their shape independently, consecutively appending them to calculatedPath.
|
|
|
|
int start = 0;
|
|
int end = 0;
|
|
|
|
for (int i = 0; i < ControlPoints.Length; ++i)
|
|
{
|
|
end++;
|
|
|
|
if (i == ControlPoints.Length - 1 || ControlPoints[i] == ControlPoints[i + 1])
|
|
{
|
|
ReadOnlySpan<Vector2> cpSpan = ControlPoints.AsSpan().Slice(start, end - start);
|
|
|
|
foreach (Vector2 t in calculateSubpath(cpSpan))
|
|
if (calculatedPath.Count == 0 || calculatedPath.Last() != t)
|
|
calculatedPath.Add(t);
|
|
|
|
start = end;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void calculateCumulativeLengthAndTrimPath()
|
|
{
|
|
double l = 0;
|
|
|
|
cumulativeLength.Clear();
|
|
cumulativeLength.Add(l);
|
|
|
|
for (int i = 0; i < calculatedPath.Count - 1; ++i)
|
|
{
|
|
Vector2 diff = calculatedPath[i + 1] - calculatedPath[i];
|
|
double d = diff.Length;
|
|
|
|
// Shorten slider paths that are too long compared to what's
|
|
// in the .osu file.
|
|
if (Distance - l < d)
|
|
{
|
|
calculatedPath[i + 1] = calculatedPath[i] + diff * (float)((Distance - l) / d);
|
|
calculatedPath.RemoveRange(i + 2, calculatedPath.Count - 2 - i);
|
|
|
|
l = Distance;
|
|
cumulativeLength.Add(l);
|
|
break;
|
|
}
|
|
|
|
l += d;
|
|
cumulativeLength.Add(l);
|
|
}
|
|
|
|
// Lengthen slider paths that are too short compared to what's
|
|
// in the .osu file.
|
|
if (l < Distance && calculatedPath.Count > 1)
|
|
{
|
|
Vector2 diff = calculatedPath[calculatedPath.Count - 1] - calculatedPath[calculatedPath.Count - 2];
|
|
double d = diff.Length;
|
|
|
|
if (d <= 0)
|
|
return;
|
|
|
|
calculatedPath[calculatedPath.Count - 1] += diff * (float)((Distance - l) / d);
|
|
cumulativeLength[calculatedPath.Count - 1] = Distance;
|
|
}
|
|
}
|
|
|
|
private void calculateCumulativeLength()
|
|
{
|
|
double l = 0;
|
|
|
|
cumulativeLength.Clear();
|
|
cumulativeLength.Add(l);
|
|
|
|
for (int i = 0; i < calculatedPath.Count - 1; ++i)
|
|
{
|
|
Vector2 diff = calculatedPath[i + 1] - calculatedPath[i];
|
|
double d = diff.Length;
|
|
|
|
l += d;
|
|
cumulativeLength.Add(l);
|
|
}
|
|
|
|
Distance = l;
|
|
}
|
|
|
|
public void Calculate(bool updateDistance = false)
|
|
{
|
|
calculatePath();
|
|
|
|
if (!updateDistance)
|
|
calculateCumulativeLengthAndTrimPath();
|
|
else
|
|
calculateCumulativeLength();
|
|
}
|
|
|
|
private int indexOfDistance(double d)
|
|
{
|
|
int i = cumulativeLength.BinarySearch(d);
|
|
if (i < 0) i = ~i;
|
|
|
|
return i;
|
|
}
|
|
|
|
private double progressToDistance(double progress)
|
|
{
|
|
return MathHelper.Clamp(progress, 0, 1) * Distance;
|
|
}
|
|
|
|
private Vector2 interpolateVertices(int i, double d)
|
|
{
|
|
if (calculatedPath.Count == 0)
|
|
return Vector2.Zero;
|
|
|
|
if (i <= 0)
|
|
return calculatedPath.First();
|
|
else if (i >= calculatedPath.Count)
|
|
return calculatedPath.Last();
|
|
|
|
Vector2 p0 = calculatedPath[i - 1];
|
|
Vector2 p1 = calculatedPath[i];
|
|
|
|
double d0 = cumulativeLength[i - 1];
|
|
double d1 = cumulativeLength[i];
|
|
|
|
// Avoid division by and almost-zero number in case two points are extremely close to each other.
|
|
if (Precision.AlmostEquals(d0, d1))
|
|
return p0;
|
|
|
|
double w = (d - d0) / (d1 - d0);
|
|
return p0 + (p1 - p0) * (float)w;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Computes the slider path until a given progress that ranges from 0 (beginning of the slider)
|
|
/// to 1 (end of the slider) and stores the generated path in the given list.
|
|
/// </summary>
|
|
/// <param name="path">The list to be filled with the computed path.</param>
|
|
/// <param name="p0">Start progress. Ranges from 0 (beginning of the slider) to 1 (end of the slider).</param>
|
|
/// <param name="p1">End progress. Ranges from 0 (beginning of the slider) to 1 (end of the slider).</param>
|
|
public void GetPathToProgress(List<Vector2> path, double p0, double p1)
|
|
{
|
|
if (calculatedPath.Count == 0 && ControlPoints.Length > 0)
|
|
Calculate();
|
|
|
|
double d0 = progressToDistance(p0);
|
|
double d1 = progressToDistance(p1);
|
|
|
|
path.Clear();
|
|
|
|
int i = 0;
|
|
for (; i < calculatedPath.Count && cumulativeLength[i] < d0; ++i) { }
|
|
|
|
path.Add(interpolateVertices(i, d0) + Offset);
|
|
|
|
for (; i < calculatedPath.Count && cumulativeLength[i] <= d1; ++i)
|
|
path.Add(calculatedPath[i] + Offset);
|
|
|
|
path.Add(interpolateVertices(i, d1) + Offset);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Computes the position on the slider at a given progress that ranges from 0 (beginning of the path)
|
|
/// to 1 (end of the path).
|
|
/// </summary>
|
|
/// <param name="progress">Ranges from 0 (beginning of the path) to 1 (end of the path).</param>
|
|
/// <returns></returns>
|
|
public Vector2 PositionAt(double progress)
|
|
{
|
|
if (calculatedPath.Count == 0 && ControlPoints.Length > 0)
|
|
Calculate();
|
|
|
|
double d = progressToDistance(progress);
|
|
return interpolateVertices(indexOfDistance(d), d) + Offset;
|
|
}
|
|
}
|
|
}
|