1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5#[repr(u8)]
6pub enum MathOp {
7 Sin = 0,
8 Cos = 1,
9 Tan = 2,
10 Asin = 3,
11 Acos = 4,
12 Atan = 5,
13 Atan2 = 6,
14 Sqrt = 7,
15 Hypot = 8,
16 Abs = 9,
17 Sign = 10,
18 Min = 11,
19 Max = 12,
20 Clamp = 13,
21 Floor = 14,
22 Ceil = 15,
23 Round = 16,
24 RoundTiesToEven = 17,
25 Trunc = 18,
26 DegreesToRadians = 19,
27 RadiansToDegrees = 20,
28 Lerp = 21,
29}
30
31impl MathOp {
32 pub const ALL: [Self; 22] = [
33 Self::Sin,
34 Self::Cos,
35 Self::Tan,
36 Self::Asin,
37 Self::Acos,
38 Self::Atan,
39 Self::Atan2,
40 Self::Sqrt,
41 Self::Hypot,
42 Self::Abs,
43 Self::Sign,
44 Self::Min,
45 Self::Max,
46 Self::Clamp,
47 Self::Floor,
48 Self::Ceil,
49 Self::Round,
50 Self::RoundTiesToEven,
51 Self::Trunc,
52 Self::DegreesToRadians,
53 Self::RadiansToDegrees,
54 Self::Lerp,
55 ];
56 pub fn from_code(code: u32) -> Option<Self> {
57 Self::ALL.get(code as usize).copied()
58 }
59 pub fn name(self) -> &'static str {
60 match self {
61 Self::Sin => "sin",
62 Self::Cos => "cos",
63 Self::Tan => "tan",
64 Self::Asin => "asin",
65 Self::Acos => "acos",
66 Self::Atan => "atan",
67 Self::Atan2 => "atan2",
68 Self::Sqrt => "sqrt",
69 Self::Hypot => "hypot",
70 Self::Abs => "abs",
71 Self::Sign => "sign",
72 Self::Min => "min",
73 Self::Max => "max",
74 Self::Clamp => "clamp",
75 Self::Floor => "floor",
76 Self::Ceil => "ceil",
77 Self::Round => "round",
78 Self::RoundTiesToEven => "roundTiesToEven",
79 Self::Trunc => "trunc",
80 Self::DegreesToRadians => "degreesToRadians",
81 Self::RadiansToDegrees => "radiansToDegrees",
82 Self::Lerp => "lerp",
83 }
84 }
85 pub fn params(self) -> &'static [&'static str] {
86 match self {
87 Self::Sin => &["x"],
88 Self::Cos => &["x"],
89 Self::Tan => &["x"],
90 Self::Asin => &["x"],
91 Self::Acos => &["x"],
92 Self::Atan => &["x"],
93 Self::Atan2 => &["y", "x"],
94 Self::Sqrt => &["x"],
95 Self::Hypot => &["x", "y"],
96 Self::Abs => &["x"],
97 Self::Sign => &["x"],
98 Self::Min => &["a", "b"],
99 Self::Max => &["a", "b"],
100 Self::Clamp => &["value", "min", "max"],
101 Self::Floor => &["x"],
102 Self::Ceil => &["x"],
103 Self::Round => &["x"],
104 Self::RoundTiesToEven => &["x"],
105 Self::Trunc => &["x"],
106 Self::DegreesToRadians => &["x"],
107 Self::RadiansToDegrees => &["x"],
108 Self::Lerp => &["a", "b", "t"],
109 }
110 }
111 pub fn arity(self) -> usize {
112 self.params().len()
113 }
114 pub fn descriptor(self, owner_module: &str) -> String {
116 format!(
117 "{owner_module}::{}({}):float64={}:v1",
118 self.name(),
119 self.params()
120 .iter()
121 .map(|p| format!("{p}:float64"))
122 .collect::<Vec<_>>()
123 .join(","),
124 self as u8
125 )
126 }
127}
128
129#[derive(Debug, Clone, PartialEq, Eq)]
130pub struct MathError {
131 pub code: &'static str,
132 pub message: String,
133}
134
135fn minimum(a: f64, b: f64) -> f64 {
136 if a == 0.0 && b == 0.0 {
137 if a.is_sign_negative() || b.is_sign_negative() {
138 -0.0
139 } else {
140 0.0
141 }
142 } else {
143 a.min(b)
144 }
145}
146fn maximum(a: f64, b: f64) -> f64 {
147 if a == 0.0 && b == 0.0 {
148 if a.is_sign_positive() || b.is_sign_positive() {
149 0.0
150 } else {
151 -0.0
152 }
153 } else {
154 a.max(b)
155 }
156}
157
158pub fn evaluate(op: MathOp, args: &[f64]) -> Result<f64, MathError> {
160 let error = |code, detail| MathError {
161 code,
162 message: format!("{}: {detail}", op.name()),
163 };
164 if args.len() != op.arity() {
165 return Err(error("nx-math-domain", "wrong argument count"));
166 }
167 if args.iter().any(|x| !x.is_finite()) {
168 return Err(error(
169 "nx-math-non-finite-input",
170 "arguments must be finite",
171 ));
172 }
173 let x = args[0];
174 let y = args.get(1).copied().unwrap_or(0.0);
175 use MathOp::*;
176 if (op == Sqrt && x < 0.0)
177 || (matches!(op, Asin | Acos) && x.abs() > 1.0)
178 || (op == Clamp && y > args[2])
179 {
180 return Err(error("nx-math-domain", "invalid domain or bounds"));
181 }
182 let result = match op {
183 Sin => x.sin(),
184 Cos => x.cos(),
185 Tan => x.tan(),
186 Asin => x.asin(),
187 Acos => x.acos(),
188 Atan => x.atan(),
189 Atan2 => x.atan2(y),
190 Sqrt => x.sqrt(),
191 Hypot => x.hypot(y),
192 Abs => x.abs(),
193 Sign => {
194 if x == 0.0 {
195 x
196 } else {
197 x.signum()
198 }
199 }
200 Min => minimum(x, y),
201 Max => maximum(x, y),
202 Clamp => minimum(maximum(x, y), args[2]),
203 Floor => x.floor(),
204 Ceil => x.ceil(),
205 Trunc => x.trunc(),
206 RoundTiesToEven => x.round_ties_even(),
207 Round => {
208 let f = x.floor();
210 let r = if x - f < 0.5 { f } else { f + 1.0 };
211 if r == 0.0 {
212 0.0_f64.copysign(x)
213 } else {
214 r
215 }
216 }
217 DegreesToRadians => x * (std::f64::consts::PI / 180.0),
218 RadiansToDegrees => x * (180.0 / std::f64::consts::PI),
219 Lerp => {
220 let t = args[2];
221 if t == 0.0 {
222 x
223 } else if t == 1.0 {
224 y
225 } else if x == y {
226 x
227 } else if (x <= 0.0 && y >= 0.0) || (x >= 0.0 && y <= 0.0) {
228 (1.0 - t) * x + t * y
229 } else {
230 let result = x + t * (y - x);
231 if result.is_finite() {
232 result
233 } else {
234 let alternative = (1.0 - t) * x + t * y;
236 if alternative.is_finite() {
237 alternative
238 } else {
239 (x * 0.5 + t * (y * 0.5 - x * 0.5)) * 2.0
241 }
242 }
243 }
244 }
245 };
246 if !result.is_finite() {
247 return Err(error("nx-math-overflow", "result is not finite"));
248 }
249 Ok(result)
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255 #[test]
256 fn shared_reference_corpus() {
257 let cases: serde_json::Value =
258 serde_json::from_str(include_str!("math-cases.json")).unwrap();
259 for case in cases.as_array().unwrap() {
260 let op = MathOp::ALL
261 .into_iter()
262 .find(|op| op.name() == case["op"].as_str().unwrap())
263 .unwrap();
264 let args = case["args"]
265 .as_array()
266 .unwrap()
267 .iter()
268 .map(|x| x.as_str().unwrap().parse::<f64>().unwrap())
269 .collect::<Vec<_>>();
270 let actual = evaluate(op, &args);
271 if let Some(code) = case["error"].as_str() {
272 assert_eq!(actual.unwrap_err().code, code, "{}", case["name"]);
273 } else {
274 let actual = actual.unwrap();
275 let expected = case["expected"].as_str().unwrap().parse::<f64>().unwrap();
276 if case["exact"] == true {
277 assert_eq!(actual.to_bits(), expected.to_bits(), "{}", case["name"]);
278 } else {
279 assert!(
280 (actual - expected).abs() <= 1e-12 * expected.abs().max(1.0),
281 "{}: {actual} vs {expected}",
282 case["name"]
283 );
284 }
285 }
286 }
287 }
288}