steel_core/entity/manager/
spatial_query.rs1use std::{slice, vec::IntoIter};
4
5use rustc_hash::FxHashSet;
6use smallvec::SmallVec;
7use steel_utils::WorldAabb;
8
9use super::{EntityEntry, EntityQueryOrder, EntitySlot, ManagerState};
10
11const MAX_MERGED_QUERY_CELLS: usize = 8;
12
13#[expect(
14 clippy::large_enum_variant,
15 reason = "Common query cursors stay inline to avoid an allocation per spatial lookup"
16)]
17pub(super) enum EntityQueryEntries<'a> {
18 Single {
19 state: &'a ManagerState,
20 aabb: Option<WorldAabb>,
21 remaining: slice::Iter<'a, EntitySlot>,
22 },
23 Merged {
24 state: &'a ManagerState,
25 aabb: Option<WorldAabb>,
26 cursors: SmallVec<[EntityQueryCursor<'a>; MAX_MERGED_QUERY_CELLS]>,
27 },
28 Sorted(IntoIter<&'a EntityEntry>),
29}
30
31pub(super) struct EntityQueryCursor<'a> {
32 entry: &'a EntityEntry,
33 order: EntityQueryOrder,
34 remaining: slice::Iter<'a, EntitySlot>,
35}
36
37impl<'a> EntityQueryCursor<'a> {
38 fn new(
39 state: &'a ManagerState,
40 aabb: Option<&WorldAabb>,
41 cell: &'a [EntitySlot],
42 ) -> Option<Self> {
43 let mut remaining = cell.iter();
44 let entry = next_candidate(state, aabb, &mut remaining)?;
45
46 Some(Self {
47 entry,
48 order: entry.query_order,
49 remaining,
50 })
51 }
52
53 fn advance(&mut self, state: &'a ManagerState, aabb: Option<&WorldAabb>) -> bool {
54 let Some(entry) = next_candidate(state, aabb, &mut self.remaining) else {
55 return false;
56 };
57
58 self.entry = entry;
59 self.order = entry.query_order;
60
61 true
62 }
63}
64
65fn next_candidate<'a>(
66 state: &'a ManagerState,
67 aabb: Option<&WorldAabb>,
68 remaining: &mut slice::Iter<'a, EntitySlot>,
69) -> Option<&'a EntityEntry> {
70 remaining.find_map(|slot| {
71 let entry = state.live_by_id.entry(*slot);
72
73 aabb.is_none_or(|aabb| entry.bounding_box.intersects(*aabb))
74 .then_some(entry)
75 })
76}
77
78impl<'a> EntityQueryEntries<'a> {
79 pub(super) fn new(
80 state: &'a ManagerState,
81 aabb: Option<&WorldAabb>,
82 cells: SmallVec<[&'a [EntitySlot]; MAX_MERGED_QUERY_CELLS]>,
83 ) -> Self {
84 if let [cell] = cells.as_slice() {
85 return Self::Single {
86 state,
87 aabb: aabb.copied(),
88 remaining: cell.iter(),
89 };
90 }
91
92 if cells.len() <= MAX_MERGED_QUERY_CELLS {
93 return Self::Merged {
94 state,
95 aabb: aabb.copied(),
96 cursors: cells
97 .into_iter()
98 .filter_map(|cell| EntityQueryCursor::new(state, aabb, cell))
99 .collect(),
100 };
101 }
102
103 let mut entity_ids = FxHashSet::default();
104
105 for cell in cells {
106 entity_ids.extend(cell.iter().copied());
107 }
108
109 let mut entries = entity_ids
110 .into_iter()
111 .map(|slot| state.live_by_id.entry(slot))
112 .filter(|entry| aabb.is_none_or(|aabb| entry.bounding_box.intersects(*aabb)))
113 .collect::<Vec<_>>();
114
115 entries.sort_unstable_by_key(|entry| entry.query_order);
116 Self::Sorted(entries.into_iter())
117 }
118}
119
120impl<'a> Iterator for EntityQueryEntries<'a> {
121 type Item = &'a EntityEntry;
122
123 fn fold<B, F>(self, initial: B, mut visit: F) -> B
124 where
125 F: FnMut(B, Self::Item) -> B,
126 {
127 match self {
128 Self::Single {
129 state,
130 aabb,
131 remaining,
132 } => remaining
133 .map(|slot| state.live_by_id.entry(*slot))
134 .filter(|entry| aabb.is_none_or(|aabb| entry.bounding_box.intersects(aabb)))
135 .fold(initial, visit),
136 Self::Sorted(entries) => entries.fold(initial, visit),
137 entries @ Self::Merged { .. } => {
138 let mut result = initial;
139
140 for entry in entries {
141 result = visit(result, entry);
142 }
143
144 result
145 }
146 }
147 }
148
149 fn next(&mut self) -> Option<Self::Item> {
150 match self {
151 Self::Single {
152 state,
153 aabb,
154 remaining,
155 } => next_candidate(state, aabb.as_ref(), remaining),
156 Self::Merged {
157 state,
158 aabb,
159 cursors,
160 } => {
161 let cursor = cursors.iter().min_by_key(|cursor| cursor.order)?;
162 let entry = cursor.entry;
163 let order = cursor.order;
164
165 let mut index = 0;
166
167 while index < cursors.len() {
168 if cursors[index].order != order || cursors[index].advance(state, aabb.as_ref())
169 {
170 index += 1;
171 } else {
172 cursors.swap_remove(index);
173 }
174 }
175
176 Some(entry)
177 }
178 Self::Sorted(entries) => entries.next(),
179 }
180 }
181}