2019-01-24 16:43:03 +08:00
|
|
|
// 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.
|
2018-11-16 16:12:24 +08:00
|
|
|
|
2020-09-29 18:50:03 +08:00
|
|
|
using System;
|
2020-09-30 13:41:32 +08:00
|
|
|
using System.Collections.Generic;
|
2018-11-16 16:12:24 +08:00
|
|
|
using System.Linq;
|
2020-09-29 18:43:50 +08:00
|
|
|
using osu.Framework.Graphics;
|
2020-09-29 18:50:03 +08:00
|
|
|
using osu.Framework.Graphics.Primitives;
|
2020-09-30 12:02:05 +08:00
|
|
|
using osu.Framework.Utils;
|
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;
|
2018-11-16 16:12:24 +08:00
|
|
|
using osu.Game.Rulesets.Osu.Objects;
|
|
|
|
using osu.Game.Screens.Edit.Compose.Components;
|
2019-11-06 16:27:41 +08:00
|
|
|
using osuTK;
|
2018-11-16 16:12:24 +08:00
|
|
|
|
|
|
|
namespace osu.Game.Rulesets.Osu.Edit
|
|
|
|
{
|
2018-11-19 15:58:11 +08:00
|
|
|
public class OsuSelectionHandler : SelectionHandler
|
2018-11-16 16:12:24 +08:00
|
|
|
{
|
2020-09-30 12:52:57 +08:00
|
|
|
protected override void OnSelectionChanged()
|
|
|
|
{
|
|
|
|
base.OnSelectionChanged();
|
2020-09-29 18:43:50 +08:00
|
|
|
|
2020-11-13 06:19:29 +08:00
|
|
|
Quad quad = selectedMovableObjects.Length > 0 ? getSurroundingQuad(selectedMovableObjects) : new Quad();
|
2020-09-30 12:52:57 +08:00
|
|
|
|
2020-11-13 06:19:29 +08:00
|
|
|
SelectionBox.CanRotate = quad.Width > 0 || quad.Height > 0;
|
|
|
|
SelectionBox.CanScaleX = quad.Width > 0;
|
|
|
|
SelectionBox.CanScaleY = quad.Height > 0;
|
|
|
|
SelectionBox.CanReverse = EditorBeatmap.SelectedHitObjects.Count > 1 || EditorBeatmap.SelectedHitObjects.Any(s => s is Slider);
|
2020-09-30 12:52:57 +08:00
|
|
|
}
|
2020-09-29 19:08:28 +08:00
|
|
|
|
2020-10-01 15:24:04 +08:00
|
|
|
protected override void OnOperationEnded()
|
2020-09-30 12:52:57 +08:00
|
|
|
{
|
2020-10-01 15:24:04 +08:00
|
|
|
base.OnOperationEnded();
|
2020-09-30 12:52:57 +08:00
|
|
|
referenceOrigin = null;
|
|
|
|
}
|
2020-09-29 18:43:50 +08:00
|
|
|
|
2021-02-22 00:38:50 +08:00
|
|
|
public override bool HandleMovement(MoveSelectionEvent moveEvent)
|
|
|
|
{
|
2021-02-24 03:58:46 +08:00
|
|
|
var hitObjects = selectedMovableObjects;
|
|
|
|
|
|
|
|
foreach (var h in hitObjects)
|
|
|
|
h.Position += moveEvent.InstantDelta;
|
|
|
|
|
2021-02-22 00:38:50 +08:00
|
|
|
moveSelectionInBounds();
|
2021-02-24 03:58:46 +08:00
|
|
|
return true;
|
2021-02-22 00:38:50 +08:00
|
|
|
}
|
2020-09-29 19:08:28 +08:00
|
|
|
|
2020-09-30 12:02:05 +08:00
|
|
|
/// <summary>
|
|
|
|
/// During a transform, the initial origin is stored so it can be used throughout the operation.
|
|
|
|
/// </summary>
|
|
|
|
private Vector2? referenceOrigin;
|
2020-09-29 19:08:28 +08:00
|
|
|
|
2020-10-09 05:32:33 +08:00
|
|
|
public override bool HandleReverse()
|
|
|
|
{
|
2020-11-13 07:36:47 +08:00
|
|
|
var hitObjects = EditorBeatmap.SelectedHitObjects;
|
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
|
|
|
|
|
|
|
foreach (var h in hitObjects)
|
|
|
|
{
|
|
|
|
if (moreThanOneObject)
|
|
|
|
h.StartTime = endTime - (h.GetEndTime() - startTime);
|
|
|
|
|
|
|
|
if (h is Slider slider)
|
|
|
|
{
|
|
|
|
var points = slider.Path.ControlPoints.ToArray();
|
|
|
|
Vector2 endPos = points.Last().Position.Value;
|
|
|
|
|
|
|
|
slider.Path.ControlPoints.Clear();
|
|
|
|
|
|
|
|
slider.Position += endPos;
|
|
|
|
|
|
|
|
PathType? lastType = null;
|
|
|
|
|
|
|
|
for (var i = 0; i < points.Length; i++)
|
|
|
|
{
|
|
|
|
var p = points[i];
|
|
|
|
p.Position.Value -= endPos;
|
|
|
|
|
|
|
|
// propagate types forwards to last null type
|
|
|
|
if (i == points.Length - 1)
|
|
|
|
p.Type.Value = lastType;
|
|
|
|
else if (p.Type.Value != null)
|
|
|
|
{
|
|
|
|
var newType = p.Type.Value;
|
|
|
|
p.Type.Value = lastType;
|
|
|
|
lastType = newType;
|
|
|
|
}
|
|
|
|
|
|
|
|
slider.Path.ControlPoints.Insert(0, p);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2020-10-01 15:24:50 +08:00
|
|
|
public override bool HandleFlip(Direction direction)
|
|
|
|
{
|
|
|
|
var hitObjects = selectedMovableObjects;
|
|
|
|
|
|
|
|
var selectedObjectsQuad = getSurroundingQuad(hitObjects);
|
|
|
|
var centre = selectedObjectsQuad.Centre;
|
|
|
|
|
|
|
|
foreach (var h in hitObjects)
|
|
|
|
{
|
|
|
|
var pos = h.Position;
|
|
|
|
|
|
|
|
switch (direction)
|
|
|
|
{
|
|
|
|
case Direction.Horizontal:
|
|
|
|
pos.X = centre.X - (pos.X - centre.X);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Direction.Vertical:
|
|
|
|
pos.Y = centre.Y - (pos.Y - centre.Y);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
h.Position = pos;
|
|
|
|
|
|
|
|
if (h is Slider slider)
|
|
|
|
{
|
|
|
|
foreach (var point in slider.Path.ControlPoints)
|
|
|
|
{
|
|
|
|
point.Position.Value = new Vector2(
|
|
|
|
(direction == Direction.Horizontal ? -1 : 1) * point.Position.Value.X,
|
|
|
|
(direction == Direction.Vertical ? -1 : 1) * point.Position.Value.Y
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2020-09-30 14:08:56 +08:00
|
|
|
public override bool HandleScale(Vector2 scale, Anchor reference)
|
2020-09-29 19:00:19 +08:00
|
|
|
{
|
2020-09-30 14:17:27 +08:00
|
|
|
adjustScaleFromAnchor(ref scale, reference);
|
2020-09-29 19:00:19 +08:00
|
|
|
|
2020-09-30 13:41:32 +08:00
|
|
|
var hitObjects = selectedMovableObjects;
|
2021-02-21 19:12:32 +08:00
|
|
|
bool result;
|
2021-02-22 00:40:57 +08:00
|
|
|
|
2020-09-30 13:41:32 +08:00
|
|
|
// for the time being, allow resizing of slider paths only if the slider is
|
|
|
|
// the only hit object selected. with a group selection, it's likely the user
|
|
|
|
// is not looking to change the duration of the slider but expand the whole pattern.
|
|
|
|
if (hitObjects.Length == 1 && hitObjects.First() is Slider slider)
|
2021-02-21 19:12:32 +08:00
|
|
|
result = scaleSlider(slider, scale);
|
2020-09-30 13:41:32 +08:00
|
|
|
else
|
2021-02-21 19:12:32 +08:00
|
|
|
result = scaleHitObjects(hitObjects, reference, scale);
|
2020-09-29 18:43:50 +08:00
|
|
|
|
2021-02-21 04:50:30 +08:00
|
|
|
moveSelectionInBounds();
|
|
|
|
|
2021-02-21 19:12:32 +08:00
|
|
|
return result;
|
2020-09-29 18:43:50 +08:00
|
|
|
}
|
|
|
|
|
2020-09-30 14:17:27 +08:00
|
|
|
private static void adjustScaleFromAnchor(ref Vector2 scale, Anchor reference)
|
|
|
|
{
|
|
|
|
// cancel out scale in axes we don't care about (based on which drag handle was used).
|
|
|
|
if ((reference & Anchor.x1) > 0) scale.X = 0;
|
|
|
|
if ((reference & Anchor.y1) > 0) scale.Y = 0;
|
|
|
|
|
|
|
|
// reverse the scale direction if dragging from top or left.
|
|
|
|
if ((reference & Anchor.x0) > 0) scale.X = -scale.X;
|
|
|
|
if ((reference & Anchor.y0) > 0) scale.Y = -scale.Y;
|
|
|
|
}
|
|
|
|
|
2020-09-30 14:08:56 +08:00
|
|
|
public override bool HandleRotation(float delta)
|
|
|
|
{
|
|
|
|
var hitObjects = selectedMovableObjects;
|
|
|
|
|
|
|
|
Quad quad = getSurroundingQuad(hitObjects);
|
|
|
|
|
|
|
|
referenceOrigin ??= quad.Centre;
|
|
|
|
|
|
|
|
foreach (var h in hitObjects)
|
|
|
|
{
|
|
|
|
h.Position = rotatePointAroundOrigin(h.Position, referenceOrigin.Value, delta);
|
|
|
|
|
|
|
|
if (h is IHasPath path)
|
|
|
|
{
|
|
|
|
foreach (var point in path.Path.ControlPoints)
|
|
|
|
point.Position.Value = rotatePointAroundOrigin(point.Position.Value, Vector2.Zero, delta);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// this isn't always the case but let's be lenient for now.
|
|
|
|
return true;
|
|
|
|
}
|
2021-02-21 19:12:32 +08:00
|
|
|
|
|
|
|
private bool scaleSlider(Slider slider, Vector2 scale)
|
|
|
|
{
|
2021-02-22 00:40:57 +08:00
|
|
|
Quad sliderQuad = getSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position.Value));
|
|
|
|
Vector2 pathRelativeDeltaScale = new Vector2(1 + scale.X / sliderQuad.Width, 1 + scale.Y / sliderQuad.Height);
|
|
|
|
|
|
|
|
Quad selectionQuad = getSurroundingQuad(new OsuHitObject[] { slider });
|
|
|
|
Quad scaledQuad = new Quad(selectionQuad.TopLeft.X, selectionQuad.TopLeft.Y, selectionQuad.Width + scale.X, selectionQuad.Height + scale.Y);
|
2021-02-23 07:16:35 +08:00
|
|
|
(bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad);
|
2021-02-22 00:40:57 +08:00
|
|
|
|
2021-02-23 07:16:35 +08:00
|
|
|
if (!xInBounds)
|
2021-02-22 00:40:57 +08:00
|
|
|
pathRelativeDeltaScale.X = 1;
|
|
|
|
|
2021-02-23 07:16:35 +08:00
|
|
|
if (!yInBounds)
|
2021-02-22 00:40:57 +08:00
|
|
|
pathRelativeDeltaScale.Y = 1;
|
2021-02-21 19:12:32 +08:00
|
|
|
|
|
|
|
foreach (var point in slider.Path.ControlPoints)
|
|
|
|
point.Position.Value *= pathRelativeDeltaScale;
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
private bool scaleHitObjects(OsuHitObject[] hitObjects, Anchor reference, Vector2 scale)
|
|
|
|
{
|
|
|
|
// move the selection before scaling if dragging from top or left anchors.
|
|
|
|
if ((reference & Anchor.x0) > 0 && !moveSelection(new Vector2(-scale.X, 0))) return false;
|
|
|
|
if ((reference & Anchor.y0) > 0 && !moveSelection(new Vector2(0, -scale.Y))) return false;
|
|
|
|
|
|
|
|
Quad selectionQuad = getSurroundingQuad(hitObjects);
|
2021-02-22 00:40:57 +08:00
|
|
|
Quad scaledQuad = new Quad(selectionQuad.TopLeft.X, selectionQuad.TopLeft.Y, selectionQuad.Width + scale.X, selectionQuad.Height + scale.Y);
|
2021-02-23 07:16:35 +08:00
|
|
|
(bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad);
|
2021-02-21 19:12:32 +08:00
|
|
|
|
|
|
|
foreach (var h in hitObjects)
|
|
|
|
{
|
|
|
|
var newPosition = h.Position;
|
|
|
|
|
|
|
|
// guard against no-ops and NaN.
|
2021-02-23 07:16:35 +08:00
|
|
|
if (scale.X != 0 && selectionQuad.Width > 0 && xInBounds)
|
2021-02-21 19:12:32 +08:00
|
|
|
newPosition.X = selectionQuad.TopLeft.X + (h.X - selectionQuad.TopLeft.X) / selectionQuad.Width * (selectionQuad.Width + scale.X);
|
|
|
|
|
2021-02-23 07:16:35 +08:00
|
|
|
if (scale.Y != 0 && selectionQuad.Height > 0 && yInBounds)
|
2021-02-21 19:12:32 +08:00
|
|
|
newPosition.Y = selectionQuad.TopLeft.Y + (h.Y - selectionQuad.TopLeft.Y) / selectionQuad.Height * (selectionQuad.Height + scale.Y);
|
|
|
|
|
|
|
|
h.Position = newPosition;
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
2020-09-30 14:08:56 +08:00
|
|
|
|
2021-02-22 00:40:57 +08:00
|
|
|
private (bool X, bool Y) isQuadInBounds(Quad quad)
|
|
|
|
{
|
|
|
|
(bool X, bool Y) result;
|
|
|
|
|
|
|
|
result.X = (quad.TopLeft.X >= 0) && (quad.BottomRight.X < DrawWidth);
|
|
|
|
result.Y = (quad.TopLeft.Y >= 0) && (quad.BottomRight.Y < DrawHeight);
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2021-02-21 04:48:31 +08:00
|
|
|
private void moveSelectionInBounds()
|
|
|
|
{
|
|
|
|
var hitObjects = selectedMovableObjects;
|
|
|
|
|
2020-09-30 13:41:32 +08:00
|
|
|
Quad quad = getSurroundingQuad(hitObjects);
|
2021-02-21 04:48:31 +08:00
|
|
|
Vector2 delta = new Vector2(0);
|
2019-11-06 16:27:41 +08:00
|
|
|
|
2021-02-21 04:48:31 +08:00
|
|
|
if (quad.TopLeft.X < 0)
|
|
|
|
delta.X -= quad.TopLeft.X;
|
|
|
|
if (quad.TopLeft.Y < 0)
|
|
|
|
delta.Y -= quad.TopLeft.Y;
|
2020-11-15 21:22:46 +08:00
|
|
|
|
2021-02-21 04:48:31 +08:00
|
|
|
if (quad.BottomRight.X > DrawWidth)
|
|
|
|
delta.X -= quad.BottomRight.X - DrawWidth;
|
|
|
|
if (quad.BottomRight.Y > DrawHeight)
|
|
|
|
delta.Y -= quad.BottomRight.Y - DrawHeight;
|
2019-11-06 16:27:41 +08:00
|
|
|
|
2020-09-30 13:41:32 +08:00
|
|
|
foreach (var h in hitObjects)
|
2020-09-29 18:43:50 +08:00
|
|
|
h.Position += delta;
|
2018-11-16 16:12:24 +08:00
|
|
|
}
|
2020-09-29 18:50:03 +08:00
|
|
|
|
2020-09-30 12:52:57 +08:00
|
|
|
/// <summary>
|
2020-09-30 13:41:32 +08:00
|
|
|
/// Returns a gamefield-space quad surrounding the provided hit objects.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="hitObjects">The hit objects to calculate a quad for.</param>
|
|
|
|
private Quad getSurroundingQuad(OsuHitObject[] hitObjects) =>
|
2021-01-18 15:56:10 +08:00
|
|
|
getSurroundingQuad(hitObjects.SelectMany(h =>
|
|
|
|
{
|
|
|
|
if (h is IHasPath path)
|
2021-01-18 16:13:47 +08:00
|
|
|
{
|
2021-01-18 15:56:10 +08:00
|
|
|
return new[]
|
|
|
|
{
|
|
|
|
h.Position,
|
|
|
|
// can't use EndPosition for reverse slider cases.
|
|
|
|
h.Position + path.Path.PositionAt(1)
|
|
|
|
};
|
2021-01-18 16:13:47 +08:00
|
|
|
}
|
2021-01-18 15:56:10 +08:00
|
|
|
|
|
|
|
return new[] { h.Position };
|
|
|
|
}));
|
2020-09-30 13:41:32 +08:00
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Returns a gamefield-space quad surrounding the provided points.
|
2020-09-30 12:52:57 +08:00
|
|
|
/// </summary>
|
2020-09-30 13:41:32 +08:00
|
|
|
/// <param name="points">The points to calculate a quad for.</param>
|
|
|
|
private Quad getSurroundingQuad(IEnumerable<Vector2> points)
|
2020-09-29 18:50:03 +08:00
|
|
|
{
|
2020-10-09 17:50:05 +08:00
|
|
|
if (!EditorBeatmap.SelectedHitObjects.Any())
|
2020-09-30 12:52:57 +08:00
|
|
|
return new Quad();
|
|
|
|
|
2020-09-29 18:50:03 +08:00
|
|
|
Vector2 minPosition = new Vector2(float.MaxValue, float.MaxValue);
|
|
|
|
Vector2 maxPosition = new Vector2(float.MinValue, float.MinValue);
|
|
|
|
|
|
|
|
// Go through all hitobjects to make sure they would remain in the bounds of the editor after movement, before any movement is attempted
|
2020-09-30 13:41:32 +08:00
|
|
|
foreach (var p in points)
|
2020-09-29 18:50:03 +08:00
|
|
|
{
|
2020-09-30 13:41:32 +08:00
|
|
|
minPosition = Vector2.ComponentMin(minPosition, p);
|
|
|
|
maxPosition = Vector2.ComponentMax(maxPosition, p);
|
2020-09-29 18:50:03 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
Vector2 size = maxPosition - minPosition;
|
|
|
|
|
|
|
|
return new Quad(minPosition.X, minPosition.Y, size.X, size.Y);
|
|
|
|
}
|
|
|
|
|
2020-09-30 13:41:32 +08:00
|
|
|
/// <summary>
|
|
|
|
/// All osu! hitobjects which can be moved/rotated/scaled.
|
|
|
|
/// </summary>
|
2020-10-09 17:50:05 +08:00
|
|
|
private OsuHitObject[] selectedMovableObjects => EditorBeatmap.SelectedHitObjects
|
|
|
|
.OfType<OsuHitObject>()
|
|
|
|
.Where(h => !(h is Spinner))
|
|
|
|
.ToArray();
|
2020-09-30 13:41:32 +08:00
|
|
|
|
2020-09-29 18:50:03 +08:00
|
|
|
/// <summary>
|
2020-09-30 12:02:05 +08:00
|
|
|
/// Rotate a point around an arbitrary origin.
|
2020-09-29 18:50:03 +08:00
|
|
|
/// </summary>
|
2020-09-30 12:02:05 +08:00
|
|
|
/// <param name="point">The point.</param>
|
|
|
|
/// <param name="origin">The centre origin to rotate around.</param>
|
2020-09-29 18:50:03 +08:00
|
|
|
/// <param name="angle">The angle to rotate (in degrees).</param>
|
2020-09-30 12:02:05 +08:00
|
|
|
private static Vector2 rotatePointAroundOrigin(Vector2 point, Vector2 origin, float angle)
|
2020-09-29 18:50:03 +08:00
|
|
|
{
|
|
|
|
angle = -angle;
|
|
|
|
|
2020-09-30 12:02:05 +08:00
|
|
|
point.X -= origin.X;
|
|
|
|
point.Y -= origin.Y;
|
2020-09-29 18:50:03 +08:00
|
|
|
|
|
|
|
Vector2 ret;
|
2020-10-01 17:10:05 +08:00
|
|
|
ret.X = point.X * MathF.Cos(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Sin(MathUtils.DegreesToRadians(angle));
|
|
|
|
ret.Y = point.X * -MathF.Sin(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Cos(MathUtils.DegreesToRadians(angle));
|
2020-09-29 18:50:03 +08:00
|
|
|
|
2020-09-30 12:02:05 +08:00
|
|
|
ret.X += origin.X;
|
|
|
|
ret.Y += origin.Y;
|
2020-09-29 18:50:03 +08:00
|
|
|
|
|
|
|
return ret;
|
|
|
|
}
|
2018-11-16 16:12:24 +08:00
|
|
|
}
|
|
|
|
}
|