1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-26 19:22:55 +08:00

Add free-hand drawing of sliders to the editor

This commit is contained in:
cs 2023-11-11 10:45:22 +01:00
parent 926636cc03
commit 3f85aa79c5
5 changed files with 171 additions and 11 deletions

View File

@ -10,6 +10,7 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
@ -44,6 +45,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private IDistanceSnapProvider distanceSnapProvider { get; set; } private IDistanceSnapProvider distanceSnapProvider { get; set; }
[Resolved(CanBeNull = true)]
private ISliderDrawingSettingsProvider drawingSettingsProvider { get; set; }
private readonly IncrementalBSplineBuilder bSplineBuilder = new IncrementalBSplineBuilder();
protected override bool IsValidForPlacement => HitObject.Path.HasValidLength; protected override bool IsValidForPlacement => HitObject.Path.HasValidLength;
public SliderPlacementBlueprint() public SliderPlacementBlueprint()
@ -73,6 +79,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{ {
base.LoadComplete(); base.LoadComplete();
inputManager = GetContainingInputManager(); inputManager = GetContainingInputManager();
drawingSettingsProvider.Tolerance.BindValueChanged(e =>
{
if (bSplineBuilder.Tolerance != e.NewValue)
bSplineBuilder.Tolerance = e.NewValue;
updateSliderPathFromBSplineBuilder();
}, true);
} }
[Resolved] [Resolved]
@ -98,7 +111,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
ApplyDefaultsToHitObject(); ApplyDefaultsToHitObject();
break; break;
case SliderPlacementState.Body: case SliderPlacementState.ControlPoints:
updateCursor(); updateCursor();
break; break;
} }
@ -115,7 +128,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
beginCurve(); beginCurve();
break; break;
case SliderPlacementState.Body: case SliderPlacementState.ControlPoints:
if (canPlaceNewControlPoint(out var lastPoint)) if (canPlaceNewControlPoint(out var lastPoint))
{ {
// Place a new point by detatching the current cursor. // Place a new point by detatching the current cursor.
@ -139,9 +152,62 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
return true; return true;
} }
protected override bool OnDragStart(DragStartEvent e)
{
if (e.Button == MouseButton.Left)
{
switch (state)
{
case SliderPlacementState.Initial:
return true;
case SliderPlacementState.ControlPoints:
if (HitObject.Path.ControlPoints.Count < 3)
{
var lastCp = HitObject.Path.ControlPoints.LastOrDefault();
if (lastCp != cursor)
return false;
bSplineBuilder.Clear();
bSplineBuilder.AddLinearPoint(ToLocalSpace(e.ScreenSpaceMousePosition) - HitObject.Position);
setState(SliderPlacementState.Drawing);
return true;
}
return false;
}
}
return base.OnDragStart(e);
}
protected override void OnDrag(DragEvent e)
{
base.OnDrag(e);
bSplineBuilder.AddLinearPoint(ToLocalSpace(e.ScreenSpaceMousePosition) - HitObject.Position);
updateSliderPathFromBSplineBuilder();
}
private void updateSliderPathFromBSplineBuilder()
{
Scheduler.AddOnce(static self =>
{
var cps = self.bSplineBuilder.GetControlPoints();
self.HitObject.Path.ControlPoints.RemoveRange(1, self.HitObject.Path.ControlPoints.Count - 1);
self.HitObject.Path.ControlPoints.AddRange(cps.Select(v => new PathControlPoint(v)));
}, this);
}
protected override void OnDragEnd(DragEndEvent e)
{
base.OnDragEnd(e);
if (state == SliderPlacementState.Drawing)
endCurve();
}
protected override void OnMouseUp(MouseUpEvent e) protected override void OnMouseUp(MouseUpEvent e)
{ {
if (state == SliderPlacementState.Body && e.Button == MouseButton.Right) if (state == SliderPlacementState.ControlPoints && e.Button == MouseButton.Right)
endCurve(); endCurve();
base.OnMouseUp(e); base.OnMouseUp(e);
} }
@ -149,7 +215,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private void beginCurve() private void beginCurve()
{ {
BeginPlacement(commitStart: true); BeginPlacement(commitStart: true);
setState(SliderPlacementState.Body); setState(SliderPlacementState.ControlPoints);
} }
private void endCurve() private void endCurve()
@ -169,6 +235,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private void updatePathType() private void updatePathType()
{ {
if (state == SliderPlacementState.Drawing)
{
segmentStart.Type = PathType.BSpline(3);
return;
}
switch (currentSegmentLength) switch (currentSegmentLength)
{ {
case 1: case 1:
@ -201,7 +273,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
} }
// Update the cursor position. // Update the cursor position.
var result = positionSnapProvider?.FindSnappedPositionAndTime(inputManager.CurrentState.Mouse.Position, state == SliderPlacementState.Body ? SnapType.GlobalGrids : SnapType.All); var result = positionSnapProvider?.FindSnappedPositionAndTime(inputManager.CurrentState.Mouse.Position, state == SliderPlacementState.ControlPoints ? SnapType.GlobalGrids : SnapType.All);
cursor.Position = ToLocalSpace(result?.ScreenSpacePosition ?? inputManager.CurrentState.Mouse.Position) - HitObject.Position; cursor.Position = ToLocalSpace(result?.ScreenSpacePosition ?? inputManager.CurrentState.Mouse.Position) - HitObject.Position;
} }
else if (cursor != null) else if (cursor != null)
@ -248,7 +320,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private enum SliderPlacementState private enum SliderPlacementState
{ {
Initial, Initial,
Body, ControlPoints,
Drawing,
DrawingFinalization
} }
} }
} }

