Skip to content

Commit

Permalink
Wip implementation of QuadTree for Path input
Browse files Browse the repository at this point in the history
  • Loading branch information
smoogipoo committed Oct 9, 2023
1 parent fe27691 commit ab78528
Show file tree
Hide file tree
Showing 3 changed files with 357 additions and 10 deletions.
104 changes: 104 additions & 0 deletions osu.Framework.Tests/Visual/Graphics/TestSceneQuadTree.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// 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 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;
}
}
}
30 changes: 20 additions & 10 deletions osu.Framework/Graphics/Lines/Path.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -240,16 +232,34 @@ public void AddVertex(Vector2 pos)
private readonly List<Line> segmentsBacking = new List<Line>();
private readonly Cached segmentsCache = new Cached();
private List<Line> segments => segmentsCache.IsValid ? segmentsBacking : generateSegments();
private QuadTree quadTree = new QuadTree(RectangleF.Empty);

private List<Line> 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();
Expand Down
233 changes: 233 additions & 0 deletions osu.Framework/Graphics/QuadTree.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
// 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.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<RectangleF> EnumerateAreas() => rootNode.EnumerateAreas();

private class QuadTreeNode
{
private const int capacity = 4;

public readonly RectangleF Area;
private readonly QuadTreeNode? parent;

private List<Vector2>? points = new List<Vector2>(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<RectangleF> 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<Vector2> 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;
}
}
}
}

0 comments on commit ab78528

Please sign in to comment.