From 85fcd2358f82b56b11a6fb6d2581ebffa28cd9b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Sat, 27 Sep 2025 23:23:38 +0200 Subject: [PATCH] Remove entity boundaries from Quadtree The Quadtree did no meaningful management of entity boundaries. All it did was store the maximum and inflate the query rectangle accordingly. The caller can easily take over this responsibility. Consequently, this simplifies queries. --- Quadtree/Quadtree.cs | 69 ++++++++++++++++++------------------------ QuadtreeTests/Tests.cs | 11 ++----- 2 files changed, 33 insertions(+), 47 deletions(-) diff --git a/Quadtree/Quadtree.cs b/Quadtree/Quadtree.cs index 89c9c3f..5577a6a 100644 --- a/Quadtree/Quadtree.cs +++ b/Quadtree/Quadtree.cs @@ -32,11 +32,6 @@ namespace SpatialCollections /// private readonly Func _getPositionCallback; - /// - /// Callback to determine the bounding box of an entity. - /// - private readonly Func _getBoundingBoxCallback; - /// /// The list of quadtree vertices. The root vertex is always the first item. The four child vertices of a branch /// are always created together and stored contiguously. @@ -50,11 +45,6 @@ namespace SpatialCollections private Rectangle _rootBoundingBox; - /// - /// The maximum bounding radius of all stored entities, used to inflate the bounding box for queries. - /// - private float _maxEntityBoundingRadius; - /// /// Stack of vertex indices with their bounding boxes. It is reused for each query. /// @@ -65,31 +55,24 @@ namespace SpatialCollections /// private readonly Stack _vertexStack; - public Quadtree(int maxLeafSize, int maxTreeDepth, Func getPositionCallback, - Func getBoundingBoxCallback) + public Quadtree(int maxLeafSize, int maxTreeDepth, Func getPositionCallback) { _maxLeafSize = maxLeafSize; _maxTreeDepth = maxTreeDepth; _getPositionCallback = getPositionCallback; - _getBoundingBoxCallback = getBoundingBoxCallback; _vertices = new List() { new(-1, 0) }; _items = new List>(); _rootBoundingBox = new Rectangle(Vector2.Zero, Vector2.Zero); - _maxEntityBoundingRadius = 0f; _queryStack = new Stack(); _vertexStack = new Stack(); } public int Count => _items.Count; - public void Add(T entity, float boundingRadius) + public void Add(T entity) { var itemIndex = _items.Count; _items.Add(new QuadtreeItem(entity)); - if (_maxEntityBoundingRadius < boundingRadius) - { - _maxEntityBoundingRadius = boundingRadius; - } if (_vertices.Count == 1 && _vertices[0].ChildCount < _maxLeafSize) { @@ -165,10 +148,7 @@ namespace SpatialCollections public void Query(Rectangle box, List resultList) { - Rectangle inflatedBox = box; - inflatedBox.Expand(_maxEntityBoundingRadius); - - QueryProcessChildVertex(inflatedBox, box, 0, _rootBoundingBox, resultList, _queryStack); + QueryProcessChildVertex(box, 0, _rootBoundingBox, resultList, _queryStack); while (_queryStack.Count > 0) { @@ -177,16 +157,16 @@ namespace SpatialCollections int childIndex = _vertices[query.VertexIndex].FirstChildIndex; Rectangle halfBounds = new(query.VertexBounds.Center, query.VertexBounds.Max); Vector2 halfSize = halfBounds.Size; - QueryProcessChildVertex(inflatedBox, box, childIndex++, halfBounds, resultList, _queryStack); + QueryProcessChildVertex(box, childIndex++, halfBounds, resultList, _queryStack); halfBounds.Translate(new Vector2(-halfSize.X, 0f)); - QueryProcessChildVertex(inflatedBox, box, childIndex++, halfBounds, resultList, _queryStack); + QueryProcessChildVertex(box, childIndex++, halfBounds, resultList, _queryStack); halfBounds.Translate(new Vector2(halfSize.X, -halfSize.Y)); - QueryProcessChildVertex(inflatedBox, box, childIndex++, halfBounds, resultList, _queryStack); + QueryProcessChildVertex(box, childIndex++, halfBounds, resultList, _queryStack); halfBounds.Translate(new Vector2(-halfSize.X, 0f)); - QueryProcessChildVertex(inflatedBox, box, childIndex, halfBounds, resultList, _queryStack); + QueryProcessChildVertex(box, childIndex, halfBounds, resultList, _queryStack); } } @@ -196,7 +176,6 @@ namespace SpatialCollections _vertices.Add(new QuadtreeVertex(-1, 0)); _items.Clear(); _rootBoundingBox = new Rectangle(Vector2.Zero, Vector2.Zero); - _maxEntityBoundingRadius = 0f; } /// @@ -342,21 +321,21 @@ namespace SpatialCollections return (vertexIndex, depth, bounds); } - private void QueryProcessChildVertex(Rectangle inflatedBox, Rectangle box, int vertexIndex, - Rectangle vertexBounds, List resultList, Stack stack) + private void QueryProcessChildVertex(Rectangle box, int vertexIndex, Rectangle vertexBounds, + List resultList, Stack stack) { - switch (inflatedBox.Intersects(vertexBounds)) + switch (box.Intersects(vertexBounds)) { case IntersectionType.Contains: if (_vertices[vertexIndex].ChildCount == -1) { // Found contained vertex, appends items of all child vertices. - QueryAppendContainedItems(vertexIndex, box, resultList); + QueryAppendItems(vertexIndex, resultList); } else { // Found leaf, appends items. - QueryAppendContainedLeafItems(vertexIndex, box, resultList); + QueryAppendLeafItems(vertexIndex, resultList); } break; case IntersectionType.Intersects: @@ -367,14 +346,14 @@ namespace SpatialCollections } else { - // Found leaf, appends items. + // Found leaf, appends contained items. QueryAppendContainedLeafItems(vertexIndex, box, resultList); } break; } } - private void QueryAppendContainedItems(int vertexIndex, Rectangle box, List resultList) + private void QueryAppendItems(int vertexIndex, List resultList) { _vertexStack.Push(vertexIndex); @@ -393,22 +372,34 @@ namespace SpatialCollections else { // Found leaf, appends items. - QueryAppendContainedLeafItems(current, box, resultList); + QueryAppendLeafItems(current, resultList); } } } + private void QueryAppendLeafItems(int vertexIndex, List resultList) + { + int index = _vertices[vertexIndex].FirstChildIndex; + while (index != -1) + { + var item = _items[index]; + resultList.Add(item.Entity); + index = item.Next; + } + } + private void QueryAppendContainedLeafItems(int vertexIndex, Rectangle box, List resultList) { int index = _vertices[vertexIndex].FirstChildIndex; while (index != -1) { - T entity = _items[index].Entity; - if (box.Intersects(_getBoundingBoxCallback(entity)) != IntersectionType.Disjoint) + var item = _items[index]; + T entity = item.Entity; + if (box.Contains(_getPositionCallback(entity))) { resultList.Add(entity); } - index = _items[index].Next; + index = item.Next; } } diff --git a/QuadtreeTests/Tests.cs b/QuadtreeTests/Tests.cs index 5865896..bf03783 100644 --- a/QuadtreeTests/Tests.cs +++ b/QuadtreeTests/Tests.cs @@ -1,4 +1,3 @@ -using NSubstitute; using SpatialCollections; using System.Numerics; @@ -6,13 +5,9 @@ namespace QuadtreeTests { public class Tests { - private const float BoundingRadius = 1.0f; - private class TestEntity(Vector2 position) { public Vector2 Position { get; } = position; - - public Rectangle BoundingBox { get; } = new Rectangle(position - Vector2.One, position + Vector2.One); } private List _positions; @@ -26,7 +21,7 @@ namespace QuadtreeTests { _positions = [new Vector2(1.0f, 1.0f), new Vector2(3.0f, 3.0f), new Vector2(5.0f, 8.0f)]; _entities = new(); - _quadtree = new(20, 4, e => e.Position, e => e.BoundingBox); + _quadtree = new(20, 4, e => e.Position); for (int i = 0; i < _positions.Count; i++) { @@ -58,7 +53,7 @@ namespace QuadtreeTests { AddObjectsAndAssertCount(); List result = new(); - _quadtree.Query(new Rectangle(new Vector2(3.5f, 3.5f), new Vector2(10.0f, 10.0f)), result); + _quadtree.Query(new Rectangle(new Vector2(2.5f, 2.5f), new Vector2(10.0f, 10.0f)), result); Assert.That(result.Count, Is.EqualTo(2)); } @@ -66,7 +61,7 @@ namespace QuadtreeTests { for (int i = 0; i < _entities.Count; i++) { - _quadtree.Add(_entities[i], BoundingRadius); + _quadtree.Add(_entities[i]); Assert.That(_quadtree.Count, Is.EqualTo(i + 1)); } }