Skip to main content

box3d_rust/math_functions/
scalar.rs

1// Scalar helpers: int/float min/max/abs/clamp, deterministic atan2 and cos/sin.
2// Part of the math_functions module.
3
4use super::*;
5
6/// @return the minimum of two integers.
7pub fn min_int(a: i32, b: i32) -> i32 {
8    if a < b {
9        a
10    } else {
11        b
12    }
13}
14
15/// @return the maximum of two integers.
16pub fn max_int(a: i32, b: i32) -> i32 {
17    if a > b {
18        a
19    } else {
20        b
21    }
22}
23
24/// @return an integer clamped between a lower and upper bound.
25pub fn clamp_int(a: i32, lower: i32, upper: i32) -> i32 {
26    if a < lower {
27        lower
28    } else if upper < a {
29        upper
30    } else {
31        a
32    }
33}
34
35/// @return the absolute value of a float.
36pub fn abs_float(a: f32) -> f32 {
37    if a < 0.0 {
38        -a
39    } else {
40        a
41    }
42}
43
44/// @return the minimum of two floats.
45/// Matches the C ternary exactly, including NaN propagation (`a < b` is false for NaN).
46pub fn min_float(a: f32, b: f32) -> f32 {
47    if a < b {
48        a
49    } else {
50        b
51    }
52}
53
54/// @return the maximum of two floats.
55pub fn max_float(a: f32, b: f32) -> f32 {
56    if a > b {
57        a
58    } else {
59        b
60    }
61}
62
63/// @return a float clamped between a lower and upper bound.
64pub fn clamp_float(a: f32, lower: f32, upper: f32) -> f32 {
65    if a < lower {
66        lower
67    } else if upper < a {
68        upper
69    } else {
70        a
71    }
72}
73
74/// Interpolate a scalar.
75pub fn lerp_float(a: f32, b: f32, alpha: f32) -> f32 {
76    (1.0 - alpha) * a + alpha * b
77}
78
79/// Compute an approximate arctangent in the range [-pi, pi]
80/// This is hand coded for cross-platform determinism. The atan2f
81/// function in the standard library is not cross-platform deterministic.
82/// Accurate to around 0.0023 degrees.
83// https://stackoverflow.com/questions/46210708/atan2-approximation-with-11bits-in-mantissa-on-x86with-sse2-and-armwith-vfpv4
84pub fn atan2(y: f32, x: f32) -> f32 {
85    // Added check for (0,0) to match atan2f and avoid NaN
86    if x == 0.0 && y == 0.0 {
87        return 0.0;
88    }
89
90    let ax = abs_float(x);
91    let ay = abs_float(y);
92    let mx = max_float(ay, ax);
93    let mn = min_float(ay, ax);
94    let a = mn / mx;
95
96    // Minimax polynomial approximation to atan(a) on [0,1]
97    let s = a * a;
98    let c = s * a;
99    let q = s * s;
100    let mut r = 0.024840285 * q + 0.18681418;
101    let t = -0.094097948 * q - 0.33213072;
102    r = r * s + t;
103    r = r * c + a;
104
105    // Map to full circle
106    if ay > ax {
107        r = 1.57079637 - r;
108    }
109
110    if x < 0.0 {
111        r = 3.14159274 - r;
112    }
113
114    if y < 0.0 {
115        r = -r;
116    }
117
118    r
119}
120
121/// Compute the cosine and sine of an angle in radians. Implemented
122/// for cross-platform determinism.
123// Approximate cosine and sine for determinism. In my testing cosf and sinf produced
124// the same results on x64 and ARM using MSVC, GCC, and Clang. However, I don't trust
125// this result.
126// https://en.wikipedia.org/wiki/Bh%C4%81skara_I%27s_sine_approximation_formula
127pub fn compute_cos_sin(radians: f32) -> CosSin {
128    let x = unwind_angle(radians);
129    let pi2 = PI * PI;
130
131    // cosine needs angle in [-pi/2, pi/2]
132    let c: f32 = if x < -0.5 * PI {
133        let y = x + PI;
134        let y2 = y * y;
135        -(pi2 - 4.0 * y2) / (pi2 + y2)
136    } else if x > 0.5 * PI {
137        let y = x - PI;
138        let y2 = y * y;
139        -(pi2 - 4.0 * y2) / (pi2 + y2)
140    } else {
141        let y2 = x * x;
142        (pi2 - 4.0 * y2) / (pi2 + y2)
143    };
144
145    // sine needs angle in [0, pi]
146    let s: f32 = if x < 0.0 {
147        let y = x + PI;
148        -16.0 * y * (PI - y) / (5.0 * pi2 - 4.0 * y * (PI - y))
149    } else {
150        16.0 * x * (PI - x) / (5.0 * pi2 - 4.0 * x * (PI - x))
151    };
152
153    let mag = (s * s + c * c).sqrt();
154    let inv_mag = if mag > 0.0 { 1.0 / mag } else { 0.0 };
155    CosSin {
156        cosine: c * inv_mag,
157        sine: s * inv_mag,
158    }
159}
160
161/// @deprecated
162pub fn sin(radians: f32) -> f32 {
163    let cs = compute_cos_sin(radians);
164    cs.sine
165}
166
167/// @deprecated
168pub fn cos(radians: f32) -> f32 {
169    let cs = compute_cos_sin(radians);
170    cs.cosine
171}
172
173/// Convert any angle into the range [-pi, pi].
174pub fn unwind_angle(radians: f32) -> f32 {
175    // Assuming this is deterministic
176    remainder_f32(radians, 2.0 * PI)
177}
178
179/// C remainderf: IEEE 754 remainder, result in [-|y|/2, |y|/2].
180/// Rust has no stable std equivalent (`f32::rem_euclid` differs), so implement it directly.
181pub(crate) fn remainder_f32(x: f32, y: f32) -> f32 {
182    if y == 0.0 || x.is_infinite() || x.is_nan() || y.is_nan() {
183        return f32::NAN;
184    }
185
186    // Round-half-to-even quotient, computed in f64 to avoid double-rounding error
187    // for the magnitudes used here (angle unwinding).
188    let q = (x as f64 / y as f64).round_ties_even();
189    (x as f64 - q * y as f64) as f32
190}