mirror of
https://github.com/ppy/osu.git
synced 2024-12-13 03:42:57 +08:00
Merge pull request #26643 from OliBomby/scaling
Refactor scale handling in editor to facilitate reuse
This commit is contained in:
commit
22c42573b4
@ -3,7 +3,6 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
@ -25,33 +24,17 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
public partial class OsuSelectionHandler : EditorSelectionHandler
|
||||
{
|
||||
[Resolved(CanBeNull = true)]
|
||||
private IDistanceSnapProvider? snapProvider { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// During a transform, the initial path types of a single selected slider are stored so they
|
||||
/// can be maintained throughout the operation.
|
||||
/// </summary>
|
||||
private List<PathType?>? referencePathTypes;
|
||||
|
||||
protected override void OnSelectionChanged()
|
||||
{
|
||||
base.OnSelectionChanged();
|
||||
|
||||
Quad quad = selectedMovableObjects.Length > 0 ? GeometryUtils.GetSurroundingQuad(selectedMovableObjects) : new Quad();
|
||||
|
||||
SelectionBox.CanFlipX = SelectionBox.CanScaleX = quad.Width > 0;
|
||||
SelectionBox.CanFlipY = SelectionBox.CanScaleY = quad.Height > 0;
|
||||
SelectionBox.CanScaleDiagonally = SelectionBox.CanScaleX && SelectionBox.CanScaleY;
|
||||
SelectionBox.CanFlipX = quad.Width > 0;
|
||||
SelectionBox.CanFlipY = quad.Height > 0;
|
||||
SelectionBox.CanReverse = EditorBeatmap.SelectedHitObjects.Count > 1 || EditorBeatmap.SelectedHitObjects.Any(s => s is Slider);
|
||||
}
|
||||
|
||||
protected override void OnOperationEnded()
|
||||
{
|
||||
base.OnOperationEnded();
|
||||
referencePathTypes = null;
|
||||
}
|
||||
|
||||
protected override bool OnKeyDown(KeyDownEvent e)
|
||||
{
|
||||
if (e.Key == Key.M && e.ControlPressed && e.ShiftPressed)
|
||||
@ -149,96 +132,9 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
return didFlip;
|
||||
}
|
||||
|
||||
public override bool HandleScale(Vector2 scale, Anchor reference)
|
||||
{
|
||||
adjustScaleFromAnchor(ref scale, reference);
|
||||
|
||||
var hitObjects = selectedMovableObjects;
|
||||
|
||||
// for the time being, allow resizing of slider paths only if the slider is
|
||||
// the only hit object selected. with a group selection, it's likely the user
|
||||
// is not looking to change the duration of the slider but expand the whole pattern.
|
||||
if (hitObjects.Length == 1 && hitObjects.First() is Slider slider)
|
||||
scaleSlider(slider, scale);
|
||||
else
|
||||
scaleHitObjects(hitObjects, reference, scale);
|
||||
|
||||
moveSelectionInBounds();
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void adjustScaleFromAnchor(ref Vector2 scale, Anchor reference)
|
||||
{
|
||||
// cancel out scale in axes we don't care about (based on which drag handle was used).
|
||||
if ((reference & Anchor.x1) > 0) scale.X = 0;
|
||||
if ((reference & Anchor.y1) > 0) scale.Y = 0;
|
||||
|
||||
// reverse the scale direction if dragging from top or left.
|
||||
if ((reference & Anchor.x0) > 0) scale.X = -scale.X;
|
||||
if ((reference & Anchor.y0) > 0) scale.Y = -scale.Y;
|
||||
}
|
||||
|
||||
public override SelectionRotationHandler CreateRotationHandler() => new OsuSelectionRotationHandler();
|
||||
|
||||
private void scaleSlider(Slider slider, Vector2 scale)
|
||||
{
|
||||
referencePathTypes ??= slider.Path.ControlPoints.Select(p => p.Type).ToList();
|
||||
|
||||
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;
|
||||
|
||||
Vector2 pathRelativeDeltaScale = new Vector2(
|
||||
sliderQuad.Width == 0 ? 0 : 1 + scale.X / sliderQuad.Width,
|
||||
sliderQuad.Height == 0 ? 0 : 1 + scale.Y / sliderQuad.Height);
|
||||
|
||||
Queue<Vector2> oldControlPoints = new Queue<Vector2>();
|
||||
|
||||
foreach (var point in slider.Path.ControlPoints)
|
||||
{
|
||||
oldControlPoints.Enqueue(point.Position);
|
||||
point.Position *= pathRelativeDeltaScale;
|
||||
}
|
||||
|
||||
// Maintain the path types in case they were defaulted to bezier at some point during scaling
|
||||
for (int i = 0; i < slider.Path.ControlPoints.Count; ++i)
|
||||
slider.Path.ControlPoints[i].Type = referencePathTypes[i];
|
||||
|
||||
// Snap the slider's length to the current beat divisor
|
||||
// to calculate the final resulting duration / bounding box before the final checks.
|
||||
slider.SnapTo(snapProvider);
|
||||
|
||||
//if sliderhead or sliderend end up outside playfield, revert scaling.
|
||||
Quad scaledQuad = GeometryUtils.GetSurroundingQuad(new OsuHitObject[] { slider });
|
||||
(bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad);
|
||||
|
||||
if (xInBounds && yInBounds && slider.Path.HasValidLength)
|
||||
return;
|
||||
|
||||
foreach (var point in slider.Path.ControlPoints)
|
||||
point.Position = oldControlPoints.Dequeue();
|
||||
|
||||
// Snap the slider's length again to undo the potentially-invalid length applied by the previous snap.
|
||||
slider.SnapTo(snapProvider);
|
||||
}
|
||||
|
||||
private void scaleHitObjects(OsuHitObject[] hitObjects, Anchor reference, Vector2 scale)
|
||||
{
|
||||
scale = getClampedScale(hitObjects, reference, scale);
|
||||
Quad selectionQuad = GeometryUtils.GetSurroundingQuad(hitObjects);
|
||||
|
||||
foreach (var h in hitObjects)
|
||||
h.Position = GeometryUtils.GetScaledPosition(reference, scale, selectionQuad, h.Position);
|
||||
}
|
||||
|
||||
private (bool X, bool Y) isQuadInBounds(Quad quad)
|
||||
{
|
||||
bool xInBounds = (quad.TopLeft.X >= 0) && (quad.BottomRight.X <= DrawWidth);
|
||||
bool yInBounds = (quad.TopLeft.Y >= 0) && (quad.BottomRight.Y <= DrawHeight);
|
||||
|
||||
return (xInBounds, yInBounds);
|
||||
}
|
||||
public override SelectionScaleHandler CreateScaleHandler() => new OsuSelectionScaleHandler();
|
||||
|
||||
private void moveSelectionInBounds()
|
||||
{
|
||||
@ -262,43 +158,6 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
h.Position += delta;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clamp scale for multi-object-scaling where selection does not exceed playfield bounds or flip.
|
||||
/// </summary>
|
||||
/// <param name="hitObjects">The hitobjects to be scaled</param>
|
||||
/// <param name="reference">The anchor from which the scale operation is performed</param>
|
||||
/// <param name="scale">The scale to be clamped</param>
|
||||
/// <returns>The clamped scale vector</returns>
|
||||
private Vector2 getClampedScale(OsuHitObject[] hitObjects, Anchor reference, Vector2 scale)
|
||||
{
|
||||
float xOffset = ((reference & Anchor.x0) > 0) ? -scale.X : 0;
|
||||
float yOffset = ((reference & Anchor.y0) > 0) ? -scale.Y : 0;
|
||||
|
||||
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);
|
||||
|
||||
//max Size -> playfield bounds
|
||||
if (scaledQuad.TopLeft.X < 0)
|
||||
scale.X += scaledQuad.TopLeft.X;
|
||||
if (scaledQuad.TopLeft.Y < 0)
|
||||
scale.Y += scaledQuad.TopLeft.Y;
|
||||
|
||||
if (scaledQuad.BottomRight.X > DrawWidth)
|
||||
scale.X -= scaledQuad.BottomRight.X - DrawWidth;
|
||||
if (scaledQuad.BottomRight.Y > DrawHeight)
|
||||
scale.Y -= scaledQuad.BottomRight.Y - DrawHeight;
|
||||
|
||||
//min Size -> almost 0. Less than 0 causes the quad to flip, exactly 0 causes scaling to get stuck at minimum scale.
|
||||
Vector2 scaledSize = selectionQuad.Size + scale;
|
||||
Vector2 minSize = new Vector2(Precision.FLOAT_EPSILON);
|
||||
|
||||
scale = Vector2.ComponentMax(minSize, scaledSize) - selectionQuad.Size;
|
||||
|
||||
return scale;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// All osu! hitobjects which can be moved/rotated/scaled.
|
||||
/// </summary>
|
||||
|
220
osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs
Normal file
220
osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs
Normal file
@ -0,0 +1,220 @@
|
||||
// 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.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
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 OsuSelectionScaleHandler : SelectionScaleHandler
|
||||
{
|
||||
[Resolved]
|
||||
private IEditorChangeHandler? changeHandler { get; set; }
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private IDistanceSnapProvider? snapProvider { 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);
|
||||
|
||||
CanScaleX.Value = quad.Width > 0;
|
||||
CanScaleY.Value = quad.Height > 0;
|
||||
CanScaleDiagonally.Value = CanScaleX.Value && CanScaleY.Value;
|
||||
}
|
||||
|
||||
private Dictionary<OsuHitObject, OriginalHitObjectState>? objectsInScale;
|
||||
private Vector2? defaultOrigin;
|
||||
|
||||
public override void Begin()
|
||||
{
|
||||
if (objectsInScale != null)
|
||||
throw new InvalidOperationException($"Cannot {nameof(Begin)} a scale operation while another is in progress!");
|
||||
|
||||
changeHandler?.BeginChange();
|
||||
|
||||
objectsInScale = selectedMovableObjects.ToDictionary(ho => ho, ho => new OriginalHitObjectState(ho));
|
||||
OriginalSurroundingQuad = objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider
|
||||
? GeometryUtils.GetSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position))
|
||||
: GeometryUtils.GetSurroundingQuad(objectsInScale.Keys);
|
||||
defaultOrigin = OriginalSurroundingQuad.Value.Centre;
|
||||
}
|
||||
|
||||
public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both)
|
||||
{
|
||||
if (objectsInScale == null)
|
||||
throw new InvalidOperationException($"Cannot {nameof(Update)} a scale operation without calling {nameof(Begin)} first!");
|
||||
|
||||
Debug.Assert(defaultOrigin != null && OriginalSurroundingQuad != null);
|
||||
|
||||
Vector2 actualOrigin = origin ?? defaultOrigin.Value;
|
||||
|
||||
// for the time being, allow resizing of slider paths only if the slider is
|
||||
// the only hit object selected. with a group selection, it's likely the user
|
||||
// is not looking to change the duration of the slider but expand the whole pattern.
|
||||
if (objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider)
|
||||
{
|
||||
var originalInfo = objectsInScale[slider];
|
||||
Debug.Assert(originalInfo.PathControlPointPositions != null && originalInfo.PathControlPointTypes != null);
|
||||
scaleSlider(slider, scale, originalInfo.PathControlPointPositions, originalInfo.PathControlPointTypes);
|
||||
}
|
||||
else
|
||||
{
|
||||
scale = getClampedScale(OriginalSurroundingQuad.Value, actualOrigin, scale);
|
||||
|
||||
foreach (var (ho, originalState) in objectsInScale)
|
||||
{
|
||||
ho.Position = GeometryUtils.GetScaledPosition(scale, actualOrigin, originalState.Position);
|
||||
}
|
||||
}
|
||||
|
||||
moveSelectionInBounds();
|
||||
}
|
||||
|
||||
public override void Commit()
|
||||
{
|
||||
if (objectsInScale == null)
|
||||
throw new InvalidOperationException($"Cannot {nameof(Commit)} a rotate operation without calling {nameof(Begin)} first!");
|
||||
|
||||
changeHandler?.EndChange();
|
||||
|
||||
objectsInScale = null;
|
||||
OriginalSurroundingQuad = null;
|
||||
defaultOrigin = null;
|
||||
}
|
||||
|
||||
private IEnumerable<OsuHitObject> selectedMovableObjects => selectedItems.Cast<OsuHitObject>()
|
||||
.Where(h => h is not Spinner);
|
||||
|
||||
private void scaleSlider(Slider slider, Vector2 scale, Vector2[] originalPathPositions, PathType?[] originalPathTypes)
|
||||
{
|
||||
scale = Vector2.ComponentMax(scale, new Vector2(Precision.FLOAT_EPSILON));
|
||||
|
||||
// Maintain the path types in case they were defaulted to bezier at some point during scaling
|
||||
for (int i = 0; i < slider.Path.ControlPoints.Count; i++)
|
||||
{
|
||||
slider.Path.ControlPoints[i].Position = originalPathPositions[i] * scale;
|
||||
slider.Path.ControlPoints[i].Type = originalPathTypes[i];
|
||||
}
|
||||
|
||||
// Snap the slider's length to the current beat divisor
|
||||
// to calculate the final resulting duration / bounding box before the final checks.
|
||||
slider.SnapTo(snapProvider);
|
||||
|
||||
//if sliderhead or sliderend end up outside playfield, revert scaling.
|
||||
Quad scaledQuad = GeometryUtils.GetSurroundingQuad(new OsuHitObject[] { slider });
|
||||
(bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad);
|
||||
|
||||
if (xInBounds && yInBounds && slider.Path.HasValidLength)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < slider.Path.ControlPoints.Count; i++)
|
||||
slider.Path.ControlPoints[i].Position = originalPathPositions[i];
|
||||
|
||||
// Snap the slider's length again to undo the potentially-invalid length applied by the previous snap.
|
||||
slider.SnapTo(snapProvider);
|
||||
}
|
||||
|
||||
private (bool X, bool Y) isQuadInBounds(Quad quad)
|
||||
{
|
||||
bool xInBounds = (quad.TopLeft.X >= 0) && (quad.BottomRight.X <= OsuPlayfield.BASE_SIZE.X);
|
||||
bool yInBounds = (quad.TopLeft.Y >= 0) && (quad.BottomRight.Y <= OsuPlayfield.BASE_SIZE.Y);
|
||||
|
||||
return (xInBounds, yInBounds);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clamp scale for multi-object-scaling where selection does not exceed playfield bounds or flip.
|
||||
/// </summary>
|
||||
/// <param name="selectionQuad">The quad surrounding the hitobjects</param>
|
||||
/// <param name="origin">The origin from which the scale operation is performed</param>
|
||||
/// <param name="scale">The scale to be clamped</param>
|
||||
/// <returns>The clamped scale vector</returns>
|
||||
private Vector2 getClampedScale(Quad selectionQuad, Vector2 origin, Vector2 scale)
|
||||
{
|
||||
//todo: this is not always correct for selections involving sliders. This approximation assumes each point is scaled independently, but sliderends move with the sliderhead.
|
||||
|
||||
var tl1 = Vector2.Divide(-origin, selectionQuad.TopLeft - origin);
|
||||
var tl2 = Vector2.Divide(OsuPlayfield.BASE_SIZE - origin, selectionQuad.TopLeft - origin);
|
||||
var br1 = Vector2.Divide(-origin, selectionQuad.BottomRight - origin);
|
||||
var br2 = Vector2.Divide(OsuPlayfield.BASE_SIZE - origin, selectionQuad.BottomRight - origin);
|
||||
|
||||
if (!Precision.AlmostEquals(selectionQuad.TopLeft.X - origin.X, 0))
|
||||
scale.X = selectionQuad.TopLeft.X - origin.X < 0 ? MathHelper.Clamp(scale.X, tl2.X, tl1.X) : MathHelper.Clamp(scale.X, tl1.X, tl2.X);
|
||||
if (!Precision.AlmostEquals(selectionQuad.TopLeft.Y - origin.Y, 0))
|
||||
scale.Y = selectionQuad.TopLeft.Y - origin.Y < 0 ? MathHelper.Clamp(scale.Y, tl2.Y, tl1.Y) : MathHelper.Clamp(scale.Y, tl1.Y, tl2.Y);
|
||||
if (!Precision.AlmostEquals(selectionQuad.BottomRight.X - origin.X, 0))
|
||||
scale.X = selectionQuad.BottomRight.X - origin.X < 0 ? MathHelper.Clamp(scale.X, br2.X, br1.X) : MathHelper.Clamp(scale.X, br1.X, br2.X);
|
||||
if (!Precision.AlmostEquals(selectionQuad.BottomRight.Y - origin.Y, 0))
|
||||
scale.Y = selectionQuad.BottomRight.Y - origin.Y < 0 ? MathHelper.Clamp(scale.Y, br2.Y, br1.Y) : MathHelper.Clamp(scale.Y, br1.Y, br2.Y);
|
||||
|
||||
return Vector2.ComponentMax(scale, new Vector2(Precision.FLOAT_EPSILON));
|
||||
}
|
||||
|
||||
private void moveSelectionInBounds()
|
||||
{
|
||||
Quad quad = GeometryUtils.GetSurroundingQuad(objectsInScale!.Keys);
|
||||
|
||||
Vector2 delta = Vector2.Zero;
|
||||
|
||||
if (quad.TopLeft.X < 0)
|
||||
delta.X -= quad.TopLeft.X;
|
||||
if (quad.TopLeft.Y < 0)
|
||||
delta.Y -= quad.TopLeft.Y;
|
||||
|
||||
if (quad.BottomRight.X > OsuPlayfield.BASE_SIZE.X)
|
||||
delta.X -= quad.BottomRight.X - OsuPlayfield.BASE_SIZE.X;
|
||||
if (quad.BottomRight.Y > OsuPlayfield.BASE_SIZE.Y)
|
||||
delta.Y -= quad.BottomRight.Y - OsuPlayfield.BASE_SIZE.Y;
|
||||
|
||||
foreach (var (h, _) in objectsInScale!)
|
||||
h.Position += delta;
|
||||
}
|
||||
|
||||
private struct OriginalHitObjectState
|
||||
{
|
||||
public Vector2 Position { get; }
|
||||
public Vector2[]? PathControlPointPositions { get; }
|
||||
public PathType?[]? PathControlPointTypes { get; }
|
||||
|
||||
public OriginalHitObjectState(OsuHitObject hitObject)
|
||||
{
|
||||
Position = hitObject.Position;
|
||||
PathControlPointPositions = (hitObject as IHasPath)?.Path.ControlPoints.Select(p => p.Position).ToArray();
|
||||
PathControlPointTypes = (hitObject as IHasPath)?.Path.ControlPoints.Select(p => p.Type).ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -10,9 +10,11 @@ using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osu.Game.Utils;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
@ -26,9 +28,13 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
[Cached(typeof(SelectionRotationHandler))]
|
||||
private TestSelectionRotationHandler rotationHandler;
|
||||
|
||||
[Cached(typeof(SelectionScaleHandler))]
|
||||
private TestSelectionScaleHandler scaleHandler;
|
||||
|
||||
public TestSceneComposeSelectBox()
|
||||
{
|
||||
rotationHandler = new TestSelectionRotationHandler(() => selectionArea);
|
||||
scaleHandler = new TestSelectionScaleHandler(() => selectionArea);
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
@ -45,13 +51,8 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
|
||||
CanScaleX = true,
|
||||
CanScaleY = true,
|
||||
CanScaleDiagonally = true,
|
||||
CanFlipX = true,
|
||||
CanFlipY = true,
|
||||
|
||||
OnScale = handleScale
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -60,27 +61,6 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
InputManager.ReleaseButton(MouseButton.Left);
|
||||
});
|
||||
|
||||
private bool handleScale(Vector2 amount, Anchor reference)
|
||||
{
|
||||
if ((reference & Anchor.y1) == 0)
|
||||
{
|
||||
int directionY = (reference & Anchor.y0) > 0 ? -1 : 1;
|
||||
if (directionY < 0)
|
||||
selectionArea.Y += amount.Y;
|
||||
selectionArea.Height += directionY * amount.Y;
|
||||
}
|
||||
|
||||
if ((reference & Anchor.x1) == 0)
|
||||
{
|
||||
int directionX = (reference & Anchor.x0) > 0 ? -1 : 1;
|
||||
if (directionX < 0)
|
||||
selectionArea.X += amount.X;
|
||||
selectionArea.Width += directionX * amount.X;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private partial class TestSelectionRotationHandler : SelectionRotationHandler
|
||||
{
|
||||
private readonly Func<Container> getTargetContainer;
|
||||
@ -125,6 +105,51 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
}
|
||||
}
|
||||
|
||||
private partial class TestSelectionScaleHandler : SelectionScaleHandler
|
||||
{
|
||||
private readonly Func<Container> getTargetContainer;
|
||||
|
||||
public TestSelectionScaleHandler(Func<Container> getTargetContainer)
|
||||
{
|
||||
this.getTargetContainer = getTargetContainer;
|
||||
|
||||
CanScaleX.Value = true;
|
||||
CanScaleY.Value = true;
|
||||
CanScaleDiagonally.Value = true;
|
||||
}
|
||||
|
||||
[CanBeNull]
|
||||
private Container targetContainer;
|
||||
|
||||
public override void Begin()
|
||||
{
|
||||
if (targetContainer != null)
|
||||
throw new InvalidOperationException($"Cannot {nameof(Begin)} a scale operation while another is in progress!");
|
||||
|
||||
targetContainer = getTargetContainer();
|
||||
OriginalSurroundingQuad = new Quad(targetContainer!.X, targetContainer.Y, targetContainer.Width, targetContainer.Height);
|
||||
}
|
||||
|
||||
public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both)
|
||||
{
|
||||
if (targetContainer == null)
|
||||
throw new InvalidOperationException($"Cannot {nameof(Update)} a scale operation without calling {nameof(Begin)} first!");
|
||||
|
||||
Vector2 actualOrigin = origin ?? Vector2.Zero;
|
||||
|
||||
targetContainer.Position = GeometryUtils.GetScaledPosition(scale, actualOrigin, OriginalSurroundingQuad!.Value.TopLeft);
|
||||
targetContainer.Size = OriginalSurroundingQuad!.Value.Size * scale;
|
||||
}
|
||||
|
||||
public override void Commit()
|
||||
{
|
||||
if (targetContainer == null)
|
||||
throw new InvalidOperationException($"Cannot {nameof(Commit)} a scale operation without calling {nameof(Begin)} first!");
|
||||
|
||||
targetContainer = null;
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRotationHandleShownOnHover()
|
||||
{
|
||||
|
@ -8,10 +8,8 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.EnumExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
@ -34,148 +32,16 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
UpdatePosition = updateDrawablePosition
|
||||
};
|
||||
|
||||
private bool allSelectedSupportManualSizing(Axes axis) => SelectedItems.All(b => (b as CompositeDrawable)?.AutoSizeAxes.HasFlagFast(axis) == false);
|
||||
|
||||
public override bool HandleScale(Vector2 scale, Anchor anchor)
|
||||
public override SelectionScaleHandler CreateScaleHandler()
|
||||
{
|
||||
Axes adjustAxis;
|
||||
|
||||
switch (anchor)
|
||||
var scaleHandler = new SkinSelectionScaleHandler
|
||||
{
|
||||
// for corners, adjust scale.
|
||||
case Anchor.TopLeft:
|
||||
case Anchor.TopRight:
|
||||
case Anchor.BottomLeft:
|
||||
case Anchor.BottomRight:
|
||||
adjustAxis = Axes.Both;
|
||||
break;
|
||||
UpdatePosition = updateDrawablePosition
|
||||
};
|
||||
|
||||
// for edges, adjust size.
|
||||
// autosize elements can't be easily handled so just disable sizing for now.
|
||||
case Anchor.TopCentre:
|
||||
case Anchor.BottomCentre:
|
||||
if (!allSelectedSupportManualSizing(Axes.Y))
|
||||
return false;
|
||||
scaleHandler.PerformFlipFromScaleHandles += a => SelectionBox.PerformFlipFromScaleHandles(a);
|
||||
|
||||
adjustAxis = Axes.Y;
|
||||
break;
|
||||
|
||||
case Anchor.CentreLeft:
|
||||
case Anchor.CentreRight:
|
||||
if (!allSelectedSupportManualSizing(Axes.X))
|
||||
return false;
|
||||
|
||||
adjustAxis = Axes.X;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(anchor), anchor, null);
|
||||
}
|
||||
|
||||
// convert scale to screen space
|
||||
scale = ToScreenSpace(scale) - ToScreenSpace(Vector2.Zero);
|
||||
|
||||
adjustScaleFromAnchor(ref scale, anchor);
|
||||
|
||||
// the selection quad is always upright, so use an AABB rect to make mutating the values easier.
|
||||
var selectionRect = getSelectionQuad().AABBFloat;
|
||||
|
||||
// If the selection has no area we cannot scale it
|
||||
if (selectionRect.Area == 0)
|
||||
return false;
|
||||
|
||||
// copy to mutate, as we will need to compare to the original later on.
|
||||
var adjustedRect = selectionRect;
|
||||
bool isRotated = false;
|
||||
|
||||
// for now aspect lock scale adjustments that occur at corners..
|
||||
if (!anchor.HasFlagFast(Anchor.x1) && !anchor.HasFlagFast(Anchor.y1))
|
||||
{
|
||||
// project scale vector along diagonal
|
||||
Vector2 diag = (selectionRect.TopLeft - selectionRect.BottomRight).Normalized();
|
||||
scale = Vector2.Dot(scale, diag) * diag;
|
||||
}
|
||||
// ..or if any of the selection have been rotated.
|
||||
// this is to avoid requiring skew logic (which would likely not be the user's expected transform anyway).
|
||||
else if (SelectedBlueprints.Any(b => !Precision.AlmostEquals(((Drawable)b.Item).Rotation % 90, 0)))
|
||||
{
|
||||
isRotated = true;
|
||||
if (anchor.HasFlagFast(Anchor.x1))
|
||||
// if dragging from the horizontal centre, only a vertical component is available.
|
||||
scale.X = scale.Y / selectionRect.Height * selectionRect.Width;
|
||||
else
|
||||
// in all other cases (arbitrarily) use the horizontal component for aspect lock.
|
||||
scale.Y = scale.X / selectionRect.Width * selectionRect.Height;
|
||||
}
|
||||
|
||||
if (anchor.HasFlagFast(Anchor.x0)) adjustedRect.X -= scale.X;
|
||||
if (anchor.HasFlagFast(Anchor.y0)) adjustedRect.Y -= scale.Y;
|
||||
|
||||
// Maintain the selection's centre position if dragging from the centre anchors and selection is rotated.
|
||||
if (isRotated && anchor.HasFlagFast(Anchor.x1)) adjustedRect.X -= scale.X / 2;
|
||||
if (isRotated && anchor.HasFlagFast(Anchor.y1)) adjustedRect.Y -= scale.Y / 2;
|
||||
|
||||
adjustedRect.Width += scale.X;
|
||||
adjustedRect.Height += scale.Y;
|
||||
|
||||
if (adjustedRect.Width <= 0 || adjustedRect.Height <= 0)
|
||||
{
|
||||
Axes toFlip = Axes.None;
|
||||
|
||||
if (adjustedRect.Width <= 0) toFlip |= Axes.X;
|
||||
if (adjustedRect.Height <= 0) toFlip |= Axes.Y;
|
||||
|
||||
SelectionBox.PerformFlipFromScaleHandles(toFlip);
|
||||
return true;
|
||||
}
|
||||
|
||||
// scale adjust applied to each individual item should match that of the quad itself.
|
||||
var scaledDelta = new Vector2(
|
||||
adjustedRect.Width / selectionRect.Width,
|
||||
adjustedRect.Height / selectionRect.Height
|
||||
);
|
||||
|
||||
foreach (var b in SelectedBlueprints)
|
||||
{
|
||||
var drawableItem = (Drawable)b.Item;
|
||||
|
||||
// each drawable's relative position should be maintained in the scaled quad.
|
||||
var screenPosition = drawableItem.ToScreenSpace(drawableItem.OriginPosition);
|
||||
|
||||
var relativePositionInOriginal =
|
||||
new Vector2(
|
||||
(screenPosition.X - selectionRect.TopLeft.X) / selectionRect.Width,
|
||||
(screenPosition.Y - selectionRect.TopLeft.Y) / selectionRect.Height
|
||||
);
|
||||
|
||||
var newPositionInAdjusted = new Vector2(
|
||||
adjustedRect.TopLeft.X + adjustedRect.Width * relativePositionInOriginal.X,
|
||||
adjustedRect.TopLeft.Y + adjustedRect.Height * relativePositionInOriginal.Y
|
||||
);
|
||||
|
||||
updateDrawablePosition(drawableItem, newPositionInAdjusted);
|
||||
|
||||
var currentScaledDelta = scaledDelta;
|
||||
if (Precision.AlmostEquals(MathF.Abs(drawableItem.Rotation) % 180, 90))
|
||||
currentScaledDelta = new Vector2(scaledDelta.Y, scaledDelta.X);
|
||||
|
||||
switch (adjustAxis)
|
||||
{
|
||||
case Axes.X:
|
||||
drawableItem.Width *= currentScaledDelta.X;
|
||||
break;
|
||||
|
||||
case Axes.Y:
|
||||
drawableItem.Height *= currentScaledDelta.Y;
|
||||
break;
|
||||
|
||||
case Axes.Both:
|
||||
drawableItem.Scale *= currentScaledDelta;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return scaleHandler;
|
||||
}
|
||||
|
||||
public override bool HandleFlip(Direction direction, bool flipOverOrigin)
|
||||
@ -226,9 +92,6 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
{
|
||||
base.OnSelectionChanged();
|
||||
|
||||
SelectionBox.CanScaleX = allSelectedSupportManualSizing(Axes.X);
|
||||
SelectionBox.CanScaleY = allSelectedSupportManualSizing(Axes.Y);
|
||||
SelectionBox.CanScaleDiagonally = true;
|
||||
SelectionBox.CanFlipX = true;
|
||||
SelectionBox.CanFlipY = true;
|
||||
SelectionBox.CanReverse = false;
|
||||
@ -460,16 +323,5 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
drawable.Origin = localOrigin;
|
||||
drawable.Position += offset;
|
||||
}
|
||||
|
||||
private static void adjustScaleFromAnchor(ref Vector2 scale, Anchor reference)
|
||||
{
|
||||
// cancel out scale in axes we don't care about (based on which drag handle was used).
|
||||
if ((reference & Anchor.x1) > 0) scale.X = 0;
|
||||
if ((reference & Anchor.y1) > 0) scale.Y = 0;
|
||||
|
||||
// reverse the scale direction if dragging from top or left.
|
||||
if ((reference & Anchor.x0) > 0) scale.X = -scale.X;
|
||||
if ((reference & Anchor.y0) > 0) scale.Y = -scale.Y;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
178
osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs
Normal file
178
osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs
Normal file
@ -0,0 +1,178 @@
|
||||
// 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.Extensions.EnumExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Utils;
|
||||
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 SkinSelectionScaleHandler : SelectionScaleHandler
|
||||
{
|
||||
public Action<Drawable, Vector2> UpdatePosition { get; init; } = null!;
|
||||
|
||||
public event Action<Axes>? PerformFlipFromScaleHandles;
|
||||
|
||||
[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()
|
||||
{
|
||||
CanScaleX.Value = allSelectedSupportManualSizing(Axes.X);
|
||||
CanScaleY.Value = allSelectedSupportManualSizing(Axes.Y);
|
||||
CanScaleDiagonally.Value = true;
|
||||
}
|
||||
|
||||
private bool allSelectedSupportManualSizing(Axes axis) => selectedItems.All(b => (b as CompositeDrawable)?.AutoSizeAxes.HasFlagFast(axis) == false);
|
||||
|
||||
private Dictionary<Drawable, OriginalDrawableState>? objectsInScale;
|
||||
private Vector2? defaultOrigin;
|
||||
|
||||
private bool isFlippedX;
|
||||
private bool isFlippedY;
|
||||
|
||||
public override void Begin()
|
||||
{
|
||||
if (objectsInScale != null)
|
||||
throw new InvalidOperationException($"Cannot {nameof(Begin)} a scale operation while another is in progress!");
|
||||
|
||||
changeHandler?.BeginChange();
|
||||
|
||||
objectsInScale = selectedItems.Cast<Drawable>().ToDictionary(d => d, d => new OriginalDrawableState(d));
|
||||
OriginalSurroundingQuad = ToLocalSpace(GeometryUtils.GetSurroundingQuad(objectsInScale.SelectMany(d => d.Key.ScreenSpaceDrawQuad.GetVertices().ToArray())));
|
||||
defaultOrigin = OriginalSurroundingQuad.Value.Centre;
|
||||
|
||||
isFlippedX = false;
|
||||
isFlippedY = false;
|
||||
}
|
||||
|
||||
public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both)
|
||||
{
|
||||
if (objectsInScale == null)
|
||||
throw new InvalidOperationException($"Cannot {nameof(Update)} a scale operation without calling {nameof(Begin)} first!");
|
||||
|
||||
Debug.Assert(defaultOrigin != null && OriginalSurroundingQuad != null);
|
||||
|
||||
var actualOrigin = ToScreenSpace(origin ?? defaultOrigin.Value);
|
||||
|
||||
if ((adjustAxis == Axes.Y && !allSelectedSupportManualSizing(Axes.Y)) ||
|
||||
(adjustAxis == Axes.X && !allSelectedSupportManualSizing(Axes.X)))
|
||||
return;
|
||||
|
||||
// If the selection has no area we cannot scale it
|
||||
if (OriginalSurroundingQuad.Value.Width == 0 || OriginalSurroundingQuad.Value.Height == 0)
|
||||
return;
|
||||
|
||||
// for now aspect lock scale adjustments that occur at corners.
|
||||
if (adjustAxis == Axes.Both)
|
||||
{
|
||||
// project scale vector along diagonal
|
||||
scale = new Vector2((scale.X + scale.Y) * 0.5f);
|
||||
}
|
||||
// If any of the selection have been rotated and the adjust axis is not both,
|
||||
// we would require skew logic to achieve a correct image editor-like scale.
|
||||
// For now we just ignore, because it would likely not be the user's expected transform anyway.
|
||||
|
||||
bool flippedX = scale.X < 0;
|
||||
bool flippedY = scale.Y < 0;
|
||||
Axes toFlip = Axes.None;
|
||||
|
||||
if (flippedX != isFlippedX)
|
||||
{
|
||||
isFlippedX = flippedX;
|
||||
toFlip |= Axes.X;
|
||||
}
|
||||
|
||||
if (flippedY != isFlippedY)
|
||||
{
|
||||
isFlippedY = flippedY;
|
||||
toFlip |= Axes.Y;
|
||||
}
|
||||
|
||||
if (toFlip != Axes.None)
|
||||
{
|
||||
PerformFlipFromScaleHandles?.Invoke(toFlip);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var (b, originalState) in objectsInScale)
|
||||
{
|
||||
UpdatePosition(b, GeometryUtils.GetScaledPosition(scale, actualOrigin, originalState.ScreenSpaceOriginPosition));
|
||||
|
||||
var currentScale = scale;
|
||||
if (Precision.AlmostEquals(MathF.Abs(b.Rotation) % 180, 90))
|
||||
currentScale = new Vector2(scale.Y, scale.X);
|
||||
|
||||
switch (adjustAxis)
|
||||
{
|
||||
case Axes.X:
|
||||
b.Width = MathF.Abs(originalState.Width * currentScale.X);
|
||||
break;
|
||||
|
||||
case Axes.Y:
|
||||
b.Height = MathF.Abs(originalState.Height * currentScale.Y);
|
||||
break;
|
||||
|
||||
case Axes.Both:
|
||||
b.Scale = originalState.Scale * currentScale;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void Commit()
|
||||
{
|
||||
if (objectsInScale == null)
|
||||
throw new InvalidOperationException($"Cannot {nameof(Commit)} a scale operation without calling {nameof(Begin)} first!");
|
||||
|
||||
changeHandler?.EndChange();
|
||||
|
||||
objectsInScale = null;
|
||||
defaultOrigin = null;
|
||||
}
|
||||
|
||||
private struct OriginalDrawableState
|
||||
{
|
||||
public float Width { get; }
|
||||
public float Height { get; }
|
||||
public Vector2 Scale { get; }
|
||||
public Vector2 ScreenSpaceOriginPosition { get; }
|
||||
|
||||
public OriginalDrawableState(Drawable drawable)
|
||||
{
|
||||
Width = drawable.Width;
|
||||
Height = drawable.Height;
|
||||
Scale = drawable.Scale;
|
||||
ScreenSpaceOriginPosition = drawable.ToScreenSpace(drawable.OriginPosition);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -27,7 +27,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
[Resolved]
|
||||
private SelectionRotationHandler? rotationHandler { get; set; }
|
||||
|
||||
public Func<Vector2, Anchor, bool>? OnScale;
|
||||
[Resolved]
|
||||
private SelectionScaleHandler? scaleHandler { get; set; }
|
||||
|
||||
public Func<Direction, bool, bool>? OnFlip;
|
||||
public Func<bool>? OnReverse;
|
||||
|
||||
@ -57,60 +59,11 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
|
||||
private readonly IBindable<bool> canRotate = new BindableBool();
|
||||
|
||||
private bool canScaleX;
|
||||
private readonly IBindable<bool> canScaleX = new BindableBool();
|
||||
|
||||
/// <summary>
|
||||
/// Whether horizontal scaling (from the left or right edge) support should be enabled.
|
||||
/// </summary>
|
||||
public bool CanScaleX
|
||||
{
|
||||
get => canScaleX;
|
||||
set
|
||||
{
|
||||
if (canScaleX == value) return;
|
||||
private readonly IBindable<bool> canScaleY = new BindableBool();
|
||||
|
||||
canScaleX = value;
|
||||
recreate();
|
||||
}
|
||||
}
|
||||
|
||||
private bool canScaleY;
|
||||
|
||||
/// <summary>
|
||||
/// Whether vertical scaling (from the top or bottom edge) support should be enabled.
|
||||
/// </summary>
|
||||
public bool CanScaleY
|
||||
{
|
||||
get => canScaleY;
|
||||
set
|
||||
{
|
||||
if (canScaleY == value) return;
|
||||
|
||||
canScaleY = value;
|
||||
recreate();
|
||||
}
|
||||
}
|
||||
|
||||
private bool canScaleDiagonally;
|
||||
|
||||
/// <summary>
|
||||
/// Whether diagonal scaling (from a corner) support should be enabled.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// There are some cases where we only want to allow proportional resizing, and not allow
|
||||
/// one or both explicit directions of scale.
|
||||
/// </remarks>
|
||||
public bool CanScaleDiagonally
|
||||
{
|
||||
get => canScaleDiagonally;
|
||||
set
|
||||
{
|
||||
if (canScaleDiagonally == value) return;
|
||||
|
||||
canScaleDiagonally = value;
|
||||
recreate();
|
||||
}
|
||||
}
|
||||
private readonly IBindable<bool> canScaleDiagonally = new BindableBool();
|
||||
|
||||
private bool canFlipX;
|
||||
|
||||
@ -176,7 +129,17 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
if (rotationHandler != null)
|
||||
canRotate.BindTo(rotationHandler.CanRotateSelectionOrigin);
|
||||
|
||||
canRotate.BindValueChanged(_ => recreate(), true);
|
||||
if (scaleHandler != null)
|
||||
{
|
||||
canScaleX.BindTo(scaleHandler.CanScaleX);
|
||||
canScaleY.BindTo(scaleHandler.CanScaleY);
|
||||
canScaleDiagonally.BindTo(scaleHandler.CanScaleDiagonally);
|
||||
}
|
||||
|
||||
canRotate.BindValueChanged(_ => recreate());
|
||||
canScaleX.BindValueChanged(_ => recreate());
|
||||
canScaleY.BindValueChanged(_ => recreate());
|
||||
canScaleDiagonally.BindValueChanged(_ => recreate(), true);
|
||||
}
|
||||
|
||||
protected override bool OnKeyDown(KeyDownEvent e)
|
||||
@ -265,9 +228,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
}
|
||||
};
|
||||
|
||||
if (CanScaleX) addXScaleComponents();
|
||||
if (CanScaleDiagonally) addFullScaleComponents();
|
||||
if (CanScaleY) addYScaleComponents();
|
||||
if (canScaleX.Value) addXScaleComponents();
|
||||
if (canScaleDiagonally.Value) addFullScaleComponents();
|
||||
if (canScaleY.Value) addYScaleComponents();
|
||||
if (CanFlipX) addXFlipComponents();
|
||||
if (CanFlipY) addYFlipComponents();
|
||||
if (canRotate.Value) addRotationComponents();
|
||||
@ -353,7 +316,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
var handle = new SelectionBoxScaleHandle
|
||||
{
|
||||
Anchor = anchor,
|
||||
HandleScale = (delta, a) => OnScale?.Invoke(delta, a)
|
||||
};
|
||||
|
||||
handle.OperationStarted += operationStarted;
|
||||
|
@ -1,19 +1,21 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.EnumExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Utils;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Compose.Components
|
||||
{
|
||||
public partial class SelectionBoxScaleHandle : SelectionBoxDragHandle
|
||||
{
|
||||
public Action<Vector2, Anchor> HandleScale { get; set; }
|
||||
[Resolved]
|
||||
private SelectionScaleHandler? scaleHandler { get; set; }
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
@ -21,10 +23,108 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
Size = new Vector2(10);
|
||||
}
|
||||
|
||||
private Anchor originalAnchor;
|
||||
|
||||
protected override bool OnDragStart(DragStartEvent e)
|
||||
{
|
||||
if (e.Button != MouseButton.Left)
|
||||
return false;
|
||||
|
||||
if (scaleHandler == null) return false;
|
||||
|
||||
originalAnchor = Anchor;
|
||||
|
||||
scaleHandler.Begin();
|
||||
return true;
|
||||
}
|
||||
|
||||
private Vector2 rawScale;
|
||||
|
||||
protected override void OnDrag(DragEvent e)
|
||||
{
|
||||
HandleScale?.Invoke(e.Delta, Anchor);
|
||||
base.OnDrag(e);
|
||||
|
||||
if (scaleHandler == null) return;
|
||||
|
||||
rawScale = convertDragEventToScaleMultiplier(e);
|
||||
|
||||
applyScale(shouldLockAspectRatio: isCornerAnchor(originalAnchor) && e.ShiftPressed);
|
||||
}
|
||||
|
||||
protected override bool OnKeyDown(KeyDownEvent e)
|
||||
{
|
||||
if (IsDragged)
|
||||
{
|
||||
applyScale(shouldLockAspectRatio: isCornerAnchor(originalAnchor) && e.ShiftPressed);
|
||||
return true;
|
||||
}
|
||||
|
||||
return base.OnKeyDown(e);
|
||||
}
|
||||
|
||||
protected override void OnKeyUp(KeyUpEvent e)
|
||||
{
|
||||
base.OnKeyUp(e);
|
||||
|
||||
if (IsDragged)
|
||||
applyScale(shouldLockAspectRatio: isCornerAnchor(originalAnchor) && e.ShiftPressed);
|
||||
}
|
||||
|
||||
protected override void OnDragEnd(DragEndEvent e)
|
||||
{
|
||||
scaleHandler?.Commit();
|
||||
}
|
||||
|
||||
private Vector2 convertDragEventToScaleMultiplier(DragEvent e)
|
||||
{
|
||||
Vector2 scale = e.MousePosition - e.MouseDownPosition;
|
||||
adjustScaleFromAnchor(ref scale);
|
||||
|
||||
var surroundingQuad = scaleHandler!.OriginalSurroundingQuad!.Value;
|
||||
scale.X = Precision.AlmostEquals(surroundingQuad.Width, 0) ? 0 : scale.X / surroundingQuad.Width;
|
||||
scale.Y = Precision.AlmostEquals(surroundingQuad.Height, 0) ? 0 : scale.Y / surroundingQuad.Height;
|
||||
|
||||
return scale + Vector2.One;
|
||||
}
|
||||
|
||||
private void adjustScaleFromAnchor(ref Vector2 scale)
|
||||
{
|
||||
// cancel out scale in axes we don't care about (based on which drag handle was used).
|
||||
if ((originalAnchor & Anchor.x1) > 0) scale.X = 0;
|
||||
if ((originalAnchor & Anchor.y1) > 0) scale.Y = 0;
|
||||
|
||||
// reverse the scale direction if dragging from top or left.
|
||||
if ((originalAnchor & Anchor.x0) > 0) scale.X = -scale.X;
|
||||
if ((originalAnchor & Anchor.y0) > 0) scale.Y = -scale.Y;
|
||||
}
|
||||
|
||||
private void applyScale(bool shouldLockAspectRatio)
|
||||
{
|
||||
var newScale = shouldLockAspectRatio
|
||||
? new Vector2((rawScale.X + rawScale.Y) * 0.5f)
|
||||
: rawScale;
|
||||
|
||||
var scaleOrigin = originalAnchor.Opposite().PositionOnQuad(scaleHandler!.OriginalSurroundingQuad!.Value);
|
||||
scaleHandler!.Update(newScale, scaleOrigin, getAdjustAxis());
|
||||
}
|
||||
|
||||
private Axes getAdjustAxis()
|
||||
{
|
||||
switch (originalAnchor)
|
||||
{
|
||||
case Anchor.TopCentre:
|
||||
case Anchor.BottomCentre:
|
||||
return Axes.Y;
|
||||
|
||||
case Anchor.CentreLeft:
|
||||
case Anchor.CentreRight:
|
||||
return Axes.X;
|
||||
|
||||
default:
|
||||
return Axes.Both;
|
||||
}
|
||||
}
|
||||
|
||||
private bool isCornerAnchor(Anchor anchor) => !anchor.HasFlagFast(Anchor.x1) && !anchor.HasFlagFast(Anchor.y1);
|
||||
}
|
||||
}
|
||||
|
@ -57,6 +57,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
|
||||
public SelectionRotationHandler RotationHandler { get; private set; }
|
||||
|
||||
public SelectionScaleHandler ScaleHandler { get; private set; }
|
||||
|
||||
protected SelectionHandler()
|
||||
{
|
||||
selectedBlueprints = new List<SelectionBlueprint<T>>();
|
||||
@ -69,6 +71,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
{
|
||||
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
|
||||
dependencies.CacheAs(RotationHandler = CreateRotationHandler());
|
||||
dependencies.CacheAs(ScaleHandler = CreateScaleHandler());
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
@ -78,6 +81,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
AddRangeInternal(new Drawable[]
|
||||
{
|
||||
RotationHandler,
|
||||
ScaleHandler,
|
||||
SelectionBox = CreateSelectionBox(),
|
||||
});
|
||||
|
||||
@ -93,7 +97,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
OperationStarted = OnOperationBegan,
|
||||
OperationEnded = OnOperationEnded,
|
||||
|
||||
OnScale = HandleScale,
|
||||
OnFlip = HandleFlip,
|
||||
OnReverse = HandleReverse,
|
||||
};
|
||||
@ -157,6 +160,11 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
/// <returns>Whether any items could be scaled.</returns>
|
||||
public virtual bool HandleScale(Vector2 scale, Anchor anchor) => false;
|
||||
|
||||
/// <summary>
|
||||
/// Creates the handler to use for scale operations.
|
||||
/// </summary>
|
||||
public virtual SelectionScaleHandler CreateScaleHandler() => new SelectionScaleHandler();
|
||||
|
||||
/// <summary>
|
||||
/// Handles the selected items being flipped.
|
||||
/// </summary>
|
||||
|
@ -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 osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Compose.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// Base handler for editor scale operations.
|
||||
/// </summary>
|
||||
public partial class SelectionScaleHandler : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether horizontal scaling (from the left or right edge) support should be enabled.
|
||||
/// </summary>
|
||||
public Bindable<bool> CanScaleX { get; private set; } = new BindableBool();
|
||||
|
||||
/// <summary>
|
||||
/// Whether vertical scaling (from the top or bottom edge) support should be enabled.
|
||||
/// </summary>
|
||||
public Bindable<bool> CanScaleY { get; private set; } = new BindableBool();
|
||||
|
||||
/// <summary>
|
||||
/// Whether diagonal scaling (from a corner) support should be enabled.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// There are some cases where we only want to allow proportional resizing, and not allow
|
||||
/// one or both explicit directions of scale.
|
||||
/// </remarks>
|
||||
public Bindable<bool> CanScaleDiagonally { get; private set; } = new BindableBool();
|
||||
|
||||
public Quad? OriginalSurroundingQuad { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// Performs a single, instant, atomic scale 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="scale">The scale to apply, as multiplier.</param>
|
||||
/// <param name="origin">
|
||||
/// The origin point to scale from.
|
||||
/// If the default <see langword="null"/> value is supplied, a sane implementation-defined default will be used.
|
||||
/// </param>
|
||||
/// <param name="adjustAxis">The axes to adjust the scale in.</param>
|
||||
public void ScaleSelection(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both)
|
||||
{
|
||||
Begin();
|
||||
Update(scale, origin, adjustAxis);
|
||||
Commit();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Begins a continuous scale operation.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This flow is intended to be used when a scale operation is made incrementally (such as when dragging a scale handle or slider).
|
||||
/// For instantaneous, atomic operations, use the convenience <see cref="ScaleSelection"/> method.
|
||||
/// </remarks>
|
||||
public virtual void Begin()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates a continuous scale operation.
|
||||
/// Must be preceded by a <see cref="Begin"/> call.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This flow is intended to be used when a scale operation is made incrementally (such as when dragging a scale handle or slider).
|
||||
/// As such, the values of <paramref name="scale"/> and <paramref name="origin"/> supplied should be relative to the state of the objects being scaled
|
||||
/// when <see cref="Begin"/> was called, rather than instantaneous deltas.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// For instantaneous, atomic operations, use the convenience <see cref="ScaleSelection"/> method.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <param name="scale">The Scale to apply, as multiplier.</param>
|
||||
/// <param name="origin">
|
||||
/// The origin point to scale from.
|
||||
/// If the default <see langword="null"/> value is supplied, a sane implementation-defined default will be used.
|
||||
/// </param>
|
||||
/// <param name="adjustAxis">The axes to adjust the scale in.</param>
|
||||
public virtual void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ends a continuous scale operation.
|
||||
/// Must be preceded by a <see cref="Begin"/> call.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This flow is intended to be used when a scale operation is made incrementally (such as when dragging a scale handle or slider).
|
||||
/// For instantaneous, atomic operations, use the convenience <see cref="ScaleSelection"/> method.
|
||||
/// </remarks>
|
||||
public virtual void Commit()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -78,6 +78,15 @@ namespace osu.Game.Utils
|
||||
return position;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Given a scale multiplier, an origin, and a position,
|
||||
/// will return the scaled position in screen space coordinates.
|
||||
/// </summary>
|
||||
public static Vector2 GetScaledPosition(Vector2 scale, Vector2 origin, Vector2 position)
|
||||
{
|
||||
return origin + (position - origin) * scale;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a quad surrounding the provided points.
|
||||
/// </summary>
|
||||
|
Loading…
Reference in New Issue
Block a user