1use super::*;
2
3fn strip_for_node(node_idx: u32, strip_lengths: &[u32]) -> u32 {
9 let mut offset = 0u32;
10 for (i, &len) in strip_lengths.iter().enumerate() {
11 offset += len;
12 if node_idx < offset {
13 return i as u32;
14 }
15 }
16 strip_lengths.len().saturating_sub(1) as u32
17}
18
19fn pick_closest_polyline_segment(
26 click_pos: glam::Vec2,
27 viewport_size: glam::Vec2,
28 view_proj: glam::Mat4,
29 positions: &[[f32; 3]],
30 strip_lengths: &[u32],
31 threshold_px: f32,
32) -> Option<(u32, glam::Vec3)> {
33 let project = |p: [f32; 3]| -> Option<glam::Vec2> {
34 let clip = view_proj * glam::Vec4::new(p[0], p[1], p[2], 1.0);
35 if clip.w <= 0.0 {
36 return None;
37 }
38 Some(glam::Vec2::new(
39 (clip.x / clip.w + 1.0) * 0.5 * viewport_size.x,
40 (1.0 - clip.y / clip.w) * 0.5 * viewport_size.y,
41 ))
42 };
43
44 let mut best_dist = threshold_px;
45 let mut best: Option<(u32, glam::Vec3)> = None;
46
47 macro_rules! try_seg {
48 ($ai:expr, $bi:expr, $seg:expr) => {{
49 if let (Some(sa), Some(sb)) = (project(positions[$ai]), project(positions[$bi])) {
50 let ab = sb - sa;
51 let len_sq = ab.length_squared();
52 let t = if len_sq < 1e-6 {
53 0.0f32
54 } else {
55 ((click_pos - sa).dot(ab) / len_sq).clamp(0.0, 1.0)
56 };
57 let dist = (click_pos - (sa + ab * t)).length();
58 if dist < best_dist {
59 best_dist = dist;
60 let wa = glam::Vec3::from(positions[$ai]);
61 let wb = glam::Vec3::from(positions[$bi]);
62 best = Some(($seg as u32, wa.lerp(wb, t)));
63 }
64 }
65 }};
66 }
67
68 if strip_lengths.is_empty() {
69 for j in 0..positions.len().saturating_sub(1) {
70 try_seg!(j, j + 1, j);
71 }
72 } else {
73 let mut node_off = 0usize;
74 let mut seg_off = 0u32;
75 for &slen in strip_lengths {
76 let slen = slen as usize;
77 for j in 0..slen.saturating_sub(1) {
78 try_seg!(node_off + j, node_off + j + 1, seg_off + j as u32);
79 }
80 seg_off += slen.saturating_sub(1) as u32;
81 node_off += slen;
82 }
83 }
84
85 best
86}
87
88fn segment_in_rect(
90 a: glam::Vec2,
91 b: glam::Vec2,
92 rect_min: glam::Vec2,
93 rect_max: glam::Vec2,
94) -> bool {
95 if a.x.min(b.x) > rect_max.x
97 || a.x.max(b.x) < rect_min.x
98 || a.y.min(b.y) > rect_max.y
99 || a.y.max(b.y) < rect_min.y
100 {
101 return false;
102 }
103 let in_r = |p: glam::Vec2| {
105 p.x >= rect_min.x && p.x <= rect_max.x && p.y >= rect_min.y && p.y <= rect_max.y
106 };
107 if in_r(a) || in_r(b) {
108 return true;
109 }
110 let crosses = |p0: glam::Vec2, p1: glam::Vec2, q0: glam::Vec2, q1: glam::Vec2| -> bool {
112 let d = p1 - p0;
113 let e = q1 - q0;
114 let denom = d.x * e.y - d.y * e.x;
115 if denom.abs() < 1e-10 {
116 return false;
117 }
118 let diff = q0 - p0;
119 let t = (diff.x * e.y - diff.y * e.x) / denom;
120 let u = (diff.x * d.y - diff.y * d.x) / denom;
121 t >= 0.0 && t <= 1.0 && u >= 0.0 && u <= 1.0
122 };
123 let tl = rect_min;
124 let tr = glam::Vec2::new(rect_max.x, rect_min.y);
125 let bl = glam::Vec2::new(rect_min.x, rect_max.y);
126 let br = rect_max;
127 crosses(a, b, tl, tr) || crosses(a, b, tr, br) || crosses(a, b, br, bl) || crosses(a, b, bl, tl)
128}
129
130fn strip_for_segment(seg_idx: u32, strip_lengths: &[u32]) -> u32 {
132 let mut offset = 0u32;
133 for (i, &len) in strip_lengths.iter().enumerate() {
134 let segs = len.saturating_sub(1);
135 offset += segs;
136 if seg_idx < offset {
137 return i as u32;
138 }
139 }
140 strip_lengths.len().saturating_sub(1) as u32
141}
142
143#[inline]
148fn ray_triangle(
149 ray_orig: glam::Vec3,
150 ray_dir: glam::Vec3,
151 v0: glam::Vec3,
152 v1: glam::Vec3,
153 v2: glam::Vec3,
154) -> Option<f32> {
155 let e1 = v1 - v0;
156 let e2 = v2 - v0;
157 let h = ray_dir.cross(e2);
158 let a = e1.dot(h);
159 if a.abs() < 1e-10 {
160 return None;
161 }
162 let f = 1.0 / a;
163 let s = ray_orig - v0;
164 let u = f * s.dot(h);
165 if u < 0.0 || u > 1.0 {
166 return None;
167 }
168 let q = s.cross(e1);
169 let v = f * ray_dir.dot(q);
170 if v < 0.0 || u + v > 1.0 {
171 return None;
172 }
173 let t = f * e2.dot(q);
174 if t > 0.0 { Some(t) } else { None }
175}
176
177fn ribbon_lateral_frames(
183 positions: &[[f32; 3]],
184 strip_lengths: &[u32],
185 width: f32,
186 width_attribute: Option<&[f32]>,
187 twist_attribute: Option<&[[f32; 3]]>,
188) -> Vec<(glam::Vec3, f32)> {
189 let n = positions.len();
190 let mut frames: Vec<(glam::Vec3, f32)> = vec![(glam::Vec3::X, 0.0); n];
192
193 let single;
194 let strips: &[u32] = if strip_lengths.is_empty() {
195 single = [positions.len() as u32];
196 &single
197 } else {
198 strip_lengths
199 };
200
201 let mut node_off = 0usize;
202 for &slen in strips {
203 let slen = slen as usize;
204 if slen < 2 {
205 node_off += slen;
206 continue;
207 }
208
209 let pts: Vec<glam::Vec3> = positions[node_off..node_off + slen]
210 .iter()
211 .map(|&p| glam::Vec3::from(p))
212 .collect();
213
214 let t0 = (pts[1] - pts[0]).normalize_or_zero();
215 if t0.length_squared() < 1e-10 {
216 node_off += slen;
217 continue;
218 }
219 let ref_v = if t0.x.abs() < 0.9 {
220 glam::Vec3::X
221 } else {
222 glam::Vec3::Y
223 };
224 let mut u = t0.cross(ref_v).normalize();
225
226 for k in 0..slen {
227 let tangent = if k + 1 < slen {
228 (pts[k + 1] - pts[k]).normalize_or_zero()
229 } else {
230 (pts[k] - pts[k - 1]).normalize_or_zero()
231 };
232
233 if k > 0 {
235 let t_prev = (pts[k] - pts[k - 1]).normalize_or_zero();
236 let axis = t_prev.cross(tangent);
237 let sin_a = axis.length().min(1.0);
238 if sin_a > 1e-6 {
239 let cos_a = t_prev.dot(tangent).clamp(-1.0, 1.0);
240 let ax = axis / sin_a;
241 u = u * cos_a + ax.cross(u) * sin_a + ax * ax.dot(u) * (1.0 - cos_a);
242 u = u.normalize_or_zero();
243 }
244 }
245
246 let mut lateral = u;
248 if let Some(twist) = twist_attribute {
249 if let Some(&tv) = twist.get(node_off + k) {
250 let tv = glam::Vec3::from(tv);
251 let proj = tv - tangent * tangent.dot(tv);
252 if proj.length_squared() > 1e-10 {
253 lateral = proj.normalize();
254 }
255 }
256 }
257
258 let half_w = width_attribute
259 .and_then(|wa| wa.get(node_off + k).copied())
260 .unwrap_or(width)
261 * 0.5;
262
263 frames[node_off + k] = (lateral, half_w);
264 }
265
266 node_off += slen;
267 }
268
269 frames
270}
271
272fn eval_implicit_primitive(p: glam::Vec3, prim: &crate::resources::ImplicitPrimitive) -> f32 {
278 match prim.kind {
279 1 => {
280 let center = glam::Vec3::new(prim.params[0], prim.params[1], prim.params[2]);
282 (p - center).length() - prim.params[3]
283 }
284 2 => {
285 let center = glam::Vec3::new(prim.params[0], prim.params[1], prim.params[2]);
287 let half = glam::Vec3::new(prim.params[4], prim.params[5], prim.params[6]);
288 let q = (p - center).abs() - half;
289 q.max(glam::Vec3::ZERO).length() + q.x.max(q.y).max(q.z).min(0.0)
290 }
291 3 => {
292 let n =
294 glam::Vec3::new(prim.params[0], prim.params[1], prim.params[2]).normalize_or_zero();
295 p.dot(n) + prim.params[3]
296 }
297 4 => {
298 let a = glam::Vec3::new(prim.params[0], prim.params[1], prim.params[2]);
300 let r = prim.params[3];
301 let b = glam::Vec3::new(prim.params[4], prim.params[5], prim.params[6]);
302 let pa = p - a;
303 let ba = b - a;
304 let h = (pa.dot(ba) / ba.dot(ba).max(1e-10)).clamp(0.0, 1.0);
305 (pa - ba * h).length() - r
306 }
307 _ => f32::MAX,
308 }
309}
310
311#[inline]
313fn smin_implicit(a: f32, b: f32, k: f32) -> f32 {
314 let h = (0.5 + 0.5 * (b - a) / k).clamp(0.0, 1.0);
315 a * h + b * (1.0 - h) - k * h * (1.0 - h)
316}
317
318fn eval_implicit_sdf(p: glam::Vec3, item: &GpuImplicitPickItem) -> f32 {
320 use crate::resources::ImplicitBlendMode;
321 let mut d = item.max_distance;
322 for (i, prim) in item.primitives.iter().enumerate() {
323 let pd = eval_implicit_primitive(p, prim);
324 match item.blend_mode {
325 ImplicitBlendMode::Union => {
326 d = d.min(pd);
327 }
328 ImplicitBlendMode::SmoothUnion => {
329 let k = if prim.blend > 0.0 { prim.blend } else { 1e-5 };
330 d = smin_implicit(d, pd, k);
331 }
332 ImplicitBlendMode::Intersection => {
333 if i == 0 {
334 d = pd;
335 } else {
336 d = d.max(pd);
337 }
338 }
339 }
340 }
341 d
342}
343
344fn pick_implicit_sdf(
346 ray_origin: glam::Vec3,
347 ray_dir: glam::Vec3,
348 item: &GpuImplicitPickItem,
349) -> Option<(f32, glam::Vec3)> {
350 let max_steps = item.max_steps.min(512) as usize;
351 let scale = item.step_scale.clamp(0.01, 1.0);
352 let hit_thr = item.hit_threshold;
353 let max_dist = item.max_distance;
354 let min_step = hit_thr * 0.5;
355
356 let mut t = 0.0f32;
357 for _ in 0..max_steps {
358 if t > max_dist {
359 break;
360 }
361 let p = ray_origin + ray_dir * t;
362 let d = eval_implicit_sdf(p, item);
363 if d < hit_thr {
364 return Some((t, p));
365 }
366 t += d.abs().max(min_step) * scale;
367 }
368 None
369}
370
371fn ray_aabb_slab(
377 ray_orig: glam::Vec3,
378 ray_dir: glam::Vec3,
379 bbox_min: glam::Vec3,
380 bbox_max: glam::Vec3,
381) -> Option<(f32, f32)> {
382 let inv = glam::Vec3::new(
384 if ray_dir.x.abs() > 1e-30 {
385 1.0 / ray_dir.x
386 } else {
387 f32::INFINITY * ray_dir.x.signum()
388 },
389 if ray_dir.y.abs() > 1e-30 {
390 1.0 / ray_dir.y
391 } else {
392 f32::INFINITY * ray_dir.y.signum()
393 },
394 if ray_dir.z.abs() > 1e-30 {
395 1.0 / ray_dir.z
396 } else {
397 f32::INFINITY * ray_dir.z.signum()
398 },
399 );
400 let t1 = (bbox_min - ray_orig) * inv;
401 let t2 = (bbox_max - ray_orig) * inv;
402 let tmin = t1.min(t2);
403 let tmax = t1.max(t2);
404 let t_enter = tmin.x.max(tmin.y).max(tmin.z);
405 let t_exit = tmax.x.min(tmax.y).min(tmax.z);
406 if t_enter <= t_exit && t_exit >= 0.0 {
407 Some((t_enter, t_exit))
408 } else {
409 None
410 }
411}
412
413fn bisect_mc_crossing(
415 ray_orig: glam::Vec3,
416 ray_dir: glam::Vec3,
417 vol: &crate::geometry::marching_cubes::VolumeData,
418 isovalue: f32,
419 mut t_lo: f32,
420 mut t_hi: f32,
421) -> f32 {
422 let s0 = crate::geometry::marching_cubes::trilinear_sample(
423 vol,
424 (ray_orig + ray_dir * t_lo).to_array(),
425 ) - isovalue;
426 let mut lo_sign = s0 < 0.0;
427 for _ in 0..8 {
428 let mid = (t_lo + t_hi) * 0.5;
429 let s = crate::geometry::marching_cubes::trilinear_sample(
430 vol,
431 (ray_orig + ray_dir * mid).to_array(),
432 ) - isovalue;
433 if (s < 0.0) == lo_sign {
434 t_lo = mid;
435 } else {
436 t_hi = mid;
437 lo_sign = !lo_sign;
438 }
439 }
440 (t_lo + t_hi) * 0.5
441}
442
443fn pick_mc_volume(
448 ray_orig: glam::Vec3,
449 ray_dir: glam::Vec3,
450 item: &GpuMcPickItem,
451) -> Option<(f32, glam::Vec3)> {
452 use crate::geometry::marching_cubes::trilinear_sample;
453
454 let vol = &item.volume_data;
455 let isovalue = item.isovalue;
456 let [nx, ny, nz] = vol.dims;
457 let origin = glam::Vec3::from(vol.origin);
458 let spacing = glam::Vec3::from(vol.spacing);
459 let extent = spacing * glam::Vec3::new(nx as f32, ny as f32, nz as f32);
460
461 let (t_enter, t_exit) = ray_aabb_slab(ray_orig, ray_dir, origin, origin + extent)?;
462 let t_start = t_enter.max(0.0);
463 if t_start >= t_exit {
464 return None;
465 }
466
467 let step = spacing.min_element() * 0.5;
469 let mut t = t_start;
470 let mut prev = trilinear_sample(vol, (ray_orig + ray_dir * t).to_array()) - isovalue;
471
472 loop {
473 t += step;
474 if t > t_exit {
475 break;
476 }
477 let p = ray_orig + ray_dir * t;
478 let cur = trilinear_sample(vol, p.to_array()) - isovalue;
479 if prev * cur <= 0.0 {
480 let t_hit = bisect_mc_crossing(ray_orig, ray_dir, vol, isovalue, t - step, t);
482 let world_pos = ray_orig + ray_dir * t_hit;
483 return Some((t_hit, world_pos));
484 }
485 prev = cur;
486 }
487 None
488}
489
490#[derive(Clone, Debug, Default)]
496pub struct PickRectResult {
497 pub objects: Vec<u64>,
501 pub elements: Vec<(u64, crate::interaction::sub_object::SubObjectRef)>,
507}
508
509impl PickRectResult {
510 pub fn is_empty(&self) -> bool {
512 self.objects.is_empty() && self.elements.is_empty()
513 }
514}
515
516impl ViewportRenderer {
517 pub fn pick(
541 &self,
542 click_pos: glam::Vec2,
543 viewport_size: glam::Vec2,
544 view_proj: glam::Mat4,
545 mask: crate::interaction::pick_mask::PickMask,
546 ) -> Option<crate::interaction::picking::PickHit> {
547 use crate::interaction::pick_mask::PickMask;
548 use crate::interaction::picking::{
549 PickHit, pick_gaussian_splat_cpu, pick_point_cloud_cpu,
550 pick_transparent_volume_mesh_cpu, pick_volume_cpu, screen_to_ray,
551 };
552 use crate::interaction::sub_object::SubObjectRef;
553 use parry3d::math::{Pose, Vector};
554 use parry3d::query::{Ray, RayCast};
555
556 if viewport_size.x <= 0.0 || viewport_size.y <= 0.0 {
557 return None;
558 }
559
560 let view_proj_inv = view_proj.inverse();
561 let (ray_origin, ray_dir) = screen_to_ray(click_pos, viewport_size, view_proj_inv);
562
563 let wants_face = mask.intersects(PickMask::FACE);
564 let wants_vertex = mask.intersects(PickMask::VERTEX);
565 let wants_cell = mask.intersects(PickMask::CELL);
566 let wants_cloud = mask.intersects(PickMask::CLOUD_POINT);
567 let wants_splat = mask.intersects(PickMask::SPLAT);
568 let wants_object = mask.intersects(PickMask::OBJECT);
569 let wants_mesh_sub = wants_face || wants_vertex || mask.intersects(PickMask::EDGE);
570
571 let mut best: Option<(f32, PickHit)> = None;
573
574 let mut consider = |toi: f32, hit: PickHit| {
575 if best.as_ref().map_or(true, |(bt, _)| toi < *bt) {
576 best = Some((toi, hit));
577 }
578 };
579
580 let vm_cell_map: std::collections::HashMap<u64, &[u32]> = self
583 .pick_volume_mesh_items
584 .iter()
585 .filter(|item| item.settings.pick_id != PickId::NONE && !item.face_to_cell.is_empty())
586 .map(|item| (item.settings.pick_id.0, item.face_to_cell.as_slice()))
587 .collect();
588
589 if wants_mesh_sub || wants_cell || wants_object {
591 let ray = Ray::new(
592 Vector::new(ray_origin.x, ray_origin.y, ray_origin.z),
593 Vector::new(ray_dir.x, ray_dir.y, ray_dir.z),
594 );
595 for item in &self.pick_scene_items {
596 if item.settings.hidden || item.settings.pick_id == PickId::NONE {
597 continue;
598 }
599 let Some(mesh) = self.resources.mesh_store.get(item.mesh_id) else {
600 continue;
601 };
602 let (Some(positions), Some(indices)) = (&mesh.cpu_positions, &mesh.cpu_indices)
603 else {
604 continue;
605 };
606
607 let model = glam::Mat4::from_cols_array_2d(&item.model);
608
609 let verts: Vec<Vector> = positions
612 .iter()
613 .map(|p| {
614 let wp = model.transform_point3(glam::Vec3::from(*p));
615 Vector::new(wp.x, wp.y, wp.z)
616 })
617 .collect();
618
619 let tri_indices: Vec<[u32; 3]> = indices
620 .chunks(3)
621 .filter(|c| c.len() == 3)
622 .map(|c| [c[0], c[1], c[2]])
623 .collect();
624
625 if tri_indices.is_empty() {
626 continue;
627 }
628
629 match parry3d::shape::TriMesh::new(verts, tri_indices) {
630 Ok(trimesh) => {
631 let identity = Pose::identity();
633 let Some(intersection) =
634 trimesh.cast_ray_and_get_normal(&identity, &ray, f32::MAX, true)
635 else {
636 continue;
637 };
638 let toi = intersection.time_of_impact;
639 let world_pos = ray_origin + ray_dir * toi;
640 let normal = intersection.normal;
641
642 let feature_sub = SubObjectRef::from_feature_id(intersection.feature);
643
644 let sub_object = if wants_face {
645 feature_sub
646 } else if wants_cell {
647 if let Some(f2c) = vm_cell_map.get(&item.settings.pick_id.0) {
649 match feature_sub {
650 Some(SubObjectRef::Face(face_raw)) => {
651 let n_tri = indices.len() / 3;
652 let face = if (face_raw as usize) >= n_tri {
653 face_raw as usize - n_tri
654 } else {
655 face_raw as usize
656 };
657 f2c.get(face).map(|&ci| SubObjectRef::Cell(ci))
658 }
659 other => other,
660 }
661 } else if wants_vertex {
662 match feature_sub {
666 Some(SubObjectRef::Face(face_raw)) => {
667 let n_tri = indices.len() / 3;
668 let face = if (face_raw as usize) >= n_tri {
669 face_raw as usize - n_tri
670 } else {
671 face_raw as usize
672 };
673 if face * 3 + 2 < indices.len() {
674 let vis = [
675 indices[face * 3] as usize,
676 indices[face * 3 + 1] as usize,
677 indices[face * 3 + 2] as usize,
678 ];
679 let (best_vi, _) = vis
680 .iter()
681 .map(|&i| {
682 let p = model.transform_point3(
683 glam::Vec3::from(positions[i]),
684 );
685 (i, p.distance(world_pos))
686 })
687 .fold((vis[0], f32::MAX), |acc, (i, d)| {
688 if d < acc.1 { (i, d) } else { acc }
689 });
690 Some(SubObjectRef::Vertex(best_vi as u32))
691 } else {
692 None
693 }
694 }
695 other => other,
696 }
697 } else {
698 None
700 }
701 } else if wants_vertex {
702 match feature_sub {
704 Some(SubObjectRef::Face(face_raw)) => {
705 let n_tri = indices.len() / 3;
706 let face = if (face_raw as usize) >= n_tri {
707 face_raw as usize - n_tri
708 } else {
709 face_raw as usize
710 };
711 if face * 3 + 2 < indices.len() {
712 let vis = [
713 indices[face * 3] as usize,
714 indices[face * 3 + 1] as usize,
715 indices[face * 3 + 2] as usize,
716 ];
717 let (best_vi, _) = vis
718 .iter()
719 .map(|&i| {
720 let p = model.transform_point3(glam::Vec3::from(
721 positions[i],
722 ));
723 (i, p.distance(world_pos))
724 })
725 .fold((vis[0], f32::MAX), |acc, (i, d)| {
726 if d < acc.1 { (i, d) } else { acc }
727 });
728 Some(SubObjectRef::Vertex(best_vi as u32))
729 } else {
730 None
731 }
732 }
733 other => other,
734 }
735 } else {
736 None
738 };
739
740 if sub_object.is_some() || wants_object {
746 #[allow(deprecated)]
747 let hit = PickHit {
748 id: item.settings.pick_id.0,
749 sub_object,
750 world_pos,
751 normal,
752 triangle_index: u32::MAX,
753 point_index: None,
754 scalar_value: None,
755 };
756 consider(toi, hit);
757 }
758 }
759 Err(e) => {
760 tracing::warn!(
761 pick_id = item.settings.pick_id.0,
762 error = %e,
763 "TriMesh build failed in renderer.pick()"
764 );
765 }
766 }
767 }
768 }
769
770 if wants_object {
776 for item in &self.pick_scatter_volume_items {
777 if item.settings.hidden || item.settings.pick_id == PickId::NONE {
778 continue;
779 }
780 if let Some((t_enter, _)) = crate::scene::scatter_volume::ray_intersect(
781 &item.volume.shape,
782 ray_origin,
783 ray_dir,
784 ) {
785 let world_pos = ray_origin + ray_dir * t_enter;
786 let normal = (world_pos - match item.volume.shape {
787 crate::scene::scatter_volume::ScatterShape::Box(b) => {
788 (b.min + b.max) * 0.5
789 }
790 crate::scene::scatter_volume::ScatterShape::Sphere { center, .. } => {
791 glam::Vec3::from(center)
792 }
793 })
794 .try_normalize()
795 .unwrap_or(glam::Vec3::Z);
796 consider(
797 t_enter,
798 PickHit::object_hit(item.settings.pick_id.0, world_pos, normal),
799 );
800 }
801 }
802 }
803
804 if wants_cell || wants_object {
806 for item in &self.pick_tvm_items {
807 if item.settings.pick_id == PickId::NONE {
808 continue;
809 }
810 let Some(data) = item.volume_mesh_data.as_deref() else {
811 continue;
812 };
813 if let Some(mut hit) = pick_transparent_volume_mesh_cpu(
814 ray_origin,
815 ray_dir,
816 item.settings.pick_id.0,
817 glam::Mat4::IDENTITY,
818 data,
819 ) {
820 let toi = (hit.world_pos - ray_origin).dot(ray_dir).max(0.0);
821 if !wants_cell {
822 hit.sub_object = None;
823 }
824 consider(toi, hit);
825 }
826 }
827 }
828
829 if wants_cloud || wants_object {
831 for item in &self.pick_point_cloud_items {
832 if item.settings.pick_id == PickId::NONE || item.positions.is_empty() {
833 continue;
834 }
835 let radius_px = item.point_size.max(4.0);
836 if let Some(mut hit) = pick_point_cloud_cpu(
837 click_pos,
838 item.settings.pick_id.0,
839 item,
840 view_proj,
841 viewport_size,
842 radius_px,
843 ) {
844 let toi = (hit.world_pos - ray_origin).dot(ray_dir).max(0.0);
845 if !wants_cloud {
846 hit.sub_object = None;
847 }
848 consider(toi, hit);
849 }
850 }
851 }
852
853 let wants_voxel = mask.intersects(PickMask::VOXEL);
855 if wants_voxel || wants_object {
856 for item in &self.pick_volume_items {
857 if item.settings.pick_id == PickId::NONE {
858 continue;
859 }
860 let Some(vol_data) = item.volume_data.as_deref() else {
861 continue;
862 };
863 if let Some(mut hit) =
864 pick_volume_cpu(ray_origin, ray_dir, item.settings.pick_id.0, item, vol_data)
865 {
866 let toi = (hit.world_pos - ray_origin).dot(ray_dir).max(0.0);
867 if !wants_voxel {
868 hit.sub_object = None;
869 }
870 consider(toi, hit);
871 }
872 }
873 }
874
875 if wants_splat || wants_object {
877 for item in &self.pick_splat_items {
878 if item.settings.pick_id == PickId::NONE {
879 continue;
880 }
881 let Some(gpu_set) = self.resources.gaussian_splat_store.get(item.id.0) else {
882 continue;
883 };
884 if gpu_set.cpu_positions.is_empty() {
885 continue;
886 }
887 let model = glam::Mat4::from_cols_array_2d(&item.model);
888 let mean_max_scale: f32 = if gpu_set.cpu_scales.is_empty() {
891 0.05
892 } else {
893 gpu_set
894 .cpu_scales
895 .iter()
896 .map(|s| s[0].max(s[1]).max(s[2]))
897 .sum::<f32>()
898 / gpu_set.cpu_scales.len() as f32
899 };
900 let world_radius = mean_max_scale * 3.0;
901 let center_w = model.transform_point3(glam::Vec3::ZERO);
902 let p0_clip = view_proj * center_w.extend(1.0);
903 let p1_clip = view_proj * (center_w + glam::Vec3::X * world_radius).extend(1.0);
904 let radius_px = if p0_clip.w.abs() > 1e-6 && p1_clip.w.abs() > 1e-6 {
905 let p0_ndc = glam::Vec2::new(p0_clip.x, p0_clip.y) / p0_clip.w;
906 let p1_ndc = glam::Vec2::new(p1_clip.x, p1_clip.y) / p1_clip.w;
907 ((p1_ndc - p0_ndc).length() * 0.5 * viewport_size.x.max(viewport_size.y))
908 .max(4.0)
909 } else {
910 world_radius * 100.0
911 };
912 if let Some(mut hit) = pick_gaussian_splat_cpu(
913 click_pos,
914 item.settings.pick_id.0,
915 &gpu_set.cpu_positions,
916 model,
917 view_proj,
918 viewport_size,
919 radius_px,
920 ) {
921 let toi = (hit.world_pos - ray_origin).dot(ray_dir).max(0.0);
923 if wants_splat {
924 if let Some(SubObjectRef::Point(idx)) = hit.sub_object {
925 hit.sub_object = Some(SubObjectRef::Splat(idx));
926 }
927 } else {
928 hit.sub_object = None;
929 }
930 consider(toi, hit);
931 }
932 }
933 }
934
935 let wants_instance = mask.intersects(PickMask::INSTANCE);
937 if wants_instance || wants_object {
938 let instance_radius_px = |world_center: glam::Vec3, world_r: f32| -> f32 {
942 let p0 = view_proj * world_center.extend(1.0);
943 let p1 = view_proj * (world_center + glam::Vec3::X * world_r).extend(1.0);
944 if p0.w.abs() > 1e-6 && p1.w.abs() > 1e-6 {
945 let n0 = glam::Vec2::new(p0.x, p0.y) / p0.w;
946 let n1 = glam::Vec2::new(p1.x, p1.y) / p1.w;
947 ((n1 - n0).length() * 0.5 * viewport_size.x.max(viewport_size.y)).max(4.0)
948 } else {
949 (world_r * 100.0_f32).max(4.0)
950 }
951 };
952
953 for item in &self.pick_glyph_items {
955 if item.settings.pick_id == PickId::NONE || item.positions.is_empty() {
956 continue;
957 }
958 let model = glam::Mat4::from_cols_array_2d(&item.model);
959 let full_len = if item.scale_by_magnitude && !item.vectors.is_empty() {
960 let mean_mag = item
961 .vectors
962 .iter()
963 .map(|v| glam::Vec3::from(*v).length())
964 .sum::<f32>()
965 / item.vectors.len() as f32;
966 (mean_mag * item.scale).max(0.01)
967 } else {
968 item.scale.max(0.01)
969 };
970 let has_vecs = item.vectors.len() == item.positions.len();
974 let midpoints: Vec<[f32; 3]> = item
975 .positions
976 .iter()
977 .enumerate()
978 .map(|(i, pos)| {
979 if has_vecs {
980 let p = glam::Vec3::from(*pos);
981 let v = glam::Vec3::from(item.vectors[i]);
982 let len = if item.scale_by_magnitude {
983 v.length() * item.scale
984 } else {
985 item.scale
986 };
987 (p + v.normalize_or_zero() * len * 0.5).to_array()
988 } else {
989 *pos
990 }
991 })
992 .collect();
993 let n = midpoints.len() as f32;
994 let centroid = model.transform_point3(
995 midpoints
996 .iter()
997 .map(|p| glam::Vec3::from(*p))
998 .sum::<glam::Vec3>()
999 / n,
1000 );
1001 let radius_px = instance_radius_px(centroid, full_len * 0.5);
1002 if let Some(mut hit) = pick_gaussian_splat_cpu(
1003 click_pos,
1004 item.settings.pick_id.0,
1005 &midpoints,
1006 model,
1007 view_proj,
1008 viewport_size,
1009 radius_px,
1010 ) {
1011 if let Some(SubObjectRef::Point(idx)) = hit.sub_object {
1013 if let Some(base) = item.positions.get(idx as usize) {
1014 hit.world_pos = model.transform_point3(glam::Vec3::from(*base));
1015 }
1016 if wants_instance {
1017 hit.sub_object = Some(SubObjectRef::Instance(idx));
1018 } else {
1019 hit.sub_object = None;
1020 }
1021 }
1022 let toi = (hit.world_pos - ray_origin).dot(ray_dir).max(0.0);
1023 consider(toi, hit);
1024 }
1025 }
1026
1027 for item in &self.pick_tensor_glyph_items {
1029 if item.settings.pick_id == PickId::NONE || item.positions.is_empty() {
1030 continue;
1031 }
1032 let model = glam::Mat4::from_cols_array_2d(&item.model);
1033 let world_r = if !item.eigenvalues.is_empty() {
1037 let max_ev = item
1038 .eigenvalues
1039 .iter()
1040 .map(|ev| ev[0].abs().max(ev[1].abs()).max(ev[2].abs()))
1041 .fold(0.0_f32, f32::max);
1042 (max_ev * item.scale).max(0.01)
1043 } else {
1044 item.scale.max(0.01)
1045 };
1046 let n = item.positions.len() as f32;
1047 let centroid = model.transform_point3(
1048 item.positions
1049 .iter()
1050 .map(|p| glam::Vec3::from(*p))
1051 .sum::<glam::Vec3>()
1052 / n,
1053 );
1054 let radius_px = instance_radius_px(centroid, world_r);
1055 if let Some(mut hit) = pick_gaussian_splat_cpu(
1056 click_pos,
1057 item.settings.pick_id.0,
1058 &item.positions,
1059 model,
1060 view_proj,
1061 viewport_size,
1062 radius_px,
1063 ) {
1064 let toi = (hit.world_pos - ray_origin).dot(ray_dir).max(0.0);
1065 if wants_instance {
1066 if let Some(SubObjectRef::Point(idx)) = hit.sub_object {
1067 hit.sub_object = Some(SubObjectRef::Instance(idx));
1068 }
1069 } else {
1070 hit.sub_object = None;
1071 }
1072 consider(toi, hit);
1073 }
1074 }
1075
1076 for item in &self.pick_sprite_items {
1078 if item.settings.pick_id == PickId::NONE || item.positions.is_empty() {
1079 continue;
1080 }
1081 let model = glam::Mat4::from_cols_array_2d(&item.model);
1082 let radius_px = match item.size_mode {
1083 SpriteSizeMode::ScreenSpace => (item.default_size * 0.5).max(4.0),
1084 SpriteSizeMode::WorldSpace => {
1085 let n = item.positions.len() as f32;
1086 let centroid = model.transform_point3(
1087 item.positions
1088 .iter()
1089 .map(|p| glam::Vec3::from(*p))
1090 .sum::<glam::Vec3>()
1091 / n,
1092 );
1093 instance_radius_px(centroid, (item.default_size * 0.5).max(0.01))
1094 }
1095 };
1096 if let Some(mut hit) = pick_gaussian_splat_cpu(
1097 click_pos,
1098 item.settings.pick_id.0,
1099 &item.positions,
1100 model,
1101 view_proj,
1102 viewport_size,
1103 radius_px,
1104 ) {
1105 let toi = (hit.world_pos - ray_origin).dot(ray_dir).max(0.0);
1106 if wants_instance {
1107 if let Some(SubObjectRef::Point(idx)) = hit.sub_object {
1108 hit.sub_object = Some(SubObjectRef::Instance(idx));
1109 }
1110 } else {
1111 hit.sub_object = None;
1112 }
1113 consider(toi, hit);
1114 }
1115 }
1116 }
1117
1118 let wants_poly_node = mask.intersects(PickMask::POLY_NODE);
1120 let wants_strip = mask.intersects(PickMask::STRIP);
1121 if wants_poly_node || wants_strip || wants_object {
1122 for item in &self.pick_polyline_items {
1123 if item.settings.pick_id == PickId::NONE || item.positions.is_empty() {
1124 continue;
1125 }
1126 let radius_px = (item.line_width + 4.0).max(8.0);
1127 if let Some(mut hit) = pick_gaussian_splat_cpu(
1128 click_pos,
1129 item.settings.pick_id.0,
1130 &item.positions,
1131 glam::Mat4::IDENTITY,
1132 view_proj,
1133 viewport_size,
1134 radius_px,
1135 ) {
1136 let toi = (hit.world_pos - ray_origin).dot(ray_dir).max(0.0);
1137 if wants_poly_node {
1138 } else if wants_strip {
1140 if let Some(SubObjectRef::Point(idx)) = hit.sub_object {
1141 hit.sub_object = Some(SubObjectRef::Strip(strip_for_node(
1142 idx,
1143 &item.strip_lengths,
1144 )));
1145 }
1146 } else {
1147 hit.sub_object = None;
1148 }
1149 consider(toi, hit);
1150 }
1151 }
1152 }
1153
1154 let wants_segment = mask.intersects(PickMask::SEGMENT);
1158 if wants_segment || wants_strip || wants_object {
1159 for item in &self.pick_polyline_items {
1160 if item.settings.pick_id == PickId::NONE || item.positions.is_empty() {
1161 continue;
1162 }
1163 let threshold_px = (item.line_width / 2.0 + 4.0).max(4.0);
1165 let Some((seg_idx, world_pos)) = pick_closest_polyline_segment(
1166 click_pos,
1167 viewport_size,
1168 view_proj,
1169 &item.positions,
1170 &item.strip_lengths,
1171 threshold_px,
1172 ) else {
1173 continue;
1174 };
1175 let toi = (world_pos - ray_origin).dot(ray_dir).max(0.0);
1176 let sub_object = if wants_segment {
1177 Some(SubObjectRef::Segment(seg_idx))
1178 } else if wants_strip {
1179 Some(SubObjectRef::Strip(strip_for_segment(
1180 seg_idx,
1181 &item.strip_lengths,
1182 )))
1183 } else {
1184 None
1185 };
1186 #[allow(deprecated)]
1187 let hit = PickHit {
1188 id: item.settings.pick_id.0,
1189 sub_object,
1190 world_pos,
1191 normal: glam::Vec3::Y,
1192 triangle_index: u32::MAX,
1193 point_index: None,
1194 scalar_value: None,
1195 };
1196 consider(toi, hit);
1197 }
1198 }
1199
1200 if wants_poly_node || wants_segment || wants_strip || wants_object {
1207 let world_r_to_px = |ref_world: glam::Vec3, world_r: f32| -> f32 {
1209 let p0 = view_proj * ref_world.extend(1.0);
1210 let p1 = view_proj * (ref_world + glam::Vec3::X * world_r).extend(1.0);
1211 if p0.w.abs() > 1e-6 && p1.w.abs() > 1e-6 {
1212 let n0 = glam::Vec2::new(p0.x, p0.y) / p0.w;
1213 let n1 = glam::Vec2::new(p1.x, p1.y) / p1.w;
1214 ((n1 - n0).length() * 0.5 * viewport_size.x.max(viewport_size.y)).max(4.0)
1215 } else {
1216 (world_r * 100.0_f32).max(4.0)
1217 }
1218 };
1219
1220 if wants_poly_node || wants_strip || wants_object {
1222 for item in &self.pick_streamtube_items {
1223 if item.settings.pick_id == PickId::NONE || item.positions.is_empty() {
1224 continue;
1225 }
1226 let ref_pos = glam::Vec3::from(item.positions[0]);
1227 let radius_px = world_r_to_px(ref_pos, item.radius.max(0.01)).max(8.0);
1228 if let Some(mut hit) = pick_gaussian_splat_cpu(
1229 click_pos,
1230 item.settings.pick_id.0,
1231 &item.positions,
1232 glam::Mat4::IDENTITY,
1233 view_proj,
1234 viewport_size,
1235 radius_px,
1236 ) {
1237 let toi = (hit.world_pos - ray_origin).dot(ray_dir).max(0.0);
1238 if wants_poly_node {
1239 } else if wants_strip {
1241 if let Some(SubObjectRef::Point(idx)) = hit.sub_object {
1242 hit.sub_object = Some(SubObjectRef::Strip(strip_for_node(
1243 idx,
1244 &item.strip_lengths,
1245 )));
1246 }
1247 } else {
1248 hit.sub_object = None;
1249 }
1250 consider(toi, hit);
1251 }
1252 }
1253 for item in &self.pick_tube_items {
1254 if item.settings.pick_id == PickId::NONE || item.positions.is_empty() {
1255 continue;
1256 }
1257 let ref_pos = glam::Vec3::from(item.positions[0]);
1258 let max_r = item
1259 .radius_attribute
1260 .as_ref()
1261 .and_then(|ra| ra.iter().copied().reduce(f32::max))
1262 .unwrap_or(0.0)
1263 .max(item.radius)
1264 .max(0.01);
1265 let radius_px = world_r_to_px(ref_pos, max_r).max(8.0);
1266 if let Some(mut hit) = pick_gaussian_splat_cpu(
1267 click_pos,
1268 item.settings.pick_id.0,
1269 &item.positions,
1270 glam::Mat4::IDENTITY,
1271 view_proj,
1272 viewport_size,
1273 radius_px,
1274 ) {
1275 let toi = (hit.world_pos - ray_origin).dot(ray_dir).max(0.0);
1276 if wants_poly_node {
1277 } else if wants_strip {
1279 if let Some(SubObjectRef::Point(idx)) = hit.sub_object {
1280 hit.sub_object = Some(SubObjectRef::Strip(strip_for_node(
1281 idx,
1282 &item.strip_lengths,
1283 )));
1284 }
1285 } else {
1286 hit.sub_object = None;
1287 }
1288 consider(toi, hit);
1289 }
1290 }
1291 for item in &self.pick_ribbon_items {
1292 if item.settings.pick_id == PickId::NONE || item.positions.is_empty() {
1293 continue;
1294 }
1295 let ref_pos = glam::Vec3::from(item.positions[0]);
1296 let radius_px = world_r_to_px(ref_pos, item.width * 0.5).max(8.0);
1297 if let Some(mut hit) = pick_gaussian_splat_cpu(
1298 click_pos,
1299 item.settings.pick_id.0,
1300 &item.positions,
1301 glam::Mat4::IDENTITY,
1302 view_proj,
1303 viewport_size,
1304 radius_px,
1305 ) {
1306 let toi = (hit.world_pos - ray_origin).dot(ray_dir).max(0.0);
1307 if wants_poly_node {
1308 } else if wants_strip {
1310 if let Some(SubObjectRef::Point(idx)) = hit.sub_object {
1311 hit.sub_object = Some(SubObjectRef::Strip(strip_for_node(
1312 idx,
1313 &item.strip_lengths,
1314 )));
1315 }
1316 } else {
1317 hit.sub_object = None;
1318 }
1319 consider(toi, hit);
1320 }
1321 }
1322 }
1323
1324 if wants_segment || wants_strip || wants_object {
1326 for item in &self.pick_streamtube_items {
1329 if item.settings.pick_id == PickId::NONE || item.positions.is_empty() {
1330 continue;
1331 }
1332 let ref_pos = glam::Vec3::from(item.positions[0]);
1333 let threshold_px = world_r_to_px(ref_pos, item.radius.max(0.01));
1334 let Some((seg_idx, world_pos)) = pick_closest_polyline_segment(
1335 click_pos,
1336 viewport_size,
1337 view_proj,
1338 &item.positions,
1339 &item.strip_lengths,
1340 threshold_px,
1341 ) else {
1342 continue;
1343 };
1344 let toi = (world_pos - ray_origin).dot(ray_dir).max(0.0);
1345 let sub_object = if wants_segment {
1346 Some(SubObjectRef::Segment(seg_idx))
1347 } else if wants_strip {
1348 Some(SubObjectRef::Strip(strip_for_segment(
1349 seg_idx,
1350 &item.strip_lengths,
1351 )))
1352 } else {
1353 None
1354 };
1355 #[allow(deprecated)]
1356 consider(
1357 toi,
1358 PickHit {
1359 id: item.settings.pick_id.0,
1360 sub_object,
1361 world_pos,
1362 normal: glam::Vec3::Y,
1363 triangle_index: u32::MAX,
1364 point_index: None,
1365 scalar_value: None,
1366 },
1367 );
1368 }
1369
1370 for item in &self.pick_tube_items {
1373 if item.settings.pick_id == PickId::NONE || item.positions.is_empty() {
1374 continue;
1375 }
1376 let ref_pos = glam::Vec3::from(item.positions[0]);
1377 let max_r = item
1378 .radius_attribute
1379 .as_ref()
1380 .and_then(|ra| ra.iter().copied().reduce(f32::max))
1381 .unwrap_or(0.0)
1382 .max(item.radius)
1383 .max(0.01);
1384 let threshold_px = world_r_to_px(ref_pos, max_r);
1385 let Some((seg_idx, world_pos)) = pick_closest_polyline_segment(
1386 click_pos,
1387 viewport_size,
1388 view_proj,
1389 &item.positions,
1390 &item.strip_lengths,
1391 threshold_px,
1392 ) else {
1393 continue;
1394 };
1395 let toi = (world_pos - ray_origin).dot(ray_dir).max(0.0);
1396 let sub_object = if wants_segment {
1397 Some(SubObjectRef::Segment(seg_idx))
1398 } else if wants_strip {
1399 Some(SubObjectRef::Strip(strip_for_segment(
1400 seg_idx,
1401 &item.strip_lengths,
1402 )))
1403 } else {
1404 None
1405 };
1406 #[allow(deprecated)]
1407 consider(
1408 toi,
1409 PickHit {
1410 id: item.settings.pick_id.0,
1411 sub_object,
1412 world_pos,
1413 normal: glam::Vec3::Y,
1414 triangle_index: u32::MAX,
1415 point_index: None,
1416 scalar_value: None,
1417 },
1418 );
1419 }
1420
1421 for item in &self.pick_ribbon_items {
1424 if item.settings.pick_id == PickId::NONE || item.positions.is_empty() {
1425 continue;
1426 }
1427 let frames = ribbon_lateral_frames(
1428 &item.positions,
1429 &item.strip_lengths,
1430 item.width,
1431 item.width_attribute.as_deref(),
1432 item.twist_attribute.as_deref(),
1433 );
1434
1435 let single;
1436 let strips: &[u32] = if item.strip_lengths.is_empty() {
1437 single = [item.positions.len() as u32];
1438 &single
1439 } else {
1440 &item.strip_lengths
1441 };
1442
1443 let mut best_t = f32::MAX;
1444 let mut best_seg: Option<(u32, glam::Vec3)> = None;
1445 let mut node_off = 0usize;
1446 let mut seg_off = 0u32;
1447
1448 for &slen in strips {
1449 let slen = slen as usize;
1450 for k in 0..slen.saturating_sub(1) {
1451 let ia = node_off + k;
1452 let ib = node_off + k + 1;
1453 let pa = glam::Vec3::from(item.positions[ia]);
1454 let pb = glam::Vec3::from(item.positions[ib]);
1455 let (ua, wa) = frames[ia];
1456 let (ub, wb) = frames[ib];
1457 let c0 = pa + ua * wa; let c1 = pa - ua * wa; let c2 = pb + ub * wb; let c3 = pb - ub * wb; let t = ray_triangle(ray_origin, ray_dir, c0, c1, c2)
1464 .or_else(|| ray_triangle(ray_origin, ray_dir, c1, c3, c2))
1465 .or_else(|| ray_triangle(ray_origin, ray_dir, c2, c1, c0))
1466 .or_else(|| ray_triangle(ray_origin, ray_dir, c2, c3, c1));
1467 if let Some(t) = t {
1468 if t < best_t {
1469 best_t = t;
1470 best_seg = Some((seg_off + k as u32, ray_origin + ray_dir * t));
1471 }
1472 }
1473 }
1474 seg_off += slen.saturating_sub(1) as u32;
1475 node_off += slen;
1476 }
1477
1478 if let Some((seg_idx, world_pos)) = best_seg {
1479 let sub_object = if wants_segment {
1480 Some(SubObjectRef::Segment(seg_idx))
1481 } else if wants_strip {
1482 Some(SubObjectRef::Strip(strip_for_segment(
1483 seg_idx,
1484 &item.strip_lengths,
1485 )))
1486 } else {
1487 None
1488 };
1489 #[allow(deprecated)]
1490 consider(
1491 best_t,
1492 PickHit {
1493 id: item.settings.pick_id.0,
1494 sub_object,
1495 world_pos,
1496 normal: glam::Vec3::Y,
1497 triangle_index: u32::MAX,
1498 point_index: None,
1499 scalar_value: None,
1500 },
1501 );
1502 }
1503 }
1504 }
1505 }
1506
1507 if wants_object {
1509 for item in &self.pick_image_slice_items {
1511 if item.settings.pick_id == PickId::NONE {
1512 continue;
1513 }
1514 let [bmin, bmax] = [item.bbox_min, item.bbox_max];
1515 let t = item.offset;
1516 let (axis_idx, plane_pos) = match item.axis {
1518 SliceAxis::X => (0usize, bmin[0] + t * (bmax[0] - bmin[0])),
1519 SliceAxis::Y => (1usize, bmin[1] + t * (bmax[1] - bmin[1])),
1520 SliceAxis::Z => (2usize, bmin[2] + t * (bmax[2] - bmin[2])),
1521 };
1522 let plane_n = {
1523 let mut n = glam::Vec3::ZERO;
1524 n[axis_idx] = 1.0;
1525 n
1526 };
1527 let denom = plane_n.dot(ray_dir);
1528 if denom.abs() < 1e-6 {
1529 continue;
1530 }
1531 let toi = (plane_pos - ray_origin[axis_idx]) / denom;
1532 if toi <= 0.0 {
1533 continue;
1534 }
1535 let hit_pos = ray_origin + ray_dir * toi;
1536 let in_bounds = (0..3)
1538 .filter(|&i| i != axis_idx)
1539 .all(|i| hit_pos[i] >= bmin[i] - 1e-4 && hit_pos[i] <= bmax[i] + 1e-4);
1540 if in_bounds {
1541 #[allow(deprecated)]
1542 consider(
1543 toi,
1544 PickHit {
1545 id: item.settings.pick_id.0,
1546 sub_object: None,
1547 world_pos: hit_pos,
1548 normal: plane_n,
1549 triangle_index: u32::MAX,
1550 point_index: None,
1551 scalar_value: None,
1552 },
1553 );
1554 }
1555 }
1556
1557 for item in &self.pick_volume_surface_slice_items {
1559 if item.settings.pick_id == PickId::NONE {
1560 continue;
1561 }
1562 let Some(mesh) = self.resources.mesh_store.get(item.mesh_id) else {
1563 continue;
1564 };
1565 let (Some(positions), Some(indices)) = (&mesh.cpu_positions, &mesh.cpu_indices)
1566 else {
1567 continue;
1568 };
1569 let model = glam::Mat4::from_cols_array_2d(&item.model);
1570 let verts: Vec<parry3d::math::Vector> = positions
1571 .iter()
1572 .map(|p| {
1573 let wp = model.transform_point3(glam::Vec3::from(*p));
1574 parry3d::math::Vector::new(wp.x, wp.y, wp.z)
1575 })
1576 .collect();
1577 let tri_indices: Vec<[u32; 3]> = indices
1578 .chunks(3)
1579 .filter(|c| c.len() == 3)
1580 .map(|c| [c[0], c[1], c[2]])
1581 .collect();
1582 if tri_indices.is_empty() {
1583 continue;
1584 }
1585 let ray = parry3d::query::Ray::new(
1586 parry3d::math::Vector::new(ray_origin.x, ray_origin.y, ray_origin.z),
1587 parry3d::math::Vector::new(ray_dir.x, ray_dir.y, ray_dir.z),
1588 );
1589 if let Ok(trimesh) = parry3d::shape::TriMesh::new(verts, tri_indices) {
1590 use parry3d::query::RayCast;
1591 if let Some(hit) = trimesh.cast_ray_and_get_normal(
1592 &parry3d::math::Pose::identity(),
1593 &ray,
1594 f32::MAX,
1595 true,
1596 ) {
1597 let world_pos = ray_origin + ray_dir * hit.time_of_impact;
1598 let n = hit.normal;
1599 #[allow(deprecated)]
1600 consider(
1601 hit.time_of_impact,
1602 PickHit {
1603 id: item.settings.pick_id.0,
1604 sub_object: None,
1605 world_pos,
1606 normal: glam::Vec3::new(n.x, n.y, n.z),
1607 triangle_index: u32::MAX,
1608 point_index: None,
1609 scalar_value: None,
1610 },
1611 );
1612 }
1613 }
1614 }
1615
1616 for item in &self.pick_screen_image_items {
1618 if item.settings.pick_id == PickId::NONE || item.width == 0 || item.height == 0 {
1619 continue;
1620 }
1621 let img_w = item.width as f32 * item.scale;
1622 let img_h = item.height as f32 * item.scale;
1623 let (sx, sy) = match item.anchor {
1624 ImageAnchor::TopLeft => (0.0, 0.0),
1625 ImageAnchor::TopRight => (viewport_size.x - img_w, 0.0),
1626 ImageAnchor::BottomLeft => (0.0, viewport_size.y - img_h),
1627 ImageAnchor::BottomRight => (viewport_size.x - img_w, viewport_size.y - img_h),
1628 ImageAnchor::Center => (
1629 (viewport_size.x - img_w) * 0.5,
1630 (viewport_size.y - img_h) * 0.5,
1631 ),
1632 };
1633 if click_pos.x >= sx
1634 && click_pos.x <= sx + img_w
1635 && click_pos.y >= sy
1636 && click_pos.y <= sy + img_h
1637 {
1638 let world_pos = ray_origin + ray_dir * 0.001;
1640 #[allow(deprecated)]
1641 consider(
1642 0.0,
1643 PickHit {
1644 id: item.settings.pick_id.0,
1645 sub_object: None,
1646 world_pos,
1647 normal: -ray_dir,
1648 triangle_index: u32::MAX,
1649 point_index: None,
1650 scalar_value: None,
1651 },
1652 );
1653 }
1654 }
1655 }
1656
1657 if wants_object {
1659 for item in &self.pick_implicit_items {
1660 if let Some((toi, world_pos)) = pick_implicit_sdf(ray_origin, ray_dir, item) {
1661 #[allow(deprecated)]
1662 consider(
1663 toi,
1664 PickHit {
1665 id: item.id,
1666 sub_object: None,
1667 world_pos,
1668 normal: glam::Vec3::Y,
1669 triangle_index: u32::MAX,
1670 point_index: None,
1671 scalar_value: None,
1672 },
1673 );
1674 }
1675 }
1676 }
1677
1678 if wants_object {
1680 for item in &self.pick_mc_items {
1681 if let Some((toi, world_pos)) = pick_mc_volume(ray_origin, ray_dir, item) {
1682 #[allow(deprecated)]
1683 consider(
1684 toi,
1685 PickHit {
1686 id: item.id,
1687 sub_object: None,
1688 world_pos,
1689 normal: glam::Vec3::Y,
1690 triangle_index: u32::MAX,
1691 point_index: None,
1692 scalar_value: None,
1693 },
1694 );
1695 }
1696 }
1697 }
1698
1699 best.map(|(_, hit)| hit)
1700 }
1701
1702 pub fn pick_rect(
1718 &self,
1719 rect_min: glam::Vec2,
1720 rect_max: glam::Vec2,
1721 viewport_size: glam::Vec2,
1722 view_proj: glam::Mat4,
1723 mask: crate::interaction::pick_mask::PickMask,
1724 ) -> PickRectResult {
1725 use crate::interaction::pick_mask::PickMask;
1726 use crate::interaction::sub_object::SubObjectRef;
1727
1728 let mut result = PickRectResult::default();
1729
1730 if viewport_size.x <= 0.0 || viewport_size.y <= 0.0 {
1731 return result;
1732 }
1733
1734 let wants_face = mask.intersects(PickMask::FACE);
1735 let wants_vertex = mask.intersects(PickMask::VERTEX);
1736 let wants_cell = mask.intersects(PickMask::CELL);
1737 let wants_cloud = mask.intersects(PickMask::CLOUD_POINT);
1738 let wants_splat = mask.intersects(PickMask::SPLAT);
1739 let wants_object = mask.intersects(PickMask::OBJECT);
1740
1741 let vm_cell_map: std::collections::HashMap<u64, &[u32]> = self
1743 .pick_volume_mesh_items
1744 .iter()
1745 .filter(|item| item.settings.pick_id != PickId::NONE && !item.face_to_cell.is_empty())
1746 .map(|item| (item.settings.pick_id.0, item.face_to_cell.as_slice()))
1747 .collect();
1748
1749 let project = |mvp: glam::Mat4, local: glam::Vec3| -> Option<(f32, f32)> {
1752 let clip = mvp * local.extend(1.0);
1753 if clip.w <= 0.0 {
1754 return None;
1755 }
1756 let sx = (clip.x / clip.w + 1.0) * 0.5 * viewport_size.x;
1757 let sy = (1.0 - clip.y / clip.w) * 0.5 * viewport_size.y;
1758 Some((sx, sy))
1759 };
1760
1761 let in_rect = |sx: f32, sy: f32| -> bool {
1762 sx >= rect_min.x && sx <= rect_max.x && sy >= rect_min.y && sy <= rect_max.y
1763 };
1764
1765 if wants_face || wants_vertex || wants_cell || wants_object {
1767 for item in &self.pick_scene_items {
1768 if item.settings.hidden || item.settings.pick_id == PickId::NONE {
1769 continue;
1770 }
1771 let Some(mesh) = self.resources.mesh_store.get(item.mesh_id) else {
1772 continue;
1773 };
1774 let (Some(positions), Some(indices)) = (&mesh.cpu_positions, &mesh.cpu_indices)
1775 else {
1776 continue;
1777 };
1778
1779 let model = glam::Mat4::from_cols_array_2d(&item.model);
1780 let mvp = view_proj * model;
1781 let id = item.settings.pick_id.0;
1782 let mut item_hit = false;
1783
1784 if wants_face {
1785 for (tri_idx, chunk) in indices.chunks(3).enumerate() {
1786 if chunk.len() < 3 {
1787 continue;
1788 }
1789 let [i0, i1, i2] =
1790 [chunk[0] as usize, chunk[1] as usize, chunk[2] as usize];
1791 if i0 >= positions.len() || i1 >= positions.len() || i2 >= positions.len() {
1792 continue;
1793 }
1794 let centroid = (glam::Vec3::from(positions[i0])
1795 + glam::Vec3::from(positions[i1])
1796 + glam::Vec3::from(positions[i2]))
1797 / 3.0;
1798 if let Some((sx, sy)) = project(mvp, centroid) {
1799 if in_rect(sx, sy) {
1800 result
1801 .elements
1802 .push((id, SubObjectRef::Face(tri_idx as u32)));
1803 item_hit = true;
1804 }
1805 }
1806 }
1807 } else if wants_cell {
1808 if let Some(f2c) = vm_cell_map.get(&id) {
1810 let mut seen = std::collections::HashSet::new();
1811 for (tri_idx, chunk) in indices.chunks(3).enumerate() {
1812 if chunk.len() < 3 {
1813 continue;
1814 }
1815 let [i0, i1, i2] =
1816 [chunk[0] as usize, chunk[1] as usize, chunk[2] as usize];
1817 if i0 >= positions.len()
1818 || i1 >= positions.len()
1819 || i2 >= positions.len()
1820 {
1821 continue;
1822 }
1823 let centroid = (glam::Vec3::from(positions[i0])
1824 + glam::Vec3::from(positions[i1])
1825 + glam::Vec3::from(positions[i2]))
1826 / 3.0;
1827 if let Some((sx, sy)) = project(mvp, centroid) {
1828 if in_rect(sx, sy) {
1829 if let Some(&ci) = f2c.get(tri_idx) {
1830 if seen.insert(ci) {
1831 result.elements.push((id, SubObjectRef::Cell(ci)));
1832 }
1833 }
1834 item_hit = true;
1835 }
1836 }
1837 }
1838 } else if wants_vertex {
1839 for (vi, pos) in positions.iter().enumerate() {
1841 if let Some((sx, sy)) = project(mvp, glam::Vec3::from(*pos)) {
1842 if in_rect(sx, sy) {
1843 result.elements.push((id, SubObjectRef::Vertex(vi as u32)));
1844 item_hit = true;
1845 }
1846 }
1847 }
1848 }
1849 } else if wants_vertex {
1850 for (vi, pos) in positions.iter().enumerate() {
1851 if let Some((sx, sy)) = project(mvp, glam::Vec3::from(*pos)) {
1852 if in_rect(sx, sy) {
1853 result.elements.push((id, SubObjectRef::Vertex(vi as u32)));
1854 item_hit = true;
1855 }
1856 }
1857 }
1858 } else {
1859 'tri_scan: for chunk in indices.chunks(3) {
1861 if chunk.len() < 3 {
1862 continue;
1863 }
1864 let [i0, i1, i2] =
1865 [chunk[0] as usize, chunk[1] as usize, chunk[2] as usize];
1866 if i0 >= positions.len() || i1 >= positions.len() || i2 >= positions.len() {
1867 continue;
1868 }
1869 let centroid = (glam::Vec3::from(positions[i0])
1870 + glam::Vec3::from(positions[i1])
1871 + glam::Vec3::from(positions[i2]))
1872 / 3.0;
1873 if let Some((sx, sy)) = project(mvp, centroid) {
1874 if in_rect(sx, sy) {
1875 item_hit = true;
1876 break 'tri_scan;
1877 }
1878 }
1879 }
1880 }
1881
1882 if wants_object && item_hit {
1883 result.objects.push(id);
1884 }
1885 }
1886 }
1887
1888 if wants_cell || wants_object {
1893 for item in &self.pick_tvm_items {
1894 if item.settings.pick_id == PickId::NONE {
1895 continue;
1896 }
1897 let Some(data) = item.volume_mesh_data.as_deref() else {
1898 continue;
1899 };
1900 use crate::resources::volume_mesh::CELL_SENTINEL;
1901 let id = item.settings.pick_id.0;
1902 let mvp = view_proj; let mut item_hit = false;
1904
1905 for (cell_idx, cell) in data.cells.iter().enumerate() {
1906 let nv: usize = if cell[4] == CELL_SENTINEL {
1907 4
1908 } else if cell[5] == CELL_SENTINEL {
1909 5
1910 } else if cell[6] == CELL_SENTINEL {
1911 6
1912 } else {
1913 8
1914 };
1915 let centroid: glam::Vec3 = cell[..nv]
1916 .iter()
1917 .map(|&vi| glam::Vec3::from(data.positions[vi as usize]))
1918 .sum::<glam::Vec3>()
1919 / nv as f32;
1920 if let Some((sx, sy)) = project(mvp, centroid) {
1921 if in_rect(sx, sy) {
1922 if wants_cell {
1923 result
1924 .elements
1925 .push((id, SubObjectRef::Cell(cell_idx as u32)));
1926 }
1927 item_hit = true;
1928 }
1929 }
1930 }
1931
1932 if wants_object && item_hit {
1933 result.objects.push(id);
1934 }
1935 }
1936 }
1937
1938 if wants_cloud || wants_object {
1940 for item in &self.pick_point_cloud_items {
1941 if item.settings.pick_id == PickId::NONE || item.positions.is_empty() {
1942 continue;
1943 }
1944 let model = glam::Mat4::from_cols_array_2d(&item.model);
1945 let mvp = view_proj * model;
1946 let id = item.settings.pick_id.0;
1947 let mut item_hit = false;
1948
1949 for (pt_idx, pos) in item.positions.iter().enumerate() {
1950 if let Some((sx, sy)) = project(mvp, glam::Vec3::from(*pos)) {
1951 if in_rect(sx, sy) {
1952 if wants_cloud {
1953 result
1954 .elements
1955 .push((id, SubObjectRef::Point(pt_idx as u32)));
1956 }
1957 item_hit = true;
1958 }
1959 }
1960 }
1961
1962 if wants_object && item_hit {
1963 result.objects.push(id);
1964 }
1965 }
1966 }
1967
1968 let wants_voxel = mask.intersects(PickMask::VOXEL);
1970 if wants_voxel || wants_object {
1971 for item in &self.pick_volume_items {
1972 if item.settings.pick_id == PickId::NONE {
1973 continue;
1974 }
1975 let Some(vol_data) = item.volume_data.as_deref() else {
1976 continue;
1977 };
1978 let [nx, ny, nz] = vol_data.dims;
1979 if nx == 0 || ny == 0 || nz == 0 || vol_data.data.is_empty() {
1980 continue;
1981 }
1982 let model = glam::Mat4::from_cols_array_2d(&item.model);
1983 let mvp = view_proj * model;
1984 let bbox_min = glam::Vec3::from(item.bbox_min);
1985 let bbox_max = glam::Vec3::from(item.bbox_max);
1986 let cell = (bbox_max - bbox_min) / glam::Vec3::new(nx as f32, ny as f32, nz as f32);
1987 let id = item.settings.pick_id.0;
1988 let mut item_hit = false;
1989
1990 for iz in 0..nz {
1991 for iy in 0..ny {
1992 for ix in 0..nx {
1993 let flat = (ix + iy * nx + iz * nx * ny) as usize;
1994 let scalar = vol_data.data[flat];
1995 if scalar.is_nan()
1996 || scalar < item.threshold_min
1997 || scalar > item.threshold_max
1998 {
1999 continue;
2000 }
2001 let center = bbox_min
2002 + cell
2003 * glam::Vec3::new(
2004 ix as f32 + 0.5,
2005 iy as f32 + 0.5,
2006 iz as f32 + 0.5,
2007 );
2008 if let Some((sx, sy)) = project(mvp, center) {
2009 if in_rect(sx, sy) {
2010 if wants_voxel {
2011 result
2012 .elements
2013 .push((id, SubObjectRef::Voxel(flat as u32)));
2014 }
2015 item_hit = true;
2016 }
2017 }
2018 }
2019 }
2020 }
2021
2022 if wants_object && item_hit {
2023 result.objects.push(id);
2024 }
2025 }
2026 }
2027
2028 if wants_splat || wants_object {
2030 for item in &self.pick_splat_items {
2031 if item.settings.pick_id == PickId::NONE {
2032 continue;
2033 }
2034 let Some(gpu_set) = self.resources.gaussian_splat_store.get(item.id.0) else {
2035 continue;
2036 };
2037 if gpu_set.cpu_positions.is_empty() {
2038 continue;
2039 }
2040 let model = glam::Mat4::from_cols_array_2d(&item.model);
2041 let mvp = view_proj * model;
2042 let id = item.settings.pick_id.0;
2043 let mut item_hit = false;
2044
2045 for (i, pos) in gpu_set.cpu_positions.iter().enumerate() {
2046 if let Some((sx, sy)) = project(mvp, glam::Vec3::from(*pos)) {
2047 if in_rect(sx, sy) {
2048 if wants_splat {
2049 result.elements.push((id, SubObjectRef::Splat(i as u32)));
2050 }
2051 item_hit = true;
2052 }
2053 }
2054 }
2055
2056 if wants_object && item_hit {
2057 result.objects.push(id);
2058 }
2059 }
2060 }
2061
2062 let wants_instance = mask.intersects(PickMask::INSTANCE);
2064 if wants_instance || wants_object {
2065 for item in &self.pick_glyph_items {
2067 if item.settings.pick_id == PickId::NONE || item.positions.is_empty() {
2068 continue;
2069 }
2070 let model = glam::Mat4::from_cols_array_2d(&item.model);
2071 let mvp = view_proj * model;
2072 let id = item.settings.pick_id.0;
2073 let mut item_hit = false;
2074 for (i, pos) in item.positions.iter().enumerate() {
2075 if let Some((sx, sy)) = project(mvp, glam::Vec3::from(*pos)) {
2076 if in_rect(sx, sy) {
2077 if wants_instance {
2078 result.elements.push((id, SubObjectRef::Instance(i as u32)));
2079 }
2080 item_hit = true;
2081 }
2082 }
2083 }
2084 if wants_object && item_hit {
2085 result.objects.push(id);
2086 }
2087 }
2088
2089 for item in &self.pick_tensor_glyph_items {
2091 if item.settings.pick_id == PickId::NONE || item.positions.is_empty() {
2092 continue;
2093 }
2094 let model = glam::Mat4::from_cols_array_2d(&item.model);
2095 let mvp = view_proj * model;
2096 let id = item.settings.pick_id.0;
2097 let mut item_hit = false;
2098 for (i, pos) in item.positions.iter().enumerate() {
2099 if let Some((sx, sy)) = project(mvp, glam::Vec3::from(*pos)) {
2100 if in_rect(sx, sy) {
2101 if wants_instance {
2102 result.elements.push((id, SubObjectRef::Instance(i as u32)));
2103 }
2104 item_hit = true;
2105 }
2106 }
2107 }
2108 if wants_object && item_hit {
2109 result.objects.push(id);
2110 }
2111 }
2112
2113 for item in &self.pick_sprite_items {
2115 if item.settings.pick_id == PickId::NONE || item.positions.is_empty() {
2116 continue;
2117 }
2118 let model = glam::Mat4::from_cols_array_2d(&item.model);
2119 let mvp = view_proj * model;
2120 let id = item.settings.pick_id.0;
2121 let mut item_hit = false;
2122 for (i, pos) in item.positions.iter().enumerate() {
2123 if let Some((sx, sy)) = project(mvp, glam::Vec3::from(*pos)) {
2124 if in_rect(sx, sy) {
2125 if wants_instance {
2126 result.elements.push((id, SubObjectRef::Instance(i as u32)));
2127 }
2128 item_hit = true;
2129 }
2130 }
2131 }
2132 if wants_object && item_hit {
2133 result.objects.push(id);
2134 }
2135 }
2136 }
2137
2138 let wants_poly_node = mask.intersects(PickMask::POLY_NODE);
2140 let wants_segment = mask.intersects(PickMask::SEGMENT);
2141 let wants_strip = mask.intersects(PickMask::STRIP);
2142 if wants_poly_node || wants_segment || wants_strip || wants_object {
2143 for item in &self.pick_polyline_items {
2144 if item.settings.pick_id == PickId::NONE || item.positions.is_empty() {
2145 continue;
2146 }
2147 let id = item.settings.pick_id.0;
2148 let mut item_hit = false;
2149 let mut strips_hit = std::collections::HashSet::<u32>::new();
2150
2151 if wants_poly_node || wants_strip || wants_object {
2153 for (node_idx, pos) in item.positions.iter().enumerate() {
2154 if let Some((sx, sy)) = project(view_proj, glam::Vec3::from(*pos)) {
2155 if in_rect(sx, sy) {
2156 item_hit = true;
2157 if wants_poly_node {
2158 result
2159 .elements
2160 .push((id, SubObjectRef::Point(node_idx as u32)));
2161 } else if wants_strip {
2162 let s = strip_for_node(node_idx as u32, &item.strip_lengths);
2163 strips_hit.insert(s);
2164 }
2165 }
2166 }
2167 }
2168 }
2169
2170 if wants_segment || (wants_strip && !wants_poly_node) || wants_object {
2172 let mut node_off = 0usize;
2173 let mut seg_off = 0u32;
2174 macro_rules! try_seg_rect {
2175 ($ai:expr, $bi:expr, $seg:expr) => {{
2176 if let (Some((sax, say)), Some((sbx, sby))) = (
2177 project(view_proj, glam::Vec3::from(item.positions[$ai])),
2178 project(view_proj, glam::Vec3::from(item.positions[$bi])),
2179 ) {
2180 if segment_in_rect(
2181 glam::Vec2::new(sax, say),
2182 glam::Vec2::new(sbx, sby),
2183 rect_min,
2184 rect_max,
2185 ) {
2186 item_hit = true;
2187 if wants_segment {
2188 result.elements.push((id, SubObjectRef::Segment($seg)));
2189 } else if wants_strip {
2190 let s = strip_for_segment($seg, &item.strip_lengths);
2191 strips_hit.insert(s);
2192 }
2193 }
2194 }
2195 }};
2196 }
2197 if item.strip_lengths.is_empty() {
2198 for j in 0..item.positions.len().saturating_sub(1) {
2199 try_seg_rect!(j, j + 1, j as u32);
2200 }
2201 } else {
2202 for &slen in &item.strip_lengths {
2203 let slen = slen as usize;
2204 for j in 0..slen.saturating_sub(1) {
2205 try_seg_rect!(node_off + j, node_off + j + 1, seg_off + j as u32);
2206 }
2207 seg_off += slen.saturating_sub(1) as u32;
2208 node_off += slen;
2209 }
2210 }
2211 }
2212
2213 if wants_strip {
2214 for s in strips_hit {
2215 result.elements.push((id, SubObjectRef::Strip(s)));
2216 }
2217 }
2218 if wants_object && item_hit {
2219 result.objects.push(id);
2220 }
2221 }
2222 }
2223
2224 if wants_poly_node || wants_segment || wants_strip || wants_object {
2226 let st_tube_iter = self
2230 .pick_streamtube_items
2231 .iter()
2232 .map(|it| (it.settings.pick_id.0, it.positions.as_slice(), it.strip_lengths.as_slice()))
2233 .chain(
2234 self.pick_tube_items
2235 .iter()
2236 .map(|it| (it.settings.pick_id.0, it.positions.as_slice(), it.strip_lengths.as_slice())),
2237 );
2238
2239 for (id, positions, strip_lengths) in st_tube_iter {
2240 if id == 0 || positions.is_empty() {
2241 continue;
2242 }
2243 let mut item_hit = false;
2244 let mut strips_hit = std::collections::HashSet::<u32>::new();
2245
2246 let single_st;
2247 let strips_st: &[u32] = if strip_lengths.is_empty() {
2248 single_st = [positions.len() as u32];
2249 &single_st
2250 } else {
2251 strip_lengths
2252 };
2253
2254 if wants_poly_node || wants_strip || wants_object {
2256 'st_nodes: for (ni, pos) in positions.iter().enumerate() {
2257 if let Some((sx, sy)) = project(view_proj, glam::Vec3::from(*pos)) {
2258 if in_rect(sx, sy) {
2259 item_hit = true;
2260 if wants_poly_node {
2261 result.elements.push((id, SubObjectRef::Point(ni as u32)));
2262 } else if wants_strip {
2263 let s = strip_for_node(ni as u32, strip_lengths);
2264 strips_hit.insert(s);
2265 } else {
2266 break 'st_nodes;
2268 }
2269 }
2270 }
2271 }
2272 }
2273
2274 if wants_segment || wants_strip || wants_object {
2276 let mut node_off = 0usize;
2277 let mut seg_off = 0u32;
2278 'st_strips: for &slen in strips_st {
2279 let slen = slen as usize;
2280 for j in 0..slen.saturating_sub(1) {
2281 let seg_idx = seg_off + j as u32;
2282 let pa = glam::Vec3::from(positions[node_off + j]);
2283 let pb = glam::Vec3::from(positions[node_off + j + 1]);
2284 let hit = match (project(view_proj, pa), project(view_proj, pb)) {
2285 (Some((ax, ay)), Some((bx, by))) => segment_in_rect(
2286 glam::Vec2::new(ax, ay),
2287 glam::Vec2::new(bx, by),
2288 rect_min,
2289 rect_max,
2290 ),
2291 (Some((ax, ay)), None) => in_rect(ax, ay),
2292 (None, Some((bx, by))) => in_rect(bx, by),
2293 (None, None) => false,
2294 };
2295 if hit {
2296 item_hit = true;
2297 if wants_segment {
2298 result.elements.push((id, SubObjectRef::Segment(seg_idx)));
2299 } else if wants_strip {
2300 let s = strip_for_segment(seg_idx, strip_lengths);
2301 strips_hit.insert(s);
2302 } else {
2303 break 'st_strips;
2305 }
2306 }
2307 }
2308 seg_off += slen.saturating_sub(1) as u32;
2309 node_off += slen;
2310 }
2311 }
2312
2313 if wants_strip {
2314 for s in strips_hit {
2315 result.elements.push((id, SubObjectRef::Strip(s)));
2316 }
2317 }
2318 if wants_object && item_hit {
2319 result.objects.push(id);
2320 }
2321 }
2322
2323 for item in &self.pick_ribbon_items {
2328 if item.settings.pick_id == PickId::NONE || item.positions.is_empty() {
2329 continue;
2330 }
2331
2332 let single_r;
2333 let strips_r: &[u32] = if item.strip_lengths.is_empty() {
2334 single_r = [item.positions.len() as u32];
2335 &single_r
2336 } else {
2337 &item.strip_lengths
2338 };
2339
2340 let mut item_hit = false;
2341 let mut strips_hit = std::collections::HashSet::<u32>::new();
2342
2343 let proj2 = |p: glam::Vec3| -> Option<glam::Vec2> {
2345 project(view_proj, p).map(|(x, y)| glam::Vec2::new(x, y))
2346 };
2347
2348 if wants_poly_node || wants_strip || wants_object {
2350 'rb_nodes: for (ni, pos) in item.positions.iter().enumerate() {
2351 if let Some((sx, sy)) = project(view_proj, glam::Vec3::from(*pos)) {
2352 if in_rect(sx, sy) {
2353 item_hit = true;
2354 if wants_poly_node {
2355 result
2356 .elements
2357 .push((item.settings.pick_id.0, SubObjectRef::Point(ni as u32)));
2358 } else if wants_strip {
2359 let s = strip_for_node(ni as u32, &item.strip_lengths);
2360 strips_hit.insert(s);
2361 } else {
2362 break 'rb_nodes;
2363 }
2364 }
2365 }
2366 }
2367 }
2368
2369 if wants_segment || wants_strip || wants_object {
2371 let frames = ribbon_lateral_frames(
2372 &item.positions,
2373 &item.strip_lengths,
2374 item.width,
2375 item.width_attribute.as_deref(),
2376 item.twist_attribute.as_deref(),
2377 );
2378 let mut node_off = 0usize;
2379 let mut seg_off = 0u32;
2380
2381 'rb_strips: for &slen in strips_r {
2382 let slen = slen as usize;
2383 for k in 0..slen.saturating_sub(1) {
2384 let seg_idx = seg_off + k as u32;
2385 let ia = node_off + k;
2386 let ib = node_off + k + 1;
2387 let pa = glam::Vec3::from(item.positions[ia]);
2388 let pb = glam::Vec3::from(item.positions[ib]);
2389 let (ua, wa) = frames[ia];
2390 let (ub, wb) = frames[ib];
2391 let c0 = pa + ua * wa; let c1 = pa - ua * wa; let c2 = pb + ub * wb; let c3 = pb - ub * wb; let sc0 = proj2(c0);
2396 let sc1 = proj2(c1);
2397 let sc2 = proj2(c2);
2398 let sc3 = proj2(c3);
2399 let edge_hit = |a: Option<glam::Vec2>, b: Option<glam::Vec2>| -> bool {
2400 match (a, b) {
2401 (Some(a), Some(b)) => segment_in_rect(a, b, rect_min, rect_max),
2402 (Some(a), None) => in_rect(a.x, a.y),
2403 (None, Some(b)) => in_rect(b.x, b.y),
2404 (None, None) => false,
2405 }
2406 };
2407 let hit = edge_hit(sc0, sc1)
2408 || edge_hit(sc2, sc3)
2409 || edge_hit(sc0, sc2)
2410 || edge_hit(sc1, sc3);
2411 if hit {
2412 item_hit = true;
2413 if wants_segment {
2414 result
2415 .elements
2416 .push((item.settings.pick_id.0, SubObjectRef::Segment(seg_idx)));
2417 } else if wants_strip {
2418 let s = strip_for_segment(seg_idx, &item.strip_lengths);
2419 strips_hit.insert(s);
2420 } else {
2421 break 'rb_strips;
2422 }
2423 }
2424 }
2425 seg_off += slen.saturating_sub(1) as u32;
2426 node_off += slen;
2427 }
2428 }
2429
2430 if wants_strip {
2431 for s in strips_hit {
2432 result.elements.push((item.settings.pick_id.0, SubObjectRef::Strip(s)));
2433 }
2434 }
2435 if wants_object && item_hit {
2436 result.objects.push(item.settings.pick_id.0);
2437 }
2438 }
2439 }
2440
2441 if wants_object {
2443 for item in &self.pick_image_slice_items {
2445 if item.settings.pick_id == PickId::NONE {
2446 continue;
2447 }
2448 let [bmin, bmax] = [item.bbox_min, item.bbox_max];
2449 let t = item.offset;
2450 let corners: [[f32; 3]; 4] = match item.axis {
2451 SliceAxis::X => {
2452 let x = bmin[0] + t * (bmax[0] - bmin[0]);
2453 [
2454 [x, bmin[1], bmin[2]],
2455 [x, bmax[1], bmin[2]],
2456 [x, bmax[1], bmax[2]],
2457 [x, bmin[1], bmax[2]],
2458 ]
2459 }
2460 SliceAxis::Y => {
2461 let y = bmin[1] + t * (bmax[1] - bmin[1]);
2462 [
2463 [bmin[0], y, bmin[2]],
2464 [bmax[0], y, bmin[2]],
2465 [bmax[0], y, bmax[2]],
2466 [bmin[0], y, bmax[2]],
2467 ]
2468 }
2469 SliceAxis::Z => {
2470 let z = bmin[2] + t * (bmax[2] - bmin[2]);
2471 [
2472 [bmin[0], bmin[1], z],
2473 [bmax[0], bmin[1], z],
2474 [bmax[0], bmax[1], z],
2475 [bmin[0], bmax[1], z],
2476 ]
2477 }
2478 };
2479 let sc: Vec<Option<glam::Vec2>> = corners
2480 .iter()
2481 .map(|&c| {
2482 project(view_proj, glam::Vec3::from(c)).map(|(x, y)| glam::Vec2::new(x, y))
2483 })
2484 .collect();
2485 let hit = sc.iter().any(|p| p.map_or(false, |p| in_rect(p.x, p.y)))
2486 || (0..4).any(|i| {
2487 let a = sc[i];
2488 let b = sc[(i + 1) % 4];
2489 match (a, b) {
2490 (Some(a), Some(b)) => segment_in_rect(a, b, rect_min, rect_max),
2491 (Some(a), None) => in_rect(a.x, a.y),
2492 (None, Some(b)) => in_rect(b.x, b.y),
2493 (None, None) => false,
2494 }
2495 });
2496 if hit {
2497 result.objects.push(item.settings.pick_id.0);
2498 }
2499 }
2500
2501 for item in &self.pick_volume_surface_slice_items {
2503 if item.settings.pick_id == PickId::NONE {
2504 continue;
2505 }
2506 let Some(mesh) = self.resources.mesh_store.get(item.mesh_id) else {
2507 continue;
2508 };
2509 let Some(positions) = &mesh.cpu_positions else {
2510 continue;
2511 };
2512 let model = glam::Mat4::from_cols_array_2d(&item.model);
2513 let hit = positions.iter().any(|&p| {
2514 let wp = model.transform_point3(glam::Vec3::from(p));
2515 project(view_proj, wp).map_or(false, |(sx, sy)| in_rect(sx, sy))
2516 });
2517 if hit {
2518 result.objects.push(item.settings.pick_id.0);
2519 }
2520 }
2521
2522 for item in &self.pick_screen_image_items {
2524 if item.settings.pick_id == PickId::NONE || item.width == 0 || item.height == 0 {
2525 continue;
2526 }
2527 let img_w = item.width as f32 * item.scale;
2528 let img_h = item.height as f32 * item.scale;
2529 let (sx, sy) = match item.anchor {
2530 ImageAnchor::TopLeft => (0.0, 0.0),
2531 ImageAnchor::TopRight => (viewport_size.x - img_w, 0.0),
2532 ImageAnchor::BottomLeft => (0.0, viewport_size.y - img_h),
2533 ImageAnchor::BottomRight => (viewport_size.x - img_w, viewport_size.y - img_h),
2534 ImageAnchor::Center => (
2535 (viewport_size.x - img_w) * 0.5,
2536 (viewport_size.y - img_h) * 0.5,
2537 ),
2538 };
2539 let overlap = sx <= rect_max.x
2541 && sx + img_w >= rect_min.x
2542 && sy <= rect_max.y
2543 && sy + img_h >= rect_min.y;
2544 if overlap {
2545 result.objects.push(item.settings.pick_id.0);
2546 }
2547 }
2548 }
2549
2550 if wants_object {
2557 for item in &self.pick_implicit_items {
2558 let mut hit = false;
2559 'prim_loop: for prim in &item.primitives {
2560 let (center, radius) = match prim.kind {
2562 1 => {
2563 let c = glam::Vec3::new(prim.params[0], prim.params[1], prim.params[2]);
2565 (c, prim.params[3].abs())
2566 }
2567 2 => {
2568 let c = glam::Vec3::new(prim.params[0], prim.params[1], prim.params[2]);
2570 let h = glam::Vec3::new(prim.params[4], prim.params[5], prim.params[6]);
2571 (c, h.length())
2572 }
2573 3 => {
2574 continue;
2576 }
2577 4 => {
2578 let a = glam::Vec3::new(prim.params[0], prim.params[1], prim.params[2]);
2580 let b = glam::Vec3::new(prim.params[4], prim.params[5], prim.params[6]);
2581 let r = prim.params[3].abs();
2582 ((a + b) * 0.5, (b - a).length() * 0.5 + r)
2583 }
2584 _ => continue,
2585 };
2586 for dx in [-radius, radius] {
2588 for dy in [-radius, radius] {
2589 for dz in [-radius, radius] {
2590 let corner = center + glam::Vec3::new(dx, dy, dz);
2591 if let Some((sx, sy)) = project(view_proj, corner) {
2592 if in_rect(sx, sy) {
2593 hit = true;
2594 break 'prim_loop;
2595 }
2596 }
2597 }
2598 }
2599 }
2600 }
2601 if hit {
2602 result.objects.push(item.id);
2603 }
2604 }
2605 }
2606
2607 if wants_object {
2613 for item in &self.pick_mc_items {
2614 let vol = &item.volume_data;
2615 let isovalue = item.isovalue;
2616 let [nx, ny, nz] = vol.dims;
2617 let origin = glam::Vec3::from(vol.origin);
2618 let spacing = glam::Vec3::from(vol.spacing);
2619
2620 let mut hit = false;
2621 'mc_rect: for iz in 0..nz.saturating_sub(1) {
2622 for iy in 0..ny.saturating_sub(1) {
2623 for ix in 0..nx.saturating_sub(1) {
2624 let mut has_below = false;
2627 let mut has_above = false;
2628 'corners: for dz in 0u32..=1 {
2629 for dy in 0u32..=1 {
2630 for dx in 0u32..=1 {
2631 let s = vol.sample(ix + dx, iy + dy, iz + dz);
2632 if s < isovalue {
2633 has_below = true;
2634 } else {
2635 has_above = true;
2636 }
2637 if has_below && has_above {
2638 break 'corners;
2639 }
2640 }
2641 }
2642 }
2643 if !(has_below && has_above) {
2644 continue;
2645 }
2646 let cell_center = origin
2647 + spacing
2648 * glam::Vec3::new(
2649 ix as f32 + 0.5,
2650 iy as f32 + 0.5,
2651 iz as f32 + 0.5,
2652 );
2653 if let Some((sx, sy)) = project(view_proj, cell_center) {
2654 if in_rect(sx, sy) {
2655 hit = true;
2656 break 'mc_rect;
2657 }
2658 }
2659 }
2660 }
2661 }
2662 if hit {
2663 result.objects.push(item.id);
2664 }
2665 }
2666 }
2667
2668 result
2669 }
2670
2671 pub fn pick_scene_gpu(
2695 &mut self,
2696 device: &wgpu::Device,
2697 queue: &wgpu::Queue,
2698 cursor: glam::Vec2,
2699 frame: &FrameData,
2700 ) -> Option<crate::interaction::picking::GpuPickHit> {
2701 if self.runtime_mode == crate::renderer::stats::RuntimeMode::Playback
2704 && self.frame_counter % 4 != 0
2705 {
2706 return None;
2707 }
2708
2709 let scene_items: &[SceneRenderItem] = match &frame.scene.surfaces {
2711 SurfaceSubmission::Flat(items) => items.as_ref(),
2712 };
2713
2714 let ppp = frame.camera.pixels_per_point;
2715 let vp_w = (frame.camera.viewport_size[0] * ppp).round() as u32;
2716 let vp_h = (frame.camera.viewport_size[1] * ppp).round() as u32;
2717
2718 if cursor.x < 0.0
2720 || cursor.y < 0.0
2721 || cursor.x >= frame.camera.viewport_size[0]
2722 || cursor.y >= frame.camera.viewport_size[1]
2723 || vp_w == 0
2724 || vp_h == 0
2725 {
2726 return None;
2727 }
2728
2729 self.resources.ensure_pick_pipeline(device);
2731
2732 let pickable_items: Vec<&SceneRenderItem> = scene_items
2736 .iter()
2737 .filter(|item| !item.settings.hidden && item.settings.pick_id != PickId::NONE)
2738 .collect();
2739
2740 let pick_instances: Vec<PickInstance> = pickable_items
2741 .iter()
2742 .map(|item| {
2743 let m = item.model;
2744 PickInstance {
2745 model_c0: m[0],
2746 model_c1: m[1],
2747 model_c2: m[2],
2748 model_c3: m[3],
2749 object_id: item.settings.pick_id.0 as u32,
2750 _pad: [0; 3],
2751 }
2752 })
2753 .collect();
2754
2755 if pick_instances.is_empty() {
2756 return None;
2757 }
2758
2759 let pick_instance_bytes = bytemuck::cast_slice(&pick_instances);
2761 let pick_instance_buf = device.create_buffer(&wgpu::BufferDescriptor {
2762 label: Some("pick_instance_buf"),
2763 size: pick_instance_bytes.len().max(80) as u64,
2764 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
2765 mapped_at_creation: false,
2766 });
2767 queue.write_buffer(&pick_instance_buf, 0, pick_instance_bytes);
2768
2769 let pick_instance_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
2770 label: Some("pick_instance_bg"),
2771 layout: self
2772 .resources
2773 .pick_bind_group_layout_1
2774 .as_ref()
2775 .expect("ensure_pick_pipeline must be called first"),
2776 entries: &[wgpu::BindGroupEntry {
2777 binding: 0,
2778 resource: pick_instance_buf.as_entire_binding(),
2779 }],
2780 });
2781
2782 let camera_uniform = frame.camera.render_camera.camera_uniform();
2784 let camera_bytes = bytemuck::bytes_of(&camera_uniform);
2785 let pick_camera_buf = device.create_buffer(&wgpu::BufferDescriptor {
2786 label: Some("pick_camera_buf"),
2787 size: std::mem::size_of::<CameraUniform>() as u64,
2788 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
2789 mapped_at_creation: false,
2790 });
2791 queue.write_buffer(&pick_camera_buf, 0, camera_bytes);
2792
2793 let pick_camera_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
2794 label: Some("pick_camera_bg"),
2795 layout: self
2796 .resources
2797 .pick_camera_bgl
2798 .as_ref()
2799 .expect("ensure_pick_pipeline must be called first"),
2800 entries: &[
2801 wgpu::BindGroupEntry {
2802 binding: 0,
2803 resource: pick_camera_buf.as_entire_binding(),
2804 },
2805 wgpu::BindGroupEntry {
2806 binding: 6,
2807 resource: self.resources.clip_volume_uniform_buf.as_entire_binding(),
2808 },
2809 ],
2810 });
2811
2812 let pick_id_texture = device.create_texture(&wgpu::TextureDescriptor {
2814 label: Some("pick_id_texture"),
2815 size: wgpu::Extent3d {
2816 width: vp_w,
2817 height: vp_h,
2818 depth_or_array_layers: 1,
2819 },
2820 mip_level_count: 1,
2821 sample_count: 1,
2822 dimension: wgpu::TextureDimension::D2,
2823 format: wgpu::TextureFormat::R32Uint,
2824 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
2825 view_formats: &[],
2826 });
2827 let pick_id_view = pick_id_texture.create_view(&wgpu::TextureViewDescriptor::default());
2828
2829 let pick_depth_texture = device.create_texture(&wgpu::TextureDescriptor {
2830 label: Some("pick_depth_colour_texture"),
2831 size: wgpu::Extent3d {
2832 width: vp_w,
2833 height: vp_h,
2834 depth_or_array_layers: 1,
2835 },
2836 mip_level_count: 1,
2837 sample_count: 1,
2838 dimension: wgpu::TextureDimension::D2,
2839 format: wgpu::TextureFormat::R32Float,
2840 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
2841 view_formats: &[],
2842 });
2843 let pick_depth_view =
2844 pick_depth_texture.create_view(&wgpu::TextureViewDescriptor::default());
2845
2846 let depth_stencil_texture = device.create_texture(&wgpu::TextureDescriptor {
2847 label: Some("pick_ds_texture"),
2848 size: wgpu::Extent3d {
2849 width: vp_w,
2850 height: vp_h,
2851 depth_or_array_layers: 1,
2852 },
2853 mip_level_count: 1,
2854 sample_count: 1,
2855 dimension: wgpu::TextureDimension::D2,
2856 format: wgpu::TextureFormat::Depth24PlusStencil8,
2857 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
2858 view_formats: &[],
2859 });
2860 let depth_stencil_view =
2861 depth_stencil_texture.create_view(&wgpu::TextureViewDescriptor::default());
2862
2863 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
2865 label: Some("pick_pass_encoder"),
2866 });
2867 {
2868 let mut pick_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2869 label: Some("pick_pass"),
2870 color_attachments: &[
2871 Some(wgpu::RenderPassColorAttachment {
2872 view: &pick_id_view,
2873 resolve_target: None,
2874 depth_slice: None,
2875 ops: wgpu::Operations {
2876 load: wgpu::LoadOp::Clear(wgpu::Color {
2877 r: 0.0,
2878 g: 0.0,
2879 b: 0.0,
2880 a: 0.0,
2881 }),
2882 store: wgpu::StoreOp::Store,
2883 },
2884 }),
2885 Some(wgpu::RenderPassColorAttachment {
2886 view: &pick_depth_view,
2887 resolve_target: None,
2888 depth_slice: None,
2889 ops: wgpu::Operations {
2890 load: wgpu::LoadOp::Clear(wgpu::Color {
2891 r: 1.0,
2892 g: 0.0,
2893 b: 0.0,
2894 a: 0.0,
2895 }),
2896 store: wgpu::StoreOp::Store,
2897 },
2898 }),
2899 ],
2900 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
2901 view: &depth_stencil_view,
2902 depth_ops: Some(wgpu::Operations {
2903 load: wgpu::LoadOp::Clear(1.0),
2904 store: wgpu::StoreOp::Store,
2905 }),
2906 stencil_ops: None,
2907 }),
2908 timestamp_writes: None,
2909 occlusion_query_set: None,
2910 });
2911
2912 pick_pass.set_pipeline(
2913 self.resources
2914 .pick_pipeline
2915 .as_ref()
2916 .expect("ensure_pick_pipeline must be called first"),
2917 );
2918 pick_pass.set_bind_group(0, &pick_camera_bg, &[]);
2919 pick_pass.set_bind_group(1, &pick_instance_bg, &[]);
2920
2921 for (instance_slot, item) in pickable_items.iter().enumerate() {
2924 let Some(mesh) = self.resources.mesh_store.get(item.mesh_id) else {
2925 continue;
2926 };
2927 pick_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
2928 pick_pass.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
2929 let slot = instance_slot as u32;
2930 pick_pass.draw_indexed(0..mesh.index_count, 0, slot..slot + 1);
2931 }
2932 }
2933
2934 let bytes_per_row_aligned = 256u32; let id_staging = device.create_buffer(&wgpu::BufferDescriptor {
2939 label: Some("pick_id_staging"),
2940 size: bytes_per_row_aligned as u64,
2941 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
2942 mapped_at_creation: false,
2943 });
2944 let depth_staging = device.create_buffer(&wgpu::BufferDescriptor {
2945 label: Some("pick_depth_staging"),
2946 size: bytes_per_row_aligned as u64,
2947 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
2948 mapped_at_creation: false,
2949 });
2950
2951 let px = (cursor.x * ppp).round() as u32;
2953 let py = (cursor.y * ppp).round() as u32;
2954
2955 encoder.copy_texture_to_buffer(
2956 wgpu::TexelCopyTextureInfo {
2957 texture: &pick_id_texture,
2958 mip_level: 0,
2959 origin: wgpu::Origin3d { x: px, y: py, z: 0 },
2960 aspect: wgpu::TextureAspect::All,
2961 },
2962 wgpu::TexelCopyBufferInfo {
2963 buffer: &id_staging,
2964 layout: wgpu::TexelCopyBufferLayout {
2965 offset: 0,
2966 bytes_per_row: Some(bytes_per_row_aligned),
2967 rows_per_image: Some(1),
2968 },
2969 },
2970 wgpu::Extent3d {
2971 width: 1,
2972 height: 1,
2973 depth_or_array_layers: 1,
2974 },
2975 );
2976 encoder.copy_texture_to_buffer(
2977 wgpu::TexelCopyTextureInfo {
2978 texture: &pick_depth_texture,
2979 mip_level: 0,
2980 origin: wgpu::Origin3d { x: px, y: py, z: 0 },
2981 aspect: wgpu::TextureAspect::All,
2982 },
2983 wgpu::TexelCopyBufferInfo {
2984 buffer: &depth_staging,
2985 layout: wgpu::TexelCopyBufferLayout {
2986 offset: 0,
2987 bytes_per_row: Some(bytes_per_row_aligned),
2988 rows_per_image: Some(1),
2989 },
2990 },
2991 wgpu::Extent3d {
2992 width: 1,
2993 height: 1,
2994 depth_or_array_layers: 1,
2995 },
2996 );
2997
2998 queue.submit(std::iter::once(encoder.finish()));
2999
3000 let (tx_id, rx_id) = std::sync::mpsc::channel::<Result<(), wgpu::BufferAsyncError>>();
3002 let (tx_dep, rx_dep) = std::sync::mpsc::channel::<Result<(), wgpu::BufferAsyncError>>();
3003 id_staging
3004 .slice(..)
3005 .map_async(wgpu::MapMode::Read, move |r| {
3006 let _ = tx_id.send(r);
3007 });
3008 depth_staging
3009 .slice(..)
3010 .map_async(wgpu::MapMode::Read, move |r| {
3011 let _ = tx_dep.send(r);
3012 });
3013 device
3014 .poll(wgpu::PollType::Wait {
3015 submission_index: None,
3016 timeout: Some(std::time::Duration::from_secs(5)),
3017 })
3018 .unwrap();
3019 let _ = rx_id.recv().unwrap_or(Err(wgpu::BufferAsyncError));
3020 let _ = rx_dep.recv().unwrap_or(Err(wgpu::BufferAsyncError));
3021
3022 let object_id = {
3023 let data = id_staging.slice(..).get_mapped_range();
3024 u32::from_le_bytes([data[0], data[1], data[2], data[3]])
3025 };
3026 id_staging.unmap();
3027
3028 let depth = {
3029 let data = depth_staging.slice(..).get_mapped_range();
3030 f32::from_le_bytes([data[0], data[1], data[2], data[3]])
3031 };
3032 depth_staging.unmap();
3033
3034 if object_id == 0 {
3036 return None;
3037 }
3038
3039 Some(crate::interaction::picking::GpuPickHit {
3040 object_id: PickId(object_id as u64),
3041 depth,
3042 })
3043 }
3044}