nightshade 0.14.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,
}

/// Mouse button + optional keyboard modifier bindings.
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct PanOrbitBindings {
    pub orbit: PanOrbitButton,
    pub pan: PanOrbitButton,
    pub orbit_modifier: Option<PanOrbitModifier>,
    pub pan_modifier: Option<PanOrbitModifier>,
}

impl Default for PanOrbitBindings {
    fn default() -> Self {
        Self {
            orbit: PanOrbitButton::Left,
            pan: PanOrbitButton::Right,
            orbit_modifier: None,
            pan_modifier: None,
        }
    }
}

/// Per-axis input scale factors.
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct PanOrbitSensitivity {
    pub orbit: f32,
    pub pan: f32,
    pub zoom: f32,
    pub gamepad_orbit: f32,
    pub gamepad_pan: f32,
    pub gamepad_zoom: f32,
}

impl Default for PanOrbitSensitivity {
    fn default() -> Self {
        Self {
            orbit: 1.0,
            pan: 1.0,
            zoom: 1.0,
            gamepad_orbit: 2.0,
            gamepad_pan: 10.0,
            gamepad_zoom: 5.0,
        }
    }
}

/// Per-axis interpolation smoothness (0 = instant, 1 = no change).
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct PanOrbitSmoothness {
    pub orbit: f32,
    pub pan: f32,
    pub zoom: f32,
    pub gamepad: f32,
}

impl Default for PanOrbitSmoothness {
    fn default() -> Self {
        Self {
            orbit: 0.1,
            pan: 0.02,
            zoom: 0.1,
            gamepad: 0.06,
        }
    }
}

/// Range and behavior limits.
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct PanOrbitLimits {
    pub pitch_lower: f32,
    pub pitch_upper: f32,
    pub zoom_lower: f32,
    pub zoom_upper: Option<f32>,
    pub gamepad_deadzone: f32,
    pub allow_upside_down: bool,
}

impl Default for PanOrbitLimits {
    fn default() -> Self {
        Self {
            pitch_lower: -(std::f32::consts::FRAC_PI_2 - 0.01),
            pitch_upper: std::f32::consts::FRAC_PI_2 - 0.01,
            zoom_lower: 0.05,
            zoom_upper: None,
            gamepad_deadzone: 0.15,
            allow_upside_down: false,
        }
    }
}

/// Arc-ball style camera controller for orbiting around a focus point.
///
/// Pose fields hold the current and target orbit state. Configuration is
/// grouped into [`PanOrbitBindings`], [`PanOrbitSensitivity`],
/// [`PanOrbitSmoothness`], and [`PanOrbitLimits`].
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct PanOrbitCamera {
    pub focus: Vec3,
    pub radius: f32,
    pub yaw: f32,
    pub pitch: f32,
    pub target_focus: Vec3,
    pub target_radius: f32,
    pub target_yaw: f32,
    pub target_pitch: f32,
    pub bindings: PanOrbitBindings,
    pub sensitivity: PanOrbitSensitivity,
    pub smoothness: PanOrbitSmoothness,
    pub limits: PanOrbitLimits,
    pub enabled: bool,
    /// Override distance used for pan-rate calculations. When `Some`, pan
    /// scales by this value rather than the current orbit radius, so
    /// panning stays responsive when zoomed in close. Apps typically set
    /// this to the loaded scene's bounding-sphere radius.
    pub pan_distance: Option<f32>,
    #[serde(skip)]
    pub smoothed_gamepad_orbit: Vec2,
    #[serde(skip)]
    pub smoothed_gamepad_pan: Vec2,
    #[serde(skip)]
    pub is_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,
            bindings: PanOrbitBindings::default(),
            sensitivity: PanOrbitSensitivity::default(),
            smoothness: PanOrbitSmoothness::default(),
            limits: PanOrbitLimits::default(),
            enabled: true,
            pan_distance: None,
            smoothed_gamepad_orbit: Vec2::zeros(),
            smoothed_gamepad_pan: Vec2::zeros(),
            is_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.limits.zoom_lower = min;
        self.limits.zoom_upper = max;
        self
    }

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

    /// Sets per-axis interpolation smoothness.
    pub fn with_smoothness(mut self, orbit: f32, pan: f32, zoom: f32) -> Self {
        self.smoothness.orbit = orbit;
        self.smoothness.pan = pan;
        self.smoothness.zoom = zoom;
        self
    }

    /// Sets the mouse buttons for orbit and pan.
    pub fn with_buttons(mut self, orbit: PanOrbitButton, pan: PanOrbitButton) -> Self {
        self.bindings.orbit = orbit;
        self.bindings.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.bindings.orbit_modifier = orbit;
        self.bindings.pan_modifier = pan;
        self
    }

    /// Allows the camera to go upside down (pitch beyond ±90°).
    pub fn with_upside_down(mut self, allow: bool) -> Self {
        self.limits.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.
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)
}