1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-05 10:45:36 +08:00

Merge pull request #2 from OliBomby/command-pattern-real-2

Implement variant type generic proxies without heap allocations
This commit is contained in:
maarvin 2024-10-10 21:10:05 +02:00 committed by GitHub
commit ce12b487a8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 327 additions and 386 deletions

View File

@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
private readonly T hitObject;
private readonly bool allowSelection;
private SliderPathCommandProxy pathProxy = null;
private CommandProxy<T> proxy;
private InputManager inputManager;
@ -79,6 +79,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
controlPoints.CollectionChanged += onControlPointsChanged;
controlPoints.BindTo(hitObject.Path.ControlPoints);
proxy = new CommandProxy<T>(commandHandler, hitObject);
}
// Generally all the control points are within the visible area all the time.
@ -407,6 +409,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
public void DragInProgress(DragEvent e)
{
var controlPointsProxy = proxy.Path().ControlPoints();
Vector2[] oldControlPoints = hitObject.Path.ControlPoints.Select(cp => cp.Position).ToArray();
Vector2 oldPosition = hitObject.Position;
double oldStartTime = hitObject.StartTime;
@ -419,18 +422,18 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
Vector2 movementDelta = Parent!.ToLocalSpace(result?.ScreenSpacePosition ?? newHeadPosition) - hitObject.Position;
commandHandler.SafeSubmit(new MoveCommand(hitObject, hitObject.Position + movementDelta));
commandHandler.SafeSubmit(new SetStartTimeCommand(hitObject, result?.Time ?? hitObject.StartTime));
proxy.SetPosition(hitObject.Position + movementDelta);
proxy.SetStartTime(result?.Time ?? hitObject.StartTime);
for (int i = 1; i < hitObject.Path.ControlPoints.Count; i++)
for (int i = 1; i < controlPointsProxy.Count; i++)
{
PathControlPoint controlPoint = hitObject.Path.ControlPoints[i];
var controlPointProxy = controlPointsProxy[i];
// Since control points are relative to the position of the hit object, all points that are _not_ selected
// need to be offset _back_ by the delta corresponding to the movement of 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).
if (!selectedControlPoints.Contains(controlPoint))
commandHandler.SafeSubmit(new UpdateControlPointCommand(controlPoint) { Position = controlPoint.Position - movementDelta });
if (!selectedControlPoints.Contains(controlPointProxy.Target))
controlPointProxy.SetPosition(controlPointProxy.Position() - movementDelta);
}
}
else
@ -439,32 +442,32 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
Vector2 movementDelta = Parent!.ToLocalSpace(result?.ScreenSpacePosition ?? Parent!.ToScreenSpace(e.MousePosition)) - dragStartPositions[draggedControlPointIndex] - hitObject.Position;
for (int i = 0; i < controlPoints.Count; ++i)
for (int i = 0; i < controlPointsProxy.Count; ++i)
{
PathControlPoint controlPoint = controlPoints[i];
if (selectedControlPoints.Contains(controlPoint))
commandHandler.SafeSubmit(new UpdateControlPointCommand(controlPoint) { Position = dragStartPositions[i] + movementDelta });
var controlPointProxy = controlPointsProxy[i];
if (selectedControlPoints.Contains(controlPointProxy.Target))
controlPointProxy.SetPosition(dragStartPositions[i] + movementDelta);
}
}
// Snap the path to the current beat divisor before checking length validity.
hitObject.SnapTo(distanceSnapProvider, commandHandler);
proxy.SnapTo(distanceSnapProvider);
if (!hitObject.Path.HasValidLength)
{
for (int i = 0; i < hitObject.Path.ControlPoints.Count; i++)
commandHandler.SafeSubmit(new UpdateControlPointCommand(hitObject.Path.ControlPoints[i]) { Position = oldControlPoints[i] });
for (int i = 0; i < controlPointsProxy.Count; i++)
controlPointsProxy[i].SetPosition(oldControlPoints[i]);
commandHandler.SafeSubmit(new MoveCommand(hitObject, oldPosition));
commandHandler.SafeSubmit(new SetStartTimeCommand(hitObject, oldStartTime));
proxy.SetPosition(oldPosition);
proxy.SetStartTime(oldStartTime);
// Snap the path length again to undo the invalid length.
hitObject.SnapTo(distanceSnapProvider, commandHandler);
proxy.SnapTo(distanceSnapProvider);
return;
}
// 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++)
commandHandler.SafeSubmit(new UpdateControlPointCommand(hitObject.Path.ControlPoints[i]) { Type = dragPathTypes[i] });
for (int i = 0; i < controlPointsProxy.Count; i++)
controlPointsProxy[i].SetType(dragPathTypes[i]);
EnsureValidPathTypes();
}

