1use libm::{cos, sin, sqrt};
14
15#[must_use]
17#[derive(Debug, Clone, Copy)]
18pub struct Matrix3 {
19 pub data: [[f64; 3]; 3],
20}
21
22#[must_use]
24#[derive(Debug, Clone, Copy)]
25pub struct Vector3 {
26 pub x: f64,
27 pub y: f64,
28 pub z: f64,
29}
30
31impl Matrix3 {
32 pub const IDENTITY: Self = Self {
34 data: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
35 };
36
37 pub fn rotation_x(theta: f64) -> Self {
45 let (s, c) = (sin(theta), cos(theta));
46 Self {
47 data: [[1.0, 0.0, 0.0], [0.0, c, s], [0.0, -s, c]],
48 }
49 }
50
51 pub fn rotation_y(theta: f64) -> Self {
59 let (s, c) = (sin(theta), cos(theta));
60 Self {
61 data: [[c, 0.0, -s], [0.0, 1.0, 0.0], [s, 0.0, c]],
62 }
63 }
64
65 pub fn rotation_z(theta: f64) -> Self {
73 let (s, c) = (sin(theta), cos(theta));
74 Self {
75 data: [[c, s, 0.0], [-s, c, 0.0], [0.0, 0.0, 1.0]],
76 }
77 }
78
79 pub fn transpose(&self) -> Self {
81 let d = &self.data;
82 Self {
83 data: [
84 [d[0][0], d[1][0], d[2][0]],
85 [d[0][1], d[1][1], d[2][1]],
86 [d[0][2], d[1][2], d[2][2]],
87 ],
88 }
89 }
90
91 #[allow(clippy::needless_range_loop)]
93 pub fn multiply(&self, other: &Self) -> Self {
94 let mut result = [[0.0_f64; 3]; 3];
95 for r in 0..3 {
96 for c in 0..3 {
97 result[r][c] = self.data[r][0] * other.data[0][c]
98 + self.data[r][1] * other.data[1][c]
99 + self.data[r][2] * other.data[2][c];
100 }
101 }
102 Self { data: result }
103 }
104
105 pub fn apply(&self, v: &Vector3) -> Vector3 {
107 Vector3 {
108 x: self.data[0][0] * v.x + self.data[0][1] * v.y + self.data[0][2] * v.z,
109 y: self.data[1][0] * v.x + self.data[1][1] * v.y + self.data[1][2] * v.z,
110 z: self.data[2][0] * v.x + self.data[2][1] * v.y + self.data[2][2] * v.z,
111 }
112 }
113}
114
115impl Vector3 {
116 pub const fn new(x: f64, y: f64, z: f64) -> Self {
118 Self { x, y, z }
119 }
120
121 #[must_use]
123 pub fn length(&self) -> f64 {
124 sqrt(self.x * self.x + self.y * self.y + self.z * self.z)
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131 use core::f64::consts::{FRAC_PI_2, PI};
132
133 const EPS: f64 = 1e-12;
134
135 fn assert_vec_eq(a: &Vector3, b: &Vector3) {
136 assert!((a.x - b.x).abs() < EPS, "x: {} != {}", a.x, b.x);
137 assert!((a.y - b.y).abs() < EPS, "y: {} != {}", a.y, b.y);
138 assert!((a.z - b.z).abs() < EPS, "z: {} != {}", a.z, b.z);
139 }
140
141 fn assert_mat_eq(a: &Matrix3, b: &Matrix3) {
142 for r in 0..3 {
143 for c in 0..3 {
144 assert!(
145 (a.data[r][c] - b.data[r][c]).abs() < EPS,
146 "data[{}][{}]: {} != {}",
147 r,
148 c,
149 a.data[r][c],
150 b.data[r][c]
151 );
152 }
153 }
154 }
155
156 #[test]
157 fn identity_apply() {
158 let v = Vector3::new(1.0, 2.0, 3.0);
159 let result = Matrix3::IDENTITY.apply(&v);
160 assert_vec_eq(&result, &v);
161 }
162
163 #[test]
164 fn identity_multiply() {
165 let r = Matrix3::rotation_z(0.5);
166 let result = Matrix3::IDENTITY.multiply(&r);
167 assert_mat_eq(&result, &r);
168 }
169
170 #[test]
171 fn rotation_x_zero() {
172 assert_mat_eq(&Matrix3::rotation_x(0.0), &Matrix3::IDENTITY);
173 }
174
175 #[test]
176 fn rotation_x_90_rotates_y_to_neg_z() {
177 let v = Vector3::new(0.0, 1.0, 0.0);
178 let result = Matrix3::rotation_x(FRAC_PI_2).apply(&v);
179 assert_vec_eq(&result, &Vector3::new(0.0, 0.0, -1.0));
180 }
181
182 #[test]
183 fn rotation_x_preserves_length() {
184 let v = Vector3::new(1.0, 2.0, 3.0);
185 let result = Matrix3::rotation_x(1.23).apply(&v);
186 assert!((result.length() - v.length()).abs() < EPS);
187 }
188
189 #[test]
190 fn rotation_y_zero() {
191 assert_mat_eq(&Matrix3::rotation_y(0.0), &Matrix3::IDENTITY);
192 }
193
194 #[test]
195 fn rotation_y_90_rotates_z_to_neg_x() {
196 let v = Vector3::new(0.0, 0.0, 1.0);
197 let result = Matrix3::rotation_y(FRAC_PI_2).apply(&v);
198 assert_vec_eq(&result, &Vector3::new(-1.0, 0.0, 0.0));
199 }
200
201 #[test]
202 fn rotation_z_zero() {
203 assert_mat_eq(&Matrix3::rotation_z(0.0), &Matrix3::IDENTITY);
204 }
205
206 #[test]
207 fn rotation_z_90_rotates_x_to_neg_y() {
208 let v = Vector3::new(1.0, 0.0, 0.0);
209 let result = Matrix3::rotation_z(FRAC_PI_2).apply(&v);
210 assert_vec_eq(&result, &Vector3::new(0.0, -1.0, 0.0));
211 }
212
213 #[test]
214 fn transpose_identity() {
215 assert_mat_eq(&Matrix3::IDENTITY.transpose(), &Matrix3::IDENTITY);
216 }
217
218 #[test]
219 fn rotation_transpose_is_inverse() {
220 let r = Matrix3::rotation_x(0.7);
221 let result = r.multiply(&r.transpose());
222 assert_mat_eq(&result, &Matrix3::IDENTITY);
223 }
224
225 #[test]
226 fn double_transpose_is_original() {
227 let r = Matrix3::rotation_y(1.1);
228 assert_mat_eq(&r.transpose().transpose(), &r);
229 }
230
231 #[test]
232 fn multiply_associative() {
233 let a = Matrix3::rotation_x(0.3);
234 let b = Matrix3::rotation_y(0.5);
235 let c = Matrix3::rotation_z(0.7);
236 let ab_c = a.multiply(&b).multiply(&c);
237 let a_bc = a.multiply(&b.multiply(&c));
238 assert_mat_eq(&ab_c, &a_bc);
239 }
240
241 #[test]
242 fn full_rotation_is_identity() {
243 assert_mat_eq(&Matrix3::rotation_z(2.0 * PI), &Matrix3::IDENTITY);
244 }
245
246 #[test]
247 fn composition_equals_sum() {
248 let a = 0.4_f64;
249 let b = 0.6_f64;
250 let composed = Matrix3::rotation_x(a).multiply(&Matrix3::rotation_x(b));
251 let direct = Matrix3::rotation_x(a + b);
252 assert_mat_eq(&composed, &direct);
253 }
254
255 #[test]
256 fn vector_length() {
257 let v = Vector3::new(3.0, 4.0, 0.0);
258 assert!((v.length() - 5.0).abs() < EPS);
259 }
260}