Skip to main content

concinnity_core/math/
vec3.rs

1//! The crate's shared 3-component vector math. Every module that needs a dot,
2//! cross, or component-wise op reaches for these rather than redeclaring them.
3//!
4//! Normalisation deliberately stays with its caller: the degenerate-input rule
5//! differs by site (a fallback axis vs `None` vs a clamped length), and folding
6//! those together would change behavior at grazing inputs.
7
8use crate::math::sqrt;
9
10/// Dot product.
11pub fn dot(a: [f32; 3], b: [f32; 3]) -> f32 {
12    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
13}
14
15/// Cross product.
16pub fn cross(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
17    [
18        a[1] * b[2] - a[2] * b[1],
19        a[2] * b[0] - a[0] * b[2],
20        a[0] * b[1] - a[1] * b[0],
21    ]
22}
23
24/// Component-wise difference, `a - b`.
25pub fn sub(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
26    [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
27}
28
29/// Component-wise sum, `a + b`.
30pub fn add(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
31    [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
32}
33
34/// Every component scaled by `s`.
35pub fn scale(v: [f32; 3], s: f32) -> [f32; 3] {
36    [v[0] * s, v[1] * s, v[2] * s]
37}
38
39/// Euclidean length.
40pub fn length(v: [f32; 3]) -> f32 {
41    sqrt(dot(v, v))
42}
43
44/// Component-wise linear interpolation from `a` to `b`.
45pub fn lerp(a: [f32; 3], b: [f32; 3], t: f32) -> [f32; 3] {
46    [
47        a[0] + (b[0] - a[0]) * t,
48        a[1] + (b[1] - a[1]) * t,
49        a[2] + (b[2] - a[2]) * t,
50    ]
51}
52
53/// Accumulate `src` into `dst` in place. Used by the smooth-normal passes, which
54/// sum every incident face normal per vertex before normalising once.
55pub fn vec3_add(dst: &mut [f32; 3], src: [f32; 3]) {
56    dst[0] += src[0];
57    dst[1] += src[1];
58    dst[2] += src[2];
59}
60
61/// Unit-length `n`, falling back to `+Y` when it is too short to have a
62/// direction.
63pub fn vec3_normalise(n: [f32; 3]) -> [f32; 3] {
64    let len = length(n);
65    if len < 1e-6 {
66        [0.0, 1.0, 0.0]
67    } else {
68        scale(n, 1.0 / len)
69    }
70}
71
72/// Newell-style face normal from three CCW positions. Shared with the cook
73/// generators' smooth-normal pass.
74pub fn vec3_face_normal(a: [f32; 3], b: [f32; 3], c: [f32; 3]) -> [f32; 3] {
75    vec3_normalise(cross(sub(b, a), sub(c, a)))
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    #[test]
83    fn cross_is_right_handed() {
84        assert_eq!(cross([1.0, 0.0, 0.0], [0.0, 1.0, 0.0]), [0.0, 0.0, 1.0]);
85        assert_eq!(cross([0.0, 1.0, 0.0], [0.0, 0.0, 1.0]), [1.0, 0.0, 0.0]);
86    }
87
88    #[test]
89    fn dot_and_length_agree() {
90        let v = [3.0, 4.0, 0.0];
91        assert_eq!(dot(v, v), 25.0);
92        assert_eq!(length(v), 5.0);
93    }
94
95    #[test]
96    fn component_ops_are_component_wise() {
97        assert_eq!(add([1.0, 2.0, 3.0], [4.0, 5.0, 6.0]), [5.0, 7.0, 9.0]);
98        assert_eq!(sub([4.0, 5.0, 6.0], [1.0, 2.0, 3.0]), [3.0, 3.0, 3.0]);
99        assert_eq!(scale([1.0, 2.0, 3.0], 2.0), [2.0, 4.0, 6.0]);
100        assert_eq!(lerp([0.0, 0.0, 0.0], [2.0, 4.0, 6.0], 0.5), [1.0, 2.0, 3.0]);
101    }
102
103    #[test]
104    fn vec3_add_accumulates_in_place() {
105        let mut acc = [1.0, 1.0, 1.0];
106        vec3_add(&mut acc, [1.0, 2.0, 3.0]);
107        vec3_add(&mut acc, [1.0, 2.0, 3.0]);
108        assert_eq!(acc, [3.0, 5.0, 7.0]);
109    }
110
111    #[test]
112    fn normalise_falls_back_on_a_degenerate_vector() {
113        assert_eq!(vec3_normalise([0.0, 0.0, 0.0]), [0.0, 1.0, 0.0]);
114        assert_eq!(vec3_normalise([0.0, 0.0, 2.0]), [0.0, 0.0, 1.0]);
115    }
116
117    #[test]
118    fn face_normal_of_a_ccw_triangle_points_up() {
119        let n = vec3_face_normal([0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, -1.0]);
120        assert!(n[1] > 0.99, "expected +Y, got {n:?}");
121    }
122}