From 55a4c75e769432141a9f28038017b922f99f941f Mon Sep 17 00:00:00 2001 From: Olivier Schipper Date: Thu, 18 Sep 2025 21:26:49 +0200 Subject: [PATCH 1/3] Allow slider control points to snap to nearby objects and a bit of code cleanup to reduce code duplication with the slider head anchor snapping --- .../Components/PathControlPointVisualiser.cs | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index 5ae9b194be..b6b1185816 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -440,21 +440,26 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components Vector2 oldPosition = hitObject.Position; double oldStartTime = hitObject.StartTime; + SnapResult snapControlPoint(Vector2 newScreenSpacePosition, bool trySnapToDistanceGrid) + { + var result = positionSnapProvider?.TrySnapToNearbyObjects(newScreenSpacePosition, oldStartTime); + if (trySnapToDistanceGrid) + result ??= positionSnapProvider?.TrySnapToDistanceGrid(newScreenSpacePosition, limitedDistanceSnap.Value ? oldStartTime : null); + if (positionSnapProvider?.TrySnapToPositionGrid(result?.ScreenSpacePosition ?? newScreenSpacePosition, result?.Time ?? oldStartTime) is SnapResult gridSnapResult) + result = gridSnapResult; + result ??= new SnapResult(newScreenSpacePosition, oldStartTime); + return result; + } + if (selectedControlPoints.Contains(hitObject.Path.ControlPoints[0])) { // Special handling for selections containing head control point - the position of the hit object changes which means the snapped position and time have to be taken into account Vector2 newHeadPosition = Parent!.ToScreenSpace(e.MousePosition + (dragStartPositions[0] - dragStartPositions[draggedControlPointIndex])); - - var result = positionSnapProvider?.TrySnapToNearbyObjects(newHeadPosition, oldStartTime); - result ??= positionSnapProvider?.TrySnapToDistanceGrid(newHeadPosition, limitedDistanceSnap.Value ? oldStartTime : null); - if (positionSnapProvider?.TrySnapToPositionGrid(result?.ScreenSpacePosition ?? newHeadPosition, result?.Time ?? oldStartTime) is SnapResult gridSnapResult) - result = gridSnapResult; - result ??= new SnapResult(newHeadPosition, oldStartTime); - - Vector2 movementDelta = Parent!.ToLocalSpace(result.ScreenSpacePosition) - hitObject.Position; + var snapResult = snapControlPoint(newHeadPosition, true); + Vector2 movementDelta = Parent!.ToLocalSpace(snapResult.ScreenSpacePosition) - hitObject.Position; hitObject.Position += movementDelta; - hitObject.StartTime = result.Time ?? hitObject.StartTime; + hitObject.StartTime = snapResult.Time ?? hitObject.StartTime; for (int i = 1; i < hitObject.Path.ControlPoints.Count; i++) { @@ -469,9 +474,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components } else { - SnapResult result = positionSnapProvider?.TrySnapToPositionGrid(Parent!.ToScreenSpace(e.MousePosition)); - - Vector2 movementDelta = Parent!.ToLocalSpace(result?.ScreenSpacePosition ?? Parent!.ToScreenSpace(e.MousePosition)) - dragStartPositions[draggedControlPointIndex] - hitObject.Position; + Vector2 newControlPointPosition = Parent!.ToScreenSpace(e.MousePosition); + var snapResult = snapControlPoint(newControlPointPosition, false); + Vector2 movementDelta = Parent!.ToLocalSpace(snapResult.ScreenSpacePosition) - dragStartPositions[draggedControlPointIndex] - hitObject.Position; for (int i = 0; i < controlPoints.Count; ++i) { From 40cbe58220d434c4fe3c099b607af5076002a2c4 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 29 Sep 2025 14:23:19 +0200 Subject: [PATCH 2/3] Revert inline method for code abstraction --- .../Components/PathControlPointVisualiser.cs | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index b6b1185816..99002c2ef4 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -440,26 +440,21 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components Vector2 oldPosition = hitObject.Position; double oldStartTime = hitObject.StartTime; - SnapResult snapControlPoint(Vector2 newScreenSpacePosition, bool trySnapToDistanceGrid) - { - var result = positionSnapProvider?.TrySnapToNearbyObjects(newScreenSpacePosition, oldStartTime); - if (trySnapToDistanceGrid) - result ??= positionSnapProvider?.TrySnapToDistanceGrid(newScreenSpacePosition, limitedDistanceSnap.Value ? oldStartTime : null); - if (positionSnapProvider?.TrySnapToPositionGrid(result?.ScreenSpacePosition ?? newScreenSpacePosition, result?.Time ?? oldStartTime) is SnapResult gridSnapResult) - result = gridSnapResult; - result ??= new SnapResult(newScreenSpacePosition, oldStartTime); - return result; - } - if (selectedControlPoints.Contains(hitObject.Path.ControlPoints[0])) { // Special handling for selections containing head control point - the position of the hit object changes which means the snapped position and time have to be taken into account Vector2 newHeadPosition = Parent!.ToScreenSpace(e.MousePosition + (dragStartPositions[0] - dragStartPositions[draggedControlPointIndex])); - var snapResult = snapControlPoint(newHeadPosition, true); - Vector2 movementDelta = Parent!.ToLocalSpace(snapResult.ScreenSpacePosition) - hitObject.Position; + + var result = positionSnapProvider?.TrySnapToNearbyObjects(newHeadPosition, oldStartTime); + result ??= positionSnapProvider?.TrySnapToDistanceGrid(newHeadPosition, limitedDistanceSnap.Value ? oldStartTime : null); + if (positionSnapProvider?.TrySnapToPositionGrid(result?.ScreenSpacePosition ?? newHeadPosition, result?.Time ?? oldStartTime) is SnapResult gridSnapResult) + result = gridSnapResult; + result ??= new SnapResult(newHeadPosition, oldStartTime); + + Vector2 movementDelta = Parent!.ToLocalSpace(result.ScreenSpacePosition) - hitObject.Position; hitObject.Position += movementDelta; - hitObject.StartTime = snapResult.Time ?? hitObject.StartTime; + hitObject.StartTime = result.Time ?? hitObject.StartTime; for (int i = 1; i < hitObject.Path.ControlPoints.Count; i++) { @@ -475,8 +470,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components else { Vector2 newControlPointPosition = Parent!.ToScreenSpace(e.MousePosition); - var snapResult = snapControlPoint(newControlPointPosition, false); - Vector2 movementDelta = Parent!.ToLocalSpace(snapResult.ScreenSpacePosition) - dragStartPositions[draggedControlPointIndex] - hitObject.Position; + + var result = positionSnapProvider?.TrySnapToNearbyObjects(newControlPointPosition, oldStartTime); + if (positionSnapProvider?.TrySnapToPositionGrid(result?.ScreenSpacePosition ?? newControlPointPosition, result?.Time ?? oldStartTime) is SnapResult gridSnapResult) + result = gridSnapResult; + result ??= new SnapResult(newControlPointPosition, oldStartTime); + + Vector2 movementDelta = Parent!.ToLocalSpace(result.ScreenSpacePosition) - dragStartPositions[draggedControlPointIndex] - hitObject.Position; for (int i = 0; i < controlPoints.Count; ++i) { From d76dce76ec5a7d7329ec7f98d73464fd8f708952 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 29 Sep 2025 14:44:12 +0200 Subject: [PATCH 3/3] dont snap inherited bspline type control points to nearby objects --- .../Sliders/Components/PathControlPointVisualiser.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index 99002c2ef4..bff6701826 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -471,7 +471,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { Vector2 newControlPointPosition = Parent!.ToScreenSpace(e.MousePosition); - var result = positionSnapProvider?.TrySnapToNearbyObjects(newControlPointPosition, oldStartTime); + // Snapping inherited B-spline control points to nearby objects would be unintuitive, because snapping them does not equate to snapping the interpolated slider path. + bool shouldSnapToNearbyObjects = dragPathTypes[draggedControlPointIndex] is not null || + dragPathTypes[..draggedControlPointIndex].LastOrDefault(t => t is not null)?.Type != SplineType.BSpline; + + SnapResult result = null; + if (shouldSnapToNearbyObjects) + result = positionSnapProvider?.TrySnapToNearbyObjects(newControlPointPosition, oldStartTime); if (positionSnapProvider?.TrySnapToPositionGrid(result?.ScreenSpacePosition ?? newControlPointPosition, result?.Time ?? oldStartTime) is SnapResult gridSnapResult) result = gridSnapResult; result ??= new SnapResult(newControlPointPosition, oldStartTime);