From a0f67ef3bc1bab4ae0d07115d6df97ef23bb88d8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 18 May 2021 18:57:52 +0900 Subject: [PATCH 1/6] Move scaling logic out of `OsuSelectionHandler` for reuse --- .../Edit/OsuSelectionHandler.cs | 18 +---------------- .../Compose/Components/SelectionHandler.cs | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index aaf3517c9c..8cb86bc108 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -225,26 +225,10 @@ namespace osu.Game.Rulesets.Osu.Edit private void scaleHitObjects(OsuHitObject[] hitObjects, Anchor reference, Vector2 scale) { scale = getClampedScale(hitObjects, reference, scale); - - // move the selection before scaling if dragging from top or left anchors. - float xOffset = ((reference & Anchor.x0) > 0) ? -scale.X : 0; - float yOffset = ((reference & Anchor.y0) > 0) ? -scale.Y : 0; - Quad selectionQuad = getSurroundingQuad(hitObjects); foreach (var h in hitObjects) - { - var newPosition = h.Position; - - // guard against no-ops and NaN. - if (scale.X != 0 && selectionQuad.Width > 0) - newPosition.X = selectionQuad.TopLeft.X + xOffset + (h.X - selectionQuad.TopLeft.X) / selectionQuad.Width * (selectionQuad.Width + scale.X); - - if (scale.Y != 0 && selectionQuad.Height > 0) - newPosition.Y = selectionQuad.TopLeft.Y + yOffset + (h.Y - selectionQuad.TopLeft.Y) / selectionQuad.Height * (selectionQuad.Height + scale.Y); - - h.Position = newPosition; - } + h.Position = GetScaledPosition(reference, scale, selectionQuad, h.Position); } private (bool X, bool Y) isQuadInBounds(Quad quad) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index bfd5ab7afa..26328b4dc7 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -375,6 +375,26 @@ namespace osu.Game.Screens.Edit.Compose.Components return position; } + /// + /// Given a scale vector, a surrounding quad for all selected objects, and a position, + /// will return the scaled position in screen space coordinates. + /// + protected static Vector2 GetScaledPosition(Anchor reference, Vector2 scale, Quad selectionQuad, Vector2 position) + { + // adjust the direction of scale depending on which side the user is dragging. + float xOffset = ((reference & Anchor.x0) > 0) ? -scale.X : 0; + float yOffset = ((reference & Anchor.y0) > 0) ? -scale.Y : 0; + + // guard against no-ops and NaN. + if (scale.X != 0 && selectionQuad.Width > 0) + position.X = selectionQuad.TopLeft.X + xOffset + (position.X - selectionQuad.TopLeft.X) / selectionQuad.Width * (selectionQuad.Width + scale.X); + + if (scale.Y != 0 && selectionQuad.Height > 0) + position.Y = selectionQuad.TopLeft.Y + yOffset + (position.Y - selectionQuad.TopLeft.Y) / selectionQuad.Height * (selectionQuad.Height + scale.Y); + + return position; + } + /// /// Returns a quad surrounding the provided points. /// From 6a3c58b9adfc6184f23ae0eefa6c39c38bbfdc47 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 May 2021 19:58:55 +0900 Subject: [PATCH 2/6] Implement proper scaling algorithms --- .../Skinning/Editor/SkinSelectionHandler.cs | 98 +++++++++++++++++-- 1 file changed, 88 insertions(+), 10 deletions(-) diff --git a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs index 2eb4ea107d..18c9341db9 100644 --- a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs +++ b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs @@ -34,9 +34,94 @@ namespace osu.Game.Skinning.Editor { adjustScaleFromAnchor(ref scale, anchor); - foreach (var c in SelectedBlueprints) - // TODO: this is temporary and will be fixed with a separate refactor of selection transform logic. - ((Drawable)c.Item).Scale += scale * 0.02f; + if (SelectedBlueprints.Count > 1) + { + var selectionQuad = GetSurroundingQuad(SelectedBlueprints.SelectMany(b => + b.Item.ScreenSpaceDrawQuad.GetVertices().ToArray())); + + // the selection quad is always upright, so use a rect to make mutating the values easier. + var adjustedRect = selectionQuad.AABBFloat; + + // for now aspect lock scale adjustments that occur at corners. + if (!anchor.HasFlagFast(Anchor.x1) && !anchor.HasFlagFast(Anchor.y1)) + scale.Y = scale.X / selectionQuad.Width * selectionQuad.Height; + + if (anchor.HasFlagFast(Anchor.x0)) + { + adjustedRect.X -= scale.X; + adjustedRect.Width += scale.X; + } + else if (anchor.HasFlagFast(Anchor.x2)) + { + adjustedRect.Width += scale.X; + } + + if (anchor.HasFlagFast(Anchor.y0)) + { + adjustedRect.Y -= scale.Y; + adjustedRect.Height += scale.Y; + } + else if (anchor.HasFlagFast(Anchor.y2)) + { + adjustedRect.Height += scale.Y; + } + + // scale adjust should match that of the quad itself. + var scaledDelta = new Vector2( + adjustedRect.Width / selectionQuad.Width - 1, + adjustedRect.Height / selectionQuad.Height - 1 + ); + + foreach (var b in SelectedBlueprints) + { + var drawableItem = (Drawable)b.Item; + + if (SelectedBlueprints.Count > 1) + { + // each drawable's relative position should be maintained in the scaled quad. + var screenPosition = b.ScreenSpaceSelectionPoint; + + var relativePositionInOriginal = + new Vector2( + (screenPosition.X - selectionQuad.TopLeft.X) / selectionQuad.Width, + (screenPosition.Y - selectionQuad.TopLeft.Y) / selectionQuad.Height + ); + + var newPositionInAdjusted = new Vector2( + adjustedRect.TopLeft.X + adjustedRect.Width * relativePositionInOriginal.X, + adjustedRect.TopLeft.Y + adjustedRect.Height * relativePositionInOriginal.Y + ); + + drawableItem.Position = drawableItem.Parent.ToLocalSpace(newPositionInAdjusted) - drawableItem.AnchorPosition; + drawableItem.Scale += scaledDelta; + } + } + } + else + { + var blueprint = SelectedBlueprints.First(); + var drawableItem = (Drawable)blueprint.Item; + + // the number of local "pixels" the drag operation resulted in. + // our goal is to increase the drawable's draw size by this amount. + var scaledDelta = drawableItem.ScreenSpaceDeltaToParentSpace(scale); + + scaledDelta = new Vector2( + scaledDelta.X / drawableItem.DrawWidth, + scaledDelta.Y / drawableItem.DrawHeight + ); + + // handle the case where scaling with a centre origin needs double the adjustments to match + // user cursor movement. + if (drawableItem.Origin.HasFlagFast(Anchor.x1)) scaledDelta.X *= 2; + if (drawableItem.Origin.HasFlagFast(Anchor.y1)) scaledDelta.Y *= 2; + + // for now aspect lock scale adjustments that occur at corners. + if (!anchor.HasFlagFast(Anchor.x1) && !anchor.HasFlagFast(Anchor.y1)) + scaledDelta.Y = scaledDelta.X; + + drawableItem.Scale += scaledDelta; + } return true; } @@ -158,13 +243,6 @@ namespace osu.Game.Skinning.Editor // 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; - - // for now aspect lock scale adjustments that occur at corners. - if (!reference.HasFlagFast(Anchor.x1) && !reference.HasFlagFast(Anchor.y1)) - { - // TODO: temporary implementation - only dragging the corner handles across the X axis changes size. - scale.Y = scale.X; - } } public class AnchorMenuItem : TernaryStateMenuItem From 14af86d6c55ee8ccae7ae0b531b537eb0d9cbbb2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 May 2021 21:46:41 +0900 Subject: [PATCH 3/6] Use the same code path for all scaling --- .../Skinning/Editor/SkinSelectionHandler.cs | 127 +++++++----------- 1 file changed, 48 insertions(+), 79 deletions(-) diff --git a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs index 18c9341db9..af013bfd52 100644 --- a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs +++ b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs @@ -34,92 +34,61 @@ namespace osu.Game.Skinning.Editor { adjustScaleFromAnchor(ref scale, anchor); - if (SelectedBlueprints.Count > 1) + var selectionQuad = GetSurroundingQuad(SelectedBlueprints.SelectMany(b => + b.Item.ScreenSpaceDrawQuad.GetVertices().ToArray())); + + // the selection quad is always upright, so use a rect to make mutating the values easier. + var adjustedRect = selectionQuad.AABBFloat; + + // for now aspect lock scale adjustments that occur at corners. + if (!anchor.HasFlagFast(Anchor.x1) && !anchor.HasFlagFast(Anchor.y1)) + scale.Y = scale.X / selectionQuad.Width * selectionQuad.Height; + + if (anchor.HasFlagFast(Anchor.x0)) { - var selectionQuad = GetSurroundingQuad(SelectedBlueprints.SelectMany(b => - b.Item.ScreenSpaceDrawQuad.GetVertices().ToArray())); - - // the selection quad is always upright, so use a rect to make mutating the values easier. - var adjustedRect = selectionQuad.AABBFloat; - - // for now aspect lock scale adjustments that occur at corners. - if (!anchor.HasFlagFast(Anchor.x1) && !anchor.HasFlagFast(Anchor.y1)) - scale.Y = scale.X / selectionQuad.Width * selectionQuad.Height; - - if (anchor.HasFlagFast(Anchor.x0)) - { - adjustedRect.X -= scale.X; - adjustedRect.Width += scale.X; - } - else if (anchor.HasFlagFast(Anchor.x2)) - { - adjustedRect.Width += scale.X; - } - - if (anchor.HasFlagFast(Anchor.y0)) - { - adjustedRect.Y -= scale.Y; - adjustedRect.Height += scale.Y; - } - else if (anchor.HasFlagFast(Anchor.y2)) - { - adjustedRect.Height += scale.Y; - } - - // scale adjust should match that of the quad itself. - var scaledDelta = new Vector2( - adjustedRect.Width / selectionQuad.Width - 1, - adjustedRect.Height / selectionQuad.Height - 1 - ); - - foreach (var b in SelectedBlueprints) - { - var drawableItem = (Drawable)b.Item; - - if (SelectedBlueprints.Count > 1) - { - // each drawable's relative position should be maintained in the scaled quad. - var screenPosition = b.ScreenSpaceSelectionPoint; - - var relativePositionInOriginal = - new Vector2( - (screenPosition.X - selectionQuad.TopLeft.X) / selectionQuad.Width, - (screenPosition.Y - selectionQuad.TopLeft.Y) / selectionQuad.Height - ); - - var newPositionInAdjusted = new Vector2( - adjustedRect.TopLeft.X + adjustedRect.Width * relativePositionInOriginal.X, - adjustedRect.TopLeft.Y + adjustedRect.Height * relativePositionInOriginal.Y - ); - - drawableItem.Position = drawableItem.Parent.ToLocalSpace(newPositionInAdjusted) - drawableItem.AnchorPosition; - drawableItem.Scale += scaledDelta; - } - } + adjustedRect.X -= scale.X; + adjustedRect.Width += scale.X; } - else + else if (anchor.HasFlagFast(Anchor.x2)) { - var blueprint = SelectedBlueprints.First(); - var drawableItem = (Drawable)blueprint.Item; + adjustedRect.Width += scale.X; + } - // the number of local "pixels" the drag operation resulted in. - // our goal is to increase the drawable's draw size by this amount. - var scaledDelta = drawableItem.ScreenSpaceDeltaToParentSpace(scale); + if (anchor.HasFlagFast(Anchor.y0)) + { + adjustedRect.Y -= scale.Y; + adjustedRect.Height += scale.Y; + } + else if (anchor.HasFlagFast(Anchor.y2)) + { + adjustedRect.Height += scale.Y; + } - scaledDelta = new Vector2( - scaledDelta.X / drawableItem.DrawWidth, - scaledDelta.Y / drawableItem.DrawHeight + // scale adjust should match that of the quad itself. + var scaledDelta = new Vector2( + adjustedRect.Width / selectionQuad.Width - 1, + adjustedRect.Height / selectionQuad.Height - 1 + ); + + 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 - selectionQuad.TopLeft.X) / selectionQuad.Width, + (screenPosition.Y - selectionQuad.TopLeft.Y) / selectionQuad.Height + ); + + var newPositionInAdjusted = new Vector2( + adjustedRect.TopLeft.X + adjustedRect.Width * relativePositionInOriginal.X, + adjustedRect.TopLeft.Y + adjustedRect.Height * relativePositionInOriginal.Y ); - // handle the case where scaling with a centre origin needs double the adjustments to match - // user cursor movement. - if (drawableItem.Origin.HasFlagFast(Anchor.x1)) scaledDelta.X *= 2; - if (drawableItem.Origin.HasFlagFast(Anchor.y1)) scaledDelta.Y *= 2; - - // for now aspect lock scale adjustments that occur at corners. - if (!anchor.HasFlagFast(Anchor.x1) && !anchor.HasFlagFast(Anchor.y1)) - scaledDelta.Y = scaledDelta.X; - + drawableItem.Position = drawableItem.Parent.ToLocalSpace(newPositionInAdjusted) - drawableItem.AnchorPosition; drawableItem.Scale += scaledDelta; } From a55879e5115895c940411bc9d7000532e32d5219 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 20 May 2021 01:47:31 +0900 Subject: [PATCH 4/6] Fix oversights in scale algorithm --- osu.Game/Skinning/Editor/SkinSelectionHandler.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs index af013bfd52..2f9611ba65 100644 --- a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs +++ b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs @@ -32,6 +32,9 @@ namespace osu.Game.Skinning.Editor public override bool HandleScale(Vector2 scale, Anchor anchor) { + // convert scale to screen space + scale = ToScreenSpace(scale) - ToScreenSpace(Vector2.Zero); + adjustScaleFromAnchor(ref scale, anchor); var selectionQuad = GetSurroundingQuad(SelectedBlueprints.SelectMany(b => @@ -66,8 +69,8 @@ namespace osu.Game.Skinning.Editor // scale adjust should match that of the quad itself. var scaledDelta = new Vector2( - adjustedRect.Width / selectionQuad.Width - 1, - adjustedRect.Height / selectionQuad.Height - 1 + adjustedRect.Width / selectionQuad.Width, + adjustedRect.Height / selectionQuad.Height ); foreach (var b in SelectedBlueprints) @@ -89,7 +92,7 @@ namespace osu.Game.Skinning.Editor ); drawableItem.Position = drawableItem.Parent.ToLocalSpace(newPositionInAdjusted) - drawableItem.AnchorPosition; - drawableItem.Scale += scaledDelta; + drawableItem.Scale *= scaledDelta; } return true; From f9d51656b6dff672eab55e46eb1bad287d70b84e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 21 May 2021 15:02:36 +0900 Subject: [PATCH 5/6] Fix scaling of rotated items not behaving in an understandable way --- .../Skinning/Editor/SkinSelectionHandler.cs | 61 ++++++++++--------- 1 file changed, 33 insertions(+), 28 deletions(-) diff --git a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs index 2f9611ba65..accb65483b 100644 --- a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs +++ b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs @@ -8,6 +8,7 @@ using osu.Framework.Allocation; using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Utils; using osu.Game.Extensions; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; @@ -37,40 +38,44 @@ namespace osu.Game.Skinning.Editor adjustScaleFromAnchor(ref scale, anchor); - var selectionQuad = GetSurroundingQuad(SelectedBlueprints.SelectMany(b => - b.Item.ScreenSpaceDrawQuad.GetVertices().ToArray())); + // the selection quad is always upright, so use an AABB rect to make mutating the values easier. + var selectionRect = GetSurroundingQuad(SelectedBlueprints.SelectMany(b => + b.Item.ScreenSpaceDrawQuad.GetVertices().ToArray())).AABBFloat; - // the selection quad is always upright, so use a rect to make mutating the values easier. - var adjustedRect = selectionQuad.AABBFloat; + // 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 (!anchor.HasFlagFast(Anchor.x1) && !anchor.HasFlagFast(Anchor.y1)) - scale.Y = scale.X / selectionQuad.Width * selectionQuad.Height; + // first, remove any scale axis we are not interested in. + if (anchor.HasFlagFast(Anchor.x1)) scale.X = 0; + if (anchor.HasFlagFast(Anchor.y1)) scale.Y = 0; - if (anchor.HasFlagFast(Anchor.x0)) + bool shouldAspectLock = + // for now aspect lock scale adjustments that occur at corners.. + (!anchor.HasFlagFast(Anchor.x1) && !anchor.HasFlagFast(Anchor.y1)) + // ..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). + || SelectedBlueprints.Any(b => !Precision.AlmostEquals(((Drawable)b.Item).Rotation % 90, 0)); + + if (shouldAspectLock) { - adjustedRect.X -= scale.X; - adjustedRect.Width += scale.X; - } - else if (anchor.HasFlagFast(Anchor.x2)) - { - adjustedRect.Width += scale.X; + 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.y0)) - { - adjustedRect.Y -= scale.Y; - adjustedRect.Height += scale.Y; - } - else if (anchor.HasFlagFast(Anchor.y2)) - { - adjustedRect.Height += scale.Y; - } + if (anchor.HasFlagFast(Anchor.x0)) adjustedRect.X -= scale.X; + if (anchor.HasFlagFast(Anchor.y0)) adjustedRect.Y -= scale.Y; - // scale adjust should match that of the quad itself. + adjustedRect.Width += scale.X; + adjustedRect.Height += scale.Y; + + // scale adjust applied to each individual item should match that of the quad itself. var scaledDelta = new Vector2( - adjustedRect.Width / selectionQuad.Width, - adjustedRect.Height / selectionQuad.Height + adjustedRect.Width / selectionRect.Width, + adjustedRect.Height / selectionRect.Height ); foreach (var b in SelectedBlueprints) @@ -82,8 +87,8 @@ namespace osu.Game.Skinning.Editor var relativePositionInOriginal = new Vector2( - (screenPosition.X - selectionQuad.TopLeft.X) / selectionQuad.Width, - (screenPosition.Y - selectionQuad.TopLeft.Y) / selectionQuad.Height + (screenPosition.X - selectionRect.TopLeft.X) / selectionRect.Width, + (screenPosition.Y - selectionRect.TopLeft.Y) / selectionRect.Height ); var newPositionInAdjusted = new Vector2( From 0d575f572866e84421829b7002ef0b695ad4e59f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 21 May 2021 15:06:53 +0900 Subject: [PATCH 6/6] Remove incorrect (and unintended) modulus logic --- osu.Game/Skinning/Editor/SkinSelectionHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs index accb65483b..0b6d9222ce 100644 --- a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs +++ b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs @@ -54,7 +54,7 @@ namespace osu.Game.Skinning.Editor (!anchor.HasFlagFast(Anchor.x1) && !anchor.HasFlagFast(Anchor.y1)) // ..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). - || SelectedBlueprints.Any(b => !Precision.AlmostEquals(((Drawable)b.Item).Rotation % 90, 0)); + || SelectedBlueprints.Any(b => !Precision.AlmostEquals(((Drawable)b.Item).Rotation, 0)); if (shouldAspectLock) {