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

Merge pull request #24341 from bdach/selection-operations-refactor

Refactor rotation handling in editor to facilitate reuse
This commit is contained in:
Dean Herbert 2023-07-31 17:18:05 +09:00 committed by GitHub
commit a3afb198a1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 541 additions and 226 deletions

View File

@ -17,6 +17,7 @@ using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Utils;
using osuTK;
using osuTK.Input;
@ -27,11 +28,6 @@ namespace osu.Game.Rulesets.Osu.Edit
[Resolved(CanBeNull = true)]
private IDistanceSnapProvider? snapProvider { get; set; }
/// <summary>
/// During a transform, the initial origin is stored so it can be used throughout the operation.
/// </summary>
private Vector2? referenceOrigin;
/// <summary>
/// During a transform, the initial path types of a single selected slider are stored so they
/// can be maintained throughout the operation.
@ -42,9 +38,8 @@ namespace osu.Game.Rulesets.Osu.Edit
{
base.OnSelectionChanged();
Quad quad = selectedMovableObjects.Length > 0 ? getSurroundingQuad(selectedMovableObjects) : new Quad();
Quad quad = selectedMovableObjects.Length > 0 ? GeometryUtils.GetSurroundingQuad(selectedMovableObjects) : new Quad();
SelectionBox.CanRotate = quad.Width > 0 || quad.Height > 0;
SelectionBox.CanFlipX = SelectionBox.CanScaleX = quad.Width > 0;
SelectionBox.CanFlipY = SelectionBox.CanScaleY = quad.Height > 0;
SelectionBox.CanReverse = EditorBeatmap.SelectedHitObjects.Count > 1 || EditorBeatmap.SelectedHitObjects.Any(s => s is Slider);
@ -53,7 +48,6 @@ namespace osu.Game.Rulesets.Osu.Edit
protected override void OnOperationEnded()
{
base.OnOperationEnded();
referenceOrigin = null;
referencePathTypes = null;
}
@ -109,13 +103,13 @@ namespace osu.Game.Rulesets.Osu.Edit
{
var hitObjects = selectedMovableObjects;
var flipQuad = flipOverOrigin ? new Quad(0, 0, OsuPlayfield.BASE_SIZE.X, OsuPlayfield.BASE_SIZE.Y) : getSurroundingQuad(hitObjects);
var flipQuad = flipOverOrigin ? new Quad(0, 0, OsuPlayfield.BASE_SIZE.X, OsuPlayfield.BASE_SIZE.Y) : GeometryUtils.GetSurroundingQuad(hitObjects);
bool didFlip = false;
foreach (var h in hitObjects)
{
var flippedPosition = GetFlippedPosition(direction, flipQuad, h.Position);
var flippedPosition = GeometryUtils.GetFlippedPosition(direction, flipQuad, h.Position);
if (!Precision.AlmostEquals(flippedPosition, h.Position))
{
@ -169,34 +163,13 @@ namespace osu.Game.Rulesets.Osu.Edit
if ((reference & Anchor.y0) > 0) scale.Y = -scale.Y;
}
public override bool HandleRotation(float delta)
{
var hitObjects = selectedMovableObjects;
Quad quad = getSurroundingQuad(hitObjects);
referenceOrigin ??= quad.Centre;
foreach (var h in hitObjects)
{
h.Position = RotatePointAroundOrigin(h.Position, referenceOrigin.Value, delta);
if (h is IHasPath path)
{
foreach (PathControlPoint cp in path.Path.ControlPoints)
cp.Position = RotatePointAroundOrigin(cp.Position, Vector2.Zero, delta);
}
}
// this isn't always the case but let's be lenient for now.
return true;
}
public override SelectionRotationHandler CreateRotationHandler() => new OsuSelectionRotationHandler();
private void scaleSlider(Slider slider, Vector2 scale)
{
referencePathTypes ??= slider.Path.ControlPoints.Select(p => p.Type).ToList();
Quad sliderQuad = GetSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position));
Quad sliderQuad = GeometryUtils.GetSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position));
// Limit minimum distance between control points after scaling to almost 0. Less than 0 causes the slider to flip, exactly 0 causes a crash through division by 0.
scale = Vector2.ComponentMax(new Vector2(Precision.FLOAT_EPSILON), sliderQuad.Size + scale) - sliderQuad.Size;
@ -222,7 +195,7 @@ namespace osu.Game.Rulesets.Osu.Edit
slider.SnapTo(snapProvider);
//if sliderhead or sliderend end up outside playfield, revert scaling.
Quad scaledQuad = getSurroundingQuad(new OsuHitObject[] { slider });
Quad scaledQuad = GeometryUtils.GetSurroundingQuad(new OsuHitObject[] { slider });
(bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad);
if (xInBounds && yInBounds && slider.Path.HasValidLength)
@ -238,10 +211,10 @@ namespace osu.Game.Rulesets.Osu.Edit
private void scaleHitObjects(OsuHitObject[] hitObjects, Anchor reference, Vector2 scale)
{
scale = getClampedScale(hitObjects, reference, scale);
Quad selectionQuad = getSurroundingQuad(hitObjects);
Quad selectionQuad = GeometryUtils.GetSurroundingQuad(hitObjects);
foreach (var h in hitObjects)
h.Position = GetScaledPosition(reference, scale, selectionQuad, h.Position);
h.Position = GeometryUtils.GetScaledPosition(reference, scale, selectionQuad, h.Position);
}
private (bool X, bool Y) isQuadInBounds(Quad quad)
@ -256,7 +229,7 @@ namespace osu.Game.Rulesets.Osu.Edit
{
var hitObjects = selectedMovableObjects;
Quad quad = getSurroundingQuad(hitObjects);
Quad quad = GeometryUtils.GetSurroundingQuad(hitObjects);
Vector2 delta = Vector2.Zero;
@ -286,7 +259,7 @@ namespace osu.Game.Rulesets.Osu.Edit
float xOffset = ((reference & Anchor.x0) > 0) ? -scale.X : 0;
float yOffset = ((reference & Anchor.y0) > 0) ? -scale.Y : 0;
Quad selectionQuad = getSurroundingQuad(hitObjects);
Quad selectionQuad = GeometryUtils.GetSurroundingQuad(hitObjects);
//todo: this is not always correct for selections involving sliders. This approximation assumes each point is scaled independently, but sliderends move with the sliderhead.
Quad scaledQuad = new Quad(selectionQuad.TopLeft.X + xOffset, selectionQuad.TopLeft.Y + yOffset, selectionQuad.Width + scale.X, selectionQuad.Height + scale.Y);
@ -311,26 +284,6 @@ namespace osu.Game.Rulesets.Osu.Edit
return scale;
}
/// <summary>
/// Returns a gamefield-space quad surrounding the provided hit objects.
/// </summary>
/// <param name="hitObjects">The hit objects to calculate a quad for.</param>
private Quad getSurroundingQuad(OsuHitObject[] hitObjects) =>
GetSurroundingQuad(hitObjects.SelectMany(h =>
{
if (h is IHasPath path)
{
return new[]
{
h.Position,
// can't use EndPosition for reverse slider cases.
h.Position + path.Path.PositionAt(1)
};
}
return new[] { h.Position };
}));
/// <summary>
/// All osu! hitobjects which can be moved/rotated/scaled.
/// </summary>