View File

@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{
protected new DrawableSlider DrawableObject => (DrawableSlider)base.DrawableObject;
protected SliderCommandProxy Proxy;
protected CommandProxy<Slider> Proxy { get; private set; }
protected SliderBodyPiece BodyPiece { get; private set; } = null!;
protected SliderCircleOverlay HeadOverlay { get; private set; } = null!;
@ -98,8 +98,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
Proxy = new SliderCommandProxy(commandHandler, HitObject);
InternalChildren = new Drawable[]
{
BodyPiece = new SliderBodyPiece(),
@ -121,6 +119,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{
base.LoadComplete();
Proxy = new CommandProxy<Slider>(commandHandler, HitObject);
controlPoints.BindTo(HitObject.Path.ControlPoints);
controlPoints.CollectionChanged += (_, _) => fullPathCache.Invalidate();
@ -253,7 +253,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private void endAdjustLength()
{
trimExcessControlPoints(Proxy.Path);
trimExcessControlPoints(Proxy.Path());
commandHandler?.Commit();
isAdjustingLength = false;
}
@ -281,8 +281,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
if (Precision.AlmostEquals(proposedDistance, HitObject.Path.Distance) && Precision.AlmostEquals(proposedVelocity, HitObject.SliderVelocityMultiplier))
return;
Proxy.SliderVelocityMultiplier = proposedVelocity;
Proxy.Path.ExpectedDistance = proposedDistance;
Proxy.SetSliderVelocityMultiplier(proposedVelocity);
Proxy.Path().SetExpectedDistance(proposedDistance);
editorBeatmap?.Update(HitObject);
}
@ -290,22 +290,22 @@ 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.
/// </summary>
/// <param name="sliderPath">The slider path to trim control points of.</param>
private void trimExcessControlPoints(SliderPathCommandProxy sliderPath)
private void trimExcessControlPoints(CommandProxy<SliderPath> sliderPath)
{
if (!sliderPath.ExpectedDistance.HasValue)
if (!sliderPath.ExpectedDistance().HasValue)
return;
double[] segmentEnds = sliderPath.GetSegmentEnds().ToArray();
int segmentIndex = 0;
for (int i = 1; i < sliderPath.ControlPoints.Count - 1; i++)
for (int i = 1; i < sliderPath.ControlPoints().Count - 1; i++)
{
if (!sliderPath.ControlPoints[i].Type.HasValue) continue;
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;
sliderPath.ControlPoints().RemoveRange(i + 1, sliderPath.ControlPoints().Count - i - 1);
sliderPath.ControlPoints()[^1].SetType(null);
break;
}
@ -439,7 +439,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
var pathControlPoint = new PathControlPoint { Position = position };
Proxy.Path.ControlPoints.Insert(insertionIndex, pathControlPoint);
Proxy.Path().ControlPoints().Insert(insertionIndex, pathControlPoint);
ControlPointVisualiser?.EnsureValidPathTypes();
@ -459,9 +459,9 @@ 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
// Todo: Should be handled within SliderPath itself
if (c == controlPoints[0] && controlPoints.Count > 1 && controlPoints[1].Type == null)
new PathControlPointCommandProxy(commandHandler, c).Type = controlPoints[0].Type;
new CommandProxy<PathControlPoint>(commandHandler, c).SetType(controlPoints[0].Type);
Proxy.Path.ControlPoints.Remove(c);
Proxy.Path().ControlPoints().Remove(c);
}
ControlPointVisualiser?.EnsureValidPathTypes();
@ -499,7 +499,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
editorBeatmap.SelectedHitObjects.Clear();
var controlPointsProxy = new PathControlPointsCommandProxy(commandHandler, controlPoints);
var controlPointsProxy = new ListCommandProxy<BindableList<PathControlPoint>, CommandProxy<PathControlPoint>, PathControlPoint>(commandHandler, controlPoints);
foreach (var splitPoint in controlPointsToSplitAt)
{
@ -527,19 +527,19 @@ 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())
};
Proxy.StartTime += split_gap;
Proxy.SetStartTime(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.
commandHandler.SafeSubmit(new AddHitObjectCommand(editorBeatmap, newSlider));
Proxy.NewCombo = false;
Proxy.Path.ExpectedDistance -= newSlider.Path.CalculatedDistance;
Proxy.StartTime += newSlider.SpanDuration;
Proxy.SetNewCombo(false);
Proxy.Path().SetExpectedDistance(Proxy.Path().ExpectedDistance() - newSlider.Path.CalculatedDistance);
Proxy.SetStartTime(Proxy.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.
if (HitObject.Path.ExpectedDistance.Value <= Precision.DOUBLE_EPSILON)
{
Proxy.Path.ExpectedDistance = null;
Proxy.Path().SetExpectedDistance(null);
}
}
@ -547,9 +547,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
// As a final step, we must reset its control points to have an origin of (0,0).
Vector2 first = controlPoints[0].Position;
foreach (var c in controlPointsProxy)
c.Position -= first;
c.SetPosition(c.Position() - first);
Proxy.Position += first;
Proxy.SetPosition(Proxy.Position() + first);
}
private void convertToStream()

View File

@ -1,29 +0,0 @@
// 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

@ -2,31 +2,21 @@
// 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 static class OsuHitObjectCommandProxy
{
public OsuHitObjectCommandProxy(EditorCommandHandler? commandHandler, OsuHitObject hitObject)
: base(commandHandler, hitObject)
{
}
public static Vector2 Position<T>(this CommandProxy<T> proxy) where T : OsuHitObject => proxy.Target.Position;
protected new OsuHitObject HitObject => (OsuHitObject)base.HitObject;
public static void SetPosition<T>(this CommandProxy<T> proxy, Vector2 value) where T : OsuHitObject =>
proxy.Submit(new MoveCommand(proxy.Target, value));
public Vector2 Position
{
get => HitObject.Position;
set => Submit(new MoveCommand(HitObject, value));
}
public static bool NewCombo<T>(this CommandProxy<T> proxy) where T : OsuHitObject => proxy.Target.NewCombo;
public bool NewCombo
{
get => HitObject.NewCombo;
set => Submit(new SetNewComboCommand(HitObject, value));
}
public static void SetNewCombo<T>(this CommandProxy<T> proxy, bool value) where T : OsuHitObject =>
proxy.Submit(new SetNewComboCommand(proxy.Target, value));
}
}

