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    /// A voxel within a ray-marched volume, by its flat grid index.
50    ///
51    /// The flat index encodes `(ix, iy, iz)` as `ix + iy * nx + iz * nx * ny`.
52    /// Recover the 3-D indices using the grid dimensions from
53    /// [`VolumeData`](crate::geometry::marching_cubes::VolumeData).
54    Voxel(u32),
55}
56
57impl SubObjectRef {
58    /// Returns `true` if this is a [`Face`](SubObjectRef::Face).
59    pub fn is_face(&self) -> bool {
60        matches!(self, Self::Face(_))
61    }
62
63    /// Returns `true` if this is a [`Point`](SubObjectRef::Point).
64    pub fn is_point(&self) -> bool {
65        matches!(self, Self::Point(_))
66    }
67
68    /// Returns `true` if this is a [`Vertex`](SubObjectRef::Vertex).
69    pub fn is_vertex(&self) -> bool {
70        matches!(self, Self::Vertex(_))
71    }
72
73    /// Returns `true` if this is an [`Edge`](SubObjectRef::Edge).
74    pub fn is_edge(&self) -> bool {
75        matches!(self, Self::Edge(_))
76    }
77
78    /// Returns `true` if this is a [`Voxel`](SubObjectRef::Voxel).
79    pub fn is_voxel(&self) -> bool {
80        matches!(self, Self::Voxel(_))
81    }
82
83    /// Returns the raw index regardless of variant.
84    pub fn index(&self) -> u32 {
85        match *self {
86            Self::Face(i) | Self::Vertex(i) | Self::Edge(i) | Self::Point(i) | Self::Voxel(i) => i,
87        }
88    }
89
90    /// Convert from a parry3d [`FeatureId`](parry3d::shape::FeatureId).
91    ///
92    /// Returns `None` for `FeatureId::Unknown` (not expected from TriMesh ray casts).
93    pub fn from_feature_id(f: parry3d::shape::FeatureId) -> Option<Self> {
94        match f {
95            parry3d::shape::FeatureId::Face(i) => Some(Self::Face(i)),
96            parry3d::shape::FeatureId::Vertex(i) => Some(Self::Vertex(i)),
97            parry3d::shape::FeatureId::Edge(i) => Some(Self::Edge(i)),
98            _ => None,
99        }
100    }
101}
102
103// ---------------------------------------------------------------------------
104// SubSelection
105// ---------------------------------------------------------------------------
106
107/// A set of selected sub-objects (faces, vertices, edges, or points) across
108/// one or more parent objects.
109///
110/// Parallel to [`crate::interaction::selection::Selection`] but operates at
111/// sub-object granularity. Each entry pairs a parent `object_id` with a
112/// [`SubObjectRef`]. No ordering is maintained beyond the tracked `primary`.
113///
114/// # Typical usage
115///
116/// Hold a `SubSelection` alongside a `Selection`. Use `Selection` to track
117/// which objects are selected at object level; use `SubSelection` to track
118/// which specific faces or points within those objects are highlighted.
119///
120/// ```rust,ignore
121/// // On rect-pick:
122/// let rect_result = pick_rect(...);
123/// sub_sel.clear();
124/// sub_sel.extend_from_rect_pick(&rect_result);
125///
126/// // On ray-pick (face highlight):
127/// if let Some(sub) = hit.sub_object {
128///     sub_sel.select_one(hit.id, sub);
129/// }
130/// ```
131#[derive(Debug, Clone, Default)]
132pub struct SubSelection {
133    selected: HashSet<(NodeId, SubObjectRef)>,
134    primary: Option<(NodeId, SubObjectRef)>,
135    version: u64,
136}
137
138impl SubSelection {
139    /// Create an empty sub-selection.
140    pub fn new() -> Self {
141        Self::default()
142    }
143
144    /// Monotonically increasing generation counter.
145    ///
146    /// Incremented by `wrapping_add(1)` on every mutation. Compare against a
147    /// cached value to detect changes without re-hashing.
148    pub fn version(&self) -> u64 {
149        self.version
150    }
151
152    /// Clear and select exactly one sub-object.
153    pub fn select_one(&mut self, object_id: NodeId, sub: SubObjectRef) {
154        self.selected.clear();
155        self.selected.insert((object_id, sub));
156        self.primary = Some((object_id, sub));
157        self.version = self.version.wrapping_add(1);
158    }
159
160    /// Toggle a sub-object in or out of the selection.
161    ///
162    /// If added, it becomes the primary. If removed, primary is cleared or set
163    /// to an arbitrary remaining entry.
164    pub fn toggle(&mut self, object_id: NodeId, sub: SubObjectRef) {
165        let key = (object_id, sub);
166        if self.selected.contains(&key) {
167            self.selected.remove(&key);
168            if self.primary == Some(key) {
169                self.primary = self.selected.iter().next().copied();
170            }
171        } else {
172            self.selected.insert(key);
173            self.primary = Some(key);
174        }
175        self.version = self.version.wrapping_add(1);
176    }
177
178    /// Add a sub-object without clearing others.
179    pub fn add(&mut self, object_id: NodeId, sub: SubObjectRef) {
180        self.selected.insert((object_id, sub));
181        self.primary = Some((object_id, sub));
182        self.version = self.version.wrapping_add(1);
183    }
184
185    /// Remove a sub-object from the selection.
186    pub fn remove(&mut self, object_id: NodeId, sub: SubObjectRef) {
187        let key = (object_id, sub);
188        self.selected.remove(&key);
189        if self.primary == Some(key) {
190            self.primary = self.selected.iter().next().copied();
191        }
192        self.version = self.version.wrapping_add(1);
193    }
194
195    /// Clear the entire sub-selection.
196    pub fn clear(&mut self) {
197        self.selected.clear();
198        self.primary = None;
199        self.version = self.version.wrapping_add(1);
200    }
201
202    /// Extend from an iterator of `(object_id, SubObjectRef)` pairs.
203    ///
204    /// The last pair becomes primary.
205    pub fn extend(&mut self, items: impl IntoIterator<Item = (NodeId, SubObjectRef)>) {
206        let mut last = None;
207        for item in items {
208            self.selected.insert(item);
209            last = Some(item);
210        }
211        if let Some(item) = last {
212            self.primary = Some(item);
213        }
214        self.version = self.version.wrapping_add(1);
215    }
216
217    /// Populate from a [`RectPickResult`](crate::interaction::picking::RectPickResult).
218    ///
219    /// Adds all sub-objects from the rect pick without clearing the current
220    /// selection. Call [`clear`](Self::clear) first if you want a fresh selection.
221    pub fn extend_from_rect_pick(&mut self, result: &crate::interaction::picking::RectPickResult) {
222        for (&object_id, subs) in &result.hits {
223            for &sub in subs {
224                self.selected.insert((object_id, sub));
225                self.primary = Some((object_id, sub));
226            }
227        }
228        self.version = self.version.wrapping_add(1);
229    }
230
231    /// Whether a specific sub-object is selected.
232    pub fn contains(&self, object_id: NodeId, sub: SubObjectRef) -> bool {
233        self.selected.contains(&(object_id, sub))
234    }
235
236    /// The most recently selected `(object_id, SubObjectRef)` pair.
237    pub fn primary(&self) -> Option<(NodeId, SubObjectRef)> {
238        self.primary
239    }
240
241    /// Iterate over all selected `(object_id, SubObjectRef)` pairs.
242    pub fn iter(&self) -> impl Iterator<Item = &(NodeId, SubObjectRef)> {
243        self.selected.iter()
244    }
245
246    /// All sub-object refs for a specific parent object.
247    pub fn for_object(&self, object_id: NodeId) -> impl Iterator<Item = SubObjectRef> + '_ {
248        self.selected
249            .iter()
250            .filter(move |(id, _)| *id == object_id)
251            .map(|(_, sub)| *sub)
252    }
253
254    /// Number of selected sub-objects.
255    pub fn len(&self) -> usize {
256        self.selected.len()
257    }
258
259    /// Whether the sub-selection is empty.
260    pub fn is_empty(&self) -> bool {
261        self.selected.is_empty()
262    }
263
264    /// Count of selected faces across all objects.
265    pub fn face_count(&self) -> usize {
266        self.selected.iter().filter(|(_, s)| s.is_face()).count()
267    }
268
269    /// Count of selected points across all objects.
270    pub fn point_count(&self) -> usize {
271        self.selected.iter().filter(|(_, s)| s.is_point()).count()
272    }
273
274    /// Count of selected vertices across all objects.
275    pub fn vertex_count(&self) -> usize {
276        self.selected.iter().filter(|(_, s)| s.is_vertex()).count()
277    }
278
279    /// Count of selected voxels across all objects.
280    pub fn voxel_count(&self) -> usize {
281        self.selected.iter().filter(|(_, s)| s.is_voxel()).count()
282    }
283}
284
285// ---------------------------------------------------------------------------
286// SubSelectionRef
287// ---------------------------------------------------------------------------
288
289/// Geometry info needed to decode a [`SubObjectRef::Voxel`] flat index into
290/// world-space AABB corners for highlight rendering.
291///
292/// Pass one entry per volume object via [`SubSelectionRef::with_voxels`].
293pub struct VolumeSelectionInfo {
294    /// Grid dimensions `[nx, ny, nz]` — same as [`VolumeData::dims`].
295    pub dims: [u32; 3],
296    /// Local-space bounding-box minimum corner (matches [`VolumeItem::bbox_min`]).
297    pub bbox_min: [f32; 3],
298    /// Local-space bounding-box maximum corner (matches [`VolumeItem::bbox_max`]).
299    pub bbox_max: [f32; 3],
300    /// World-space transform (matches [`VolumeItem::model`]).
301    pub model: [[f32; 4]; 4],
302}
303
304/// A renderer-owned snapshot of a [`SubSelection`] taken at frame submission time.
305///
306/// Bundles the selection items with the CPU-side mesh and point cloud data the
307/// renderer needs to build highlight geometry. The renderer does not hold a
308/// reference to any app-owned data between frames.
309///
310/// # Usage
311///
312/// ```ignore
313/// fd.interaction.sub_selection = Some(SubSelectionRef::new(
314///     &self.sub_selection,
315///     mesh_lookup,
316///     model_matrices,
317///     point_positions,
318/// ));
319/// ```
320pub struct SubSelectionRef {
321    /// Snapshot of all selected (node_id, sub_object) pairs.
322    pub(crate) items: Vec<(NodeId, SubObjectRef)>,
323    /// CPU-side vertex positions and triangle indices keyed by node id.
324    ///
325    /// Same format as the `mesh_lookup` parameter to
326    /// [`pick_scene_cpu`](crate::interaction::picking::pick_scene_cpu):
327    /// the value is `(positions, indices)` where every three consecutive
328    /// indices form one triangle.
329    pub(crate) mesh_lookup:
330        std::collections::HashMap<u64, (Vec<[f32; 3]>, Vec<u32>)>,
331    /// World-space model matrix for each node, keyed by node id.
332    ///
333    /// Used to transform local-space mesh positions into world space when
334    /// building fill and edge geometry. Nodes absent from the map are treated
335    /// as having an identity transform.
336    pub(crate) model_matrices: std::collections::HashMap<u64, glam::Mat4>,
337    /// World-space point cloud positions keyed by node id.
338    ///
339    /// Required for [`SubObjectRef::Point`] highlights. The index carried by
340    /// `Point(i)` addresses `point_positions[node_id][i]`.
341    pub(crate) point_positions: std::collections::HashMap<u64, Vec<[f32; 3]>>,
342    /// Volume geometry info keyed by node id.
343    ///
344    /// Required for [`SubObjectRef::Voxel`] highlights. Each entry provides the
345    /// grid dimensions and bounding box so the renderer can decode flat voxel
346    /// indices into world-space AABB wireframes.
347    pub(crate) voxel_lookup: std::collections::HashMap<u64, VolumeSelectionInfo>,
348    /// Version counter copied from the source [`SubSelection::version()`].
349    ///
350    /// The renderer uses this to skip GPU buffer rebuilds when the selection
351    /// has not changed since the previous frame.
352    pub version: u64,
353}
354
355impl SubSelectionRef {
356    /// Create a snapshot from a live [`SubSelection`].
357    ///
358    /// - `mesh_lookup` : CPU positions + indices per node id (same type as the
359    ///   `mesh_lookup` argument to the CPU pick functions).
360    /// - `model_matrices` : world transform per node id.
361    /// - `point_positions` : point cloud positions per node id (for
362    ///   [`SubObjectRef::Point`] entries).
363    pub fn new(
364        sub_selection: &SubSelection,
365        mesh_lookup: std::collections::HashMap<u64, (Vec<[f32; 3]>, Vec<u32>)>,
366        model_matrices: std::collections::HashMap<u64, glam::Mat4>,
367        point_positions: std::collections::HashMap<u64, Vec<[f32; 3]>>,
368    ) -> Self {
369        Self {
370            items: sub_selection
371                .iter()
372                .map(|(n, s)| (*n, *s))
373                .collect(),
374            mesh_lookup,
375            model_matrices,
376            point_positions,
377            voxel_lookup: std::collections::HashMap::new(),
378            version: sub_selection.version(),
379        }
380    }
381
382    /// Attach volume geometry info for [`SubObjectRef::Voxel`] highlight rendering.
383    ///
384    /// `lookup` maps each volume's node id to its [`VolumeSelectionInfo`]. Without
385    /// this, selected voxels are silently skipped during highlight geometry build.
386    pub fn with_voxels(
387        mut self,
388        lookup: std::collections::HashMap<u64, VolumeSelectionInfo>,
389    ) -> Self {
390        self.voxel_lookup = lookup;
391        self
392    }
393
394    /// Returns `true` if the snapshot contains no selected sub-objects.
395    pub fn is_empty(&self) -> bool {
396        self.items.is_empty()
397    }
398}
399
400// ---------------------------------------------------------------------------
401// Tests
402// ---------------------------------------------------------------------------
403
404#[cfg(test)]
405mod tests {
406    use super::*;
407    use crate::interaction::picking::RectPickResult;
408
409    // --- SubObjectRef ---
410
411    #[test]
412    fn sub_object_ref_kind_checks() {
413        assert!(SubObjectRef::Face(0).is_face());
414        assert!(!SubObjectRef::Face(0).is_point());
415        assert!(!SubObjectRef::Face(0).is_vertex());
416        assert!(!SubObjectRef::Face(0).is_edge());
417
418        assert!(SubObjectRef::Point(1).is_point());
419        assert!(SubObjectRef::Vertex(2).is_vertex());
420        assert!(SubObjectRef::Edge(3).is_edge());
421    }
422
423    #[test]
424    fn sub_object_ref_index() {
425        assert_eq!(SubObjectRef::Face(7).index(), 7);
426        assert_eq!(SubObjectRef::Vertex(42).index(), 42);
427        assert_eq!(SubObjectRef::Edge(0).index(), 0);
428        assert_eq!(SubObjectRef::Point(99).index(), 99);
429    }
430
431    #[test]
432    fn sub_object_ref_from_feature_id() {
433        use parry3d::shape::FeatureId;
434        assert_eq!(
435            SubObjectRef::from_feature_id(FeatureId::Face(3)),
436            Some(SubObjectRef::Face(3))
437        );
438        assert_eq!(
439            SubObjectRef::from_feature_id(FeatureId::Vertex(1)),
440            Some(SubObjectRef::Vertex(1))
441        );
442        assert_eq!(
443            SubObjectRef::from_feature_id(FeatureId::Edge(2)),
444            Some(SubObjectRef::Edge(2))
445        );
446        assert_eq!(SubObjectRef::from_feature_id(FeatureId::Unknown), None);
447    }
448
449    #[test]
450    fn sub_object_ref_hashable() {
451        let mut set = std::collections::HashSet::new();
452        set.insert(SubObjectRef::Face(0));
453        set.insert(SubObjectRef::Face(0)); // duplicate
454        set.insert(SubObjectRef::Face(1));
455        set.insert(SubObjectRef::Point(0)); // same index, different variant
456        assert_eq!(set.len(), 3);
457    }
458
459    // --- SubSelection ---
460
461    #[test]
462    fn sub_selection_select_one_clears_others() {
463        let mut sel = SubSelection::new();
464        sel.add(1, SubObjectRef::Face(0));
465        sel.add(1, SubObjectRef::Face(1));
466        sel.select_one(1, SubObjectRef::Face(5));
467        assert_eq!(sel.len(), 1);
468        assert!(sel.contains(1, SubObjectRef::Face(5)));
469        assert!(!sel.contains(1, SubObjectRef::Face(0)));
470    }
471
472    #[test]
473    fn sub_selection_toggle() {
474        let mut sel = SubSelection::new();
475        sel.toggle(1, SubObjectRef::Face(0));
476        assert!(sel.contains(1, SubObjectRef::Face(0)));
477        sel.toggle(1, SubObjectRef::Face(0));
478        assert!(!sel.contains(1, SubObjectRef::Face(0)));
479        assert!(sel.is_empty());
480    }
481
482    #[test]
483    fn sub_selection_add_preserves_others() {
484        let mut sel = SubSelection::new();
485        sel.add(1, SubObjectRef::Face(0));
486        sel.add(1, SubObjectRef::Face(1));
487        assert_eq!(sel.len(), 2);
488        assert!(sel.contains(1, SubObjectRef::Face(0)));
489        assert!(sel.contains(1, SubObjectRef::Face(1)));
490    }
491
492    #[test]
493    fn sub_selection_remove() {
494        let mut sel = SubSelection::new();
495        sel.add(1, SubObjectRef::Face(0));
496        sel.add(1, SubObjectRef::Face(1));
497        sel.remove(1, SubObjectRef::Face(0));
498        assert!(!sel.contains(1, SubObjectRef::Face(0)));
499        assert_eq!(sel.len(), 1);
500    }
501
502    #[test]
503    fn sub_selection_clear() {
504        let mut sel = SubSelection::new();
505        sel.add(1, SubObjectRef::Face(0));
506        sel.add(2, SubObjectRef::Point(3));
507        sel.clear();
508        assert!(sel.is_empty());
509        assert_eq!(sel.primary(), None);
510    }
511
512    #[test]
513    fn sub_selection_primary_tracks_last() {
514        let mut sel = SubSelection::new();
515        sel.add(1, SubObjectRef::Face(0));
516        assert_eq!(sel.primary(), Some((1, SubObjectRef::Face(0))));
517        sel.add(2, SubObjectRef::Point(5));
518        assert_eq!(sel.primary(), Some((2, SubObjectRef::Point(5))));
519    }
520
521    #[test]
522    fn sub_selection_contains() {
523        let mut sel = SubSelection::new();
524        sel.add(10, SubObjectRef::Face(3));
525        assert!(sel.contains(10, SubObjectRef::Face(3)));
526        assert!(!sel.contains(10, SubObjectRef::Face(4)));
527        assert!(!sel.contains(99, SubObjectRef::Face(3)));
528    }
529
530    #[test]
531    fn sub_selection_for_object() {
532        let mut sel = SubSelection::new();
533        sel.add(1, SubObjectRef::Face(0));
534        sel.add(1, SubObjectRef::Face(1));
535        sel.add(2, SubObjectRef::Face(0));
536        let obj1: Vec<SubObjectRef> = {
537            let mut v: Vec<_> = sel.for_object(1).collect();
538            v.sort_by_key(|s| s.index());
539            v
540        };
541        assert_eq!(obj1, vec![SubObjectRef::Face(0), SubObjectRef::Face(1)]);
542        let obj2: Vec<SubObjectRef> = sel.for_object(2).collect();
543        assert_eq!(obj2, vec![SubObjectRef::Face(0)]);
544        assert_eq!(sel.for_object(99).count(), 0);
545    }
546
547    #[test]
548    fn sub_selection_version_increments() {
549        let mut sel = SubSelection::new();
550        let v0 = sel.version();
551        sel.add(1, SubObjectRef::Face(0));
552        assert!(sel.version() > v0);
553        let v1 = sel.version();
554        sel.clear();
555        assert!(sel.version() > v1);
556    }
557
558    #[test]
559    fn sub_selection_kind_counts() {
560        let mut sel = SubSelection::new();
561        sel.add(1, SubObjectRef::Face(0));
562        sel.add(1, SubObjectRef::Face(1));
563        sel.add(2, SubObjectRef::Point(0));
564        sel.add(3, SubObjectRef::Vertex(0));
565        assert_eq!(sel.face_count(), 2);
566        assert_eq!(sel.point_count(), 1);
567        assert_eq!(sel.vertex_count(), 1);
568    }
569
570    #[test]
571    fn sub_selection_extend() {
572        let mut sel = SubSelection::new();
573        sel.extend([
574            (1, SubObjectRef::Face(0)),
575            (1, SubObjectRef::Face(1)),
576            (2, SubObjectRef::Point(3)),
577        ]);
578        assert_eq!(sel.len(), 3);
579        assert_eq!(sel.primary(), Some((2, SubObjectRef::Point(3))));
580    }
581
582    #[test]
583    fn sub_selection_extend_from_rect_pick() {
584        let mut result = RectPickResult::default();
585        result
586            .hits
587            .insert(10, vec![SubObjectRef::Face(0), SubObjectRef::Face(1)]);
588        result.hits.insert(20, vec![SubObjectRef::Point(5)]);
589
590        let mut sel = SubSelection::new();
591        sel.extend_from_rect_pick(&result);
592
593        assert_eq!(sel.len(), 3);
594        assert!(sel.contains(10, SubObjectRef::Face(0)));
595        assert!(sel.contains(10, SubObjectRef::Face(1)));
596        assert!(sel.contains(20, SubObjectRef::Point(5)));
597        assert_eq!(sel.face_count(), 2);
598        assert_eq!(sel.point_count(), 1);
599    }
600}