Skip to main content

viewport_lib/interaction/
selection.rs

1//! Multi-select system for viewport objects.
2//!
3//! `Selection` tracks a set of selected node IDs with a designated primary
4//! (most recently selected). Supports single-click, shift-click toggle,
5//! box select, and select-all operations.
6
7use std::collections::HashSet;
8
9/// Node identifier — matches `ViewportObject::id()` return type.
10pub type NodeId = u64;
11
12/// A set of selected nodes with a primary (most recently selected) node.
13#[derive(Debug, Clone)]
14pub struct Selection {
15    selected: HashSet<NodeId>,
16    primary: Option<NodeId>,
17    /// Monotonically increasing generation counter. Incremented on every mutation.
18    /// Compare against a cached value to detect selection changes without hashing.
19    version: u64,
20}
21
22impl Default for Selection {
23    fn default() -> Self {
24        Self {
25            selected: HashSet::new(),
26            primary: None,
27            version: 0,
28        }
29    }
30}
31
32impl Selection {
33    /// Create an empty selection.
34    pub fn new() -> Self {
35        Self::default()
36    }
37
38    /// Monotonically increasing generation counter.
39    ///
40    /// Incremented by `wrapping_add(1)` on every mutation. Compare against a
41    /// cached value to detect selection changes without hashing.
42    pub fn version(&self) -> u64 {
43        self.version
44    }
45
46    /// Clear the selection and select a single node.
47    pub fn select_one(&mut self, id: NodeId) {
48        self.selected.clear();
49        self.selected.insert(id);
50        self.primary = Some(id);
51        self.version = self.version.wrapping_add(1);
52    }
53
54    /// Toggle a node's selection (shift-click behavior).
55    /// If added, it becomes the primary. If removed, primary is cleared
56    /// (or set to an arbitrary remaining node).
57    pub fn toggle(&mut self, id: NodeId) {
58        if self.selected.contains(&id) {
59            self.selected.remove(&id);
60            if self.primary == Some(id) {
61                self.primary = self.selected.iter().next().copied();
62            }
63        } else {
64            self.selected.insert(id);
65            self.primary = Some(id);
66        }
67        self.version = self.version.wrapping_add(1);
68    }
69
70    /// Add a node to the selection without removing others.
71    pub fn add(&mut self, id: NodeId) {
72        self.selected.insert(id);
73        self.primary = Some(id);
74        self.version = self.version.wrapping_add(1);
75    }
76
77    /// Remove a node from the selection.
78    pub fn remove(&mut self, id: NodeId) {
79        self.selected.remove(&id);
80        if self.primary == Some(id) {
81            self.primary = self.selected.iter().next().copied();
82        }
83        self.version = self.version.wrapping_add(1);
84    }
85
86    /// Clear the entire selection.
87    pub fn clear(&mut self) {
88        self.selected.clear();
89        self.primary = None;
90        self.version = self.version.wrapping_add(1);
91    }
92
93    /// Add multiple nodes (e.g. from box select). The last one becomes primary.
94    pub fn extend(&mut self, ids: impl IntoIterator<Item = NodeId>) {
95        let mut last = None;
96        for id in ids {
97            self.selected.insert(id);
98            last = Some(id);
99        }
100        if let Some(id) = last {
101            self.primary = Some(id);
102        }
103        self.version = self.version.wrapping_add(1);
104    }
105
106    /// Replace the entire selection with the given set.
107    pub fn select_all(&mut self, ids: impl IntoIterator<Item = NodeId>) {
108        self.selected.clear();
109        self.primary = None;
110        // Collect first so we can detect the empty-iterator case where extend()
111        // would still increment the version (which is correct — clearing is a mutation).
112        let ids: Vec<NodeId> = ids.into_iter().collect();
113        if ids.is_empty() {
114            self.version = self.version.wrapping_add(1);
115        } else {
116            self.extend(ids);
117            // extend() already incremented version.
118        }
119    }
120
121    /// Whether the given node is selected.
122    pub fn contains(&self, id: NodeId) -> bool {
123        self.selected.contains(&id)
124    }
125
126    /// The most recently selected node.
127    pub fn primary(&self) -> Option<NodeId> {
128        self.primary
129    }
130
131    /// Iterate over all selected node IDs.
132    pub fn iter(&self) -> impl Iterator<Item = &NodeId> {
133        self.selected.iter()
134    }
135
136    /// Number of selected nodes.
137    pub fn len(&self) -> usize {
138        self.selected.len()
139    }
140
141    /// Whether the selection is empty.
142    pub fn is_empty(&self) -> bool {
143        self.selected.is_empty()
144    }
145
146    /// Compute the centroid (average position) of all selected nodes.
147    ///
148    /// `position_fn` resolves a node ID to its world-space position.
149    /// Returns `None` if the selection is empty or no positions are available.
150    pub fn centroid(
151        &self,
152        position_fn: impl Fn(NodeId) -> Option<glam::Vec3>,
153    ) -> Option<glam::Vec3> {
154        let mut sum = glam::Vec3::ZERO;
155        let mut count = 0u32;
156        for &id in &self.selected {
157            if let Some(pos) = position_fn(id) {
158                sum += pos;
159                count += 1;
160            }
161        }
162        if count > 0 {
163            Some(sum / count as f32)
164        } else {
165            None
166        }
167    }
168
169    /// Compute the difference between this selection and a previous one.
170    ///
171    /// Returns `(added, removed)` — IDs that are in `self` but not `previous`,
172    /// and IDs that are in `previous` but not `self`.
173    pub fn diff(&self, previous: &Selection) -> (Vec<NodeId>, Vec<NodeId>) {
174        let added: Vec<NodeId> = self
175            .selected
176            .difference(&previous.selected)
177            .copied()
178            .collect();
179        let removed: Vec<NodeId> = previous
180            .selected
181            .difference(&self.selected)
182            .copied()
183            .collect();
184        (added, removed)
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191
192    #[test]
193    fn test_select_one_clears_others() {
194        let mut sel = Selection::new();
195        sel.add(1);
196        sel.add(2);
197        sel.select_one(3);
198        assert_eq!(sel.len(), 1);
199        assert!(sel.contains(3));
200        assert!(!sel.contains(1));
201        assert!(!sel.contains(2));
202    }
203
204    #[test]
205    fn test_toggle_adds_and_removes() {
206        let mut sel = Selection::new();
207        sel.toggle(1);
208        assert!(sel.contains(1));
209        sel.toggle(1);
210        assert!(!sel.contains(1));
211        assert!(sel.is_empty());
212    }
213
214    #[test]
215    fn test_add_preserves_existing() {
216        let mut sel = Selection::new();
217        sel.add(1);
218        sel.add(2);
219        assert!(sel.contains(1));
220        assert!(sel.contains(2));
221        assert_eq!(sel.len(), 2);
222    }
223
224    #[test]
225    fn test_clear_empties() {
226        let mut sel = Selection::new();
227        sel.add(1);
228        sel.add(2);
229        sel.clear();
230        assert!(sel.is_empty());
231        assert_eq!(sel.primary(), None);
232    }
233
234    #[test]
235    fn test_primary_tracks_last() {
236        let mut sel = Selection::new();
237        sel.add(1);
238        assert_eq!(sel.primary(), Some(1));
239        sel.add(2);
240        assert_eq!(sel.primary(), Some(2));
241        sel.select_one(3);
242        assert_eq!(sel.primary(), Some(3));
243    }
244
245    #[test]
246    fn test_centroid_computes_average() {
247        let mut sel = Selection::new();
248        sel.add(1);
249        sel.add(2);
250        let centroid = sel.centroid(|id| match id {
251            1 => Some(glam::Vec3::new(0.0, 0.0, 0.0)),
252            2 => Some(glam::Vec3::new(4.0, 0.0, 0.0)),
253            _ => None,
254        });
255        let c = centroid.unwrap();
256        assert!((c.x - 2.0).abs() < 1e-5);
257        assert!((c.y).abs() < 1e-5);
258    }
259
260    #[test]
261    fn test_diff_reports_changes() {
262        let mut prev = Selection::new();
263        prev.add(1);
264        prev.add(2);
265
266        let mut curr = Selection::new();
267        curr.add(2);
268        curr.add(3);
269
270        let (added, removed) = curr.diff(&prev);
271        assert_eq!(added, vec![3]);
272        assert_eq!(removed, vec![1]);
273    }
274}