2019-01-24 16:43:03 +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.
2018-04-13 17:19:50 +08:00
2019-12-11 17:52:38 +08:00
using System.Collections.Generic ;
2019-11-12 14:00:57 +08:00
using System.Diagnostics ;
2020-11-03 19:45:48 +08:00
using System.Linq ;
2021-03-20 05:44:31 +08:00
using JetBrains.Annotations ;
2019-10-24 18:02:59 +08:00
using osu.Framework.Allocation ;
2019-12-06 15:36:08 +08:00
using osu.Framework.Bindables ;
2018-04-13 17:19:50 +08:00
using osu.Framework.Graphics ;
2019-11-12 13:44:11 +08:00
using osu.Framework.Graphics.Primitives ;
2019-11-12 12:32:31 +08:00
using osu.Framework.Graphics.UserInterface ;
using osu.Framework.Input.Events ;
2021-11-02 02:37:29 +08:00
using osu.Framework.Utils ;
2019-11-12 12:32:31 +08:00
using osu.Game.Graphics.UserInterface ;
2019-10-24 18:02:59 +08:00
using osu.Game.Rulesets.Edit ;
using osu.Game.Rulesets.Objects ;
2018-11-07 15:08:56 +08:00
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components ;
2018-04-13 17:19:50 +08:00
using osu.Game.Rulesets.Osu.Objects ;
2021-05-24 16:15:57 +08:00
using osu.Game.Rulesets.Osu.Objects.Drawables ;
2020-04-09 18:54:58 +08:00
using osu.Game.Screens.Edit ;
2019-12-11 17:52:38 +08:00
using osu.Game.Screens.Edit.Compose ;
2018-11-20 15:51:59 +08:00
using osuTK ;
2019-11-12 12:32:31 +08:00
using osuTK.Input ;
2018-04-13 17:19:50 +08:00
2018-11-07 15:08:56 +08:00
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
2018-04-13 17:19:50 +08:00
{
2019-11-12 12:38:42 +08:00
public class SliderSelectionBlueprint : OsuSelectionBlueprint < Slider >
2018-04-13 17:19:50 +08:00
{
2021-05-24 16:15:57 +08:00
protected new DrawableSlider DrawableObject = > ( DrawableSlider ) base . DrawableObject ;
2020-10-09 15:54:43 +08:00
protected SliderBodyPiece BodyPiece { get ; private set ; }
2021-05-18 13:19:11 +08:00
protected SliderCircleOverlay HeadOverlay { get ; private set ; }
protected SliderCircleOverlay TailOverlay { get ; private set ; }
2021-03-20 05:44:31 +08:00
[CanBeNull]
2020-10-09 15:54:43 +08:00
protected PathControlPointVisualiser ControlPointVisualiser { get ; private set ; }
2020-10-09 13:05:00 +08:00
2019-10-24 18:04:00 +08:00
[Resolved(CanBeNull = true)]
2019-10-24 18:02:59 +08:00
private HitObjectComposer composer { get ; set ; }
2019-12-11 17:52:38 +08:00
[Resolved(CanBeNull = true)]
private IPlacementHandler placementHandler { get ; set ; }
2020-04-09 18:54:58 +08:00
[Resolved(CanBeNull = true)]
private EditorBeatmap editorBeatmap { get ; set ; }
2020-04-09 21:00:56 +08:00
[Resolved(CanBeNull = true)]
private IEditorChangeHandler changeHandler { get ; set ; }
2021-11-02 02:37:29 +08:00
[Resolved(CanBeNull = true)]
private BindableBeatDivisor beatDivisor { get ; set ; }
2021-01-18 15:57:36 +08:00
public override Quad SelectionQuad = > BodyPiece . ScreenSpaceDrawQuad ;
2020-11-20 21:43:10 +08:00
private readonly BindableList < PathControlPoint > controlPoints = new BindableList < PathControlPoint > ( ) ;
private readonly IBindable < int > pathVersion = new Bindable < int > ( ) ;
2021-05-13 18:53:32 +08:00
public SliderSelectionBlueprint ( Slider slider )
2018-04-13 17:19:50 +08:00
: base ( slider )
{
2020-10-09 13:05:00 +08:00
}
2018-04-13 17:19:50 +08:00
2020-10-09 13:05:00 +08:00
[BackgroundDependencyLoader]
private void load ( )
{
2018-04-13 17:19:50 +08:00
InternalChildren = new Drawable [ ]
{
2019-10-01 18:33:24 +08:00
BodyPiece = new SliderBodyPiece ( ) ,
2021-05-18 13:19:11 +08:00
HeadOverlay = CreateCircleOverlay ( HitObject , SliderPosition . Start ) ,
TailOverlay = CreateCircleOverlay ( HitObject , SliderPosition . End ) ,
2018-04-13 17:19:50 +08:00
} ;
}
2019-12-06 15:36:08 +08:00
protected override void LoadComplete ( )
{
base . LoadComplete ( ) ;
2020-11-20 21:43:10 +08:00
controlPoints . BindTo ( HitObject . Path . ControlPoints ) ;
pathVersion . BindTo ( HitObject . Path . Version ) ;
2019-12-06 15:36:08 +08:00
pathVersion . BindValueChanged ( _ = > updatePath ( ) ) ;
2020-10-09 13:04:26 +08:00
BodyPiece . UpdateFrom ( HitObject ) ;
2019-12-06 15:36:08 +08:00
}
2020-11-03 19:45:48 +08:00
public override bool HandleQuickDeletion ( )
{
2020-11-07 08:56:41 +08:00
var hoveredControlPoint = ControlPointVisualiser ? . Pieces . FirstOrDefault ( p = > p . IsHovered ) ;
2020-11-03 19:45:48 +08:00
if ( hoveredControlPoint = = null )
return false ;
hoveredControlPoint . IsSelected . Value = true ;
ControlPointVisualiser . DeleteSelected ( ) ;
return true ;
}
2019-09-27 17:45:22 +08:00
protected override void Update ( )
{
base . Update ( ) ;
2020-10-09 13:04:26 +08:00
if ( IsSelected )
BodyPiece . UpdateFrom ( HitObject ) ;
2019-09-27 17:45:22 +08:00
}
2020-10-09 13:05:00 +08:00
protected override void OnSelected ( )
{
2021-05-13 18:53:32 +08:00
AddInternal ( ControlPointVisualiser = new PathControlPointVisualiser ( HitObject , true )
2020-10-09 13:05:00 +08:00
{
RemoveControlPointsRequested = removeControlPoints
} ) ;
base . OnSelected ( ) ;
}
protected override void OnDeselected ( )
{
base . OnDeselected ( ) ;
// throw away frame buffers on deselection.
ControlPointVisualiser ? . Expire ( ) ;
2021-03-20 04:36:28 +08:00
ControlPointVisualiser = null ;
2020-10-09 13:05:00 +08:00
BodyPiece . RecyclePath ( ) ;
2019-09-27 17:45:22 +08:00
}
2019-11-12 12:32:31 +08:00
private Vector2 rightClickPosition ;
protected override bool OnMouseDown ( MouseDownEvent e )
{
2019-11-12 14:00:57 +08:00
switch ( e . Button )
{
case MouseButton . Right :
rightClickPosition = e . MouseDownPosition ;
return false ; // Allow right click to be handled by context menu
2020-11-05 12:58:41 +08:00
case MouseButton . Left :
if ( e . ControlPressed & & IsSelected )
{
placementControlPointIndex = addControlPoint ( e . MousePosition ) ;
return true ; // Stop input from being handled and modifying the selection
}
break ;
2019-11-12 14:00:57 +08:00
}
2019-11-12 12:32:31 +08:00
return false ;
}
2019-11-12 14:00:57 +08:00
private int? placementControlPointIndex ;
2020-04-09 21:00:56 +08:00
protected override bool OnDragStart ( DragStartEvent e )
{
if ( placementControlPointIndex ! = null )
{
changeHandler ? . BeginChange ( ) ;
return true ;
}
return false ;
}
2019-11-12 14:00:57 +08:00
2020-01-20 17:17:21 +08:00
protected override void OnDrag ( DragEvent e )
2019-11-12 12:32:31 +08:00
{
2019-11-12 14:00:57 +08:00
Debug . Assert ( placementControlPointIndex ! = null ) ;
2021-08-26 00:42:57 +08:00
HitObject . Path . ControlPoints [ placementControlPointIndex . Value ] . Position = e . MousePosition - HitObject . Position ;
2019-11-12 14:00:57 +08:00
}
2020-01-20 17:17:21 +08:00
protected override void OnDragEnd ( DragEndEvent e )
2019-11-12 14:00:57 +08:00
{
2020-04-09 21:00:56 +08:00
if ( placementControlPointIndex ! = null )
{
placementControlPointIndex = null ;
changeHandler ? . EndChange ( ) ;
}
2019-11-12 14:00:57 +08:00
}
2021-11-02 02:37:29 +08:00
protected override bool OnKeyDown ( KeyDownEvent e )
{
if ( ! IsSelected )
return false ;
if ( e . Key = = Key . F & & e . ControlPressed & & e . ShiftPressed )
{
convertToStream ( ) ;
return true ;
}
return false ;
}
2019-11-12 14:00:57 +08:00
private int addControlPoint ( Vector2 position )
{
position - = HitObject . Position ;
2019-11-12 12:32:31 +08:00
2019-11-12 13:37:07 +08:00
int insertionIndex = 0 ;
float minDistance = float . MaxValue ;
2019-11-12 12:32:31 +08:00
2019-12-11 17:52:38 +08:00
for ( int i = 0 ; i < controlPoints . Count - 1 ; i + + )
2019-11-12 13:37:07 +08:00
{
2021-08-26 00:42:57 +08:00
float dist = new Line ( controlPoints [ i ] . Position , controlPoints [ i + 1 ] . Position ) . DistanceToPoint ( position ) ;
2019-11-12 13:37:07 +08:00
if ( dist < minDistance )
{
insertionIndex = i + 1 ;
minDistance = dist ;
}
2019-11-12 12:32:31 +08:00
}
2019-11-12 13:37:07 +08:00
// Move the control points from the insertion index onwards to make room for the insertion
2021-08-26 00:42:57 +08:00
controlPoints . Insert ( insertionIndex , new PathControlPoint { Position = position } ) ;
2019-11-12 14:00:57 +08:00
return insertionIndex ;
2019-11-12 12:32:31 +08:00
}
2019-12-11 17:52:38 +08:00
private void removeControlPoints ( List < PathControlPoint > toRemove )
{
// Ensure that there are any points to be deleted
if ( toRemove . Count = = 0 )
return ;
foreach ( var c in toRemove )
{
// The first control point in the slider must have a type, so take it from the previous "first" one
// Todo: Should be handled within SliderPath itself
2021-08-26 00:42:57 +08:00
if ( c = = controlPoints [ 0 ] & & controlPoints . Count > 1 & & controlPoints [ 1 ] . Type = = null )
controlPoints [ 1 ] . Type = controlPoints [ 0 ] . Type ;
2019-12-11 17:52:38 +08:00
controlPoints . Remove ( c ) ;
}
// If there are 0 or 1 remaining control points, the slider is in a degenerate (single point) form and should be deleted
2021-05-13 18:53:32 +08:00
if ( controlPoints . Count < = 1 | | ! HitObject . Path . HasValidLength )
2019-12-11 17:52:38 +08:00
{
placementHandler ? . Delete ( HitObject ) ;
return ;
}
// The path will have a non-zero offset if the head is removed, but sliders don't support this behaviour since the head is positioned at the slider's position
// So the slider needs to be offset by this amount instead, and all control points offset backwards such that the path is re-positioned at (0, 0)
2021-08-26 00:42:57 +08:00
Vector2 first = controlPoints [ 0 ] . Position ;
2019-12-11 17:52:38 +08:00
foreach ( var c in controlPoints )
2021-08-26 00:42:57 +08:00
c . Position - = first ;
2019-12-11 17:52:38 +08:00
HitObject . Position + = first ;
}
2019-12-06 15:36:08 +08:00
private void updatePath ( )
2019-10-24 18:02:59 +08:00
{
2021-09-01 17:05:10 +08:00
HitObject . Path . ExpectedDistance . Value = composer ? . GetSnappedDistanceFromDistance ( HitObject , ( float ) HitObject . Path . CalculatedDistance ) ? ? ( float ) HitObject . Path . CalculatedDistance ;
2020-10-08 17:06:46 +08:00
editorBeatmap ? . Update ( HitObject ) ;
2019-10-24 18:02:59 +08:00
}
2021-11-02 02:37:29 +08:00
private void convertToStream ( )
{
if ( editorBeatmap = = null | | changeHandler = = null | | beatDivisor = = null )
return ;
var timingPoint = editorBeatmap . ControlPointInfo . TimingPointAt ( HitObject . StartTime ) ;
double streamSpacing = timingPoint . BeatLength / beatDivisor . Value ;
changeHandler . BeginChange ( ) ;
int i = 0 ;
double time = HitObject . StartTime ;
while ( ! Precision . DefinitelyBigger ( time , HitObject . GetEndTime ( ) , 1 ) )
{
Vector2 position = HitObject . Position + HitObject . Path . PositionAt ( ( time - HitObject . StartTime ) / HitObject . Duration ) ;
editorBeatmap . Add ( new HitCircle
{
StartTime = time ,
Position = position ,
NewCombo = i = = 0 & & HitObject . NewCombo
} ) ;
i + = 1 ;
time = HitObject . StartTime + i * streamSpacing ;
}
editorBeatmap . Remove ( HitObject ) ;
changeHandler . EndChange ( ) ;
}
2019-11-12 12:38:42 +08:00
public override MenuItem [ ] ContextMenuItems = > new MenuItem [ ]
2019-11-12 12:32:31 +08:00
{
2019-11-12 14:00:57 +08:00
new OsuMenuItem ( "Add control point" , MenuItemType . Standard , ( ) = > addControlPoint ( rightClickPosition ) ) ,
2021-11-02 02:37:29 +08:00
new OsuMenuItem ( "Convert to stream" , MenuItemType . Destructive , convertToStream ) ,
2019-11-12 12:32:31 +08:00
} ;
2021-05-24 16:15:57 +08:00
// Always refer to the drawable object's slider body so subsequent movement deltas are calculated with updated positions.
public override Vector2 ScreenSpaceSelectionPoint = > DrawableObject . SliderBody ? . ToScreenSpace ( DrawableObject . SliderBody . PathOffset )
? ? BodyPiece . ToScreenSpace ( BodyPiece . PathStartLocation ) ;
2019-10-01 18:33:24 +08:00
2020-11-03 19:45:48 +08:00
public override bool ReceivePositionalInputAt ( Vector2 screenSpacePos ) = >
2020-11-05 12:58:52 +08:00
BodyPiece . ReceivePositionalInputAt ( screenSpacePos ) | | ControlPointVisualiser ? . Pieces . Any ( p = > p . ReceivePositionalInputAt ( screenSpacePos ) ) = = true ;
2019-10-25 17:37:44 +08:00
2021-05-18 13:19:11 +08:00
protected virtual SliderCircleOverlay CreateCircleOverlay ( Slider slider , SliderPosition position ) = > new SliderCircleOverlay ( slider , position ) ;
2018-04-13 17:19:50 +08:00
}
}