sphereql_layout/
managed.rs1use std::collections::HashSet;
2
3use sphereql_core::SphericalPoint;
4
5use crate::traits::{DimensionMapper, LayoutStrategy};
6use crate::types::LayoutEntry;
7
8pub struct ManagedLayout<T> {
13 items: Vec<T>,
14 positions: Vec<SphericalPoint>,
15 dirty: HashSet<usize>,
16 needs_full_reflow: bool,
17}
18
19impl<T: Clone + Send + Sync> ManagedLayout<T> {
20 pub fn new() -> Self {
21 Self {
22 items: Vec::new(),
23 positions: Vec::new(),
24 dirty: HashSet::new(),
25 needs_full_reflow: false,
26 }
27 }
28
29 pub fn add(&mut self, item: T) {
30 let idx = self.items.len();
31 self.items.push(item);
32 self.positions.push(SphericalPoint::origin());
33 self.dirty.insert(idx);
34 }
35
36 pub fn remove(&mut self, index: usize) -> Option<T> {
37 if index >= self.items.len() {
38 return None;
39 }
40
41 let item = self.items.remove(index);
42 let _ = self.positions.remove(index);
43 self.dirty.remove(&index);
44
45 if index != self.items.len() {
46 self.needs_full_reflow = true;
47 let shifted: HashSet<usize> = self
48 .dirty
49 .iter()
50 .map(|&i| if i > index { i - 1 } else { i })
51 .collect();
52 self.dirty = shifted;
53 }
54 self.dirty.retain(|&i| i < self.items.len());
55
56 Some(item)
57 }
58
59 pub fn mark_dirty(&mut self, index: usize) {
60 if index < self.items.len() {
61 self.dirty.insert(index);
62 }
63 }
64
65 pub fn reflow(
66 &mut self,
67 strategy: &dyn LayoutStrategy<T>,
68 mapper: &dyn DimensionMapper<Item = T>,
69 ) {
70 let result = strategy.layout(&self.items, mapper);
71 debug_assert_eq!(
72 result.entries.len(),
73 self.positions.len(),
74 "LayoutStrategy must return one entry per item"
75 );
76 for (i, entry) in result.entries.into_iter().enumerate() {
77 if i < self.positions.len() {
78 self.positions[i] = entry.position;
79 }
80 }
81 self.dirty.clear();
82 self.needs_full_reflow = false;
83 }
84
85 pub fn reflow_incremental(
86 &mut self,
87 strategy: &dyn LayoutStrategy<T>,
88 mapper: &dyn DimensionMapper<Item = T>,
89 ) {
90 if self.needs_full_reflow {
91 self.reflow(strategy, mapper);
92 return;
93 }
94
95 if self.dirty.is_empty() {
96 return;
97 }
98
99 let dirty_indices: Vec<usize> = self.dirty.iter().copied().collect();
100
101 let result = strategy.layout(&self.items, mapper);
106 debug_assert_eq!(
107 result.entries.len(),
108 self.items.len(),
109 "layout strategy returned {} entries for {} items",
110 result.entries.len(),
111 self.items.len()
112 );
113 for &i in &dirty_indices {
114 if let Some(entry) = result.entries.get(i) {
115 self.positions[i] = entry.position;
116 }
117 }
118
119 self.dirty.clear();
120 }
121
122 pub fn items(&self) -> &[T] {
123 &self.items
124 }
125
126 pub fn positions(&self) -> &[SphericalPoint] {
127 &self.positions
128 }
129
130 pub fn len(&self) -> usize {
131 self.items.len()
132 }
133
134 pub fn is_empty(&self) -> bool {
135 self.items.is_empty()
136 }
137
138 pub fn get_entry(&self, index: usize) -> Option<LayoutEntry<&T>> {
139 if index >= self.items.len() {
140 return None;
141 }
142 Some(LayoutEntry {
143 item: &self.items[index],
144 position: self.positions[index],
145 })
146 }
147}
148
149impl<T: Clone + Send + Sync> Default for ManagedLayout<T> {
150 fn default() -> Self {
151 Self::new()
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158 use crate::traits::DimensionMapper;
159 use crate::types::{LayoutQuality, LayoutResult};
160 use std::f64::consts::FRAC_PI_2;
161
162 struct IdentityMapper;
163
164 impl DimensionMapper for IdentityMapper {
165 type Item = u32;
166 fn map(&self, _item: &u32) -> SphericalPoint {
167 SphericalPoint::origin()
168 }
169 }
170
171 struct DeterministicStrategy;
172
173 impl LayoutStrategy<u32> for DeterministicStrategy {
174 fn layout(
175 &self,
176 items: &[u32],
177 _mapper: &dyn DimensionMapper<Item = u32>,
178 ) -> LayoutResult<u32> {
179 let entries = items
180 .iter()
181 .map(|&item| LayoutEntry {
182 item,
183 position: SphericalPoint::new_unchecked(1.0, item as f64 * 0.1, FRAC_PI_2),
184 })
185 .collect();
186 LayoutResult {
187 entries,
188 quality: LayoutQuality::default(),
189 }
190 }
191 }
192
193 #[test]
194 fn new_layout_is_empty() {
195 let layout: ManagedLayout<u32> = ManagedLayout::new();
196 assert!(layout.is_empty());
197 assert_eq!(layout.len(), 0);
198 assert!(layout.items().is_empty());
199 assert!(layout.positions().is_empty());
200 }
201
202 #[test]
203 fn add_increases_len() {
204 let mut layout = ManagedLayout::new();
205 layout.add(1u32);
206 assert_eq!(layout.len(), 1);
207 layout.add(2);
208 layout.add(3);
209 assert_eq!(layout.len(), 3);
210 assert!(!layout.is_empty());
211 }
212
213 #[test]
214 fn remove_returns_item_and_decrements() {
215 let mut layout = ManagedLayout::new();
216 layout.add(10u32);
217 layout.add(20);
218 layout.add(30);
219 assert_eq!(layout.len(), 3);
220
221 let removed = layout.remove(1);
222 assert_eq!(removed, Some(20));
223 assert_eq!(layout.len(), 2);
224 assert_eq!(layout.items(), &[10, 30]);
225
226 let removed = layout.remove(1);
227 assert_eq!(removed, Some(30));
228 assert_eq!(layout.len(), 1);
229
230 assert_eq!(layout.remove(5), None);
231 }
232
233 #[test]
234 fn incremental_reflow_only_updates_dirty() {
235 let mut layout = ManagedLayout::new();
236 layout.add(1u32);
237 layout.add(2);
238 layout.add(3);
239
240 layout.reflow(&DeterministicStrategy, &IdentityMapper);
241 let pos_0_after_full = layout.positions()[0];
242 let pos_2_after_full = layout.positions()[2];
243
244 layout.mark_dirty(1);
245
246 layout.reflow_incremental(&DeterministicStrategy, &IdentityMapper);
247
248 assert_eq!(layout.positions()[0].theta, pos_0_after_full.theta);
249 assert_eq!(layout.positions()[2].theta, pos_2_after_full.theta);
250 assert!(layout.positions()[1].theta.is_finite());
251 }
252
253 #[test]
254 fn full_reflow_updates_all_positions() {
255 let mut layout = ManagedLayout::new();
256 layout.add(5u32);
257 layout.add(10);
258
259 assert_eq!(layout.positions()[0].theta, 0.0);
260 assert_eq!(layout.positions()[1].theta, 0.0);
261
262 layout.reflow(&DeterministicStrategy, &IdentityMapper);
263
264 assert!((layout.positions()[0].theta - 0.5).abs() < 1e-12);
265 assert!((layout.positions()[1].theta - 1.0).abs() < 1e-12);
266 }
267
268 #[test]
269 fn incremental_falls_back_to_full_when_needed() {
270 let mut layout = ManagedLayout::new();
271 layout.add(1u32);
272 layout.add(2);
273 layout.add(3);
274
275 layout.reflow(&DeterministicStrategy, &IdentityMapper);
276
277 layout.remove(0);
278 assert_eq!(layout.items(), &[2, 3]);
279
280 layout.reflow_incremental(&DeterministicStrategy, &IdentityMapper);
281
282 assert!((layout.positions()[0].theta - 0.2).abs() < 1e-12);
283 assert!((layout.positions()[1].theta - 0.3).abs() < 1e-12);
284 }
285
286 #[test]
287 fn get_entry_returns_correct_data() {
288 let mut layout = ManagedLayout::new();
289 layout.add(42u32);
290 layout.reflow(&DeterministicStrategy, &IdentityMapper);
291
292 let entry = layout.get_entry(0).unwrap();
293 assert_eq!(*entry.item, 42);
294 assert!((entry.position.theta - 4.2).abs() < 1e-12);
295
296 assert!(layout.get_entry(1).is_none());
297 }
298}