diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs index d88f48858230..82b919ac97a4 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs @@ -288,6 +288,55 @@ public void TestDragHoldNoteTail() AddAssert("tail blueprint positioned correctly", () => this.ChildrenOfType().ElementAt(1).DrawPosition == drawableHoldNote.Tail.DrawPosition); } + [Test] + public void TestDragHoldNoteTailOutsidePlayfield() + { + setScrollStep(ScrollingDirection.Down); + + HoldNote holdNote = null; + AddStep("setup beatmap", () => + { + composer.EditorBeatmap.Clear(); + composer.EditorBeatmap.Add(holdNote = new HoldNote + { + Column = 1, + StartTime = 250, + EndTime = 750, + }); + }); + + DrawableHoldNote drawableHoldNote = null; + EditHoldNoteEndPiece tailPiece = null; + + AddStep("select blueprint", () => + { + drawableHoldNote = this.ChildrenOfType().Single(); + InputManager.MoveMouseTo(drawableHoldNote); + InputManager.Click(MouseButton.Left); + }); + AddStep("grab hold note tail", () => + { + tailPiece = this.ChildrenOfType().Last(); + InputManager.MoveMouseTo(tailPiece); + InputManager.PressButton(MouseButton.Left); + }); + + AddStep("drag tail upwards", () => + { + InputManager.MoveMouseTo(tailPiece, new Vector2(-500, -120)); + InputManager.ReleaseButton(MouseButton.Left); + }); + + AddAssert("start time unchanged", () => holdNote!.StartTime, () => Is.EqualTo(250)); + AddAssert("end time is snapped", () => holdNote.EndTime % 125, () => Is.Zero); + + AddAssert("head note positioned correctly", () => Precision.AlmostEquals(drawableHoldNote.ScreenSpaceDrawQuad.BottomLeft, drawableHoldNote.Head.ScreenSpaceDrawQuad.BottomLeft)); + AddAssert("tail note positioned correctly", () => Precision.AlmostEquals(drawableHoldNote.ScreenSpaceDrawQuad.TopLeft, drawableHoldNote.Tail.ScreenSpaceDrawQuad.BottomLeft)); + + AddAssert("head blueprint positioned correctly", () => this.ChildrenOfType().ElementAt(0).DrawPosition == drawableHoldNote.Head.DrawPosition); + AddAssert("tail blueprint positioned correctly", () => this.ChildrenOfType().ElementAt(1).DrawPosition == drawableHoldNote.Tail.DrawPosition); + } + private void setScrollStep(ScrollingDirection direction) => AddStep($"set scroll direction = {direction}", () => ((Bindable)composer.Composer.ScrollingInfo.Direction).Value = direction); diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNoteTailDrag.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneTimelineHoldNote.cs similarity index 99% rename from osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNoteTailDrag.cs rename to osu.Game.Rulesets.Mania.Tests/Editor/TestSceneTimelineHoldNote.cs index bdbf24bb95b3..de9929458d46 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNoteTailDrag.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneTimelineHoldNote.cs @@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor { - public partial class TestSceneHoldNoteTailDrag : EditorTestScene + public partial class TestSceneTimelineHoldNote : EditorTestScene { protected override Ruleset CreateEditorRuleset() => new ManiaRuleset(); diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs index 423f14b092fb..ff387ba93ab4 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs @@ -51,6 +51,9 @@ protected override bool OnMouseDown(MouseDownEvent e) if (e.Button != MouseButton.Left) return false; + if (!IsValidForPlacement) + return false; + if (Column == null) return false; diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index 7da501063dfe..a2a949ed91b3 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -39,7 +39,7 @@ public ManiaHitObjectComposer(Ruleset ruleset) public IScrollingInfo ScrollingInfo => drawableRuleset.ScrollingInfo; protected override Playfield PlayfieldAtScreenSpacePosition(Vector2 screenSpacePosition) => - Playfield.GetColumnByPosition(screenSpacePosition); + Playfield.GetClosestColumnByPosition(screenSpacePosition.X); protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods) => drawableRuleset = new DrawableManiaEditorRuleset(ruleset, beatmap, mods); diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs index 74e616ac3fdd..b5fde726c8ec 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs @@ -92,7 +92,7 @@ private void performColumnMovement(int lastColumn, MoveSelectionEvent { var maniaPlayfield = ((ManiaHitObjectComposer)composer).Playfield; - var currentColumn = maniaPlayfield.GetColumnByPosition(moveEvent.Blueprint.ScreenSpaceSelectionPoint + moveEvent.ScreenSpaceDelta); + var currentColumn = maniaPlayfield.GetClosestColumnByPosition((moveEvent.Blueprint.ScreenSpaceSelectionPoint + moveEvent.ScreenSpaceDelta).X); if (currentColumn == null) return; diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs index a4ebb3347a42..26fb69354dcb 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics.Containers; using System; using System.Collections.Generic; +using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Graphics.Primitives; @@ -97,30 +98,15 @@ public ManiaPlayfield(List stageDefinitions) public void Add(BarLine barLine) => stages.ForEach(s => s.Add(barLine)); /// - /// Retrieves a column from a screen-space position. + /// Find the closest column to the proposed screen space position. /// - /// The screen-space position. - /// The column which the lies in. - public Column GetColumnByPosition(Vector2 screenSpacePosition) + /// The screen-space X coordinate. + /// The column which the is closest to. + public Column GetClosestColumnByPosition(float screenSpaceX) { - Column found = null; - - foreach (var stage in stages) - { - foreach (var column in stage.Columns) - { - if (column.ReceivePositionalInputAt(new Vector2(screenSpacePosition.X, column.ScreenSpaceDrawQuad.Centre.Y))) - { - found = column; - break; - } - } - - if (found != null) - break; - } - - return found; + return stages + .SelectMany(s => s.Columns.Select((Column column, float distance) (c) => (c, Math.Abs(screenSpaceX - c.ScreenSpaceDrawQuad.Centre.X)))) + .MinBy(c => c.distance).column; } /// diff --git a/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs b/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs index a24249d42c7f..1963614e1dcd 100644 --- a/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs @@ -48,7 +48,7 @@ public abstract partial class HitObjectPlacementBlueprint : PlacementBlueprint private HitObject? getPreviousHitObject() => beatmap.HitObjects.TakeWhile(h => h.StartTime <= startTimeBindable.Value).LastOrDefault(); - protected override bool IsValidForPlacement => HitObject.StartTime >= beatmap.ControlPointInfo.TimingPoints.FirstOrDefault()?.Time; + protected override bool IsValidForPlacement => base.IsValidForPlacement && HitObject.StartTime >= beatmap.ControlPointInfo.TimingPoints.FirstOrDefault()?.Time; [Resolved] private IPlacementHandler placementHandler { get; set; } = null!; diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs index f2d501d1c401..bc0766a9a88f 100644 --- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.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.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Bindings; @@ -29,13 +30,16 @@ public abstract partial class PlacementBlueprint : VisibilityContainer, IKeyBind /// Override this with any preconditions that should be double-checked on committing. /// If false is returned and a commit is attempted, the blueprint will be destroyed instead. /// - protected virtual bool IsValidForPlacement => true; + protected virtual bool IsValidForPlacement => PlacementActive != PlacementState.Waiting || hitObjectComposer.CursorInPlacementArea; // the blueprint should still be considered for input even if it is hidden, // especially when such input is the reason for making the blueprint become visible. public override bool PropagatePositionalInputSubTree => true; public override bool PropagateNonPositionalInputSubTree => true; + [Resolved] + private HitObjectComposer hitObjectComposer { get; set; } = null!; + protected PlacementBlueprint() { RelativeSizeAxes = Axes.Both; @@ -69,6 +73,9 @@ public virtual void EndPlacement(bool commit) case PlacementState.Waiting: // ensure placement was started before ending to make state handling simpler. + if (!IsValidForPlacement) + return; + BeginPlacement(); break; } diff --git a/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs index e8de1eaad950..b974197545d1 100644 --- a/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs @@ -24,6 +24,7 @@ public abstract partial class EditorBlueprintContainer : BlueprintContainer