1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-22 22:07:28 +08:00
osu-lazer/osu.Game/Rulesets/Objects/SliderPath.cs

410 lines
15 KiB
C#
Raw Normal View History

2019-12-05 16:49:32 +08:00
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
2018-04-13 17:19:50 +08:00
2018-10-11 16:44:25 +08:00
using System;
2018-04-13 17:19:50 +08:00
using System.Collections.Generic;
using System.Collections.Specialized;
2018-04-13 17:19:50 +08:00
using System.Linq;
using Newtonsoft.Json;
2019-12-05 16:49:32 +08:00
using osu.Framework.Bindables;
using osu.Framework.Caching;
2020-01-09 12:43:44 +08:00
using osu.Framework.Utils;
2018-04-13 17:19:50 +08:00
using osu.Game.Rulesets.Objects.Types;
2018-11-20 15:51:59 +08:00
using osuTK;
2018-04-13 17:19:50 +08:00
namespace osu.Game.Rulesets.Objects
{
2019-12-05 16:49:32 +08:00
public class SliderPath
2018-04-13 17:19:50 +08:00
{
/// <summary>
/// The current version of this <see cref="SliderPath"/>. Updated when any change to the path occurs.
/// </summary>
2019-12-09 19:18:18 +08:00
[JsonIgnore]
public IBindable<int> Version => version;
private readonly Bindable<int> version = new Bindable<int>();
2018-11-12 13:16:21 +08:00
/// <summary>
/// The user-set distance of the path. If non-null, <see cref="Distance"/> will match this value,
/// and the path will be shortened/lengthened to match this length.
/// </summary>
2019-12-05 16:49:54 +08:00
public readonly Bindable<double?> ExpectedDistance = new Bindable<double?>();
2018-04-13 17:19:50 +08:00
2018-11-12 15:38:14 +08:00
/// <summary>
2019-12-05 16:49:32 +08:00
/// The control points of the path.
2018-11-12 15:38:14 +08:00
/// </summary>
2019-12-05 16:49:32 +08:00
public readonly BindableList<PathControlPoint> ControlPoints = new BindableList<PathControlPoint>();
2018-11-12 15:38:14 +08:00
2019-12-05 16:49:32 +08:00
private readonly List<Vector2> calculatedPath = new List<Vector2>();
private readonly List<double> cumulativeLength = new List<double>();
2019-12-09 16:45:08 +08:00
private readonly Cached pathCache = new Cached();
private double calculatedLength;
2018-11-12 13:08:36 +08:00
2018-11-12 13:16:21 +08:00
/// <summary>
/// Creates a new <see cref="SliderPath"/>.
/// </summary>
2019-12-05 17:19:42 +08:00
public SliderPath()
{
ExpectedDistance.ValueChanged += _ => invalidate();
2018-04-13 17:19:50 +08:00
ControlPoints.CollectionChanged += (_, args) =>
2019-12-05 16:49:32 +08:00
{
switch (args.Action)
{
case NotifyCollectionChangedAction.Add:
foreach (var c in args.NewItems.Cast<PathControlPoint>())
{
c.Changed += invalidate;
c.Changed += updatePathTypes;
}
2021-03-23 00:32:40 +08:00
break;
2018-10-26 13:18:48 +08:00
case NotifyCollectionChangedAction.Reset:
case NotifyCollectionChangedAction.Remove:
foreach (var c in args.OldItems.Cast<PathControlPoint>())
{
c.Changed -= invalidate;
c.Changed -= updatePathTypes;
}
2021-03-23 00:32:40 +08:00
break;
}
2019-12-05 16:49:32 +08:00
invalidate();
2019-12-05 16:49:32 +08:00
};
2019-12-05 17:19:42 +08:00
}
/// <summary>
2019-12-09 16:45:08 +08:00
/// Creates a new <see cref="SliderPath"/> initialised with a list of control points.
2019-12-05 17:19:42 +08:00
/// </summary>
/// <param name="controlPoints">An optional set of <see cref="PathControlPoint"/>s to initialise the path with.</param>
/// <param name="expectedDistance">A user-set distance of the path that may be shorter or longer than the true distance between all control points.
/// The path will be shortened/lengthened to match this length. If null, the path will use the true distance between all control points.</param>
[JsonConstructor]
public SliderPath(PathControlPoint[] controlPoints, double? expectedDistance = null)
: this()
{
2019-12-05 16:49:32 +08:00
ControlPoints.AddRange(controlPoints);
2019-12-05 17:19:42 +08:00
ExpectedDistance.Value = expectedDistance;
}
2019-12-05 16:49:32 +08:00
2019-12-05 17:19:42 +08:00
public SliderPath(PathType type, Vector2[] controlPoints, double? expectedDistance = null)
2019-12-09 16:45:08 +08:00
: this(controlPoints.Select((c, i) => new PathControlPoint(c, i == 0 ? (PathType?)type : null)).ToArray(), expectedDistance)
2019-12-05 17:19:42 +08:00
{
}
2018-11-12 13:16:21 +08:00
/// <summary>
/// The distance of the path after lengthening/shortening to account for <see cref="ExpectedDistance"/>.
/// </summary>
2018-11-12 15:38:39 +08:00
[JsonIgnore]
2018-11-12 15:38:14 +08:00
public double Distance
{
get
{
2019-12-05 16:49:32 +08:00
ensureValid();
2019-12-14 20:54:22 +08:00
return cumulativeLength.Count == 0 ? 0 : cumulativeLength[^1];
2018-11-12 15:38:14 +08:00
}
}
2018-11-12 13:08:36 +08:00
/// <summary>
/// The distance of the path prior to lengthening/shortening to account for <see cref="ExpectedDistance"/>.
/// </summary>
public double CalculatedDistance
{
get
{
ensureValid();
return calculatedLength;
}
}
2018-11-12 13:08:36 +08:00
/// <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)
{
2019-12-05 16:49:32 +08:00
ensureValid();
2018-11-12 15:38:14 +08:00
2018-11-12 13:08:36 +08:00
double d0 = progressToDistance(p0);
double d1 = progressToDistance(p1);
path.Clear();
int i = 0;
2019-04-01 11:16:05 +08:00
2018-11-12 13:08:36 +08:00
for (; i < calculatedPath.Count && cumulativeLength[i] < d0; ++i)
{
}
path.Add(interpolateVertices(i, d0));
for (; i < calculatedPath.Count && cumulativeLength[i] <= d1; ++i)
path.Add(calculatedPath[i]);
path.Add(interpolateVertices(i, d1));
}
/// <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)
{
2019-12-05 16:49:32 +08:00
ensureValid();
2018-11-12 15:38:14 +08:00
2018-11-12 13:08:36 +08:00
double d = progressToDistance(progress);
return interpolateVertices(indexOfDistance(d), d);
}
2018-04-13 17:19:50 +08:00
2021-03-22 22:55:30 +08:00
/// <summary>
/// Returns the control points belonging to the same segment as the one given.
/// The first point has a PathType which all other points inherit.
/// </summary>
/// <param name="controlPoint">One of the control points in the segment.</param>
/// <returns></returns>
public List<PathControlPoint> PointsInSegment(PathControlPoint controlPoint)
{
bool found = false;
List<PathControlPoint> pointsInCurrentSegment = new List<PathControlPoint>();
2021-03-23 02:03:55 +08:00
2021-03-22 22:55:30 +08:00
foreach (PathControlPoint point in ControlPoints)
{
2021-03-23 00:32:55 +08:00
if (point.Type.Value != null)
2021-03-22 22:55:30 +08:00
{
if (!found)
pointsInCurrentSegment.Clear();
else
{
pointsInCurrentSegment.Add(point);
break;
}
}
pointsInCurrentSegment.Add(point);
if (point == controlPoint)
found = true;
}
return pointsInCurrentSegment;
}
2021-03-23 02:42:27 +08:00
/// <summary>
/// Handles correction of invalid path types.
/// </summary>
private void updatePathTypes()
{
foreach (PathControlPoint segmentStartPoint in ControlPoints.Where(p => p.Type.Value != null))
{
if (segmentStartPoint.Type.Value != PathType.PerfectCurve)
continue;
Vector2[] points = PointsInSegment(segmentStartPoint).Select(p => p.Position.Value).ToArray();
if (points.Length == 3 && !validCircularArcSegment(points))
segmentStartPoint.Type.Value = PathType.Bezier;
}
}
2021-03-23 02:42:27 +08:00
/// <summary>
/// Returns whether the given points are arranged in a valid way. Invalid if points
/// are almost entirely linear - as this causes the radius to approach infinity,
/// which would exhaust memory when drawing / approximating.
/// </summary>
/// <param name="points">The three points that make up this circular arc segment.</param>
/// <returns></returns>
private bool validCircularArcSegment(IReadOnlyList<Vector2> points)
{
2021-03-23 02:09:28 +08:00
Vector2 a = points[0];
Vector2 b = points[1];
Vector2 c = points[2];
2021-03-23 02:09:28 +08:00
float maxLength = points.Max(p => p.Length);
2021-03-23 02:09:28 +08:00
Vector2 normA = new Vector2(a.X / maxLength, a.Y / maxLength);
Vector2 normB = new Vector2(b.X / maxLength, b.Y / maxLength);
Vector2 normC = new Vector2(c.X / maxLength, c.Y / maxLength);
2021-03-23 02:09:28 +08:00
float det = (normA.X - normB.X) * (normB.Y - normC.Y) - (normB.X - normC.X) * (normA.Y - normB.Y);
2021-03-23 02:09:28 +08:00
float acSq = (a - c).LengthSquared;
float abSq = (a - b).LengthSquared;
float bcSq = (b - c).LengthSquared;
2021-03-23 02:09:28 +08:00
// Exterior = curve wraps around the long way between end-points
// Exterior bottleneck is drawing-related, interior bottleneck is approximation-related,
2021-03-23 02:03:55 +08:00
// where the latter is much faster, hence differing thresholds
bool exterior = abSq > acSq || bcSq > acSq;
float threshold = exterior ? 0.05f : 0.001f;
return Math.Abs(det) >= threshold;
}
private void invalidate()
{
pathCache.Invalidate();
version.Value++;
}
2019-12-05 16:49:32 +08:00
private void ensureValid()
2018-11-12 15:38:14 +08:00
{
2019-12-05 16:49:32 +08:00
if (pathCache.IsValid)
2018-11-12 15:38:14 +08:00
return;
2019-02-28 12:31:40 +08:00
2018-11-12 15:38:14 +08:00
calculatePath();
calculateLength();
2019-12-05 16:49:32 +08:00
pathCache.Validate();
2018-11-12 15:38:14 +08:00
}
2019-12-05 16:49:32 +08:00
private void calculatePath()
2018-04-13 17:19:50 +08:00
{
2019-12-05 16:49:32 +08:00
calculatedPath.Clear();
2019-04-01 11:16:05 +08:00
2019-12-05 16:49:32 +08:00
if (ControlPoints.Count == 0)
return;
2018-04-13 17:19:50 +08:00
2019-12-05 16:49:32 +08:00
Vector2[] vertices = new Vector2[ControlPoints.Count];
for (int i = 0; i < ControlPoints.Count; i++)
vertices[i] = ControlPoints[i].Position.Value;
2018-04-13 17:19:50 +08:00
2019-12-05 16:49:32 +08:00
int start = 0;
2019-04-01 11:16:05 +08:00
2019-12-05 16:49:32 +08:00
for (int i = 0; i < ControlPoints.Count; i++)
{
if (ControlPoints[i].Type.Value == null && i < ControlPoints.Count - 1)
continue;
2018-04-13 17:19:50 +08:00
2019-12-05 16:49:32 +08:00
// The current vertex ends the segment
var segmentVertices = vertices.AsSpan().Slice(start, i - start + 1);
2019-12-09 16:47:05 +08:00
var segmentType = ControlPoints[start].Type.Value ?? PathType.Linear;
2018-04-13 17:19:50 +08:00
2019-12-09 17:25:13 +08:00
foreach (Vector2 t in calculateSubPath(segmentVertices, segmentType))
2019-12-05 16:49:32 +08:00
{
if (calculatedPath.Count == 0 || calculatedPath.Last() != t)
calculatedPath.Add(t);
}
2018-10-11 16:44:25 +08:00
2019-12-05 16:49:32 +08:00
// Start the new segment at the current vertex
start = i;
}
2019-12-09 17:25:13 +08:00
}
2018-10-11 16:44:25 +08:00
2019-12-09 17:25:13 +08:00
private List<Vector2> calculateSubPath(ReadOnlySpan<Vector2> subControlPoints, PathType type)
{
switch (type)
2018-04-13 17:19:50 +08:00
{
2019-12-09 17:25:13 +08:00
case PathType.Linear:
return PathApproximator.ApproximateLinear(subControlPoints);
2019-12-05 16:49:32 +08:00
2019-12-09 17:25:13 +08:00
case PathType.PerfectCurve:
if (subControlPoints.Length != 3)
break;
2018-10-11 16:44:25 +08:00
2019-12-09 17:25:13 +08:00
List<Vector2> subpath = PathApproximator.ApproximateCircularArc(subControlPoints);
2018-10-11 16:44:25 +08:00
2021-03-22 22:54:33 +08:00
// 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)
2019-12-09 17:25:13 +08:00
break;
2019-12-05 16:49:32 +08:00
2019-12-09 17:25:13 +08:00
return subpath;
2019-12-05 16:49:32 +08:00
2019-12-09 17:25:13 +08:00
case PathType.Catmull:
return PathApproximator.ApproximateCatmull(subControlPoints);
2018-04-13 17:19:50 +08:00
}
2019-12-09 17:25:13 +08:00
return PathApproximator.ApproximateBezier(subControlPoints);
2018-04-13 17:19:50 +08:00
}
private void calculateLength()
2018-04-13 17:19:50 +08:00
{
calculatedLength = 0;
2018-04-13 17:19:50 +08:00
cumulativeLength.Clear();
cumulativeLength.Add(0);
2019-12-05 16:49:54 +08:00
for (int i = 0; i < calculatedPath.Count - 1; i++)
2018-04-13 17:19:50 +08:00
{
Vector2 diff = calculatedPath[i + 1] - calculatedPath[i];
calculatedLength += diff.Length;
cumulativeLength.Add(calculatedLength);
2018-04-13 17:19:50 +08:00
}
if (ExpectedDistance.Value is double expectedDistance && calculatedLength != expectedDistance)
2018-04-13 17:19:50 +08:00
{
// The last length is always incorrect
cumulativeLength.RemoveAt(cumulativeLength.Count - 1);
2018-04-13 17:19:50 +08:00
int pathEndIndex = calculatedPath.Count - 1;
if (calculatedLength > expectedDistance)
{
// The path will be shortened further, in which case we should trim any more unnecessary lengths and their associated path segments
2019-12-14 20:54:22 +08:00
while (cumulativeLength.Count > 0 && cumulativeLength[^1] >= expectedDistance)
{
cumulativeLength.RemoveAt(cumulativeLength.Count - 1);
calculatedPath.RemoveAt(pathEndIndex--);
}
}
if (pathEndIndex <= 0)
{
// The expected distance is negative or zero
// TODO: Perhaps negative path lengths should be disallowed altogether
cumulativeLength.Add(0);
2018-04-13 17:19:50 +08:00
return;
}
// The direction of the segment to shorten or lengthen
Vector2 dir = (calculatedPath[pathEndIndex] - calculatedPath[pathEndIndex - 1]).Normalized();
2018-04-13 17:19:50 +08:00
2019-12-14 20:54:22 +08:00
calculatedPath[pathEndIndex] = calculatedPath[pathEndIndex - 1] + dir * (float)(expectedDistance - cumulativeLength[^1]);
cumulativeLength.Add(expectedDistance);
}
2018-04-13 17:19:50 +08:00
}
private int indexOfDistance(double d)
{
int i = cumulativeLength.BinarySearch(d);
if (i < 0) i = ~i;
return i;
}
private double progressToDistance(double progress)
{
return Math.Clamp(progress, 0, 1) * Distance;
2018-04-13 17:19:50 +08:00
}
private Vector2 interpolateVertices(int i, double d)
{
if (calculatedPath.Count == 0)
return Vector2.Zero;
if (i <= 0)
return calculatedPath.First();
if (i >= calculatedPath.Count)
2018-04-13 17:19:50 +08:00
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;
}
}
}