diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj
index 936808f38b..52b728a115 100644
--- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj
@@ -11,7 +11,7 @@
-
+
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
index 35e7742172..95b96adab0 100644
--- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
@@ -11,7 +11,7 @@
-
+
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj
index c1044965b5..d12403016d 100644
--- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj
@@ -11,7 +11,7 @@
-
+
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
index 35e7742172..95b96adab0 100644
--- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
@@ -11,7 +11,7 @@
-
+
diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj
index 3f926ed45a..1f4544098b 100644
--- a/osu.Desktop/osu.Desktop.csproj
+++ b/osu.Desktop/osu.Desktop.csproj
@@ -27,7 +27,7 @@
-
+
diff --git a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
index d62d422f33..f47b069373 100644
--- a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
+++ b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
@@ -9,7 +9,7 @@
-
+
diff --git a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
index c9db824615..5a2e8e0bf0 100644
--- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
+++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
@@ -3,7 +3,7 @@
-
+
WinExe
diff --git a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
index 0d7b03d830..be51dc0e4c 100644
--- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
+++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
@@ -3,7 +3,7 @@
-
+
WinExe
diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
index 1eb1c85d93..c10c3ffb15 100644
--- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
+++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
@@ -4,7 +4,7 @@
-
+
WinExe
diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerDisc.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerDisc.cs
index d88752f025..f99d4275bd 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerDisc.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerDisc.cs
@@ -15,7 +15,6 @@ using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.Skinning.Default;
using osuTK;
-using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Skinning.Argon
{
@@ -51,6 +50,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
private Container centre = null!;
private CircularContainer fill = null!;
+ private Container ticksContainer = null!;
+ private ArgonSpinnerTicks ticks = null!;
+
[BackgroundDependencyLoader]
private void load(DrawableHitObject drawableHitObject)
{
@@ -69,41 +71,85 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
- fill = new CircularContainer
+ new Container
{
- Name = @"Fill",
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
- Masking = true,
- EdgeEffect = new EdgeEffectParameters
+ Padding = new MarginPadding(8f),
+ Children = new[]
{
- Type = EdgeEffectType.Shadow,
- Colour = Colour4.FromHex("FC618F").Opacity(1f),
- Radius = 40,
+ fill = new CircularContainer
+ {
+ Name = @"Fill",
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Masking = true,
+ EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Shadow,
+ Colour = Colour4.FromHex("FC618F").Opacity(1f),
+ Radius = 40,
+ },
+ Child = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 0f,
+ AlwaysPresent = true,
+ }
+ },
+ ticksContainer = new Container
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Child = ticks = new ArgonSpinnerTicks(),
+ }
},
- Child = new Box
- {
- RelativeSizeAxes = Axes.Both,
- Alpha = 0f,
- AlwaysPresent = true,
- }
},
- new CircularContainer
+ new Container
{
Name = @"Ring",
Masking = true,
- BorderColour = Color4.White,
- BorderThickness = 5,
RelativeSizeAxes = Axes.Both,
- Child = new Box
+ Padding = new MarginPadding(8f),
+ Children = new[]
{
- RelativeSizeAxes = Axes.Both,
- Alpha = 0,
- AlwaysPresent = true,
+ new ArgonSpinnerRingArc
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Name = "Top Arc",
+ },
+ new ArgonSpinnerRingArc
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Name = "Bottom Arc",
+ Scale = new Vector2(1, -1),
+ },
+ }
+ },
+ new Container
+ {
+ Name = @"Sides",
+ RelativeSizeAxes = Axes.Both,
+ Children = new[]
+ {
+ new ArgonSpinnerProgressArc
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Name = "Left Bar"
+ },
+ new ArgonSpinnerProgressArc
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Name = "Right Bar",
+ Scale = new Vector2(-1, 1),
+ },
}
},
- new ArgonSpinnerTicks(),
}
},
centre = new Container
@@ -169,7 +215,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
float targetScale = initial_fill_scale + (0.98f - initial_fill_scale) * drawableSpinner.Progress;
fill.Scale = new Vector2((float)Interpolation.Lerp(fill.Scale.X, targetScale, Math.Clamp(Math.Abs(Time.Elapsed) / 100, 0, 1)));
- disc.Rotation = drawableSpinner.RotationTracker.Rotation;
+ ticks.Rotation = drawableSpinner.RotationTracker.Rotation;
}
private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
@@ -182,35 +228,16 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt))
{
this.ScaleTo(initial_scale);
- this.RotateTo(0);
+ ticksContainer.RotateTo(0);
+ centre.ScaleTo(0);
+ disc.ScaleTo(0);
using (BeginDelayedSequence(spinner.TimePreempt / 2))
{
// constant ambient rotation to give the spinner "spinning" character.
- this.RotateTo((float)(25 * spinner.Duration / 2000), spinner.TimePreempt + spinner.Duration);
+ ticksContainer.RotateTo((float)(25 * spinner.Duration / 2000), spinner.TimePreempt + spinner.Duration);
}
- using (BeginDelayedSequence(spinner.TimePreempt + spinner.Duration + drawableHitObject.Result.TimeOffset))
- {
- switch (state)
- {
- case ArmedState.Hit:
- this.ScaleTo(initial_scale * 1.2f, 320, Easing.Out);
- this.RotateTo(Rotation + 180, 320);
- break;
-
- case ArmedState.Miss:
- this.ScaleTo(initial_scale * 0.8f, 320, Easing.In);
- break;
- }
- }
- }
-
- using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt))
- {
- centre.ScaleTo(0);
- disc.ScaleTo(0);
-
using (BeginDelayedSequence(spinner.TimePreempt / 2))
{
centre.ScaleTo(0.3f, spinner.TimePreempt / 4, Easing.OutQuint);
@@ -222,6 +249,21 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
disc.ScaleTo(1, spinner.TimePreempt / 2, Easing.OutQuint);
}
}
+
+ using (BeginDelayedSequence(spinner.TimePreempt + spinner.Duration + drawableHitObject.Result.TimeOffset))
+ {
+ switch (state)
+ {
+ case ArmedState.Hit:
+ disc.ScaleTo(initial_scale * 1.2f, 320, Easing.Out);
+ ticksContainer.RotateTo(ticksContainer.Rotation + 180, 320);
+ break;
+
+ case ArmedState.Miss:
+ disc.ScaleTo(initial_scale * 0.8f, 320, Easing.In);
+ break;
+ }
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerProgressArc.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerProgressArc.cs
new file mode 100644
index 0000000000..e998f55755
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerProgressArc.cs
@@ -0,0 +1,71 @@
+// 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 osu.Framework.Allocation;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.UserInterface;
+using osu.Framework.Utils;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Osu.Skinning.Argon
+{
+ public class ArgonSpinnerProgressArc : CompositeDrawable
+ {
+ private const float arc_fill = 0.15f;
+ private const float arc_radius = 0.12f;
+
+ private CircularProgress fill = null!;
+
+ private DrawableSpinner spinner = null!;
+
+ private CircularProgress background = null!;
+
+ [BackgroundDependencyLoader]
+ private void load(DrawableHitObject drawableHitObject)
+ {
+ RelativeSizeAxes = Axes.Both;
+
+ spinner = (DrawableSpinner)drawableHitObject;
+
+ InternalChildren = new Drawable[]
+ {
+ background = new CircularProgress
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Colour = Color4.White.Opacity(0.25f),
+ RelativeSizeAxes = Axes.Both,
+ Current = { Value = arc_fill },
+ Rotation = 90 - arc_fill * 180,
+ InnerRadius = arc_radius,
+ RoundedCaps = true,
+ },
+ fill = new CircularProgress
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ InnerRadius = arc_radius,
+ RoundedCaps = true,
+ }
+ };
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ background.Alpha = spinner.Progress >= 1 ? 0 : 1;
+
+ fill.Alpha = (float)Interpolation.DampContinuously(fill.Alpha, spinner.Progress > 0 && spinner.Progress < 1 ? 1 : 0, 40f, (float)Math.Abs(Time.Elapsed));
+ fill.Current.Value = (float)Interpolation.DampContinuously(fill.Current.Value, spinner.Progress >= 1 ? 0 : arc_fill * spinner.Progress, 40f, (float)Math.Abs(Time.Elapsed));
+
+ fill.Rotation = (float)(90 - fill.Current.Value * 180);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerRingArc.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerRingArc.cs
new file mode 100644
index 0000000000..57fb57a09e
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerRingArc.cs
@@ -0,0 +1,53 @@
+// 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 osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.UserInterface;
+using osu.Framework.Utils;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+
+namespace osu.Game.Rulesets.Osu.Skinning.Argon
+{
+ public class ArgonSpinnerRingArc : CompositeDrawable
+ {
+ private const float arc_fill = 0.31f;
+ private const float arc_fill_complete = 0.50f;
+
+ private const float arc_radius = 0.02f;
+
+ private DrawableSpinner spinner = null!;
+ private CircularProgress fill = null!;
+
+ [BackgroundDependencyLoader]
+ private void load(DrawableHitObject drawableHitObject)
+ {
+ RelativeSizeAxes = Axes.Both;
+
+ spinner = (DrawableSpinner)drawableHitObject;
+ InternalChild = fill = new CircularProgress
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Current = { Value = arc_fill },
+ Rotation = -arc_fill * 180,
+ InnerRadius = arc_radius,
+ RoundedCaps = true,
+ };
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ fill.Current.Value = (float)Interpolation.DampContinuously(fill.Current.Value, spinner.Progress >= 1 ? arc_fill_complete : arc_fill, 40f, (float)Math.Abs(Time.Elapsed));
+ fill.InnerRadius = (float)Interpolation.DampContinuously(fill.InnerRadius, spinner.Progress >= 1 ? arc_radius * 2.2f : arc_radius, 40f, (float)Math.Abs(Time.Elapsed));
+
+ fill.Rotation = (float)(-fill.Current.Value * 180);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
index 38e61f5624..6af1beff69 100644
--- a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
+++ b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
@@ -3,7 +3,7 @@
-
+
WinExe
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBezierConverter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBezierConverter.cs
new file mode 100644
index 0000000000..28a9d17882
--- /dev/null
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBezierConverter.cs
@@ -0,0 +1,190 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Lines;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
+using osuTK;
+
+namespace osu.Game.Tests.Visual.Gameplay
+{
+ public class TestSceneBezierConverter : OsuTestScene
+ {
+ private readonly SmoothPath drawablePath;
+ private readonly SmoothPath controlPointDrawablePath;
+ private readonly SmoothPath convertedDrawablePath;
+ private readonly SmoothPath convertedControlPointDrawablePath;
+
+ private SliderPath path = null!;
+ private SliderPath convertedPath = null!;
+
+ public TestSceneBezierConverter()
+ {
+ Children = new Drawable[]
+ {
+ new Container
+ {
+ Children =
+ new Drawable[]
+ {
+ drawablePath = new SmoothPath(),
+ controlPointDrawablePath = new SmoothPath
+ {
+ Colour = Colour4.Magenta,
+ PathRadius = 1f
+ }
+ },
+ Position = new Vector2(100)
+ },
+ new Container
+ {
+ Children =
+ new Drawable[]
+ {
+ convertedDrawablePath = new SmoothPath(),
+ convertedControlPointDrawablePath = new SmoothPath
+ {
+ Colour = Colour4.Magenta,
+ PathRadius = 1f
+ }
+ },
+ Position = new Vector2(100, 300)
+ }
+ };
+
+ resetPath();
+ }
+
+ [SetUp]
+ public void Setup() => Schedule(resetPath);
+
+ private void resetPath()
+ {
+ path = new SliderPath();
+ convertedPath = new SliderPath();
+
+ path.Version.ValueChanged += getConvertedControlPoints;
+ }
+
+ private void getConvertedControlPoints(ValueChangedEvent obj)
+ {
+ convertedPath.ControlPoints.Clear();
+ convertedPath.ControlPoints.AddRange(BezierConverter.ConvertToModernBezier(path.ControlPoints));
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ List vertices = new List();
+
+ path.GetPathToProgress(vertices, 0, 1);
+
+ drawablePath.Vertices = vertices;
+ controlPointDrawablePath.Vertices = path.ControlPoints.Select(o => o.Position).ToList();
+
+ if (controlPointDrawablePath.Vertices.Count > 0)
+ {
+ controlPointDrawablePath.Position =
+ drawablePath.PositionInBoundingBox(drawablePath.Vertices[0]) - controlPointDrawablePath.PositionInBoundingBox(controlPointDrawablePath.Vertices[0]);
+ }
+
+ vertices.Clear();
+
+ convertedPath.GetPathToProgress(vertices, 0, 1);
+
+ convertedDrawablePath.Vertices = vertices;
+ convertedControlPointDrawablePath.Vertices = convertedPath.ControlPoints.Select(o => o.Position).ToList();
+
+ if (convertedControlPointDrawablePath.Vertices.Count > 0)
+ {
+ convertedControlPointDrawablePath.Position = convertedDrawablePath.PositionInBoundingBox(convertedDrawablePath.Vertices[0])
+ - convertedControlPointDrawablePath.PositionInBoundingBox(convertedControlPointDrawablePath.Vertices[0]);
+ }
+ }
+
+ [Test]
+ public void TestEmptyPath()
+ {
+ }
+
+ [TestCase(PathType.Linear)]
+ [TestCase(PathType.Bezier)]
+ [TestCase(PathType.Catmull)]
+ [TestCase(PathType.PerfectCurve)]
+ public void TestSingleSegment(PathType type)
+ => AddStep("create path", () => path.ControlPoints.AddRange(createSegment(type, Vector2.Zero, new Vector2(0, 100), new Vector2(100))));
+
+ [TestCase(PathType.Linear)]
+ [TestCase(PathType.Bezier)]
+ [TestCase(PathType.Catmull)]
+ [TestCase(PathType.PerfectCurve)]
+ public void TestMultipleSegment(PathType type)
+ {
+ AddStep("create path", () =>
+ {
+ path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero));
+ path.ControlPoints.AddRange(createSegment(type, new Vector2(0, 100), new Vector2(100), Vector2.Zero));
+ });
+ }
+
+ [Test]
+ public void TestComplex()
+ {
+ AddStep("create path", () =>
+ {
+ path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(100, 0)));
+ path.ControlPoints.AddRange(createSegment(PathType.Bezier, new Vector2(100, 0), new Vector2(150, 30), new Vector2(100, 100)));
+ path.ControlPoints.AddRange(createSegment(PathType.PerfectCurve, new Vector2(100, 100), new Vector2(25, 50), Vector2.Zero));
+ });
+ }
+
+ [TestCase(0, 100)]
+ [TestCase(1, 100)]
+ [TestCase(5, 100)]
+ [TestCase(10, 100)]
+ [TestCase(30, 100)]
+ [TestCase(50, 100)]
+ [TestCase(100, 100)]
+ [TestCase(100, 1)]
+ public void TestPerfectCurveAngles(float height, float width)
+ {
+ AddStep("create path", () =>
+ {
+ path.ControlPoints.AddRange(createSegment(PathType.PerfectCurve, Vector2.Zero, new Vector2(width / 2, height), new Vector2(width, 0)));
+ });
+ }
+
+ [TestCase(2)]
+ [TestCase(4)]
+ public void TestPerfectCurveFallbackScenarios(int points)
+ {
+ AddStep("create path", () =>
+ {
+ switch (points)
+ {
+ case 2:
+ path.ControlPoints.AddRange(createSegment(PathType.PerfectCurve, Vector2.Zero, new Vector2(0, 100)));
+ break;
+
+ case 4:
+ path.ControlPoints.AddRange(createSegment(PathType.PerfectCurve, Vector2.Zero, new Vector2(0, 100), new Vector2(100), new Vector2(100, 0)));
+ break;
+ }
+ });
+ }
+
+ private List createSegment(PathType type, params Vector2[] controlPoints)
+ {
+ var points = controlPoints.Select(p => new PathControlPoint { Position = p }).ToList();
+ points[0].Type = type;
+ return points;
+ }
+ }
+}
diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj
index bdf8cc5136..24969414d0 100644
--- a/osu.Game.Tests/osu.Game.Tests.csproj
+++ b/osu.Game.Tests/osu.Game.Tests.csproj
@@ -1,11 +1,11 @@
-
+
-
+
diff --git a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
index bdef46a6b2..9f2a088a4b 100644
--- a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
+++ b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
@@ -6,7 +6,7 @@
-
+
WinExe
diff --git a/osu.Game/Rulesets/Objects/BezierConverter.cs b/osu.Game/Rulesets/Objects/BezierConverter.cs
new file mode 100644
index 0000000000..ebee36a7db
--- /dev/null
+++ b/osu.Game/Rulesets/Objects/BezierConverter.cs
@@ -0,0 +1,287 @@
+// 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.Linq;
+using osu.Framework.Utils;
+using osu.Game.Rulesets.Objects.Types;
+using osuTK;
+
+namespace osu.Game.Rulesets.Objects
+{
+ public static class BezierConverter
+ {
+ private struct CircleBezierPreset
+ {
+ public readonly double ArcLength;
+ public readonly Vector2d[] ControlPoints;
+
+ public CircleBezierPreset(double arcLength, Vector2d[] controlPoints)
+ {
+ ArcLength = arcLength;
+ ControlPoints = controlPoints;
+ }
+ }
+
+ // Extremely accurate a bezier anchor positions for approximating circles of several arc lengths
+ private static readonly CircleBezierPreset[] circle_presets =
+ {
+ new CircleBezierPreset(0.4993379862754501,
+ new[] { new Vector2d(1, 0), new Vector2d(1, 0.2549893626632736f), new Vector2d(0.8778997558480327f, 0.47884446188920726f) }),
+ new CircleBezierPreset(1.7579419829169447,
+ new[] { new Vector2d(1, 0), new Vector2d(1, 0.6263026f), new Vector2d(0.42931178f, 1.0990661f), new Vector2d(-0.18605515f, 0.9825393f) }),
+ new CircleBezierPreset(3.1385246920140215,
+ new[] { new Vector2d(1, 0), new Vector2d(1, 0.87084764f), new Vector2d(0.002304826f, 1.5033062f), new Vector2d(-0.9973236f, 0.8739115f), new Vector2d(-0.9999953f, 0.0030679568f) }),
+ new CircleBezierPreset(5.69720464620727,
+ new[] { new Vector2d(1, 0), new Vector2d(1, 1.4137783f), new Vector2d(-1.4305235f, 2.0779421f), new Vector2d(-2.3410065f, -0.94017583f), new Vector2d(0.05132711f, -1.7309346f), new Vector2d(0.8331702f, -0.5530167f) }),
+ new CircleBezierPreset(2 * Math.PI,
+ new[] { new Vector2d(1, 0), new Vector2d(1, 1.2447058f), new Vector2d(-0.8526471f, 2.118367f), new Vector2d(-2.6211002f, 7.854936e-06f), new Vector2d(-0.8526448f, -2.118357f), new Vector2d(1, -1.2447058f), new Vector2d(1, 0) })
+ };
+
+ ///
+ /// Converts a slider path to bezier control point positions compatible with the legacy osu! client.
+ ///
+ /// The control points of the path.
+ /// The offset for the whole path.
+ /// The list of legacy bezier control point positions.
+ public static List ConvertToLegacyBezier(IList controlPoints, Vector2 position)
+ {
+ Vector2[] vertices = new Vector2[controlPoints.Count];
+ for (int i = 0; i < controlPoints.Count; i++)
+ vertices[i] = controlPoints[i].Position;
+
+ var result = new List();
+ int start = 0;
+
+ for (int i = 0; i < controlPoints.Count; i++)
+ {
+ if (controlPoints[i].Type == null && i < controlPoints.Count - 1)
+ continue;
+
+ // The current vertex ends the segment
+ var segmentVertices = vertices.AsSpan().Slice(start, i - start + 1);
+ var segmentType = controlPoints[start].Type ?? PathType.Linear;
+
+ switch (segmentType)
+ {
+ case PathType.Catmull:
+ result.AddRange(from segment in ConvertCatmullToBezierAnchors(segmentVertices) from v in segment select v + position);
+
+ break;
+
+ case PathType.Linear:
+ result.AddRange(from segment in ConvertLinearToBezierAnchors(segmentVertices) from v in segment select v + position);
+
+ break;
+
+ case PathType.PerfectCurve:
+ result.AddRange(ConvertCircleToBezierAnchors(segmentVertices).Select(v => v + position));
+
+ break;
+
+ default:
+ foreach (Vector2 v in segmentVertices)
+ {
+ result.Add(v + position);
+ }
+
+ break;
+ }
+
+ // Start the new segment at the current vertex
+ start = i;
+ }
+
+ return result;
+ }
+
+ ///
+ /// Converts a path of control points to an identical path using only Bezier type control points.
+ ///
+ /// The control points of the path.
+ /// The list of bezier control points.
+ public static List ConvertToModernBezier(IList controlPoints)
+ {
+ Vector2[] vertices = new Vector2[controlPoints.Count];
+ for (int i = 0; i < controlPoints.Count; i++)
+ vertices[i] = controlPoints[i].Position;
+
+ var result = new List();
+ int start = 0;
+
+ for (int i = 0; i < controlPoints.Count; i++)
+ {
+ if (controlPoints[i].Type == null && i < controlPoints.Count - 1)
+ continue;
+
+ // The current vertex ends the segment
+ var segmentVertices = vertices.AsSpan().Slice(start, i - start + 1);
+ var segmentType = controlPoints[start].Type ?? PathType.Linear;
+
+ switch (segmentType)
+ {
+ case PathType.Catmull:
+ foreach (var segment in ConvertCatmullToBezierAnchors(segmentVertices))
+ {
+ for (int j = 0; j < segment.Length - 1; j++)
+ {
+ result.Add(new PathControlPoint(segment[j], j == 0 ? PathType.Bezier : null));
+ }
+ }
+
+ break;
+
+ case PathType.Linear:
+ foreach (var segment in ConvertLinearToBezierAnchors(segmentVertices))
+ {
+ for (int j = 0; j < segment.Length - 1; j++)
+ {
+ result.Add(new PathControlPoint(segment[j], j == 0 ? PathType.Bezier : null));
+ }
+ }
+
+ break;
+
+ case PathType.PerfectCurve:
+ var circleResult = ConvertCircleToBezierAnchors(segmentVertices);
+
+ for (int j = 0; j < circleResult.Length - 1; j++)
+ {
+ result.Add(new PathControlPoint(circleResult[j], j == 0 ? PathType.Bezier : null));
+ }
+
+ break;
+
+ default:
+ for (int j = 0; j < segmentVertices.Length - 1; j++)
+ {
+ result.Add(new PathControlPoint(segmentVertices[j], j == 0 ? PathType.Bezier : null));
+ }
+
+ break;
+ }
+
+ // Start the new segment at the current vertex
+ start = i;
+ }
+
+ result.Add(new PathControlPoint(controlPoints[^1].Position));
+
+ return result;
+ }
+
+ ///
+ /// Converts perfect curve anchors to bezier anchors.
+ ///
+ /// The control point positions to convert.
+ public static Vector2[] ConvertCircleToBezierAnchors(ReadOnlySpan controlPoints)
+ {
+ if (controlPoints.Length != 3)
+ return controlPoints.ToArray();
+
+ var pr = new CircularArcProperties(controlPoints);
+ if (!pr.IsValid)
+ return controlPoints.ToArray();
+
+ CircleBezierPreset preset = circle_presets.Last();
+
+ foreach (CircleBezierPreset cbp in circle_presets)
+ {
+ if (cbp.ArcLength < pr.ThetaRange) continue;
+
+ preset = cbp;
+ break;
+ }
+
+ double arcLength = preset.ArcLength;
+ var arc = new Vector2d[preset.ControlPoints.Length];
+ preset.ControlPoints.CopyTo(arc, 0);
+
+ // Converge on arcLength of thetaRange
+ int n = arc.Length - 1;
+ double tf = pr.ThetaRange / arcLength;
+
+ while (Math.Abs(tf - 1) > 1E-7)
+ {
+ for (int j = 0; j < n; j++)
+ {
+ for (int i = n; i > j; i--)
+ {
+ arc[i] = arc[i] * tf + arc[i - 1] * (1 - tf);
+ }
+ }
+
+ arcLength = Math.Atan2(arc.Last()[1], arc.Last()[0]);
+
+ if (arcLength < 0)
+ {
+ arcLength += 2 * Math.PI;
+ }
+
+ tf = pr.ThetaRange / arcLength;
+ }
+
+ // Adjust rotation, radius, and position
+ var result = new Vector2[arc.Length];
+
+ for (int i = 0; i < arc.Length; i++)
+ {
+ result[i] = new Vector2(
+ (float)((Math.Cos(pr.ThetaStart) * arc[i].X + -Math.Sin(pr.ThetaStart) * pr.Direction * arc[i].Y) * pr.Radius + pr.Centre.X),
+ (float)((Math.Sin(pr.ThetaStart) * arc[i].X + Math.Cos(pr.ThetaStart) * pr.Direction * arc[i].Y) * pr.Radius + pr.Centre.Y));
+ }
+
+ return result;
+ }
+
+ ///
+ /// Converts catmull anchors to bezier anchors.
+ ///
+ /// The control point positions to convert.
+ public static Vector2[][] ConvertCatmullToBezierAnchors(ReadOnlySpan controlPoints)
+ {
+ int iLen = controlPoints.Length;
+ var bezier = new Vector2[iLen - 1][];
+
+ for (int i = 0; i < iLen - 1; i++)
+ {
+ var v1 = i > 0 ? controlPoints[i - 1] : controlPoints[i];
+ var v2 = controlPoints[i];
+ var v3 = i < iLen - 1 ? controlPoints[i + 1] : v2 + v2 - v1;
+ var v4 = i < iLen - 2 ? controlPoints[i + 2] : v3 + v3 - v2;
+
+ bezier[i] = new[]
+ {
+ v2,
+ (-v1 + 6 * v2 + v3) / 6,
+ (-v4 + 6 * v3 + v2) / 6,
+ v3
+ };
+ }
+
+ return bezier;
+ }
+
+ ///
+ /// Converts linear anchors to bezier anchors.
+ ///
+ /// The control point positions to convert.
+ public static Vector2[][] ConvertLinearToBezierAnchors(ReadOnlySpan controlPoints)
+ {
+ int iLen = controlPoints.Length;
+ var bezier = new Vector2[iLen - 1][];
+
+ for (int i = 0; i < iLen - 1; i++)
+ {
+ bezier[i] = new[]
+ {
+ controlPoints[i],
+ controlPoints[i + 1]
+ };
+ }
+
+ return bezier;
+ }
+ }
+}
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index fe44ed3688..9a6866d264 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -23,10 +23,10 @@
-
-
-
-
+
+
+
+
@@ -34,10 +34,10 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
-
+