Skip to main content

sphereql_layout/
managed.rs

1use 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) {
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}