1use crate::interaction::sub_object::SubObjectRef;
6use crate::resources::{AttributeData, AttributeKind, AttributeRef};
7use crate::scene::traits::ViewportObject;
8use parry3d::math::{Pose, Vector};
9use parry3d::query::{Ray, RayCast};
10
11#[derive(Clone, Copy, Debug)]
20#[non_exhaustive]
21pub struct PickHit {
22 pub id: u64,
24 pub sub_object: Option<SubObjectRef>,
29 pub world_pos: glam::Vec3,
31 pub normal: glam::Vec3,
33 #[deprecated(since = "0.5.0", note = "use `sub_object` instead")]
38 pub triangle_index: u32,
39 #[deprecated(since = "0.5.0", note = "use `sub_object` instead")]
44 pub point_index: Option<u32>,
45 pub scalar_value: Option<f32>,
52}
53
54#[derive(Clone, Copy, Debug)]
66#[non_exhaustive]
67pub struct GpuPickHit {
68 pub object_id: crate::renderer::PickId,
75 pub depth: f32,
84}
85
86pub fn screen_to_ray(
99 screen_pos: glam::Vec2,
100 viewport_size: glam::Vec2,
101 view_proj_inv: glam::Mat4,
102) -> (glam::Vec3, glam::Vec3) {
103 let ndc_x = (screen_pos.x / viewport_size.x) * 2.0 - 1.0;
104 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));
106 let far = view_proj_inv.project_point3(glam::Vec3::new(ndc_x, ndc_y, 1.0));
107 let dir = (far - near).normalize();
108 (near, dir)
109}
110
111pub fn pick_scene_cpu(
120 ray_origin: glam::Vec3,
121 ray_dir: glam::Vec3,
122 objects: &[&dyn ViewportObject],
123 mesh_lookup: &std::collections::HashMap<u64, (Vec<[f32; 3]>, Vec<u32>)>,
124) -> Option<PickHit> {
125 let ray = Ray::new(
127 Vector::new(ray_origin.x, ray_origin.y, ray_origin.z),
128 Vector::new(ray_dir.x, ray_dir.y, ray_dir.z),
129 );
130
131 let mut best_hit: Option<(u64, f32, PickHit)> = None;
132
133 for obj in objects {
134 if !obj.is_visible() {
135 continue;
136 }
137 let Some(mesh_id) = obj.mesh_id() else {
138 continue;
139 };
140
141 if let Some((positions, indices)) = mesh_lookup.get(&mesh_id) {
142 let s = obj.scale();
147 let verts: Vec<Vector> = positions
148 .iter()
149 .map(|p: &[f32; 3]| Vector::new(p[0] * s.x, p[1] * s.y, p[2] * s.z))
150 .collect();
151
152 let tri_indices: Vec<[u32; 3]> = indices
153 .chunks(3)
154 .filter(|c: &&[u32]| c.len() == 3)
155 .map(|c: &[u32]| [c[0], c[1], c[2]])
156 .collect();
157
158 if tri_indices.is_empty() {
159 continue;
160 }
161
162 match parry3d::shape::TriMesh::new(verts, tri_indices) {
163 Ok(trimesh) => {
164 let pose = Pose::from_parts(obj.position(), obj.rotation());
168 if let Some(intersection) =
169 trimesh.cast_ray_and_get_normal(&pose, &ray, f32::MAX, true)
170 {
171 let toi = intersection.time_of_impact;
172 if best_hit.is_none() || toi < best_hit.as_ref().unwrap().1 {
173 let sub_object = SubObjectRef::from_feature_id(intersection.feature);
174 let world_pos = ray_origin + ray_dir * toi;
175 let normal = intersection.normal;
177 let triangle_index = if let Some(SubObjectRef::Face(i)) = sub_object {
178 i
179 } else {
180 u32::MAX
181 };
182 #[allow(deprecated)]
183 let hit = PickHit {
184 id: obj.id(),
185 sub_object,
186 triangle_index,
187 world_pos,
188 normal,
189 point_index: None,
190 scalar_value: None,
191 };
192 best_hit = Some((obj.id(), toi, hit));
193 }
194 }
195 }
196 Err(e) => {
197 tracing::warn!(object_id = obj.id(), error = %e, "TriMesh construction failed for picking");
198 }
199 }
200 }
201 }
202
203 best_hit.map(|(_, _, hit)| hit)
204}
205
206pub fn pick_scene_nodes_cpu(
211 ray_origin: glam::Vec3,
212 ray_dir: glam::Vec3,
213 scene: &crate::scene::scene::Scene,
214 mesh_lookup: &std::collections::HashMap<u64, (Vec<[f32; 3]>, Vec<u32>)>,
215) -> Option<PickHit> {
216 let nodes: Vec<&dyn ViewportObject> = scene.nodes().map(|n| n as &dyn ViewportObject).collect();
217 pick_scene_cpu(ray_origin, ray_dir, &nodes, mesh_lookup)
218}
219
220pub struct ProbeBinding<'a> {
229 pub id: u64,
231 pub attribute_ref: &'a AttributeRef,
233 pub attribute_data: &'a AttributeData,
235 pub positions: &'a [[f32; 3]],
237 pub indices: &'a [u32],
239}
240
241fn barycentric(p: glam::Vec3, a: glam::Vec3, b: glam::Vec3, c: glam::Vec3) -> (f32, f32, f32) {
246 let v0 = b - a;
247 let v1 = c - a;
248 let v2 = p - a;
249 let d00 = v0.dot(v0);
250 let d01 = v0.dot(v1);
251 let d11 = v1.dot(v1);
252 let d20 = v2.dot(v0);
253 let d21 = v2.dot(v1);
254 let denom = d00 * d11 - d01 * d01;
255 if denom.abs() < 1e-12 {
256 return (1.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0);
258 }
259 let inv = 1.0 / denom;
260 let v = (d11 * d20 - d01 * d21) * inv;
261 let w = (d00 * d21 - d01 * d20) * inv;
262 let u = 1.0 - v - w;
263 (u, v, w)
264}
265
266fn probe_scalar(hit: &mut PickHit, binding: &ProbeBinding<'_>) {
269 let tri_idx_raw = match hit.sub_object {
270 Some(SubObjectRef::Face(i)) => i,
271 _ => return,
272 };
273
274 let num_triangles = binding.indices.len() / 3;
275 let tri_idx = if (tri_idx_raw as usize) >= num_triangles && num_triangles > 0 {
278 tri_idx_raw as usize - num_triangles
279 } else {
280 tri_idx_raw as usize
281 };
282
283 match binding.attribute_ref.kind {
284 AttributeKind::Cell => {
285 if let AttributeData::Cell(data) = binding.attribute_data {
287 if let Some(&val) = data.get(tri_idx) {
288 hit.scalar_value = Some(val);
289 }
290 }
291 }
292 AttributeKind::Face => {
293 if let AttributeData::Face(data) = binding.attribute_data {
295 if let Some(&val) = data.get(tri_idx) {
296 hit.scalar_value = Some(val);
297 }
298 }
299 }
300 AttributeKind::FaceColor => {
301 }
303 AttributeKind::Vertex => {
304 if let AttributeData::Vertex(data) = binding.attribute_data {
306 let base = tri_idx * 3;
307 if base + 2 >= binding.indices.len() {
308 return;
309 }
310 let i0 = binding.indices[base] as usize;
311 let i1 = binding.indices[base + 1] as usize;
312 let i2 = binding.indices[base + 2] as usize;
313
314 if i0 >= data.len() || i1 >= data.len() || i2 >= data.len() {
315 return;
316 }
317 if i0 >= binding.positions.len()
318 || i1 >= binding.positions.len()
319 || i2 >= binding.positions.len()
320 {
321 return;
322 }
323
324 let a = glam::Vec3::from(binding.positions[i0]);
325 let b = glam::Vec3::from(binding.positions[i1]);
326 let c = glam::Vec3::from(binding.positions[i2]);
327 let (u, v, w) = barycentric(hit.world_pos, a, b, c);
328 hit.scalar_value = Some(data[i0] * u + data[i1] * v + data[i2] * w);
329 }
330 }
331 AttributeKind::Edge => {
332 if let AttributeData::Edge(data) = binding.attribute_data {
335 let base = tri_idx * 3;
336 if base + 2 >= binding.indices.len() || data.is_empty() {
337 return;
338 }
339 let i0 = binding.indices[base] as usize;
340 let i1 = binding.indices[base + 1] as usize;
341 let i2 = binding.indices[base + 2] as usize;
342 if i0 < data.len() || i1 < data.len() || i2 < data.len() {
343 if i0 < data.len()
345 && i1 < data.len()
346 && i2 < data.len()
347 && i0 < binding.positions.len()
348 && i1 < binding.positions.len()
349 && i2 < binding.positions.len()
350 {
351 let a = glam::Vec3::from(binding.positions[i0]);
352 let b = glam::Vec3::from(binding.positions[i1]);
353 let c = glam::Vec3::from(binding.positions[i2]);
354 let (u, v, w) = barycentric(hit.world_pos, a, b, c);
355 hit.scalar_value = Some(data[i0] * u + data[i1] * v + data[i2] * w);
356 }
357 }
358 }
359 }
360 AttributeKind::Halfedge | AttributeKind::Corner => {
361 let extract = |data: &[f32]| -> Option<f32> {
364 let base = tri_idx * 3;
365 if base + 2 >= data.len() {
366 return None;
367 }
368 Some(data[base])
370 };
371 match binding.attribute_data {
372 AttributeData::Halfedge(data) | AttributeData::Corner(data) => {
373 hit.scalar_value = extract(data);
374 }
375 _ => {}
376 }
377 }
378 }
379}
380
381pub fn pick_scene_with_probe_cpu(
388 ray_origin: glam::Vec3,
389 ray_dir: glam::Vec3,
390 objects: &[&dyn ViewportObject],
391 mesh_lookup: &std::collections::HashMap<u64, (Vec<[f32; 3]>, Vec<u32>)>,
392 probe_bindings: &[ProbeBinding<'_>],
393) -> Option<PickHit> {
394 let mut hit = pick_scene_cpu(ray_origin, ray_dir, objects, mesh_lookup)?;
395 if let Some(binding) = probe_bindings.iter().find(|b| b.id == hit.id) {
396 probe_scalar(&mut hit, binding);
397 }
398 Some(hit)
399}
400
401pub fn pick_scene_nodes_with_probe_cpu(
405 ray_origin: glam::Vec3,
406 ray_dir: glam::Vec3,
407 scene: &crate::scene::scene::Scene,
408 mesh_lookup: &std::collections::HashMap<u64, (Vec<[f32; 3]>, Vec<u32>)>,
409 probe_bindings: &[ProbeBinding<'_>],
410) -> Option<PickHit> {
411 let mut hit = pick_scene_nodes_cpu(ray_origin, ray_dir, scene, mesh_lookup)?;
412 if let Some(binding) = probe_bindings.iter().find(|b| b.id == hit.id) {
413 probe_scalar(&mut hit, binding);
414 }
415 Some(hit)
416}
417
418pub fn pick_scene_accelerated_with_probe_cpu(
423 ray_origin: glam::Vec3,
424 ray_dir: glam::Vec3,
425 accelerator: &mut crate::geometry::bvh::PickAccelerator,
426 mesh_lookup: &std::collections::HashMap<u64, (Vec<[f32; 3]>, Vec<u32>)>,
427 probe_bindings: &[ProbeBinding<'_>],
428) -> Option<PickHit> {
429 let mut hit = accelerator.pick(ray_origin, ray_dir, mesh_lookup)?;
430 if let Some(binding) = probe_bindings.iter().find(|b| b.id == hit.id) {
431 probe_scalar(&mut hit, binding);
432 }
433 Some(hit)
434}
435
436#[derive(Clone, Debug, Default)]
447pub struct RectPickResult {
448 pub hits: std::collections::HashMap<u64, Vec<SubObjectRef>>,
455}
456
457impl RectPickResult {
458 pub fn is_empty(&self) -> bool {
460 self.hits.is_empty()
461 }
462
463 pub fn total_count(&self) -> usize {
465 self.hits.values().map(|v| v.len()).sum()
466 }
467}
468
469pub fn pick_rect(
487 rect_min: glam::Vec2,
488 rect_max: glam::Vec2,
489 scene_items: &[crate::renderer::SceneRenderItem],
490 mesh_lookup: &std::collections::HashMap<usize, (Vec<[f32; 3]>, Vec<u32>)>,
491 point_clouds: &[crate::renderer::PointCloudItem],
492 view_proj: glam::Mat4,
493 viewport_size: glam::Vec2,
494) -> RectPickResult {
495 let ndc_min = glam::Vec2::new(
498 rect_min.x / viewport_size.x * 2.0 - 1.0,
499 1.0 - rect_max.y / viewport_size.y * 2.0, );
501 let ndc_max = glam::Vec2::new(
502 rect_max.x / viewport_size.x * 2.0 - 1.0,
503 1.0 - rect_min.y / viewport_size.y * 2.0, );
505
506 let mut result = RectPickResult::default();
507
508 for item in scene_items {
510 if !item.visible {
511 continue;
512 }
513 let Some((positions, indices)) = mesh_lookup.get(&item.mesh_id.index()) else {
514 continue;
515 };
516
517 let model = glam::Mat4::from_cols_array_2d(&item.model);
518 let mvp = view_proj * model;
519
520 let mut tri_hits: Vec<SubObjectRef> = Vec::new();
521
522 for (tri_idx, chunk) in indices.chunks(3).enumerate() {
523 if chunk.len() < 3 {
524 continue;
525 }
526 let i0 = chunk[0] as usize;
527 let i1 = chunk[1] as usize;
528 let i2 = chunk[2] as usize;
529
530 if i0 >= positions.len() || i1 >= positions.len() || i2 >= positions.len() {
531 continue;
532 }
533
534 let p0 = glam::Vec3::from(positions[i0]);
535 let p1 = glam::Vec3::from(positions[i1]);
536 let p2 = glam::Vec3::from(positions[i2]);
537 let centroid = (p0 + p1 + p2) / 3.0;
538
539 let clip = mvp * centroid.extend(1.0);
540 if clip.w <= 0.0 {
541 continue;
543 }
544 let ndc = glam::Vec2::new(clip.x / clip.w, clip.y / clip.w);
545
546 if ndc.x >= ndc_min.x && ndc.x <= ndc_max.x && ndc.y >= ndc_min.y && ndc.y <= ndc_max.y
547 {
548 tri_hits.push(SubObjectRef::Face(tri_idx as u32));
549 }
550 }
551
552 if !tri_hits.is_empty() {
553 result.hits.insert(item.mesh_id.index() as u64, tri_hits);
554 }
555 }
556
557 for pc in point_clouds {
559 if pc.id == 0 {
560 continue;
562 }
563
564 let model = glam::Mat4::from_cols_array_2d(&pc.model);
565 let mvp = view_proj * model;
566
567 let mut pt_hits: Vec<SubObjectRef> = Vec::new();
568
569 for (pt_idx, pos) in pc.positions.iter().enumerate() {
570 let p = glam::Vec3::from(*pos);
571 let clip = mvp * p.extend(1.0);
572 if clip.w <= 0.0 {
573 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 pt_hits.push(SubObjectRef::Point(pt_idx as u32));
580 }
581 }
582
583 if !pt_hits.is_empty() {
584 result.hits.insert(pc.id, pt_hits);
585 }
586 }
587
588 result
589}
590
591pub fn box_select(
600 rect_min: glam::Vec2,
601 rect_max: glam::Vec2,
602 objects: &[&dyn ViewportObject],
603 view_proj: glam::Mat4,
604 viewport_size: glam::Vec2,
605) -> Vec<u64> {
606 let mut hits = Vec::new();
607 for obj in objects {
608 if !obj.is_visible() {
609 continue;
610 }
611 let pos = obj.position();
612 let clip = view_proj * pos.extend(1.0);
613 if clip.w <= 0.0 {
615 continue;
616 }
617 let ndc = glam::Vec3::new(clip.x / clip.w, clip.y / clip.w, clip.z / clip.w);
618 let screen = glam::Vec2::new(
619 (ndc.x + 1.0) * 0.5 * viewport_size.x,
620 (1.0 - ndc.y) * 0.5 * viewport_size.y,
621 );
622 if screen.x >= rect_min.x
623 && screen.x <= rect_max.x
624 && screen.y >= rect_min.y
625 && screen.y <= rect_max.y
626 {
627 hits.push(obj.id());
628 }
629 }
630 hits
631}
632
633#[cfg(test)]
634mod tests {
635 use super::*;
636 use crate::scene::traits::ViewportObject;
637 use std::collections::HashMap;
638
639 struct TestObject {
640 id: u64,
641 mesh_id: u64,
642 position: glam::Vec3,
643 visible: bool,
644 }
645
646 impl ViewportObject for TestObject {
647 fn id(&self) -> u64 {
648 self.id
649 }
650 fn mesh_id(&self) -> Option<u64> {
651 Some(self.mesh_id)
652 }
653 fn model_matrix(&self) -> glam::Mat4 {
654 glam::Mat4::from_translation(self.position)
655 }
656 fn position(&self) -> glam::Vec3 {
657 self.position
658 }
659 fn rotation(&self) -> glam::Quat {
660 glam::Quat::IDENTITY
661 }
662 fn is_visible(&self) -> bool {
663 self.visible
664 }
665 fn color(&self) -> glam::Vec3 {
666 glam::Vec3::ONE
667 }
668 }
669
670 fn unit_cube_mesh() -> (Vec<[f32; 3]>, Vec<u32>) {
672 let positions = vec![
673 [-0.5, -0.5, -0.5],
674 [0.5, -0.5, -0.5],
675 [0.5, 0.5, -0.5],
676 [-0.5, 0.5, -0.5],
677 [-0.5, -0.5, 0.5],
678 [0.5, -0.5, 0.5],
679 [0.5, 0.5, 0.5],
680 [-0.5, 0.5, 0.5],
681 ];
682 let indices = vec![
683 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, ];
690 (positions, indices)
691 }
692
693 #[test]
694 fn test_screen_to_ray_center() {
695 let vp_inv = glam::Mat4::IDENTITY;
697 let (origin, dir) = screen_to_ray(
698 glam::Vec2::new(400.0, 300.0),
699 glam::Vec2::new(800.0, 600.0),
700 vp_inv,
701 );
702 assert!(origin.x.abs() < 1e-3, "origin.x={}", origin.x);
704 assert!(origin.y.abs() < 1e-3, "origin.y={}", origin.y);
705 assert!(dir.z.abs() > 0.9, "dir should be along Z, got {dir:?}");
706 }
707
708 #[test]
709 fn test_pick_scene_hit() {
710 let (positions, indices) = unit_cube_mesh();
711 let mut mesh_lookup = HashMap::new();
712 mesh_lookup.insert(1u64, (positions, indices));
713
714 let obj = TestObject {
715 id: 42,
716 mesh_id: 1,
717 position: glam::Vec3::ZERO,
718 visible: true,
719 };
720 let objects: Vec<&dyn ViewportObject> = vec![&obj];
721
722 let result = pick_scene_cpu(
724 glam::Vec3::new(0.0, 0.0, 5.0),
725 glam::Vec3::new(0.0, 0.0, -1.0),
726 &objects,
727 &mesh_lookup,
728 );
729 assert!(result.is_some(), "expected a hit");
730 let hit = result.unwrap();
731 assert_eq!(hit.id, 42);
732 assert!(
734 (hit.world_pos.z - 0.5).abs() < 0.01,
735 "world_pos.z={}",
736 hit.world_pos.z
737 );
738 assert!(hit.normal.z > 0.9, "normal={:?}", hit.normal);
740 }
741
742 #[test]
743 fn test_pick_scene_miss() {
744 let (positions, indices) = unit_cube_mesh();
745 let mut mesh_lookup = HashMap::new();
746 mesh_lookup.insert(1u64, (positions, indices));
747
748 let obj = TestObject {
749 id: 42,
750 mesh_id: 1,
751 position: glam::Vec3::ZERO,
752 visible: true,
753 };
754 let objects: Vec<&dyn ViewportObject> = vec![&obj];
755
756 let result = pick_scene_cpu(
758 glam::Vec3::new(100.0, 100.0, 5.0),
759 glam::Vec3::new(0.0, 0.0, -1.0),
760 &objects,
761 &mesh_lookup,
762 );
763 assert!(result.is_none());
764 }
765
766 #[test]
767 fn test_pick_nearest_wins() {
768 let (positions, indices) = unit_cube_mesh();
769 let mut mesh_lookup = HashMap::new();
770 mesh_lookup.insert(1u64, (positions.clone(), indices.clone()));
771 mesh_lookup.insert(2u64, (positions, indices));
772
773 let near_obj = TestObject {
774 id: 10,
775 mesh_id: 1,
776 position: glam::Vec3::new(0.0, 0.0, 2.0),
777 visible: true,
778 };
779 let far_obj = TestObject {
780 id: 20,
781 mesh_id: 2,
782 position: glam::Vec3::new(0.0, 0.0, -2.0),
783 visible: true,
784 };
785 let objects: Vec<&dyn ViewportObject> = vec![&far_obj, &near_obj];
786
787 let result = pick_scene_cpu(
789 glam::Vec3::new(0.0, 0.0, 10.0),
790 glam::Vec3::new(0.0, 0.0, -1.0),
791 &objects,
792 &mesh_lookup,
793 );
794 assert!(result.is_some(), "expected a hit");
795 assert_eq!(result.unwrap().id, 10);
796 }
797
798 #[test]
799 fn test_box_select_hits_inside_rect() {
800 let view = glam::Mat4::look_at_rh(
802 glam::Vec3::new(0.0, 0.0, 5.0),
803 glam::Vec3::ZERO,
804 glam::Vec3::Y,
805 );
806 let proj = glam::Mat4::perspective_rh(std::f32::consts::FRAC_PI_4, 1.0, 0.1, 100.0);
807 let vp = proj * view;
808 let viewport_size = glam::Vec2::new(800.0, 600.0);
809
810 let obj = TestObject {
811 id: 42,
812 mesh_id: 1,
813 position: glam::Vec3::ZERO,
814 visible: true,
815 };
816 let objects: Vec<&dyn ViewportObject> = vec![&obj];
817
818 let result = box_select(
820 glam::Vec2::new(300.0, 200.0),
821 glam::Vec2::new(500.0, 400.0),
822 &objects,
823 vp,
824 viewport_size,
825 );
826 assert_eq!(result, vec![42]);
827 }
828
829 #[test]
830 fn test_box_select_skips_hidden() {
831 let view = glam::Mat4::look_at_rh(
832 glam::Vec3::new(0.0, 0.0, 5.0),
833 glam::Vec3::ZERO,
834 glam::Vec3::Y,
835 );
836 let proj = glam::Mat4::perspective_rh(std::f32::consts::FRAC_PI_4, 1.0, 0.1, 100.0);
837 let vp = proj * view;
838 let viewport_size = glam::Vec2::new(800.0, 600.0);
839
840 let obj = TestObject {
841 id: 42,
842 mesh_id: 1,
843 position: glam::Vec3::ZERO,
844 visible: false,
845 };
846 let objects: Vec<&dyn ViewportObject> = vec![&obj];
847
848 let result = box_select(
849 glam::Vec2::new(0.0, 0.0),
850 glam::Vec2::new(800.0, 600.0),
851 &objects,
852 vp,
853 viewport_size,
854 );
855 assert!(result.is_empty());
856 }
857
858 #[test]
859 fn test_pick_scene_nodes_hit() {
860 let (positions, indices) = unit_cube_mesh();
861 let mut mesh_lookup = HashMap::new();
862 mesh_lookup.insert(0u64, (positions, indices));
863
864 let mut scene = crate::scene::scene::Scene::new();
865 scene.add(
866 Some(crate::resources::mesh_store::MeshId(0)),
867 glam::Mat4::IDENTITY,
868 crate::scene::material::Material::default(),
869 );
870 scene.update_transforms();
871
872 let result = pick_scene_nodes_cpu(
873 glam::Vec3::new(0.0, 0.0, 5.0),
874 glam::Vec3::new(0.0, 0.0, -1.0),
875 &scene,
876 &mesh_lookup,
877 );
878 assert!(result.is_some());
879 }
880
881 #[test]
882 fn test_pick_scene_nodes_miss() {
883 let (positions, indices) = unit_cube_mesh();
884 let mut mesh_lookup = HashMap::new();
885 mesh_lookup.insert(0u64, (positions, indices));
886
887 let mut scene = crate::scene::scene::Scene::new();
888 scene.add(
889 Some(crate::resources::mesh_store::MeshId(0)),
890 glam::Mat4::IDENTITY,
891 crate::scene::material::Material::default(),
892 );
893 scene.update_transforms();
894
895 let result = pick_scene_nodes_cpu(
896 glam::Vec3::new(100.0, 100.0, 5.0),
897 glam::Vec3::new(0.0, 0.0, -1.0),
898 &scene,
899 &mesh_lookup,
900 );
901 assert!(result.is_none());
902 }
903
904 #[test]
905 fn test_probe_vertex_attribute() {
906 let (positions, indices) = unit_cube_mesh();
907 let mut mesh_lookup = HashMap::new();
908 mesh_lookup.insert(1u64, (positions.clone(), indices.clone()));
909
910 let vertex_scalars: Vec<f32> = (0..positions.len()).map(|i| i as f32).collect();
912
913 let obj = TestObject {
914 id: 42,
915 mesh_id: 1,
916 position: glam::Vec3::ZERO,
917 visible: true,
918 };
919 let objects: Vec<&dyn ViewportObject> = vec![&obj];
920
921 let attr_ref = AttributeRef {
922 name: "test".to_string(),
923 kind: AttributeKind::Vertex,
924 };
925 let attr_data = AttributeData::Vertex(vertex_scalars);
926 let bindings = vec![ProbeBinding {
927 id: 42,
928 attribute_ref: &attr_ref,
929 attribute_data: &attr_data,
930 positions: &positions,
931 indices: &indices,
932 }];
933
934 let result = pick_scene_with_probe_cpu(
935 glam::Vec3::new(0.0, 0.0, 5.0),
936 glam::Vec3::new(0.0, 0.0, -1.0),
937 &objects,
938 &mesh_lookup,
939 &bindings,
940 );
941 assert!(result.is_some(), "expected a hit");
942 let hit = result.unwrap();
943 assert_eq!(hit.id, 42);
944 assert!(
946 hit.scalar_value.is_some(),
947 "expected scalar_value to be set"
948 );
949 }
950
951 #[test]
952 fn test_probe_cell_attribute() {
953 let (positions, indices) = unit_cube_mesh();
954 let mut mesh_lookup = HashMap::new();
955 mesh_lookup.insert(1u64, (positions.clone(), indices.clone()));
956
957 let num_triangles = indices.len() / 3;
959 let cell_scalars: Vec<f32> = (0..num_triangles).map(|i| (i as f32) * 10.0).collect();
960
961 let obj = TestObject {
962 id: 42,
963 mesh_id: 1,
964 position: glam::Vec3::ZERO,
965 visible: true,
966 };
967 let objects: Vec<&dyn ViewportObject> = vec![&obj];
968
969 let attr_ref = AttributeRef {
970 name: "pressure".to_string(),
971 kind: AttributeKind::Cell,
972 };
973 let attr_data = AttributeData::Cell(cell_scalars.clone());
974 let bindings = vec![ProbeBinding {
975 id: 42,
976 attribute_ref: &attr_ref,
977 attribute_data: &attr_data,
978 positions: &positions,
979 indices: &indices,
980 }];
981
982 let result = pick_scene_with_probe_cpu(
983 glam::Vec3::new(0.0, 0.0, 5.0),
984 glam::Vec3::new(0.0, 0.0, -1.0),
985 &objects,
986 &mesh_lookup,
987 &bindings,
988 );
989 assert!(result.is_some());
990 let hit = result.unwrap();
991 assert!(hit.scalar_value.is_some());
993 let val = hit.scalar_value.unwrap();
994 assert!(
995 cell_scalars.contains(&val),
996 "scalar_value {val} not in cell_scalars"
997 );
998 }
999
1000 #[test]
1001 fn test_probe_no_binding_leaves_none() {
1002 let (positions, indices) = unit_cube_mesh();
1003 let mut mesh_lookup = HashMap::new();
1004 mesh_lookup.insert(1u64, (positions, indices));
1005
1006 let obj = TestObject {
1007 id: 42,
1008 mesh_id: 1,
1009 position: glam::Vec3::ZERO,
1010 visible: true,
1011 };
1012 let objects: Vec<&dyn ViewportObject> = vec![&obj];
1013
1014 let result = pick_scene_with_probe_cpu(
1016 glam::Vec3::new(0.0, 0.0, 5.0),
1017 glam::Vec3::new(0.0, 0.0, -1.0),
1018 &objects,
1019 &mesh_lookup,
1020 &[],
1021 );
1022 assert!(result.is_some());
1023 assert!(result.unwrap().scalar_value.is_none());
1024 }
1025
1026 fn make_view_proj() -> glam::Mat4 {
1032 let view = glam::Mat4::look_at_rh(
1033 glam::Vec3::new(0.0, 0.0, 5.0),
1034 glam::Vec3::ZERO,
1035 glam::Vec3::Y,
1036 );
1037 let proj = glam::Mat4::perspective_rh(std::f32::consts::FRAC_PI_4, 1.0, 0.1, 100.0);
1038 proj * view
1039 }
1040
1041 #[test]
1042 fn test_pick_rect_mesh_full_screen() {
1043 let (positions, indices) = unit_cube_mesh();
1045 let mut mesh_lookup: std::collections::HashMap<usize, (Vec<[f32; 3]>, Vec<u32>)> =
1046 std::collections::HashMap::new();
1047 mesh_lookup.insert(0, (positions, indices.clone()));
1048
1049 let item = crate::renderer::SceneRenderItem {
1050 mesh_id: crate::resources::mesh_store::MeshId(0),
1051 model: glam::Mat4::IDENTITY.to_cols_array_2d(),
1052 visible: true,
1053 ..Default::default()
1054 };
1055
1056 let view_proj = make_view_proj();
1057 let viewport = glam::Vec2::new(800.0, 600.0);
1058
1059 let result = pick_rect(
1060 glam::Vec2::ZERO,
1061 viewport,
1062 &[item],
1063 &mesh_lookup,
1064 &[],
1065 view_proj,
1066 viewport,
1067 );
1068
1069 assert!(!result.is_empty(), "expected at least one triangle hit");
1071 assert!(result.total_count() > 0);
1072 }
1073
1074 #[test]
1075 fn test_pick_rect_miss() {
1076 let (positions, indices) = unit_cube_mesh();
1078 let mut mesh_lookup: std::collections::HashMap<usize, (Vec<[f32; 3]>, Vec<u32>)> =
1079 std::collections::HashMap::new();
1080 mesh_lookup.insert(0, (positions, indices));
1081
1082 let item = crate::renderer::SceneRenderItem {
1083 mesh_id: crate::resources::mesh_store::MeshId(0),
1084 model: glam::Mat4::IDENTITY.to_cols_array_2d(),
1085 visible: true,
1086 ..Default::default()
1087 };
1088
1089 let view_proj = make_view_proj();
1090 let viewport = glam::Vec2::new(800.0, 600.0);
1091
1092 let result = pick_rect(
1093 glam::Vec2::new(700.0, 500.0), glam::Vec2::new(799.0, 599.0),
1095 &[item],
1096 &mesh_lookup,
1097 &[],
1098 view_proj,
1099 viewport,
1100 );
1101
1102 assert!(result.is_empty(), "expected no hits in off-center rect");
1103 }
1104
1105 #[test]
1106 fn test_pick_rect_point_cloud() {
1107 let view_proj = make_view_proj();
1109 let viewport = glam::Vec2::new(800.0, 600.0);
1110
1111 let pc = crate::renderer::PointCloudItem {
1112 positions: vec![[0.0, 0.0, 0.0], [0.1, 0.1, 0.0]],
1113 model: glam::Mat4::IDENTITY.to_cols_array_2d(),
1114 id: 99,
1115 ..Default::default()
1116 };
1117
1118 let result = pick_rect(
1119 glam::Vec2::ZERO,
1120 viewport,
1121 &[],
1122 &std::collections::HashMap::new(),
1123 &[pc],
1124 view_proj,
1125 viewport,
1126 );
1127
1128 assert!(!result.is_empty(), "expected point cloud hits");
1129 let hits = result.hits.get(&99).expect("expected hits for id 99");
1130 assert_eq!(
1131 hits.len(),
1132 2,
1133 "both points should be inside the full-screen rect"
1134 );
1135 assert!(
1137 hits.iter().all(|s| s.is_point()),
1138 "expected SubObjectRef::Point entries"
1139 );
1140 assert_eq!(hits[0], SubObjectRef::Point(0));
1141 assert_eq!(hits[1], SubObjectRef::Point(1));
1142 }
1143
1144 #[test]
1145 fn test_pick_rect_skips_invisible() {
1146 let (positions, indices) = unit_cube_mesh();
1147 let mut mesh_lookup: std::collections::HashMap<usize, (Vec<[f32; 3]>, Vec<u32>)> =
1148 std::collections::HashMap::new();
1149 mesh_lookup.insert(0, (positions, indices));
1150
1151 let item = crate::renderer::SceneRenderItem {
1152 mesh_id: crate::resources::mesh_store::MeshId(0),
1153 model: glam::Mat4::IDENTITY.to_cols_array_2d(),
1154 visible: false, ..Default::default()
1156 };
1157
1158 let view_proj = make_view_proj();
1159 let viewport = glam::Vec2::new(800.0, 600.0);
1160
1161 let result = pick_rect(
1162 glam::Vec2::ZERO,
1163 viewport,
1164 &[item],
1165 &mesh_lookup,
1166 &[],
1167 view_proj,
1168 viewport,
1169 );
1170
1171 assert!(result.is_empty(), "invisible items should be skipped");
1172 }
1173
1174 #[test]
1175 fn test_pick_rect_result_type() {
1176 let mut r = RectPickResult::default();
1178 assert!(r.is_empty());
1179 assert_eq!(r.total_count(), 0);
1180
1181 r.hits.insert(
1182 1,
1183 vec![
1184 SubObjectRef::Face(0),
1185 SubObjectRef::Face(1),
1186 SubObjectRef::Face(2),
1187 ],
1188 );
1189 r.hits.insert(2, vec![SubObjectRef::Point(5)]);
1190 assert!(!r.is_empty());
1191 assert_eq!(r.total_count(), 4);
1192 }
1193
1194 #[test]
1195 fn test_barycentric_at_vertices() {
1196 let a = glam::Vec3::new(0.0, 0.0, 0.0);
1197 let b = glam::Vec3::new(1.0, 0.0, 0.0);
1198 let c = glam::Vec3::new(0.0, 1.0, 0.0);
1199
1200 let (u, v, w) = super::barycentric(a, a, b, c);
1202 assert!((u - 1.0).abs() < 1e-5, "u={u}");
1203 assert!(v.abs() < 1e-5, "v={v}");
1204 assert!(w.abs() < 1e-5, "w={w}");
1205
1206 let (u, v, w) = super::barycentric(b, a, b, c);
1208 assert!(u.abs() < 1e-5, "u={u}");
1209 assert!((v - 1.0).abs() < 1e-5, "v={v}");
1210 assert!(w.abs() < 1e-5, "w={w}");
1211
1212 let centroid = (a + b + c) / 3.0;
1214 let (u, v, w) = super::barycentric(centroid, a, b, c);
1215 assert!((u - 1.0 / 3.0).abs() < 1e-4, "u={u}");
1216 assert!((v - 1.0 / 3.0).abs() < 1e-4, "v={v}");
1217 assert!((w - 1.0 / 3.0).abs() < 1e-4, "w={w}");
1218 }
1219}