Use draggable handle for length adjust

This commit is contained in:
OliBomby 2024-07-03 20:17:39 +02:00
parent 956bdbca50
commit 3926af1053
3 changed files with 173 additions and 46 deletions

View File

@ -3,7 +3,6 @@
#nullable disable
using System.Linq;
using NUnit.Framework;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
@ -165,23 +164,35 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
}
[Test]
public void TestAdjustDistance()
public void TestAdjustLength()
{
AddStep("start adjust length",
() => blueprint.ContextMenuItems.Single(o => o.Text.Value == "Adjust length").Action.Value());
moveMouseToControlPoint(1);
AddStep("end adjust length", () => InputManager.Click(MouseButton.Right));
AddStep("move mouse to drag marker", () =>
{
Vector2 position = slider.Position + slider.Path.PositionAt(1) + new Vector2(60, 0);
InputManager.MoveMouseTo(drawableObject.Parent!.ToScreenSpace(position));
});
AddStep("start drag", () => InputManager.PressButton(MouseButton.Left));
AddStep("move mouse to control point 1", () =>
{
Vector2 position = slider.Position + slider.Path.ControlPoints[1].Position + new Vector2(60, 0);
InputManager.MoveMouseTo(drawableObject.Parent!.ToScreenSpace(position));
});
AddStep("end adjust length", () => InputManager.ReleaseButton(MouseButton.Left));
AddAssert("expected distance halved",
() => Precision.AlmostEquals(slider.Path.Distance, 172.2, 0.1));
AddStep("start adjust length",
() => blueprint.ContextMenuItems.Single(o => o.Text.Value == "Adjust length").Action.Value());
AddStep("move mouse beyond last control point", () =>
AddStep("move mouse to drag marker", () =>
{
Vector2 position = slider.Position + slider.Path.ControlPoints[2].Position + new Vector2(50, 0);
Vector2 position = slider.Position + slider.Path.PositionAt(1) + new Vector2(60, 0);
InputManager.MoveMouseTo(drawableObject.Parent!.ToScreenSpace(position));
});
AddStep("end adjust length", () => InputManager.Click(MouseButton.Right));
AddStep("start drag", () => InputManager.PressButton(MouseButton.Left));
AddStep("move mouse beyond last control point", () =>
{
Vector2 position = slider.Position + slider.Path.ControlPoints[2].Position + new Vector2(100, 0);
InputManager.MoveMouseTo(drawableObject.Parent!.ToScreenSpace(position));
});
AddStep("end adjust length", () => InputManager.ReleaseButton(MouseButton.Left));
AddAssert("expected distance is calculated distance",
() => Precision.AlmostEquals(slider.Path.Distance, slider.Path.CalculatedDistance, 0.1));

View File

@ -1,24 +1,47 @@
// 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.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Lines;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Graphics;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
using osu.Game.Rulesets.Osu.Objects;
using osuTK;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{
public partial class SliderCircleOverlay : CompositeDrawable
{
protected readonly HitCirclePiece CirclePiece;
protected readonly Slider Slider;
public RectangleF VisibleQuad
{
get
{
var result = CirclePiece.ScreenSpaceDrawQuad.AABBFloat;
private readonly HitCircleOverlapMarker marker;
if (endDragMarkerContainer == null) return result;
var size = result.Size * 1.4f;
var location = result.TopLeft - result.Size * 0.2f;
return new RectangleF(location, size);
}
}
protected readonly HitCirclePiece CirclePiece;
private readonly Slider slider;
private readonly SliderPosition position;
private readonly HitCircleOverlapMarker marker;
private readonly Container? endDragMarkerContainer;
public SliderCircleOverlay(Slider slider, SliderPosition position)
{
Slider = slider;
this.slider = slider;
this.position = position;
InternalChildren = new Drawable[]
@ -26,27 +49,121 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
marker = new HitCircleOverlapMarker(),
CirclePiece = new HitCirclePiece(),
};
if (position == SliderPosition.End)
{
AddInternal(endDragMarkerContainer = new Container
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Padding = new MarginPadding(-2.5f),
Child = EndDragMarker = new SliderEndDragMarker()
});
}
}
public SliderEndDragMarker? EndDragMarker { get; }
protected override void Update()
{
base.Update();
var circle = position == SliderPosition.Start ? (HitCircle)Slider.HeadCircle :
Slider.RepeatCount % 2 == 0 ? Slider.TailCircle : Slider.LastRepeat!;
var circle = position == SliderPosition.Start ? (HitCircle)slider.HeadCircle :
slider.RepeatCount % 2 == 0 ? slider.TailCircle : slider.LastRepeat!;
CirclePiece.UpdateFrom(circle);
marker.UpdateFrom(circle);
if (endDragMarkerContainer != null)
{
endDragMarkerContainer.Position = circle.Position;
endDragMarkerContainer.Scale = CirclePiece.Scale * 1.2f;
var diff = slider.Path.PositionAt(1) - slider.Path.PositionAt(0.99f);
endDragMarkerContainer.Rotation = float.RadiansToDegrees(MathF.Atan2(diff.Y, diff.X));
}
}
public override void Hide()
{
CirclePiece.Hide();
endDragMarkerContainer?.Hide();
}
public override void Show()
{
CirclePiece.Show();
endDragMarkerContainer?.Show();
}
public partial class SliderEndDragMarker : SmoothPath
{
public Action<DragStartEvent>? StartDrag { get; set; }
public Action<DragEvent>? Drag { get; set; }
public Action? EndDrag { get; set; }
[Resolved]
private OsuColour colours { get; set; } = null!;
[BackgroundDependencyLoader]
private void load()
{
var path = PathApproximator.CircularArcToPiecewiseLinear([
new Vector2(0, OsuHitObject.OBJECT_RADIUS),
new Vector2(OsuHitObject.OBJECT_RADIUS, 0),
new Vector2(0, -OsuHitObject.OBJECT_RADIUS)
]);
Anchor = Anchor.CentreLeft;
Origin = Anchor.CentreLeft;
PathRadius = 5;
Vertices = path;
}
protected override void LoadComplete()
{
base.LoadComplete();
updateState();
}
protected override bool OnHover(HoverEvent e)
{
updateState();
return true;
}
protected override void OnHoverLost(HoverLostEvent e)
{
updateState();
base.OnHoverLost(e);
}
protected override bool OnDragStart(DragStartEvent e)
{
updateState();
StartDrag?.Invoke(e);
return true;
}
protected override void OnDrag(DragEvent e)
{
updateState();
base.OnDrag(e);
Drag?.Invoke(e);
}
protected override void OnDragEnd(DragEndEvent e)
{
updateState();
EndDrag?.Invoke();
base.OnDragEnd(e);
}
private void updateState()
{
Colour = IsHovered || IsDragged ? colours.Red : colours.Yellow;
}
}
}
}

