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(); /// - /// The screen-space point that causes this to be selected via a drag. + /// The screen-space main point that causes this to be selected via a drag. /// public virtual Vector2 ScreenSpaceSelectionPoint => ScreenSpaceDrawQuad.Centre; + /// + /// Any points that should be used for snapping purposes in addition to . Exposed via . + /// + protected virtual Vector2[] ScreenSpaceAdditionalNodes => Array.Empty(); + + /// + /// The screen-space collection of base points on this that other objects can be snapped to. + /// The first element of this collection is + /// + public Vector2[] ScreenSpaceSnapPoints => ScreenSpaceAdditionalNodes.Prepend(ScreenSpaceSelectionPoint).ToArray(); + /// /// The screen-space quad that outlines this for selections. /// diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 77892f21e6..e4e67d10d7 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -439,7 +439,7 @@ namespace osu.Game.Screens.Edit.Compose.Components #region Selection Movement - private Vector2[] movementBlueprintOriginalPositions; + private Vector2[][] movementBlueprintsOriginalPositions; private SelectionBlueprint[] movementBlueprints; private bool isDraggingBlueprint; @@ -459,7 +459,7 @@ namespace osu.Game.Screens.Edit.Compose.Components // Movement is tracked from the blueprint of the earliest item, since it only makes sense to distance snap from that item movementBlueprints = SortForMovement(SelectionHandler.SelectedBlueprints).ToArray(); - movementBlueprintOriginalPositions = movementBlueprints.Select(m => m.ScreenSpaceSelectionPoint).ToArray(); + movementBlueprintsOriginalPositions = movementBlueprints.Select(m => m.ScreenSpaceSnapPoints).ToArray(); return true; } @@ -480,26 +480,15 @@ namespace osu.Game.Screens.Edit.Compose.Components if (movementBlueprints == null) return false; - Debug.Assert(movementBlueprintOriginalPositions != null); + Debug.Assert(movementBlueprintsOriginalPositions != null); Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition; if (snapProvider != null) { - // check for positional snap for every object in selection (for things like object-object snapping) - for (int i = 0; i < movementBlueprintOriginalPositions.Length; i++) + for (int i = 0; i < movementBlueprints.Length; i++) { - Vector2 originalPosition = movementBlueprintOriginalPositions[i]; - var testPosition = originalPosition + distanceTravelled; - - var positionalResult = snapProvider.FindSnappedPositionAndTime(testPosition, SnapType.NearbyObjects); - - if (positionalResult.ScreenSpacePosition == testPosition) continue; - - var delta = positionalResult.ScreenSpacePosition - movementBlueprints[i].ScreenSpaceSelectionPoint; - - // attempt to move the objects, and abort any time based snapping if we can. - if (SelectionHandler.HandleMovement(new MoveSelectionEvent(movementBlueprints[i], delta))) + if (checkSnappingBlueprintToNearbyObjects(movementBlueprints[i], distanceTravelled, movementBlueprintsOriginalPositions[i])) return true; } } @@ -508,7 +497,7 @@ namespace osu.Game.Screens.Edit.Compose.Components // item in the selection. // The final movement position, relative to movementBlueprintOriginalPosition. - Vector2 movePosition = movementBlueprintOriginalPositions.First() + distanceTravelled; + Vector2 movePosition = movementBlueprintsOriginalPositions.First().First() + distanceTravelled; // Retrieve a snapped position. var result = snapProvider?.FindSnappedPositionAndTime(movePosition, ~SnapType.NearbyObjects); @@ -521,6 +510,36 @@ namespace osu.Game.Screens.Edit.Compose.Components return ApplySnapResult(movementBlueprints, result); } + /// + /// Check for positional snap for given blueprint. + /// + /// The blueprint to check for snapping. + /// Distance travelled since start of dragging action. + /// The snap positions of blueprint before start of dragging action. + /// Whether an object to snap to was found. + private bool checkSnappingBlueprintToNearbyObjects(SelectionBlueprint blueprint, Vector2 distanceTravelled, Vector2[] originalPositions) + { + var currentPositions = blueprint.ScreenSpaceSnapPoints; + + for (int i = 0; i < originalPositions.Length; i++) + { + Vector2 originalPosition = originalPositions[i]; + var testPosition = originalPosition + distanceTravelled; + + var positionalResult = snapProvider.FindSnappedPositionAndTime(testPosition, SnapType.NearbyObjects); + + if (positionalResult.ScreenSpacePosition == testPosition) continue; + + var delta = positionalResult.ScreenSpacePosition - currentPositions[i]; + + // attempt to move the objects, and abort any time based snapping if we can. + if (SelectionHandler.HandleMovement(new MoveSelectionEvent(blueprint, delta))) + return true; + } + + return false; + } + protected virtual bool ApplySnapResult(SelectionBlueprint[] blueprints, SnapResult result) => SelectionHandler.HandleMovement(new MoveSelectionEvent(blueprints.First(), result.ScreenSpacePosition - blueprints.First().ScreenSpaceSelectionPoint)); @@ -533,7 +552,7 @@ namespace osu.Game.Screens.Edit.Compose.Components if (movementBlueprints == null) return false; - movementBlueprintOriginalPositions = null; + movementBlueprintsOriginalPositions = null; movementBlueprints = null; return true;