2019-01-24 08:43:03 +00: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.
2018-04-13 09:19:50 +00:00
2022-06-17 07:37:17 +00:00
#nullable disable
2019-11-06 05:55:05 +00:00
using System ;
2017-11-30 07:58:14 +00:00
using System.Collections.Generic ;
2019-10-16 11:20:07 +00:00
using System.Linq ;
2020-05-21 08:13:22 +00:00
using osu.Framework.Allocation ;
2020-09-09 10:14:28 +00:00
using osu.Framework.Bindables ;
2020-05-21 08:13:22 +00:00
using osu.Framework.Caching ;
2022-05-12 06:23:41 +00:00
using osu.Framework.Extensions.EnumExtensions ;
2020-05-21 08:13:22 +00:00
using osu.Framework.Graphics ;
using osu.Framework.Graphics.Containers ;
2020-09-25 07:33:22 +00:00
using osu.Framework.Graphics.Sprites ;
2022-10-19 11:06:39 +00:00
using osu.Framework.Input.Events ;
2023-05-02 06:36:01 +00:00
using osu.Framework.Utils ;
2017-11-30 10:19:34 +00:00
using osu.Game.Beatmaps ;
2020-09-25 07:33:22 +00:00
using osu.Game.Graphics.UserInterface ;
2022-10-21 07:11:19 +00:00
using osu.Game.Input.Bindings ;
2017-11-30 09:48:00 +00:00
using osu.Game.Rulesets.Edit ;
2017-11-30 07:58:14 +00:00
using osu.Game.Rulesets.Edit.Tools ;
2019-04-08 09:32:05 +00:00
using osu.Game.Rulesets.Mods ;
2019-10-16 11:20:07 +00:00
using osu.Game.Rulesets.Objects ;
2018-10-17 09:01:38 +00:00
using osu.Game.Rulesets.Osu.Objects ;
using osu.Game.Rulesets.UI ;
2020-09-25 07:33:22 +00:00
using osu.Game.Screens.Edit.Components.TernaryButtons ;
2018-11-16 08:12:24 +00:00
using osu.Game.Screens.Edit.Compose.Components ;
2020-05-21 08:13:22 +00:00
using osuTK ;
2018-04-13 09:19:50 +00:00
2017-11-29 07:22:11 +00:00
namespace osu.Game.Rulesets.Osu.Edit
{
2022-04-28 02:49:37 +00:00
public partial class OsuHitObjectComposer : DistancedHitObjectComposer < OsuHitObject >
2017-11-29 07:22:11 +00:00
{
2017-11-29 08:46:12 +00:00
public OsuHitObjectComposer ( Ruleset ruleset )
: base ( ruleset )
{
}
2018-04-13 09:19:50 +00:00
2019-12-12 06:58:11 +00:00
protected override DrawableRuleset < OsuHitObject > CreateDrawableRuleset ( Ruleset ruleset , IBeatmap beatmap , IReadOnlyList < Mod > mods = null )
2021-04-26 05:33:30 +00:00
= > new DrawableOsuEditorRuleset ( ruleset , beatmap , mods ) ;
2018-04-13 09:19:50 +00:00
2018-10-04 04:44:49 +00:00
protected override IReadOnlyList < HitObjectCompositionTool > CompositionTools = > new HitObjectCompositionTool [ ]
2017-11-30 07:58:14 +00:00
{
2018-10-03 07:27:26 +00:00
new HitCircleCompositionTool ( ) ,
2018-10-04 04:44:49 +00:00
new SliderCompositionTool ( ) ,
2018-10-29 09:35:46 +00:00
new SpinnerCompositionTool ( )
2017-11-30 07:58:14 +00:00
} ;
2018-04-13 09:19:50 +00:00
2021-09-20 18:14:28 +00:00
private readonly Bindable < TernaryState > rectangularGridSnapToggle = new Bindable < TernaryState > ( ) ;
2020-09-09 10:14:28 +00:00
2020-09-25 08:45:19 +00:00
protected override IEnumerable < TernaryButton > CreateTernaryButtons ( ) = > base . CreateTernaryButtons ( ) . Concat ( new [ ]
2020-09-09 10:14:28 +00:00
{
2021-09-20 18:14:28 +00:00
new TernaryButton ( rectangularGridSnapToggle , "Grid Snap" , ( ) = > new SpriteIcon { Icon = FontAwesome . Solid . Th } )
2020-09-25 05:09:31 +00:00
} ) ;
2020-09-09 10:14:28 +00:00
2020-09-21 09:15:33 +00:00
private BindableList < HitObject > selectedHitObjects ;
private Bindable < HitObject > placementObject ;
2020-05-21 08:13:22 +00:00
[BackgroundDependencyLoader]
private void load ( )
{
2022-10-18 05:31:54 +00:00
// Give a bit of breathing room around the playfield content.
2023-05-02 06:36:01 +00:00
PlayfieldContentContainer . Padding = new MarginPadding
{
Vertical = 10 ,
Left = TOOLBOX_CONTRACTED_SIZE_LEFT + 10 ,
Right = TOOLBOX_CONTRACTED_SIZE_RIGHT + 10 ,
} ;
2022-10-18 05:31:54 +00:00
2020-10-20 04:59:03 +00:00
LayerBelowRuleset . AddRange ( new Drawable [ ]
2020-10-19 19:20:13 +00:00
{
2020-10-20 04:59:03 +00:00
distanceSnapGridContainer = new Container
2021-09-19 15:48:29 +00:00
{
RelativeSizeAxes = Axes . Both
} ,
2021-09-20 18:39:39 +00:00
rectangularPositionSnapGrid = new OsuRectangularPositionSnapGrid
2020-10-20 04:59:03 +00:00
{
RelativeSizeAxes = Axes . Both
}
2020-10-19 19:20:13 +00:00
} ) ;
2020-05-23 01:44:53 +00:00
2020-09-21 09:15:33 +00:00
selectedHitObjects = EditorBeatmap . SelectedHitObjects . GetBoundCopy ( ) ;
2022-06-24 12:25:23 +00:00
selectedHitObjects . CollectionChanged + = ( _ , _ ) = > updateDistanceSnapGrid ( ) ;
2020-09-21 09:15:33 +00:00
placementObject = EditorBeatmap . PlacementObject . GetBoundCopy ( ) ;
placementObject . ValueChanged + = _ = > updateDistanceSnapGrid ( ) ;
2022-10-26 04:27:23 +00:00
DistanceSnapToggle . ValueChanged + = _ = > updateDistanceSnapGrid ( ) ;
2020-09-22 07:02:07 +00:00
// we may be entering the screen with a selection already active
updateDistanceSnapGrid ( ) ;
2020-05-21 08:13:22 +00:00
}
2020-11-12 10:52:02 +00:00
protected override ComposeBlueprintContainer CreateBlueprintContainer ( )
= > new OsuBlueprintContainer ( this ) ;
2019-10-16 11:20:07 +00:00
2021-03-29 09:29:05 +00:00
public override string ConvertSelectionToString ( )
= > string . Join ( ',' , selectedHitObjects . Cast < OsuHitObject > ( ) . OrderBy ( h = > h . StartTime ) . Select ( h = > ( h . IndexInCurrentCombo + 1 ) . ToString ( ) ) ) ;
2021-03-26 07:25:20 +00:00
2020-05-21 08:13:22 +00:00
private DistanceSnapGrid distanceSnapGrid ;
private Container distanceSnapGridContainer ;
private readonly Cached distanceSnapGridCache = new Cached ( ) ;
private double? lastDistanceSnapGridTime ;
2021-09-20 18:13:06 +00:00
private RectangularPositionSnapGrid rectangularPositionSnapGrid ;
2022-10-21 13:58:36 +00:00
protected override double ReadCurrentDistanceSnap ( HitObject before , HitObject after )
{
float expectedDistance = DurationToDistance ( before , after . StartTime - before . GetEndTime ( ) ) ;
float actualDistance = Vector2 . Distance ( ( ( OsuHitObject ) before ) . EndPosition , ( ( OsuHitObject ) after ) . Position ) ;
return actualDistance / expectedDistance ;
}
2020-05-21 08:13:22 +00:00
protected override void Update ( )
{
base . Update ( ) ;
if ( ! ( BlueprintContainer . CurrentTool is SelectTool ) )
{
if ( EditorClock . CurrentTime ! = lastDistanceSnapGridTime )
{
distanceSnapGridCache . Invalidate ( ) ;
lastDistanceSnapGridTime = EditorClock . CurrentTime ;
}
if ( ! distanceSnapGridCache . IsValid )
updateDistanceSnapGrid ( ) ;
}
}
2022-05-12 06:23:41 +00:00
public override SnapResult FindSnappedPositionAndTime ( Vector2 screenSpacePosition , SnapType snapType = SnapType . All )
2020-05-21 08:13:22 +00:00
{
2022-05-12 06:23:41 +00:00
if ( snapType . HasFlagFast ( SnapType . NearbyObjects ) & & snapToVisibleBlueprints ( screenSpacePosition , out var snapResult ) )
2022-10-26 05:39:39 +00:00
{
// In the case of snapping to nearby objects, a time value is not provided.
// This matches the stable editor (which also uses current time), but with the introduction of time-snapping distance snap
2022-10-26 21:30:14 +00:00
// this could result in unexpected behaviour when distance snapping is turned on and a user attempts to place an object that is
2022-10-26 05:39:39 +00:00
// BOTH on a valid distance snap ring, and also at the same position as a previous object.
//
// We want to ensure that in this particular case, the time-snapping component of distance snap is still applied.
// The easiest way to ensure this is to attempt application of distance snap after a nearby object is found, and copy over
// the time value if the proposed positions are roughly the same.
2023-05-25 12:41:19 +00:00
if ( snapType . HasFlagFast ( SnapType . RelativeGrids ) & & DistanceSnapToggle . Value = = TernaryState . True & & distanceSnapGrid ! = null )
2022-10-26 05:39:39 +00:00
{
( Vector2 distanceSnappedPosition , double distanceSnappedTime ) = distanceSnapGrid . GetSnappedPosition ( distanceSnapGrid . ToLocalSpace ( snapResult . ScreenSpacePosition ) ) ;
if ( Precision . AlmostEquals ( distanceSnapGrid . ToScreenSpace ( distanceSnappedPosition ) , snapResult . ScreenSpacePosition , 1 ) )
snapResult . Time = distanceSnappedTime ;
}
2020-09-24 05:22:18 +00:00
return snapResult ;
2022-10-26 05:39:39 +00:00
}
2020-09-24 05:22:18 +00:00
2022-10-25 05:09:22 +00:00
SnapResult result = base . FindSnappedPositionAndTime ( screenSpacePosition , snapType ) ;
2023-05-25 12:41:19 +00:00
if ( snapType . HasFlagFast ( SnapType . RelativeGrids ) )
2021-09-19 15:48:29 +00:00
{
2022-10-26 04:27:23 +00:00
if ( DistanceSnapToggle . Value = = TernaryState . True & & distanceSnapGrid ! = null )
2022-05-12 06:23:41 +00:00
{
( Vector2 pos , double time ) = distanceSnapGrid . GetSnappedPosition ( distanceSnapGrid . ToLocalSpace ( screenSpacePosition ) ) ;
2022-10-25 05:09:22 +00:00
result . ScreenSpacePosition = distanceSnapGrid . ToScreenSpace ( pos ) ;
result . Time = time ;
2022-05-12 06:23:41 +00:00
}
2023-05-25 12:41:19 +00:00
}
2020-05-21 08:13:22 +00:00
2023-05-25 12:41:19 +00:00
if ( snapType . HasFlagFast ( SnapType . GlobalGrids ) )
{
2022-05-12 06:23:41 +00:00
if ( rectangularGridSnapToggle . Value = = TernaryState . True )
{
2022-10-25 05:09:22 +00:00
Vector2 pos = rectangularPositionSnapGrid . GetSnappedPosition ( rectangularPositionSnapGrid . ToLocalSpace ( result . ScreenSpacePosition ) ) ;
result . ScreenSpacePosition = rectangularPositionSnapGrid . ToScreenSpace ( pos ) ;
2022-05-12 06:23:41 +00:00
}
2021-09-19 15:48:29 +00:00
}
2020-05-21 08:13:22 +00:00
2022-10-25 05:09:22 +00:00
return result ;
2020-05-21 08:13:22 +00:00
}
2020-09-24 05:22:18 +00:00
private bool snapToVisibleBlueprints ( Vector2 screenSpacePosition , out SnapResult snapResult )
{
// check other on-screen objects for snapping/stacking
var blueprints = BlueprintContainer . SelectionBlueprints . AliveChildren ;
var playfield = PlayfieldAtScreenSpacePosition ( screenSpacePosition ) ;
float snapRadius =
2023-06-08 08:44:32 +00:00
playfield . GamefieldToScreenSpace ( new Vector2 ( OsuHitObject . OBJECT_RADIUS * 0.10f ) ) . X -
2020-09-24 05:22:18 +00:00
playfield . GamefieldToScreenSpace ( Vector2 . Zero ) . X ;
foreach ( var b in blueprints )
{
if ( b . IsSelected )
continue ;
2023-02-02 13:22:30 +00:00
var snapPositions = b . ScreenSpaceSnapPoints ;
2020-09-24 05:22:18 +00:00
2023-02-03 15:05:16 +00:00
if ( ! snapPositions . Any ( ) )
continue ;
2023-02-03 15:13:37 +00:00
2023-02-03 15:05:16 +00:00
var closestSnapPosition = snapPositions . MinBy ( p = > Vector2 . Distance ( p , screenSpacePosition ) ) ;
2020-09-24 05:22:18 +00:00
2023-02-03 15:05:16 +00:00
if ( Vector2 . Distance ( closestSnapPosition , screenSpacePosition ) < snapRadius )
2020-09-24 05:34:41 +00:00
{
// only return distance portion, since time is not really valid
2023-02-04 13:36:30 +00:00
snapResult = new SnapResult ( closestSnapPosition , null , playfield ) ;
2020-09-24 05:34:41 +00:00
return true ;
}
2020-09-24 05:22:18 +00:00
}
snapResult = null ;
return false ;
}
2020-05-21 08:13:22 +00:00
private void updateDistanceSnapGrid ( )
{
distanceSnapGridContainer . Clear ( ) ;
distanceSnapGridCache . Invalidate ( ) ;
2020-09-09 10:14:28 +00:00
distanceSnapGrid = null ;
2022-10-26 04:27:23 +00:00
if ( DistanceSnapToggle . Value ! = TernaryState . True )
2020-09-09 10:14:28 +00:00
return ;
2020-05-21 08:13:22 +00:00
2020-05-23 01:57:17 +00:00
switch ( BlueprintContainer . CurrentTool )
{
2022-06-24 12:25:23 +00:00
case SelectTool :
2020-05-23 01:57:17 +00:00
if ( ! EditorBeatmap . SelectedHitObjects . Any ( ) )
return ;
distanceSnapGrid = createDistanceSnapGrid ( EditorBeatmap . SelectedHitObjects ) ;
break ;
default :
if ( ! CursorInPlacementArea )
return ;
distanceSnapGrid = createDistanceSnapGrid ( Enumerable . Empty < HitObject > ( ) ) ;
break ;
}
2020-05-21 08:13:22 +00:00
2020-05-23 01:57:17 +00:00
if ( distanceSnapGrid ! = null )
2020-05-21 08:13:22 +00:00
{
distanceSnapGridContainer . Add ( distanceSnapGrid ) ;
distanceSnapGridCache . Validate ( ) ;
}
}
2022-10-19 11:06:39 +00:00
protected override bool OnKeyDown ( KeyDownEvent e )
{
if ( e . Repeat )
return false ;
2022-10-21 07:11:19 +00:00
handleToggleViaKey ( e ) ;
2022-10-19 11:06:39 +00:00
return base . OnKeyDown ( e ) ;
}
protected override void OnKeyUp ( KeyUpEvent e )
{
2022-10-21 07:11:19 +00:00
handleToggleViaKey ( e ) ;
2022-10-19 11:06:39 +00:00
base . OnKeyUp ( e ) ;
}
2022-10-21 07:11:19 +00:00
protected override bool AdjustDistanceSpacing ( GlobalAction action , float amount )
{
// To allow better visualisation, ensure that the spacing grid is visible before adjusting.
2022-10-26 04:27:23 +00:00
DistanceSnapToggle . Value = TernaryState . True ;
2022-10-21 07:11:19 +00:00
return base . AdjustDistanceSpacing ( action , amount ) ;
}
2022-10-25 05:22:28 +00:00
private bool gridSnapMomentary ;
2022-10-21 07:11:19 +00:00
private void handleToggleViaKey ( KeyboardEvent key )
2022-10-19 11:06:39 +00:00
{
2022-10-25 05:22:28 +00:00
bool shiftPressed = key . ShiftPressed ;
if ( shiftPressed ! = gridSnapMomentary )
2022-10-21 07:11:19 +00:00
{
2022-10-25 05:22:28 +00:00
gridSnapMomentary = shiftPressed ;
rectangularGridSnapToggle . Value = rectangularGridSnapToggle . Value = = TernaryState . False ? TernaryState . True : TernaryState . False ;
2022-10-19 11:06:39 +00:00
}
}
2020-05-21 08:13:22 +00:00
private DistanceSnapGrid createDistanceSnapGrid ( IEnumerable < HitObject > selectedHitObjects )
2019-10-16 11:20:07 +00:00
{
2020-02-07 10:08:49 +00:00
if ( BlueprintContainer . CurrentTool is SpinnerCompositionTool )
return null ;
2019-10-16 11:20:07 +00:00
var objects = selectedHitObjects . ToList ( ) ;
if ( objects . Count = = 0 )
2020-05-22 07:40:52 +00:00
// use accurate time value to give more instantaneous feedback to the user.
return createGrid ( h = > h . StartTime < = EditorClock . CurrentTimeAccurate ) ;
2019-11-06 05:55:05 +00:00
double minTime = objects . Min ( h = > h . StartTime ) ;
2019-11-06 07:04:20 +00:00
return createGrid ( h = > h . StartTime < minTime , objects . Count + 1 ) ;
2019-11-06 05:55:05 +00:00
}
2019-11-06 07:04:20 +00:00
/// <summary>
/// Creates a grid from the last <see cref="HitObject"/> matching a predicate to a target <see cref="HitObject"/>.
/// </summary>
/// <param name="sourceSelector">A predicate that matches <see cref="HitObject"/>s where the grid can start from.
/// Only the last <see cref="HitObject"/> matching the predicate is used.</param>
/// <param name="targetOffset">An offset from the <see cref="HitObject"/> selected via <paramref name="sourceSelector"/> at which the grid should stop.</param>
/// <returns>The <see cref="OsuDistanceSnapGrid"/> from a selected <see cref="HitObject"/> to a target <see cref="HitObject"/>.</returns>
private OsuDistanceSnapGrid createGrid ( Func < HitObject , bool > sourceSelector , int targetOffset = 1 )
2019-11-06 05:55:05 +00:00
{
2019-11-06 07:04:20 +00:00
if ( targetOffset < 1 ) throw new ArgumentOutOfRangeException ( nameof ( targetOffset ) ) ;
int sourceIndex = - 1 ;
2019-11-06 05:55:05 +00:00
for ( int i = 0 ; i < EditorBeatmap . HitObjects . Count ; i + + )
2019-10-16 11:20:07 +00:00
{
2019-11-06 07:04:20 +00:00
if ( ! sourceSelector ( EditorBeatmap . HitObjects [ i ] ) )
2019-11-06 05:55:05 +00:00
break ;
2019-10-16 11:20:07 +00:00
2019-11-06 07:04:20 +00:00
sourceIndex = i ;
2019-10-16 11:20:07 +00:00
}
2019-11-06 07:04:20 +00:00
if ( sourceIndex = = - 1 )
2019-11-06 05:55:05 +00:00
return null ;
2019-10-16 11:20:07 +00:00
2019-12-27 10:39:30 +00:00
HitObject sourceObject = EditorBeatmap . HitObjects [ sourceIndex ] ;
2019-12-17 07:35:40 +00:00
int targetIndex = sourceIndex + targetOffset ;
2019-12-27 10:39:30 +00:00
HitObject targetObject = null ;
2019-12-17 07:35:40 +00:00
// Keep advancing the target object while its start time falls before the end time of the source object
while ( true )
{
if ( targetIndex > = EditorBeatmap . HitObjects . Count )
break ;
if ( EditorBeatmap . HitObjects [ targetIndex ] . StartTime > = sourceObject . GetEndTime ( ) )
{
targetObject = EditorBeatmap . HitObjects [ targetIndex ] ;
break ;
}
targetIndex + + ;
}
2019-10-16 11:20:07 +00:00
2020-02-07 10:08:49 +00:00
if ( sourceObject is Spinner )
return null ;
2019-12-27 10:39:30 +00:00
return new OsuDistanceSnapGrid ( ( OsuHitObject ) sourceObject , ( OsuHitObject ) targetObject ) ;
2019-10-16 11:20:07 +00:00
}
2017-11-29 07:22:11 +00:00
}
}