use crate::camera::camera::Camera;
use crate::interaction::gizmo::{GizmoAxis, project_drag_onto_axis};
pub fn angular_rotation_from_cursor(
cursor_viewport: Option<glam::Vec2>,
pointer_delta: glam::Vec2,
gizmo_center: glam::Vec3,
axis_world: glam::Vec3,
view_proj: glam::Mat4,
viewport_size: glam::Vec2,
camera_view: glam::Mat4,
) -> f32 {
const MIN_RADIUS: f32 = 10.0;
let cursor = match cursor_viewport {
Some(c) => c,
None => return 0.0,
};
let ndc = view_proj.project_point3(gizmo_center);
let center_screen = glam::Vec2::new(
(ndc.x + 1.0) * 0.5 * viewport_size.x,
(1.0 - ndc.y) * 0.5 * viewport_size.y,
);
let r_curr = cursor - center_screen;
let r_prev = r_curr - pointer_delta;
if r_curr.length() < MIN_RADIUS || r_prev.length() < MIN_RADIUS {
return 0.0;
}
let cross2d = r_prev.x * r_curr.y - r_prev.y * r_curr.x;
let dot = r_prev.dot(r_curr);
let screen_angle = cross2d.atan2(dot);
let axis_z_cam = (camera_view * axis_world.extend(0.0)).z;
if axis_z_cam >= 0.0 {
-screen_angle
} else {
screen_angle
}
}
pub fn constrained_translation(
pointer_delta: glam::Vec2,
axis: Option<GizmoAxis>,
exclude_axis: bool,
gizmo_center: glam::Vec3,
camera: &Camera,
viewport_size: glam::Vec2,
) -> glam::Vec3 {
let pan_scale = 2.0 * camera.distance * (camera.fov_y / 2.0).tan() / viewport_size.y.max(1.0);
let camera_right = camera.right();
let camera_up = camera.up();
let camera_view = camera.view_matrix();
let view_proj = camera.proj_matrix() * camera_view;
match axis {
None => {
camera_right * pointer_delta.x * pan_scale - camera_up * pointer_delta.y * pan_scale
}
Some(ax) => {
if exclude_axis {
let mut world_delta = camera_right * pointer_delta.x * pan_scale
- camera_up * pointer_delta.y * pan_scale;
match ax {
GizmoAxis::X => world_delta.x = 0.0,
GizmoAxis::Y => world_delta.y = 0.0,
GizmoAxis::Z | GizmoAxis::None => world_delta.z = 0.0,
_ => world_delta.z = 0.0,
}
world_delta
} else {
let axis_world = gizmo_axis_to_vec3(ax);
let amount = project_drag_onto_axis(
pointer_delta,
axis_world,
view_proj,
gizmo_center,
viewport_size,
);
axis_world * amount
}
}
}
}
pub fn constrained_scale(
pointer_delta: glam::Vec2,
axis: Option<GizmoAxis>,
exclude_axis: bool,
position: glam::Vec3,
view_proj: glam::Mat4,
viewport_size: glam::Vec2,
) -> glam::Vec3 {
const MIN_SCALE: f32 = 0.001;
let sensitivity = 8.0 / viewport_size.x.max(1.0);
match axis {
None => {
let factor = (1.0 + pointer_delta.x * sensitivity).max(MIN_SCALE);
glam::Vec3::splat(factor)
}
Some(ax) => {
if exclude_axis {
let factor = (1.0 + pointer_delta.x * sensitivity).max(MIN_SCALE);
let mut scale = glam::Vec3::ONE;
match ax {
GizmoAxis::X => {
scale.y = factor;
scale.z = factor;
}
GizmoAxis::Y => {
scale.x = factor;
scale.z = factor;
}
GizmoAxis::Z | GizmoAxis::None => {
scale.x = factor;
scale.y = factor;
}
_ => {
scale.x = factor;
scale.y = factor;
}
}
scale
} else {
let axis_world = gizmo_axis_to_vec3(ax);
let base_ndc = view_proj.project_point3(position);
let tip_ndc = view_proj.project_point3(position + axis_world);
let base_screen = glam::Vec2::new(
(base_ndc.x + 1.0) * 0.5 * viewport_size.x,
(1.0 - base_ndc.y) * 0.5 * viewport_size.y,
);
let tip_screen = glam::Vec2::new(
(tip_ndc.x + 1.0) * 0.5 * viewport_size.x,
(1.0 - tip_ndc.y) * 0.5 * viewport_size.y,
);
let axis_screen = tip_screen - base_screen;
let axis_screen_len = axis_screen.length();
let amount = if axis_screen_len > 1e-4 {
pointer_delta.dot(axis_screen / axis_screen_len) / viewport_size.x.max(1.0)
* 8.0
} else {
0.0
};
let factor = (1.0 + amount).max(MIN_SCALE);
let mut scale = glam::Vec3::ONE;
match ax {
GizmoAxis::X => scale.x = factor,
GizmoAxis::Y => scale.y = factor,
GizmoAxis::Z | GizmoAxis::None => scale.z = factor,
_ => scale.z = factor,
}
scale
}
}
}
}
pub(super) fn gizmo_axis_to_vec3(axis: GizmoAxis) -> glam::Vec3 {
match axis {
GizmoAxis::X => glam::Vec3::X,
GizmoAxis::Y => glam::Vec3::Y,
GizmoAxis::Z | GizmoAxis::None => glam::Vec3::Z,
_ => glam::Vec3::Z,
}
}
pub(super) fn excluded_axes(axis: GizmoAxis) -> (glam::Vec3, glam::Vec3) {
match axis {
GizmoAxis::X => (glam::Vec3::Y, glam::Vec3::Z),
GizmoAxis::Y => (glam::Vec3::X, glam::Vec3::Z),
GizmoAxis::Z | GizmoAxis::None => (glam::Vec3::X, glam::Vec3::Y),
_ => (glam::Vec3::X, glam::Vec3::Y),
}
}
pub(super) use crate::interaction::gizmo::project_drag_onto_rotation as drag_onto_rotation;
#[cfg(test)]
mod tests {
use super::*;
fn make_camera() -> Camera {
Camera::default()
}
#[test]
fn gizmo_axis_to_vec3_x() {
assert_eq!(gizmo_axis_to_vec3(GizmoAxis::X), glam::Vec3::X);
}
#[test]
fn gizmo_axis_to_vec3_y() {
assert_eq!(gizmo_axis_to_vec3(GizmoAxis::Y), glam::Vec3::Y);
}
#[test]
fn gizmo_axis_to_vec3_z_and_none_fallback_to_z() {
assert_eq!(gizmo_axis_to_vec3(GizmoAxis::Z), glam::Vec3::Z);
assert_eq!(gizmo_axis_to_vec3(GizmoAxis::None), glam::Vec3::Z);
}
#[test]
fn excluded_axes_x() {
let (a, b) = excluded_axes(GizmoAxis::X);
assert_eq!(a, glam::Vec3::Y);
assert_eq!(b, glam::Vec3::Z);
}
#[test]
fn excluded_axes_y() {
let (a, b) = excluded_axes(GizmoAxis::Y);
assert_eq!(a, glam::Vec3::X);
assert_eq!(b, glam::Vec3::Z);
}
#[test]
fn excluded_axes_z() {
let (a, b) = excluded_axes(GizmoAxis::Z);
assert_eq!(a, glam::Vec3::X);
assert_eq!(b, glam::Vec3::Y);
}
#[test]
fn angular_rotation_none_cursor_returns_zero() {
let angle = angular_rotation_from_cursor(
None,
glam::Vec2::new(10.0, 0.0),
glam::Vec3::ZERO,
glam::Vec3::Z,
glam::Mat4::IDENTITY,
glam::Vec2::new(800.0, 600.0),
glam::Mat4::IDENTITY,
);
assert_eq!(angle, 0.0);
}
#[test]
fn angular_rotation_near_center_returns_zero() {
let cam = make_camera();
let vp = cam.proj_matrix() * cam.view_matrix();
let ndc = vp.project_point3(glam::Vec3::ZERO);
let cx = (ndc.x + 1.0) * 0.5 * 800.0;
let cy = (1.0 - ndc.y) * 0.5 * 600.0;
let angle = angular_rotation_from_cursor(
Some(glam::Vec2::new(cx + 2.0, cy)), glam::Vec2::new(1.0, 0.0),
glam::Vec3::ZERO,
glam::Vec3::Z,
vp,
glam::Vec2::new(800.0, 600.0),
cam.view_matrix(),
);
assert_eq!(angle, 0.0);
}
#[test]
fn angular_rotation_zero_delta_returns_zero() {
let cam = make_camera();
let vp = cam.proj_matrix() * cam.view_matrix();
let ndc = vp.project_point3(glam::Vec3::ZERO);
let cx = (ndc.x + 1.0) * 0.5 * 800.0;
let cy = (1.0 - ndc.y) * 0.5 * 600.0;
let angle = angular_rotation_from_cursor(
Some(glam::Vec2::new(cx + 100.0, cy)),
glam::Vec2::ZERO, glam::Vec3::ZERO,
glam::Vec3::Z,
vp,
glam::Vec2::new(800.0, 600.0),
cam.view_matrix(),
);
assert!(angle.abs() < 1e-6);
}
#[test]
fn constrained_translation_zero_delta_is_zero() {
let cam = make_camera();
let result = constrained_translation(
glam::Vec2::ZERO,
None,
false,
glam::Vec3::ZERO,
&cam,
glam::Vec2::new(800.0, 600.0),
);
assert!(result.length() < 1e-6);
}
#[test]
fn constrained_translation_free_is_in_camera_plane() {
let cam = make_camera();
let delta = glam::Vec2::new(50.0, 0.0); let result = constrained_translation(
delta,
None,
false,
glam::Vec3::ZERO,
&cam,
glam::Vec2::new(800.0, 600.0),
);
let cam_right = cam.right().normalize();
let result_dir = result.normalize();
assert!(
cam_right.dot(result_dir) > 0.9,
"free translation should be along camera right"
);
}
#[test]
fn constrained_translation_x_axis_only_x() {
let cam = make_camera();
let result = constrained_translation(
glam::Vec2::new(50.0, 0.0),
Some(GizmoAxis::X),
false,
glam::Vec3::ZERO,
&cam,
glam::Vec2::new(800.0, 600.0),
);
assert!(result.y.abs() < 1e-5, "Y should be 0 when constrained to X");
assert!(result.z.abs() < 1e-5, "Z should be 0 when constrained to X");
}
#[test]
fn constrained_translation_exclude_x_zeros_x() {
let cam = make_camera();
let result = constrained_translation(
glam::Vec2::new(50.0, 30.0),
Some(GizmoAxis::X),
true,
glam::Vec3::ZERO,
&cam,
glam::Vec2::new(800.0, 600.0),
);
assert!(result.x.abs() < 1e-5, "X should be 0 when excluded");
}
#[test]
fn constrained_scale_zero_delta_is_one() {
let cam = make_camera();
let vp = cam.proj_matrix() * cam.view_matrix();
let result = constrained_scale(
glam::Vec2::ZERO,
None,
false,
glam::Vec3::ZERO,
vp,
glam::Vec2::new(800.0, 600.0),
);
assert!((result.x - 1.0).abs() < 1e-5);
assert!((result.y - 1.0).abs() < 1e-5);
assert!((result.z - 1.0).abs() < 1e-5);
}
#[test]
fn constrained_scale_uniform_positive_drag() {
let cam = make_camera();
let vp = cam.proj_matrix() * cam.view_matrix();
let result = constrained_scale(
glam::Vec2::new(100.0, 0.0), None,
false,
glam::Vec3::ZERO,
vp,
glam::Vec2::new(800.0, 600.0),
);
assert!(result.x > 1.0);
assert!((result.x - result.y).abs() < 1e-5);
assert!((result.x - result.z).abs() < 1e-5);
}
#[test]
fn constrained_scale_min_clamp() {
let cam = make_camera();
let vp = cam.proj_matrix() * cam.view_matrix();
let result = constrained_scale(
glam::Vec2::new(-10000.0, 0.0), None,
false,
glam::Vec3::ZERO,
vp,
glam::Vec2::new(800.0, 600.0),
);
assert!(result.x >= 0.001);
}
#[test]
fn constrained_scale_exclude_x_keeps_x_at_one() {
let cam = make_camera();
let vp = cam.proj_matrix() * cam.view_matrix();
let result = constrained_scale(
glam::Vec2::new(100.0, 0.0),
Some(GizmoAxis::X),
true,
glam::Vec3::ZERO,
vp,
glam::Vec2::new(800.0, 600.0),
);
assert!(
(result.x - 1.0).abs() < 1e-5,
"excluded X should stay at 1.0"
);
assert!(result.y > 1.0, "non-excluded Y should scale");
assert!(result.z > 1.0, "non-excluded Z should scale");
}
}