mirror of
https://github.com/ppy/osu
synced 2025-03-23 03:16:53 +00:00
Merge pull request #24341 from bdach/selection-operations-refactor
Refactor rotation handling in editor to facilitate reuse
This commit is contained in:
commit
a3afb198a1
@ -17,6 +17,7 @@ using osu.Game.Rulesets.Objects.Types;
|
|||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Rulesets.Osu.UI;
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
using osu.Game.Screens.Edit.Compose.Components;
|
using osu.Game.Screens.Edit.Compose.Components;
|
||||||
|
using osu.Game.Utils;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Input;
|
using osuTK.Input;
|
||||||
|
|
||||||
@ -27,11 +28,6 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
[Resolved(CanBeNull = true)]
|
[Resolved(CanBeNull = true)]
|
||||||
private IDistanceSnapProvider? snapProvider { get; set; }
|
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>
|
/// <summary>
|
||||||
/// During a transform, the initial path types of a single selected slider are stored so they
|
/// During a transform, the initial path types of a single selected slider are stored so they
|
||||||
/// can be maintained throughout the operation.
|
/// can be maintained throughout the operation.
|
||||||
@ -42,9 +38,8 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
{
|
{
|
||||||
base.OnSelectionChanged();
|
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.CanFlipX = SelectionBox.CanScaleX = quad.Width > 0;
|
||||||
SelectionBox.CanFlipY = SelectionBox.CanScaleY = quad.Height > 0;
|
SelectionBox.CanFlipY = SelectionBox.CanScaleY = quad.Height > 0;
|
||||||
SelectionBox.CanReverse = EditorBeatmap.SelectedHitObjects.Count > 1 || EditorBeatmap.SelectedHitObjects.Any(s => s is Slider);
|
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()
|
protected override void OnOperationEnded()
|
||||||
{
|
{
|
||||||
base.OnOperationEnded();
|
base.OnOperationEnded();
|
||||||
referenceOrigin = null;
|
|
||||||
referencePathTypes = null;
|
referencePathTypes = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,13 +103,13 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
{
|
{
|
||||||
var hitObjects = selectedMovableObjects;
|
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;
|
bool didFlip = false;
|
||||||
|
|
||||||
foreach (var h in hitObjects)
|
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))
|
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;
|
if ((reference & Anchor.y0) > 0) scale.Y = -scale.Y;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool HandleRotation(float delta)
|
public override SelectionRotationHandler CreateRotationHandler() => new OsuSelectionRotationHandler();
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void scaleSlider(Slider slider, Vector2 scale)
|
private void scaleSlider(Slider slider, Vector2 scale)
|
||||||
{
|
{
|
||||||
referencePathTypes ??= slider.Path.ControlPoints.Select(p => p.Type).ToList();
|
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.
|
// 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;
|
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);
|
slider.SnapTo(snapProvider);
|
||||||
|
|
||||||
//if sliderhead or sliderend end up outside playfield, revert scaling.
|
//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);
|
(bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad);
|
||||||
|
|
||||||
if (xInBounds && yInBounds && slider.Path.HasValidLength)
|
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)
|
private void scaleHitObjects(OsuHitObject[] hitObjects, Anchor reference, Vector2 scale)
|
||||||
{
|
{
|
||||||
scale = getClampedScale(hitObjects, reference, scale);
|
scale = getClampedScale(hitObjects, reference, scale);
|
||||||
Quad selectionQuad = getSurroundingQuad(hitObjects);
|
Quad selectionQuad = GeometryUtils.GetSurroundingQuad(hitObjects);
|
||||||
|
|
||||||
foreach (var h in 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)
|
private (bool X, bool Y) isQuadInBounds(Quad quad)
|
||||||
@ -256,7 +229,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
{
|
{
|
||||||
var hitObjects = selectedMovableObjects;
|
var hitObjects = selectedMovableObjects;
|
||||||
|
|
||||||
Quad quad = getSurroundingQuad(hitObjects);
|
Quad quad = GeometryUtils.GetSurroundingQuad(hitObjects);
|
||||||
|
|
||||||
Vector2 delta = Vector2.Zero;
|
Vector2 delta = Vector2.Zero;
|
||||||
|
|
||||||
@ -286,7 +259,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
float xOffset = ((reference & Anchor.x0) > 0) ? -scale.X : 0;
|
float xOffset = ((reference & Anchor.x0) > 0) ? -scale.X : 0;
|
||||||
float yOffset = ((reference & Anchor.y0) > 0) ? -scale.Y : 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.
|
//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);
|
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;
|
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>
|
/// <summary>
|
||||||
/// All osu! hitobjects which can be moved/rotated/scaled.
|
/// All osu! hitobjects which can be moved/rotated/scaled.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
107
osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs
Normal file
107
osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -3,8 +3,11 @@
|
|||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using JetBrains.Annotations;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
@ -20,6 +23,14 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
private Container selectionArea;
|
private Container selectionArea;
|
||||||
private SelectionBox selectionBox;
|
private SelectionBox selectionBox;
|
||||||
|
|
||||||
|
[Cached(typeof(SelectionRotationHandler))]
|
||||||
|
private TestSelectionRotationHandler rotationHandler;
|
||||||
|
|
||||||
|
public TestSceneComposeSelectBox()
|
||||||
|
{
|
||||||
|
rotationHandler = new TestSelectionRotationHandler(() => selectionArea);
|
||||||
|
}
|
||||||
|
|
||||||
[SetUp]
|
[SetUp]
|
||||||
public void SetUp() => Schedule(() =>
|
public void SetUp() => Schedule(() =>
|
||||||
{
|
{
|
||||||
@ -34,13 +45,11 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
|
||||||
CanRotate = true,
|
|
||||||
CanScaleX = true,
|
CanScaleX = true,
|
||||||
CanScaleY = true,
|
CanScaleY = true,
|
||||||
CanFlipX = true,
|
CanFlipX = true,
|
||||||
CanFlipY = true,
|
CanFlipY = true,
|
||||||
|
|
||||||
OnRotation = handleRotation,
|
|
||||||
OnScale = handleScale
|
OnScale = handleScale
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -71,11 +80,48 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool handleRotation(float angle)
|
private partial class TestSelectionRotationHandler : SelectionRotationHandler
|
||||||
{
|
{
|
||||||
|
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.
|
// kinda silly and wrong, but just showing that the drag handles work.
|
||||||
selectionArea.Rotation += angle;
|
targetContainer.Rotation = initialRotation!.Value + rotation;
|
||||||
return true;
|
}
|
||||||
|
|
||||||
|
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]
|
[Test]
|
||||||
|
@ -16,6 +16,7 @@ using osu.Game.Rulesets.Edit;
|
|||||||
using osu.Game.Screens.Edit.Components.Menus;
|
using osu.Game.Screens.Edit.Components.Menus;
|
||||||
using osu.Game.Screens.Edit.Compose.Components;
|
using osu.Game.Screens.Edit.Compose.Components;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
|
using osu.Game.Utils;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Overlays.SkinEditor
|
namespace osu.Game.Overlays.SkinEditor
|
||||||
@ -25,31 +26,10 @@ namespace osu.Game.Overlays.SkinEditor
|
|||||||
[Resolved]
|
[Resolved]
|
||||||
private SkinEditor skinEditor { get; set; } = null!;
|
private SkinEditor skinEditor { get; set; } = null!;
|
||||||
|
|
||||||
public override bool HandleRotation(float angle)
|
public override SelectionRotationHandler CreateRotationHandler() => new SkinSelectionRotationHandler
|
||||||
{
|
{
|
||||||
if (SelectedBlueprints.Count == 1)
|
UpdatePosition = updateDrawablePosition
|
||||||
{
|
};
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool HandleScale(Vector2 scale, Anchor anchor)
|
public override bool HandleScale(Vector2 scale, Anchor anchor)
|
||||||
{
|
{
|
||||||
@ -137,7 +117,7 @@ namespace osu.Game.Overlays.SkinEditor
|
|||||||
{
|
{
|
||||||
var drawableItem = (Drawable)b.Item;
|
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);
|
updateDrawablePosition(drawableItem, flippedPosition);
|
||||||
|
|
||||||
@ -171,7 +151,6 @@ namespace osu.Game.Overlays.SkinEditor
|
|||||||
{
|
{
|
||||||
base.OnSelectionChanged();
|
base.OnSelectionChanged();
|
||||||
|
|
||||||
SelectionBox.CanRotate = true;
|
|
||||||
SelectionBox.CanScaleX = true;
|
SelectionBox.CanScaleX = true;
|
||||||
SelectionBox.CanScaleY = true;
|
SelectionBox.CanScaleY = true;
|
||||||
SelectionBox.CanFlipX = true;
|
SelectionBox.CanFlipX = true;
|
||||||
@ -275,7 +254,7 @@ namespace osu.Game.Overlays.SkinEditor
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
private Quad getSelectionQuad() =>
|
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)
|
private void applyFixedAnchors(Anchor anchor)
|
||||||
{
|
{
|
||||||
|
104
osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs
Normal file
104
osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
@ -22,7 +23,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
|||||||
|
|
||||||
private const float button_padding = 5;
|
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<Vector2, Anchor, bool>? OnScale;
|
||||||
public Func<Direction, bool, bool>? OnFlip;
|
public Func<Direction, bool, bool>? OnFlip;
|
||||||
public Func<bool>? OnReverse;
|
public Func<bool>? OnReverse;
|
||||||
@ -51,22 +54,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool canRotate;
|
private readonly IBindable<bool> canRotate = new BindableBool();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether rotation support should be enabled.
|
|
||||||
/// </summary>
|
|
||||||
public bool CanRotate
|
|
||||||
{
|
|
||||||
get => canRotate;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (canRotate == value) return;
|
|
||||||
|
|
||||||
canRotate = value;
|
|
||||||
recreate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool canScaleX;
|
private bool canScaleX;
|
||||||
|
|
||||||
@ -161,7 +149,13 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
|||||||
private OsuColour colours { get; set; } = null!;
|
private OsuColour colours { get; set; } = null!;
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[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)
|
protected override bool OnKeyDown(KeyDownEvent e)
|
||||||
{
|
{
|
||||||
@ -174,10 +168,10 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
|||||||
return CanReverse && reverseButton?.TriggerClick() == true;
|
return CanReverse && reverseButton?.TriggerClick() == true;
|
||||||
|
|
||||||
case Key.Comma:
|
case Key.Comma:
|
||||||
return CanRotate && rotateCounterClockwiseButton?.TriggerClick() == true;
|
return canRotate.Value && rotateCounterClockwiseButton?.TriggerClick() == true;
|
||||||
|
|
||||||
case Key.Period:
|
case Key.Period:
|
||||||
return CanRotate && rotateClockwiseButton?.TriggerClick() == true;
|
return canRotate.Value && rotateClockwiseButton?.TriggerClick() == true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return base.OnKeyDown(e);
|
return base.OnKeyDown(e);
|
||||||
@ -254,14 +248,14 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
|||||||
if (CanScaleY) addYScaleComponents();
|
if (CanScaleY) addYScaleComponents();
|
||||||
if (CanFlipX) addXFlipComponents();
|
if (CanFlipX) addXFlipComponents();
|
||||||
if (CanFlipY) addYFlipComponents();
|
if (CanFlipY) addYFlipComponents();
|
||||||
if (CanRotate) addRotationComponents();
|
if (canRotate.Value) addRotationComponents();
|
||||||
if (CanReverse) reverseButton = addButton(FontAwesome.Solid.Backward, "Reverse pattern (Ctrl-G)", () => OnReverse?.Invoke());
|
if (CanReverse) reverseButton = addButton(FontAwesome.Solid.Backward, "Reverse pattern (Ctrl-G)", () => OnReverse?.Invoke());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addRotationComponents()
|
private void addRotationComponents()
|
||||||
{
|
{
|
||||||
rotateCounterClockwiseButton = addButton(FontAwesome.Solid.Undo, "Rotate 90 degrees counter-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->)", () => OnRotation?.Invoke(90));
|
rotateClockwiseButton = addButton(FontAwesome.Solid.Redo, "Rotate 90 degrees clockwise (Ctrl->)", () => rotationHandler?.Rotate(90));
|
||||||
|
|
||||||
addRotateHandle(Anchor.TopLeft);
|
addRotateHandle(Anchor.TopLeft);
|
||||||
addRotateHandle(Anchor.TopRight);
|
addRotateHandle(Anchor.TopRight);
|
||||||
@ -331,7 +325,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
|||||||
var handle = new SelectionBoxRotationHandle
|
var handle = new SelectionBoxRotationHandle
|
||||||
{
|
{
|
||||||
Anchor = anchor,
|
Anchor = anchor,
|
||||||
HandleRotate = angle => OnRotation?.Invoke(angle)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
handle.OperationStarted += operationStarted;
|
handle.OperationStarted += operationStarted;
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
@ -15,24 +13,25 @@ using osu.Framework.Localisation;
|
|||||||
using osu.Game.Localisation;
|
using osu.Game.Localisation;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
using Key = osuTK.Input.Key;
|
using osuTK.Input;
|
||||||
|
|
||||||
namespace osu.Game.Screens.Edit.Compose.Components
|
namespace osu.Game.Screens.Edit.Compose.Components
|
||||||
{
|
{
|
||||||
public partial class SelectionBoxRotationHandle : SelectionBoxDragHandle, IHasTooltip
|
public partial class SelectionBoxRotationHandle : SelectionBoxDragHandle, IHasTooltip
|
||||||
{
|
{
|
||||||
public Action<float> HandleRotate { get; set; }
|
|
||||||
|
|
||||||
public LocalisableString TooltipText { get; private set; }
|
public LocalisableString TooltipText { get; private set; }
|
||||||
|
|
||||||
private SpriteIcon icon;
|
private SpriteIcon icon = null!;
|
||||||
|
|
||||||
private const float snap_step = 15;
|
private const float snap_step = 15;
|
||||||
|
|
||||||
private readonly Bindable<float?> cumulativeRotation = new Bindable<float?>();
|
private readonly Bindable<float?> cumulativeRotation = new Bindable<float?>();
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private SelectionBox selectionBox { get; set; }
|
private SelectionBox selectionBox { get; set; } = null!;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private SelectionRotationHandler? rotationHandler { get; set; }
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load()
|
||||||
@ -63,10 +62,10 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
|||||||
|
|
||||||
protected override bool OnDragStart(DragStartEvent e)
|
protected override bool OnDragStart(DragStartEvent e)
|
||||||
{
|
{
|
||||||
bool handle = base.OnDragStart(e);
|
if (rotationHandler == null) return false;
|
||||||
if (handle)
|
|
||||||
cumulativeRotation.Value = 0;
|
rotationHandler.Begin();
|
||||||
return handle;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnDrag(DragEvent e)
|
protected override void OnDrag(DragEvent e)
|
||||||
@ -99,7 +98,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
|||||||
|
|
||||||
protected override void OnDragEnd(DragEndEvent e)
|
protected override void OnDragEnd(DragEndEvent e)
|
||||||
{
|
{
|
||||||
base.OnDragEnd(e);
|
rotationHandler?.Commit();
|
||||||
|
UpdateHoverState();
|
||||||
|
|
||||||
cumulativeRotation.Value = null;
|
cumulativeRotation.Value = null;
|
||||||
rawCumulativeRotation = 0;
|
rawCumulativeRotation = 0;
|
||||||
TooltipText = default;
|
TooltipText = default;
|
||||||
@ -116,14 +117,12 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
|||||||
|
|
||||||
private void applyRotation(bool shouldSnap)
|
private void applyRotation(bool shouldSnap)
|
||||||
{
|
{
|
||||||
float oldRotation = cumulativeRotation.Value ?? 0;
|
|
||||||
|
|
||||||
float newRotation = shouldSnap ? snap(rawCumulativeRotation, snap_step) : MathF.Round(rawCumulativeRotation);
|
float newRotation = shouldSnap ? snap(rawCumulativeRotation, snap_step) : MathF.Round(rawCumulativeRotation);
|
||||||
newRotation = (newRotation - 180) % 360 + 180;
|
newRotation = (newRotation - 180) % 360 + 180;
|
||||||
|
|
||||||
cumulativeRotation.Value = newRotation;
|
cumulativeRotation.Value = newRotation;
|
||||||
|
|
||||||
HandleRotate?.Invoke(newRotation - oldRotation);
|
rotationHandler?.Update(newRotation);
|
||||||
TooltipText = shouldSnap ? EditorStrings.RotationSnapped(newRotation) : EditorStrings.RotationUnsnapped(newRotation);
|
TooltipText = shouldSnap ? EditorStrings.RotationSnapped(newRotation) : EditorStrings.RotationUnsnapped(newRotation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,7 +16,6 @@ using osu.Framework.Graphics.UserInterface;
|
|||||||
using osu.Framework.Input;
|
using osu.Framework.Input;
|
||||||
using osu.Framework.Input.Bindings;
|
using osu.Framework.Input.Bindings;
|
||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
using osu.Framework.Utils;
|
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Input.Bindings;
|
using osu.Game.Input.Bindings;
|
||||||
using osu.Game.Resources.Localisation.Web;
|
using osu.Game.Resources.Localisation.Web;
|
||||||
@ -56,6 +55,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
|||||||
[Resolved(CanBeNull = true)]
|
[Resolved(CanBeNull = true)]
|
||||||
protected IEditorChangeHandler ChangeHandler { get; private set; }
|
protected IEditorChangeHandler ChangeHandler { get; private set; }
|
||||||
|
|
||||||
|
protected SelectionRotationHandler RotationHandler { get; private set; }
|
||||||
|
|
||||||
protected SelectionHandler()
|
protected SelectionHandler()
|
||||||
{
|
{
|
||||||
selectedBlueprints = new List<SelectionBlueprint<T>>();
|
selectedBlueprints = new List<SelectionBlueprint<T>>();
|
||||||
@ -64,10 +65,21 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
|||||||
AlwaysPresent = true;
|
AlwaysPresent = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
|
||||||
|
{
|
||||||
|
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
|
||||||
|
dependencies.CacheAs(RotationHandler = CreateRotationHandler());
|
||||||
|
return dependencies;
|
||||||
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load()
|
||||||
{
|
{
|
||||||
InternalChild = SelectionBox = CreateSelectionBox();
|
AddRangeInternal(new Drawable[]
|
||||||
|
{
|
||||||
|
RotationHandler,
|
||||||
|
SelectionBox = CreateSelectionBox(),
|
||||||
|
});
|
||||||
|
|
||||||
SelectedItems.CollectionChanged += (_, _) =>
|
SelectedItems.CollectionChanged += (_, _) =>
|
||||||
{
|
{
|
||||||
@ -81,7 +93,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
|||||||
OperationStarted = OnOperationBegan,
|
OperationStarted = OnOperationBegan,
|
||||||
OperationEnded = OnOperationEnded,
|
OperationEnded = OnOperationEnded,
|
||||||
|
|
||||||
OnRotation = HandleRotation,
|
|
||||||
OnScale = HandleScale,
|
OnScale = HandleScale,
|
||||||
OnFlip = HandleFlip,
|
OnFlip = HandleFlip,
|
||||||
OnReverse = HandleReverse,
|
OnReverse = HandleReverse,
|
||||||
@ -133,6 +144,11 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
|||||||
/// <returns>Whether any items could be rotated.</returns>
|
/// <returns>Whether any items could be rotated.</returns>
|
||||||
public virtual bool HandleRotation(float angle) => false;
|
public virtual bool HandleRotation(float angle) => false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates the handler to use for rotation operations.
|
||||||
|
/// </summary>
|
||||||
|
public virtual SelectionRotationHandler CreateRotationHandler() => new SelectionRotationHandler();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles the selected items being scaled.
|
/// Handles the selected items being scaled.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -401,98 +417,5 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
|||||||
=> Enumerable.Empty<MenuItem>();
|
=> Enumerable.Empty<MenuItem>();
|
||||||
|
|
||||||
#endregion
|
#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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
126
osu.Game/Utils/GeometryUtils.cs
Normal file
126
osu.Game/Utils/GeometryUtils.cs
Normal 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 };
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user