// Copyright (c) ppy Pty Ltd . 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; using osu.Framework.Graphics; using osu.Framework.Graphics.Primitives; using osu.Game.Rulesets.Objects.Types; using osuTK; namespace osu.Game.Utils { public static class GeometryUtils { /// /// Rotate a point around an arbitrary origin. /// /// The point. /// The centre origin to rotate around. /// The angle to rotate (in degrees). public static Vector2 RotatePointAroundOrigin(Vector2 point, Vector2 origin, float angle) { angle = -angle; point.X -= origin.X; point.Y -= origin.Y; Vector2 ret = RotateVector(point, angle); ret.X += origin.X; ret.Y += origin.Y; return ret; } /// /// Rotate a vector around the origin. /// /// The vector. /// The angle to rotate (in degrees). public static Vector2 RotateVector(Vector2 vector, float angle) { return new Vector2( vector.X * MathF.Cos(float.DegreesToRadians(angle)) + vector.Y * MathF.Sin(float.DegreesToRadians(angle)), vector.X * -MathF.Sin(float.DegreesToRadians(angle)) + vector.Y * MathF.Cos(float.DegreesToRadians(angle)) ); } /// /// Given a flip direction, a surrounding quad for all selected objects, and a position, /// will return the flipped position in screen space coordinates. /// public static Vector2 GetFlippedPosition(Direction direction, Quad quad, Vector2 position) { var centre = quad.Centre; switch (direction) { case Direction.Horizontal: position.X = centre.X - (position.X - centre.X); break; case Direction.Vertical: position.Y = centre.Y - (position.Y - centre.Y); break; } return position; } /// /// Given a flip axis vector, a surrounding quad for all selected objects, and a position, /// will return the flipped position in screen space coordinates. /// public static Vector2 GetFlippedPosition(Vector2 axis, Quad quad, Vector2 position) { var centre = quad.Centre; return position - 2 * Vector2.Dot(position - centre, axis) * axis; } /// /// Given a scale vector, a surrounding quad for all selected objects, and a position, /// will return the scaled position in screen space coordinates. /// public static Vector2 GetScaledPosition(Anchor reference, Vector2 scale, Quad selectionQuad, Vector2 position) { // adjust the direction of scale depending on which side the user is dragging. float xOffset = ((reference & Anchor.x0) > 0) ? -scale.X : 0; float yOffset = ((reference & Anchor.y0) > 0) ? -scale.Y : 0; // guard against no-ops and NaN. if (scale.X != 0 && selectionQuad.Width > 0) position.X = selectionQuad.TopLeft.X + xOffset + (position.X - selectionQuad.TopLeft.X) / selectionQuad.Width * (selectionQuad.Width + scale.X); if (scale.Y != 0 && selectionQuad.Height > 0) position.Y = selectionQuad.TopLeft.Y + yOffset + (position.Y - selectionQuad.TopLeft.Y) / selectionQuad.Height * (selectionQuad.Height + scale.Y); return position; } /// /// Given a scale multiplier, an origin, and a position, /// will return the scaled position in screen space coordinates. /// public static Vector2 GetScaledPosition(Vector2 scale, Vector2 origin, Vector2 position, float axisRotation = 0) { return origin + RotateVector(RotateVector(position - origin, axisRotation) * scale, -axisRotation); } /// /// Returns a quad surrounding the provided points. /// /// The points to calculate a quad for. public static Quad GetSurroundingQuad(IEnumerable points) { if (!points.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); } /// /// Returns a gamefield-space quad surrounding the provided hit objects. /// /// The hit objects to calculate a quad for. public static Quad GetSurroundingQuad(IEnumerable hitObjects) => GetSurroundingQuad(enumerateStartAndEndPositions(hitObjects)); /// /// Returns the points that make up the convex hull of the provided points. /// /// The points to calculate a convex hull. public static List GetConvexHull(IEnumerable points) { List p = points.ToList(); if (p.Count <= 1) return p; int n = p.Count, k = 0; List hull = new List(new Vector2[2 * n]); p.Sort((a, b) => a.X == b.X ? a.Y.CompareTo(b.Y) : a.X.CompareTo(b.X)); // Build lower hull for (int i = 0; i < n; ++i) { while (k >= 2 && cross(hull[k - 2], hull[k - 1], p[i]) <= 0) k--; hull[k] = p[i]; k++; } // Build upper hull for (int i = n - 2, t = k + 1; i >= 0; i--) { while (k >= t && cross(hull[k - 2], hull[k - 1], p[i]) <= 0) k--; hull[k] = p[i]; k++; } return hull.Take(k - 1).ToList(); float cross(Vector2 o, Vector2 a, Vector2 b) => (a.X - o.X) * (b.Y - o.Y) - (a.Y - o.Y) * (b.X - o.X); } public static List GetConvexHull(IEnumerable hitObjects) => GetConvexHull(enumerateStartAndEndPositions(hitObjects)); private static IEnumerable enumerateStartAndEndPositions(IEnumerable hitObjects) => hitObjects.SelectMany(h => { if (h is IHasPath path) { return new[] { h.Position, // can't use EndPosition for reverse slider cases. h.Position + path.Path.PositionAt(1) }; } return new[] { h.Position }; }); } }