1use ailloli_ui_core::{Point, Size};
4use openxr as xr;
5
6use crate::math::Vec3;
7
8use super::composer::OpenXrQuadLayerOptions;
9
10const PANEL_FACING_EPSILON: f32 = 1e-6;
12pub const DEFAULT_PANEL_PITCH_MIN_RAD: f32 = -std::f32::consts::FRAC_PI_4;
21pub const DEFAULT_PANEL_PITCH_MAX_RAD: f32 = std::f32::consts::FRAC_PI_4;
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum OpenXrPanelFacingMode {
41 Fixed,
43 FaceUserOnDrag,
45 FaceUserAlways,
47 FaceUserYawPitchOnDrag,
49}
50
51#[derive(Debug, Clone, Copy, PartialEq)]
52pub struct OpenXrPanelFacingOptions {
64 pub mode: OpenXrPanelFacingMode,
66 pub pitch_min_rad: f32,
68 pub pitch_max_rad: f32,
70}
71
72impl Default for OpenXrPanelFacingOptions {
73 fn default() -> Self {
74 Self {
75 mode: OpenXrPanelFacingMode::FaceUserOnDrag,
76 pitch_min_rad: DEFAULT_PANEL_PITCH_MIN_RAD,
77 pitch_max_rad: DEFAULT_PANEL_PITCH_MAX_RAD,
78 }
79 }
80}
81
82impl OpenXrPanelFacingOptions {
83 pub fn new(mode: OpenXrPanelFacingMode, pitch_min_rad: f32, pitch_max_rad: f32) -> Self {
93 let mut options = Self {
94 mode,
95 pitch_min_rad,
96 pitch_max_rad,
97 };
98 options.normalize_pitch_bounds();
99 options
100 }
101
102 pub fn normalize_pitch_bounds(&mut self) {
113 if !self.pitch_min_rad.is_finite() {
114 self.pitch_min_rad = DEFAULT_PANEL_PITCH_MIN_RAD;
115 }
116 if !self.pitch_max_rad.is_finite() {
117 self.pitch_max_rad = DEFAULT_PANEL_PITCH_MAX_RAD;
118 }
119 if self.pitch_min_rad > self.pitch_max_rad {
120 std::mem::swap(&mut self.pitch_min_rad, &mut self.pitch_max_rad);
121 }
122 }
123}
124
125#[derive(Debug, Clone, Copy)]
126pub struct OpenXrPanelGrabState {
140 pub local_grab_point_m: Vec3,
142 pub last_valid_head_pos: Option<Vec3>,
144 pub last_valid_rotation: xr::Quaternionf,
146 pub depth_axis: Option<Vec3>,
148 pub last_ray_origin: Option<Vec3>,
150}
151
152impl OpenXrPanelGrabState {
153 pub fn new(local_grab_point_m: Vec3, initial_rotation: xr::Quaternionf) -> Self {
164 Self {
165 local_grab_point_m,
166 last_valid_head_pos: None,
167 last_valid_rotation: initial_rotation,
168 depth_axis: None,
169 last_ray_origin: None,
170 }
171 }
172
173 pub fn with_pointer_depth(mut self, ray_origin: Vec3, ray_direction: Vec3) -> Self {
186 self.set_pointer_depth(ray_origin, ray_direction);
187 self
188 }
189
190 pub fn set_pointer_depth(&mut self, ray_origin: Vec3, ray_direction: Vec3) {
204 self.depth_axis = ray_direction.normalize();
205 self.last_ray_origin = self.depth_axis.map(|_| ray_origin);
206 }
207}
208
209#[derive(Clone, Copy)]
210pub struct OpenXrPanelGrabUpdate {
221 pub layer: OpenXrQuadLayerOptions,
223 pub hmd_pose_seen: bool,
225 pub yaw_applied: bool,
227 pub grab_point_stable: bool,
229 pub pitch_deg: f32,
231 pub pitch_clamped: bool,
233}
234
235#[derive(Debug, Clone, Copy, PartialEq)]
236pub struct PanelDepthUpdate {
247 pub adjusted_grab_world: Vec3,
249 pub depth_delta_m: f32,
251 pub depth_applied: bool,
253}
254
255pub fn vec3_from_xr(v: xr::Vector3f) -> Vec3 {
264 Vec3::new(v.x, v.y, v.z)
265}
266
267pub fn xr_vec3(v: Vec3) -> xr::Vector3f {
278 xr::Vector3f {
279 x: v.x,
280 y: v.y,
281 z: v.z,
282 }
283}
284
285pub fn rotate_vec3(q: xr::Quaternionf, v: Vec3) -> Vec3 {
297 let q_vec = Vec3::new(q.x, q.y, q.z);
298 let uv = q_vec.cross(v);
299 let uuv = q_vec.cross(uv);
300 v + (uv * q.w + uuv) * 2.0
301}
302
303pub fn panel_local_to_world(layer: OpenXrQuadLayerOptions, local: Vec3) -> Vec3 {
317 vec3_from_xr(layer.pose.position) + rotate_vec3(layer.pose.orientation, local)
318}
319
320pub fn logical_point_to_panel_local(
335 point: Point,
336 logical_size: Size,
337 layer_size: xr::Extent2Df,
338) -> Vec3 {
339 let w = logical_size.w.max(1.0);
340 let h = logical_size.h.max(1.0);
341 Vec3::new(
342 (point.x / w - 0.5) * layer_size.width,
343 (0.5 - point.y / h) * layer_size.height,
344 0.0,
345 )
346}
347
348pub fn face_user_yaw_only(
363 panel_pos: Vec3,
364 head_pos: Vec3,
365 fallback: xr::Quaternionf,
366) -> (xr::Quaternionf, bool) {
367 let mut dir = head_pos - panel_pos;
368 dir.y = 0.0;
369 if dir.len_sq() <= PANEL_FACING_EPSILON {
370 return (fallback, false);
371 }
372
373 let dir = dir.normalize_or(Vec3::new(0.0, 0.0, 1.0));
374 let yaw = dir.x.atan2(dir.z);
375 (yaw_quaternion(yaw), true)
376}
377
378pub fn face_user_yaw_pitch_clamped(
396 panel_pos: Vec3,
397 head_pos: Vec3,
398 fallback: xr::Quaternionf,
399 pitch_min_rad: f32,
400 pitch_max_rad: f32,
401) -> (xr::Quaternionf, bool, f32, bool) {
402 let dir = head_pos - panel_pos;
403 if dir.len_sq() <= PANEL_FACING_EPSILON {
404 return (fallback, false, 0.0, false);
405 }
406
407 let horizontal_len = (dir.x * dir.x + dir.z * dir.z).sqrt();
408 if horizontal_len <= PANEL_FACING_EPSILON {
409 return (fallback, false, 0.0, false);
410 }
411
412 let mut min = if pitch_min_rad.is_finite() {
413 pitch_min_rad
414 } else {
415 DEFAULT_PANEL_PITCH_MIN_RAD
416 };
417 let mut max = if pitch_max_rad.is_finite() {
418 pitch_max_rad
419 } else {
420 DEFAULT_PANEL_PITCH_MAX_RAD
421 };
422 if min > max {
423 std::mem::swap(&mut min, &mut max);
424 }
425
426 let unclamped_pitch = dir.y.atan2(horizontal_len);
427 let pitch = unclamped_pitch.clamp(min, max);
428 let pitch_clamped = (pitch - unclamped_pitch).abs() > 1e-5;
429 let yaw = dir.x.atan2(dir.z);
430
431 let cos_pitch = pitch.cos();
432 let forward = Vec3::new(yaw.sin() * cos_pitch, pitch.sin(), yaw.cos() * cos_pitch)
433 .normalize_or(Vec3::new(0.0, 0.0, 1.0));
434 let right = Vec3::new(0.0, 1.0, 0.0)
435 .cross(forward)
436 .normalize_or(Vec3::new(1.0, 0.0, 0.0));
437 let up = forward.cross(right).normalize_or(Vec3::new(0.0, 1.0, 0.0));
438
439 (
440 rotation_matrix_to_quaternion([
441 [right.x, up.x, forward.x],
442 [right.y, up.y, forward.y],
443 [right.z, up.z, forward.z],
444 ]),
445 true,
446 pitch.to_degrees(),
447 pitch_clamped,
448 )
449}
450
451pub fn apply_pointer_depth_delta(
466 grab_world: Vec3,
467 depth_axis: Option<Vec3>,
468 last_origin: Option<Vec3>,
469 current_origin: Option<Vec3>,
470) -> PanelDepthUpdate {
471 let Some(axis) = depth_axis.and_then(Vec3::normalize) else {
472 return PanelDepthUpdate {
473 adjusted_grab_world: grab_world,
474 depth_delta_m: 0.0,
475 depth_applied: false,
476 };
477 };
478 let (Some(last_origin), Some(current_origin)) = (last_origin, current_origin) else {
479 return PanelDepthUpdate {
480 adjusted_grab_world: grab_world,
481 depth_delta_m: 0.0,
482 depth_applied: false,
483 };
484 };
485
486 let depth_delta_m = (current_origin - last_origin).dot(axis);
487 PanelDepthUpdate {
488 adjusted_grab_world: grab_world + axis * depth_delta_m,
489 depth_delta_m,
490 depth_applied: depth_delta_m.abs() > 1e-6,
491 }
492}
493
494pub fn apply_yaw_only_stable_grab(
511 layer: OpenXrQuadLayerOptions,
512 grab: &mut OpenXrPanelGrabState,
513 grab_world: Vec3,
514 head_pos: Option<Vec3>,
515) -> OpenXrPanelGrabUpdate {
516 let Some(head_pos) = head_pos else {
517 return OpenXrPanelGrabUpdate {
518 layer,
519 hmd_pose_seen: false,
520 yaw_applied: false,
521 grab_point_stable: false,
522 pitch_deg: 0.0,
523 pitch_clamped: false,
524 };
525 };
526
527 grab.last_valid_head_pos = Some(head_pos);
528 let provisional_pos = grab_world - rotate_vec3(layer.pose.orientation, grab.local_grab_point_m);
529 let (rotation, yaw_applied) =
530 face_user_yaw_only(provisional_pos, head_pos, grab.last_valid_rotation);
531 if yaw_applied {
532 grab.last_valid_rotation = rotation;
533 }
534
535 let panel_pos = grab_world - rotate_vec3(rotation, grab.local_grab_point_m);
536 let mut next = layer;
537 next.pose.position = xr_vec3(panel_pos);
538 next.pose.orientation = rotation;
539 let stable_error = (panel_local_to_world(next, grab.local_grab_point_m) - grab_world).len();
540
541 OpenXrPanelGrabUpdate {
542 layer: next,
543 hmd_pose_seen: true,
544 yaw_applied,
545 grab_point_stable: stable_error <= 0.001,
546 pitch_deg: 0.0,
547 pitch_clamped: false,
548 }
549}
550
551pub fn apply_facing_stable_grab(
569 layer: OpenXrQuadLayerOptions,
570 grab: &mut OpenXrPanelGrabState,
571 grab_world: Vec3,
572 head_pos: Option<Vec3>,
573 facing: OpenXrPanelFacingOptions,
574) -> OpenXrPanelGrabUpdate {
575 let Some(head_pos) = head_pos else {
576 return OpenXrPanelGrabUpdate {
577 layer,
578 hmd_pose_seen: false,
579 yaw_applied: false,
580 grab_point_stable: false,
581 pitch_deg: 0.0,
582 pitch_clamped: false,
583 };
584 };
585
586 grab.last_valid_head_pos = Some(head_pos);
587 let provisional_pos = grab_world - rotate_vec3(layer.pose.orientation, grab.local_grab_point_m);
588 let (rotation, applied, pitch_deg, pitch_clamped) = match facing.mode {
589 OpenXrPanelFacingMode::FaceUserYawPitchOnDrag => face_user_yaw_pitch_clamped(
590 provisional_pos,
591 head_pos,
592 grab.last_valid_rotation,
593 facing.pitch_min_rad,
594 facing.pitch_max_rad,
595 ),
596 OpenXrPanelFacingMode::FaceUserOnDrag | OpenXrPanelFacingMode::FaceUserAlways => {
597 let (rotation, applied) =
598 face_user_yaw_only(provisional_pos, head_pos, grab.last_valid_rotation);
599 (rotation, applied, 0.0, false)
600 }
601 OpenXrPanelFacingMode::Fixed => (layer.pose.orientation, false, 0.0, false),
602 };
603
604 if applied {
605 grab.last_valid_rotation = rotation;
606 }
607
608 let panel_pos = grab_world - rotate_vec3(rotation, grab.local_grab_point_m);
609 let mut next = layer;
610 next.pose.position = xr_vec3(panel_pos);
611 next.pose.orientation = rotation;
612 let stable_error = (panel_local_to_world(next, grab.local_grab_point_m) - grab_world).len();
613
614 OpenXrPanelGrabUpdate {
615 layer: next,
616 hmd_pose_seen: true,
617 yaw_applied: applied,
618 grab_point_stable: stable_error <= 0.001,
619 pitch_deg,
620 pitch_clamped,
621 }
622}
623
624fn yaw_quaternion(yaw: f32) -> xr::Quaternionf {
626 let half = yaw * 0.5;
627 xr::Quaternionf {
628 x: 0.0,
629 y: half.sin(),
630 z: 0.0,
631 w: half.cos(),
632 }
633}
634
635fn rotation_matrix_to_quaternion(m: [[f32; 3]; 3]) -> xr::Quaternionf {
637 let trace = m[0][0] + m[1][1] + m[2][2];
638 if trace > 0.0 {
639 let s = (trace + 1.0).sqrt() * 2.0;
640 xr::Quaternionf {
641 w: 0.25 * s,
642 x: (m[2][1] - m[1][2]) / s,
643 y: (m[0][2] - m[2][0]) / s,
644 z: (m[1][0] - m[0][1]) / s,
645 }
646 } else if m[0][0] > m[1][1] && m[0][0] > m[2][2] {
647 let s = (1.0 + m[0][0] - m[1][1] - m[2][2]).sqrt() * 2.0;
648 xr::Quaternionf {
649 w: (m[2][1] - m[1][2]) / s,
650 x: 0.25 * s,
651 y: (m[0][1] + m[1][0]) / s,
652 z: (m[0][2] + m[2][0]) / s,
653 }
654 } else if m[1][1] > m[2][2] {
655 let s = (1.0 + m[1][1] - m[0][0] - m[2][2]).sqrt() * 2.0;
656 xr::Quaternionf {
657 w: (m[0][2] - m[2][0]) / s,
658 x: (m[0][1] + m[1][0]) / s,
659 y: 0.25 * s,
660 z: (m[1][2] + m[2][1]) / s,
661 }
662 } else {
663 let s = (1.0 + m[2][2] - m[0][0] - m[1][1]).sqrt() * 2.0;
664 xr::Quaternionf {
665 w: (m[1][0] - m[0][1]) / s,
666 x: (m[0][2] + m[2][0]) / s,
667 y: (m[1][2] + m[2][1]) / s,
668 z: 0.25 * s,
669 }
670 }
671}
672
673#[cfg(test)]
674mod tests {
676 use super::*;
677
678 fn identity() -> xr::Quaternionf {
680 xr::Quaternionf {
681 x: 0.0,
682 y: 0.0,
683 z: 0.0,
684 w: 1.0,
685 }
686 }
687
688 fn layer() -> OpenXrQuadLayerOptions {
690 OpenXrQuadLayerOptions {
691 pose: xr::Posef {
692 orientation: identity(),
693 position: xr::Vector3f {
694 x: 0.0,
695 y: 0.0,
696 z: -2.0,
697 },
698 },
699 size: xr::Extent2Df {
700 width: 1.6,
701 height: 0.9,
702 },
703 eye_visibility: xr::EyeVisibility::BOTH,
704 layer_flags: xr::CompositionLayerFlags::EMPTY,
705 }
706 }
707
708 fn approx(a: f32, b: f32) {
710 assert!(
711 (a - b).abs() < 1e-5,
712 "expected {a:?} to be approximately {b:?}"
713 );
714 }
715
716 fn approx_vec(a: Vec3, b: Vec3) {
718 approx(a.x, b.x);
719 approx(a.y, b.y);
720 approx(a.z, b.z);
721 }
722
723 fn facing_options(
725 mode: OpenXrPanelFacingMode,
726 min_deg: f32,
727 max_deg: f32,
728 ) -> OpenXrPanelFacingOptions {
729 OpenXrPanelFacingOptions::new(mode, min_deg.to_radians(), max_deg.to_radians())
730 }
731
732 #[test]
733 fn face_user_yaw_only_keeps_identity_when_head_is_in_front() {
734 let (rotation, applied) = face_user_yaw_only(
735 Vec3::new(0.0, 0.0, -2.0),
736 Vec3::new(0.0, 0.0, 0.0),
737 identity(),
738 );
739 assert!(applied);
740 approx(rotation.x, 0.0);
741 approx(rotation.y, 0.0);
742 approx(rotation.z, 0.0);
743 approx(rotation.w, 1.0);
744 }
745
746 #[test]
747 fn face_user_yaw_only_rotates_local_plus_z_toward_head_x() {
748 let (rotation, applied) = face_user_yaw_only(
749 Vec3::new(0.0, 0.0, -2.0),
750 Vec3::new(2.0, 0.0, -2.0),
751 identity(),
752 );
753 assert!(applied);
754 let forward = rotate_vec3(rotation, Vec3::new(0.0, 0.0, 1.0));
755 approx_vec(forward, Vec3::new(1.0, 0.0, 0.0));
756 }
757
758 #[test]
759 fn face_user_yaw_only_ignores_vertical_difference() {
760 let (low, _) = face_user_yaw_only(
761 Vec3::new(0.0, 0.0, -2.0),
762 Vec3::new(1.0, -10.0, 0.0),
763 identity(),
764 );
765 let (high, _) = face_user_yaw_only(
766 Vec3::new(0.0, 0.0, -2.0),
767 Vec3::new(1.0, 10.0, 0.0),
768 identity(),
769 );
770 approx(low.y, high.y);
771 approx(low.w, high.w);
772 }
773
774 #[test]
775 fn face_user_yaw_only_keeps_fallback_when_direction_is_too_short() {
776 let fallback = xr::Quaternionf {
777 x: 0.0,
778 y: 0.25,
779 z: 0.0,
780 w: 0.96,
781 };
782 let (rotation, applied) =
783 face_user_yaw_only(Vec3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 2.0, 0.0), fallback);
784 assert!(!applied);
785 assert_eq!(rotation, fallback);
786 }
787
788 #[test]
789 fn facing_options_default_use_yaw_only_and_forty_five_degree_pitch_clamp() {
790 let options = OpenXrPanelFacingOptions::default();
791 assert_eq!(options.mode, OpenXrPanelFacingMode::FaceUserOnDrag);
792 approx(options.pitch_min_rad.to_degrees(), -45.0);
793 approx(options.pitch_max_rad.to_degrees(), 45.0);
794 }
795
796 #[test]
797 fn facing_options_normalize_invalid_or_reversed_pitch_bounds() {
798 let options = OpenXrPanelFacingOptions::new(
799 OpenXrPanelFacingMode::FaceUserYawPitchOnDrag,
800 30.0_f32.to_radians(),
801 -10.0_f32.to_radians(),
802 );
803 approx(options.pitch_min_rad.to_degrees(), -10.0);
804 approx(options.pitch_max_rad.to_degrees(), 30.0);
805
806 let options = OpenXrPanelFacingOptions::new(
807 OpenXrPanelFacingMode::FaceUserYawPitchOnDrag,
808 f32::NAN,
809 f32::INFINITY,
810 );
811 approx(options.pitch_min_rad.to_degrees(), -45.0);
812 approx(options.pitch_max_rad.to_degrees(), 45.0);
813 }
814
815 #[test]
816 fn face_user_yaw_pitch_tracks_head_above_without_roll() {
817 let (rotation, applied, pitch_deg, clamped) = face_user_yaw_pitch_clamped(
818 Vec3::new(0.0, 0.0, -2.0),
819 Vec3::new(0.0, 1.0, 0.0),
820 identity(),
821 DEFAULT_PANEL_PITCH_MIN_RAD,
822 DEFAULT_PANEL_PITCH_MAX_RAD,
823 );
824 assert!(applied);
825 assert!(!clamped);
826 assert!(pitch_deg > 20.0);
827
828 let forward = rotate_vec3(rotation, Vec3::new(0.0, 0.0, 1.0));
829 assert!(forward.y > 0.4);
830 let right = rotate_vec3(rotation, Vec3::new(1.0, 0.0, 0.0));
831 approx(right.y, 0.0);
832 }
833
834 #[test]
835 fn face_user_yaw_pitch_clamps_to_default_limits() {
836 let (rotation, applied, pitch_deg, clamped) = face_user_yaw_pitch_clamped(
837 Vec3::new(0.0, 0.0, -2.0),
838 Vec3::new(0.0, 100.0, -1.9),
839 identity(),
840 DEFAULT_PANEL_PITCH_MIN_RAD,
841 DEFAULT_PANEL_PITCH_MAX_RAD,
842 );
843 assert!(applied);
844 assert!(clamped);
845 approx(pitch_deg, 45.0);
846 let forward = rotate_vec3(rotation, Vec3::new(0.0, 0.0, 1.0));
847 approx(forward.y, 45.0_f32.to_radians().sin());
848 }
849
850 #[test]
851 fn face_user_yaw_pitch_uses_custom_clamp_limits() {
852 let (_, applied, pitch_deg, clamped) = face_user_yaw_pitch_clamped(
853 Vec3::new(0.0, 0.0, -2.0),
854 Vec3::new(0.0, 100.0, -1.9),
855 identity(),
856 -15.0_f32.to_radians(),
857 20.0_f32.to_radians(),
858 );
859 assert!(applied);
860 assert!(clamped);
861 approx(pitch_deg, 20.0);
862 }
863
864 #[test]
865 fn logical_point_to_panel_local_maps_center_and_corners() {
866 let size = xr::Extent2Df {
867 width: 1.6,
868 height: 0.9,
869 };
870 approx_vec(
871 logical_point_to_panel_local(Point::new(512.0, 288.0), Size::new(1024.0, 576.0), size),
872 Vec3::new(0.0, 0.0, 0.0),
873 );
874 approx_vec(
875 logical_point_to_panel_local(Point::new(0.0, 0.0), Size::new(1024.0, 576.0), size),
876 Vec3::new(-0.8, 0.45, 0.0),
877 );
878 approx_vec(
879 logical_point_to_panel_local(Point::new(1024.0, 576.0), Size::new(1024.0, 576.0), size),
880 Vec3::new(0.8, -0.45, 0.0),
881 );
882 }
883
884 #[test]
885 fn pointer_depth_delta_moves_grab_world_forward_on_initial_ray_axis() {
886 let grab_world = Vec3::new(0.0, 0.0, -2.0);
887 let update = apply_pointer_depth_delta(
888 grab_world,
889 Some(Vec3::new(0.0, 0.0, -1.0)),
890 Some(Vec3::new(0.0, 0.0, 0.0)),
891 Some(Vec3::new(0.0, 0.0, -0.25)),
892 );
893 assert!(update.depth_applied);
894 approx(update.depth_delta_m, 0.25);
895 approx_vec(update.adjusted_grab_world, Vec3::new(0.0, 0.0, -2.25));
896 }
897
898 #[test]
899 fn pointer_depth_delta_moves_grab_world_backward_on_initial_ray_axis() {
900 let grab_world = Vec3::new(0.0, 0.0, -2.0);
901 let update = apply_pointer_depth_delta(
902 grab_world,
903 Some(Vec3::new(0.0, 0.0, -1.0)),
904 Some(Vec3::new(0.0, 0.0, 0.0)),
905 Some(Vec3::new(0.0, 0.0, 0.25)),
906 );
907 assert!(update.depth_applied);
908 approx(update.depth_delta_m, -0.25);
909 approx_vec(update.adjusted_grab_world, Vec3::new(0.0, 0.0, -1.75));
910 }
911
912 #[test]
913 fn pointer_depth_delta_ignores_perpendicular_origin_motion() {
914 let grab_world = Vec3::new(0.0, 0.0, -2.0);
915 let update = apply_pointer_depth_delta(
916 grab_world,
917 Some(Vec3::new(0.0, 0.0, -1.0)),
918 Some(Vec3::new(0.0, 0.0, 0.0)),
919 Some(Vec3::new(0.5, 0.25, 0.0)),
920 );
921 assert!(!update.depth_applied);
922 approx(update.depth_delta_m, 0.0);
923 approx_vec(update.adjusted_grab_world, grab_world);
924 }
925
926 #[test]
927 fn pointer_depth_delta_does_not_drift_when_origin_is_unchanged() {
928 let grab_world = Vec3::new(0.2, -0.1, -2.0);
929 let update = apply_pointer_depth_delta(
930 grab_world,
931 Some(Vec3::new(0.0, 0.0, -1.0)),
932 Some(Vec3::new(0.1, 0.2, 0.3)),
933 Some(Vec3::new(0.1, 0.2, 0.3)),
934 );
935 assert!(!update.depth_applied);
936 approx(update.depth_delta_m, 0.0);
937 approx_vec(update.adjusted_grab_world, grab_world);
938 }
939
940 #[test]
941 fn pointer_depth_delta_without_axis_or_origins_leaves_grab_world_unchanged() {
942 let grab_world = Vec3::new(0.2, -0.1, -2.0);
943 for update in [
944 apply_pointer_depth_delta(
945 grab_world,
946 None,
947 Some(Vec3::new(0.0, 0.0, 0.0)),
948 Some(Vec3::new(0.0, 0.0, -0.2)),
949 ),
950 apply_pointer_depth_delta(
951 grab_world,
952 Some(Vec3::new(0.0, 0.0, -1.0)),
953 None,
954 Some(Vec3::new(0.0, 0.0, -0.2)),
955 ),
956 apply_pointer_depth_delta(
957 grab_world,
958 Some(Vec3::new(0.0, 0.0, -1.0)),
959 Some(Vec3::new(0.0, 0.0, 0.0)),
960 None,
961 ),
962 ] {
963 assert!(!update.depth_applied);
964 approx(update.depth_delta_m, 0.0);
965 approx_vec(update.adjusted_grab_world, grab_world);
966 }
967 }
968
969 #[test]
970 fn stable_grab_keeps_non_center_point_under_world_hit() {
971 let mut grab = OpenXrPanelGrabState::new(Vec3::new(0.4, 0.2, 0.0), identity());
972 let grab_world = Vec3::new(0.8, 0.25, -1.6);
973 let update = apply_yaw_only_stable_grab(
974 layer(),
975 &mut grab,
976 grab_world,
977 Some(Vec3::new(1.4, 0.3, 0.0)),
978 );
979 assert!(update.hmd_pose_seen);
980 assert!(update.yaw_applied);
981 assert!(update.grab_point_stable);
982 approx_vec(
983 panel_local_to_world(update.layer, grab.local_grab_point_m),
984 grab_world,
985 );
986 }
987
988 #[test]
989 fn yaw_pitch_stable_grab_keeps_non_center_point_under_world_hit() {
990 let mut grab = OpenXrPanelGrabState::new(Vec3::new(0.4, 0.2, 0.0), identity());
991 let grab_world = Vec3::new(0.8, 0.25, -1.6);
992 let update = apply_facing_stable_grab(
993 layer(),
994 &mut grab,
995 grab_world,
996 Some(Vec3::new(1.4, 1.2, 0.0)),
997 facing_options(OpenXrPanelFacingMode::FaceUserYawPitchOnDrag, -45.0, 45.0),
998 );
999 assert!(update.hmd_pose_seen);
1000 assert!(update.yaw_applied);
1001 assert!(update.grab_point_stable);
1002 assert!(update.pitch_deg > 0.0);
1003 approx_vec(
1004 panel_local_to_world(update.layer, grab.local_grab_point_m),
1005 grab_world,
1006 );
1007 let right = rotate_vec3(update.layer.pose.orientation, Vec3::new(1.0, 0.0, 0.0));
1008 approx(right.y, 0.0);
1009 }
1010}