Skip to main content

horizon_event_system/gorc/spatial/
rtree.rs

1//! R*-tree based spatial indexing for GORC
2//!
3//! This module provides a 3D spatial index backed by the `rstar` crate. It replaces
4//! the legacy quadtree implementation while preserving the existing public API
5//! expected by the rest of the system.
6
7use super::query::{QueryFilters, QueryResult, SpatialQuery};
8use crate::types::{PlayerId, Position, Vec3};
9use crate::utils::current_timestamp;
10use rstar::{PointDistance, RTree, RTreeObject, AABB};
11use std::collections::HashMap;
12
13/// Entry stored inside the R-tree.
14#[derive(Debug, Clone)]
15struct SpatialEntry {
16    object: SpatialObject,
17    point: [f64; 3],
18}
19
20impl SpatialEntry {
21    fn new(object: SpatialObject) -> Self {
22        let point = [object.position.x, object.position.y, object.position.z];
23        Self { object, point }
24    }
25}
26
27impl PartialEq for SpatialEntry {
28    fn eq(&self, other: &Self) -> bool {
29        self.object.player_id == other.object.player_id
30    }
31}
32
33impl Eq for SpatialEntry {}
34
35impl RTreeObject for SpatialEntry {
36    type Envelope = AABB<[f64; 3]>;
37
38    fn envelope(&self) -> Self::Envelope {
39        AABB::from_point(self.point)
40    }
41}
42
43impl PointDistance for SpatialEntry {
44    fn distance_2(&self, point: &[f64; 3]) -> f64 {
45        let dx = self.point[0] - point[0];
46        let dy = self.point[1] - point[1];
47        let dz = self.point[2] - point[2];
48        dx * dx + dy * dy + dz * dz
49    }
50
51    fn contains_point(&self, point: &[f64; 3]) -> bool {
52        (self.point[0] - point[0]).abs() < f64::EPSILON
53            && (self.point[1] - point[1]).abs() < f64::EPSILON
54            && (self.point[2] - point[2]).abs() < f64::EPSILON
55    }
56}
57
58/// Object stored in the spatial index
59#[derive(Debug, Clone)]
60pub struct SpatialObject {
61    /// Player identifier
62    pub player_id: PlayerId,
63    /// Object position
64    pub position: Position,
65    /// Last update timestamp
66    pub last_updated: u64,
67}
68
69impl SpatialObject {
70    /// Creates a new spatial object
71    pub fn new(player_id: PlayerId, position: Position) -> Self {
72        Self {
73            player_id,
74            position,
75            last_updated: current_timestamp(),
76        }
77    }
78}
79
80/// Statistics for analyzing R-tree performance
81#[derive(Debug, Clone, Default)]
82pub struct SpatialIndexStats {
83    pub total_insertions: usize,
84    pub total_queries: usize,
85    pub total_removals: usize,
86    pub total_clears: usize,
87    pub total_rebuilds: usize,
88    pub last_query_result_count: usize,
89    pub current_depth: u8,
90    pub leaf_nodes: usize,
91    pub internal_nodes: usize,
92}
93
94/// Detailed node statistics (approximated for R-tree)
95#[derive(Debug, Clone, Default)]
96pub struct NodeStats {
97    pub total_objects: usize,
98    pub max_depth: u8,
99    pub leaf_nodes: usize,
100    pub internal_nodes: usize,
101}
102
103/// High-performance regional R*-tree for efficient spatial queries
104#[derive(Debug)]
105pub struct RegionRTree {
106    /// Root bounds of the tree (used for stats)
107    bounds: (Vec3, Vec3),
108    /// Underlying R-tree
109    tree: RTree<SpatialEntry>,
110    /// Cached entries for efficient updates/removals
111    player_entries: HashMap<PlayerId, SpatialEntry>,
112    /// Total objects in the tree
113    object_count: usize,
114    /// Performance statistics
115    stats: SpatialIndexStats,
116}
117
118impl RegionRTree {
119    /// Creates a new R-tree with specified bounds
120    pub fn new(min: Vec3, max: Vec3) -> Self {
121        Self {
122            bounds: (min, max),
123            tree: RTree::new(),
124            player_entries: HashMap::new(),
125            object_count: 0,
126            stats: SpatialIndexStats::default(),
127        }
128    }
129
130    /// Inserts or updates a player at a position with O(log n) performance
131    pub fn insert_player(&mut self, player_id: PlayerId, position: Position) {
132        let object = SpatialObject::new(player_id, position);
133        self.insert_object(object);
134    }
135
136    /// Inserts or updates any spatial object with O(log n) performance
137    pub fn insert_object(&mut self, object: SpatialObject) {
138        let player_id = object.player_id;
139        let entry = SpatialEntry::new(object);
140
141        if let Some(existing) = self.player_entries.remove(&player_id) {
142            let _ = self.tree.remove(&existing);
143            self.object_count = self.object_count.saturating_sub(1);
144        }
145
146        self.tree.insert(entry.clone());
147        self.player_entries.insert(player_id, entry);
148        self.object_count += 1;
149        self.stats.total_insertions += 1;
150    }
151
152    /// Queries players within a radius with O(log n) performance
153    pub fn query_radius(&mut self, center: Position, radius: f64) -> Vec<QueryResult> {
154        let query = SpatialQuery {
155            center,
156            radius,
157            filters: QueryFilters::default(),
158        };
159        self.query(query)
160    }
161
162    /// Executes a spatial query with optional filters
163    pub fn query(&mut self, query: SpatialQuery) -> Vec<QueryResult> {
164        let center_point = [query.center.x, query.center.y, query.center.z];
165        let radius_sq = query.radius * query.radius;
166
167        let search_distance = radius_sq;
168
169        let mut results: Vec<QueryResult> = self
170            .tree
171            .locate_within_distance(center_point, search_distance)
172            .filter_map(|entry| {
173                let object = &entry.object;
174
175                // Apply include/exclude filters
176                if let Some(include) = &query.filters.include_players {
177                    if !include.contains(&object.player_id) {
178                        return None;
179                    }
180                }
181
182                if let Some(exclude) = &query.filters.exclude_players {
183                    if exclude.contains(&object.player_id) {
184                        return None;
185                    }
186                }
187
188                let distance_sq = entry.distance_2(&center_point);
189                if distance_sq > radius_sq {
190                    return None;
191                }
192
193                let distance = distance_sq.sqrt();
194
195                if let Some(min_distance) = query.filters.min_distance {
196                    if distance < min_distance {
197                        return None;
198                    }
199                }
200
201                Some(QueryResult {
202                    player_id: object.player_id,
203                    position: object.position,
204                    distance,
205                    metadata: HashMap::new(),
206                })
207            })
208            .collect();
209
210        if let Some(max_results) = query.filters.max_results {
211            results.truncate(max_results);
212        }
213
214        self.stats.total_queries += 1;
215        self.stats.last_query_result_count = results.len();
216        results
217    }
218
219    /// Removes all objects for a player (O(log n))
220    pub fn remove_player(&mut self, player_id: PlayerId) -> usize {
221        if let Some(existing) = self.player_entries.remove(&player_id) {
222            let removed = self.tree.remove(&existing).is_some();
223            if removed {
224                self.object_count = self.object_count.saturating_sub(1);
225                self.stats.total_removals += 1;
226                return 1;
227            }
228        }
229        0
230    }
231
232    /// Gets the total number of objects
233    pub fn object_count(&self) -> usize {
234        self.object_count
235    }
236
237    /// Checks whether a given player is indexed
238    pub fn contains_player(&self, player_id: PlayerId) -> bool {
239        self.player_entries.contains_key(&player_id)
240    }
241
242    /// Gets performance statistics
243    pub fn get_stats(&mut self) -> SpatialIndexStats {
244        let mut stats = self.stats.clone();
245        let node_stats = self.get_node_stats();
246        stats.current_depth = node_stats.max_depth;
247        stats.leaf_nodes = node_stats.leaf_nodes;
248        stats.internal_nodes = node_stats.internal_nodes;
249        stats
250    }
251
252    /// Gets detailed tree structure statistics
253    pub fn get_detailed_stats(&mut self) -> (SpatialIndexStats, NodeStats) {
254        let stats = self.get_stats();
255        let node_stats = self.get_node_stats();
256        (stats, node_stats)
257    }
258
259    /// Estimates query efficiency (for monitoring)
260    pub fn estimate_query_efficiency(&self, radius: f64) -> f64 {
261        let total_volume = {
262            let (min, max) = &self.bounds;
263            (max.x - min.x).max(1.0)
264                * (max.y - min.y).max(1.0)
265                * (max.z - min.z).max(1.0)
266        };
267        let query_volume = (4.0 / 3.0) * std::f64::consts::PI * radius * radius * radius;
268        let coverage_ratio = (query_volume / total_volume).min(1.0);
269        1.0 - coverage_ratio
270    }
271
272    /// Clears all objects and resets the tree
273    pub fn clear(&mut self) {
274        let (min, max) = self.bounds;
275        self.tree = RTree::new();
276        self.player_entries.clear();
277        self.object_count = 0;
278        self.stats.total_clears += 1;
279        self.bounds = (min, max);
280    }
281
282    /// Rebuilds the tree for better balance
283    pub fn rebuild(&mut self) {
284        let entries: Vec<_> = self.player_entries.values().cloned().collect();
285        self.tree = RTree::bulk_load(entries);
286        self.stats.total_rebuilds += 1;
287    }
288
289    /// Collects all objects from the tree
290    pub fn collect_all_objects(&self) -> Vec<SpatialObject> {
291        self.player_entries.values().map(|entry| entry.object.clone()).collect()
292    }
293
294    fn get_node_stats(&self) -> NodeStats {
295        let leaf_nodes = self.tree.size();
296
297        NodeStats {
298            total_objects: self.object_count,
299            max_depth: if leaf_nodes > 0 { 1 } else { 0 },
300            leaf_nodes,
301            internal_nodes: 0,
302        }
303    }
304}
305
306#[cfg(test)]
307mod tests {
308    use super::*;
309    use crate::types::Vec3;
310
311    #[test]
312    fn test_insert_and_query() {
313        let mut tree = RegionRTree::new(
314            Vec3::new(-100.0, -100.0, -100.0),
315            Vec3::new(100.0, 100.0, 100.0),
316        );
317
318        let player_a = PlayerId::new();
319        let player_b = PlayerId::new();
320
321        tree.insert_player(player_a, Position::new(0.0, 0.0, 0.0));
322        tree.insert_player(player_b, Position::new(50.0, 0.0, 0.0));
323
324        assert_eq!(tree.object_count(), 2);
325    assert!(tree.contains_player(player_a));
326    assert!(tree.contains_player(player_b));
327
328        let results = tree.query_radius(Position::new(0.0, 0.0, 0.0), 10.0);
329        assert_eq!(results.len(), 1);
330        assert_eq!(results[0].player_id, player_a);
331
332        let wider = tree.query_radius(Position::new(0.0, 0.0, 0.0), 60.0);
333        let ids: Vec<PlayerId> = wider.iter().map(|r| r.player_id).collect();
334        assert!(ids.contains(&player_a));
335        assert!(ids.contains(&player_b));
336        assert_eq!(wider.len(), 2);
337    }
338
339    #[test]
340    fn test_update_player_position() {
341        let mut tree = RegionRTree::new(
342            Vec3::new(-100.0, -100.0, -100.0),
343            Vec3::new(100.0, 100.0, 100.0),
344        );
345
346        let player = PlayerId::new();
347        tree.insert_player(player, Position::new(0.0, 0.0, 0.0));
348        tree.insert_player(player, Position::new(20.0, 0.0, 0.0));
349
350        let results = tree.query_radius(Position::new(0.0, 0.0, 0.0), 5.0);
351        assert!(results.is_empty(), "Player should have moved out of range");
352
353        let results = tree.query_radius(Position::new(20.0, 0.0, 0.0), 5.0);
354        assert_eq!(results.len(), 1);
355        assert_eq!(results[0].player_id, player);
356    }
357
358    #[test]
359    fn test_remove_player() {
360        let mut tree = RegionRTree::new(
361            Vec3::new(-100.0, -100.0, -100.0),
362            Vec3::new(100.0, 100.0, 100.0),
363        );
364
365        let player = PlayerId::new();
366        tree.insert_player(player, Position::new(0.0, 0.0, 0.0));
367        assert_eq!(tree.object_count(), 1);
368
369        let removed = tree.remove_player(player);
370        assert_eq!(removed, 1);
371        assert_eq!(tree.object_count(), 0);
372    }
373}