diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/CatchPlacementBlueprintTestScene.cs b/osu.Game.Rulesets.Catch.Tests/Editor/CatchPlacementBlueprintTestScene.cs index 0578010c25..a327e6d4c9 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/CatchPlacementBlueprintTestScene.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/CatchPlacementBlueprintTestScene.cs @@ -12,7 +12,6 @@ using osu.Framework.Testing; using osu.Framework.Timing; using osu.Game.Rulesets.Catch.Edit.Blueprints.Components; using osu.Game.Rulesets.Catch.Objects.Drawables; -using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Tests.Visual; @@ -71,11 +70,11 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor contentContainer.Playfield.HitObjectContainer.Add(hitObject); } - protected override SnapResult SnapForBlueprint(HitObjectPlacementBlueprint blueprint) + protected override void UpdatePlacementTimeAndPosition() { - var result = base.SnapForBlueprint(blueprint); - result.Time = Math.Round(HitObjectContainer.TimeAtScreenSpacePosition(result.ScreenSpacePosition) / TIME_SNAP) * TIME_SNAP; - return result; + var position = InputManager.CurrentState.Mouse.Position; + double time = Math.Round(HitObjectContainer.TimeAtScreenSpacePosition(position) / TIME_SNAP) * TIME_SNAP; + CurrentBlueprint.UpdateTimeAndPosition(position, time); } } } diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs index 6902f78172..971c98cafd 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs @@ -7,6 +7,7 @@ using osu.Framework.Utils; using osu.Game.Rulesets.Catch.Edit.Blueprints.Components; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Edit; +using osuTK; using osuTK.Input; namespace osu.Game.Rulesets.Catch.Edit.Blueprints @@ -59,11 +60,13 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints return base.OnMouseDown(e); } - public override void UpdateTimeAndPosition(SnapResult result) + public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) { - base.UpdateTimeAndPosition(result); + var result = Composer?.FindSnappedPositionAndTime(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime); - if (!(result.Time is double time)) return; + base.UpdateTimeAndPosition(result.ScreenSpacePosition, result.Time ?? fallbackTime); + + if (!(result.Time is double time)) return result; switch (PlacementActive) { @@ -78,6 +81,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints HitObject.StartTime = Math.Min(placementStartTime, placementEndTime); HitObject.EndTime = Math.Max(placementStartTime, placementEndTime); + return result; } } } diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchPlacementBlueprint.cs index aa862375c5..90b7fa172c 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchPlacementBlueprint.cs @@ -9,7 +9,7 @@ using osu.Game.Rulesets.UI.Scrolling; namespace osu.Game.Rulesets.Catch.Edit.Blueprints { - public partial class CatchPlacementBlueprint : HitObjectPlacementBlueprint + public abstract partial class CatchPlacementBlueprint : HitObjectPlacementBlueprint where THitObject : CatchHitObject, new() { protected new THitObject HitObject => (THitObject)base.HitObject; @@ -19,7 +19,10 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints [Resolved] private Playfield playfield { get; set; } = null!; - public CatchPlacementBlueprint() + [Resolved] + protected CatchHitObjectComposer? Composer { get; private set; } + + protected CatchPlacementBlueprint() : base(new THitObject()) { } diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitPlacementBlueprint.cs index 72592891fb..96cfbcb046 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitPlacementBlueprint.cs @@ -5,6 +5,7 @@ using osu.Framework.Input.Events; using osu.Game.Rulesets.Catch.Edit.Blueprints.Components; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Edit; +using osuTK; using osuTK.Input; namespace osu.Game.Rulesets.Catch.Edit.Blueprints @@ -41,11 +42,20 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints return true; } - public override void UpdateTimeAndPosition(SnapResult result) + public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) { - base.UpdateTimeAndPosition(result); + var gridSnapResult = Composer?.FindSnappedPositionAndTime(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime); + gridSnapResult.ScreenSpacePosition.X = screenSpacePosition.X; + var distanceSnapResult = Composer?.TryDistanceSnap(gridSnapResult.ScreenSpacePosition); + + var result = distanceSnapResult != null && Vector2.Distance(gridSnapResult.ScreenSpacePosition, distanceSnapResult.ScreenSpacePosition) < CatchHitObjectComposer.DISTANCE_SNAP_RADIUS + ? distanceSnapResult + : gridSnapResult; + + base.UpdateTimeAndPosition(result.ScreenSpacePosition, result.Time ?? fallbackTime); HitObject.X = ToLocalSpace(result.ScreenSpacePosition).X; + return result; } } } diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs index 21cc260462..292175353a 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs @@ -83,8 +83,16 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints return base.OnMouseDown(e); } - public override void UpdateTimeAndPosition(SnapResult result) + public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) { + var gridSnapResult = Composer?.FindSnappedPositionAndTime(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime); + gridSnapResult.ScreenSpacePosition.X = screenSpacePosition.X; + var distanceSnapResult = Composer?.TryDistanceSnap(gridSnapResult.ScreenSpacePosition); + + var result = distanceSnapResult != null && Vector2.Distance(gridSnapResult.ScreenSpacePosition, distanceSnapResult.ScreenSpacePosition) < CatchHitObjectComposer.DISTANCE_SNAP_RADIUS + ? distanceSnapResult + : gridSnapResult; + switch (PlacementActive) { case PlacementState.Waiting: @@ -99,7 +107,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints break; default: - return; + return result; } // Make sure the up-to-date position is used for outlines. @@ -113,6 +121,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints ApplyDefaultsToHitObject(); scrollingPath.UpdatePathFrom(HitObjectContainer, HitObject); nestedOutlineContainer.UpdateNestedObjectsFrom(HitObjectContainer, HitObject); + return result; } private double positionToTime(float relativeYPosition) diff --git a/osu.Game.Rulesets.Catch/Edit/CatchBlueprintContainer.cs b/osu.Game.Rulesets.Catch/Edit/CatchBlueprintContainer.cs index 3979d30616..47035b0227 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchBlueprintContainer.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchBlueprintContainer.cs @@ -1,16 +1,22 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Input.Events; using osu.Game.Rulesets.Catch.Edit.Blueprints; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Screens.Edit.Compose.Components; +using osuTK; namespace osu.Game.Rulesets.Catch.Edit { public partial class CatchBlueprintContainer : ComposeBlueprintContainer { + public new CatchHitObjectComposer Composer => (CatchHitObjectComposer)base.Composer; + public CatchBlueprintContainer(CatchHitObjectComposer composer) : base(composer) { @@ -36,5 +42,28 @@ namespace osu.Game.Rulesets.Catch.Edit } protected sealed override DragBox CreateDragBox() => new ScrollingDragBox(Composer.Playfield); + + protected override bool TryMoveBlueprints(DragEvent e, IList<(SelectionBlueprint blueprint, Vector2[] originalSnapPositions)> blueprints) + { + Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition; + + // The final movement position, relative to movementBlueprintOriginalPosition. + Vector2 movePosition = blueprints.First().originalSnapPositions.First() + distanceTravelled; + + // Retrieve a snapped position. + var gridSnapResult = Composer.FindSnappedPositionAndTime(movePosition); + gridSnapResult.ScreenSpacePosition.X = movePosition.X; + var distanceSnapResult = Composer.TryDistanceSnap(gridSnapResult.ScreenSpacePosition); + + var result = distanceSnapResult != null && Vector2.Distance(gridSnapResult.ScreenSpacePosition, distanceSnapResult.ScreenSpacePosition) < CatchHitObjectComposer.DISTANCE_SNAP_RADIUS + ? distanceSnapResult + : gridSnapResult; + + var referenceBlueprint = blueprints.First().blueprint; + bool moved = SelectionHandler.HandleMovement(new MoveSelectionEvent(referenceBlueprint, result.ScreenSpacePosition - referenceBlueprint.ScreenSpaceSelectionPoint)); + if (moved) + ApplySnapResultTime(result, referenceBlueprint.Item.StartTime); + return moved; + } } } diff --git a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs index 7bb5539963..dfe9dc9dd8 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs @@ -23,9 +23,10 @@ using osuTK; namespace osu.Game.Rulesets.Catch.Edit { + [Cached] public partial class CatchHitObjectComposer : ScrollingHitObjectComposer, IKeyBindingHandler { - private const float distance_snap_radius = 50; + public const float DISTANCE_SNAP_RADIUS = 50; private CatchDistanceSnapGrid distanceSnapGrid = null!; @@ -135,22 +136,12 @@ namespace osu.Game.Rulesets.Catch.Edit DistanceSnapProvider.HandleToggleViaKey(key); } - public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All) + public SnapResult? TryDistanceSnap(Vector2 screenSpacePosition) { - var result = base.FindSnappedPositionAndTime(screenSpacePosition, snapType); + if (distanceSnapGrid.IsPresent && distanceSnapGrid.GetSnappedPosition(screenSpacePosition) is SnapResult snapResult) + return snapResult; - result.ScreenSpacePosition.X = screenSpacePosition.X; - - if (snapType.HasFlag(SnapType.RelativeGrids)) - { - if (distanceSnapGrid.IsPresent && distanceSnapGrid.GetSnappedPosition(result.ScreenSpacePosition) is SnapResult snapResult && - Vector2.Distance(snapResult.ScreenSpacePosition, result.ScreenSpacePosition) < distance_snap_radius) - { - result = snapResult; - } - } - - return result; + return null; } private PalpableCatchHitObject? getLastSnappableHitObject(double time) diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/ManiaPlacementBlueprintTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Editor/ManiaPlacementBlueprintTestScene.cs index 5e633c3161..0f913a6a7d 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/ManiaPlacementBlueprintTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/ManiaPlacementBlueprintTestScene.cs @@ -9,7 +9,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Timing; -using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.UI; @@ -47,12 +46,11 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor }); } - protected override SnapResult SnapForBlueprint(HitObjectPlacementBlueprint blueprint) + protected override void UpdatePlacementTimeAndPosition() { double time = column.TimeAtScreenSpacePosition(InputManager.CurrentState.Mouse.Position); var pos = column.ScreenSpacePositionAtTime(time); - - return new SnapResult(pos, time, column); + CurrentBlueprint.UpdateTimeAndPosition(pos, time); } protected override Container CreateHitObjectContainer() => new ScrollingTestContainer(ScrollingDirection.Down) { RelativeSizeAxes = Axes.Both }; diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs index 127beed83e..19ff13e216 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs @@ -20,7 +20,6 @@ using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Tests.Visual; -using osuTK; namespace osu.Game.Rulesets.Mania.Tests.Editor { @@ -100,10 +99,5 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor { set => InternalChild = value; } - - public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All) - { - throw new NotImplementedException(); - } } } diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs index 13cfc5f691..094c59da46 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs @@ -98,9 +98,9 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints private double originalStartTime; - public override void UpdateTimeAndPosition(SnapResult result) + public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) { - base.UpdateTimeAndPosition(result); + var result = base.UpdateTimeAndPosition(screenSpacePosition, fallbackTime); if (PlacementActive == PlacementState.Active) { @@ -121,6 +121,8 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints if (result.Time is double startTime) originalStartTime = HitObject.StartTime = startTime; } + + return result; } } } diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs index 915706c044..ff29154f87 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs @@ -5,7 +5,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Primitives; -using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mania.Edit.Blueprints.Components; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects.Drawables; @@ -24,7 +23,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints private EditorBeatmap? editorBeatmap { get; set; } [Resolved] - private IPositionSnapProvider? positionSnapProvider { get; set; } + private ManiaHitObjectComposer? positionSnapProvider { get; set; } private EditBodyPiece body = null!; private EditHoldNoteEndPiece head = null!; diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs index a68bd5d6d6..423f14b092 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs @@ -1,8 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - +using System; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Input.Events; using osu.Game.Rulesets.Edit; @@ -20,13 +20,18 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints { protected new T HitObject => (T)base.HitObject; - private Column column; + [Resolved] + private ManiaHitObjectComposer? composer { get; set; } - public Column Column + private Column? column; + + public Column? Column { get => column; set { + ArgumentNullException.ThrowIfNull(value); + if (value == column) return; @@ -53,9 +58,11 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints return true; } - public override void UpdateTimeAndPosition(SnapResult result) + public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) { - base.UpdateTimeAndPosition(result); + var result = composer?.FindSnappedPositionAndTime(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime); + + base.UpdateTimeAndPosition(result.ScreenSpacePosition, result.Time ?? fallbackTime); if (result.Playfield is Column col) { @@ -76,6 +83,8 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints if (PlacementActive == PlacementState.Waiting) Column = col; } + + return result; } private float getNoteHeight(Column resultPlayfield) => diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs index 422215db57..a8cccfb067 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs @@ -8,6 +8,7 @@ using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mania.Objects; +using osuTK; using osuTK.Input; namespace osu.Game.Rulesets.Mania.Edit.Blueprints @@ -35,15 +36,17 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints }; } - public override void UpdateTimeAndPosition(SnapResult result) + public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double referenceTime) { - base.UpdateTimeAndPosition(result); + var result = base.UpdateTimeAndPosition(screenSpacePosition, referenceTime); if (result.Playfield != null) { piece.Width = result.Playfield.DrawWidth; piece.Position = ToLocalSpace(result.ScreenSpacePosition); } + + return result; } protected override bool OnMouseDown(MouseDownEvent e) diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs index d0eb8c1e6e..4eb54e6366 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs @@ -1,17 +1,23 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Input.Events; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mania.Edit.Blueprints; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Objects; using osu.Game.Screens.Edit.Compose.Components; +using osuTK; namespace osu.Game.Rulesets.Mania.Edit { public partial class ManiaBlueprintContainer : ComposeBlueprintContainer { - public ManiaBlueprintContainer(HitObjectComposer composer) + public new ManiaHitObjectComposer Composer => (ManiaHitObjectComposer)base.Composer; + + public ManiaBlueprintContainer(ManiaHitObjectComposer composer) : base(composer) { } @@ -33,5 +39,22 @@ namespace osu.Game.Rulesets.Mania.Edit protected override SelectionHandler CreateSelectionHandler() => new ManiaSelectionHandler(); protected sealed override DragBox CreateDragBox() => new ScrollingDragBox(Composer.Playfield); + + protected override bool TryMoveBlueprints(DragEvent e, IList<(SelectionBlueprint blueprint, Vector2[] originalSnapPositions)> blueprints) + { + Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition; + + // The final movement position, relative to movementBlueprintOriginalPosition. + Vector2 movePosition = blueprints.First().originalSnapPositions.First() + distanceTravelled; + + // Retrieve a snapped position. + var result = Composer.FindSnappedPositionAndTime(movePosition); + + var referenceBlueprint = blueprints.First().blueprint; + bool moved = SelectionHandler.HandleMovement(new MoveSelectionEvent(referenceBlueprint, result.ScreenSpacePosition - referenceBlueprint.ScreenSpaceSelectionPoint)); + if (moved) + ApplySnapResultTime(result, referenceBlueprint.Item.StartTime); + return moved; + } } } diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index 9062c32b7b..bc20456722 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -19,6 +19,7 @@ using osuTK; namespace osu.Game.Rulesets.Mania.Edit { + [Cached] public partial class ManiaHitObjectComposer : ScrollingHitObjectComposer { private DrawableManiaEditorRuleset drawableRuleset = null!; diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/GridPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/GridPlacementBlueprint.cs index 163b42bcfd..d9edc8dbd4 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/GridPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/GridPlacementBlueprint.cs @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints public partial class GridPlacementBlueprint : PlacementBlueprint { [Resolved] - private HitObjectComposer? hitObjectComposer { get; set; } + private OsuHitObjectComposer? hitObjectComposer { get; set; } private OsuGridToolboxGroup gridToolboxGroup = null!; private Vector2 originalOrigin; @@ -95,12 +95,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints base.OnDragEnd(e); } - public override SnapType SnapType => ~SnapType.GlobalGrids; - - public override void UpdateTimeAndPosition(SnapResult result) + public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) { if (State.Value == Visibility.Hidden) - return; + return new SnapResult(screenSpacePosition, fallbackTime); + + var result = hitObjectComposer?.TrySnapToNearbyObjects(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime); var pos = ToLocalSpace(result.ScreenSpacePosition); @@ -120,6 +120,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints gridToolboxGroup.SetGridFromPoints(gridToolboxGroup.StartPosition.Value, pos); } } + + return result; } protected override void PopOut() diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs index 78a0e36dc2..93d79a50ab 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs @@ -1,10 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Allocation; using osu.Framework.Input.Events; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components; using osu.Game.Rulesets.Osu.Objects; +using osuTK; using osuTK.Input; namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles @@ -15,6 +17,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles private readonly HitCirclePiece circlePiece; + [Resolved] + private OsuHitObjectComposer? composer { get; set; } + public HitCirclePlacementBlueprint() : base(new HitCircle()) { @@ -45,10 +50,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles return base.OnMouseDown(e); } - public override void UpdateTimeAndPosition(SnapResult result) + public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) { - base.UpdateTimeAndPosition(result); + var result = composer?.TrySnapToNearbyObjects(screenSpacePosition, fallbackTime); + result ??= composer?.TrySnapToDistanceGrid(screenSpacePosition); + if (composer?.TrySnapToPositionGrid(result?.ScreenSpacePosition ?? screenSpacePosition, result?.Time ?? fallbackTime) is SnapResult gridSnapResult) + result = gridSnapResult; + result ??= new SnapResult(screenSpacePosition, fallbackTime); + + base.UpdateTimeAndPosition(result.ScreenSpacePosition, result.Time ?? fallbackTime); HitObject.Position = ToLocalSpace(result.ScreenSpacePosition); + return result; } } } 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 f98117c0fa..189bb005a7 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -9,6 +9,7 @@ using System.Collections.Specialized; using System.Diagnostics; using System.Linq; using Humanizer; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -48,7 +49,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components public Action> SplitControlPointsRequested; [Resolved(CanBeNull = true)] - private IPositionSnapProvider positionSnapProvider { get; set; } + [CanBeNull] + private OsuHitObjectComposer positionSnapProvider { get; set; } [Resolved(CanBeNull = true)] private IDistanceSnapProvider distanceSnapProvider { get; set; } @@ -433,12 +435,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { // 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])); - SnapResult result = positionSnapProvider?.FindSnappedPositionAndTime(newHeadPosition); - Vector2 movementDelta = Parent!.ToLocalSpace(result?.ScreenSpacePosition ?? newHeadPosition) - hitObject.Position; + var result = positionSnapProvider?.TrySnapToNearbyObjects(newHeadPosition, oldStartTime); + result ??= positionSnapProvider?.TrySnapToDistanceGrid(newHeadPosition); + 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 = result?.Time ?? hitObject.StartTime; + hitObject.StartTime = result.Time ?? hitObject.StartTime; for (int i = 1; i < hitObject.Path.ControlPoints.Count; i++) { @@ -453,7 +460,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components } else { - SnapResult result = positionSnapProvider?.FindSnappedPositionAndTime(Parent!.ToScreenSpace(e.MousePosition), SnapType.GlobalGrids); + SnapResult result = positionSnapProvider?.TrySnapToPositionGrid(Parent!.ToScreenSpace(e.MousePosition)); Vector2 movementDelta = Parent!.ToLocalSpace(result?.ScreenSpacePosition ?? Parent!.ToScreenSpace(e.MousePosition)) - dragStartPositions[draggedControlPointIndex] - hitObject.Position; diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index 4f2f6516a8..1012578375 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -25,6 +25,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders { public new Slider HitObject => (Slider)base.HitObject; + [Resolved] + private OsuHitObjectComposer? composer { get; set; } + private SliderBodyPiece bodyPiece = null!; private HitCirclePiece headCirclePiece = null!; private HitCirclePiece tailCirclePiece = null!; @@ -40,9 +43,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders private int currentSegmentLength; private bool usingCustomSegmentType; - [Resolved] - private IPositionSnapProvider? positionSnapProvider { get; set; } - [Resolved] private IDistanceSnapProvider? distanceSnapProvider { get; set; } @@ -106,9 +106,15 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders [Resolved] private EditorBeatmap editorBeatmap { get; set; } = null!; - public override void UpdateTimeAndPosition(SnapResult result) + public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) { - base.UpdateTimeAndPosition(result); + var result = composer?.TrySnapToNearbyObjects(screenSpacePosition, fallbackTime); + result ??= composer?.TrySnapToDistanceGrid(screenSpacePosition); + if (composer?.TrySnapToPositionGrid(result?.ScreenSpacePosition ?? screenSpacePosition, result?.Time ?? fallbackTime) is SnapResult gridSnapResult) + result = gridSnapResult; + result ??= new SnapResult(screenSpacePosition, fallbackTime); + + base.UpdateTimeAndPosition(result.ScreenSpacePosition, result.Time ?? fallbackTime); switch (state) { @@ -131,6 +137,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders updateCursor(); break; } + + return result; } protected override bool OnMouseDown(MouseDownEvent e) @@ -375,7 +383,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders private Vector2 getCursorPosition() { - var result = positionSnapProvider?.FindSnappedPositionAndTime(inputManager.CurrentState.Mouse.Position, state == SliderPlacementState.ControlPoints ? SnapType.GlobalGrids : SnapType.All); + SnapResult? result = null; + var mousePosition = inputManager.CurrentState.Mouse.Position; + + if (state != SliderPlacementState.ControlPoints) + { + result ??= composer?.TrySnapToNearbyObjects(mousePosition); + result ??= composer?.TrySnapToDistanceGrid(mousePosition); + } + + result ??= composer?.TrySnapToPositionGrid(mousePosition); + return ToLocalSpace(result?.ScreenSpacePosition ?? inputManager.CurrentState.Mouse.Position) - HitObject.Position; } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs b/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs index 54c54fca17..5eff95adec 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs @@ -1,6 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Input.Events; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles; @@ -8,12 +11,15 @@ using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders; using osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Screens.Edit.Compose.Components; +using osuTK; namespace osu.Game.Rulesets.Osu.Edit { public partial class OsuBlueprintContainer : ComposeBlueprintContainer { - public OsuBlueprintContainer(HitObjectComposer composer) + public new OsuHitObjectComposer Composer => (OsuHitObjectComposer)base.Composer; + + public OsuBlueprintContainer(OsuHitObjectComposer composer) : base(composer) { } @@ -36,5 +42,68 @@ namespace osu.Game.Rulesets.Osu.Edit return base.CreateHitObjectBlueprintFor(hitObject); } + + protected override bool TryMoveBlueprints(DragEvent e, IList<(SelectionBlueprint blueprint, Vector2[] originalSnapPositions)> blueprints) + { + Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition; + + for (int i = 0; i < blueprints.Count; i++) + { + if (checkSnappingBlueprintToNearbyObjects(blueprints[i].blueprint, distanceTravelled, blueprints[i].originalSnapPositions)) + return true; + } + + // if no positional snapping could be performed, try unrestricted snapping from the earliest + // item in the selection. + + // The final movement position, relative to movementBlueprintOriginalPosition. + Vector2 movePosition = blueprints.First().originalSnapPositions.First() + distanceTravelled; + + // Retrieve a snapped position. + var result = Composer.TrySnapToNearbyObjects(movePosition); + result ??= Composer.TrySnapToDistanceGrid(movePosition); + if (Composer.TrySnapToPositionGrid(result?.ScreenSpacePosition ?? movePosition, result?.Time) is SnapResult gridSnapResult) + result = gridSnapResult; + result ??= new SnapResult(movePosition, null); + + var referenceBlueprint = blueprints.First().blueprint; + bool moved = SelectionHandler.HandleMovement(new MoveSelectionEvent(referenceBlueprint, result.ScreenSpacePosition - referenceBlueprint.ScreenSpaceSelectionPoint)); + if (moved) + ApplySnapResultTime(result, referenceBlueprint.Item.StartTime); + return moved; + } + + /// + /// 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 = Composer.TrySnapToNearbyObjects(testPosition); + + if (positionalResult == null || positionalResult.ScreenSpacePosition == testPosition) continue; + + var delta = positionalResult.ScreenSpacePosition - currentPositions[i]; + + // attempt to move the objects, and apply any time based snapping if we can. + if (SelectionHandler.HandleMovement(new MoveSelectionEvent(blueprint, delta))) + { + ApplySnapResultTime(positionalResult, blueprint.Item.StartTime); + return true; + } + } + + return false; + } } } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index aad3d0c93b..194276baf9 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Caching; @@ -31,6 +32,7 @@ using osuTK; namespace osu.Game.Rulesets.Osu.Edit { + [Cached] public partial class OsuHitObjectComposer : HitObjectComposer { public OsuHitObjectComposer(Ruleset ruleset) @@ -222,56 +224,56 @@ namespace osu.Game.Rulesets.Osu.Edit } } - public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All) + [CanBeNull] + public SnapResult TrySnapToNearbyObjects(Vector2 screenSpacePosition, double? fallbackTime = null) { - if (snapType.HasFlag(SnapType.NearbyObjects) && snapToVisibleBlueprints(screenSpacePosition, out var snapResult)) - { - // In the case of snapping to nearby objects, a time value is not provided. - // This matches the stable editor (which also uses current time), but with the introduction of time-snapping distance snap - // this could result in unexpected behaviour when distance snapping is turned on and a user attempts to place an object that is - // BOTH on a valid distance snap ring, and also at the same position as a previous object. - // - // We want to ensure that in this particular case, the time-snapping component of distance snap is still applied. - // The easiest way to ensure this is to attempt application of distance snap after a nearby object is found, and copy over - // the time value if the proposed positions are roughly the same. - if (snapType.HasFlag(SnapType.RelativeGrids) && DistanceSnapProvider.DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null) - { - (Vector2 distanceSnappedPosition, double distanceSnappedTime) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(snapResult.ScreenSpacePosition)); - if (Precision.AlmostEquals(distanceSnapGrid.ToScreenSpace(distanceSnappedPosition), snapResult.ScreenSpacePosition, 1)) - snapResult.Time = distanceSnappedTime; - } + if (!snapToVisibleBlueprints(screenSpacePosition, out var snapResult)) + return null; + if (DistanceSnapProvider.DistanceSnapToggle.Value != TernaryState.True || distanceSnapGrid == null) return snapResult; - } - SnapResult result = base.FindSnappedPositionAndTime(screenSpacePosition, snapType); + // In the case of snapping to nearby objects, a time value is not provided. + // This matches the stable editor (which also uses current time), but with the introduction of time-snapping distance snap + // this could result in unexpected behaviour when distance snapping is turned on and a user attempts to place an object that is + // BOTH on a valid distance snap ring, and also at the same position as a previous object. + // + // We want to ensure that in this particular case, the time-snapping component of distance snap is still applied. + // The easiest way to ensure this is to attempt application of distance snap after a nearby object is found, and copy over + // the time value if the proposed positions are roughly the same. + (Vector2 distanceSnappedPosition, double distanceSnappedTime) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(snapResult.ScreenSpacePosition)); + snapResult.Time = Precision.AlmostEquals(distanceSnapGrid.ToScreenSpace(distanceSnappedPosition), snapResult.ScreenSpacePosition, 1) + ? distanceSnappedTime + : fallbackTime; - if (snapType.HasFlag(SnapType.RelativeGrids)) - { - if (DistanceSnapProvider.DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null) - { - (Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition)); + return snapResult; + } - result.ScreenSpacePosition = distanceSnapGrid.ToScreenSpace(pos); - result.Time = time; - } - } + [CanBeNull] + public SnapResult TrySnapToDistanceGrid(Vector2 screenSpacePosition) + { + if (DistanceSnapProvider.DistanceSnapToggle.Value != TernaryState.True || distanceSnapGrid == null) + return null; - if (snapType.HasFlag(SnapType.GlobalGrids)) - { - if (rectangularGridSnapToggle.Value == TernaryState.True) - { - Vector2 pos = positionSnapGrid.GetSnappedPosition(positionSnapGrid.ToLocalSpace(result.ScreenSpacePosition)); + var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition); + (Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition)); + return new SnapResult(distanceSnapGrid.ToScreenSpace(pos), time, playfield); + } - // A grid which doesn't perfectly fit the playfield can produce a position that is outside of the playfield. - // We need to clamp the position to the playfield bounds to ensure that the snapped position is always in bounds. - pos = Vector2.Clamp(pos, Vector2.Zero, OsuPlayfield.BASE_SIZE); + [CanBeNull] + public SnapResult TrySnapToPositionGrid(Vector2 screenSpacePosition, double? fallbackTime = null) + { + if (rectangularGridSnapToggle.Value != TernaryState.True) + return null; - result.ScreenSpacePosition = positionSnapGrid.ToScreenSpace(pos); - } - } + Vector2 pos = positionSnapGrid.GetSnappedPosition(positionSnapGrid.ToLocalSpace(screenSpacePosition)); - return result; + // A grid which doesn't perfectly fit the playfield can produce a position that is outside of the playfield. + // We need to clamp the position to the playfield bounds to ensure that the snapped position is always in bounds. + pos = Vector2.Clamp(pos, Vector2.Zero, OsuPlayfield.BASE_SIZE); + + var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition); + return new SnapResult(positionSnapGrid.ToScreenSpace(pos), null, playfield); } private bool snapToVisibleBlueprints(Vector2 screenSpacePosition, out SnapResult snapResult) diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs index 7f45123bd6..ce2a674e92 100644 --- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Allocation; using osu.Framework.Input.Events; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Taiko.Objects; @@ -16,6 +17,9 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints public new Hit HitObject => (Hit)base.HitObject; + [Resolved] + private TaikoHitObjectComposer? composer { get; set; } + public HitPlacementBlueprint() : base(new Hit()) { @@ -40,10 +44,12 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints return true; } - public override void UpdateTimeAndPosition(SnapResult result) + public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) { + var result = composer?.FindSnappedPositionAndTime(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime); piece.Position = ToLocalSpace(result.ScreenSpacePosition); - base.UpdateTimeAndPosition(result); + base.UpdateTimeAndPosition(result.ScreenSpacePosition, result.Time ?? fallbackTime); + return result; } } } diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs index de3a4d96eb..3d5c95e1e8 100644 --- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs @@ -1,9 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Input.Events; using osu.Framework.Utils; @@ -26,12 +25,15 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints private readonly IHasDuration spanPlacementObject; + [Resolved] + private TaikoHitObjectComposer? composer { get; set; } + protected override bool IsValidForPlacement => Precision.DefinitelyBigger(spanPlacementObject.Duration, 0); public TaikoSpanPlacementBlueprint(HitObject hitObject) : base(hitObject) { - spanPlacementObject = hitObject as IHasDuration; + spanPlacementObject = (hitObject as IHasDuration)!; RelativeSizeAxes = Axes.Both; @@ -79,9 +81,11 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints EndPlacement(true); } - public override void UpdateTimeAndPosition(SnapResult result) + public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) { - base.UpdateTimeAndPosition(result); + var result = composer?.FindSnappedPositionAndTime(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime); + + base.UpdateTimeAndPosition(result.ScreenSpacePosition, result.Time ?? fallbackTime); if (PlacementActive == PlacementState.Active) { @@ -116,6 +120,8 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints originalPosition = ToLocalSpace(result.ScreenSpacePosition); } } + + return result; } } } diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoBlueprintContainer.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoBlueprintContainer.cs index 027723c02c..f0c3eec044 100644 --- a/osu.Game.Rulesets.Taiko/Edit/TaikoBlueprintContainer.cs +++ b/osu.Game.Rulesets.Taiko/Edit/TaikoBlueprintContainer.cs @@ -1,16 +1,22 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Input.Events; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Taiko.Edit.Blueprints; using osu.Game.Screens.Edit.Compose.Components; +using osuTK; namespace osu.Game.Rulesets.Taiko.Edit { public partial class TaikoBlueprintContainer : ComposeBlueprintContainer { - public TaikoBlueprintContainer(HitObjectComposer composer) + public new TaikoHitObjectComposer Composer => (TaikoHitObjectComposer)base.Composer; + + public TaikoBlueprintContainer(TaikoHitObjectComposer composer) : base(composer) { } @@ -19,5 +25,22 @@ namespace osu.Game.Rulesets.Taiko.Edit public override HitObjectSelectionBlueprint CreateHitObjectBlueprintFor(HitObject hitObject) => new TaikoSelectionBlueprint(hitObject); + + protected override bool TryMoveBlueprints(DragEvent e, IList<(SelectionBlueprint blueprint, Vector2[] originalSnapPositions)> blueprints) + { + Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition; + + // The final movement position, relative to movementBlueprintOriginalPosition. + Vector2 movePosition = blueprints.First().originalSnapPositions.First() + distanceTravelled; + + // Retrieve a snapped position. + var result = Composer.FindSnappedPositionAndTime(movePosition); + + var referenceBlueprint = blueprints.First().blueprint; + bool moved = SelectionHandler.HandleMovement(new MoveSelectionEvent(referenceBlueprint, result.ScreenSpacePosition - referenceBlueprint.ScreenSpaceSelectionPoint)); + if (moved) + ApplySnapResultTime(result, referenceBlueprint.Item.StartTime); + return moved; + } } } diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs index d97a854ff7..54031f0c9f 100644 --- a/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs +++ b/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using osu.Framework.Allocation; using osu.Game.Beatmaps; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Tools; @@ -12,6 +13,7 @@ using osu.Game.Screens.Edit.Compose.Components; namespace osu.Game.Rulesets.Taiko.Edit { + [Cached] public partial class TaikoHitObjectComposer : ScrollingHitObjectComposer { protected override bool ApplyHorizontalCentering => false; diff --git a/osu.Game/Overlays/SkinEditor/SkinBlueprintContainer.cs b/osu.Game/Overlays/SkinEditor/SkinBlueprintContainer.cs index 3f8d9f80d4..df8cb33a71 100644 --- a/osu.Game/Overlays/SkinEditor/SkinBlueprintContainer.cs +++ b/osu.Game/Overlays/SkinEditor/SkinBlueprintContainer.cs @@ -111,6 +111,16 @@ namespace osu.Game.Overlays.SkinEditor SelectedItems.AddRange(targetComponents.SelectMany(list => list).Except(SelectedItems).ToArray()); } + protected override bool TryMoveBlueprints(DragEvent e, IList<(SelectionBlueprint blueprint, Vector2[] originalSnapPositions)> blueprints) + { + Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition; + + // The final movement position, relative to movementBlueprintOriginalPosition. + var referenceBlueprint = blueprints.First().blueprint; + Vector2 movePosition = blueprints.First().originalSnapPositions.First() + distanceTravelled; + return SelectionHandler.HandleMovement(new MoveSelectionEvent(referenceBlueprint, movePosition - referenceBlueprint.ScreenSpaceSelectionPoint)); + } + /// /// Move the current selection spatially by the specified delta, in screen coordinates (ie. the same coordinates as the blueprints). /// diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 15b60114af..b38b0291e8 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -376,7 +376,7 @@ namespace osu.Game.Rulesets.Edit /// /// Construct a relevant blueprint container. This will manage hitobject selection/placement input handling and display logic. /// - protected virtual ComposeBlueprintContainer CreateBlueprintContainer() => new ComposeBlueprintContainer(this); + protected abstract ComposeBlueprintContainer CreateBlueprintContainer(); protected virtual Drawable CreateHitObjectInspector() => new HitObjectInspector(); @@ -566,28 +566,6 @@ namespace osu.Game.Rulesets.Edit /// The most relevant . protected virtual Playfield PlayfieldAtScreenSpacePosition(Vector2 screenSpacePosition) => drawableRulesetWrapper.Playfield; - public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All) - { - var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition); - double? targetTime = null; - - if (snapType.HasFlag(SnapType.GlobalGrids)) - { - if (playfield is ScrollingPlayfield scrollingPlayfield) - { - targetTime = scrollingPlayfield.TimeAtScreenSpacePosition(screenSpacePosition); - - // apply beat snapping - targetTime = BeatSnapProvider.SnapTime(targetTime.Value); - - // convert back to screen space - screenSpacePosition = scrollingPlayfield.ScreenSpacePositionAtTime(targetTime.Value); - } - } - - return new SnapResult(screenSpacePosition, targetTime, playfield); - } - #endregion } @@ -596,7 +574,7 @@ namespace osu.Game.Rulesets.Edit /// Generally used to access certain methods without requiring a generic type for . /// [Cached] - public abstract partial class HitObjectComposer : CompositeDrawable, IPositionSnapProvider + public abstract partial class HitObjectComposer : CompositeDrawable { public const float TOOLBOX_CONTRACTED_SIZE_LEFT = 60; public const float TOOLBOX_CONTRACTED_SIZE_RIGHT = 120; @@ -639,11 +617,5 @@ namespace osu.Game.Rulesets.Edit /// The time instant to seek to, in milliseconds. /// The ruleset-specific description of objects to select at the given timestamp. public virtual void SelectFromTimestamp(double timestamp, string objectDescription) { } - - #region IPositionSnapProvider - - public abstract SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All); - - #endregion } } diff --git a/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs b/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs index 4df2a52743..6720540ec2 100644 --- a/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs @@ -13,6 +13,7 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Compose; +using osuTK; namespace osu.Game.Rulesets.Edit { @@ -87,14 +88,13 @@ namespace osu.Game.Rulesets.Edit } /// - /// Updates the time and position of this based on the provided snap information. + /// Updates the time and position of this . /// - /// The snap result information. - public override void UpdateTimeAndPosition(SnapResult result) + public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double time) { if (PlacementActive == PlacementState.Waiting) { - HitObject.StartTime = result.Time ?? EditorClock.CurrentTime; + HitObject.StartTime = time; if (HitObject is IHasComboInformation comboInformation) comboInformation.UpdateComboInformation(getPreviousHitObject() as IHasComboInformation); @@ -129,6 +129,8 @@ namespace osu.Game.Rulesets.Edit for (int i = 0; i < hasRepeats.NodeSamples.Count; i++) hasRepeats.NodeSamples[i] = HitObject.Samples.Select(o => o.With()).ToList(); } + + return new SnapResult(screenSpacePosition, time); } /// diff --git a/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs b/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs deleted file mode 100644 index 002a0aafe6..0000000000 --- a/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Allocation; -using osuTK; - -namespace osu.Game.Rulesets.Edit -{ - /// - /// A snap provider which given a proposed position for a hit object, potentially offers a more correct position and time value inferred from the context of the beatmap. - /// - [Cached] - public interface IPositionSnapProvider - { - /// - /// Given a position, find a valid time and position snap. - /// - /// The screen-space position to be snapped. - /// The type of snapping to apply. - /// The time and position post-snapping. - SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All); - } -} diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs index 52b8a5c796..f2d501d1c4 100644 --- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs @@ -7,6 +7,7 @@ using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Input.Bindings; using osu.Game.Rulesets.Objects; +using osuTK; using osuTK.Input; namespace osu.Game.Rulesets.Edit @@ -75,18 +76,7 @@ namespace osu.Game.Rulesets.Edit PlacementActive = PlacementState.Finished; } - /// - /// Determines which objects to snap to for the snap result in . - /// - public virtual SnapType SnapType => SnapType.All; - - /// - /// Updates the time and position of this based on the provided snap information. - /// - /// The snap result information. - public virtual void UpdateTimeAndPosition(SnapResult result) - { - } + public abstract SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime); public bool OnPressed(KeyBindingPressEvent e) { diff --git a/osu.Game/Rulesets/Edit/ScrollingHitObjectComposer.cs b/osu.Game/Rulesets/Edit/ScrollingHitObjectComposer.cs index e7161ce36c..3671724042 100644 --- a/osu.Game/Rulesets/Edit/ScrollingHitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/ScrollingHitObjectComposer.cs @@ -117,6 +117,23 @@ namespace osu.Game.Rulesets.Edit } } + public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition) + { + var scrollingPlayfield = PlayfieldAtScreenSpacePosition(screenSpacePosition) as ScrollingPlayfield; + if (scrollingPlayfield == null) + return new SnapResult(screenSpacePosition, null); + + double? targetTime = scrollingPlayfield.TimeAtScreenSpacePosition(screenSpacePosition); + + // apply beat snapping + targetTime = BeatSnapProvider.SnapTime(targetTime.Value); + + // convert back to screen space + screenSpacePosition = scrollingPlayfield.ScreenSpacePositionAtTime(targetTime.Value); + + return new SnapResult(screenSpacePosition, targetTime, scrollingPlayfield); + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); diff --git a/osu.Game/Rulesets/Edit/SnapType.cs b/osu.Game/Rulesets/Edit/SnapType.cs deleted file mode 100644 index cf743f6ace..0000000000 --- a/osu.Game/Rulesets/Edit/SnapType.cs +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; - -namespace osu.Game.Rulesets.Edit -{ - [Flags] - public enum SnapType - { - None = 0, - - /// - /// Snapping to visible nearby objects. - /// - NearbyObjects = 1 << 0, - - /// - /// Grids which are global to the playfield. - /// - GlobalGrids = 1 << 1, - - /// - /// Grids which are relative to other nearby hit objects. - /// - RelativeGrids = 1 << 2, - - AllGrids = RelativeGrids | GlobalGrids, - - All = NearbyObjects | GlobalGrids | RelativeGrids, - } -} diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 4a321f4a81..dc04561242 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -43,9 +43,6 @@ namespace osu.Game.Screens.Edit.Compose.Components private readonly Dictionary> blueprintMap = new Dictionary>(); - [Resolved(canBeNull: true)] - private IPositionSnapProvider snapProvider { get; set; } - [Resolved(CanBeNull = true)] private IEditorChangeHandler changeHandler { get; set; } @@ -333,19 +330,19 @@ namespace osu.Game.Screens.Edit.Compose.Components protected void RemoveBlueprintFor(T item) { - if (!blueprintMap.Remove(item, out var blueprint)) + if (!blueprintMap.Remove(item, out var blueprintToRemove)) return; - blueprint.Deselect(); - blueprint.Selected -= OnBlueprintSelected; - blueprint.Deselected -= OnBlueprintDeselected; + blueprintToRemove.Deselect(); + blueprintToRemove.Selected -= OnBlueprintSelected; + blueprintToRemove.Deselected -= OnBlueprintDeselected; - SelectionBlueprints.Remove(blueprint, true); + SelectionBlueprints.Remove(blueprintToRemove, true); - if (movementBlueprints?.Contains(blueprint) == true) + if (movementBlueprints?.Any(m => m.blueprint == blueprintToRemove) == true) finishSelectionMovement(); - OnBlueprintRemoved(blueprint.Item); + OnBlueprintRemoved(blueprintToRemove.Item); } /// @@ -538,8 +535,7 @@ namespace osu.Game.Screens.Edit.Compose.Components #region Selection Movement - private Vector2[][] movementBlueprintsOriginalPositions; - private SelectionBlueprint[] movementBlueprints; + private (SelectionBlueprint blueprint, Vector2[] originalSnapPositions)[] movementBlueprints; /// /// Whether a blueprint is currently being dragged. @@ -572,8 +568,7 @@ namespace osu.Game.Screens.Edit.Compose.Components return false; // 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(); - movementBlueprintsOriginalPositions = movementBlueprints.Select(m => m.ScreenSpaceSnapPoints).ToArray(); + movementBlueprints = SortForMovement(SelectionHandler.SelectedBlueprints).Select(b => (b, b.ScreenSpaceSnapPoints)).ToArray(); return true; } @@ -594,68 +589,10 @@ namespace osu.Game.Screens.Edit.Compose.Components if (movementBlueprints == null) return false; - Debug.Assert(movementBlueprintsOriginalPositions != null); - - Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition; - - if (snapProvider != null) - { - for (int i = 0; i < movementBlueprints.Length; i++) - { - if (checkSnappingBlueprintToNearbyObjects(movementBlueprints[i], distanceTravelled, movementBlueprintsOriginalPositions[i])) - return true; - } - } - - // if no positional snapping could be performed, try unrestricted snapping from the earliest - // item in the selection. - - // The final movement position, relative to movementBlueprintOriginalPosition. - Vector2 movePosition = movementBlueprintsOriginalPositions.First().First() + distanceTravelled; - - // Retrieve a snapped position. - var result = snapProvider?.FindSnappedPositionAndTime(movePosition, ~SnapType.NearbyObjects); - - if (result == null) - { - return SelectionHandler.HandleMovement(new MoveSelectionEvent(movementBlueprints.First(), movePosition - movementBlueprints.First().ScreenSpaceSelectionPoint)); - } - - return ApplySnapResult(movementBlueprints, result); + return TryMoveBlueprints(e, movementBlueprints); } - /// - /// 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)); + protected abstract bool TryMoveBlueprints(DragEvent e, IList<(SelectionBlueprint blueprint, Vector2[] originalSnapPositions)> blueprints); /// /// Finishes the current movement of selected blueprints. @@ -666,7 +603,6 @@ namespace osu.Game.Screens.Edit.Compose.Components if (movementBlueprints == null) return false; - movementBlueprintsOriginalPositions = null; movementBlueprints = null; return true; diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index 15bbddd97e..de1f589135 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -33,7 +33,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// /// A blueprint container generally displayed as an overlay to a ruleset's playfield. /// - public partial class ComposeBlueprintContainer : EditorBlueprintContainer + public abstract partial class ComposeBlueprintContainer : EditorBlueprintContainer { private readonly Container placementBlueprintContainer; @@ -52,7 +52,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => editorScreen?.MainContent.ReceivePositionalInputAt(screenSpacePos) ?? base.ReceivePositionalInputAt(screenSpacePos); - public ComposeBlueprintContainer(HitObjectComposer composer) + protected ComposeBlueprintContainer(HitObjectComposer composer) : base(composer) { placementBlueprintContainer = new Container @@ -340,12 +340,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private void updatePlacementTimeAndPosition() { - var snapResult = Composer.FindSnappedPositionAndTime(InputManager.CurrentState.Mouse.Position, CurrentPlacement.SnapType); - - // if no time was found from positional snapping, we should still quantize to the beat. - snapResult.Time ??= Beatmap.SnapTime(EditorClock.CurrentTime, null); - - CurrentPlacement.UpdateTimeAndPosition(snapResult); + CurrentPlacement.UpdateTimeAndPosition(InputManager.CurrentState.Mouse.Position, Beatmap.SnapTime(EditorClock.CurrentTime, null)); } #endregion diff --git a/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs index 7b046251e0..f1811dd84f 100644 --- a/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs @@ -17,7 +17,7 @@ using osu.Game.Rulesets.Objects.Drawables; namespace osu.Game.Screens.Edit.Compose.Components { - public partial class EditorBlueprintContainer : BlueprintContainer + public abstract partial class EditorBlueprintContainer : BlueprintContainer { [Resolved] protected EditorClock EditorClock { get; private set; } @@ -73,27 +73,22 @@ namespace osu.Game.Screens.Edit.Compose.Components protected override IEnumerable> SortForMovement(IReadOnlyList> blueprints) => blueprints.OrderBy(b => b.Item.StartTime); - protected override bool ApplySnapResult(SelectionBlueprint[] blueprints, SnapResult result) + protected void ApplySnapResultTime(SnapResult result, double referenceTime) { - if (!base.ApplySnapResult(blueprints, result)) - return false; + if (!result.Time.HasValue) + return; - if (result.Time.HasValue) + // Apply the start time at the newly snapped-to position + double offset = result.Time.Value - referenceTime; + + if (offset != 0) { - // Apply the start time at the newly snapped-to position - double offset = result.Time.Value - blueprints.First().Item.StartTime; - - if (offset != 0) + Beatmap.PerformOnSelection(obj => { - Beatmap.PerformOnSelection(obj => - { - obj.StartTime += offset; - Beatmap.Update(obj); - }); - } + obj.StartTime += offset; + Beatmap.Update(obj); + }); } - - return true; } protected override void AddBlueprintFor(HitObject item) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index 5f46b3d937..cbf49e62e7 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -22,7 +22,7 @@ using osuTK.Input; namespace osu.Game.Screens.Edit.Compose.Components.Timeline { [Cached] - public partial class Timeline : ZoomableScrollContainer, IPositionSnapProvider + public partial class Timeline : ZoomableScrollContainer { private const float timeline_height = 80; @@ -332,7 +332,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline return (float)(time / editorClock.TrackLength * Content.DrawWidth); } - public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All) + public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition) { double time = TimeAtPosition(Content.ToLocalSpace(screenSpacePosition).X); return new SnapResult(screenSpacePosition, beatSnapProvider.SnapTime(time)); diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index 2b5667ff9c..011ff17b30 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -107,6 +107,23 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline return base.OnDragStart(e); } + protected override bool TryMoveBlueprints(DragEvent e, IList<(SelectionBlueprint blueprint, Vector2[] originalSnapPositions)> blueprints) + { + Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition; + + // The final movement position, relative to movementBlueprintOriginalPosition. + Vector2 movePosition = blueprints.First().originalSnapPositions.First() + distanceTravelled; + + // Retrieve a snapped position. + var result = timeline?.FindSnappedPositionAndTime(movePosition) ?? new SnapResult(movePosition, null); + + var referenceBlueprint = blueprints.First().blueprint; + bool moved = SelectionHandler.HandleMovement(new MoveSelectionEvent(referenceBlueprint, result.ScreenSpacePosition - referenceBlueprint.ScreenSpaceSelectionPoint)); + if (moved) + ApplySnapResultTime(result, referenceBlueprint.Item.StartTime); + return moved; + } + private float dragTimeAccumulated; protected override void Update() diff --git a/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs b/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs index aa8aff3adc..baf614d1c8 100644 --- a/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs +++ b/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs @@ -28,7 +28,7 @@ namespace osu.Game.Tests.Visual base.Content.Add(HitObjectContainer = CreateHitObjectContainer().With(c => c.Clock = new FramedClock(new StopwatchClock()))); base.Content.Add(new MouseMovementInterceptor { - MouseMoved = updatePlacementTimeAndPosition, + MouseMoved = UpdatePlacementTimeAndPosition, }); } @@ -93,13 +93,10 @@ namespace osu.Game.Tests.Visual if (CurrentBlueprint.PlacementActive == PlacementBlueprint.PlacementState.Finished) ResetPlacement(); - updatePlacementTimeAndPosition(); + UpdatePlacementTimeAndPosition(); } - private void updatePlacementTimeAndPosition() => CurrentBlueprint.UpdateTimeAndPosition(SnapForBlueprint(CurrentBlueprint)); - - protected virtual SnapResult SnapForBlueprint(HitObjectPlacementBlueprint blueprint) => - new SnapResult(InputManager.CurrentState.Mouse.Position, null); + protected virtual void UpdatePlacementTimeAndPosition() => CurrentBlueprint.UpdateTimeAndPosition(InputManager.CurrentState.Mouse.Position, 0); public override void Add(Drawable drawable) { @@ -108,7 +105,7 @@ namespace osu.Game.Tests.Visual if (drawable is HitObjectPlacementBlueprint blueprint) { blueprint.Show(); - blueprint.UpdateTimeAndPosition(SnapForBlueprint(blueprint)); + UpdatePlacementTimeAndPosition(); } }