diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs index 99694f82d1..ef3824b5b0 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs @@ -6,6 +6,7 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Testing; +using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; @@ -68,108 +69,52 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddAssert("slider length shrunk", () => slider.Path.Distance < distanceBefore); } - [Test] - [Timeout(4000)] //Catches crashes in other threads, but not ideal. Hopefully there is a improvement to this. - public void TestScalingSliderFlat( - [Values(0, 1, 2, 3)] int typeInt - ) - { - Slider slider = null!; - - switch (typeInt) - { - case 0: - AddStep("Add linear slider", () => - { - slider = new Slider { StartTime = EditorClock.CurrentTime, Position = new Vector2(300) }; - - PathControlPoint[] points = - { - new PathControlPoint(new Vector2(0), PathType.LINEAR), - new PathControlPoint(new Vector2(50, 100)), - }; - - slider.Path = new SliderPath(points); - EditorBeatmap.Add(slider); - }); - break; - - case 1: - AddStep("Add perfect curve slider", () => - { - slider = new Slider { StartTime = EditorClock.CurrentTime, Position = new Vector2(300) }; - - PathControlPoint[] points = - { - new PathControlPoint(new Vector2(0), PathType.PERFECT_CURVE), - new PathControlPoint(new Vector2(50, 25)), - new PathControlPoint(new Vector2(25, 100)), - }; - - slider.Path = new SliderPath(points); - EditorBeatmap.Add(slider); - }); - break; - - case 3: - AddStep("Add catmull slider", () => - { - slider = new Slider { StartTime = EditorClock.CurrentTime, Position = new Vector2(300) }; - - PathControlPoint[] points = - { - new PathControlPoint(new Vector2(0), PathType.CATMULL), - new PathControlPoint(new Vector2(50, 25)), - new PathControlPoint(new Vector2(25, 80)), - new PathControlPoint(new Vector2(40, 100)), - }; - - slider.Path = new SliderPath(points); - EditorBeatmap.Add(slider); - }); - break; - - default: - AddStep("Add bezier slider", () => - { - slider = new Slider { StartTime = EditorClock.CurrentTime, Position = new Vector2(300) }; - - PathControlPoint[] points = - { - new PathControlPoint(new Vector2(0), PathType.BEZIER), - new PathControlPoint(new Vector2(50, 25)), - new PathControlPoint(new Vector2(25, 80)), - new PathControlPoint(new Vector2(40, 100)), - }; - - slider.Path = new SliderPath(points); - EditorBeatmap.Add(slider); - }); - break; - } - - AddAssert("ensure object placed", () => EditorBeatmap.HitObjects.Count == 1); - - moveMouse(new Vector2(300)); - AddStep("select slider", () => InputManager.Click(MouseButton.Left)); - AddStep("slider is valid", () => slider.Path.GetSegmentEnds()); //To run ensureValid(); - - SelectionBoxDragHandle dragHandle = null!; - AddStep("store drag handle", () => dragHandle = Editor.ChildrenOfType().Skip(1).First()); - AddAssert("is dragHandle not null", () => dragHandle != null); - - AddStep("move mouse to handle", () => InputManager.MoveMouseTo(dragHandle)); - AddStep("begin drag", () => InputManager.PressButton(MouseButton.Left)); - moveMouse(new Vector2(0, 300)); - AddStep("end drag", () => InputManager.ReleaseButton(MouseButton.Left)); - - AddStep("move mouse to handle", () => InputManager.MoveMouseTo(dragHandle)); - AddStep("begin drag", () => InputManager.PressButton(MouseButton.Left)); - moveMouse(new Vector2(0, 300)); //Should crash here if broken, although doesn't count as failed... - AddStep("end drag", () => InputManager.ReleaseButton(MouseButton.Left)); - } - private void moveMouse(Vector2 pos) => AddStep($"move mouse to {pos}", () => InputManager.MoveMouseTo(playfield.ToScreenSpace(pos))); } + [TestFixture] + public class TestSliderNearLinearScaling + { + [Test] + public void TestScalingSliderFlat() + { + Slider sliderPerfect = new Slider + { + Position = new Vector2(300), + Path = new SliderPath( + [ + new PathControlPoint(new Vector2(0), PathType.PERFECT_CURVE), + new PathControlPoint(new Vector2(50, 25)), + new PathControlPoint(new Vector2(25, 100)), + ]) + }; + + Slider sliderBezier = new Slider + { + Position = new Vector2(300), + Path = new SliderPath( + [ + new PathControlPoint(new Vector2(0), PathType.BEZIER), + new PathControlPoint(new Vector2(50, 25)), + new PathControlPoint(new Vector2(25, 100)), + ]) + }; + + scaleSlider(sliderPerfect, new Vector2(0.000001f, 1)); + scaleSlider(sliderBezier, new Vector2(0.000001f, 1)); + + for (int i = 0; i < 100; i++) + { + Assert.True(Precision.AlmostEquals(sliderPerfect.Path.PositionAt(i / 100.0f), sliderBezier.Path.PositionAt(i / 100.0f))); + } + } + + private void scaleSlider(Slider slider, Vector2 scale) + { + for (int i = 0; i < slider.Path.ControlPoints.Count; i++) + { + slider.Path.ControlPoints[i].Position *= scale; + } + } + } } diff --git a/osu.Game/Rulesets/Objects/SliderPath.cs b/osu.Game/Rulesets/Objects/SliderPath.cs index ddea23034c..eca14269fe 100644 --- a/osu.Game/Rulesets/Objects/SliderPath.cs +++ b/osu.Game/Rulesets/Objects/SliderPath.cs @@ -269,38 +269,6 @@ namespace osu.Game.Rulesets.Objects pathCache.Validate(); } - /// - /// Checks if the array of vectors is almost straight. - /// - /// - /// The angle is first obtained based on the farthest vector from the first, - /// then we find the angle of each vector from the first, - /// and calculate the distance between the two angle vectors. - /// We than scale this distance to the distance from the first vector - /// (or by 10 if the distance is smaller), - /// and if it is greater than acceptableDifference, we return false. - /// - private static bool isAlmostStraight(Vector2[] vectors, float acceptableDifference = 0.1f) - { - if (vectors.Length <= 2 || vectors.All(x => x == vectors.First())) return true; - - Vector2 first = vectors.First(); - Vector2 farthest = vectors.MaxBy(x => Vector2.Distance(first, x)); - - Vector2 angle = Vector2.Normalize(farthest - first); - - foreach (Vector2 vector in vectors) - { - if (vector == first) - continue; - - if (Math.Max(10.0f, Vector2.Distance(vector, first)) * Vector2.Distance(Vector2.Normalize(vector - first), angle) > acceptableDifference) - return false; - } - - return true; - } - private void calculatePath() { calculatedPath.Clear(); @@ -325,10 +293,6 @@ namespace osu.Game.Rulesets.Objects var segmentVertices = vertices.AsSpan().Slice(start, i - start + 1); var segmentType = ControlPoints[start].Type ?? PathType.LINEAR; - //If a segment is almost straight, treat it as linear. - if (segmentType != PathType.LINEAR && isAlmostStraight(segmentVertices.ToArray())) - segmentType = PathType.LINEAR; - // No need to calculate path when there is only 1 vertex if (segmentVertices.Length == 1) calculatedPath.Add(segmentVertices[0]); @@ -366,6 +330,13 @@ namespace osu.Game.Rulesets.Objects if (subControlPoints.Length != 3) break; + //If a curve's theta range almost equals zero, the radius needed to have more than a + //floating point error difference is very large and results in a nearly straight path. + //Calculate it via a bezier aproximation instead. + //0.0005 corresponds with a radius of 8000 to have a more than 0.001 shift in the X value + if (Math.Abs(new CircularArcProperties(subControlPoints).ThetaRange) <= 0.0005d) + break; + List subPath = PathApproximator.CircularArcToPiecewiseLinear(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.