diff --git a/osu.Framework.Tests/Visual/Graphics/TestSceneQuadTree.cs b/osu.Framework.Tests/Visual/Graphics/TestSceneQuadTree.cs new file mode 100644 index 0000000000..826eff8346 --- /dev/null +++ b/osu.Framework.Tests/Visual/Graphics/TestSceneQuadTree.cs @@ -0,0 +1,104 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; +using osu.Framework.Utils; +using osuTK; +using osuTK.Graphics; +using osuTK.Input; + +namespace osu.Framework.Tests.Visual.Graphics +{ + public partial class TestSceneQuadTree : FrameworkTestScene + { + private readonly QuadTree quadTree; + private readonly Container boxes; + private readonly Container points; + + public TestSceneQuadTree() + { + quadTree = new QuadTree(new RectangleF(0, 0, 800, 600)); + + Child = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(800, 600), + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0.2f, + }, + boxes = new Container + { + RelativeSizeAxes = Axes.Both + }, + points = new Container + { + RelativeSizeAxes = Axes.Both + } + } + }; + } + + protected override bool OnMouseDown(MouseDownEvent e) + { + if (e.Button != MouseButton.Left) + return base.OnMouseDown(e); + + Vector2 localPos = points.ToLocalSpace(e.ScreenSpaceMouseDownPosition); + + quadTree.Insert(localPos); + + points.Add(new Circle + { + Origin = Anchor.Centre, + Size = new Vector2(12), + Colour = Color4.Yellow, + Position = localPos + }); + + boxes.Clear(); + + foreach (var area in quadTree.EnumerateAreas()) + { + boxes.Add(new Container + { + Position = area.Location, + Size = area.Size, + Masking = true, + BorderColour = Color4.White, + BorderThickness = 2, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true + } + }); + } + + return true; + } + + protected override bool OnMouseMove(MouseMoveEvent e) + { + if (!e.IsPressed(MouseButton.Right)) + return base.OnMouseMove(e); + + if (quadTree.TryGetClosest(points.ToLocalSpace(e.ScreenSpaceMousePosition), out Vector2 closest)) + { + foreach (var p in points) + p.Colour = Precision.AlmostEquals(p.Position, closest) ? Color4.Blue : Color4.Yellow; + } + + return true; + } + } +} diff --git a/osu.Framework/Graphics/Lines/Path.cs b/osu.Framework/Graphics/Lines/Path.cs index 509d88eb63..05c9f545a9 100644 --- a/osu.Framework/Graphics/Lines/Path.cs +++ b/osu.Framework/Graphics/Lines/Path.cs @@ -201,15 +201,7 @@ private RectangleF vertexBounds public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) { var localPos = ToLocalSpace(screenSpacePos); - float pathRadiusSquared = PathRadius * PathRadius; - - foreach (var t in segments) - { - if (t.DistanceSquaredToPoint(localPos) <= pathRadiusSquared) - return true; - } - - return false; + return quadTree.TryGetClosest(localPos, out Vector2 closest) && (closest - localPos).LengthSquared <= PathRadius * PathRadius; } public Vector2 PositionInBoundingBox(Vector2 pos) => pos - vertexBounds.TopLeft; @@ -240,16 +232,34 @@ public void AddVertex(Vector2 pos) private readonly List segmentsBacking = new List(); private readonly Cached segmentsCache = new Cached(); private List segments => segmentsCache.IsValid ? segmentsBacking : generateSegments(); + private QuadTree quadTree = new QuadTree(RectangleF.Empty); private List generateSegments() { segmentsBacking.Clear(); + quadTree = new QuadTree(vertexBounds.AABB); + if (vertices.Count > 1) { Vector2 offset = vertexBounds.TopLeft; + for (int i = 0; i < vertices.Count - 1; ++i) - segmentsBacking.Add(new Line(vertices[i] - offset, vertices[i + 1] - offset)); + { + Line segment = new Line(vertices[i] - offset, vertices[i + 1] - offset); + segmentsBacking.Add(segment); + + if (i == 0) + quadTree.Insert(segment.StartPoint); + + int intermediatePoints = (int)(Math.Ceiling(segment.Rho / (PathRadius / 2))); + Vector2 intermediateSegmentLength = segment.Direction / intermediatePoints; + + for (int x = 1; x <= intermediatePoints; x++) + quadTree.Insert(segment.StartPoint + x * intermediateSegmentLength); + + quadTree.Insert(segment.EndPoint); + } } segmentsCache.Validate(); diff --git a/osu.Framework/Graphics/QuadTree.cs b/osu.Framework/Graphics/QuadTree.cs new file mode 100644 index 0000000000..a250bb37b8 --- /dev/null +++ b/osu.Framework/Graphics/QuadTree.cs @@ -0,0 +1,233 @@ +// 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.Diagnostics; +using osu.Framework.Graphics.Primitives; +using osuTK; + +namespace osu.Framework.Graphics +{ + public class QuadTree + { + private readonly QuadTreeNode rootNode; + + public QuadTree(RectangleF area) + { + rootNode = new QuadTreeNode(null, area); + } + + public bool Insert(Vector2 point) => rootNode.Insert(point); + + public bool TryGetClosest(Vector2 point, out Vector2 closest) => rootNode.TryGetClosest(point, out closest); + + public IEnumerable EnumerateAreas() => rootNode.EnumerateAreas(); + + private class QuadTreeNode + { + private const int capacity = 4; + + public readonly RectangleF Area; + private readonly QuadTreeNode? parent; + + private List? points = new List(capacity); + private QuadTreeNode? topLeft; + private QuadTreeNode? topRight; + private QuadTreeNode? bottomLeft; + private QuadTreeNode? bottomRight; + + public QuadTreeNode(QuadTreeNode? parent, RectangleF area) + { + this.parent = parent; + Area = area; + } + + public bool Insert(Vector2 point) + { + // ReSharper disable once PossiblyImpureMethodCallOnReadonlyVariable + if (!Area.Contains(point)) + return false; + + if (points?.Count == capacity) + subDivide(); + + if (points != null) + { + points.Add(point); + return true; + } + + Debug.Assert(topLeft != null); + Debug.Assert(topRight != null); + Debug.Assert(bottomLeft != null); + Debug.Assert(bottomRight != null); + + return topLeft.Insert(point) + || topRight.Insert(point) + || bottomLeft.Insert(point) + || bottomRight.Insert(point); + } + + public bool TryGetClosest(Vector2 point, out Vector2 closest) + { + QuadTreeNode? closestNode = findContainingNode(point); + + if (closestNode == null) + { + closest = default; + return false; + } + + Debug.Assert(closestNode.points != null); + + float distToLeft = MathF.Abs(point.X - closestNode.Area.Left); + float distToRight = MathF.Abs(point.X - closestNode.Area.Right); + float distToTop = MathF.Abs(point.Y - closestNode.Area.Top); + float distToBottom = MathF.Abs(point.Y - closestNode.Area.Bottom); + float distToClosestBoundary = MathF.Min(MathF.Min(MathF.Min(distToLeft, distToRight), distToTop), distToBottom); + + Vector2 closestPoint = Vector2.Zero; + float distToClosestPoint = float.MaxValue; + + foreach (var pt in closestNode.points) + computeDistanceTo(pt); + + // We're closer to the point than to any boundary of this node. + if (closestNode.parent == null || distToClosestBoundary >= distToClosestPoint) + { + closest = closestPoint; + return true; + } + + // We're closer to the boundary than to any point in this node. + // We need to check the neighbouring boundaries to see if there's any point closer. + foreach (var pt in closestNode.parent.enumeratePoints(closestNode)) + computeDistanceTo(pt); + + closest = closestPoint; + return true; + + void computeDistanceTo(Vector2 pt) + { + float dist = (point - pt).Length; + + if (dist < distToClosestPoint) + { + distToClosestPoint = dist; + closestPoint = pt; + } + } + } + + private QuadTreeNode? findContainingNode(Vector2 point) + { + // ReSharper disable once PossiblyImpureMethodCallOnReadonlyVariable + if (!Area.Contains(point)) + return null; + + if (points != null) + return this; + + Debug.Assert(topLeft != null); + Debug.Assert(topRight != null); + Debug.Assert(bottomLeft != null); + Debug.Assert(bottomRight != null); + + return topLeft.findContainingNode(point) + ?? topRight.findContainingNode(point) + ?? bottomLeft.findContainingNode(point) + ?? bottomRight.findContainingNode(point); + } + + public IEnumerable EnumerateAreas() + { + yield return Area; + + if (topLeft != null) + { + foreach (var area in topLeft.EnumerateAreas()) + yield return area; + } + + if (topRight != null) + { + foreach (var area in topRight.EnumerateAreas()) + yield return area; + } + + if (bottomLeft != null) + { + foreach (var area in bottomLeft.EnumerateAreas()) + yield return area; + } + + if (bottomRight != null) + { + foreach (var area in bottomRight.EnumerateAreas()) + yield return area; + } + } + + private IEnumerable enumeratePoints(QuadTreeNode? exceptNode) + { + if (points != null) + { + foreach (var pt in points) + yield return pt; + + yield break; + } + + Debug.Assert(topLeft != null); + Debug.Assert(topRight != null); + Debug.Assert(bottomLeft != null); + Debug.Assert(bottomRight != null); + + if (topLeft != exceptNode) + { + foreach (var pt in topLeft.enumeratePoints(exceptNode)) + yield return pt; + } + + if (topRight != exceptNode) + { + foreach (var pt in topRight.enumeratePoints(exceptNode)) + yield return pt; + } + + if (bottomLeft != exceptNode) + { + foreach (var pt in bottomLeft.enumeratePoints(exceptNode)) + yield return pt; + } + + if (bottomRight != exceptNode) + { + foreach (var pt in bottomRight.enumeratePoints(exceptNode)) + yield return pt; + } + } + + private void subDivide() + { + Debug.Assert(points != null); + + topLeft = new QuadTreeNode(this, new RectangleF(Area.Location, Area.Size / 2)); + topRight = new QuadTreeNode(this, new RectangleF(new Vector2(Area.Centre.X, Area.Y), Area.Size / 2)); + bottomLeft = new QuadTreeNode(this, new RectangleF(new Vector2(Area.X, Area.Centre.Y), Area.Size / 2)); + bottomRight = new QuadTreeNode(this, new RectangleF(Area.Centre, Area.Size / 2)); + + foreach (var p in points) + { + bool _ = topLeft.Insert(p) + || topRight.Insert(p) + || bottomLeft.Insert(p) + || bottomRight.Insert(p); + } + + points = null; + } + } + } +}