scena 1.1.0

A Rust-native scene-graph renderer with typed scene state, glTF assets, and explicit prepare/render lifecycles.
Documentation
//! Platform-neutral orbit, pan, fly, and focus controls.

use crate::diagnostics::LookupError;
use crate::scene::Vec3;
use crate::scene::{CameraKey, Scene, Transform};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum PointerButton {
    Primary,
    Secondary,
    Auxiliary,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum PointerEventKind {
    Pressed,
    Released,
    Moved,
    Wheel,
    Cancelled,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub struct PointerEvent {
    pub kind: PointerEventKind,
    pub position: (f32, f32),
    pub button: Option<PointerButton>,
    pub delta: (f32, f32),
    pub scroll_delta: f32,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum TouchEventKind {
    Started,
    Moved,
    Pinched,
    Ended,
    Cancelled,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TouchEvent {
    pub kind: TouchEventKind,
    pub position: (f32, f32),
    pub delta: (f32, f32),
    pub pinch_delta: f32,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum OrbitControlAction {
    None,
    BeginOrbit,
    Orbit,
    Pan,
    Zoom,
    End,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub struct OrbitControls {
    target: Vec3,
    distance: f32,
    yaw_radians: f32,
    pitch_radians: f32,
    damping_factor: f32,
    orbiting: bool,
    panning: bool,
}

impl OrbitControls {
    pub fn new(target: Vec3, distance: f32) -> Self {
        Self {
            target,
            distance: distance.max(MIN_DISTANCE),
            yaw_radians: 0.0,
            pitch_radians: 0.0,
            damping_factor: 0.0,
            orbiting: false,
            panning: false,
        }
    }

    pub fn focus(mut self, target: Vec3, distance: f32) -> Self {
        self.target = target;
        self.distance = distance.max(MIN_DISTANCE);
        self
    }

    pub fn with_damping(mut self, factor: f32) -> Self {
        self.damping_factor = if factor.is_finite() {
            factor.clamp(0.0, 1.0)
        } else {
            0.0
        };
        self
    }

    pub fn handle_pointer(&mut self, event: PointerEvent) -> OrbitControlAction {
        match event.kind {
            PointerEventKind::Pressed => match event.button {
                Some(PointerButton::Primary) => {
                    self.orbiting = true;
                    OrbitControlAction::BeginOrbit
                }
                Some(PointerButton::Secondary) => {
                    self.panning = true;
                    OrbitControlAction::Pan
                }
                Some(PointerButton::Auxiliary) | None => OrbitControlAction::None,
            },
            PointerEventKind::Moved if self.orbiting => {
                self.yaw_radians += event.delta.0 * ORBIT_RADIANS_PER_PIXEL;
                self.pitch_radians = (self.pitch_radians + event.delta.1 * ORBIT_RADIANS_PER_PIXEL)
                    .clamp(-MAX_PITCH_RADIANS, MAX_PITCH_RADIANS);
                OrbitControlAction::Orbit
            }
            PointerEventKind::Moved if self.panning => {
                self.target.x -= event.delta.0 * PAN_UNITS_PER_PIXEL * self.distance;
                self.target.y += event.delta.1 * PAN_UNITS_PER_PIXEL * self.distance;
                OrbitControlAction::Pan
            }
            PointerEventKind::Wheel => {
                let zoom = (1.0 + event.scroll_delta * ZOOM_SCALE).max(0.05);
                self.distance = (self.distance * zoom).max(MIN_DISTANCE);
                OrbitControlAction::Zoom
            }
            PointerEventKind::Released | PointerEventKind::Cancelled => {
                self.orbiting = false;
                self.panning = false;
                OrbitControlAction::End
            }
            PointerEventKind::Moved => OrbitControlAction::None,
        }
    }

    pub fn handle_touch(&mut self, event: TouchEvent) -> OrbitControlAction {
        match event.kind {
            TouchEventKind::Started => {
                self.orbiting = true;
                OrbitControlAction::BeginOrbit
            }
            TouchEventKind::Moved if self.orbiting => {
                self.apply_orbit_delta(event.delta);
                OrbitControlAction::Orbit
            }
            TouchEventKind::Pinched => {
                self.apply_zoom_delta(event.pinch_delta);
                OrbitControlAction::Zoom
            }
            TouchEventKind::Ended | TouchEventKind::Cancelled => {
                self.orbiting = false;
                self.panning = false;
                OrbitControlAction::End
            }
            TouchEventKind::Moved => OrbitControlAction::None,
        }
    }

    pub const fn target(&self) -> Vec3 {
        self.target
    }

    pub const fn distance(&self) -> f32 {
        self.distance
    }

    pub const fn yaw_radians(&self) -> f32 {
        self.yaw_radians
    }

    pub const fn pitch_radians(&self) -> f32 {
        self.pitch_radians
    }

    pub const fn damping_factor(&self) -> f32 {
        self.damping_factor
    }

    pub fn apply_to_scene(&self, scene: &mut Scene, camera: CameraKey) -> Result<(), LookupError> {
        let camera_node = scene
            .camera_node(camera)
            .ok_or(LookupError::CameraNotFound(camera))?;
        let offset = self.camera_offset();
        scene.align_to(
            camera_node,
            Transform::at(Vec3::new(
                self.target.x + offset.x,
                self.target.y + offset.y,
                self.target.z + offset.z,
            )),
        )?;
        scene.ensure_camera_depth_reaches(camera, self.distance)?;
        scene.look_at_point(camera, self.target)
    }

    fn camera_offset(&self) -> Vec3 {
        let pitch_cos = self.pitch_radians.cos();
        Vec3::new(
            self.distance * self.yaw_radians.sin() * pitch_cos,
            self.distance * self.pitch_radians.sin(),
            self.distance * self.yaw_radians.cos() * pitch_cos,
        )
    }

    fn apply_orbit_delta(&mut self, delta: (f32, f32)) {
        self.yaw_radians += delta.0 * ORBIT_RADIANS_PER_PIXEL;
        self.pitch_radians = (self.pitch_radians + delta.1 * ORBIT_RADIANS_PER_PIXEL)
            .clamp(-MAX_PITCH_RADIANS, MAX_PITCH_RADIANS);
    }

    fn apply_zoom_delta(&mut self, delta: f32) {
        let zoom = (1.0 + delta * ZOOM_SCALE).max(0.05);
        self.distance = (self.distance * zoom).max(MIN_DISTANCE);
    }
}

impl PointerEvent {
    pub const fn primary_pressed(x: f32, y: f32) -> Self {
        Self::pressed(x, y, PointerButton::Primary)
    }

    pub const fn secondary_pressed(x: f32, y: f32) -> Self {
        Self::pressed(x, y, PointerButton::Secondary)
    }

    pub const fn released(x: f32, y: f32) -> Self {
        Self {
            kind: PointerEventKind::Released,
            position: (x, y),
            button: None,
            delta: (0.0, 0.0),
            scroll_delta: 0.0,
        }
    }

    pub const fn moved(x: f32, y: f32, delta_x: f32, delta_y: f32) -> Self {
        Self {
            kind: PointerEventKind::Moved,
            position: (x, y),
            button: None,
            delta: (delta_x, delta_y),
            scroll_delta: 0.0,
        }
    }

    pub const fn wheel(x: f32, y: f32, scroll_delta: f32) -> Self {
        Self {
            kind: PointerEventKind::Wheel,
            position: (x, y),
            button: None,
            delta: (0.0, 0.0),
            scroll_delta,
        }
    }

    const fn pressed(x: f32, y: f32, button: PointerButton) -> Self {
        Self {
            kind: PointerEventKind::Pressed,
            position: (x, y),
            button: Some(button),
            delta: (0.0, 0.0),
            scroll_delta: 0.0,
        }
    }
}

impl TouchEvent {
    pub const fn start(x: f32, y: f32) -> Self {
        Self {
            kind: TouchEventKind::Started,
            position: (x, y),
            delta: (0.0, 0.0),
            pinch_delta: 0.0,
        }
    }

    pub const fn move_by(x: f32, y: f32, delta_x: f32, delta_y: f32) -> Self {
        Self {
            kind: TouchEventKind::Moved,
            position: (x, y),
            delta: (delta_x, delta_y),
            pinch_delta: 0.0,
        }
    }

    pub const fn pinch(x: f32, y: f32, pinch_delta: f32) -> Self {
        Self {
            kind: TouchEventKind::Pinched,
            position: (x, y),
            delta: (0.0, 0.0),
            pinch_delta,
        }
    }

    pub const fn end(x: f32, y: f32) -> Self {
        Self {
            kind: TouchEventKind::Ended,
            position: (x, y),
            delta: (0.0, 0.0),
            pinch_delta: 0.0,
        }
    }

    pub const fn cancel(x: f32, y: f32) -> Self {
        Self {
            kind: TouchEventKind::Cancelled,
            position: (x, y),
            delta: (0.0, 0.0),
            pinch_delta: 0.0,
        }
    }
}

const ORBIT_RADIANS_PER_PIXEL: f32 = 0.01;
const PAN_UNITS_PER_PIXEL: f32 = 0.001;
const ZOOM_SCALE: f32 = 0.1;
const MIN_DISTANCE: f32 = 0.001;
const MAX_PITCH_RADIANS: f32 = 1.553_343;