nightshade 0.13.0

A cross-platform data-oriented game engine.
Documentation
use nalgebra_glm::{Vec2, Vec3};

/// Mouse button used for camera control.
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Default)]
pub enum PanOrbitButton {
    #[default]
    Left,
    Right,
    Middle,
}

/// Keyboard modifier key for camera control.
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum PanOrbitModifier {
    Shift,
    Control,
    Alt,
}

/// Arc-ball style camera controller for orbiting around a focus point.
///
/// Supports mouse, touch, and gamepad input with configurable sensitivity and smoothing.
/// The camera orbits around `focus` at `radius` distance with `yaw`/`pitch` angles.
///
/// Target values are interpolated towards for smooth motion. Each action (orbit, pan, zoom)
/// has its own smoothness parameter for fine-grained control.
///
/// # Default Controls (Blender-style)
///
/// **Mouse:**
/// - Middle mouse button: Orbit around focus
/// - Shift + Middle mouse button: Pan focus point
/// - Ctrl + Middle mouse button: Zoom (drag up/down)
/// - Scroll wheel: Zoom in/out
///
/// **Touch:**
/// - Single finger drag: Orbit
/// - Two finger drag: Pan
/// - Pinch: Zoom
///
/// **Gamepad:**
/// - Right stick: Orbit
/// - Left stick: Pan
/// - Triggers: Zoom
///
/// # Customization
///
/// Button bindings can be changed via [`button_orbit`](Self::button_orbit) and
/// [`button_pan`](Self::button_pan). Optional modifier keys can be required via
/// [`modifier_orbit`](Self::modifier_orbit) and [`modifier_pan`](Self::modifier_pan).
///
/// Set [`allow_upside_down`](Self::allow_upside_down) to `true` to allow the camera
/// pitch to exceed ±90°. When upside down, yaw direction is automatically reversed
/// for intuitive control.
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct PanOrbitCamera {
    /// Current focus point the camera orbits around.
    pub focus: Vec3,
    /// Current distance from focus point.
    pub radius: f32,
    /// Current horizontal rotation in radians.
    pub yaw: f32,
    /// Current vertical rotation in radians.
    pub pitch: f32,
    /// Target focus point (interpolated towards when smoothing enabled).
    pub target_focus: Vec3,
    /// Target orbit radius.
    pub target_radius: f32,
    /// Target yaw angle.
    pub target_yaw: f32,
    /// Target pitch angle.
    pub target_pitch: f32,
    /// Maximum pitch angle in radians (looking up).
    pub pitch_upper_limit: f32,
    /// Minimum pitch angle in radians (looking down).
    pub pitch_lower_limit: f32,
    /// Maximum zoom distance. If `None`, unlimited.
    pub zoom_upper_limit: Option<f32>,
    /// Minimum zoom distance.
    pub zoom_lower_limit: f32,
    /// Mouse button for orbiting. Defaults to Left.
    pub button_orbit: PanOrbitButton,
    /// Mouse button for panning. Defaults to Right.
    pub button_pan: PanOrbitButton,
    /// Optional modifier key required for orbit (e.g., Shift).
    pub modifier_orbit: Option<PanOrbitModifier>,
    /// Optional modifier key required for pan (e.g., Shift).
    pub modifier_pan: Option<PanOrbitModifier>,
    /// Mouse orbit speed multiplier.
    pub orbit_sensitivity: f32,
    /// Mouse pan speed multiplier.
    pub pan_sensitivity: f32,
    /// Mouse scroll zoom speed multiplier.
    pub zoom_sensitivity: f32,
    /// Orbit interpolation smoothness (0 = instant, 1 = no change).
    pub orbit_smoothness: f32,
    /// Pan interpolation smoothness (0 = instant, 1 = no change).
    pub pan_smoothness: f32,
    /// Zoom interpolation smoothness (0 = instant, 1 = no change).
    pub zoom_smoothness: f32,
    /// Gamepad stick orbit speed multiplier.
    pub gamepad_orbit_sensitivity: f32,
    /// Gamepad stick pan speed multiplier.
    pub gamepad_pan_sensitivity: f32,
    /// Gamepad trigger zoom speed multiplier.
    pub gamepad_zoom_sensitivity: f32,
    /// Gamepad stick deadzone threshold.
    pub gamepad_deadzone: f32,
    /// Gamepad input smoothing factor (0 = instant, 1 = no change).
    pub gamepad_smoothness: f32,
    /// Smoothed gamepad orbit input (internal state).
    pub smoothed_gamepad_orbit: Vec2,
    /// Smoothed gamepad pan input (internal state).
    pub smoothed_gamepad_pan: Vec2,
    /// Whether the camera controller processes input.
    pub enabled: bool,
    /// Whether the camera is currently upside down (pitch beyond ±90°).
    pub is_upside_down: bool,
    /// Allow the camera to go upside down (pitch beyond ±90°).
    pub allow_upside_down: bool,
}

