1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 10:33:30 +08:00
osu-lazer/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs

194 lines
7.4 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;
using System.Collections.Generic;
using System.Linq;
2020-09-29 18:43:50 +08:00
using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
2020-09-30 12:02:05 +08:00
using osu.Framework.Utils;
2020-09-29 19:00:19 +08:00
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
namespace osu.Game.Rulesets.Osu.Edit
{
2018-11-19 15:58:11 +08:00
public class OsuSelectionHandler : SelectionHandler
{
protected override void OnSelectionChanged()
{
base.OnSelectionChanged();
2020-09-29 18:43:50 +08:00
bool canOperate = SelectedHitObjects.Count() > 1 || SelectedHitObjects.Any(s => s is Slider);
SelectionBox.CanRotate = canOperate;
SelectionBox.CanScaleX = canOperate;
SelectionBox.CanScaleY = canOperate;
}
2020-09-29 19:08:28 +08:00
protected override void OnDragOperationEnded()
{
base.OnDragOperationEnded();
referenceOrigin = null;
}
2020-09-29 18:43:50 +08:00
2020-09-30 12:02:05 +08:00
public override bool HandleMovement(MoveSelectionEvent moveEvent) =>
moveSelection(moveEvent.InstantDelta);
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
public override bool HandleScaleY(in float scale, Anchor reference) =>
scaleSelection(new Vector2(0, ((reference & Anchor.y0) > 0 ? -1 : 1) * scale), reference);
2020-09-29 18:43:50 +08:00
public override bool HandleScaleX(in float scale, Anchor reference) =>
scaleSelection(new Vector2(((reference & Anchor.x0) > 0 ? -1 : 1) * scale, 0), reference);
2020-09-29 18:43:50 +08:00
public override bool HandleRotation(float delta)
2020-09-29 19:00:19 +08:00
{
var hitObjects = selectedMovableObjects;
Quad quad = getSurroundingQuad(hitObjects);
2020-09-29 19:00:19 +08:00
2020-09-30 12:02:05 +08:00
referenceOrigin ??= quad.Centre;
2020-09-29 19:00:19 +08:00
foreach (var h in hitObjects)
2020-09-29 19:00:19 +08:00
{
2020-09-30 12:02:05 +08:00
h.Position = rotatePointAroundOrigin(h.Position, referenceOrigin.Value, delta);
2020-09-29 19:00:19 +08:00
if (h is IHasPath path)
{
foreach (var point in path.Path.ControlPoints)
2020-09-30 12:02:05 +08:00
point.Position.Value = rotatePointAroundOrigin(point.Position.Value, Vector2.Zero, delta);
2020-09-29 19:00:19 +08:00
}
}
// this isn't always the case but let's be lenient for now.
2020-09-29 19:00:19 +08:00
return true;
}
private bool scaleSelection(Vector2 scale, Anchor reference)
2020-09-29 18:43:50 +08:00
{
var hitObjects = selectedMovableObjects;
2020-09-29 18:43:50 +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)
{
var quad = getSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position.Value));
Vector2 delta = Vector2.One + new Vector2(scale.X / quad.Width, scale.Y / quad.Height);
2020-09-29 18:43:50 +08:00
foreach (var point in slider.Path.ControlPoints)
point.Position.Value *= delta;
}
else
2020-09-29 18:43:50 +08:00
{
// 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 quad = getSurroundingQuad(hitObjects);
Vector2 minPosition = quad.TopLeft;
Vector2 size = quad.Size;
Vector2 newSize = size + scale;
foreach (var h in hitObjects)
2020-09-29 18:43:50 +08:00
{
if (scale.X != 1)
h.Position = new Vector2(minPosition.X + (h.X - minPosition.X) / size.X * newSize.X, h.Y);
if (scale.Y != 1)
h.Position = new Vector2(h.X, minPosition.Y + (h.Y - minPosition.Y) / size.Y * newSize.Y);
2020-09-29 18:43:50 +08:00
}
}
return true;
}
private bool moveSelection(Vector2 delta)
{
var hitObjects = selectedMovableObjects;
Quad quad = getSurroundingQuad(hitObjects);
if (quad.TopLeft.X + delta.X < 0 ||
quad.TopLeft.Y + delta.Y < 0 ||
quad.BottomRight.X + delta.X > DrawWidth ||
quad.BottomRight.Y + delta.Y > DrawHeight)
return false;
foreach (var h in hitObjects)
2020-09-29 18:43:50 +08:00
h.Position += delta;
return true;
}
/// <summary>
/// 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) =>
getSurroundingQuad(hitObjects.SelectMany(h => new[] { h.Position, h.EndPosition }));
/// <summary>
/// Returns a gamefield-space quad surrounding the provided points.
/// </summary>
/// <param name="points">The points to calculate a quad for.</param>
private Quad getSurroundingQuad(IEnumerable<Vector2> points)
{
if (!SelectedHitObjects.Any())
return new Quad();
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
foreach (var p in points)
{
minPosition = Vector2.ComponentMin(minPosition, p);
maxPosition = Vector2.ComponentMax(maxPosition, p);
}
Vector2 size = maxPosition - minPosition;
return new Quad(minPosition.X, minPosition.Y, size.X, size.Y);
}
/// <summary>
/// All osu! hitobjects which can be moved/rotated/scaled.
/// </summary>
private OsuHitObject[] selectedMovableObjects => SelectedHitObjects
.OfType<OsuHitObject>()
.Where(h => !(h is Spinner))
.ToArray();
/// <summary>
2020-09-30 12:02:05 +08:00
/// Rotate a point around an arbitrary origin.
/// </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>
/// <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)
{
angle = -angle;
2020-09-30 12:02:05 +08:00
point.X -= origin.X;
point.Y -= origin.Y;
Vector2 ret;
2020-09-30 12:02:05 +08:00
ret.X = (float)(point.X * Math.Cos(MathUtils.DegreesToRadians(angle)) + point.Y * Math.Sin(angle / 180f * Math.PI));
ret.Y = (float)(point.X * -Math.Sin(MathUtils.DegreesToRadians(angle)) + point.Y * Math.Cos(angle / 180f * Math.PI));
2020-09-30 12:02:05 +08:00
ret.X += origin.X;
ret.Y += origin.Y;
return ret;
}
}
}