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
2024-07-04 02:33:00 +08:00
using System ;
2019-12-11 17:52:38 +08:00
using System.Collections.Generic ;
2024-08-27 12:13:22 +08:00
using System.Diagnostics ;
2020-11-03 19:45:48 +08:00
using System.Linq ;
2019-10-24 18:02:59 +08:00
using osu.Framework.Allocation ;
2019-12-06 15:36:08 +08:00
using osu.Framework.Bindables ;
2024-06-20 06:02:43 +08:00
using osu.Framework.Caching ;
2018-02-20 17:01:45 +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 ;
2024-07-18 17:20:31 +08:00
using osu.Framework.Input.Bindings ;
2019-11-12 12:32:31 +08:00
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 ;
2024-06-19 19:18:56 +08:00
using osu.Game.Configuration ;
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-02-20 17:01:45 +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-02-20 17:01:45 +08:00
{
2019-11-12 12:38:42 +08:00
public partial class SliderSelectionBlueprint : OsuSelectionBlueprint < Slider >
2018-02-20 17:01:45 +08:00
{
2021-05-24 16:15:57 +08:00
protected new DrawableSlider DrawableObject = > ( DrawableSlider ) base . DrawableObject ;
2024-08-27 12:18:55 +08:00
protected SliderBodyPiece BodyPiece { get ; private set ; } = null ! ;
protected SliderCircleOverlay HeadOverlay { get ; private set ; } = null ! ;
protected SliderCircleOverlay TailOverlay { get ; private set ; } = null ! ;
2021-03-20 05:44:31 +08:00
2024-08-27 12:18:55 +08:00
protected PathControlPointVisualiser < Slider > ? ControlPointVisualiser { get ; private set ; }
2020-10-09 13:05:00 +08:00
2024-08-27 12:18:55 +08:00
[Resolved]
private IDistanceSnapProvider ? distanceSnapProvider { get ; set ; }
2019-10-24 18:02:59 +08:00
2024-08-27 12:18:55 +08:00
[Resolved]
private IPlacementHandler ? placementHandler { get ; set ; }
2019-12-11 17:52:38 +08:00
2024-08-27 12:18:55 +08:00
[Resolved]
private EditorBeatmap ? editorBeatmap { get ; set ; }
2020-04-09 18:54:58 +08:00
2024-08-27 12:18:55 +08:00
[Resolved]
private IEditorChangeHandler ? changeHandler { get ; set ; }
2020-04-09 21:00:56 +08:00
2024-08-27 12:18:55 +08:00
[Resolved]
private BindableBeatDivisor ? beatDivisor { get ; set ; }
2021-11-02 02:37:29 +08:00
2024-08-27 12:18:55 +08:00
private PathControlPoint ? placementControlPoint ;
2024-08-27 12:13:22 +08:00
2024-07-04 02:17:39 +08:00
public override Quad SelectionQuad
{
get
{
var result = BodyPiece . ScreenSpaceDrawQuad . AABBFloat ;
result = RectangleF . Union ( result , HeadOverlay . VisibleQuad ) ;
result = RectangleF . Union ( result , TailOverlay . VisibleQuad ) ;
2024-06-18 16:28:36 +08:00
if ( ControlPointVisualiser ! = null )
{
foreach ( var piece in ControlPointVisualiser . Pieces )
result = RectangleF . Union ( result , piece . ScreenSpaceDrawQuad . AABBFloat ) ;
}
2024-07-04 02:17:39 +08:00
return result ;
}
}
2021-01-18 15:57:36 +08:00
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 > ( ) ;
2024-06-19 19:18:56 +08:00
private readonly Bindable < bool > showHitMarkers = new Bindable < bool > ( ) ;
2020-11-20 21:43:10 +08:00
2024-06-20 06:02:43 +08:00
// Cached slider path which ignored the expected distance value.
private readonly Cached < SliderPath > fullPathCache = new Cached < SliderPath > ( ) ;
2024-08-27 12:13:22 +08:00
private Vector2 lastRightClickPosition ;
2021-05-13 18:53:32 +08:00
public SliderSelectionBlueprint ( Slider slider )
2018-02-20 17:01:45 +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]
2024-06-19 19:18:56 +08:00
private void load ( OsuConfigManager config )
2020-10-09 13:05:00 +08:00
{
2018-02-20 17:01:45 +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 ) ,
2024-06-20 06:17:16 +08:00
TailOverlay = CreateCircleOverlay ( HitObject , SliderPosition . End ) ,
2018-02-20 17:01:45 +08:00
} ;
2024-07-04 02:17:39 +08:00
2024-08-27 12:13:22 +08:00
// tail will always have a non-null end drag marker.
Debug . Assert ( TailOverlay . EndDragMarker ! = null ) ;
TailOverlay . EndDragMarker . StartDrag + = startAdjustingLength ;
2024-07-04 02:17:39 +08:00
TailOverlay . EndDragMarker . Drag + = adjustLength ;
TailOverlay . EndDragMarker . EndDrag + = endAdjustLength ;
2024-07-04 02:40:25 +08:00
2024-06-19 19:18:56 +08:00
config . BindWith ( OsuSetting . EditorShowHitMarkers , showHitMarkers ) ;
2018-02-20 17:01:45 +08:00
}
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 ) ;
2024-06-20 06:02:43 +08:00
controlPoints . CollectionChanged + = ( _ , _ ) = > fullPathCache . Invalidate ( ) ;
2020-11-20 21:43:10 +08:00
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 ) ;
2024-06-19 19:18:56 +08:00
showHitMarkers . BindValueChanged ( _ = >
{
if ( ! showHitMarkers . Value )
DrawableObject . RestoreHitAnimations ( ) ;
} ) ;
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 ;
2024-08-27 12:18:55 +08:00
ControlPointVisualiser ? . DeleteSelected ( ) ;
2020-11-03 19:45:48 +08:00
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 ) ;
2024-06-19 19:18:56 +08:00
if ( showHitMarkers . Value )
DrawableObject . SuppressHitAnimations ( ) ;
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-11-04 18:36:59 +08:00
return base . OnHover ( e ) ;
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 ( )
{
2022-11-05 02:48:19 +08:00
// To reduce overhead of drawing these blueprints, only add extra detail when only this slider is selected.
2022-11-04 19:47:49 +08:00
if ( IsSelected & & selectedObjects . Count < 2 )
2022-10-27 13:40:14 +08:00
{
if ( ControlPointVisualiser = = null )
{
2022-11-03 19:25:23 +08:00
AddInternal ( ControlPointVisualiser = new PathControlPointVisualiser < Slider > ( HitObject , true )
2022-10-27 13:40:14 +08:00
{
RemoveControlPointsRequested = removeControlPoints ,
SplitControlPointsRequested = splitControlPoints
} ) ;
}
}
else
{
ControlPointVisualiser ? . Expire ( ) ;
ControlPointVisualiser = null ;
}
}
2019-11-12 12:32:31 +08:00
protected override bool OnMouseDown ( MouseDownEvent e )
{
2019-11-12 14:00:57 +08:00
switch ( e . Button )
{
case MouseButton . Right :
2024-08-27 12:13:22 +08:00
lastRightClickPosition = e . MouseDownPosition ;
2019-11-12 14:00:57 +08:00
return false ; // Allow right click to be handled by context menu
2020-11-05 12:58:41 +08:00
case MouseButton . Left :
2024-06-20 06:02:43 +08:00
2024-02-06 07:28:39 +08:00
// If there's more than two objects selected, ctrl+click should deselect
if ( e . ControlPressed & & IsSelected & & selectedObjects . Count < 2 )
2020-11-05 12:58:41 +08:00
{
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 ;
}
2024-08-27 12:13:22 +08:00
#region Length Adjustment ( independent of path nodes )
2024-07-04 02:17:39 +08:00
private Vector2 lengthAdjustMouseOffset ;
2024-07-22 17:34:07 +08:00
private double oldDuration ;
2024-07-22 17:58:53 +08:00
private double oldVelocityMultiplier ;
2024-07-22 17:34:07 +08:00
private double desiredDistance ;
private bool isAdjustingLength ;
private bool adjustVelocityMomentary ;
2024-07-04 02:17:39 +08:00
private void startAdjustingLength ( DragStartEvent e )
{
2024-07-22 17:34:07 +08:00
isAdjustingLength = true ;
adjustVelocityMomentary = e . ShiftPressed ;
2024-07-04 02:17:39 +08:00
lengthAdjustMouseOffset = ToLocalSpace ( e . ScreenSpaceMouseDownPosition ) - HitObject . Position - HitObject . Path . PositionAt ( 1 ) ;
2024-07-22 17:34:07 +08:00
oldDuration = HitObject . Path . Distance / HitObject . SliderVelocityMultiplier ;
2024-07-22 17:58:53 +08:00
oldVelocityMultiplier = HitObject . SliderVelocityMultiplier ;
2024-07-04 02:17:39 +08:00
changeHandler ? . BeginChange ( ) ;
}
2024-06-20 06:02:43 +08:00
private void endAdjustLength ( )
{
trimExcessControlPoints ( HitObject . Path ) ;
changeHandler ? . EndChange ( ) ;
2024-07-22 17:34:07 +08:00
isAdjustingLength = false ;
2024-06-20 06:02:43 +08:00
}
2024-07-22 17:34:07 +08:00
private void adjustLength ( MouseEvent e ) = > adjustLength ( findClosestPathDistance ( e ) , e . ShiftPressed ) ;
private void adjustLength ( double proposedDistance , bool adjustVelocity )
2024-06-20 06:02:43 +08:00
{
2024-07-22 17:34:07 +08:00
desiredDistance = proposedDistance ;
2024-07-22 17:58:53 +08:00
double proposedVelocity = oldVelocityMultiplier ;
2024-06-20 06:02:43 +08:00
2024-07-22 17:34:07 +08:00
if ( adjustVelocity )
{
proposedVelocity = proposedDistance / oldDuration ;
2024-07-22 17:58:53 +08:00
proposedDistance = MathHelper . Clamp ( proposedDistance , 0.1 * oldDuration , 10 * oldDuration ) ;
}
else
{
double minDistance = distanceSnapProvider ? . GetBeatSnapDistanceAt ( HitObject , false ) * oldVelocityMultiplier ? ? 1 ;
2024-08-21 18:28:56 +08:00
// Add a small amount to the proposed distance to make it easier to snap to the full length of the slider.
proposedDistance = distanceSnapProvider ? . FindSnappedDistance ( HitObject , ( float ) proposedDistance + 1 ) ? ? proposedDistance ;
2024-07-22 17:58:53 +08:00
proposedDistance = MathHelper . Clamp ( proposedDistance , minDistance , HitObject . Path . CalculatedDistance ) ;
2024-07-22 17:34:07 +08:00
}
2024-06-20 06:02:43 +08:00
2024-07-22 17:34:07 +08:00
if ( Precision . AlmostEquals ( proposedDistance , HitObject . Path . Distance ) & & Precision . AlmostEquals ( proposedVelocity , HitObject . SliderVelocityMultiplier ) )
2024-07-04 02:17:39 +08:00
return ;
2024-06-20 06:02:43 +08:00
2024-07-22 17:34:07 +08:00
HitObject . SliderVelocityMultiplier = proposedVelocity ;
2024-06-20 06:02:43 +08:00
HitObject . Path . ExpectedDistance . Value = proposedDistance ;
editorBeatmap ? . Update ( HitObject ) ;
}
/// <summary>
/// Trims control points from the end of the slider path which are not required to reach the expected end of the slider.
/// </summary>
/// <param name="sliderPath">The slider path to trim control points of.</param>
private void trimExcessControlPoints ( SliderPath sliderPath )
{
if ( ! sliderPath . ExpectedDistance . Value . HasValue )
return ;
double [ ] segmentEnds = sliderPath . GetSegmentEnds ( ) . ToArray ( ) ;
int segmentIndex = 0 ;
for ( int i = 1 ; i < sliderPath . ControlPoints . Count - 1 ; i + + )
{
if ( ! sliderPath . ControlPoints [ i ] . Type . HasValue ) continue ;
if ( Precision . AlmostBigger ( segmentEnds [ segmentIndex ] , 1 , 1E-3 ) )
{
sliderPath . ControlPoints . RemoveRange ( i + 1 , sliderPath . ControlPoints . Count - i - 1 ) ;
sliderPath . ControlPoints [ ^ 1 ] . Type = null ;
break ;
}
segmentIndex + + ;
}
}
/// <summary>
/// Finds the expected distance value for which the slider end is closest to the mouse position.
/// </summary>
2024-07-04 02:17:39 +08:00
private double findClosestPathDistance ( MouseEvent e )
2024-06-20 06:02:43 +08:00
{
const double step1 = 10 ;
const double step2 = 0.1 ;
2024-07-04 02:33:00 +08:00
const double longer_distance_bias = 0.01 ;
2024-06-20 06:02:43 +08:00
2024-07-04 02:17:39 +08:00
var desiredPosition = ToLocalSpace ( e . ScreenSpaceMousePosition ) - HitObject . Position - lengthAdjustMouseOffset ;
2024-06-20 06:02:43 +08:00
if ( ! fullPathCache . IsValid )
fullPathCache . Value = new SliderPath ( HitObject . Path . ControlPoints . ToArray ( ) ) ;
// Do a linear search to find the closest point on the path to the mouse position.
double bestValue = 0 ;
double minDistance = double . MaxValue ;
for ( double d = 0 ; d < = fullPathCache . Value . CalculatedDistance ; d + = step1 )
{
double t = d / fullPathCache . Value . CalculatedDistance ;
2024-07-04 02:33:00 +08:00
double dist = Vector2 . Distance ( fullPathCache . Value . PositionAt ( t ) , desiredPosition ) - d * longer_distance_bias ;
2024-06-20 06:02:43 +08:00
if ( dist > = minDistance ) continue ;
minDistance = dist ;
bestValue = d ;
}
// Do another linear search to fine-tune the result.
2024-07-04 02:33:00 +08:00
double maxValue = Math . Min ( bestValue + step1 , fullPathCache . Value . CalculatedDistance ) ;
for ( double d = bestValue - step1 ; d < = maxValue ; d + = step2 )
2024-06-20 06:02:43 +08:00
{
double t = d / fullPathCache . Value . CalculatedDistance ;
2024-07-04 02:33:00 +08:00
double dist = Vector2 . Distance ( fullPathCache . Value . PositionAt ( t ) , desiredPosition ) - d * longer_distance_bias ;
2024-06-20 06:02:43 +08:00
if ( dist > = minDistance ) continue ;
minDistance = dist ;
bestValue = d ;
}
return bestValue ;
}
2024-08-27 12:13:22 +08:00
#endregion
2019-11-12 14:00:57 +08:00
2023-11-23 08:26:31 +08:00
protected override bool OnDragStart ( DragStartEvent e )
{
if ( placementControlPoint = = null )
return base . OnDragStart ( e ) ;
ControlPointVisualiser ? . DragStarted ( placementControlPoint ) ;
return true ;
}
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
{
2023-11-23 08:26:31 +08:00
base . OnDrag ( e ) ;
if ( placementControlPoint ! = null )
ControlPointVisualiser ? . DragInProgress ( e ) ;
}
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
{
2023-11-23 13:00:42 +08:00
if ( IsDragged )
ControlPointVisualiser ? . DragEnded ( ) ;
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 ;
}
2024-07-22 17:34:07 +08:00
if ( isAdjustingLength & & e . ShiftPressed ! = adjustVelocityMomentary )
{
adjustVelocityMomentary = e . ShiftPressed ;
adjustLength ( desiredDistance , adjustVelocityMomentary ) ;
return true ;
}
2021-11-02 02:37:29 +08:00
return false ;
}
2024-07-22 17:34:07 +08:00
protected override void OnKeyUp ( KeyUpEvent e )
{
if ( ! IsSelected | | ! isAdjustingLength | | e . ShiftPressed = = adjustVelocityMomentary ) return ;
adjustVelocityMomentary = e . ShiftPressed ;
adjustLength ( desiredDistance , adjustVelocityMomentary ) ;
}
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
2024-01-14 05:39:09 +08:00
ControlPointVisualiser ? . EnsureValidPathTypes ( ) ;
2023-10-19 15:56:40 +08:00
HitObject . SnapTo ( distanceSnapProvider ) ;
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 ) ;
}
2024-01-14 05:39:09 +08:00
ControlPointVisualiser ? . EnsureValidPathTypes ( ) ;
2022-01-07 22:11:38 +08:00
// Snap the slider to the current beat divisor before checking length validity.
2023-10-19 15:56:40 +08:00
HitObject . SnapTo ( distanceSnapProvider ) ;
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
{
2024-08-27 12:18:55 +08:00
if ( editorBeatmap = = null )
return ;
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
{
2024-04-03 01:50:39 +08:00
if ( splitPoint = = controlPoints [ 0 ] | | splitPoint = = controlPoints [ ^ 1 ] | | splitPoint . Type = = 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
var newSlider = new Slider
{
StartTime = HitObject . StartTime ,
Position = HitObject . Position + splitControlPoints [ 0 ] . Position ,
NewCombo = HitObject . NewCombo ,
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
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 ,
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
{
2024-04-25 15:01:47 +08:00
new OsuMenuItem ( "Add control point" , MenuItemType . Standard , ( ) = >
{
changeHandler ? . BeginChange ( ) ;
2024-08-27 12:13:22 +08:00
addControlPoint ( lastRightClickPosition ) ;
2024-04-25 15:01:47 +08:00
changeHandler ? . EndChange ( ) ;
2024-07-18 17:20:31 +08:00
} )
{
Hotkey = new Hotkey ( new KeyCombination ( InputKey . Control , InputKey . MouseLeft ) )
} ,
new OsuMenuItem ( "Convert to stream" , MenuItemType . Destructive , convertToStream )
{
Hotkey = new Hotkey ( new KeyCombination ( InputKey . Control , InputKey . Shift , InputKey . F ) )
} ,
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
2023-01-23 13:13:46 +08:00
protected override Vector2 [ ] ScreenSpaceAdditionalNodes = > new [ ]
{
DrawableObject . SliderBody ? . ToScreenSpace ( DrawableObject . SliderBody . PathEndOffset ) ? ? BodyPiece . ToScreenSpace ( BodyPiece . PathEndLocation )
2023-01-19 04:34:23 +08:00
} ;
2023-01-11 04:20:09 +08:00
2024-02-20 01:26:15 +08:00
public override bool ReceivePositionalInputAt ( Vector2 screenSpacePos )
{
if ( BodyPiece . ReceivePositionalInputAt ( screenSpacePos ) )
return true ;
if ( ControlPointVisualiser = = null )
return false ;
foreach ( var p in ControlPointVisualiser . Pieces )
{
if ( p . ReceivePositionalInputAt ( screenSpacePos ) )
return true ;
}
return false ;
}
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-02-20 17:01:45 +08:00
}
}