View File

@ -1,34 +0,0 @@
// 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

@ -1,113 +0,0 @@
// 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

@ -1,32 +0,0 @@
// 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

@ -1,25 +0,0 @@
// 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

@ -1,26 +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.
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit;
namespace osu.Game.Rulesets.Osu.Edit.Commands
{
public class SliderCommandProxy : OsuHitObjectCommandProxy
public static class SliderCommandProxy
{
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

@ -1,31 +0,0 @@
// 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

@ -24,6 +24,19 @@ namespace osu.Game.Rulesets.Objects
commandHandler.SafeSubmit(new SetExpectedDistanceCommand(hitObject.Path, distance));
}
/// <summary>
/// Snaps the provided <paramref name="proxy"/>'s duration using the <paramref name="snapProvider"/>.
/// </summary>
public static void SnapTo<THitObject>(this CommandProxy<THitObject> proxy, IDistanceSnapProvider? snapProvider)
where THitObject : HitObject, IHasPath
{
var hitObject = proxy.Target;
double distance = snapProvider?.FindSnappedDistance(hitObject, (float)hitObject.Path.CalculatedDistance, DistanceSnapTarget.Start) ?? hitObject.Path.CalculatedDistance;
proxy.Path().SetExpectedDistance(distance);
proxy.Path().ControlPoints();
}
/// <summary>
/// Reverse the direction of this path.
/// </summary>

View File

@ -1,21 +1,126 @@
// 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;
namespace osu.Game.Screens.Edit.Commands
{
public abstract class CommandProxy
public interface ICommandProxy<T>
{
protected EditorCommandHandler? CommandHandler;
EditorCommandHandler? CommandHandler { get; init; }
T Target { get; init; }
void Submit(IEditorCommand command);
}
protected CommandProxy(EditorCommandHandler? commandHandler)
public readonly struct CommandProxy<T> : ICommandProxy<T>
{
public CommandProxy(EditorCommandHandler? commandHandler, T target)
{
CommandHandler = commandHandler;
Target = target;
}
protected void Submit(IEditorCommand command) => CommandHandler.SafeSubmit(command);
public EditorCommandHandler? CommandHandler { get; init; }
public T Target { get; init; }
public void Submit(IEditorCommand command) => CommandHandler.SafeSubmit(command);
}
protected void Submit(IEnumerable<IEditorCommand> command) => CommandHandler.SafeSubmit(command);
public readonly struct ListCommandProxy<T, TItemProxy, TItem> : ICommandProxy<T>, IList<TItemProxy> where T : IList<TItem> where TItemProxy : ICommandProxy<TItem>, new()
{
public ListCommandProxy(EditorCommandHandler? commandHandler, T target)
{
CommandHandler = commandHandler;
Target = target;
}
public EditorCommandHandler? CommandHandler { get; init; }
public T Target { get; init; }
public void Submit(IEditorCommand command) => CommandHandler.SafeSubmit(command);
public IEnumerator<TItemProxy> GetEnumerator()
{
var commandHandler = CommandHandler;
return Target.Select(o => new TItemProxy { CommandHandler = commandHandler, Target = o }).GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public void Add(TItemProxy item)
{
Add(item.Target);
}
public void Add(TItem item)
{
Insert(Count, item);
}
public void Clear()
{
while (Target.Count > 0)
RemoveAt(Count - 1);
}
public bool Contains(TItemProxy item)
{
return Target.Contains(item.Target);
}
public void CopyTo(TItemProxy[] array, int arrayIndex)
{
for (int i = 0; i < Target.Count; i++)
array[arrayIndex + i] = new TItemProxy { CommandHandler = CommandHandler, Target = Target[i] };
}
public bool Remove(TItemProxy item) => Remove(item.Target);
public bool Remove(TItem item)
{
if (!Target.Contains(item))
return false;
Submit(new RemoveCommand<T, TItem>(Target, item));
return true;
}
public int Count => Target.Count;
public bool IsReadOnly => Target.IsReadOnly;
public int IndexOf(TItemProxy item)
{
return Target.IndexOf(item.Target);
}
public void Insert(int index, TItemProxy item)
{
Insert(index, item.Target);
}
public void Insert(int index, TItem item)
{
Submit(new InsertCommand<T, TItem>(Target, index, item));
}
public void RemoveAt(int index)
{
Submit(new RemoveCommand<T, TItem>(Target, index));
}
public void RemoveRange(int index, int count)
{
for (int i = 0; i < count; i++)
RemoveAt(index);
}
public TItemProxy this[int index]
{
get => new TItemProxy { CommandHandler = CommandHandler, Target = Target[index] };
set => Submit(new InsertCommand<T, TItem>(Target, index, value.Target));
}
}
}

View File

@ -2,23 +2,22 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Screens.Edit.Commands
{
public class HitObjectCommandProxy : CommandProxy
public static class HitObjectCommandProxy
{
public HitObjectCommandProxy(EditorCommandHandler? commandHandler, HitObject hitObject)
: base(commandHandler)
{
HitObject = hitObject;
}
public static double StartTime<T>(this CommandProxy<T> proxy) where T : HitObject => proxy.Target.StartTime;
protected HitObject HitObject;
public static void SetStartTime<T>(this CommandProxy<T> proxy, double value) where T : HitObject => proxy.Submit(new SetStartTimeCommand(proxy.Target, value));
public double StartTime
{
get => HitObject.StartTime;
set => Submit(new SetStartTimeCommand(HitObject, value));
}
public static CommandProxy<SliderPath> Path<T>(this CommandProxy<T> proxy) where T : IHasPath =>
new CommandProxy<SliderPath>(proxy.CommandHandler, proxy.Target.Path);
public static double SliderVelocityMultiplier<T>(this CommandProxy<T> proxy) where T : IHasSliderVelocity => proxy.Target.SliderVelocityMultiplier;
public static void SetSliderVelocityMultiplier<T>(this CommandProxy<T> proxy, double value) where T : IHasSliderVelocity =>
proxy.Submit(new SetSliderVelocityMultiplierCommand(proxy.Target, value));
}
}

View File

@ -9,6 +9,6 @@ namespace osu.Game.Screens.Edit.Commands
public IEditorCommand CreateUndo();
public virtual bool IsRedundant => false;
public bool IsRedundant => false;
}
}

