use crate::view::{add3, cross3, dot3, len3, norm3, rotate3, scale3, sub3, Projection, ViewCamera};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Gesture {
None,
Rotate,
Pan,
}
pub const BUTTON_LEFT: i32 = 0;
pub const BUTTON_MIDDLE: i32 = 1;
pub const BUTTON_RIGHT: i32 = 2;
const ZOOM_PER_NOTCH: f64 = 1.08;
const WHEEL_POINTS_PER_NOTCH: f64 = 70.0;
const MAX_WHEEL_NOTCHES_PER_EVENT: f64 = 3.0;
const MIN_ORTHO_HALF_HEIGHT: f64 = 1e-9;
const MIN_PERSP_DISTANCE: f64 = 1e-6;
#[derive(Debug, Default)]
pub struct ArcballControls {
pub enabled: bool,
gesture: GestureState,
}
#[derive(Debug)]
struct GestureState {
kind: Gesture,
last: (f64, f64),
}
impl Default for GestureState {
fn default() -> Self {
Self {
kind: Gesture::None,
last: (0.0, 0.0),
}
}
}
impl ArcballControls {
pub fn new() -> Self {
Self {
enabled: true,
gesture: GestureState::default(),
}
}
pub fn active_gesture(&self) -> Gesture {
self.gesture.kind
}
pub fn pointer_down(&mut self, x: f64, y: f64, button: i32) -> bool {
if !self.enabled {
return false;
}
self.gesture.kind = match button {
BUTTON_LEFT => Gesture::Rotate,
BUTTON_MIDDLE | BUTTON_RIGHT => Gesture::Pan,
_ => Gesture::None,
};
self.gesture.last = (x, y);
self.gesture.kind != Gesture::None
}
pub fn pointer_move(&mut self, camera: &mut ViewCamera, x: f64, y: f64) -> bool {
if !self.enabled || self.gesture.kind == Gesture::None {
return false;
}
let (lx, ly) = self.gesture.last;
if (x - lx).abs() < f64::EPSILON && (y - ly).abs() < f64::EPSILON {
return false;
}
match self.gesture.kind {
Gesture::Rotate => rotate_arcball(camera, (lx, ly), (x, y)),
Gesture::Pan => pan(camera, x - lx, y - ly),
Gesture::None => {}
}
self.gesture.last = (x, y);
true
}
pub fn pointer_up(&mut self) -> bool {
let was = self.gesture.kind != Gesture::None;
self.gesture.kind = Gesture::None;
was
}
pub fn wheel(&mut self, camera: &mut ViewCamera, delta_y: f64, cursor: Option<[f64; 2]>) -> bool {
if !self.enabled || delta_y == 0.0 {
return false;
}
let notches = (delta_y / WHEEL_POINTS_PER_NOTCH)
.clamp(-MAX_WHEEL_NOTCHES_PER_EVENT, MAX_WHEEL_NOTCHES_PER_EVENT);
let factor = ZOOM_PER_NOTCH.powf(notches);
match cursor {
Some([cx, cy]) => zoom_toward(camera, factor, cx, cy),
None => zoom(camera, factor),
}
true
}
}
pub fn zoom_toward(camera: &mut ViewCamera, factor: f64, cx: f64, cy: f64) {
let wpp = camera.world_per_pixel();
let (right, up, _) = camera.basis();
let sx = cx - camera.width * 0.5;
let sy = -(cy - camera.height * 0.5); let off = add3(scale3(right, sx * wpp), scale3(up, sy * wpp));
match &mut camera.projection {
Projection::Orthographic { half_height } => {
let old = *half_height;
*half_height = (old * factor).max(MIN_ORTHO_HALF_HEIGHT);
let f = *half_height / old; let shift = scale3(off, 1.0 - f);
camera.target = add3(camera.target, shift);
camera.eye = add3(camera.eye, shift);
}
Projection::Perspective { .. } => {
let cursor_world = add3(camera.target, off);
let nt = add3(cursor_world, scale3(sub3(camera.target, cursor_world), factor));
let ne = add3(cursor_world, scale3(sub3(camera.eye, cursor_world), factor));
let dir = sub3(ne, nt);
let dist = len3(dir).max(MIN_PERSP_DISTANCE);
camera.target = nt;
camera.eye = add3(nt, scale3(norm3(dir), dist));
}
}
}
pub fn zoom(camera: &mut ViewCamera, factor: f64) {
match &mut camera.projection {
Projection::Orthographic { half_height } => {
*half_height = (*half_height * factor).max(MIN_ORTHO_HALF_HEIGHT);
}
Projection::Perspective { .. } => {
let dir = sub3(camera.eye, camera.target);
let dist = (len3(dir) * factor).max(MIN_PERSP_DISTANCE);
camera.eye = add3(camera.target, scale3(norm3(dir), dist));
}
}
}
pub fn pan(camera: &mut ViewCamera, dx: f64, dy: f64) {
let (right, up, _) = camera.basis();
let wpp = camera.world_per_pixel();
let offset = add3(scale3(right, -dx * wpp), scale3(up, dy * wpp));
camera.eye = add3(camera.eye, offset);
camera.target = add3(camera.target, offset);
}
fn trackball_point(camera: &ViewCamera, x: f64, y: f64) -> [f64; 3] {
let radius = 0.5 * camera.width.min(camera.height).max(1.0) * 0.75;
let cx = camera.width * 0.5;
let cy = camera.height * 0.5;
let px = x - cx;
let py = cy - y; let r2 = radius * radius;
let d2 = px * px + py * py;
let pz = if d2 <= r2 * 0.5 {
(r2 - d2).sqrt()
} else {
r2 * 0.5 / d2.sqrt()
};
norm3([px, py, pz])
}
fn rotate_arcball(camera: &mut ViewCamera, from: (f64, f64), to: (f64, f64)) {
let v0 = trackball_point(camera, from.0, from.1);
let v1 = trackball_point(camera, to.0, to.1);
let axis_cam = cross3(v0, v1);
let axis_len = len3(axis_cam);
if axis_len < 1e-12 {
return;
}
let angle = dot3(v0, v1).clamp(-1.0, 1.0).acos();
if angle.abs() < 1e-12 {
return;
}
let (right, up, forward) = camera.basis();
let axis_cam = scale3(axis_cam, 1.0 / axis_len);
let axis_world = norm3(add3(
add3(scale3(right, axis_cam[0]), scale3(up, axis_cam[1])),
scale3(forward, -axis_cam[2]),
));
let offset = sub3(camera.eye, camera.target);
camera.eye = add3(camera.target, rotate3(offset, axis_world, -angle));
camera.up = norm3(rotate3(camera.up, axis_world, -angle));
}