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
787 - match item.volume.shape {
788 crate::scene::scatter_volume::ScatterShape::Box(b) => {
789 (b.min + b.max) * 0.5
790 }
791 crate::scene::scatter_volume::ScatterShape::Sphere {
792 center, ..
793 } => glam::Vec3::from(center),
794 })
795 .try_normalize()
796 .unwrap_or(glam::Vec3::Z);
797 consider(
798 t_enter,
799 PickHit::object_hit(item.settings.pick_id.0, world_pos, normal),
800 );
801 }
802 }
803 }
804
805 if wants_cell || wants_object {
809 for item in &self.pick_volume_mesh_items {
810 if item.settings.pick_id == PickId::NONE || item.transparency.is_none() {
811 continue;
812 }
813 let Some(data) = item.volume_mesh_data.as_deref() else {
814 continue;
815 };
816 let model = glam::Mat4::from_cols_array_2d(&item.model);
817 if let Some(mut hit) = pick_transparent_volume_mesh_cpu(
818 ray_origin,
819 ray_dir,
820 item.settings.pick_id.0,
821 model,
822 data,
823 ) {
824 let toi = (hit.world_pos - ray_origin).dot(ray_dir).max(0.0);
825 if !wants_cell {
826 hit.sub_object = None;
827 }
828 consider(toi, hit);
829 }
830 }
831 }
832
833 if wants_cloud || wants_object {
835 for item in &self.pick_point_cloud_items {
836 if item.settings.pick_id == PickId::NONE || item.positions.is_empty() {
837 continue;
838 }
839 let radius_px = item.point_size.max(4.0);
840 if let Some(mut hit) = pick_point_cloud_cpu(
841 click_pos,
842 item.settings.pick_id.0,
843 item,
844 view_proj,
845 viewport_size,
846 radius_px,
847 ) {
848 let toi = (hit.world_pos - ray_origin).dot(ray_dir).max(0.0);
849 if !wants_cloud {
850 hit.sub_object = None;
851 }
852 consider(toi, hit);
853 }
854 }
855 }
856
857 let wants_voxel = mask.intersects(PickMask::VOXEL);
859 if wants_voxel || wants_object {
860 for item in &self.pick_volume_items {
861 if item.settings.pick_id == PickId::NONE {
862 continue;
863 }
864 let Some(vol_data) = item.volume_data.as_deref() else {
865 continue;
866 };
867 if let Some(mut hit) =
868 pick_volume_cpu(ray_origin, ray_dir, item.settings.pick_id.0, item, vol_data)
869 {
870 let toi = (hit.world_pos - ray_origin).dot(ray_dir).max(0.0);
871 if !wants_voxel {
872 hit.sub_object = None;
873 }
874 consider(toi, hit);
875 }
876 }
877 }
878
879 if wants_splat || wants_object {
881 for item in &self.pick_splat_items {
882 if item.settings.pick_id == PickId::NONE {
883 continue;
884 }
885 let Some(gpu_set) = self.resources.gaussian_splat_store.get(item.id.0) else {
886 continue;
887 };
888 if gpu_set.cpu_positions.is_empty() {
889 continue;
890 }
891 let model = glam::Mat4::from_cols_array_2d(&item.model);
892 let mean_max_scale: f32 = if gpu_set.cpu_scales.is_empty() {
895 0.05
896 } else {
897 gpu_set
898 .cpu_scales
899 .iter()
900 .map(|s| s[0].max(s[1]).max(s[2]))
901 .sum::<f32>()
902 / gpu_set.cpu_scales.len() as f32
903 };
904 let world_radius = mean_max_scale * 3.0;
905 let center_w = model.transform_point3(glam::Vec3::ZERO);
906 let p0_clip = view_proj * center_w.extend(1.0);
907 let p1_clip = view_proj * (center_w + glam::Vec3::X * world_radius).extend(1.0);
908 let radius_px = if p0_clip.w.abs() > 1e-6 && p1_clip.w.abs() > 1e-6 {
909 let p0_ndc = glam::Vec2::new(p0_clip.x, p0_clip.y) / p0_clip.w;
910 let p1_ndc = glam::Vec2::new(p1_clip.x, p1_clip.y) / p1_clip.w;
911 ((p1_ndc - p0_ndc).length() * 0.5 * viewport_size.x.max(viewport_size.y))
912 .max(4.0)
913 } else {
914 world_radius * 100.0
915 };
916 if let Some(mut hit) = pick_gaussian_splat_cpu(
917 click_pos,
918 item.settings.pick_id.0,
919 &gpu_set.cpu_positions,
920 model,
921 view_proj,
922 viewport_size,
923 radius_px,
924 ) {
925 let toi = (hit.world_pos - ray_origin).dot(ray_dir).max(0.0);
927 if wants_splat {
928 if let Some(SubObjectRef::Point(idx)) = hit.sub_object {
929 hit.sub_object = Some(SubObjectRef::Splat(idx));
930 }
931 } else {
932 hit.sub_object = None;
933 }
934 consider(toi, hit);
935 }
936 }
937 }
938
939 let wants_instance = mask.intersects(PickMask::INSTANCE);
941 if wants_instance || wants_object {
942 let instance_radius_px = |world_center: glam::Vec3, world_r: f32| -> f32 {
946 let p0 = view_proj * world_center.extend(1.0);
947 let p1 = view_proj * (world_center + glam::Vec3::X * world_r).extend(1.0);
948 if p0.w.abs() > 1e-6 && p1.w.abs() > 1e-6 {
949 let n0 = glam::Vec2::new(p0.x, p0.y) / p0.w;
950 let n1 = glam::Vec2::new(p1.x, p1.y) / p1.w;
951 ((n1 - n0).length() * 0.5 * viewport_size.x.max(viewport_size.y)).max(4.0)
952 } else {
953 (world_r * 100.0_f32).max(4.0)
954 }
955 };
956
957 for item in &self.pick_glyph_items {
959 if item.settings.pick_id == PickId::NONE || item.positions.is_empty() {
960 continue;
961 }
962 let model = glam::Mat4::from_cols_array_2d(&item.model);
963 let full_len = if item.scale_by_magnitude && !item.vectors.is_empty() {
964 let mean_mag = item
965 .vectors
966 .iter()
967 .map(|v| glam::Vec3::from(*v).length())
968 .sum::<f32>()
969 / item.vectors.len() as f32;
970 (mean_mag * item.scale).max(0.01)
971 } else {
972 item.scale.max(0.01)
973 };
974 let has_vecs = item.vectors.len() == item.positions.len();
978 let midpoints: Vec<[f32; 3]> = item
979 .positions
980 .iter()
981 .enumerate()
982 .map(|(i, pos)| {
983 if has_vecs {
984 let p = glam::Vec3::from(*pos);
985 let v = glam::Vec3::from(item.vectors[i]);
986 let len = if item.scale_by_magnitude {
987 v.length() * item.scale
988 } else {
989 item.scale
990 };
991 (p + v.normalize_or_zero() * len * 0.5).to_array()
992 } else {
993 *pos
994 }
995 })
996 .collect();
997 let n = midpoints.len() as f32;
998 let centroid = model.transform_point3(
999 midpoints
1000 .iter()
1001 .map(|p| glam::Vec3::from(*p))
1002 .sum::<glam::Vec3>()
1003 / n,
1004 );
1005 let radius_px = instance_radius_px(centroid, full_len * 0.5);
1006 if let Some(mut hit) = pick_gaussian_splat_cpu(
1007 click_pos,
1008 item.settings.pick_id.0,
1009 &midpoints,
1010 model,
1011 view_proj,
1012 viewport_size,
1013 radius_px,
1014 ) {
1015 if let Some(SubObjectRef::Point(idx)) = hit.sub_object {
1017 if let Some(base) = item.positions.get(idx as usize) {
1018 hit.world_pos = model.transform_point3(glam::Vec3::from(*base));
1019 }
1020 if wants_instance {
1021 hit.sub_object = Some(SubObjectRef::Instance(idx));
1022 } else {
1023 hit.sub_object = None;
1024 }
1025 }
1026 let toi = (hit.world_pos - ray_origin).dot(ray_dir).max(0.0);
1027 consider(toi, hit);
1028 }
1029 }
1030
1031 for item in &self.pick_tensor_glyph_items {
1033 if item.settings.pick_id == PickId::NONE || item.positions.is_empty() {
1034 continue;
1035 }
1036 let model = glam::Mat4::from_cols_array_2d(&item.model);
1037 let world_r = if !item.eigenvalues.is_empty() {
1041 let max_ev = item
1042 .eigenvalues
1043 .iter()
1044 .map(|ev| ev[0].abs().max(ev[1].abs()).max(ev[2].abs()))
1045 .fold(0.0_f32, f32::max);
1046 (max_ev * item.scale).max(0.01)
1047 } else {
1048 item.scale.max(0.01)
1049 };
1050 let n = item.positions.len() as f32;
1051 let centroid = model.transform_point3(
1052 item.positions
1053 .iter()
1054 .map(|p| glam::Vec3::from(*p))
1055 .sum::<glam::Vec3>()
1056 / n,
1057 );
1058 let radius_px = instance_radius_px(centroid, world_r);
1059 if let Some(mut hit) = pick_gaussian_splat_cpu(
1060 click_pos,
1061 item.settings.pick_id.0,
1062 &item.positions,
1063 model,
1064 view_proj,
1065 viewport_size,
1066 radius_px,
1067 ) {
1068 let toi = (hit.world_pos - ray_origin).dot(ray_dir).max(0.0);
1069 if wants_instance {
1070 if let Some(SubObjectRef::Point(idx)) = hit.sub_object {
1071 hit.sub_object = Some(SubObjectRef::Instance(idx));
1072 }
1073 } else {
1074 hit.sub_object = None;
1075 }
1076 consider(toi, hit);
1077 }
1078 }
1079
1080 for item in &self.pick_sprite_items {
1082 if item.settings.pick_id == PickId::NONE || item.positions.is_empty() {
1083 continue;
1084 }
1085 let model = glam::Mat4::from_cols_array_2d(&item.model);
1086 let radius_px = match item.size_mode {
1087 SpriteSizeMode::ScreenSpace => (item.default_size * 0.5).max(4.0),
1088 SpriteSizeMode::WorldSpace => {
1089 let n = item.positions.len() as f32;
1090 let centroid = model.transform_point3(
1091 item.positions
1092 .iter()
1093 .map(|p| glam::Vec3::from(*p))
1094 .sum::<glam::Vec3>()
1095 / n,
1096 );
1097 instance_radius_px(centroid, (item.default_size * 0.5).max(0.01))
1098 }
1099 };
1100 if let Some(mut hit) = pick_gaussian_splat_cpu(
1101 click_pos,
1102 item.settings.pick_id.0,
1103 &item.positions,
1104 model,
1105 view_proj,
1106 viewport_size,
1107 radius_px,
1108 ) {
1109 let toi = (hit.world_pos - ray_origin).dot(ray_dir).max(0.0);
1110 if wants_instance {
1111 if let Some(SubObjectRef::Point(idx)) = hit.sub_object {
1112 hit.sub_object = Some(SubObjectRef::Instance(idx));
1113 }
1114 } else {
1115 hit.sub_object = None;
1116 }
1117 consider(toi, hit);
1118 }
1119 }
1120 }
1121
1122 let wants_poly_node = mask.intersects(PickMask::POLY_NODE);
1124 let wants_strip = mask.intersects(PickMask::STRIP);
1125 if wants_poly_node || wants_strip || wants_object {
1126 for item in &self.pick_polyline_items {
1127 if item.settings.pick_id == PickId::NONE || item.positions.is_empty() {
1128 continue;
1129 }
1130 let radius_px = (item.line_width + 4.0).max(8.0);
1131 if let Some(mut hit) = pick_gaussian_splat_cpu(
1132 click_pos,
1133 item.settings.pick_id.0,
1134 &item.positions,
1135 glam::Mat4::IDENTITY,
1136 view_proj,
1137 viewport_size,
1138 radius_px,
1139 ) {
1140 let toi = (hit.world_pos - ray_origin).dot(ray_dir).max(0.0);
1141 if wants_poly_node {
1142 } else if wants_strip {
1144 if let Some(SubObjectRef::Point(idx)) = hit.sub_object {
1145 hit.sub_object = Some(SubObjectRef::Strip(strip_for_node(
1146 idx,
1147 &item.strip_lengths,
1148 )));
1149 }
1150 } else {
1151 hit.sub_object = None;
1152 }
1153 consider(toi, hit);
1154 }
1155 }
1156 }
1157
1158 let wants_segment = mask.intersects(PickMask::SEGMENT);
1162 if wants_segment || wants_strip || wants_object {
1163 for item in &self.pick_polyline_items {
1164 if item.settings.pick_id == PickId::NONE || item.positions.is_empty() {
1165 continue;
1166 }
1167 let threshold_px = (item.line_width / 2.0 + 4.0).max(4.0);
1169 let Some((seg_idx, world_pos)) = pick_closest_polyline_segment(
1170 click_pos,
1171 viewport_size,
1172 view_proj,
1173 &item.positions,
1174 &item.strip_lengths,
1175 threshold_px,
1176 ) else {
1177 continue;
1178 };
1179 let toi = (world_pos - ray_origin).dot(ray_dir).max(0.0);
1180 let sub_object = if wants_segment {
1181 Some(SubObjectRef::Segment(seg_idx))
1182 } else if wants_strip {
1183 Some(SubObjectRef::Strip(strip_for_segment(
1184 seg_idx,
1185 &item.strip_lengths,
1186 )))
1187 } else {
1188 None
1189 };
1190 #[allow(deprecated)]
1191 let hit = PickHit {
1192 id: item.settings.pick_id.0,
1193 sub_object,
1194 world_pos,
1195 normal: glam::Vec3::Z,
1196 triangle_index: u32::MAX,
1197 point_index: None,
1198 scalar_value: None,
1199 };
1200 consider(toi, hit);
1201 }
1202 }
1203
1204 if wants_poly_node || wants_segment || wants_strip || wants_object {
1211 let world_r_to_px = |ref_world: glam::Vec3, world_r: f32| -> f32 {
1213 let p0 = view_proj * ref_world.extend(1.0);
1214 let p1 = view_proj * (ref_world + glam::Vec3::X * world_r).extend(1.0);
1215 if p0.w.abs() > 1e-6 && p1.w.abs() > 1e-6 {
1216 let n0 = glam::Vec2::new(p0.x, p0.y) / p0.w;
1217 let n1 = glam::Vec2::new(p1.x, p1.y) / p1.w;
1218 ((n1 - n0).length() * 0.5 * viewport_size.x.max(viewport_size.y)).max(4.0)
1219 } else {
1220 (world_r * 100.0_f32).max(4.0)
1221 }
1222 };
1223
1224 if wants_poly_node || wants_strip || wants_object {
1226 for item in &self.pick_streamtube_items {
1227 if item.settings.pick_id == PickId::NONE || item.positions.is_empty() {
1228 continue;
1229 }
1230 let ref_pos = glam::Vec3::from(item.positions[0]);
1231 let radius_px = world_r_to_px(ref_pos, item.radius.max(0.01)).max(8.0);
1232 if let Some(mut hit) = pick_gaussian_splat_cpu(
1233 click_pos,
1234 item.settings.pick_id.0,
1235 &item.positions,
1236 glam::Mat4::IDENTITY,
1237 view_proj,
1238 viewport_size,
1239 radius_px,
1240 ) {
1241 let toi = (hit.world_pos - ray_origin).dot(ray_dir).max(0.0);
1242 if wants_poly_node {
1243 } else if wants_strip {
1245 if let Some(SubObjectRef::Point(idx)) = hit.sub_object {
1246 hit.sub_object = Some(SubObjectRef::Strip(strip_for_node(
1247 idx,
1248 &item.strip_lengths,
1249 )));
1250 }
1251 } else {
1252 hit.sub_object = None;
1253 }
1254 consider(toi, hit);
1255 }
1256 }
1257 for item in &self.pick_tube_items {
1258 if item.settings.pick_id == PickId::NONE || item.positions.is_empty() {
1259 continue;
1260 }
1261 let ref_pos = glam::Vec3::from(item.positions[0]);
1262 let max_r = item
1263 .radius_attribute
1264 .as_ref()
1265 .and_then(|ra| ra.iter().copied().reduce(f32::max))
1266 .unwrap_or(0.0)
1267 .max(item.radius)
1268 .max(0.01);
1269 let radius_px = world_r_to_px(ref_pos, max_r).max(8.0);
1270 if let Some(mut hit) = pick_gaussian_splat_cpu(
1271 click_pos,
1272 item.settings.pick_id.0,
1273 &item.positions,
1274 glam::Mat4::IDENTITY,
1275 view_proj,
1276 viewport_size,
1277 radius_px,
1278 ) {
1279 let toi = (hit.world_pos - ray_origin).dot(ray_dir).max(0.0);
1280 if wants_poly_node {
1281 } else if wants_strip {
1283 if let Some(SubObjectRef::Point(idx)) = hit.sub_object {
1284 hit.sub_object = Some(SubObjectRef::Strip(strip_for_node(
1285 idx,
1286 &item.strip_lengths,
1287 )));
1288 }
1289 } else {
1290 hit.sub_object = None;
1291 }
1292 consider(toi, hit);
1293 }
1294 }
1295 for item in &self.pick_ribbon_items {
1296 if item.settings.pick_id == PickId::NONE || item.positions.is_empty() {
1297 continue;
1298 }
1299 let ref_pos = glam::Vec3::from(item.positions[0]);
1300 let radius_px = world_r_to_px(ref_pos, item.width * 0.5).max(8.0);
1301 if let Some(mut hit) = pick_gaussian_splat_cpu(
1302 click_pos,
1303 item.settings.pick_id.0,
1304 &item.positions,
1305 glam::Mat4::IDENTITY,
1306 view_proj,
1307 viewport_size,
1308 radius_px,
1309 ) {
1310 let toi = (hit.world_pos - ray_origin).dot(ray_dir).max(0.0);
1311 if wants_poly_node {
1312 } else if wants_strip {
1314 if let Some(SubObjectRef::Point(idx)) = hit.sub_object {
1315 hit.sub_object = Some(SubObjectRef::Strip(strip_for_node(
1316 idx,
1317 &item.strip_lengths,
1318 )));
1319 }
1320 } else {
1321 hit.sub_object = None;
1322 }
1323 consider(toi, hit);
1324 }
1325 }
1326 }
1327
1328 if wants_segment || wants_strip || wants_object {
1330 for item in &self.pick_streamtube_items {
1333 if item.settings.pick_id == PickId::NONE || item.positions.is_empty() {
1334 continue;
1335 }
1336 let ref_pos = glam::Vec3::from(item.positions[0]);
1337 let threshold_px = world_r_to_px(ref_pos, item.radius.max(0.01));
1338 let Some((seg_idx, world_pos)) = pick_closest_polyline_segment(
1339 click_pos,
1340 viewport_size,
1341 view_proj,
1342 &item.positions,
1343 &item.strip_lengths,
1344 threshold_px,
1345 ) else {
1346 continue;
1347 };
1348 let toi = (world_pos - ray_origin).dot(ray_dir).max(0.0);
1349 let sub_object = if wants_segment {
1350 Some(SubObjectRef::Segment(seg_idx))
1351 } else if wants_strip {
1352 Some(SubObjectRef::Strip(strip_for_segment(
1353 seg_idx,
1354 &item.strip_lengths,
1355 )))
1356 } else {
1357 None
1358 };
1359 #[allow(deprecated)]
1360 consider(
1361 toi,
1362 PickHit {
1363 id: item.settings.pick_id.0,
1364 sub_object,
1365 world_pos,
1366 normal: glam::Vec3::Z,
1367 triangle_index: u32::MAX,
1368 point_index: None,
1369 scalar_value: None,
1370 },
1371 );
1372 }
1373
1374 for item in &self.pick_tube_items {
1377 if item.settings.pick_id == PickId::NONE || item.positions.is_empty() {
1378 continue;
1379 }
1380 let ref_pos = glam::Vec3::from(item.positions[0]);
1381 let max_r = item
1382 .radius_attribute
1383 .as_ref()
1384 .and_then(|ra| ra.iter().copied().reduce(f32::max))
1385 .unwrap_or(0.0)
1386 .max(item.radius)
1387 .max(0.01);
1388 let threshold_px = world_r_to_px(ref_pos, max_r);
1389 let Some((seg_idx, world_pos)) = pick_closest_polyline_segment(
1390 click_pos,
1391 viewport_size,
1392 view_proj,
1393 &item.positions,
1394 &item.strip_lengths,
1395 threshold_px,
1396 ) else {
1397 continue;
1398 };
1399 let toi = (world_pos - ray_origin).dot(ray_dir).max(0.0);
1400 let sub_object = if wants_segment {
1401 Some(SubObjectRef::Segment(seg_idx))
1402 } else if wants_strip {
1403 Some(SubObjectRef::Strip(strip_for_segment(
1404 seg_idx,
1405 &item.strip_lengths,
1406 )))
1407 } else {
1408 None
1409 };
1410 #[allow(deprecated)]
1411 consider(
1412 toi,
1413 PickHit {
1414 id: item.settings.pick_id.0,
1415 sub_object,
1416 world_pos,
1417 normal: glam::Vec3::Z,
1418 triangle_index: u32::MAX,
1419 point_index: None,
1420 scalar_value: None,
1421 },
1422 );
1423 }
1424
1425 for item in &self.pick_ribbon_items {
1428 if item.settings.pick_id == PickId::NONE || item.positions.is_empty() {
1429 continue;
1430 }
1431 let frames = ribbon_lateral_frames(
1432 &item.positions,
1433 &item.strip_lengths,
1434 item.width,
1435 item.width_attribute.as_deref(),
1436 item.twist_attribute.as_deref(),
1437 );
1438
1439 let single;
1440 let strips: &[u32] = if item.strip_lengths.is_empty() {
1441 single = [item.positions.len() as u32];
1442 &single
1443 } else {
1444 &item.strip_lengths
1445 };
1446
1447 let mut best_t = f32::MAX;
1448 let mut best_seg: Option<(u32, glam::Vec3)> = None;
1449 let mut node_off = 0usize;
1450 let mut seg_off = 0u32;
1451
1452 for &slen in strips {
1453 let slen = slen as usize;
1454 for k in 0..slen.saturating_sub(1) {
1455 let ia = node_off + k;
1456 let ib = node_off + k + 1;
1457 let pa = glam::Vec3::from(item.positions[ia]);
1458 let pb = glam::Vec3::from(item.positions[ib]);
1459 let (ua, wa) = frames[ia];
1460 let (ub, wb) = frames[ib];
1461 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)
1468 .or_else(|| ray_triangle(ray_origin, ray_dir, c1, c3, c2))
1469 .or_else(|| ray_triangle(ray_origin, ray_dir, c2, c1, c0))
1470 .or_else(|| ray_triangle(ray_origin, ray_dir, c2, c3, c1));
1471 if let Some(t) = t {
1472 if t < best_t {
1473 best_t = t;
1474 best_seg = Some((seg_off + k as u32, ray_origin + ray_dir * t));
1475 }
1476 }
1477 }
1478 seg_off += slen.saturating_sub(1) as u32;
1479 node_off += slen;
1480 }
1481
1482 if let Some((seg_idx, world_pos)) = best_seg {
1483 let sub_object = if wants_segment {
1484 Some(SubObjectRef::Segment(seg_idx))
1485 } else if wants_strip {
1486 Some(SubObjectRef::Strip(strip_for_segment(
1487 seg_idx,
1488 &item.strip_lengths,
1489 )))
1490 } else {
1491 None
1492 };
1493 #[allow(deprecated)]
1494 consider(
1495 best_t,
1496 PickHit {
1497 id: item.settings.pick_id.0,
1498 sub_object,
1499 world_pos,
1500 normal: glam::Vec3::Z,
1501 triangle_index: u32::MAX,
1502 point_index: None,
1503 scalar_value: None,
1504 },
1505 );
1506 }
1507 }
1508 }
1509 }
1510
1511 if wants_object {
1513 for item in &self.pick_image_slice_items {
1515 if item.settings.pick_id == PickId::NONE {
1516 continue;
1517 }
1518 let [bmin, bmax] = [item.bbox_min, item.bbox_max];
1519 let t = item.offset;
1520 let (axis_idx, plane_pos) = match item.axis {
1522 SliceAxis::X => (0usize, bmin[0] + t * (bmax[0] - bmin[0])),
1523 SliceAxis::Y => (1usize, bmin[1] + t * (bmax[1] - bmin[1])),
1524 SliceAxis::Z => (2usize, bmin[2] + t * (bmax[2] - bmin[2])),
1525 };
1526 let plane_n = {
1527 let mut n = glam::Vec3::ZERO;
1528 n[axis_idx] = 1.0;
1529 n
1530 };
1531 let denom = plane_n.dot(ray_dir);
1532 if denom.abs() < 1e-6 {
1533 continue;
1534 }
1535 let toi = (plane_pos - ray_origin[axis_idx]) / denom;
1536 if toi <= 0.0 {
1537 continue;
1538 }
1539 let hit_pos = ray_origin + ray_dir * toi;
1540 let in_bounds = (0..3)
1542 .filter(|&i| i != axis_idx)
1543 .all(|i| hit_pos[i] >= bmin[i] - 1e-4 && hit_pos[i] <= bmax[i] + 1e-4);
1544 if in_bounds {
1545 #[allow(deprecated)]
1546 consider(
1547 toi,
1548 PickHit {
1549 id: item.settings.pick_id.0,
1550 sub_object: None,
1551 world_pos: hit_pos,
1552 normal: plane_n,
1553 triangle_index: u32::MAX,
1554 point_index: None,
1555 scalar_value: None,
1556 },
1557 );
1558 }
1559 }
1560
1561 for item in &self.pick_volume_surface_slice_items {
1563 if item.settings.pick_id == PickId::NONE {
1564 continue;
1565 }
1566 let Some(mesh) = self.resources.mesh_store.get(item.mesh_id) else {
1567 continue;
1568 };
1569 let (Some(positions), Some(indices)) = (&mesh.cpu_positions, &mesh.cpu_indices)
1570 else {
1571 continue;
1572 };
1573 let model = glam::Mat4::from_cols_array_2d(&item.model);
1574 let verts: Vec<parry3d::math::Vector> = positions
1575 .iter()
1576 .map(|p| {
1577 let wp = model.transform_point3(glam::Vec3::from(*p));
1578 parry3d::math::Vector::new(wp.x, wp.y, wp.z)
1579 })
1580 .collect();
1581 let tri_indices: Vec<[u32; 3]> = indices
1582 .chunks(3)
1583 .filter(|c| c.len() == 3)
1584 .map(|c| [c[0], c[1], c[2]])
1585 .collect();
1586 if tri_indices.is_empty() {
1587 continue;
1588 }
1589 let ray = parry3d::query::Ray::new(
1590 parry3d::math::Vector::new(ray_origin.x, ray_origin.y, ray_origin.z),
1591 parry3d::math::Vector::new(ray_dir.x, ray_dir.y, ray_dir.z),
1592 );
1593 if let Ok(trimesh) = parry3d::shape::TriMesh::new(verts, tri_indices) {
1594 use parry3d::query::RayCast;
1595 if let Some(hit) = trimesh.cast_ray_and_get_normal(
1596 &parry3d::math::Pose::identity(),
1597 &ray,
1598 f32::MAX,
1599 true,
1600 ) {
1601 let world_pos = ray_origin + ray_dir * hit.time_of_impact;
1602 let n = hit.normal;
1603 #[allow(deprecated)]
1604 consider(
1605 hit.time_of_impact,
1606 PickHit {
1607 id: item.settings.pick_id.0,
1608 sub_object: None,
1609 world_pos,
1610 normal: glam::Vec3::new(n.x, n.y, n.z),
1611 triangle_index: u32::MAX,
1612 point_index: None,
1613 scalar_value: None,
1614 },
1615 );
1616 }
1617 }
1618 }
1619
1620 for item in &self.pick_screen_image_items {
1622 if item.settings.pick_id == PickId::NONE || item.width == 0 || item.height == 0 {
1623 continue;
1624 }
1625 let img_w = item.width as f32 * item.scale;
1626 let img_h = item.height as f32 * item.scale;
1627 let (sx, sy) = match item.anchor {
1628 ImageAnchor::TopLeft => (0.0, 0.0),
1629 ImageAnchor::TopRight => (viewport_size.x - img_w, 0.0),
1630 ImageAnchor::BottomLeft => (0.0, viewport_size.y - img_h),
1631 ImageAnchor::BottomRight => (viewport_size.x - img_w, viewport_size.y - img_h),
1632 ImageAnchor::Center => (
1633 (viewport_size.x - img_w) * 0.5,
1634 (viewport_size.y - img_h) * 0.5,
1635 ),
1636 };
1637 if click_pos.x >= sx
1638 && click_pos.x <= sx + img_w
1639 && click_pos.y >= sy
1640 && click_pos.y <= sy + img_h
1641 {
1642 let world_pos = ray_origin + ray_dir * 0.001;
1644 #[allow(deprecated)]
1645 consider(
1646 0.0,
1647 PickHit {
1648 id: item.settings.pick_id.0,
1649 sub_object: None,
1650 world_pos,
1651 normal: -ray_dir,
1652 triangle_index: u32::MAX,
1653 point_index: None,
1654 scalar_value: None,
1655 },
1656 );
1657 }
1658 }
1659 }
1660
1661 if wants_object {
1663 for item in &self.pick_implicit_items {
1664 if let Some((toi, world_pos)) = pick_implicit_sdf(ray_origin, ray_dir, item) {
1665 #[allow(deprecated)]
1666 consider(
1667 toi,
1668 PickHit {
1669 id: item.id,
1670 sub_object: None,
1671 world_pos,
1672 normal: glam::Vec3::Z,
1673 triangle_index: u32::MAX,
1674 point_index: None,
1675 scalar_value: None,
1676 },
1677 );
1678 }
1679 }
1680 }
1681
1682 if wants_object {
1684 for item in &self.pick_mc_items {
1685 if let Some((toi, world_pos)) = pick_mc_volume(ray_origin, ray_dir, item) {
1686 #[allow(deprecated)]
1687 consider(
1688 toi,
1689 PickHit {
1690 id: item.id,
1691 sub_object: None,
1692 world_pos,
1693 normal: glam::Vec3::Z,
1694 triangle_index: u32::MAX,
1695 point_index: None,
1696 scalar_value: None,
1697 },
1698 );
1699 }
1700 }
1701 }
1702
1703 if !self.item_type_plugins.is_empty() {
1707 let plugin_ray = crate::plugin_api::PickRay {
1708 origin: ray_origin,
1709 direction: ray_dir,
1710 };
1711 for plugin in self.item_type_plugins.values() {
1712 if let Some((t, hit)) = plugin.pick(&plugin_ray) {
1713 consider(t, hit);
1714 }
1715 }
1716 }
1717
1718 best.map(|(_, hit)| hit)
1719 }
1720
1721 pub fn pick_rect(
1737 &self,
1738 rect_min: glam::Vec2,
1739 rect_max: glam::Vec2,
1740 viewport_size: glam::Vec2,
1741 view_proj: glam::Mat4,
1742 mask: crate::interaction::pick_mask::PickMask,
1743 ) -> PickRectResult {
1744 use crate::interaction::pick_mask::PickMask;
1745 use crate::interaction::sub_object::SubObjectRef;
1746
1747 let mut result = PickRectResult::default();
1748
1749 if viewport_size.x <= 0.0 || viewport_size.y <= 0.0 {
1750 return result;
1751 }
1752
1753 let wants_face = mask.intersects(PickMask::FACE);
1754 let wants_vertex = mask.intersects(PickMask::VERTEX);
1755 let wants_cell = mask.intersects(PickMask::CELL);
1756 let wants_cloud = mask.intersects(PickMask::CLOUD_POINT);
1757 let wants_splat = mask.intersects(PickMask::SPLAT);
1758 let wants_object = mask.intersects(PickMask::OBJECT);
1759
1760 let vm_cell_map: std::collections::HashMap<u64, &[u32]> = self
1762 .pick_volume_mesh_items
1763 .iter()
1764 .filter(|item| item.settings.pick_id != PickId::NONE && !item.face_to_cell.is_empty())
1765 .map(|item| (item.settings.pick_id.0, item.face_to_cell.as_slice()))
1766 .collect();
1767
1768 let project = |mvp: glam::Mat4, local: glam::Vec3| -> Option<(f32, f32)> {
1771 let clip = mvp * local.extend(1.0);
1772 if clip.w <= 0.0 {
1773 return None;
1774 }
1775 let sx = (clip.x / clip.w + 1.0) * 0.5 * viewport_size.x;
1776 let sy = (1.0 - clip.y / clip.w) * 0.5 * viewport_size.y;
1777 Some((sx, sy))
1778 };
1779
1780 let in_rect = |sx: f32, sy: f32| -> bool {
1781 sx >= rect_min.x && sx <= rect_max.x && sy >= rect_min.y && sy <= rect_max.y
1782 };
1783
1784 if wants_face || wants_vertex || wants_cell || wants_object {
1786 for item in &self.pick_scene_items {
1787 if item.settings.hidden || item.settings.pick_id == PickId::NONE {
1788 continue;
1789 }
1790 let Some(mesh) = self.resources.mesh_store.get(item.mesh_id) else {
1791 continue;
1792 };
1793 let (Some(positions), Some(indices)) = (&mesh.cpu_positions, &mesh.cpu_indices)
1794 else {
1795 continue;
1796 };
1797
1798 let model = glam::Mat4::from_cols_array_2d(&item.model);
1799 let mvp = view_proj * model;
1800 let id = item.settings.pick_id.0;
1801 let mut item_hit = false;
1802
1803 if wants_face {
1804 for (tri_idx, chunk) in indices.chunks(3).enumerate() {
1805 if chunk.len() < 3 {
1806 continue;
1807 }
1808 let [i0, i1, i2] =
1809 [chunk[0] as usize, chunk[1] as usize, chunk[2] as usize];
1810 if i0 >= positions.len() || i1 >= positions.len() || i2 >= positions.len() {
1811 continue;
1812 }
1813 let centroid = (glam::Vec3::from(positions[i0])
1814 + glam::Vec3::from(positions[i1])
1815 + glam::Vec3::from(positions[i2]))
1816 / 3.0;
1817 if let Some((sx, sy)) = project(mvp, centroid) {
1818 if in_rect(sx, sy) {
1819 result
1820 .elements
1821 .push((id, SubObjectRef::Face(tri_idx as u32)));
1822 item_hit = true;
1823 }
1824 }
1825 }
1826 } else if wants_cell {
1827 if let Some(f2c) = vm_cell_map.get(&id) {
1829 let mut seen = std::collections::HashSet::new();
1830 for (tri_idx, chunk) in indices.chunks(3).enumerate() {
1831 if chunk.len() < 3 {
1832 continue;
1833 }
1834 let [i0, i1, i2] =
1835 [chunk[0] as usize, chunk[1] as usize, chunk[2] as usize];
1836 if i0 >= positions.len()
1837 || i1 >= positions.len()
1838 || i2 >= positions.len()
1839 {
1840 continue;
1841 }
1842 let centroid = (glam::Vec3::from(positions[i0])
1843 + glam::Vec3::from(positions[i1])
1844 + glam::Vec3::from(positions[i2]))
1845 / 3.0;
1846 if let Some((sx, sy)) = project(mvp, centroid) {
1847 if in_rect(sx, sy) {
1848 if let Some(&ci) = f2c.get(tri_idx) {
1849 if seen.insert(ci) {
1850 result.elements.push((id, SubObjectRef::Cell(ci)));
1851 }
1852 }
1853 item_hit = true;
1854 }
1855 }
1856 }
1857 } else if wants_vertex {
1858 for (vi, pos) in positions.iter().enumerate() {
1860 if let Some((sx, sy)) = project(mvp, glam::Vec3::from(*pos)) {
1861 if in_rect(sx, sy) {
1862 result.elements.push((id, SubObjectRef::Vertex(vi as u32)));
1863 item_hit = true;
1864 }
1865 }
1866 }
1867 }
1868 } else if wants_vertex {
1869 for (vi, pos) in positions.iter().enumerate() {
1870 if let Some((sx, sy)) = project(mvp, glam::Vec3::from(*pos)) {
1871 if in_rect(sx, sy) {
1872 result.elements.push((id, SubObjectRef::Vertex(vi as u32)));
1873 item_hit = true;
1874 }
1875 }
1876 }
1877 } else {
1878 'tri_scan: for chunk in indices.chunks(3) {
1880 if chunk.len() < 3 {
1881 continue;
1882 }
1883 let [i0, i1, i2] =
1884 [chunk[0] as usize, chunk[1] as usize, chunk[2] as usize];
1885 if i0 >= positions.len() || i1 >= positions.len() || i2 >= positions.len() {
1886 continue;
1887 }
1888 let centroid = (glam::Vec3::from(positions[i0])
1889 + glam::Vec3::from(positions[i1])
1890 + glam::Vec3::from(positions[i2]))
1891 / 3.0;
1892 if let Some((sx, sy)) = project(mvp, centroid) {
1893 if in_rect(sx, sy) {
1894 item_hit = true;
1895 break 'tri_scan;
1896 }
1897 }
1898 }
1899 }
1900
1901 if wants_object && item_hit {
1902 result.objects.push(id);
1903 }
1904 }
1905 }
1906
1907 if wants_cell || wants_object {
1914 for item in &self.pick_volume_mesh_items {
1915 if item.settings.pick_id == PickId::NONE || item.transparency.is_none() {
1916 continue;
1917 }
1918 let Some(data) = item.volume_mesh_data.as_deref() else {
1919 continue;
1920 };
1921 use crate::resources::volume_mesh::CELL_SENTINEL;
1922 let id = item.settings.pick_id.0;
1923 let mvp = view_proj * glam::Mat4::from_cols_array_2d(&item.model);
1924 let mut item_hit = false;
1925
1926 for (cell_idx, cell) in data.cells.iter().enumerate() {
1927 let nv: usize = if cell[4] == CELL_SENTINEL {
1928 4
1929 } else if cell[5] == CELL_SENTINEL {
1930 5
1931 } else if cell[6] == CELL_SENTINEL {
1932 6
1933 } else {
1934 8
1935 };
1936 let centroid: glam::Vec3 = cell[..nv]
1937 .iter()
1938 .map(|&vi| glam::Vec3::from(data.positions[vi as usize]))
1939 .sum::<glam::Vec3>()
1940 / nv as f32;
1941 if let Some((sx, sy)) = project(mvp, centroid) {
1942 if in_rect(sx, sy) {
1943 if wants_cell {
1944 result
1945 .elements
1946 .push((id, SubObjectRef::Cell(cell_idx as u32)));
1947 }
1948 item_hit = true;
1949 }
1950 }
1951 }
1952
1953 if wants_object && item_hit {
1954 result.objects.push(id);
1955 }
1956 }
1957 }
1958
1959 if wants_cloud || wants_object {
1961 for item in &self.pick_point_cloud_items {
1962 if item.settings.pick_id == PickId::NONE || item.positions.is_empty() {
1963 continue;
1964 }
1965 let model = glam::Mat4::from_cols_array_2d(&item.model);
1966 let mvp = view_proj * model;
1967 let id = item.settings.pick_id.0;
1968 let mut item_hit = false;
1969
1970 for (pt_idx, pos) in item.positions.iter().enumerate() {
1971 if let Some((sx, sy)) = project(mvp, glam::Vec3::from(*pos)) {
1972 if in_rect(sx, sy) {
1973 if wants_cloud {
1974 result
1975 .elements
1976 .push((id, SubObjectRef::Point(pt_idx as u32)));
1977 }
1978 item_hit = true;
1979 }
1980 }
1981 }
1982
1983 if wants_object && item_hit {
1984 result.objects.push(id);
1985 }
1986 }
1987 }
1988
1989 let wants_voxel = mask.intersects(PickMask::VOXEL);
1991 if wants_voxel || wants_object {
1992 for item in &self.pick_volume_items {
1993 if item.settings.pick_id == PickId::NONE {
1994 continue;
1995 }
1996 let Some(vol_data) = item.volume_data.as_deref() else {
1997 continue;
1998 };
1999 let [nx, ny, nz] = vol_data.dims;
2000 if nx == 0 || ny == 0 || nz == 0 || vol_data.data.is_empty() {
2001 continue;
2002 }
2003 let model = glam::Mat4::from_cols_array_2d(&item.model);
2004 let mvp = view_proj * model;
2005 let bbox_min = glam::Vec3::from(item.bbox_min);
2006 let bbox_max = glam::Vec3::from(item.bbox_max);
2007 let cell = (bbox_max - bbox_min) / glam::Vec3::new(nx as f32, ny as f32, nz as f32);
2008 let id = item.settings.pick_id.0;
2009 let mut item_hit = false;
2010
2011 for iz in 0..nz {
2012 for iy in 0..ny {
2013 for ix in 0..nx {
2014 let flat = (ix + iy * nx + iz * nx * ny) as usize;
2015 let scalar = vol_data.data[flat];
2016 if scalar.is_nan()
2017 || scalar < item.threshold_min
2018 || scalar > item.threshold_max
2019 {
2020 continue;
2021 }
2022 let center = bbox_min
2023 + cell
2024 * glam::Vec3::new(
2025 ix as f32 + 0.5,
2026 iy as f32 + 0.5,
2027 iz as f32 + 0.5,
2028 );
2029 if let Some((sx, sy)) = project(mvp, center) {
2030 if in_rect(sx, sy) {
2031 if wants_voxel {
2032 result
2033 .elements
2034 .push((id, SubObjectRef::Voxel(flat as u32)));
2035 }
2036 item_hit = true;
2037 }
2038 }
2039 }
2040 }
2041 }
2042
2043 if wants_object && item_hit {
2044 result.objects.push(id);
2045 }
2046 }
2047 }
2048
2049 if wants_splat || wants_object {
2051 for item in &self.pick_splat_items {
2052 if item.settings.pick_id == PickId::NONE {
2053 continue;
2054 }
2055 let Some(gpu_set) = self.resources.gaussian_splat_store.get(item.id.0) else {
2056 continue;
2057 };
2058 if gpu_set.cpu_positions.is_empty() {
2059 continue;
2060 }
2061 let model = glam::Mat4::from_cols_array_2d(&item.model);
2062 let mvp = view_proj * model;
2063 let id = item.settings.pick_id.0;
2064 let mut item_hit = false;
2065
2066 for (i, pos) in gpu_set.cpu_positions.iter().enumerate() {
2067 if let Some((sx, sy)) = project(mvp, glam::Vec3::from(*pos)) {
2068 if in_rect(sx, sy) {
2069 if wants_splat {
2070 result.elements.push((id, SubObjectRef::Splat(i as u32)));
2071 }
2072 item_hit = true;
2073 }
2074 }
2075 }
2076
2077 if wants_object && item_hit {
2078 result.objects.push(id);
2079 }
2080 }
2081 }
2082
2083 let wants_instance = mask.intersects(PickMask::INSTANCE);
2085 if wants_instance || wants_object {
2086 for item in &self.pick_glyph_items {
2088 if item.settings.pick_id == PickId::NONE || item.positions.is_empty() {
2089 continue;
2090 }
2091 let model = glam::Mat4::from_cols_array_2d(&item.model);
2092 let mvp = view_proj * model;
2093 let id = item.settings.pick_id.0;
2094 let mut item_hit = false;
2095 for (i, pos) in item.positions.iter().enumerate() {
2096 if let Some((sx, sy)) = project(mvp, glam::Vec3::from(*pos)) {
2097 if in_rect(sx, sy) {
2098 if wants_instance {
2099 result.elements.push((id, SubObjectRef::Instance(i as u32)));
2100 }
2101 item_hit = true;
2102 }
2103 }
2104 }
2105 if wants_object && item_hit {
2106 result.objects.push(id);
2107 }
2108 }
2109
2110 for item in &self.pick_tensor_glyph_items {
2112 if item.settings.pick_id == PickId::NONE || item.positions.is_empty() {
2113 continue;
2114 }
2115 let model = glam::Mat4::from_cols_array_2d(&item.model);
2116 let mvp = view_proj * model;
2117 let id = item.settings.pick_id.0;
2118 let mut item_hit = false;
2119 for (i, pos) in item.positions.iter().enumerate() {
2120 if let Some((sx, sy)) = project(mvp, glam::Vec3::from(*pos)) {
2121 if in_rect(sx, sy) {
2122 if wants_instance {
2123 result.elements.push((id, SubObjectRef::Instance(i as u32)));
2124 }
2125 item_hit = true;
2126 }
2127 }
2128 }
2129 if wants_object && item_hit {
2130 result.objects.push(id);
2131 }
2132 }
2133
2134 for item in &self.pick_sprite_items {
2136 if item.settings.pick_id == PickId::NONE || item.positions.is_empty() {
2137 continue;
2138 }
2139 let model = glam::Mat4::from_cols_array_2d(&item.model);
2140 let mvp = view_proj * model;
2141 let id = item.settings.pick_id.0;
2142 let mut item_hit = false;
2143 for (i, pos) in item.positions.iter().enumerate() {
2144 if let Some((sx, sy)) = project(mvp, glam::Vec3::from(*pos)) {
2145 if in_rect(sx, sy) {
2146 if wants_instance {
2147 result.elements.push((id, SubObjectRef::Instance(i as u32)));
2148 }
2149 item_hit = true;
2150 }
2151 }
2152 }
2153 if wants_object && item_hit {
2154 result.objects.push(id);
2155 }
2156 }
2157 }
2158
2159 let wants_poly_node = mask.intersects(PickMask::POLY_NODE);
2161 let wants_segment = mask.intersects(PickMask::SEGMENT);
2162 let wants_strip = mask.intersects(PickMask::STRIP);
2163 if wants_poly_node || wants_segment || wants_strip || wants_object {
2164 for item in &self.pick_polyline_items {
2165 if item.settings.pick_id == PickId::NONE || item.positions.is_empty() {
2166 continue;
2167 }
2168 let id = item.settings.pick_id.0;
2169 let mut item_hit = false;
2170 let mut strips_hit = std::collections::HashSet::<u32>::new();
2171
2172 if wants_poly_node || wants_strip || wants_object {
2174 for (node_idx, pos) in item.positions.iter().enumerate() {
2175 if let Some((sx, sy)) = project(view_proj, glam::Vec3::from(*pos)) {
2176 if in_rect(sx, sy) {
2177 item_hit = true;
2178 if wants_poly_node {
2179 result
2180 .elements
2181 .push((id, SubObjectRef::Point(node_idx as u32)));
2182 } else if wants_strip {
2183 let s = strip_for_node(node_idx as u32, &item.strip_lengths);
2184 strips_hit.insert(s);
2185 }
2186 }
2187 }
2188 }
2189 }
2190
2191 if wants_segment || (wants_strip && !wants_poly_node) || wants_object {
2193 let mut node_off = 0usize;
2194 let mut seg_off = 0u32;
2195 macro_rules! try_seg_rect {
2196 ($ai:expr, $bi:expr, $seg:expr) => {{
2197 if let (Some((sax, say)), Some((sbx, sby))) = (
2198 project(view_proj, glam::Vec3::from(item.positions[$ai])),
2199 project(view_proj, glam::Vec3::from(item.positions[$bi])),
2200 ) {
2201 if segment_in_rect(
2202 glam::Vec2::new(sax, say),
2203 glam::Vec2::new(sbx, sby),
2204 rect_min,
2205 rect_max,
2206 ) {
2207 item_hit = true;
2208 if wants_segment {
2209 result.elements.push((id, SubObjectRef::Segment($seg)));
2210 } else if wants_strip {
2211 let s = strip_for_segment($seg, &item.strip_lengths);
2212 strips_hit.insert(s);
2213 }
2214 }
2215 }
2216 }};
2217 }
2218 if item.strip_lengths.is_empty() {
2219 for j in 0..item.positions.len().saturating_sub(1) {
2220 try_seg_rect!(j, j + 1, j as u32);
2221 }
2222 } else {
2223 for &slen in &item.strip_lengths {
2224 let slen = slen as usize;
2225 for j in 0..slen.saturating_sub(1) {
2226 try_seg_rect!(node_off + j, node_off + j + 1, seg_off + j as u32);
2227 }
2228 seg_off += slen.saturating_sub(1) as u32;
2229 node_off += slen;
2230 }
2231 }
2232 }
2233
2234 if wants_strip {
2235 for s in strips_hit {
2236 result.elements.push((id, SubObjectRef::Strip(s)));
2237 }
2238 }
2239 if wants_object && item_hit {
2240 result.objects.push(id);
2241 }
2242 }
2243 }
2244
2245 if wants_poly_node || wants_segment || wants_strip || wants_object {
2247 let st_tube_iter = self
2251 .pick_streamtube_items
2252 .iter()
2253 .map(|it| {
2254 (
2255 it.settings.pick_id.0,
2256 it.positions.as_slice(),
2257 it.strip_lengths.as_slice(),
2258 )
2259 })
2260 .chain(self.pick_tube_items.iter().map(|it| {
2261 (
2262 it.settings.pick_id.0,
2263 it.positions.as_slice(),
2264 it.strip_lengths.as_slice(),
2265 )
2266 }));
2267
2268 for (id, positions, strip_lengths) in st_tube_iter {
2269 if id == 0 || positions.is_empty() {
2270 continue;
2271 }
2272 let mut item_hit = false;
2273 let mut strips_hit = std::collections::HashSet::<u32>::new();
2274
2275 let single_st;
2276 let strips_st: &[u32] = if strip_lengths.is_empty() {
2277 single_st = [positions.len() as u32];
2278 &single_st
2279 } else {
2280 strip_lengths
2281 };
2282
2283 if wants_poly_node || wants_strip || wants_object {
2285 'st_nodes: for (ni, pos) in positions.iter().enumerate() {
2286 if let Some((sx, sy)) = project(view_proj, glam::Vec3::from(*pos)) {
2287 if in_rect(sx, sy) {
2288 item_hit = true;
2289 if wants_poly_node {
2290 result.elements.push((id, SubObjectRef::Point(ni as u32)));
2291 } else if wants_strip {
2292 let s = strip_for_node(ni as u32, strip_lengths);
2293 strips_hit.insert(s);
2294 } else {
2295 break 'st_nodes;
2297 }
2298 }
2299 }
2300 }
2301 }
2302
2303 if wants_segment || wants_strip || wants_object {
2305 let mut node_off = 0usize;
2306 let mut seg_off = 0u32;
2307 'st_strips: for &slen in strips_st {
2308 let slen = slen as usize;
2309 for j in 0..slen.saturating_sub(1) {
2310 let seg_idx = seg_off + j as u32;
2311 let pa = glam::Vec3::from(positions[node_off + j]);
2312 let pb = glam::Vec3::from(positions[node_off + j + 1]);
2313 let hit = match (project(view_proj, pa), project(view_proj, pb)) {
2314 (Some((ax, ay)), Some((bx, by))) => segment_in_rect(
2315 glam::Vec2::new(ax, ay),
2316 glam::Vec2::new(bx, by),
2317 rect_min,
2318 rect_max,
2319 ),
2320 (Some((ax, ay)), None) => in_rect(ax, ay),
2321 (None, Some((bx, by))) => in_rect(bx, by),
2322 (None, None) => false,
2323 };
2324 if hit {
2325 item_hit = true;
2326 if wants_segment {
2327 result.elements.push((id, SubObjectRef::Segment(seg_idx)));
2328 } else if wants_strip {
2329 let s = strip_for_segment(seg_idx, strip_lengths);
2330 strips_hit.insert(s);
2331 } else {
2332 break 'st_strips;
2334 }
2335 }
2336 }
2337 seg_off += slen.saturating_sub(1) as u32;
2338 node_off += slen;
2339 }
2340 }
2341
2342 if wants_strip {
2343 for s in strips_hit {
2344 result.elements.push((id, SubObjectRef::Strip(s)));
2345 }
2346 }
2347 if wants_object && item_hit {
2348 result.objects.push(id);
2349 }
2350 }
2351
2352 for item in &self.pick_ribbon_items {
2357 if item.settings.pick_id == PickId::NONE || item.positions.is_empty() {
2358 continue;
2359 }
2360
2361 let single_r;
2362 let strips_r: &[u32] = if item.strip_lengths.is_empty() {
2363 single_r = [item.positions.len() as u32];
2364 &single_r
2365 } else {
2366 &item.strip_lengths
2367 };
2368
2369 let mut item_hit = false;
2370 let mut strips_hit = std::collections::HashSet::<u32>::new();
2371
2372 let proj2 = |p: glam::Vec3| -> Option<glam::Vec2> {
2374 project(view_proj, p).map(|(x, y)| glam::Vec2::new(x, y))
2375 };
2376
2377 if wants_poly_node || wants_strip || wants_object {
2379 'rb_nodes: for (ni, pos) in item.positions.iter().enumerate() {
2380 if let Some((sx, sy)) = project(view_proj, glam::Vec3::from(*pos)) {
2381 if in_rect(sx, sy) {
2382 item_hit = true;
2383 if wants_poly_node {
2384 result.elements.push((
2385 item.settings.pick_id.0,
2386 SubObjectRef::Point(ni as u32),
2387 ));
2388 } else if wants_strip {
2389 let s = strip_for_node(ni as u32, &item.strip_lengths);
2390 strips_hit.insert(s);
2391 } else {
2392 break 'rb_nodes;
2393 }
2394 }
2395 }
2396 }
2397 }
2398
2399 if wants_segment || wants_strip || wants_object {
2401 let frames = ribbon_lateral_frames(
2402 &item.positions,
2403 &item.strip_lengths,
2404 item.width,
2405 item.width_attribute.as_deref(),
2406 item.twist_attribute.as_deref(),
2407 );
2408 let mut node_off = 0usize;
2409 let mut seg_off = 0u32;
2410
2411 'rb_strips: for &slen in strips_r {
2412 let slen = slen as usize;
2413 for k in 0..slen.saturating_sub(1) {
2414 let seg_idx = seg_off + k as u32;
2415 let ia = node_off + k;
2416 let ib = node_off + k + 1;
2417 let pa = glam::Vec3::from(item.positions[ia]);
2418 let pb = glam::Vec3::from(item.positions[ib]);
2419 let (ua, wa) = frames[ia];
2420 let (ub, wb) = frames[ib];
2421 let c0 = pa + ua * wa; let c1 = pa - ua * wa; let c2 = pb + ub * wb; let c3 = pb - ub * wb; let sc0 = proj2(c0);
2426 let sc1 = proj2(c1);
2427 let sc2 = proj2(c2);
2428 let sc3 = proj2(c3);
2429 let edge_hit = |a: Option<glam::Vec2>, b: Option<glam::Vec2>| -> bool {
2430 match (a, b) {
2431 (Some(a), Some(b)) => segment_in_rect(a, b, rect_min, rect_max),
2432 (Some(a), None) => in_rect(a.x, a.y),
2433 (None, Some(b)) => in_rect(b.x, b.y),
2434 (None, None) => false,
2435 }
2436 };
2437 let hit = edge_hit(sc0, sc1)
2438 || edge_hit(sc2, sc3)
2439 || edge_hit(sc0, sc2)
2440 || edge_hit(sc1, sc3);
2441 if hit {
2442 item_hit = true;
2443 if wants_segment {
2444 result.elements.push((
2445 item.settings.pick_id.0,
2446 SubObjectRef::Segment(seg_idx),
2447 ));
2448 } else if wants_strip {
2449 let s = strip_for_segment(seg_idx, &item.strip_lengths);
2450 strips_hit.insert(s);
2451 } else {
2452 break 'rb_strips;
2453 }
2454 }
2455 }
2456 seg_off += slen.saturating_sub(1) as u32;
2457 node_off += slen;
2458 }
2459 }
2460
2461 if wants_strip {
2462 for s in strips_hit {
2463 result
2464 .elements
2465 .push((item.settings.pick_id.0, SubObjectRef::Strip(s)));
2466 }
2467 }
2468 if wants_object && item_hit {
2469 result.objects.push(item.settings.pick_id.0);
2470 }
2471 }
2472 }
2473
2474 if wants_object {
2476 for item in &self.pick_image_slice_items {
2478 if item.settings.pick_id == PickId::NONE {
2479 continue;
2480 }
2481 let [bmin, bmax] = [item.bbox_min, item.bbox_max];
2482 let t = item.offset;
2483 let corners: [[f32; 3]; 4] = match item.axis {
2484 SliceAxis::X => {
2485 let x = bmin[0] + t * (bmax[0] - bmin[0]);
2486 [
2487 [x, bmin[1], bmin[2]],
2488 [x, bmax[1], bmin[2]],
2489 [x, bmax[1], bmax[2]],
2490 [x, bmin[1], bmax[2]],
2491 ]
2492 }
2493 SliceAxis::Y => {
2494 let y = bmin[1] + t * (bmax[1] - bmin[1]);
2495 [
2496 [bmin[0], y, bmin[2]],
2497 [bmax[0], y, bmin[2]],
2498 [bmax[0], y, bmax[2]],
2499 [bmin[0], y, bmax[2]],
2500 ]
2501 }
2502 SliceAxis::Z => {
2503 let z = bmin[2] + t * (bmax[2] - bmin[2]);
2504 [
2505 [bmin[0], bmin[1], z],
2506 [bmax[0], bmin[1], z],
2507 [bmax[0], bmax[1], z],
2508 [bmin[0], bmax[1], z],
2509 ]
2510 }
2511 };
2512 let sc: Vec<Option<glam::Vec2>> = corners
2513 .iter()
2514 .map(|&c| {
2515 project(view_proj, glam::Vec3::from(c)).map(|(x, y)| glam::Vec2::new(x, y))
2516 })
2517 .collect();
2518 let hit = sc.iter().any(|p| p.map_or(false, |p| in_rect(p.x, p.y)))
2519 || (0..4).any(|i| {
2520 let a = sc[i];
2521 let b = sc[(i + 1) % 4];
2522 match (a, b) {
2523 (Some(a), Some(b)) => segment_in_rect(a, b, rect_min, rect_max),
2524 (Some(a), None) => in_rect(a.x, a.y),
2525 (None, Some(b)) => in_rect(b.x, b.y),
2526 (None, None) => false,
2527 }
2528 });
2529 if hit {
2530 result.objects.push(item.settings.pick_id.0);
2531 }
2532 }
2533
2534 for item in &self.pick_volume_surface_slice_items {
2536 if item.settings.pick_id == PickId::NONE {
2537 continue;
2538 }
2539 let Some(mesh) = self.resources.mesh_store.get(item.mesh_id) else {
2540 continue;
2541 };
2542 let Some(positions) = &mesh.cpu_positions else {
2543 continue;
2544 };
2545 let model = glam::Mat4::from_cols_array_2d(&item.model);
2546 let hit = positions.iter().any(|&p| {
2547 let wp = model.transform_point3(glam::Vec3::from(p));
2548 project(view_proj, wp).map_or(false, |(sx, sy)| in_rect(sx, sy))
2549 });
2550 if hit {
2551 result.objects.push(item.settings.pick_id.0);
2552 }
2553 }
2554
2555 for item in &self.pick_screen_image_items {
2557 if item.settings.pick_id == PickId::NONE || item.width == 0 || item.height == 0 {
2558 continue;
2559 }
2560 let img_w = item.width as f32 * item.scale;
2561 let img_h = item.height as f32 * item.scale;
2562 let (sx, sy) = match item.anchor {
2563 ImageAnchor::TopLeft => (0.0, 0.0),
2564 ImageAnchor::TopRight => (viewport_size.x - img_w, 0.0),
2565 ImageAnchor::BottomLeft => (0.0, viewport_size.y - img_h),
2566 ImageAnchor::BottomRight => (viewport_size.x - img_w, viewport_size.y - img_h),
2567 ImageAnchor::Center => (
2568 (viewport_size.x - img_w) * 0.5,
2569 (viewport_size.y - img_h) * 0.5,
2570 ),
2571 };
2572 let overlap = sx <= rect_max.x
2574 && sx + img_w >= rect_min.x
2575 && sy <= rect_max.y
2576 && sy + img_h >= rect_min.y;
2577 if overlap {
2578 result.objects.push(item.settings.pick_id.0);
2579 }
2580 }
2581 }
2582
2583 if wants_object {
2590 for item in &self.pick_implicit_items {
2591 let mut hit = false;
2592 'prim_loop: for prim in &item.primitives {
2593 let (center, radius) = match prim.kind {
2595 1 => {
2596 let c = glam::Vec3::new(prim.params[0], prim.params[1], prim.params[2]);
2598 (c, prim.params[3].abs())
2599 }
2600 2 => {
2601 let c = glam::Vec3::new(prim.params[0], prim.params[1], prim.params[2]);
2603 let h = glam::Vec3::new(prim.params[4], prim.params[5], prim.params[6]);
2604 (c, h.length())
2605 }
2606 3 => {
2607 continue;
2609 }
2610 4 => {
2611 let a = glam::Vec3::new(prim.params[0], prim.params[1], prim.params[2]);
2613 let b = glam::Vec3::new(prim.params[4], prim.params[5], prim.params[6]);
2614 let r = prim.params[3].abs();
2615 ((a + b) * 0.5, (b - a).length() * 0.5 + r)
2616 }
2617 _ => continue,
2618 };
2619 for dx in [-radius, radius] {
2621 for dy in [-radius, radius] {
2622 for dz in [-radius, radius] {
2623 let corner = center + glam::Vec3::new(dx, dy, dz);
2624 if let Some((sx, sy)) = project(view_proj, corner) {
2625 if in_rect(sx, sy) {
2626 hit = true;
2627 break 'prim_loop;
2628 }
2629 }
2630 }
2631 }
2632 }
2633 }
2634 if hit {
2635 result.objects.push(item.id);
2636 }
2637 }
2638 }
2639
2640 if wants_object {
2646 for item in &self.pick_mc_items {
2647 let vol = &item.volume_data;
2648 let isovalue = item.isovalue;
2649 let [nx, ny, nz] = vol.dims;
2650 let origin = glam::Vec3::from(vol.origin);
2651 let spacing = glam::Vec3::from(vol.spacing);
2652
2653 let mut hit = false;
2654 'mc_rect: for iz in 0..nz.saturating_sub(1) {
2655 for iy in 0..ny.saturating_sub(1) {
2656 for ix in 0..nx.saturating_sub(1) {
2657 let mut has_below = false;
2660 let mut has_above = false;
2661 'corners: for dz in 0u32..=1 {
2662 for dy in 0u32..=1 {
2663 for dx in 0u32..=1 {
2664 let s = vol.sample(ix + dx, iy + dy, iz + dz);
2665 if s < isovalue {
2666 has_below = true;
2667 } else {
2668 has_above = true;
2669 }
2670 if has_below && has_above {
2671 break 'corners;
2672 }
2673 }
2674 }
2675 }
2676 if !(has_below && has_above) {
2677 continue;
2678 }
2679 let cell_center = origin
2680 + spacing
2681 * glam::Vec3::new(
2682 ix as f32 + 0.5,
2683 iy as f32 + 0.5,
2684 iz as f32 + 0.5,
2685 );
2686 if let Some((sx, sy)) = project(view_proj, cell_center) {
2687 if in_rect(sx, sy) {
2688 hit = true;
2689 break 'mc_rect;
2690 }
2691 }
2692 }
2693 }
2694 }
2695 if hit {
2696 result.objects.push(item.id);
2697 }
2698 }
2699 }
2700
2701 result
2702 }
2703
2704 pub fn pick_scene_gpu(
2728 &mut self,
2729 device: &wgpu::Device,
2730 queue: &wgpu::Queue,
2731 cursor: glam::Vec2,
2732 frame: &FrameData,
2733 ) -> Option<crate::interaction::picking::GpuPickHit> {
2734 if self.runtime_mode == crate::renderer::stats::RuntimeMode::Playback
2737 && self.frame_counter % 4 != 0
2738 {
2739 return None;
2740 }
2741
2742 let scene_items: &[SceneRenderItem] = match &frame.scene.surfaces {
2744 SurfaceSubmission::Flat(items) => items.as_ref(),
2745 };
2746
2747 let ppp = frame.camera.pixels_per_point;
2748 let vp_w = (frame.camera.viewport_size[0] * ppp).round() as u32;
2749 let vp_h = (frame.camera.viewport_size[1] * ppp).round() as u32;
2750
2751 if cursor.x < 0.0
2753 || cursor.y < 0.0
2754 || cursor.x >= frame.camera.viewport_size[0]
2755 || cursor.y >= frame.camera.viewport_size[1]
2756 || vp_w == 0
2757 || vp_h == 0
2758 {
2759 return None;
2760 }
2761
2762 self.resources.ensure_pick_pipeline(device);
2764
2765 let pickable_items: Vec<&SceneRenderItem> = scene_items
2769 .iter()
2770 .filter(|item| !item.settings.hidden && item.settings.pick_id != PickId::NONE)
2771 .collect();
2772
2773 let pick_instances: Vec<PickInstance> = pickable_items
2774 .iter()
2775 .map(|item| {
2776 let m = item.model;
2777 PickInstance {
2778 model_c0: m[0],
2779 model_c1: m[1],
2780 model_c2: m[2],
2781 model_c3: m[3],
2782 object_id: item.settings.pick_id.0 as u32,
2783 _pad: [0; 3],
2784 }
2785 })
2786 .collect();
2787
2788 if pick_instances.is_empty() {
2789 return None;
2790 }
2791
2792 let pick_instance_bytes = bytemuck::cast_slice(&pick_instances);
2794 let pick_instance_buf = device.create_buffer(&wgpu::BufferDescriptor {
2795 label: Some("pick_instance_buf"),
2796 size: pick_instance_bytes.len().max(80) as u64,
2797 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
2798 mapped_at_creation: false,
2799 });
2800 queue.write_buffer(&pick_instance_buf, 0, pick_instance_bytes);
2801
2802 let pick_instance_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
2803 label: Some("pick_instance_bg"),
2804 layout: self
2805 .resources
2806 .pick_bind_group_layout_1
2807 .as_ref()
2808 .expect("ensure_pick_pipeline must be called first"),
2809 entries: &[wgpu::BindGroupEntry {
2810 binding: 0,
2811 resource: pick_instance_buf.as_entire_binding(),
2812 }],
2813 });
2814
2815 let camera_uniform = frame.camera.render_camera.camera_uniform();
2817 let camera_bytes = bytemuck::bytes_of(&camera_uniform);
2818 let pick_camera_buf = device.create_buffer(&wgpu::BufferDescriptor {
2819 label: Some("pick_camera_buf"),
2820 size: std::mem::size_of::<CameraUniform>() as u64,
2821 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
2822 mapped_at_creation: false,
2823 });
2824 queue.write_buffer(&pick_camera_buf, 0, camera_bytes);
2825
2826 let pick_camera_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
2827 label: Some("pick_camera_bg"),
2828 layout: self
2829 .resources
2830 .pick_camera_bgl
2831 .as_ref()
2832 .expect("ensure_pick_pipeline must be called first"),
2833 entries: &[
2834 wgpu::BindGroupEntry {
2835 binding: 0,
2836 resource: pick_camera_buf.as_entire_binding(),
2837 },
2838 wgpu::BindGroupEntry {
2839 binding: 6,
2840 resource: self.resources.clip_volume_uniform_buf.as_entire_binding(),
2841 },
2842 ],
2843 });
2844
2845 let pick_id_texture = device.create_texture(&wgpu::TextureDescriptor {
2847 label: Some("pick_id_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::R32Uint,
2857 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
2858 view_formats: &[],
2859 });
2860 let pick_id_view = pick_id_texture.create_view(&wgpu::TextureViewDescriptor::default());
2861
2862 let pick_depth_texture = device.create_texture(&wgpu::TextureDescriptor {
2863 label: Some("pick_depth_colour_texture"),
2864 size: wgpu::Extent3d {
2865 width: vp_w,
2866 height: vp_h,
2867 depth_or_array_layers: 1,
2868 },
2869 mip_level_count: 1,
2870 sample_count: 1,
2871 dimension: wgpu::TextureDimension::D2,
2872 format: wgpu::TextureFormat::R32Float,
2873 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
2874 view_formats: &[],
2875 });
2876 let pick_depth_view =
2877 pick_depth_texture.create_view(&wgpu::TextureViewDescriptor::default());
2878
2879 let depth_stencil_texture = device.create_texture(&wgpu::TextureDescriptor {
2880 label: Some("pick_ds_texture"),
2881 size: wgpu::Extent3d {
2882 width: vp_w,
2883 height: vp_h,
2884 depth_or_array_layers: 1,
2885 },
2886 mip_level_count: 1,
2887 sample_count: 1,
2888 dimension: wgpu::TextureDimension::D2,
2889 format: wgpu::TextureFormat::Depth24PlusStencil8,
2890 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
2891 view_formats: &[],
2892 });
2893 let depth_stencil_view =
2894 depth_stencil_texture.create_view(&wgpu::TextureViewDescriptor::default());
2895
2896 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
2898 label: Some("pick_pass_encoder"),
2899 });
2900 {
2901 let mut pick_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2902 label: Some("pick_pass"),
2903 color_attachments: &[
2904 Some(wgpu::RenderPassColorAttachment {
2905 view: &pick_id_view,
2906 resolve_target: None,
2907 depth_slice: None,
2908 ops: wgpu::Operations {
2909 load: wgpu::LoadOp::Clear(wgpu::Color {
2910 r: 0.0,
2911 g: 0.0,
2912 b: 0.0,
2913 a: 0.0,
2914 }),
2915 store: wgpu::StoreOp::Store,
2916 },
2917 }),
2918 Some(wgpu::RenderPassColorAttachment {
2919 view: &pick_depth_view,
2920 resolve_target: None,
2921 depth_slice: None,
2922 ops: wgpu::Operations {
2923 load: wgpu::LoadOp::Clear(wgpu::Color {
2924 r: 1.0,
2925 g: 0.0,
2926 b: 0.0,
2927 a: 0.0,
2928 }),
2929 store: wgpu::StoreOp::Store,
2930 },
2931 }),
2932 ],
2933 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
2934 view: &depth_stencil_view,
2935 depth_ops: Some(wgpu::Operations {
2936 load: wgpu::LoadOp::Clear(1.0),
2937 store: wgpu::StoreOp::Store,
2938 }),
2939 stencil_ops: None,
2940 }),
2941 timestamp_writes: None,
2942 occlusion_query_set: None,
2943 });
2944
2945 pick_pass.set_pipeline(
2946 self.resources
2947 .pick_pipeline
2948 .as_ref()
2949 .expect("ensure_pick_pipeline must be called first"),
2950 );
2951 pick_pass.set_bind_group(0, &pick_camera_bg, &[]);
2952 pick_pass.set_bind_group(1, &pick_instance_bg, &[]);
2953
2954 for (instance_slot, item) in pickable_items.iter().enumerate() {
2957 let Some(mesh) = self.resources.mesh_store.get(item.mesh_id) else {
2958 continue;
2959 };
2960 pick_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
2961 pick_pass.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
2962 let slot = instance_slot as u32;
2963 pick_pass.draw_indexed(0..mesh.index_count, 0, slot..slot + 1);
2964 }
2965 }
2966
2967 let bytes_per_row_aligned = 256u32; let id_staging = device.create_buffer(&wgpu::BufferDescriptor {
2972 label: Some("pick_id_staging"),
2973 size: bytes_per_row_aligned as u64,
2974 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
2975 mapped_at_creation: false,
2976 });
2977 let depth_staging = device.create_buffer(&wgpu::BufferDescriptor {
2978 label: Some("pick_depth_staging"),
2979 size: bytes_per_row_aligned as u64,
2980 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
2981 mapped_at_creation: false,
2982 });
2983
2984 let px = (cursor.x * ppp).round() as u32;
2986 let py = (cursor.y * ppp).round() as u32;
2987
2988 encoder.copy_texture_to_buffer(
2989 wgpu::TexelCopyTextureInfo {
2990 texture: &pick_id_texture,
2991 mip_level: 0,
2992 origin: wgpu::Origin3d { x: px, y: py, z: 0 },
2993 aspect: wgpu::TextureAspect::All,
2994 },
2995 wgpu::TexelCopyBufferInfo {
2996 buffer: &id_staging,
2997 layout: wgpu::TexelCopyBufferLayout {
2998 offset: 0,
2999 bytes_per_row: Some(bytes_per_row_aligned),
3000 rows_per_image: Some(1),
3001 },
3002 },
3003 wgpu::Extent3d {
3004 width: 1,
3005 height: 1,
3006 depth_or_array_layers: 1,
3007 },
3008 );
3009 encoder.copy_texture_to_buffer(
3010 wgpu::TexelCopyTextureInfo {
3011 texture: &pick_depth_texture,
3012 mip_level: 0,
3013 origin: wgpu::Origin3d { x: px, y: py, z: 0 },
3014 aspect: wgpu::TextureAspect::All,
3015 },
3016 wgpu::TexelCopyBufferInfo {
3017 buffer: &depth_staging,
3018 layout: wgpu::TexelCopyBufferLayout {
3019 offset: 0,
3020 bytes_per_row: Some(bytes_per_row_aligned),
3021 rows_per_image: Some(1),
3022 },
3023 },
3024 wgpu::Extent3d {
3025 width: 1,
3026 height: 1,
3027 depth_or_array_layers: 1,
3028 },
3029 );
3030
3031 queue.submit(std::iter::once(encoder.finish()));
3032
3033 let (tx_id, rx_id) = std::sync::mpsc::channel::<Result<(), wgpu::BufferAsyncError>>();
3035 let (tx_dep, rx_dep) = std::sync::mpsc::channel::<Result<(), wgpu::BufferAsyncError>>();
3036 id_staging
3037 .slice(..)
3038 .map_async(wgpu::MapMode::Read, move |r| {
3039 let _ = tx_id.send(r);
3040 });
3041 depth_staging
3042 .slice(..)
3043 .map_async(wgpu::MapMode::Read, move |r| {
3044 let _ = tx_dep.send(r);
3045 });
3046 device
3047 .poll(wgpu::PollType::Wait {
3048 submission_index: None,
3049 timeout: Some(std::time::Duration::from_secs(5)),
3050 })
3051 .unwrap();
3052 let _ = rx_id.recv().unwrap_or(Err(wgpu::BufferAsyncError));
3053 let _ = rx_dep.recv().unwrap_or(Err(wgpu::BufferAsyncError));
3054
3055 let object_id = {
3056 let data = id_staging.slice(..).get_mapped_range();
3057 u32::from_le_bytes([data[0], data[1], data[2], data[3]])
3058 };
3059 id_staging.unmap();
3060
3061 let depth = {
3062 let data = depth_staging.slice(..).get_mapped_range();
3063 f32::from_le_bytes([data[0], data[1], data[2], data[3]])
3064 };
3065 depth_staging.unmap();
3066
3067 if object_id == 0 {
3069 return None;
3070 }
3071
3072 Some(crate::interaction::picking::GpuPickHit {
3073 object_id: PickId(object_id as u64),
3074 depth,
3075 })
3076 }
3077}