1use core::ops::{Add, Mul, Neg, Sub};
8
9use ogeom_core::{OgeomResult, ogeom_bail};
10
11use crate::{Direction, Vector, Vector2};
12
13#[derive(Debug, Clone, Copy, PartialEq)]
15pub struct Matrix3 {
16 pub rows: [[f64; 3]; 3],
18}
19
20#[derive(Debug, Clone, Copy, PartialEq)]
22pub struct Matrix2 {
23 pub rows: [[f64; 2]; 2],
25}
26
27impl Default for Matrix3 {
28 fn default() -> Self {
29 Self::IDENTITY
30 }
31}
32
33impl Default for Matrix2 {
34 fn default() -> Self {
35 Self::IDENTITY
36 }
37}
38
39impl Matrix3 {
40 pub const IDENTITY: Self = Self::new([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]);
42 pub const ZERO: Self = Self::new([[0.0; 3]; 3]);
44
45 #[must_use]
47 pub const fn new(rows: [[f64; 3]; 3]) -> Self {
48 Self { rows }
49 }
50
51 #[must_use]
53 pub const fn from_columns(a: Vector, b: Vector, c: Vector) -> Self {
54 Self::new([[a.x, b.x, c.x], [a.y, b.y, c.y], [a.z, b.z, c.z]])
55 }
56
57 #[must_use]
59 pub const fn from_rows(a: Vector, b: Vector, c: Vector) -> Self {
60 Self::new([[a.x, a.y, a.z], [b.x, b.y, b.z], [c.x, c.y, c.z]])
61 }
62
63 #[must_use]
65 pub const fn scaling(s: f64) -> Self {
66 Self::new([[s, 0.0, 0.0], [0.0, s, 0.0], [0.0, 0.0, s]])
67 }
68
69 #[must_use]
71 pub const fn scaling_xyz(x: f64, y: f64, z: f64) -> Self {
72 Self::new([[x, 0.0, 0.0], [0.0, y, 0.0], [0.0, 0.0, z]])
73 }
74
75 #[must_use]
80 pub fn rotation(axis: Direction, angle: f64) -> Self {
81 let (s, c) = angle.sin_cos();
82 let t = 1.0 - c;
83 let (x, y, z) = (axis.x(), axis.y(), axis.z());
84 Self::new([
85 [
86 t.mul_add(x * x, c),
87 t.mul_add(x * y, -(s * z)),
88 t.mul_add(x * z, s * y),
89 ],
90 [
91 t.mul_add(x * y, s * z),
92 t.mul_add(y * y, c),
93 t.mul_add(y * z, -(s * x)),
94 ],
95 [
96 t.mul_add(x * z, -(s * y)),
97 t.mul_add(y * z, s * x),
98 t.mul_add(z * z, c),
99 ],
100 ])
101 }
102
103 #[must_use]
105 pub fn reflection(n: Direction) -> Self {
106 let (x, y, z) = (n.x(), n.y(), n.z());
107 Self::new([
108 [(-2.0f64).mul_add(x * x, 1.0), -2.0 * x * y, -2.0 * x * z],
109 [-2.0 * x * y, (-2.0f64).mul_add(y * y, 1.0), -2.0 * y * z],
110 [-2.0 * x * z, -2.0 * y * z, (-2.0f64).mul_add(z * z, 1.0)],
111 ])
112 }
113
114 pub fn get(&self, row: usize, col: usize) -> OgeomResult<f64> {
120 if row > 2 || col > 2 {
121 ogeom_bail!(Range, "matrix index ({row}, {col}) of 3x3");
122 }
123 Ok(self.rows[row][col])
124 }
125
126 pub fn column(&self, i: usize) -> OgeomResult<Vector> {
132 if i > 2 {
133 ogeom_bail!(Range, "matrix column {i} of 3");
134 }
135 Ok(Vector::new(
136 self.rows[0][i],
137 self.rows[1][i],
138 self.rows[2][i],
139 ))
140 }
141
142 pub fn row(&self, i: usize) -> OgeomResult<Vector> {
148 if i > 2 {
149 ogeom_bail!(Range, "matrix row {i} of 3");
150 }
151 Ok(Vector::from_array(self.rows[i]))
152 }
153
154 #[must_use]
156 pub const fn transposed(&self) -> Self {
157 let m = &self.rows;
158 Self::new([
159 [m[0][0], m[1][0], m[2][0]],
160 [m[0][1], m[1][1], m[2][1]],
161 [m[0][2], m[1][2], m[2][2]],
162 ])
163 }
164
165 #[must_use]
167 pub fn determinant(&self) -> f64 {
168 let m = &self.rows;
169 m[0][0].mul_add(
170 m[1][1].mul_add(m[2][2], -(m[1][2] * m[2][1])),
171 m[0][1].mul_add(
172 -m[1][0].mul_add(m[2][2], -(m[1][2] * m[2][0])),
173 m[0][2] * m[1][0].mul_add(m[2][1], -(m[1][1] * m[2][0])),
174 ),
175 )
176 }
177
178 #[must_use]
180 pub fn trace(&self) -> f64 {
181 self.rows[0][0] + self.rows[1][1] + self.rows[2][2]
182 }
183
184 pub fn inverse(&self) -> OgeomResult<Self> {
200 let d = self.determinant();
201 let scale = self
202 .rows
203 .iter()
204 .flatten()
205 .fold(0.0_f64, |acc, v| acc.max(v.abs()));
206 if d.abs() <= 18.0 * f64::EPSILON * scale * scale * scale {
207 ogeom_bail!(Numeric, "matrix is singular (determinant {d})");
208 }
209 let m = &self.rows;
210 let cof =
211 |a: usize, b: usize, c: usize, e: usize| m[a][b].mul_add(m[c][e], -(m[a][e] * m[c][b]));
212 Ok(Self::new([
214 [
215 cof(1, 1, 2, 2) / d,
216 -cof(0, 1, 2, 2) / d,
217 cof(0, 1, 1, 2) / d,
218 ],
219 [
220 -cof(1, 0, 2, 2) / d,
221 cof(0, 0, 2, 2) / d,
222 -cof(0, 0, 1, 2) / d,
223 ],
224 [
225 cof(1, 0, 2, 1) / d,
226 -cof(0, 0, 2, 1) / d,
227 cof(0, 0, 1, 1) / d,
228 ],
229 ]))
230 }
231
232 #[must_use]
236 pub fn is_orthonormal(&self, eps: f64) -> bool {
237 let p = *self * self.transposed();
238 p.is_equal(&Self::IDENTITY, eps)
239 }
240
241 #[must_use]
246 pub fn is_equal(&self, other: &Self, eps: f64) -> bool {
247 self.rows
248 .iter()
249 .flatten()
250 .zip(other.rows.iter().flatten())
251 .all(|(a, b)| (a - b).abs() <= eps)
252 }
253
254 #[must_use]
256 pub fn is_finite(&self) -> bool {
257 self.rows.iter().flatten().all(|v| v.is_finite())
258 }
259}
260
261impl Matrix2 {
262 pub const IDENTITY: Self = Self::new([[1.0, 0.0], [0.0, 1.0]]);
264 pub const ZERO: Self = Self::new([[0.0; 2]; 2]);
266
267 #[must_use]
269 pub const fn new(rows: [[f64; 2]; 2]) -> Self {
270 Self { rows }
271 }
272
273 #[must_use]
275 pub fn rotation(angle: f64) -> Self {
276 let (s, c) = angle.sin_cos();
277 Self::new([[c, -s], [s, c]])
278 }
279
280 #[must_use]
282 pub const fn scaling(s: f64) -> Self {
283 Self::new([[s, 0.0], [0.0, s]])
284 }
285
286 #[must_use]
288 pub const fn transposed(&self) -> Self {
289 let m = &self.rows;
290 Self::new([[m[0][0], m[1][0]], [m[0][1], m[1][1]]])
291 }
292
293 #[must_use]
295 pub fn determinant(&self) -> f64 {
296 let m = &self.rows;
297 m[0][0].mul_add(m[1][1], -(m[0][1] * m[1][0]))
298 }
299
300 pub fn inverse(&self) -> OgeomResult<Self> {
307 let d = self.determinant();
308 let scale = self
309 .rows
310 .iter()
311 .flatten()
312 .fold(0.0_f64, |acc, v| acc.max(v.abs()));
313 if d.abs() <= 4.0 * f64::EPSILON * scale * scale {
314 ogeom_bail!(Numeric, "matrix is singular (determinant {d})");
315 }
316 let m = &self.rows;
317 Ok(Self::new([
318 [m[1][1] / d, -m[0][1] / d],
319 [-m[1][0] / d, m[0][0] / d],
320 ]))
321 }
322
323 #[must_use]
325 pub fn is_equal(&self, other: &Self, eps: f64) -> bool {
326 self.rows
327 .iter()
328 .flatten()
329 .zip(other.rows.iter().flatten())
330 .all(|(a, b)| (a - b).abs() <= eps)
331 }
332}
333
334impl Mul<Vector> for Matrix3 {
335 type Output = Vector;
336 fn mul(self, v: Vector) -> Vector {
337 let m = &self.rows;
338 Vector::new(
339 m[0][0].mul_add(v.x, m[0][1].mul_add(v.y, m[0][2] * v.z)),
340 m[1][0].mul_add(v.x, m[1][1].mul_add(v.y, m[1][2] * v.z)),
341 m[2][0].mul_add(v.x, m[2][1].mul_add(v.y, m[2][2] * v.z)),
342 )
343 }
344}
345
346impl Mul for Matrix3 {
347 type Output = Self;
348 fn mul(self, o: Self) -> Self {
349 let mut out = [[0.0_f64; 3]; 3];
350 for (i, row) in out.iter_mut().enumerate() {
351 for (j, cell) in row.iter_mut().enumerate() {
352 *cell = self.rows[i][0].mul_add(
353 o.rows[0][j],
354 self.rows[i][1].mul_add(o.rows[1][j], self.rows[i][2] * o.rows[2][j]),
355 );
356 }
357 }
358 Self::new(out)
359 }
360}
361
362impl Mul<f64> for Matrix3 {
363 type Output = Self;
364 fn mul(self, s: f64) -> Self {
365 let mut out = self.rows;
366 for cell in out.iter_mut().flatten() {
367 *cell *= s;
368 }
369 Self::new(out)
370 }
371}
372
373impl Add for Matrix3 {
374 type Output = Self;
375 fn add(self, o: Self) -> Self {
376 let mut out = self.rows;
377 for (i, row) in out.iter_mut().enumerate() {
378 for (j, cell) in row.iter_mut().enumerate() {
379 *cell += o.rows[i][j];
380 }
381 }
382 Self::new(out)
383 }
384}
385
386impl Sub for Matrix3 {
387 type Output = Self;
388 fn sub(self, o: Self) -> Self {
389 self + (-o)
390 }
391}
392
393impl Neg for Matrix3 {
394 type Output = Self;
395 fn neg(self) -> Self {
396 self * -1.0
397 }
398}
399
400impl Mul<Vector2> for Matrix2 {
401 type Output = Vector2;
402 fn mul(self, v: Vector2) -> Vector2 {
403 let m = &self.rows;
404 Vector2::new(
405 m[0][0].mul_add(v.x, m[0][1] * v.y),
406 m[1][0].mul_add(v.x, m[1][1] * v.y),
407 )
408 }
409}
410
411impl Mul for Matrix2 {
412 type Output = Self;
413 fn mul(self, o: Self) -> Self {
414 let mut out = [[0.0_f64; 2]; 2];
415 for (i, row) in out.iter_mut().enumerate() {
416 for (j, cell) in row.iter_mut().enumerate() {
417 *cell = self.rows[i][0].mul_add(o.rows[0][j], self.rows[i][1] * o.rows[1][j]);
418 }
419 }
420 Self::new(out)
421 }
422}
423
424#[cfg(test)]
425#[allow(clippy::unwrap_used)]
426mod tests {
427 use super::*;
428 use approx::assert_relative_eq;
429 use ogeom_core::Tolerances;
430
431 const T: Tolerances = Tolerances::millimetres();
433 const EPS: f64 = 1e-13;
436
437 #[test]
438 fn identity_is_neutral() {
439 let v = Vector::new(1.0, 2.0, 3.0);
440 assert_eq!(Matrix3::IDENTITY * v, v);
441 let m = Matrix3::rotation(Direction::Z, 0.7);
442 assert!((m * Matrix3::IDENTITY).is_equal(&m, EPS));
443 assert!((Matrix3::IDENTITY * m).is_equal(&m, EPS));
444 }
445
446 #[test]
447 fn rotation_is_orthonormal_and_preserves_length() {
448 let axis = Direction::from_coords(1.0, 2.0, 3.0, T).unwrap();
449 for k in 0..8 {
450 let m = Matrix3::rotation(axis, f64::from(k) * 0.7);
451 assert!(m.is_orthonormal(EPS));
452 assert_relative_eq!(m.determinant(), 1.0, epsilon = 1e-14);
453 let v = Vector::new(3.0, -1.0, 2.0);
454 assert_relative_eq!((m * v).magnitude(), v.magnitude(), epsilon = 1e-13);
455 }
456 }
457
458 #[test]
459 fn rotation_about_z_matches_the_hand_computation() {
460 let m = Matrix3::rotation(Direction::Z, core::f64::consts::FRAC_PI_2);
461 let v = m * Vector::X;
462 assert_relative_eq!(v.x, 0.0, epsilon = 1e-15);
463 assert_relative_eq!(v.y, 1.0, epsilon = 1e-15);
464 assert_relative_eq!(v.z, 0.0, epsilon = 1e-15);
465 }
466
467 #[test]
468 fn rotation_composes_additively_in_angle() {
469 let axis = Direction::from_coords(0.0, 1.0, 1.0, T).unwrap();
470 let a = Matrix3::rotation(axis, 0.3);
471 let b = Matrix3::rotation(axis, 0.4);
472 let ab = Matrix3::rotation(axis, 0.7);
473 assert!((a * b).is_equal(&ab, EPS));
474 }
475
476 #[test]
477 fn reflection_is_an_involution_with_negative_determinant() {
478 let n = Direction::from_coords(1.0, 1.0, 0.0, T).unwrap();
479 let m = Matrix3::reflection(n);
480 assert_relative_eq!(m.determinant(), -1.0, epsilon = 1e-14);
481 assert!((m * m).is_equal(&Matrix3::IDENTITY, EPS));
482 let in_plane = Vector::new(1.0, -1.0, 0.0);
484 assert!((m * in_plane).is_equal(in_plane, T));
485 assert!((m * n.vector()).is_equal(-n.vector(), T));
486 }
487
488 #[test]
489 fn inverse_round_trips() {
490 let m = Matrix3::new([[2.0, 1.0, 0.0], [1.0, 3.0, 1.0], [0.0, 1.0, 4.0]]);
491 let inv = m.inverse().unwrap();
492 assert!((m * inv).is_equal(&Matrix3::IDENTITY, EPS));
493 assert!((inv * m).is_equal(&Matrix3::IDENTITY, EPS));
494 }
495
496 #[test]
497 fn singular_matrices_are_refused() {
498 let m = Matrix3::new([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [5.0, 7.0, 9.0]]);
500 assert!(m.inverse().is_err());
501 assert!(Matrix3::ZERO.inverse().is_err());
502 }
503
504 #[test]
505 fn singularity_threshold_is_relative_not_absolute() {
506 let m = Matrix3::IDENTITY * 1e-3;
510 assert_relative_eq!(m.determinant(), 1e-9, max_relative = 1e-12);
511 let inv = m.inverse().unwrap();
512 assert!((m * inv).is_equal(&Matrix3::IDENTITY, EPS));
513 let big = Matrix3::IDENTITY * 1e6;
515 assert!((big * big.inverse().unwrap()).is_equal(&Matrix3::IDENTITY, EPS));
516 }
517
518 #[test]
519 fn orthonormal_inverse_equals_transpose() {
520 let axis = Direction::from_coords(2.0, -1.0, 0.5, T).unwrap();
521 let m = Matrix3::rotation(axis, 1.1);
522 assert!(m.inverse().unwrap().is_equal(&m.transposed(), EPS));
523 assert!(!Matrix3::scaling(2.0).is_orthonormal(EPS));
524 }
525
526 #[test]
527 fn determinant_and_trace() {
528 let m = Matrix3::scaling_xyz(2.0, 3.0, 4.0);
529 assert_relative_eq!(m.determinant(), 24.0);
530 assert_relative_eq!(m.trace(), 9.0);
531 }
532
533 #[test]
534 fn columns_and_rows_are_bounds_checked() {
535 let m = Matrix3::from_columns(Vector::X, Vector::Y, Vector::Z);
536 assert_eq!(m.column(0).unwrap(), Vector::X);
537 assert_eq!(m.row(1).unwrap(), Vector::Y);
538 assert!(m.column(3).is_err());
539 assert!(m.row(3).is_err());
540 assert!(m.get(0, 3).is_err());
541 assert!(m.is_equal(&Matrix3::IDENTITY, EPS));
542 }
543
544 #[test]
545 fn matrix2_rotation_and_inverse() {
546 let m = Matrix2::rotation(core::f64::consts::FRAC_PI_2);
547 let v = m * Vector2::X;
548 assert_relative_eq!(v.x, 0.0, epsilon = 1e-15);
549 assert_relative_eq!(v.y, 1.0, epsilon = 1e-15);
550 assert_relative_eq!(m.determinant(), 1.0, epsilon = 1e-15);
551 assert!((m * m.inverse().unwrap()).is_equal(&Matrix2::IDENTITY, EPS));
552 assert!(Matrix2::ZERO.inverse().is_err());
553 }
554}