mirror of
https://github.com/ppy/osu.git
synced 2024-12-05 10:33:22 +08:00
Merge 21de5a837a
into ce8e4120b7
This commit is contained in:
commit
35323152a9
@ -10,6 +10,7 @@ using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Screens.Edit.Changes;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osuTK;
|
||||
using Direction = osu.Framework.Graphics.Direction;
|
||||
@ -95,9 +96,10 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
|
||||
if (h is JuiceStream juiceStream)
|
||||
{
|
||||
juiceStream.Path.Reverse(out Vector2 positionalOffset);
|
||||
juiceStream.OriginalX += positionalOffset.X;
|
||||
juiceStream.LegacyConvertedY += positionalOffset.Y;
|
||||
var reverse = new ReverseSliderPathChange(juiceStream.Path);
|
||||
reverse.Apply();
|
||||
juiceStream.OriginalX += reverse.PositionalOffset.X;
|
||||
juiceStream.LegacyConvertedY += reverse.PositionalOffset.Y;
|
||||
EditorBeatmap.Update(juiceStream);
|
||||
}
|
||||
}
|
||||
|
@ -24,8 +24,10 @@ using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Edit.Changes;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Changes;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
@ -113,7 +115,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
return;
|
||||
|
||||
if (segment.Count > 3)
|
||||
first.Type = PathType.BEZIER;
|
||||
new PathControlPointTypeChange(first, PathType.BEZIER).Apply(changeHandler);
|
||||
|
||||
if (segment.Count != 3)
|
||||
return;
|
||||
@ -121,7 +123,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
ReadOnlySpan<Vector2> points = segment.Select(p => p.Position).ToArray();
|
||||
RectangleF boundingBox = PathApproximator.CircularArcBoundingBox(points);
|
||||
if (boundingBox.Width >= 640 || boundingBox.Height >= 480)
|
||||
first.Type = PathType.BEZIER;
|
||||
new PathControlPointTypeChange(first, PathType.BEZIER).Apply(changeHandler);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -371,26 +373,23 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
int thirdPointIndex = indexInSegment + 2;
|
||||
|
||||
if (pointsInSegment.Count > thirdPointIndex + 1)
|
||||
pointsInSegment[thirdPointIndex].Type = pointsInSegment[0].Type;
|
||||
new PathControlPointTypeChange(pointsInSegment[thirdPointIndex], pointsInSegment[0].Type).Apply(changeHandler);
|
||||
}
|
||||
|
||||
hitObject.Path.ExpectedDistance.Value = null;
|
||||
p.ControlPoint.Type = type;
|
||||
new ExpectedDistanceChange(hitObject.Path, null).Apply(changeHandler);
|
||||
new PathControlPointTypeChange(p.ControlPoint, type).Apply(changeHandler);
|
||||
}
|
||||
|
||||
EnsureValidPathTypes();
|
||||
|
||||
if (hitObject.Path.Distance < originalDistance)
|
||||
hitObject.SnapTo(distanceSnapProvider);
|
||||
new SnapToChange<T>(hitObject, distanceSnapProvider).Apply(changeHandler);
|
||||
else
|
||||
hitObject.Path.ExpectedDistance.Value = originalDistance;
|
||||
new ExpectedDistanceChange(hitObject.Path, originalDistance).Apply(changeHandler);
|
||||
|
||||
changeHandler?.EndChange();
|
||||
}
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private IEditorChangeHandler changeHandler { get; set; }
|
||||
|
||||
#region Drag handling
|
||||
|
||||
private Vector2[] dragStartPositions;
|
||||
@ -412,6 +411,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
changeHandler?.BeginChange();
|
||||
}
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private NewBeatmapEditorChangeHandler changeHandler { get; set; }
|
||||
|
||||
public void DragInProgress(DragEvent e)
|
||||
{
|
||||
Vector2[] oldControlPoints = hitObject.Path.ControlPoints.Select(cp => cp.Position).ToArray();
|
||||
@ -426,8 +428,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
|
||||
Vector2 movementDelta = Parent!.ToLocalSpace(result?.ScreenSpacePosition ?? newHeadPosition) - hitObject.Position;
|
||||
|
||||
hitObject.Position += movementDelta;
|
||||
hitObject.StartTime = result?.Time ?? hitObject.StartTime;
|
||||
new PositionChange(hitObject, hitObject.Position + movementDelta).Apply(changeHandler);
|
||||
new StartTimeChange(hitObject, result?.Time ?? hitObject.StartTime).Apply(changeHandler);
|
||||
|
||||
for (int i = 1; i < hitObject.Path.ControlPoints.Count; i++)
|
||||
{
|
||||
@ -437,7 +439,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
// 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))
|
||||
controlPoint.Position -= movementDelta;
|
||||
new PathControlPointPositionChange(controlPoint, controlPoint.Position - movementDelta).Apply(changeHandler);
|
||||
}
|
||||
}
|
||||
else
|
||||
@ -450,28 +452,28 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
{
|
||||
PathControlPoint controlPoint = controlPoints[i];
|
||||
if (selectedControlPoints.Contains(controlPoint))
|
||||
controlPoint.Position = dragStartPositions[i] + movementDelta;
|
||||
new PathControlPointPositionChange(controlPoint, dragStartPositions[i] + movementDelta).Apply(changeHandler);
|
||||
}
|
||||
}
|
||||
|
||||
// Snap the path to the current beat divisor before checking length validity.
|
||||
hitObject.SnapTo(distanceSnapProvider);
|
||||
new SnapToChange<T>(hitObject, distanceSnapProvider).Apply(changeHandler);
|
||||
|
||||
if (!hitObject.Path.HasValidLength)
|
||||
{
|
||||
for (int i = 0; i < hitObject.Path.ControlPoints.Count; i++)
|
||||
hitObject.Path.ControlPoints[i].Position = oldControlPoints[i];
|
||||
new PathControlPointPositionChange(hitObject.Path.ControlPoints[i], oldControlPoints[i]).Apply(changeHandler);
|
||||
|
||||
hitObject.Position = oldPosition;
|
||||
hitObject.StartTime = oldStartTime;
|
||||
new PositionChange(hitObject, oldPosition).Apply(changeHandler);
|
||||
new StartTimeChange(hitObject, oldStartTime).Apply(changeHandler);
|
||||
// Snap the path length again to undo the invalid length.
|
||||
hitObject.SnapTo(distanceSnapProvider);
|
||||
new SnapToChange<T>(hitObject, distanceSnapProvider).Apply(changeHandler);
|
||||
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++)
|
||||
hitObject.Path.ControlPoints[i].Type = dragPathTypes[i];
|
||||
new PathControlPointTypeChange(hitObject.Path.ControlPoints[i], dragPathTypes[i]).Apply(changeHandler);
|
||||
|
||||
EnsureValidPathTypes();
|
||||
}
|
||||
|
@ -21,9 +21,11 @@ using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
|
||||
using osu.Game.Rulesets.Osu.Edit.Changes;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Changes;
|
||||
using osu.Game.Screens.Edit.Compose;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
@ -50,7 +52,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
private EditorBeatmap? editorBeatmap { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private IEditorChangeHandler? changeHandler { get; set; }
|
||||
private NewBeatmapEditorChangeHandler? changeHandler { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private BindableBeatDivisor? beatDivisor { get; set; }
|
||||
@ -122,6 +124,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
pathVersion.BindValueChanged(_ => editorBeatmap?.Update(HitObject));
|
||||
|
||||
BodyPiece.UpdateFrom(HitObject);
|
||||
HitObject.DefaultsApplied += _ => BodyPiece.UpdateFrom(HitObject);
|
||||
|
||||
if (editorBeatmap != null)
|
||||
selectedObjects.BindTo(editorBeatmap.SelectedHitObjects);
|
||||
@ -280,9 +283,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
if (Precision.AlmostEquals(proposedDistance, HitObject.Path.Distance) && Precision.AlmostEquals(proposedVelocity, HitObject.SliderVelocityMultiplier))
|
||||
return;
|
||||
|
||||
HitObject.SliderVelocityMultiplier = proposedVelocity;
|
||||
HitObject.Path.ExpectedDistance.Value = proposedDistance;
|
||||
new SliderVelocityMultiplierChange(HitObject, proposedVelocity).Apply(changeHandler);
|
||||
new ExpectedDistanceChange(HitObject.Path, proposedDistance).Apply(changeHandler);
|
||||
editorBeatmap?.Update(HitObject);
|
||||
changeHandler?.RecordUpdate(HitObject);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -303,8 +307,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
|
||||
if (Precision.AlmostBigger(segmentEnds[segmentIndex], 1, 1E-3))
|
||||
{
|
||||
sliderPath.ControlPoints.RemoveRange(i + 1, sliderPath.ControlPoints.Count - i - 1);
|
||||
sliderPath.ControlPoints[^1].Type = null;
|
||||
new RemoveRangePathControlPointChange(sliderPath.ControlPoints, i + 1, sliderPath.ControlPoints.Count - i - 1).Apply(changeHandler);
|
||||
new PathControlPointTypeChange(sliderPath.ControlPoints[^1], null).Apply(changeHandler);
|
||||
break;
|
||||
}
|
||||
|
||||
@ -442,11 +446,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
var pathControlPoint = new PathControlPoint { Position = position };
|
||||
|
||||
// Move the control points from the insertion index onwards to make room for the insertion
|
||||
controlPoints.Insert(insertionIndex, pathControlPoint);
|
||||
new InsertPathControlPointChange(HitObject.Path.ControlPoints, insertionIndex, pathControlPoint).Apply(changeHandler);
|
||||
|
||||
ControlPointVisualiser?.EnsureValidPathTypes();
|
||||
|
||||
HitObject.SnapTo(distanceSnapProvider);
|
||||
new SnapToChange<Slider>(HitObject, distanceSnapProvider).Apply(changeHandler);
|
||||
|
||||
return pathControlPoint;
|
||||
}
|
||||
@ -462,15 +466,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
|
||||
// Todo: Should be handled within SliderPath itself
|
||||
if (c == controlPoints[0] && controlPoints.Count > 1 && controlPoints[1].Type == null)
|
||||
controlPoints[1].Type = controlPoints[0].Type;
|
||||
new PathControlPointTypeChange(controlPoints[1], controlPoints[0].Type).Apply(changeHandler);
|
||||
|
||||
controlPoints.Remove(c);
|
||||
new RemovePathControlPointChange(HitObject.Path.ControlPoints, c).Apply(changeHandler);
|
||||
}
|
||||
|
||||
ControlPointVisualiser?.EnsureValidPathTypes();
|
||||
|
||||
// Snap the slider to the current beat divisor before checking length validity.
|
||||
HitObject.SnapTo(distanceSnapProvider);
|
||||
new SnapToChange<Slider>(HitObject, distanceSnapProvider).Apply(changeHandler);
|
||||
|
||||
// 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)
|
||||
@ -483,8 +487,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)
|
||||
Vector2 first = controlPoints[0].Position;
|
||||
foreach (var c in controlPoints)
|
||||
c.Position -= first;
|
||||
HitObject.Position += first;
|
||||
new PathControlPointPositionChange(c, c.Position - first).Apply(changeHandler);
|
||||
|
||||
new PositionChange(HitObject, HitObject.Position + first).Apply(changeHandler);
|
||||
}
|
||||
|
||||
private void splitControlPoints(List<PathControlPoint> controlPointsToSplitAt)
|
||||
@ -514,7 +519,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
|
||||
// Extract the split portion and remove from the original slider.
|
||||
var splitControlPoints = controlPoints.Take(index + 1).ToList();
|
||||
controlPoints.RemoveRange(0, index);
|
||||
new RemoveRangePathControlPointChange(HitObject.Path.ControlPoints, 0, index).Apply(changeHandler);
|
||||
|
||||
var newSlider = new Slider
|
||||
{
|
||||
@ -528,18 +533,18 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
};
|
||||
|
||||
// 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;
|
||||
new StartTimeChange(HitObject, HitObject.StartTime + split_gap).Apply(changeHandler);
|
||||
|
||||
editorBeatmap.Add(newSlider);
|
||||
new AddHitObjectChange(editorBeatmap, newSlider).Apply(changeHandler);
|
||||
|
||||
HitObject.NewCombo = false;
|
||||
HitObject.Path.ExpectedDistance.Value -= newSlider.Path.CalculatedDistance;
|
||||
HitObject.StartTime += newSlider.SpanDuration;
|
||||
new NewComboChange(HitObject, false).Apply(changeHandler);
|
||||
new ExpectedDistanceChange(HitObject.Path, HitObject.Path.ExpectedDistance.Value - newSlider.Path.CalculatedDistance).Apply(changeHandler);
|
||||
new StartTimeChange(HitObject, HitObject.StartTime + newSlider.SpanDuration).Apply(changeHandler);
|
||||
|
||||
// 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)
|
||||
{
|
||||
HitObject.Path.ExpectedDistance.Value = null;
|
||||
new ExpectedDistanceChange(HitObject.Path, null).Apply(changeHandler);
|
||||
}
|
||||
}
|
||||
|
||||
@ -547,8 +552,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 controlPoints)
|
||||
c.Position -= first;
|
||||
HitObject.Position += first;
|
||||
new PathControlPointPositionChange(c, c.Position - first).Apply(changeHandler);
|
||||
|
||||
new PositionChange(HitObject, HitObject.Position + first).Apply(changeHandler);
|
||||
}
|
||||
|
||||
private void convertToStream()
|
||||
@ -576,19 +582,19 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
|
||||
Vector2 position = HitObject.Position + HitObject.Path.PositionAt(pathPosition);
|
||||
|
||||
editorBeatmap.Add(new HitCircle
|
||||
new AddHitObjectChange(editorBeatmap, new HitCircle
|
||||
{
|
||||
StartTime = time,
|
||||
Position = position,
|
||||
NewCombo = i == 0 && HitObject.NewCombo,
|
||||
Samples = HitObject.HeadCircle.Samples.Select(s => s.With()).ToList()
|
||||
});
|
||||
}).Apply(changeHandler);
|
||||
|
||||
i += 1;
|
||||
time = HitObject.StartTime + i * streamSpacing;
|
||||
}
|
||||
|
||||
editorBeatmap.Remove(HitObject);
|
||||
new RemoveHitObjectChange(editorBeatmap, HitObject).Apply(changeHandler);
|
||||
|
||||
changeHandler?.EndChange();
|
||||
}
|
||||
|
21
osu.Game.Rulesets.Osu/Edit/Changes/PositionChange.cs
Normal file
21
osu.Game.Rulesets.Osu/Edit/Changes/PositionChange.cs
Normal 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 osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Screens.Edit.Changes;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit.Changes
|
||||
{
|
||||
public class PositionChange : PropertyChange<OsuHitObject, Vector2>
|
||||
{
|
||||
public PositionChange(OsuHitObject target, Vector2 value)
|
||||
: base(target, value)
|
||||
{
|
||||
}
|
||||
|
||||
protected override Vector2 ReadValue(OsuHitObject target) => target.Position;
|
||||
|
||||
protected override void WriteValue(OsuHitObject target, Vector2 value) => target.Position = value;
|
||||
}
|
||||
}
|
@ -15,8 +15,11 @@ using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Beatmaps;
|
||||
using osu.Game.Rulesets.Osu.Edit.Changes;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Changes;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osu.Game.Utils;
|
||||
using osuTK;
|
||||
@ -51,6 +54,9 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
return false;
|
||||
}
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
private NewBeatmapEditorChangeHandler? changeHandler { get; set; }
|
||||
|
||||
public override bool HandleMovement(MoveSelectionEvent<HitObject> moveEvent)
|
||||
{
|
||||
var hitObjects = selectedMovableObjects;
|
||||
@ -72,7 +78,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
// this will potentially move the selection out of bounds...
|
||||
foreach (var h in hitObjects)
|
||||
h.Position += localDelta;
|
||||
new PositionChange(h, h.Position + localDelta).Apply(changeHandler);
|
||||
|
||||
// but this will be corrected.
|
||||
moveSelectionInBounds();
|
||||
@ -105,12 +111,13 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
foreach (var h in hitObjects)
|
||||
{
|
||||
if (moreThanOneObject)
|
||||
h.StartTime = endTime - (h.GetEndTime() - startTime);
|
||||
new StartTimeChange(h, endTime - (h.GetEndTime() - startTime)).Apply(changeHandler);
|
||||
|
||||
if (h is Slider slider)
|
||||
{
|
||||
slider.Path.Reverse(out Vector2 offset);
|
||||
slider.Position += offset;
|
||||
var reverse = new ReverseSliderPathChange(slider.Path);
|
||||
reverse.Apply(changeHandler);
|
||||
new PositionChange(slider, slider.Position + reverse.PositionalOffset).Apply(changeHandler);
|
||||
}
|
||||
}
|
||||
|
||||
@ -118,7 +125,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
hitObjects = hitObjects.OrderBy(obj => obj.StartTime).ToList();
|
||||
|
||||
for (int i = 0; i < hitObjects.Count; ++i)
|
||||
hitObjects[i].NewCombo = newComboOrder[i];
|
||||
new NewComboChange(hitObjects[i], newComboOrder[i]).Apply(changeHandler);
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -167,7 +174,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
if (!Precision.AlmostEquals(flippedPosition, h.Position))
|
||||
{
|
||||
h.Position = flippedPosition;
|
||||
new PositionChange(h, flippedPosition).Apply(changeHandler);
|
||||
didFlip = true;
|
||||
}
|
||||
|
||||
@ -176,7 +183,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
didFlip = true;
|
||||
|
||||
foreach (var cp in slider.Path.ControlPoints)
|
||||
cp.Position = GeometryUtils.GetFlippedPosition(flipAxis, controlPointFlipQuad, cp.Position);
|
||||
new PathControlPointPositionChange(cp, GeometryUtils.GetFlippedPosition(flipAxis, controlPointFlipQuad, cp.Position)).Apply(changeHandler);
|
||||
}
|
||||
}
|
||||
|
||||
@ -206,7 +213,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
delta.Y -= quad.BottomRight.Y - DrawHeight;
|
||||
|
||||
foreach (var h in hitObjects)
|
||||
h.Position += delta;
|
||||
new PositionChange(h, h.Position + delta).Apply(changeHandler);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -245,7 +252,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
if (mergedHitObject.Path.ControlPoints.Count == 0)
|
||||
{
|
||||
mergedHitObject.Path.ControlPoints.Add(new PathControlPoint(Vector2.Zero, PathType.LINEAR));
|
||||
new InsertPathControlPointChange(mergedHitObject.Path.ControlPoints, mergedHitObject.Path.ControlPoints.Count, new PathControlPoint(Vector2.Zero, PathType.LINEAR)).Apply(changeHandler);
|
||||
}
|
||||
|
||||
// Merge all the selected hit objects into one slider path.
|
||||
@ -259,15 +266,15 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
float distanceToLastControlPoint = Vector2.Distance(mergedHitObject.Path.ControlPoints[^1].Position, offset);
|
||||
|
||||
// Calculate the distance required to travel to the expected distance of the merging slider.
|
||||
mergedHitObject.Path.ExpectedDistance.Value = mergedHitObject.Path.CalculatedDistance + distanceToLastControlPoint + hasPath.Path.Distance;
|
||||
new ExpectedDistanceChange(mergedHitObject.Path, mergedHitObject.Path.CalculatedDistance + distanceToLastControlPoint + hasPath.Path.Distance).Apply(changeHandler);
|
||||
|
||||
// Remove the last control point if it sits exactly on the start of the next control point.
|
||||
if (Precision.AlmostEquals(distanceToLastControlPoint, 0))
|
||||
{
|
||||
mergedHitObject.Path.ControlPoints.RemoveAt(mergedHitObject.Path.ControlPoints.Count - 1);
|
||||
new RemovePathControlPointChange(mergedHitObject.Path.ControlPoints, mergedHitObject.Path.ControlPoints.Count - 1).Apply(changeHandler);
|
||||
}
|
||||
|
||||
mergedHitObject.Path.ControlPoints.AddRange(hasPath.Path.ControlPoints.Select(o => new PathControlPoint(o.Position + offset, o.Type)));
|
||||
new AddRangePathControlPointChange(mergedHitObject.Path.ControlPoints, hasPath.Path.ControlPoints.Select(o => new PathControlPoint(o.Position + offset, o.Type))).Apply(changeHandler);
|
||||
lastCircle = false;
|
||||
}
|
||||
else
|
||||
@ -275,11 +282,11 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
// Turn the last control point into a linear type if this is the first merging circle in a sequence, so the subsequent control points can be inherited path type.
|
||||
if (!lastCircle)
|
||||
{
|
||||
mergedHitObject.Path.ControlPoints.Last().Type = PathType.LINEAR;
|
||||
new PathControlPointTypeChange(mergedHitObject.Path.ControlPoints.Last(), PathType.LINEAR).Apply(changeHandler);
|
||||
}
|
||||
|
||||
mergedHitObject.Path.ControlPoints.Add(new PathControlPoint(selectedMergeableObject.Position - mergedHitObject.Position));
|
||||
mergedHitObject.Path.ExpectedDistance.Value = null;
|
||||
new InsertPathControlPointChange(mergedHitObject.Path.ControlPoints, mergedHitObject.Path.ControlPoints.Count, new PathControlPoint(selectedMergeableObject.Position - mergedHitObject.Position)).Apply(changeHandler);
|
||||
new ExpectedDistanceChange(mergedHitObject.Path, null).Apply(changeHandler);
|
||||
lastCircle = true;
|
||||
}
|
||||
}
|
||||
@ -289,17 +296,17 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
foreach (var selectedMergeableObject in mergeableObjects.Skip(1))
|
||||
{
|
||||
EditorBeatmap.Remove(selectedMergeableObject);
|
||||
new RemoveHitObjectChange(EditorBeatmap, selectedMergeableObject).Apply(changeHandler);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var selectedMergeableObject in mergeableObjects)
|
||||
{
|
||||
EditorBeatmap.Remove(selectedMergeableObject);
|
||||
new RemoveHitObjectChange(EditorBeatmap, selectedMergeableObject).Apply(changeHandler);
|
||||
}
|
||||
|
||||
EditorBeatmap.Add(mergedHitObject);
|
||||
new AddHitObjectChange(EditorBeatmap, mergedHitObject).Apply(changeHandler);
|
||||
}
|
||||
|
||||
// Make sure the merged hitobject is selected.
|
||||
|
@ -9,8 +9,10 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Edit.Changes;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Changes;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osu.Game.Utils;
|
||||
using osuTK;
|
||||
@ -20,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
public partial class OsuSelectionRotationHandler : SelectionRotationHandler
|
||||
{
|
||||
[Resolved]
|
||||
private IEditorChangeHandler? changeHandler { get; set; }
|
||||
private NewBeatmapEditorChangeHandler? changeHandler { get; set; }
|
||||
|
||||
private BindableList<HitObject> selectedItems { get; } = new BindableList<HitObject>();
|
||||
|
||||
@ -78,14 +80,17 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
foreach (var ho in objectsInRotation)
|
||||
{
|
||||
ho.Position = GeometryUtils.RotatePointAroundOrigin(originalPositions[ho], actualOrigin, rotation);
|
||||
new PositionChange(ho, GeometryUtils.RotatePointAroundOrigin(originalPositions[ho], actualOrigin, rotation)).Apply(changeHandler);
|
||||
|
||||
if (ho is IHasPath withPath)
|
||||
{
|
||||
var originalPath = originalPathControlPointPositions[withPath];
|
||||
|
||||
for (int i = 0; i < withPath.Path.ControlPoints.Count; ++i)
|
||||
withPath.Path.ControlPoints[i].Position = GeometryUtils.RotatePointAroundOrigin(originalPath[i], Vector2.Zero, rotation);
|
||||
{
|
||||
new PathControlPointPositionChange(withPath.Path.ControlPoints[i],
|
||||
GeometryUtils.RotatePointAroundOrigin(originalPath[i], Vector2.Zero, rotation)).Apply(changeHandler);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,9 +13,11 @@ using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Edit.Changes;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Changes;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osu.Game.Utils;
|
||||
using osuTK;
|
||||
@ -35,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
public Bindable<bool> IsScalingSlider { get; private set; } = new BindableBool();
|
||||
|
||||
[Resolved]
|
||||
private IEditorChangeHandler? changeHandler { get; set; }
|
||||
private NewBeatmapEditorChangeHandler? changeHandler { get; set; }
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private IDistanceSnapProvider? snapProvider { get; set; }
|
||||
@ -113,7 +115,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
foreach (var (ho, originalState) in objectsInScale)
|
||||
{
|
||||
ho.Position = GeometryUtils.GetScaledPosition(scale, actualOrigin, originalState.Position, axisRotation);
|
||||
new PositionChange(ho, GeometryUtils.GetScaledPosition(scale, actualOrigin, originalState.Position, axisRotation)).Apply(changeHandler);
|
||||
}
|
||||
}
|
||||
|
||||
@ -166,15 +168,16 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
// Maintain the path types in case they were defaulted to bezier at some point during scaling
|
||||
for (int i = 0; i < slider.Path.ControlPoints.Count; i++)
|
||||
{
|
||||
slider.Path.ControlPoints[i].Position = GeometryUtils.GetScaledPosition(scale, Vector2.Zero, originalInfo.PathControlPointPositions[i], axisRotation);
|
||||
slider.Path.ControlPoints[i].Type = originalInfo.PathControlPointTypes[i];
|
||||
new PathControlPointPositionChange(slider.Path.ControlPoints[i],
|
||||
GeometryUtils.GetScaledPosition(scale, Vector2.Zero, originalInfo.PathControlPointPositions[i], axisRotation)).Apply(changeHandler);
|
||||
new PathControlPointTypeChange(slider.Path.ControlPoints[i], originalInfo.PathControlPointTypes[i]).Apply(changeHandler);
|
||||
}
|
||||
|
||||
// Snap the slider's length to the current beat divisor
|
||||
// to calculate the final resulting duration / bounding box before the final checks.
|
||||
slider.SnapTo(snapProvider);
|
||||
new SnapToChange<Slider>(slider, snapProvider).Apply(changeHandler);
|
||||
|
||||
slider.Position = GeometryUtils.GetScaledPosition(scale, origin, originalInfo.Position, axisRotation);
|
||||
new PositionChange(slider, GeometryUtils.GetScaledPosition(scale, origin, originalInfo.Position, axisRotation)).Apply(changeHandler);
|
||||
|
||||
//if sliderhead or sliderend end up outside playfield, revert scaling.
|
||||
Quad scaledQuad = GeometryUtils.GetSurroundingQuad(new OsuHitObject[] { slider });
|
||||
@ -184,12 +187,14 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
return;
|
||||
|
||||
for (int i = 0; i < slider.Path.ControlPoints.Count; i++)
|
||||
slider.Path.ControlPoints[i].Position = originalInfo.PathControlPointPositions[i];
|
||||
{
|
||||
new PathControlPointPositionChange(slider.Path.ControlPoints[i], originalInfo.PathControlPointPositions[i]).Apply(changeHandler);
|
||||
}
|
||||
|
||||
slider.Position = originalInfo.Position;
|
||||
new PositionChange(slider, originalInfo.Position).Apply(changeHandler);
|
||||
|
||||
// Snap the slider's length again to undo the potentially-invalid length applied by the previous snap.
|
||||
slider.SnapTo(snapProvider);
|
||||
new SnapToChange<Slider>(slider, snapProvider).Apply(changeHandler);
|
||||
}
|
||||
|
||||
private (bool X, bool Y) isQuadInBounds(Quad quad)
|
||||
@ -327,7 +332,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
delta.Y -= quad.BottomRight.Y - OsuPlayfield.BASE_SIZE.Y;
|
||||
|
||||
foreach (var (h, _) in objectsInScale!)
|
||||
h.Position += delta;
|
||||
new PositionChange(h, h.Position + delta).Apply(changeHandler);
|
||||
}
|
||||
|
||||
private struct OriginalHitObjectState
|
||||
|
@ -28,6 +28,7 @@ using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Changes;
|
||||
using osu.Game.Screens.Edit.Components.RadioButtons;
|
||||
using osu.Game.Screens.Edit.Components.TernaryButtons;
|
||||
using osu.Game.Screens.Edit.Compose;
|
||||
@ -67,6 +68,9 @@ namespace osu.Game.Rulesets.Edit
|
||||
[Resolved]
|
||||
private OverlayColourProvider colourProvider { get; set; }
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
private NewBeatmapEditorChangeHandler changeHandler { get; set; }
|
||||
|
||||
public override ComposeBlueprintContainer BlueprintContainer => blueprintContainer;
|
||||
private ComposeBlueprintContainer blueprintContainer;
|
||||
|
||||
@ -272,7 +276,8 @@ namespace osu.Game.Rulesets.Edit
|
||||
TernaryStates = CreateTernaryButtons().ToArray();
|
||||
togglesCollection.AddRange(TernaryStates.Select(b => new DrawableTernaryButton(b)));
|
||||
|
||||
sampleBankTogglesCollection.AddRange(BlueprintContainer.SampleBankTernaryStates.Zip(BlueprintContainer.SampleAdditionBankTernaryStates).Select(b => new SampleBankTernaryButton(b.First, b.Second)));
|
||||
sampleBankTogglesCollection.AddRange(BlueprintContainer.SampleBankTernaryStates.Zip(BlueprintContainer.SampleAdditionBankTernaryStates)
|
||||
.Select(b => new SampleBankTernaryButton(b.First, b.Second)));
|
||||
|
||||
SetSelectTool();
|
||||
|
||||
@ -550,13 +555,13 @@ namespace osu.Game.Rulesets.Edit
|
||||
public void CommitPlacement(HitObject hitObject)
|
||||
{
|
||||
EditorBeatmap.PlacementObject.Value = null;
|
||||
EditorBeatmap.Add(hitObject);
|
||||
new AddHitObjectChange(EditorBeatmap, hitObject).Apply(changeHandler);
|
||||
|
||||
if (autoSeekOnPlacement.Value && EditorClock.CurrentTime < hitObject.StartTime)
|
||||
EditorClock.SeekSmoothlyTo(hitObject.StartTime);
|
||||
}
|
||||
|
||||
public void Delete(HitObject hitObject) => EditorBeatmap.Remove(hitObject);
|
||||
public void Delete(HitObject hitObject) => new RemoveHitObjectChange(EditorBeatmap, hitObject).Apply(changeHandler);
|
||||
|
||||
#endregion
|
||||
|
||||
|
24
osu.Game/Screens/Edit/Changes/AddHitObjectChange.cs
Normal file
24
osu.Game/Screens/Edit/Changes/AddHitObjectChange.cs
Normal 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.Changes
|
||||
{
|
||||
public class AddHitObjectChange : IRevertibleChange
|
||||
{
|
||||
public EditorBeatmap Beatmap;
|
||||
|
||||
public HitObject HitObject;
|
||||
|
||||
public AddHitObjectChange(EditorBeatmap beatmap, HitObject hitObject)
|
||||
{
|
||||
Beatmap = beatmap;
|
||||
HitObject = hitObject;
|
||||
}
|
||||
|
||||
public void Apply() => Beatmap.Add(HitObject);
|
||||
|
||||
public void Revert() => Beatmap.Remove(HitObject);
|
||||
}
|
||||
}
|
@ -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;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Changes
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds a range of <see cref="PathControlPoint"/>s to the provided <see cref="BindableList{T}"/>.
|
||||
/// </summary>
|
||||
public class AddRangePathControlPointChange : CompositeChange
|
||||
{
|
||||
private readonly BindableList<PathControlPoint> controlPoints;
|
||||
private readonly IEnumerable<PathControlPoint> points;
|
||||
|
||||
public AddRangePathControlPointChange(BindableList<PathControlPoint> controlPoints, IEnumerable<PathControlPoint> points)
|
||||
{
|
||||
this.controlPoints = controlPoints;
|
||||
this.points = points;
|
||||
}
|
||||
|
||||
protected override void SubmitChanges()
|
||||
{
|
||||
foreach (var point in points)
|
||||
Submit(new InsertPathControlPointChange(controlPoints, controlPoints.Count, point));
|
||||
}
|
||||
}
|
||||
}
|
46
osu.Game/Screens/Edit/Changes/CompositeChange.cs
Normal file
46
osu.Game/Screens/Edit/Changes/CompositeChange.cs
Normal file
@ -0,0 +1,46 @@
|
||||
// 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.Changes
|
||||
{
|
||||
public abstract class CompositeChange : IRevertibleChange
|
||||
{
|
||||
private List<IRevertibleChange>? changes;
|
||||
|
||||
public void Apply()
|
||||
{
|
||||
if (changes == null)
|
||||
{
|
||||
changes = new List<IRevertibleChange>();
|
||||
SubmitChanges();
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var change in changes)
|
||||
change.Apply();
|
||||
}
|
||||
|
||||
public void Revert()
|
||||
{
|
||||
if (changes == null)
|
||||
throw new System.InvalidOperationException("Cannot revert before applying.");
|
||||
|
||||
for (int i = changes.Count - 1; i >= 0; i--)
|
||||
changes[i].Revert();
|
||||
}
|
||||
|
||||
protected void Submit(IRevertibleChange change)
|
||||
{
|
||||
change.Apply();
|
||||
changes!.Add(change);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the tracks the changes of this <see cref="CompositeChange"/>.
|
||||
/// </summary>
|
||||
/// <remarks>Use <see cref="Submit"/> to apply the <see cref="IRevertibleChange"/> created in this method.</remarks>
|
||||
protected abstract void SubmitChanges();
|
||||
}
|
||||
}
|
19
osu.Game/Screens/Edit/Changes/ExpectedDistanceChange.cs
Normal file
19
osu.Game/Screens/Edit/Changes/ExpectedDistanceChange.cs
Normal file
@ -0,0 +1,19 @@
|
||||
// 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.Changes
|
||||
{
|
||||
public class ExpectedDistanceChange : PropertyChange<SliderPath, double?>
|
||||
{
|
||||
public ExpectedDistanceChange(SliderPath target, double? value)
|
||||
: base(target, value)
|
||||
{
|
||||
}
|
||||
|
||||
protected override double? ReadValue(SliderPath target) => target.ExpectedDistance.Value;
|
||||
|
||||
protected override void WriteValue(SliderPath target, double? value) => target.ExpectedDistance.Value = value;
|
||||
}
|
||||
}
|
32
osu.Game/Screens/Edit/Changes/IRevertibleChange.cs
Normal file
32
osu.Game/Screens/Edit/Changes/IRevertibleChange.cs
Normal 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.
|
||||
|
||||
namespace osu.Game.Screens.Edit.Changes
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a change which can be undone.
|
||||
/// </summary>
|
||||
public interface IRevertibleChange
|
||||
{
|
||||
/// <summary>
|
||||
/// Applies this change to the current state.
|
||||
/// </summary>
|
||||
void Apply();
|
||||
|
||||
/// <summary>
|
||||
/// Applies the inverse of this change to the current state.
|
||||
/// </summary>
|
||||
void Revert();
|
||||
}
|
||||
|
||||
public static class RevertibleChangeExtension
|
||||
{
|
||||
public static void Apply(this IRevertibleChange change, NewBeatmapEditorChangeHandler? changeHandler, bool commitImmediately = false)
|
||||
{
|
||||
if (changeHandler != null)
|
||||
changeHandler.Submit(change, commitImmediately);
|
||||
else
|
||||
change.Apply();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
// 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;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Changes
|
||||
{
|
||||
public class InsertPathControlPointChange : IRevertibleChange
|
||||
{
|
||||
public readonly IList<PathControlPoint> Target;
|
||||
|
||||
public readonly int InsertionIndex;
|
||||
|
||||
public readonly PathControlPoint Item;
|
||||
|
||||
public InsertPathControlPointChange(IList<PathControlPoint> target, int insertionIndex, PathControlPoint item)
|
||||
{
|
||||
Target = target;
|
||||
InsertionIndex = insertionIndex;
|
||||
Item = item;
|
||||
}
|
||||
|
||||
public void Apply() => Target.Insert(InsertionIndex, Item);
|
||||
|
||||
public void Revert() => Target.RemoveAt(InsertionIndex);
|
||||
}
|
||||
}
|
19
osu.Game/Screens/Edit/Changes/NewComboChange.cs
Normal file
19
osu.Game/Screens/Edit/Changes/NewComboChange.cs
Normal file
@ -0,0 +1,19 @@
|
||||
// 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.Changes
|
||||
{
|
||||
public class NewComboChange : PropertyChange<IHasComboInformation, bool>
|
||||
{
|
||||
public NewComboChange(IHasComboInformation target, bool value)
|
||||
: base(target, value)
|
||||
{
|
||||
}
|
||||
|
||||
protected override bool ReadValue(IHasComboInformation target) => target.NewCombo;
|
||||
|
||||
protected override void WriteValue(IHasComboInformation target, bool value) => target.NewCombo = value;
|
||||
}
|
||||
}
|
@ -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 osuTK;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Changes
|
||||
{
|
||||
public class PathControlPointPositionChange : PropertyChange<PathControlPoint, Vector2>
|
||||
{
|
||||
public PathControlPointPositionChange(PathControlPoint target, Vector2 value)
|
||||
: base(target, value)
|
||||
{
|
||||
}
|
||||
|
||||
protected override Vector2 ReadValue(PathControlPoint target) => target.Position;
|
||||
|
||||
protected override void WriteValue(PathControlPoint target, Vector2 value) => target.Position = value;
|
||||
}
|
||||
}
|
20
osu.Game/Screens/Edit/Changes/PathControlPointTypeChange.cs
Normal file
20
osu.Game/Screens/Edit/Changes/PathControlPointTypeChange.cs
Normal 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;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Changes
|
||||
{
|
||||
public class PathControlPointTypeChange : PropertyChange<PathControlPoint, PathType?>
|
||||
{
|
||||
public PathControlPointTypeChange(PathControlPoint target, PathType? value)
|
||||
: base(target, value)
|
||||
{
|
||||
}
|
||||
|
||||
protected override PathType? ReadValue(PathControlPoint target) => target.Type;
|
||||
|
||||
protected override void WriteValue(PathControlPoint target, PathType? value) => target.Type = value;
|
||||
}
|
||||
}
|
49
osu.Game/Screens/Edit/Changes/PropertyChange.cs
Normal file
49
osu.Game/Screens/Edit/Changes/PropertyChange.cs
Normal file
@ -0,0 +1,49 @@
|
||||
// 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.
|
||||
|
||||
namespace osu.Game.Screens.Edit.Changes
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a single property update on a given <see cref="Target"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TTarget">Type of the object owning the property</typeparam>
|
||||
/// <typeparam name="TValue">Type of the property to update</typeparam>
|
||||
public abstract class PropertyChange<TTarget, TValue> : IRevertibleChange where TTarget : class
|
||||
{
|
||||
/// <summary>
|
||||
/// Reads the current value of the property from the target.
|
||||
/// </summary>
|
||||
protected abstract TValue ReadValue(TTarget target);
|
||||
|
||||
/// <summary>
|
||||
/// Writes the new value to the target object.
|
||||
/// </summary>
|
||||
protected abstract void WriteValue(TTarget target, TValue value);
|
||||
|
||||
/// <summary>
|
||||
/// The target object, which owns the property to change.
|
||||
/// </summary>
|
||||
public readonly TTarget Target;
|
||||
|
||||
/// <summary>
|
||||
/// The value to change the property to.
|
||||
/// </summary>
|
||||
public readonly TValue Value;
|
||||
|
||||
/// <summary>
|
||||
/// The original value of the property before the change.
|
||||
/// </summary>
|
||||
public readonly TValue OriginalValue;
|
||||
|
||||
protected PropertyChange(TTarget target, TValue value)
|
||||
{
|
||||
Target = target;
|
||||
Value = value;
|
||||
OriginalValue = ReadValue(target);
|
||||
}
|
||||
|
||||
public void Apply() => WriteValue(Target, Value);
|
||||
|
||||
public void Revert() => WriteValue(Target, OriginalValue);
|
||||
}
|
||||
}
|
24
osu.Game/Screens/Edit/Changes/RemoveHitObjectChange.cs
Normal file
24
osu.Game/Screens/Edit/Changes/RemoveHitObjectChange.cs
Normal 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.Changes
|
||||
{
|
||||
public class RemoveHitObjectChange : IRevertibleChange
|
||||
{
|
||||
public EditorBeatmap Beatmap;
|
||||
|
||||
public HitObject HitObject;
|
||||
|
||||
public RemoveHitObjectChange(EditorBeatmap beatmap, HitObject hitObject)
|
||||
{
|
||||
Beatmap = beatmap;
|
||||
HitObject = hitObject;
|
||||
}
|
||||
|
||||
public void Apply() => Beatmap.Remove(HitObject);
|
||||
|
||||
public void Revert() => Beatmap.Add(HitObject);
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
// 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;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Changes
|
||||
{
|
||||
public class RemovePathControlPointChange : IRevertibleChange
|
||||
{
|
||||
public readonly IList<PathControlPoint> Target;
|
||||
|
||||
public readonly int Index;
|
||||
|
||||
public readonly PathControlPoint Item;
|
||||
|
||||
public RemovePathControlPointChange(IList<PathControlPoint> target, int index)
|
||||
{
|
||||
Target = target;
|
||||
Index = index;
|
||||
Item = target[index];
|
||||
}
|
||||
|
||||
public RemovePathControlPointChange(IList<PathControlPoint> target, PathControlPoint item)
|
||||
{
|
||||
Target = target;
|
||||
Index = target.IndexOf(item);
|
||||
Item = item;
|
||||
}
|
||||
|
||||
public void Apply() => Target.RemoveAt(Index);
|
||||
|
||||
public void Revert() => Target.Insert(Index, Item);
|
||||
}
|
||||
}
|
@ -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 osu.Framework.Bindables;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Changes
|
||||
{
|
||||
/// <summary>
|
||||
/// Removes a range of <see cref="PathControlPoint"/>s from the provided <see cref="BindableList{T}"/>.
|
||||
/// </summary>
|
||||
public class RemoveRangePathControlPointChange : CompositeChange
|
||||
{
|
||||
private readonly BindableList<PathControlPoint> controlPoints;
|
||||
private readonly int startIndex;
|
||||
private readonly int count;
|
||||
|
||||
public RemoveRangePathControlPointChange(BindableList<PathControlPoint> controlPoints, int startIndex, int count)
|
||||
{
|
||||
this.controlPoints = controlPoints;
|
||||
this.startIndex = startIndex;
|
||||
this.count = count;
|
||||
}
|
||||
|
||||
protected override void SubmitChanges()
|
||||
{
|
||||
for (int i = 0; i < count; i++)
|
||||
Submit(new RemovePathControlPointChange(controlPoints, startIndex));
|
||||
}
|
||||
}
|
||||
}
|
@ -3,29 +3,34 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Objects
|
||||
namespace osu.Game.Screens.Edit.Changes
|
||||
{
|
||||
public static class SliderPathExtensions
|
||||
/// <summary>
|
||||
/// Reverse the direction of this path.
|
||||
/// </summary>
|
||||
public class ReverseSliderPathChange : CompositeChange
|
||||
{
|
||||
/// <summary>
|
||||
/// Snaps the provided <paramref name="hitObject"/>'s duration using the <paramref name="snapProvider"/>.
|
||||
/// The positional offset of the resulting path. It should be added to the start position of the path.
|
||||
/// </summary>
|
||||
public static void SnapTo<THitObject>(this THitObject hitObject, IDistanceSnapProvider? snapProvider)
|
||||
where THitObject : HitObject, IHasPath
|
||||
{
|
||||
hitObject.Path.ExpectedDistance.Value = snapProvider?.FindSnappedDistance(hitObject, (float)hitObject.Path.CalculatedDistance, DistanceSnapTarget.Start) ?? hitObject.Path.CalculatedDistance;
|
||||
}
|
||||
public Vector2 PositionalOffset { get; private set; }
|
||||
|
||||
private readonly SliderPath sliderPath;
|
||||
|
||||
/// <summary>
|
||||
/// Reverse the direction of this path.
|
||||
/// </summary>
|
||||
/// <param name="sliderPath">The <see cref="SliderPath"/>.</param>
|
||||
/// <param name="positionalOffset">The positional offset of the resulting path. It should be added to the start position of this path.</param>
|
||||
public static void Reverse(this SliderPath sliderPath, out Vector2 positionalOffset)
|
||||
public ReverseSliderPathChange(SliderPath sliderPath)
|
||||
{
|
||||
this.sliderPath = sliderPath;
|
||||
}
|
||||
|
||||
protected override void SubmitChanges()
|
||||
{
|
||||
var controlPoints = sliderPath.ControlPoints;
|
||||
|
||||
@ -33,7 +38,7 @@ namespace osu.Game.Rulesets.Objects
|
||||
|
||||
// Inherited points after a linear point, as well as the first control point if it inherited,
|
||||
// should be treated as linear points, so their types are temporarily changed to linear.
|
||||
inheritedLinearPoints.ForEach(p => p.Type = PathType.LINEAR);
|
||||
inheritedLinearPoints.ForEach(p => Submit(new PathControlPointTypeChange(p, PathType.LINEAR)));
|
||||
|
||||
double[] segmentEnds = sliderPath.GetSegmentEnds().ToArray();
|
||||
|
||||
@ -46,11 +51,11 @@ namespace osu.Game.Rulesets.Objects
|
||||
segmentEnds = segmentEnds[..^1];
|
||||
}
|
||||
|
||||
controlPoints.RemoveAt(controlPoints.Count - 1);
|
||||
Submit(new RemovePathControlPointChange(controlPoints, controlPoints.Count - 1));
|
||||
}
|
||||
|
||||
// Restore original control point types.
|
||||
inheritedLinearPoints.ForEach(p => p.Type = null);
|
||||
inheritedLinearPoints.ForEach(p => Submit(new PathControlPointTypeChange(p, null)));
|
||||
|
||||
// Recalculate middle perfect curve control points at the end of the slider path.
|
||||
if (controlPoints.Count >= 3 && controlPoints[^3].Type == PathType.PERFECT_CURVE && controlPoints[^2].Type == null && segmentEnds.Any())
|
||||
@ -61,30 +66,25 @@ namespace osu.Game.Rulesets.Objects
|
||||
var circleArcPath = new List<Vector2>();
|
||||
sliderPath.GetPathToProgress(circleArcPath, lastSegmentStart / lastSegmentEnd, 1);
|
||||
|
||||
controlPoints[^2].Position = circleArcPath[circleArcPath.Count / 2];
|
||||
Submit(new PathControlPointPositionChange(controlPoints[^2], circleArcPath[circleArcPath.Count / 2]));
|
||||
}
|
||||
|
||||
sliderPath.reverseControlPoints(out positionalOffset);
|
||||
reverseControlPoints();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reverses the order of the provided <see cref="SliderPath"/>'s <see cref="PathControlPoint"/>s.
|
||||
/// </summary>
|
||||
/// <param name="sliderPath">The <see cref="SliderPath"/>.</param>
|
||||
/// <param name="positionalOffset">The positional offset of the resulting path. It should be added to the start position of this path.</param>
|
||||
private static void reverseControlPoints(this SliderPath sliderPath, out Vector2 positionalOffset)
|
||||
private void reverseControlPoints()
|
||||
{
|
||||
var points = sliderPath.ControlPoints.ToArray();
|
||||
positionalOffset = sliderPath.PositionAt(1);
|
||||
PositionalOffset = sliderPath.PositionAt(1);
|
||||
|
||||
sliderPath.ControlPoints.Clear();
|
||||
Submit(new RemoveRangePathControlPointChange(sliderPath.ControlPoints, 0, sliderPath.ControlPoints.Count));
|
||||
|
||||
PathType? lastType = null;
|
||||
|
||||
for (int i = 0; i < points.Length; i++)
|
||||
{
|
||||
var p = points[i];
|
||||
p.Position -= positionalOffset;
|
||||
var p = new PathControlPoint(points[i].Position, points[i].Type);
|
||||
p.Position -= PositionalOffset;
|
||||
|
||||
// propagate types forwards to last null type
|
||||
if (i == points.Length - 1)
|
||||
@ -95,7 +95,7 @@ namespace osu.Game.Rulesets.Objects
|
||||
else if (p.Type != null)
|
||||
(p.Type, lastType) = (lastType, p.Type);
|
||||
|
||||
sliderPath.ControlPoints.Insert(0, p);
|
||||
Submit(new InsertPathControlPointChange(sliderPath.ControlPoints, 0, p));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
// 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.Changes
|
||||
{
|
||||
public class SliderVelocityMultiplierChange : PropertyChange<IHasSliderVelocity, double>
|
||||
{
|
||||
public SliderVelocityMultiplierChange(IHasSliderVelocity target, double value)
|
||||
: base(target, value)
|
||||
{
|
||||
}
|
||||
|
||||
protected override double ReadValue(IHasSliderVelocity target) => target.SliderVelocityMultiplier;
|
||||
|
||||
protected override void WriteValue(IHasSliderVelocity target, double value) => target.SliderVelocityMultiplier = value;
|
||||
}
|
||||
}
|
30
osu.Game/Screens/Edit/Changes/SnapToChange.cs
Normal file
30
osu.Game/Screens/Edit/Changes/SnapToChange.cs
Normal 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.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Changes
|
||||
{
|
||||
/// <summary>
|
||||
/// Snaps the provided <see cref="HitObject"/>'s duration using the <see cref="IDistanceSnapProvider"/>.
|
||||
/// </summary>
|
||||
public class SnapToChange<THitObject> : CompositeChange where THitObject : HitObject, IHasPath
|
||||
{
|
||||
private readonly THitObject hitObject;
|
||||
private readonly IDistanceSnapProvider? snapProvider;
|
||||
|
||||
public SnapToChange(THitObject hitObject, IDistanceSnapProvider? snapProvider)
|
||||
{
|
||||
this.hitObject = hitObject;
|
||||
this.snapProvider = snapProvider;
|
||||
}
|
||||
|
||||
protected override void SubmitChanges()
|
||||
{
|
||||
double newDistance = snapProvider?.FindSnappedDistance(hitObject, (float)hitObject.Path.CalculatedDistance, DistanceSnapTarget.Start) ?? hitObject.Path.CalculatedDistance;
|
||||
Submit(new ExpectedDistanceChange(hitObject.Path, newDistance));
|
||||
}
|
||||
}
|
||||
}
|
19
osu.Game/Screens/Edit/Changes/StartTimeChange.cs
Normal file
19
osu.Game/Screens/Edit/Changes/StartTimeChange.cs
Normal file
@ -0,0 +1,19 @@
|
||||
// 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.Changes
|
||||
{
|
||||
public class StartTimeChange : PropertyChange<HitObject, double>
|
||||
{
|
||||
public StartTimeChange(HitObject target, double value)
|
||||
: base(target, value)
|
||||
{
|
||||
}
|
||||
|
||||
protected override double ReadValue(HitObject target) => target.StartTime;
|
||||
|
||||
protected override void WriteValue(HitObject target, double value) => target.StartTime = value;
|
||||
}
|
||||
}
|
@ -47,7 +47,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
private IPositionSnapProvider snapProvider { get; set; }
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private IEditorChangeHandler changeHandler { get; set; }
|
||||
private NewBeatmapEditorChangeHandler changeHandler { get; set; }
|
||||
|
||||
protected readonly BindableList<T> SelectedItems = new BindableList<T>();
|
||||
|
||||
|
@ -14,6 +14,7 @@ using osu.Framework.Input.Events;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Screens.Edit.Changes;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Compose.Components
|
||||
{
|
||||
@ -25,6 +26,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
[Resolved]
|
||||
protected EditorBeatmap Beatmap { get; private set; }
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
private NewBeatmapEditorChangeHandler changeHandler { get; set; }
|
||||
|
||||
protected readonly HitObjectComposer Composer;
|
||||
|
||||
private HitObjectUsageEventBuffer usageEventBuffer;
|
||||
@ -87,8 +91,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
{
|
||||
Beatmap.PerformOnSelection(obj =>
|
||||
{
|
||||
obj.StartTime += offset;
|
||||
new StartTimeChange(obj, obj.StartTime + offset).Apply(changeHandler);
|
||||
Beatmap.Update(obj);
|
||||
changeHandler?.RecordUpdate(obj);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -119,7 +124,10 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
|
||||
// handle positional change etc.
|
||||
foreach (var blueprint in SelectionBlueprints)
|
||||
{
|
||||
Beatmap.Update(blueprint.Item);
|
||||
changeHandler?.RecordUpdate(blueprint.Item);
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool OnDoubleClick(DoubleClickEvent e)
|
||||
|
@ -52,7 +52,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
protected SelectionBox SelectionBox { get; private set; } = null!;
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
protected IEditorChangeHandler? ChangeHandler { get; private set; }
|
||||
protected NewBeatmapEditorChangeHandler? ChangeHandler { get; private set; }
|
||||
|
||||
public SelectionRotationHandler RotationHandler { get; private set; } = null!;
|
||||
|
||||
|
@ -177,6 +177,8 @@ namespace osu.Game.Screens.Edit
|
||||
[CanBeNull] // Should be non-null once it can support custom rulesets.
|
||||
private EditorChangeHandler changeHandler;
|
||||
|
||||
private NewBeatmapEditorChangeHandler newChangeHandler;
|
||||
|
||||
private DependencyContainer dependencies;
|
||||
|
||||
private bool isNewBeatmap;
|
||||
@ -302,6 +304,9 @@ namespace osu.Game.Screens.Edit
|
||||
dependencies.CacheAs<IEditorChangeHandler>(changeHandler);
|
||||
}
|
||||
|
||||
newChangeHandler = new NewBeatmapEditorChangeHandler(editorBeatmap);
|
||||
dependencies.CacheAs(newChangeHandler);
|
||||
|
||||
beatDivisor.SetArbitraryDivisor(editorBeatmap.BeatmapInfo.BeatDivisor);
|
||||
beatDivisor.BindValueChanged(divisor => editorBeatmap.BeatmapInfo.BeatDivisor = divisor.NewValue);
|
||||
|
||||
@ -440,8 +445,8 @@ namespace osu.Game.Screens.Edit
|
||||
}
|
||||
});
|
||||
|
||||
changeHandler?.CanUndo.BindValueChanged(v => undoMenuItem.Action.Disabled = !v.NewValue, true);
|
||||
changeHandler?.CanRedo.BindValueChanged(v => redoMenuItem.Action.Disabled = !v.NewValue, true);
|
||||
newChangeHandler.CanUndo.BindValueChanged(v => undoMenuItem.Action.Disabled = !v.NewValue, true);
|
||||
newChangeHandler.CanRedo.BindValueChanged(v => redoMenuItem.Action.Disabled = !v.NewValue, true);
|
||||
|
||||
editorBackgroundDim.BindValueChanged(_ => dimBackground());
|
||||
}
|
||||
@ -971,9 +976,9 @@ namespace osu.Game.Screens.Edit
|
||||
|
||||
#endregion
|
||||
|
||||
protected void Undo() => changeHandler?.RestoreState(-1);
|
||||
protected void Undo() => newChangeHandler.Undo();
|
||||
|
||||
protected void Redo() => changeHandler?.RestoreState(1);
|
||||
protected void Redo() => newChangeHandler.Redo();
|
||||
|
||||
protected void SetPreviewPointToCurrentTime()
|
||||
{
|
||||
|
206
osu.Game/Screens/Edit/NewBeatmapEditorChangeHandler.cs
Normal file
206
osu.Game/Screens/Edit/NewBeatmapEditorChangeHandler.cs
Normal file
@ -0,0 +1,206 @@
|
||||
// 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 System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Screens.Edit.Changes;
|
||||
|
||||
namespace osu.Game.Screens.Edit
|
||||
{
|
||||
public partial class NewBeatmapEditorChangeHandler : TransactionalCommitComponent
|
||||
{
|
||||
private readonly EditorBeatmap editorBeatmap;
|
||||
|
||||
public readonly Bindable<bool> CanUndo = new BindableBool();
|
||||
|
||||
public readonly Bindable<bool> CanRedo = new BindableBool();
|
||||
|
||||
public bool HasUncommittedChanges => currentTransaction.UndoChanges.Count != 0;
|
||||
|
||||
private Transaction currentTransaction;
|
||||
|
||||
private readonly Stack<Transaction> undoStack = new Stack<Transaction>();
|
||||
|
||||
private readonly Stack<Transaction> redoStack = new Stack<Transaction>();
|
||||
|
||||
private bool isRestoring;
|
||||
|
||||
public NewBeatmapEditorChangeHandler(EditorBeatmap editorBeatmap)
|
||||
{
|
||||
currentTransaction = new Transaction();
|
||||
this.editorBeatmap = editorBeatmap;
|
||||
|
||||
editorBeatmap.TransactionBegan += BeginChange;
|
||||
editorBeatmap.TransactionEnded += EndChange;
|
||||
editorBeatmap.SaveStateTriggered += SaveState;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Submits a change to be applied and added to the history.
|
||||
/// </summary>
|
||||
/// <param name="change">Change to be applied.</param>
|
||||
/// <param name="commitImmediately">Whether to commit the current transaction and push it onto the undo stack immediately.</param>
|
||||
public void Submit(IRevertibleChange change, bool commitImmediately = false)
|
||||
{
|
||||
change.Apply();
|
||||
record(change);
|
||||
|
||||
if (commitImmediately)
|
||||
UpdateState();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Submits a collection of changes to be applied and added to the history.
|
||||
/// </summary>
|
||||
/// <param name="changes">Changes to be applied.</param>
|
||||
/// <param name="commitImmediately">Whether to commit the current transaction and push it onto the undo stack immediately.</param>
|
||||
public void Submit(IEnumerable<IRevertibleChange> changes, bool commitImmediately = false)
|
||||
{
|
||||
foreach (var change in changes)
|
||||
Submit(change);
|
||||
|
||||
if (commitImmediately)
|
||||
UpdateState();
|
||||
}
|
||||
|
||||
protected override void UpdateState()
|
||||
{
|
||||
if (isRestoring)
|
||||
return;
|
||||
|
||||
if (!HasUncommittedChanges)
|
||||
{
|
||||
Logger.Log("Nothing to commit");
|
||||
return;
|
||||
}
|
||||
|
||||
undoStack.Push(currentTransaction);
|
||||
redoStack.Clear();
|
||||
|
||||
Logger.Log($"Added {currentTransaction.UndoChanges.Count} change(s) to undo stack");
|
||||
|
||||
currentTransaction = new Transaction();
|
||||
|
||||
historyChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Undoes the last transaction from the undo stack.
|
||||
/// Returns false if there are is nothing to undo.
|
||||
/// </summary>
|
||||
public bool Undo()
|
||||
{
|
||||
if (undoStack.Count == 0)
|
||||
return false;
|
||||
|
||||
var transaction = undoStack.Pop();
|
||||
|
||||
revertTransaction(transaction);
|
||||
|
||||
redoStack.Push(transaction);
|
||||
|
||||
historyChanged();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Redoes the last transaction from the redo stack.
|
||||
/// Returns false if there are is nothing to redo.
|
||||
/// </summary>
|
||||
public bool Redo()
|
||||
{
|
||||
if (redoStack.Count == 0)
|
||||
return false;
|
||||
|
||||
var transaction = redoStack.Pop();
|
||||
|
||||
applyTransaction(transaction);
|
||||
|
||||
undoStack.Push(transaction);
|
||||
|
||||
historyChanged();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void revertTransaction(Transaction transaction)
|
||||
{
|
||||
isRestoring = true;
|
||||
editorBeatmap.BeginChange();
|
||||
|
||||
foreach (var change in transaction.UndoChanges.Reverse())
|
||||
change.Revert();
|
||||
|
||||
foreach (var hitObject in transaction.HitObjectUpdates)
|
||||
editorBeatmap.Update(hitObject);
|
||||
|
||||
editorBeatmap.EndChange();
|
||||
isRestoring = false;
|
||||
}
|
||||
|
||||
private void applyTransaction(Transaction transaction)
|
||||
{
|
||||
isRestoring = true;
|
||||
editorBeatmap.BeginChange();
|
||||
|
||||
foreach (var change in transaction.UndoChanges)
|
||||
change.Apply();
|
||||
|
||||
foreach (var hitObject in transaction.HitObjectUpdates)
|
||||
editorBeatmap.Update(hitObject);
|
||||
|
||||
editorBeatmap.EndChange();
|
||||
isRestoring = false;
|
||||
}
|
||||
|
||||
private void historyChanged()
|
||||
{
|
||||
CanUndo.Value = undoStack.Count > 0;
|
||||
CanRedo.Value = redoStack.Count > 0;
|
||||
}
|
||||
|
||||
private void record(IRevertibleChange change)
|
||||
{
|
||||
currentTransaction.Add(change);
|
||||
}
|
||||
|
||||
public void RecordUpdate(HitObject hitObject)
|
||||
{
|
||||
currentTransaction.RecordUpdate(hitObject);
|
||||
}
|
||||
|
||||
private readonly struct Transaction
|
||||
{
|
||||
public Transaction()
|
||||
{
|
||||
undoChanges = new List<IRevertibleChange>();
|
||||
}
|
||||
|
||||
private readonly List<IRevertibleChange> undoChanges;
|
||||
|
||||
private readonly HashSet<HitObject> hitObjectUpdates = new HashSet<HitObject>();
|
||||
|
||||
/// <summary>
|
||||
/// The changes to undo the given transaction.
|
||||
/// Stored in reverse order of original changes to match execution order when undoing.
|
||||
/// </summary>
|
||||
public IReadOnlyList<IRevertibleChange> UndoChanges => undoChanges;
|
||||
|
||||
public IReadOnlySet<HitObject> HitObjectUpdates => hitObjectUpdates;
|
||||
|
||||
public void Add(IRevertibleChange change)
|
||||
{
|
||||
undoChanges.Add(change);
|
||||
}
|
||||
|
||||
public void RecordUpdate(HitObject hitObject)
|
||||
{
|
||||
hitObjectUpdates.Add(hitObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user