View File

@ -0,0 +1,107 @@
// 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;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
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 partial class OsuSelectionRotationHandler : SelectionRotationHandler
{
[Resolved]
private IEditorChangeHandler? changeHandler { get; set; }
private BindableList<HitObject> selectedItems { get; } = new BindableList<HitObject>();
[BackgroundDependencyLoader]
private void load(EditorBeatmap editorBeatmap)
{
selectedItems.BindTo(editorBeatmap.SelectedHitObjects);
}
protected override void LoadComplete()
{
base.LoadComplete();
selectedItems.CollectionChanged += (_, __) => updateState();
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<OsuHitObject, Vector2>? originalPositions;
private Dictionary<IHasPath, Vector2[]>? 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<IHasPath>().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<OsuHitObject> selectedMovableObjects => selectedItems.Cast<OsuHitObject>()
.Where(h => h is not Spinner);
}
}

View File

@ -3,8 +3,11 @@
#nullable disable
using System;
using System.Linq;
using JetBrains.Annotations;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
@ -20,6 +23,14 @@ namespace osu.Game.Tests.Visual.Editing
private Container selectionArea;
private SelectionBox selectionBox;
[Cached(typeof(SelectionRotationHandler))]
private TestSelectionRotationHandler rotationHandler;
public TestSceneComposeSelectBox()
{
rotationHandler = new TestSelectionRotationHandler(() => selectionArea);
}
[SetUp]
public void SetUp() => Schedule(() =>
{
@ -34,13 +45,11 @@ namespace osu.Game.Tests.Visual.Editing
{
RelativeSizeAxes = Axes.Both,
CanRotate = true,
CanScaleX = true,
CanScaleY = true,
CanFlipX = true,
CanFlipY = true,
OnRotation = handleRotation,
OnScale = handleScale
}
}
@ -71,11 +80,48 @@ namespace osu.Game.Tests.Visual.Editing
return true;
}
private bool handleRotation(float angle)
private partial class TestSelectionRotationHandler : SelectionRotationHandler
{
// kinda silly and wrong, but just showing that the drag handles work.
selectionArea.Rotation += angle;
return true;
private readonly Func<Container> getTargetContainer;
public TestSelectionRotationHandler(Func<Container> getTargetContainer)
{
this.getTargetContainer = getTargetContainer;
CanRotate.Value = true;
}
[CanBeNull]
private Container targetContainer;
private float? initialRotation;
public override void Begin()
{
if (targetContainer != null)
throw new InvalidOperationException($"Cannot {nameof(Begin)} a rotate operation while another is in progress!");
targetContainer = getTargetContainer();
initialRotation = targetContainer!.Rotation;
}
public override void Update(float rotation, Vector2? origin = null)
{
if (targetContainer == null)
throw new InvalidOperationException($"Cannot {nameof(Update)} a rotate operation without calling {nameof(Begin)} first!");
// kinda silly and wrong, but just showing that the drag handles work.
targetContainer.Rotation = initialRotation!.Value + rotation;
}
public override void Commit()
{
if (targetContainer == null)
throw new InvalidOperationException($"Cannot {nameof(Commit)} a rotate operation without calling {nameof(Begin)} first!");
targetContainer = null;
initialRotation = null;
}
}
[Test]

