1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-15 23:03:20 +08:00

Merge pull request #12122 from Naxesss/circular-arc-freeze

Fix freezes due to large circular arc radius
This commit is contained in:
Dean Herbert 2021-04-08 15:18:23 +09:00 committed by GitHub
commit 0a4b621739
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 356 additions and 0 deletions

View File

@ -0,0 +1,175 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Utils;
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.HitCircles.Components;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Tests.Visual;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Tests.Editor
{
public class TestSceneSliderControlPointPiece : SelectionBlueprintTestScene
{
private Slider slider;
private DrawableSlider drawableObject;
[SetUp]
public void Setup() => Schedule(() =>
{
Clear();
slider = new Slider
{
Position = new Vector2(256, 192),
Path = new SliderPath(new[]
{
new PathControlPoint(Vector2.Zero, PathType.PerfectCurve),
new PathControlPoint(new Vector2(150, 150)),
new PathControlPoint(new Vector2(300, 0), PathType.PerfectCurve),
new PathControlPoint(new Vector2(400, 0)),
new PathControlPoint(new Vector2(400, 150))
})
};
slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = 2 });
Add(drawableObject = new DrawableSlider(slider));
AddBlueprint(new TestSliderBlueprint(drawableObject));
});
[Test]
public void TestDragControlPoint()
{
moveMouseToControlPoint(1);
AddStep("hold", () => InputManager.PressButton(MouseButton.Left));
addMovementStep(new Vector2(150, 50));
AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left));
assertControlPointPosition(1, new Vector2(150, 50));
assertControlPointType(0, PathType.PerfectCurve);
}
[Test]
public void TestDragControlPointAlmostLinearlyExterior()
{
moveMouseToControlPoint(1);
AddStep("hold", () => InputManager.PressButton(MouseButton.Left));
addMovementStep(new Vector2(400, 0.01f));
AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left));
assertControlPointPosition(1, new Vector2(400, 0.01f));
assertControlPointType(0, PathType.Bezier);
}
[Test]
public void TestDragControlPointPathRecovery()
{
moveMouseToControlPoint(1);
AddStep("hold", () => InputManager.PressButton(MouseButton.Left));
addMovementStep(new Vector2(400, 0.01f));
assertControlPointType(0, PathType.Bezier);
addMovementStep(new Vector2(150, 50));
AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left));
assertControlPointPosition(1, new Vector2(150, 50));
assertControlPointType(0, PathType.PerfectCurve);
}
[Test]
public void TestDragControlPointPathRecoveryOtherSegment()
{
moveMouseToControlPoint(4);
AddStep("hold", () => InputManager.PressButton(MouseButton.Left));
addMovementStep(new Vector2(350, 0.01f));
assertControlPointType(2, PathType.Bezier);
addMovementStep(new Vector2(150, 150));
AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left));
assertControlPointPosition(4, new Vector2(150, 150));
assertControlPointType(2, PathType.PerfectCurve);
}
[Test]
public void TestDragControlPointPathAfterChangingType()
{
AddStep("change type to bezier", () => slider.Path.ControlPoints[2].Type.Value = PathType.Bezier);
AddStep("add point", () => slider.Path.ControlPoints.Add(new PathControlPoint(new Vector2(500, 10))));
AddStep("change type to perfect", () => slider.Path.ControlPoints[3].Type.Value = PathType.PerfectCurve);
moveMouseToControlPoint(4);
AddStep("hold", () => InputManager.PressButton(MouseButton.Left));
assertControlPointType(3, PathType.PerfectCurve);
addMovementStep(new Vector2(350, 0.01f));
AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left));
assertControlPointPosition(4, new Vector2(350, 0.01f));
assertControlPointType(3, PathType.Bezier);
}
private void addMovementStep(Vector2 relativePosition)
{
AddStep($"move mouse to {relativePosition}", () =>
{
Vector2 position = slider.Position + relativePosition;
InputManager.MoveMouseTo(drawableObject.Parent.ToScreenSpace(position));
});
}
private void moveMouseToControlPoint(int index)
{
AddStep($"move mouse to control point {index}", () =>
{
Vector2 position = slider.Position + slider.Path.ControlPoints[index].Position.Value;
InputManager.MoveMouseTo(drawableObject.Parent.ToScreenSpace(position));
});
}
private void assertControlPointType(int index, PathType type) => AddAssert($"control point {index} is {type}", () => slider.Path.ControlPoints[index].Type.Value == type);
private void assertControlPointPosition(int index, Vector2 position) =>
AddAssert($"control point {index} at {position}", () => Precision.AlmostEquals(position, slider.Path.ControlPoints[index].Position.Value, 1));
private class TestSliderBlueprint : SliderSelectionBlueprint
{
public new SliderBodyPiece BodyPiece => base.BodyPiece;
public new TestSliderCircleBlueprint HeadBlueprint => (TestSliderCircleBlueprint)base.HeadBlueprint;
public new TestSliderCircleBlueprint TailBlueprint => (TestSliderCircleBlueprint)base.TailBlueprint;
public new PathControlPointVisualiser ControlPointVisualiser => base.ControlPointVisualiser;
public TestSliderBlueprint(DrawableSlider slider)
: base(slider)
{
}
protected override SliderCircleSelectionBlueprint CreateCircleSelectionBlueprint(DrawableSlider slider, SliderPosition position) => new TestSliderCircleBlueprint(slider, position);
}
private class TestSliderCircleBlueprint : SliderCircleSelectionBlueprint
{
public new HitCirclePiece CirclePiece => base.CirclePiece;
public TestSliderCircleBlueprint(DrawableSlider slider, SliderPosition position)
: base(slider, position)
{
}
}
}
}

