Skip to main content

viewport_lib/renderer/picking/
rect.rs

1//! Rectangle pick: collect all items and sub-elements whose projected
2//! geometry falls inside a screen-space selection rectangle.
3
4use super::*;
5
6impl ViewportRenderer {
7    // -----------------------------------------------------------------------
8    // Unified CPU rect pick : renderer.pick_rect()
9    // -----------------------------------------------------------------------
10
11    /// Pick all items or sub-elements inside a screen-space rectangle.
12    ///
13    /// Dispatches across all item types retained from the last `prepare()` call.
14    /// The `mask` controls which item types and sub-element levels participate.
15    ///
16    /// # Arguments
17    /// * `rect_min`      - top-left corner of the selection rect in viewport pixels
18    /// * `rect_max`      - bottom-right corner of the selection rect in viewport pixels
19    /// * `viewport_size` - viewport width x height in pixels
20    /// * `view_proj`     - combined view x projection matrix from the last frame
21    /// * `mask`          - which item types and sub-element levels to include
22    pub fn pick_rect(
23        &self,
24        rect_min: glam::Vec2,
25        rect_max: glam::Vec2,
26        viewport_size: glam::Vec2,
27        view_proj: glam::Mat4,
28        mask: crate::interaction::select::pick_mask::PickMask,
29    ) -> PickRectResult {
30        use crate::interaction::select::pick_mask::PickMask;
31        use crate::interaction::select::sub_object::SubObjectRef;
32
33        let mut result = PickRectResult::default();
34
35        if !self.cpu_pick_cache_enabled {
36            warn_pick_cache_disabled();
37            return result;
38        }
39
40        if viewport_size.x <= 0.0 || viewport_size.y <= 0.0 {
41            return result;
42        }
43
44        let wants_face = mask.intersects(PickMask::FACE);
45        let wants_vertex = mask.intersects(PickMask::VERTEX);
46        let wants_cell = mask.intersects(PickMask::CELL);
47        let wants_cloud = mask.intersects(PickMask::CLOUD_POINT);
48        let wants_splat = mask.intersects(PickMask::SPLAT);
49        let wants_object = mask.intersects(PickMask::OBJECT);
50
51        // Build lookup for opaque volume mesh face_to_cell maps.
52        let vm_cell_map: std::collections::HashMap<u64, &[u32]> = self
53            .pick_volume_mesh_items
54            .iter()
55            .filter(|item| item.settings.pick_id != PickId::NONE && !item.face_to_cell.is_empty())
56            .map(|item| (item.settings.pick_id.0, item.face_to_cell.as_slice()))
57            .collect();
58
59        // Project a local-space point through mvp and return screen coords,
60        // or None if the point is behind the camera.
61        let project = |mvp: glam::Mat4, local: glam::Vec3| -> Option<(f32, f32)> {
62            let clip = mvp * local.extend(1.0);
63            if clip.w <= 0.0 {
64                return None;
65            }
66            let sx = (clip.x / clip.w + 1.0) * 0.5 * viewport_size.x;
67            let sy = (1.0 - clip.y / clip.w) * 0.5 * viewport_size.y;
68            Some((sx, sy))
69        };
70
71        let in_rect = |sx: f32, sy: f32| -> bool {
72            sx >= rect_min.x && sx <= rect_max.x && sy >= rect_min.y && sy <= rect_max.y
73        };
74
75        // 1. Surface mesh picks (FACE, VERTEX, CELL, or OBJECT).
76        if wants_face || wants_vertex || wants_cell || wants_object {
77            for item in &self.pick_scene_items {
78                if item.settings.hidden || item.settings.pick_id == PickId::NONE {
79                    continue;
80                }
81                let Some(mesh) = self.resources.mesh_store.get(item.mesh_id) else {
82                    continue;
83                };
84                let (Some(positions), Some(indices)) = (&mesh.cpu_positions, &mesh.cpu_indices)
85                else {
86                    continue;
87                };
88
89                let model = glam::Mat4::from_cols_array_2d(&item.model);
90                let mvp = view_proj * model;
91                let id = item.settings.pick_id.0;
92                let mut item_hit = false;
93
94                if wants_face {
95                    for (tri_idx, chunk) in indices.chunks(3).enumerate() {
96                        if chunk.len() < 3 {
97                            continue;
98                        }
99                        let [i0, i1, i2] =
100                            [chunk[0] as usize, chunk[1] as usize, chunk[2] as usize];
101                        if i0 >= positions.len() || i1 >= positions.len() || i2 >= positions.len() {
102                            continue;
103                        }
104                        let centroid = (glam::Vec3::from(positions[i0])
105                            + glam::Vec3::from(positions[i1])
106                            + glam::Vec3::from(positions[i2]))
107                            / 3.0;
108                        if let Some((sx, sy)) = project(mvp, centroid) {
109                            if in_rect(sx, sy) {
110                                result
111                                    .elements
112                                    .push((id, SubObjectRef::Face(tri_idx as u32)));
113                                item_hit = true;
114                            }
115                        }
116                    }
117                } else if wants_cell {
118                    // Convert boundary triangle hits to originating cell indices.
119                    if let Some(f2c) = vm_cell_map.get(&id) {
120                        let mut seen = std::collections::HashSet::new();
121                        for (tri_idx, chunk) in indices.chunks(3).enumerate() {
122                            if chunk.len() < 3 {
123                                continue;
124                            }
125                            let [i0, i1, i2] =
126                                [chunk[0] as usize, chunk[1] as usize, chunk[2] as usize];
127                            if i0 >= positions.len()
128                                || i1 >= positions.len()
129                                || i2 >= positions.len()
130                            {
131                                continue;
132                            }
133                            let centroid = (glam::Vec3::from(positions[i0])
134                                + glam::Vec3::from(positions[i1])
135                                + glam::Vec3::from(positions[i2]))
136                                / 3.0;
137                            if let Some((sx, sy)) = project(mvp, centroid) {
138                                if in_rect(sx, sy) {
139                                    if let Some(&ci) = f2c.get(tri_idx) {
140                                        if seen.insert(ci) {
141                                            result.elements.push((id, SubObjectRef::Cell(ci)));
142                                        }
143                                    }
144                                    item_hit = true;
145                                }
146                            }
147                        }
148                    } else if wants_vertex {
149                        // No cell map; fall through to vertex picking for regular meshes.
150                        for (vi, pos) in positions.iter().enumerate() {
151                            if let Some((sx, sy)) = project(mvp, glam::Vec3::from(*pos)) {
152                                if in_rect(sx, sy) {
153                                    result.elements.push((id, SubObjectRef::Vertex(vi as u32)));
154                                    item_hit = true;
155                                }
156                            }
157                        }
158                    }
159                } else if wants_vertex {
160                    for (vi, pos) in positions.iter().enumerate() {
161                        if let Some((sx, sy)) = project(mvp, glam::Vec3::from(*pos)) {
162                            if in_rect(sx, sy) {
163                                result.elements.push((id, SubObjectRef::Vertex(vi as u32)));
164                                item_hit = true;
165                            }
166                        }
167                    }
168                } else {
169                    // OBJECT only: mark as hit if any triangle centroid is in rect.
170                    'tri_scan: for chunk in indices.chunks(3) {
171                        if chunk.len() < 3 {
172                            continue;
173                        }
174                        let [i0, i1, i2] =
175                            [chunk[0] as usize, chunk[1] as usize, chunk[2] as usize];
176                        if i0 >= positions.len() || i1 >= positions.len() || i2 >= positions.len() {
177                            continue;
178                        }
179                        let centroid = (glam::Vec3::from(positions[i0])
180                            + glam::Vec3::from(positions[i1])
181                            + glam::Vec3::from(positions[i2]))
182                            / 3.0;
183                        if let Some((sx, sy)) = project(mvp, centroid) {
184                            if in_rect(sx, sy) {
185                                item_hit = true;
186                                break 'tri_scan;
187                            }
188                        }
189                    }
190                }
191
192                if wants_object && item_hit {
193                    result.objects.push(id);
194                }
195            }
196        }
197
198        // 2. Opaque volume mesh cell picks are handled in section 1 above via
199        // vm_cell_map (face_to_cell conversion on boundary triangle hits).
200
201        // 2b. Interior-inclusive cell picks for volume meshes rendering
202        //     transparently. Items rendering as opaque are handled in section 1
203        //     above via vm_cell_map (face_to_cell on the boundary surface).
204        if wants_cell || wants_object {
205            for item in &self.pick_volume_mesh_items {
206                if item.settings.pick_id == PickId::NONE || item.transparency.is_none() {
207                    continue;
208                }
209                let Some(data) = item.volume_mesh_data.as_deref() else {
210                    continue;
211                };
212                use crate::resources::volume::volume_mesh::CELL_SENTINEL;
213                let id = item.settings.pick_id.0;
214                let mvp = view_proj * glam::Mat4::from_cols_array_2d(&item.model);
215                let mut item_hit = false;
216
217                for (cell_idx, cell) in data.cells.iter().enumerate() {
218                    let nv: usize = if cell[4] == CELL_SENTINEL {
219                        4
220                    } else if cell[5] == CELL_SENTINEL {
221                        5
222                    } else if cell[6] == CELL_SENTINEL {
223                        6
224                    } else {
225                        8
226                    };
227                    let centroid: glam::Vec3 = cell[..nv]
228                        .iter()
229                        .map(|&vi| glam::Vec3::from(data.positions[vi as usize]))
230                        .sum::<glam::Vec3>()
231                        / nv as f32;
232                    if let Some((sx, sy)) = project(mvp, centroid) {
233                        if in_rect(sx, sy) {
234                            if wants_cell {
235                                result
236                                    .elements
237                                    .push((id, SubObjectRef::Cell(cell_idx as u32)));
238                            }
239                            item_hit = true;
240                        }
241                    }
242                }
243
244                if wants_object && item_hit {
245                    result.objects.push(id);
246                }
247            }
248        }
249
250        // 3. Point cloud picks (CLOUD_POINT or OBJECT).
251        if wants_cloud || wants_object {
252            for item in &self.pick_point_cloud_items {
253                if item.settings.pick_id == PickId::NONE || item.positions.is_empty() {
254                    continue;
255                }
256                let model = glam::Mat4::from_cols_array_2d(&item.model);
257                let mvp = view_proj * model;
258                let id = item.settings.pick_id.0;
259                let mut item_hit = false;
260
261                for (pt_idx, pos) in item.positions.iter().enumerate() {
262                    if let Some((sx, sy)) = project(mvp, glam::Vec3::from(*pos)) {
263                        if in_rect(sx, sy) {
264                            if wants_cloud {
265                                result
266                                    .elements
267                                    .push((id, SubObjectRef::Point(pt_idx as u32)));
268                            }
269                            item_hit = true;
270                        }
271                    }
272                }
273
274                if wants_object && item_hit {
275                    result.objects.push(id);
276                }
277            }
278        }
279
280        // 4. Volume voxel picks (VOXEL or OBJECT).
281        let wants_voxel = mask.intersects(PickMask::VOXEL);
282        if wants_voxel || wants_object {
283            for item in &self.pick_volume_items {
284                if item.settings.pick_id == PickId::NONE {
285                    continue;
286                }
287                let Some(vol_data) = item.volume_data.as_deref() else {
288                    continue;
289                };
290                let [nx, ny, nz] = vol_data.dims;
291                if nx == 0 || ny == 0 || nz == 0 || vol_data.data.is_empty() {
292                    continue;
293                }
294                let model = glam::Mat4::from_cols_array_2d(&item.model);
295                let mvp = view_proj * model;
296                let bbox_min = glam::Vec3::from(item.bbox_min);
297                let bbox_max = glam::Vec3::from(item.bbox_max);
298                let cell = (bbox_max - bbox_min) / glam::Vec3::new(nx as f32, ny as f32, nz as f32);
299                let id = item.settings.pick_id.0;
300                let mut item_hit = false;
301
302                for iz in 0..nz {
303                    for iy in 0..ny {
304                        for ix in 0..nx {
305                            let flat = (ix + iy * nx + iz * nx * ny) as usize;
306                            let scalar = vol_data.data[flat];
307                            if scalar.is_nan()
308                                || scalar < item.threshold_min
309                                || scalar > item.threshold_max
310                            {
311                                continue;
312                            }
313                            let center = bbox_min
314                                + cell
315                                    * glam::Vec3::new(
316                                        ix as f32 + 0.5,
317                                        iy as f32 + 0.5,
318                                        iz as f32 + 0.5,
319                                    );
320                            if let Some((sx, sy)) = project(mvp, center) {
321                                if in_rect(sx, sy) {
322                                    if wants_voxel {
323                                        result
324                                            .elements
325                                            .push((id, SubObjectRef::Voxel(flat as u32)));
326                                    }
327                                    item_hit = true;
328                                }
329                            }
330                        }
331                    }
332                }
333
334                if wants_object && item_hit {
335                    result.objects.push(id);
336                }
337            }
338        }
339
340        // 5. Gaussian splat picks (SPLAT or OBJECT).
341        if wants_splat || wants_object {
342            for item in &self.pick_splat_items {
343                if item.settings.pick_id == PickId::NONE {
344                    continue;
345                }
346                let Some(gpu_set) = self.resources.content.gaussian_splat_store.get(item.source)
347                else {
348                    continue;
349                };
350                if gpu_set.cpu_positions.is_empty() {
351                    continue;
352                }
353                let model = glam::Mat4::from_cols_array_2d(&item.model);
354                let mvp = view_proj * model;
355                let id = item.settings.pick_id.0;
356                let mut item_hit = false;
357
358                for (i, pos) in gpu_set.cpu_positions.iter().enumerate() {
359                    if let Some((sx, sy)) = project(mvp, glam::Vec3::from(*pos)) {
360                        if in_rect(sx, sy) {
361                            if wants_splat {
362                                result.elements.push((id, SubObjectRef::Splat(i as u32)));
363                            }
364                            item_hit = true;
365                        }
366                    }
367                }
368
369                if wants_object && item_hit {
370                    result.objects.push(id);
371                }
372            }
373        }
374
375        // 6. Instance picks (INSTANCE or OBJECT) for glyphs, tensor glyphs, sprites.
376        let wants_instance = mask.intersects(PickMask::INSTANCE);
377        if wants_instance || wants_object {
378            // Glyphs
379            for item in &self.pick_glyph_items {
380                if item.settings.pick_id == PickId::NONE || item.positions.is_empty() {
381                    continue;
382                }
383                let model = glam::Mat4::from_cols_array_2d(&item.model);
384                let mvp = view_proj * model;
385                let id = item.settings.pick_id.0;
386                let mut item_hit = false;
387                for (i, pos) in item.positions.iter().enumerate() {
388                    if let Some((sx, sy)) = project(mvp, glam::Vec3::from(*pos)) {
389                        if in_rect(sx, sy) {
390                            if wants_instance {
391                                result.elements.push((id, SubObjectRef::Instance(i as u32)));
392                            }
393                            item_hit = true;
394                        }
395                    }
396                }
397                if wants_object && item_hit {
398                    result.objects.push(id);
399                }
400            }
401
402            // Tensor glyphs
403            for item in &self.pick_tensor_glyph_items {
404                if item.settings.pick_id == PickId::NONE || item.positions.is_empty() {
405                    continue;
406                }
407                let model = glam::Mat4::from_cols_array_2d(&item.model);
408                let mvp = view_proj * model;
409                let id = item.settings.pick_id.0;
410                let mut item_hit = false;
411                for (i, pos) in item.positions.iter().enumerate() {
412                    if let Some((sx, sy)) = project(mvp, glam::Vec3::from(*pos)) {
413                        if in_rect(sx, sy) {
414                            if wants_instance {
415                                result.elements.push((id, SubObjectRef::Instance(i as u32)));
416                            }
417                            item_hit = true;
418                        }
419                    }
420                }
421                if wants_object && item_hit {
422                    result.objects.push(id);
423                }
424            }
425
426            // Sprites
427            for item in &self.pick_sprite_items {
428                if item.settings.pick_id == PickId::NONE || item.positions.is_empty() {
429                    continue;
430                }
431                let model = glam::Mat4::from_cols_array_2d(&item.model);
432                let mvp = view_proj * model;
433                let id = item.settings.pick_id.0;
434                let mut item_hit = false;
435                for (i, pos) in item.positions.iter().enumerate() {
436                    if let Some((sx, sy)) = project(mvp, glam::Vec3::from(*pos)) {
437                        if in_rect(sx, sy) {
438                            if wants_instance {
439                                result.elements.push((id, SubObjectRef::Instance(i as u32)));
440                            }
441                            item_hit = true;
442                        }
443                    }
444                }
445                if wants_object && item_hit {
446                    result.objects.push(id);
447                }
448            }
449        }
450
451        // 7. Polyline node / segment / strip / object rect picks.
452        let wants_poly_node = mask.intersects(PickMask::POLY_NODE);
453        let wants_segment = mask.intersects(PickMask::SEGMENT);
454        let wants_strip = mask.intersects(PickMask::STRIP);
455        if wants_poly_node || wants_segment || wants_strip || wants_object {
456            for item in &self.pick_polyline_items {
457                if item.settings.pick_id == PickId::NONE || item.positions.is_empty() {
458                    continue;
459                }
460                let id = item.settings.pick_id.0;
461                let mut item_hit = false;
462                let mut strips_hit = std::collections::HashSet::<u32>::new();
463
464                // Node pass (POLY_NODE or STRIP or OBJECT).
465                if wants_poly_node || wants_strip || wants_object {
466                    for (node_idx, pos) in item.positions.iter().enumerate() {
467                        if let Some((sx, sy)) = project(view_proj, glam::Vec3::from(*pos)) {
468                            if in_rect(sx, sy) {
469                                item_hit = true;
470                                if wants_poly_node {
471                                    result
472                                        .elements
473                                        .push((id, SubObjectRef::Point(node_idx as u32)));
474                                } else if wants_strip {
475                                    let s = strip_for_node(node_idx as u32, &item.strip_lengths);
476                                    strips_hit.insert(s);
477                                }
478                            }
479                        }
480                    }
481                }
482
483                // Segment pass (SEGMENT or STRIP or OBJECT) -- full segment/rect intersection.
484                if wants_segment || (wants_strip && !wants_poly_node) || wants_object {
485                    let mut node_off = 0usize;
486                    let mut seg_off = 0u32;
487                    macro_rules! try_seg_rect {
488                        ($ai:expr, $bi:expr, $seg:expr) => {{
489                            if let (Some((sax, say)), Some((sbx, sby))) = (
490                                project(view_proj, glam::Vec3::from(item.positions[$ai])),
491                                project(view_proj, glam::Vec3::from(item.positions[$bi])),
492                            ) {
493                                if segment_in_rect(
494                                    glam::Vec2::new(sax, say),
495                                    glam::Vec2::new(sbx, sby),
496                                    rect_min,
497                                    rect_max,
498                                ) {
499                                    item_hit = true;
500                                    if wants_segment {
501                                        result.elements.push((id, SubObjectRef::Segment($seg)));
502                                    } else if wants_strip {
503                                        let s = strip_for_segment($seg, &item.strip_lengths);
504                                        strips_hit.insert(s);
505                                    }
506                                }
507                            }
508                        }};
509                    }
510                    if item.strip_lengths.is_empty() {
511                        for j in 0..item.positions.len().saturating_sub(1) {
512                            try_seg_rect!(j, j + 1, j as u32);
513                        }
514                    } else {
515                        for &slen in &item.strip_lengths {
516                            let slen = slen as usize;
517                            for j in 0..slen.saturating_sub(1) {
518                                try_seg_rect!(node_off + j, node_off + j + 1, seg_off + j as u32);
519                            }
520                            seg_off += slen.saturating_sub(1) as u32;
521                            node_off += slen;
522                        }
523                    }
524                }
525
526                if wants_strip {
527                    for s in strips_hit {
528                        result.elements.push((id, SubObjectRef::Strip(s)));
529                    }
530                }
531                if wants_object && item_hit {
532                    result.objects.push(id);
533                }
534            }
535        }
536
537        // 8. Streamtube / tube / ribbon segment / strip / object rect picks.
538        if wants_poly_node || wants_segment || wants_strip || wants_object {
539            // Streamtube and tube: test both projected endpoints of each segment
540            // with segment_in_rect instead of the midpoint projection heuristic.
541            // POLY_NODE: also check each control point individually.
542            let st_tube_iter = self
543                .pick_streamtube_items
544                .iter()
545                .map(|it| {
546                    (
547                        it.settings.pick_id.0,
548                        it.positions.as_slice(),
549                        it.strip_lengths.as_slice(),
550                    )
551                })
552                .chain(self.pick_tube_items.iter().map(|it| {
553                    (
554                        it.settings.pick_id.0,
555                        it.positions.as_slice(),
556                        it.strip_lengths.as_slice(),
557                    )
558                }));
559
560            for (id, positions, strip_lengths) in st_tube_iter {
561                if id == 0 || positions.is_empty() {
562                    continue;
563                }
564                let mut item_hit = false;
565                let mut strips_hit = std::collections::HashSet::<u32>::new();
566
567                let single_st;
568                let strips_st: &[u32] = if strip_lengths.is_empty() {
569                    single_st = [positions.len() as u32];
570                    &single_st
571                } else {
572                    strip_lengths
573                };
574
575                // POLY_NODE pass: project each control point and check in_rect.
576                if wants_poly_node || wants_strip || wants_object {
577                    'st_nodes: for (ni, pos) in positions.iter().enumerate() {
578                        if let Some((sx, sy)) = project(view_proj, glam::Vec3::from(*pos)) {
579                            if in_rect(sx, sy) {
580                                item_hit = true;
581                                if wants_poly_node {
582                                    result.elements.push((id, SubObjectRef::Point(ni as u32)));
583                                } else if wants_strip {
584                                    let s = strip_for_node(ni as u32, strip_lengths);
585                                    strips_hit.insert(s);
586                                } else {
587                                    // wants_object only: no need to enumerate further nodes.
588                                    break 'st_nodes;
589                                }
590                            }
591                        }
592                    }
593                }
594
595                // SEGMENT pass: test both projected endpoints of each segment.
596                if wants_segment || wants_strip || wants_object {
597                    let mut node_off = 0usize;
598                    let mut seg_off = 0u32;
599                    'st_strips: for &slen in strips_st {
600                        let slen = slen as usize;
601                        for j in 0..slen.saturating_sub(1) {
602                            let seg_idx = seg_off + j as u32;
603                            let pa = glam::Vec3::from(positions[node_off + j]);
604                            let pb = glam::Vec3::from(positions[node_off + j + 1]);
605                            let hit = match (project(view_proj, pa), project(view_proj, pb)) {
606                                (Some((ax, ay)), Some((bx, by))) => segment_in_rect(
607                                    glam::Vec2::new(ax, ay),
608                                    glam::Vec2::new(bx, by),
609                                    rect_min,
610                                    rect_max,
611                                ),
612                                (Some((ax, ay)), None) => in_rect(ax, ay),
613                                (None, Some((bx, by))) => in_rect(bx, by),
614                                (None, None) => false,
615                            };
616                            if hit {
617                                item_hit = true;
618                                if wants_segment {
619                                    result.elements.push((id, SubObjectRef::Segment(seg_idx)));
620                                } else if wants_strip {
621                                    let s = strip_for_segment(seg_idx, strip_lengths);
622                                    strips_hit.insert(s);
623                                } else {
624                                    // wants_object only: no need to enumerate further segments.
625                                    break 'st_strips;
626                                }
627                            }
628                        }
629                        seg_off += slen.saturating_sub(1) as u32;
630                        node_off += slen;
631                    }
632                }
633
634                if wants_strip {
635                    for s in strips_hit {
636                        result.elements.push((id, SubObjectRef::Strip(s)));
637                    }
638                }
639                if wants_object && item_hit {
640                    result.objects.push(id);
641                }
642            }
643
644            // Ribbon: reconstruct the swept quad per segment and test all four
645            // quad edges with segment_in_rect (also catches quad corners inside
646            // the rect via the endpoint check inside segment_in_rect).
647            // POLY_NODE: also check each control point individually.
648            for item in &self.pick_ribbon_items {
649                if item.settings.pick_id == PickId::NONE || item.positions.is_empty() {
650                    continue;
651                }
652
653                let single_r;
654                let strips_r: &[u32] = if item.strip_lengths.is_empty() {
655                    single_r = [item.positions.len() as u32];
656                    &single_r
657                } else {
658                    &item.strip_lengths
659                };
660
661                let mut item_hit = false;
662                let mut strips_hit = std::collections::HashSet::<u32>::new();
663
664                // Project a world point to screen Vec2; returns None if behind camera.
665                let proj2 = |p: glam::Vec3| -> Option<glam::Vec2> {
666                    project(view_proj, p).map(|(x, y)| glam::Vec2::new(x, y))
667                };
668
669                // POLY_NODE pass: project each control point and check in_rect.
670                if wants_poly_node || wants_strip || wants_object {
671                    'rb_nodes: for (ni, pos) in item.positions.iter().enumerate() {
672                        if let Some((sx, sy)) = project(view_proj, glam::Vec3::from(*pos)) {
673                            if in_rect(sx, sy) {
674                                item_hit = true;
675                                if wants_poly_node {
676                                    result.elements.push((
677                                        item.settings.pick_id.0,
678                                        SubObjectRef::Point(ni as u32),
679                                    ));
680                                } else if wants_strip {
681                                    let s = strip_for_node(ni as u32, &item.strip_lengths);
682                                    strips_hit.insert(s);
683                                } else {
684                                    break 'rb_nodes;
685                                }
686                            }
687                        }
688                    }
689                }
690
691                // SEGMENT pass: quad edge tests using ribbon_lateral_frames.
692                if wants_segment || wants_strip || wants_object {
693                    let frames = ribbon_lateral_frames(
694                        &item.positions,
695                        &item.strip_lengths,
696                        item.width,
697                        item.width_attribute.as_deref(),
698                        item.twist_attribute.as_deref(),
699                    );
700                    let mut node_off = 0usize;
701                    let mut seg_off = 0u32;
702
703                    'rb_strips: for &slen in strips_r {
704                        let slen = slen as usize;
705                        for k in 0..slen.saturating_sub(1) {
706                            let seg_idx = seg_off + k as u32;
707                            let ia = node_off + k;
708                            let ib = node_off + k + 1;
709                            let pa = glam::Vec3::from(item.positions[ia]);
710                            let pb = glam::Vec3::from(item.positions[ib]);
711                            let (ua, wa) = frames[ia];
712                            let (ub, wb) = frames[ib];
713                            let c0 = pa + ua * wa; // left  at a
714                            let c1 = pa - ua * wa; // right at a
715                            let c2 = pb + ub * wb; // left  at b
716                            let c3 = pb - ub * wb; // right at b
717                            let sc0 = proj2(c0);
718                            let sc1 = proj2(c1);
719                            let sc2 = proj2(c2);
720                            let sc3 = proj2(c3);
721                            let edge_hit = |a: Option<glam::Vec2>, b: Option<glam::Vec2>| -> bool {
722                                match (a, b) {
723                                    (Some(a), Some(b)) => segment_in_rect(a, b, rect_min, rect_max),
724                                    (Some(a), None) => in_rect(a.x, a.y),
725                                    (None, Some(b)) => in_rect(b.x, b.y),
726                                    (None, None) => false,
727                                }
728                            };
729                            let hit = edge_hit(sc0, sc1)
730                                || edge_hit(sc2, sc3)
731                                || edge_hit(sc0, sc2)
732                                || edge_hit(sc1, sc3);
733                            if hit {
734                                item_hit = true;
735                                if wants_segment {
736                                    result.elements.push((
737                                        item.settings.pick_id.0,
738                                        SubObjectRef::Segment(seg_idx),
739                                    ));
740                                } else if wants_strip {
741                                    let s = strip_for_segment(seg_idx, &item.strip_lengths);
742                                    strips_hit.insert(s);
743                                } else {
744                                    break 'rb_strips;
745                                }
746                            }
747                        }
748                        seg_off += slen.saturating_sub(1) as u32;
749                        node_off += slen;
750                    }
751                }
752
753                if wants_strip {
754                    for s in strips_hit {
755                        result
756                            .elements
757                            .push((item.settings.pick_id.0, SubObjectRef::Strip(s)));
758                    }
759                }
760                if wants_object && item_hit {
761                    result.objects.push(item.settings.pick_id.0);
762                }
763            }
764        }
765
766        // 9. Image slice / volume surface slice / screen image object rect picks (OBJECT only).
767        if wants_object {
768            // Image slice: project all 4 quad corners and check containment/edge intersection.
769            for item in &self.pick_image_slice_items {
770                if item.settings.pick_id == PickId::NONE {
771                    continue;
772                }
773                let [bmin, bmax] = [item.bbox_min, item.bbox_max];
774                let t = item.offset;
775                let corners: [[f32; 3]; 4] = match item.axis {
776                    SliceAxis::X => {
777                        let x = bmin[0] + t * (bmax[0] - bmin[0]);
778                        [
779                            [x, bmin[1], bmin[2]],
780                            [x, bmax[1], bmin[2]],
781                            [x, bmax[1], bmax[2]],
782                            [x, bmin[1], bmax[2]],
783                        ]
784                    }
785                    SliceAxis::Y => {
786                        let y = bmin[1] + t * (bmax[1] - bmin[1]);
787                        [
788                            [bmin[0], y, bmin[2]],
789                            [bmax[0], y, bmin[2]],
790                            [bmax[0], y, bmax[2]],
791                            [bmin[0], y, bmax[2]],
792                        ]
793                    }
794                    SliceAxis::Z => {
795                        let z = bmin[2] + t * (bmax[2] - bmin[2]);
796                        [
797                            [bmin[0], bmin[1], z],
798                            [bmax[0], bmin[1], z],
799                            [bmax[0], bmax[1], z],
800                            [bmin[0], bmax[1], z],
801                        ]
802                    }
803                };
804                let sc: Vec<Option<glam::Vec2>> = corners
805                    .iter()
806                    .map(|&c| {
807                        project(view_proj, glam::Vec3::from(c)).map(|(x, y)| glam::Vec2::new(x, y))
808                    })
809                    .collect();
810                let hit = sc.iter().any(|p| p.map_or(false, |p| in_rect(p.x, p.y)))
811                    || (0..4).any(|i| {
812                        let a = sc[i];
813                        let b = sc[(i + 1) % 4];
814                        match (a, b) {
815                            (Some(a), Some(b)) => segment_in_rect(a, b, rect_min, rect_max),
816                            (Some(a), None) => in_rect(a.x, a.y),
817                            (None, Some(b)) => in_rect(b.x, b.y),
818                            (None, None) => false,
819                        }
820                    });
821                if hit {
822                    result.objects.push(item.settings.pick_id.0);
823                }
824            }
825
826            // Volume surface slice: project each mesh vertex (with model transform) and check.
827            for item in &self.pick_volume_surface_slice_items {
828                if item.settings.pick_id == PickId::NONE {
829                    continue;
830                }
831                let Some(mesh) = self.resources.mesh_store.get(item.mesh_id) else {
832                    continue;
833                };
834                let Some(positions) = &mesh.cpu_positions else {
835                    continue;
836                };
837                let model = glam::Mat4::from_cols_array_2d(&item.model);
838                let hit = positions.iter().any(|&p| {
839                    let wp = model.transform_point3(glam::Vec3::from(p));
840                    project(view_proj, wp).map_or(false, |(sx, sy)| in_rect(sx, sy))
841                });
842                if hit {
843                    result.objects.push(item.settings.pick_id.0);
844                }
845            }
846
847            // Screen image: check if the image's screen rect overlaps the pick rect.
848            for item in &self.pick_screen_image_items {
849                if item.settings.pick_id == PickId::NONE || item.width == 0 || item.height == 0 {
850                    continue;
851                }
852                let img_w = item.width as f32 * item.scale;
853                let img_h = item.height as f32 * item.scale;
854                let (sx, sy) = match item.anchor {
855                    ImageAnchor::TopLeft => (0.0, 0.0),
856                    ImageAnchor::TopRight => (viewport_size.x - img_w, 0.0),
857                    ImageAnchor::BottomLeft => (0.0, viewport_size.y - img_h),
858                    ImageAnchor::BottomRight => (viewport_size.x - img_w, viewport_size.y - img_h),
859                    ImageAnchor::Center => (
860                        (viewport_size.x - img_w) * 0.5,
861                        (viewport_size.y - img_h) * 0.5,
862                    ),
863                };
864                // Overlap: image rect [sx, sx+img_w] x [sy, sy+img_h] vs pick rect.
865                let overlap = sx <= rect_max.x
866                    && sx + img_w >= rect_min.x
867                    && sy <= rect_max.y
868                    && sy + img_h >= rect_min.y;
869                if overlap {
870                    result.objects.push(item.settings.pick_id.0);
871                }
872            }
873        }
874
875        // 11. GPU implicit surface rect picks (OBJECT only).
876        //
877        // For each primitive compute a conservative screen-space AABB by projecting
878        // the primitive's bounding sphere. If any projected AABB corner falls inside
879        // the pick rect, the item is a hit. This is approximate (the actual rendered
880        // surface may be smaller) but avoids per-pixel SDF marching for rect queries.
881        if wants_object {
882            for item in &self.pick_implicit_items {
883                let mut hit = false;
884                'prim_loop: for prim in &item.primitives {
885                    // Derive a bounding sphere center and radius for each primitive.
886                    let (center, radius) = match prim.kind {
887                        1 => {
888                            // Sphere
889                            let c = glam::Vec3::new(prim.params[0], prim.params[1], prim.params[2]);
890                            (c, prim.params[3].abs())
891                        }
892                        2 => {
893                            // Box: center + max half-extent as radius
894                            let c = glam::Vec3::new(prim.params[0], prim.params[1], prim.params[2]);
895                            let h = glam::Vec3::new(prim.params[4], prim.params[5], prim.params[6]);
896                            (c, h.length())
897                        }
898                        3 => {
899                            // Plane: not bounded -- skip.
900                            continue;
901                        }
902                        4 => {
903                            // Capsule: midpoint of segment + (half-length + radius)
904                            let a = glam::Vec3::new(prim.params[0], prim.params[1], prim.params[2]);
905                            let b = glam::Vec3::new(prim.params[4], prim.params[5], prim.params[6]);
906                            let r = prim.params[3].abs();
907                            ((a + b) * 0.5, (b - a).length() * 0.5 + r)
908                        }
909                        _ => continue,
910                    };
911                    // Project 8 AABB corners of the bounding sphere box.
912                    for dx in [-radius, radius] {
913                        for dy in [-radius, radius] {
914                            for dz in [-radius, radius] {
915                                let corner = center + glam::Vec3::new(dx, dy, dz);
916                                if let Some((sx, sy)) = project(view_proj, corner) {
917                                    if in_rect(sx, sy) {
918                                        hit = true;
919                                        break 'prim_loop;
920                                    }
921                                }
922                            }
923                        }
924                    }
925                }
926                if hit {
927                    result.objects.push(item.id);
928                }
929            }
930        }
931
932        // 12. GPU marching cubes surface rect picks (OBJECT only).
933        //
934        // Iterates over all cells in the volume where the scalar field straddles
935        // the isovalue (MC would generate triangles there). If any such cell's
936        // center projects into the pick rect, the item is a hit.
937        if wants_object {
938            for item in &self.pick_mc_items {
939                let vol = &item.volume_data;
940                let isovalue = item.isovalue;
941                let [nx, ny, nz] = vol.dims;
942                let origin = glam::Vec3::from(vol.origin);
943                let spacing = glam::Vec3::from(vol.spacing);
944
945                let mut hit = false;
946                'mc_rect: for iz in 0..nz.saturating_sub(1) {
947                    for iy in 0..ny.saturating_sub(1) {
948                        for ix in 0..nx.saturating_sub(1) {
949                            // A cell straddles the isovalue when not all 8 corners
950                            // are on the same side. Check for both above and below.
951                            let mut has_below = false;
952                            let mut has_above = false;
953                            'corners: for dz in 0u32..=1 {
954                                for dy in 0u32..=1 {
955                                    for dx in 0u32..=1 {
956                                        let s = vol.sample(ix + dx, iy + dy, iz + dz);
957                                        if s < isovalue {
958                                            has_below = true;
959                                        } else {
960                                            has_above = true;
961                                        }
962                                        if has_below && has_above {
963                                            break 'corners;
964                                        }
965                                    }
966                                }
967                            }
968                            if !(has_below && has_above) {
969                                continue;
970                            }
971                            let cell_center = origin
972                                + spacing
973                                    * glam::Vec3::new(
974                                        ix as f32 + 0.5,
975                                        iy as f32 + 0.5,
976                                        iz as f32 + 0.5,
977                                    );
978                            if let Some((sx, sy)) = project(view_proj, cell_center) {
979                                if in_rect(sx, sy) {
980                                    hit = true;
981                                    break 'mc_rect;
982                                }
983                            }
984                        }
985                    }
986                }
987                if hit {
988                    result.objects.push(item.id);
989                }
990            }
991        }
992
993        // 13. Decal rect picks (OBJECT only): project the decal projection box
994        // (unit cube [-0.5, 0.5]^3 mapped by `transform`) and test its corners
995        // and edges against the selection rect. Mirrors the ray-versus-box test
996        // used by the single-item pick so box-select and click agree.
997        if wants_object {
998            // Unit-box corners in (x, y, z) bit order, and the 12 edges joining
999            // corners that differ in exactly one axis.
1000            const CORNERS: [[f32; 3]; 8] = [
1001                [-0.5, -0.5, -0.5],
1002                [0.5, -0.5, -0.5],
1003                [-0.5, 0.5, -0.5],
1004                [0.5, 0.5, -0.5],
1005                [-0.5, -0.5, 0.5],
1006                [0.5, -0.5, 0.5],
1007                [-0.5, 0.5, 0.5],
1008                [0.5, 0.5, 0.5],
1009            ];
1010            const EDGES: [(usize, usize); 12] = [
1011                (0, 1),
1012                (0, 2),
1013                (0, 4),
1014                (1, 3),
1015                (1, 5),
1016                (2, 3),
1017                (2, 6),
1018                (3, 7),
1019                (4, 5),
1020                (4, 6),
1021                (5, 7),
1022                (6, 7),
1023            ];
1024            for item in &self.pick_decal_items {
1025                if item.settings.hidden || item.settings.pick_id == PickId::NONE {
1026                    continue;
1027                }
1028                let model = glam::Mat4::from_cols_array_2d(&item.transform);
1029                if model.determinant().abs() < 1e-12 {
1030                    continue;
1031                }
1032                let mvp = view_proj * model;
1033                let sc: [Option<glam::Vec2>; 8] = std::array::from_fn(|i| {
1034                    project(mvp, glam::Vec3::from(CORNERS[i])).map(|(x, y)| glam::Vec2::new(x, y))
1035                });
1036                let hit = sc.iter().any(|p| p.map_or(false, |p| in_rect(p.x, p.y)))
1037                    || EDGES.iter().any(|&(a, b)| match (sc[a], sc[b]) {
1038                        (Some(a), Some(b)) => segment_in_rect(a, b, rect_min, rect_max),
1039                        (Some(a), None) => in_rect(a.x, a.y),
1040                        (None, Some(b)) => in_rect(b.x, b.y),
1041                        (None, None) => false,
1042                    });
1043                if hit {
1044                    result.objects.push(item.settings.pick_id.0);
1045                }
1046            }
1047        }
1048
1049        result
1050    }
1051}