impl Default for PanOrbitCamera {
    fn default() -> Self {
        Self {
            focus: Vec3::zeros(),
            radius: 10.0,
            yaw: 0.0,
            pitch: 0.0,
            target_focus: Vec3::zeros(),
            target_radius: 10.0,
            target_yaw: 0.0,
            target_pitch: 0.0,
            pitch_upper_limit: std::f32::consts::FRAC_PI_2 - 0.01,
            pitch_lower_limit: -(std::f32::consts::FRAC_PI_2 - 0.01),
            zoom_upper_limit: None,
            zoom_lower_limit: 0.05,
            button_orbit: PanOrbitButton::Middle,
            button_pan: PanOrbitButton::Middle,
            modifier_orbit: None,
            modifier_pan: Some(PanOrbitModifier::Shift),
            orbit_sensitivity: 1.0,
            pan_sensitivity: 1.0,
            zoom_sensitivity: 1.0,
            orbit_smoothness: 0.1,
            pan_smoothness: 0.02,
            zoom_smoothness: 0.1,
            gamepad_orbit_sensitivity: 2.0,
            gamepad_pan_sensitivity: 10.0,
            gamepad_zoom_sensitivity: 5.0,
            gamepad_deadzone: 0.15,
            gamepad_smoothness: 0.06,
            smoothed_gamepad_orbit: Vec2::zeros(),
            smoothed_gamepad_pan: Vec2::zeros(),
            enabled: true,
            is_upside_down: false,
            allow_upside_down: false,
        }
    }
}

impl PanOrbitCamera {
    /// Creates a new pan-orbit camera focused on a point at the given distance.
    pub fn new(focus: Vec3, radius: f32) -> Self {
        Self {
            focus,
            radius,
            target_focus: focus,
            target_radius: radius,
            ..Default::default()
        }
    }

    /// Sets initial yaw and pitch angles in radians.
    pub fn with_yaw_pitch(mut self, yaw: f32, pitch: f32) -> Self {
        self.yaw = yaw;
        self.pitch = pitch;
        self.target_yaw = yaw;
        self.target_pitch = pitch;
        self
    }

    /// Sets minimum and maximum zoom distances.
    pub fn with_zoom_limits(mut self, min: f32, max: Option<f32>) -> Self {
        self.zoom_lower_limit = min;
        self.zoom_upper_limit = max;
        self
    }

    /// Sets minimum and maximum pitch angles in radians.
    pub fn with_pitch_limits(mut self, min: f32, max: f32) -> Self {
        self.pitch_lower_limit = min;
        self.pitch_upper_limit = max;
        self
    }

    /// Sets the smoothing factors for camera motion interpolation.
    pub fn with_smoothness(mut self, orbit: f32, pan: f32, zoom: f32) -> Self {
        self.orbit_smoothness = orbit;
        self.pan_smoothness = pan;
        self.zoom_smoothness = zoom;
        self
    }

    /// Sets the mouse buttons for orbit and pan.
    pub fn with_buttons(mut self, orbit: PanOrbitButton, pan: PanOrbitButton) -> Self {
        self.button_orbit = orbit;
        self.button_pan = pan;
        self
    }

    /// Sets modifier keys required for orbit and pan.
    pub fn with_modifiers(
        mut self,
        orbit: Option<PanOrbitModifier>,
        pan: Option<PanOrbitModifier>,
    ) -> Self {
        self.modifier_orbit = orbit;
        self.modifier_pan = pan;
        self
    }

    /// Allows the camera to go upside down (pitch beyond ±90°).
    pub fn with_upside_down(mut self, allow: bool) -> Self {
        self.allow_upside_down = allow;
        self
    }

    /// Computes the current camera position and rotation from orbit parameters.
    pub fn compute_camera_transform(&self) -> (Vec3, nalgebra_glm::Quat) {
        compute_pan_orbit_transform(self.focus, self.yaw, self.pitch, self.radius)
    }

    /// Computes the target camera position and rotation from target parameters.
    pub fn compute_target_camera_transform(&self) -> (Vec3, nalgebra_glm::Quat) {
        compute_pan_orbit_transform(
            self.target_focus,
            self.target_yaw,
            self.target_pitch,
            self.target_radius,
        )
    }
}

/// Computes camera position and rotation for pan-orbit parameters.
///
/// Returns (position, rotation) where the camera is placed at `radius` distance
/// from `focus`, rotated by `yaw` (around Y) and `pitch` (around X).
pub fn compute_pan_orbit_transform(
    focus: Vec3,
    yaw: f32,
    pitch: f32,
    radius: f32,
) -> (Vec3, nalgebra_glm::Quat) {
    let yaw_quat = nalgebra_glm::quat_angle_axis(yaw, &Vec3::y());
    let pitch_quat = nalgebra_glm::quat_angle_axis(-pitch, &Vec3::x());
    let rotation = yaw_quat * pitch_quat;
    let position = focus + nalgebra_glm::quat_rotate_vec3(&rotation, &Vec3::new(0.0, 0.0, radius));
    (position, rotation)
}