pub type Point = [f64; 3];
pub(crate) fn sub(a: Point, b: Point) -> Point {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
pub(crate) fn dot(a: Point, b: Point) -> f64 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
pub(crate) fn cross(a: Point, b: Point) -> Point {
[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 add(a: Point, b: Point) -> Point {
[a[0] + b[0], a[1] + b[1], a[2] + b[2]]
}
pub(crate) fn scale(a: Point, s: f64) -> Point {
a.map(|x| x * s)
}
pub(crate) fn length(a: Point) -> f64 {
dot(a, a).sqrt()
}
pub(crate) fn along(a: Point, b: Point, s: f64) -> Point {
[a[0] + s * (b[0] - a[0]), a[1] + s * (b[1] - a[1]), a[2] + s * (b[2] - a[2])]
}
pub(crate) fn has_area(t: [Point; 3]) -> bool {
let [a, b, c] = t;
cross(sub(b, a), sub(c, a)).iter().any(|x| *x != 0.0)
}
pub(crate) fn unit(n: Point) -> Option<Point> {
let largest = n.iter().fold(0.0_f64, |m, x| m.max(x.abs()));
if largest == 0.0 || !n.iter().all(|x| x.is_finite()) {
return None;
}
let m = n.map(|x| x / largest);
Some(scale(m, 1.0 / length(m)))
}
pub fn squared(a: Point, b: Point) -> f64 {
let d = sub(a, b);
dot(d, d)
}
pub fn on_segment(p: Point, a: Point, b: Point) -> Point {
along(a, b, fraction_on_segment(p, a, b))
}
pub(crate) fn fraction_on_segment(p: Point, a: Point, b: Point) -> f64 {
let ab = sub(b, a);
let length = dot(ab, ab);
if length <= 0.0 { 0.0 } else { (dot(sub(p, a), ab) / length).clamp(0.0, 1.0) }
}
pub(crate) fn centroid(t: [Point; 3]) -> Point {
let [a, b, c] = t;
[0, 1, 2].map(|k| (a[k] + b[k] + c[k]) / 3.0)
}
pub(crate) fn area(t: [Point; 3]) -> f64 {
let [a, b, c] = t;
let n = cross(sub(b, a), sub(c, a));
let largest = n.iter().fold(0.0_f64, |m, x| m.max(x.abs()));
if largest == 0.0 || !largest.is_finite() {
return 0.0;
}
largest * length(n.map(|x| x / largest)) / 2.0
}
pub fn on_triangle(p: Point, t: [Point; 3]) -> Point {
let [a, b, c] = t;
let unscaled = [sub(b, a), sub(c, a), sub(p, a)];
let [ab, ac, ap] = unscaled;
let lengths = [dot(ab, ab), dot(ac, ac)];
if lengths.iter().all(|l| UNSCALED.contains(l)) && dot(ap, ap) <= *UNSCALED.end() {
return in_regions(p, t, unscaled, lengths, 1.0);
}
let largest = unscaled.iter().flatten().fold(0.0_f64, |m, x| m.max(x.abs()));
if largest == 0.0 || !largest.is_finite() {
return a;
}
let [ab, ac, ap] = unscaled.map(|e| e.map(|x| x / largest));
in_regions(p, t, [ab, ac, ap], [dot(ab, ab), dot(ac, ac)], largest)
}
const UNSCALED: std::ops::RangeInclusive<f64> =
3.872_591_914_849_318e-121..=2.582_249_878_086_908_6e120;
#[inline(always)]
fn in_regions(
p: Point,
[a, b, c]: [Point; 3],
[ab, ac, ap]: [Point; 3],
[lab, lac]: [f64; 2],
unit: f64,
) -> Point {
let n = cross(ab, ac);
if !(dot(n, n) > 0.0 && dot(n, n) >= f64::EPSILON * lab * lac) {
return nearest_edge(p, a, [on_segment(p, a, b), on_segment(p, b, c), on_segment(p, c, a)]);
}
let (d1, d2) = (dot(ab, ap), dot(ac, ap));
if d1 <= 0.0 && d2 <= 0.0 {
return a;
}
let bp = sub(ap, ab);
let (d3, d4) = (dot(ab, bp), dot(ac, bp));
if d3 >= 0.0 && d4 <= d3 {
return b;
}
let vc = d1 * d4 - d3 * d2;
if vc <= 0.0 && d1 >= 0.0 && d3 <= 0.0 {
return along(a, b, d1 / (d1 - d3));
}
let cp = sub(ap, ac);
let (d5, d6) = (dot(ab, cp), dot(ac, cp));
if d6 >= 0.0 && d5 <= d6 {
return c;
}
let vb = d5 * d2 - d1 * d6;
if vb <= 0.0 && d2 >= 0.0 && d6 <= 0.0 {
return along(a, c, d2 / (d2 - d6));
}
let va = d3 * d6 - d5 * d4;
if va <= 0.0 && d4 - d3 >= 0.0 && d5 - d6 >= 0.0 {
return along(b, c, (d4 - d3) / ((d4 - d3) + (d5 - d6)));
}
sub(p, scale(n, unit * (dot(ap, n) / dot(n, n))))
}
fn nearest_edge(p: Point, first: Point, others: [Point; 3]) -> Point {
others
.into_iter()
.fold(first, |best, q| if squared(p, q) < squared(p, best) { q } else { best })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_vector_helpers_give_what_is_worked_by_hand() {
let (a, b) = ([1.0, -2.0, 3.0], [4.0, 0.5, -1.0]);
let bits = |p: Point| p.map(f64::to_bits);
assert_eq!(bits(add(a, b)), bits([5.0, -1.5, 2.0]));
assert_eq!(bits(sub(a, b)), bits([-3.0, -2.5, 4.0]));
assert_eq!(bits(scale(a, 2.0)), bits([2.0, -4.0, 6.0]));
assert_eq!(bits(cross([1.0, 0.0, 0.0], [0.0, 1.0, 0.0])), bits([0.0, 0.0, 1.0]));
assert_eq!(dot(a, [2.0, 1.0, -1.0]).to_bits(), (-3.0_f64).to_bits());
assert_eq!(length([3.0, 4.0, 12.0]).to_bits(), 13.0_f64.to_bits());
assert_eq!(squared(a, b).to_bits(), (9.0_f64 + 6.25 + 16.0).to_bits());
}
#[test]
fn a_normal_of_any_size_is_made_unit() {
for size in [1e-90, 1e-30, 1.0, 1e30, 1e80] {
let t = [[0.0; 3], [size, 0.0, 0.0], [0.0, 2.0 * size, 0.0]];
assert!(has_area(t), "{size}");
let [a, b, c] = t;
let n = unit(cross(sub(b, a), sub(c, a))).unwrap();
assert_eq!(n.map(f64::to_bits), [0.0, 0.0, 1.0].map(f64::to_bits), "{size}");
let tilted = unit([size, -size, 2.0 * size]).unwrap();
assert!((length(tilted) - 1.0).abs() <= 4.0 * f64::EPSILON, "{size}: {tilted:?}");
}
assert_eq!(unit([0.0; 3]), None);
assert_eq!(unit([f64::INFINITY, 0.0, 0.0]), None);
assert_eq!(unit([f64::NAN, 1.0, 0.0]), None);
assert!(!has_area([[1.0, 2.0, 3.0], [2.0, 4.0, 6.0], [3.0, 6.0, 9.0]]));
}
#[test]
fn an_area_is_worked_by_hand_at_any_size() {
for s in [1e-90, 1e-30, 1.0, 1e30, 1e80] {
let t = [[0.0; 3], [s, 0.0, 0.0], [0.0, 2.0 * s, 0.0]];
assert_eq!(area(t).to_bits(), (s * s).to_bits(), "{s}");
let tilted = area([[0.0; 3], [s, 0.0, 0.0], [0.0, s, s]]);
let half_root_two = std::f64::consts::SQRT_2 / 2.0;
assert!((tilted / (s * s) - half_root_two).abs() <= 4.0 * f64::EPSILON, "{s}");
}
assert_eq!(area([[1.0, 2.0, 3.0]; 3]).to_bits(), 0.0_f64.to_bits());
assert_eq!(area([[0.0; 3], [1.0, 1.0, 1.0], [2.0, 2.0, 2.0]]).to_bits(), 0.0_f64.to_bits());
}
#[test]
fn a_triangle_beyond_the_range_of_its_differences_gives_its_first_corner() {
let bits = |p: Point| p.map(f64::to_bits);
let wide = [[-1e308, 0.0, 0.0], [1e308, 0.0, 0.0], [0.0, 1.0, 0.0]];
assert_eq!(bits(on_triangle([0.0, 5.0, 0.0], wide)), bits(wide[0]));
let far = [[1e308, 0.0, 0.0], [1e308, 1.0, 0.0], [1e308, 0.0, 1.0]];
assert_eq!(bits(on_triangle([-1e308, 0.5, 0.5], far)), bits(far[0]));
}
}