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)