1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-22 23:22:54 +08:00

Attempt to convert slider editing to command pattern

This commit is contained in:
Marvin Schürz 2024-10-09 21:20:07 +02:00
parent 307d52549e
commit 39dc35712c
18 changed files with 543 additions and 61 deletions

View File

@ -24,6 +24,7 @@ using osu.Game.Graphics.UserInterface;
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;
using osu.Game.Rulesets.Osu.Edit.Commands;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Commands; using osu.Game.Screens.Edit.Commands;
@ -43,6 +44,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
private readonly T hitObject; private readonly T hitObject;
private readonly bool allowSelection; private readonly bool allowSelection;
private SliderPathCommandProxy pathProxy = null;
private InputManager inputManager; private InputManager inputManager;
public Action<List<PathControlPoint>> RemoveControlPointsRequested; public Action<List<PathControlPoint>> RemoveControlPointsRequested;
@ -114,7 +117,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
return; return;
if (segment.Count > 3) if (segment.Count > 3)
first.Type = PathType.BEZIER; commandHandler.SafeSubmit(new UpdateControlPointCommand(first) { Type = PathType.BEZIER });
if (segment.Count != 3) if (segment.Count != 3)
return; return;
@ -122,7 +125,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
ReadOnlySpan<Vector2> points = segment.Select(p => p.Position).ToArray(); ReadOnlySpan<Vector2> points = segment.Select(p => p.Position).ToArray();
RectangleF boundingBox = PathApproximator.CircularArcBoundingBox(points); RectangleF boundingBox = PathApproximator.CircularArcBoundingBox(points);
if (boundingBox.Width >= 640 || boundingBox.Height >= 480) if (boundingBox.Width >= 640 || boundingBox.Height >= 480)
first.Type = PathType.BEZIER; commandHandler.SafeSubmit(new UpdateControlPointCommand(first) { Type = PathType.BEZIER });
} }
/// <summary> /// <summary>
@ -147,9 +150,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
if (toRemove.Count == 0) if (toRemove.Count == 0)
return false; return false;
changeHandler?.BeginChange();
RemoveControlPointsRequested?.Invoke(toRemove); RemoveControlPointsRequested?.Invoke(toRemove);
changeHandler?.EndChange(); commandHandler?.Commit();
// Since pieces are re-used, they will not point to the deleted control points while remaining selected // Since pieces are re-used, they will not point to the deleted control points while remaining selected
foreach (var piece in Pieces) foreach (var piece in Pieces)
@ -166,9 +168,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
if (controlPointsToSplitAt.Count == 0) if (controlPointsToSplitAt.Count == 0)
return false; return false;
changeHandler?.BeginChange();
SplitControlPointsRequested?.Invoke(controlPointsToSplitAt); SplitControlPointsRequested?.Invoke(controlPointsToSplitAt);
changeHandler?.EndChange(); commandHandler?.Commit();
// Since pieces are re-used, they will not point to the deleted control points while remaining selected // Since pieces are re-used, they will not point to the deleted control points while remaining selected
foreach (var piece in Pieces) foreach (var piece in Pieces)
@ -287,8 +288,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
if (currentTypeIndex < 0 && e.ShiftPressed) if (currentTypeIndex < 0 && e.ShiftPressed)
currentTypeIndex = 0; currentTypeIndex = 0;
changeHandler?.BeginChange();
do do
{ {
currentTypeIndex = (validTypes.Length + currentTypeIndex + (e.ShiftPressed ? -1 : 1)) % validTypes.Length; currentTypeIndex = (validTypes.Length + currentTypeIndex + (e.ShiftPressed ? -1 : 1)) % validTypes.Length;
@ -296,7 +295,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
updatePathTypeOfSelectedPieces(validTypes[currentTypeIndex]); updatePathTypeOfSelectedPieces(validTypes[currentTypeIndex]);
} while (selectedPoint.Type != validTypes[currentTypeIndex]); } while (selectedPoint.Type != validTypes[currentTypeIndex]);
changeHandler?.EndChange(); commandHandler?.Commit();
return true; return true;
} }
@ -352,8 +351,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
/// <param name="type">The path type we want to assign to the given control point piece.</param> /// <param name="type">The path type we want to assign to the given control point piece.</param>
private void updatePathTypeOfSelectedPieces(PathType? type) private void updatePathTypeOfSelectedPieces(PathType? type)
{ {
changeHandler?.BeginChange();
double originalDistance = hitObject.Path.Distance; double originalDistance = hitObject.Path.Distance;
foreach (var p in Pieces.Where(p => p.IsSelected.Value)) foreach (var p in Pieces.Where(p => p.IsSelected.Value))
@ -383,12 +380,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
else else
hitObject.Path.ExpectedDistance.Value = originalDistance; hitObject.Path.ExpectedDistance.Value = originalDistance;
changeHandler?.EndChange(); commandHandler?.Commit();
} }
[Resolved(CanBeNull = true)]
private IEditorChangeHandler changeHandler { get; set; }
#region Drag handling #region Drag handling
private Vector2[] dragStartPositions; private Vector2[] dragStartPositions;
@ -407,7 +401,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
Debug.Assert(draggedControlPointIndex >= 0); Debug.Assert(draggedControlPointIndex >= 0);
changeHandler?.BeginChange(); commandHandler?.Commit();
} }
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
@ -438,7 +432,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
// All other selected control points (if any) will move together with the head point // All other selected control points (if any) will move together with the head point
// (and so they will not move at all, relative to each other). // (and so they will not move at all, relative to each other).
if (!selectedControlPoints.Contains(controlPoint)) if (!selectedControlPoints.Contains(controlPoint))
controlPoint.Position -= movementDelta; commandHandler.SafeSubmit(new UpdateControlPointCommand(controlPoint) { Position = controlPoint.Position - movementDelta });
} }
} }
else else
@ -451,33 +445,33 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{ {
PathControlPoint controlPoint = controlPoints[i]; PathControlPoint controlPoint = controlPoints[i];
if (selectedControlPoints.Contains(controlPoint)) if (selectedControlPoints.Contains(controlPoint))
controlPoint.Position = dragStartPositions[i] + movementDelta; commandHandler.SafeSubmit(new UpdateControlPointCommand(controlPoint) { Position = dragStartPositions[i] + movementDelta });
} }
} }
// Snap the path to the current beat divisor before checking length validity. // Snap the path to the current beat divisor before checking length validity.
hitObject.SnapTo(distanceSnapProvider); hitObject.SnapTo(distanceSnapProvider, commandHandler);
if (!hitObject.Path.HasValidLength) if (!hitObject.Path.HasValidLength)
{ {
for (int i = 0; i < hitObject.Path.ControlPoints.Count; i++) for (int i = 0; i < hitObject.Path.ControlPoints.Count; i++)
hitObject.Path.ControlPoints[i].Position = oldControlPoints[i]; commandHandler.SafeSubmit(new UpdateControlPointCommand(hitObject.Path.ControlPoints[i]) { Position = oldControlPoints[i] });
commandHandler.SafeSubmit(new MoveCommand(hitObject, oldPosition)); commandHandler.SafeSubmit(new MoveCommand(hitObject, oldPosition));
commandHandler.SafeSubmit(new SetStartTimeCommand(hitObject, oldStartTime)); commandHandler.SafeSubmit(new SetStartTimeCommand(hitObject, oldStartTime));
// Snap the path length again to undo the invalid length. // Snap the path length again to undo the invalid length.
hitObject.SnapTo(distanceSnapProvider); hitObject.SnapTo(distanceSnapProvider, commandHandler);
return; return;
} }
// Maintain the path types in case they got defaulted to bezier at some point during the drag. // Maintain the path types in case they got defaulted to bezier at some point during the drag.
for (int i = 0; i < hitObject.Path.ControlPoints.Count; i++) for (int i = 0; i < hitObject.Path.ControlPoints.Count; i++)
hitObject.Path.ControlPoints[i].Type = dragPathTypes[i]; commandHandler.SafeSubmit(new UpdateControlPointCommand(hitObject.Path.ControlPoints[i]) { Type = dragPathTypes[i] });
EnsureValidPathTypes(); EnsureValidPathTypes();
} }
public void DragEnded() => changeHandler?.EndChange(); public void DragEnded() => commandHandler?.Commit();
#endregion #endregion

