1use crate::geometry::marching_cubes::VolumeData;
31use crate::interaction::select::sub_object::SubObjectRef;
32use crate::resources::volume::volume_mesh::{CELL_SENTINEL, VolumeMeshData};
33use crate::resources::{AttributeData, AttributeKind, AttributeRef};
34use crate::scene::traits::ViewportObject;
35use parry3d::math::{Pose, Vector};
36use parry3d::query::{Ray, RayCast};
37
38#[derive(Clone, Copy, Debug)]
47#[non_exhaustive]
48pub struct PickHit {
49 pub id: u64,
51 pub sub_object: Option<SubObjectRef>,
56 pub world_pos: glam::Vec3,
58 pub normal: glam::Vec3,
60 #[deprecated(since = "0.5.0", note = "use `sub_object` instead")]
65 pub triangle_index: u32,
66 #[deprecated(since = "0.5.0", note = "use `sub_object` instead")]
71 pub point_index: Option<u32>,
72 pub scalar_value: Option<f32>,
79}
80
81impl PickHit {
82 #[allow(deprecated)]
99 pub fn object_hit(id: u64, world_pos: glam::Vec3, normal: glam::Vec3) -> Self {
100 Self {
101 id,
102 sub_object: None,
103 world_pos,
104 normal,
105 triangle_index: u32::MAX,
106 point_index: None,
107 scalar_value: None,
108 }
109 }
110}
111
112#[derive(Clone, Copy, Debug)]
124#[non_exhaustive]
125pub struct GpuPickHit {
126 pub object_id: crate::renderer::PickId,
133 pub depth: f32,
142}
143
144pub fn screen_to_ray(
157 screen_pos: glam::Vec2,
158 viewport_size: glam::Vec2,
159 view_proj_inv: glam::Mat4,
160) -> (glam::Vec3, glam::Vec3) {
161 let ndc_x = (screen_pos.x / viewport_size.x) * 2.0 - 1.0;
162 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));
164 let far = view_proj_inv.project_point3(glam::Vec3::new(ndc_x, ndc_y, 1.0));
165 let dir = (far - near).normalize();
166 (near, dir)
167}
168
169pub fn pick_scene_cpu(
178 ray_origin: glam::Vec3,
179 ray_dir: glam::Vec3,
180 objects: &[&dyn ViewportObject],
181 mesh_lookup: &std::collections::HashMap<u64, (Vec<[f32; 3]>, Vec<u32>)>,
182) -> Option<PickHit> {
183 let ray = Ray::new(
185 Vector::new(ray_origin.x, ray_origin.y, ray_origin.z),
186 Vector::new(ray_dir.x, ray_dir.y, ray_dir.z),
187 );
188
189 let mut best_hit: Option<(u64, f32, PickHit)> = None;
190
191 for obj in objects {
192 if !obj.is_visible() {
193 continue;
194 }
195 let Some(mesh_id) = obj.mesh_id() else {
196 continue;
197 };
198
199 if let Some((positions, indices)) = mesh_lookup.get(&mesh_id) {
200 let s = obj.scale();
205 let verts: Vec<Vector> = positions
206 .iter()
207 .map(|p: &[f32; 3]| Vector::new(p[0] * s.x, p[1] * s.y, p[2] * s.z))
208 .collect();
209
210 let tri_indices: Vec<[u32; 3]> = indices
211 .chunks(3)
212 .filter(|c: &&[u32]| c.len() == 3)
213 .map(|c: &[u32]| [c[0], c[1], c[2]])
214 .collect();
215
216 if tri_indices.is_empty() {
217 continue;
218 }
219
220 match parry3d::shape::TriMesh::new(verts, tri_indices) {
221 Ok(trimesh) => {
222 let pose = Pose::from_parts(obj.position(), obj.rotation());
226 if let Some(intersection) =
227 trimesh.cast_ray_and_get_normal(&pose, &ray, f32::MAX, true)
228 {
229 let toi = intersection.time_of_impact;
230 if best_hit.is_none() || toi < best_hit.as_ref().unwrap().1 {
231 let sub_object = SubObjectRef::from_feature_id(intersection.feature);
232 let world_pos = ray_origin + ray_dir * toi;
233 let normal = intersection.normal;
235 let triangle_index = if let Some(SubObjectRef::Face(i)) = sub_object {
236 i
237 } else {
238 u32::MAX
239 };
240 #[allow(deprecated)]
241 let hit = PickHit {
242 id: obj.id(),
243 sub_object,
244 triangle_index,
245 world_pos,
246 normal,
247 point_index: None,
248 scalar_value: None,
249 };
250 best_hit = Some((obj.id(), toi, hit));
251 }
252 }
253 }
254 Err(e) => {
255 tracing::warn!(object_id = obj.id(), error = %e, "TriMesh construction failed for picking");
256 }
257 }
258 }
259 }
260
261 best_hit.map(|(_, _, hit)| hit)
262}
263
264pub fn pick_scene_nodes_cpu(
269 ray_origin: glam::Vec3,
270 ray_dir: glam::Vec3,
271 scene: &crate::scene::scene::Scene,
272 mesh_lookup: &std::collections::HashMap<u64, (Vec<[f32; 3]>, Vec<u32>)>,
273) -> Option<PickHit> {
274 if let Some(hit) = pick_light_glyphs_cpu(ray_origin, ray_dir, scene) {
278 return Some(hit);
279 }
280
281 let nodes: Vec<&dyn ViewportObject> = scene.nodes().map(|n| n as &dyn ViewportObject).collect();
282 pick_scene_cpu(ray_origin, ray_dir, &nodes, mesh_lookup)
283}
284
285const LIGHT_GLYPH_PICK_RADIUS: f32 = 0.35;
289
290fn pick_light_glyphs_cpu(
293 ray_origin: glam::Vec3,
294 ray_dir: glam::Vec3,
295 scene: &crate::scene::scene::Scene,
296) -> Option<PickHit> {
297 let mut best: Option<(f32, PickHit)> = None;
298 for node in scene.nodes() {
299 if node.light.is_none() {
300 continue;
301 }
302 if !node.is_visible() {
303 continue;
304 }
305 let world = node.world_transform();
306 let center = world.col(3).truncate();
307 let Some(toi) = ray_sphere_intersect(ray_origin, ray_dir, center, LIGHT_GLYPH_PICK_RADIUS)
308 else {
309 continue;
310 };
311 if best.as_ref().map(|(t, _)| toi < *t).unwrap_or(true) {
312 let world_pos = ray_origin + ray_dir * toi;
313 let normal = (world_pos - center).normalize_or_zero();
314 best = Some((toi, PickHit::object_hit(node.id(), world_pos, normal)));
315 }
316 }
317 best.map(|(_, h)| h)
318}
319
320fn ray_sphere_intersect(
323 origin: glam::Vec3,
324 dir: glam::Vec3,
325 center: glam::Vec3,
326 radius: f32,
327) -> Option<f32> {
328 let oc = origin - center;
329 let a = dir.dot(dir);
330 if a <= 0.0 {
331 return None;
332 }
333 let b = 2.0 * oc.dot(dir);
334 let c = oc.dot(oc) - radius * radius;
335 let disc = b * b - 4.0 * a * c;
336 if disc < 0.0 {
337 return None;
338 }
339 let sqd = disc.sqrt();
340 let t0 = (-b - sqd) / (2.0 * a);
341 let t1 = (-b + sqd) / (2.0 * a);
342 if t0 > 1.0e-4 {
343 Some(t0)
344 } else if t1 > 1.0e-4 {
345 Some(t1)
346 } else {
347 None
348 }
349}
350
351pub struct ProbeBinding<'a> {
360 pub id: u64,
362 pub attribute_ref: &'a AttributeRef,
364 pub attribute_data: &'a AttributeData,
366 pub positions: &'a [[f32; 3]],
368 pub indices: &'a [u32],
370}
371
372fn barycentric(p: glam::Vec3, a: glam::Vec3, b: glam::Vec3, c: glam::Vec3) -> (f32, f32, f32) {
377 let v0 = b - a;
378 let v1 = c - a;
379 let v2 = p - a;
380 let d00 = v0.dot(v0);
381 let d01 = v0.dot(v1);
382 let d11 = v1.dot(v1);
383 let d20 = v2.dot(v0);
384 let d21 = v2.dot(v1);
385 let denom = d00 * d11 - d01 * d01;
386 if denom.abs() < 1e-12 {
387 return (1.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0);
389 }
390 let inv = 1.0 / denom;
391 let v = (d11 * d20 - d01 * d21) * inv;
392 let w = (d00 * d21 - d01 * d20) * inv;
393 let u = 1.0 - v - w;
394 (u, v, w)
395}
396
397fn probe_scalar(hit: &mut PickHit, binding: &ProbeBinding<'_>) {
400 let tri_idx_raw = match hit.sub_object {
401 Some(SubObjectRef::Face(i)) => i,
402 _ => return,
403 };
404
405 let num_triangles = binding.indices.len() / 3;
406 let tri_idx = if (tri_idx_raw as usize) >= num_triangles && num_triangles > 0 {
409 tri_idx_raw as usize - num_triangles
410 } else {
411 tri_idx_raw as usize
412 };
413
414 match binding.attribute_ref.kind {
415 AttributeKind::Cell => {
416 if let AttributeData::Cell(data) = binding.attribute_data {
418 if let Some(&val) = data.get(tri_idx) {
419 hit.scalar_value = Some(val);
420 }
421 }
422 }
423 AttributeKind::Face => {
424 if let AttributeData::Face(data) = binding.attribute_data {
426 if let Some(&val) = data.get(tri_idx) {
427 hit.scalar_value = Some(val);
428 }
429 }
430 }
431 AttributeKind::FaceColour => {
432 }
434 AttributeKind::Vertex => {
435 if let AttributeData::Vertex(data) = binding.attribute_data {
437 let base = tri_idx * 3;
438 if base + 2 >= binding.indices.len() {
439 return;
440 }
441 let i0 = binding.indices[base] as usize;
442 let i1 = binding.indices[base + 1] as usize;
443 let i2 = binding.indices[base + 2] as usize;
444
445 if i0 >= data.len() || i1 >= data.len() || i2 >= data.len() {
446 return;
447 }
448 if i0 >= binding.positions.len()
449 || i1 >= binding.positions.len()
450 || i2 >= binding.positions.len()
451 {
452 return;
453 }
454
455 let a = glam::Vec3::from(binding.positions[i0]);
456 let b = glam::Vec3::from(binding.positions[i1]);
457 let c = glam::Vec3::from(binding.positions[i2]);
458 let (u, v, w) = barycentric(hit.world_pos, a, b, c);
459 hit.scalar_value = Some(data[i0] * u + data[i1] * v + data[i2] * w);
460 }
461 }
462 AttributeKind::Edge => {
463 if let AttributeData::Edge(data) = binding.attribute_data {
466 let base = tri_idx * 3;
467 if base + 2 >= binding.indices.len() || data.is_empty() {
468 return;
469 }
470 let i0 = binding.indices[base] as usize;
471 let i1 = binding.indices[base + 1] as usize;
472 let i2 = binding.indices[base + 2] as usize;
473 if i0 < data.len() || i1 < data.len() || i2 < data.len() {
474 if i0 < data.len()
476 && i1 < data.len()
477 && i2 < data.len()
478 && i0 < binding.positions.len()
479 && i1 < binding.positions.len()
480 && i2 < binding.positions.len()
481 {
482 let a = glam::Vec3::from(binding.positions[i0]);
483 let b = glam::Vec3::from(binding.positions[i1]);
484 let c = glam::Vec3::from(binding.positions[i2]);
485 let (u, v, w) = barycentric(hit.world_pos, a, b, c);
486 hit.scalar_value = Some(data[i0] * u + data[i1] * v + data[i2] * w);
487 }
488 }
489 }
490 }
491 AttributeKind::Halfedge | AttributeKind::Corner => {
492 let extract = |data: &[f32]| -> Option<f32> {
495 let base = tri_idx * 3;
496 if base + 2 >= data.len() {
497 return None;
498 }
499 Some(data[base])
501 };
502 match binding.attribute_data {
503 AttributeData::Halfedge(data) | AttributeData::Corner(data) => {
504 hit.scalar_value = extract(data);
505 }
506 _ => {}
507 }
508 }
509 }
510}
511
512pub fn pick_scene_with_probe_cpu(
519 ray_origin: glam::Vec3,
520 ray_dir: glam::Vec3,
521 objects: &[&dyn ViewportObject],
522 mesh_lookup: &std::collections::HashMap<u64, (Vec<[f32; 3]>, Vec<u32>)>,
523 probe_bindings: &[ProbeBinding<'_>],
524) -> Option<PickHit> {
525 let mut hit = pick_scene_cpu(ray_origin, ray_dir, objects, mesh_lookup)?;
526 if let Some(binding) = probe_bindings.iter().find(|b| b.id == hit.id) {
527 probe_scalar(&mut hit, binding);
528 }
529 Some(hit)
530}
531
532pub fn pick_scene_nodes_with_probe_cpu(
536 ray_origin: glam::Vec3,
537 ray_dir: glam::Vec3,
538 scene: &crate::scene::scene::Scene,
539 mesh_lookup: &std::collections::HashMap<u64, (Vec<[f32; 3]>, Vec<u32>)>,
540 probe_bindings: &[ProbeBinding<'_>],
541) -> Option<PickHit> {
542 let mut hit = pick_scene_nodes_cpu(ray_origin, ray_dir, scene, mesh_lookup)?;
543 if let Some(binding) = probe_bindings.iter().find(|b| b.id == hit.id) {
544 probe_scalar(&mut hit, binding);
545 }
546 Some(hit)
547}
548
549pub fn pick_scene_accelerated_with_probe_cpu(
554 ray_origin: glam::Vec3,
555 ray_dir: glam::Vec3,
556 accelerator: &mut crate::geometry::bvh::PickAccelerator,
557 mesh_lookup: &std::collections::HashMap<u64, (Vec<[f32; 3]>, Vec<u32>)>,
558 probe_bindings: &[ProbeBinding<'_>],
559) -> Option<PickHit> {
560 let mut hit = accelerator.pick(ray_origin, ray_dir, mesh_lookup)?;
561 if let Some(binding) = probe_bindings.iter().find(|b| b.id == hit.id) {
562 probe_scalar(&mut hit, binding);
563 }
564 Some(hit)
565}
566
567#[derive(Clone, Debug, Default)]
578pub struct RectPickResult {
579 pub hits: std::collections::HashMap<u64, Vec<SubObjectRef>>,
586}
587
588impl RectPickResult {
589 pub fn is_empty(&self) -> bool {
591 self.hits.is_empty()
592 }
593
594 pub fn total_count(&self) -> usize {
596 self.hits.values().map(|v| v.len()).sum()
597 }
598}
599
600fn ray_aabb_volume(
611 origin: glam::Vec3,
612 dir: glam::Vec3,
613 bbox_min: glam::Vec3,
614 bbox_max: glam::Vec3,
615) -> Option<(f32, f32, usize, f32)> {
616 let mut t_min = f32::NEG_INFINITY;
617 let mut t_max = f32::INFINITY;
618 let mut entry_axis = 0usize;
619 let mut entry_sign = -1.0f32;
620
621 let dirs = [dir.x, dir.y, dir.z];
622 let origins = [origin.x, origin.y, origin.z];
623 let mins = [bbox_min.x, bbox_min.y, bbox_min.z];
624 let maxs = [bbox_max.x, bbox_max.y, bbox_max.z];
625
626 for i in 0..3 {
627 let d = dirs[i];
628 let o = origins[i];
629 if d.abs() < 1e-12 {
630 if o < mins[i] || o > maxs[i] {
632 return None;
633 }
634 } else {
635 let t1 = (mins[i] - o) / d;
636 let t2 = (maxs[i] - o) / d;
637 let (t_near, t_far) = if t1 <= t2 { (t1, t2) } else { (t2, t1) };
638 if t_near > t_min {
639 t_min = t_near;
640 entry_axis = i;
641 entry_sign = if d > 0.0 { -1.0 } else { 1.0 };
644 }
645 if t_far < t_max {
646 t_max = t_far;
647 }
648 }
649 }
650
651 if t_min > t_max || t_max < 0.0 {
652 return None;
653 }
654 Some((t_min, t_max, entry_axis, entry_sign))
655}
656
657pub fn pick_volume_cpu(
682 ray_origin: glam::Vec3,
683 ray_dir: glam::Vec3,
684 id: u64,
685 item: &crate::renderer::VolumeItem,
686 volume: &VolumeData,
687) -> Option<PickHit> {
688 let [nx, ny, nz] = volume.dims;
689 if nx == 0 || ny == 0 || nz == 0 || volume.data.is_empty() {
690 return None;
691 }
692
693 let model = glam::Mat4::from_cols_array_2d(&item.model);
695 let inv_model = model.inverse();
696 let local_origin = inv_model.transform_point3(ray_origin);
697 let local_dir = inv_model.transform_vector3(ray_dir);
698
699 let bbox_min = glam::Vec3::from(item.bbox_min);
700 let bbox_max = glam::Vec3::from(item.bbox_max);
701
702 let (t_entry, t_exit, entry_axis, entry_sign) =
703 ray_aabb_volume(local_origin, local_dir, bbox_min, bbox_max)?;
704
705 let t_start = t_entry.max(0.0);
707 if t_start >= t_exit {
708 return None;
709 }
710
711 let extent = bbox_max - bbox_min;
713 let cell = extent / glam::Vec3::new(nx as f32, ny as f32, nz as f32);
714
715 let p_entry = local_origin + t_start * local_dir;
717
718 let eps = 1e-4_f32;
721 let frac =
722 ((p_entry - bbox_min) / extent).clamp(glam::Vec3::splat(eps), glam::Vec3::splat(1.0 - eps));
723 let mut ix = (frac.x * nx as f32).floor() as i32;
724 let mut iy = (frac.y * ny as f32).floor() as i32;
725 let mut iz = (frac.z * nz as f32).floor() as i32;
726 ix = ix.clamp(0, nx as i32 - 1);
727 iy = iy.clamp(0, ny as i32 - 1);
728 iz = iz.clamp(0, nz as i32 - 1);
729
730 let step_x: i32 = if local_dir.x >= 0.0 { 1 } else { -1 };
732 let step_y: i32 = if local_dir.y >= 0.0 { 1 } else { -1 };
733 let step_z: i32 = if local_dir.z >= 0.0 { 1 } else { -1 };
734
735 let td_x = if local_dir.x.abs() > 1e-12 {
737 cell.x / local_dir.x.abs()
738 } else {
739 f32::INFINITY
740 };
741 let td_y = if local_dir.y.abs() > 1e-12 {
742 cell.y / local_dir.y.abs()
743 } else {
744 f32::INFINITY
745 };
746 let td_z = if local_dir.z.abs() > 1e-12 {
747 cell.z / local_dir.z.abs()
748 } else {
749 f32::INFINITY
750 };
751
752 let next_bx = bbox_min.x + (if step_x > 0 { ix + 1 } else { ix }) as f32 * cell.x;
754 let next_by = bbox_min.y + (if step_y > 0 { iy + 1 } else { iy }) as f32 * cell.y;
755 let next_bz = bbox_min.z + (if step_z > 0 { iz + 1 } else { iz }) as f32 * cell.z;
756
757 let mut tmax_x = if local_dir.x.abs() > 1e-12 {
758 t_start + (next_bx - p_entry.x) / local_dir.x
759 } else {
760 f32::INFINITY
761 };
762 let mut tmax_y = if local_dir.y.abs() > 1e-12 {
763 t_start + (next_by - p_entry.y) / local_dir.y
764 } else {
765 f32::INFINITY
766 };
767 let mut tmax_z = if local_dir.z.abs() > 1e-12 {
768 t_start + (next_bz - p_entry.z) / local_dir.z
769 } else {
770 f32::INFINITY
771 };
772
773 let mut entry_normal_local = glam::Vec3::ZERO;
775 match entry_axis {
776 0 => entry_normal_local.x = entry_sign,
777 1 => entry_normal_local.y = entry_sign,
778 _ => entry_normal_local.z = entry_sign,
779 }
780
781 let mut t_voxel_entry = t_start;
783
784 loop {
785 if ix < 0 || ix >= nx as i32 || iy < 0 || iy >= ny as i32 || iz < 0 || iz >= nz as i32 {
787 break;
788 }
789
790 let flat = ix as u32 + iy as u32 * nx + iz as u32 * nx * ny;
791 let scalar = volume.data[flat as usize];
792
793 if !scalar.is_nan() && scalar >= item.threshold_min && scalar <= item.threshold_max {
795 let local_hit = local_origin + t_voxel_entry * local_dir;
796 let world_pos = model.transform_point3(local_hit);
797 let world_normal = inv_model
799 .transpose()
800 .transform_vector3(entry_normal_local)
801 .normalize();
802
803 #[allow(deprecated)]
804 return Some(PickHit {
805 id,
806 sub_object: Some(SubObjectRef::Voxel(flat)),
807 world_pos,
808 normal: world_normal,
809 triangle_index: u32::MAX,
810 point_index: None,
811 scalar_value: Some(scalar),
812 });
813 }
814
815 if tmax_x <= tmax_y && tmax_x <= tmax_z {
817 if tmax_x > t_exit {
818 break;
819 }
820 t_voxel_entry = tmax_x;
821 tmax_x += td_x;
822 ix += step_x;
823 entry_normal_local = glam::Vec3::new(-(step_x as f32), 0.0, 0.0);
824 } else if tmax_y <= tmax_z {
825 if tmax_y > t_exit {
826 break;
827 }
828 t_voxel_entry = tmax_y;
829 tmax_y += td_y;
830 iy += step_y;
831 entry_normal_local = glam::Vec3::new(0.0, -(step_y as f32), 0.0);
832 } else {
833 if tmax_z > t_exit {
834 break;
835 }
836 t_voxel_entry = tmax_z;
837 tmax_z += td_z;
838 iz += step_z;
839 entry_normal_local = glam::Vec3::new(0.0, 0.0, -(step_z as f32));
840 }
841 }
842
843 None
844}
845
846pub fn voxel_world_aabb(
860 flat_index: u32,
861 volume: &VolumeData,
862 item: &crate::renderer::VolumeItem,
863) -> (glam::Vec3, glam::Vec3) {
864 let [nx, ny, nz] = volume.dims;
865 let ix = flat_index % nx;
866 let iy = (flat_index / nx) % ny;
867 let iz = flat_index / (nx * ny);
868 assert!(
869 ix < nx && iy < ny && iz < nz,
870 "flat_index {} out of bounds for dims {:?}",
871 flat_index,
872 volume.dims
873 );
874
875 let bbox_min = glam::Vec3::from(item.bbox_min);
876 let bbox_max = glam::Vec3::from(item.bbox_max);
877 let cell = (bbox_max - bbox_min) / glam::Vec3::new(nx as f32, ny as f32, nz as f32);
878
879 let local_lo =
880 bbox_min + glam::Vec3::new(ix as f32 * cell.x, iy as f32 * cell.y, iz as f32 * cell.z);
881 let local_hi = local_lo + cell;
882
883 let model = glam::Mat4::from_cols_array_2d(&item.model);
884 let corners = [
885 glam::Vec3::new(local_lo.x, local_lo.y, local_lo.z),
886 glam::Vec3::new(local_hi.x, local_lo.y, local_lo.z),
887 glam::Vec3::new(local_lo.x, local_hi.y, local_lo.z),
888 glam::Vec3::new(local_hi.x, local_hi.y, local_lo.z),
889 glam::Vec3::new(local_lo.x, local_lo.y, local_hi.z),
890 glam::Vec3::new(local_hi.x, local_lo.y, local_hi.z),
891 glam::Vec3::new(local_lo.x, local_hi.y, local_hi.z),
892 glam::Vec3::new(local_hi.x, local_hi.y, local_hi.z),
893 ];
894
895 let world_min = corners
896 .iter()
897 .map(|&c| model.transform_point3(c))
898 .fold(glam::Vec3::splat(f32::INFINITY), |acc, c| acc.min(c));
899 let world_max = corners
900 .iter()
901 .map(|&c| model.transform_point3(c))
902 .fold(glam::Vec3::splat(f32::NEG_INFINITY), |acc, c| acc.max(c));
903
904 (world_min, world_max)
905}
906
907pub fn pick_point_cloud_cpu(
921 click_pos: glam::Vec2,
922 id: u64,
923 item: &crate::renderer::PointCloudItem,
924 view_proj: glam::Mat4,
925 viewport_size: glam::Vec2,
926 radius_px: f32,
927) -> Option<PickHit> {
928 let model = glam::Mat4::from_cols_array_2d(&item.model);
931 pick_gaussian_splat_cpu(
932 click_pos,
933 id,
934 &item.positions,
935 model,
936 view_proj,
937 viewport_size,
938 radius_px,
939 )
940}
941
942pub fn nearest_vertex_on_hit(
961 hit: &PickHit,
962 positions: &[[f32; 3]],
963 indices: &[u32],
964 model: glam::Mat4,
965) -> Option<SubObjectRef> {
966 let face_raw = match hit.sub_object {
967 Some(SubObjectRef::Face(i)) => i as usize,
968 _ => return None,
969 };
970 let n_tri = indices.len() / 3;
971 if n_tri == 0 {
972 return None;
973 }
974 let face = if face_raw >= n_tri {
976 face_raw - n_tri
977 } else {
978 face_raw
979 };
980 if face * 3 + 2 >= indices.len() {
981 return None;
982 }
983 let vi = [
984 indices[face * 3] as usize,
985 indices[face * 3 + 1] as usize,
986 indices[face * 3 + 2] as usize,
987 ];
988 let (best_vi, _) = vi
989 .iter()
990 .map(|&i| {
991 let p = model.transform_point3(glam::Vec3::from(positions[i]));
992 (i, p.distance(hit.world_pos))
993 })
994 .fold(
995 (vi[0], f32::MAX),
996 |acc, (i, d)| {
997 if d < acc.1 { (i, d) } else { acc }
998 },
999 );
1000 Some(SubObjectRef::Vertex(best_vi as u32))
1001}
1002
1003pub fn pick_gaussian_splat_cpu(
1024 click_pos: glam::Vec2,
1025 id: u64,
1026 positions: &[[f32; 3]],
1027 model: glam::Mat4,
1028 view_proj: glam::Mat4,
1029 viewport_size: glam::Vec2,
1030 radius_px: f32,
1031) -> Option<PickHit> {
1032 if id == 0 || positions.is_empty() {
1033 return None;
1034 }
1035 let mvp = view_proj * model;
1036 let mut best_dist_sq = radius_px * radius_px;
1037 let mut best_idx: Option<u32> = None;
1038 let mut best_world = glam::Vec3::ZERO;
1039
1040 for (i, pos) in positions.iter().enumerate() {
1041 let local = glam::Vec3::from(*pos);
1042 let clip = mvp * local.extend(1.0);
1043 if clip.w <= 0.0 {
1044 continue;
1045 }
1046 let sx = (clip.x / clip.w + 1.0) * 0.5 * viewport_size.x;
1047 let sy = (1.0 - clip.y / clip.w) * 0.5 * viewport_size.y;
1048 let dx = sx - click_pos.x;
1049 let dy = sy - click_pos.y;
1050 let dist_sq = dx * dx + dy * dy;
1051 if dist_sq < best_dist_sq {
1052 best_dist_sq = dist_sq;
1053 best_idx = Some(i as u32);
1054 best_world = model.transform_point3(local);
1055 }
1056 }
1057
1058 let idx = best_idx?;
1059 #[allow(deprecated)]
1060 Some(PickHit {
1061 id,
1062 sub_object: Some(SubObjectRef::Point(idx)),
1063 world_pos: best_world,
1064 normal: glam::Vec3::Y,
1065 triangle_index: u32::MAX,
1066 point_index: Some(idx),
1067 scalar_value: None,
1068 })
1069}
1070
1071fn ray_tri_mt_ds(
1079 orig: glam::Vec3,
1080 dir: glam::Vec3,
1081 v0: glam::Vec3,
1082 v1: glam::Vec3,
1083 v2: glam::Vec3,
1084) -> Option<f32> {
1085 let e1 = v1 - v0;
1086 let e2 = v2 - v0;
1087 let h = dir.cross(e2);
1088 let a = e1.dot(h);
1089 if a.abs() < 1e-8 {
1090 return None;
1091 }
1092 let f = 1.0 / a;
1093 let s = orig - v0;
1094 let u = f * s.dot(h);
1095 if !(0.0..=1.0).contains(&u) {
1096 return None;
1097 }
1098 let q = s.cross(e1);
1099 let v = f * dir.dot(q);
1100 if v < 0.0 || u + v > 1.0 {
1101 return None;
1102 }
1103 let t = f * e2.dot(q);
1104 if t > 1e-6 { Some(t) } else { None }
1105}
1106
1107const VM_TET_FACES: [[usize; 3]; 4] = [[1, 2, 3], [0, 3, 2], [0, 1, 3], [0, 2, 1]];
1112
1113const VM_HEX_TRIS: [[usize; 3]; 12] = [
1115 [0, 1, 2],
1116 [0, 2, 3], [4, 7, 6],
1118 [4, 6, 5], [0, 4, 5],
1120 [0, 5, 1], [2, 6, 7],
1122 [2, 7, 3], [0, 3, 7],
1124 [0, 7, 4], [1, 5, 6],
1126 [1, 6, 2], ];
1128
1129const VM_PYRAMID_TRIS: [[usize; 3]; 6] = [
1131 [0, 1, 2],
1132 [0, 2, 3], [0, 4, 1],
1134 [1, 4, 2],
1135 [2, 4, 3],
1136 [3, 4, 0], ];
1138
1139const VM_WEDGE_TRIS: [[usize; 3]; 8] = [
1141 [0, 2, 1],
1142 [3, 4, 5], [0, 1, 4],
1144 [0, 4, 3], [1, 2, 5],
1146 [1, 5, 4], [2, 0, 3],
1148 [2, 3, 5], ];
1150
1151pub fn pick_transparent_volume_mesh_cpu(
1166 ray_origin: glam::Vec3,
1167 ray_dir: glam::Vec3,
1168 id: u64,
1169 model: glam::Mat4,
1170 data: &VolumeMeshData,
1171) -> Option<PickHit> {
1172 if id == 0 || data.cells.is_empty() {
1173 return None;
1174 }
1175 let model_inv = model.inverse();
1176 let local_origin = model_inv.transform_point3(ray_origin);
1177 let local_dir = model_inv.transform_vector3(ray_dir);
1178
1179 let mut best_t = f32::MAX;
1180 let mut best_cell: Option<u32> = None;
1181
1182 for (cell_idx, cell) in data.cells.iter().enumerate() {
1183 let p = |i: usize| glam::Vec3::from(data.positions[cell[i] as usize]);
1184 let tris: &[[usize; 3]] = if cell[4] == CELL_SENTINEL {
1185 &VM_TET_FACES
1186 } else if cell[5] == CELL_SENTINEL {
1187 &VM_PYRAMID_TRIS
1188 } else if cell[6] == CELL_SENTINEL {
1189 &VM_WEDGE_TRIS
1190 } else {
1191 &VM_HEX_TRIS
1192 };
1193 for tri in tris {
1194 if let Some(t) = ray_tri_mt_ds(local_origin, local_dir, p(tri[0]), p(tri[1]), p(tri[2]))
1195 {
1196 if t < best_t {
1197 best_t = t;
1198 best_cell = Some(cell_idx as u32);
1199 }
1200 }
1201 }
1202 }
1203
1204 let cell_idx = best_cell?;
1205 let local_hit = local_origin + local_dir * best_t;
1206 let world_hit = model.transform_point3(local_hit);
1207 #[allow(deprecated)]
1208 Some(PickHit {
1209 id,
1210 sub_object: Some(SubObjectRef::Cell(cell_idx)),
1211 world_pos: world_hit,
1212 normal: -ray_dir.normalize(),
1213 triangle_index: u32::MAX,
1214 point_index: None,
1215 scalar_value: None,
1216 })
1217}
1218
1219fn project_to_screen(
1226 mvp: glam::Mat4,
1227 p: glam::Vec3,
1228 viewport_size: glam::Vec2,
1229) -> Option<(f32, f32)> {
1230 let clip = mvp * p.extend(1.0);
1231 if clip.w <= 0.0 {
1232 return None;
1233 }
1234 let sx = (clip.x / clip.w + 1.0) * 0.5 * viewport_size.x;
1235 let sy = (1.0 - clip.y / clip.w) * 0.5 * viewport_size.y;
1236 Some((sx, sy))
1237}
1238
1239fn in_rect(sx: f32, sy: f32, rect_min: glam::Vec2, rect_max: glam::Vec2) -> bool {
1241 sx >= rect_min.x && sx <= rect_max.x && sy >= rect_min.y && sy <= rect_max.y
1242}
1243
1244pub fn pick_volume_rect(
1264 rect_min: glam::Vec2,
1265 rect_max: glam::Vec2,
1266 id: u64,
1267 item: &crate::renderer::VolumeItem,
1268 volume: &VolumeData,
1269 view_proj: glam::Mat4,
1270 viewport_size: glam::Vec2,
1271) -> RectPickResult {
1272 let mut result = RectPickResult::default();
1273 if id == 0 {
1274 return result;
1275 }
1276 let model = glam::Mat4::from_cols_array_2d(&item.model);
1277 let bbox_min = glam::Vec3::from(item.bbox_min);
1278 let bbox_max = glam::Vec3::from(item.bbox_max);
1279 let [nx, ny, nz] = volume.dims;
1280 let cell = (bbox_max - bbox_min) / glam::Vec3::new(nx as f32, ny as f32, nz as f32);
1281 let mvp = view_proj * model;
1282
1283 let mut hits: Vec<SubObjectRef> = Vec::new();
1284 for iz in 0..nz {
1285 for iy in 0..ny {
1286 for ix in 0..nx {
1287 let flat = ix + iy * nx + iz * nx * ny;
1288 let scalar = volume.data[flat as usize];
1289 if scalar.is_nan() || scalar < item.threshold_min || scalar > item.threshold_max {
1290 continue;
1291 }
1292 let local_center = bbox_min
1293 + cell * glam::Vec3::new(ix as f32 + 0.5, iy as f32 + 0.5, iz as f32 + 0.5);
1294 let Some((sx, sy)) = project_to_screen(mvp, local_center, viewport_size) else {
1295 continue;
1296 };
1297 if in_rect(sx, sy, rect_min, rect_max) {
1298 hits.push(SubObjectRef::Voxel(flat));
1299 }
1300 }
1301 }
1302 }
1303 if !hits.is_empty() {
1304 result.hits.insert(id, hits);
1305 }
1306 result
1307}
1308
1309pub fn pick_transparent_volume_mesh_rect(
1328 rect_min: glam::Vec2,
1329 rect_max: glam::Vec2,
1330 id: u64,
1331 model: glam::Mat4,
1332 data: &VolumeMeshData,
1333 view_proj: glam::Mat4,
1334 viewport_size: glam::Vec2,
1335) -> RectPickResult {
1336 let mut result = RectPickResult::default();
1337 if id == 0 || data.cells.is_empty() {
1338 return result;
1339 }
1340 let mvp = view_proj * model;
1341 let mut hits: Vec<SubObjectRef> = Vec::new();
1342
1343 for (cell_idx, cell) in data.cells.iter().enumerate() {
1344 let nv: usize = if cell[4] == CELL_SENTINEL {
1345 4
1346 } else if cell[5] == CELL_SENTINEL {
1347 5
1348 } else if cell[6] == CELL_SENTINEL {
1349 6
1350 } else {
1351 8
1352 };
1353 let centroid: glam::Vec3 = cell[..nv]
1354 .iter()
1355 .map(|&vi| glam::Vec3::from(data.positions[vi as usize]))
1356 .sum::<glam::Vec3>()
1357 / nv as f32;
1358 let Some((sx, sy)) = project_to_screen(mvp, centroid, viewport_size) else {
1359 continue;
1360 };
1361 if in_rect(sx, sy, rect_min, rect_max) {
1362 hits.push(SubObjectRef::Cell(cell_idx as u32));
1363 }
1364 }
1365 if !hits.is_empty() {
1366 result.hits.insert(id, hits);
1367 }
1368 result
1369}
1370
1371pub fn pick_gaussian_splat_rect(
1390 rect_min: glam::Vec2,
1391 rect_max: glam::Vec2,
1392 id: u64,
1393 positions: &[[f32; 3]],
1394 model: glam::Mat4,
1395 view_proj: glam::Mat4,
1396 viewport_size: glam::Vec2,
1397) -> RectPickResult {
1398 let mut result = RectPickResult::default();
1399 if id == 0 || positions.is_empty() {
1400 return result;
1401 }
1402 let mvp = view_proj * model;
1403 let mut hits: Vec<SubObjectRef> = Vec::new();
1404
1405 for (i, pos) in positions.iter().enumerate() {
1406 let local = glam::Vec3::from(*pos);
1407 let Some((sx, sy)) = project_to_screen(mvp, local, viewport_size) else {
1408 continue;
1409 };
1410 if in_rect(sx, sy, rect_min, rect_max) {
1411 hits.push(SubObjectRef::Point(i as u32));
1412 }
1413 }
1414 if !hits.is_empty() {
1415 result.hits.insert(id, hits);
1416 }
1417 result
1418}
1419
1420#[cfg(test)]
1421mod tests {
1422 use super::*;
1423 use crate::scene::traits::ViewportObject;
1424 use std::collections::HashMap;
1425
1426 struct TestObject {
1427 id: u64,
1428 mesh_id: u64,
1429 position: glam::Vec3,
1430 visible: bool,
1431 }
1432
1433 impl ViewportObject for TestObject {
1434 fn id(&self) -> u64 {
1435 self.id
1436 }
1437 fn mesh_id(&self) -> Option<u64> {
1438 Some(self.mesh_id)
1439 }
1440 fn model_matrix(&self) -> glam::Mat4 {
1441 glam::Mat4::from_translation(self.position)
1442 }
1443 fn position(&self) -> glam::Vec3 {
1444 self.position
1445 }
1446 fn rotation(&self) -> glam::Quat {
1447 glam::Quat::IDENTITY
1448 }
1449 fn is_visible(&self) -> bool {
1450 self.visible
1451 }
1452 fn colour(&self) -> glam::Vec3 {
1453 glam::Vec3::ONE
1454 }
1455 }
1456
1457 fn unit_cube_mesh() -> (Vec<[f32; 3]>, Vec<u32>) {
1459 let positions = vec![
1460 [-0.5, -0.5, -0.5],
1461 [0.5, -0.5, -0.5],
1462 [0.5, 0.5, -0.5],
1463 [-0.5, 0.5, -0.5],
1464 [-0.5, -0.5, 0.5],
1465 [0.5, -0.5, 0.5],
1466 [0.5, 0.5, 0.5],
1467 [-0.5, 0.5, 0.5],
1468 ];
1469 let indices = vec![
1470 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, ];
1477 (positions, indices)
1478 }
1479
1480 #[test]
1481 fn test_screen_to_ray_center() {
1482 let vp_inv = glam::Mat4::IDENTITY;
1484 let (origin, dir) = screen_to_ray(
1485 glam::Vec2::new(400.0, 300.0),
1486 glam::Vec2::new(800.0, 600.0),
1487 vp_inv,
1488 );
1489 assert!(origin.x.abs() < 1e-3, "origin.x={}", origin.x);
1491 assert!(origin.y.abs() < 1e-3, "origin.y={}", origin.y);
1492 assert!(dir.z.abs() > 0.9, "dir should be along Z, got {dir:?}");
1493 }
1494
1495 #[test]
1496 fn test_pick_scene_hit() {
1497 let (positions, indices) = unit_cube_mesh();
1498 let mut mesh_lookup = HashMap::new();
1499 mesh_lookup.insert(1u64, (positions, indices));
1500
1501 let obj = TestObject {
1502 id: 42,
1503 mesh_id: 1,
1504 position: glam::Vec3::ZERO,
1505 visible: true,
1506 };
1507 let objects: Vec<&dyn ViewportObject> = vec![&obj];
1508
1509 let result = pick_scene_cpu(
1511 glam::Vec3::new(0.0, 0.0, 5.0),
1512 glam::Vec3::new(0.0, 0.0, -1.0),
1513 &objects,
1514 &mesh_lookup,
1515 );
1516 assert!(result.is_some(), "expected a hit");
1517 let hit = result.unwrap();
1518 assert_eq!(hit.id, 42);
1519 assert!(
1521 (hit.world_pos.z - 0.5).abs() < 0.01,
1522 "world_pos.z={}",
1523 hit.world_pos.z
1524 );
1525 assert!(hit.normal.z > 0.9, "normal={:?}", hit.normal);
1527 }
1528
1529 #[test]
1530 fn test_pick_scene_miss() {
1531 let (positions, indices) = unit_cube_mesh();
1532 let mut mesh_lookup = HashMap::new();
1533 mesh_lookup.insert(1u64, (positions, indices));
1534
1535 let obj = TestObject {
1536 id: 42,
1537 mesh_id: 1,
1538 position: glam::Vec3::ZERO,
1539 visible: true,
1540 };
1541 let objects: Vec<&dyn ViewportObject> = vec![&obj];
1542
1543 let result = pick_scene_cpu(
1545 glam::Vec3::new(100.0, 100.0, 5.0),
1546 glam::Vec3::new(0.0, 0.0, -1.0),
1547 &objects,
1548 &mesh_lookup,
1549 );
1550 assert!(result.is_none());
1551 }
1552
1553 #[test]
1554 fn test_pick_nearest_wins() {
1555 let (positions, indices) = unit_cube_mesh();
1556 let mut mesh_lookup = HashMap::new();
1557 mesh_lookup.insert(1u64, (positions.clone(), indices.clone()));
1558 mesh_lookup.insert(2u64, (positions, indices));
1559
1560 let near_obj = TestObject {
1561 id: 10,
1562 mesh_id: 1,
1563 position: glam::Vec3::new(0.0, 0.0, 2.0),
1564 visible: true,
1565 };
1566 let far_obj = TestObject {
1567 id: 20,
1568 mesh_id: 2,
1569 position: glam::Vec3::new(0.0, 0.0, -2.0),
1570 visible: true,
1571 };
1572 let objects: Vec<&dyn ViewportObject> = vec![&far_obj, &near_obj];
1573
1574 let result = pick_scene_cpu(
1576 glam::Vec3::new(0.0, 0.0, 10.0),
1577 glam::Vec3::new(0.0, 0.0, -1.0),
1578 &objects,
1579 &mesh_lookup,
1580 );
1581 assert!(result.is_some(), "expected a hit");
1582 assert_eq!(result.unwrap().id, 10);
1583 }
1584
1585 #[test]
1586 fn test_pick_scene_nodes_hit() {
1587 let (positions, indices) = unit_cube_mesh();
1588 let mut mesh_lookup = HashMap::new();
1589 mesh_lookup.insert(0u64, (positions, indices));
1590
1591 let mut scene = crate::scene::scene::Scene::new();
1592 scene.add(
1593 Some(crate::resources::mesh::mesh_store::MeshId::new(0, 0)),
1594 glam::Mat4::IDENTITY,
1595 crate::scene::material::Material::default(),
1596 );
1597 scene.update_transforms();
1598
1599 let result = pick_scene_nodes_cpu(
1600 glam::Vec3::new(0.0, 0.0, 5.0),
1601 glam::Vec3::new(0.0, 0.0, -1.0),
1602 &scene,
1603 &mesh_lookup,
1604 );
1605 assert!(result.is_some());
1606 }
1607
1608 #[test]
1609 fn test_pick_scene_nodes_miss() {
1610 let (positions, indices) = unit_cube_mesh();
1611 let mut mesh_lookup = HashMap::new();
1612 mesh_lookup.insert(0u64, (positions, indices));
1613
1614 let mut scene = crate::scene::scene::Scene::new();
1615 scene.add(
1616 Some(crate::resources::mesh::mesh_store::MeshId::new(0, 0)),
1617 glam::Mat4::IDENTITY,
1618 crate::scene::material::Material::default(),
1619 );
1620 scene.update_transforms();
1621
1622 let result = pick_scene_nodes_cpu(
1623 glam::Vec3::new(100.0, 100.0, 5.0),
1624 glam::Vec3::new(0.0, 0.0, -1.0),
1625 &scene,
1626 &mesh_lookup,
1627 );
1628 assert!(result.is_none());
1629 }
1630
1631 #[test]
1632 fn test_probe_vertex_attribute() {
1633 let (positions, indices) = unit_cube_mesh();
1634 let mut mesh_lookup = HashMap::new();
1635 mesh_lookup.insert(1u64, (positions.clone(), indices.clone()));
1636
1637 let vertex_scalars: Vec<f32> = (0..positions.len()).map(|i| i as f32).collect();
1639
1640 let obj = TestObject {
1641 id: 42,
1642 mesh_id: 1,
1643 position: glam::Vec3::ZERO,
1644 visible: true,
1645 };
1646 let objects: Vec<&dyn ViewportObject> = vec![&obj];
1647
1648 let attr_ref = AttributeRef {
1649 name: "test".to_string(),
1650 kind: AttributeKind::Vertex,
1651 };
1652 let attr_data = AttributeData::Vertex(vertex_scalars);
1653 let bindings = vec![ProbeBinding {
1654 id: 42,
1655 attribute_ref: &attr_ref,
1656 attribute_data: &attr_data,
1657 positions: &positions,
1658 indices: &indices,
1659 }];
1660
1661 let result = pick_scene_with_probe_cpu(
1662 glam::Vec3::new(0.0, 0.0, 5.0),
1663 glam::Vec3::new(0.0, 0.0, -1.0),
1664 &objects,
1665 &mesh_lookup,
1666 &bindings,
1667 );
1668 assert!(result.is_some(), "expected a hit");
1669 let hit = result.unwrap();
1670 assert_eq!(hit.id, 42);
1671 assert!(
1673 hit.scalar_value.is_some(),
1674 "expected scalar_value to be set"
1675 );
1676 }
1677
1678 #[test]
1679 fn test_probe_cell_attribute() {
1680 let (positions, indices) = unit_cube_mesh();
1681 let mut mesh_lookup = HashMap::new();
1682 mesh_lookup.insert(1u64, (positions.clone(), indices.clone()));
1683
1684 let num_triangles = indices.len() / 3;
1686 let cell_scalars: Vec<f32> = (0..num_triangles).map(|i| (i as f32) * 10.0).collect();
1687
1688 let obj = TestObject {
1689 id: 42,
1690 mesh_id: 1,
1691 position: glam::Vec3::ZERO,
1692 visible: true,
1693 };
1694 let objects: Vec<&dyn ViewportObject> = vec![&obj];
1695
1696 let attr_ref = AttributeRef {
1697 name: "pressure".to_string(),
1698 kind: AttributeKind::Cell,
1699 };
1700 let attr_data = AttributeData::Cell(cell_scalars.clone());
1701 let bindings = vec![ProbeBinding {
1702 id: 42,
1703 attribute_ref: &attr_ref,
1704 attribute_data: &attr_data,
1705 positions: &positions,
1706 indices: &indices,
1707 }];
1708
1709 let result = pick_scene_with_probe_cpu(
1710 glam::Vec3::new(0.0, 0.0, 5.0),
1711 glam::Vec3::new(0.0, 0.0, -1.0),
1712 &objects,
1713 &mesh_lookup,
1714 &bindings,
1715 );
1716 assert!(result.is_some());
1717 let hit = result.unwrap();
1718 assert!(hit.scalar_value.is_some());
1720 let val = hit.scalar_value.unwrap();
1721 assert!(
1722 cell_scalars.contains(&val),
1723 "scalar_value {val} not in cell_scalars"
1724 );
1725 }
1726
1727 #[test]
1728 fn test_probe_no_binding_leaves_none() {
1729 let (positions, indices) = unit_cube_mesh();
1730 let mut mesh_lookup = HashMap::new();
1731 mesh_lookup.insert(1u64, (positions, indices));
1732
1733 let obj = TestObject {
1734 id: 42,
1735 mesh_id: 1,
1736 position: glam::Vec3::ZERO,
1737 visible: true,
1738 };
1739 let objects: Vec<&dyn ViewportObject> = vec![&obj];
1740
1741 let result = pick_scene_with_probe_cpu(
1743 glam::Vec3::new(0.0, 0.0, 5.0),
1744 glam::Vec3::new(0.0, 0.0, -1.0),
1745 &objects,
1746 &mesh_lookup,
1747 &[],
1748 );
1749 assert!(result.is_some());
1750 assert!(result.unwrap().scalar_value.is_none());
1751 }
1752
1753 #[test]
1758 fn test_pick_rect_result_type() {
1759 let mut r = RectPickResult::default();
1761 assert!(r.is_empty());
1762 assert_eq!(r.total_count(), 0);
1763
1764 r.hits.insert(
1765 1,
1766 vec![
1767 SubObjectRef::Face(0),
1768 SubObjectRef::Face(1),
1769 SubObjectRef::Face(2),
1770 ],
1771 );
1772 r.hits.insert(2, vec![SubObjectRef::Point(5)]);
1773 assert!(!r.is_empty());
1774 assert_eq!(r.total_count(), 4);
1775 }
1776
1777 #[test]
1778 fn test_barycentric_at_vertices() {
1779 let a = glam::Vec3::new(0.0, 0.0, 0.0);
1780 let b = glam::Vec3::new(1.0, 0.0, 0.0);
1781 let c = glam::Vec3::new(0.0, 1.0, 0.0);
1782
1783 let (u, v, w) = super::barycentric(a, a, b, c);
1785 assert!((u - 1.0).abs() < 1e-5, "u={u}");
1786 assert!(v.abs() < 1e-5, "v={v}");
1787 assert!(w.abs() < 1e-5, "w={w}");
1788
1789 let (u, v, w) = super::barycentric(b, a, b, c);
1791 assert!(u.abs() < 1e-5, "u={u}");
1792 assert!((v - 1.0).abs() < 1e-5, "v={v}");
1793 assert!(w.abs() < 1e-5, "w={w}");
1794
1795 let centroid = (a + b + c) / 3.0;
1797 let (u, v, w) = super::barycentric(centroid, a, b, c);
1798 assert!((u - 1.0 / 3.0).abs() < 1e-4, "u={u}");
1799 assert!((v - 1.0 / 3.0).abs() < 1e-4, "v={v}");
1800 assert!((w - 1.0 / 3.0).abs() < 1e-4, "w={w}");
1801 }
1802
1803 fn make_volume_item(
1808 bbox_min: [f32; 3],
1809 bbox_max: [f32; 3],
1810 threshold_min: f32,
1811 threshold_max: f32,
1812 ) -> crate::renderer::VolumeItem {
1813 crate::renderer::VolumeItem {
1814 bbox_min,
1815 bbox_max,
1816 threshold_min,
1817 threshold_max,
1818 ..crate::renderer::VolumeItem::default()
1819 }
1820 }
1821
1822 fn make_volume_data(dims: [u32; 3], fill: f32) -> crate::geometry::marching_cubes::VolumeData {
1823 let n = (dims[0] * dims[1] * dims[2]) as usize;
1824 crate::geometry::marching_cubes::VolumeData {
1825 data: vec![fill; n],
1826 dims,
1827 origin: [0.0; 3],
1828 spacing: [1.0; 3],
1829 }
1830 }
1831
1832 #[test]
1833 fn test_pick_volume_basic_hit() {
1834 let item = make_volume_item([0.0; 3], [3.0, 3.0, 3.0], 0.5, 1.0);
1837 let volume = make_volume_data([3, 3, 3], 0.8);
1838
1839 let hit = super::pick_volume_cpu(
1840 glam::Vec3::new(1.5, 10.0, 1.5),
1841 glam::Vec3::new(0.0, -1.0, 0.0),
1842 42,
1843 &item,
1844 &volume,
1845 );
1846 assert!(hit.is_some(), "expected a hit");
1847 let hit = hit.unwrap();
1848
1849 assert_eq!(hit.id, 42);
1850 assert_eq!(hit.scalar_value, Some(0.8));
1851
1852 let flat = hit.sub_object.unwrap().index();
1854 let nx = 3u32;
1855 let ny = 3u32;
1856 let ix = flat % nx;
1857 let iy = (flat / nx) % ny;
1858 let iz = flat / (nx * ny);
1859 assert_eq!((ix, iy, iz), (1, 2, 1), "expected top-centre voxel");
1860
1861 assert!(hit.world_pos.y > 2.9, "world_pos.y={}", hit.world_pos.y);
1863
1864 assert!(hit.normal.y > 0.9, "normal={:?}", hit.normal);
1866 }
1867
1868 #[test]
1869 fn test_pick_volume_miss_aabb() {
1870 let item = make_volume_item([0.0; 3], [1.0; 3], 0.0, 1.0);
1871 let volume = make_volume_data([4, 4, 4], 0.5);
1872
1873 let hit = super::pick_volume_cpu(
1875 glam::Vec3::new(10.0, 5.0, 0.5),
1876 glam::Vec3::new(0.0, -1.0, 0.0),
1877 1,
1878 &item,
1879 &volume,
1880 );
1881 assert!(hit.is_none(), "expected miss");
1882 }
1883
1884 #[test]
1885 fn test_pick_volume_threshold_miss() {
1886 let item = make_volume_item([0.0; 3], [1.0; 3], 0.5, 1.0);
1888 let volume = make_volume_data([4, 4, 4], 0.3);
1889
1890 let hit = super::pick_volume_cpu(
1891 glam::Vec3::new(0.5, 5.0, 0.5),
1892 glam::Vec3::new(0.0, -1.0, 0.0),
1893 1,
1894 &item,
1895 &volume,
1896 );
1897 assert!(
1898 hit.is_none(),
1899 "expected no hit when all scalars below threshold"
1900 );
1901 }
1902
1903 #[test]
1904 fn test_pick_volume_threshold_skip() {
1905 let item = make_volume_item([0.0; 3], [1.0, 3.0, 1.0], 0.5, 1.0);
1910 let mut volume = make_volume_data([1, 3, 1], 0.0);
1911 volume.data[2] = 0.3;
1913 volume.data[1] = 0.8;
1914 volume.data[0] = 0.8;
1915
1916 let hit = super::pick_volume_cpu(
1917 glam::Vec3::new(0.5, 10.0, 0.5),
1918 glam::Vec3::new(0.0, -1.0, 0.0),
1919 1,
1920 &item,
1921 &volume,
1922 );
1923 assert!(hit.is_some(), "expected a hit");
1924 let hit = hit.unwrap();
1925 let flat = hit.sub_object.unwrap().index();
1926 assert_eq!(flat, 1, "expected iy=1 (flat=1), got flat={flat}");
1927 assert_eq!(hit.scalar_value, Some(0.8));
1928 }
1929
1930 #[test]
1931 fn test_pick_volume_nan_skip() {
1932 let item = make_volume_item([0.0; 3], [1.0, 2.0, 1.0], 0.0, 1.0);
1935 let mut volume = make_volume_data([1, 2, 1], 0.0);
1936 volume.data[1] = f32::NAN;
1937 volume.data[0] = 0.5;
1938
1939 let hit = super::pick_volume_cpu(
1940 glam::Vec3::new(0.5, 10.0, 0.5),
1941 glam::Vec3::new(0.0, -1.0, 0.0),
1942 1,
1943 &item,
1944 &volume,
1945 );
1946 assert!(hit.is_some(), "expected hit after NaN skip");
1947 let hit = hit.unwrap();
1948 assert_eq!(hit.sub_object.unwrap().index(), 0, "expected iy=0 (flat=0)");
1949 assert_eq!(hit.scalar_value, Some(0.5));
1950 }
1951
1952 #[test]
1953 fn test_pick_volume_dda_no_skip() {
1954 let item = make_volume_item([0.0; 3], [10.0, 1.0, 1.0], 0.5, 1.0);
1958 let mut volume = make_volume_data([10, 1, 1], 0.0);
1959 volume.data[9] = 0.8;
1960
1961 let dir = glam::Vec3::new(1.0, 0.0, 0.001).normalize();
1962 let hit = super::pick_volume_cpu(glam::Vec3::new(-1.0, 0.5, 0.5), dir, 1, &item, &volume);
1963 assert!(
1964 hit.is_some(),
1965 "DDA must reach the last voxel without skipping"
1966 );
1967 let flat = hit.unwrap().sub_object.unwrap().index();
1968 assert_eq!(flat, 9, "expected ix=9 (flat=9), got flat={flat}");
1969 }
1970
1971 #[test]
1972 fn test_voxel_world_aabb_identity() {
1973 let item = make_volume_item([0.0; 3], [4.0, 4.0, 4.0], 0.0, 1.0);
1975 let volume = make_volume_data([4, 4, 4], 0.0);
1976
1977 let (lo, hi) = super::voxel_world_aabb(0, &volume, &item);
1979 assert!((lo - glam::Vec3::ZERO).length() < 1e-5, "lo={lo:?}");
1980 assert!((hi - glam::Vec3::ONE).length() < 1e-5, "hi={hi:?}");
1981
1982 let (lo, hi) = super::voxel_world_aabb(1, &volume, &item);
1984 assert!((lo.x - 1.0).abs() < 1e-5 && (hi.x - 2.0).abs() < 1e-5);
1985
1986 let (lo, hi) = super::voxel_world_aabb(57, &volume, &item);
1988 assert!(
1989 (lo - glam::Vec3::new(1.0, 2.0, 3.0)).length() < 1e-5,
1990 "lo={lo:?}"
1991 );
1992 assert!(
1993 (hi - glam::Vec3::new(2.0, 3.0, 4.0)).length() < 1e-5,
1994 "hi={hi:?}"
1995 );
1996 }
1997
1998 #[test]
1999 fn test_voxel_world_aabb_round_trip() {
2000 let item = make_volume_item([0.0; 3], [3.0, 3.0, 3.0], 0.5, 1.0);
2003 let volume = make_volume_data([3, 3, 3], 0.8);
2004
2005 let hit = super::pick_volume_cpu(
2006 glam::Vec3::new(1.5, 10.0, 1.5),
2007 glam::Vec3::new(0.0, -1.0, 0.0),
2008 1,
2009 &item,
2010 &volume,
2011 )
2012 .expect("expected a hit for round-trip test");
2013
2014 let flat = hit.sub_object.unwrap().index();
2015 let (lo, hi) = super::voxel_world_aabb(flat, &volume, &item);
2016
2017 let tol = 1e-3;
2018 assert!(
2019 hit.world_pos.x >= lo.x - tol && hit.world_pos.x <= hi.x + tol,
2020 "world_pos.x={} outside [{}, {}]",
2021 hit.world_pos.x,
2022 lo.x,
2023 hi.x
2024 );
2025 assert!(
2026 hit.world_pos.y >= lo.y - tol && hit.world_pos.y <= hi.y + tol,
2027 "world_pos.y={} outside [{}, {}]",
2028 hit.world_pos.y,
2029 lo.y,
2030 hi.y
2031 );
2032 assert!(
2033 hit.world_pos.z >= lo.z - tol && hit.world_pos.z <= hi.z + tol,
2034 "world_pos.z={} outside [{}, {}]",
2035 hit.world_pos.z,
2036 lo.z,
2037 hi.z
2038 );
2039 }
2040}