diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs index 738a21b17e..35b79aa8ac 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs @@ -4,9 +4,11 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; +using osu.Framework.Graphics.UserInterface; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Tests.Visual; @@ -14,7 +16,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { - public class TestScenePathControlPointVisualiser : OsuTestScene + public class TestScenePathControlPointVisualiser : OsuManualInputManagerTestScene { private Slider slider; private PathControlPointVisualiser visualiser; @@ -43,12 +45,145 @@ public void TestAddOverlappingControlPoints() }); } + [Test] + public void TestPerfectCurveTooManyPoints() + { + createVisualiser(true); + + addControlPointStep(new Vector2(200), PathType.Bezier); + addControlPointStep(new Vector2(300)); + addControlPointStep(new Vector2(500, 300)); + addControlPointStep(new Vector2(700, 200)); + addControlPointStep(new Vector2(500, 100)); + + // Must be both hovering and selecting the control point for the context menu to work. + moveMouseToControlPoint(1); + AddStep("select control point", () => visualiser.Pieces[1].IsSelected.Value = true); + addContextMenuItemStep("Perfect curve"); + + assertControlPointPathType(0, PathType.Bezier); + assertControlPointPathType(1, PathType.PerfectCurve); + assertControlPointPathType(3, PathType.Bezier); + } + + [Test] + public void TestPerfectCurveLastThreePoints() + { + createVisualiser(true); + + addControlPointStep(new Vector2(200), PathType.Bezier); + addControlPointStep(new Vector2(300)); + addControlPointStep(new Vector2(500, 300)); + addControlPointStep(new Vector2(700, 200)); + addControlPointStep(new Vector2(500, 100)); + + moveMouseToControlPoint(2); + AddStep("select control point", () => visualiser.Pieces[2].IsSelected.Value = true); + addContextMenuItemStep("Perfect curve"); + + assertControlPointPathType(0, PathType.Bezier); + assertControlPointPathType(2, PathType.PerfectCurve); + assertControlPointPathType(4, null); + } + + [Test] + public void TestPerfectCurveLastTwoPoints() + { + createVisualiser(true); + + addControlPointStep(new Vector2(200), PathType.Bezier); + addControlPointStep(new Vector2(300)); + addControlPointStep(new Vector2(500, 300)); + addControlPointStep(new Vector2(700, 200)); + addControlPointStep(new Vector2(500, 100)); + + moveMouseToControlPoint(3); + AddStep("select control point", () => visualiser.Pieces[3].IsSelected.Value = true); + addContextMenuItemStep("Perfect curve"); + + assertControlPointPathType(0, PathType.Bezier); + AddAssert("point 3 is not inherited", () => slider.Path.ControlPoints[3].Type != null); + } + + [Test] + public void TestPerfectCurveTooManyPointsLinear() + { + createVisualiser(true); + + addControlPointStep(new Vector2(200), PathType.Linear); + addControlPointStep(new Vector2(300)); + addControlPointStep(new Vector2(500, 300)); + addControlPointStep(new Vector2(700, 200)); + addControlPointStep(new Vector2(500, 100)); + + // Must be both hovering and selecting the control point for the context menu to work. + moveMouseToControlPoint(1); + AddStep("select control point", () => visualiser.Pieces[1].IsSelected.Value = true); + addContextMenuItemStep("Perfect curve"); + + assertControlPointPathType(0, PathType.Linear); + assertControlPointPathType(1, PathType.PerfectCurve); + assertControlPointPathType(3, PathType.Linear); + } + + [Test] + public void TestPerfectCurveChangeToBezier() + { + createVisualiser(true); + + addControlPointStep(new Vector2(200), PathType.Bezier); + addControlPointStep(new Vector2(300), PathType.PerfectCurve); + addControlPointStep(new Vector2(500, 300)); + addControlPointStep(new Vector2(700, 200), PathType.Bezier); + addControlPointStep(new Vector2(500, 100)); + + moveMouseToControlPoint(3); + AddStep("select control point", () => visualiser.Pieces[3].IsSelected.Value = true); + addContextMenuItemStep("Inherit"); + + assertControlPointPathType(0, PathType.Bezier); + assertControlPointPathType(1, PathType.Bezier); + assertControlPointPathType(3, null); + } + private void createVisualiser(bool allowSelection) => AddStep("create visualiser", () => Child = visualiser = new PathControlPointVisualiser(slider, allowSelection) { Anchor = Anchor.Centre, Origin = Anchor.Centre }); - private void addControlPointStep(Vector2 position) => AddStep($"add control point {position}", () => slider.Path.ControlPoints.Add(new PathControlPoint(position))); + private void addControlPointStep(Vector2 position) => addControlPointStep(position, null); + + private void addControlPointStep(Vector2 position, PathType? type) + { + AddStep($"add {type} control point at {position}", () => + { + slider.Path.ControlPoints.Add(new PathControlPoint(position, type)); + }); + } + + private void moveMouseToControlPoint(int index) + { + AddStep($"move mouse to control point {index}", () => + { + Vector2 position = slider.Path.ControlPoints[index].Position.Value; + InputManager.MoveMouseTo(visualiser.Pieces[0].Parent.ToScreenSpace(position)); + }); + } + + private void assertControlPointPathType(int controlPointIndex, PathType? type) + { + AddAssert($"point {controlPointIndex} is {type}", () => slider.Path.ControlPoints[controlPointIndex].Type.Value == type); + } + + private void addContextMenuItemStep(string contextMenuText) + { + AddStep($"click context menu item \"{contextMenuText}\"", () => + { + MenuItem item = visualiser.ContextMenuItems[1].Items.FirstOrDefault(menuItem => menuItem.Text.Value == contextMenuText); + + item?.Action?.Value(); + }); + } } } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs index 20d4fe4ea9..6b78cff33e 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs @@ -213,10 +213,13 @@ private void updatePathType() if (ControlPoint.Type.Value != PathType.PerfectCurve) return; - ReadOnlySpan points = PointsInSegment.Select(p => p.Position.Value).ToArray(); - if (points.Length != 3) + if (PointsInSegment.Count > 3) + ControlPoint.Type.Value = PathType.Bezier; + + if (PointsInSegment.Count != 3) return; + ReadOnlySpan points = PointsInSegment.Select(p => p.Position.Value).ToArray(); RectangleF boundingBox = PathApproximator.CircularArcBoundingBox(points); if (boundingBox.Width >= 640 || boundingBox.Height >= 480) ControlPoint.Type.Value = PathType.Bezier; diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index ce5dc4855e..44c3056910 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -153,6 +153,34 @@ private void selectPiece(PathControlPointPiece piece, MouseButtonEvent e) } } + /// + /// Attempts to set the given control point piece to the given path type. + /// If that would fail, try to change the path such that it instead succeeds + /// in a UX-friendly way. + /// + /// The control point piece that we want to change the path type of. + /// The path type we want to assign to the given control point piece. + private void updatePathType(PathControlPointPiece piece, PathType? type) + { + int indexInSegment = piece.PointsInSegment.IndexOf(piece.ControlPoint); + + switch (type) + { + case PathType.PerfectCurve: + // Can't always create a circular arc out of 4 or more points, + // so we split the segment into one 3-point circular arc segment + // and one segment of the previous type. + int thirdPointIndex = indexInSegment + 2; + + if (piece.PointsInSegment.Count > thirdPointIndex + 1) + piece.PointsInSegment[thirdPointIndex].Type.Value = piece.PointsInSegment[0].Type.Value; + + break; + } + + piece.ControlPoint.Type.Value = type; + } + [Resolved(CanBeNull = true)] private IEditorChangeHandler changeHandler { get; set; } @@ -218,7 +246,7 @@ private MenuItem createMenuItemForPathType(PathType? type) var item = new PathTypeMenuItem(type, () => { foreach (var p in Pieces.Where(p => p.IsSelected.Value)) - p.ControlPoint.Type.Value = type; + updatePathType(p, type); }); if (countOfState == totalCount)