2024-01-20 07:22:53 +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 ;
2024-01-20 22:39:38 +08:00
using osu.Framework.Graphics ;
2024-01-20 07:22:53 +08:00
using osu.Framework.Graphics.Primitives ;
2024-01-20 22:15:49 +08:00
using osu.Framework.Utils ;
2024-01-20 07:22:53 +08:00
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
{
2024-05-28 23:12:16 +08:00
/// <summary>
/// Whether scaling anchored by the center of the playfield can currently be performed.
/// </summary>
public Bindable < bool > CanScaleFromPlayfieldOrigin { get ; private set ; } = new BindableBool ( ) ;
2024-05-29 00:27:01 +08:00
/// <summary>
/// Whether a single slider is currently selected, which results in a different scaling behaviour.
/// </summary>
public Bindable < bool > IsScalingSlider { get ; private set ; } = new BindableBool ( ) ;
2024-01-20 07:22:53 +08:00
[Resolved]
private IEditorChangeHandler ? changeHandler { get ; set ; }
[Resolved(CanBeNull = true)]
private IDistanceSnapProvider ? snapProvider { get ; set ; }
private BindableList < HitObject > selectedItems { get ; } = new BindableList < HitObject > ( ) ;
[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 ) ;
2024-01-20 08:13:01 +08:00
CanScaleX . Value = quad . Width > 0 ;
CanScaleY . Value = quad . Height > 0 ;
CanScaleDiagonally . Value = CanScaleX . Value & & CanScaleY . Value ;
2024-05-28 22:14:16 +08:00
CanScaleFromPlayfieldOrigin . Value = selectedMovableObjects . Any ( ) ;
2024-05-29 00:27:01 +08:00
IsScalingSlider . Value = selectedMovableObjects . Count ( ) = = 1 & & selectedMovableObjects . First ( ) is Slider ;
2024-01-20 07:22:53 +08:00
}
2024-05-23 20:41:59 +08:00
private Dictionary < OsuHitObject , OriginalHitObjectState > ? objectsInScale ;
2024-01-20 07:22:53 +08:00
private Vector2 ? defaultOrigin ;
2024-07-14 22:58:05 +08:00
private List < Vector2 > ? originalConvexHull ;
2024-01-20 07:22:53 +08:00
public override void Begin ( )
{
2024-06-25 21:01:43 +08:00
if ( OperationInProgress . Value )
2024-01-20 07:22:53 +08:00
throw new InvalidOperationException ( $"Cannot {nameof(Begin)} a scale operation while another is in progress!" ) ;
2024-06-25 21:01:43 +08:00
base . Begin ( ) ;
2024-01-20 07:22:53 +08:00
changeHandler ? . BeginChange ( ) ;
2024-05-23 20:41:59 +08:00
objectsInScale = selectedMovableObjects . ToDictionary ( ho = > ho , ho = > new OriginalHitObjectState ( ho ) ) ;
2024-07-14 21:46:40 +08:00
OriginalSurroundingQuad = objectsInScale . Count = = 1 & & objectsInScale . First ( ) . Key is Slider slider
? GeometryUtils . GetSurroundingQuad ( slider . Path . ControlPoints . Select ( p = > slider . Position + p . Position ) )
: GeometryUtils . GetSurroundingQuad ( objectsInScale . Keys ) ;
2024-07-14 22:58:05 +08:00
originalConvexHull = objectsInScale . Count = = 1 & & objectsInScale . First ( ) . Key is Slider slider2
? GeometryUtils . GetConvexHull ( slider2 . Path . ControlPoints . Select ( p = > slider2 . Position + p . Position ) )
: GeometryUtils . GetConvexHull ( objectsInScale . Keys ) ;
2024-09-20 07:07:54 +08:00
defaultOrigin = GeometryUtils . MinimumEnclosingCircle ( originalConvexHull ) . Item1 ;
2024-01-20 07:22:53 +08:00
}
2024-07-03 22:23:19 +08:00
public override void Update ( Vector2 scale , Vector2 ? origin = null , Axes adjustAxis = Axes . Both , float axisRotation = 0 )
2024-01-20 07:22:53 +08:00
{
2024-06-25 21:01:43 +08:00
if ( ! OperationInProgress . Value )
2024-01-20 07:22:53 +08:00
throw new InvalidOperationException ( $"Cannot {nameof(Update)} a scale operation without calling {nameof(Begin)} first!" ) ;
2024-06-25 21:01:43 +08:00
Debug . Assert ( objectsInScale ! = null & & defaultOrigin ! = null & & OriginalSurroundingQuad ! = null ) ;
2024-01-20 07:22:53 +08:00
Vector2 actualOrigin = origin ? ? defaultOrigin . Value ;
2024-07-03 22:23:19 +08:00
scale = clampScaleToAdjustAxis ( scale , adjustAxis ) ;
2024-01-20 07:22:53 +08:00
// 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.
2024-05-23 20:41:59 +08:00
if ( objectsInScale . Count = = 1 & & objectsInScale . First ( ) . Key is Slider slider )
{
2024-09-20 23:19:38 +08:00
scaleSlider ( slider , scale , actualOrigin , objectsInScale [ slider ] , axisRotation ) ;
2024-05-23 20:41:59 +08:00
}
2024-01-20 07:22:53 +08:00
else
{
2024-07-03 22:23:19 +08:00
scale = ClampScaleToPlayfieldBounds ( scale , actualOrigin , adjustAxis , axisRotation ) ;
2024-01-20 07:22:53 +08:00
2024-05-23 20:41:59 +08:00
foreach ( var ( ho , originalState ) in objectsInScale )
2024-01-20 07:22:53 +08:00
{
2024-07-03 22:23:19 +08:00
ho . Position = GeometryUtils . GetScaledPosition ( scale , actualOrigin , originalState . Position , axisRotation ) ;
2024-01-20 07:22:53 +08:00
}
}
moveSelectionInBounds ( ) ;
}
public override void Commit ( )
{
2024-06-25 21:01:43 +08:00
if ( ! OperationInProgress . Value )
2024-01-20 07:22:53 +08:00
throw new InvalidOperationException ( $"Cannot {nameof(Commit)} a rotate operation without calling {nameof(Begin)} first!" ) ;
changeHandler ? . EndChange ( ) ;
2024-06-25 21:01:43 +08:00
base . Commit ( ) ;
2024-01-20 07:22:53 +08:00
objectsInScale = null ;
OriginalSurroundingQuad = null ;
defaultOrigin = null ;
}
private IEnumerable < OsuHitObject > selectedMovableObjects = > selectedItems . Cast < OsuHitObject > ( )
. Where ( h = > h is not Spinner ) ;
2024-07-03 22:23:19 +08:00
private Vector2 clampScaleToAdjustAxis ( Vector2 scale , Axes adjustAxis )
{
switch ( adjustAxis )
{
case Axes . Y :
scale . X = 1 ;
break ;
case Axes . X :
scale . Y = 1 ;
break ;
case Axes . None :
scale = Vector2 . One ;
break ;
}
return scale ;
}
2024-09-20 23:19:38 +08:00
private void scaleSlider ( Slider slider , Vector2 scale , Vector2 origin , OriginalHitObjectState originalInfo , float axisRotation = 0 )
2024-01-20 07:22:53 +08:00
{
2024-09-20 23:19:38 +08:00
Debug . Assert ( originalInfo . PathControlPointPositions ! = null & & originalInfo . PathControlPointTypes ! = null ) ;
2024-05-23 20:56:08 +08:00
scale = Vector2 . ComponentMax ( scale , new Vector2 ( Precision . FLOAT_EPSILON ) ) ;
2024-01-20 07:22:53 +08:00
// 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 + + )
{
2024-09-20 23:19:38 +08:00
slider . Path . ControlPoints [ i ] . Position = GeometryUtils . GetScaledPosition ( scale , Vector2 . Zero , originalInfo . PathControlPointPositions [ i ] , axisRotation ) ;
slider . Path . ControlPoints [ i ] . Type = originalInfo . PathControlPointTypes [ i ] ;
2024-01-20 07:22:53 +08:00
}
// 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 ) ;
2024-09-20 23:19:38 +08:00
slider . Position = GeometryUtils . GetScaledPosition ( scale , origin , originalInfo . Position , axisRotation ) ;
2024-01-20 07:22:53 +08:00
//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 + + )
2024-09-20 23:19:38 +08:00
slider . Path . ControlPoints [ i ] . Position = originalInfo . PathControlPointPositions [ i ] ;
slider . Position = originalInfo . Position ;
2024-01-20 07:22:53 +08:00
// 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 ) ;
}
2024-05-28 23:12:16 +08:00
/// <summary>
/// Clamp scale for multi-object-scaling where selection does not exceed playfield bounds or flip.
2024-07-03 22:41:57 +08:00
/// </summary>
2024-05-28 23:12:16 +08:00
/// <param name="origin">The origin from which the scale operation is performed</param>
/// <param name="scale">The scale to be clamped</param>
2024-07-03 22:23:19 +08:00
/// <param name="adjustAxis">The axes to adjust the scale in.</param>
/// <param name="axisRotation">The rotation of the axes in degrees</param>
2024-05-28 23:12:16 +08:00
/// <returns>The clamped scale vector</returns>
2024-07-03 22:23:19 +08:00
public Vector2 ClampScaleToPlayfieldBounds ( Vector2 scale , Vector2 ? origin = null , Axes adjustAxis = Axes . Both , float axisRotation = 0 )
2024-01-20 07:22:53 +08:00
{
//todo: this is not always correct for selections involving sliders. This approximation assumes each point is scaled independently, but sliderends move with the sliderhead.
2024-07-03 22:23:19 +08:00
if ( objectsInScale = = null | | adjustAxis = = Axes . None )
2024-05-26 02:17:27 +08:00
return scale ;
Debug . Assert ( defaultOrigin ! = null & & OriginalSurroundingQuad ! = null ) ;
2024-01-20 07:22:53 +08:00
2024-05-26 02:17:27 +08:00
if ( objectsInScale . Count = = 1 & & objectsInScale . First ( ) . Key is Slider slider )
origin = slider . Position ;
2024-07-14 22:58:05 +08:00
float cos = MathF . Cos ( float . DegreesToRadians ( - axisRotation ) ) ;
float sin = MathF . Sin ( float . DegreesToRadians ( - axisRotation ) ) ;
2024-07-03 22:23:19 +08:00
scale = clampScaleToAdjustAxis ( scale , adjustAxis ) ;
2024-05-26 02:17:27 +08:00
Vector2 actualOrigin = origin ? ? defaultOrigin . Value ;
2024-07-14 22:58:05 +08:00
IEnumerable < Vector2 > points ;
2024-05-26 02:17:27 +08:00
2024-07-14 22:58:05 +08:00
if ( axisRotation = = 0 )
{
var selectionQuad = OriginalSurroundingQuad . Value ;
points = new [ ]
{
selectionQuad . TopLeft ,
selectionQuad . TopRight ,
selectionQuad . BottomLeft ,
selectionQuad . BottomRight
} ;
}
else
points = originalConvexHull ! ;
foreach ( var point in points )
{
2024-09-22 02:34:29 +08:00
scale = clampToBound ( scale , point , Vector2 . Zero , OsuPlayfield . BASE_SIZE ) ;
2024-07-14 22:58:05 +08:00
}
2024-01-20 07:22:53 +08:00
2024-05-23 20:56:08 +08:00
return Vector2 . ComponentMax ( scale , new Vector2 ( Precision . FLOAT_EPSILON ) ) ;
2024-07-03 22:23:19 +08:00
2024-09-22 02:34:29 +08:00
float minComponent ( Vector2 v ) = > MathF . Min ( v . X , v . Y ) ;
float maxComponent ( Vector2 v ) = > MathF . Max ( v . X , v . Y ) ;
2024-07-03 22:41:41 +08:00
2024-09-22 02:34:29 +08:00
Vector2 clampToBound ( Vector2 s , Vector2 p , Vector2 lowerBound , Vector2 upperBound )
2024-07-03 22:23:19 +08:00
{
2024-07-03 22:41:41 +08:00
p - = actualOrigin ;
2024-09-22 02:34:29 +08:00
lowerBound - = actualOrigin ;
upperBound - = actualOrigin ;
2024-07-03 22:41:41 +08:00
var a = new Vector2 ( cos * cos * p . X - sin * cos * p . Y , - sin * cos * p . X + sin * sin * p . Y ) ;
var b = new Vector2 ( sin * sin * p . X + sin * cos * p . Y , sin * cos * p . X + cos * cos * p . Y ) ;
2024-07-03 22:23:19 +08:00
switch ( adjustAxis )
{
case Axes . X :
2024-09-22 02:34:29 +08:00
var lowerBounds = Vector2 . Divide ( lowerBound - b , a ) ;
var upperBounds = Vector2 . Divide ( upperBound - b , a ) ;
if ( a . X < 0 )
( lowerBounds , upperBounds ) = ( upperBounds , lowerBounds ) ;
s . X = MathHelper . Clamp ( s . X , maxComponent ( lowerBounds ) , minComponent ( upperBounds ) ) ;
2024-07-03 22:23:19 +08:00
break ;
case Axes . Y :
2024-09-22 02:34:29 +08:00
var lowerBoundsY = Vector2 . Divide ( lowerBound - a , b ) ;
var upperBoundsY = Vector2 . Divide ( upperBound - a , b ) ;
if ( b . Y < 0 )
( lowerBoundsY , upperBoundsY ) = ( upperBoundsY , lowerBoundsY ) ;
s . Y = MathHelper . Clamp ( s . Y , maxComponent ( lowerBoundsY ) , minComponent ( upperBoundsY ) ) ;
2024-07-03 22:23:19 +08:00
break ;
case Axes . Both :
2024-09-22 02:34:29 +08:00
// s = Vector2.ComponentMin(s, s * minPositiveComponent(Vector2.Divide(bound, a * s.X + b * s.Y)));
2024-07-03 22:23:19 +08:00
break ;
}
return s ;
}
2024-01-20 07:22:53 +08:00
}
private void moveSelectionInBounds ( )
{
2024-05-23 20:41:59 +08:00
Quad quad = GeometryUtils . GetSurroundingQuad ( objectsInScale ! . Keys ) ;
2024-01-20 07:22:53 +08:00
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 ;
2024-05-23 20:41:59 +08:00
foreach ( var ( h , _ ) in objectsInScale ! )
2024-01-20 07:22:53 +08:00
h . Position + = delta ;
}
2024-05-23 20:41:59 +08:00
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 ( ) ;
}
}
2024-01-20 07:22:53 +08:00
}
}