diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs
index 468d8ae9f5..1d46b8ff8a 100644
--- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs
+++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs
@@ -28,11 +28,6 @@ public partial class OsuSelectionHandler : EditorSelectionHandler
[Resolved(CanBeNull = true)]
private IDistanceSnapProvider? snapProvider { get; set; }
- ///
- /// During a transform, the initial origin is stored so it can be used throughout the operation.
- ///
- private Vector2? referenceOrigin;
-
///
/// During a transform, the initial path types of a single selected slider are stored so they
/// can be maintained throughout the operation.
@@ -54,7 +49,6 @@ protected override void OnSelectionChanged()
protected override void OnOperationEnded()
{
base.OnOperationEnded();
- referenceOrigin = null;
referencePathTypes = null;
}
@@ -170,28 +164,10 @@ private static void adjustScaleFromAnchor(ref Vector2 scale, Anchor reference)
if ((reference & Anchor.y0) > 0) scale.Y = -scale.Y;
}
- public override bool HandleRotation(float delta)
+ public override SelectionRotationHandler CreateRotationHandler() => new OsuSelectionRotationHandler(ChangeHandler)
{
- var hitObjects = selectedMovableObjects;
-
- Quad quad = GeometryUtils.GetSurroundingQuad(hitObjects);
-
- referenceOrigin ??= quad.Centre;
-
- foreach (var h in hitObjects)
- {
- h.Position = GeometryUtils.RotatePointAroundOrigin(h.Position, referenceOrigin.Value, delta);
-
- if (h is IHasPath path)
- {
- foreach (PathControlPoint cp in path.Path.ControlPoints)
- cp.Position = GeometryUtils.RotatePointAroundOrigin(cp.Position, Vector2.Zero, delta);
- }
- }
-
- // this isn't always the case but let's be lenient for now.
- return true;
- }
+ SelectedItems = { BindTarget = SelectedItems }
+ };
private void scaleSlider(Slider slider, Vector2 scale)
{
diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs
new file mode 100644
index 0000000000..0eb7637786
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs
@@ -0,0 +1,98 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using osu.Framework.Bindables;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Screens.Edit;
+using osu.Game.Screens.Edit.Compose.Components;
+using osu.Game.Utils;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Edit
+{
+ public class OsuSelectionRotationHandler : SelectionRotationHandler
+ {
+ private readonly IEditorChangeHandler? changeHandler;
+
+ public BindableList SelectedItems { get; } = new BindableList();
+
+ public OsuSelectionRotationHandler(IEditorChangeHandler? changeHandler)
+ {
+ this.changeHandler = changeHandler;
+
+ SelectedItems.CollectionChanged += (_, __) => updateState();
+ }
+
+ private void updateState()
+ {
+ var quad = GeometryUtils.GetSurroundingQuad(selectedMovableObjects);
+ CanRotate.Value = quad.Width > 0 || quad.Height > 0;
+ }
+
+ private OsuHitObject[]? objectsInRotation;
+
+ private Vector2? defaultOrigin;
+ private Dictionary? originalPositions;
+ private Dictionary? originalPathControlPointPositions;
+
+ public override void Begin()
+ {
+ if (objectsInRotation != null)
+ throw new InvalidOperationException($"Cannot {nameof(Begin)} a rotate operation while another is in progress!");
+
+ changeHandler?.BeginChange();
+
+ objectsInRotation = selectedMovableObjects.ToArray();
+ defaultOrigin = GeometryUtils.GetSurroundingQuad(objectsInRotation).Centre;
+ originalPositions = objectsInRotation.ToDictionary(obj => obj, obj => obj.Position);
+ originalPathControlPointPositions = objectsInRotation.OfType().ToDictionary(
+ obj => obj,
+ obj => obj.Path.ControlPoints.Select(point => point.Position).ToArray());
+ }
+
+ public override void Update(float rotation, Vector2? origin = null)
+ {
+ if (objectsInRotation == null)
+ throw new InvalidOperationException($"Cannot {nameof(Update)} a rotate operation without calling {nameof(Begin)} first!");
+
+ Debug.Assert(originalPositions != null && originalPathControlPointPositions != null && defaultOrigin != null);
+
+ Vector2 actualOrigin = origin ?? defaultOrigin.Value;
+
+ foreach (var ho in objectsInRotation)
+ {
+ ho.Position = GeometryUtils.RotatePointAroundOrigin(originalPositions[ho], actualOrigin, rotation);
+
+ 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);
+ }
+ }
+ }
+
+ public override void Commit()
+ {
+ if (objectsInRotation == null)
+ throw new InvalidOperationException($"Cannot {nameof(Commit)} a rotate operation without calling {nameof(Begin)} first!");
+
+ changeHandler?.EndChange();
+
+ objectsInRotation = null;
+ originalPositions = null;
+ originalPathControlPointPositions = null;
+ defaultOrigin = null;
+ }
+
+ private IEnumerable selectedMovableObjects => SelectedItems.Cast()
+ .Where(h => h is not Spinner);
+ }
+}