forked from 1b-agent/fieldcraft-upstream
The platform maths libraries' atan2 and cbrt differ in the last bits between macOS and glibc, so the dihedral angles and the shape measure a mesh reports did too. The crate's own versions use + - * / and sqrt only (correctly rounded everywhere) and agree with the platform's to a few ulps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
120 lines
4.1 KiB
Rust
120 lines
4.1 KiB
Rust
//! Small 3-vector helpers on `[f64; 3]` (floating point: measures and display, never meshing
|
||
//! decisions, which use [`crate::predicates`]).
|
||
|
||
pub(crate) type P = [f64; 3];
|
||
|
||
pub(crate) fn sub(a: P, b: P) -> P {
|
||
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
|
||
}
|
||
pub(crate) fn dot(a: P, b: P) -> f64 {
|
||
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
|
||
}
|
||
pub(crate) fn cross(a: P, b: P) -> P {
|
||
[a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]
|
||
}
|
||
pub(crate) fn norm(a: P) -> f64 {
|
||
dot(a, a).sqrt()
|
||
}
|
||
pub(crate) fn det(a: P, b: P, c: P) -> f64 {
|
||
dot(a, cross(b, c))
|
||
}
|
||
|
||
/// The angle between two vectors in degrees (0 when either is zero).
|
||
pub(crate) fn angle_deg(a: P, b: P) -> f64 {
|
||
atan2(norm(cross(a, b)), dot(a, b)).to_degrees()
|
||
}
|
||
|
||
/// `atan(t)` for `0 ≤ t ≤ 1` from `+ − × ÷ √` only: two half-angle reductions
|
||
/// (`atan t = 2 atan(t / (1 + √(1 + t²)))`) bring `t` below tan(π/16) ≈ 0.199, where the Taylor
|
||
/// series converges to double precision in 20 terms.
|
||
fn atan_unit(t: f64) -> f64 {
|
||
let mut t = t;
|
||
for _ in 0..2 {
|
||
t /= 1.0 + (1.0 + t * t).sqrt();
|
||
}
|
||
let (t2, mut term, mut sum) = (t * t, t, 0.0);
|
||
for k in 0..20 {
|
||
let s = term / (2 * k + 1) as f64;
|
||
sum += if k % 2 == 0 { s } else { -s };
|
||
term *= t2;
|
||
}
|
||
4.0 * sum
|
||
}
|
||
|
||
/// The angle of `(x, y)` in radians, in `(−π, π]`, like [`f64::atan2`] but the same bit for bit
|
||
/// on every platform: the platform maths libraries' `atan2` differ in the last bits (macOS and
|
||
/// glibc), and a mesh or report must not (AGENTS.md, Determinism). 0 for `(0, 0)` and for
|
||
/// non-finite input.
|
||
pub(crate) fn atan2(y: f64, x: f64) -> f64 {
|
||
use std::f64::consts::{FRAC_PI_2, PI};
|
||
if !(x.is_finite() && y.is_finite()) || (x == 0.0 && y == 0.0) {
|
||
return 0.0;
|
||
}
|
||
let (ax, ay) = (x.abs(), y.abs());
|
||
let a = if ay <= ax { atan_unit(ay / ax) } else { FRAC_PI_2 - atan_unit(ax / ay) };
|
||
let a = if x < 0.0 { PI - a } else { a };
|
||
if y < 0.0 { -a } else { a }
|
||
}
|
||
|
||
/// The cube root, from `+ − × ÷` only (Newton's iteration from an exact power-of-two estimate),
|
||
/// the same on every platform (see [`atan2`]).
|
||
pub(crate) fn cbrt(x: f64) -> f64 {
|
||
if !x.is_finite() || x == 0.0 {
|
||
return x;
|
||
}
|
||
let a = x.abs();
|
||
// 2^(e/3) for a's binary exponent e: within a factor 2 of the root.
|
||
let e = ((a.to_bits() >> 52) & 0x7ff) as i64 - 1023;
|
||
let mut r = f64::from_bits((((e / 3) + 1023).clamp(1, 2046) as u64) << 52);
|
||
for _ in 0..60 {
|
||
let next = r - (r * r * r - a) / (3.0 * r * r);
|
||
if next == r || !next.is_finite() {
|
||
break;
|
||
}
|
||
r = next;
|
||
}
|
||
if x < 0.0 { -r } else { r }
|
||
}
|
||
pub(crate) fn add(a: P, b: P) -> P {
|
||
[a[0] + b[0], a[1] + b[1], a[2] + b[2]]
|
||
}
|
||
pub(crate) fn scale(a: P, s: f64) -> P {
|
||
[a[0] * s, a[1] * s, a[2] * s]
|
||
}
|
||
pub(crate) fn dist(a: P, b: P) -> f64 {
|
||
norm(sub(a, b))
|
||
}
|
||
pub(crate) fn lerp(a: P, b: P, t: f64) -> P {
|
||
[a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t]
|
||
}
|
||
pub(crate) fn mid(a: P, b: P) -> P {
|
||
[0.5 * (a[0] + b[0]), 0.5 * (a[1] + b[1]), 0.5 * (a[2] + b[2])]
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
/// The portable atan2 and cbrt agree with the platform's to a few ulps.
|
||
#[test]
|
||
fn portable_functions_match_the_platform() {
|
||
let mut worst: f64 = 0.0;
|
||
for i in 0..2000 {
|
||
let a = -std::f64::consts::PI + i as f64 * (2.0 * std::f64::consts::PI / 1999.0);
|
||
for r in [1e-300, 1e-3, 1.0, 7.5, 1e200] {
|
||
let (y, x) = (r * a.sin(), r * a.cos());
|
||
worst = worst.max((atan2(y, x) - y.atan2(x)).abs());
|
||
}
|
||
}
|
||
assert!(worst < 4e-15, "{worst}");
|
||
assert_eq!(atan2(0.0, 0.0), 0.0);
|
||
assert_eq!(atan2(1.0, 0.0), std::f64::consts::FRAC_PI_2);
|
||
assert_eq!(atan2(0.0, -1.0), std::f64::consts::PI);
|
||
for x in [1e-300, 2.5e-7, 0.3, 1.0, 8.0, 27.0, 1234.5, 1e300, -64.0] {
|
||
let rel: f64 = (cbrt(x) - x.cbrt()).abs() / x.cbrt().abs();
|
||
assert!(rel < 4e-16, "{x}: {} vs {}", cbrt(x), x.cbrt());
|
||
}
|
||
assert_eq!(cbrt(0.0), 0.0);
|
||
}
|
||
}
|