8 changed files with 372 additions and 30 deletions
@ -0,0 +1,204 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using Avalonia.Collections.Pooled; |
||||
|
using Avalonia.Platform; |
||||
|
using Avalonia.Rendering; |
||||
|
|
||||
|
namespace Avalonia.Rendering.Composition; |
||||
|
|
||||
|
internal readonly struct CompositionHitTestCandidate(CompositionVisual visual, int order) |
||||
|
{ |
||||
|
|
||||
|
public CompositionVisual Visual { get; } = visual; |
||||
|
public int Order { get; } = order; |
||||
|
} |
||||
|
|
||||
|
internal sealed class CompositionHitTestRTree |
||||
|
{ |
||||
|
private const int MaxChildren = 8; |
||||
|
private readonly List<Entry> _entries = new(); |
||||
|
private readonly List<CompositionHitTestCandidate> _unbounded = new(); |
||||
|
private Node? _root; |
||||
|
private CompositionVisual? _indexedRoot; |
||||
|
|
||||
|
public ulong Revision { get; private set; } |
||||
|
|
||||
|
public bool IsCurrent(CompositionVisual root, ulong revision) => |
||||
|
ReferenceEquals(_indexedRoot, root) && Revision == revision; |
||||
|
|
||||
|
public void Rebuild(CompositionVisual? root, ulong revision) |
||||
|
{ |
||||
|
_indexedRoot = root; |
||||
|
Revision = revision; |
||||
|
_root = null; |
||||
|
_entries.Clear(); |
||||
|
_unbounded.Clear(); |
||||
|
|
||||
|
if (root == null) |
||||
|
return; |
||||
|
|
||||
|
var order = 0; |
||||
|
AddVisual(root, ref order); |
||||
|
_root = BuildTree(_entries); |
||||
|
_entries.Clear(); |
||||
|
} |
||||
|
|
||||
|
public void Query(Point point, PooledList<CompositionHitTestCandidate> results) |
||||
|
{ |
||||
|
if (_root != null) |
||||
|
Query(_root, point, results); |
||||
|
|
||||
|
foreach (var candidate in _unbounded) |
||||
|
results.Add(candidate); |
||||
|
} |
||||
|
|
||||
|
private void AddVisual(CompositionVisual visual, ref int order) |
||||
|
{ |
||||
|
if (visual is CompositionContainerVisual container) |
||||
|
{ |
||||
|
for (var c = container.Children.Count - 1; c >= 0; c--) |
||||
|
AddVisual(container.Children[c], ref order); |
||||
|
} |
||||
|
|
||||
|
if (visual is not CompositionDrawListVisual drawListVisual) |
||||
|
return; |
||||
|
|
||||
|
var visualOrder = order++; |
||||
|
|
||||
|
if (drawListVisual.Visual is ICustomHitTest) |
||||
|
{ |
||||
|
if (drawListVisual.TryGetServerGlobalTransform() != null) |
||||
|
_unbounded.Add(new CompositionHitTestCandidate(drawListVisual, visualOrder)); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (TryGetTransformedBounds(drawListVisual, out var bounds)) |
||||
|
_entries.Add(new Entry(bounds, drawListVisual, visualOrder)); |
||||
|
} |
||||
|
|
||||
|
private static bool TryGetTransformedBounds(CompositionDrawListVisual visual, out LtrbRect bounds) |
||||
|
{ |
||||
|
bounds = default; |
||||
|
|
||||
|
if (visual.DrawList?.Bounds is not { } localBounds) |
||||
|
return false; |
||||
|
|
||||
|
if (visual.TryGetServerGlobalTransform() is not { } transform) |
||||
|
return false; |
||||
|
|
||||
|
bounds = localBounds.TransformToAABB(transform); |
||||
|
return !bounds.IsZeroSize; |
||||
|
} |
||||
|
|
||||
|
private static Node? BuildTree(List<Entry> entries) |
||||
|
{ |
||||
|
if (entries.Count == 0) |
||||
|
return null; |
||||
|
|
||||
|
entries.Sort(CompareEntries); |
||||
|
|
||||
|
var nodes = new List<Node>((entries.Count + MaxChildren - 1) / MaxChildren); |
||||
|
for (var i = 0; i < entries.Count; i += MaxChildren) |
||||
|
{ |
||||
|
var count = Math.Min(MaxChildren, entries.Count - i); |
||||
|
var leafEntries = new Entry[count]; |
||||
|
entries.CopyTo(i, leafEntries, 0, count); |
||||
|
nodes.Add(new Node(GetBounds(leafEntries), leafEntries, null)); |
||||
|
} |
||||
|
|
||||
|
while (nodes.Count > 1) |
||||
|
{ |
||||
|
nodes.Sort(CompareNodes); |
||||
|
|
||||
|
var parents = new List<Node>((nodes.Count + MaxChildren - 1) / MaxChildren); |
||||
|
for (var i = 0; i < nodes.Count; i += MaxChildren) |
||||
|
{ |
||||
|
var count = Math.Min(MaxChildren, nodes.Count - i); |
||||
|
var children = new Node[count]; |
||||
|
nodes.CopyTo(i, children, 0, count); |
||||
|
parents.Add(new Node(GetBounds(children), null, children)); |
||||
|
} |
||||
|
|
||||
|
nodes = parents; |
||||
|
} |
||||
|
|
||||
|
return nodes[0]; |
||||
|
} |
||||
|
|
||||
|
private static void Query(Node node, Point point, PooledList<CompositionHitTestCandidate> results) |
||||
|
{ |
||||
|
if (!Contains(node.Bounds, point)) |
||||
|
return; |
||||
|
|
||||
|
if (node.Entries != null) |
||||
|
{ |
||||
|
foreach (var entry in node.Entries) |
||||
|
{ |
||||
|
if (Contains(entry.Bounds, point)) |
||||
|
results.Add(new CompositionHitTestCandidate(entry.Visual, entry.Order)); |
||||
|
} |
||||
|
} |
||||
|
else if (node.Children != null) |
||||
|
{ |
||||
|
foreach (var child in node.Children) |
||||
|
Query(child, point, results); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static bool Contains(LtrbRect bounds, Point point) => |
||||
|
point.X >= bounds.Left && point.X <= bounds.Right && |
||||
|
point.Y >= bounds.Top && point.Y <= bounds.Bottom; |
||||
|
|
||||
|
private static int CompareEntries(Entry left, Entry right) |
||||
|
{ |
||||
|
var result = (left.Bounds.Left + left.Bounds.Right).CompareTo(right.Bounds.Left + right.Bounds.Right); |
||||
|
return result != 0 |
||||
|
? result |
||||
|
: (left.Bounds.Top + left.Bounds.Bottom).CompareTo(right.Bounds.Top + right.Bounds.Bottom); |
||||
|
} |
||||
|
|
||||
|
private static int CompareNodes(Node left, Node right) |
||||
|
{ |
||||
|
var result = (left.Bounds.Left + left.Bounds.Right).CompareTo(right.Bounds.Left + right.Bounds.Right); |
||||
|
return result != 0 |
||||
|
? result |
||||
|
: (left.Bounds.Top + left.Bounds.Bottom).CompareTo(right.Bounds.Top + right.Bounds.Bottom); |
||||
|
} |
||||
|
|
||||
|
private static LtrbRect GetBounds(Entry[] entries) |
||||
|
{ |
||||
|
var bounds = entries[0].Bounds; |
||||
|
for (var i = 1; i < entries.Length; i++) |
||||
|
bounds = Union(bounds, entries[i].Bounds); |
||||
|
return bounds; |
||||
|
} |
||||
|
|
||||
|
private static LtrbRect GetBounds(Node[] nodes) |
||||
|
{ |
||||
|
var bounds = nodes[0].Bounds; |
||||
|
for (var i = 1; i < nodes.Length; i++) |
||||
|
bounds = Union(bounds, nodes[i].Bounds); |
||||
|
return bounds; |
||||
|
} |
||||
|
|
||||
|
private static LtrbRect Union(LtrbRect left, LtrbRect right) => |
||||
|
new( |
||||
|
Math.Min(left.Left, right.Left), |
||||
|
Math.Min(left.Top, right.Top), |
||||
|
Math.Max(left.Right, right.Right), |
||||
|
Math.Max(left.Bottom, right.Bottom)); |
||||
|
|
||||
|
private readonly struct Entry(LtrbRect bounds, CompositionVisual visual, int order) |
||||
|
{ |
||||
|
public LtrbRect Bounds { get; } = bounds; |
||||
|
public CompositionVisual Visual { get; } = visual; |
||||
|
public int Order { get; } = order; |
||||
|
} |
||||
|
|
||||
|
private sealed class Node(LtrbRect bounds, Entry[]? entries, Node[]? children) |
||||
|
{ |
||||
|
public LtrbRect Bounds { get; } = bounds; |
||||
|
public Entry[]? Entries { get; } = entries; |
||||
|
public Node[]? Children { get; } = children; |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue