From 277d53a4f1719da16cd6edf7437962e5ce8caf4b Mon Sep 17 00:00:00 2001 From: Arthur Araujo <90941580+64ArthurAraujo@users.noreply.github.com> Date: Tue, 10 Mar 2026 00:36:32 -0300 Subject: [PATCH] Adjust all selected hold notes if they have the same StartTime and Duration (#36656) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses #36267 The drag now checks all the selected objects in the blueprint container to see if they have the same `StartTime` and `Duration` as the dragged note, and if so, adjust them accordingly. --------- Co-authored-by: Bartłomiej Dach --- .../Editor/TestSceneHoldNoteTailDrag.cs | 351 ++++++++++++++++++ .../Timeline/TimelineHitObjectBlueprint.cs | 32 +- 2 files changed, 381 insertions(+), 2 deletions(-) create mode 100644 osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNoteTailDrag.cs diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNoteTailDrag.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNoteTailDrag.cs new file mode 100644 index 0000000000..bdbf24bb95 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNoteTailDrag.cs @@ -0,0 +1,351 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Testing; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Tests.Visual; +using osuTK; +using osuTK.Input; +using DragArea = osu.Game.Screens.Edit.Compose.Components.Timeline.TimelineHitObjectBlueprint.DragArea; + +namespace osu.Game.Rulesets.Mania.Tests.Editor +{ + public partial class TestSceneHoldNoteTailDrag : EditorTestScene + { + protected override Ruleset CreateEditorRuleset() => new ManiaRuleset(); + + [SetUpSteps] + public override void SetUpSteps() + { + base.SetUpSteps(); + AddStep("Clear objects", () => EditorBeatmap.Clear()); + } + + [Test] + public void TestSimpleTailDragForward() + { + AddStep("Add hold note", () => + { + EditorBeatmap.Add(new HoldNote { StartTime = 2170, Duration = 937.5 }); + }); + + AddStep("Drag tail", () => + { + var blueprintDragArea = this.ChildrenOfType().Single(); + dragForward(blueprintDragArea); + }); + + AddStep("Release tail", () => InputManager.ReleaseButton(MouseButton.Left)); + + AddAssert("Duration is higher", () => ((HoldNote)EditorBeatmap.HitObjects.First())!.Duration > 937.5f); + } + + [Test] + public void TestSimpleTailDragBackwards() + { + AddStep("Add hold note", () => + { + EditorBeatmap.Add(new HoldNote { StartTime = 2170, Duration = 937.5 }); + }); + + AddStep("Drag tail", () => + { + var blueprintDragArea = this.ChildrenOfType().Single(); + dragBackward(blueprintDragArea); + }); + + AddStep("Release tail", () => InputManager.ReleaseButton(MouseButton.Left)); + + AddAssert("Duration is lower", () => ((HoldNote)EditorBeatmap.HitObjects[0]).Duration < 937.5f); + } + + [Test] + public void TestSamePositionButNotSelectedDragForward() + { + AddStep("Add hold notes", () => + { + EditorBeatmap.AddRange([ + new HoldNote { StartTime = 2170, Duration = 937.5, Column = 0 }, + new HoldNote { StartTime = 2170, Duration = 937.5, Column = 1 } + ]); + }); + + AddStep("Drag tail", () => + { + var blueprintDragArea = this.ChildrenOfType().First(); + dragForward(blueprintDragArea); + }); + + AddStep("Release tail", () => InputManager.ReleaseButton(MouseButton.Left)); + + AddAssert("Duration is higher, other is unchanged", () => + ((HoldNote)EditorBeatmap.HitObjects[0]).Duration > 937.5f && + ((HoldNote)EditorBeatmap.HitObjects[^1]).Duration == 937.5f + ); + } + + [Test] + public void TestSamePositionButNotSelectedDragBackward() + { + AddStep("Add hold notes", () => + { + EditorBeatmap.AddRange([ + new HoldNote { StartTime = 2170, Duration = 937.5, Column = 0 }, + new HoldNote { StartTime = 2170, Duration = 937.5, Column = 1 } + ]); + }); + + AddStep("Drag tail", () => + { + var blueprintDragArea = this.ChildrenOfType().First(); + dragBackward(blueprintDragArea); + }); + + AddStep("Release tail", () => InputManager.ReleaseButton(MouseButton.Left)); + + AddAssert("Duration is lower, other is unchanged", () => + ((HoldNote)EditorBeatmap.HitObjects[0]).Duration < 937.5f && + ((HoldNote)EditorBeatmap.HitObjects[^1]).Duration == 937.5f + ); + } + + [Test] + public void TestSamePositionSelectedDragForward() + { + AddStep("Add hold notes", () => + { + EditorBeatmap.AddRange([ + new HoldNote { StartTime = 2170, Duration = 937.5, Column = 0 }, + new HoldNote { StartTime = 2170, Duration = 937.5, Column = 1 } + ]); + }); + + AddStep("Select all", () => + { + EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects); + }); + + AddStep("Drag tail", () => + { + var blueprintDragArea = this.ChildrenOfType().First(); + dragForward(blueprintDragArea); + }); + + AddStep("Release tail", () => InputManager.ReleaseButton(MouseButton.Left)); + + AddAssert("Both durations are higher", () => + ((HoldNote)EditorBeatmap.HitObjects[0]).Duration > 937.5f && + ((HoldNote)EditorBeatmap.HitObjects[^1]).Duration > 937.5f + ); + } + + [Test] + public void TestSamePositionSelectedDragBackward() + { + AddStep("Add hold notes", () => + { + EditorBeatmap.AddRange([ + new HoldNote { StartTime = 2170, Duration = 937.5, Column = 0 }, + new HoldNote { StartTime = 2170, Duration = 937.5, Column = 1 } + ]); + }); + + AddStep("Select all", () => + { + EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects); + }); + + AddStep("Drag tail", () => + { + var blueprintDragArea = this.ChildrenOfType().First(); + dragBackward(blueprintDragArea); + }); + + AddStep("Release tail", () => InputManager.ReleaseButton(MouseButton.Left)); + + AddAssert("Both durations are lower", () => + ((HoldNote)EditorBeatmap.HitObjects[0]).Duration < 937.5f && + ((HoldNote)EditorBeatmap.HitObjects[^1]).Duration < 937.5f + ); + } + + [Test] + public void TestSelectedButDifferentPositions() + { + AddStep("Add hold notes", () => + { + EditorBeatmap.AddRange([ + new HoldNote { StartTime = 2170, Duration = 937.5, Column = 0 }, + new HoldNote { StartTime = 2404, Duration = 937.5, Column = 1 } + ]); + }); + + AddStep("Select all", () => + { + EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects); + }); + + AddStep("Drag tail", () => + { + var blueprintDragArea = this.ChildrenOfType().First(); + dragBackward(blueprintDragArea); + }); + + AddStep("Release tail", () => InputManager.ReleaseButton(MouseButton.Left)); + + AddAssert("Duration is unchanged, other is lower", () => + ((HoldNote)EditorBeatmap.HitObjects[0]).Duration == 937.5f && + ((HoldNote)EditorBeatmap.HitObjects[^1]).Duration < 937.5f + ); + } + + [Test] + public void TestSelectedSameStartTimeDifferentDurations() + { + AddStep("Add hold notes", () => + { + EditorBeatmap.AddRange([ + new HoldNote { StartTime = 2170, Duration = 937.5, Column = 0 }, + new HoldNote { StartTime = 2170, Duration = 1171.8, Column = 1 } + ]); + }); + + AddStep("Select all", () => + { + EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects); + }); + + AddStep("Drag until both match", () => + { + var blueprintDragArea = this.ChildrenOfType().First(); + InputManager.MoveMouseTo(blueprintDragArea); + InputManager.PressKey(Key.LShift); + InputManager.PressButton(MouseButton.Left); + InputManager.MoveMouseTo(new Vector2(1000, 110)); + }); + + AddStep("Continue the drag", () => + { + var blueprintDragArea = this.ChildrenOfType().First(); + dragBackward(blueprintDragArea); + }); + + AddStep("Release tail", () => InputManager.ReleaseButton(MouseButton.Left)); + + AddAssert("Duration is unchanged, other is lower", () => + ((HoldNote)EditorBeatmap.HitObjects[0]).Duration == 937.5f && + ((HoldNote)EditorBeatmap.HitObjects[^1]).Duration < 937.5f + ); + } + + [Test] + public void TestSelectedSameDurationDifferentStartTimes() + { + AddStep("Add hold notes", () => + { + EditorBeatmap.AddRange([ + new HoldNote { StartTime = 2170, Duration = 937.5, Column = 0 }, + new HoldNote { StartTime = 2638.7, Duration = 937.5, Column = 1 } + ]); + }); + + AddStep("Select all", () => + { + EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects); + }); + + AddStep("Drag tail", () => + { + var blueprintDragArea = this.ChildrenOfType().First(); + dragBackward(blueprintDragArea); + }); + + AddStep("Release tail", () => InputManager.ReleaseButton(MouseButton.Left)); + + AddAssert("Duration is unchanged, other is lower", () => + ((HoldNote)EditorBeatmap.HitObjects[0]).Duration == 937.5f && + ((HoldNote)EditorBeatmap.HitObjects[^1]).Duration < 937.5f + ); + } + + [Test] + public void TestDragNoteOutsideOfSelection() + { + AddStep("Add hold notes", () => + { + EditorBeatmap.AddRange([ + new HoldNote { StartTime = 2170, Duration = 937.5, Column = 0 }, + new HoldNote { StartTime = 2170, Duration = 937.5, Column = 1 } + ]); + }); + + AddStep("Select the back stack slider", () => + { + EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.Last()); + }); + + AddStep("Drag tail", () => + { + var blueprintDragArea = this.ChildrenOfType().First(); + dragBackward(blueprintDragArea); + }); + + AddStep("Release tail", () => InputManager.ReleaseButton(MouseButton.Left)); + + AddAssert("Duration is lower, other is unchanged", () => + ((HoldNote)EditorBeatmap.HitObjects[0]).Duration < 937.5f && + ((HoldNote)EditorBeatmap.HitObjects[^1]).Duration == 937.5f + ); + } + + [Test] + public void TestDragHoldNoteWithNotes() + { + AddStep("Add notes", () => + { + EditorBeatmap.AddRange([ + new HoldNote { StartTime = 2170, Duration = 937.5, Column = 0 }, + new Note { StartTime = 2170, Column = 1 }, + new Note { StartTime = 3107.5, Column = 2 }, + new HoldNote { StartTime = 2170, Duration = 937.5, Column = 3 } + ]); + }); + + AddStep("Select all", () => + { + EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects); + }); + + AddStep("Drag tail", () => + { + var blueprintDragArea = this.ChildrenOfType().First(); + dragBackward(blueprintDragArea); + }); + + AddStep("Release tail", () => InputManager.ReleaseButton(MouseButton.Left)); + + AddAssert("Both durations are lower", () => + { + var holdNotes = EditorBeatmap.HitObjects.OfType(); + return holdNotes.First().Duration < 937.5f && holdNotes.Last().Duration < 937.5f; + } + ); + } + + private void dragForward(DragArea dragArea) + { + InputManager.MoveMouseTo(dragArea); + InputManager.PressButton(MouseButton.Left); + InputManager.MoveMouseTo(new Vector2(1100, 110)); + } + + private void dragBackward(DragArea dragArea) + { + InputManager.MoveMouseTo(dragArea); + InputManager.PressButton(MouseButton.Left); + InputManager.MoveMouseTo(new Vector2(700, 110)); + } + } +} diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs index f60d1b023b..9f02800993 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -312,6 +313,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { private readonly HitObject? hitObject; + private readonly List objsToAdjust = new List(); + [Resolved] private EditorBeatmap beatmap { get; set; } = null!; @@ -404,6 +407,23 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline protected override bool OnDragStart(DragStartEvent e) { changeHandler?.BeginChange(); + + var selectionItems = beatmap.SelectedHitObjects; + + if (!selectionItems.Contains(hitObject)) + return true; + + foreach (var item in selectionItems) + { + if (item == hitObject || item is not IHasDuration durationItem) continue; + + if (Precision.AlmostEquals(durationItem.Duration, (hitObject as IHasDuration)!.Duration, 1) && + Precision.AlmostEquals(item.StartTime, hitObject!.StartTime, 1)) + { + objsToAdjust.Add(item); + } + } + return true; } @@ -456,8 +476,15 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (endTimeHitObject.EndTime == snappedTime) return; - endTimeHitObject.Duration = snappedTime - hitObject.StartTime; - beatmap.Update(hitObject); + if (!objsToAdjust.Contains(hitObject)) + objsToAdjust.Add(hitObject); + + foreach (var obj in objsToAdjust) + { + (obj as IHasDuration)!.Duration = snappedTime - obj.StartTime; + beatmap.Update(obj); + } + break; } } @@ -473,6 +500,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline changeHandler?.EndChange(); OnDragHandled?.Invoke(null); + objsToAdjust.Clear(); } }