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