viewport_lib/interaction/
snap.rs1#[derive(Clone, Debug, Default)]
8pub struct SnapConfig {
9 pub translation: Option<f32>,
11 pub rotation: Option<f32>,
13 pub scale: Option<f32>,
15}
16
17pub fn snap_value(value: f32, increment: f32) -> f32 {
21 if increment <= 0.0 {
22 return value;
23 }
24 (value / increment).round() * increment
25}
26
27pub fn snap_vec3(v: glam::Vec3, increment: f32) -> glam::Vec3 {
29 glam::Vec3::new(
30 snap_value(v.x, increment),
31 snap_value(v.y, increment),
32 snap_value(v.z, increment),
33 )
34}
35
36pub fn snap_angle(angle_rad: f32, increment_rad: f32) -> f32 {
38 snap_value(angle_rad, increment_rad)
39}
40
41pub fn snap_scale(scale: f32, increment: f32) -> f32 {
45 snap_value(scale, increment)
46}
47
48#[derive(Debug, Clone)]
52#[non_exhaustive]
53pub enum ConstraintOverlay {
54 AxisLine {
56 origin: glam::Vec3,
58 direction: glam::Vec3,
60 color: [f32; 4],
62 },
63 Plane {
65 origin: glam::Vec3,
67 axis_a: glam::Vec3,
69 axis_b: glam::Vec3,
71 color: [f32; 4],
73 },
74}
75
76#[cfg(test)]
77mod tests {
78 use super::*;
79
80 #[test]
81 fn test_snap_value_rounds_to_nearest() {
82 assert!((snap_value(0.7, 0.5) - 0.5).abs() < 1e-6);
83 assert!((snap_value(0.8, 0.5) - 1.0).abs() < 1e-6);
84 assert!((snap_value(-0.3, 0.5) - -0.5).abs() < 1e-6);
85 assert!((snap_value(0.25, 0.5) - 0.5).abs() < 1e-6); }
87
88 #[test]
89 fn test_snap_vec3_per_component() {
90 let v = glam::Vec3::new(0.7, 1.3, -0.8);
91 let snapped = snap_vec3(v, 0.5);
92 assert!((snapped.x - 0.5).abs() < 1e-6);
93 assert!((snapped.y - 1.5).abs() < 1e-6);
94 assert!((snapped.z - -1.0).abs() < 1e-6);
95 }
96
97 #[test]
98 fn test_snap_angle_15_degrees() {
99 let deg15 = std::f32::consts::PI / 12.0;
100 let deg20 = 20.0_f32.to_radians();
101 let deg40 = 40.0_f32.to_radians();
102 let snapped_20 = snap_angle(deg20, deg15);
103 let snapped_40 = snap_angle(deg40, deg15);
104 assert!(
106 (snapped_20 - deg15).abs() < 1e-5,
107 "20° snapped to {}, expected {}",
108 snapped_20.to_degrees(),
109 15.0
110 );
111 let deg45 = 45.0_f32.to_radians();
113 assert!(
114 (snapped_40 - deg45).abs() < 1e-5,
115 "40° snapped to {}, expected {}",
116 snapped_40.to_degrees(),
117 45.0
118 );
119 }
120
121 #[test]
122 fn test_snap_scale_around_one() {
123 let snapped = snap_scale(1.37, 0.1);
124 assert!(
125 (snapped - 1.4).abs() < 1e-5,
126 "1.37 @ 0.1 -> {snapped}, expected 1.4"
127 );
128 }
129
130 #[test]
131 fn test_snap_config_none_passthrough() {
132 let config = SnapConfig::default();
133 let value = 1.234;
135 let result = config
136 .translation
137 .map(|inc| snap_value(value, inc))
138 .unwrap_or(value);
139 assert!((result - value).abs() < 1e-6, "None should pass through");
140 }
141}