View File

@ -55,7 +55,18 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
[Resolved(CanBeNull = true)]
private BindableBeatDivisor beatDivisor { get; set; }
public override Quad SelectionQuad => BodyPiece.ScreenSpaceDrawQuad;
public override Quad SelectionQuad
{
get
{
var result = BodyPiece.ScreenSpaceDrawQuad.AABBFloat;
result = RectangleF.Union(result, HeadOverlay.VisibleQuad);
result = RectangleF.Union(result, TailOverlay.VisibleQuad);
return result;
}
}
private readonly BindableList<PathControlPoint> controlPoints = new BindableList<PathControlPoint>();
private readonly IBindable<int> pathVersion = new Bindable<int>();
@ -63,7 +74,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
// Cached slider path which ignored the expected distance value.
private readonly Cached<SliderPath> fullPathCache = new Cached<SliderPath>();
private bool isAdjustingLength;
public SliderSelectionBlueprint(Slider slider)
: base(slider)
@ -79,6 +89,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
HeadOverlay = CreateCircleOverlay(HitObject, SliderPosition.Start),
TailOverlay = CreateCircleOverlay(HitObject, SliderPosition.End),
};
TailOverlay.EndDragMarker!.StartDrag += startAdjustingLength;
TailOverlay.EndDragMarker.Drag += adjustLength;
TailOverlay.EndDragMarker.EndDrag += endAdjustLength;
}
protected override void LoadComplete()
@ -141,9 +155,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{
base.OnDeselected();
if (isAdjustingLength)
endAdjustLength();
updateVisualDefinition();
BodyPiece.RecyclePath();
}
@ -173,12 +184,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
protected override bool OnMouseDown(MouseDownEvent e)
{
if (isAdjustingLength)
{
endAdjustLength();
return true;
}
switch (e.Button)
{
case MouseButton.Right:
@ -202,18 +207,22 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
return false;
}
private Vector2 lengthAdjustMouseOffset;
private void startAdjustingLength(DragStartEvent e)
{
lengthAdjustMouseOffset = ToLocalSpace(e.ScreenSpaceMouseDownPosition) - HitObject.Position - HitObject.Path.PositionAt(1);
changeHandler?.BeginChange();
}
private void endAdjustLength()
{
trimExcessControlPoints(HitObject.Path);
isAdjustingLength = false;
changeHandler?.EndChange();
}
protected override bool OnMouseMove(MouseMoveEvent e)
private void adjustLength(MouseEvent e)
{
if (!isAdjustingLength)
return base.OnMouseMove(e);
double oldDistance = HitObject.Path.Distance;
double proposedDistance = findClosestPathDistance(e);
@ -223,13 +232,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
10 * oldDistance / HitObject.SliderVelocityMultiplier);
if (Precision.AlmostEquals(proposedDistance, oldDistance))
return false;
return;
HitObject.SliderVelocityMultiplier *= proposedDistance / oldDistance;
HitObject.Path.ExpectedDistance.Value = proposedDistance;
editorBeatmap?.Update(HitObject);
return false;
}
/// <summary>
@ -262,12 +269,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
/// <summary>
/// Finds the expected distance value for which the slider end is closest to the mouse position.
/// </summary>
private double findClosestPathDistance(MouseMoveEvent e)
private double findClosestPathDistance(MouseEvent e)
{
const double step1 = 10;
const double step2 = 0.1;
var desiredPosition = e.MousePosition - HitObject.Position;
var desiredPosition = ToLocalSpace(e.ScreenSpaceMousePosition) - HitObject.Position - lengthAdjustMouseOffset;
if (!fullPathCache.IsValid)
fullPathCache.Value = new SliderPath(HitObject.Path.ControlPoints.ToArray());
@ -525,11 +532,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
addControlPoint(rightClickPosition);
changeHandler?.EndChange();
}),
new OsuMenuItem("Adjust length", MenuItemType.Standard, () =>
{
isAdjustingLength = true;
changeHandler?.BeginChange();
}),
new OsuMenuItem("Convert to stream", MenuItemType.Destructive, convertToStream),
};
@ -544,9 +546,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
{
if (isAdjustingLength)
return true;
if (BodyPiece.ReceivePositionalInputAt(screenSpacePos))
return true;