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
143fn build_segment_midpoints(positions: &[[f32; 3]], strip_lengths: &[u32]) -> Vec<[f32; 3]> {
148 let mut midpoints = Vec::new();
149 if strip_lengths.is_empty() {
150 for j in 0..positions.len().saturating_sub(1) {
151 let a = glam::Vec3::from(positions[j]);
152 let b = glam::Vec3::from(positions[j + 1]);
153 midpoints.push(((a + b) * 0.5).to_array());
154 }
155 } else {
156 let mut node_off = 0usize;
157 for &slen in strip_lengths {
158 let slen = slen as usize;
159 for j in 0..slen.saturating_sub(1) {
160 let a = glam::Vec3::from(positions[node_off + j]);
161 let b = glam::Vec3::from(positions[node_off + j + 1]);
162 midpoints.push(((a + b) * 0.5).to_array());
163 }
164 node_off += slen;
165 }
166 }
167 midpoints
168}
169
170#[derive(Clone, Debug, Default)]
176pub struct PickRectResult {
177 pub objects: Vec<u64>,
181 pub elements: Vec<(u64, crate::interaction::sub_object::SubObjectRef)>,
187}
188
189impl PickRectResult {
190 pub fn is_empty(&self) -> bool {
192 self.objects.is_empty() && self.elements.is_empty()
193 }
194}
195
196impl ViewportRenderer {
197 pub fn pick(
221 &self,
222 click_pos: glam::Vec2,
223 viewport_size: glam::Vec2,
224 view_proj: glam::Mat4,
225 mask: crate::interaction::pick_mask::PickMask,
226 ) -> Option<crate::interaction::picking::PickHit> {
227 use crate::interaction::pick_mask::PickMask;
228 use crate::interaction::picking::{
229 screen_to_ray, pick_point_cloud_cpu, pick_gaussian_splat_cpu, pick_volume_cpu,
230 pick_transparent_volume_mesh_cpu, PickHit,
231 };
232 use crate::interaction::sub_object::SubObjectRef;
233 use parry3d::math::{Pose, Vector};
234 use parry3d::query::{Ray, RayCast};
235
236 if viewport_size.x <= 0.0 || viewport_size.y <= 0.0 {
237 return None;
238 }
239
240 let view_proj_inv = view_proj.inverse();
241 let (ray_origin, ray_dir) = screen_to_ray(click_pos, viewport_size, view_proj_inv);
242
243 let wants_face = mask.intersects(PickMask::FACE);
244 let wants_vertex = mask.intersects(PickMask::VERTEX);
245 let wants_cell = mask.intersects(PickMask::CELL);
246 let wants_cloud = mask.intersects(PickMask::CLOUD_POINT);
247 let wants_splat = mask.intersects(PickMask::SPLAT);
248 let wants_object = mask.intersects(PickMask::OBJECT);
249 let wants_mesh_sub = wants_face || wants_vertex || mask.intersects(PickMask::EDGE);
250
251 let mut best: Option<(f32, PickHit)> = None;
253
254 let mut consider = |toi: f32, hit: PickHit| {
255 if best.as_ref().map_or(true, |(bt, _)| toi < *bt) {
256 best = Some((toi, hit));
257 }
258 };
259
260 let vm_cell_map: std::collections::HashMap<u64, &[u32]> = self
263 .pick_volume_mesh_items
264 .iter()
265 .filter(|item| item.pick_id != PickId::NONE && !item.face_to_cell.is_empty())
266 .map(|item| (item.pick_id.0, item.face_to_cell.as_slice()))
267 .collect();
268
269 if wants_mesh_sub || wants_cell || wants_object {
271 let ray = Ray::new(
272 Vector::new(ray_origin.x, ray_origin.y, ray_origin.z),
273 Vector::new(ray_dir.x, ray_dir.y, ray_dir.z),
274 );
275 for item in &self.pick_scene_items {
276 if !item.visible || item.pick_id == PickId::NONE {
277 continue;
278 }
279 let Some(mesh) = self.resources.mesh_store.get(item.mesh_id) else {
280 continue;
281 };
282 let (Some(positions), Some(indices)) = (&mesh.cpu_positions, &mesh.cpu_indices)
283 else {
284 continue;
285 };
286
287 let model = glam::Mat4::from_cols_array_2d(&item.model);
288
289 let verts: Vec<Vector> = positions
292 .iter()
293 .map(|p| {
294 let wp = model.transform_point3(glam::Vec3::from(*p));
295 Vector::new(wp.x, wp.y, wp.z)
296 })
297 .collect();
298
299 let tri_indices: Vec<[u32; 3]> = indices
300 .chunks(3)
301 .filter(|c| c.len() == 3)
302 .map(|c| [c[0], c[1], c[2]])
303 .collect();
304
305 if tri_indices.is_empty() {
306 continue;
307 }
308
309 match parry3d::shape::TriMesh::new(verts, tri_indices) {
310 Ok(trimesh) => {
311 let identity = Pose::identity();
313 let Some(intersection) =
314 trimesh.cast_ray_and_get_normal(&identity, &ray, f32::MAX, true)
315 else {
316 continue;
317 };
318 let toi = intersection.time_of_impact;
319 let world_pos = ray_origin + ray_dir * toi;
320 let normal = intersection.normal;
321
322 let feature_sub = SubObjectRef::from_feature_id(intersection.feature);
323
324 let sub_object = if wants_face {
325 feature_sub
326 } else if wants_cell {
327 if let Some(f2c) = vm_cell_map.get(&item.pick_id.0) {
329 match feature_sub {
330 Some(SubObjectRef::Face(face_raw)) => {
331 let n_tri = indices.len() / 3;
332 let face = if (face_raw as usize) >= n_tri {
333 face_raw as usize - n_tri
334 } else {
335 face_raw as usize
336 };
337 f2c.get(face).map(|&ci| SubObjectRef::Cell(ci))
338 }
339 other => other,
340 }
341 } else {
342 None
344 }
345 } else if wants_vertex {
346 match feature_sub {
348 Some(SubObjectRef::Face(face_raw)) => {
349 let n_tri = indices.len() / 3;
350 let face = if (face_raw as usize) >= n_tri {
351 face_raw as usize - n_tri
352 } else {
353 face_raw as usize
354 };
355 if face * 3 + 2 < indices.len() {
356 let vis = [
357 indices[face * 3] as usize,
358 indices[face * 3 + 1] as usize,
359 indices[face * 3 + 2] as usize,
360 ];
361 let (best_vi, _) = vis
362 .iter()
363 .map(|&i| {
364 let p = model.transform_point3(
365 glam::Vec3::from(positions[i]),
366 );
367 (i, p.distance(world_pos))
368 })
369 .fold((vis[0], f32::MAX), |acc, (i, d)| {
370 if d < acc.1 { (i, d) } else { acc }
371 });
372 Some(SubObjectRef::Vertex(best_vi as u32))
373 } else {
374 None
375 }
376 }
377 other => other,
378 }
379 } else {
380 None
382 };
383
384 if sub_object.is_some() || wants_object {
390 #[allow(deprecated)]
391 let hit = PickHit {
392 id: item.pick_id.0,
393 sub_object,
394 world_pos,
395 normal,
396 triangle_index: u32::MAX,
397 point_index: None,
398 scalar_value: None,
399 };
400 consider(toi, hit);
401 }
402 }
403 Err(e) => {
404 tracing::warn!(
405 pick_id = item.pick_id.0,
406 error = %e,
407 "TriMesh build failed in renderer.pick()"
408 );
409 }
410 }
411 }
412 }
413
414 if wants_cell || wants_object {
419 for item in &self.pick_tvm_items {
420 if item.pick_id == 0 {
421 continue;
422 }
423 let Some(data) = item.volume_mesh_data.as_deref() else {
424 continue;
425 };
426 if let Some(mut hit) = pick_transparent_volume_mesh_cpu(
427 ray_origin,
428 ray_dir,
429 item.pick_id,
430 glam::Mat4::IDENTITY,
431 data,
432 ) {
433 let toi = (hit.world_pos - ray_origin).dot(ray_dir).max(0.0);
434 if !wants_cell {
435 hit.sub_object = None;
436 }
437 consider(toi, hit);
438 }
439 }
440 }
441
442 if wants_cloud || wants_object {
444 for item in &self.pick_point_cloud_items {
445 if item.id == 0 || item.positions.is_empty() {
446 continue;
447 }
448 let radius_px = item.point_size.max(4.0);
449 if let Some(mut hit) = pick_point_cloud_cpu(
450 click_pos,
451 item.id,
452 item,
453 view_proj,
454 viewport_size,
455 radius_px,
456 ) {
457 let toi = (hit.world_pos - ray_origin).dot(ray_dir).max(0.0);
458 if !wants_cloud {
459 hit.sub_object = None;
460 }
461 consider(toi, hit);
462 }
463 }
464 }
465
466 let wants_voxel = mask.intersects(PickMask::VOXEL);
468 if wants_voxel || wants_object {
469 for item in &self.pick_volume_items {
470 if item.pick_id == 0 {
471 continue;
472 }
473 let Some(vol_data) = item.volume_data.as_deref() else {
474 continue;
475 };
476 if let Some(mut hit) = pick_volume_cpu(ray_origin, ray_dir, item.pick_id, item, vol_data) {
477 let toi = (hit.world_pos - ray_origin).dot(ray_dir).max(0.0);
478 if !wants_voxel {
479 hit.sub_object = None;
480 }
481 consider(toi, hit);
482 }
483 }
484 }
485
486 if wants_splat || wants_object {
488 for item in &self.pick_splat_items {
489 if item.pick_id == 0 {
490 continue;
491 }
492 let Some(gpu_set) = self.resources.gaussian_splat_store.get(item.id.0) else {
493 continue;
494 };
495 if gpu_set.cpu_positions.is_empty() {
496 continue;
497 }
498 let model = glam::Mat4::from_cols_array_2d(&item.model);
499 let mean_max_scale: f32 = if gpu_set.cpu_scales.is_empty() {
502 0.05
503 } else {
504 gpu_set.cpu_scales.iter()
505 .map(|s| s[0].max(s[1]).max(s[2]))
506 .sum::<f32>()
507 / gpu_set.cpu_scales.len() as f32
508 };
509 let world_radius = mean_max_scale * 3.0;
510 let center_w = model.transform_point3(glam::Vec3::ZERO);
511 let p0_clip = view_proj * center_w.extend(1.0);
512 let p1_clip = view_proj * (center_w + glam::Vec3::X * world_radius).extend(1.0);
513 let radius_px = if p0_clip.w.abs() > 1e-6 && p1_clip.w.abs() > 1e-6 {
514 let p0_ndc = glam::Vec2::new(p0_clip.x, p0_clip.y) / p0_clip.w;
515 let p1_ndc = glam::Vec2::new(p1_clip.x, p1_clip.y) / p1_clip.w;
516 ((p1_ndc - p0_ndc).length() * 0.5 * viewport_size.x.max(viewport_size.y))
517 .max(4.0)
518 } else {
519 world_radius * 100.0
520 };
521 if let Some(mut hit) = pick_gaussian_splat_cpu(
522 click_pos,
523 item.pick_id,
524 &gpu_set.cpu_positions,
525 model,
526 view_proj,
527 viewport_size,
528 radius_px,
529 ) {
530 let toi = (hit.world_pos - ray_origin).dot(ray_dir).max(0.0);
532 if wants_splat {
533 if let Some(SubObjectRef::Point(idx)) = hit.sub_object {
534 hit.sub_object = Some(SubObjectRef::Splat(idx));
535 }
536 } else {
537 hit.sub_object = None;
538 }
539 consider(toi, hit);
540 }
541 }
542 }
543
544 let wants_instance = mask.intersects(PickMask::INSTANCE);
546 if wants_instance || wants_object {
547 let instance_radius_px = |world_center: glam::Vec3, world_r: f32| -> f32 {
551 let p0 = view_proj * world_center.extend(1.0);
552 let p1 = view_proj * (world_center + glam::Vec3::X * world_r).extend(1.0);
553 if p0.w.abs() > 1e-6 && p1.w.abs() > 1e-6 {
554 let n0 = glam::Vec2::new(p0.x, p0.y) / p0.w;
555 let n1 = glam::Vec2::new(p1.x, p1.y) / p1.w;
556 ((n1 - n0).length() * 0.5 * viewport_size.x.max(viewport_size.y)).max(4.0)
557 } else {
558 (world_r * 100.0_f32).max(4.0)
559 }
560 };
561
562 for item in &self.pick_glyph_items {
564 if item.id == 0 || item.positions.is_empty() {
565 continue;
566 }
567 let model = glam::Mat4::from_cols_array_2d(&item.model);
568 let full_len = if item.scale_by_magnitude && !item.vectors.is_empty() {
569 let mean_mag = item.vectors.iter()
570 .map(|v| glam::Vec3::from(*v).length())
571 .sum::<f32>() / item.vectors.len() as f32;
572 (mean_mag * item.scale).max(0.01)
573 } else {
574 item.scale.max(0.01)
575 };
576 let has_vecs = item.vectors.len() == item.positions.len();
580 let midpoints: Vec<[f32; 3]> = item.positions.iter().enumerate().map(|(i, pos)| {
581 if has_vecs {
582 let p = glam::Vec3::from(*pos);
583 let v = glam::Vec3::from(item.vectors[i]);
584 let len = if item.scale_by_magnitude { v.length() * item.scale } else { item.scale };
585 (p + v.normalize_or_zero() * len * 0.5).to_array()
586 } else {
587 *pos
588 }
589 }).collect();
590 let n = midpoints.len() as f32;
591 let centroid = model.transform_point3(
592 midpoints.iter().map(|p| glam::Vec3::from(*p)).sum::<glam::Vec3>() / n
593 );
594 let radius_px = instance_radius_px(centroid, full_len * 0.5);
595 if let Some(mut hit) = pick_gaussian_splat_cpu(
596 click_pos, item.id, &midpoints, model, view_proj, viewport_size, radius_px,
597 ) {
598 if let Some(SubObjectRef::Point(idx)) = hit.sub_object {
600 if let Some(base) = item.positions.get(idx as usize) {
601 hit.world_pos = model.transform_point3(glam::Vec3::from(*base));
602 }
603 if wants_instance {
604 hit.sub_object = Some(SubObjectRef::Instance(idx));
605 } else {
606 hit.sub_object = None;
607 }
608 }
609 let toi = (hit.world_pos - ray_origin).dot(ray_dir).max(0.0);
610 consider(toi, hit);
611 }
612 }
613
614 for item in &self.pick_tensor_glyph_items {
616 if item.id == 0 || item.positions.is_empty() {
617 continue;
618 }
619 let model = glam::Mat4::from_cols_array_2d(&item.model);
620 let world_r = if !item.eigenvalues.is_empty() {
624 let max_ev = item.eigenvalues.iter()
625 .map(|ev| ev[0].abs().max(ev[1].abs()).max(ev[2].abs()))
626 .fold(0.0_f32, f32::max);
627 (max_ev * item.scale).max(0.01)
628 } else {
629 item.scale.max(0.01)
630 };
631 let n = item.positions.len() as f32;
632 let centroid = model.transform_point3(
633 item.positions.iter().map(|p| glam::Vec3::from(*p)).sum::<glam::Vec3>() / n
634 );
635 let radius_px = instance_radius_px(centroid, world_r);
636 if let Some(mut hit) = pick_gaussian_splat_cpu(
637 click_pos, item.id, &item.positions, model, view_proj, viewport_size, radius_px,
638 ) {
639 let toi = (hit.world_pos - ray_origin).dot(ray_dir).max(0.0);
640 if wants_instance {
641 if let Some(SubObjectRef::Point(idx)) = hit.sub_object {
642 hit.sub_object = Some(SubObjectRef::Instance(idx));
643 }
644 } else {
645 hit.sub_object = None;
646 }
647 consider(toi, hit);
648 }
649 }
650
651 for item in &self.pick_sprite_items {
653 if item.id == 0 || item.positions.is_empty() {
654 continue;
655 }
656 let model = glam::Mat4::from_cols_array_2d(&item.model);
657 let radius_px = match item.size_mode {
658 SpriteSizeMode::ScreenSpace => (item.default_size * 0.5).max(4.0),
659 SpriteSizeMode::WorldSpace => {
660 let n = item.positions.len() as f32;
661 let centroid = model.transform_point3(
662 item.positions.iter().map(|p| glam::Vec3::from(*p)).sum::<glam::Vec3>() / n
663 );
664 instance_radius_px(centroid, (item.default_size * 0.5).max(0.01))
665 }
666 };
667 if let Some(mut hit) = pick_gaussian_splat_cpu(
668 click_pos, item.id, &item.positions, model, view_proj, viewport_size, radius_px,
669 ) {
670 let toi = (hit.world_pos - ray_origin).dot(ray_dir).max(0.0);
671 if wants_instance {
672 if let Some(SubObjectRef::Point(idx)) = hit.sub_object {
673 hit.sub_object = Some(SubObjectRef::Instance(idx));
674 }
675 } else {
676 hit.sub_object = None;
677 }
678 consider(toi, hit);
679 }
680 }
681 }
682
683 let wants_poly_node = mask.intersects(PickMask::POLY_NODE);
685 let wants_strip = mask.intersects(PickMask::STRIP);
686 if wants_poly_node || wants_strip || wants_object {
687 for item in &self.pick_polyline_items {
688 if item.id == 0 || item.positions.is_empty() {
689 continue;
690 }
691 let radius_px = (item.line_width + 4.0).max(8.0);
692 if let Some(mut hit) = pick_gaussian_splat_cpu(
693 click_pos,
694 item.id,
695 &item.positions,
696 glam::Mat4::IDENTITY,
697 view_proj,
698 viewport_size,
699 radius_px,
700 ) {
701 let toi = (hit.world_pos - ray_origin).dot(ray_dir).max(0.0);
702 if wants_poly_node {
703 } else if wants_strip {
705 if let Some(SubObjectRef::Point(idx)) = hit.sub_object {
706 hit.sub_object = Some(SubObjectRef::Strip(
707 strip_for_node(idx, &item.strip_lengths),
708 ));
709 }
710 } else {
711 hit.sub_object = None;
712 }
713 consider(toi, hit);
714 }
715 }
716 }
717
718 let wants_segment = mask.intersects(PickMask::SEGMENT);
722 if wants_segment || wants_strip || wants_object {
723 for item in &self.pick_polyline_items {
724 if item.id == 0 || item.positions.is_empty() {
725 continue;
726 }
727 let threshold_px = (item.line_width / 2.0 + 4.0).max(4.0);
729 let Some((seg_idx, world_pos)) = pick_closest_polyline_segment(
730 click_pos,
731 viewport_size,
732 view_proj,
733 &item.positions,
734 &item.strip_lengths,
735 threshold_px,
736 ) else {
737 continue;
738 };
739 let toi = (world_pos - ray_origin).dot(ray_dir).max(0.0);
740 let sub_object = if wants_segment {
741 Some(SubObjectRef::Segment(seg_idx))
742 } else if wants_strip {
743 Some(SubObjectRef::Strip(strip_for_segment(seg_idx, &item.strip_lengths)))
744 } else {
745 None
746 };
747 #[allow(deprecated)]
748 let hit = PickHit {
749 id: item.id,
750 sub_object,
751 world_pos,
752 normal: glam::Vec3::Y,
753 triangle_index: u32::MAX,
754 point_index: None,
755 scalar_value: None,
756 };
757 consider(toi, hit);
758 }
759 }
760
761 if wants_segment || wants_strip || wants_object {
763 let world_r_to_px = |ref_world: glam::Vec3, world_r: f32| -> f32 {
765 let p0 = view_proj * ref_world.extend(1.0);
766 let p1 = view_proj * (ref_world + glam::Vec3::X * world_r).extend(1.0);
767 if p0.w.abs() > 1e-6 && p1.w.abs() > 1e-6 {
768 let n0 = glam::Vec2::new(p0.x, p0.y) / p0.w;
769 let n1 = glam::Vec2::new(p1.x, p1.y) / p1.w;
770 ((n1 - n0).length() * 0.5 * viewport_size.x.max(viewport_size.y)).max(4.0)
771 } else {
772 (world_r * 100.0_f32).max(4.0)
773 }
774 };
775
776 for item in &self.pick_streamtube_items {
777 if item.id == 0 || item.positions.is_empty() { continue; }
778 let midpoints = build_segment_midpoints(&item.positions, &item.strip_lengths);
779 if midpoints.is_empty() { continue; }
780 let radius_px = world_r_to_px(glam::Vec3::from(midpoints[0]), item.radius.max(0.01));
781 if let Some(mut hit) = pick_gaussian_splat_cpu(
782 click_pos, item.id, &midpoints, glam::Mat4::IDENTITY, view_proj, viewport_size, radius_px,
783 ) {
784 let toi = (hit.world_pos - ray_origin).dot(ray_dir).max(0.0);
785 if wants_segment {
786 if let Some(SubObjectRef::Point(idx)) = hit.sub_object {
787 hit.sub_object = Some(SubObjectRef::Segment(idx));
788 }
789 } else if wants_strip {
790 if let Some(SubObjectRef::Point(idx)) = hit.sub_object {
791 hit.sub_object = Some(SubObjectRef::Strip(strip_for_segment(idx, &item.strip_lengths)));
792 }
793 } else {
794 hit.sub_object = None;
795 }
796 consider(toi, hit);
797 }
798 }
799
800 for item in &self.pick_tube_items {
801 if item.id == 0 || item.positions.is_empty() { continue; }
802 let midpoints = build_segment_midpoints(&item.positions, &item.strip_lengths);
803 if midpoints.is_empty() { continue; }
804 let radius_px = world_r_to_px(glam::Vec3::from(midpoints[0]), item.radius.max(0.01));
805 if let Some(mut hit) = pick_gaussian_splat_cpu(
806 click_pos, item.id, &midpoints, glam::Mat4::IDENTITY, view_proj, viewport_size, radius_px,
807 ) {
808 let toi = (hit.world_pos - ray_origin).dot(ray_dir).max(0.0);
809 if wants_segment {
810 if let Some(SubObjectRef::Point(idx)) = hit.sub_object {
811 hit.sub_object = Some(SubObjectRef::Segment(idx));
812 }
813 } else if wants_strip {
814 if let Some(SubObjectRef::Point(idx)) = hit.sub_object {
815 hit.sub_object = Some(SubObjectRef::Strip(strip_for_segment(idx, &item.strip_lengths)));
816 }
817 } else {
818 hit.sub_object = None;
819 }
820 consider(toi, hit);
821 }
822 }
823
824 for item in &self.pick_ribbon_items {
825 if item.id == 0 || item.positions.is_empty() { continue; }
826 let midpoints = build_segment_midpoints(&item.positions, &item.strip_lengths);
827 if midpoints.is_empty() { continue; }
828 let radius_px = world_r_to_px(glam::Vec3::from(midpoints[0]), item.width.max(0.01));
829 if let Some(mut hit) = pick_gaussian_splat_cpu(
830 click_pos, item.id, &midpoints, glam::Mat4::IDENTITY, view_proj, viewport_size, radius_px,
831 ) {
832 let toi = (hit.world_pos - ray_origin).dot(ray_dir).max(0.0);
833 if wants_segment {
834 if let Some(SubObjectRef::Point(idx)) = hit.sub_object {
835 hit.sub_object = Some(SubObjectRef::Segment(idx));
836 }
837 } else if wants_strip {
838 if let Some(SubObjectRef::Point(idx)) = hit.sub_object {
839 hit.sub_object = Some(SubObjectRef::Strip(strip_for_segment(idx, &item.strip_lengths)));
840 }
841 } else {
842 hit.sub_object = None;
843 }
844 consider(toi, hit);
845 }
846 }
847 }
848
849 best.map(|(_, hit)| hit)
850 }
851
852 pub fn pick_rect(
868 &self,
869 rect_min: glam::Vec2,
870 rect_max: glam::Vec2,
871 viewport_size: glam::Vec2,
872 view_proj: glam::Mat4,
873 mask: crate::interaction::pick_mask::PickMask,
874 ) -> PickRectResult {
875 use crate::interaction::pick_mask::PickMask;
876 use crate::interaction::sub_object::SubObjectRef;
877
878 let mut result = PickRectResult::default();
879
880 if viewport_size.x <= 0.0 || viewport_size.y <= 0.0 {
881 return result;
882 }
883
884 let wants_face = mask.intersects(PickMask::FACE);
885 let wants_vertex = mask.intersects(PickMask::VERTEX);
886 let wants_cell = mask.intersects(PickMask::CELL);
887 let wants_cloud = mask.intersects(PickMask::CLOUD_POINT);
888 let wants_splat = mask.intersects(PickMask::SPLAT);
889 let wants_object = mask.intersects(PickMask::OBJECT);
890
891 let vm_cell_map: std::collections::HashMap<u64, &[u32]> = self
893 .pick_volume_mesh_items
894 .iter()
895 .filter(|item| item.pick_id != PickId::NONE && !item.face_to_cell.is_empty())
896 .map(|item| (item.pick_id.0, item.face_to_cell.as_slice()))
897 .collect();
898
899 let project = |mvp: glam::Mat4, local: glam::Vec3| -> Option<(f32, f32)> {
902 let clip = mvp * local.extend(1.0);
903 if clip.w <= 0.0 {
904 return None;
905 }
906 let sx = (clip.x / clip.w + 1.0) * 0.5 * viewport_size.x;
907 let sy = (1.0 - clip.y / clip.w) * 0.5 * viewport_size.y;
908 Some((sx, sy))
909 };
910
911 let in_rect = |sx: f32, sy: f32| -> bool {
912 sx >= rect_min.x && sx <= rect_max.x && sy >= rect_min.y && sy <= rect_max.y
913 };
914
915 if wants_face || wants_vertex || wants_cell || wants_object {
917 for item in &self.pick_scene_items {
918 if !item.visible || item.pick_id == PickId::NONE {
919 continue;
920 }
921 let Some(mesh) = self.resources.mesh_store.get(item.mesh_id) else {
922 continue;
923 };
924 let (Some(positions), Some(indices)) =
925 (&mesh.cpu_positions, &mesh.cpu_indices)
926 else {
927 continue;
928 };
929
930 let model = glam::Mat4::from_cols_array_2d(&item.model);
931 let mvp = view_proj * model;
932 let id = item.pick_id.0;
933 let mut item_hit = false;
934
935 if wants_face {
936 for (tri_idx, chunk) in indices.chunks(3).enumerate() {
937 if chunk.len() < 3 {
938 continue;
939 }
940 let [i0, i1, i2] =
941 [chunk[0] as usize, chunk[1] as usize, chunk[2] as usize];
942 if i0 >= positions.len()
943 || i1 >= positions.len()
944 || i2 >= positions.len()
945 {
946 continue;
947 }
948 let centroid = (glam::Vec3::from(positions[i0])
949 + glam::Vec3::from(positions[i1])
950 + glam::Vec3::from(positions[i2]))
951 / 3.0;
952 if let Some((sx, sy)) = project(mvp, centroid) {
953 if in_rect(sx, sy) {
954 result.elements.push((id, SubObjectRef::Face(tri_idx as u32)));
955 item_hit = true;
956 }
957 }
958 }
959 } else if wants_cell {
960 if let Some(f2c) = vm_cell_map.get(&id) {
962 let mut seen = std::collections::HashSet::new();
963 for (tri_idx, chunk) in indices.chunks(3).enumerate() {
964 if chunk.len() < 3 {
965 continue;
966 }
967 let [i0, i1, i2] =
968 [chunk[0] as usize, chunk[1] as usize, chunk[2] as usize];
969 if i0 >= positions.len()
970 || i1 >= positions.len()
971 || i2 >= positions.len()
972 {
973 continue;
974 }
975 let centroid = (glam::Vec3::from(positions[i0])
976 + glam::Vec3::from(positions[i1])
977 + glam::Vec3::from(positions[i2]))
978 / 3.0;
979 if let Some((sx, sy)) = project(mvp, centroid) {
980 if in_rect(sx, sy) {
981 if let Some(&ci) = f2c.get(tri_idx) {
982 if seen.insert(ci) {
983 result.elements.push((id, SubObjectRef::Cell(ci)));
984 }
985 }
986 item_hit = true;
987 }
988 }
989 }
990 }
991 } else if wants_vertex {
992 for (vi, pos) in positions.iter().enumerate() {
993 if let Some((sx, sy)) = project(mvp, glam::Vec3::from(*pos)) {
994 if in_rect(sx, sy) {
995 result.elements.push((id, SubObjectRef::Vertex(vi as u32)));
996 item_hit = true;
997 }
998 }
999 }
1000 } else {
1001 'tri_scan: for chunk in indices.chunks(3) {
1003 if chunk.len() < 3 {
1004 continue;
1005 }
1006 let [i0, i1, i2] =
1007 [chunk[0] as usize, chunk[1] as usize, chunk[2] as usize];
1008 if i0 >= positions.len()
1009 || i1 >= positions.len()
1010 || i2 >= positions.len()
1011 {
1012 continue;
1013 }
1014 let centroid = (glam::Vec3::from(positions[i0])
1015 + glam::Vec3::from(positions[i1])
1016 + glam::Vec3::from(positions[i2]))
1017 / 3.0;
1018 if let Some((sx, sy)) = project(mvp, centroid) {
1019 if in_rect(sx, sy) {
1020 item_hit = true;
1021 break 'tri_scan;
1022 }
1023 }
1024 }
1025 }
1026
1027 if wants_object && item_hit {
1028 result.objects.push(id);
1029 }
1030 }
1031 }
1032
1033 if wants_cell || wants_object {
1038 for item in &self.pick_tvm_items {
1039 if item.pick_id == 0 {
1040 continue;
1041 }
1042 let Some(data) = item.volume_mesh_data.as_deref() else {
1043 continue;
1044 };
1045 use crate::resources::volume_mesh::CELL_SENTINEL;
1046 let id = item.pick_id;
1047 let mvp = view_proj; let mut item_hit = false;
1049
1050 for (cell_idx, cell) in data.cells.iter().enumerate() {
1051 let nv: usize = if cell[4] == CELL_SENTINEL {
1052 4
1053 } else if cell[5] == CELL_SENTINEL {
1054 5
1055 } else if cell[6] == CELL_SENTINEL {
1056 6
1057 } else {
1058 8
1059 };
1060 let centroid: glam::Vec3 = cell[..nv]
1061 .iter()
1062 .map(|&vi| glam::Vec3::from(data.positions[vi as usize]))
1063 .sum::<glam::Vec3>()
1064 / nv as f32;
1065 if let Some((sx, sy)) = project(mvp, centroid) {
1066 if in_rect(sx, sy) {
1067 if wants_cell {
1068 result.elements.push((id, SubObjectRef::Cell(cell_idx as u32)));
1069 }
1070 item_hit = true;
1071 }
1072 }
1073 }
1074
1075 if wants_object && item_hit {
1076 result.objects.push(id);
1077 }
1078 }
1079 }
1080
1081 if wants_cloud || wants_object {
1083 for item in &self.pick_point_cloud_items {
1084 if item.id == 0 || item.positions.is_empty() {
1085 continue;
1086 }
1087 let model = glam::Mat4::from_cols_array_2d(&item.model);
1088 let mvp = view_proj * model;
1089 let id = item.id;
1090 let mut item_hit = false;
1091
1092 for (pt_idx, pos) in item.positions.iter().enumerate() {
1093 if let Some((sx, sy)) = project(mvp, glam::Vec3::from(*pos)) {
1094 if in_rect(sx, sy) {
1095 if wants_cloud {
1096 result.elements.push((id, SubObjectRef::Point(pt_idx as u32)));
1097 }
1098 item_hit = true;
1099 }
1100 }
1101 }
1102
1103 if wants_object && item_hit {
1104 result.objects.push(id);
1105 }
1106 }
1107 }
1108
1109 let wants_voxel = mask.intersects(PickMask::VOXEL);
1111 if wants_voxel || wants_object {
1112 for item in &self.pick_volume_items {
1113 if item.pick_id == 0 {
1114 continue;
1115 }
1116 let Some(vol_data) = item.volume_data.as_deref() else {
1117 continue;
1118 };
1119 let [nx, ny, nz] = vol_data.dims;
1120 if nx == 0 || ny == 0 || nz == 0 || vol_data.data.is_empty() {
1121 continue;
1122 }
1123 let model = glam::Mat4::from_cols_array_2d(&item.model);
1124 let mvp = view_proj * model;
1125 let bbox_min = glam::Vec3::from(item.bbox_min);
1126 let bbox_max = glam::Vec3::from(item.bbox_max);
1127 let cell = (bbox_max - bbox_min)
1128 / glam::Vec3::new(nx as f32, ny as f32, nz as f32);
1129 let id = item.pick_id;
1130 let mut item_hit = false;
1131
1132 for iz in 0..nz {
1133 for iy in 0..ny {
1134 for ix in 0..nx {
1135 let flat = (ix + iy * nx + iz * nx * ny) as usize;
1136 let scalar = vol_data.data[flat];
1137 if scalar.is_nan()
1138 || scalar < item.threshold_min
1139 || scalar > item.threshold_max
1140 {
1141 continue;
1142 }
1143 let center = bbox_min
1144 + cell * glam::Vec3::new(
1145 ix as f32 + 0.5,
1146 iy as f32 + 0.5,
1147 iz as f32 + 0.5,
1148 );
1149 if let Some((sx, sy)) = project(mvp, center) {
1150 if in_rect(sx, sy) {
1151 if wants_voxel {
1152 result.elements.push((id, SubObjectRef::Voxel(flat as u32)));
1153 }
1154 item_hit = true;
1155 }
1156 }
1157 }
1158 }
1159 }
1160
1161 if wants_object && item_hit {
1162 result.objects.push(id);
1163 }
1164 }
1165 }
1166
1167 if wants_splat || wants_object {
1169 for item in &self.pick_splat_items {
1170 if item.pick_id == 0 {
1171 continue;
1172 }
1173 let Some(gpu_set) = self.resources.gaussian_splat_store.get(item.id.0) else {
1174 continue;
1175 };
1176 if gpu_set.cpu_positions.is_empty() {
1177 continue;
1178 }
1179 let model = glam::Mat4::from_cols_array_2d(&item.model);
1180 let mvp = view_proj * model;
1181 let id = item.pick_id;
1182 let mut item_hit = false;
1183
1184 for (i, pos) in gpu_set.cpu_positions.iter().enumerate() {
1185 if let Some((sx, sy)) = project(mvp, glam::Vec3::from(*pos)) {
1186 if in_rect(sx, sy) {
1187 if wants_splat {
1188 result.elements.push((id, SubObjectRef::Splat(i as u32)));
1189 }
1190 item_hit = true;
1191 }
1192 }
1193 }
1194
1195 if wants_object && item_hit {
1196 result.objects.push(id);
1197 }
1198 }
1199 }
1200
1201 let wants_instance = mask.intersects(PickMask::INSTANCE);
1203 if wants_instance || wants_object {
1204 for item in &self.pick_glyph_items {
1206 if item.id == 0 || item.positions.is_empty() {
1207 continue;
1208 }
1209 let model = glam::Mat4::from_cols_array_2d(&item.model);
1210 let mvp = view_proj * model;
1211 let id = item.id;
1212 let mut item_hit = false;
1213 for (i, pos) in item.positions.iter().enumerate() {
1214 if let Some((sx, sy)) = project(mvp, glam::Vec3::from(*pos)) {
1215 if in_rect(sx, sy) {
1216 if wants_instance {
1217 result.elements.push((id, SubObjectRef::Instance(i as u32)));
1218 }
1219 item_hit = true;
1220 }
1221 }
1222 }
1223 if wants_object && item_hit {
1224 result.objects.push(id);
1225 }
1226 }
1227
1228 for item in &self.pick_tensor_glyph_items {
1230 if item.id == 0 || item.positions.is_empty() {
1231 continue;
1232 }
1233 let model = glam::Mat4::from_cols_array_2d(&item.model);
1234 let mvp = view_proj * model;
1235 let id = item.id;
1236 let mut item_hit = false;
1237 for (i, pos) in item.positions.iter().enumerate() {
1238 if let Some((sx, sy)) = project(mvp, glam::Vec3::from(*pos)) {
1239 if in_rect(sx, sy) {
1240 if wants_instance {
1241 result.elements.push((id, SubObjectRef::Instance(i as u32)));
1242 }
1243 item_hit = true;
1244 }
1245 }
1246 }
1247 if wants_object && item_hit {
1248 result.objects.push(id);
1249 }
1250 }
1251
1252 for item in &self.pick_sprite_items {
1254 if item.id == 0 || item.positions.is_empty() {
1255 continue;
1256 }
1257 let model = glam::Mat4::from_cols_array_2d(&item.model);
1258 let mvp = view_proj * model;
1259 let id = item.id;
1260 let mut item_hit = false;
1261 for (i, pos) in item.positions.iter().enumerate() {
1262 if let Some((sx, sy)) = project(mvp, glam::Vec3::from(*pos)) {
1263 if in_rect(sx, sy) {
1264 if wants_instance {
1265 result.elements.push((id, SubObjectRef::Instance(i as u32)));
1266 }
1267 item_hit = true;
1268 }
1269 }
1270 }
1271 if wants_object && item_hit {
1272 result.objects.push(id);
1273 }
1274 }
1275 }
1276
1277 let wants_poly_node = mask.intersects(PickMask::POLY_NODE);
1279 let wants_segment = mask.intersects(PickMask::SEGMENT);
1280 let wants_strip = mask.intersects(PickMask::STRIP);
1281 if wants_poly_node || wants_segment || wants_strip || wants_object {
1282 for item in &self.pick_polyline_items {
1283 if item.id == 0 || item.positions.is_empty() {
1284 continue;
1285 }
1286 let id = item.id;
1287 let mut item_hit = false;
1288 let mut strips_hit = std::collections::HashSet::<u32>::new();
1289
1290 if wants_poly_node || wants_strip || wants_object {
1292 for (node_idx, pos) in item.positions.iter().enumerate() {
1293 if let Some((sx, sy)) = project(view_proj, glam::Vec3::from(*pos)) {
1294 if in_rect(sx, sy) {
1295 item_hit = true;
1296 if wants_poly_node {
1297 result.elements.push((id, SubObjectRef::Point(node_idx as u32)));
1298 } else if wants_strip {
1299 let s = strip_for_node(node_idx as u32, &item.strip_lengths);
1300 strips_hit.insert(s);
1301 }
1302 }
1303 }
1304 }
1305 }
1306
1307 if wants_segment || (wants_strip && !wants_poly_node) || wants_object {
1309 let mut node_off = 0usize;
1310 let mut seg_off = 0u32;
1311 macro_rules! try_seg_rect {
1312 ($ai:expr, $bi:expr, $seg:expr) => {{
1313 if let (Some((sax, say)), Some((sbx, sby))) = (
1314 project(view_proj, glam::Vec3::from(item.positions[$ai])),
1315 project(view_proj, glam::Vec3::from(item.positions[$bi])),
1316 ) {
1317 if segment_in_rect(
1318 glam::Vec2::new(sax, say),
1319 glam::Vec2::new(sbx, sby),
1320 rect_min, rect_max,
1321 ) {
1322 item_hit = true;
1323 if wants_segment {
1324 result.elements.push((id, SubObjectRef::Segment($seg)));
1325 } else if wants_strip {
1326 let s = strip_for_segment($seg, &item.strip_lengths);
1327 strips_hit.insert(s);
1328 }
1329 }
1330 }
1331 }};
1332 }
1333 if item.strip_lengths.is_empty() {
1334 for j in 0..item.positions.len().saturating_sub(1) {
1335 try_seg_rect!(j, j + 1, j as u32);
1336 }
1337 } else {
1338 for &slen in &item.strip_lengths {
1339 let slen = slen as usize;
1340 for j in 0..slen.saturating_sub(1) {
1341 try_seg_rect!(node_off + j, node_off + j + 1, seg_off + j as u32);
1342 }
1343 seg_off += slen.saturating_sub(1) as u32;
1344 node_off += slen;
1345 }
1346 }
1347 }
1348
1349 if wants_strip {
1350 for s in strips_hit {
1351 result.elements.push((id, SubObjectRef::Strip(s)));
1352 }
1353 }
1354 if wants_object && item_hit {
1355 result.objects.push(id);
1356 }
1357 }
1358 }
1359
1360 if wants_segment || wants_strip || wants_object {
1362 let curve_iter = self.pick_streamtube_items.iter()
1363 .map(|it| (it.id, it.positions.as_slice(), it.strip_lengths.as_slice()))
1364 .chain(self.pick_tube_items.iter()
1365 .map(|it| (it.id, it.positions.as_slice(), it.strip_lengths.as_slice())))
1366 .chain(self.pick_ribbon_items.iter()
1367 .map(|it| (it.id, it.positions.as_slice(), it.strip_lengths.as_slice())));
1368
1369 for (id, positions, strip_lengths) in curve_iter {
1370 if id == 0 || positions.is_empty() { continue; }
1371 let mut item_hit = false;
1372 let mut strips_hit = std::collections::HashSet::<u32>::new();
1373 let mut midpoints: Vec<([f32; 3], u32)> = Vec::new();
1375 if strip_lengths.is_empty() {
1376 for j in 0..positions.len().saturating_sub(1) {
1377 let a = glam::Vec3::from(positions[j]);
1378 let b = glam::Vec3::from(positions[j + 1]);
1379 midpoints.push((((a + b) * 0.5).to_array(), j as u32));
1380 }
1381 } else {
1382 let mut node_off = 0usize;
1383 let mut seg_off = 0u32;
1384 for &slen in strip_lengths {
1385 let slen = slen as usize;
1386 for j in 0..slen.saturating_sub(1) {
1387 let a = glam::Vec3::from(positions[node_off + j]);
1388 let b = glam::Vec3::from(positions[node_off + j + 1]);
1389 midpoints.push((((a + b) * 0.5).to_array(), seg_off + j as u32));
1390 }
1391 seg_off += slen.saturating_sub(1) as u32;
1392 node_off += slen;
1393 }
1394 }
1395 for (mid, seg_idx) in &midpoints {
1396 if let Some((sx, sy)) = project(view_proj, glam::Vec3::from(*mid)) {
1397 if in_rect(sx, sy) {
1398 item_hit = true;
1399 if wants_segment {
1400 result.elements.push((id, SubObjectRef::Segment(*seg_idx)));
1401 } else if wants_strip {
1402 let s = strip_for_segment(*seg_idx, strip_lengths);
1403 strips_hit.insert(s);
1404 }
1405 }
1406 }
1407 }
1408 if wants_strip {
1409 for s in strips_hit {
1410 result.elements.push((id, SubObjectRef::Strip(s)));
1411 }
1412 }
1413 if wants_object && item_hit {
1414 result.objects.push(id);
1415 }
1416 }
1417 }
1418
1419 result
1420 }
1421
1422 pub fn pick_scene_gpu(
1446 &mut self,
1447 device: &wgpu::Device,
1448 queue: &wgpu::Queue,
1449 cursor: glam::Vec2,
1450 frame: &FrameData,
1451 ) -> Option<crate::interaction::picking::GpuPickHit> {
1452 if self.runtime_mode == crate::renderer::stats::RuntimeMode::Playback
1455 && self.frame_counter % 4 != 0
1456 {
1457 return None;
1458 }
1459
1460 let scene_items: &[SceneRenderItem] = match &frame.scene.surfaces {
1462 SurfaceSubmission::Flat(items) => items.as_ref(),
1463 };
1464
1465 let ppp = frame.camera.pixels_per_point;
1466 let vp_w = (frame.camera.viewport_size[0] * ppp).round() as u32;
1467 let vp_h = (frame.camera.viewport_size[1] * ppp).round() as u32;
1468
1469 if cursor.x < 0.0
1471 || cursor.y < 0.0
1472 || cursor.x >= frame.camera.viewport_size[0]
1473 || cursor.y >= frame.camera.viewport_size[1]
1474 || vp_w == 0
1475 || vp_h == 0
1476 {
1477 return None;
1478 }
1479
1480 self.resources.ensure_pick_pipeline(device);
1482
1483 let pickable_items: Vec<&SceneRenderItem> = scene_items
1487 .iter()
1488 .filter(|item| item.visible && item.pick_id != PickId::NONE)
1489 .collect();
1490
1491 let pick_instances: Vec<PickInstance> = pickable_items
1492 .iter()
1493 .map(|item| {
1494 let m = item.model;
1495 PickInstance {
1496 model_c0: m[0],
1497 model_c1: m[1],
1498 model_c2: m[2],
1499 model_c3: m[3],
1500 object_id: item.pick_id.0 as u32,
1501 _pad: [0; 3],
1502 }
1503 })
1504 .collect();
1505
1506 if pick_instances.is_empty() {
1507 return None;
1508 }
1509
1510 let pick_instance_bytes = bytemuck::cast_slice(&pick_instances);
1512 let pick_instance_buf = device.create_buffer(&wgpu::BufferDescriptor {
1513 label: Some("pick_instance_buf"),
1514 size: pick_instance_bytes.len().max(80) as u64,
1515 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
1516 mapped_at_creation: false,
1517 });
1518 queue.write_buffer(&pick_instance_buf, 0, pick_instance_bytes);
1519
1520 let pick_instance_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
1521 label: Some("pick_instance_bg"),
1522 layout: self
1523 .resources
1524 .pick_bind_group_layout_1
1525 .as_ref()
1526 .expect("ensure_pick_pipeline must be called first"),
1527 entries: &[wgpu::BindGroupEntry {
1528 binding: 0,
1529 resource: pick_instance_buf.as_entire_binding(),
1530 }],
1531 });
1532
1533 let camera_uniform = frame.camera.render_camera.camera_uniform();
1535 let camera_bytes = bytemuck::bytes_of(&camera_uniform);
1536 let pick_camera_buf = device.create_buffer(&wgpu::BufferDescriptor {
1537 label: Some("pick_camera_buf"),
1538 size: std::mem::size_of::<CameraUniform>() as u64,
1539 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1540 mapped_at_creation: false,
1541 });
1542 queue.write_buffer(&pick_camera_buf, 0, camera_bytes);
1543
1544 let pick_camera_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
1545 label: Some("pick_camera_bg"),
1546 layout: self
1547 .resources
1548 .pick_camera_bgl
1549 .as_ref()
1550 .expect("ensure_pick_pipeline must be called first"),
1551 entries: &[
1552 wgpu::BindGroupEntry {
1553 binding: 0,
1554 resource: pick_camera_buf.as_entire_binding(),
1555 },
1556 wgpu::BindGroupEntry {
1557 binding: 6,
1558 resource: self.resources.clip_volume_uniform_buf.as_entire_binding(),
1559 },
1560 ],
1561 });
1562
1563 let pick_id_texture = device.create_texture(&wgpu::TextureDescriptor {
1565 label: Some("pick_id_texture"),
1566 size: wgpu::Extent3d {
1567 width: vp_w,
1568 height: vp_h,
1569 depth_or_array_layers: 1,
1570 },
1571 mip_level_count: 1,
1572 sample_count: 1,
1573 dimension: wgpu::TextureDimension::D2,
1574 format: wgpu::TextureFormat::R32Uint,
1575 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
1576 view_formats: &[],
1577 });
1578 let pick_id_view = pick_id_texture.create_view(&wgpu::TextureViewDescriptor::default());
1579
1580 let pick_depth_texture = device.create_texture(&wgpu::TextureDescriptor {
1581 label: Some("pick_depth_color_texture"),
1582 size: wgpu::Extent3d {
1583 width: vp_w,
1584 height: vp_h,
1585 depth_or_array_layers: 1,
1586 },
1587 mip_level_count: 1,
1588 sample_count: 1,
1589 dimension: wgpu::TextureDimension::D2,
1590 format: wgpu::TextureFormat::R32Float,
1591 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
1592 view_formats: &[],
1593 });
1594 let pick_depth_view =
1595 pick_depth_texture.create_view(&wgpu::TextureViewDescriptor::default());
1596
1597 let depth_stencil_texture = device.create_texture(&wgpu::TextureDescriptor {
1598 label: Some("pick_ds_texture"),
1599 size: wgpu::Extent3d {
1600 width: vp_w,
1601 height: vp_h,
1602 depth_or_array_layers: 1,
1603 },
1604 mip_level_count: 1,
1605 sample_count: 1,
1606 dimension: wgpu::TextureDimension::D2,
1607 format: wgpu::TextureFormat::Depth24PlusStencil8,
1608 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
1609 view_formats: &[],
1610 });
1611 let depth_stencil_view =
1612 depth_stencil_texture.create_view(&wgpu::TextureViewDescriptor::default());
1613
1614 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
1616 label: Some("pick_pass_encoder"),
1617 });
1618 {
1619 let mut pick_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1620 label: Some("pick_pass"),
1621 color_attachments: &[
1622 Some(wgpu::RenderPassColorAttachment {
1623 view: &pick_id_view,
1624 resolve_target: None,
1625 depth_slice: None,
1626 ops: wgpu::Operations {
1627 load: wgpu::LoadOp::Clear(wgpu::Color {
1628 r: 0.0,
1629 g: 0.0,
1630 b: 0.0,
1631 a: 0.0,
1632 }),
1633 store: wgpu::StoreOp::Store,
1634 },
1635 }),
1636 Some(wgpu::RenderPassColorAttachment {
1637 view: &pick_depth_view,
1638 resolve_target: None,
1639 depth_slice: None,
1640 ops: wgpu::Operations {
1641 load: wgpu::LoadOp::Clear(wgpu::Color {
1642 r: 1.0,
1643 g: 0.0,
1644 b: 0.0,
1645 a: 0.0,
1646 }),
1647 store: wgpu::StoreOp::Store,
1648 },
1649 }),
1650 ],
1651 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
1652 view: &depth_stencil_view,
1653 depth_ops: Some(wgpu::Operations {
1654 load: wgpu::LoadOp::Clear(1.0),
1655 store: wgpu::StoreOp::Store,
1656 }),
1657 stencil_ops: None,
1658 }),
1659 timestamp_writes: None,
1660 occlusion_query_set: None,
1661 });
1662
1663 pick_pass.set_pipeline(
1664 self.resources
1665 .pick_pipeline
1666 .as_ref()
1667 .expect("ensure_pick_pipeline must be called first"),
1668 );
1669 pick_pass.set_bind_group(0, &pick_camera_bg, &[]);
1670 pick_pass.set_bind_group(1, &pick_instance_bg, &[]);
1671
1672 for (instance_slot, item) in pickable_items.iter().enumerate() {
1675 let Some(mesh) = self
1676 .resources
1677 .mesh_store
1678 .get(item.mesh_id)
1679 else {
1680 continue;
1681 };
1682 pick_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
1683 pick_pass.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
1684 let slot = instance_slot as u32;
1685 pick_pass.draw_indexed(0..mesh.index_count, 0, slot..slot + 1);
1686 }
1687 }
1688
1689 let bytes_per_row_aligned = 256u32; let id_staging = device.create_buffer(&wgpu::BufferDescriptor {
1694 label: Some("pick_id_staging"),
1695 size: bytes_per_row_aligned as u64,
1696 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
1697 mapped_at_creation: false,
1698 });
1699 let depth_staging = device.create_buffer(&wgpu::BufferDescriptor {
1700 label: Some("pick_depth_staging"),
1701 size: bytes_per_row_aligned as u64,
1702 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
1703 mapped_at_creation: false,
1704 });
1705
1706 let px = (cursor.x * ppp).round() as u32;
1708 let py = (cursor.y * ppp).round() as u32;
1709
1710 encoder.copy_texture_to_buffer(
1711 wgpu::TexelCopyTextureInfo {
1712 texture: &pick_id_texture,
1713 mip_level: 0,
1714 origin: wgpu::Origin3d { x: px, y: py, z: 0 },
1715 aspect: wgpu::TextureAspect::All,
1716 },
1717 wgpu::TexelCopyBufferInfo {
1718 buffer: &id_staging,
1719 layout: wgpu::TexelCopyBufferLayout {
1720 offset: 0,
1721 bytes_per_row: Some(bytes_per_row_aligned),
1722 rows_per_image: Some(1),
1723 },
1724 },
1725 wgpu::Extent3d {
1726 width: 1,
1727 height: 1,
1728 depth_or_array_layers: 1,
1729 },
1730 );
1731 encoder.copy_texture_to_buffer(
1732 wgpu::TexelCopyTextureInfo {
1733 texture: &pick_depth_texture,
1734 mip_level: 0,
1735 origin: wgpu::Origin3d { x: px, y: py, z: 0 },
1736 aspect: wgpu::TextureAspect::All,
1737 },
1738 wgpu::TexelCopyBufferInfo {
1739 buffer: &depth_staging,
1740 layout: wgpu::TexelCopyBufferLayout {
1741 offset: 0,
1742 bytes_per_row: Some(bytes_per_row_aligned),
1743 rows_per_image: Some(1),
1744 },
1745 },
1746 wgpu::Extent3d {
1747 width: 1,
1748 height: 1,
1749 depth_or_array_layers: 1,
1750 },
1751 );
1752
1753 queue.submit(std::iter::once(encoder.finish()));
1754
1755 let (tx_id, rx_id) = std::sync::mpsc::channel::<Result<(), wgpu::BufferAsyncError>>();
1757 let (tx_dep, rx_dep) = std::sync::mpsc::channel::<Result<(), wgpu::BufferAsyncError>>();
1758 id_staging
1759 .slice(..)
1760 .map_async(wgpu::MapMode::Read, move |r| {
1761 let _ = tx_id.send(r);
1762 });
1763 depth_staging
1764 .slice(..)
1765 .map_async(wgpu::MapMode::Read, move |r| {
1766 let _ = tx_dep.send(r);
1767 });
1768 device
1769 .poll(wgpu::PollType::Wait {
1770 submission_index: None,
1771 timeout: Some(std::time::Duration::from_secs(5)),
1772 })
1773 .unwrap();
1774 let _ = rx_id.recv().unwrap_or(Err(wgpu::BufferAsyncError));
1775 let _ = rx_dep.recv().unwrap_or(Err(wgpu::BufferAsyncError));
1776
1777 let object_id = {
1778 let data = id_staging.slice(..).get_mapped_range();
1779 u32::from_le_bytes([data[0], data[1], data[2], data[3]])
1780 };
1781 id_staging.unmap();
1782
1783 let depth = {
1784 let data = depth_staging.slice(..).get_mapped_range();
1785 f32::from_le_bytes([data[0], data[1], data[2], data[3]])
1786 };
1787 depth_staging.unmap();
1788
1789 if object_id == 0 {
1791 return None;
1792 }
1793
1794 Some(crate::interaction::picking::GpuPickHit {
1795 object_id: PickId(object_id as u64),
1796 depth,
1797 })
1798 }
1799}