// Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System.Collections.Generic; using OpenTK; using System.Linq; using osu.Framework.MathUtils; using System.Diagnostics; namespace osu.Game.Modes.Osu.Objects { public class SliderCurve { public double Length; public List ControlPoints; public CurveTypes CurveType; public Vector2 Offset; private List calculatedPath = new List(); private List cumulativeLength = new List(); private List calculateSubpath(List subControlPoints) { switch (CurveType) { case CurveTypes.Linear: return subControlPoints; case CurveTypes.PerfectCurve: // If we have a different amount than 3 control points, use bezier for perfect curves. if (ControlPoints.Count != 3) return new BezierApproximator(subControlPoints).CreateBezier(); else { Debug.Assert(subControlPoints.Count == 3); // Here we have exactly 3 control points. Attempt to fit a circular arc. List subpath = new CircularArcApproximator(subControlPoints[0], subControlPoints[1], subControlPoints[2]).CreateArc(); if (subpath.Count == 0) // For some reason a circular arc could not be fit to the 3 given points. Fall back // to a numerically stable bezier approximation. subpath = new BezierApproximator(subControlPoints).CreateBezier(); return subpath; } default: return new BezierApproximator(subControlPoints).CreateBezier(); } } 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. List subControlPoints = new List(); for (int i = 0; i < ControlPoints.Count; ++i) { subControlPoints.Add(ControlPoints[i]); if (i == ControlPoints.Count - 1 || ControlPoints[i] == ControlPoints[i + 1]) { List subpath = calculateSubpath(subControlPoints); for (int j = 0; j < subpath.Count; ++j) // Only add those vertices that add a new segment to the path. if (calculatedPath.Count == 0 || calculatedPath.Last() != subpath[j]) calculatedPath.Add(subpath[j]); subControlPoints.Clear(); } } } 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 curves that are too long compared to what's // in the .osu file. if (Length - l < d) { calculatedPath[i + 1] = calculatedPath[i] + diff * (float)((Length - l) / d); calculatedPath.RemoveRange(i + 2, calculatedPath.Count - 2 - i); l = Length; cumulativeLength.Add(l); break; } l += d; cumulativeLength.Add(l); } //TODO: Figure out if the following code is needed in some cases. Judging by the map // "Transform" http://osu.ppy.sh/s/484689 it seems like we should _not_ be doing this. // Lengthen slider curves that are too short compared to what's // in the .osu file. /*if (l < Length && 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)((Length - l) / d); cumulativeLength[calculatedPath.Count - 1] = Length; }*/ } public void Calculate() { calculatePath(); calculateCumulativeLengthAndTrimPath(); } 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) * Length; } 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; } /// /// Computes the slider curve 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. /// /// The list to be filled with the computed curve. /// Start progress. Ranges from 0 (beginning of the slider) to 1 (end of the slider). /// End progress. Ranges from 0 (beginning of the slider) to 1 (end of the slider). public void GetPathToProgress(List path, double p0, double p1) { if (calculatedPath.Count == 0 && ControlPoints.Count > 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); } /// /// Computes the position on the slider at a given progress that ranges from 0 (beginning of the slider) /// to 1 (end of the slider). /// /// Ranges from 0 (beginning of the slider) to 1 (end of the slider). /// public Vector2 PositionAt(double progress) { if (calculatedPath.Count == 0 && ControlPoints.Count > 0) Calculate(); double d = progressToDistance(progress); return interpolateVertices(indexOfDistance(d), d) + Offset; } } }