diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs
index ecd840dda6..68a44eb2f8 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs
@@ -22,6 +22,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
///
public Vector2 PathStartLocation => body.PathOffset;
+ ///
+ /// Offset in absolute (local) coordinates from the end of the curve.
+ ///
+ public Vector2 PathEndLocation => body.PathEndOffset;
+
public SliderBodyPiece()
{
InternalChild = body = new ManualSliderBody
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
index a51c223785..b502839e22 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
@@ -409,6 +409,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
public override Vector2 ScreenSpaceSelectionPoint => DrawableObject.SliderBody?.ToScreenSpace(DrawableObject.SliderBody.PathOffset)
?? BodyPiece.ToScreenSpace(BodyPiece.PathStartLocation);
+ protected override Vector2[] ScreenSpaceAdditionalNodes => new[]
+ {
+ DrawableObject.SliderBody?.ToScreenSpace(DrawableObject.SliderBody.PathEndOffset) ?? BodyPiece.ToScreenSpace(BodyPiece.PathEndLocation)
+ };
+
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
BodyPiece.ReceivePositionalInputAt(screenSpacePos) || ControlPointVisualiser?.Pieces.Any(p => p.ReceivePositionalInputAt(screenSpacePos)) == true;
diff --git a/osu.Game.Rulesets.Osu/Skinning/SliderBody.cs b/osu.Game.Rulesets.Osu/Skinning/SliderBody.cs
index 283687adfd..e7885e65de 100644
--- a/osu.Game.Rulesets.Osu/Skinning/SliderBody.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/SliderBody.cs
@@ -31,6 +31,11 @@ namespace osu.Game.Rulesets.Osu.Skinning
///
public virtual Vector2 PathOffset => path.PositionInBoundingBox(path.Vertices[0]);
+ ///
+ /// Offset in absolute coordinates from the end of the curve.
+ ///
+ public virtual Vector2 PathEndOffset => path.PositionInBoundingBox(path.Vertices[^1]);
+
///
/// Used to colour the path.
///
diff --git a/osu.Game.Rulesets.Osu/Skinning/SnakingSliderBody.cs b/osu.Game.Rulesets.Osu/Skinning/SnakingSliderBody.cs
index f8ee465cd6..0b7acc1f47 100644
--- a/osu.Game.Rulesets.Osu/Skinning/SnakingSliderBody.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/SnakingSliderBody.cs
@@ -43,6 +43,8 @@ namespace osu.Game.Rulesets.Osu.Skinning
public override Vector2 PathOffset => snakedPathOffset;
+ public override Vector2 PathEndOffset => snakedPathEndOffset;
+
///
/// The top-left position of the path when fully snaked.
///
@@ -53,6 +55,11 @@ namespace osu.Game.Rulesets.Osu.Skinning
///
private Vector2 snakedPathOffset;
+ ///
+ /// The offset of the end of path from when fully snaked.
+ ///
+ private Vector2 snakedPathEndOffset;
+
private DrawableSlider drawableSlider = null!;
[BackgroundDependencyLoader]
@@ -109,6 +116,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
snakedPosition = Path.PositionInBoundingBox(Vector2.Zero);
snakedPathOffset = Path.PositionInBoundingBox(Path.Vertices[0]);
+ snakedPathEndOffset = Path.PositionInBoundingBox(Path.Vertices[^1]);
double lastSnakedStart = SnakedStart ?? 0;
double lastSnakedEnd = SnakedEnd ?? 0;
diff --git a/osu.Game/Rulesets/Edit/SelectionBlueprint.cs b/osu.Game/Rulesets/Edit/SelectionBlueprint.cs
index 4e0e45e0f5..3c878ffd33 100644
--- a/osu.Game/Rulesets/Edit/SelectionBlueprint.cs
+++ b/osu.Game/Rulesets/Edit/SelectionBlueprint.cs
@@ -4,6 +4,7 @@
#nullable disable
using System;
+using System.Linq;
using osu.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -125,10 +126,21 @@ namespace osu.Game.Rulesets.Edit
public virtual MenuItem[] ContextMenuItems => Array.Empty