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
2022-06-17 15:37:17 +08:00
#nullable disable
2019-12-11 17:52:38 +08:00
using System.Collections.Generic ;
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 ;
2022-08-18 07:29:03 +08:00
using osu.Game.Audio ;
2021-11-12 04:43:06 +08:00
using osu.Game.Beatmaps.ControlPoints ;
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 ;
2021-11-12 05:20:16 +08:00
using osu.Game.Rulesets.Objects.Types ;
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)]
2022-04-28 10:48:45 +08:00
private IDistanceSnapProvider snapProvider { get ; set ; }
2019-10-24 18:02:59 +08:00
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 > ( ) ;
2022-10-28 00:20:17 +08:00
private readonly BindableList < HitObject > selectedObjects = new BindableList < HitObject > ( ) ;
2020-11-20 21:43:10 +08:00
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 ) ;
2022-01-07 22:11:38 +08:00
pathVersion . BindValueChanged ( _ = > editorBeatmap ? . Update ( HitObject ) ) ;
2020-10-09 13:04:26 +08:00
BodyPiece . UpdateFrom ( HitObject ) ;
2022-10-27 13:40:14 +08:00
2022-10-28 00:20:17 +08:00
if ( editorBeatmap ! = null )
selectedObjects . BindTo ( editorBeatmap . SelectedHitObjects ) ;
2022-10-28 13:34:24 +08:00
selectedObjects . BindCollectionChanged ( ( _ , _ ) = > updateVisualDefinition ( ) , true ) ;
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 ;
}
2022-10-28 00:20:17 +08:00
private bool hasSingleObjectSelected = > editorBeatmap = = null | | selectedObjects . Count = = 1 ;
2022-10-27 13:52:46 +08:00
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
}
2022-10-27 13:40:14 +08:00
protected override bool OnHover ( HoverEvent e )
2020-10-09 13:05:00 +08:00
{
2022-10-27 13:40:14 +08:00
updateVisualDefinition ( ) ;
2022-10-27 13:52:46 +08:00
// In the case more than a single object is selected, block hover from arriving at sliders behind this one.
// Without doing this, the path visualisers of potentially hundreds of sliders will render, which is not only
// visually noisy but also functionally useless.
return ! hasSingleObjectSelected ;
2022-10-27 13:40:14 +08:00
}
protected override void OnHoverLost ( HoverLostEvent e )
{
updateVisualDefinition ( ) ;
base . OnHoverLost ( e ) ;
}
2020-10-09 13:05:00 +08:00
2022-10-27 13:40:14 +08:00
protected override void OnSelected ( )
{
updateVisualDefinition ( ) ;
2020-10-09 13:05:00 +08:00
base . OnSelected ( ) ;
}
protected override void OnDeselected ( )
{
base . OnDeselected ( ) ;
2022-10-27 13:40:14 +08:00
updateVisualDefinition ( ) ;
2020-10-09 13:05:00 +08:00
BodyPiece . RecyclePath ( ) ;
2019-09-27 17:45:22 +08:00
}
2022-10-27 13:40:14 +08:00
private void updateVisualDefinition ( )
{
// To reduce overhead of drawing these blueprints, only add extra detail when hovered or when only this slider is selected.
if ( IsSelected & & ( hasSingleObjectSelected | | IsHovered ) )
{
if ( ControlPointVisualiser = = null )
{
AddInternal ( ControlPointVisualiser = new PathControlPointVisualiser ( HitObject , true )
{
RemoveControlPointsRequested = removeControlPoints ,
SplitControlPointsRequested = splitControlPoints
} ) ;
}
}
else
{
ControlPointVisualiser ? . Expire ( ) ;
ControlPointVisualiser = null ;
}
}
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 )
{
2021-12-23 16:19:21 +08:00
changeHandler ? . BeginChange ( ) ;
2021-12-22 17:03:58 +08:00
placementControlPoint = addControlPoint ( e . MousePosition ) ;
ControlPointVisualiser ? . SetSelectionTo ( placementControlPoint ) ;
2020-11-05 12:58:41 +08:00
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 ;
}
2021-12-22 17:03:58 +08:00
[CanBeNull]
private PathControlPoint placementControlPoint ;
2019-11-12 14:00:57 +08:00
2021-12-23 16:19:21 +08:00
protected override bool OnDragStart ( DragStartEvent e ) = > placementControlPoint ! = null ;
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
{
2021-12-22 17:28:35 +08:00
if ( placementControlPoint ! = null )
2022-09-20 03:33:38 +08:00
{
var result = snapProvider ? . FindSnappedPositionAndTime ( ToScreenSpace ( e . MousePosition ) ) ;
2022-09-22 00:53:25 +08:00
placementControlPoint . Position = ToLocalSpace ( result ? . ScreenSpacePosition ? ? ToScreenSpace ( e . MousePosition ) ) - HitObject . Position ;
2022-09-20 03:33:38 +08:00
}
2019-11-12 14:00:57 +08:00
}
2021-12-23 16:09:14 +08:00
protected override void OnMouseUp ( MouseUpEvent e )
2019-11-12 14:00:57 +08:00
{
2021-12-22 17:03:58 +08:00
if ( placementControlPoint ! = null )
2020-04-09 21:00:56 +08:00
{
2021-12-22 17:03:58 +08:00
placementControlPoint = null ;
2020-04-09 21:00:56 +08:00
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 ;
}
2021-12-22 17:03:58 +08:00
private PathControlPoint addControlPoint ( Vector2 position )
2019-11-12 14:00:57 +08:00
{
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
2021-12-22 17:03:58 +08:00
var pathControlPoint = new PathControlPoint { Position = position } ;
2019-11-12 13:37:07 +08:00
// Move the control points from the insertion index onwards to make room for the insertion
2021-12-22 17:03:58 +08:00
controlPoints . Insert ( insertionIndex , pathControlPoint ) ;
2019-11-12 14:00:57 +08:00
2022-04-28 10:48:45 +08:00
HitObject . SnapTo ( snapProvider ) ;
2022-01-07 22:11:38 +08:00
2021-12-22 17:03:58 +08:00
return pathControlPoint ;
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 ) ;
}
2022-01-07 22:11:38 +08:00
// Snap the slider to the current beat divisor before checking length validity.
2022-04-28 10:48:45 +08:00
HitObject . SnapTo ( snapProvider ) ;
2022-01-07 22:11:38 +08:00
// If there are 0 or 1 remaining control points, or the slider has an invalid length, it is in a degenerate 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 ;
}
2022-08-18 07:29:03 +08:00
// 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)
Vector2 first = controlPoints [ 0 ] . Position ;
foreach ( var c in controlPoints )
c . Position - = first ;
HitObject . Position + = first ;
}
2022-08-26 18:09:03 +08:00
private void splitControlPoints ( List < PathControlPoint > controlPointsToSplitAt )
2022-08-18 07:29:03 +08:00
{
2022-08-24 05:28:50 +08:00
// Arbitrary gap in milliseconds to put between split slider pieces
const double split_gap = 100 ;
2022-08-18 07:29:03 +08:00
// Ensure that there are any points to be split
2022-08-26 18:09:03 +08:00
if ( controlPointsToSplitAt . Count = = 0 )
2022-08-18 07:29:03 +08:00
return ;
2022-08-26 17:55:20 +08:00
editorBeatmap . SelectedHitObjects . Clear ( ) ;
2022-08-26 18:09:03 +08:00
foreach ( var splitPoint in controlPointsToSplitAt )
2022-08-18 07:29:03 +08:00
{
2022-08-26 18:09:03 +08:00
if ( splitPoint = = controlPoints [ 0 ] | | splitPoint = = controlPoints [ ^ 1 ] | | splitPoint . Type is null )
2022-08-18 07:29:03 +08:00
continue ;
// Split off the section of slider before this control point so the remaining control points to split are in the latter part of the slider.
2022-08-26 18:09:03 +08:00
int index = controlPoints . IndexOf ( splitPoint ) ;
2022-08-18 07:29:03 +08:00
2022-08-26 18:09:03 +08:00
if ( index < = 0 )
2022-08-18 07:29:03 +08:00
continue ;
2022-08-26 18:09:03 +08:00
// Extract the split portion and remove from the original slider.
var splitControlPoints = controlPoints . Take ( index + 1 ) . ToList ( ) ;
controlPoints . RemoveRange ( 0 , index ) ;
2022-08-18 07:29:03 +08:00
// Turn the control points which were split off into a new slider.
var samplePoint = ( SampleControlPoint ) HitObject . SampleControlPoint . DeepClone ( ) ;
var difficultyPoint = ( DifficultyControlPoint ) HitObject . DifficultyControlPoint . DeepClone ( ) ;
var newSlider = new Slider
{
StartTime = HitObject . StartTime ,
Position = HitObject . Position + splitControlPoints [ 0 ] . Position ,
NewCombo = HitObject . NewCombo ,
SampleControlPoint = samplePoint ,
DifficultyControlPoint = difficultyPoint ,
2022-08-24 05:28:50 +08:00
LegacyLastTickOffset = HitObject . LegacyLastTickOffset ,
2022-08-18 07:29:03 +08:00
Samples = HitObject . Samples . Select ( s = > s . With ( ) ) . ToList ( ) ,
RepeatCount = HitObject . RepeatCount ,
NodeSamples = HitObject . NodeSamples . Select ( n = > ( IList < HitSampleInfo > ) n . Select ( s = > s . With ( ) ) . ToList ( ) ) . ToList ( ) ,
Path = new SliderPath ( splitControlPoints . Select ( o = > new PathControlPoint ( o . Position - splitControlPoints [ 0 ] . Position , o = = splitControlPoints [ ^ 1 ] ? null : o . Type ) ) . ToArray ( ) )
} ;
2022-08-20 00:29:01 +08:00
// Increase the start time of the slider before adding the new slider so the new slider is immediately inserted at the correct index and internal state remains valid.
2022-08-24 05:28:50 +08:00
HitObject . StartTime + = split_gap ;
2022-08-19 07:10:54 +08:00
2022-08-18 07:29:03 +08:00
editorBeatmap . Add ( newSlider ) ;
HitObject . NewCombo = false ;
HitObject . Path . ExpectedDistance . Value - = newSlider . Path . CalculatedDistance ;
2022-08-24 05:28:50 +08:00
HitObject . StartTime + = newSlider . SpanDuration ;
2022-08-18 07:29:03 +08:00
// In case the remainder of the slider has no length left over, give it length anyways so we don't get a 0 length slider.
if ( HitObject . Path . ExpectedDistance . Value < = Precision . DOUBLE_EPSILON )
{
HitObject . Path . ExpectedDistance . Value = null ;
}
}
2022-08-26 18:09:03 +08:00
// Once all required pieces have been split off, the original slider has the final split.
// As a final step, we must reset its control points to have an origin of (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 ;
}
2021-11-02 02:37:29 +08:00
private void convertToStream ( )
{
2022-08-17 12:35:44 +08:00
if ( editorBeatmap = = null | | beatDivisor = = null )
2021-11-02 02:37:29 +08:00
return ;
var timingPoint = editorBeatmap . ControlPointInfo . TimingPointAt ( HitObject . StartTime ) ;
double streamSpacing = timingPoint . BeatLength / beatDivisor . Value ;
2022-08-17 12:35:44 +08:00
changeHandler ? . BeginChange ( ) ;
2021-11-02 02:37:29 +08:00
int i = 0 ;
double time = HitObject . StartTime ;
while ( ! Precision . DefinitelyBigger ( time , HitObject . GetEndTime ( ) , 1 ) )
{
2021-11-12 05:20:16 +08:00
// positionWithRepeats is a fractional number in the range of [0, HitObject.SpanCount()]
// and indicates how many fractional spans of a slider have passed up to time.
double positionWithRepeats = ( time - HitObject . StartTime ) / HitObject . Duration * HitObject . SpanCount ( ) ;
double pathPosition = positionWithRepeats - ( int ) positionWithRepeats ;
// every second span is in the reverse direction - need to reverse the path position.
2022-10-09 05:43:23 +08:00
if ( positionWithRepeats % 2 > = 1 )
2021-11-12 05:20:16 +08:00
pathPosition = 1 - pathPosition ;
Vector2 position = HitObject . Position + HitObject . Path . PositionAt ( pathPosition ) ;
2021-11-12 04:43:06 +08:00
var samplePoint = ( SampleControlPoint ) HitObject . SampleControlPoint . DeepClone ( ) ;
samplePoint . Time = time ;
2021-11-02 02:37:29 +08:00
editorBeatmap . Add ( new HitCircle
{
StartTime = time ,
Position = position ,
2021-11-12 04:43:06 +08:00
NewCombo = i = = 0 & & HitObject . NewCombo ,
SampleControlPoint = samplePoint ,
Samples = HitObject . HeadCircle . Samples . Select ( s = > s . With ( ) ) . ToList ( )
2021-11-02 02:37:29 +08:00
} ) ;
i + = 1 ;
time = HitObject . StartTime + i * streamSpacing ;
}
editorBeatmap . Remove ( HitObject ) ;
2022-08-17 12:35:44 +08:00
changeHandler ? . EndChange ( ) ;
2021-11-02 02:37:29 +08:00
}
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
}
}