Refactor scale handling in editor to facilitate reuse

This commit is contained in:
OliBomby 2024-01-20 00:22:53 +01:00
parent e669e28dc9
commit 26c0d1077a
7 changed files with 402 additions and 148 deletions

View File

@ -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>

View 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;
}
}
}

View File

@ -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;

View File

@ -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());
}
}
}

View File

@ -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>

View File

@ -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()
{
}
}
}

View File

@ -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>