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::pick_mask::PickMask,
29    ) -> PickRectResult {
30        use crate::interaction::pick_mask::PickMask;
31        use crate::interaction::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_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.gaussian_splat_store.get(item.id.0) else {
347                    continue;
348                };
349                if gpu_set.cpu_positions.is_empty() {
350                    continue;
351                }
352                let model = glam::Mat4::from_cols_array_2d(&item.model);
353                let mvp = view_proj * model;
354                let id = item.settings.pick_id.0;
355                let mut item_hit = false;
356
357                for (i, pos) in gpu_set.cpu_positions.iter().enumerate() {
358                    if let Some((sx, sy)) = project(mvp, glam::Vec3::from(*pos)) {
359                        if in_rect(sx, sy) {
360                            if wants_splat {
361                                result.elements.push((id, SubObjectRef::Splat(i as u32)));
362                            }
363                            item_hit = true;
364                        }
365                    }
366                }
367
368                if wants_object && item_hit {
369                    result.objects.push(id);
370                }
371            }
372        }
373
374        // 6. Instance picks (INSTANCE or OBJECT) for glyphs, tensor glyphs, sprites.
375        let wants_instance = mask.intersects(PickMask::INSTANCE);
376        if wants_instance || wants_object {
377            // Glyphs
378            for item in &self.pick_glyph_items {
379                if item.settings.pick_id == PickId::NONE || item.positions.is_empty() {
380                    continue;
381                }
382                let model = glam::Mat4::from_cols_array_2d(&item.model);
383                let mvp = view_proj * model;
384                let id = item.settings.pick_id.0;
385                let mut item_hit = false;
386                for (i, pos) in item.positions.iter().enumerate() {
387                    if let Some((sx, sy)) = project(mvp, glam::Vec3::from(*pos)) {
388                        if in_rect(sx, sy) {
389                            if wants_instance {
390                                result.elements.push((id, SubObjectRef::Instance(i as u32)));
391                            }
392                            item_hit = true;
393                        }
394                    }
395                }
396                if wants_object && item_hit {
397                    result.objects.push(id);
398                }
399            }
400
401            // Tensor glyphs
402            for item in &self.pick_tensor_glyph_items {
403                if item.settings.pick_id == PickId::NONE || item.positions.is_empty() {
404                    continue;
405                }
406                let model = glam::Mat4::from_cols_array_2d(&item.model);
407                let mvp = view_proj * model;
408                let id = item.settings.pick_id.0;
409                let mut item_hit = false;
410                for (i, pos) in item.positions.iter().enumerate() {
411                    if let Some((sx, sy)) = project(mvp, glam::Vec3::from(*pos)) {
412                        if in_rect(sx, sy) {
413                            if wants_instance {
414                                result.elements.push((id, SubObjectRef::Instance(i as u32)));
415                            }
416                            item_hit = true;
417                        }
418                    }
419                }
420                if wants_object && item_hit {
421                    result.objects.push(id);
422                }
423            }
424
425            // Sprites
426            for item in &self.pick_sprite_items {
427                if item.settings.pick_id == PickId::NONE || item.positions.is_empty() {
428                    continue;
429                }
430                let model = glam::Mat4::from_cols_array_2d(&item.model);
431                let mvp = view_proj * model;
432                let id = item.settings.pick_id.0;
433                let mut item_hit = false;
434                for (i, pos) in item.positions.iter().enumerate() {
435                    if let Some((sx, sy)) = project(mvp, glam::Vec3::from(*pos)) {
436                        if in_rect(sx, sy) {
437                            if wants_instance {
438                                result.elements.push((id, SubObjectRef::Instance(i as u32)));
439                            }
440                            item_hit = true;
441                        }
442                    }
443                }
444                if wants_object && item_hit {
445                    result.objects.push(id);
446                }
447            }
448        }
449
450        // 7. Polyline node / segment / strip / object rect picks.
451        let wants_poly_node = mask.intersects(PickMask::POLY_NODE);
452        let wants_segment = mask.intersects(PickMask::SEGMENT);
453        let wants_strip = mask.intersects(PickMask::STRIP);
454        if wants_poly_node || wants_segment || wants_strip || wants_object {
455            for item in &self.pick_polyline_items {
456                if item.settings.pick_id == PickId::NONE || item.positions.is_empty() {
457                    continue;
458                }
459                let id = item.settings.pick_id.0;
460                let mut item_hit = false;
461                let mut strips_hit = std::collections::HashSet::<u32>::new();
462
463                // Node pass (POLY_NODE or STRIP or OBJECT).
464                if wants_poly_node || wants_strip || wants_object {
465                    for (node_idx, pos) in item.positions.iter().enumerate() {
466                        if let Some((sx, sy)) = project(view_proj, glam::Vec3::from(*pos)) {
467                            if in_rect(sx, sy) {
468                                item_hit = true;
469                                if wants_poly_node {
470                                    result
471                                        .elements
472                                        .push((id, SubObjectRef::Point(node_idx as u32)));
473                                } else if wants_strip {
474                                    let s = strip_for_node(node_idx as u32, &item.strip_lengths);
475                                    strips_hit.insert(s);
476                                }
477                            }
478                        }
479                    }
480                }
481
482                // Segment pass (SEGMENT or STRIP or OBJECT) -- full segment/rect intersection.
483                if wants_segment || (wants_strip && !wants_poly_node) || wants_object {
484                    let mut node_off = 0usize;
485                    let mut seg_off = 0u32;
486                    macro_rules! try_seg_rect {
487                        ($ai:expr, $bi:expr, $seg:expr) => {{
488                            if let (Some((sax, say)), Some((sbx, sby))) = (
489                                project(view_proj, glam::Vec3::from(item.positions[$ai])),
490                                project(view_proj, glam::Vec3::from(item.positions[$bi])),
491                            ) {
492                                if segment_in_rect(
493                                    glam::Vec2::new(sax, say),
494                                    glam::Vec2::new(sbx, sby),
495                                    rect_min,
496                                    rect_max,
497                                ) {
498                                    item_hit = true;
499                                    if wants_segment {
500                                        result.elements.push((id, SubObjectRef::Segment($seg)));
501                                    } else if wants_strip {
502                                        let s = strip_for_segment($seg, &item.strip_lengths);
503                                        strips_hit.insert(s);
504                                    }
505                                }
506                            }
507                        }};
508                    }
509                    if item.strip_lengths.is_empty() {
510                        for j in 0..item.positions.len().saturating_sub(1) {
511                            try_seg_rect!(j, j + 1, j as u32);
512                        }
513                    } else {
514                        for &slen in &item.strip_lengths {
515                            let slen = slen as usize;
516                            for j in 0..slen.saturating_sub(1) {
517                                try_seg_rect!(node_off + j, node_off + j + 1, seg_off + j as u32);
518                            }
519                            seg_off += slen.saturating_sub(1) as u32;
520                            node_off += slen;
521                        }
522                    }
523                }
524
525                if wants_strip {
526                    for s in strips_hit {
527                        result.elements.push((id, SubObjectRef::Strip(s)));
528                    }
529                }
530                if wants_object && item_hit {
531                    result.objects.push(id);
532                }
533            }
534        }
535
536        // 8. Streamtube / tube / ribbon segment / strip / object rect picks.
537        if wants_poly_node || wants_segment || wants_strip || wants_object {
538            // Streamtube and tube: test both projected endpoints of each segment
539            // with segment_in_rect instead of the midpoint projection heuristic.
540            // POLY_NODE: also check each control point individually.
541            let st_tube_iter = self
542                .pick_streamtube_items
543                .iter()
544                .map(|it| {
545                    (
546                        it.settings.pick_id.0,
547                        it.positions.as_slice(),
548                        it.strip_lengths.as_slice(),
549                    )
550                })
551                .chain(self.pick_tube_items.iter().map(|it| {
552                    (
553                        it.settings.pick_id.0,
554                        it.positions.as_slice(),
555                        it.strip_lengths.as_slice(),
556                    )
557                }));
558
559            for (id, positions, strip_lengths) in st_tube_iter {
560                if id == 0 || positions.is_empty() {
561                    continue;
562                }
563                let mut item_hit = false;
564                let mut strips_hit = std::collections::HashSet::<u32>::new();
565
566                let single_st;
567                let strips_st: &[u32] = if strip_lengths.is_empty() {
568                    single_st = [positions.len() as u32];
569                    &single_st
570                } else {
571                    strip_lengths
572                };
573
574                // POLY_NODE pass: project each control point and check in_rect.
575                if wants_poly_node || wants_strip || wants_object {
576                    'st_nodes: for (ni, pos) in positions.iter().enumerate() {
577                        if let Some((sx, sy)) = project(view_proj, glam::Vec3::from(*pos)) {
578                            if in_rect(sx, sy) {
579                                item_hit = true;
580                                if wants_poly_node {
581                                    result.elements.push((id, SubObjectRef::Point(ni as u32)));
582                                } else if wants_strip {
583                                    let s = strip_for_node(ni as u32, strip_lengths);
584                                    strips_hit.insert(s);
585                                } else {
586                                    // wants_object only: no need to enumerate further nodes.
587                                    break 'st_nodes;
588                                }
589                            }
590                        }
591                    }
592                }
593
594                // SEGMENT pass: test both projected endpoints of each segment.
595                if wants_segment || wants_strip || wants_object {
596                    let mut node_off = 0usize;
597                    let mut seg_off = 0u32;
598                    'st_strips: for &slen in strips_st {
599                        let slen = slen as usize;
600                        for j in 0..slen.saturating_sub(1) {
601                            let seg_idx = seg_off + j as u32;
602                            let pa = glam::Vec3::from(positions[node_off + j]);
603                            let pb = glam::Vec3::from(positions[node_off + j + 1]);
604                            let hit = match (project(view_proj, pa), project(view_proj, pb)) {
605                                (Some((ax, ay)), Some((bx, by))) => segment_in_rect(
606                                    glam::Vec2::new(ax, ay),
607                                    glam::Vec2::new(bx, by),
608                                    rect_min,
609                                    rect_max,
610                                ),
611                                (Some((ax, ay)), None) => in_rect(ax, ay),
612                                (None, Some((bx, by))) => in_rect(bx, by),
613                                (None, None) => false,
614                            };
615                            if hit {
616                                item_hit = true;
617                                if wants_segment {
618                                    result.elements.push((id, SubObjectRef::Segment(seg_idx)));
619                                } else if wants_strip {
620                                    let s = strip_for_segment(seg_idx, strip_lengths);
621                                    strips_hit.insert(s);
622                                } else {
623                                    // wants_object only: no need to enumerate further segments.
624                                    break 'st_strips;
625                                }
626                            }
627                        }
628                        seg_off += slen.saturating_sub(1) as u32;
629                        node_off += slen;
630                    }
631                }
632
633                if wants_strip {
634                    for s in strips_hit {
635                        result.elements.push((id, SubObjectRef::Strip(s)));
636                    }
637                }
638                if wants_object && item_hit {
639                    result.objects.push(id);
640                }
641            }
642
643            // Ribbon: reconstruct the swept quad per segment and test all four
644            // quad edges with segment_in_rect (also catches quad corners inside
645            // the rect via the endpoint check inside segment_in_rect).
646            // POLY_NODE: also check each control point individually.
647            for item in &self.pick_ribbon_items {
648                if item.settings.pick_id == PickId::NONE || item.positions.is_empty() {
649                    continue;
650                }
651
652                let single_r;
653                let strips_r: &[u32] = if item.strip_lengths.is_empty() {
654                    single_r = [item.positions.len() as u32];
655                    &single_r
656                } else {
657                    &item.strip_lengths
658                };
659
660                let mut item_hit = false;
661                let mut strips_hit = std::collections::HashSet::<u32>::new();
662
663                // Project a world point to screen Vec2; returns None if behind camera.
664                let proj2 = |p: glam::Vec3| -> Option<glam::Vec2> {
665                    project(view_proj, p).map(|(x, y)| glam::Vec2::new(x, y))
666                };
667
668                // POLY_NODE pass: project each control point and check in_rect.
669                if wants_poly_node || wants_strip || wants_object {
670                    'rb_nodes: for (ni, pos) in item.positions.iter().enumerate() {
671                        if let Some((sx, sy)) = project(view_proj, glam::Vec3::from(*pos)) {
672                            if in_rect(sx, sy) {
673                                item_hit = true;
674                                if wants_poly_node {
675                                    result.elements.push((
676                                        item.settings.pick_id.0,
677                                        SubObjectRef::Point(ni as u32),
678                                    ));
679                                } else if wants_strip {
680                                    let s = strip_for_node(ni as u32, &item.strip_lengths);
681                                    strips_hit.insert(s);
682                                } else {
683                                    break 'rb_nodes;
684                                }
685                            }
686                        }
687                    }
688                }
689
690                // SEGMENT pass: quad edge tests using ribbon_lateral_frames.
691                if wants_segment || wants_strip || wants_object {
692                    let frames = ribbon_lateral_frames(
693                        &item.positions,
694                        &item.strip_lengths,
695                        item.width,
696                        item.width_attribute.as_deref(),
697                        item.twist_attribute.as_deref(),
698                    );
699                    let mut node_off = 0usize;
700                    let mut seg_off = 0u32;
701
702                    'rb_strips: for &slen in strips_r {
703                        let slen = slen as usize;
704                        for k in 0..slen.saturating_sub(1) {
705                            let seg_idx = seg_off + k as u32;
706                            let ia = node_off + k;
707                            let ib = node_off + k + 1;
708                            let pa = glam::Vec3::from(item.positions[ia]);
709                            let pb = glam::Vec3::from(item.positions[ib]);
710                            let (ua, wa) = frames[ia];
711                            let (ub, wb) = frames[ib];
712                            let c0 = pa + ua * wa; // left  at a
713                            let c1 = pa - ua * wa; // right at a
714                            let c2 = pb + ub * wb; // left  at b
715                            let c3 = pb - ub * wb; // right at b
716                            let sc0 = proj2(c0);
717                            let sc1 = proj2(c1);
718                            let sc2 = proj2(c2);
719                            let sc3 = proj2(c3);
720                            let edge_hit = |a: Option<glam::Vec2>, b: Option<glam::Vec2>| -> bool {
721                                match (a, b) {
722                                    (Some(a), Some(b)) => segment_in_rect(a, b, rect_min, rect_max),
723                                    (Some(a), None) => in_rect(a.x, a.y),
724                                    (None, Some(b)) => in_rect(b.x, b.y),
725                                    (None, None) => false,
726                                }
727                            };
728                            let hit = edge_hit(sc0, sc1)
729                                || edge_hit(sc2, sc3)
730                                || edge_hit(sc0, sc2)
731                                || edge_hit(sc1, sc3);
732                            if hit {
733                                item_hit = true;
734                                if wants_segment {
735                                    result.elements.push((
736                                        item.settings.pick_id.0,
737                                        SubObjectRef::Segment(seg_idx),
738                                    ));
739                                } else if wants_strip {
740                                    let s = strip_for_segment(seg_idx, &item.strip_lengths);
741                                    strips_hit.insert(s);
742                                } else {
743                                    break 'rb_strips;
744                                }
745                            }
746                        }
747                        seg_off += slen.saturating_sub(1) as u32;
748                        node_off += slen;
749                    }
750                }
751
752                if wants_strip {
753                    for s in strips_hit {
754                        result
755                            .elements
756                            .push((item.settings.pick_id.0, SubObjectRef::Strip(s)));
757                    }
758                }
759                if wants_object && item_hit {
760                    result.objects.push(item.settings.pick_id.0);
761                }
762            }
763        }
764
765        // 9. Image slice / volume surface slice / screen image object rect picks (OBJECT only).
766        if wants_object {
767            // Image slice: project all 4 quad corners and check containment/edge intersection.
768            for item in &self.pick_image_slice_items {
769                if item.settings.pick_id == PickId::NONE {
770                    continue;
771                }
772                let [bmin, bmax] = [item.bbox_min, item.bbox_max];
773                let t = item.offset;
774                let corners: [[f32; 3]; 4] = match item.axis {
775                    SliceAxis::X => {
776                        let x = bmin[0] + t * (bmax[0] - bmin[0]);
777                        [
778                            [x, bmin[1], bmin[2]],
779                            [x, bmax[1], bmin[2]],
780                            [x, bmax[1], bmax[2]],
781                            [x, bmin[1], bmax[2]],
782                        ]
783                    }
784                    SliceAxis::Y => {
785                        let y = bmin[1] + t * (bmax[1] - bmin[1]);
786                        [
787                            [bmin[0], y, bmin[2]],
788                            [bmax[0], y, bmin[2]],
789                            [bmax[0], y, bmax[2]],
790                            [bmin[0], y, bmax[2]],
791                        ]
792                    }
793                    SliceAxis::Z => {
794                        let z = bmin[2] + t * (bmax[2] - bmin[2]);
795                        [
796                            [bmin[0], bmin[1], z],
797                            [bmax[0], bmin[1], z],
798                            [bmax[0], bmax[1], z],
799                            [bmin[0], bmax[1], z],
800                        ]
801                    }
802                };
803                let sc: Vec<Option<glam::Vec2>> = corners
804                    .iter()
805                    .map(|&c| {
806                        project(view_proj, glam::Vec3::from(c)).map(|(x, y)| glam::Vec2::new(x, y))
807                    })
808                    .collect();
809                let hit = sc.iter().any(|p| p.map_or(false, |p| in_rect(p.x, p.y)))
810                    || (0..4).any(|i| {
811                        let a = sc[i];
812                        let b = sc[(i + 1) % 4];
813                        match (a, b) {
814                            (Some(a), Some(b)) => segment_in_rect(a, b, rect_min, rect_max),
815                            (Some(a), None) => in_rect(a.x, a.y),
816                            (None, Some(b)) => in_rect(b.x, b.y),
817                            (None, None) => false,
818                        }
819                    });
820                if hit {
821                    result.objects.push(item.settings.pick_id.0);
822                }
823            }
824
825            // Volume surface slice: project each mesh vertex (with model transform) and check.
826            for item in &self.pick_volume_surface_slice_items {
827                if item.settings.pick_id == PickId::NONE {
828                    continue;
829                }
830                let Some(mesh) = self.resources.mesh_store.get(item.mesh_id) else {
831                    continue;
832                };
833                let Some(positions) = &mesh.cpu_positions else {
834                    continue;
835                };
836                let model = glam::Mat4::from_cols_array_2d(&item.model);
837                let hit = positions.iter().any(|&p| {
838                    let wp = model.transform_point3(glam::Vec3::from(p));
839                    project(view_proj, wp).map_or(false, |(sx, sy)| in_rect(sx, sy))
840                });
841                if hit {
842                    result.objects.push(item.settings.pick_id.0);
843                }
844            }
845
846            // Screen image: check if the image's screen rect overlaps the pick rect.
847            for item in &self.pick_screen_image_items {
848                if item.settings.pick_id == PickId::NONE || item.width == 0 || item.height == 0 {
849                    continue;
850                }
851                let img_w = item.width as f32 * item.scale;
852                let img_h = item.height as f32 * item.scale;
853                let (sx, sy) = match item.anchor {
854                    ImageAnchor::TopLeft => (0.0, 0.0),
855                    ImageAnchor::TopRight => (viewport_size.x - img_w, 0.0),
856                    ImageAnchor::BottomLeft => (0.0, viewport_size.y - img_h),
857                    ImageAnchor::BottomRight => (viewport_size.x - img_w, viewport_size.y - img_h),
858                    ImageAnchor::Center => (
859                        (viewport_size.x - img_w) * 0.5,
860                        (viewport_size.y - img_h) * 0.5,
861                    ),
862                };
863                // Overlap: image rect [sx, sx+img_w] x [sy, sy+img_h] vs pick rect.
864                let overlap = sx <= rect_max.x
865                    && sx + img_w >= rect_min.x
866                    && sy <= rect_max.y
867                    && sy + img_h >= rect_min.y;
868                if overlap {
869                    result.objects.push(item.settings.pick_id.0);
870                }
871            }
872        }
873
874        // 11. GPU implicit surface rect picks (OBJECT only).
875        //
876        // For each primitive compute a conservative screen-space AABB by projecting
877        // the primitive's bounding sphere. If any projected AABB corner falls inside
878        // the pick rect, the item is a hit. This is approximate (the actual rendered
879        // surface may be smaller) but avoids per-pixel SDF marching for rect queries.
880        if wants_object {
881            for item in &self.pick_implicit_items {
882                let mut hit = false;
883                'prim_loop: for prim in &item.primitives {
884                    // Derive a bounding sphere center and radius for each primitive.
885                    let (center, radius) = match prim.kind {
886                        1 => {
887                            // Sphere
888                            let c = glam::Vec3::new(prim.params[0], prim.params[1], prim.params[2]);
889                            (c, prim.params[3].abs())
890                        }
891                        2 => {
892                            // Box: center + max half-extent as radius
893                            let c = glam::Vec3::new(prim.params[0], prim.params[1], prim.params[2]);
894                            let h = glam::Vec3::new(prim.params[4], prim.params[5], prim.params[6]);
895                            (c, h.length())
896                        }
897                        3 => {
898                            // Plane: not bounded -- skip.
899                            continue;
900                        }
901                        4 => {
902                            // Capsule: midpoint of segment + (half-length + radius)
903                            let a = glam::Vec3::new(prim.params[0], prim.params[1], prim.params[2]);
904                            let b = glam::Vec3::new(prim.params[4], prim.params[5], prim.params[6]);
905                            let r = prim.params[3].abs();
906                            ((a + b) * 0.5, (b - a).length() * 0.5 + r)
907                        }
908                        _ => continue,
909                    };
910                    // Project 8 AABB corners of the bounding sphere box.
911                    for dx in [-radius, radius] {
912                        for dy in [-radius, radius] {
913                            for dz in [-radius, radius] {
914                                let corner = center + glam::Vec3::new(dx, dy, dz);
915                                if let Some((sx, sy)) = project(view_proj, corner) {
916                                    if in_rect(sx, sy) {
917                                        hit = true;
918                                        break 'prim_loop;
919                                    }
920                                }
921                            }
922                        }
923                    }
924                }
925                if hit {
926                    result.objects.push(item.id);
927                }
928            }
929        }
930
931        // 12. GPU marching cubes surface rect picks (OBJECT only).
932        //
933        // Iterates over all cells in the volume where the scalar field straddles
934        // the isovalue (MC would generate triangles there). If any such cell's
935        // center projects into the pick rect, the item is a hit.
936        if wants_object {
937            for item in &self.pick_mc_items {
938                let vol = &item.volume_data;
939                let isovalue = item.isovalue;
940                let [nx, ny, nz] = vol.dims;
941                let origin = glam::Vec3::from(vol.origin);
942                let spacing = glam::Vec3::from(vol.spacing);
943
944                let mut hit = false;
945                'mc_rect: for iz in 0..nz.saturating_sub(1) {
946                    for iy in 0..ny.saturating_sub(1) {
947                        for ix in 0..nx.saturating_sub(1) {
948                            // A cell straddles the isovalue when not all 8 corners
949                            // are on the same side. Check for both above and below.
950                            let mut has_below = false;
951                            let mut has_above = false;
952                            'corners: for dz in 0u32..=1 {
953                                for dy in 0u32..=1 {
954                                    for dx in 0u32..=1 {
955                                        let s = vol.sample(ix + dx, iy + dy, iz + dz);
956                                        if s < isovalue {
957                                            has_below = true;
958                                        } else {
959                                            has_above = true;
960                                        }
961                                        if has_below && has_above {
962                                            break 'corners;
963                                        }
964                                    }
965                                }
966                            }
967                            if !(has_below && has_above) {
968                                continue;
969                            }
970                            let cell_center = origin
971                                + spacing
972                                    * glam::Vec3::new(
973                                        ix as f32 + 0.5,
974                                        iy as f32 + 0.5,
975                                        iz as f32 + 0.5,
976                                    );
977                            if let Some((sx, sy)) = project(view_proj, cell_center) {
978                                if in_rect(sx, sy) {
979                                    hit = true;
980                                    break 'mc_rect;
981                                }
982                            }
983                        }
984                    }
985                }
986                if hit {
987                    result.objects.push(item.id);
988                }
989            }
990        }
991
992        result
993    }
994}