From b24bfa290806117753dc1e7969ddf3966d09094e Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 20 Jun 2024 00:02:43 +0200 Subject: [PATCH] click to choose length instead of drag --- .../Sliders/Components/SliderTailPiece.cs | 185 ------------------ .../Sliders/SliderSelectionBlueprint.cs | 129 +++++++++++- 2 files changed, 124 insertions(+), 190 deletions(-) delete mode 100644 osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderTailPiece.cs diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderTailPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderTailPiece.cs deleted file mode 100644 index 2ebdf87606..0000000000 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderTailPiece.cs +++ /dev/null @@ -1,185 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Linq; -using osu.Framework.Allocation; -using osu.Framework.Caching; -using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Input; -using osu.Framework.Input.Events; -using osu.Framework.Utils; -using osu.Game.Graphics; -using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Screens.Edit; -using osuTK; -using osuTK.Graphics; -using osuTK.Input; - -namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components -{ - public partial class SliderTailPiece : SliderCircleOverlay - { - /// - /// Whether this slider tail is draggable, changing the distance of the slider. - /// - public bool IsDraggable { get; set; } - - /// - /// Whether this is currently being dragged. - /// - private bool isDragging; - - private InputManager inputManager = null!; - - private readonly Cached fullPathCache = new Cached(); - - [Resolved] - private EditorBeatmap? editorBeatmap { get; set; } - - [Resolved] - private OsuColour colours { get; set; } = null!; - - public SliderTailPiece(Slider slider, SliderPosition position) - : base(slider, position) - { - Slider.Path.ControlPoints.CollectionChanged += (_, _) => fullPathCache.Invalidate(); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - inputManager = GetContainingInputManager(); - } - - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => CirclePiece.ReceivePositionalInputAt(screenSpacePos); - - protected override void Update() - { - updateCirclePieceColour(); - base.Update(); - } - - private void updateCirclePieceColour() - { - Color4 colour = colours.Yellow; - - if (IsHovered && IsDraggable - && !inputManager.HoveredDrawables.Any(o => o is PathControlPointPiece)) - colour = colour.Lighten(1); - - CirclePiece.Colour = colour; - } - - protected override bool OnDragStart(DragStartEvent e) - { - if (e.Button == MouseButton.Right || !IsDraggable) - return false; - - isDragging = true; - editorBeatmap?.BeginChange(); - - return true; - } - - protected override void OnDrag(DragEvent e) - { - double oldDistance = Slider.Path.Distance; - double proposedDistance = findClosestPathDistance(e); - - proposedDistance = MathHelper.Clamp(proposedDistance, 0, Slider.Path.CalculatedDistance); - proposedDistance = MathHelper.Clamp(proposedDistance, - 0.1 * oldDistance / Slider.SliderVelocityMultiplier, - 10 * oldDistance / Slider.SliderVelocityMultiplier); - - if (Precision.AlmostEquals(proposedDistance, oldDistance)) - return; - - Slider.SliderVelocityMultiplier *= proposedDistance / oldDistance; - Slider.Path.ExpectedDistance.Value = proposedDistance; - editorBeatmap?.Update(Slider); - } - - protected override void OnDragEnd(DragEndEvent e) - { - if (!isDragging) return; - - trimExcessControlPoints(Slider.Path); - - isDragging = false; - IsDraggable = false; - editorBeatmap?.EndChange(); - } - - /// - /// Trims control points from the end of the slider path which are not required to reach the expected end of the slider. - /// - /// The slider path to trim control points of. - private void trimExcessControlPoints(SliderPath sliderPath) - { - if (!sliderPath.ExpectedDistance.Value.HasValue) - return; - - double[] segmentEnds = sliderPath.GetSegmentEnds().ToArray(); - int segmentIndex = 0; - - for (int i = 1; i < sliderPath.ControlPoints.Count - 1; i++) - { - if (!sliderPath.ControlPoints[i].Type.HasValue) continue; - - if (Precision.AlmostBigger(segmentEnds[segmentIndex], 1, 1E-3)) - { - sliderPath.ControlPoints.RemoveRange(i + 1, sliderPath.ControlPoints.Count - i - 1); - sliderPath.ControlPoints[^1].Type = null; - break; - } - - segmentIndex++; - } - } - - /// - /// Finds the expected distance value for which the slider end is closest to the mouse position. - /// - private double findClosestPathDistance(DragEvent e) - { - const double step1 = 10; - const double step2 = 0.1; - - var desiredPosition = e.MousePosition - Slider.Position; - - if (!fullPathCache.IsValid) - fullPathCache.Value = new SliderPath(Slider.Path.ControlPoints.ToArray()); - - // Do a linear search to find the closest point on the path to the mouse position. - double bestValue = 0; - double minDistance = double.MaxValue; - - for (double d = 0; d <= fullPathCache.Value.CalculatedDistance; d += step1) - { - double t = d / fullPathCache.Value.CalculatedDistance; - float dist = Vector2.Distance(fullPathCache.Value.PositionAt(t), desiredPosition); - - if (dist >= minDistance) continue; - - minDistance = dist; - bestValue = d; - } - - // Do another linear search to fine-tune the result. - for (double d = bestValue - step1; d <= bestValue + step1; d += step2) - { - double t = d / fullPathCache.Value.CalculatedDistance; - float dist = Vector2.Distance(fullPathCache.Value.PositionAt(t), desiredPosition); - - if (dist >= minDistance) continue; - - minDistance = dist; - bestValue = d; - } - - return bestValue; - } - } -} diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 4a949f5b48..f59ef298a7 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -8,6 +8,7 @@ using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Caching; using osu.Framework.Graphics; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.UserInterface; @@ -34,7 +35,7 @@ public partial class SliderSelectionBlueprint : OsuSelectionBlueprint protected SliderBodyPiece BodyPiece { get; private set; } protected SliderCircleOverlay HeadOverlay { get; private set; } - protected SliderTailPiece TailPiece { get; private set; } + protected SliderCircleOverlay TailPiece { get; private set; } [CanBeNull] protected PathControlPointVisualiser ControlPointVisualiser { get; private set; } @@ -60,6 +61,10 @@ public partial class SliderSelectionBlueprint : OsuSelectionBlueprint private readonly IBindable pathVersion = new Bindable(); private readonly BindableList selectedObjects = new BindableList(); + // Cached slider path which ignored the expected distance value. + private readonly Cached fullPathCache = new Cached(); + private bool isAdjustingLength; + public SliderSelectionBlueprint(Slider slider) : base(slider) { @@ -72,7 +77,7 @@ private void load() { BodyPiece = new SliderBodyPiece(), HeadOverlay = CreateCircleOverlay(HitObject, SliderPosition.Start), - TailPiece = CreateTailPiece(HitObject, SliderPosition.End), + TailPiece = CreateCircleOverlay(HitObject, SliderPosition.End), }; } @@ -81,6 +86,7 @@ protected override void LoadComplete() base.LoadComplete(); controlPoints.BindTo(HitObject.Path.ControlPoints); + controlPoints.CollectionChanged += (_, _) => fullPathCache.Invalidate(); pathVersion.BindTo(HitObject.Path.Version); pathVersion.BindValueChanged(_ => editorBeatmap?.Update(HitObject)); @@ -135,6 +141,9 @@ protected override void OnDeselected() { base.OnDeselected(); + if (isAdjustingLength) + endAdjustLength(); + updateVisualDefinition(); BodyPiece.RecyclePath(); } @@ -164,6 +173,12 @@ private void updateVisualDefinition() protected override bool OnMouseDown(MouseDownEvent e) { + if (isAdjustingLength) + { + endAdjustLength(); + return true; + } + switch (e.Button) { case MouseButton.Right: @@ -171,6 +186,7 @@ protected override bool OnMouseDown(MouseDownEvent e) return false; // Allow right click to be handled by context menu case MouseButton.Left: + // If there's more than two objects selected, ctrl+click should deselect if (e.ControlPressed && IsSelected && selectedObjects.Count < 2) { @@ -186,6 +202,106 @@ protected override bool OnMouseDown(MouseDownEvent e) return false; } + private void endAdjustLength() + { + trimExcessControlPoints(HitObject.Path); + isAdjustingLength = false; + changeHandler?.EndChange(); + } + + protected override bool OnMouseMove(MouseMoveEvent e) + { + if (!isAdjustingLength) + return base.OnMouseMove(e); + + double oldDistance = HitObject.Path.Distance; + double proposedDistance = findClosestPathDistance(e); + + proposedDistance = MathHelper.Clamp(proposedDistance, 0, HitObject.Path.CalculatedDistance); + proposedDistance = MathHelper.Clamp(proposedDistance, + 0.1 * oldDistance / HitObject.SliderVelocityMultiplier, + 10 * oldDistance / HitObject.SliderVelocityMultiplier); + + if (Precision.AlmostEquals(proposedDistance, oldDistance)) + return false; + + HitObject.SliderVelocityMultiplier *= proposedDistance / oldDistance; + HitObject.Path.ExpectedDistance.Value = proposedDistance; + editorBeatmap?.Update(HitObject); + + return false; + } + + /// + /// Trims control points from the end of the slider path which are not required to reach the expected end of the slider. + /// + /// The slider path to trim control points of. + private void trimExcessControlPoints(SliderPath sliderPath) + { + if (!sliderPath.ExpectedDistance.Value.HasValue) + return; + + double[] segmentEnds = sliderPath.GetSegmentEnds().ToArray(); + int segmentIndex = 0; + + for (int i = 1; i < sliderPath.ControlPoints.Count - 1; i++) + { + if (!sliderPath.ControlPoints[i].Type.HasValue) continue; + + if (Precision.AlmostBigger(segmentEnds[segmentIndex], 1, 1E-3)) + { + sliderPath.ControlPoints.RemoveRange(i + 1, sliderPath.ControlPoints.Count - i - 1); + sliderPath.ControlPoints[^1].Type = null; + break; + } + + segmentIndex++; + } + } + + /// + /// Finds the expected distance value for which the slider end is closest to the mouse position. + /// + private double findClosestPathDistance(MouseMoveEvent e) + { + const double step1 = 10; + const double step2 = 0.1; + + var desiredPosition = e.MousePosition - HitObject.Position; + + if (!fullPathCache.IsValid) + fullPathCache.Value = new SliderPath(HitObject.Path.ControlPoints.ToArray()); + + // Do a linear search to find the closest point on the path to the mouse position. + double bestValue = 0; + double minDistance = double.MaxValue; + + for (double d = 0; d <= fullPathCache.Value.CalculatedDistance; d += step1) + { + double t = d / fullPathCache.Value.CalculatedDistance; + float dist = Vector2.Distance(fullPathCache.Value.PositionAt(t), desiredPosition); + + if (dist >= minDistance) continue; + + minDistance = dist; + bestValue = d; + } + + // Do another linear search to fine-tune the result. + for (double d = bestValue - step1; d <= bestValue + step1; d += step2) + { + double t = d / fullPathCache.Value.CalculatedDistance; + float dist = Vector2.Distance(fullPathCache.Value.PositionAt(t), desiredPosition); + + if (dist >= minDistance) continue; + + minDistance = dist; + bestValue = d; + } + + return bestValue; + } + [CanBeNull] private PathControlPoint placementControlPoint; @@ -409,9 +525,10 @@ private void convertToStream() addControlPoint(rightClickPosition); changeHandler?.EndChange(); }), - new OsuMenuItem("Adjust distance", MenuItemType.Standard, () => + new OsuMenuItem("Adjust length", MenuItemType.Standard, () => { - TailPiece.IsDraggable = true; + isAdjustingLength = true; + changeHandler?.BeginChange(); }), new OsuMenuItem("Convert to stream", MenuItemType.Destructive, convertToStream), }; @@ -427,6 +544,9 @@ private void convertToStream() public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) { + if (isAdjustingLength) + return true; + if (BodyPiece.ReceivePositionalInputAt(screenSpacePos)) return true; @@ -443,6 +563,5 @@ public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) } protected virtual SliderCircleOverlay CreateCircleOverlay(Slider slider, SliderPosition position) => new SliderCircleOverlay(slider, position); - protected virtual SliderTailPiece CreateTailPiece(Slider slider, SliderPosition position) => new SliderTailPiece(slider, position); } }