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; } + } + } +}