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/// - [`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_mesh::VolumeMeshData::cells).
65 ///
66 /// Produced by [`pick_transparent_volume_mesh_cpu`](crate::interaction::picking::pick_transparent_volume_mesh_cpu)
67 /// and [`pick_transparent_volume_mesh_rect`](crate::interaction::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::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::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(&mut self, result: &crate::interaction::picking::RectPickResult) {
258 for (&object_id, subs) in &result.hits {
259 for &sub in subs {
260 self.selected.insert((object_id, sub));
261 self.primary = Some((object_id, sub));
262 }
263 }
264 self.version = self.version.wrapping_add(1);
265 }
266
267 /// Whether a specific sub-object is selected.
268 pub fn contains(&self, object_id: NodeId, sub: SubObjectRef) -> bool {
269 self.selected.contains(&(object_id, sub))
270 }
271
272 /// The most recently selected `(object_id, SubObjectRef)` pair.
273 pub fn primary(&self) -> Option<(NodeId, SubObjectRef)> {
274 self.primary
275 }
276
277 /// Iterate over all selected `(object_id, SubObjectRef)` pairs.
278 pub fn iter(&self) -> impl Iterator<Item = &(NodeId, SubObjectRef)> {
279 self.selected.iter()
280 }
281
282 /// All sub-object refs for a specific parent object.
283 pub fn for_object(&self, object_id: NodeId) -> impl Iterator<Item = SubObjectRef> + '_ {
284 self.selected
285 .iter()
286 .filter(move |(id, _)| *id == object_id)
287 .map(|(_, sub)| *sub)
288 }
289
290 /// Number of selected sub-objects.
291 pub fn len(&self) -> usize {
292 self.selected.len()
293 }
294
295 /// Whether the sub-selection is empty.
296 pub fn is_empty(&self) -> bool {
297 self.selected.is_empty()
298 }
299
300 /// Count of selected faces across all objects.
301 pub fn face_count(&self) -> usize {
302 self.selected.iter().filter(|(_, s)| s.is_face()).count()
303 }
304
305 /// Count of selected points across all objects.
306 pub fn point_count(&self) -> usize {
307 self.selected.iter().filter(|(_, s)| s.is_point()).count()
308 }
309
310 /// Count of selected vertices across all objects.
311 pub fn vertex_count(&self) -> usize {
312 self.selected.iter().filter(|(_, s)| s.is_vertex()).count()
313 }
314
315 /// Count of selected voxels across all objects.
316 pub fn voxel_count(&self) -> usize {
317 self.selected.iter().filter(|(_, s)| s.is_voxel()).count()
318 }
319
320 /// Count of selected cells across all objects.
321 pub fn cell_count(&self) -> usize {
322 self.selected.iter().filter(|(_, s)| s.is_cell()).count()
323 }
324}
325
326// ---------------------------------------------------------------------------
327// SubSelectionRef
328// ---------------------------------------------------------------------------
329
330/// Geometry info needed to decode a [`SubObjectRef::Voxel`] flat index into
331/// world-space AABB corners for highlight rendering.
332///
333/// Pass one entry per volume object via [`SubSelectionRef::with_voxels`].
334pub struct VolumeSelectionInfo {
335 /// Grid dimensions `[nx, ny, nz]` — same as [`VolumeData::dims`].
336 pub dims: [u32; 3],
337 /// Local-space bounding-box minimum corner (matches [`VolumeItem::bbox_min`]).
338 pub bbox_min: [f32; 3],
339 /// Local-space bounding-box maximum corner (matches [`VolumeItem::bbox_max`]).
340 pub bbox_max: [f32; 3],
341 /// World-space transform (matches [`VolumeItem::model`]).
342 pub model: [[f32; 4]; 4],
343}
344
345/// Geometry info needed to highlight [`SubObjectRef::Point`], [`SubObjectRef::Segment`],
346/// and [`SubObjectRef::Strip`] selections on a polyline item.
347///
348/// Pass one entry per polyline object via [`SubSelectionRef::with_polylines`].
349pub struct PolylineSelectionInfo {
350 /// World-space vertex positions. Each entry is one polyline node.
351 pub positions: Vec<[f32; 3]>,
352 /// Strip lengths. Same encoding as [`PolylineItem::strip_lengths`](crate::renderer::types::items::PolylineItem::strip_lengths):
353 /// each entry is the number of nodes in that strip. If empty, all positions
354 /// belong to a single strip.
355 pub strip_lengths: Vec<u32>,
356}
357
358/// Geometry info needed to highlight a [`SubObjectRef::Cell`] selection.
359///
360/// Contains the vertex positions and cell connectivity from the host's
361/// [`VolumeMeshData`](crate::resources::volume_mesh::VolumeMeshData). Pass one
362/// entry per volume mesh object via [`SubSelectionRef::with_cells`].
363pub struct CellSelectionInfo {
364 /// World-space vertex positions. Indexed by cell connectivity entries.
365 pub positions: Vec<[f32; 3]>,
366 /// Cell connectivity. Each entry is `[u32; 8]` with
367 /// `u32::MAX` padding for cells with fewer than 8 vertices (same encoding
368 /// as [`VolumeMeshData::cells`](crate::resources::volume_mesh::VolumeMeshData::cells)).
369 pub cells: Vec<[u32; 8]>,
370}
371
372/// A renderer-owned snapshot of a [`SubSelection`] taken at frame submission time.
373///
374/// Bundles the selection items with the CPU-side mesh and point cloud data the
375/// renderer needs to build highlight geometry. The renderer does not hold a
376/// reference to any app-owned data between frames.
377///
378/// # Usage
379///
380/// ```ignore
381/// fd.interaction.sub_selection = Some(SubSelectionRef::new(
382/// &self.sub_selection,
383/// mesh_lookup,
384/// model_matrices,
385/// point_positions,
386/// ));
387/// ```
388pub struct SubSelectionRef {
389 /// Snapshot of all selected (node_id, sub_object) pairs.
390 pub(crate) items: Vec<(NodeId, SubObjectRef)>,
391 /// CPU-side vertex positions and triangle indices keyed by node id.
392 ///
393 /// Same format as the `mesh_lookup` parameter to
394 /// [`pick_scene_cpu`](crate::interaction::picking::pick_scene_cpu):
395 /// the value is `(positions, indices)` where every three consecutive
396 /// indices form one triangle.
397 pub(crate) mesh_lookup: std::collections::HashMap<u64, (Vec<[f32; 3]>, Vec<u32>)>,
398 /// World-space model matrix for each node, keyed by node id.
399 ///
400 /// Used to transform local-space mesh positions into world space when
401 /// building fill and edge geometry. Nodes absent from the map are treated
402 /// as having an identity transform.
403 pub(crate) model_matrices: std::collections::HashMap<u64, glam::Mat4>,
404 /// World-space point cloud positions keyed by node id.
405 ///
406 /// Required for [`SubObjectRef::Point`] highlights. The index carried by
407 /// `Point(i)` addresses `point_positions[node_id][i]`.
408 pub(crate) point_positions: std::collections::HashMap<u64, Vec<[f32; 3]>>,
409 /// Volume geometry info keyed by node id.
410 ///
411 /// Required for [`SubObjectRef::Voxel`] highlights. Each entry provides the
412 /// grid dimensions and bounding box so the renderer can decode flat voxel
413 /// indices into world-space AABB wireframes.
414 pub(crate) voxel_lookup: std::collections::HashMap<u64, VolumeSelectionInfo>,
415 /// Unstructured volume mesh geometry keyed by node id.
416 ///
417 /// Required for [`SubObjectRef::Cell`] highlights. Each entry provides the
418 /// vertex positions and cell connectivity so the renderer can draw edge
419 /// outlines around selected cells.
420 pub(crate) cell_lookup: std::collections::HashMap<u64, CellSelectionInfo>,
421 /// Polyline geometry keyed by node id.
422 ///
423 /// Required for [`SubObjectRef::Point`], [`SubObjectRef::Segment`], and
424 /// [`SubObjectRef::Strip`] highlights on polyline items. Each entry provides
425 /// the positions and strip lengths so the renderer can draw node sprites and
426 /// segment edge lines.
427 pub(crate) polyline_lookup: std::collections::HashMap<u64, PolylineSelectionInfo>,
428 /// Curve-family geometry keyed by node id.
429 ///
430 /// Covers [`StreamtubeItem`](crate::renderer::types::items::StreamtubeItem),
431 /// [`TubeItem`](crate::renderer::types::items::TubeItem), and
432 /// [`RibbonItem`](crate::renderer::types::items::RibbonItem).
433 /// Required for [`SubObjectRef::Segment`] and [`SubObjectRef::Strip`] highlights
434 /// on these types. Uses the same [`PolylineSelectionInfo`] encoding since all
435 /// three share identical `positions` / `strip_lengths` fields.
436 pub(crate) curve_family_lookup: std::collections::HashMap<u64, PolylineSelectionInfo>,
437 /// Version counter copied from the source [`SubSelection::version()`].
438 ///
439 /// The renderer uses this to skip GPU buffer rebuilds when the selection
440 /// has not changed since the previous frame.
441 pub version: u64,
442}
443
444impl SubSelectionRef {
445 /// Create a snapshot from a live [`SubSelection`].
446 ///
447 /// - `mesh_lookup` : CPU positions + indices per node id (same type as the
448 /// `mesh_lookup` argument to the CPU pick functions).
449 /// - `model_matrices` : world transform per node id.
450 /// - `point_positions` : point cloud positions per node id (for
451 /// [`SubObjectRef::Point`] entries).
452 pub fn new(
453 sub_selection: &SubSelection,
454 mesh_lookup: std::collections::HashMap<u64, (Vec<[f32; 3]>, Vec<u32>)>,
455 model_matrices: std::collections::HashMap<u64, glam::Mat4>,
456 point_positions: std::collections::HashMap<u64, Vec<[f32; 3]>>,
457 ) -> Self {
458 Self {
459 items: sub_selection.iter().map(|(n, s)| (*n, *s)).collect(),
460 mesh_lookup,
461 model_matrices,
462 point_positions,
463 voxel_lookup: std::collections::HashMap::new(),
464 cell_lookup: std::collections::HashMap::new(),
465 polyline_lookup: std::collections::HashMap::new(),
466 curve_family_lookup: std::collections::HashMap::new(),
467 version: sub_selection.version(),
468 }
469 }
470
471 /// Attach volume geometry info for [`SubObjectRef::Voxel`] highlight rendering.
472 ///
473 /// `lookup` maps each volume's node id to its [`VolumeSelectionInfo`]. Without
474 /// this, selected voxels are silently skipped during highlight geometry build.
475 pub fn with_voxels(
476 mut self,
477 lookup: std::collections::HashMap<u64, VolumeSelectionInfo>,
478 ) -> Self {
479 self.voxel_lookup = lookup;
480 self
481 }
482
483 /// Attach unstructured volume mesh geometry for [`SubObjectRef::Cell`] highlight rendering.
484 ///
485 /// `lookup` maps each volume mesh's node id to its [`CellSelectionInfo`]. Without
486 /// this, selected cells are silently skipped during highlight geometry build.
487 pub fn with_cells(mut self, lookup: std::collections::HashMap<u64, CellSelectionInfo>) -> Self {
488 self.cell_lookup = lookup;
489 self
490 }
491
492 /// Attach polyline geometry for [`SubObjectRef::Point`], [`SubObjectRef::Segment`],
493 /// and [`SubObjectRef::Strip`] highlight rendering.
494 ///
495 /// `lookup` maps each polyline item's node id to its [`PolylineSelectionInfo`].
496 /// Without this, selected polyline nodes and segments are silently skipped during
497 /// highlight geometry build.
498 pub fn with_polylines(
499 mut self,
500 lookup: std::collections::HashMap<u64, PolylineSelectionInfo>,
501 ) -> Self {
502 self.polyline_lookup = lookup;
503 self
504 }
505
506 /// Attach curve-family geometry for [`SubObjectRef::Segment`] and
507 /// [`SubObjectRef::Strip`] highlight rendering on streamtube, tube, and ribbon items.
508 ///
509 /// `lookup` maps each item's node id to a [`PolylineSelectionInfo`] (same
510 /// `positions` / `strip_lengths` encoding). Without this, selected segments
511 /// and strips on these types are silently skipped during highlight geometry build.
512 pub fn with_curve_families(
513 mut self,
514 lookup: std::collections::HashMap<u64, PolylineSelectionInfo>,
515 ) -> Self {
516 self.curve_family_lookup = lookup;
517 self
518 }
519
520 /// Returns `true` if the snapshot contains no selected sub-objects.
521 pub fn is_empty(&self) -> bool {
522 self.items.is_empty()
523 }
524}
525
526// ---------------------------------------------------------------------------
527// Tests
528// ---------------------------------------------------------------------------
529
530#[cfg(test)]
531mod tests {
532 use super::*;
533 use crate::interaction::picking::RectPickResult;
534
535 // --- SubObjectRef ---
536
537 #[test]
538 fn sub_object_ref_kind_checks() {
539 assert!(SubObjectRef::Face(0).is_face());
540 assert!(!SubObjectRef::Face(0).is_point());
541 assert!(!SubObjectRef::Face(0).is_vertex());
542 assert!(!SubObjectRef::Face(0).is_edge());
543
544 assert!(SubObjectRef::Point(1).is_point());
545 assert!(SubObjectRef::Vertex(2).is_vertex());
546 assert!(SubObjectRef::Edge(3).is_edge());
547 }
548
549 #[test]
550 fn sub_object_ref_index() {
551 assert_eq!(SubObjectRef::Face(7).index(), 7);
552 assert_eq!(SubObjectRef::Vertex(42).index(), 42);
553 assert_eq!(SubObjectRef::Edge(0).index(), 0);
554 assert_eq!(SubObjectRef::Point(99).index(), 99);
555 }
556
557 #[test]
558 fn sub_object_ref_from_feature_id() {
559 use parry3d::shape::FeatureId;
560 assert_eq!(
561 SubObjectRef::from_feature_id(FeatureId::Face(3)),
562 Some(SubObjectRef::Face(3))
563 );
564 assert_eq!(
565 SubObjectRef::from_feature_id(FeatureId::Vertex(1)),
566 Some(SubObjectRef::Vertex(1))
567 );
568 assert_eq!(
569 SubObjectRef::from_feature_id(FeatureId::Edge(2)),
570 Some(SubObjectRef::Edge(2))
571 );
572 assert_eq!(SubObjectRef::from_feature_id(FeatureId::Unknown), None);
573 }
574
575 #[test]
576 fn sub_object_ref_hashable() {
577 let mut set = std::collections::HashSet::new();
578 set.insert(SubObjectRef::Face(0));
579 set.insert(SubObjectRef::Face(0)); // duplicate
580 set.insert(SubObjectRef::Face(1));
581 set.insert(SubObjectRef::Point(0)); // same index, different variant
582 assert_eq!(set.len(), 3);
583 }
584
585 // --- SubSelection ---
586
587 #[test]
588 fn sub_selection_select_one_clears_others() {
589 let mut sel = SubSelection::new();
590 sel.add(1, SubObjectRef::Face(0));
591 sel.add(1, SubObjectRef::Face(1));
592 sel.select_one(1, SubObjectRef::Face(5));
593 assert_eq!(sel.len(), 1);
594 assert!(sel.contains(1, SubObjectRef::Face(5)));
595 assert!(!sel.contains(1, SubObjectRef::Face(0)));
596 }
597
598 #[test]
599 fn sub_selection_toggle() {
600 let mut sel = SubSelection::new();
601 sel.toggle(1, SubObjectRef::Face(0));
602 assert!(sel.contains(1, SubObjectRef::Face(0)));
603 sel.toggle(1, SubObjectRef::Face(0));
604 assert!(!sel.contains(1, SubObjectRef::Face(0)));
605 assert!(sel.is_empty());
606 }
607
608 #[test]
609 fn sub_selection_add_preserves_others() {
610 let mut sel = SubSelection::new();
611 sel.add(1, SubObjectRef::Face(0));
612 sel.add(1, SubObjectRef::Face(1));
613 assert_eq!(sel.len(), 2);
614 assert!(sel.contains(1, SubObjectRef::Face(0)));
615 assert!(sel.contains(1, SubObjectRef::Face(1)));
616 }
617
618 #[test]
619 fn sub_selection_remove() {
620 let mut sel = SubSelection::new();
621 sel.add(1, SubObjectRef::Face(0));
622 sel.add(1, SubObjectRef::Face(1));
623 sel.remove(1, SubObjectRef::Face(0));
624 assert!(!sel.contains(1, SubObjectRef::Face(0)));
625 assert_eq!(sel.len(), 1);
626 }
627
628 #[test]
629 fn sub_selection_clear() {
630 let mut sel = SubSelection::new();
631 sel.add(1, SubObjectRef::Face(0));
632 sel.add(2, SubObjectRef::Point(3));
633 sel.clear();
634 assert!(sel.is_empty());
635 assert_eq!(sel.primary(), None);
636 }
637
638 #[test]
639 fn sub_selection_primary_tracks_last() {
640 let mut sel = SubSelection::new();
641 sel.add(1, SubObjectRef::Face(0));
642 assert_eq!(sel.primary(), Some((1, SubObjectRef::Face(0))));
643 sel.add(2, SubObjectRef::Point(5));
644 assert_eq!(sel.primary(), Some((2, SubObjectRef::Point(5))));
645 }
646
647 #[test]
648 fn sub_selection_contains() {
649 let mut sel = SubSelection::new();
650 sel.add(10, SubObjectRef::Face(3));
651 assert!(sel.contains(10, SubObjectRef::Face(3)));
652 assert!(!sel.contains(10, SubObjectRef::Face(4)));
653 assert!(!sel.contains(99, SubObjectRef::Face(3)));
654 }
655
656 #[test]
657 fn sub_selection_for_object() {
658 let mut sel = SubSelection::new();
659 sel.add(1, SubObjectRef::Face(0));
660 sel.add(1, SubObjectRef::Face(1));
661 sel.add(2, SubObjectRef::Face(0));
662 let obj1: Vec<SubObjectRef> = {
663 let mut v: Vec<_> = sel.for_object(1).collect();
664 v.sort_by_key(|s| s.index());
665 v
666 };
667 assert_eq!(obj1, vec![SubObjectRef::Face(0), SubObjectRef::Face(1)]);
668 let obj2: Vec<SubObjectRef> = sel.for_object(2).collect();
669 assert_eq!(obj2, vec![SubObjectRef::Face(0)]);
670 assert_eq!(sel.for_object(99).count(), 0);
671 }
672
673 #[test]
674 fn sub_selection_version_increments() {
675 let mut sel = SubSelection::new();
676 let v0 = sel.version();
677 sel.add(1, SubObjectRef::Face(0));
678 assert!(sel.version() > v0);
679 let v1 = sel.version();
680 sel.clear();
681 assert!(sel.version() > v1);
682 }
683
684 #[test]
685 fn sub_selection_kind_counts() {
686 let mut sel = SubSelection::new();
687 sel.add(1, SubObjectRef::Face(0));
688 sel.add(1, SubObjectRef::Face(1));
689 sel.add(2, SubObjectRef::Point(0));
690 sel.add(3, SubObjectRef::Vertex(0));
691 assert_eq!(sel.face_count(), 2);
692 assert_eq!(sel.point_count(), 1);
693 assert_eq!(sel.vertex_count(), 1);
694 }
695
696 #[test]
697 fn sub_selection_extend() {
698 let mut sel = SubSelection::new();
699 sel.extend([
700 (1, SubObjectRef::Face(0)),
701 (1, SubObjectRef::Face(1)),
702 (2, SubObjectRef::Point(3)),
703 ]);
704 assert_eq!(sel.len(), 3);
705 assert_eq!(sel.primary(), Some((2, SubObjectRef::Point(3))));
706 }
707
708 #[test]
709 fn sub_selection_extend_from_rect_pick() {
710 let mut result = RectPickResult::default();
711 result
712 .hits
713 .insert(10, vec![SubObjectRef::Face(0), SubObjectRef::Face(1)]);
714 result.hits.insert(20, vec![SubObjectRef::Point(5)]);
715
716 let mut sel = SubSelection::new();
717 sel.extend_from_rect_pick(&result);
718
719 assert_eq!(sel.len(), 3);
720 assert!(sel.contains(10, SubObjectRef::Face(0)));
721 assert!(sel.contains(10, SubObjectRef::Face(1)));
722 assert!(sel.contains(20, SubObjectRef::Point(5)));
723 assert_eq!(sel.face_count(), 2);
724 assert_eq!(sel.point_count(), 1);
725 }
726}