mirror of
https://github.com/ppy/osu
synced 2025-01-24 23:03:14 +00:00
Refactor scale handling in editor to facilitate reuse
This commit is contained in:
parent
e669e28dc9
commit
26c0d1077a
@ -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,15 +24,6 @@ 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();
|
||||
@ -46,12 +36,6 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
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)
|
||||
@ -135,96 +119,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()
|
||||
{
|
||||
@ -248,43 +145,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>
|
||||
|
205
osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs
Normal file
205
osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs
Normal file
@ -0,0 +1,205 @@
|
||||
// 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.Primitives;
|
||||
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);
|
||||
CanScale.Value = quad.Width > 0 || quad.Height > 0;
|
||||
}
|
||||
|
||||
private OsuHitObject[]? objectsInScale;
|
||||
|
||||
private Vector2? defaultOrigin;
|
||||
private Dictionary<OsuHitObject, Vector2>? originalPositions;
|
||||
private Dictionary<IHasPath, Vector2[]>? originalPathControlPointPositions;
|
||||
private Dictionary<IHasPath, PathType?[]>? originalPathControlPointTypes;
|
||||
|
||||
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.ToArray();
|
||||
OriginalSurroundingQuad = objectsInScale.Length == 1 && objectsInScale.First() is Slider slider
|
||||
? GeometryUtils.GetSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position))
|
||||
: GeometryUtils.GetSurroundingQuad(objectsInScale);
|
||||
defaultOrigin = OriginalSurroundingQuad.Value.Centre;
|
||||
originalPositions = objectsInScale.ToDictionary(obj => obj, obj => obj.Position);
|
||||
originalPathControlPointPositions = objectsInScale.OfType<IHasPath>().ToDictionary(
|
||||
obj => obj,
|
||||
obj => obj.Path.ControlPoints.Select(point => point.Position).ToArray());
|
||||
originalPathControlPointTypes = objectsInScale.OfType<IHasPath>().ToDictionary(
|
||||
obj => obj,
|
||||
obj => obj.Path.ControlPoints.Select(p => p.Type).ToArray());
|
||||
}
|
||||
|
||||
public override void Update(Vector2 scale, Vector2? origin = null)
|
||||
{
|
||||
if (objectsInScale == null)
|
||||
throw new InvalidOperationException($"Cannot {nameof(Update)} a scale operation without calling {nameof(Begin)} first!");
|
||||
|
||||
Debug.Assert(originalPositions != null && originalPathControlPointPositions != null && defaultOrigin != null && originalPathControlPointTypes != 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.Length == 1 && objectsInScale.First() is Slider slider)
|
||||
scaleSlider(slider, scale, originalPathControlPointPositions[slider], originalPathControlPointTypes[slider]);
|
||||
else
|
||||
{
|
||||
scale = getClampedScale(OriginalSurroundingQuad.Value, actualOrigin, scale);
|
||||
|
||||
foreach (var ho in objectsInScale)
|
||||
{
|
||||
ho.Position = GeometryUtils.GetScaledPositionMultiply(scale, actualOrigin, originalPositions[ho]);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
originalPositions = null;
|
||||
originalPathControlPointPositions = null;
|
||||
originalPathControlPointTypes = 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)
|
||||
{
|
||||
// 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);
|
||||
|
||||
scale.X = selectionQuad.TopLeft.X - origin.X < 0 ? MathHelper.Clamp(scale.X, tl2.X, tl1.X) : MathHelper.Clamp(scale.X, tl1.X, tl2.X);
|
||||
scale.Y = selectionQuad.TopLeft.Y - origin.Y < 0 ? MathHelper.Clamp(scale.Y, tl2.Y, tl1.Y) : MathHelper.Clamp(scale.Y, tl1.Y, tl2.Y);
|
||||
scale.X = selectionQuad.BottomRight.X - origin.X < 0 ? MathHelper.Clamp(scale.X, br2.X, br1.X) : MathHelper.Clamp(scale.X, br1.X, br2.X);
|
||||
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 scale;
|
||||
}
|
||||
|
||||
private void moveSelectionInBounds()
|
||||
{
|
||||
Quad quad = GeometryUtils.GetSurroundingQuad(objectsInScale!);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
@ -27,7 +27,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
[Resolved]
|
||||
private SelectionRotationHandler? rotationHandler { get; set; }
|
||||
|
||||
public Func<Vector2, Anchor, bool>? OnScale;
|
||||
public Func<Direction, bool, bool>? OnFlip;
|
||||
public Func<bool>? OnReverse;
|
||||
|
||||
@ -353,7 +352,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,22 @@
|
||||
// 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.Graphics;
|
||||
using osu.Framework.Input.Events;
|
||||
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 SelectionBox selectionBox { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private SelectionScaleHandler? scaleHandler { get; set; }
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
@ -21,10 +24,93 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
Size = new Vector2(10);
|
||||
}
|
||||
|
||||
protected override bool OnDragStart(DragStartEvent e)
|
||||
{
|
||||
if (e.Button != MouseButton.Left)
|
||||
return false;
|
||||
|
||||
if (scaleHandler == null) return false;
|
||||
|
||||
scaleHandler.Begin();
|
||||
return true;
|
||||
}
|
||||
|
||||
private Vector2 getOriginPosition()
|
||||
{
|
||||
var quad = scaleHandler!.OriginalSurroundingQuad!.Value;
|
||||
Vector2 origin = quad.TopLeft;
|
||||
|
||||
if ((Anchor & Anchor.x0) > 0)
|
||||
origin.X += quad.Width;
|
||||
|
||||
if ((Anchor & Anchor.y0) > 0)
|
||||
origin.Y += quad.Height;
|
||||
|
||||
return origin;
|
||||
}
|
||||
|
||||
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(shouldKeepAspectRatio: e.ShiftPressed);
|
||||
}
|
||||
|
||||
protected override bool OnKeyDown(KeyDownEvent e)
|
||||
{
|
||||
if (IsDragged && (e.Key == Key.ShiftLeft || e.Key == Key.ShiftRight))
|
||||
{
|
||||
applyScale(shouldKeepAspectRatio: true);
|
||||
return true;
|
||||
}
|
||||
|
||||
return base.OnKeyDown(e);
|
||||
}
|
||||
|
||||
protected override void OnKeyUp(KeyUpEvent e)
|
||||
{
|
||||
base.OnKeyUp(e);
|
||||
|
||||
if (IsDragged && (e.Key == Key.ShiftLeft || e.Key == Key.ShiftRight))
|
||||
applyScale(shouldKeepAspectRatio: false);
|
||||
}
|
||||
|
||||
protected override void OnDragEnd(DragEndEvent e)
|
||||
{
|
||||
scaleHandler?.Commit();
|
||||
}
|
||||
|
||||
private Vector2 convertDragEventToScaleMultiplier(DragEvent e)
|
||||
{
|
||||
Vector2 scale = e.MousePosition - e.MouseDownPosition;
|
||||
adjustScaleFromAnchor(ref scale);
|
||||
return Vector2.Divide(scale, scaleHandler!.OriginalSurroundingQuad!.Value.Size) + 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 ((Anchor & Anchor.x1) > 0) scale.X = 1;
|
||||
if ((Anchor & Anchor.y1) > 0) scale.Y = 1;
|
||||
|
||||
// reverse the scale direction if dragging from top or left.
|
||||
if ((Anchor & Anchor.x0) > 0) scale.X = -scale.X;
|
||||
if ((Anchor & Anchor.y0) > 0) scale.Y = -scale.Y;
|
||||
}
|
||||
|
||||
private void applyScale(bool shouldKeepAspectRatio)
|
||||
{
|
||||
var newScale = shouldKeepAspectRatio
|
||||
? new Vector2(MathF.Max(rawScale.X, rawScale.Y))
|
||||
: rawScale;
|
||||
|
||||
scaleHandler!.Update(newScale, getOriginPosition());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,88 @@
|
||||
// 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 the scale can currently be performed.
|
||||
/// </summary>
|
||||
public Bindable<bool> CanScale { 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>
|
||||
public void ScaleSelection(Vector2 scale, Vector2? origin = null)
|
||||
{
|
||||
Begin();
|
||||
Update(scale, origin);
|
||||
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>
|
||||
public virtual void Update(Vector2 scale, Vector2? origin = null)
|
||||
{
|
||||
}
|
||||
|
||||
/// <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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -79,6 +79,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 GetScaledPositionMultiply(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