Reworked linear line check, and optimized scaled flat slider test

This commit is contained in:
Aurelian 2024-05-24 14:05:56 +02:00
parent fff52be59a
commit b2c4e0e951
2 changed files with 53 additions and 137 deletions

View File

@ -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<SelectionBoxDragHandle>().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;
}
}
}
}

View File

@ -269,38 +269,6 @@ namespace osu.Game.Rulesets.Objects
pathCache.Validate();
}
/// <summary>
/// Checks if the array of vectors is almost straight.
/// </summary>
/// <para>
/// 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.
/// </para>
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<Vector2> 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.