1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-18 01:23:05 +08:00
osu-lazer/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs

285 lines
12 KiB
C#
Raw Normal View History

// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
2024-07-03 18:36:12 +08:00
using osu.Framework.Allocation;
2020-09-29 18:43:50 +08:00
using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
2022-08-12 07:17:33 +08:00
using osu.Framework.Graphics.UserInterface;
2022-08-15 23:18:55 +08:00
using osu.Framework.Input.Events;
2020-09-30 12:02:05 +08:00
using osu.Framework.Utils;
using osu.Game.Extensions;
2022-08-12 07:17:33 +08:00
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
2020-10-09 05:32:33 +08:00
using osu.Game.Rulesets.Objects;
2020-09-29 19:00:19 +08:00
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
2024-07-04 01:08:31 +08:00
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Utils;
using osuTK;
2022-08-15 23:18:55 +08:00
using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Edit
{
2022-11-24 13:32:20 +08:00
public partial class OsuSelectionHandler : EditorSelectionHandler
{
2023-12-30 08:38:08 +08:00
[Resolved]
private OsuGridToolboxGroup gridToolbox { get; set; } = null!;
protected override void OnSelectionChanged()
{
base.OnSelectionChanged();
2020-09-29 18:43:50 +08:00
Quad quad = selectedMovableObjects.Length > 0 ? GeometryUtils.GetSurroundingQuad(selectedMovableObjects) : new Quad();
2024-01-20 08:13:01 +08:00
SelectionBox.CanFlipX = quad.Width > 0;
SelectionBox.CanFlipY = quad.Height > 0;
SelectionBox.CanReverse = EditorBeatmap.SelectedHitObjects.Count > 1 || EditorBeatmap.SelectedHitObjects.Any(s => s is Slider);
}
2020-09-29 19:08:28 +08:00
2022-08-15 23:18:55 +08:00
protected override bool OnKeyDown(KeyDownEvent e)
{
if (e.Key == Key.M && e.ControlPressed && e.ShiftPressed)
{
mergeSelection();
return true;
}
return false;
}
public override bool HandleMovement(MoveSelectionEvent<HitObject> moveEvent)
{
2021-02-24 03:58:46 +08:00
var hitObjects = selectedMovableObjects;
// this will potentially move the selection out of bounds...
2021-02-24 03:58:46 +08:00
foreach (var h in hitObjects)
h.Position += this.ScreenSpaceDeltaToParentSpace(moveEvent.ScreenSpaceDelta);
2021-02-24 03:58:46 +08:00
// but this will be corrected.
moveSelectionInBounds();
2021-02-24 03:58:46 +08:00
return true;
}
2020-09-29 19:08:28 +08:00
2020-10-09 05:32:33 +08:00
public override bool HandleReverse()
{
var hitObjects = EditorBeatmap.SelectedHitObjects
2024-03-28 17:12:27 +08:00
.OfType<OsuHitObject>()
.OrderBy(obj => obj.StartTime)
.ToList();
2020-10-09 05:32:33 +08:00
double endTime = hitObjects.Max(h => h.GetEndTime());
double startTime = hitObjects.Min(h => h.StartTime);
2020-11-13 07:36:47 +08:00
bool moreThanOneObject = hitObjects.Count > 1;
2020-10-09 05:32:33 +08:00
2024-03-28 17:12:27 +08:00
// the expectation is that even if the objects themselves are reversed temporally,
// the position of new combos in the selection should remain the same.
// preserve it for later before doing the reversal.
var newComboOrder = hitObjects.Select(obj => obj.NewCombo).ToList();
2020-10-09 05:32:33 +08:00
foreach (var h in hitObjects)
{
if (moreThanOneObject)
h.StartTime = endTime - (h.GetEndTime() - startTime);
if (h is Slider slider)
{
slider.Path.Reverse(out Vector2 offset);
slider.Position += offset;
2020-10-09 05:32:33 +08:00
}
}
2024-03-28 17:12:27 +08:00
// re-order objects by start time again after reversing, and restore new combo flag positioning
hitObjects = hitObjects.OrderBy(obj => obj.StartTime).ToList();
2024-03-28 17:12:27 +08:00
for (int i = 0; i < hitObjects.Count; ++i)
hitObjects[i].NewCombo = newComboOrder[i];
2024-03-25 15:19:14 +08:00
2020-10-09 05:32:33 +08:00
return true;
}
2022-01-05 15:46:34 +08:00
public override bool HandleFlip(Direction direction, bool flipOverOrigin)
2020-10-01 15:24:50 +08:00
{
var hitObjects = selectedMovableObjects;
// If we're flipping over the origin, we take the grid origin position from the grid toolbox.
2023-12-30 08:38:08 +08:00
var flipQuad = flipOverOrigin ? new Quad(gridToolbox.StartPositionX.Value, gridToolbox.StartPositionY.Value, 0, 0) : GeometryUtils.GetSurroundingQuad(hitObjects);
// If we're flipping over the origin, we take the grid rotation from the grid toolbox.
// We want to normalize the rotation angle to -45 to 45 degrees, so horizontal vs vertical flip is not mixed up by the rotation and it stays intuitive to use.
var flipAxis = flipOverOrigin ? GeometryUtils.RotateVector(Vector2.UnitX, -((gridToolbox.GridLinesRotation.Value + 405) % 90 - 45)) : Vector2.UnitX;
2023-12-30 08:38:08 +08:00
flipAxis = direction == Direction.Vertical ? flipAxis.PerpendicularLeft : flipAxis;
2023-12-30 08:38:08 +08:00
var controlPointFlipQuad = new Quad();
2020-10-01 15:24:50 +08:00
bool didFlip = false;
2020-10-01 15:24:50 +08:00
foreach (var h in hitObjects)
{
2023-12-30 08:38:08 +08:00
var flippedPosition = GeometryUtils.GetFlippedPosition(flipAxis, flipQuad, h.Position);
2024-07-04 01:08:31 +08:00
// Clamp the flipped position inside the playfield bounds, because the flipped position might be outside the playfield bounds if the origin is not centered.
flippedPosition = Vector2.Clamp(flippedPosition, Vector2.Zero, OsuPlayfield.BASE_SIZE);
if (!Precision.AlmostEquals(flippedPosition, h.Position))
{
h.Position = flippedPosition;
didFlip = true;
}
2020-10-01 15:24:50 +08:00
if (h is Slider slider)
{
didFlip = true;
2022-09-15 18:55:18 +08:00
foreach (var cp in slider.Path.ControlPoints)
2023-12-30 08:38:08 +08:00
cp.Position = GeometryUtils.GetFlippedPosition(flipAxis, controlPointFlipQuad, cp.Position);
2020-10-01 15:24:50 +08:00
}
}
return didFlip;
2020-10-01 15:24:50 +08:00
}
public override SelectionRotationHandler CreateRotationHandler() => new OsuSelectionRotationHandler();
public override SelectionScaleHandler CreateScaleHandler() => new OsuSelectionScaleHandler();
private void moveSelectionInBounds()
{
var hitObjects = selectedMovableObjects;
Quad quad = GeometryUtils.GetSurroundingQuad(hitObjects);
Vector2 delta = Vector2.Zero;
if (quad.TopLeft.X < 0)
delta.X -= quad.TopLeft.X;
if (quad.TopLeft.Y < 0)
delta.Y -= quad.TopLeft.Y;
if (quad.BottomRight.X > DrawWidth)
delta.X -= quad.BottomRight.X - DrawWidth;
if (quad.BottomRight.Y > DrawHeight)
delta.Y -= quad.BottomRight.Y - DrawHeight;
foreach (var h in hitObjects)
2020-09-29 18:43:50 +08:00
h.Position += delta;
}
/// <summary>
/// All osu! hitobjects which can be moved/rotated/scaled.
/// </summary>
private OsuHitObject[] selectedMovableObjects => SelectedItems.OfType<OsuHitObject>()
.Where(h => h is not Spinner)
2020-10-09 17:50:05 +08:00
.ToArray();
2022-08-12 07:17:33 +08:00
/// <summary>
/// All osu! hitobjects which can be merged.
/// </summary>
private OsuHitObject[] selectedMergeableObjects => SelectedItems.OfType<OsuHitObject>()
.Where(h => h is HitCircle or Slider)
.OrderBy(h => h.StartTime)
.ToArray();
private void mergeSelection()
{
var mergeableObjects = selectedMergeableObjects;
if (!canMerge(mergeableObjects))
2022-08-12 07:17:33 +08:00
return;
2022-08-27 23:43:32 +08:00
EditorBeatmap.BeginChange();
2022-08-12 07:17:33 +08:00
// Have an initial slider object.
var firstHitObject = mergeableObjects[0];
2022-08-12 07:17:33 +08:00
var mergedHitObject = firstHitObject as Slider ?? new Slider
{
StartTime = firstHitObject.StartTime,
Position = firstHitObject.Position,
NewCombo = firstHitObject.NewCombo,
Samples = firstHitObject.Samples,
2022-08-12 07:17:33 +08:00
};
if (mergedHitObject.Path.ControlPoints.Count == 0)
{
mergedHitObject.Path.ControlPoints.Add(new PathControlPoint(Vector2.Zero, PathType.LINEAR));
2022-08-12 07:17:33 +08:00
}
// Merge all the selected hit objects into one slider path.
bool lastCircle = firstHitObject is HitCircle;
foreach (var selectedMergeableObject in mergeableObjects.Skip(1))
2022-08-12 07:17:33 +08:00
{
if (selectedMergeableObject is IHasPath hasPath)
{
var offset = lastCircle ? selectedMergeableObject.Position - mergedHitObject.Position : mergedHitObject.Path.ControlPoints[^1].Position;
float distanceToLastControlPoint = Vector2.Distance(mergedHitObject.Path.ControlPoints[^1].Position, offset);
// Calculate the distance required to travel to the expected distance of the merging slider.
mergedHitObject.Path.ExpectedDistance.Value = mergedHitObject.Path.CalculatedDistance + distanceToLastControlPoint + hasPath.Path.Distance;
// Remove the last control point if it sits exactly on the start of the next control point.
if (Precision.AlmostEquals(distanceToLastControlPoint, 0))
{
mergedHitObject.Path.ControlPoints.RemoveAt(mergedHitObject.Path.ControlPoints.Count - 1);
}
mergedHitObject.Path.ControlPoints.AddRange(hasPath.Path.ControlPoints.Select(o => new PathControlPoint(o.Position + offset, o.Type)));
lastCircle = false;
}
else
{
// Turn the last control point into a linear type if this is the first merging circle in a sequence, so the subsequent control points can be inherited path type.
if (!lastCircle)
{
mergedHitObject.Path.ControlPoints.Last().Type = PathType.LINEAR;
2022-08-12 07:17:33 +08:00
}
mergedHitObject.Path.ControlPoints.Add(new PathControlPoint(selectedMergeableObject.Position - mergedHitObject.Position));
mergedHitObject.Path.ExpectedDistance.Value = null;
lastCircle = true;
}
}
// Make sure only the merged hit object is in the beatmap.
if (firstHitObject is Slider)
{
foreach (var selectedMergeableObject in mergeableObjects.Skip(1))
2022-08-12 07:17:33 +08:00
{
EditorBeatmap.Remove(selectedMergeableObject);
2022-08-12 07:17:33 +08:00
}
}
else
{
foreach (var selectedMergeableObject in mergeableObjects)
2022-08-12 07:17:33 +08:00
{
EditorBeatmap.Remove(selectedMergeableObject);
2022-08-12 07:17:33 +08:00
}
EditorBeatmap.Add(mergedHitObject);
2022-08-12 07:17:33 +08:00
}
// Make sure the merged hitobject is selected.
SelectedItems.Clear();
SelectedItems.Add(mergedHitObject);
2022-08-27 23:43:32 +08:00
EditorBeatmap.EndChange();
2022-08-12 07:17:33 +08:00
}
protected override IEnumerable<MenuItem> GetContextMenuItemsForSelection(IEnumerable<SelectionBlueprint<HitObject>> selection)
{
foreach (var item in base.GetContextMenuItemsForSelection(selection))
yield return item;
if (canMerge(selectedMergeableObjects))
2022-08-12 07:17:33 +08:00
yield return new OsuMenuItem("Merge selection", MenuItemType.Destructive, mergeSelection);
}
2022-08-30 01:51:42 +08:00
private bool canMerge(IReadOnlyList<OsuHitObject> objects) =>
objects.Count > 1
&& (objects.Any(h => h is Slider)
|| objects.Zip(objects.Skip(1), (h1, h2) => Precision.DefinitelyBigger(Vector2.DistanceSquared(h1.Position, h2.Position), 1)).Any(x => x));
}
}