1use crate::geometry::marching_cubes::VolumeData;
18use crate::interaction::sub_object::SubObjectRef;
19use crate::resources::volume_mesh::{CELL_SENTINEL, VolumeMeshData};
20use crate::resources::{AttributeData, AttributeKind, AttributeRef};
21use crate::scene::traits::ViewportObject;
22use parry3d::math::{Pose, Vector};
23use parry3d::query::{Ray, RayCast};
24
25#[derive(Clone, Copy, Debug)]
34#[non_exhaustive]
35pub struct PickHit {
36 pub id: u64,
38 pub sub_object: Option<SubObjectRef>,
43 pub world_pos: glam::Vec3,
45 pub normal: glam::Vec3,
47 #[deprecated(since = "0.5.0", note = "use `sub_object` instead")]
52 pub triangle_index: u32,
53 #[deprecated(since = "0.5.0", note = "use `sub_object` instead")]
58 pub point_index: Option<u32>,
59 pub scalar_value: Option<f32>,
66}
67
68impl PickHit {
69 #[allow(deprecated)]
72 pub fn object_hit(id: u64, world_pos: glam::Vec3, normal: glam::Vec3) -> Self {
73 Self {
74 id,
75 sub_object: None,
76 world_pos,
77 normal,
78 triangle_index: u32::MAX,
79 point_index: None,
80 scalar_value: None,
81 }
82 }
83}
84
85#[derive(Clone, Copy, Debug)]
97#[non_exhaustive]
98pub struct GpuPickHit {
99 pub object_id: crate::renderer::PickId,
106 pub depth: f32,
115}
116
117pub fn screen_to_ray(
130 screen_pos: glam::Vec2,
131 viewport_size: glam::Vec2,
132 view_proj_inv: glam::Mat4,
133) -> (glam::Vec3, glam::Vec3) {
134 let ndc_x = (screen_pos.x / viewport_size.x) * 2.0 - 1.0;
135 let ndc_y = 1.0 - (screen_pos.y / viewport_size.y) * 2.0; let near = view_proj_inv.project_point3(glam::Vec3::new(ndc_x, ndc_y, 0.0));
137 let far = view_proj_inv.project_point3(glam::Vec3::new(ndc_x, ndc_y, 1.0));
138 let dir = (far - near).normalize();
139 (near, dir)
140}
141
142pub fn pick_scene_cpu(
151 ray_origin: glam::Vec3,
152 ray_dir: glam::Vec3,
153 objects: &[&dyn ViewportObject],
154 mesh_lookup: &std::collections::HashMap<u64, (Vec<[f32; 3]>, Vec<u32>)>,
155) -> Option<PickHit> {
156 let ray = Ray::new(
158 Vector::new(ray_origin.x, ray_origin.y, ray_origin.z),
159 Vector::new(ray_dir.x, ray_dir.y, ray_dir.z),
160 );
161
162 let mut best_hit: Option<(u64, f32, PickHit)> = None;
163
164 for obj in objects {
165 if !obj.is_visible() {
166 continue;
167 }
168 let Some(mesh_id) = obj.mesh_id() else {
169 continue;
170 };
171
172 if let Some((positions, indices)) = mesh_lookup.get(&mesh_id) {
173 let s = obj.scale();
178 let verts: Vec<Vector> = positions
179 .iter()
180 .map(|p: &[f32; 3]| Vector::new(p[0] * s.x, p[1] * s.y, p[2] * s.z))
181 .collect();
182
183 let tri_indices: Vec<[u32; 3]> = indices
184 .chunks(3)
185 .filter(|c: &&[u32]| c.len() == 3)
186 .map(|c: &[u32]| [c[0], c[1], c[2]])
187 .collect();
188
189 if tri_indices.is_empty() {
190 continue;
191 }
192
193 match parry3d::shape::TriMesh::new(verts, tri_indices) {
194 Ok(trimesh) => {
195 let pose = Pose::from_parts(obj.position(), obj.rotation());
199 if let Some(intersection) =
200 trimesh.cast_ray_and_get_normal(&pose, &ray, f32::MAX, true)
201 {
202 let toi = intersection.time_of_impact;
203 if best_hit.is_none() || toi < best_hit.as_ref().unwrap().1 {
204 let sub_object = SubObjectRef::from_feature_id(intersection.feature);
205 let world_pos = ray_origin + ray_dir * toi;
206 let normal = intersection.normal;
208 let triangle_index = if let Some(SubObjectRef::Face(i)) = sub_object {
209 i
210 } else {
211 u32::MAX
212 };
213 #[allow(deprecated)]
214 let hit = PickHit {
215 id: obj.id(),
216 sub_object,
217 triangle_index,
218 world_pos,
219 normal,
220 point_index: None,
221 scalar_value: None,
222 };
223 best_hit = Some((obj.id(), toi, hit));
224 }
225 }
226 }
227 Err(e) => {
228 tracing::warn!(object_id = obj.id(), error = %e, "TriMesh construction failed for picking");
229 }
230 }
231 }
232 }
233
234 best_hit.map(|(_, _, hit)| hit)
235}
236
237pub fn pick_scene_nodes_cpu(
242 ray_origin: glam::Vec3,
243 ray_dir: glam::Vec3,
244 scene: &crate::scene::scene::Scene,
245 mesh_lookup: &std::collections::HashMap<u64, (Vec<[f32; 3]>, Vec<u32>)>,
246) -> Option<PickHit> {
247 let nodes: Vec<&dyn ViewportObject> = scene.nodes().map(|n| n as &dyn ViewportObject).collect();
248 pick_scene_cpu(ray_origin, ray_dir, &nodes, mesh_lookup)
249}
250
251pub struct ProbeBinding<'a> {
260 pub id: u64,
262 pub attribute_ref: &'a AttributeRef,
264 pub attribute_data: &'a AttributeData,
266 pub positions: &'a [[f32; 3]],
268 pub indices: &'a [u32],
270}
271
272fn barycentric(p: glam::Vec3, a: glam::Vec3, b: glam::Vec3, c: glam::Vec3) -> (f32, f32, f32) {
277 let v0 = b - a;
278 let v1 = c - a;
279 let v2 = p - a;
280 let d00 = v0.dot(v0);
281 let d01 = v0.dot(v1);
282 let d11 = v1.dot(v1);
283 let d20 = v2.dot(v0);
284 let d21 = v2.dot(v1);
285 let denom = d00 * d11 - d01 * d01;
286 if denom.abs() < 1e-12 {
287 return (1.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0);
289 }
290 let inv = 1.0 / denom;
291 let v = (d11 * d20 - d01 * d21) * inv;
292 let w = (d00 * d21 - d01 * d20) * inv;
293 let u = 1.0 - v - w;
294 (u, v, w)
295}
296
297fn probe_scalar(hit: &mut PickHit, binding: &ProbeBinding<'_>) {
300 let tri_idx_raw = match hit.sub_object {
301 Some(SubObjectRef::Face(i)) => i,
302 _ => return,
303 };
304
305 let num_triangles = binding.indices.len() / 3;
306 let tri_idx = if (tri_idx_raw as usize) >= num_triangles && num_triangles > 0 {
309 tri_idx_raw as usize - num_triangles
310 } else {
311 tri_idx_raw as usize
312 };
313
314 match binding.attribute_ref.kind {
315 AttributeKind::Cell => {
316 if let AttributeData::Cell(data) = binding.attribute_data {
318 if let Some(&val) = data.get(tri_idx) {
319 hit.scalar_value = Some(val);
320 }
321 }
322 }
323 AttributeKind::Face => {
324 if let AttributeData::Face(data) = binding.attribute_data {
326 if let Some(&val) = data.get(tri_idx) {
327 hit.scalar_value = Some(val);
328 }
329 }
330 }
331 AttributeKind::FaceColour => {
332 }
334 AttributeKind::Vertex => {
335 if let AttributeData::Vertex(data) = binding.attribute_data {
337 let base = tri_idx * 3;
338 if base + 2 >= binding.indices.len() {
339 return;
340 }
341 let i0 = binding.indices[base] as usize;
342 let i1 = binding.indices[base + 1] as usize;
343 let i2 = binding.indices[base + 2] as usize;
344
345 if i0 >= data.len() || i1 >= data.len() || i2 >= data.len() {
346 return;
347 }
348 if i0 >= binding.positions.len()
349 || i1 >= binding.positions.len()
350 || i2 >= binding.positions.len()
351 {
352 return;
353 }
354
355 let a = glam::Vec3::from(binding.positions[i0]);
356 let b = glam::Vec3::from(binding.positions[i1]);
357 let c = glam::Vec3::from(binding.positions[i2]);
358 let (u, v, w) = barycentric(hit.world_pos, a, b, c);
359 hit.scalar_value = Some(data[i0] * u + data[i1] * v + data[i2] * w);
360 }
361 }
362 AttributeKind::Edge => {
363 if let AttributeData::Edge(data) = binding.attribute_data {
366 let base = tri_idx * 3;
367 if base + 2 >= binding.indices.len() || data.is_empty() {
368 return;
369 }
370 let i0 = binding.indices[base] as usize;
371 let i1 = binding.indices[base + 1] as usize;
372 let i2 = binding.indices[base + 2] as usize;
373 if i0 < data.len() || i1 < data.len() || i2 < data.len() {
374 if i0 < data.len()
376 && i1 < data.len()
377 && i2 < data.len()
378 && i0 < binding.positions.len()
379 && i1 < binding.positions.len()
380 && i2 < binding.positions.len()
381 {
382 let a = glam::Vec3::from(binding.positions[i0]);
383 let b = glam::Vec3::from(binding.positions[i1]);
384 let c = glam::Vec3::from(binding.positions[i2]);
385 let (u, v, w) = barycentric(hit.world_pos, a, b, c);
386 hit.scalar_value = Some(data[i0] * u + data[i1] * v + data[i2] * w);
387 }
388 }
389 }
390 }
391 AttributeKind::Halfedge | AttributeKind::Corner => {
392 let extract = |data: &[f32]| -> Option<f32> {
395 let base = tri_idx * 3;
396 if base + 2 >= data.len() {
397 return None;
398 }
399 Some(data[base])
401 };
402 match binding.attribute_data {
403 AttributeData::Halfedge(data) | AttributeData::Corner(data) => {
404 hit.scalar_value = extract(data);
405 }
406 _ => {}
407 }
408 }
409 }
410}
411
412pub fn pick_scene_with_probe_cpu(
419 ray_origin: glam::Vec3,
420 ray_dir: glam::Vec3,
421 objects: &[&dyn ViewportObject],
422 mesh_lookup: &std::collections::HashMap<u64, (Vec<[f32; 3]>, Vec<u32>)>,
423 probe_bindings: &[ProbeBinding<'_>],
424) -> Option<PickHit> {
425 let mut hit = pick_scene_cpu(ray_origin, ray_dir, objects, mesh_lookup)?;
426 if let Some(binding) = probe_bindings.iter().find(|b| b.id == hit.id) {
427 probe_scalar(&mut hit, binding);
428 }
429 Some(hit)
430}
431
432pub fn pick_scene_nodes_with_probe_cpu(
436 ray_origin: glam::Vec3,
437 ray_dir: glam::Vec3,
438 scene: &crate::scene::scene::Scene,
439 mesh_lookup: &std::collections::HashMap<u64, (Vec<[f32; 3]>, Vec<u32>)>,
440 probe_bindings: &[ProbeBinding<'_>],
441) -> Option<PickHit> {
442 let mut hit = pick_scene_nodes_cpu(ray_origin, ray_dir, scene, mesh_lookup)?;
443 if let Some(binding) = probe_bindings.iter().find(|b| b.id == hit.id) {
444 probe_scalar(&mut hit, binding);
445 }
446 Some(hit)
447}
448
449pub fn pick_scene_accelerated_with_probe_cpu(
454 ray_origin: glam::Vec3,
455 ray_dir: glam::Vec3,
456 accelerator: &mut crate::geometry::bvh::PickAccelerator,
457 mesh_lookup: &std::collections::HashMap<u64, (Vec<[f32; 3]>, Vec<u32>)>,
458 probe_bindings: &[ProbeBinding<'_>],
459) -> Option<PickHit> {
460 let mut hit = accelerator.pick(ray_origin, ray_dir, mesh_lookup)?;
461 if let Some(binding) = probe_bindings.iter().find(|b| b.id == hit.id) {
462 probe_scalar(&mut hit, binding);
463 }
464 Some(hit)
465}
466
467#[derive(Clone, Debug, Default)]
478pub struct RectPickResult {
479 pub hits: std::collections::HashMap<u64, Vec<SubObjectRef>>,
486}
487
488impl RectPickResult {
489 pub fn is_empty(&self) -> bool {
491 self.hits.is_empty()
492 }
493
494 pub fn total_count(&self) -> usize {
496 self.hits.values().map(|v| v.len()).sum()
497 }
498}
499
500pub fn pick_rect(
518 rect_min: glam::Vec2,
519 rect_max: glam::Vec2,
520 scene_items: &[crate::renderer::SceneRenderItem],
521 mesh_lookup: &std::collections::HashMap<usize, (Vec<[f32; 3]>, Vec<u32>)>,
522 point_clouds: &[crate::renderer::PointCloudItem],
523 view_proj: glam::Mat4,
524 viewport_size: glam::Vec2,
525) -> RectPickResult {
526 let ndc_min = glam::Vec2::new(
529 rect_min.x / viewport_size.x * 2.0 - 1.0,
530 1.0 - rect_max.y / viewport_size.y * 2.0, );
532 let ndc_max = glam::Vec2::new(
533 rect_max.x / viewport_size.x * 2.0 - 1.0,
534 1.0 - rect_min.y / viewport_size.y * 2.0, );
536
537 let mut result = RectPickResult::default();
538
539 for item in scene_items {
541 if item.appearance.hidden {
542 continue;
543 }
544 let Some((positions, indices)) = mesh_lookup.get(&item.mesh_id.index()) else {
545 continue;
546 };
547
548 let model = glam::Mat4::from_cols_array_2d(&item.model);
549 let mvp = view_proj * model;
550
551 let mut tri_hits: Vec<SubObjectRef> = Vec::new();
552
553 for (tri_idx, chunk) in indices.chunks(3).enumerate() {
554 if chunk.len() < 3 {
555 continue;
556 }
557 let i0 = chunk[0] as usize;
558 let i1 = chunk[1] as usize;
559 let i2 = chunk[2] as usize;
560
561 if i0 >= positions.len() || i1 >= positions.len() || i2 >= positions.len() {
562 continue;
563 }
564
565 let p0 = glam::Vec3::from(positions[i0]);
566 let p1 = glam::Vec3::from(positions[i1]);
567 let p2 = glam::Vec3::from(positions[i2]);
568 let centroid = (p0 + p1 + p2) / 3.0;
569
570 let clip = mvp * centroid.extend(1.0);
571 if clip.w <= 0.0 {
572 continue;
574 }
575 let ndc = glam::Vec2::new(clip.x / clip.w, clip.y / clip.w);
576
577 if ndc.x >= ndc_min.x && ndc.x <= ndc_max.x && ndc.y >= ndc_min.y && ndc.y <= ndc_max.y
578 {
579 tri_hits.push(SubObjectRef::Face(tri_idx as u32));
580 }
581 }
582
583 if !tri_hits.is_empty() {
584 result.hits.insert(item.pick_id.0, tri_hits);
585 }
586 }
587
588 for pc in point_clouds {
590 if pc.id == 0 {
591 continue;
593 }
594
595 let model = glam::Mat4::from_cols_array_2d(&pc.model);
596 let mvp = view_proj * model;
597
598 let mut pt_hits: Vec<SubObjectRef> = Vec::new();
599
600 for (pt_idx, pos) in pc.positions.iter().enumerate() {
601 let p = glam::Vec3::from(*pos);
602 let clip = mvp * p.extend(1.0);
603 if clip.w <= 0.0 {
604 continue;
605 }
606 let ndc = glam::Vec2::new(clip.x / clip.w, clip.y / clip.w);
607
608 if ndc.x >= ndc_min.x && ndc.x <= ndc_max.x && ndc.y >= ndc_min.y && ndc.y <= ndc_max.y
609 {
610 pt_hits.push(SubObjectRef::Point(pt_idx as u32));
611 }
612 }
613
614 if !pt_hits.is_empty() {
615 result.hits.insert(pc.id, pt_hits);
616 }
617 }
618
619 result
620}
621
622pub fn box_select(
631 rect_min: glam::Vec2,
632 rect_max: glam::Vec2,
633 objects: &[&dyn ViewportObject],
634 view_proj: glam::Mat4,
635 viewport_size: glam::Vec2,
636) -> Vec<u64> {
637 let mut hits = Vec::new();
638 for obj in objects {
639 if !obj.is_visible() {
640 continue;
641 }
642 let pos = obj.position();
643 let clip = view_proj * pos.extend(1.0);
644 if clip.w <= 0.0 {
646 continue;
647 }
648 let ndc = glam::Vec3::new(clip.x / clip.w, clip.y / clip.w, clip.z / clip.w);
649 let screen = glam::Vec2::new(
650 (ndc.x + 1.0) * 0.5 * viewport_size.x,
651 (1.0 - ndc.y) * 0.5 * viewport_size.y,
652 );
653 if screen.x >= rect_min.x
654 && screen.x <= rect_max.x
655 && screen.y >= rect_min.y
656 && screen.y <= rect_max.y
657 {
658 hits.push(obj.id());
659 }
660 }
661 hits
662}
663
664fn ray_aabb_volume(
675 origin: glam::Vec3,
676 dir: glam::Vec3,
677 bbox_min: glam::Vec3,
678 bbox_max: glam::Vec3,
679) -> Option<(f32, f32, usize, f32)> {
680 let mut t_min = f32::NEG_INFINITY;
681 let mut t_max = f32::INFINITY;
682 let mut entry_axis = 0usize;
683 let mut entry_sign = -1.0f32;
684
685 let dirs = [dir.x, dir.y, dir.z];
686 let origins = [origin.x, origin.y, origin.z];
687 let mins = [bbox_min.x, bbox_min.y, bbox_min.z];
688 let maxs = [bbox_max.x, bbox_max.y, bbox_max.z];
689
690 for i in 0..3 {
691 let d = dirs[i];
692 let o = origins[i];
693 if d.abs() < 1e-12 {
694 if o < mins[i] || o > maxs[i] {
696 return None;
697 }
698 } else {
699 let t1 = (mins[i] - o) / d;
700 let t2 = (maxs[i] - o) / d;
701 let (t_near, t_far) = if t1 <= t2 { (t1, t2) } else { (t2, t1) };
702 if t_near > t_min {
703 t_min = t_near;
704 entry_axis = i;
705 entry_sign = if d > 0.0 { -1.0 } else { 1.0 };
708 }
709 if t_far < t_max {
710 t_max = t_far;
711 }
712 }
713 }
714
715 if t_min > t_max || t_max < 0.0 {
716 return None;
717 }
718 Some((t_min, t_max, entry_axis, entry_sign))
719}
720
721pub fn pick_volume_cpu(
746 ray_origin: glam::Vec3,
747 ray_dir: glam::Vec3,
748 id: u64,
749 item: &crate::renderer::VolumeItem,
750 volume: &VolumeData,
751) -> Option<PickHit> {
752 let [nx, ny, nz] = volume.dims;
753 if nx == 0 || ny == 0 || nz == 0 || volume.data.is_empty() {
754 return None;
755 }
756
757 let model = glam::Mat4::from_cols_array_2d(&item.model);
759 let inv_model = model.inverse();
760 let local_origin = inv_model.transform_point3(ray_origin);
761 let local_dir = inv_model.transform_vector3(ray_dir);
762
763 let bbox_min = glam::Vec3::from(item.bbox_min);
764 let bbox_max = glam::Vec3::from(item.bbox_max);
765
766 let (t_entry, t_exit, entry_axis, entry_sign) =
767 ray_aabb_volume(local_origin, local_dir, bbox_min, bbox_max)?;
768
769 let t_start = t_entry.max(0.0);
771 if t_start >= t_exit {
772 return None;
773 }
774
775 let extent = bbox_max - bbox_min;
777 let cell = extent / glam::Vec3::new(nx as f32, ny as f32, nz as f32);
778
779 let p_entry = local_origin + t_start * local_dir;
781
782 let eps = 1e-4_f32;
785 let frac =
786 ((p_entry - bbox_min) / extent).clamp(glam::Vec3::splat(eps), glam::Vec3::splat(1.0 - eps));
787 let mut ix = (frac.x * nx as f32).floor() as i32;
788 let mut iy = (frac.y * ny as f32).floor() as i32;
789 let mut iz = (frac.z * nz as f32).floor() as i32;
790 ix = ix.clamp(0, nx as i32 - 1);
791 iy = iy.clamp(0, ny as i32 - 1);
792 iz = iz.clamp(0, nz as i32 - 1);
793
794 let step_x: i32 = if local_dir.x >= 0.0 { 1 } else { -1 };
796 let step_y: i32 = if local_dir.y >= 0.0 { 1 } else { -1 };
797 let step_z: i32 = if local_dir.z >= 0.0 { 1 } else { -1 };
798
799 let td_x = if local_dir.x.abs() > 1e-12 {
801 cell.x / local_dir.x.abs()
802 } else {
803 f32::INFINITY
804 };
805 let td_y = if local_dir.y.abs() > 1e-12 {
806 cell.y / local_dir.y.abs()
807 } else {
808 f32::INFINITY
809 };
810 let td_z = if local_dir.z.abs() > 1e-12 {
811 cell.z / local_dir.z.abs()
812 } else {
813 f32::INFINITY
814 };
815
816 let next_bx = bbox_min.x + (if step_x > 0 { ix + 1 } else { ix }) as f32 * cell.x;
818 let next_by = bbox_min.y + (if step_y > 0 { iy + 1 } else { iy }) as f32 * cell.y;
819 let next_bz = bbox_min.z + (if step_z > 0 { iz + 1 } else { iz }) as f32 * cell.z;
820
821 let mut tmax_x = if local_dir.x.abs() > 1e-12 {
822 t_start + (next_bx - p_entry.x) / local_dir.x
823 } else {
824 f32::INFINITY
825 };
826 let mut tmax_y = if local_dir.y.abs() > 1e-12 {
827 t_start + (next_by - p_entry.y) / local_dir.y
828 } else {
829 f32::INFINITY
830 };
831 let mut tmax_z = if local_dir.z.abs() > 1e-12 {
832 t_start + (next_bz - p_entry.z) / local_dir.z
833 } else {
834 f32::INFINITY
835 };
836
837 let mut entry_normal_local = glam::Vec3::ZERO;
839 match entry_axis {
840 0 => entry_normal_local.x = entry_sign,
841 1 => entry_normal_local.y = entry_sign,
842 _ => entry_normal_local.z = entry_sign,
843 }
844
845 let mut t_voxel_entry = t_start;
847
848 loop {
849 if ix < 0 || ix >= nx as i32 || iy < 0 || iy >= ny as i32 || iz < 0 || iz >= nz as i32 {
851 break;
852 }
853
854 let flat = ix as u32 + iy as u32 * nx + iz as u32 * nx * ny;
855 let scalar = volume.data[flat as usize];
856
857 if !scalar.is_nan() && scalar >= item.threshold_min && scalar <= item.threshold_max {
859 let local_hit = local_origin + t_voxel_entry * local_dir;
860 let world_pos = model.transform_point3(local_hit);
861 let world_normal = inv_model
863 .transpose()
864 .transform_vector3(entry_normal_local)
865 .normalize();
866
867 #[allow(deprecated)]
868 return Some(PickHit {
869 id,
870 sub_object: Some(SubObjectRef::Voxel(flat)),
871 world_pos,
872 normal: world_normal,
873 triangle_index: u32::MAX,
874 point_index: None,
875 scalar_value: Some(scalar),
876 });
877 }
878
879 if tmax_x <= tmax_y && tmax_x <= tmax_z {
881 if tmax_x > t_exit {
882 break;
883 }
884 t_voxel_entry = tmax_x;
885 tmax_x += td_x;
886 ix += step_x;
887 entry_normal_local = glam::Vec3::new(-(step_x as f32), 0.0, 0.0);
888 } else if tmax_y <= tmax_z {
889 if tmax_y > t_exit {
890 break;
891 }
892 t_voxel_entry = tmax_y;
893 tmax_y += td_y;
894 iy += step_y;
895 entry_normal_local = glam::Vec3::new(0.0, -(step_y as f32), 0.0);
896 } else {
897 if tmax_z > t_exit {
898 break;
899 }
900 t_voxel_entry = tmax_z;
901 tmax_z += td_z;
902 iz += step_z;
903 entry_normal_local = glam::Vec3::new(0.0, 0.0, -(step_z as f32));
904 }
905 }
906
907 None
908}
909
910pub fn voxel_world_aabb(
924 flat_index: u32,
925 volume: &VolumeData,
926 item: &crate::renderer::VolumeItem,
927) -> (glam::Vec3, glam::Vec3) {
928 let [nx, ny, nz] = volume.dims;
929 let ix = flat_index % nx;
930 let iy = (flat_index / nx) % ny;
931 let iz = flat_index / (nx * ny);
932 assert!(
933 ix < nx && iy < ny && iz < nz,
934 "flat_index {} out of bounds for dims {:?}",
935 flat_index,
936 volume.dims
937 );
938
939 let bbox_min = glam::Vec3::from(item.bbox_min);
940 let bbox_max = glam::Vec3::from(item.bbox_max);
941 let cell = (bbox_max - bbox_min) / glam::Vec3::new(nx as f32, ny as f32, nz as f32);
942
943 let local_lo =
944 bbox_min + glam::Vec3::new(ix as f32 * cell.x, iy as f32 * cell.y, iz as f32 * cell.z);
945 let local_hi = local_lo + cell;
946
947 let model = glam::Mat4::from_cols_array_2d(&item.model);
948 let corners = [
949 glam::Vec3::new(local_lo.x, local_lo.y, local_lo.z),
950 glam::Vec3::new(local_hi.x, local_lo.y, local_lo.z),
951 glam::Vec3::new(local_lo.x, local_hi.y, local_lo.z),
952 glam::Vec3::new(local_hi.x, local_hi.y, local_lo.z),
953 glam::Vec3::new(local_lo.x, local_lo.y, local_hi.z),
954 glam::Vec3::new(local_hi.x, local_lo.y, local_hi.z),
955 glam::Vec3::new(local_lo.x, local_hi.y, local_hi.z),
956 glam::Vec3::new(local_hi.x, local_hi.y, local_hi.z),
957 ];
958
959 let world_min = corners
960 .iter()
961 .map(|&c| model.transform_point3(c))
962 .fold(glam::Vec3::splat(f32::INFINITY), |acc, c| acc.min(c));
963 let world_max = corners
964 .iter()
965 .map(|&c| model.transform_point3(c))
966 .fold(glam::Vec3::splat(f32::NEG_INFINITY), |acc, c| acc.max(c));
967
968 (world_min, world_max)
969}
970
971pub fn pick_point_cloud_cpu(
985 click_pos: glam::Vec2,
986 id: u64,
987 item: &crate::renderer::PointCloudItem,
988 view_proj: glam::Mat4,
989 viewport_size: glam::Vec2,
990 radius_px: f32,
991) -> Option<PickHit> {
992 if id == 0 || item.positions.is_empty() {
993 return None;
994 }
995
996 let model = glam::Mat4::from_cols_array_2d(&item.model);
997 let mvp = view_proj * model;
998
999 let mut best_dist_sq = radius_px * radius_px;
1000 let mut best_idx: Option<u32> = None;
1001 let mut best_world = glam::Vec3::ZERO;
1002
1003 for (pt_idx, pos) in item.positions.iter().enumerate() {
1004 let local = glam::Vec3::from(*pos);
1005 let clip = mvp * local.extend(1.0);
1006 if clip.w <= 0.0 {
1007 continue;
1008 }
1009 let ndc_x = clip.x / clip.w;
1010 let ndc_y = clip.y / clip.w;
1011 let sx = (ndc_x + 1.0) * 0.5 * viewport_size.x;
1012 let sy = (1.0 - ndc_y) * 0.5 * viewport_size.y;
1013 let dx = sx - click_pos.x;
1014 let dy = sy - click_pos.y;
1015 let dist_sq = dx * dx + dy * dy;
1016 if dist_sq < best_dist_sq {
1017 best_dist_sq = dist_sq;
1018 best_idx = Some(pt_idx as u32);
1019 best_world = model.transform_point3(local);
1020 }
1021 }
1022
1023 let pt_idx = best_idx?;
1024 #[allow(deprecated)]
1025 Some(PickHit {
1026 id,
1027 sub_object: Some(SubObjectRef::Point(pt_idx)),
1028 world_pos: best_world,
1029 normal: glam::Vec3::Y,
1030 triangle_index: u32::MAX,
1031 point_index: Some(pt_idx),
1032 scalar_value: None,
1033 })
1034}
1035
1036pub fn nearest_vertex_on_hit(
1055 hit: &PickHit,
1056 positions: &[[f32; 3]],
1057 indices: &[u32],
1058 model: glam::Mat4,
1059) -> Option<SubObjectRef> {
1060 let face_raw = match hit.sub_object {
1061 Some(SubObjectRef::Face(i)) => i as usize,
1062 _ => return None,
1063 };
1064 let n_tri = indices.len() / 3;
1065 if n_tri == 0 {
1066 return None;
1067 }
1068 let face = if face_raw >= n_tri {
1070 face_raw - n_tri
1071 } else {
1072 face_raw
1073 };
1074 if face * 3 + 2 >= indices.len() {
1075 return None;
1076 }
1077 let vi = [
1078 indices[face * 3] as usize,
1079 indices[face * 3 + 1] as usize,
1080 indices[face * 3 + 2] as usize,
1081 ];
1082 let (best_vi, _) = vi
1083 .iter()
1084 .map(|&i| {
1085 let p = model.transform_point3(glam::Vec3::from(positions[i]));
1086 (i, p.distance(hit.world_pos))
1087 })
1088 .fold(
1089 (vi[0], f32::MAX),
1090 |acc, (i, d)| {
1091 if d < acc.1 { (i, d) } else { acc }
1092 },
1093 );
1094 Some(SubObjectRef::Vertex(best_vi as u32))
1095}
1096
1097pub fn pick_gaussian_splat_cpu(
1118 click_pos: glam::Vec2,
1119 id: u64,
1120 positions: &[[f32; 3]],
1121 model: glam::Mat4,
1122 view_proj: glam::Mat4,
1123 viewport_size: glam::Vec2,
1124 radius_px: f32,
1125) -> Option<PickHit> {
1126 if id == 0 || positions.is_empty() {
1127 return None;
1128 }
1129 let mvp = view_proj * model;
1130 let mut best_dist_sq = radius_px * radius_px;
1131 let mut best_idx: Option<u32> = None;
1132 let mut best_world = glam::Vec3::ZERO;
1133
1134 for (i, pos) in positions.iter().enumerate() {
1135 let local = glam::Vec3::from(*pos);
1136 let clip = mvp * local.extend(1.0);
1137 if clip.w <= 0.0 {
1138 continue;
1139 }
1140 let sx = (clip.x / clip.w + 1.0) * 0.5 * viewport_size.x;
1141 let sy = (1.0 - clip.y / clip.w) * 0.5 * viewport_size.y;
1142 let dx = sx - click_pos.x;
1143 let dy = sy - click_pos.y;
1144 let dist_sq = dx * dx + dy * dy;
1145 if dist_sq < best_dist_sq {
1146 best_dist_sq = dist_sq;
1147 best_idx = Some(i as u32);
1148 best_world = model.transform_point3(local);
1149 }
1150 }
1151
1152 let idx = best_idx?;
1153 #[allow(deprecated)]
1154 Some(PickHit {
1155 id,
1156 sub_object: Some(SubObjectRef::Point(idx)),
1157 world_pos: best_world,
1158 normal: glam::Vec3::Y,
1159 triangle_index: u32::MAX,
1160 point_index: Some(idx),
1161 scalar_value: None,
1162 })
1163}
1164
1165fn ray_tri_mt_ds(
1173 orig: glam::Vec3,
1174 dir: glam::Vec3,
1175 v0: glam::Vec3,
1176 v1: glam::Vec3,
1177 v2: glam::Vec3,
1178) -> Option<f32> {
1179 let e1 = v1 - v0;
1180 let e2 = v2 - v0;
1181 let h = dir.cross(e2);
1182 let a = e1.dot(h);
1183 if a.abs() < 1e-8 {
1184 return None;
1185 }
1186 let f = 1.0 / a;
1187 let s = orig - v0;
1188 let u = f * s.dot(h);
1189 if !(0.0..=1.0).contains(&u) {
1190 return None;
1191 }
1192 let q = s.cross(e1);
1193 let v = f * dir.dot(q);
1194 if v < 0.0 || u + v > 1.0 {
1195 return None;
1196 }
1197 let t = f * e2.dot(q);
1198 if t > 1e-6 { Some(t) } else { None }
1199}
1200
1201const VM_TET_FACES: [[usize; 3]; 4] = [[1, 2, 3], [0, 3, 2], [0, 1, 3], [0, 2, 1]];
1206
1207const VM_HEX_TRIS: [[usize; 3]; 12] = [
1209 [0, 1, 2],
1210 [0, 2, 3], [4, 7, 6],
1212 [4, 6, 5], [0, 4, 5],
1214 [0, 5, 1], [2, 6, 7],
1216 [2, 7, 3], [0, 3, 7],
1218 [0, 7, 4], [1, 5, 6],
1220 [1, 6, 2], ];
1222
1223const VM_PYRAMID_TRIS: [[usize; 3]; 6] = [
1225 [0, 1, 2],
1226 [0, 2, 3], [0, 4, 1],
1228 [1, 4, 2],
1229 [2, 4, 3],
1230 [3, 4, 0], ];
1232
1233const VM_WEDGE_TRIS: [[usize; 3]; 8] = [
1235 [0, 2, 1],
1236 [3, 4, 5], [0, 1, 4],
1238 [0, 4, 3], [1, 2, 5],
1240 [1, 5, 4], [2, 0, 3],
1242 [2, 3, 5], ];
1244
1245pub fn pick_transparent_volume_mesh_cpu(
1260 ray_origin: glam::Vec3,
1261 ray_dir: glam::Vec3,
1262 id: u64,
1263 model: glam::Mat4,
1264 data: &VolumeMeshData,
1265) -> Option<PickHit> {
1266 if id == 0 || data.cells.is_empty() {
1267 return None;
1268 }
1269 let model_inv = model.inverse();
1270 let local_origin = model_inv.transform_point3(ray_origin);
1271 let local_dir = model_inv.transform_vector3(ray_dir);
1272
1273 let mut best_t = f32::MAX;
1274 let mut best_cell: Option<u32> = None;
1275
1276 for (cell_idx, cell) in data.cells.iter().enumerate() {
1277 let p = |i: usize| glam::Vec3::from(data.positions[cell[i] as usize]);
1278 let tris: &[[usize; 3]] = if cell[4] == CELL_SENTINEL {
1279 &VM_TET_FACES
1280 } else if cell[5] == CELL_SENTINEL {
1281 &VM_PYRAMID_TRIS
1282 } else if cell[6] == CELL_SENTINEL {
1283 &VM_WEDGE_TRIS
1284 } else {
1285 &VM_HEX_TRIS
1286 };
1287 for tri in tris {
1288 if let Some(t) = ray_tri_mt_ds(local_origin, local_dir, p(tri[0]), p(tri[1]), p(tri[2]))
1289 {
1290 if t < best_t {
1291 best_t = t;
1292 best_cell = Some(cell_idx as u32);
1293 }
1294 }
1295 }
1296 }
1297
1298 let cell_idx = best_cell?;
1299 let local_hit = local_origin + local_dir * best_t;
1300 let world_hit = model.transform_point3(local_hit);
1301 #[allow(deprecated)]
1302 Some(PickHit {
1303 id,
1304 sub_object: Some(SubObjectRef::Cell(cell_idx)),
1305 world_pos: world_hit,
1306 normal: -ray_dir.normalize(),
1307 triangle_index: u32::MAX,
1308 point_index: None,
1309 scalar_value: None,
1310 })
1311}
1312
1313pub fn pick_volume_rect(
1333 rect_min: glam::Vec2,
1334 rect_max: glam::Vec2,
1335 id: u64,
1336 item: &crate::renderer::VolumeItem,
1337 volume: &VolumeData,
1338 view_proj: glam::Mat4,
1339 viewport_size: glam::Vec2,
1340) -> RectPickResult {
1341 let mut result = RectPickResult::default();
1342 if id == 0 {
1343 return result;
1344 }
1345 let model = glam::Mat4::from_cols_array_2d(&item.model);
1346 let bbox_min = glam::Vec3::from(item.bbox_min);
1347 let bbox_max = glam::Vec3::from(item.bbox_max);
1348 let [nx, ny, nz] = volume.dims;
1349 let cell = (bbox_max - bbox_min) / glam::Vec3::new(nx as f32, ny as f32, nz as f32);
1350 let mvp = view_proj * model;
1351
1352 let mut hits: Vec<SubObjectRef> = Vec::new();
1353 for iz in 0..nz {
1354 for iy in 0..ny {
1355 for ix in 0..nx {
1356 let flat = ix + iy * nx + iz * nx * ny;
1357 let scalar = volume.data[flat as usize];
1358 if scalar.is_nan() || scalar < item.threshold_min || scalar > item.threshold_max {
1359 continue;
1360 }
1361 let local_center = bbox_min
1362 + cell * glam::Vec3::new(ix as f32 + 0.5, iy as f32 + 0.5, iz as f32 + 0.5);
1363 let clip = mvp * local_center.extend(1.0);
1364 if clip.w <= 0.0 {
1365 continue;
1366 }
1367 let sx = (clip.x / clip.w + 1.0) * 0.5 * viewport_size.x;
1368 let sy = (1.0 - clip.y / clip.w) * 0.5 * viewport_size.y;
1369 if sx >= rect_min.x && sx <= rect_max.x && sy >= rect_min.y && sy <= rect_max.y {
1370 hits.push(SubObjectRef::Voxel(flat));
1371 }
1372 }
1373 }
1374 }
1375 if !hits.is_empty() {
1376 result.hits.insert(id, hits);
1377 }
1378 result
1379}
1380
1381pub fn pick_transparent_volume_mesh_rect(
1400 rect_min: glam::Vec2,
1401 rect_max: glam::Vec2,
1402 id: u64,
1403 model: glam::Mat4,
1404 data: &VolumeMeshData,
1405 view_proj: glam::Mat4,
1406 viewport_size: glam::Vec2,
1407) -> RectPickResult {
1408 let mut result = RectPickResult::default();
1409 if id == 0 || data.cells.is_empty() {
1410 return result;
1411 }
1412 let mvp = view_proj * model;
1413 let mut hits: Vec<SubObjectRef> = Vec::new();
1414
1415 for (cell_idx, cell) in data.cells.iter().enumerate() {
1416 let nv: usize = if cell[4] == CELL_SENTINEL {
1417 4
1418 } else if cell[5] == CELL_SENTINEL {
1419 5
1420 } else if cell[6] == CELL_SENTINEL {
1421 6
1422 } else {
1423 8
1424 };
1425 let centroid: glam::Vec3 = cell[..nv]
1426 .iter()
1427 .map(|&vi| glam::Vec3::from(data.positions[vi as usize]))
1428 .sum::<glam::Vec3>()
1429 / nv as f32;
1430 let clip = mvp * centroid.extend(1.0);
1431 if clip.w <= 0.0 {
1432 continue;
1433 }
1434 let sx = (clip.x / clip.w + 1.0) * 0.5 * viewport_size.x;
1435 let sy = (1.0 - clip.y / clip.w) * 0.5 * viewport_size.y;
1436 if sx >= rect_min.x && sx <= rect_max.x && sy >= rect_min.y && sy <= rect_max.y {
1437 hits.push(SubObjectRef::Cell(cell_idx as u32));
1438 }
1439 }
1440 if !hits.is_empty() {
1441 result.hits.insert(id, hits);
1442 }
1443 result
1444}
1445
1446pub fn pick_gaussian_splat_rect(
1465 rect_min: glam::Vec2,
1466 rect_max: glam::Vec2,
1467 id: u64,
1468 positions: &[[f32; 3]],
1469 model: glam::Mat4,
1470 view_proj: glam::Mat4,
1471 viewport_size: glam::Vec2,
1472) -> RectPickResult {
1473 let mut result = RectPickResult::default();
1474 if id == 0 || positions.is_empty() {
1475 return result;
1476 }
1477 let mvp = view_proj * model;
1478 let mut hits: Vec<SubObjectRef> = Vec::new();
1479
1480 for (i, pos) in positions.iter().enumerate() {
1481 let local = glam::Vec3::from(*pos);
1482 let clip = mvp * local.extend(1.0);
1483 if clip.w <= 0.0 {
1484 continue;
1485 }
1486 let sx = (clip.x / clip.w + 1.0) * 0.5 * viewport_size.x;
1487 let sy = (1.0 - clip.y / clip.w) * 0.5 * viewport_size.y;
1488 if sx >= rect_min.x && sx <= rect_max.x && sy >= rect_min.y && sy <= rect_max.y {
1489 hits.push(SubObjectRef::Point(i as u32));
1490 }
1491 }
1492 if !hits.is_empty() {
1493 result.hits.insert(id, hits);
1494 }
1495 result
1496}
1497
1498#[cfg(test)]
1499mod tests {
1500 use super::*;
1501 use crate::scene::traits::ViewportObject;
1502 use std::collections::HashMap;
1503
1504 struct TestObject {
1505 id: u64,
1506 mesh_id: u64,
1507 position: glam::Vec3,
1508 visible: bool,
1509 }
1510
1511 impl ViewportObject for TestObject {
1512 fn id(&self) -> u64 {
1513 self.id
1514 }
1515 fn mesh_id(&self) -> Option<u64> {
1516 Some(self.mesh_id)
1517 }
1518 fn model_matrix(&self) -> glam::Mat4 {
1519 glam::Mat4::from_translation(self.position)
1520 }
1521 fn position(&self) -> glam::Vec3 {
1522 self.position
1523 }
1524 fn rotation(&self) -> glam::Quat {
1525 glam::Quat::IDENTITY
1526 }
1527 fn is_visible(&self) -> bool {
1528 self.visible
1529 }
1530 fn colour(&self) -> glam::Vec3 {
1531 glam::Vec3::ONE
1532 }
1533 }
1534
1535 fn unit_cube_mesh() -> (Vec<[f32; 3]>, Vec<u32>) {
1537 let positions = vec![
1538 [-0.5, -0.5, -0.5],
1539 [0.5, -0.5, -0.5],
1540 [0.5, 0.5, -0.5],
1541 [-0.5, 0.5, -0.5],
1542 [-0.5, -0.5, 0.5],
1543 [0.5, -0.5, 0.5],
1544 [0.5, 0.5, 0.5],
1545 [-0.5, 0.5, 0.5],
1546 ];
1547 let indices = vec![
1548 0, 1, 2, 2, 3, 0, 4, 6, 5, 6, 4, 7, 0, 3, 7, 7, 4, 0, 1, 5, 6, 6, 2, 1, 3, 2, 6, 6, 7, 3, 0, 4, 5, 5, 1, 0, ];
1555 (positions, indices)
1556 }
1557
1558 #[test]
1559 fn test_screen_to_ray_center() {
1560 let vp_inv = glam::Mat4::IDENTITY;
1562 let (origin, dir) = screen_to_ray(
1563 glam::Vec2::new(400.0, 300.0),
1564 glam::Vec2::new(800.0, 600.0),
1565 vp_inv,
1566 );
1567 assert!(origin.x.abs() < 1e-3, "origin.x={}", origin.x);
1569 assert!(origin.y.abs() < 1e-3, "origin.y={}", origin.y);
1570 assert!(dir.z.abs() > 0.9, "dir should be along Z, got {dir:?}");
1571 }
1572
1573 #[test]
1574 fn test_pick_scene_hit() {
1575 let (positions, indices) = unit_cube_mesh();
1576 let mut mesh_lookup = HashMap::new();
1577 mesh_lookup.insert(1u64, (positions, indices));
1578
1579 let obj = TestObject {
1580 id: 42,
1581 mesh_id: 1,
1582 position: glam::Vec3::ZERO,
1583 visible: true,
1584 };
1585 let objects: Vec<&dyn ViewportObject> = vec![&obj];
1586
1587 let result = pick_scene_cpu(
1589 glam::Vec3::new(0.0, 0.0, 5.0),
1590 glam::Vec3::new(0.0, 0.0, -1.0),
1591 &objects,
1592 &mesh_lookup,
1593 );
1594 assert!(result.is_some(), "expected a hit");
1595 let hit = result.unwrap();
1596 assert_eq!(hit.id, 42);
1597 assert!(
1599 (hit.world_pos.z - 0.5).abs() < 0.01,
1600 "world_pos.z={}",
1601 hit.world_pos.z
1602 );
1603 assert!(hit.normal.z > 0.9, "normal={:?}", hit.normal);
1605 }
1606
1607 #[test]
1608 fn test_pick_scene_miss() {
1609 let (positions, indices) = unit_cube_mesh();
1610 let mut mesh_lookup = HashMap::new();
1611 mesh_lookup.insert(1u64, (positions, indices));
1612
1613 let obj = TestObject {
1614 id: 42,
1615 mesh_id: 1,
1616 position: glam::Vec3::ZERO,
1617 visible: true,
1618 };
1619 let objects: Vec<&dyn ViewportObject> = vec![&obj];
1620
1621 let result = pick_scene_cpu(
1623 glam::Vec3::new(100.0, 100.0, 5.0),
1624 glam::Vec3::new(0.0, 0.0, -1.0),
1625 &objects,
1626 &mesh_lookup,
1627 );
1628 assert!(result.is_none());
1629 }
1630
1631 #[test]
1632 fn test_pick_nearest_wins() {
1633 let (positions, indices) = unit_cube_mesh();
1634 let mut mesh_lookup = HashMap::new();
1635 mesh_lookup.insert(1u64, (positions.clone(), indices.clone()));
1636 mesh_lookup.insert(2u64, (positions, indices));
1637
1638 let near_obj = TestObject {
1639 id: 10,
1640 mesh_id: 1,
1641 position: glam::Vec3::new(0.0, 0.0, 2.0),
1642 visible: true,
1643 };
1644 let far_obj = TestObject {
1645 id: 20,
1646 mesh_id: 2,
1647 position: glam::Vec3::new(0.0, 0.0, -2.0),
1648 visible: true,
1649 };
1650 let objects: Vec<&dyn ViewportObject> = vec![&far_obj, &near_obj];
1651
1652 let result = pick_scene_cpu(
1654 glam::Vec3::new(0.0, 0.0, 10.0),
1655 glam::Vec3::new(0.0, 0.0, -1.0),
1656 &objects,
1657 &mesh_lookup,
1658 );
1659 assert!(result.is_some(), "expected a hit");
1660 assert_eq!(result.unwrap().id, 10);
1661 }
1662
1663 #[test]
1664 fn test_box_select_hits_inside_rect() {
1665 let view = glam::Mat4::look_at_rh(
1667 glam::Vec3::new(0.0, 0.0, 5.0),
1668 glam::Vec3::ZERO,
1669 glam::Vec3::Y,
1670 );
1671 let proj = glam::Mat4::perspective_rh(std::f32::consts::FRAC_PI_4, 1.0, 0.1, 100.0);
1672 let vp = proj * view;
1673 let viewport_size = glam::Vec2::new(800.0, 600.0);
1674
1675 let obj = TestObject {
1676 id: 42,
1677 mesh_id: 1,
1678 position: glam::Vec3::ZERO,
1679 visible: true,
1680 };
1681 let objects: Vec<&dyn ViewportObject> = vec![&obj];
1682
1683 let result = box_select(
1685 glam::Vec2::new(300.0, 200.0),
1686 glam::Vec2::new(500.0, 400.0),
1687 &objects,
1688 vp,
1689 viewport_size,
1690 );
1691 assert_eq!(result, vec![42]);
1692 }
1693
1694 #[test]
1695 fn test_box_select_skips_hidden() {
1696 let view = glam::Mat4::look_at_rh(
1697 glam::Vec3::new(0.0, 0.0, 5.0),
1698 glam::Vec3::ZERO,
1699 glam::Vec3::Y,
1700 );
1701 let proj = glam::Mat4::perspective_rh(std::f32::consts::FRAC_PI_4, 1.0, 0.1, 100.0);
1702 let vp = proj * view;
1703 let viewport_size = glam::Vec2::new(800.0, 600.0);
1704
1705 let obj = TestObject {
1706 id: 42,
1707 mesh_id: 1,
1708 position: glam::Vec3::ZERO,
1709 visible: false,
1710 };
1711 let objects: Vec<&dyn ViewportObject> = vec![&obj];
1712
1713 let result = box_select(
1714 glam::Vec2::new(0.0, 0.0),
1715 glam::Vec2::new(800.0, 600.0),
1716 &objects,
1717 vp,
1718 viewport_size,
1719 );
1720 assert!(result.is_empty());
1721 }
1722
1723 #[test]
1724 fn test_pick_scene_nodes_hit() {
1725 let (positions, indices) = unit_cube_mesh();
1726 let mut mesh_lookup = HashMap::new();
1727 mesh_lookup.insert(0u64, (positions, indices));
1728
1729 let mut scene = crate::scene::scene::Scene::new();
1730 scene.add(
1731 Some(crate::resources::mesh_store::MeshId(0)),
1732 glam::Mat4::IDENTITY,
1733 crate::scene::material::Material::default(),
1734 );
1735 scene.update_transforms();
1736
1737 let result = pick_scene_nodes_cpu(
1738 glam::Vec3::new(0.0, 0.0, 5.0),
1739 glam::Vec3::new(0.0, 0.0, -1.0),
1740 &scene,
1741 &mesh_lookup,
1742 );
1743 assert!(result.is_some());
1744 }
1745
1746 #[test]
1747 fn test_pick_scene_nodes_miss() {
1748 let (positions, indices) = unit_cube_mesh();
1749 let mut mesh_lookup = HashMap::new();
1750 mesh_lookup.insert(0u64, (positions, indices));
1751
1752 let mut scene = crate::scene::scene::Scene::new();
1753 scene.add(
1754 Some(crate::resources::mesh_store::MeshId(0)),
1755 glam::Mat4::IDENTITY,
1756 crate::scene::material::Material::default(),
1757 );
1758 scene.update_transforms();
1759
1760 let result = pick_scene_nodes_cpu(
1761 glam::Vec3::new(100.0, 100.0, 5.0),
1762 glam::Vec3::new(0.0, 0.0, -1.0),
1763 &scene,
1764 &mesh_lookup,
1765 );
1766 assert!(result.is_none());
1767 }
1768
1769 #[test]
1770 fn test_probe_vertex_attribute() {
1771 let (positions, indices) = unit_cube_mesh();
1772 let mut mesh_lookup = HashMap::new();
1773 mesh_lookup.insert(1u64, (positions.clone(), indices.clone()));
1774
1775 let vertex_scalars: Vec<f32> = (0..positions.len()).map(|i| i as f32).collect();
1777
1778 let obj = TestObject {
1779 id: 42,
1780 mesh_id: 1,
1781 position: glam::Vec3::ZERO,
1782 visible: true,
1783 };
1784 let objects: Vec<&dyn ViewportObject> = vec![&obj];
1785
1786 let attr_ref = AttributeRef {
1787 name: "test".to_string(),
1788 kind: AttributeKind::Vertex,
1789 };
1790 let attr_data = AttributeData::Vertex(vertex_scalars);
1791 let bindings = vec![ProbeBinding {
1792 id: 42,
1793 attribute_ref: &attr_ref,
1794 attribute_data: &attr_data,
1795 positions: &positions,
1796 indices: &indices,
1797 }];
1798
1799 let result = pick_scene_with_probe_cpu(
1800 glam::Vec3::new(0.0, 0.0, 5.0),
1801 glam::Vec3::new(0.0, 0.0, -1.0),
1802 &objects,
1803 &mesh_lookup,
1804 &bindings,
1805 );
1806 assert!(result.is_some(), "expected a hit");
1807 let hit = result.unwrap();
1808 assert_eq!(hit.id, 42);
1809 assert!(
1811 hit.scalar_value.is_some(),
1812 "expected scalar_value to be set"
1813 );
1814 }
1815
1816 #[test]
1817 fn test_probe_cell_attribute() {
1818 let (positions, indices) = unit_cube_mesh();
1819 let mut mesh_lookup = HashMap::new();
1820 mesh_lookup.insert(1u64, (positions.clone(), indices.clone()));
1821
1822 let num_triangles = indices.len() / 3;
1824 let cell_scalars: Vec<f32> = (0..num_triangles).map(|i| (i as f32) * 10.0).collect();
1825
1826 let obj = TestObject {
1827 id: 42,
1828 mesh_id: 1,
1829 position: glam::Vec3::ZERO,
1830 visible: true,
1831 };
1832 let objects: Vec<&dyn ViewportObject> = vec![&obj];
1833
1834 let attr_ref = AttributeRef {
1835 name: "pressure".to_string(),
1836 kind: AttributeKind::Cell,
1837 };
1838 let attr_data = AttributeData::Cell(cell_scalars.clone());
1839 let bindings = vec![ProbeBinding {
1840 id: 42,
1841 attribute_ref: &attr_ref,
1842 attribute_data: &attr_data,
1843 positions: &positions,
1844 indices: &indices,
1845 }];
1846
1847 let result = pick_scene_with_probe_cpu(
1848 glam::Vec3::new(0.0, 0.0, 5.0),
1849 glam::Vec3::new(0.0, 0.0, -1.0),
1850 &objects,
1851 &mesh_lookup,
1852 &bindings,
1853 );
1854 assert!(result.is_some());
1855 let hit = result.unwrap();
1856 assert!(hit.scalar_value.is_some());
1858 let val = hit.scalar_value.unwrap();
1859 assert!(
1860 cell_scalars.contains(&val),
1861 "scalar_value {val} not in cell_scalars"
1862 );
1863 }
1864
1865 #[test]
1866 fn test_probe_no_binding_leaves_none() {
1867 let (positions, indices) = unit_cube_mesh();
1868 let mut mesh_lookup = HashMap::new();
1869 mesh_lookup.insert(1u64, (positions, indices));
1870
1871 let obj = TestObject {
1872 id: 42,
1873 mesh_id: 1,
1874 position: glam::Vec3::ZERO,
1875 visible: true,
1876 };
1877 let objects: Vec<&dyn ViewportObject> = vec![&obj];
1878
1879 let result = pick_scene_with_probe_cpu(
1881 glam::Vec3::new(0.0, 0.0, 5.0),
1882 glam::Vec3::new(0.0, 0.0, -1.0),
1883 &objects,
1884 &mesh_lookup,
1885 &[],
1886 );
1887 assert!(result.is_some());
1888 assert!(result.unwrap().scalar_value.is_none());
1889 }
1890
1891 fn make_view_proj() -> glam::Mat4 {
1897 let view = glam::Mat4::look_at_rh(
1898 glam::Vec3::new(0.0, 0.0, 5.0),
1899 glam::Vec3::ZERO,
1900 glam::Vec3::Y,
1901 );
1902 let proj = glam::Mat4::perspective_rh(std::f32::consts::FRAC_PI_4, 1.0, 0.1, 100.0);
1903 proj * view
1904 }
1905
1906 #[test]
1907 fn test_pick_rect_mesh_full_screen() {
1908 let (positions, indices) = unit_cube_mesh();
1910 let mut mesh_lookup: std::collections::HashMap<usize, (Vec<[f32; 3]>, Vec<u32>)> =
1911 std::collections::HashMap::new();
1912 mesh_lookup.insert(0, (positions, indices.clone()));
1913
1914 let item = crate::renderer::SceneRenderItem {
1915 mesh_id: crate::resources::mesh_store::MeshId(0),
1916 model: glam::Mat4::IDENTITY.to_cols_array_2d(),
1917 ..Default::default()
1918 };
1919
1920 let view_proj = make_view_proj();
1921 let viewport = glam::Vec2::new(800.0, 600.0);
1922
1923 let result = pick_rect(
1924 glam::Vec2::ZERO,
1925 viewport,
1926 &[item],
1927 &mesh_lookup,
1928 &[],
1929 view_proj,
1930 viewport,
1931 );
1932
1933 assert!(!result.is_empty(), "expected at least one triangle hit");
1935 assert!(result.total_count() > 0);
1936 }
1937
1938 #[test]
1939 fn test_pick_rect_miss() {
1940 let (positions, indices) = unit_cube_mesh();
1942 let mut mesh_lookup: std::collections::HashMap<usize, (Vec<[f32; 3]>, Vec<u32>)> =
1943 std::collections::HashMap::new();
1944 mesh_lookup.insert(0, (positions, indices));
1945
1946 let item = crate::renderer::SceneRenderItem {
1947 mesh_id: crate::resources::mesh_store::MeshId(0),
1948 model: glam::Mat4::IDENTITY.to_cols_array_2d(),
1949 ..Default::default()
1950 };
1951
1952 let view_proj = make_view_proj();
1953 let viewport = glam::Vec2::new(800.0, 600.0);
1954
1955 let result = pick_rect(
1956 glam::Vec2::new(700.0, 500.0), glam::Vec2::new(799.0, 599.0),
1958 &[item],
1959 &mesh_lookup,
1960 &[],
1961 view_proj,
1962 viewport,
1963 );
1964
1965 assert!(result.is_empty(), "expected no hits in off-center rect");
1966 }
1967
1968 #[test]
1969 fn test_pick_rect_point_cloud() {
1970 let view_proj = make_view_proj();
1972 let viewport = glam::Vec2::new(800.0, 600.0);
1973
1974 let pc = crate::renderer::PointCloudItem {
1975 positions: vec![[0.0, 0.0, 0.0], [0.1, 0.1, 0.0]],
1976 model: glam::Mat4::IDENTITY.to_cols_array_2d(),
1977 id: 99,
1978 ..Default::default()
1979 };
1980
1981 let result = pick_rect(
1982 glam::Vec2::ZERO,
1983 viewport,
1984 &[],
1985 &std::collections::HashMap::new(),
1986 &[pc],
1987 view_proj,
1988 viewport,
1989 );
1990
1991 assert!(!result.is_empty(), "expected point cloud hits");
1992 let hits = result.hits.get(&99).expect("expected hits for id 99");
1993 assert_eq!(
1994 hits.len(),
1995 2,
1996 "both points should be inside the full-screen rect"
1997 );
1998 assert!(
2000 hits.iter().all(|s| s.is_point()),
2001 "expected SubObjectRef::Point entries"
2002 );
2003 assert_eq!(hits[0], SubObjectRef::Point(0));
2004 assert_eq!(hits[1], SubObjectRef::Point(1));
2005 }
2006
2007 #[test]
2008 fn test_pick_rect_skips_invisible() {
2009 let (positions, indices) = unit_cube_mesh();
2010 let mut mesh_lookup: std::collections::HashMap<usize, (Vec<[f32; 3]>, Vec<u32>)> =
2011 std::collections::HashMap::new();
2012 mesh_lookup.insert(0, (positions, indices));
2013
2014 let item = crate::renderer::SceneRenderItem {
2015 mesh_id: crate::resources::mesh_store::MeshId(0),
2016 model: glam::Mat4::IDENTITY.to_cols_array_2d(),
2017 appearance: crate::scene::material::AppearanceSettings {
2018 hidden: true,
2019 ..Default::default()
2020 },
2021 ..Default::default()
2022 };
2023
2024 let view_proj = make_view_proj();
2025 let viewport = glam::Vec2::new(800.0, 600.0);
2026
2027 let result = pick_rect(
2028 glam::Vec2::ZERO,
2029 viewport,
2030 &[item],
2031 &mesh_lookup,
2032 &[],
2033 view_proj,
2034 viewport,
2035 );
2036
2037 assert!(result.is_empty(), "invisible items should be skipped");
2038 }
2039
2040 #[test]
2041 fn test_pick_rect_result_type() {
2042 let mut r = RectPickResult::default();
2044 assert!(r.is_empty());
2045 assert_eq!(r.total_count(), 0);
2046
2047 r.hits.insert(
2048 1,
2049 vec![
2050 SubObjectRef::Face(0),
2051 SubObjectRef::Face(1),
2052 SubObjectRef::Face(2),
2053 ],
2054 );
2055 r.hits.insert(2, vec![SubObjectRef::Point(5)]);
2056 assert!(!r.is_empty());
2057 assert_eq!(r.total_count(), 4);
2058 }
2059
2060 #[test]
2061 fn test_barycentric_at_vertices() {
2062 let a = glam::Vec3::new(0.0, 0.0, 0.0);
2063 let b = glam::Vec3::new(1.0, 0.0, 0.0);
2064 let c = glam::Vec3::new(0.0, 1.0, 0.0);
2065
2066 let (u, v, w) = super::barycentric(a, a, b, c);
2068 assert!((u - 1.0).abs() < 1e-5, "u={u}");
2069 assert!(v.abs() < 1e-5, "v={v}");
2070 assert!(w.abs() < 1e-5, "w={w}");
2071
2072 let (u, v, w) = super::barycentric(b, a, b, c);
2074 assert!(u.abs() < 1e-5, "u={u}");
2075 assert!((v - 1.0).abs() < 1e-5, "v={v}");
2076 assert!(w.abs() < 1e-5, "w={w}");
2077
2078 let centroid = (a + b + c) / 3.0;
2080 let (u, v, w) = super::barycentric(centroid, a, b, c);
2081 assert!((u - 1.0 / 3.0).abs() < 1e-4, "u={u}");
2082 assert!((v - 1.0 / 3.0).abs() < 1e-4, "v={v}");
2083 assert!((w - 1.0 / 3.0).abs() < 1e-4, "w={w}");
2084 }
2085
2086 fn make_volume_item(
2091 bbox_min: [f32; 3],
2092 bbox_max: [f32; 3],
2093 threshold_min: f32,
2094 threshold_max: f32,
2095 ) -> crate::renderer::VolumeItem {
2096 crate::renderer::VolumeItem {
2097 bbox_min,
2098 bbox_max,
2099 threshold_min,
2100 threshold_max,
2101 ..crate::renderer::VolumeItem::default()
2102 }
2103 }
2104
2105 fn make_volume_data(dims: [u32; 3], fill: f32) -> crate::geometry::marching_cubes::VolumeData {
2106 let n = (dims[0] * dims[1] * dims[2]) as usize;
2107 crate::geometry::marching_cubes::VolumeData {
2108 data: vec![fill; n],
2109 dims,
2110 origin: [0.0; 3],
2111 spacing: [1.0; 3],
2112 }
2113 }
2114
2115 #[test]
2116 fn test_pick_volume_basic_hit() {
2117 let item = make_volume_item([0.0; 3], [3.0, 3.0, 3.0], 0.5, 1.0);
2120 let volume = make_volume_data([3, 3, 3], 0.8);
2121
2122 let hit = super::pick_volume_cpu(
2123 glam::Vec3::new(1.5, 10.0, 1.5),
2124 glam::Vec3::new(0.0, -1.0, 0.0),
2125 42,
2126 &item,
2127 &volume,
2128 );
2129 assert!(hit.is_some(), "expected a hit");
2130 let hit = hit.unwrap();
2131
2132 assert_eq!(hit.id, 42);
2133 assert_eq!(hit.scalar_value, Some(0.8));
2134
2135 let flat = hit.sub_object.unwrap().index();
2137 let nx = 3u32;
2138 let ny = 3u32;
2139 let ix = flat % nx;
2140 let iy = (flat / nx) % ny;
2141 let iz = flat / (nx * ny);
2142 assert_eq!((ix, iy, iz), (1, 2, 1), "expected top-centre voxel");
2143
2144 assert!(hit.world_pos.y > 2.9, "world_pos.y={}", hit.world_pos.y);
2146
2147 assert!(hit.normal.y > 0.9, "normal={:?}", hit.normal);
2149 }
2150
2151 #[test]
2152 fn test_pick_volume_miss_aabb() {
2153 let item = make_volume_item([0.0; 3], [1.0; 3], 0.0, 1.0);
2154 let volume = make_volume_data([4, 4, 4], 0.5);
2155
2156 let hit = super::pick_volume_cpu(
2158 glam::Vec3::new(10.0, 5.0, 0.5),
2159 glam::Vec3::new(0.0, -1.0, 0.0),
2160 1,
2161 &item,
2162 &volume,
2163 );
2164 assert!(hit.is_none(), "expected miss");
2165 }
2166
2167 #[test]
2168 fn test_pick_volume_threshold_miss() {
2169 let item = make_volume_item([0.0; 3], [1.0; 3], 0.5, 1.0);
2171 let volume = make_volume_data([4, 4, 4], 0.3);
2172
2173 let hit = super::pick_volume_cpu(
2174 glam::Vec3::new(0.5, 5.0, 0.5),
2175 glam::Vec3::new(0.0, -1.0, 0.0),
2176 1,
2177 &item,
2178 &volume,
2179 );
2180 assert!(
2181 hit.is_none(),
2182 "expected no hit when all scalars below threshold"
2183 );
2184 }
2185
2186 #[test]
2187 fn test_pick_volume_threshold_skip() {
2188 let item = make_volume_item([0.0; 3], [1.0, 3.0, 1.0], 0.5, 1.0);
2193 let mut volume = make_volume_data([1, 3, 1], 0.0);
2194 volume.data[2] = 0.3;
2196 volume.data[1] = 0.8;
2197 volume.data[0] = 0.8;
2198
2199 let hit = super::pick_volume_cpu(
2200 glam::Vec3::new(0.5, 10.0, 0.5),
2201 glam::Vec3::new(0.0, -1.0, 0.0),
2202 1,
2203 &item,
2204 &volume,
2205 );
2206 assert!(hit.is_some(), "expected a hit");
2207 let hit = hit.unwrap();
2208 let flat = hit.sub_object.unwrap().index();
2209 assert_eq!(flat, 1, "expected iy=1 (flat=1), got flat={flat}");
2210 assert_eq!(hit.scalar_value, Some(0.8));
2211 }
2212
2213 #[test]
2214 fn test_pick_volume_nan_skip() {
2215 let item = make_volume_item([0.0; 3], [1.0, 2.0, 1.0], 0.0, 1.0);
2218 let mut volume = make_volume_data([1, 2, 1], 0.0);
2219 volume.data[1] = f32::NAN;
2220 volume.data[0] = 0.5;
2221
2222 let hit = super::pick_volume_cpu(
2223 glam::Vec3::new(0.5, 10.0, 0.5),
2224 glam::Vec3::new(0.0, -1.0, 0.0),
2225 1,
2226 &item,
2227 &volume,
2228 );
2229 assert!(hit.is_some(), "expected hit after NaN skip");
2230 let hit = hit.unwrap();
2231 assert_eq!(hit.sub_object.unwrap().index(), 0, "expected iy=0 (flat=0)");
2232 assert_eq!(hit.scalar_value, Some(0.5));
2233 }
2234
2235 #[test]
2236 fn test_pick_volume_dda_no_skip() {
2237 let item = make_volume_item([0.0; 3], [10.0, 1.0, 1.0], 0.5, 1.0);
2241 let mut volume = make_volume_data([10, 1, 1], 0.0);
2242 volume.data[9] = 0.8;
2243
2244 let dir = glam::Vec3::new(1.0, 0.0, 0.001).normalize();
2245 let hit = super::pick_volume_cpu(glam::Vec3::new(-1.0, 0.5, 0.5), dir, 1, &item, &volume);
2246 assert!(
2247 hit.is_some(),
2248 "DDA must reach the last voxel without skipping"
2249 );
2250 let flat = hit.unwrap().sub_object.unwrap().index();
2251 assert_eq!(flat, 9, "expected ix=9 (flat=9), got flat={flat}");
2252 }
2253
2254 #[test]
2255 fn test_voxel_world_aabb_identity() {
2256 let item = make_volume_item([0.0; 3], [4.0, 4.0, 4.0], 0.0, 1.0);
2258 let volume = make_volume_data([4, 4, 4], 0.0);
2259
2260 let (lo, hi) = super::voxel_world_aabb(0, &volume, &item);
2262 assert!((lo - glam::Vec3::ZERO).length() < 1e-5, "lo={lo:?}");
2263 assert!((hi - glam::Vec3::ONE).length() < 1e-5, "hi={hi:?}");
2264
2265 let (lo, hi) = super::voxel_world_aabb(1, &volume, &item);
2267 assert!((lo.x - 1.0).abs() < 1e-5 && (hi.x - 2.0).abs() < 1e-5);
2268
2269 let (lo, hi) = super::voxel_world_aabb(57, &volume, &item);
2271 assert!(
2272 (lo - glam::Vec3::new(1.0, 2.0, 3.0)).length() < 1e-5,
2273 "lo={lo:?}"
2274 );
2275 assert!(
2276 (hi - glam::Vec3::new(2.0, 3.0, 4.0)).length() < 1e-5,
2277 "hi={hi:?}"
2278 );
2279 }
2280
2281 #[test]
2282 fn test_voxel_world_aabb_round_trip() {
2283 let item = make_volume_item([0.0; 3], [3.0, 3.0, 3.0], 0.5, 1.0);
2286 let volume = make_volume_data([3, 3, 3], 0.8);
2287
2288 let hit = super::pick_volume_cpu(
2289 glam::Vec3::new(1.5, 10.0, 1.5),
2290 glam::Vec3::new(0.0, -1.0, 0.0),
2291 1,
2292 &item,
2293 &volume,
2294 )
2295 .expect("expected a hit for round-trip test");
2296
2297 let flat = hit.sub_object.unwrap().index();
2298 let (lo, hi) = super::voxel_world_aabb(flat, &volume, &item);
2299
2300 let tol = 1e-3;
2301 assert!(
2302 hit.world_pos.x >= lo.x - tol && hit.world_pos.x <= hi.x + tol,
2303 "world_pos.x={} outside [{}, {}]",
2304 hit.world_pos.x,
2305 lo.x,
2306 hi.x
2307 );
2308 assert!(
2309 hit.world_pos.y >= lo.y - tol && hit.world_pos.y <= hi.y + tol,
2310 "world_pos.y={} outside [{}, {}]",
2311 hit.world_pos.y,
2312 lo.y,
2313 hi.y
2314 );
2315 assert!(
2316 hit.world_pos.z >= lo.z - tol && hit.world_pos.z <= hi.z + tol,
2317 "world_pos.z={} outside [{}, {}]",
2318 hit.world_pos.z,
2319 lo.z,
2320 hi.z
2321 );
2322 }
2323}