View File

@ -21,9 +21,11 @@ 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;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components; using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
using osu.Game.Rulesets.Osu.Edit.Commands;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Commands;
using osu.Game.Screens.Edit.Compose; using osu.Game.Screens.Edit.Compose;
using osuTK; using osuTK;
using osuTK.Input; using osuTK.Input;
@ -34,6 +36,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{ {
protected new DrawableSlider DrawableObject => (DrawableSlider)base.DrawableObject; protected new DrawableSlider DrawableObject => (DrawableSlider)base.DrawableObject;
protected SliderCommandProxy Proxy;
protected SliderBodyPiece BodyPiece { get; private set; } = null!; protected SliderBodyPiece BodyPiece { get; private set; } = null!;
protected SliderCircleOverlay HeadOverlay { get; private set; } = null!; protected SliderCircleOverlay HeadOverlay { get; private set; } = null!;
protected SliderCircleOverlay TailOverlay { get; private set; } = null!; protected SliderCircleOverlay TailOverlay { get; private set; } = null!;
@ -50,7 +54,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private EditorBeatmap? editorBeatmap { get; set; } private EditorBeatmap? editorBeatmap { get; set; }
[Resolved] [Resolved]
private IEditorChangeHandler? changeHandler { get; set; } private EditorCommandHandler? commandHandler { get; set; }
[Resolved] [Resolved]
private BindableBeatDivisor? beatDivisor { get; set; } private BindableBeatDivisor? beatDivisor { get; set; }
@ -94,6 +98,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuConfigManager config) private void load(OsuConfigManager config)
{ {
Proxy = new SliderCommandProxy(commandHandler, HitObject);
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
BodyPiece = new SliderBodyPiece(), BodyPiece = new SliderBodyPiece(),
@ -216,7 +222,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
// If there's more than two objects selected, ctrl+click should deselect // If there's more than two objects selected, ctrl+click should deselect
if (e.ControlPressed && IsSelected && selectedObjects.Count < 2) if (e.ControlPressed && IsSelected && selectedObjects.Count < 2)
{ {
changeHandler?.BeginChange();
placementControlPoint = addControlPoint(e.MousePosition); placementControlPoint = addControlPoint(e.MousePosition);
ControlPointVisualiser?.SetSelectionTo(placementControlPoint); ControlPointVisualiser?.SetSelectionTo(placementControlPoint);
return true; // Stop input from being handled and modifying the selection return true; // Stop input from being handled and modifying the selection
@ -244,13 +249,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
lengthAdjustMouseOffset = ToLocalSpace(e.ScreenSpaceMouseDownPosition) - HitObject.Position - HitObject.Path.PositionAt(1); lengthAdjustMouseOffset = ToLocalSpace(e.ScreenSpaceMouseDownPosition) - HitObject.Position - HitObject.Path.PositionAt(1);
oldDuration = HitObject.Path.Distance / HitObject.SliderVelocityMultiplier; oldDuration = HitObject.Path.Distance / HitObject.SliderVelocityMultiplier;
oldVelocityMultiplier = HitObject.SliderVelocityMultiplier; oldVelocityMultiplier = HitObject.SliderVelocityMultiplier;
changeHandler?.BeginChange();
} }
private void endAdjustLength() private void endAdjustLength()
{ {
trimExcessControlPoints(HitObject.Path); trimExcessControlPoints(Proxy.Path);
changeHandler?.EndChange(); commandHandler?.Commit();
isAdjustingLength = false; isAdjustingLength = false;
} }
@ -277,8 +281,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
if (Precision.AlmostEquals(proposedDistance, HitObject.Path.Distance) && Precision.AlmostEquals(proposedVelocity, HitObject.SliderVelocityMultiplier)) if (Precision.AlmostEquals(proposedDistance, HitObject.Path.Distance) && Precision.AlmostEquals(proposedVelocity, HitObject.SliderVelocityMultiplier))
return; return;
HitObject.SliderVelocityMultiplier = proposedVelocity; Proxy.SliderVelocityMultiplier = proposedVelocity;
HitObject.Path.ExpectedDistance.Value = proposedDistance; Proxy.Path.ExpectedDistance = proposedDistance;
editorBeatmap?.Update(HitObject); editorBeatmap?.Update(HitObject);
} }
@ -286,9 +290,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
/// Trims control points from the end of the slider path which are not required to reach the expected end of the slider. /// Trims control points from the end of the slider path which are not required to reach the expected end of the slider.
/// </summary> /// </summary>
/// <param name="sliderPath">The slider path to trim control points of.</param> /// <param name="sliderPath">The slider path to trim control points of.</param>
private void trimExcessControlPoints(SliderPath sliderPath) private void trimExcessControlPoints(SliderPathCommandProxy sliderPath)
{ {
if (!sliderPath.ExpectedDistance.Value.HasValue) if (!sliderPath.ExpectedDistance.HasValue)
return; return;
double[] segmentEnds = sliderPath.GetSegmentEnds().ToArray(); double[] segmentEnds = sliderPath.GetSegmentEnds().ToArray();
@ -382,7 +386,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
ControlPointVisualiser?.DragEnded(); ControlPointVisualiser?.DragEnded();
placementControlPoint = null; placementControlPoint = null;
changeHandler?.EndChange(); commandHandler?.Commit();
} }
} }
@ -435,12 +439,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
var pathControlPoint = new PathControlPoint { Position = position }; var pathControlPoint = new PathControlPoint { Position = position };
// Move the control points from the insertion index onwards to make room for the insertion Proxy.Path.ControlPoints.Insert(insertionIndex, pathControlPoint);
controlPoints.Insert(insertionIndex, pathControlPoint);
ControlPointVisualiser?.EnsureValidPathTypes(); ControlPointVisualiser?.EnsureValidPathTypes();
HitObject.SnapTo(distanceSnapProvider); HitObject.SnapTo(distanceSnapProvider, commandHandler);
return pathControlPoint; return pathControlPoint;
} }
@ -456,15 +459,15 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
// The first control point in the slider must have a type, so take it from the previous "first" one // The first control point in the slider must have a type, so take it from the previous "first" one
// Todo: Should be handled within SliderPath itself // Todo: Should be handled within SliderPath itself
if (c == controlPoints[0] && controlPoints.Count > 1 && controlPoints[1].Type == null) if (c == controlPoints[0] && controlPoints.Count > 1 && controlPoints[1].Type == null)
controlPoints[1].Type = controlPoints[0].Type; new PathControlPointCommandProxy(commandHandler, c).Type = controlPoints[0].Type;
controlPoints.Remove(c); Proxy.Path.ControlPoints.Remove(c);
} }
ControlPointVisualiser?.EnsureValidPathTypes(); ControlPointVisualiser?.EnsureValidPathTypes();
// Snap the slider to the current beat divisor before checking length validity. // Snap the slider to the current beat divisor before checking length validity.
HitObject.SnapTo(distanceSnapProvider); HitObject.SnapTo(distanceSnapProvider, commandHandler);
// 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 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) if (controlPoints.Count <= 1 || !HitObject.Path.HasValidLength)
@ -477,8 +480,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
// So the slider needs to be offset by this amount instead, and all control points offset backwards such that the path is re-positioned at (0, 0) // So the slider needs to be offset by this amount instead, and all control points offset backwards such that the path is re-positioned at (0, 0)
Vector2 first = controlPoints[0].Position; Vector2 first = controlPoints[0].Position;
foreach (var c in controlPoints) foreach (var c in controlPoints)
c.Position -= first; commandHandler.SafeSubmit(new UpdateControlPointCommand(c) { Position = c.Position - first });
HitObject.Position += first;
commandHandler.SafeSubmit(new MoveCommand(HitObject, HitObject.Position + first));
} }
private void splitControlPoints(List<PathControlPoint> controlPointsToSplitAt) private void splitControlPoints(List<PathControlPoint> controlPointsToSplitAt)
@ -495,6 +499,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
editorBeatmap.SelectedHitObjects.Clear(); editorBeatmap.SelectedHitObjects.Clear();
var controlPointsProxy = new PathControlPointsCommandProxy(commandHandler, controlPoints);
foreach (var splitPoint in controlPointsToSplitAt) foreach (var splitPoint in controlPointsToSplitAt)
{ {
if (splitPoint == controlPoints[0] || splitPoint == controlPoints[^1] || splitPoint.Type == null) if (splitPoint == controlPoints[0] || splitPoint == controlPoints[^1] || splitPoint.Type == null)
@ -508,7 +514,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
// Extract the split portion and remove from the original slider. // Extract the split portion and remove from the original slider.
var splitControlPoints = controlPoints.Take(index + 1).ToList(); var splitControlPoints = controlPoints.Take(index + 1).ToList();
controlPoints.RemoveRange(0, index); controlPointsProxy.RemoveRange(0, index);
var newSlider = new Slider var newSlider = new Slider
{ {
@ -521,28 +527,29 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
Path = new SliderPath(splitControlPoints.Select(o => new PathControlPoint(o.Position - splitControlPoints[0].Position, o == splitControlPoints[^1] ? null : o.Type)).ToArray()) Path = new SliderPath(splitControlPoints.Select(o => new PathControlPoint(o.Position - splitControlPoints[0].Position, o == splitControlPoints[^1] ? null : o.Type)).ToArray())
}; };
Proxy.StartTime += split_gap;
// Increase the start time of the slider before adding the new slider so the new slider is immediately inserted at the correct index and internal state remains valid. // Increase the start time of the slider before adding the new slider so the new slider is immediately inserted at the correct index and internal state remains valid.
HitObject.StartTime += split_gap; commandHandler.SafeSubmit(new AddHitObjectCommand(editorBeatmap, newSlider));
editorBeatmap.Add(newSlider); Proxy.NewCombo = false;
Proxy.Path.ExpectedDistance -= newSlider.Path.CalculatedDistance;
HitObject.NewCombo = false; Proxy.StartTime += newSlider.SpanDuration;
HitObject.Path.ExpectedDistance.Value -= newSlider.Path.CalculatedDistance;
HitObject.StartTime += newSlider.SpanDuration;
// In case the remainder of the slider has no length left over, give it length anyways so we don't get a 0 length slider. // In case the remainder of the slider has no length left over, give it length anyways so we don't get a 0 length slider.
if (HitObject.Path.ExpectedDistance.Value <= Precision.DOUBLE_EPSILON) if (HitObject.Path.ExpectedDistance.Value <= Precision.DOUBLE_EPSILON)
{ {
HitObject.Path.ExpectedDistance.Value = null; Proxy.Path.ExpectedDistance = null;
} }
} }
// Once all required pieces have been split off, the original slider has the final split. // Once all required pieces have been split off, the original slider has the final split.
// As a final step, we must reset its control points to have an origin of (0,0). // As a final step, we must reset its control points to have an origin of (0,0).
Vector2 first = controlPoints[0].Position; Vector2 first = controlPoints[0].Position;
foreach (var c in controlPoints) foreach (var c in controlPointsProxy)
c.Position -= first; c.Position -= first;
HitObject.Position += first;
Proxy.Position += first;
} }
private void convertToStream() private void convertToStream()
@ -553,8 +560,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
var timingPoint = editorBeatmap.ControlPointInfo.TimingPointAt(HitObject.StartTime); var timingPoint = editorBeatmap.ControlPointInfo.TimingPointAt(HitObject.StartTime);
double streamSpacing = timingPoint.BeatLength / beatDivisor.Value; double streamSpacing = timingPoint.BeatLength / beatDivisor.Value;
changeHandler?.BeginChange();
int i = 0; int i = 0;
double time = HitObject.StartTime; double time = HitObject.StartTime;
@ -570,30 +575,29 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
Vector2 position = HitObject.Position + HitObject.Path.PositionAt(pathPosition); Vector2 position = HitObject.Position + HitObject.Path.PositionAt(pathPosition);
editorBeatmap.Add(new HitCircle commandHandler.SafeSubmit(new AddHitObjectCommand(editorBeatmap, new HitCircle
{ {
StartTime = time, StartTime = time,
Position = position, Position = position,
NewCombo = i == 0 && HitObject.NewCombo, NewCombo = i == 0 && HitObject.NewCombo,
Samples = HitObject.HeadCircle.Samples.Select(s => s.With()).ToList() Samples = HitObject.HeadCircle.Samples.Select(s => s.With()).ToList()
}); }));
i += 1; i += 1;
time = HitObject.StartTime + i * streamSpacing; time = HitObject.StartTime + i * streamSpacing;
} }
editorBeatmap.Remove(HitObject); commandHandler.SafeSubmit(new RemoveHitObjectCommand(editorBeatmap, HitObject));
changeHandler?.EndChange(); commandHandler?.Commit();
} }
public override MenuItem[] ContextMenuItems => new MenuItem[] public override MenuItem[] ContextMenuItems => new MenuItem[]
{ {
new OsuMenuItem("Add control point", MenuItemType.Standard, () => new OsuMenuItem("Add control point", MenuItemType.Standard, () =>
{ {
changeHandler?.BeginChange();
addControlPoint(lastRightClickPosition); addControlPoint(lastRightClickPosition);
changeHandler?.EndChange(); commandHandler?.Commit();
}) })
{ {
Hotkey = new Hotkey(new KeyCombination(InputKey.Control, InputKey.MouseLeft)) Hotkey = new Hotkey(new KeyCombination(InputKey.Control, InputKey.MouseLeft))

View File

@ -0,0 +1,29 @@
// 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;
using osu.Game.Rulesets.Objects;
using osu.Game.Screens.Edit.Commands;
namespace osu.Game.Rulesets.Osu.Edit.Commands
{
public class AddControlPointCommand : IEditorCommand
{
public readonly BindableList<PathControlPoint> ControlPoints;
public readonly int InsertionIndex;
public readonly PathControlPoint ControlPoint;
public AddControlPointCommand(BindableList<PathControlPoint> controlPoints, int insertionIndex, PathControlPoint controlPoint)
{
ControlPoints = controlPoints;
InsertionIndex = insertionIndex;
ControlPoint = controlPoint;
}
public void Apply() => ControlPoints.Insert(InsertionIndex, ControlPoint);
public IEditorCommand CreateUndo() => new RemoveControlPointCommand(ControlPoints, InsertionIndex);
}
}

View File

@ -0,0 +1,32 @@
// 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.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Commands;
using osuTK;
namespace osu.Game.Rulesets.Osu.Edit.Commands
{
public class OsuHitObjectCommandProxy : HitObjectCommandProxy
{
public OsuHitObjectCommandProxy(EditorCommandHandler? commandHandler, OsuHitObject hitObject)
: base(commandHandler, hitObject)
{
}
protected new OsuHitObject HitObject => (OsuHitObject)base.HitObject;
public Vector2 Position
{
get => HitObject.Position;
set => Submit(new MoveCommand(HitObject, value));
}
public bool NewCombo
{
get => HitObject.NewCombo;
set => Submit(new SetNewComboCommand(HitObject, value));
}
}
}

View File

@ -0,0 +1,34 @@
// 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.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Commands;
using osuTK;
namespace osu.Game.Rulesets.Osu.Edit.Commands
{
public class PathControlPointCommandProxy : CommandProxy
{
public PathControlPointCommandProxy(EditorCommandHandler? commandHandler, PathControlPoint controlPoint)
: base(commandHandler)
{
ControlPoint = controlPoint;
}
public readonly PathControlPoint ControlPoint;
public Vector2 Position
{
get => ControlPoint.Position;
set => Submit(new UpdateControlPointCommand(ControlPoint) { Position = Position });
}
public PathType? Type
{
get => ControlPoint.Type;
set => Submit(new UpdateControlPointCommand(ControlPoint) { Type = Type });
}
}
}

View File

@ -0,0 +1,113 @@
// 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.Collections;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Bindables;
using osu.Game.Rulesets.Objects;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Commands;
namespace osu.Game.Rulesets.Osu.Edit.Commands
{
public class PathControlPointsCommandProxy : CommandProxy, IList<PathControlPointCommandProxy>
{
public PathControlPointsCommandProxy(EditorCommandHandler? commandHandler, BindableList<PathControlPoint> controlPoints)
: base(commandHandler)
{
ControlPoints = controlPoints;
}
public readonly BindableList<PathControlPoint> ControlPoints;
public int IndexOf(PathControlPointCommandProxy item)
{
return ControlPoints.IndexOf(item.ControlPoint);
}
public void Insert(int index, PathControlPointCommandProxy item) => Insert(index, item.ControlPoint);
public void Insert(int index, PathControlPoint controlPoint) => Submit(new AddControlPointCommand(ControlPoints, index, controlPoint));
public void RemoveAt(int index) => Submit(new RemoveControlPointCommand(ControlPoints, index));
public PathControlPointCommandProxy this[int index]
{
get => new PathControlPointCommandProxy(CommandHandler, ControlPoints[index]);
set => Submit(new AddControlPointCommand(ControlPoints, index, value.ControlPoint));
}
public void RemoveRange(int index, int count)
{
for (int i = 0; i < count; i++)
Submit(new RemoveControlPointCommand(ControlPoints, index));
}
public void Add(PathControlPointCommandProxy item) => Add(item.ControlPoint);
public void Add(PathControlPoint controlPoint) => Submit(new AddControlPointCommand(ControlPoints, ControlPoints.Count, controlPoint));
public void Clear()
{
while (ControlPoints.Count > 0)
Remove(ControlPoints[0]);
}
public bool Contains(PathControlPointCommandProxy item)
{
return ControlPoints.Any(c => c.Equals(item.ControlPoint));
}
public void CopyTo(PathControlPointCommandProxy[] array, int arrayIndex)
{
for (int i = 0; i < ControlPoints.Count; i++)
array[arrayIndex + i] = new PathControlPointCommandProxy(CommandHandler, ControlPoints[i]);
}
public bool Remove(PathControlPointCommandProxy item) => Remove(item.ControlPoint);
public bool Remove(PathControlPoint controlPoint)
{
if (!ControlPoints.Contains(controlPoint))
return false;
Submit(new RemoveControlPointCommand(ControlPoints, controlPoint));
return true;
}
public int Count => ControlPoints.Count;
public bool IsReadOnly => ControlPoints.IsReadOnly;
public IEnumerator<PathControlPointCommandProxy> GetEnumerator() => new PathControlPointsCommandProxyEnumerator(CommandHandler, ControlPoints.GetEnumerator());
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
private readonly struct PathControlPointsCommandProxyEnumerator : IEnumerator<PathControlPointCommandProxy>
{
public PathControlPointsCommandProxyEnumerator(
EditorCommandHandler? commandHandler,
IEnumerator<PathControlPoint> enumerator
)
{
this.commandHandler = commandHandler;
this.enumerator = enumerator;
}
private readonly EditorCommandHandler? commandHandler;
private readonly IEnumerator<PathControlPoint> enumerator;
public bool MoveNext() => enumerator.MoveNext();
public void Reset() => enumerator.Reset();
public PathControlPointCommandProxy Current => new PathControlPointCommandProxy(commandHandler, enumerator.Current);
object IEnumerator.Current => Current;
public void Dispose() => enumerator.Dispose();
}
}
}

View File

@ -0,0 +1,32 @@
// 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;
using osu.Game.Rulesets.Objects;
using osu.Game.Screens.Edit.Commands;
namespace osu.Game.Rulesets.Osu.Edit.Commands
{
public class RemoveControlPointCommand : IEditorCommand
{
public readonly BindableList<PathControlPoint> ControlPoints;
public readonly int Index;
public RemoveControlPointCommand(BindableList<PathControlPoint> controlPoints, int index)
{
ControlPoints = controlPoints;
Index = index;
}
public RemoveControlPointCommand(BindableList<PathControlPoint> controlPoints, PathControlPoint controlPoint)
{
ControlPoints = controlPoints;
Index = controlPoints.IndexOf(controlPoint);
}
public void Apply() => ControlPoints.RemoveAt(Index);
public IEditorCommand CreateUndo() => new AddControlPointCommand(ControlPoints, Index, ControlPoints[Index]);
}
}

View File

@ -0,0 +1,25 @@
// 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.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit.Commands;
namespace osu.Game.Rulesets.Osu.Edit.Commands
{
public class SetSliderVelocityMultiplierCommand : IEditorCommand
{
public readonly Slider Slider;
public readonly double SliderVelocityMultiplier;
public SetSliderVelocityMultiplierCommand(Slider slider, double sliderVelocityMultiplier)
{
Slider = slider;
SliderVelocityMultiplier = sliderVelocityMultiplier;
}
public void Apply() => Slider.SliderVelocityMultiplier = SliderVelocityMultiplier;
public IEditorCommand CreateUndo() => new SetSliderVelocityMultiplierCommand(Slider, Slider.SliderVelocityMultiplier);
}
}

View File

@ -0,0 +1,27 @@
// 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.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit.Commands;
namespace osu.Game.Rulesets.Osu.Edit.Commands
{
public class SetNewComboCommand : IEditorCommand
{
public OsuHitObject Target;
public bool NewCombo;
public SetNewComboCommand(OsuHitObject target, bool newCombo)
{
Target = target;
NewCombo = newCombo;
}
public void Apply() => Target.NewCombo = NewCombo;
public IEditorCommand CreateUndo() => new SetNewComboCommand(Target, Target.NewCombo);
public bool IsRedundant => NewCombo == Target.NewCombo;
}
}

View File

@ -0,0 +1,26 @@
// 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.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit;
namespace osu.Game.Rulesets.Osu.Edit.Commands
{
public class SliderCommandProxy : OsuHitObjectCommandProxy
{
public SliderCommandProxy(EditorCommandHandler? commandHandler, Slider hitObject)
: base(commandHandler, hitObject)
{
}
protected new Slider HitObject => (Slider)base.HitObject;
public SliderPathCommandProxy Path => new SliderPathCommandProxy(CommandHandler, HitObject.Path);
public double SliderVelocityMultiplier
{
get => HitObject.SliderVelocityMultiplier;
set => Submit(new SetSliderVelocityMultiplierCommand(HitObject, value));
}
}
}

View File

@ -0,0 +1,31 @@
// 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.Collections.Generic;
using osu.Game.Rulesets.Objects;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Commands;
namespace osu.Game.Rulesets.Osu.Edit.Commands
{
public class SliderPathCommandProxy : CommandProxy
{
public SliderPathCommandProxy(EditorCommandHandler? commandHandler, SliderPath path)
: base(commandHandler)
{
Path = path;
}
public readonly SliderPath Path;
public double? ExpectedDistance
{
get => Path.ExpectedDistance.Value;
set => Submit(new SetExpectedDistanceCommand(Path, value));
}
public PathControlPointsCommandProxy ControlPoints => new PathControlPointsCommandProxy(CommandHandler, Path.ControlPoints);
public IEnumerable<double> GetSegmentEnds() => Path.GetSegmentEnds();
}
}

View File

@ -0,0 +1,44 @@
// 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.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Screens.Edit.Commands;
using osuTK;
namespace osu.Game.Rulesets.Osu.Edit.Commands
{
public class UpdateControlPointCommand : IEditorCommand
{
public PathControlPoint ControlPoint;
public Vector2 Position;
public PathType? Type;
public UpdateControlPointCommand(PathControlPoint controlPoint)
{
ControlPoint = controlPoint;
Position = controlPoint.Position;
Type = controlPoint.Type;
}
public UpdateControlPointCommand(PathControlPoint controlPoint, Vector2 position, PathType? type)
{
ControlPoint = controlPoint;
Position = position;
Type = type;
}
public void Apply()
{
ControlPoint.Position = Position;
ControlPoint.Type = Type;
}
public IEditorCommand CreateUndo()
{
return new UpdateControlPointCommand(ControlPoint, ControlPoint.Position, ControlPoint.Type);
}
}
}

View File

@ -5,6 +5,8 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Commands;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Objects namespace osu.Game.Rulesets.Objects
@ -14,10 +16,12 @@ namespace osu.Game.Rulesets.Objects
/// <summary> /// <summary>
/// Snaps the provided <paramref name="hitObject"/>'s duration using the <paramref name="snapProvider"/>. /// Snaps the provided <paramref name="hitObject"/>'s duration using the <paramref name="snapProvider"/>.
/// </summary> /// </summary>
public static void SnapTo<THitObject>(this THitObject hitObject, IDistanceSnapProvider? snapProvider) public static void SnapTo<THitObject>(this THitObject hitObject, IDistanceSnapProvider? snapProvider, EditorCommandHandler? commandHandler = null)
where THitObject : HitObject, IHasPath where THitObject : HitObject, IHasPath
{ {
hitObject.Path.ExpectedDistance.Value = snapProvider?.FindSnappedDistance(hitObject, (float)hitObject.Path.CalculatedDistance, DistanceSnapTarget.Start) ?? hitObject.Path.CalculatedDistance; double distance = snapProvider?.FindSnappedDistance(hitObject, (float)hitObject.Path.CalculatedDistance, DistanceSnapTarget.Start) ?? hitObject.Path.CalculatedDistance;
commandHandler.SafeSubmit(new SetExpectedDistanceCommand(hitObject.Path, distance));
} }
/// <summary> /// <summary>

View File

@ -0,0 +1,21 @@
// 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.Collections.Generic;
namespace osu.Game.Screens.Edit.Commands
{
public abstract class CommandProxy
{
protected EditorCommandHandler? CommandHandler;
protected CommandProxy(EditorCommandHandler? commandHandler)
{
CommandHandler = commandHandler;
}
protected void Submit(IEditorCommand command) => CommandHandler.SafeSubmit(command);
protected void Submit(IEnumerable<IEditorCommand> command) => CommandHandler.SafeSubmit(command);
}
}

View File

@ -0,0 +1,24 @@
// 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.Game.Rulesets.Objects;
namespace osu.Game.Screens.Edit.Commands
{
public class HitObjectCommandProxy : CommandProxy
{
public HitObjectCommandProxy(EditorCommandHandler? commandHandler, HitObject hitObject)
: base(commandHandler)
{
HitObject = hitObject;
}
protected HitObject HitObject;
public double StartTime
{
get => HitObject.StartTime;
set => Submit(new SetStartTimeCommand(HitObject, value));
}
}
}

View File

@ -0,0 +1,24 @@
// 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.Game.Rulesets.Objects;
namespace osu.Game.Screens.Edit.Commands
{
public class SetExpectedDistanceCommand : IEditorCommand
{
public readonly SliderPath Path;
public readonly double? ExpectedDistance;
public SetExpectedDistanceCommand(SliderPath path, double? expectedDistance)
{
Path = path;
ExpectedDistance = expectedDistance;
}
public void Apply() => Path.ExpectedDistance.Value = ExpectedDistance;
public IEditorCommand CreateUndo() => new SetExpectedDistanceCommand(Path, Path.ExpectedDistance.Value);
}
}

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Logging;
using osu.Game.Screens.Edit.Commands; using osu.Game.Screens.Edit.Commands;
namespace osu.Game.Screens.Edit namespace osu.Game.Screens.Edit
@ -47,11 +48,16 @@ namespace osu.Game.Screens.Edit
public bool Commit() public bool Commit()
{ {
if (!HasUncommittedChanges) if (!HasUncommittedChanges)
{
Logger.Log("Nothing to commit");
return false; return false;
}
undoStack.Push(currentTransaction); undoStack.Push(currentTransaction);
redoStack.Clear(); redoStack.Clear();
Logger.Log($"Added {currentTransaction.Entries.Count} command(s) to undo stack");
currentTransaction = new Transaction(); currentTransaction = new Transaction();
historyChanged(); historyChanged();

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Screens.Edit.Commands; using osu.Game.Screens.Edit.Commands;
namespace osu.Game.Screens.Edit namespace osu.Game.Screens.Edit
@ -14,5 +15,16 @@ namespace osu.Game.Screens.Edit
else else
command.Apply(); command.Apply();
} }
public static void SafeSubmit(this EditorCommandHandler? manager, IEnumerable<IEditorCommand> commands, bool commitImmediately = false)
{
if (manager != null)
manager.Submit(commands, commitImmediately);
else
{
foreach (var command in commands)
command.Apply();
}
}
} }
} }