diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSliderPath2.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSliderPath.cs similarity index 97% rename from osu.Game.Tests/Visual/Gameplay/TestSceneSliderPath2.cs rename to osu.Game.Tests/Visual/Gameplay/TestSceneSliderPath.cs index 08d54fcdda..fe2cc188a5 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSliderPath2.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSliderPath.cs @@ -13,12 +13,12 @@ using osuTK; namespace osu.Game.Tests.Visual.Gameplay { - public class TestSceneSliderPath2 : OsuTestScene + public class TestSceneSliderPath : OsuTestScene { private readonly SmoothPath drawablePath; - private SliderPath2 path; + private SliderPath path; - public TestSceneSliderPath2() + public TestSceneSliderPath() { Child = drawablePath = new SmoothPath { @@ -30,7 +30,7 @@ namespace osu.Game.Tests.Visual.Gameplay [SetUp] public void Setup() => Schedule(() => { - path = new SliderPath2(); + path = new SliderPath(); }); protected override void Update() diff --git a/osu.Game/Rulesets/Objects/SliderPath.cs b/osu.Game/Rulesets/Objects/SliderPath.cs index ae6aad5b9c..cc2b537afc 100644 --- a/osu.Game/Rulesets/Objects/SliderPath.cs +++ b/osu.Game/Rulesets/Objects/SliderPath.cs @@ -1,17 +1,20 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using Newtonsoft.Json; +using osu.Framework.Bindables; +using osu.Framework.Caching; using osu.Framework.MathUtils; using osu.Game.Rulesets.Objects.Types; using osuTK; namespace osu.Game.Rulesets.Objects { - public struct SliderPath : IEquatable + public class SliderPath { /// /// The user-set distance of the path. If non-null, will match this value, @@ -20,49 +23,47 @@ namespace osu.Game.Rulesets.Objects public readonly double? ExpectedDistance; /// - /// The type of path. + /// The control points of the path. /// - public readonly PathType Type; + public readonly BindableList ControlPoints = new BindableList(); - [JsonProperty] - private Vector2[] controlPoints; + public readonly List Test = new List(); - private List calculatedPath; - private List cumulativeLength; + private readonly Cached pathCache = new Cached(); - private bool isInitialised; + private readonly List calculatedPath = new List(); + private readonly List cumulativeLength = new List(); /// /// Creates a new . /// - /// The type of path. - /// The control points of the path. - /// A user-set distance of the path that may be shorter or longer than the true distance between all - /// . The path will be shortened/lengthened to match this length. - /// If null, the path will use the true distance between all . + /// An optional set of s to initialise the path with. + /// 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. [JsonConstructor] - public SliderPath(PathType type, Vector2[] controlPoints, double? expectedDistance = null) + public SliderPath(PathControlPoint[] controlPoints = null, double? expectedDistance = null) { - this = default; - this.controlPoints = controlPoints; - - Type = type; ExpectedDistance = expectedDistance; - ensureInitialised(); - } - - /// - /// The control points of the path. - /// - [JsonIgnore] - public ReadOnlySpan ControlPoints - { - get + ControlPoints.ItemsAdded += items => { - ensureInitialised(); - return controlPoints.AsSpan(); - } + foreach (var c in items) + c.Changed += onControlPointChanged; + + onControlPointChanged(); + }; + + ControlPoints.ItemsRemoved += items => + { + foreach (var c in items) + c.Changed -= onControlPointChanged; + + onControlPointChanged(); + }; + + ControlPoints.AddRange(controlPoints); + + void onControlPointChanged() => pathCache.Invalidate(); } /// @@ -73,7 +74,7 @@ namespace osu.Game.Rulesets.Objects { get { - ensureInitialised(); + ensureValid(); return cumulativeLength.Count == 0 ? 0 : cumulativeLength[cumulativeLength.Count - 1]; } } @@ -87,7 +88,7 @@ namespace osu.Game.Rulesets.Objects /// End progress. Ranges from 0 (beginning of the slider) to 1 (end of the slider). public void GetPathToProgress(List path, double p0, double p1) { - ensureInitialised(); + ensureValid(); double d0 = progressToDistance(p0); double d1 = progressToDistance(p1); @@ -116,82 +117,84 @@ namespace osu.Game.Rulesets.Objects /// public Vector2 PositionAt(double progress) { - ensureInitialised(); + ensureValid(); double d = progressToDistance(progress); return interpolateVertices(indexOfDistance(d), d); } - private void ensureInitialised() + private void ensureValid() { - if (isInitialised) + if (pathCache.IsValid) return; - isInitialised = true; - - controlPoints ??= Array.Empty(); - calculatedPath = new List(); - cumulativeLength = new List(); - calculatePath(); calculateCumulativeLength(); - } - private List calculateSubpath(ReadOnlySpan subControlPoints) - { - switch (Type) - { - 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 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); + pathCache.Validate(); } 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. + if (ControlPoints.Count == 0) + return; + + if (ControlPoints[0].Type.Value == null) + throw new InvalidOperationException($"The first control point in a {nameof(SliderPath)} must have a non-null type."); + + Vector2[] vertices = new Vector2[ControlPoints.Count]; + for (int i = 0; i < ControlPoints.Count; i++) + vertices[i] = ControlPoints[i].Position.Value; int start = 0; - int end = 0; - for (int i = 0; i < ControlPoints.Length; ++i) + for (int i = 0; i < ControlPoints.Count; i++) { - end++; + if (ControlPoints[i].Type.Value == null && i < ControlPoints.Count - 1) + continue; - if (i == ControlPoints.Length - 1 || ControlPoints[i] == ControlPoints[i + 1]) + Debug.Assert(ControlPoints[start].Type.Value.HasValue); + + // The current vertex ends the segment + var segmentVertices = vertices.AsSpan().Slice(start, i - start + 1); + var segmentType = ControlPoints[start].Type.Value.Value; + + foreach (Vector2 t in computeSubPath(segmentVertices, segmentType)) { - ReadOnlySpan cpSpan = ControlPoints.Slice(start, end - start); - - foreach (Vector2 t in calculateSubpath(cpSpan)) - { - if (calculatedPath.Count == 0 || calculatedPath.Last() != t) - calculatedPath.Add(t); - } - - start = end; + if (calculatedPath.Count == 0 || calculatedPath.Last() != t) + calculatedPath.Add(t); } + + // Start the new segment at the current vertex + start = i; + } + + static List computeSubPath(ReadOnlySpan subControlPoints, PathType type) + { + switch (type) + { + case PathType.Linear: + return PathApproximator.ApproximateLinear(subControlPoints); + + case PathType.PerfectCurve: + if (subControlPoints.Length != 3) + break; + + List 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); } } @@ -272,7 +275,5 @@ namespace osu.Game.Rulesets.Objects double w = (d - d0) / (d1 - d0); return p0 + (p1 - p0) * (float)w; } - - public bool Equals(SliderPath other) => ControlPoints.SequenceEqual(other.ControlPoints) && ExpectedDistance == other.ExpectedDistance && Type == other.Type; } } diff --git a/osu.Game/Rulesets/Objects/SliderPath2.cs b/osu.Game/Rulesets/Objects/SliderPath2.cs deleted file mode 100644 index 560313a4d5..0000000000 --- a/osu.Game/Rulesets/Objects/SliderPath2.cs +++ /dev/null @@ -1,274 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using Newtonsoft.Json; -using osu.Framework.Bindables; -using osu.Framework.Caching; -using osu.Framework.MathUtils; -using osu.Game.Rulesets.Objects.Types; -using osuTK; - -namespace osu.Game.Rulesets.Objects -{ - public class SliderPath2 - { - /// - /// The user-set distance of the path. If non-null, will match this value, - /// and the path will be shortened/lengthened to match this length. - /// - public readonly double? ExpectedDistance; - - /// - /// The control points of the path. - /// - public readonly BindableList ControlPoints = new BindableList(); - - private readonly Cached pathCache = new Cached(); - - private readonly List calculatedPath = new List(); - private readonly List cumulativeLength = new List(); - - /// - /// Creates a new . - /// - /// 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. - [JsonConstructor] - public SliderPath2(double? expectedDistance = null) - { - ExpectedDistance = expectedDistance; - - ControlPoints.ItemsAdded += items => - { - foreach (var c in items) - c.Changed += onControlPointChanged; - - onControlPointChanged(); - }; - - ControlPoints.ItemsRemoved += items => - { - foreach (var c in items) - c.Changed -= onControlPointChanged; - - onControlPointChanged(); - }; - - void onControlPointChanged() => pathCache.Invalidate(); - } - - /// - /// The distance of the path after lengthening/shortening to account for . - /// - [JsonIgnore] - public double Distance - { - get - { - ensureValid(); - return cumulativeLength.Count == 0 ? 0 : cumulativeLength[cumulativeLength.Count - 1]; - } - } - - /// - /// 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. - /// - /// The list to be filled with the computed path. - /// 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) - { - ensureValid(); - - 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)); - - for (; i < calculatedPath.Count && cumulativeLength[i] <= d1; ++i) - path.Add(calculatedPath[i]); - - path.Add(interpolateVertices(i, d1)); - } - - /// - /// Computes the position on the slider at a given progress that ranges from 0 (beginning of the path) - /// to 1 (end of the path). - /// - /// Ranges from 0 (beginning of the path) to 1 (end of the path). - /// - public Vector2 PositionAt(double progress) - { - ensureValid(); - - double d = progressToDistance(progress); - return interpolateVertices(indexOfDistance(d), d); - } - - private void ensureValid() - { - if (pathCache.IsValid) - return; - - calculatePath(); - calculateCumulativeLength(); - - pathCache.Validate(); - } - - private void calculatePath() - { - calculatedPath.Clear(); - - if (ControlPoints.Count == 0) - return; - - if (ControlPoints[0].Type.Value == null) - throw new InvalidOperationException($"The first control point in a {nameof(SliderPath2)} must have a non-null type."); - - Vector2[] vertices = new Vector2[ControlPoints.Count]; - for (int i = 0; i < ControlPoints.Count; i++) - vertices[i] = ControlPoints[i].Position.Value; - - int start = 0; - - for (int i = 0; i < ControlPoints.Count; i++) - { - if (ControlPoints[i].Type.Value == null && i < ControlPoints.Count - 1) - continue; - - Debug.Assert(ControlPoints[start].Type.Value.HasValue); - - // The current vertex ends the segment - var segmentVertices = vertices.AsSpan().Slice(start, i - start + 1); - var segmentType = ControlPoints[start].Type.Value.Value; - - foreach (Vector2 t in computeSubPath(segmentVertices, segmentType)) - { - if (calculatedPath.Count == 0 || calculatedPath.Last() != t) - calculatedPath.Add(t); - } - - // Start the new segment at the current vertex - start = i; - } - - static List computeSubPath(ReadOnlySpan subControlPoints, PathType type) - { - switch (type) - { - case PathType.Linear: - return PathApproximator.ApproximateLinear(subControlPoints); - - case PathType.PerfectCurve: - if (subControlPoints.Length != 3) - break; - - List 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 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; - - // Shorted slider paths that are too long compared to the expected distance - if (ExpectedDistance.HasValue && ExpectedDistance - l < d) - { - calculatedPath[i + 1] = calculatedPath[i] + diff * (float)((ExpectedDistance - l) / d); - calculatedPath.RemoveRange(i + 2, calculatedPath.Count - 2 - i); - - l = ExpectedDistance.Value; - cumulativeLength.Add(l); - break; - } - - l += d; - cumulativeLength.Add(l); - } - - // Lengthen slider paths that are too short compared to the expected distance - if (ExpectedDistance.HasValue && l < ExpectedDistance && 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)((ExpectedDistance - l) / d); - cumulativeLength[calculatedPath.Count - 1] = ExpectedDistance.Value; - } - } - - 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; - } - - private Vector2 interpolateVertices(int i, double d) - { - if (calculatedPath.Count == 0) - return Vector2.Zero; - - if (i <= 0) - return calculatedPath.First(); - 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; - } - } -}