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