View File

@ -0,0 +1,12 @@
// 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 osu.Framework.Bindables;
namespace osu.Game.Rulesets.Osu.Edit
{
public interface ISliderDrawingSettingsProvider
{
BindableFloat Tolerance { get; }
}
}

View File

@ -63,6 +63,9 @@ namespace osu.Game.Rulesets.Osu.Edit
[Cached(typeof(IDistanceSnapProvider))] [Cached(typeof(IDistanceSnapProvider))]
protected readonly OsuDistanceSnapProvider DistanceSnapProvider = new OsuDistanceSnapProvider(); protected readonly OsuDistanceSnapProvider DistanceSnapProvider = new OsuDistanceSnapProvider();
[Cached(typeof(ISliderDrawingSettingsProvider))]
protected readonly OsuSliderDrawingSettingsProvider SliderDrawingSettingsProvider = new OsuSliderDrawingSettingsProvider();
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
@ -96,8 +99,11 @@ namespace osu.Game.Rulesets.Osu.Edit
RightToolbox.Add(new TransformToolboxGroup RightToolbox.Add(new TransformToolboxGroup
{ {
RotationHandler = BlueprintContainer.SelectionHandler.RotationHandler RotationHandler = BlueprintContainer.SelectionHandler.RotationHandler,
}); });
AddInternal(SliderDrawingSettingsProvider);
SliderDrawingSettingsProvider.AttachToToolbox(RightToolbox);
} }
protected override ComposeBlueprintContainer CreateBlueprintContainer() protected override ComposeBlueprintContainer CreateBlueprintContainer()

View File

@ -0,0 +1,68 @@
// 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 System;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Utils;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
namespace osu.Game.Rulesets.Osu.Edit
{
public partial class OsuSliderDrawingSettingsProvider : Drawable, ISliderDrawingSettingsProvider
{
public BindableFloat Tolerance { get; } = new BindableFloat(0.1f)
{
MinValue = 0.05f,
MaxValue = 1f,
Precision = 0.01f
};
private BindableInt sliderTolerance = new BindableInt(10)
{
MinValue = 5,
MaxValue = 100
};
private ExpandableSlider<int> toleranceSlider = null!;
private EditorToolboxGroup? toolboxGroup;
public OsuSliderDrawingSettingsProvider()
{
sliderTolerance.BindValueChanged(v =>
{
float newValue = v.NewValue / 100f;
if (!Precision.AlmostEquals(newValue, Tolerance.Value, 1e-7f))
Tolerance.Value = newValue;
});
Tolerance.BindValueChanged(v =>
{
int newValue = (int)Math.Round(v.NewValue * 100f);
if (sliderTolerance.Value != newValue)
sliderTolerance.Value = newValue;
});
}
public void AttachToToolbox(ExpandingToolboxContainer toolboxContainer)
{
toolboxContainer.Add(toolboxGroup = new EditorToolboxGroup("drawing")
{
Children = new Drawable[]
{
toleranceSlider = new ExpandableSlider<int>
{
Current = sliderTolerance
}
}
});
sliderTolerance.BindValueChanged(e =>
{
toleranceSlider.ContractedLabelText = $"Tolerance: {e.NewValue:N0}";
toleranceSlider.ExpandedLabelText = $"Tolerance: {e.NewValue:N0}";
}, true);
}
}
}

View File

@ -292,13 +292,13 @@ namespace osu.Game.Rulesets.Objects
switch (type.SplineType) switch (type.SplineType)
{ {
case SplineType.Linear: case SplineType.Linear:
return PathApproximator.ApproximateLinear(subControlPoints); return PathApproximator.LinearToPiecewiseLinear(subControlPoints);
case SplineType.PerfectCurve: case SplineType.PerfectCurve:
if (subControlPoints.Length != 3) if (subControlPoints.Length != 3)
break; break;
List<Vector2> subPath = PathApproximator.ApproximateCircularArc(subControlPoints); 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. // If for some reason a circular arc could not be fit to the 3 given points, fall back to a numerically stable bezier approximation.
if (subPath.Count == 0) if (subPath.Count == 0)
@ -307,10 +307,10 @@ namespace osu.Game.Rulesets.Objects
return subPath; return subPath;
case SplineType.Catmull: case SplineType.Catmull:
return PathApproximator.ApproximateCatmull(subControlPoints); return PathApproximator.CatmullToPiecewiseLinear(subControlPoints);
} }
return PathApproximator.ApproximateBSpline(subControlPoints, type.Degree ?? subControlPoints.Length); return PathApproximator.BSplineToPiecewiseLinear(subControlPoints, type.Degree ?? subControlPoints.Length);
} }
private void calculateLength() private void calculateLength()