diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index 065d4737a5..ae4141073e 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -283,6 +283,9 @@ private void dragInProgress(DragEvent e) } } + // Snap the path to the current beat divisor before checking length validity. + slider.SnapTo(snapProvider); + if (!slider.Path.HasValidLength) { for (int i = 0; i < slider.Path.ControlPoints.Count; i++) @@ -290,6 +293,8 @@ private void dragInProgress(DragEvent e) slider.Position = oldPosition; slider.StartTime = oldStartTime; + // Snap the path length again to undo the invalid length. + slider.SnapTo(snapProvider); return; } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 2aebe05c2f..6cf2a493a9 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -80,7 +80,7 @@ protected override void LoadComplete() controlPoints.BindTo(HitObject.Path.ControlPoints); pathVersion.BindTo(HitObject.Path.Version); - pathVersion.BindValueChanged(_ => updatePath()); + pathVersion.BindValueChanged(_ => editorBeatmap?.Update(HitObject)); BodyPiece.UpdateFrom(HitObject); } @@ -208,6 +208,8 @@ private PathControlPoint addControlPoint(Vector2 position) // Move the control points from the insertion index onwards to make room for the insertion controlPoints.Insert(insertionIndex, pathControlPoint); + HitObject.SnapTo(composer); + return pathControlPoint; } @@ -227,7 +229,10 @@ private void removeControlPoints(List toRemove) controlPoints.Remove(c); } - // If there are 0 or 1 remaining control points, the slider is in a degenerate (single point) form and should be deleted + // Snap the slider to the current beat divisor before checking length validity. + HitObject.SnapTo(composer); + + // If there are 0 or 1 remaining control points, or the slider has an invalid length, it is in a degenerate form and should be deleted if (controlPoints.Count <= 1 || !HitObject.Path.HasValidLength) { placementHandler?.Delete(HitObject); @@ -242,12 +247,6 @@ private void removeControlPoints(List toRemove) HitObject.Position += first; } - private void updatePath() - { - HitObject.Path.ExpectedDistance.Value = composer?.GetSnappedDistanceFromDistance(HitObject, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance; - editorBeatmap?.Update(HitObject); - } - private void convertToStream() { if (editorBeatmap == null || changeHandler == null || beatDivisor == null) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 071ecf6329..efbac5439c 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -1,12 +1,16 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +#nullable enable + using System.Collections.Generic; using System.Linq; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Primitives; using osu.Framework.Utils; using osu.Game.Extensions; +using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; @@ -18,6 +22,9 @@ namespace osu.Game.Rulesets.Osu.Edit { public class OsuSelectionHandler : EditorSelectionHandler { + [Resolved(CanBeNull = true)] + private IPositionSnapProvider? positionSnapProvider { get; set; } + /// /// During a transform, the initial origin is stored so it can be used throughout the operation. /// @@ -27,7 +34,7 @@ public class OsuSelectionHandler : EditorSelectionHandler /// During a transform, the initial path types of a single selected slider are stored so they /// can be maintained throughout the operation. /// - private List referencePathTypes; + private List? referencePathTypes; protected override void OnSelectionChanged() { @@ -197,6 +204,10 @@ private void scaleSlider(Slider slider, Vector2 scale) for (int i = 0; i < slider.Path.ControlPoints.Count; ++i) slider.Path.ControlPoints[i].Type = referencePathTypes[i]; + // Snap the slider's length to the current beat divisor + // to calculate the final resulting duration / bounding box before the final checks. + slider.SnapTo(positionSnapProvider); + //if sliderhead or sliderend end up outside playfield, revert scaling. Quad scaledQuad = getSurroundingQuad(new OsuHitObject[] { slider }); (bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad); @@ -206,6 +217,9 @@ private void scaleSlider(Slider slider, Vector2 scale) foreach (var point in slider.Path.ControlPoints) point.Position = oldControlPoints.Dequeue(); + + // Snap the slider's length again to undo the potentially-invalid length applied by the previous snap. + slider.SnapTo(positionSnapProvider); } private void scaleHitObjects(OsuHitObject[] hitObjects, Anchor reference, Vector2 scale) diff --git a/osu.Game/Rulesets/Objects/SliderPathExtensions.cs b/osu.Game/Rulesets/Objects/SliderPathExtensions.cs index 1308fff7ae..ba614900c0 100644 --- a/osu.Game/Rulesets/Objects/SliderPathExtensions.cs +++ b/osu.Game/Rulesets/Objects/SliderPathExtensions.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Linq; +using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects.Types; using osuTK; @@ -11,6 +12,15 @@ namespace osu.Game.Rulesets.Objects { public static class SliderPathExtensions { + /// + /// Snaps the provided 's duration using the . + /// + public static void SnapTo(this THitObject hitObject, IPositionSnapProvider? snapProvider) + where THitObject : HitObject, IHasPath + { + hitObject.Path.ExpectedDistance.Value = snapProvider?.GetSnappedDistanceFromDistance(hitObject, (float)hitObject.Path.CalculatedDistance) ?? hitObject.Path.CalculatedDistance; + } + /// /// Reverse the direction of this path. ///