From 26c0d1077a7a4d00fb9ae22bcb16dde08365b987 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 20 Jan 2024 00:22:53 +0100 Subject: [PATCH 01/25] Refactor scale handling in editor to facilitate reuse --- .../Edit/OsuSelectionHandler.cs | 142 +----------- .../Edit/OsuSelectionScaleHandler.cs | 205 ++++++++++++++++++ .../Edit/Compose/Components/SelectionBox.cs | 2 - .../Components/SelectionBoxScaleHandle.cs | 94 +++++++- .../Compose/Components/SelectionHandler.cs | 10 +- .../Components/SelectionScaleHandler.cs | 88 ++++++++ osu.Game/Utils/GeometryUtils.cs | 9 + 7 files changed, 402 insertions(+), 148 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs create mode 100644 osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs 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. /// From a4f771ec089baff91ddea3d4714355e64a8237dd Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 20 Jan 2024 01:13:01 +0100 Subject: [PATCH 02/25] refactor CanScale properties --- .../Edit/OsuSelectionHandler.cs | 5 +- .../Edit/OsuSelectionScaleHandler.cs | 7 +- .../SkinEditor/SkinSelectionHandler.cs | 3 - .../Edit/Compose/Components/SelectionBox.cs | 76 +++++-------------- .../Components/SelectionScaleHandler.cs | 18 ++++- osu.Game/Utils/GeometryUtils.cs | 2 +- 6 files changed, 44 insertions(+), 67 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index c36b535bfa..00c90cdbd6 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -30,9 +30,8 @@ namespace osu.Game.Rulesets.Osu.Edit 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); } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs index 8068c73131..7b0ae947e7 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs @@ -47,7 +47,10 @@ namespace osu.Game.Rulesets.Osu.Edit private void updateState() { var quad = GeometryUtils.GetSurroundingQuad(selectedMovableObjects); - CanScale.Value = quad.Width > 0 || quad.Height > 0; + + CanScaleX.Value = quad.Width > 0; + CanScaleY.Value = quad.Height > 0; + CanScaleDiagonally.Value = CanScaleX.Value && CanScaleY.Value; } private OsuHitObject[]? objectsInScale; @@ -98,7 +101,7 @@ namespace osu.Game.Rulesets.Osu.Edit foreach (var ho in objectsInScale) { - ho.Position = GeometryUtils.GetScaledPositionMultiply(scale, actualOrigin, originalPositions[ho]); + ho.Position = GeometryUtils.GetScaledPosition(scale, actualOrigin, originalPositions[ho]); } } diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs index cf6fb60636..efca6f0080 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs @@ -218,9 +218,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; diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs index e8b3e430eb..2329a466fe 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs @@ -27,6 +27,9 @@ namespace osu.Game.Screens.Edit.Compose.Components [Resolved] private SelectionRotationHandler? rotationHandler { get; set; } + [Resolved] + private SelectionScaleHandler? scaleHandler { get; set; } + public Func? OnFlip; public Func? OnReverse; @@ -56,60 +59,11 @@ namespace osu.Game.Screens.Edit.Compose.Components private readonly IBindable canRotate = new BindableBool(); - private bool canScaleX; + private readonly IBindable canScaleX = new BindableBool(); - /// - /// Whether horizontal scaling (from the left or right edge) support should be enabled. - /// - public bool CanScaleX - { - get => canScaleX; - set - { - if (canScaleX == value) return; + private readonly IBindable canScaleY = new BindableBool(); - canScaleX = value; - recreate(); - } - } - - private bool canScaleY; - - /// - /// Whether vertical scaling (from the top or bottom edge) support should be enabled. - /// - public bool CanScaleY - { - get => canScaleY; - set - { - if (canScaleY == value) return; - - canScaleY = value; - recreate(); - } - } - - private bool canScaleDiagonally; - - /// - /// Whether diagonal scaling (from a corner) support should be enabled. - /// - /// - /// There are some cases where we only want to allow proportional resizing, and not allow - /// one or both explicit directions of scale. - /// - public bool CanScaleDiagonally - { - get => canScaleDiagonally; - set - { - if (canScaleDiagonally == value) return; - - canScaleDiagonally = value; - recreate(); - } - } + private readonly IBindable canScaleDiagonally = new BindableBool(); private bool canFlipX; @@ -175,7 +129,17 @@ namespace osu.Game.Screens.Edit.Compose.Components if (rotationHandler != null) canRotate.BindTo(rotationHandler.CanRotate); - 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) @@ -264,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(); diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs index b7c8f16a02..59406b3184 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs @@ -14,9 +14,23 @@ namespace osu.Game.Screens.Edit.Compose.Components public partial class SelectionScaleHandler : Component { /// - /// Whether the scale can currently be performed. + /// Whether horizontal scaling (from the left or right edge) support should be enabled. /// - public Bindable CanScale { get; private set; } = new BindableBool(); + public Bindable CanScaleX { get; private set; } = new BindableBool(); + + /// + /// Whether vertical scaling (from the top or bottom edge) support should be enabled. + /// + public Bindable CanScaleY { get; private set; } = new BindableBool(); + + /// + /// Whether diagonal scaling (from a corner) support should be enabled. + /// + /// + /// There are some cases where we only want to allow proportional resizing, and not allow + /// one or both explicit directions of scale. + /// + public Bindable CanScaleDiagonally { get; private set; } = new BindableBool(); public Quad? OriginalSurroundingQuad { get; protected set; } diff --git a/osu.Game/Utils/GeometryUtils.cs b/osu.Game/Utils/GeometryUtils.cs index ef362d8223..6d8237ea34 100644 --- a/osu.Game/Utils/GeometryUtils.cs +++ b/osu.Game/Utils/GeometryUtils.cs @@ -83,7 +83,7 @@ namespace osu.Game.Utils /// 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) + public static Vector2 GetScaledPosition(Vector2 scale, Vector2 origin, Vector2 position) { return origin + (position - origin) * scale; } From bc0e6baba70cd9c69dbf9f87180c24c8a47dcff9 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 20 Jan 2024 01:13:05 +0100 Subject: [PATCH 03/25] fix test --- .../Editing/TestSceneComposeSelectBox.cs | 77 ++++++++++++------- 1 file changed, 51 insertions(+), 26 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs index f6637d0e80..680a76f9b8 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs @@ -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 getTargetContainer; @@ -125,6 +105,51 @@ namespace osu.Game.Tests.Visual.Editing } } + private partial class TestSelectionScaleHandler : SelectionScaleHandler + { + private readonly Func getTargetContainer; + + public TestSelectionScaleHandler(Func 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) + { + 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() { From ed430a3df4bbcacf5860db8f21ac625d2a176bbc Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 20 Jan 2024 02:49:56 +0100 Subject: [PATCH 04/25] refactor skin editor scale --- .../SkinEditor/SkinSelectionHandler.cs | 157 +------------- .../SkinEditor/SkinSelectionScaleHandler.cs | 198 ++++++++++++++++++ 2 files changed, 204 insertions(+), 151 deletions(-) create mode 100644 osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs index efca6f0080..2d8db61ee7 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs @@ -7,10 +7,8 @@ using System.Linq; using osu.Framework.Allocation; 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; @@ -31,148 +29,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 = b.ScreenSpaceSelectionPoint; - - 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) @@ -410,16 +276,5 @@ namespace osu.Game.Overlays.SkinEditor drawable.Anchor = anchor; drawable.Position -= drawable.AnchorPosition - previousAnchor; } - - 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; - } } } diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs new file mode 100644 index 0000000000..46b39645b2 --- /dev/null +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs @@ -0,0 +1,198 @@ +// 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.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 UpdatePosition { get; init; } = null!; + + public event Action? PerformFlipFromScaleHandles; + + [Resolved] + private IEditorChangeHandler? changeHandler { get; set; } + + private BindableList selectedItems { get; } = new BindableList(); + + [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 Drawable[]? objectsInScale; + + private Vector2? defaultOrigin; + private Dictionary? originalWidths; + private Dictionary? originalHeights; + private Dictionary? originalScales; + private Dictionary? originalPositions; + + 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().ToArray(); + originalWidths = objectsInScale.ToDictionary(d => d, d => d.Width); + originalHeights = objectsInScale.ToDictionary(d => d, d => d.Height); + originalScales = objectsInScale.ToDictionary(d => d, d => d.Scale); + originalPositions = objectsInScale.ToDictionary(d => d, d => d.ToScreenSpace(d.OriginPosition)); + OriginalSurroundingQuad = GeometryUtils.GetSurroundingQuad(objectsInScale.SelectMany(d => d.ScreenSpaceDrawQuad.GetVertices().ToArray())); + defaultOrigin = OriginalSurroundingQuad.Value.Centre; + } + + 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(originalWidths != null && originalHeights != null && originalScales != null && originalPositions != null && defaultOrigin != null && OriginalSurroundingQuad != null); + + var actualOrigin = origin ?? defaultOrigin.Value; + + Axes adjustAxis = scale.X == 0 ? Axes.Y : scale.Y == 0 ? Axes.X : Axes.Both; + + if ((adjustAxis == Axes.Y && !allSelectedSupportManualSizing(Axes.Y)) || + (adjustAxis == Axes.X && !allSelectedSupportManualSizing(Axes.X))) + return; + + // the selection quad is always upright, so use an AABB rect to make mutating the values easier. + var selectionRect = OriginalSurroundingQuad.Value.AABBFloat; + + // If the selection has no area we cannot scale it + if (selectionRect.Area == 0) + return; + + // copy to mutate, as we will need to compare to the original later on. + var adjustedRect = selectionRect; + + // for now aspect lock scale adjustments that occur at corners.. + if (adjustAxis == Axes.Both) + { + // project scale vector along diagonal + Vector2 diag = new Vector2(1, 1).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 (objectsInScale.Any(b => !Precision.AlmostEquals(b.Rotation % 90, 0))) + { + if (adjustAxis == Axes.Y) + // 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; + } + + adjustedRect.Location = GeometryUtils.GetScaledPosition(scale, actualOrigin, OriginalSurroundingQuad!.Value.TopLeft); + adjustedRect.Size = OriginalSurroundingQuad!.Value.Size * scale; + + 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; + + PerformFlipFromScaleHandles?.Invoke(toFlip); + return; + } + + // 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 objectsInScale) + { + // each drawable's relative position should be maintained in the scaled quad. + var screenPosition = originalPositions[b]; + + 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 + ); + + UpdatePosition(b, newPositionInAdjusted); + + var currentScaledDelta = scaledDelta; + if (Precision.AlmostEquals(MathF.Abs(b.Rotation) % 180, 90)) + currentScaledDelta = new Vector2(scaledDelta.Y, scaledDelta.X); + + switch (adjustAxis) + { + case Axes.X: + b.Width = originalWidths[b] * currentScaledDelta.X; + break; + + case Axes.Y: + b.Height = originalHeights[b] * currentScaledDelta.Y; + break; + + case Axes.Both: + b.Scale = originalScales[b] * currentScaledDelta; + 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; + originalPositions = null; + originalWidths = null; + originalHeights = null; + originalScales = null; + defaultOrigin = null; + } + } +} From 6a57be0a50c8ddc20356f237dd80bc219226ba59 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 20 Jan 2024 13:04:05 +0100 Subject: [PATCH 05/25] clean up code and fix flipping --- .../SkinEditor/SkinSelectionScaleHandler.cs | 74 ++++++++----------- .../Components/SelectionBoxScaleHandle.cs | 19 +++-- 2 files changed, 43 insertions(+), 50 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs index 46b39645b2..c2f788a9e8 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs @@ -61,6 +61,9 @@ namespace osu.Game.Overlays.SkinEditor private Dictionary? originalScales; private Dictionary? originalPositions; + private bool isFlippedX; + private bool isFlippedY; + public override void Begin() { if (objectsInScale != null) @@ -75,6 +78,9 @@ namespace osu.Game.Overlays.SkinEditor originalPositions = objectsInScale.ToDictionary(d => d, d => d.ToScreenSpace(d.OriginPosition)); OriginalSurroundingQuad = GeometryUtils.GetSurroundingQuad(objectsInScale.SelectMany(d => d.ScreenSpaceDrawQuad.GetVertices().ToArray())); defaultOrigin = OriginalSurroundingQuad.Value.Centre; + + isFlippedX = false; + isFlippedY = false; } public override void Update(Vector2 scale, Vector2? origin = null) @@ -85,29 +91,21 @@ namespace osu.Game.Overlays.SkinEditor Debug.Assert(originalWidths != null && originalHeights != null && originalScales != null && originalPositions != null && defaultOrigin != null && OriginalSurroundingQuad != null); var actualOrigin = origin ?? defaultOrigin.Value; - Axes adjustAxis = scale.X == 0 ? Axes.Y : scale.Y == 0 ? Axes.X : Axes.Both; if ((adjustAxis == Axes.Y && !allSelectedSupportManualSizing(Axes.Y)) || (adjustAxis == Axes.X && !allSelectedSupportManualSizing(Axes.X))) return; - // the selection quad is always upright, so use an AABB rect to make mutating the values easier. - var selectionRect = OriginalSurroundingQuad.Value.AABBFloat; - // If the selection has no area we cannot scale it - if (selectionRect.Area == 0) + if (OriginalSurroundingQuad.Value.Width == 0 || OriginalSurroundingQuad.Value.Height == 0) return; - // copy to mutate, as we will need to compare to the original later on. - var adjustedRect = selectionRect; - // for now aspect lock scale adjustments that occur at corners.. if (adjustAxis == Axes.Both) { // project scale vector along diagonal - Vector2 diag = new Vector2(1, 1).Normalized(); - scale = Vector2.Dot(scale, diag) * diag; + scale = new Vector2((scale.X + scale.Y) * 0.5f); } // ..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). @@ -115,66 +113,54 @@ namespace osu.Game.Overlays.SkinEditor { if (adjustAxis == Axes.Y) // if dragging from the horizontal centre, only a vertical component is available. - scale.X = scale.Y / selectionRect.Height * selectionRect.Width; + scale.X = scale.Y; else // in all other cases (arbitrarily) use the horizontal component for aspect lock. - scale.Y = scale.X / selectionRect.Width * selectionRect.Height; + scale.Y = scale.X; } - adjustedRect.Location = GeometryUtils.GetScaledPosition(scale, actualOrigin, OriginalSurroundingQuad!.Value.TopLeft); - adjustedRect.Size = OriginalSurroundingQuad!.Value.Size * scale; + bool flippedX = scale.X < 0; + bool flippedY = scale.Y < 0; + Axes toFlip = Axes.None; - if (adjustedRect.Width <= 0 || adjustedRect.Height <= 0) + if (flippedX != isFlippedX) { - Axes toFlip = Axes.None; + isFlippedX = flippedX; + toFlip |= Axes.X; + } - if (adjustedRect.Width <= 0) toFlip |= Axes.X; - if (adjustedRect.Height <= 0) toFlip |= Axes.Y; + if (flippedY != isFlippedY) + { + isFlippedY = flippedY; + toFlip |= Axes.Y; + } + if (toFlip != Axes.None) + { PerformFlipFromScaleHandles?.Invoke(toFlip); return; } - // 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 objectsInScale) { - // each drawable's relative position should be maintained in the scaled quad. - var screenPosition = originalPositions[b]; + UpdatePosition(b, GeometryUtils.GetScaledPosition(scale, actualOrigin, originalPositions[b])); - 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 - ); - - UpdatePosition(b, newPositionInAdjusted); - - var currentScaledDelta = scaledDelta; + var currentScale = scale; if (Precision.AlmostEquals(MathF.Abs(b.Rotation) % 180, 90)) - currentScaledDelta = new Vector2(scaledDelta.Y, scaledDelta.X); + currentScale = new Vector2(scale.Y, scale.X); switch (adjustAxis) { case Axes.X: - b.Width = originalWidths[b] * currentScaledDelta.X; + b.Width = originalWidths[b] * currentScale.X; break; case Axes.Y: - b.Height = originalHeights[b] * currentScaledDelta.Y; + b.Height = originalHeights[b] * currentScale.Y; break; case Axes.Both: - b.Scale = originalScales[b] * currentScaledDelta; + b.Scale = originalScales[b] * currentScale; break; } } diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs index 56c5585ae7..6179be1d4f 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs @@ -5,6 +5,7 @@ using System; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Input.Events; +using osu.Framework.Logging; using osuTK; using osuTK.Input; @@ -24,6 +25,8 @@ 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) @@ -31,6 +34,8 @@ namespace osu.Game.Screens.Edit.Compose.Components if (scaleHandler == null) return false; + originalAnchor = Anchor; + scaleHandler.Begin(); return true; } @@ -40,10 +45,10 @@ namespace osu.Game.Screens.Edit.Compose.Components var quad = scaleHandler!.OriginalSurroundingQuad!.Value; Vector2 origin = quad.TopLeft; - if ((Anchor & Anchor.x0) > 0) + if ((originalAnchor & Anchor.x0) > 0) origin.X += quad.Width; - if ((Anchor & Anchor.y0) > 0) + if ((originalAnchor & Anchor.y0) > 0) origin.Y += quad.Height; return origin; @@ -89,6 +94,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private Vector2 convertDragEventToScaleMultiplier(DragEvent e) { Vector2 scale = e.MousePosition - e.MouseDownPosition; + Logger.Log($"Raw scale {scale}"); adjustScaleFromAnchor(ref scale); return Vector2.Divide(scale, scaleHandler!.OriginalSurroundingQuad!.Value.Size) + Vector2.One; } @@ -96,12 +102,12 @@ namespace osu.Game.Screens.Edit.Compose.Components 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; + if ((originalAnchor & Anchor.x1) > 0) scale.X = 1; + if ((originalAnchor & 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; + if ((originalAnchor & Anchor.x0) > 0) scale.X = -scale.X; + if ((originalAnchor & Anchor.y0) > 0) scale.Y = -scale.Y; } private void applyScale(bool shouldKeepAspectRatio) @@ -110,6 +116,7 @@ namespace osu.Game.Screens.Edit.Compose.Components ? new Vector2(MathF.Max(rawScale.X, rawScale.Y)) : rawScale; + Logger.Log($"Raw scale adjusted {newScale}, origin {getOriginPosition()}"); scaleHandler!.Update(newScale, getOriginPosition()); } } From fcaa5ec20e3fe43948bb1bd9d898d45dcf9b50cf Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 20 Jan 2024 13:26:08 +0100 Subject: [PATCH 06/25] remove debug logs --- .../Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs index 6179be1d4f..e0b41fd8e2 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs @@ -5,7 +5,6 @@ using System; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Input.Events; -using osu.Framework.Logging; using osuTK; using osuTK.Input; @@ -94,7 +93,6 @@ namespace osu.Game.Screens.Edit.Compose.Components private Vector2 convertDragEventToScaleMultiplier(DragEvent e) { Vector2 scale = e.MousePosition - e.MouseDownPosition; - Logger.Log($"Raw scale {scale}"); adjustScaleFromAnchor(ref scale); return Vector2.Divide(scale, scaleHandler!.OriginalSurroundingQuad!.Value.Size) + Vector2.One; } @@ -116,7 +114,6 @@ namespace osu.Game.Screens.Edit.Compose.Components ? new Vector2(MathF.Max(rawScale.X, rawScale.Y)) : rawScale; - Logger.Log($"Raw scale adjusted {newScale}, origin {getOriginPosition()}"); scaleHandler!.Update(newScale, getOriginPosition()); } } From e1f3f7d988194e2f48df3aba184f095f59d2623b Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 20 Jan 2024 14:49:47 +0100 Subject: [PATCH 07/25] fix possible NaN in clamped scale --- .../Edit/OsuSelectionScaleHandler.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs index 7b0ae947e7..3c4818a533 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs @@ -177,10 +177,14 @@ namespace osu.Game.Rulesets.Osu.Edit 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); + 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 scale; } From 6a4129dad880e839b033d77ac2bdb00f22dc1c0d Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 20 Jan 2024 15:11:35 +0100 Subject: [PATCH 08/25] fix aspect ratio transform --- .../Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs index e0b41fd8e2..ea98ac573c 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs @@ -111,7 +111,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private void applyScale(bool shouldKeepAspectRatio) { var newScale = shouldKeepAspectRatio - ? new Vector2(MathF.Max(rawScale.X, rawScale.Y)) + ? new Vector2((rawScale.X + rawScale.Y) * 0.5f) : rawScale; scaleHandler!.Update(newScale, getOriginPosition()); From 0fc448f4f3a31643903017a9881b81749561e0eb Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 20 Jan 2024 15:12:48 +0100 Subject: [PATCH 09/25] fix adjusting scale from anchor --- osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs | 2 +- .../Edit/Compose/Components/SelectionBoxScaleHandle.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs index c2f788a9e8..bf75469d7a 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs @@ -91,7 +91,7 @@ namespace osu.Game.Overlays.SkinEditor Debug.Assert(originalWidths != null && originalHeights != null && originalScales != null && originalPositions != null && defaultOrigin != null && OriginalSurroundingQuad != null); var actualOrigin = origin ?? defaultOrigin.Value; - Axes adjustAxis = scale.X == 0 ? Axes.Y : scale.Y == 0 ? Axes.X : Axes.Both; + Axes adjustAxis = scale.X == 1 ? Axes.Y : scale.Y == 1 ? Axes.X : Axes.Both; if ((adjustAxis == Axes.Y && !allSelectedSupportManualSizing(Axes.Y)) || (adjustAxis == Axes.X && !allSelectedSupportManualSizing(Axes.X))) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs index ea98ac573c..60fbeb9fff 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs @@ -100,8 +100,8 @@ namespace osu.Game.Screens.Edit.Compose.Components 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 = 1; - if ((originalAnchor & Anchor.y1) > 0) scale.Y = 1; + 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; From 1596776a81b91db1850bf3325b6d2992ed5eaf6c Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 20 Jan 2024 15:15:49 +0100 Subject: [PATCH 10/25] fix imports --- osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs | 1 + .../Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs index 3c4818a533..1e3e22e34a 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs @@ -8,6 +8,7 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; 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; diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs index 60fbeb9fff..3dde97657f 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs @@ -1,7 +1,6 @@ // 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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Input.Events; From 9b9485f656807570afd91bd3b25923147a2075f2 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 20 Jan 2024 15:39:38 +0100 Subject: [PATCH 11/25] fix adjust axes detection --- .../Edit/OsuSelectionScaleHandler.cs | 3 +- .../SkinEditor/SkinSelectionScaleHandler.cs | 3 +- .../Components/SelectionBoxScaleHandle.cs | 47 +++++++++++++------ .../Components/SelectionScaleHandler.cs | 8 ++-- 4 files changed, 40 insertions(+), 21 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs index 1e3e22e34a..7d5240fb69 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs @@ -7,6 +7,7 @@ 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; @@ -82,7 +83,7 @@ namespace osu.Game.Rulesets.Osu.Edit obj => obj.Path.ControlPoints.Select(p => p.Type).ToArray()); } - public override void Update(Vector2 scale, Vector2? origin = null) + 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!"); diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs index bf75469d7a..0bd146a0a1 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs @@ -83,7 +83,7 @@ namespace osu.Game.Overlays.SkinEditor isFlippedY = false; } - public override void Update(Vector2 scale, Vector2? origin = null) + 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!"); @@ -91,7 +91,6 @@ namespace osu.Game.Overlays.SkinEditor Debug.Assert(originalWidths != null && originalHeights != null && originalScales != null && originalPositions != null && defaultOrigin != null && OriginalSurroundingQuad != null); var actualOrigin = origin ?? defaultOrigin.Value; - Axes adjustAxis = scale.X == 1 ? Axes.Y : scale.Y == 1 ? Axes.X : Axes.Both; if ((adjustAxis == Axes.Y && !allSelectedSupportManualSizing(Axes.Y)) || (adjustAxis == Axes.X && !allSelectedSupportManualSizing(Axes.X))) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs index 3dde97657f..d433e4e860 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs @@ -38,20 +38,6 @@ namespace osu.Game.Screens.Edit.Compose.Components return true; } - private Vector2 getOriginPosition() - { - var quad = scaleHandler!.OriginalSurroundingQuad!.Value; - Vector2 origin = quad.TopLeft; - - if ((originalAnchor & Anchor.x0) > 0) - origin.X += quad.Width; - - if ((originalAnchor & Anchor.y0) > 0) - origin.Y += quad.Height; - - return origin; - } - private Vector2 rawScale; protected override void OnDrag(DragEvent e) @@ -113,7 +99,38 @@ namespace osu.Game.Screens.Edit.Compose.Components ? new Vector2((rawScale.X + rawScale.Y) * 0.5f) : rawScale; - scaleHandler!.Update(newScale, getOriginPosition()); + scaleHandler!.Update(newScale, getOriginPosition(), getAdjustAxis()); + } + + private Vector2 getOriginPosition() + { + var quad = scaleHandler!.OriginalSurroundingQuad!.Value; + Vector2 origin = quad.TopLeft; + + if ((originalAnchor & Anchor.x0) > 0) + origin.X += quad.Width; + + if ((originalAnchor & Anchor.y0) > 0) + origin.Y += quad.Height; + + return origin; + } + + 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; + } } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs index 59406b3184..a96f627e56 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs @@ -46,10 +46,11 @@ namespace osu.Game.Screens.Edit.Compose.Components /// 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) + /// The axes to adjust the scale in. + public void ScaleSelection(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both) { Begin(); - Update(scale, origin); + Update(scale, origin, adjustAxis); Commit(); } @@ -83,7 +84,8 @@ namespace osu.Game.Screens.Edit.Compose.Components /// 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) + /// The axes to adjust the scale in. + public virtual void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both) { } From ac76af5cc8f894dfb87ae4d4987172b9f5a85934 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 20 Jan 2024 15:43:47 +0100 Subject: [PATCH 12/25] fix skin scale coordinate system --- osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs index 0bd146a0a1..e87952efa0 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs @@ -76,7 +76,7 @@ namespace osu.Game.Overlays.SkinEditor originalHeights = objectsInScale.ToDictionary(d => d, d => d.Height); originalScales = objectsInScale.ToDictionary(d => d, d => d.Scale); originalPositions = objectsInScale.ToDictionary(d => d, d => d.ToScreenSpace(d.OriginPosition)); - OriginalSurroundingQuad = GeometryUtils.GetSurroundingQuad(objectsInScale.SelectMany(d => d.ScreenSpaceDrawQuad.GetVertices().ToArray())); + OriginalSurroundingQuad = ToLocalSpace(GeometryUtils.GetSurroundingQuad(objectsInScale.SelectMany(d => d.ScreenSpaceDrawQuad.GetVertices().ToArray()))); defaultOrigin = OriginalSurroundingQuad.Value.Centre; isFlippedX = false; @@ -90,7 +90,7 @@ namespace osu.Game.Overlays.SkinEditor Debug.Assert(originalWidths != null && originalHeights != null && originalScales != null && originalPositions != null && defaultOrigin != null && OriginalSurroundingQuad != null); - var actualOrigin = origin ?? defaultOrigin.Value; + var actualOrigin = ToScreenSpace(origin ?? defaultOrigin.Value); if ((adjustAxis == Axes.Y && !allSelectedSupportManualSizing(Axes.Y)) || (adjustAxis == Axes.X && !allSelectedSupportManualSizing(Axes.X))) From 9459c66981a022905283b603f9bfb0d7e3cf6e77 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 20 Jan 2024 15:53:08 +0100 Subject: [PATCH 13/25] fix test --- osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs index 680a76f9b8..4c60ecf5db 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs @@ -130,7 +130,7 @@ namespace osu.Game.Tests.Visual.Editing OriginalSurroundingQuad = new Quad(targetContainer!.X, targetContainer.Y, targetContainer.Width, targetContainer.Height); } - public override void Update(Vector2 scale, Vector2? origin = null) + 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!"); From a155b315bf8ad9060ae214ccc8763e4deebdae6c Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 20 Jan 2024 16:10:17 +0100 Subject: [PATCH 14/25] Fix negative width or height skin drawables --- osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs index e87952efa0..8daf0043da 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs @@ -151,11 +151,11 @@ namespace osu.Game.Overlays.SkinEditor switch (adjustAxis) { case Axes.X: - b.Width = originalWidths[b] * currentScale.X; + b.Width = MathF.Abs(originalWidths[b] * currentScale.X); break; case Axes.Y: - b.Height = originalHeights[b] * currentScale.Y; + b.Height = MathF.Abs(originalHeights[b] * currentScale.Y); break; case Axes.Both: From 5f40d3aed9ca859535c75d8f9e927d5cc7ad1581 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 20 Jan 2024 16:29:26 +0100 Subject: [PATCH 15/25] rename variable --- .../Edit/Compose/Components/SelectionBoxScaleHandle.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs index d433e4e860..74629a5384 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs @@ -48,14 +48,14 @@ namespace osu.Game.Screens.Edit.Compose.Components rawScale = convertDragEventToScaleMultiplier(e); - applyScale(shouldKeepAspectRatio: e.ShiftPressed); + applyScale(shouldLockAspectRatio: e.ShiftPressed); } protected override bool OnKeyDown(KeyDownEvent e) { if (IsDragged && (e.Key == Key.ShiftLeft || e.Key == Key.ShiftRight)) { - applyScale(shouldKeepAspectRatio: true); + applyScale(shouldLockAspectRatio: true); return true; } @@ -67,7 +67,7 @@ namespace osu.Game.Screens.Edit.Compose.Components base.OnKeyUp(e); if (IsDragged && (e.Key == Key.ShiftLeft || e.Key == Key.ShiftRight)) - applyScale(shouldKeepAspectRatio: false); + applyScale(shouldLockAspectRatio: false); } protected override void OnDragEnd(DragEndEvent e) @@ -93,9 +93,9 @@ namespace osu.Game.Screens.Edit.Compose.Components if ((originalAnchor & Anchor.y0) > 0) scale.Y = -scale.Y; } - private void applyScale(bool shouldKeepAspectRatio) + private void applyScale(bool shouldLockAspectRatio) { - var newScale = shouldKeepAspectRatio + var newScale = shouldLockAspectRatio ? new Vector2((rawScale.X + rawScale.Y) * 0.5f) : rawScale; From 2f924b33686ff7d7cb3080c2cc16f891d27cbc2e Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 20 Jan 2024 16:33:03 +0100 Subject: [PATCH 16/25] fix skewed single axis scale --- .../SkinEditor/SkinSelectionScaleHandler.cs | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs index 8daf0043da..0c2ee6aae3 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs @@ -100,23 +100,15 @@ namespace osu.Game.Overlays.SkinEditor if (OriginalSurroundingQuad.Value.Width == 0 || OriginalSurroundingQuad.Value.Height == 0) return; - // for now aspect lock scale adjustments that occur at corners.. + // 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); } - // ..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 (objectsInScale.Any(b => !Precision.AlmostEquals(b.Rotation % 90, 0))) - { - if (adjustAxis == Axes.Y) - // if dragging from the horizontal centre, only a vertical component is available. - scale.X = scale.Y; - else - // in all other cases (arbitrarily) use the horizontal component for aspect lock. - scale.Y = scale.X; - } + // 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; From 78e87d379b760b9ebd5d567610423b607013b16d Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 20 Jan 2024 16:49:10 +0100 Subject: [PATCH 17/25] fix divide by zero --- .../Edit/Compose/Components/SelectionBoxScaleHandle.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs index 74629a5384..a1f6a1732a 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs @@ -4,6 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Input.Events; +using osu.Framework.Utils; using osuTK; using osuTK.Input; @@ -79,7 +80,12 @@ namespace osu.Game.Screens.Edit.Compose.Components { Vector2 scale = e.MousePosition - e.MouseDownPosition; adjustScaleFromAnchor(ref scale); - return Vector2.Divide(scale, scaleHandler!.OriginalSurroundingQuad!.Value.Size) + Vector2.One; + + 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) From c4ac6d20a09b5704dd484b633142f517b527e2c2 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 15 May 2024 23:40:51 +0200 Subject: [PATCH 18/25] fix code quality --- .../Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs index a1f6a1732a..c188d23a58 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs @@ -12,9 +12,6 @@ namespace osu.Game.Screens.Edit.Compose.Components { public partial class SelectionBoxScaleHandle : SelectionBoxDragHandle { - [Resolved] - private SelectionBox selectionBox { get; set; } = null!; - [Resolved] private SelectionScaleHandler? scaleHandler { get; set; } From 070668c96f4494958b0e1b4464dd4059e9ba0ec5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 23 May 2024 13:55:11 +0200 Subject: [PATCH 19/25] Use `ShiftPressed` instead of explicitly checking both physical keys Not only is this simpler, but it also is more correct (for explanation why, try holding both shift keys while dragging, and just releasing one of them - the previous code would briefly turn aspect ratio off). --- .../Edit/Compose/Components/SelectionBoxScaleHandle.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs index c188d23a58..12787a1c55 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs @@ -51,9 +51,9 @@ namespace osu.Game.Screens.Edit.Compose.Components protected override bool OnKeyDown(KeyDownEvent e) { - if (IsDragged && (e.Key == Key.ShiftLeft || e.Key == Key.ShiftRight)) + if (IsDragged) { - applyScale(shouldLockAspectRatio: true); + applyScale(shouldLockAspectRatio: e.ShiftPressed); return true; } @@ -64,8 +64,8 @@ namespace osu.Game.Screens.Edit.Compose.Components { base.OnKeyUp(e); - if (IsDragged && (e.Key == Key.ShiftLeft || e.Key == Key.ShiftRight)) - applyScale(shouldLockAspectRatio: false); + if (IsDragged) + applyScale(shouldLockAspectRatio: e.ShiftPressed); } protected override void OnDragEnd(DragEndEvent e) From 9e86a08405db8a88fec2066975843b13ae831eed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 23 May 2024 14:07:43 +0200 Subject: [PATCH 20/25] Simplify scale origin computation --- .../Components/SelectionBoxScaleHandle.cs | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs index 12787a1c55..352a4985d6 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Input.Events; using osu.Framework.Utils; @@ -102,21 +103,8 @@ namespace osu.Game.Screens.Edit.Compose.Components ? new Vector2((rawScale.X + rawScale.Y) * 0.5f) : rawScale; - scaleHandler!.Update(newScale, getOriginPosition(), getAdjustAxis()); - } - - private Vector2 getOriginPosition() - { - var quad = scaleHandler!.OriginalSurroundingQuad!.Value; - Vector2 origin = quad.TopLeft; - - if ((originalAnchor & Anchor.x0) > 0) - origin.X += quad.Width; - - if ((originalAnchor & Anchor.y0) > 0) - origin.Y += quad.Height; - - return origin; + var scaleOrigin = originalAnchor.Opposite().PositionOnQuad(scaleHandler!.OriginalSurroundingQuad!.Value); + scaleHandler!.Update(newScale, scaleOrigin, getAdjustAxis()); } private Axes getAdjustAxis() From ac5c031a3a077cc5072b7f6b331623aa91681d58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 23 May 2024 14:18:29 +0200 Subject: [PATCH 21/25] Simplify original state management in skin selection scale handler --- .../SkinEditor/SkinSelectionScaleHandler.cs | 47 ++++++++++--------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs index 0c2ee6aae3..08df8df7e2 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs @@ -53,13 +53,8 @@ namespace osu.Game.Overlays.SkinEditor private bool allSelectedSupportManualSizing(Axes axis) => selectedItems.All(b => (b as CompositeDrawable)?.AutoSizeAxes.HasFlagFast(axis) == false); - private Drawable[]? objectsInScale; - + private Dictionary? objectsInScale; private Vector2? defaultOrigin; - private Dictionary? originalWidths; - private Dictionary? originalHeights; - private Dictionary? originalScales; - private Dictionary? originalPositions; private bool isFlippedX; private bool isFlippedY; @@ -71,12 +66,8 @@ namespace osu.Game.Overlays.SkinEditor changeHandler?.BeginChange(); - objectsInScale = selectedItems.Cast().ToArray(); - originalWidths = objectsInScale.ToDictionary(d => d, d => d.Width); - originalHeights = objectsInScale.ToDictionary(d => d, d => d.Height); - originalScales = objectsInScale.ToDictionary(d => d, d => d.Scale); - originalPositions = objectsInScale.ToDictionary(d => d, d => d.ToScreenSpace(d.OriginPosition)); - OriginalSurroundingQuad = ToLocalSpace(GeometryUtils.GetSurroundingQuad(objectsInScale.SelectMany(d => d.ScreenSpaceDrawQuad.GetVertices().ToArray()))); + objectsInScale = selectedItems.Cast().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; @@ -88,7 +79,7 @@ namespace osu.Game.Overlays.SkinEditor if (objectsInScale == null) throw new InvalidOperationException($"Cannot {nameof(Update)} a scale operation without calling {nameof(Begin)} first!"); - Debug.Assert(originalWidths != null && originalHeights != null && originalScales != null && originalPositions != null && defaultOrigin != null && OriginalSurroundingQuad != null); + Debug.Assert(defaultOrigin != null && OriginalSurroundingQuad != null); var actualOrigin = ToScreenSpace(origin ?? defaultOrigin.Value); @@ -132,9 +123,9 @@ namespace osu.Game.Overlays.SkinEditor return; } - foreach (var b in objectsInScale) + foreach (var (b, originalState) in objectsInScale) { - UpdatePosition(b, GeometryUtils.GetScaledPosition(scale, actualOrigin, originalPositions[b])); + UpdatePosition(b, GeometryUtils.GetScaledPosition(scale, actualOrigin, originalState.ScreenSpaceOriginPosition)); var currentScale = scale; if (Precision.AlmostEquals(MathF.Abs(b.Rotation) % 180, 90)) @@ -143,15 +134,15 @@ namespace osu.Game.Overlays.SkinEditor switch (adjustAxis) { case Axes.X: - b.Width = MathF.Abs(originalWidths[b] * currentScale.X); + b.Width = MathF.Abs(originalState.Width * currentScale.X); break; case Axes.Y: - b.Height = MathF.Abs(originalHeights[b] * currentScale.Y); + b.Height = MathF.Abs(originalState.Height * currentScale.Y); break; case Axes.Both: - b.Scale = originalScales[b] * currentScale; + b.Scale = originalState.Scale * currentScale; break; } } @@ -165,11 +156,23 @@ namespace osu.Game.Overlays.SkinEditor changeHandler?.EndChange(); objectsInScale = null; - originalPositions = null; - originalWidths = null; - originalHeights = null; - originalScales = 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); + } + } } } From f7bcccacb03358667fa40d0603bb01d3233bf591 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 23 May 2024 14:41:59 +0200 Subject: [PATCH 22/25] Simplify original state management in osu! scale handler --- .../Edit/OsuSelectionScaleHandler.cs | 54 ++++++++++--------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs index 7d5240fb69..b0299c5668 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs @@ -55,12 +55,8 @@ namespace osu.Game.Rulesets.Osu.Edit CanScaleDiagonally.Value = CanScaleX.Value && CanScaleY.Value; } - private OsuHitObject[]? objectsInScale; - + private Dictionary? objectsInScale; private Vector2? defaultOrigin; - private Dictionary? originalPositions; - private Dictionary? originalPathControlPointPositions; - private Dictionary? originalPathControlPointTypes; public override void Begin() { @@ -69,18 +65,11 @@ namespace osu.Game.Rulesets.Osu.Edit changeHandler?.BeginChange(); - objectsInScale = selectedMovableObjects.ToArray(); - OriginalSurroundingQuad = objectsInScale.Length == 1 && objectsInScale.First() is Slider slider + 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); + : GeometryUtils.GetSurroundingQuad(objectsInScale.Keys); 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, Axes adjustAxis = Axes.Both) @@ -88,22 +77,26 @@ namespace osu.Game.Rulesets.Osu.Edit 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); + 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.Length == 1 && objectsInScale.First() is Slider slider) - scaleSlider(slider, scale, originalPathControlPointPositions[slider], originalPathControlPointTypes[slider]); + 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 in objectsInScale) + foreach (var (ho, originalState) in objectsInScale) { - ho.Position = GeometryUtils.GetScaledPosition(scale, actualOrigin, originalPositions[ho]); + ho.Position = GeometryUtils.GetScaledPosition(scale, actualOrigin, originalState.Position); } } @@ -119,9 +112,6 @@ namespace osu.Game.Rulesets.Osu.Edit objectsInScale = null; OriginalSurroundingQuad = null; - originalPositions = null; - originalPathControlPointPositions = null; - originalPathControlPointTypes = null; defaultOrigin = null; } @@ -193,7 +183,7 @@ namespace osu.Game.Rulesets.Osu.Edit private void moveSelectionInBounds() { - Quad quad = GeometryUtils.GetSurroundingQuad(objectsInScale!); + Quad quad = GeometryUtils.GetSurroundingQuad(objectsInScale!.Keys); Vector2 delta = Vector2.Zero; @@ -207,8 +197,22 @@ namespace osu.Game.Rulesets.Osu.Edit if (quad.BottomRight.Y > OsuPlayfield.BASE_SIZE.Y) delta.Y -= quad.BottomRight.Y - OsuPlayfield.BASE_SIZE.Y; - foreach (var h in objectsInScale!) + 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(); + } + } } } From 3e34b2d37ed895f9eb707be9921964795c2e750e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 23 May 2024 14:56:08 +0200 Subject: [PATCH 23/25] Bring back clamping in osu! scale handler Being able to flip doesn't really feel all that good and `master` was already clamping, so let's just bring that back for now. Flipping can be reconsidered in a follow-up if it actually can be made to behave well. --- osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs index b0299c5668..75b404684f 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs @@ -9,6 +9,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Primitives; +using osu.Framework.Logging; using osu.Framework.Utils; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; @@ -120,6 +121,8 @@ namespace osu.Game.Rulesets.Osu.Edit 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++) { @@ -178,7 +181,8 @@ namespace osu.Game.Rulesets.Osu.Edit 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 scale; + Logger.Log($"scale = {scale}"); + return Vector2.ComponentMax(scale, new Vector2(Precision.FLOAT_EPSILON)); } private void moveSelectionInBounds() From 128029e2af5bd0d23db7935761807487ded1eb15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 23 May 2024 15:08:43 +0200 Subject: [PATCH 24/25] Fix aspect ratio lock applying when shift pressed on a non-corner anchor It doesn't make sense and it wasn't doing the right thing. --- .../Edit/Compose/Components/SelectionBoxScaleHandle.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs index 352a4985d6..eca0c08ba1 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs @@ -3,6 +3,7 @@ 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; @@ -47,14 +48,14 @@ namespace osu.Game.Screens.Edit.Compose.Components rawScale = convertDragEventToScaleMultiplier(e); - applyScale(shouldLockAspectRatio: e.ShiftPressed); + applyScale(shouldLockAspectRatio: isCornerAnchor(originalAnchor) && e.ShiftPressed); } protected override bool OnKeyDown(KeyDownEvent e) { if (IsDragged) { - applyScale(shouldLockAspectRatio: e.ShiftPressed); + applyScale(shouldLockAspectRatio: isCornerAnchor(originalAnchor) && e.ShiftPressed); return true; } @@ -66,7 +67,7 @@ namespace osu.Game.Screens.Edit.Compose.Components base.OnKeyUp(e); if (IsDragged) - applyScale(shouldLockAspectRatio: e.ShiftPressed); + applyScale(shouldLockAspectRatio: isCornerAnchor(originalAnchor) && e.ShiftPressed); } protected override void OnDragEnd(DragEndEvent e) @@ -123,5 +124,7 @@ namespace osu.Game.Screens.Edit.Compose.Components return Axes.Both; } } + + private bool isCornerAnchor(Anchor anchor) => !anchor.HasFlagFast(Anchor.x1) && !anchor.HasFlagFast(Anchor.y1); } } From d8ba95f87712a7e407a8e28c9c41cc8035b9826a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 23 May 2024 15:13:42 +0200 Subject: [PATCH 25/25] Remove leftover log whooops. --- osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs index 75b404684f..af03c4d925 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs @@ -9,7 +9,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Primitives; -using osu.Framework.Logging; using osu.Framework.Utils; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; @@ -181,7 +180,6 @@ namespace osu.Game.Rulesets.Osu.Edit 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); - Logger.Log($"scale = {scale}"); return Vector2.ComponentMax(scale, new Vector2(Precision.FLOAT_EPSILON)); }