Skip to main content

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