diff --git a/osu-framework.sln.DotSettings b/osu-framework.sln.DotSettings
index bad7c7804d..98156ad692 100644
--- a/osu-framework.sln.DotSettings
+++ b/osu-framework.sln.DotSettings
@@ -342,6 +342,7 @@
AABB
API
ARGB
+ BBH
BPM
CG
FBO
@@ -991,6 +992,7 @@ private void load()
True
True
True
+ True
True
True
True
diff --git a/osu.Framework.Benchmarks/BenchmarkPathReceivePositionalInputAt.cs b/osu.Framework.Benchmarks/BenchmarkPathReceivePositionalInputAt.cs
new file mode 100644
index 0000000000..eb9d923fee
--- /dev/null
+++ b/osu.Framework.Benchmarks/BenchmarkPathReceivePositionalInputAt.cs
@@ -0,0 +1,85 @@
+// 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 BenchmarkDotNet.Attributes;
+using osu.Framework.Graphics.Lines;
+using osuTK;
+
+namespace osu.Framework.Benchmarks
+{
+ public class BenchmarkPathReceivePositionalInputAt : BenchmarkTest
+ {
+ // We deliberately set path radius to 0 to introduce worst-case scenario in which with any position given we won't land on a path.
+ private readonly Path path100 = new Path { PathRadius = 0f };
+ private readonly Path path1K = new Path { PathRadius = 0f };
+ private readonly Path path10K = new Path { PathRadius = 0f };
+ private readonly Path path100K = new Path { PathRadius = 0f };
+ private readonly Path path1M = new Path { PathRadius = 0f };
+
+ private readonly Random random = new Random(1);
+
+ public override void SetUp()
+ {
+ base.SetUp();
+
+ List vertices100 = new List(100);
+ List vertices1K = new List(1_000);
+ List vertices10K = new List(10_000);
+ List vertices100K = new List(100_000);
+ List vertices1M = new List(1_000_000);
+
+ for (int i = 0; i < vertices100.Capacity; i++)
+ vertices100.Add(new Vector2((float)i / vertices100.Capacity * 100, random.NextSingle() * 100));
+
+ for (int i = 0; i < vertices1K.Capacity; i++)
+ vertices1K.Add(new Vector2((float)i / vertices1K.Capacity * 100, random.NextSingle() * 100));
+
+ for (int i = 0; i < vertices10K.Capacity; i++)
+ vertices10K.Add(new Vector2((float)i / vertices10K.Capacity * 100, random.NextSingle() * 100));
+
+ for (int i = 0; i < vertices100K.Capacity; i++)
+ vertices100K.Add(new Vector2((float)i / vertices100K.Capacity * 100, random.NextSingle() * 100));
+
+ for (int i = 0; i < vertices1M.Capacity; i++)
+ vertices1M.Add(new Vector2((float)i / vertices1M.Capacity * 100, random.NextSingle() * 100));
+
+ path100.Vertices = vertices100;
+ path1K.Vertices = vertices1K;
+ path10K.Vertices = vertices10K;
+ path100K.Vertices = vertices100K;
+ path1M.Vertices = vertices1M;
+ }
+
+ [Benchmark]
+ public void Contains100()
+ {
+ path100.ReceivePositionalInputAt(new Vector2(random.NextSingle() * 100, random.NextSingle() * 100));
+ }
+
+ [Benchmark]
+ public void Contains1K()
+ {
+ path1K.ReceivePositionalInputAt(new Vector2(random.NextSingle() * 100, random.NextSingle() * 100));
+ }
+
+ [Benchmark]
+ public void Contains10K()
+ {
+ path10K.ReceivePositionalInputAt(new Vector2(random.NextSingle() * 100, random.NextSingle() * 100));
+ }
+
+ [Benchmark]
+ public void Contains100K()
+ {
+ path100K.ReceivePositionalInputAt(new Vector2(random.NextSingle() * 100, random.NextSingle() * 100));
+ }
+
+ [Benchmark]
+ public void Contains1M()
+ {
+ path1M.ReceivePositionalInputAt(new Vector2(random.NextSingle() * 100, random.NextSingle() * 100));
+ }
+ }
+}
diff --git a/osu.Framework.Benchmarks/BenchmarkPathSegmentCreation.cs b/osu.Framework.Benchmarks/BenchmarkPathSegmentCreation.cs
new file mode 100644
index 0000000000..79426cd1ec
--- /dev/null
+++ b/osu.Framework.Benchmarks/BenchmarkPathSegmentCreation.cs
@@ -0,0 +1,87 @@
+// 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 BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Engines;
+using osu.Framework.Graphics.Lines;
+using osu.Framework.Graphics.Primitives;
+using osuTK;
+
+namespace osu.Framework.Benchmarks
+{
+ public partial class BenchmarkPathSegmentCreation : BenchmarkTest
+ {
+ private readonly List vertices100 = new List(100);
+ private readonly List vertices1K = new List(1_000);
+ private readonly List vertices10K = new List(10_000);
+ private readonly List vertices100K = new List(100_000);
+ private readonly List vertices1M = new List(1_000_000);
+
+ private readonly BenchPath path = new BenchPath();
+ private readonly Consumer consumer = new Consumer();
+
+ public override void SetUp()
+ {
+ base.SetUp();
+
+ var rng = new Random(1);
+
+ for (int i = 0; i < vertices100.Capacity; i++)
+ vertices100.Add(new Vector2(rng.NextSingle(), rng.NextSingle()));
+
+ for (int i = 0; i < vertices1K.Capacity; i++)
+ vertices1K.Add(new Vector2(rng.NextSingle(), rng.NextSingle()));
+
+ for (int i = 0; i < vertices10K.Capacity; i++)
+ vertices10K.Add(new Vector2(rng.NextSingle(), rng.NextSingle()));
+
+ for (int i = 0; i < vertices100K.Capacity; i++)
+ vertices100K.Add(new Vector2(rng.NextSingle(), rng.NextSingle()));
+
+ for (int i = 0; i < vertices1M.Capacity; i++)
+ vertices1M.Add(new Vector2(rng.NextSingle(), rng.NextSingle()));
+ }
+
+ [Benchmark]
+ public void Compute100Segments()
+ {
+ path.Vertices = vertices100;
+ consumer.Consume(path.Segments);
+ }
+
+ [Benchmark]
+ public void Compute1KSegments()
+ {
+ path.Vertices = vertices1K;
+ consumer.Consume(path.Segments);
+ }
+
+ [Benchmark]
+ public void Compute10KSegments()
+ {
+ path.Vertices = vertices10K;
+ consumer.Consume(path.Segments);
+ }
+
+ [Benchmark]
+ public void Compute100KSegments()
+ {
+ path.Vertices = vertices100K;
+ consumer.Consume(path.Segments);
+ }
+
+ [Benchmark]
+ public void Compute1MSegments()
+ {
+ path.Vertices = vertices1M;
+ consumer.Consume(path.Segments);
+ }
+
+ private partial class BenchPath : Path
+ {
+ public IEnumerable Segments => BBH.Segments;
+ }
+ }
+}
diff --git a/osu.Framework.Tests/Visual/Drawables/TestSceneInteractivePathDrawing.cs b/osu.Framework.Tests/Visual/Drawables/TestSceneInteractivePathDrawing.cs
index ea688a65dc..e59f209b52 100644
--- a/osu.Framework.Tests/Visual/Drawables/TestSceneInteractivePathDrawing.cs
+++ b/osu.Framework.Tests/Visual/Drawables/TestSceneInteractivePathDrawing.cs
@@ -1,12 +1,18 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Collections.Generic;
using System.Linq;
+using osu.Framework.Allocation;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Colour;
using osuTK.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Lines;
+using osu.Framework.Graphics.Primitives;
+using osu.Framework.Graphics.Rendering;
+using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osuTK.Input;
using osu.Framework.Utils;
@@ -18,9 +24,10 @@ namespace osu.Framework.Tests.Visual.Drawables
public partial class TestSceneInteractivePathDrawing : FrameworkTestScene
{
private readonly Path rawDrawnPath;
- private readonly Path approximatedDrawnPath;
+ private readonly TestPath approximatedDrawnPath;
private readonly Path controlPointPath;
private readonly Container controlPointViz;
+ private readonly BoundingBoxVisualizer bbViz;
private readonly IncrementalBSplineBuilder bSplineBuilder = new IncrementalBSplineBuilder();
@@ -36,7 +43,7 @@ public TestSceneInteractivePathDrawing()
Colour = Color4.DeepPink,
PathRadius = 5,
},
- approximatedDrawnPath = new Path
+ approximatedDrawnPath = new TestPath
{
Colour = Color4.Blue,
PathRadius = 3,
@@ -52,31 +59,42 @@ public TestSceneInteractivePathDrawing()
RelativeSizeAxes = Axes.Both,
Alpha = 0.5f,
},
+ bbViz = new BoundingBoxVisualizer
+ {
+ RelativeSizeAxes = Axes.Both,
+ }
}
};
- updateViz();
- OnUpdate += _ => updateViz();
-
AddStep("Reset path", () =>
{
bSplineBuilder.Clear();
+ updateViz();
});
AddSliderStep($"{nameof(bSplineBuilder.Degree)}", 1, 4, 3, v =>
{
bSplineBuilder.Degree = v;
+ updateViz();
});
AddSliderStep($"{nameof(bSplineBuilder.Tolerance)}", 0f, 3f, 2f, v =>
{
bSplineBuilder.Tolerance = v;
+ updateViz();
});
AddSliderStep($"{nameof(bSplineBuilder.CornerThreshold)}", 0f, 1f, 0.4f, v =>
{
bSplineBuilder.CornerThreshold = v;
+ updateViz();
});
}
+ [BackgroundDependencyLoader]
+ private void load(IRenderer renderer)
+ {
+ bbViz.Texture = renderer.WhitePixel;
+ }
+
private void updateControlPointsViz()
{
controlPointPath.Vertices = bSplineBuilder.ControlPoints.SelectMany(o => o).ToArray();
@@ -117,9 +135,18 @@ private void updateViz()
updateControlPointsViz();
}
+ protected override void Update()
+ {
+ base.Update();
+
+ approximatedDrawnPath.CollectBoundingBoxes(bbViz.Boxes);
+ bbViz.Invalidate(Invalidation.DrawNode);
+ }
+
protected override void OnDrag(DragEvent e)
{
bSplineBuilder.AddLinearPoint(rawDrawnPath.ToLocalSpace(ToScreenSpace(e.MousePosition)));
+ updateViz();
}
protected override void OnDragEnd(DragEndEvent e)
@@ -129,5 +156,65 @@ protected override void OnDragEnd(DragEndEvent e)
base.OnDragEnd(e);
}
+
+ private partial class TestPath : Path
+ {
+ public void CollectBoundingBoxes(List list) => BBH.CollectBoundingBoxes(list);
+ }
+
+ private partial class BoundingBoxVisualizer : Sprite
+ {
+ public readonly List Boxes = [];
+
+ public BoundingBoxVisualizer()
+ {
+ RelativeSizeAxes = Axes.Both;
+ }
+
+ protected override DrawNode CreateDrawNode() => new BoundingBoxDrawNode(this);
+
+ private class BoundingBoxDrawNode : SpriteDrawNode
+ {
+ public new BoundingBoxVisualizer Source => (BoundingBoxVisualizer)base.Source;
+
+ public BoundingBoxDrawNode(BoundingBoxVisualizer source)
+ : base(source)
+ {
+ }
+
+ private readonly List boxes = new List();
+
+ public override void ApplyState()
+ {
+ base.ApplyState();
+
+ boxes.Clear();
+ boxes.AddRange(Source.Boxes);
+ }
+
+ protected override void Draw(IRenderer renderer)
+ {
+ renderer.PushLocalMatrix(DrawInfo.Matrix);
+ base.Draw(renderer);
+ renderer.PopLocalMatrix();
+ }
+
+ protected override void Blit(IRenderer renderer)
+ {
+ ColourInfo colourInfo = DrawColourInfo.Colour;
+ colourInfo.ApplyChild(Color4.Red);
+
+ foreach (var box in boxes)
+ {
+ renderer.DrawQuad(Texture, new Quad(box.TopLeft, box.TopRight, box.TopLeft + new Vector2(0, 1), box.TopRight + new Vector2(0, 1)), colourInfo);
+ renderer.DrawQuad(Texture, new Quad(box.BottomLeft - new Vector2(0, 1), box.BottomRight - new Vector2(0, 1), box.BottomLeft, box.BottomRight), colourInfo);
+ renderer.DrawQuad(Texture, new Quad(box.TopLeft, box.TopLeft + new Vector2(1, 0), box.BottomLeft, box.BottomLeft + new Vector2(1, 0)), colourInfo);
+ renderer.DrawQuad(Texture, new Quad(box.TopRight - new Vector2(1, 0), box.TopRight, box.BottomRight - new Vector2(1, 0), box.BottomRight), colourInfo);
+ }
+ }
+
+ protected internal override bool CanDrawOpaqueInterior => false;
+ }
+ }
}
}
diff --git a/osu.Framework/Graphics/Lines/Path.DrawNode.cs b/osu.Framework/Graphics/Lines/Path.DrawNode.cs
index 8434cf8add..f76b9a8145 100644
--- a/osu.Framework/Graphics/Lines/Path.DrawNode.cs
+++ b/osu.Framework/Graphics/Lines/Path.DrawNode.cs
@@ -27,6 +27,8 @@ private class PathDrawNode : DrawNode
private float radius;
private IShader? pathShader;
+ private Vector2 pathOffset;
+ private int treeVersion;
private IVertexBatch? quadBatch;
@@ -39,8 +41,21 @@ public override void ApplyState()
{
base.ApplyState();
- segments.Clear();
- segments.AddRange(Source.segments);
+ var bbh = Source.BBH;
+
+ int newTreeVersion = bbh.TreeVersion;
+
+ // BufferedDrawNode can trigger ApplyState for child draw node
+ // even in cases when path isn't being redrawn (for example with alpha change)
+ if (newTreeVersion != treeVersion)
+ {
+ segments.Clear();
+ segments.AddRange(bbh.Segments);
+
+ treeVersion = newTreeVersion;
+ }
+
+ pathOffset = bbh.VertexBounds.TopLeft;
radius = Source.PathRadius;
pathShader = Source.pathShader;
@@ -158,10 +173,10 @@ private void drawQuad(Vector2 topLeft, Vector2 topRight, Vector2 bottomLeft, Vec
{
Debug.Assert(quadBatch != null);
- quadBatch.Add(new PathVertex(topLeft, start, end, radius));
- quadBatch.Add(new PathVertex(topRight, start, end, radius));
- quadBatch.Add(new PathVertex(bottomRight, start, end, radius));
- quadBatch.Add(new PathVertex(bottomLeft, start, end, radius));
+ quadBatch.Add(new PathVertex(topLeft, start, end, radius, pathOffset));
+ quadBatch.Add(new PathVertex(topRight, start, end, radius, pathOffset));
+ quadBatch.Add(new PathVertex(bottomRight, start, end, radius, pathOffset));
+ quadBatch.Add(new PathVertex(bottomLeft, start, end, radius, pathOffset));
}
private void updateVertexBuffer()
@@ -322,11 +337,11 @@ public DrawableSegment(Line guide, float radius)
[VertexMember(1, VertexAttribPointerType.Float)]
public readonly float Radius;
- public PathVertex(Vector2 position, Vector2 startPos, Vector2 endPos, float radius)
+ public PathVertex(Vector2 position, Vector2 startPos, Vector2 endPos, float radius, Vector2 pathOffset)
{
- Position = position;
- StartPos = startPos;
- EndPos = endPos;
+ Position = position - pathOffset;
+ StartPos = startPos - pathOffset;
+ EndPos = endPos - pathOffset;
Radius = radius;
}
diff --git a/osu.Framework/Graphics/Lines/Path.cs b/osu.Framework/Graphics/Lines/Path.cs
index 73a02e943f..6fab8457d0 100644
--- a/osu.Framework/Graphics/Lines/Path.cs
+++ b/osu.Framework/Graphics/Lines/Path.cs
@@ -50,8 +50,7 @@ public IReadOnlyList Vertices
vertices.Clear();
vertices.AddRange(value);
- vertexBoundsCache.Invalidate();
- segmentsCache.Invalidate();
+ bbhCache.Invalidate();
Invalidate(Invalidation.DrawSize);
}
@@ -74,8 +73,7 @@ public virtual float PathRadius
pathRadius = value;
- vertexBoundsCache.Invalidate();
- segmentsCache.Invalidate();
+ bbhCache.Invalidate();
Invalidate(Invalidation.DrawSize);
}
@@ -170,50 +168,9 @@ public override Vector2 Size
}
}
- private readonly Cached vertexBoundsCache = new Cached();
+ private RectangleF vertexBounds => BBH.VertexBounds;
- private RectangleF vertexBounds
- {
- get
- {
- if (vertexBoundsCache.IsValid)
- return vertexBoundsCache.Value;
-
- if (vertices.Count > 0)
- {
- float minX = 0;
- float minY = 0;
- float maxX = 0;
- float maxY = 0;
-
- foreach (var v in vertices)
- {
- minX = Math.Min(minX, v.X - PathRadius);
- minY = Math.Min(minY, v.Y - PathRadius);
- maxX = Math.Max(maxX, v.X + PathRadius);
- maxY = Math.Max(maxY, v.Y + PathRadius);
- }
-
- return vertexBoundsCache.Value = new RectangleF(minX, minY, maxX - minX, maxY - minY);
- }
-
- return vertexBoundsCache.Value = new RectangleF(0, 0, 0, 0);
- }
- }
-
- 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;
- }
+ public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => BBH.Contains(ToLocalSpace(screenSpacePos));
public Vector2 PositionInBoundingBox(Vector2 pos) => pos - vertexBounds.TopLeft;
@@ -224,8 +181,7 @@ public void ClearVertices()
vertices.Clear();
- vertexBoundsCache.Invalidate();
- segmentsCache.Invalidate();
+ bbhCache.Invalidate();
Invalidate(Invalidation.DrawSize);
}
@@ -234,8 +190,7 @@ public void AddVertex(Vector2 pos)
{
vertices.Add(pos);
- vertexBoundsCache.Invalidate();
- segmentsCache.Invalidate();
+ bbhCache.Invalidate();
Invalidate(Invalidation.DrawSize);
}
@@ -244,29 +199,21 @@ public void ReplaceVertex(int index, Vector2 pos)
{
vertices[index] = pos;
- vertexBoundsCache.Invalidate();
- segmentsCache.Invalidate();
+ bbhCache.Invalidate();
Invalidate(Invalidation.DrawSize);
}
- private readonly List segmentsBacking = new List();
- private readonly Cached segmentsCache = new Cached();
- private List segments => segmentsCache.IsValid ? segmentsBacking : generateSegments();
+ private readonly PathBBH bbhBacking = new PathBBH();
+ private readonly Cached bbhCache = new Cached();
- private List generateSegments()
- {
- segmentsBacking.Clear();
+ protected PathBBH BBH => bbhCache.IsValid ? bbhBacking : computeBBH();
- 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));
- }
-
- segmentsCache.Validate();
- return segmentsBacking;
+ private PathBBH computeBBH()
+ {
+ bbhBacking.SetVertices(vertices, pathRadius);
+ bbhCache.Validate();
+ return bbhBacking;
}
private Texture texture;
@@ -374,6 +321,7 @@ protected override void Dispose(bool isDisposing)
texture = null;
sharedData.Dispose();
+ bbhBacking.Dispose();
}
}
}
diff --git a/osu.Framework/Graphics/Lines/PathBBH.cs b/osu.Framework/Graphics/Lines/PathBBH.cs
new file mode 100644
index 0000000000..a4f5683b62
--- /dev/null
+++ b/osu.Framework/Graphics/Lines/PathBBH.cs
@@ -0,0 +1,261 @@
+// 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.Buffers;
+using System.Collections.Generic;
+using System.Diagnostics;
+using osu.Framework.Graphics.Primitives;
+using osuTK;
+
+namespace osu.Framework.Graphics.Lines
+{
+ ///
+ /// A Bounding Box Hierarchy of a set of vertices which when drawn consecutively represent a path.
+ ///
+ public class PathBBH : IDisposable
+ {
+ public IEnumerable Segments
+ {
+ get
+ {
+ if (segmentCount > 0 && nodes != null)
+ {
+ for (int i = firstLeafIndex; i <= lastLeafIndex; i++)
+ yield return nodes[i].Segment!.Value;
+ }
+ }
+ }
+
+ public RectangleF VertexBounds { get; private set; } = RectangleF.Empty;
+
+ public int TreeVersion { get; private set; }
+
+ private float radius;
+ private BBHNode[]? nodes;
+ private int treeDepth;
+ private int firstLeafIndex;
+ private int lastLeafIndex;
+ private int segmentCount;
+
+ public void SetVertices(IReadOnlyList vertices, float pathRadius)
+ {
+ ObjectDisposedException.ThrowIf(isDisposed, this);
+
+ TreeVersion++;
+ radius = pathRadius;
+
+ segmentCount = Math.Max(vertices.Count - 1, 0);
+ // Definition of a leaf here is a node containing a segment
+ // Since we are building a binary tree, compute the max value that is bigger than segmentCount and the power of 2.
+ // That would be the bottom layer of a tree.
+ int maxLeafCount = Math.Max((int)System.Numerics.BitOperations.RoundUpToPowerOf2((uint)segmentCount), 1);
+ treeDepth = (int)Math.Log2(maxLeafCount);
+
+ // We can avoid storing empty nodes by computing amount of all the nodes within a tree,
+ // which have descendant with at least 1 leaf (or leaf itself).
+ // That would be the size of an array holding the tree
+ int arrayLength = segmentCount;
+ int nodesOnDepth = segmentCount;
+
+ for (int i = treeDepth - 1; i >= 0; i--)
+ {
+ nodesOnDepth = (nodesOnDepth + 1) / 2;
+ arrayLength += nodesOnDepth;
+ }
+
+ firstLeafIndex = arrayLength - segmentCount;
+ lastLeafIndex = arrayLength - 1;
+
+ if (nodes != null)
+ {
+ if (nodes.Length < arrayLength)
+ {
+ ArrayPool.Shared.Return(nodes);
+ nodes = ArrayPool.Shared.Rent(arrayLength);
+ }
+ }
+ else
+ {
+ nodes = ArrayPool.Shared.Rent(arrayLength);
+ }
+
+ switch (vertices.Count)
+ {
+ case 0:
+ VertexBounds = RectangleF.Empty;
+ break;
+
+ case 1:
+ VertexBounds = RectangleF.Union(new RectangleF(vertices[0] - new Vector2(radius), new Vector2(radius * 2)), RectangleF.Empty);
+ break;
+
+ default:
+ {
+ computeNodes(vertices);
+ VertexBounds = RectangleF.Union(nodes[0].Bounds, RectangleF.Empty);
+ break;
+ }
+ }
+ }
+
+ private void computeNodes(IReadOnlyList vertices)
+ {
+ Debug.Assert(nodes != null);
+
+ for (int i = 0; i < vertices.Count - 1; i++)
+ {
+ var segment = new Line(vertices[i], vertices[i + 1]);
+
+ nodes[firstLeafIndex + i] = new BBHNode
+ {
+ Bounds = lineAABB(segment, radius),
+ Segment = segment
+ };
+ }
+
+ if (segmentCount == 1) // At this point root must contain a segment, no parent nodes exist.
+ return;
+
+ int nodesOnCurrentDepth = segmentCount;
+ int currentNodeIndex = lastLeafIndex - segmentCount;
+
+ // iterate over the tree layers starting from the bottom
+ for (int i = treeDepth - 1; i >= 0; i--)
+ {
+ int nodesOnNextDepth = nodesOnCurrentDepth;
+ nodesOnCurrentDepth = Math.Max((nodesOnCurrentDepth + 1) / 2, 1);
+
+ // iterate over the tree nodes within a layer from right to left
+ for (int j = nodesOnCurrentDepth - 1; j >= 0; j--)
+ {
+ int offset = (nodesOnCurrentDepth - j) + 2 * j;
+ int left = currentNodeIndex + offset;
+ int rightOffset = offset + 1;
+
+ // Right child exists
+ if (rightOffset <= nodesOnNextDepth)
+ {
+ int right = currentNodeIndex + rightOffset;
+
+ nodes[currentNodeIndex] = new BBHNode
+ {
+ Bounds = RectangleF.Union(nodes[left].Bounds, nodes[right].Bounds),
+ Left = left,
+ Right = right,
+ };
+ }
+ else
+ {
+ nodes[currentNodeIndex] = new BBHNode
+ {
+ Bounds = nodes[left].Bounds,
+ Left = left,
+ };
+ }
+
+ currentNodeIndex--;
+ }
+ }
+ }
+
+ ///
+ /// Whether any segment of a current path contains a given point.
+ ///
+ /// A point in local coordinates.
+ public bool Contains(Vector2 localPos) => segmentCount > 0 && nodes != null && contains(localPos + VertexBounds.TopLeft, 0);
+
+ private bool contains(Vector2 position, int? index)
+ {
+ if (!index.HasValue)
+ return false;
+
+ BBHNode node = nodes![index.Value];
+
+ if (!node.Bounds.Contains(position))
+ return false;
+
+ if (node.IsLeaf)
+ return node.Segment!.Value.DistanceSquaredToPoint(position) < radius * radius;
+
+ return contains(position, node.Left) || contains(position, node.Right);
+ }
+
+ public void CollectBoundingBoxes(List boxes)
+ {
+ boxes.Clear();
+
+ if (segmentCount == 0 || nodes?.Length == 0)
+ return;
+
+ collectBoundingBoxes(0, boxes);
+ }
+
+ private void collectBoundingBoxes(int? index, List boxes)
+ {
+ if (!index.HasValue)
+ return;
+
+ BBHNode node = nodes![index.Value];
+
+ boxes.Add(new RectangleF(node.Bounds.TopLeft - VertexBounds.TopLeft, node.Bounds.Size));
+
+ if (node.IsLeaf)
+ return;
+
+ collectBoundingBoxes(node.Left, boxes);
+ collectBoundingBoxes(node.Right, boxes);
+ }
+
+ private static RectangleF lineAABB(Line line, float radius)
+ {
+ float minX = Math.Min(line.StartPoint.X, line.EndPoint.X);
+ float minY = Math.Min(line.StartPoint.Y, line.EndPoint.Y);
+ float maxX = line.StartPoint.X + line.EndPoint.X - minX;
+ float maxY = line.StartPoint.Y + line.EndPoint.Y - minY;
+ return new RectangleF(minX - radius, minY - radius, maxX - minX + radius * 2, maxY - minY + radius * 2);
+ }
+
+ private bool isDisposed;
+
+ public void Dispose()
+ {
+ if (isDisposed)
+ return;
+
+ isDisposed = true;
+
+ if (nodes != null)
+ ArrayPool.Shared.Return(nodes);
+ }
+
+ private readonly struct BBHNode
+ {
+ ///
+ /// Index of a left child of this in the tree array.
+ ///
+ public int Left { get; init; }
+
+ ///
+ /// Index of a right child of this in the tree array. Null if no right child exists.
+ ///
+ public int? Right { get; init; }
+
+ ///
+ /// Whether this contains a path segment.
+ ///
+ public bool IsLeaf => Segment.HasValue;
+
+ ///
+ /// The line which represents a path segment in case when this is marked as a .
+ ///
+ public Line? Segment { get; init; }
+
+ ///
+ /// If - bounding box of the .
+ /// Otherwise - combined bounding box of and nodes.
+ ///
+ public required RectangleF Bounds { get; init; }
+ }
+ }
+}