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