use std::f32::consts::TAU;
mod camera_kit;
mod url_state;
pub use camera_kit::{FlyControls, FollowControls};
pub use url_state::{CameraOrbitUrlState, CameraOrbitUrlStateError};
use crate::diagnostics::LookupError;
use crate::scene::FramingOutcome;
use crate::scene::Vec3;
use crate::scene::{CameraKey, Scene, Transform};
const CINEMATIC_DAMPING: f32 = 0.18;
const PRESENTATION_DAMPING: f32 = 0.12;
const SNAPPY_DAMPING: f32 = 0.04;
const PRESENTATION_RPM: f32 = 1.0;
#[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,
auto_rotate_rpm: f32,
min_distance: f32,
max_distance: 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,
auto_rotate_rpm: 0.0,
min_distance: MIN_DISTANCE,
max_distance: f32::INFINITY,
orbiting: false,
panning: false,
}
}
pub fn focus(mut self, target: Vec3, distance: f32) -> Self {
self.target = target;
self.distance = self.clamp_distance(distance.max(MIN_DISTANCE));
self
}
pub fn from_framing(framing: FramingOutcome) -> Self {
Self::new(framing.target, framing.distance).focus_on_framing(framing)
}
pub fn focus_on_framing(mut self, framing: FramingOutcome) -> Self {
self.target = framing.target;
self.distance = self.clamp_distance(framing.distance.max(MIN_DISTANCE));
self.yaw_radians = framing.yaw_radians;
self.pitch_radians = framing
.pitch_radians
.clamp(-MAX_PITCH_RADIANS, MAX_PITCH_RADIANS);
self
}
pub fn zoom_limits_bounds_relative(self, min_factor: f32, max_factor: f32) -> Self {
let base_distance = self.distance.max(MIN_DISTANCE);
self.with_distance_limits(base_distance * min_factor, base_distance * max_factor)
}
pub fn with_distance_limits(mut self, min_distance: f32, max_distance: f32) -> Self {
let min_distance = sanitize_distance_limit(min_distance, MIN_DISTANCE);
let max_distance = sanitize_distance_limit(max_distance, f32::INFINITY);
let (min_distance, max_distance) = if min_distance <= max_distance {
(min_distance, max_distance)
} else {
(max_distance, min_distance)
};
self.min_distance = min_distance.max(MIN_DISTANCE);
self.max_distance = max_distance.max(self.min_distance);
self.distance = self.clamp_distance(self.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 cinematic(self) -> Self {
self.with_damping(CINEMATIC_DAMPING)
}
pub fn snappy(self) -> Self {
self.with_damping(SNAPPY_DAMPING)
}
pub fn presentation(self) -> Self {
self.with_damping(PRESENTATION_DAMPING)
.turntable(PRESENTATION_RPM)
}
pub fn turntable(mut self, rpm: f32) -> Self {
self.auto_rotate_rpm = if rpm.is_finite() {
rpm.clamp(-120.0, 120.0)
} else {
0.0
};
self
}
pub fn advance(&mut self, delta_seconds: f32) -> OrbitControlAction {
if !delta_seconds.is_finite()
|| delta_seconds <= 0.0
|| self.auto_rotate_rpm == 0.0
|| self.orbiting
|| self.panning
{
return OrbitControlAction::None;
}
self.yaw_radians += self.auto_rotate_radians_per_second() * delta_seconds;
OrbitControlAction::Orbit
}
pub fn with_angles(mut self, yaw_radians: f32, pitch_radians: f32) -> Self {
if yaw_radians.is_finite() {
self.yaw_radians = yaw_radians;
}
if pitch_radians.is_finite() {
self.pitch_radians = pitch_radians.clamp(-MAX_PITCH_RADIANS, MAX_PITCH_RADIANS);
}
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.clamp_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 min_distance(&self) -> f32 {
self.min_distance
}
pub const fn max_distance(&self) -> f32 {
self.max_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 const fn auto_rotate_rpm(&self) -> f32 {
self.auto_rotate_rpm
}
pub fn auto_rotate_radians_per_second(&self) -> f32 {
self.auto_rotate_rpm * TAU / 60.0
}
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.clamp_distance((self.distance * zoom).max(MIN_DISTANCE));
}
fn clamp_distance(&self, distance: f32) -> f32 {
distance.clamp(self.min_distance, self.max_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;
fn sanitize_distance_limit(value: f32, fallback: f32) -> f32 {
if value.is_finite() && value > 0.0 {
value
} else {
fallback
}
}
fn sanitize_finite(value: f32, fallback: f32) -> f32 {
if value.is_finite() { value } else { fallback }
}
fn sanitize_vec3(value: Vec3, fallback: Vec3) -> Vec3 {
if value.is_finite() { value } else { fallback }
}