From 9df059b01decf63403094a3c071a6f54bea7fe7b Mon Sep 17 00:00:00 2001
From: Naxess <30292137+Naxesss@users.noreply.github.com>
Date: Wed, 24 Mar 2021 17:25:28 +0100
Subject: [PATCH] Add bounding box limit
---
.../Components/PathControlPointVisualiser.cs | 101 ++++++++++++++++--
1 file changed, 90 insertions(+), 11 deletions(-)
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs
index c4a16f9c2a..0270485f31 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs
@@ -11,6 +11,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
+using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
@@ -165,11 +166,26 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
///
/// Returns whether the given points are arranged in a valid way. Invalid if points
/// are almost entirely linear - as this causes the radius to approach infinity,
- /// which would exhaust memory when drawing / approximating.
+ /// or if the bounding box of the arc is too large.
///
/// The three points that make up this circular arc segment.
///
private bool validCircularArcSegment(IReadOnlyList points)
+ {
+ float det = circularArcDeterminant(points);
+ RectangleF boundingBox = circularArcBoundingBox(points);
+
+ // Determinant limit prevents memory exhaustion as a result of approximating the subpath.
+ // Bounding box area limit prevents memory exhaustion as a result of drawing the texture.
+ return Math.Abs(det) > 0.001f && boundingBox.Area < 640 * 480;
+ }
+
+ ///
+ /// Computes the determinant of the circular arc segment defined by the given points.
+ ///
+ /// The three points defining the circular arc.
+ ///
+ private float circularArcDeterminant(IReadOnlyList points)
{
Vector2 a = points[0];
Vector2 b = points[1];
@@ -181,19 +197,82 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
Vector2 normB = new Vector2(b.X / maxLength, b.Y / maxLength);
Vector2 normC = new Vector2(c.X / maxLength, c.Y / maxLength);
- float det = (normA.X - normB.X) * (normB.Y - normC.Y) - (normB.X - normC.X) * (normA.Y - normB.Y);
+ return (normA.X - normB.X) * (normB.Y - normC.Y) - (normB.X - normC.X) * (normA.Y - normB.Y);
+ }
- float acSq = (a - c).LengthSquared;
- float abSq = (a - b).LengthSquared;
- float bcSq = (b - c).LengthSquared;
+ ///
+ /// Computes the bounding box of the circular arc segment defined by the given points.
+ ///
+ /// The three points defining the circular arc.
+ ///
+ private RectangleF circularArcBoundingBox(IReadOnlyList points)
+ {
+ Vector2 a = points[0];
+ Vector2 b = points[1];
+ Vector2 c = points[2];
- // Exterior = curve wraps around the long way between end-points
- // Exterior bottleneck is drawing-related, interior bottleneck is approximation-related,
- // where the latter is much faster, hence differing thresholds
- bool exterior = abSq > acSq || bcSq > acSq;
- float threshold = exterior ? 0.35f : 0.001f;
+ // See: https://en.wikipedia.org/wiki/Circumscribed_circle#Cartesian_coordinates_2
+ float d = 2 * (a.X * (b - c).Y + b.X * (c - a).Y + c.X * (a - b).Y);
+ float aSq = a.LengthSquared;
+ float bSq = b.LengthSquared;
+ float cSq = c.LengthSquared;
- return Math.Abs(det) >= threshold;
+ Vector2 center = new Vector2(
+ aSq * (b - c).Y + bSq * (c - a).Y + cSq * (a - b).Y,
+ aSq * (c - b).X + bSq * (a - c).X + cSq * (b - a).X) / d;
+
+ Vector2 dA = a - center;
+ Vector2 dC = c - center;
+
+ float r = dA.Length;
+
+ double thetaStart = Math.Atan2(dA.Y, dA.X);
+ double thetaEnd = Math.Atan2(dC.Y, dC.X);
+
+ while (thetaEnd < thetaStart)
+ thetaEnd += 2 * Math.PI;
+
+ // Decide in which direction to draw the circle, depending on which side of
+ // AC B lies.
+ Vector2 orthoAtoC = c - a;
+ orthoAtoC = new Vector2(orthoAtoC.Y, -orthoAtoC.X);
+ bool clockwise = Vector2.Dot(orthoAtoC, b - a) >= 0;
+
+ if (clockwise)
+ {
+ if (thetaEnd < thetaStart)
+ thetaEnd += Math.PI * 2;
+ }
+ else
+ {
+ if (thetaStart < thetaEnd)
+ thetaStart += Math.PI * 2;
+ }
+
+ List boundingBoxPoints = new List(points);
+
+ bool includes0Degrees = 0 > thetaStart && 0 < thetaEnd;
+ bool includes90Degrees = Math.PI / 2 > thetaStart && Math.PI / 2 < thetaEnd;
+ bool includes180Degrees = Math.PI > thetaStart && Math.PI < thetaEnd;
+ bool includes270Degrees = Math.PI * 1.5f > thetaStart && Math.PI * 1.5f < thetaEnd;
+
+ if (!clockwise)
+ {
+ includes0Degrees = 0 < thetaStart && 0 > thetaEnd;
+ includes90Degrees = Math.PI / 2 < thetaStart && Math.PI / 2 > thetaEnd;
+ includes180Degrees = Math.PI < thetaStart && Math.PI > thetaEnd;
+ includes270Degrees = Math.PI * 1.5f < thetaStart && Math.PI * 1.5f > thetaEnd;
+ }
+
+ if (includes0Degrees) boundingBoxPoints.Add(center + new Vector2(1, 0) * r);
+ if (includes90Degrees) boundingBoxPoints.Add(center + new Vector2(0, 1) * r);
+ if (includes180Degrees) boundingBoxPoints.Add(center + new Vector2(-1, 0) * r);
+ if (includes270Degrees) boundingBoxPoints.Add(center + new Vector2(0, -1) * r);
+
+ float width = Math.Abs(boundingBoxPoints.Max(p => p.X) - boundingBoxPoints.Min(p => p.X));
+ float height = Math.Abs(boundingBoxPoints.Max(p => p.Y) - boundingBoxPoints.Min(p => p.Y));
+
+ return new RectangleF(slider.Position, new Vector2(width, height));
}
private void selectPiece(PathControlPointPiece piece, MouseButtonEvent e)