forked from 1b-agent/fieldcraft-upstream
The distance and the target follow from the eight corners' exact projections, for orthographic and perspective cameras and any direction of view, so a long beam seen side on fills the width instead of a sphere around it the height (#147). The free part is given as fractions of the view, so overlays can be left out (#76); a margin is kept on each side. Camera::fit is fit_in over the whole of a square view, and the headless renderer fits to the picture's aspect. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
156 lines
4.3 KiB
Rust
156 lines
4.3 KiB
Rust
//! A small 3D vector and bounding box for view math.
|
|
|
|
use std::ops::{Add, Mul, Neg, Sub};
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
|
|
pub struct Vec3 {
|
|
pub x: f64,
|
|
pub y: f64,
|
|
pub z: f64,
|
|
}
|
|
|
|
impl Vec3 {
|
|
pub const ZERO: Vec3 = Vec3::new(0.0, 0.0, 0.0);
|
|
pub const X: Vec3 = Vec3::new(1.0, 0.0, 0.0);
|
|
pub const Y: Vec3 = Vec3::new(0.0, 1.0, 0.0);
|
|
pub const Z: Vec3 = Vec3::new(0.0, 0.0, 1.0);
|
|
|
|
pub const fn new(x: f64, y: f64, z: f64) -> Vec3 {
|
|
Vec3 { x, y, z }
|
|
}
|
|
pub fn dot(self, o: Vec3) -> f64 {
|
|
self.x * o.x + self.y * o.y + self.z * o.z
|
|
}
|
|
pub fn cross(self, o: Vec3) -> Vec3 {
|
|
Vec3::new(self.y * o.z - self.z * o.y, self.z * o.x - self.x * o.z, self.x * o.y - self.y * o.x)
|
|
}
|
|
pub fn len(self) -> f64 {
|
|
self.dot(self).sqrt()
|
|
}
|
|
pub fn dist(self, o: Vec3) -> f64 {
|
|
(self - o).len()
|
|
}
|
|
/// The unit vector, or `None` for a zero or non-finite vector.
|
|
pub fn normalized(self) -> Option<Vec3> {
|
|
let l = self.len();
|
|
(l > 1e-300 && l.is_finite()).then(|| self * (1.0 / l))
|
|
}
|
|
pub fn is_finite(self) -> bool {
|
|
self.x.is_finite() && self.y.is_finite() && self.z.is_finite()
|
|
}
|
|
pub fn to_f32(self) -> [f32; 3] {
|
|
[self.x as f32, self.y as f32, self.z as f32]
|
|
}
|
|
}
|
|
|
|
impl From<[f64; 3]> for Vec3 {
|
|
fn from(a: [f64; 3]) -> Vec3 {
|
|
Vec3::new(a[0], a[1], a[2])
|
|
}
|
|
}
|
|
|
|
impl Add for Vec3 {
|
|
type Output = Vec3;
|
|
fn add(self, o: Vec3) -> Vec3 {
|
|
Vec3::new(self.x + o.x, self.y + o.y, self.z + o.z)
|
|
}
|
|
}
|
|
|
|
impl Sub for Vec3 {
|
|
type Output = Vec3;
|
|
fn sub(self, o: Vec3) -> Vec3 {
|
|
Vec3::new(self.x - o.x, self.y - o.y, self.z - o.z)
|
|
}
|
|
}
|
|
|
|
impl Mul<f64> for Vec3 {
|
|
type Output = Vec3;
|
|
fn mul(self, k: f64) -> Vec3 {
|
|
Vec3::new(self.x * k, self.y * k, self.z * k)
|
|
}
|
|
}
|
|
|
|
impl Neg for Vec3 {
|
|
type Output = Vec3;
|
|
fn neg(self) -> Vec3 {
|
|
Vec3::new(-self.x, -self.y, -self.z)
|
|
}
|
|
}
|
|
|
|
/// An axis-aligned bounding box; empty until a point is added.
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
pub struct Aabb3 {
|
|
pub min: Vec3,
|
|
pub max: Vec3,
|
|
}
|
|
|
|
impl Default for Aabb3 {
|
|
fn default() -> Self {
|
|
Aabb3::EMPTY
|
|
}
|
|
}
|
|
|
|
impl Aabb3 {
|
|
pub const EMPTY: Aabb3 = Aabb3 {
|
|
min: Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY),
|
|
max: Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY),
|
|
};
|
|
|
|
pub fn is_empty(&self) -> bool {
|
|
!(self.min.x <= self.max.x && self.min.y <= self.max.y && self.min.z <= self.max.z)
|
|
}
|
|
pub fn add(&mut self, p: Vec3) {
|
|
if !p.is_finite() {
|
|
return;
|
|
}
|
|
self.min = Vec3::new(self.min.x.min(p.x), self.min.y.min(p.y), self.min.z.min(p.z));
|
|
self.max = Vec3::new(self.max.x.max(p.x), self.max.y.max(p.y), self.max.z.max(p.z));
|
|
}
|
|
/// Grow to hold `o` too (an empty box adds nothing).
|
|
pub fn union(&mut self, o: Aabb3) {
|
|
if !o.is_empty() {
|
|
self.add(o.min);
|
|
self.add(o.max);
|
|
}
|
|
}
|
|
pub fn center(&self) -> Vec3 {
|
|
(self.min + self.max) * 0.5
|
|
}
|
|
pub fn diagonal(&self) -> f64 {
|
|
if self.is_empty() { 0.0 } else { self.max.dist(self.min) }
|
|
}
|
|
/// The eight corners (bit 0 picks x, bit 1 y, bit 2 z: min or max).
|
|
pub fn corners(&self) -> [Vec3; 8] {
|
|
std::array::from_fn(|k| {
|
|
Vec3::new(
|
|
if k & 1 == 0 { self.min.x } else { self.max.x },
|
|
if k & 2 == 0 { self.min.y } else { self.max.y },
|
|
if k & 4 == 0 { self.min.z } else { self.max.z },
|
|
)
|
|
})
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn vector_algebra_and_boxes() {
|
|
assert_eq!(Vec3::X.cross(Vec3::Y), Vec3::Z);
|
|
assert_eq!(Vec3::new(3.0, 4.0, 0.0).len(), 5.0);
|
|
assert!(Vec3::ZERO.normalized().is_none());
|
|
assert!(Vec3::new(f64::NAN, 0.0, 0.0).normalized().is_none());
|
|
let mut b = Aabb3::EMPTY;
|
|
assert!(b.is_empty() && b.diagonal() == 0.0);
|
|
b.add(Vec3::new(f64::NAN, 1.0, 1.0));
|
|
assert!(b.is_empty());
|
|
b.add(Vec3::ZERO);
|
|
b.add(Vec3::new(2.0, 4.0, 4.0));
|
|
assert_eq!(b.center(), Vec3::new(1.0, 2.0, 2.0));
|
|
assert_eq!(b.diagonal(), 6.0);
|
|
}
|
|
}
|