From 60f876511d5de96629fe3a2e53b83e02d7e8a34f Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 21 Jul 2021 15:36:34 +0900 Subject: [PATCH 1/6] Add function of computing position range occupied by hit objects --- .../Edit/CatchHitObjectUtils.cs | 39 +++++++++++++++++++ osu.Game.Rulesets.Catch/Edit/PositionRange.cs | 33 ++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 osu.Game.Rulesets.Catch/Edit/PositionRange.cs diff --git a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectUtils.cs b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectUtils.cs index beffdf0362..b059926668 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectUtils.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectUtils.cs @@ -1,7 +1,10 @@ // 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.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.UI.Scrolling; using osuTK; @@ -20,5 +23,41 @@ namespace osu.Game.Rulesets.Catch.Edit { return new Vector2(hitObject.OriginalX, hitObjectContainer.PositionAtTime(hitObject.StartTime)); } + + /// + /// Get the range of horizontal position occupied by the hit object. + /// + /// + /// s are excluded and returns . + /// + public static PositionRange GetPositionRange(HitObject hitObject) + { + switch (hitObject) + { + case Fruit fruit: + return new PositionRange(fruit.OriginalX); + + case Droplet droplet: + return droplet is TinyDroplet ? PositionRange.EMPTY : new PositionRange(droplet.OriginalX); + + case JuiceStream _: + return GetPositionRange(hitObject.NestedHitObjects); + + case BananaShower _: + // A banana shower occupies the whole screen width. + return new PositionRange(0, CatchPlayfield.WIDTH); + + default: + return PositionRange.EMPTY; + } + } + + /// + /// Get the range of horizontal position occupied by the hit objects. + /// + /// + /// s are excluded. + /// + public static PositionRange GetPositionRange(IEnumerable hitObjects) => hitObjects.Select(GetPositionRange).Aggregate(PositionRange.EMPTY, PositionRange.Union); } } diff --git a/osu.Game.Rulesets.Catch/Edit/PositionRange.cs b/osu.Game.Rulesets.Catch/Edit/PositionRange.cs new file mode 100644 index 0000000000..6ed1ac2c06 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Edit/PositionRange.cs @@ -0,0 +1,33 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; + +#nullable enable + +namespace osu.Game.Rulesets.Catch.Edit +{ + /// + /// Represents a closed interval of horizontal positions in the playfield. + /// + public readonly struct PositionRange + { + public readonly float Min; + public readonly float Max; + + public PositionRange(float value) + : this(value, value) + { + } + + public PositionRange(float min, float max) + { + Min = min; + Max = max; + } + + public static PositionRange Union(PositionRange a, PositionRange b) => new PositionRange(Math.Min(a.Min, b.Min), Math.Max(a.Max, b.Max)); + + public static readonly PositionRange EMPTY = new PositionRange(float.PositiveInfinity, float.NegativeInfinity); + } +} From 4c8b9c168e4c32320aa3affdd874688663419158 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 21 Jul 2021 15:37:42 +0900 Subject: [PATCH 2/6] Use added position range computation in hit object move handling --- .../Edit/CatchSelectionHandler.cs | 46 ++----------------- 1 file changed, 3 insertions(+), 43 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs b/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs index 7eebf04ca2..ffb342f772 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs @@ -59,20 +59,12 @@ namespace osu.Game.Rulesets.Catch.Edit /// The positional movement with the restriction applied. private float limitMovement(float deltaX, IEnumerable movingObjects) { - float minX = float.PositiveInfinity; - float maxX = float.NegativeInfinity; - - foreach (float x in movingObjects.SelectMany(getOriginalPositions)) - { - minX = Math.Min(minX, x); - maxX = Math.Max(maxX, x); - } - + var range = CatchHitObjectUtils.GetPositionRange(movingObjects); // To make an object with position `x` stay in bounds after `deltaX` movement, `0 <= x + deltaX <= WIDTH` should be satisfied. // Subtracting `x`, we get `-x <= deltaX <= WIDTH - x`. // We only need to apply the inequality to extreme values of `x`. - float lowerBound = -minX; - float upperBound = CatchPlayfield.WIDTH - maxX; + float lowerBound = -range.Min; + float upperBound = CatchPlayfield.WIDTH - range.Max; // The inequality may be unsatisfiable if the objects were already out of bounds. // In that case, don't move objects at all. if (lowerBound > upperBound) @@ -80,37 +72,5 @@ namespace osu.Game.Rulesets.Catch.Edit return Math.Clamp(deltaX, lowerBound, upperBound); } - - /// - /// Enumerate X positions that should be contained in-bounds after move offset is applied. - /// - private IEnumerable getOriginalPositions(HitObject hitObject) - { - switch (hitObject) - { - case Fruit fruit: - yield return fruit.OriginalX; - - break; - - case JuiceStream juiceStream: - foreach (var nested in juiceStream.NestedHitObjects.OfType()) - { - // Even if `OriginalX` is outside the playfield, tiny droplets can be moved inside the playfield after the random offset application. - if (!(nested is TinyDroplet)) - yield return nested.OriginalX; - } - - break; - - case BananaShower _: - // A banana shower occupies the whole screen width. - // If the selection contains a banana shower, the selection cannot be moved horizontally. - yield return 0; - yield return CatchPlayfield.WIDTH; - - break; - } - } } } From d2d3214d47958703be35f6117b15bfd1541d2a1d Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 21 Jul 2021 15:47:16 +0900 Subject: [PATCH 3/6] Implement horizontal flipping of hit objects in catch editor --- .../Edit/CatchSelectionHandler.cs | 43 +++++++++++++++++++ osu.Game.Rulesets.Catch/Edit/PositionRange.cs | 2 + 2 files changed, 45 insertions(+) diff --git a/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs b/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs index ffb342f772..50290ae292 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs @@ -12,6 +12,7 @@ using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Screens.Edit.Compose.Components; using osuTK; +using Direction = osu.Framework.Graphics.Direction; namespace osu.Game.Rulesets.Catch.Edit { @@ -51,6 +52,26 @@ namespace osu.Game.Rulesets.Catch.Edit return true; } + public override bool HandleFlip(Direction direction) + { + var selectionRange = CatchHitObjectUtils.GetPositionRange(EditorBeatmap.SelectedHitObjects); + + bool changed = false; + EditorBeatmap.PerformOnSelection(h => + { + if (h is CatchHitObject hitObject) + changed |= handleFlip(selectionRange, hitObject); + }); + return changed; + } + + protected override void OnSelectionChanged() + { + base.OnSelectionChanged(); + + SelectionBox.CanFlipX = true; + } + /// /// Limit positional movement of the objects by the constraint that moved objects should stay in bounds. /// @@ -72,5 +93,27 @@ namespace osu.Game.Rulesets.Catch.Edit return Math.Clamp(deltaX, lowerBound, upperBound); } + + private bool handleFlip(PositionRange selectionRange, CatchHitObject hitObject) + { + switch (hitObject) + { + case BananaShower _: + return false; + + case JuiceStream juiceStream: + juiceStream.OriginalX = selectionRange.GetFlippedPosition(juiceStream.OriginalX); + + foreach (var point in juiceStream.Path.ControlPoints) + point.Position.Value *= new Vector2(-1, 1); + + EditorBeatmap.Update(juiceStream); + return true; + + default: + hitObject.OriginalX = selectionRange.GetFlippedPosition(hitObject.OriginalX); + return true; + } + } } } diff --git a/osu.Game.Rulesets.Catch/Edit/PositionRange.cs b/osu.Game.Rulesets.Catch/Edit/PositionRange.cs index 6ed1ac2c06..64e789483a 100644 --- a/osu.Game.Rulesets.Catch/Edit/PositionRange.cs +++ b/osu.Game.Rulesets.Catch/Edit/PositionRange.cs @@ -28,6 +28,8 @@ namespace osu.Game.Rulesets.Catch.Edit public static PositionRange Union(PositionRange a, PositionRange b) => new PositionRange(Math.Min(a.Min, b.Min), Math.Max(a.Max, b.Max)); + public float GetFlippedPosition(float x) => Max - (x - Min); + public static readonly PositionRange EMPTY = new PositionRange(float.PositiveInfinity, float.NegativeInfinity); } } From 7b6981c632d0f3b649c459f9cef67b7907863076 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Thu, 22 Jul 2021 13:06:48 +0900 Subject: [PATCH 4/6] Don't show the flip button when flipping is a no-op --- osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs | 3 ++- osu.Game.Rulesets.Catch/Edit/PositionRange.cs | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs b/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs index 50290ae292..0262843c43 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs @@ -69,7 +69,8 @@ namespace osu.Game.Rulesets.Catch.Edit { base.OnSelectionChanged(); - SelectionBox.CanFlipX = true; + var selectionRange = CatchHitObjectUtils.GetPositionRange(EditorBeatmap.SelectedHitObjects); + SelectionBox.CanFlipX = selectionRange.Length > 0 && EditorBeatmap.SelectedHitObjects.Any(h => h is CatchHitObject && !(h is BananaShower)); } /// diff --git a/osu.Game.Rulesets.Catch/Edit/PositionRange.cs b/osu.Game.Rulesets.Catch/Edit/PositionRange.cs index 64e789483a..543e773582 100644 --- a/osu.Game.Rulesets.Catch/Edit/PositionRange.cs +++ b/osu.Game.Rulesets.Catch/Edit/PositionRange.cs @@ -15,6 +15,8 @@ namespace osu.Game.Rulesets.Catch.Edit public readonly float Min; public readonly float Max; + public float Length => Max - Min; + public PositionRange(float value) : this(value, value) { From 19657cd00e461e4758ee6127edc61e2fa6f51b1f Mon Sep 17 00:00:00 2001 From: ekrctb Date: Thu, 22 Jul 2021 13:28:40 +0900 Subject: [PATCH 5/6] Guard against empty range in `PositionRange` --- osu.Game.Rulesets.Catch/Edit/PositionRange.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Edit/PositionRange.cs b/osu.Game.Rulesets.Catch/Edit/PositionRange.cs index 543e773582..e61603e5e6 100644 --- a/osu.Game.Rulesets.Catch/Edit/PositionRange.cs +++ b/osu.Game.Rulesets.Catch/Edit/PositionRange.cs @@ -8,14 +8,15 @@ using System; namespace osu.Game.Rulesets.Catch.Edit { /// - /// Represents a closed interval of horizontal positions in the playfield. + /// Represents either the empty range or a closed interval of horizontal positions in the playfield. + /// A represents a closed interval if it is <= , and represents the empty range otherwise. /// public readonly struct PositionRange { public readonly float Min; public readonly float Max; - public float Length => Max - Min; + public float Length => Math.Max(0, Max - Min); public PositionRange(float value) : this(value, value) @@ -30,7 +31,11 @@ namespace osu.Game.Rulesets.Catch.Edit public static PositionRange Union(PositionRange a, PositionRange b) => new PositionRange(Math.Min(a.Min, b.Min), Math.Max(a.Max, b.Max)); - public float GetFlippedPosition(float x) => Max - (x - Min); + /// + /// Get the given position flipped (mirrored) for the axis at the center of this range. + /// Returns the given position unchanged if the range was empty. + /// + public float GetFlippedPosition(float x) => Min <= Max ? Max - (x - Min) : x; public static readonly PositionRange EMPTY = new PositionRange(float.PositiveInfinity, float.NegativeInfinity); } From 2151c1863ee58a9832ab092bf2a2f14439ac9db3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Jul 2021 14:07:32 +0900 Subject: [PATCH 6/6] Rename variables for catch-specific casting to avoid any confusion --- osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs b/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs index 0262843c43..8593c452cf 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs @@ -40,12 +40,12 @@ namespace osu.Game.Rulesets.Catch.Edit EditorBeatmap.PerformOnSelection(h => { - if (!(h is CatchHitObject hitObject)) return; + if (!(h is CatchHitObject catchObject)) return; - hitObject.OriginalX += deltaX; + catchObject.OriginalX += deltaX; // Move the nested hit objects to give an instant result before nested objects are recreated. - foreach (var nested in hitObject.NestedHitObjects.OfType()) + foreach (var nested in catchObject.NestedHitObjects.OfType()) nested.OriginalX += deltaX; }); @@ -59,8 +59,8 @@ namespace osu.Game.Rulesets.Catch.Edit bool changed = false; EditorBeatmap.PerformOnSelection(h => { - if (h is CatchHitObject hitObject) - changed |= handleFlip(selectionRange, hitObject); + if (h is CatchHitObject catchObject) + changed |= handleFlip(selectionRange, catchObject); }); return changed; }