#[derive(Clone, Debug, Default)]
pub struct SnapConfig {
pub translation: Option<f32>,
pub rotation: Option<f32>,
pub scale: Option<f32>,
}
pub fn snap_value(value: f32, increment: f32) -> f32 {
if increment <= 0.0 {
return value;
}
(value / increment).round() * increment
}
pub fn snap_vec3(v: glam::Vec3, increment: f32) -> glam::Vec3 {
glam::Vec3::new(
snap_value(v.x, increment),
snap_value(v.y, increment),
snap_value(v.z, increment),
)
}
pub fn snap_angle(angle_rad: f32, increment_rad: f32) -> f32 {
snap_value(angle_rad, increment_rad)
}
pub fn snap_scale(scale: f32, increment: f32) -> f32 {
snap_value(scale, increment)
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum ConstraintOverlay {
AxisLine {
origin: glam::Vec3,
direction: glam::Vec3,
color: [f32; 4],
},
Plane {
origin: glam::Vec3,
axis_a: glam::Vec3,
axis_b: glam::Vec3,
color: [f32; 4],
},
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_snap_value_rounds_to_nearest() {
assert!((snap_value(0.7, 0.5) - 0.5).abs() < 1e-6);
assert!((snap_value(0.8, 0.5) - 1.0).abs() < 1e-6);
assert!((snap_value(-0.3, 0.5) - -0.5).abs() < 1e-6);
assert!((snap_value(0.25, 0.5) - 0.5).abs() < 1e-6); }
#[test]
fn test_snap_vec3_per_component() {
let v = glam::Vec3::new(0.7, 1.3, -0.8);
let snapped = snap_vec3(v, 0.5);
assert!((snapped.x - 0.5).abs() < 1e-6);
assert!((snapped.y - 1.5).abs() < 1e-6);
assert!((snapped.z - -1.0).abs() < 1e-6);
}
#[test]
fn test_snap_angle_15_degrees() {
let deg15 = std::f32::consts::PI / 12.0;
let deg20 = 20.0_f32.to_radians();
let deg40 = 40.0_f32.to_radians();
let snapped_20 = snap_angle(deg20, deg15);
let snapped_40 = snap_angle(deg40, deg15);
assert!(
(snapped_20 - deg15).abs() < 1e-5,
"20° snapped to {}, expected {}",
snapped_20.to_degrees(),
15.0
);
let deg45 = 45.0_f32.to_radians();
assert!(
(snapped_40 - deg45).abs() < 1e-5,
"40° snapped to {}, expected {}",
snapped_40.to_degrees(),
45.0
);
}
#[test]
fn test_snap_scale_around_one() {
let snapped = snap_scale(1.37, 0.1);
assert!(
(snapped - 1.4).abs() < 1e-5,
"1.37 @ 0.1 → {snapped}, expected 1.4"
);
}
#[test]
fn test_snap_config_none_passthrough() {
let config = SnapConfig::default();
let value = 1.234;
let result = config
.translation
.map(|inc| snap_value(value, inc))
.unwrap_or(value);
assert!((result - value).abs() < 1e-6, "None should pass through");
}
}