diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index cea2adc6e2..c36b535bfa 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -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; } - - /// - /// During a transform, the initial path types of a single selected slider are stored so they - /// can be maintained throughout the operation. - /// - private List? 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 oldControlPoints = new Queue(); - - 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; } - /// - /// Clamp scale for multi-object-scaling where selection does not exceed playfield bounds or flip. - /// - /// The hitobjects to be scaled - /// The anchor from which the scale operation is performed - /// The scale to be clamped - /// The clamped scale vector - 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; - } - /// /// All osu! hitobjects which can be moved/rotated/scaled. /// diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs new file mode 100644 index 0000000000..8068c73131 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs @@ -0,0 +1,205 @@ +// Copyright (c) ppy Pty Ltd . 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 selectedItems { get; } = new BindableList(); + + [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? originalPositions; + private Dictionary? originalPathControlPointPositions; + private Dictionary? 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().ToDictionary( + obj => obj, + obj => obj.Path.ControlPoints.Select(point => point.Position).ToArray()); + originalPathControlPointTypes = objectsInScale.OfType().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 selectedMovableObjects => selectedItems.Cast() + .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); + } + + /// + /// Clamp scale for multi-object-scaling where selection does not exceed playfield bounds or flip. + /// + /// The quad surrounding the hitobjects + /// The origin from which the scale operation is performed + /// The scale to be clamped + /// The clamped scale vector + 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; + } + } +} diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs index 0b16941bc4..e8b3e430eb 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs @@ -27,7 +27,6 @@ namespace osu.Game.Screens.Edit.Compose.Components [Resolved] private SelectionRotationHandler? rotationHandler { get; set; } - public Func? OnScale; public Func? OnFlip; public Func? 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; diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs index 7943065c82..56c5585ae7 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs @@ -1,19 +1,22 @@ // Copyright (c) ppy Pty Ltd . 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 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()); } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 3c859c65ff..dd6bd43f4d 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -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>(); @@ -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 /// Whether any items could be scaled. public virtual bool HandleScale(Vector2 scale, Anchor anchor) => false; + /// + /// Creates the handler to use for scale operations. + /// + public virtual SelectionScaleHandler CreateScaleHandler() => new SelectionScaleHandler(); + /// /// Handles the selected items being flipped. /// diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs new file mode 100644 index 0000000000..b7c8f16a02 --- /dev/null +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs @@ -0,0 +1,88 @@ +// Copyright (c) ppy Pty Ltd . 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 +{ + /// + /// Base handler for editor scale operations. + /// + public partial class SelectionScaleHandler : Component + { + /// + /// Whether the scale can currently be performed. + /// + public Bindable CanScale { get; private set; } = new BindableBool(); + + public Quad? OriginalSurroundingQuad { get; protected set; } + + /// + /// Performs a single, instant, atomic scale operation. + /// + /// + /// This method is intended to be used in atomic contexts (such as when pressing a single button). + /// For continuous operations, see the -- flow. + /// + /// The scale to apply, as multiplier. + /// + /// The origin point to scale from. + /// If the default value is supplied, a sane implementation-defined default will be used. + /// + public void ScaleSelection(Vector2 scale, Vector2? origin = null) + { + Begin(); + Update(scale, origin); + Commit(); + } + + /// + /// Begins a continuous scale operation. + /// + /// + /// 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 method. + /// + public virtual void Begin() + { + } + + /// + /// Updates a continuous scale operation. + /// Must be preceded by a call. + /// + /// + /// + /// 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 and supplied should be relative to the state of the objects being scaled + /// when was called, rather than instantaneous deltas. + /// + /// + /// For instantaneous, atomic operations, use the convenience method. + /// + /// + /// The Scale to apply, as multiplier. + /// + /// The origin point to scale from. + /// If the default value is supplied, a sane implementation-defined default will be used. + /// + public virtual void Update(Vector2 scale, Vector2? origin = null) + { + } + + /// + /// Ends a continuous scale operation. + /// Must be preceded by a call. + /// + /// + /// 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 method. + /// + public virtual void Commit() + { + } + } +} diff --git a/osu.Game/Utils/GeometryUtils.cs b/osu.Game/Utils/GeometryUtils.cs index 725e93d098..ef362d8223 100644 --- a/osu.Game/Utils/GeometryUtils.cs +++ b/osu.Game/Utils/GeometryUtils.cs @@ -79,6 +79,15 @@ namespace osu.Game.Utils return position; } + /// + /// Given a scale multiplier, an origin, and a position, + /// will return the scaled position in screen space coordinates. + /// + public static Vector2 GetScaledPositionMultiply(Vector2 scale, Vector2 origin, Vector2 position) + { + return origin + (position - origin) * scale; + } + /// /// Returns a quad surrounding the provided points. ///