2024-01-20 09:49:56 +08:00
|
|
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
|
|
// See the LICENCE file in the repository root for full licence text.
|
|
|
|
|
|
|
|
using System;
|
|
|
|
using System.Collections.Generic;
|
|
|
|
using System.Diagnostics;
|
|
|
|
using System.Linq;
|
|
|
|
using osu.Framework.Allocation;
|
|
|
|
using osu.Framework.Bindables;
|
|
|
|
using osu.Framework.Graphics;
|
|
|
|
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<Drawable, Vector2> UpdatePosition { get; init; } = null!;
|
|
|
|
|
|
|
|
public event Action<Axes>? PerformFlipFromScaleHandles;
|
|
|
|
|
|
|
|
[Resolved]
|
|
|
|
private IEditorChangeHandler? changeHandler { get; set; }
|
|
|
|
|
|
|
|
private BindableList<ISerialisableDrawable> selectedItems { get; } = new BindableList<ISerialisableDrawable>();
|
|
|
|
|
|
|
|
[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;
|
|
|
|
}
|
|
|
|
|
2024-07-02 23:19:04 +08:00
|
|
|
private bool allSelectedSupportManualSizing(Axes axis) => selectedItems.All(b => (b as CompositeDrawable)?.AutoSizeAxes.HasFlag(axis) == false);
|
2024-01-20 09:49:56 +08:00
|
|
|
|
2024-05-23 20:18:29 +08:00
|
|
|
private Dictionary<Drawable, OriginalDrawableState>? objectsInScale;
|
2024-01-20 09:49:56 +08:00
|
|
|
private Vector2? defaultOrigin;
|
|
|
|
|
2024-01-20 20:04:05 +08:00
|
|
|
private bool isFlippedX;
|
|
|
|
private bool isFlippedY;
|
|
|
|
|
2024-01-20 09:49:56 +08:00
|
|
|
public override void Begin()
|
|
|
|
{
|
|
|
|
if (objectsInScale != null)
|
|
|
|
throw new InvalidOperationException($"Cannot {nameof(Begin)} a scale operation while another is in progress!");
|
|
|
|
|
|
|
|
changeHandler?.BeginChange();
|
|
|
|
|
2024-05-23 20:18:29 +08:00
|
|
|
objectsInScale = selectedItems.Cast<Drawable>().ToDictionary(d => d, d => new OriginalDrawableState(d));
|
|
|
|
OriginalSurroundingQuad = ToLocalSpace(GeometryUtils.GetSurroundingQuad(objectsInScale.SelectMany(d => d.Key.ScreenSpaceDrawQuad.GetVertices().ToArray())));
|
2024-01-20 09:49:56 +08:00
|
|
|
defaultOrigin = OriginalSurroundingQuad.Value.Centre;
|
2024-01-20 20:04:05 +08:00
|
|
|
|
|
|
|
isFlippedX = false;
|
|
|
|
isFlippedY = false;
|
2024-01-20 09:49:56 +08:00
|
|
|
}
|
|
|
|
|
2024-01-20 22:39:38 +08:00
|
|
|
public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both)
|
2024-01-20 09:49:56 +08:00
|
|
|
{
|
|
|
|
if (objectsInScale == null)
|
|
|
|
throw new InvalidOperationException($"Cannot {nameof(Update)} a scale operation without calling {nameof(Begin)} first!");
|
|
|
|
|
2024-05-23 20:18:29 +08:00
|
|
|
Debug.Assert(defaultOrigin != null && OriginalSurroundingQuad != null);
|
2024-01-20 09:49:56 +08:00
|
|
|
|
2024-01-20 22:43:47 +08:00
|
|
|
var actualOrigin = ToScreenSpace(origin ?? defaultOrigin.Value);
|
2024-01-20 09:49:56 +08:00
|
|
|
|
|
|
|
if ((adjustAxis == Axes.Y && !allSelectedSupportManualSizing(Axes.Y)) ||
|
|
|
|
(adjustAxis == Axes.X && !allSelectedSupportManualSizing(Axes.X)))
|
|
|
|
return;
|
|
|
|
|
|
|
|
// If the selection has no area we cannot scale it
|
2024-01-20 20:04:05 +08:00
|
|
|
if (OriginalSurroundingQuad.Value.Width == 0 || OriginalSurroundingQuad.Value.Height == 0)
|
2024-01-20 09:49:56 +08:00
|
|
|
return;
|
|
|
|
|
2024-01-20 23:33:03 +08:00
|
|
|
// for now aspect lock scale adjustments that occur at corners.
|
2024-01-20 09:49:56 +08:00
|
|
|
if (adjustAxis == Axes.Both)
|
|
|
|
{
|
|
|
|
// project scale vector along diagonal
|
2024-01-20 20:04:05 +08:00
|
|
|
scale = new Vector2((scale.X + scale.Y) * 0.5f);
|
2024-01-20 09:49:56 +08:00
|
|
|
}
|
2024-01-20 23:33:03 +08:00
|
|
|
// 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.
|
2024-01-20 09:49:56 +08:00
|
|
|
|
2024-01-20 20:04:05 +08:00
|
|
|
bool flippedX = scale.X < 0;
|
|
|
|
bool flippedY = scale.Y < 0;
|
|
|
|
Axes toFlip = Axes.None;
|
2024-01-20 09:49:56 +08:00
|
|
|
|
2024-01-20 20:04:05 +08:00
|
|
|
if (flippedX != isFlippedX)
|
2024-01-20 09:49:56 +08:00
|
|
|
{
|
2024-01-20 20:04:05 +08:00
|
|
|
isFlippedX = flippedX;
|
|
|
|
toFlip |= Axes.X;
|
|
|
|
}
|
2024-01-20 09:49:56 +08:00
|
|
|
|
2024-01-20 20:04:05 +08:00
|
|
|
if (flippedY != isFlippedY)
|
|
|
|
{
|
|
|
|
isFlippedY = flippedY;
|
|
|
|
toFlip |= Axes.Y;
|
|
|
|
}
|
2024-01-20 09:49:56 +08:00
|
|
|
|
2024-01-20 20:04:05 +08:00
|
|
|
if (toFlip != Axes.None)
|
|
|
|
{
|
2024-01-20 09:49:56 +08:00
|
|
|
PerformFlipFromScaleHandles?.Invoke(toFlip);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-05-23 20:18:29 +08:00
|
|
|
foreach (var (b, originalState) in objectsInScale)
|
2024-01-20 09:49:56 +08:00
|
|
|
{
|
2024-05-23 20:18:29 +08:00
|
|
|
UpdatePosition(b, GeometryUtils.GetScaledPosition(scale, actualOrigin, originalState.ScreenSpaceOriginPosition));
|
2024-01-20 09:49:56 +08:00
|
|
|
|
2024-01-20 20:04:05 +08:00
|
|
|
var currentScale = scale;
|
2024-01-20 09:49:56 +08:00
|
|
|
if (Precision.AlmostEquals(MathF.Abs(b.Rotation) % 180, 90))
|
2024-01-20 20:04:05 +08:00
|
|
|
currentScale = new Vector2(scale.Y, scale.X);
|
2024-01-20 09:49:56 +08:00
|
|
|
|
|
|
|
switch (adjustAxis)
|
|
|
|
{
|
|
|
|
case Axes.X:
|
2024-05-23 20:18:29 +08:00
|
|
|
b.Width = MathF.Abs(originalState.Width * currentScale.X);
|
2024-01-20 09:49:56 +08:00
|
|
|
break;
|
|
|
|
|
|
|
|
case Axes.Y:
|
2024-05-23 20:18:29 +08:00
|
|
|
b.Height = MathF.Abs(originalState.Height * currentScale.Y);
|
2024-01-20 09:49:56 +08:00
|
|
|
break;
|
|
|
|
|
|
|
|
case Axes.Both:
|
2024-05-23 20:18:29 +08:00
|
|
|
b.Scale = originalState.Scale * currentScale;
|
2024-01-20 09:49:56 +08:00
|
|
|
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;
|
|
|
|
}
|
2024-05-23 20:18:29 +08:00
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
2024-01-20 09:49:56 +08:00
|
|
|
}
|
|
|
|
}
|