// 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 Dictionary? objectsInScale; private Vector2? defaultOrigin; private bool isFlippedX; private bool isFlippedY; 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().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; isFlippedY = false; } 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!"); Debug.Assert(defaultOrigin != null && OriginalSurroundingQuad != null); var actualOrigin = ToScreenSpace(origin ?? defaultOrigin.Value); if ((adjustAxis == Axes.Y && !allSelectedSupportManualSizing(Axes.Y)) || (adjustAxis == Axes.X && !allSelectedSupportManualSizing(Axes.X))) return; // If the selection has no area we cannot scale it if (OriginalSurroundingQuad.Value.Width == 0 || OriginalSurroundingQuad.Value.Height == 0) return; // 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); } // 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; Axes toFlip = Axes.None; if (flippedX != isFlippedX) { isFlippedX = flippedX; toFlip |= Axes.X; } if (flippedY != isFlippedY) { isFlippedY = flippedY; toFlip |= Axes.Y; } if (toFlip != Axes.None) { PerformFlipFromScaleHandles?.Invoke(toFlip); return; } foreach (var (b, originalState) in objectsInScale) { UpdatePosition(b, GeometryUtils.GetScaledPosition(scale, actualOrigin, originalState.ScreenSpaceOriginPosition)); var currentScale = scale; if (Precision.AlmostEquals(MathF.Abs(b.Rotation) % 180, 90)) currentScale = new Vector2(scale.Y, scale.X); switch (adjustAxis) { case Axes.X: b.Width = MathF.Abs(originalState.Width * currentScale.X); break; case Axes.Y: b.Height = MathF.Abs(originalState.Height * currentScale.Y); break; case Axes.Both: b.Scale = originalState.Scale * currentScale; 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; 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); } } } }