View File

@ -16,6 +16,7 @@ using osu.Game.Rulesets.Edit;
using osu.Game.Screens.Edit.Components.Menus;
using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Skinning;
using osu.Game.Utils;
using osuTK;
namespace osu.Game.Overlays.SkinEditor
@ -25,31 +26,10 @@ namespace osu.Game.Overlays.SkinEditor
[Resolved]
private SkinEditor skinEditor { get; set; } = null!;
public override bool HandleRotation(float angle)
public override SelectionRotationHandler CreateRotationHandler() => new SkinSelectionRotationHandler
{
if (SelectedBlueprints.Count == 1)
{
// for single items, rotate around the origin rather than the selection centre.
((Drawable)SelectedBlueprints.First().Item).Rotation += angle;
}
else
{
var selectionQuad = getSelectionQuad();
foreach (var b in SelectedBlueprints)
{
var drawableItem = (Drawable)b.Item;
var rotatedPosition = RotatePointAroundOrigin(b.ScreenSpaceSelectionPoint, selectionQuad.Centre, angle);
updateDrawablePosition(drawableItem, rotatedPosition);
drawableItem.Rotation += angle;
}
}
// this isn't always the case but let's be lenient for now.
return true;
}
UpdatePosition = updateDrawablePosition
};
public override bool HandleScale(Vector2 scale, Anchor anchor)
{
@ -137,7 +117,7 @@ namespace osu.Game.Overlays.SkinEditor
{
var drawableItem = (Drawable)b.Item;
var flippedPosition = GetFlippedPosition(direction, flipOverOrigin ? drawableItem.Parent.ScreenSpaceDrawQuad : selectionQuad, b.ScreenSpaceSelectionPoint);
var flippedPosition = GeometryUtils.GetFlippedPosition(direction, flipOverOrigin ? drawableItem.Parent.ScreenSpaceDrawQuad : selectionQuad, b.ScreenSpaceSelectionPoint);
updateDrawablePosition(drawableItem, flippedPosition);
@ -171,7 +151,6 @@ namespace osu.Game.Overlays.SkinEditor
{
base.OnSelectionChanged();
SelectionBox.CanRotate = true;
SelectionBox.CanScaleX = true;
SelectionBox.CanScaleY = true;
SelectionBox.CanFlipX = true;
@ -275,7 +254,7 @@ namespace osu.Game.Overlays.SkinEditor
/// </summary>
/// <returns></returns>
private Quad getSelectionQuad() =>
GetSurroundingQuad(SelectedBlueprints.SelectMany(b => b.Item.ScreenSpaceDrawQuad.GetVertices().ToArray()));
GeometryUtils.GetSurroundingQuad(SelectedBlueprints.SelectMany(b => b.Item.ScreenSpaceDrawQuad.GetVertices().ToArray()));
private void applyFixedAnchors(Anchor anchor)
{

View File

@ -0,0 +1,104 @@
// 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;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Skinning;
using osu.Game.Utils;
using osuTK;
namespace osu.Game.Overlays.SkinEditor
{
public partial class SkinSelectionRotationHandler : SelectionRotationHandler
{
public Action<Drawable, Vector2> UpdatePosition { get; init; } = null!;
[Resolved]
private IEditorChangeHandler? changeHandler { get; set; }
private BindableList<ISerialisableDrawable> selectedItems { get; } = new BindableList<ISerialisableDrawable>();
[BackgroundDependencyLoader]
private void load(SkinEditor skinEditor)
{
selectedItems.BindTo(skinEditor.SelectedComponents);
}
protected override void LoadComplete()
{
base.LoadComplete();
selectedItems.CollectionChanged += (_, __) => updateState();
updateState();
}
private void updateState()
{
CanRotate.Value = selectedItems.Count > 0;
}
private Drawable[]? objectsInRotation;
private Vector2? defaultOrigin;
private Dictionary<Drawable, float>? originalRotations;
private Dictionary<Drawable, Vector2>? originalPositions;
public override void Begin()
{
if (objectsInRotation != null)
throw new InvalidOperationException($"Cannot {nameof(Begin)} a rotate operation while another is in progress!");
changeHandler?.BeginChange();
objectsInRotation = selectedItems.Cast<Drawable>().ToArray();
originalRotations = objectsInRotation.ToDictionary(d => d, d => d.Rotation);
originalPositions = objectsInRotation.ToDictionary(d => d, d => d.ToScreenSpace(d.OriginPosition));
defaultOrigin = GeometryUtils.GetSurroundingQuad(objectsInRotation.SelectMany(d => d.ScreenSpaceDrawQuad.GetVertices().ToArray())).Centre;
}
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(originalRotations != null && originalPositions != null && defaultOrigin != null);
if (objectsInRotation.Length == 1 && origin == null)
{
// for single items, rotate around the origin rather than the selection centre by default.
objectsInRotation[0].Rotation = originalRotations.Single().Value + rotation;
return;
}
var actualOrigin = origin ?? defaultOrigin.Value;
foreach (var drawableItem in objectsInRotation)
{
var rotatedPosition = GeometryUtils.RotatePointAroundOrigin(originalPositions[drawableItem], actualOrigin, rotation);
UpdatePosition(drawableItem, rotatedPosition);
drawableItem.Rotation = originalRotations[drawableItem] + 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;
originalRotations = null;
defaultOrigin = null;
}
}
}

View File

@ -3,6 +3,7 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@ -22,7 +23,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
private const float button_padding = 5;
public Func<float, bool>? OnRotation;
[Resolved]
private SelectionRotationHandler? rotationHandler { get; set; }
public Func<Vector2, Anchor, bool>? OnScale;
public Func<Direction, bool, bool>? OnFlip;
public Func<bool>? OnReverse;
@ -51,22 +54,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
}
}
private bool canRotate;
/// <summary>
/// Whether rotation support should be enabled.
/// </summary>
public bool CanRotate
{
get => canRotate;
set
{
if (canRotate == value) return;
canRotate = value;
recreate();
}
}
private readonly IBindable<bool> canRotate = new BindableBool();
private bool canScaleX;
@ -161,7 +149,13 @@ namespace osu.Game.Screens.Edit.Compose.Components
private OsuColour colours { get; set; } = null!;
[BackgroundDependencyLoader]
private void load() => recreate();
private void load()
{
if (rotationHandler != null)
canRotate.BindTo(rotationHandler.CanRotate);
canRotate.BindValueChanged(_ => recreate(), true);
}
protected override bool OnKeyDown(KeyDownEvent e)
{
@ -174,10 +168,10 @@ namespace osu.Game.Screens.Edit.Compose.Components
return CanReverse && reverseButton?.TriggerClick() == true;
case Key.Comma:
return CanRotate && rotateCounterClockwiseButton?.TriggerClick() == true;
return canRotate.Value && rotateCounterClockwiseButton?.TriggerClick() == true;
case Key.Period:
return CanRotate && rotateClockwiseButton?.TriggerClick() == true;
return canRotate.Value && rotateClockwiseButton?.TriggerClick() == true;
}
return base.OnKeyDown(e);
@ -254,14 +248,14 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (CanScaleY) addYScaleComponents();
if (CanFlipX) addXFlipComponents();
if (CanFlipY) addYFlipComponents();
if (CanRotate) addRotationComponents();
if (canRotate.Value) addRotationComponents();
if (CanReverse) reverseButton = addButton(FontAwesome.Solid.Backward, "Reverse pattern (Ctrl-G)", () => OnReverse?.Invoke());
}
private void addRotationComponents()
{
rotateCounterClockwiseButton = addButton(FontAwesome.Solid.Undo, "Rotate 90 degrees counter-clockwise (Ctrl-<)", () => OnRotation?.Invoke(-90));
rotateClockwiseButton = addButton(FontAwesome.Solid.Redo, "Rotate 90 degrees clockwise (Ctrl->)", () => OnRotation?.Invoke(90));
rotateCounterClockwiseButton = addButton(FontAwesome.Solid.Undo, "Rotate 90 degrees counter-clockwise (Ctrl-<)", () => rotationHandler?.Rotate(-90));
rotateClockwiseButton = addButton(FontAwesome.Solid.Redo, "Rotate 90 degrees clockwise (Ctrl->)", () => rotationHandler?.Rotate(90));
addRotateHandle(Anchor.TopLeft);
addRotateHandle(Anchor.TopRight);
@ -331,7 +325,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
var handle = new SelectionBoxRotationHandle
{
Anchor = anchor,
HandleRotate = angle => OnRotation?.Invoke(angle)
};
handle.OperationStarted += operationStarted;

View File

@ -1,8 +1,6 @@
// 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.
#nullable disable
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@ -15,24 +13,25 @@ using osu.Framework.Localisation;
using osu.Game.Localisation;
using osuTK;
using osuTK.Graphics;
using Key = osuTK.Input.Key;
using osuTK.Input;
namespace osu.Game.Screens.Edit.Compose.Components
{
public partial class SelectionBoxRotationHandle : SelectionBoxDragHandle, IHasTooltip
{
public Action<float> HandleRotate { get; set; }
public LocalisableString TooltipText { get; private set; }
private SpriteIcon icon;
private SpriteIcon icon = null!;
private const float snap_step = 15;
private readonly Bindable<float?> cumulativeRotation = new Bindable<float?>();
[Resolved]
private SelectionBox selectionBox { get; set; }
private SelectionBox selectionBox { get; set; } = null!;
[Resolved]
private SelectionRotationHandler? rotationHandler { get; set; }
[BackgroundDependencyLoader]
private void load()
@ -63,10 +62,10 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected override bool OnDragStart(DragStartEvent e)
{
bool handle = base.OnDragStart(e);
if (handle)
cumulativeRotation.Value = 0;
return handle;
if (rotationHandler == null) return false;
rotationHandler.Begin();
return true;
}
protected override void OnDrag(DragEvent e)
@ -99,7 +98,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected override void OnDragEnd(DragEndEvent e)
{
base.OnDragEnd(e);
rotationHandler?.Commit();
UpdateHoverState();
cumulativeRotation.Value = null;
rawCumulativeRotation = 0;
TooltipText = default;
@ -116,14 +117,12 @@ namespace osu.Game.Screens.Edit.Compose.Components
private void applyRotation(bool shouldSnap)
{
float oldRotation = cumulativeRotation.Value ?? 0;
float newRotation = shouldSnap ? snap(rawCumulativeRotation, snap_step) : MathF.Round(rawCumulativeRotation);
newRotation = (newRotation - 180) % 360 + 180;
cumulativeRotation.Value = newRotation;
HandleRotate?.Invoke(newRotation - oldRotation);
rotationHandler?.Update(newRotation);
TooltipText = shouldSnap ? EditorStrings.RotationSnapped(newRotation) : EditorStrings.RotationUnsnapped(newRotation);
}

View File

@ -16,7 +16,6 @@ using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
using osu.Game.Resources.Localisation.Web;
@ -56,6 +55,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
[Resolved(CanBeNull = true)]
protected IEditorChangeHandler ChangeHandler { get; private set; }
protected SelectionRotationHandler RotationHandler { get; private set; }
protected SelectionHandler()
{
selectedBlueprints = new List<SelectionBlueprint<T>>();
@ -64,10 +65,21 @@ namespace osu.Game.Screens.Edit.Compose.Components
AlwaysPresent = true;
}
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
dependencies.CacheAs(RotationHandler = CreateRotationHandler());
return dependencies;
}
[BackgroundDependencyLoader]
private void load()
{
InternalChild = SelectionBox = CreateSelectionBox();
AddRangeInternal(new Drawable[]
{
RotationHandler,
SelectionBox = CreateSelectionBox(),
});
SelectedItems.CollectionChanged += (_, _) =>
{
@ -81,7 +93,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
OperationStarted = OnOperationBegan,
OperationEnded = OnOperationEnded,
OnRotation = HandleRotation,
OnScale = HandleScale,
OnFlip = HandleFlip,
OnReverse = HandleReverse,
@ -133,6 +144,11 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// <returns>Whether any items could be rotated.</returns>
public virtual bool HandleRotation(float angle) => false;
/// <summary>
/// Creates the handler to use for rotation operations.
/// </summary>
public virtual SelectionRotationHandler CreateRotationHandler() => new SelectionRotationHandler();
/// <summary>
/// Handles the selected items being scaled.
/// </summary>
@ -401,98 +417,5 @@ namespace osu.Game.Screens.Edit.Compose.Components
=> Enumerable.Empty<MenuItem>();
#endregion
#region Helper Methods
/// <summary>
/// Rotate a point around an arbitrary origin.
/// </summary>
/// <param name="point">The point.</param>
/// <param name="origin">The centre origin to rotate around.</param>
/// <param name="angle">The angle to rotate (in degrees).</param>
protected static Vector2 RotatePointAroundOrigin(Vector2 point, Vector2 origin, float angle)
{
angle = -angle;
point.X -= origin.X;
point.Y -= origin.Y;
Vector2 ret;
ret.X = point.X * MathF.Cos(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Sin(MathUtils.DegreesToRadians(angle));
ret.Y = point.X * -MathF.Sin(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Cos(MathUtils.DegreesToRadians(angle));
ret.X += origin.X;
ret.Y += origin.Y;
return ret;
}
/// <summary>
/// Given a flip direction, a surrounding quad for all selected objects, and a position,
/// will return the flipped position in screen space coordinates.
/// </summary>
protected static Vector2 GetFlippedPosition(Direction direction, Quad quad, Vector2 position)
{
var centre = quad.Centre;
switch (direction)
{
case Direction.Horizontal:
position.X = centre.X - (position.X - centre.X);
break;
case Direction.Vertical:
position.Y = centre.Y - (position.Y - centre.Y);
break;
}
return position;
}
/// <summary>
/// Given a scale vector, a surrounding quad for all selected objects, and a position,
/// will return the scaled position in screen space coordinates.
/// </summary>
protected static Vector2 GetScaledPosition(Anchor reference, Vector2 scale, Quad selectionQuad, Vector2 position)
{
// adjust the direction of scale depending on which side the user is dragging.
float xOffset = ((reference & Anchor.x0) > 0) ? -scale.X : 0;
float yOffset = ((reference & Anchor.y0) > 0) ? -scale.Y : 0;
// guard against no-ops and NaN.
if (scale.X != 0 && selectionQuad.Width > 0)
position.X = selectionQuad.TopLeft.X + xOffset + (position.X - selectionQuad.TopLeft.X) / selectionQuad.Width * (selectionQuad.Width + scale.X);
if (scale.Y != 0 && selectionQuad.Height > 0)
position.Y = selectionQuad.TopLeft.Y + yOffset + (position.Y - selectionQuad.TopLeft.Y) / selectionQuad.Height * (selectionQuad.Height + scale.Y);
return position;
}
/// <summary>
/// Returns a quad surrounding the provided points.
/// </summary>
/// <param name="points">The points to calculate a quad for.</param>
protected static Quad GetSurroundingQuad(IEnumerable<Vector2> points)
{
if (!points.Any())
return new Quad();
Vector2 minPosition = new Vector2(float.MaxValue, float.MaxValue);
Vector2 maxPosition = new Vector2(float.MinValue, float.MinValue);
// Go through all hitobjects to make sure they would remain in the bounds of the editor after movement, before any movement is attempted
foreach (var p in points)
{
minPosition = Vector2.ComponentMin(minPosition, p);
maxPosition = Vector2.ComponentMax(maxPosition, p);
}
Vector2 size = maxPosition - minPosition;
return new Quad(minPosition.X, minPosition.Y, size.X, size.Y);
}
#endregion
}
}

View File

@ -0,0 +1,85 @@
// 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.Framework.Graphics;
using osuTK;
namespace osu.Game.Screens.Edit.Compose.Components
{
/// <summary>
/// Base handler for editor rotation operations.
/// </summary>
public partial class SelectionRotationHandler : Component
{
/// <summary>
/// Whether the rotation can currently be performed.
/// </summary>
public Bindable<bool> CanRotate { get; private set; } = new BindableBool();
/// <summary>
/// Performs a single, instant, atomic rotation operation.
/// </summary>
/// <remarks>
/// This method is intended to be used in atomic contexts (such as when pressing a single button).
/// For continuous operations, see the <see cref="Begin"/>-<see cref="Update"/>-<see cref="Commit"/> flow.
/// </remarks>
/// <param name="rotation">Rotation to apply in degrees.</param>
/// <param name="origin">
/// The origin point to rotate around.
/// If the default <see langword="null"/> value is supplied, a sane implementation-defined default will be used.
/// </param>
public void Rotate(float rotation, Vector2? origin = null)
{
Begin();
Update(rotation, origin);
Commit();
}
/// <summary>
/// Begins a continuous rotation operation.
/// </summary>
/// <remarks>
/// This flow is intended to be used when a rotation operation is made incrementally (such as when dragging a rotation handle or slider).
/// For instantaneous, atomic operations, use the convenience <see cref="Rotate"/> method.
/// </remarks>
public virtual void Begin()
{
}
/// <summary>
/// Updates a continuous rotation operation.
/// Must be preceded by a <see cref="Begin"/> call.
/// </summary>
/// <remarks>
/// <para>
/// This flow is intended to be used when a rotation operation is made incrementally (such as when dragging a rotation handle or slider).
/// As such, the values of <paramref name="rotation"/> and <paramref name="origin"/> supplied should be relative to the state of the objects being rotated
/// when <see cref="Begin"/> was called, rather than instantaneous deltas.
/// </para>
/// <para>
/// For instantaneous, atomic operations, use the convenience <see cref="Rotate"/> method.
/// </para>
/// </remarks>
/// <param name="rotation">Rotation to apply in degrees.</param>
/// <param name="origin">
/// The origin point to rotate around.
/// If the default <see langword="null"/> value is supplied, a sane implementation-defined default will be used.
/// </param>
public virtual void Update(float rotation, Vector2? origin = null)
{
}
/// <summary>
/// Ends a continuous rotation operation.
/// Must be preceded by a <see cref="Begin"/> call.
/// </summary>
/// <remarks>
/// This flow is intended to be used when a rotation operation is made incrementally (such as when dragging a rotation handle or slider).
/// For instantaneous, atomic operations, use the convenience <see cref="Rotate"/> method.
/// </remarks>
public virtual void Commit()
{
}
}
}

View File

@ -0,0 +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;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Utils;
using osu.Game.Rulesets.Objects.Types;
using osuTK;
namespace osu.Game.Utils
{
public static class GeometryUtils
{
/// <summary>
/// Rotate a point around an arbitrary origin.
/// </summary>
/// <param name="point">The point.</param>
/// <param name="origin">The centre origin to rotate around.</param>
/// <param name="angle">The angle to rotate (in degrees).</param>
public static Vector2 RotatePointAroundOrigin(Vector2 point, Vector2 origin, float angle)
{
angle = -angle;
point.X -= origin.X;
point.Y -= origin.Y;
Vector2 ret;
ret.X = point.X * MathF.Cos(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Sin(MathUtils.DegreesToRadians(angle));
ret.Y = point.X * -MathF.Sin(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Cos(MathUtils.DegreesToRadians(angle));
ret.X += origin.X;
ret.Y += origin.Y;
return ret;
}
/// <summary>
/// Given a flip direction, a surrounding quad for all selected objects, and a position,
/// will return the flipped position in screen space coordinates.
/// </summary>
public static Vector2 GetFlippedPosition(Direction direction, Quad quad, Vector2 position)
{
var centre = quad.Centre;
switch (direction)
{
case Direction.Horizontal:
position.X = centre.X - (position.X - centre.X);
break;
case Direction.Vertical:
position.Y = centre.Y - (position.Y - centre.Y);
break;
}
return position;
}
/// <summary>
/// Given a scale vector, a surrounding quad for all selected objects, and a position,
/// will return the scaled position in screen space coordinates.
/// </summary>
public static Vector2 GetScaledPosition(Anchor reference, Vector2 scale, Quad selectionQuad, Vector2 position)
{
// adjust the direction of scale depending on which side the user is dragging.
float xOffset = ((reference & Anchor.x0) > 0) ? -scale.X : 0;
float yOffset = ((reference & Anchor.y0) > 0) ? -scale.Y : 0;
// guard against no-ops and NaN.
if (scale.X != 0 && selectionQuad.Width > 0)
position.X = selectionQuad.TopLeft.X + xOffset + (position.X - selectionQuad.TopLeft.X) / selectionQuad.Width * (selectionQuad.Width + scale.X);
if (scale.Y != 0 && selectionQuad.Height > 0)
position.Y = selectionQuad.TopLeft.Y + yOffset + (position.Y - selectionQuad.TopLeft.Y) / selectionQuad.Height * (selectionQuad.Height + scale.Y);
return position;
}
/// <summary>
/// Returns a quad surrounding the provided points.
/// </summary>
/// <param name="points">The points to calculate a quad for.</param>
public static Quad GetSurroundingQuad(IEnumerable<Vector2> points)
{
if (!points.Any())
return new Quad();
Vector2 minPosition = new Vector2(float.MaxValue, float.MaxValue);
Vector2 maxPosition = new Vector2(float.MinValue, float.MinValue);
// Go through all hitobjects to make sure they would remain in the bounds of the editor after movement, before any movement is attempted
foreach (var p in points)
{
minPosition = Vector2.ComponentMin(minPosition, p);
maxPosition = Vector2.ComponentMax(maxPosition, p);
}
Vector2 size = maxPosition - minPosition;
return new Quad(minPosition.X, minPosition.Y, size.X, size.Y);
}
/// <summary>
/// Returns a gamefield-space quad surrounding the provided hit objects.
/// </summary>
/// <param name="hitObjects">The hit objects to calculate a quad for.</param>
public static Quad GetSurroundingQuad(IEnumerable<IHasPosition> hitObjects) =>
GetSurroundingQuad(hitObjects.SelectMany(h =>
{
if (h is IHasPath path)
{
return new[]
{
h.Position,
// can't use EndPosition for reverse slider cases.
h.Position + path.Path.PositionAt(1)
};
}
return new[] { h.Position };
}));
}
}