#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
pub(crate) struct CameraPose {
pub position: [f32; 3],
pub yaw: f32,
pub pitch: f32,
}
pub(crate) const POINT_PAD: f32 = 0.5;
const MIN_RADIUS: f32 = 0.1;
const FIT_MARGIN: f32 = 1.05;
pub(crate) fn union_bounds(
boxes: impl IntoIterator<Item = ([f32; 3], [f32; 3])>,
) -> Option<([f32; 3], [f32; 3])> {
let mut out: Option<([f32; 3], [f32; 3])> = None;
for (mn, mx) in boxes {
let (omn, omx) = out.get_or_insert((mn, mx));
for a in 0..3 {
omn[a] = omn[a].min(mn[a]);
omx[a] = omx[a].max(mx[a]);
}
}
out
}
pub(crate) fn pad_point(p: [f32; 3]) -> ([f32; 3], [f32; 3]) {
(
[p[0] - POINT_PAD, p[1] - POINT_PAD, p[2] - POINT_PAD],
[p[0] + POINT_PAD, p[1] + POINT_PAD, p[2] + POINT_PAD],
)
}
pub(crate) fn bounding_sphere(mn: [f32; 3], mx: [f32; 3]) -> ([f32; 3], f32) {
let center = [
(mn[0] + mx[0]) * 0.5,
(mn[1] + mx[1]) * 0.5,
(mn[2] + mx[2]) * 0.5,
];
let half = [
(mx[0] - mn[0]) * 0.5,
(mx[1] - mn[1]) * 0.5,
(mx[2] - mn[2]) * 0.5,
];
let radius = (half[0] * half[0] + half[1] * half[1] + half[2] * half[2]).sqrt();
(center, radius)
}
pub(crate) fn fit_distance(radius: f32, fov_y_radians: f32, aspect: f32) -> f32 {
let radius = radius.max(MIN_RADIUS);
let half_v = (fov_y_radians * 0.5).clamp(0.01, std::f32::consts::FRAC_PI_2 - 0.01);
let half_h =
((half_v.tan() * aspect.max(0.1)).atan()).clamp(0.01, std::f32::consts::FRAC_PI_2 - 0.01);
let half = half_v.min(half_h);
radius * FIT_MARGIN / half.sin()
}
pub(crate) fn forward(yaw: f32, pitch: f32) -> [f32; 3] {
let cp = pitch.cos();
[-yaw.sin() * cp, pitch.sin(), -yaw.cos() * cp]
}
pub(crate) fn frame_pose(
current: &CameraPose,
center: [f32; 3],
radius: f32,
fov_y_radians: f32,
aspect: f32,
) -> CameraPose {
let d = fit_distance(radius, fov_y_radians, aspect);
let f = forward(current.yaw, current.pitch);
CameraPose {
position: [
center[0] - f[0] * d,
center[1] - f[1] * d,
center[2] - f[2] * d,
],
yaw: current.yaw,
pitch: current.pitch,
}
}
pub(crate) fn ease(t: f32) -> f32 {
let t = t.clamp(0.0, 1.0);
t * t * (3.0 - 2.0 * t)
}
fn shortest_arc(delta: f32) -> f32 {
use std::f32::consts::{PI, TAU};
(delta + PI).rem_euclid(TAU) - PI
}
pub(crate) fn lerp_pose(from: &CameraPose, to: &CameraPose, s: f32) -> CameraPose {
let lerp = |a: f32, b: f32| a + (b - a) * s;
CameraPose {
position: [
lerp(from.position[0], to.position[0]),
lerp(from.position[1], to.position[1]),
lerp(from.position[2], to.position[2]),
],
yaw: from.yaw + shortest_arc(to.yaw - from.yaw) * s,
pitch: lerp(from.pitch, to.pitch),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn union_bounds_covers_every_box() {
assert_eq!(union_bounds([]), None);
let u = union_bounds([
([-1.0, 0.0, 0.0], [1.0, 2.0, 1.0]),
([0.0, -3.0, 0.5], [0.5, 0.0, 4.0]),
])
.unwrap();
assert_eq!(u.0, [-1.0, -3.0, 0.0]);
assert_eq!(u.1, [1.0, 2.0, 4.0]);
}
#[test]
fn pad_point_is_symmetric() {
let (mn, mx) = pad_point([1.0, 2.0, 3.0]);
assert_eq!(mn, [1.0 - POINT_PAD, 2.0 - POINT_PAD, 3.0 - POINT_PAD]);
assert_eq!(mx, [1.0 + POINT_PAD, 2.0 + POINT_PAD, 3.0 + POINT_PAD]);
}
#[test]
fn fit_distance_scales_with_radius_and_never_degenerates() {
let fov = 60.0_f32.to_radians();
let d1 = fit_distance(1.0, fov, 16.0 / 9.0);
let d2 = fit_distance(2.0, fov, 16.0 / 9.0);
assert!((d2 - 2.0 * d1).abs() < 1e-4, "linear in radius");
let d0 = fit_distance(0.0, fov, 16.0 / 9.0);
assert!(d0.is_finite() && d0 > 0.0);
assert!((d0 - fit_distance(MIN_RADIUS, fov, 16.0 / 9.0)).abs() < 1e-6);
}
#[test]
fn fit_distance_respects_the_narrow_axis() {
let fov = 60.0_f32.to_radians();
assert!(fit_distance(1.0, fov, 0.5) > fit_distance(1.0, fov, 2.0));
}
#[test]
fn frame_pose_looks_at_the_center() {
let cur = CameraPose {
position: [10.0, 5.0, 3.0],
yaw: 0.7,
pitch: -0.3,
};
let center = [1.0, 2.0, 3.0];
let to = frame_pose(&cur, center, 2.0, 60.0_f32.to_radians(), 16.0 / 9.0);
let f = forward(to.yaw, to.pitch);
let d = fit_distance(2.0, 60.0_f32.to_radians(), 16.0 / 9.0);
for a in 0..3 {
assert!((to.position[a] + f[a] * d - center[a]).abs() < 1e-4);
}
assert_eq!((to.yaw, to.pitch), (cur.yaw, cur.pitch));
}
#[test]
fn ease_is_smooth_and_clamped() {
assert_eq!(ease(-1.0), 0.0);
assert_eq!(ease(0.0), 0.0);
assert_eq!(ease(1.0), 1.0);
assert_eq!(ease(2.0), 1.0);
assert!((ease(0.5) - 0.5).abs() < 1e-6);
assert!(ease(0.25) < 0.25, "eases in");
assert!(ease(0.75) > 0.75, "eases out");
}
#[test]
fn lerp_pose_takes_the_short_yaw_arc() {
use std::f32::consts::PI;
let from = CameraPose {
position: [0.0; 3],
yaw: PI - 0.1,
pitch: 0.0,
};
let to = CameraPose {
position: [0.0; 3],
yaw: -PI + 0.1,
pitch: 0.0,
};
let mid = lerp_pose(&from, &to, 0.5);
assert!(
(mid.yaw.abs() - PI).abs() < 1e-4,
"midpoint sits at the wrap seam: {}",
mid.yaw
);
let end = lerp_pose(&from, &to, 1.0);
assert!((shortest_arc(end.yaw - to.yaw)).abs() < 1e-4);
}
}