Skip to main content

viewport_lib/interaction/
sub_object.rs

1//! Typed sub-object reference and sub-object selection set.
2//!
3//! [`SubObjectRef`] is the single canonical way to identify a face, vertex,
4//! edge, or point-cloud point relative to its parent object. It is carried
5//! inside [`PickHit::sub_object`](crate::interaction::picking::PickHit::sub_object)
6//! and used as the key type in [`SubSelection`].
7//!
8//! [`SubSelection`] is the sub-object counterpart to
9//! [`crate::interaction::selection::Selection`]. Typically an app holds both:
10//! `Selection` for which objects are selected, `SubSelection` for which faces
11//! or points within those objects are selected.
12
13use std::collections::HashSet;
14
15use crate::interaction::selection::NodeId;
16
17// ---------------------------------------------------------------------------
18// SubObjectRef
19// ---------------------------------------------------------------------------
20
21/// A typed reference to a sub-object within a parent scene object.
22///
23/// Produced by all pick functions when a specific surface feature is hit, and
24/// stored in [`PickHit::sub_object`](crate::interaction::picking::PickHit::sub_object).
25///
26/// # Variants
27///
28/// - [`Face`](SubObjectRef::Face) : triangular face, by index in the triangle list.
29///   Index `i` addresses vertices `indices[3i..3i+3]`.
30/// - [`Vertex`](SubObjectRef::Vertex) : mesh vertex, by position in the vertex buffer.
31/// - [`Edge`](SubObjectRef::Edge) : mesh edge (from parry3d `FeatureId::Edge`; rarely
32///   produced by TriMesh ray casts in practice).
33/// - [`Point`](SubObjectRef::Point) : point in a point-cloud object, by index in the
34///   positions slice.
35#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
36#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
37#[non_exhaustive]
38pub enum SubObjectRef {
39    /// A triangular face identified by its index in the triangle list.
40    Face(u32),
41    /// A mesh vertex identified by its position in the vertex buffer.
42    Vertex(u32),
43    /// A mesh edge identified by its edge index (`parry3d::shape::FeatureId::Edge`).
44    ///
45    /// Rarely produced in practice; included for completeness.
46    Edge(u32),
47    /// A point within a point-cloud object, by its index in the positions slice.
48    Point(u32),
49}
50
51impl SubObjectRef {
52    /// Returns `true` if this is a [`Face`](SubObjectRef::Face).
53    pub fn is_face(&self) -> bool {
54        matches!(self, Self::Face(_))
55    }
56
57    /// Returns `true` if this is a [`Point`](SubObjectRef::Point).
58    pub fn is_point(&self) -> bool {
59        matches!(self, Self::Point(_))
60    }
61
62    /// Returns `true` if this is a [`Vertex`](SubObjectRef::Vertex).
63    pub fn is_vertex(&self) -> bool {
64        matches!(self, Self::Vertex(_))
65    }
66
67    /// Returns `true` if this is an [`Edge`](SubObjectRef::Edge).
68    pub fn is_edge(&self) -> bool {
69        matches!(self, Self::Edge(_))
70    }
71
72    /// Returns the raw index regardless of variant.
73    pub fn index(&self) -> u32 {
74        match *self {
75            Self::Face(i) | Self::Vertex(i) | Self::Edge(i) | Self::Point(i) => i,
76        }
77    }
78
79    /// Convert from a parry3d [`FeatureId`](parry3d::shape::FeatureId).
80    ///
81    /// Returns `None` for `FeatureId::Unknown` (not expected from TriMesh ray casts).
82    pub fn from_feature_id(f: parry3d::shape::FeatureId) -> Option<Self> {
83        match f {
84            parry3d::shape::FeatureId::Face(i) => Some(Self::Face(i)),
85            parry3d::shape::FeatureId::Vertex(i) => Some(Self::Vertex(i)),
86            parry3d::shape::FeatureId::Edge(i) => Some(Self::Edge(i)),
87            _ => None,
88        }
89    }
90}
91
92// ---------------------------------------------------------------------------
93// SubSelection
94// ---------------------------------------------------------------------------
95
96/// A set of selected sub-objects (faces, vertices, edges, or points) across
97/// one or more parent objects.
98///
99/// Parallel to [`crate::interaction::selection::Selection`] but operates at
100/// sub-object granularity. Each entry pairs a parent `object_id` with a
101/// [`SubObjectRef`]. No ordering is maintained beyond the tracked `primary`.
102///
103/// # Typical usage
104///
105/// Hold a `SubSelection` alongside a `Selection`. Use `Selection` to track
106/// which objects are selected at object level; use `SubSelection` to track
107/// which specific faces or points within those objects are highlighted.
108///
109/// ```rust,ignore
110/// // On rect-pick:
111/// let rect_result = pick_rect(...);
112/// sub_sel.clear();
113/// sub_sel.extend_from_rect_pick(&rect_result);
114///
115/// // On ray-pick (face highlight):
116/// if let Some(sub) = hit.sub_object {
117///     sub_sel.select_one(hit.id, sub);
118/// }
119/// ```
120#[derive(Debug, Clone, Default)]
121pub struct SubSelection {
122    selected: HashSet<(NodeId, SubObjectRef)>,
123    primary: Option<(NodeId, SubObjectRef)>,
124    version: u64,
125}
126
127impl SubSelection {
128    /// Create an empty sub-selection.
129    pub fn new() -> Self {
130        Self::default()
131    }
132
133    /// Monotonically increasing generation counter.
134    ///
135    /// Incremented by `wrapping_add(1)` on every mutation. Compare against a
136    /// cached value to detect changes without re-hashing.
137    pub fn version(&self) -> u64 {
138        self.version
139    }
140
141    /// Clear and select exactly one sub-object.
142    pub fn select_one(&mut self, object_id: NodeId, sub: SubObjectRef) {
143        self.selected.clear();
144        self.selected.insert((object_id, sub));
145        self.primary = Some((object_id, sub));
146        self.version = self.version.wrapping_add(1);
147    }
148
149    /// Toggle a sub-object in or out of the selection.
150    ///
151    /// If added, it becomes the primary. If removed, primary is cleared or set
152    /// to an arbitrary remaining entry.
153    pub fn toggle(&mut self, object_id: NodeId, sub: SubObjectRef) {
154        let key = (object_id, sub);
155        if self.selected.contains(&key) {
156            self.selected.remove(&key);
157            if self.primary == Some(key) {
158                self.primary = self.selected.iter().next().copied();
159            }
160        } else {
161            self.selected.insert(key);
162            self.primary = Some(key);
163        }
164        self.version = self.version.wrapping_add(1);
165    }
166
167    /// Add a sub-object without clearing others.
168    pub fn add(&mut self, object_id: NodeId, sub: SubObjectRef) {
169        self.selected.insert((object_id, sub));
170        self.primary = Some((object_id, sub));
171        self.version = self.version.wrapping_add(1);
172    }
173
174    /// Remove a sub-object from the selection.
175    pub fn remove(&mut self, object_id: NodeId, sub: SubObjectRef) {
176        let key = (object_id, sub);
177        self.selected.remove(&key);
178        if self.primary == Some(key) {
179            self.primary = self.selected.iter().next().copied();
180        }
181        self.version = self.version.wrapping_add(1);
182    }
183
184    /// Clear the entire sub-selection.
185    pub fn clear(&mut self) {
186        self.selected.clear();
187        self.primary = None;
188        self.version = self.version.wrapping_add(1);
189    }
190
191    /// Extend from an iterator of `(object_id, SubObjectRef)` pairs.
192    ///
193    /// The last pair becomes primary.
194    pub fn extend(&mut self, items: impl IntoIterator<Item = (NodeId, SubObjectRef)>) {
195        let mut last = None;
196        for item in items {
197            self.selected.insert(item);
198            last = Some(item);
199        }
200        if let Some(item) = last {
201            self.primary = Some(item);
202        }
203        self.version = self.version.wrapping_add(1);
204    }
205
206    /// Populate from a [`RectPickResult`](crate::interaction::picking::RectPickResult).
207    ///
208    /// Adds all sub-objects from the rect pick without clearing the current
209    /// selection. Call [`clear`](Self::clear) first if you want a fresh selection.
210    pub fn extend_from_rect_pick(&mut self, result: &crate::interaction::picking::RectPickResult) {
211        for (&object_id, subs) in &result.hits {
212            for &sub in subs {
213                self.selected.insert((object_id, sub));
214                self.primary = Some((object_id, sub));
215            }
216        }
217        self.version = self.version.wrapping_add(1);
218    }
219
220    /// Whether a specific sub-object is selected.
221    pub fn contains(&self, object_id: NodeId, sub: SubObjectRef) -> bool {
222        self.selected.contains(&(object_id, sub))
223    }
224
225    /// The most recently selected `(object_id, SubObjectRef)` pair.
226    pub fn primary(&self) -> Option<(NodeId, SubObjectRef)> {
227        self.primary
228    }
229
230    /// Iterate over all selected `(object_id, SubObjectRef)` pairs.
231    pub fn iter(&self) -> impl Iterator<Item = &(NodeId, SubObjectRef)> {
232        self.selected.iter()
233    }
234
235    /// All sub-object refs for a specific parent object.
236    pub fn for_object(&self, object_id: NodeId) -> impl Iterator<Item = SubObjectRef> + '_ {
237        self.selected
238            .iter()
239            .filter(move |(id, _)| *id == object_id)
240            .map(|(_, sub)| *sub)
241    }
242
243    /// Number of selected sub-objects.
244    pub fn len(&self) -> usize {
245        self.selected.len()
246    }
247
248    /// Whether the sub-selection is empty.
249    pub fn is_empty(&self) -> bool {
250        self.selected.is_empty()
251    }
252
253    /// Count of selected faces across all objects.
254    pub fn face_count(&self) -> usize {
255        self.selected.iter().filter(|(_, s)| s.is_face()).count()
256    }
257
258    /// Count of selected points across all objects.
259    pub fn point_count(&self) -> usize {
260        self.selected.iter().filter(|(_, s)| s.is_point()).count()
261    }
262
263    /// Count of selected vertices across all objects.
264    pub fn vertex_count(&self) -> usize {
265        self.selected.iter().filter(|(_, s)| s.is_vertex()).count()
266    }
267}
268
269// ---------------------------------------------------------------------------
270// Tests
271// ---------------------------------------------------------------------------
272
273#[cfg(test)]
274mod tests {
275    use super::*;
276    use crate::interaction::picking::RectPickResult;
277
278    // --- SubObjectRef ---
279
280    #[test]
281    fn sub_object_ref_kind_checks() {
282        assert!(SubObjectRef::Face(0).is_face());
283        assert!(!SubObjectRef::Face(0).is_point());
284        assert!(!SubObjectRef::Face(0).is_vertex());
285        assert!(!SubObjectRef::Face(0).is_edge());
286
287        assert!(SubObjectRef::Point(1).is_point());
288        assert!(SubObjectRef::Vertex(2).is_vertex());
289        assert!(SubObjectRef::Edge(3).is_edge());
290    }
291
292    #[test]
293    fn sub_object_ref_index() {
294        assert_eq!(SubObjectRef::Face(7).index(), 7);
295        assert_eq!(SubObjectRef::Vertex(42).index(), 42);
296        assert_eq!(SubObjectRef::Edge(0).index(), 0);
297        assert_eq!(SubObjectRef::Point(99).index(), 99);
298    }
299
300    #[test]
301    fn sub_object_ref_from_feature_id() {
302        use parry3d::shape::FeatureId;
303        assert_eq!(
304            SubObjectRef::from_feature_id(FeatureId::Face(3)),
305            Some(SubObjectRef::Face(3))
306        );
307        assert_eq!(
308            SubObjectRef::from_feature_id(FeatureId::Vertex(1)),
309            Some(SubObjectRef::Vertex(1))
310        );
311        assert_eq!(
312            SubObjectRef::from_feature_id(FeatureId::Edge(2)),
313            Some(SubObjectRef::Edge(2))
314        );
315        assert_eq!(SubObjectRef::from_feature_id(FeatureId::Unknown), None);
316    }
317
318    #[test]
319    fn sub_object_ref_hashable() {
320        let mut set = std::collections::HashSet::new();
321        set.insert(SubObjectRef::Face(0));
322        set.insert(SubObjectRef::Face(0)); // duplicate
323        set.insert(SubObjectRef::Face(1));
324        set.insert(SubObjectRef::Point(0)); // same index, different variant
325        assert_eq!(set.len(), 3);
326    }
327
328    // --- SubSelection ---
329
330    #[test]
331    fn sub_selection_select_one_clears_others() {
332        let mut sel = SubSelection::new();
333        sel.add(1, SubObjectRef::Face(0));
334        sel.add(1, SubObjectRef::Face(1));
335        sel.select_one(1, SubObjectRef::Face(5));
336        assert_eq!(sel.len(), 1);
337        assert!(sel.contains(1, SubObjectRef::Face(5)));
338        assert!(!sel.contains(1, SubObjectRef::Face(0)));
339    }
340
341    #[test]
342    fn sub_selection_toggle() {
343        let mut sel = SubSelection::new();
344        sel.toggle(1, SubObjectRef::Face(0));
345        assert!(sel.contains(1, SubObjectRef::Face(0)));
346        sel.toggle(1, SubObjectRef::Face(0));
347        assert!(!sel.contains(1, SubObjectRef::Face(0)));
348        assert!(sel.is_empty());
349    }
350
351    #[test]
352    fn sub_selection_add_preserves_others() {
353        let mut sel = SubSelection::new();
354        sel.add(1, SubObjectRef::Face(0));
355        sel.add(1, SubObjectRef::Face(1));
356        assert_eq!(sel.len(), 2);
357        assert!(sel.contains(1, SubObjectRef::Face(0)));
358        assert!(sel.contains(1, SubObjectRef::Face(1)));
359    }
360
361    #[test]
362    fn sub_selection_remove() {
363        let mut sel = SubSelection::new();
364        sel.add(1, SubObjectRef::Face(0));
365        sel.add(1, SubObjectRef::Face(1));
366        sel.remove(1, SubObjectRef::Face(0));
367        assert!(!sel.contains(1, SubObjectRef::Face(0)));
368        assert_eq!(sel.len(), 1);
369    }
370
371    #[test]
372    fn sub_selection_clear() {
373        let mut sel = SubSelection::new();
374        sel.add(1, SubObjectRef::Face(0));
375        sel.add(2, SubObjectRef::Point(3));
376        sel.clear();
377        assert!(sel.is_empty());
378        assert_eq!(sel.primary(), None);
379    }
380
381    #[test]
382    fn sub_selection_primary_tracks_last() {
383        let mut sel = SubSelection::new();
384        sel.add(1, SubObjectRef::Face(0));
385        assert_eq!(sel.primary(), Some((1, SubObjectRef::Face(0))));
386        sel.add(2, SubObjectRef::Point(5));
387        assert_eq!(sel.primary(), Some((2, SubObjectRef::Point(5))));
388    }
389
390    #[test]
391    fn sub_selection_contains() {
392        let mut sel = SubSelection::new();
393        sel.add(10, SubObjectRef::Face(3));
394        assert!(sel.contains(10, SubObjectRef::Face(3)));
395        assert!(!sel.contains(10, SubObjectRef::Face(4)));
396        assert!(!sel.contains(99, SubObjectRef::Face(3)));
397    }
398
399    #[test]
400    fn sub_selection_for_object() {
401        let mut sel = SubSelection::new();
402        sel.add(1, SubObjectRef::Face(0));
403        sel.add(1, SubObjectRef::Face(1));
404        sel.add(2, SubObjectRef::Face(0));
405        let obj1: Vec<SubObjectRef> = {
406            let mut v: Vec<_> = sel.for_object(1).collect();
407            v.sort_by_key(|s| s.index());
408            v
409        };
410        assert_eq!(obj1, vec![SubObjectRef::Face(0), SubObjectRef::Face(1)]);
411        let obj2: Vec<SubObjectRef> = sel.for_object(2).collect();
412        assert_eq!(obj2, vec![SubObjectRef::Face(0)]);
413        assert_eq!(sel.for_object(99).count(), 0);
414    }
415
416    #[test]
417    fn sub_selection_version_increments() {
418        let mut sel = SubSelection::new();
419        let v0 = sel.version();
420        sel.add(1, SubObjectRef::Face(0));
421        assert!(sel.version() > v0);
422        let v1 = sel.version();
423        sel.clear();
424        assert!(sel.version() > v1);
425    }
426
427    #[test]
428    fn sub_selection_kind_counts() {
429        let mut sel = SubSelection::new();
430        sel.add(1, SubObjectRef::Face(0));
431        sel.add(1, SubObjectRef::Face(1));
432        sel.add(2, SubObjectRef::Point(0));
433        sel.add(3, SubObjectRef::Vertex(0));
434        assert_eq!(sel.face_count(), 2);
435        assert_eq!(sel.point_count(), 1);
436        assert_eq!(sel.vertex_count(), 1);
437    }
438
439    #[test]
440    fn sub_selection_extend() {
441        let mut sel = SubSelection::new();
442        sel.extend([
443            (1, SubObjectRef::Face(0)),
444            (1, SubObjectRef::Face(1)),
445            (2, SubObjectRef::Point(3)),
446        ]);
447        assert_eq!(sel.len(), 3);
448        assert_eq!(sel.primary(), Some((2, SubObjectRef::Point(3))));
449    }
450
451    #[test]
452    fn sub_selection_extend_from_rect_pick() {
453        let mut result = RectPickResult::default();
454        result
455            .hits
456            .insert(10, vec![SubObjectRef::Face(0), SubObjectRef::Face(1)]);
457        result.hits.insert(20, vec![SubObjectRef::Point(5)]);
458
459        let mut sel = SubSelection::new();
460        sel.extend_from_rect_pick(&result);
461
462        assert_eq!(sel.len(), 3);
463        assert!(sel.contains(10, SubObjectRef::Face(0)));
464        assert!(sel.contains(10, SubObjectRef::Face(1)));
465        assert!(sel.contains(20, SubObjectRef::Point(5)));
466        assert_eq!(sel.face_count(), 2);
467        assert_eq!(sel.point_count(), 1);
468    }
469}