mirror of
https://github.com/ppy/osu.git
synced 2024-12-15 05:02:55 +08:00
Merge pull request #25953 from OliBomby/free-sliders
Add ability to change slider length by dragging slider tail
This commit is contained in:
commit
75f2ad1e51
@ -163,6 +163,44 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
checkControlPointSelected(1, false);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAdjustLength()
|
||||
{
|
||||
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("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 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));
|
||||
|
||||
moveMouseToControlPoint(1);
|
||||
AddAssert("expected distance is unchanged",
|
||||
() => Precision.AlmostEquals(slider.Path.Distance, slider.Path.CalculatedDistance, 0.1));
|
||||
}
|
||||
|
||||
private void moveHitObject()
|
||||
{
|
||||
AddStep("move hitobject", () =>
|
||||
|
@ -1,7 +1,10 @@
|
||||
// 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.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
|
||||
@ -9,11 +12,28 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
{
|
||||
public partial class SliderCircleOverlay : CompositeDrawable
|
||||
{
|
||||
public SliderEndDragMarker? EndDragMarker { get; }
|
||||
|
||||
public RectangleF VisibleQuad
|
||||
{
|
||||
get
|
||||
{
|
||||
var result = CirclePiece.ScreenSpaceDrawQuad.AABBFloat;
|
||||
|
||||
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)
|
||||
{
|
||||
@ -24,26 +44,49 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
AddInternal(marker = new HitCircleOverlapMarker());
|
||||
|
||||
AddInternal(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()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
var circle = position == SliderPosition.Start ? (HitCircle)slider.HeadCircle : slider.TailCircle;
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,84 @@
|
||||
// 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.Lines;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,13 +1,9 @@
|
||||
// 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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input;
|
||||
@ -29,30 +25,29 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
{
|
||||
public new Slider HitObject => (Slider)base.HitObject;
|
||||
|
||||
private SliderBodyPiece bodyPiece;
|
||||
private HitCirclePiece headCirclePiece;
|
||||
private HitCirclePiece tailCirclePiece;
|
||||
private PathControlPointVisualiser<Slider> controlPointVisualiser;
|
||||
private SliderBodyPiece bodyPiece = null!;
|
||||
private HitCirclePiece headCirclePiece = null!;
|
||||
private HitCirclePiece tailCirclePiece = null!;
|
||||
private PathControlPointVisualiser<Slider> controlPointVisualiser = null!;
|
||||
|
||||
private InputManager inputManager;
|
||||
private InputManager inputManager = null!;
|
||||
|
||||
private PathControlPoint? cursor;
|
||||
|
||||
private SliderPlacementState state;
|
||||
private PathControlPoint segmentStart;
|
||||
private PathControlPoint cursor;
|
||||
|
||||
private int currentSegmentLength;
|
||||
private bool usingCustomSegmentType;
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
[CanBeNull]
|
||||
private IPositionSnapProvider positionSnapProvider { get; set; }
|
||||
[Resolved]
|
||||
private IPositionSnapProvider? positionSnapProvider { get; set; }
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
[CanBeNull]
|
||||
private IDistanceSnapProvider distanceSnapProvider { get; set; }
|
||||
[Resolved]
|
||||
private IDistanceSnapProvider? distanceSnapProvider { get; set; }
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
[CanBeNull]
|
||||
private FreehandSliderToolboxGroup freehandToolboxGroup { get; set; }
|
||||
[Resolved]
|
||||
private FreehandSliderToolboxGroup? freehandToolboxGroup { get; set; }
|
||||
|
||||
private readonly IncrementalBSplineBuilder bSplineBuilder = new IncrementalBSplineBuilder { Degree = 4 };
|
||||
|
||||
@ -84,7 +79,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
inputManager = GetContainingInputManager();
|
||||
|
||||
inputManager = GetContainingInputManager()!;
|
||||
|
||||
if (freehandToolboxGroup != null)
|
||||
{
|
||||
@ -108,7 +104,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
}
|
||||
|
||||
[Resolved]
|
||||
private EditorBeatmap editorBeatmap { get; set; }
|
||||
private EditorBeatmap editorBeatmap { get; set; } = null!;
|
||||
|
||||
public override void UpdateTimeAndPosition(SnapResult result)
|
||||
{
|
||||
@ -151,7 +147,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
case SliderPlacementState.ControlPoints:
|
||||
if (canPlaceNewControlPoint(out var lastPoint))
|
||||
placeNewControlPoint();
|
||||
else
|
||||
else if (lastPoint != null)
|
||||
beginNewSegment(lastPoint);
|
||||
|
||||
break;
|
||||
@ -162,9 +158,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
|
||||
private void beginNewSegment(PathControlPoint lastPoint)
|
||||
{
|
||||
// Transform the last point into a new segment.
|
||||
Debug.Assert(lastPoint != null);
|
||||
|
||||
segmentStart = lastPoint;
|
||||
segmentStart.Type = PathType.LINEAR;
|
||||
|
||||
@ -384,7 +377,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
/// </summary>
|
||||
/// <param name="lastPoint">The last-placed control point. May be null, but is not null if <c>false</c> is returned.</param>
|
||||
/// <returns>Whether a new control point can be placed at the current position.</returns>
|
||||
private bool canPlaceNewControlPoint([CanBeNull] out PathControlPoint lastPoint)
|
||||
private bool canPlaceNewControlPoint(out PathControlPoint? lastPoint)
|
||||
{
|
||||
// We cannot rely on the ordering of drawable pieces, so find the respective drawable piece by searching for the last non-cursor control point.
|
||||
var last = HitObject.Path.ControlPoints.LastOrDefault(p => p != cursor);
|
||||
@ -436,7 +429,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
// Replace this segment with a circular arc if it is a reasonable substitute.
|
||||
var circleArcSegment = tryCircleArc(segment);
|
||||
|
||||
if (circleArcSegment is not null)
|
||||
if (circleArcSegment != null)
|
||||
{
|
||||
HitObject.Path.ControlPoints.Add(new PathControlPoint(circleArcSegment[0], PathType.PERFECT_CURVE));
|
||||
HitObject.Path.ControlPoints.Add(new PathControlPoint(circleArcSegment[1]));
|
||||
@ -453,7 +446,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
}
|
||||
}
|
||||
|
||||
private Vector2[] tryCircleArc(List<Vector2> segment)
|
||||
private Vector2[]? tryCircleArc(List<Vector2> segment)
|
||||
{
|
||||
if (segment.Count < 3 || freehandToolboxGroup?.CircleThreshold.Value == 0) return null;
|
||||
|
||||
|
@ -1,13 +1,13 @@
|
||||
// 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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
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;
|
||||
@ -33,27 +33,28 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
{
|
||||
protected new DrawableSlider DrawableObject => (DrawableSlider)base.DrawableObject;
|
||||
|
||||
protected SliderBodyPiece BodyPiece { get; private set; }
|
||||
protected SliderCircleOverlay HeadOverlay { get; private set; }
|
||||
protected SliderCircleOverlay TailOverlay { get; private set; }
|
||||
protected SliderBodyPiece BodyPiece { get; private set; } = null!;
|
||||
protected SliderCircleOverlay HeadOverlay { get; private set; } = null!;
|
||||
protected SliderCircleOverlay TailOverlay { get; private set; } = null!;
|
||||
|
||||
[CanBeNull]
|
||||
protected PathControlPointVisualiser<Slider> ControlPointVisualiser { get; private set; }
|
||||
protected PathControlPointVisualiser<Slider>? ControlPointVisualiser { get; private set; }
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private IDistanceSnapProvider distanceSnapProvider { get; set; }
|
||||
[Resolved]
|
||||
private IDistanceSnapProvider? distanceSnapProvider { get; set; }
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private IPlacementHandler placementHandler { get; set; }
|
||||
[Resolved]
|
||||
private IPlacementHandler? placementHandler { get; set; }
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private EditorBeatmap editorBeatmap { get; set; }
|
||||
[Resolved]
|
||||
private EditorBeatmap? editorBeatmap { get; set; }
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private IEditorChangeHandler changeHandler { get; set; }
|
||||
[Resolved]
|
||||
private IEditorChangeHandler? changeHandler { get; set; }
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private BindableBeatDivisor beatDivisor { get; set; }
|
||||
[Resolved]
|
||||
private BindableBeatDivisor? beatDivisor { get; set; }
|
||||
|
||||
private PathControlPoint? placementControlPoint;
|
||||
|
||||
public override Quad SelectionQuad
|
||||
{
|
||||
@ -61,6 +62,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
{
|
||||
var result = BodyPiece.ScreenSpaceDrawQuad.AABBFloat;
|
||||
|
||||
result = RectangleF.Union(result, HeadOverlay.VisibleQuad);
|
||||
result = RectangleF.Union(result, TailOverlay.VisibleQuad);
|
||||
|
||||
if (ControlPointVisualiser != null)
|
||||
{
|
||||
foreach (var piece in ControlPointVisualiser.Pieces)
|
||||
@ -76,6 +80,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
private readonly BindableList<HitObject> selectedObjects = new BindableList<HitObject>();
|
||||
private readonly Bindable<bool> showHitMarkers = new Bindable<bool>();
|
||||
|
||||
// Cached slider path which ignored the expected distance value.
|
||||
private readonly Cached<SliderPath> fullPathCache = new Cached<SliderPath>();
|
||||
|
||||
private Vector2 lastRightClickPosition;
|
||||
|
||||
public SliderSelectionBlueprint(Slider slider)
|
||||
: base(slider)
|
||||
{
|
||||
@ -91,6 +100,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
TailOverlay = CreateCircleOverlay(HitObject, SliderPosition.End),
|
||||
};
|
||||
|
||||
// tail will always have a non-null end drag marker.
|
||||
Debug.Assert(TailOverlay.EndDragMarker != null);
|
||||
|
||||
TailOverlay.EndDragMarker.StartDrag += startAdjustingLength;
|
||||
TailOverlay.EndDragMarker.Drag += adjustLength;
|
||||
TailOverlay.EndDragMarker.EndDrag += endAdjustLength;
|
||||
|
||||
config.BindWith(OsuSetting.EditorShowHitMarkers, showHitMarkers);
|
||||
}
|
||||
|
||||
@ -99,6 +115,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
base.LoadComplete();
|
||||
|
||||
controlPoints.BindTo(HitObject.Path.ControlPoints);
|
||||
controlPoints.CollectionChanged += (_, _) => fullPathCache.Invalidate();
|
||||
|
||||
pathVersion.BindTo(HitObject.Path.Version);
|
||||
pathVersion.BindValueChanged(_ => editorBeatmap?.Update(HitObject));
|
||||
@ -123,7 +140,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
return false;
|
||||
|
||||
hoveredControlPoint.IsSelected.Value = true;
|
||||
ControlPointVisualiser.DeleteSelected();
|
||||
ControlPointVisualiser?.DeleteSelected();
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -141,7 +158,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
updateVisualDefinition();
|
||||
|
||||
return base.OnHover(e);
|
||||
}
|
||||
|
||||
@ -186,17 +202,16 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
}
|
||||
}
|
||||
|
||||
private Vector2 rightClickPosition;
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e)
|
||||
{
|
||||
switch (e.Button)
|
||||
{
|
||||
case MouseButton.Right:
|
||||
rightClickPosition = e.MouseDownPosition;
|
||||
lastRightClickPosition = e.MouseDownPosition;
|
||||
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)
|
||||
{
|
||||
@ -212,8 +227,134 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
return false;
|
||||
}
|
||||
|
||||
[CanBeNull]
|
||||
private PathControlPoint placementControlPoint;
|
||||
#region Length Adjustment (independent of path nodes)
|
||||
|
||||
private Vector2 lengthAdjustMouseOffset;
|
||||
private double oldDuration;
|
||||
private double oldVelocityMultiplier;
|
||||
private double desiredDistance;
|
||||
private bool isAdjustingLength;
|
||||
private bool adjustVelocityMomentary;
|
||||
|
||||
private void startAdjustingLength(DragStartEvent e)
|
||||
{
|
||||
isAdjustingLength = true;
|
||||
adjustVelocityMomentary = e.ShiftPressed;
|
||||
lengthAdjustMouseOffset = ToLocalSpace(e.ScreenSpaceMouseDownPosition) - HitObject.Position - HitObject.Path.PositionAt(1);
|
||||
oldDuration = HitObject.Path.Distance / HitObject.SliderVelocityMultiplier;
|
||||
oldVelocityMultiplier = HitObject.SliderVelocityMultiplier;
|
||||
changeHandler?.BeginChange();
|
||||
}
|
||||
|
||||
private void endAdjustLength()
|
||||
{
|
||||
trimExcessControlPoints(HitObject.Path);
|
||||
changeHandler?.EndChange();
|
||||
isAdjustingLength = false;
|
||||
}
|
||||
|
||||
private void adjustLength(MouseEvent e) => adjustLength(findClosestPathDistance(e), e.ShiftPressed);
|
||||
|
||||
private void adjustLength(double proposedDistance, bool adjustVelocity)
|
||||
{
|
||||
desiredDistance = proposedDistance;
|
||||
double proposedVelocity = oldVelocityMultiplier;
|
||||
|
||||
if (adjustVelocity)
|
||||
{
|
||||
proposedVelocity = proposedDistance / oldDuration;
|
||||
proposedDistance = MathHelper.Clamp(proposedDistance, 0.1 * oldDuration, 10 * oldDuration);
|
||||
}
|
||||
else
|
||||
{
|
||||
double minDistance = distanceSnapProvider?.GetBeatSnapDistanceAt(HitObject, false) * oldVelocityMultiplier ?? 1;
|
||||
// Add a small amount to the proposed distance to make it easier to snap to the full length of the slider.
|
||||
proposedDistance = distanceSnapProvider?.FindSnappedDistance(HitObject, (float)proposedDistance + 1) ?? proposedDistance;
|
||||
proposedDistance = MathHelper.Clamp(proposedDistance, minDistance, HitObject.Path.CalculatedDistance);
|
||||
}
|
||||
|
||||
if (Precision.AlmostEquals(proposedDistance, HitObject.Path.Distance) && Precision.AlmostEquals(proposedVelocity, HitObject.SliderVelocityMultiplier))
|
||||
return;
|
||||
|
||||
HitObject.SliderVelocityMultiplier = proposedVelocity;
|
||||
HitObject.Path.ExpectedDistance.Value = proposedDistance;
|
||||
editorBeatmap?.Update(HitObject);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Trims control points from the end of the slider path which are not required to reach the expected end of the slider.
|
||||
/// </summary>
|
||||
/// <param name="sliderPath">The slider path to trim control points of.</param>
|
||||
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++;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the expected distance value for which the slider end is closest to the mouse position.
|
||||
/// </summary>
|
||||
private double findClosestPathDistance(MouseEvent e)
|
||||
{
|
||||
const double step1 = 10;
|
||||
const double step2 = 0.1;
|
||||
const double longer_distance_bias = 0.01;
|
||||
|
||||
var desiredPosition = ToLocalSpace(e.ScreenSpaceMousePosition) - HitObject.Position - lengthAdjustMouseOffset;
|
||||
|
||||
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;
|
||||
double dist = Vector2.Distance(fullPathCache.Value.PositionAt(t), desiredPosition) - d * longer_distance_bias;
|
||||
|
||||
if (dist >= minDistance) continue;
|
||||
|
||||
minDistance = dist;
|
||||
bestValue = d;
|
||||
}
|
||||
|
||||
// Do another linear search to fine-tune the result.
|
||||
double maxValue = Math.Min(bestValue + step1, fullPathCache.Value.CalculatedDistance);
|
||||
|
||||
for (double d = bestValue - step1; d <= maxValue; d += step2)
|
||||
{
|
||||
double t = d / fullPathCache.Value.CalculatedDistance;
|
||||
double dist = Vector2.Distance(fullPathCache.Value.PositionAt(t), desiredPosition) - d * longer_distance_bias;
|
||||
|
||||
if (dist >= minDistance) continue;
|
||||
|
||||
minDistance = dist;
|
||||
bestValue = d;
|
||||
}
|
||||
|
||||
return bestValue;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
protected override bool OnDragStart(DragStartEvent e)
|
||||
{
|
||||
@ -255,9 +396,24 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isAdjustingLength && e.ShiftPressed != adjustVelocityMomentary)
|
||||
{
|
||||
adjustVelocityMomentary = e.ShiftPressed;
|
||||
adjustLength(desiredDistance, adjustVelocityMomentary);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override void OnKeyUp(KeyUpEvent e)
|
||||
{
|
||||
if (!IsSelected || !isAdjustingLength || e.ShiftPressed == adjustVelocityMomentary) return;
|
||||
|
||||
adjustVelocityMomentary = e.ShiftPressed;
|
||||
adjustLength(desiredDistance, adjustVelocityMomentary);
|
||||
}
|
||||
|
||||
private PathControlPoint addControlPoint(Vector2 position)
|
||||
{
|
||||
position -= HitObject.Position;
|
||||
@ -326,6 +482,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
|
||||
private void splitControlPoints(List<PathControlPoint> controlPointsToSplitAt)
|
||||
{
|
||||
if (editorBeatmap == null)
|
||||
return;
|
||||
|
||||
// Arbitrary gap in milliseconds to put between split slider pieces
|
||||
const double split_gap = 100;
|
||||
|
||||
@ -432,7 +591,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
new OsuMenuItem("Add control point", MenuItemType.Standard, () =>
|
||||
{
|
||||
changeHandler?.BeginChange();
|
||||
addControlPoint(rightClickPosition);
|
||||
addControlPoint(lastRightClickPosition);
|
||||
changeHandler?.EndChange();
|
||||
}),
|
||||
new OsuMenuItem("Convert to stream", MenuItemType.Destructive, convertToStream),
|
||||
|
@ -9,6 +9,7 @@ using System.Collections.Generic;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using JetBrains.Annotations;
|
||||
using Newtonsoft.Json;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Caching;
|
||||
@ -162,6 +163,10 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
[JsonIgnore]
|
||||
public SliderTailCircle TailCircle { get; protected set; }
|
||||
|
||||
[JsonIgnore]
|
||||
[CanBeNull]
|
||||
public SliderRepeat LastRepeat { get; protected set; }
|
||||
|
||||
public Slider()
|
||||
{
|
||||
SamplesBindable.CollectionChanged += (_, _) => UpdateNestedSamples();
|
||||
@ -225,7 +230,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
break;
|
||||
|
||||
case SliderEventType.Repeat:
|
||||
AddNested(new SliderRepeat(this)
|
||||
AddNested(LastRepeat = new SliderRepeat(this)
|
||||
{
|
||||
RepeatIndex = e.SpanIndex,
|
||||
StartTime = StartTime + (e.SpanIndex + 1) * SpanDuration,
|
||||
@ -248,6 +253,9 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
|
||||
if (TailCircle != null)
|
||||
TailCircle.Position = EndPosition;
|
||||
|
||||
if (LastRepeat != null)
|
||||
LastRepeat.Position = RepeatCount % 2 == 0 ? Position : Position + Path.PositionAt(1);
|
||||
}
|
||||
|
||||
protected void UpdateNestedSamples()
|
||||
|
Loading…
Reference in New Issue
Block a user