View File

@ -5,6 +5,6 @@ namespace osu.Game.Screens.Edit.Commands
{
public interface IMergeableCommand : IEditorCommand
{
public IEditorCommand? MergeWith(IEditorCommand previous);
public IMergeableCommand? MergeWith(IEditorCommand previous);
}
}

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 System.Collections.Generic;
namespace osu.Game.Screens.Edit.Commands
{
public class InsertCommand<T, T2> : IEditorCommand where T : IList<T2>
{
public readonly T Target;
public readonly int InsertionIndex;
public readonly T2 Item;
public InsertCommand(T target, int insertionIndex, T2 item)
{
Target = target;
InsertionIndex = insertionIndex;
Item = item;
}
public void Apply() => Target.Insert(InsertionIndex, Item);
public IEditorCommand CreateUndo() => new RemoveCommand<T, T2>(Target, InsertionIndex);
}
}

View File

@ -6,7 +6,7 @@ using osuTK;
namespace osu.Game.Screens.Edit.Commands
{
public class MoveCommand : IEditorCommand, IMergeableCommand
public class MoveCommand : IMergeableCommand
{
public readonly IHasMutablePosition Target;
@ -24,10 +24,10 @@ namespace osu.Game.Screens.Edit.Commands
public bool IsRedundant => Position == Target.Position;
public IEditorCommand? MergeWith(IEditorCommand previous)
public IMergeableCommand? MergeWith(IEditorCommand previous)
{
if (previous is MoveCommand moveCommand)
return moveCommand.Target != Target ? null : this;
return moveCommand.Target != Target ? null : moveCommand;
return null;
}

View File

@ -0,0 +1,20 @@
// 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 osuTK;
namespace osu.Game.Screens.Edit.Commands
{
public static class PathControlPointCommandProxy
{
public static Vector2 Position<T>(this CommandProxy<T> proxy) where T : PathControlPoint => proxy.Target.Position;
public static void SetPosition(this CommandProxy<PathControlPoint> proxy, Vector2 value) => proxy.Submit(new UpdateControlPointCommand(proxy.Target) { Position = value });
public static PathType? Type<T>(this CommandProxy<T> proxy) where T : PathControlPoint => proxy.Target.Type;
public static void SetType(this CommandProxy<PathControlPoint> proxy, PathType? value) => proxy.Submit(new UpdateControlPointCommand(proxy.Target) { Type = value });
}
}

View File

@ -3,7 +3,7 @@
namespace osu.Game.Screens.Edit.Commands
{
public abstract class PropertyChangeCommand<TTarget, TProperty> : IEditorCommand, IMergeableCommand where TTarget : class
public abstract class PropertyChangeCommand<TTarget, TProperty> : IMergeableCommand where TTarget : class
{
protected abstract TProperty ReadValue(TTarget target);
@ -25,10 +25,10 @@ namespace osu.Game.Screens.Edit.Commands
public IEditorCommand CreateUndo() => CreateInstance(Target, Value);
public IEditorCommand? MergeWith(IEditorCommand previous)
public IMergeableCommand? MergeWith(IEditorCommand previous)
{
if (previous is PropertyChangeCommand<TTarget, TProperty> command && command.Target == Target)
return this;
return command;
return null;
}

View File

@ -0,0 +1,30 @@
// 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 class RemoveCommand<T, T2> : IEditorCommand where T : IList<T2>
{
public readonly T Target;
public readonly int Index;
public RemoveCommand(T target, int index)
{
Target = target;
Index = index;
}
public RemoveCommand(T target, T2 item)
{
Target = target;
Index = target.IndexOf(item);
}
public void Apply() => Target.RemoveAt(Index);
public IEditorCommand CreateUndo() => new InsertCommand<T, T2>(Target, Index, Target[Index]);
}
}

View File

@ -0,0 +1,30 @@
// 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.Types;
namespace osu.Game.Screens.Edit.Commands
{
public class SetSliderVelocityMultiplierCommand : IEditorCommand
{
public readonly IHasSliderVelocity Target;
public readonly double SliderVelocityMultiplier;
public SetSliderVelocityMultiplierCommand(IHasSliderVelocity target, double sliderVelocityMultiplier)
{
Target = target;
SliderVelocityMultiplier = sliderVelocityMultiplier;
}
public void Apply()
{
Target.SliderVelocityMultiplier = SliderVelocityMultiplier;
}
public IEditorCommand CreateUndo()
{
return new SetSliderVelocityMultiplierCommand(Target, Target.SliderVelocityMultiplier);
}
}
}

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;
using osu.Framework.Bindables;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Screens.Edit.Commands
{
public static class SliderPathCommandProxy
{
public static double? ExpectedDistance(this CommandProxy<SliderPath> proxy) => proxy.Target.ExpectedDistance.Value;
public static void SetExpectedDistance(this CommandProxy<SliderPath> proxy, double? value) => proxy.Submit(new SetExpectedDistanceCommand(proxy.Target, value));
public static ListCommandProxy<BindableList<PathControlPoint>, CommandProxy<PathControlPoint>, PathControlPoint> ControlPoints(this CommandProxy<SliderPath> proxy) =>
new ListCommandProxy<BindableList<PathControlPoint>, CommandProxy<PathControlPoint>, PathControlPoint>(proxy.CommandHandler, proxy.Target.ControlPoints);
public static IEnumerable<double> GetSegmentEnds(this ICommandProxy<SliderPath> proxy) => proxy.Target.GetSegmentEnds();
}
}

View File

@ -3,10 +3,9 @@
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
namespace osu.Game.Screens.Edit.Commands
{
public class UpdateControlPointCommand : IEditorCommand
{

View File

@ -12,10 +12,6 @@ namespace osu.Game.Screens.Edit
{
public partial class EditorCommandHandler
{
public EditorCommandHandler()
{
}
public event Action<IEditorCommand>? CommandApplied;
public readonly Bindable<bool> CanUndo = new BindableBool();
@ -143,8 +139,6 @@ namespace osu.Game.Screens.Edit
currentTransaction.Add(reverse);
}
private readonly record struct HistoryEntry(IEditorCommand Command, IEditorCommand Reverse);
private readonly struct Transaction
{
public Transaction()
@ -159,24 +153,15 @@ namespace osu.Game.Screens.Edit
{
if (command is IMergeableCommand mergeable)
{
for (int i = 0; i < commands.Count; i++)
for (int i = commands.Count - 1; i >= 0; i--)
{
var merged = mergeable.MergeWith(commands[i]);
if (merged == null)
continue;
command = merged;
commands.RemoveAt(i--);
if (command is IMergeableCommand newMergeable)
{
mergeable = newMergeable;
}
else
{
break;
}
commands[i] = merged;
return;
}
}