View File

@ -276,6 +276,104 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertControlPointType(0, PathType.Linear); assertControlPointType(0, PathType.Linear);
} }
[Test]
public void TestPlacePerfectCurveSegmentAlmostLinearlyExterior()
{
Vector2 startPosition = new Vector2(200);
addMovementStep(startPosition);
addClickStep(MouseButton.Left);
addMovementStep(startPosition + new Vector2(300, 0));
addClickStep(MouseButton.Left);
addMovementStep(startPosition + new Vector2(150, 0.1f));
addClickStep(MouseButton.Right);
assertPlaced(true);
assertControlPointCount(3);
assertControlPointType(0, PathType.Bezier);
}
[Test]
public void TestPlacePerfectCurveSegmentRecovery()
{
Vector2 startPosition = new Vector2(200);
addMovementStep(startPosition);
addClickStep(MouseButton.Left);
addMovementStep(startPosition + new Vector2(300, 0));
addClickStep(MouseButton.Left);
addMovementStep(startPosition + new Vector2(150, 0.1f)); // Should convert to bezier
addMovementStep(startPosition + new Vector2(400.0f, 50.0f)); // Should convert back to perfect
addClickStep(MouseButton.Right);
assertPlaced(true);
assertControlPointCount(3);
assertControlPointType(0, PathType.PerfectCurve);
}
[Test]
public void TestPlacePerfectCurveSegmentLarge()
{
Vector2 startPosition = new Vector2(400);
addMovementStep(startPosition);
addClickStep(MouseButton.Left);
addMovementStep(startPosition + new Vector2(220, 220));
addClickStep(MouseButton.Left);
// Playfield dimensions are 640 x 480.
// So a 440 x 440 bounding box should be ok.
addMovementStep(startPosition + new Vector2(-220, 220));
addClickStep(MouseButton.Right);
assertPlaced(true);
assertControlPointCount(3);
assertControlPointType(0, PathType.PerfectCurve);
}
[Test]
public void TestPlacePerfectCurveSegmentTooLarge()
{
Vector2 startPosition = new Vector2(480, 200);
addMovementStep(startPosition);
addClickStep(MouseButton.Left);
addMovementStep(startPosition + new Vector2(400, 400));
addClickStep(MouseButton.Left);
// Playfield dimensions are 640 x 480.
// So an 800 * 800 bounding box area should not be ok.
addMovementStep(startPosition + new Vector2(-400, 400));
addClickStep(MouseButton.Right);
assertPlaced(true);
assertControlPointCount(3);
assertControlPointType(0, PathType.Bezier);
}
[Test]
public void TestPlacePerfectCurveSegmentCompleteArc()
{
addMovementStep(new Vector2(400));
addClickStep(MouseButton.Left);
addMovementStep(new Vector2(600, 400));
addClickStep(MouseButton.Left);
addMovementStep(new Vector2(400, 410));
addClickStep(MouseButton.Right);
assertPlaced(true);
assertControlPointCount(3);
assertControlPointType(0, PathType.PerfectCurve);
}
private void addMovementStep(Vector2 position) => AddStep($"move mouse to {position}", () => InputManager.MoveMouseTo(InputManager.ToScreenSpace(position))); private void addMovementStep(Vector2 position) => AddStep($"move mouse to {position}", () => InputManager.MoveMouseTo(InputManager.ToScreenSpace(position)));
private void addClickStep(MouseButton button) private void addClickStep(MouseButton button)

