using System; using System.Collections.Generic; using Windows.Foundation; namespace Sonex.Client.Controls.SceneCanvas; internal sealed class SceneSpatialIndex { private readonly Dictionary> _cells = []; private readonly Dictionary> _objectCells = []; private readonly float _cellSizeMeters; public SceneSpatialIndex(float cellSizeMeters) { _cellSizeMeters = Math.Max(0.1f, cellSizeMeters); } public void Clear() { _cells.Clear(); _objectCells.Clear(); } public void Add(SceneCanvasObject sceneObject) { Update(sceneObject); } public bool Remove(SceneCanvasObject sceneObject) { if (sceneObject == null) { return false; } if (!_objectCells.TryGetValue(sceneObject, out var keys)) { return false; } foreach (var key in keys) { if (_cells.TryGetValue(key, out var list)) { list.Remove(sceneObject); if (list.Count == 0) { _cells.Remove(key); } } } _objectCells.Remove(sceneObject); return true; } public void Update(SceneCanvasObject sceneObject) { if (sceneObject == null) { return; } var keys = CollectKeys(sceneObject.SpatialBoundsMeters); if (_objectCells.TryGetValue(sceneObject, out var existingKeys)) { if (HaveSameKeys(existingKeys, keys)) { return; } RemoveFromCells(sceneObject, existingKeys); } _objectCells[sceneObject] = keys; AddToCells(sceneObject, keys); } public void Query(Rect worldBoundsMeters, List destination, int queryToken) { if (destination == null) { return; } if (_cells.Count == 0) { return; } var minX = ToCellX(worldBoundsMeters.X); var minY = ToCellY(worldBoundsMeters.Y); var maxX = ToCellX(worldBoundsMeters.X + worldBoundsMeters.Width); var maxY = ToCellY(worldBoundsMeters.Y + worldBoundsMeters.Height); for (int y = minY; y <= maxY; y++) { for (int x = minX; x <= maxX; x++) { var key = MakeKey(x, y); if (!_cells.TryGetValue(key, out var list)) { continue; } foreach (var sceneObject in list) { if (sceneObject.LastQueryToken == queryToken) { continue; } sceneObject.LastQueryToken = queryToken; if (!sceneObject.IsVisible) { continue; } if (!Intersects(worldBoundsMeters, sceneObject.SpatialBoundsMeters)) { continue; } destination.Add(sceneObject); } } } } private List CollectKeys(Rect boundsMeters) { var normalized = NormalizeRect(boundsMeters); var minX = ToCellX(normalized.X); var minY = ToCellY(normalized.Y); var maxX = ToCellX(normalized.X + normalized.Width); var maxY = ToCellY(normalized.Y + normalized.Height); var keys = new List((maxX - minX + 1) * (maxY - minY + 1)); for (int y = minY; y <= maxY; y++) { for (int x = minX; x <= maxX; x++) { keys.Add(MakeKey(x, y)); } } return keys; } private void AddToCells(SceneCanvasObject sceneObject, List keys) { foreach (var key in keys) { if (!_cells.TryGetValue(key, out var list)) { list = []; _cells[key] = list; } list.Add(sceneObject); } } private void RemoveFromCells(SceneCanvasObject sceneObject, List keys) { foreach (var key in keys) { if (!_cells.TryGetValue(key, out var list)) { continue; } list.Remove(sceneObject); if (list.Count == 0) { _cells.Remove(key); } } } private static bool HaveSameKeys(List currentKeys, List nextKeys) { if (ReferenceEquals(currentKeys, nextKeys)) { return true; } if (currentKeys == null || nextKeys == null || currentKeys.Count != nextKeys.Count) { return false; } for (var i = 0; i < currentKeys.Count; i++) { if (currentKeys[i] != nextKeys[i]) { return false; } } return true; } private int ToCellX(double x) { return (int)Math.Floor(x / _cellSizeMeters); } private int ToCellY(double y) { return (int)Math.Floor(y / _cellSizeMeters); } private static long MakeKey(int x, int y) { return ((long)x << 32) ^ (uint)y; } private static bool Intersects(Rect a, Rect b) { return a.X < b.X + b.Width && a.X + a.Width > b.X && a.Y < b.Y + b.Height && a.Y + a.Height > b.Y; } private static Rect NormalizeRect(Rect rect) { var x = rect.X; var y = rect.Y; var width = rect.Width; var height = rect.Height; if (width < 0d) { x += width; width = -width; } if (height < 0d) { y += height; height = -height; } return new Rect(x, y, Math.Max(0d, width), Math.Max(0d, height)); } }