View File

@ -2,14 +2,18 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
@ -28,6 +32,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
public class PathControlPointPiece : BlueprintPiece<Slider>, IHasTooltip public class PathControlPointPiece : BlueprintPiece<Slider>, IHasTooltip
{ {
public Action<PathControlPointPiece, MouseButtonEvent> RequestSelection; public Action<PathControlPointPiece, MouseButtonEvent> RequestSelection;
public List<PathControlPoint> PointsInSegment;
public readonly BindableBool IsSelected = new BindableBool(); public readonly BindableBool IsSelected = new BindableBool();
public readonly PathControlPoint ControlPoint; public readonly PathControlPoint ControlPoint;
@ -54,6 +59,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
this.slider = slider; this.slider = slider;
ControlPoint = controlPoint; ControlPoint = controlPoint;
slider.Path.Version.BindValueChanged(_ =>
{
PointsInSegment = slider.Path.PointsInSegment(ControlPoint);
updatePathType();
}, runOnceImmediately: true);
controlPoint.Type.BindValueChanged(_ => updateMarkerDisplay()); controlPoint.Type.BindValueChanged(_ => updateMarkerDisplay());
Origin = Anchor.Centre; Origin = Anchor.Centre;
@ -150,6 +161,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
protected override bool OnClick(ClickEvent e) => RequestSelection != null; protected override bool OnClick(ClickEvent e) => RequestSelection != null;
private Vector2 dragStartPosition; private Vector2 dragStartPosition;
private PathType? dragPathType;
protected override bool OnDragStart(DragStartEvent e) protected override bool OnDragStart(DragStartEvent e)
{ {
@ -159,6 +171,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
if (e.Button == MouseButton.Left) if (e.Button == MouseButton.Left)
{ {
dragStartPosition = ControlPoint.Position.Value; dragStartPosition = ControlPoint.Position.Value;
dragPathType = PointsInSegment[0].Type.Value;
changeHandler?.BeginChange(); changeHandler?.BeginChange();
return true; return true;
} }
@ -184,10 +198,30 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
} }
else else
ControlPoint.Position.Value = dragStartPosition + (e.MousePosition - e.MouseDownPosition); ControlPoint.Position.Value = dragStartPosition + (e.MousePosition - e.MouseDownPosition);
// Maintain the path type in case it got defaulted to bezier at some point during the drag.
PointsInSegment[0].Type.Value = dragPathType;
} }
protected override void OnDragEnd(DragEndEvent e) => changeHandler?.EndChange(); protected override void OnDragEnd(DragEndEvent e) => changeHandler?.EndChange();
/// <summary>
/// Handles correction of invalid path types.
/// </summary>
private void updatePathType()
{
if (ControlPoint.Type.Value != PathType.PerfectCurve)
return;
ReadOnlySpan<Vector2> points = PointsInSegment.Select(p => p.Position.Value).ToArray();
if (points.Length != 3)
return;
RectangleF boundingBox = PathApproximator.CircularArcBoundingBox(points);
if (boundingBox.Width >= 640 || boundingBox.Height >= 480)
ControlPoint.Type.Value = PathType.Bezier;
}
/// <summary> /// <summary>
/// Updates the state of the circular control point marker. /// Updates the state of the circular control point marker.
/// </summary> /// </summary>

View File

@ -142,6 +142,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{ {
base.Update(); base.Update();
updateSlider(); updateSlider();
// Maintain the path type in case it got defaulted to bezier at some point during the drag.
updatePathType();
} }
private void updatePathType() private void updatePathType()

View File

@ -33,6 +33,7 @@ namespace osu.Game.Rulesets.Osu.Edit
{ {
base.OnOperationEnded(); base.OnOperationEnded();
referenceOrigin = null; referenceOrigin = null;
referencePathTypes = null;
} }
public override bool HandleMovement(MoveSelectionEvent moveEvent) public override bool HandleMovement(MoveSelectionEvent moveEvent)
@ -53,6 +54,12 @@ namespace osu.Game.Rulesets.Osu.Edit
/// </summary> /// </summary>
private Vector2? referenceOrigin; private Vector2? referenceOrigin;
/// <summary>
/// During a transform, the initial path types of a single selected slider are stored so they
/// can be maintained throughout the operation.
/// </summary>
private List<PathType?> referencePathTypes;
public override bool HandleReverse() public override bool HandleReverse()
{ {
var hitObjects = EditorBeatmap.SelectedHitObjects; var hitObjects = EditorBeatmap.SelectedHitObjects;
@ -194,6 +201,8 @@ namespace osu.Game.Rulesets.Osu.Edit
private void scaleSlider(Slider slider, Vector2 scale) private void scaleSlider(Slider slider, Vector2 scale)
{ {
referencePathTypes ??= slider.Path.ControlPoints.Select(p => p.Type.Value).ToList();
Quad sliderQuad = getSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position.Value)); Quad sliderQuad = getSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position.Value));
// Limit minimum distance between control points after scaling to almost 0. Less than 0 causes the slider to flip, exactly 0 causes a crash through division by 0. // Limit minimum distance between control points after scaling to almost 0. Less than 0 causes the slider to flip, exactly 0 causes a crash through division by 0.
@ -209,6 +218,10 @@ namespace osu.Game.Rulesets.Osu.Edit
point.Position.Value *= pathRelativeDeltaScale; point.Position.Value *= pathRelativeDeltaScale;
} }
// Maintain the path types in case they were defaulted to bezier at some point during scaling
for (int i = 0; i < slider.Path.ControlPoints.Count; ++i)
slider.Path.ControlPoints[i].Type.Value = referencePathTypes[i];
//if sliderhead or sliderend end up outside playfield, revert scaling. //if sliderhead or sliderend end up outside playfield, revert scaling.
Quad scaledQuad = getSurroundingQuad(new OsuHitObject[] { slider }); Quad scaledQuad = getSurroundingQuad(new OsuHitObject[] { slider });
(bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad); (bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad);

View File

@ -156,6 +156,39 @@ namespace osu.Game.Rulesets.Objects
return interpolateVertices(indexOfDistance(d), d); return interpolateVertices(indexOfDistance(d), d);
} }
/// <summary>
/// Returns the control points belonging to the same segment as the one given.
/// The first point has a PathType which all other points inherit.
/// </summary>
/// <param name="controlPoint">One of the control points in the segment.</param>
/// <returns></returns>
public List<PathControlPoint> PointsInSegment(PathControlPoint controlPoint)
{
bool found = false;
List<PathControlPoint> pointsInCurrentSegment = new List<PathControlPoint>();
foreach (PathControlPoint point in ControlPoints)
{
if (point.Type.Value != null)
{
if (!found)
pointsInCurrentSegment.Clear();
else
{
pointsInCurrentSegment.Add(point);
break;
}
}
pointsInCurrentSegment.Add(point);
if (point == controlPoint)
found = true;
}
return pointsInCurrentSegment;
}
private void invalidate() private void invalidate()
{ {
pathCache.Invalidate(); pathCache.Invalidate();