use core::ops::{Add, Mul, Neg, Sub};
use ogeom_core::{OgeomResult, ogeom_bail};
use crate::{Direction, Vector, Vector2};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Matrix3 {
pub rows: [[f64; 3]; 3],
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Matrix2 {
pub rows: [[f64; 2]; 2],
}
impl Default for Matrix3 {
fn default() -> Self {
Self::IDENTITY
}
}
impl Default for Matrix2 {
fn default() -> Self {
Self::IDENTITY
}
}
impl Matrix3 {
pub const IDENTITY: Self = Self::new([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]);
pub const ZERO: Self = Self::new([[0.0; 3]; 3]);
#[must_use]
pub const fn new(rows: [[f64; 3]; 3]) -> Self {
Self { rows }
}
#[must_use]
pub const fn from_columns(a: Vector, b: Vector, c: Vector) -> Self {
Self::new([[a.x, b.x, c.x], [a.y, b.y, c.y], [a.z, b.z, c.z]])
}
#[must_use]
pub const fn from_rows(a: Vector, b: Vector, c: Vector) -> Self {
Self::new([[a.x, a.y, a.z], [b.x, b.y, b.z], [c.x, c.y, c.z]])
}
#[must_use]
pub const fn scaling(s: f64) -> Self {
Self::new([[s, 0.0, 0.0], [0.0, s, 0.0], [0.0, 0.0, s]])
}
#[must_use]
pub const fn scaling_xyz(x: f64, y: f64, z: f64) -> Self {
Self::new([[x, 0.0, 0.0], [0.0, y, 0.0], [0.0, 0.0, z]])
}
#[must_use]
pub fn rotation(axis: Direction, angle: f64) -> Self {
let (s, c) = angle.sin_cos();
let t = 1.0 - c;
let (x, y, z) = (axis.x(), axis.y(), axis.z());
Self::new([
[
t.mul_add(x * x, c),
t.mul_add(x * y, -(s * z)),
t.mul_add(x * z, s * y),
],
[
t.mul_add(x * y, s * z),
t.mul_add(y * y, c),
t.mul_add(y * z, -(s * x)),
],
[
t.mul_add(x * z, -(s * y)),
t.mul_add(y * z, s * x),
t.mul_add(z * z, c),
],
])
}
#[must_use]
pub fn reflection(n: Direction) -> Self {
let (x, y, z) = (n.x(), n.y(), n.z());
Self::new([
[(-2.0f64).mul_add(x * x, 1.0), -2.0 * x * y, -2.0 * x * z],
[-2.0 * x * y, (-2.0f64).mul_add(y * y, 1.0), -2.0 * y * z],
[-2.0 * x * z, -2.0 * y * z, (-2.0f64).mul_add(z * z, 1.0)],
])
}
pub fn get(&self, row: usize, col: usize) -> OgeomResult<f64> {
if row > 2 || col > 2 {
ogeom_bail!(Range, "matrix index ({row}, {col}) of 3x3");
}
Ok(self.rows[row][col])
}
pub fn column(&self, i: usize) -> OgeomResult<Vector> {
if i > 2 {
ogeom_bail!(Range, "matrix column {i} of 3");
}
Ok(Vector::new(
self.rows[0][i],
self.rows[1][i],
self.rows[2][i],
))
}
pub fn row(&self, i: usize) -> OgeomResult<Vector> {
if i > 2 {
ogeom_bail!(Range, "matrix row {i} of 3");
}
Ok(Vector::from_array(self.rows[i]))
}
#[must_use]
pub const fn transposed(&self) -> Self {
let m = &self.rows;
Self::new([
[m[0][0], m[1][0], m[2][0]],
[m[0][1], m[1][1], m[2][1]],
[m[0][2], m[1][2], m[2][2]],
])
}
#[must_use]
pub fn determinant(&self) -> f64 {
let m = &self.rows;
m[0][0].mul_add(
m[1][1].mul_add(m[2][2], -(m[1][2] * m[2][1])),
m[0][1].mul_add(
-m[1][0].mul_add(m[2][2], -(m[1][2] * m[2][0])),
m[0][2] * m[1][0].mul_add(m[2][1], -(m[1][1] * m[2][0])),
),
)
}
#[must_use]
pub fn trace(&self) -> f64 {
self.rows[0][0] + self.rows[1][1] + self.rows[2][2]
}
pub fn inverse(&self) -> OgeomResult<Self> {
let d = self.determinant();
let scale = self
.rows
.iter()
.flatten()
.fold(0.0_f64, |acc, v| acc.max(v.abs()));
if d.abs() <= 18.0 * f64::EPSILON * scale * scale * scale {
ogeom_bail!(Numeric, "matrix is singular (determinant {d})");
}
let m = &self.rows;
let cof =
|a: usize, b: usize, c: usize, e: usize| m[a][b].mul_add(m[c][e], -(m[a][e] * m[c][b]));
Ok(Self::new([
[
cof(1, 1, 2, 2) / d,
-cof(0, 1, 2, 2) / d,
cof(0, 1, 1, 2) / d,
],
[
-cof(1, 0, 2, 2) / d,
cof(0, 0, 2, 2) / d,
-cof(0, 0, 1, 2) / d,
],
[
cof(1, 0, 2, 1) / d,
-cof(0, 0, 2, 1) / d,
cof(0, 0, 1, 1) / d,
],
]))
}
#[must_use]
pub fn is_orthonormal(&self, eps: f64) -> bool {
let p = *self * self.transposed();
p.is_equal(&Self::IDENTITY, eps)
}
#[must_use]
pub fn is_equal(&self, other: &Self, eps: f64) -> bool {
self.rows
.iter()
.flatten()
.zip(other.rows.iter().flatten())
.all(|(a, b)| (a - b).abs() <= eps)
}
#[must_use]
pub fn is_finite(&self) -> bool {
self.rows.iter().flatten().all(|v| v.is_finite())
}
}
impl Matrix2 {
pub const IDENTITY: Self = Self::new([[1.0, 0.0], [0.0, 1.0]]);
pub const ZERO: Self = Self::new([[0.0; 2]; 2]);
#[must_use]
pub const fn new(rows: [[f64; 2]; 2]) -> Self {
Self { rows }
}
#[must_use]
pub fn rotation(angle: f64) -> Self {
let (s, c) = angle.sin_cos();
Self::new([[c, -s], [s, c]])
}
#[must_use]
pub const fn scaling(s: f64) -> Self {
Self::new([[s, 0.0], [0.0, s]])
}
#[must_use]
pub const fn transposed(&self) -> Self {
let m = &self.rows;
Self::new([[m[0][0], m[1][0]], [m[0][1], m[1][1]]])
}
#[must_use]
pub fn determinant(&self) -> f64 {
let m = &self.rows;
m[0][0].mul_add(m[1][1], -(m[0][1] * m[1][0]))
}
pub fn inverse(&self) -> OgeomResult<Self> {
let d = self.determinant();
let scale = self
.rows
.iter()
.flatten()
.fold(0.0_f64, |acc, v| acc.max(v.abs()));
if d.abs() <= 4.0 * f64::EPSILON * scale * scale {
ogeom_bail!(Numeric, "matrix is singular (determinant {d})");
}
let m = &self.rows;
Ok(Self::new([
[m[1][1] / d, -m[0][1] / d],
[-m[1][0] / d, m[0][0] / d],
]))
}
#[must_use]
pub fn is_equal(&self, other: &Self, eps: f64) -> bool {
self.rows
.iter()
.flatten()
.zip(other.rows.iter().flatten())
.all(|(a, b)| (a - b).abs() <= eps)
}
}
impl Mul<Vector> for Matrix3 {
type Output = Vector;
fn mul(self, v: Vector) -> Vector {
let m = &self.rows;
Vector::new(
m[0][0].mul_add(v.x, m[0][1].mul_add(v.y, m[0][2] * v.z)),
m[1][0].mul_add(v.x, m[1][1].mul_add(v.y, m[1][2] * v.z)),
m[2][0].mul_add(v.x, m[2][1].mul_add(v.y, m[2][2] * v.z)),
)
}
}
impl Mul for Matrix3 {
type Output = Self;
fn mul(self, o: Self) -> Self {
let mut out = [[0.0_f64; 3]; 3];
for (i, row) in out.iter_mut().enumerate() {
for (j, cell) in row.iter_mut().enumerate() {
*cell = self.rows[i][0].mul_add(
o.rows[0][j],
self.rows[i][1].mul_add(o.rows[1][j], self.rows[i][2] * o.rows[2][j]),
);
}
}
Self::new(out)
}
}
impl Mul<f64> for Matrix3 {
type Output = Self;
fn mul(self, s: f64) -> Self {
let mut out = self.rows;
for cell in out.iter_mut().flatten() {
*cell *= s;
}
Self::new(out)
}
}
impl Add for Matrix3 {
type Output = Self;
fn add(self, o: Self) -> Self {
let mut out = self.rows;
for (i, row) in out.iter_mut().enumerate() {
for (j, cell) in row.iter_mut().enumerate() {
*cell += o.rows[i][j];
}
}
Self::new(out)
}
}
impl Sub for Matrix3 {
type Output = Self;
fn sub(self, o: Self) -> Self {
self + (-o)
}
}
impl Neg for Matrix3 {
type Output = Self;
fn neg(self) -> Self {
self * -1.0
}
}
impl Mul<Vector2> for Matrix2 {
type Output = Vector2;
fn mul(self, v: Vector2) -> Vector2 {
let m = &self.rows;
Vector2::new(
m[0][0].mul_add(v.x, m[0][1] * v.y),
m[1][0].mul_add(v.x, m[1][1] * v.y),
)
}
}
impl Mul for Matrix2 {
type Output = Self;
fn mul(self, o: Self) -> Self {
let mut out = [[0.0_f64; 2]; 2];
for (i, row) in out.iter_mut().enumerate() {
for (j, cell) in row.iter_mut().enumerate() {
*cell = self.rows[i][0].mul_add(o.rows[0][j], self.rows[i][1] * o.rows[1][j]);
}
}
Self::new(out)
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use approx::assert_relative_eq;
use ogeom_core::Tolerances;
const T: Tolerances = Tolerances::millimetres();
const EPS: f64 = 1e-13;
#[test]
fn identity_is_neutral() {
let v = Vector::new(1.0, 2.0, 3.0);
assert_eq!(Matrix3::IDENTITY * v, v);
let m = Matrix3::rotation(Direction::Z, 0.7);
assert!((m * Matrix3::IDENTITY).is_equal(&m, EPS));
assert!((Matrix3::IDENTITY * m).is_equal(&m, EPS));
}
#[test]
fn rotation_is_orthonormal_and_preserves_length() {
let axis = Direction::from_coords(1.0, 2.0, 3.0, T).unwrap();
for k in 0..8 {
let m = Matrix3::rotation(axis, f64::from(k) * 0.7);
assert!(m.is_orthonormal(EPS));
assert_relative_eq!(m.determinant(), 1.0, epsilon = 1e-14);
let v = Vector::new(3.0, -1.0, 2.0);
assert_relative_eq!((m * v).magnitude(), v.magnitude(), epsilon = 1e-13);
}
}
#[test]
fn rotation_about_z_matches_the_hand_computation() {
let m = Matrix3::rotation(Direction::Z, core::f64::consts::FRAC_PI_2);
let v = m * Vector::X;
assert_relative_eq!(v.x, 0.0, epsilon = 1e-15);
assert_relative_eq!(v.y, 1.0, epsilon = 1e-15);
assert_relative_eq!(v.z, 0.0, epsilon = 1e-15);
}
#[test]
fn rotation_composes_additively_in_angle() {
let axis = Direction::from_coords(0.0, 1.0, 1.0, T).unwrap();
let a = Matrix3::rotation(axis, 0.3);
let b = Matrix3::rotation(axis, 0.4);
let ab = Matrix3::rotation(axis, 0.7);
assert!((a * b).is_equal(&ab, EPS));
}
#[test]
fn reflection_is_an_involution_with_negative_determinant() {
let n = Direction::from_coords(1.0, 1.0, 0.0, T).unwrap();
let m = Matrix3::reflection(n);
assert_relative_eq!(m.determinant(), -1.0, epsilon = 1e-14);
assert!((m * m).is_equal(&Matrix3::IDENTITY, EPS));
let in_plane = Vector::new(1.0, -1.0, 0.0);
assert!((m * in_plane).is_equal(in_plane, T));
assert!((m * n.vector()).is_equal(-n.vector(), T));
}
#[test]
fn inverse_round_trips() {
let m = Matrix3::new([[2.0, 1.0, 0.0], [1.0, 3.0, 1.0], [0.0, 1.0, 4.0]]);
let inv = m.inverse().unwrap();
assert!((m * inv).is_equal(&Matrix3::IDENTITY, EPS));
assert!((inv * m).is_equal(&Matrix3::IDENTITY, EPS));
}
#[test]
fn singular_matrices_are_refused() {
let m = Matrix3::new([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [5.0, 7.0, 9.0]]);
assert!(m.inverse().is_err());
assert!(Matrix3::ZERO.inverse().is_err());
}
#[test]
fn singularity_threshold_is_relative_not_absolute() {
let m = Matrix3::IDENTITY * 1e-3;
assert_relative_eq!(m.determinant(), 1e-9, max_relative = 1e-12);
let inv = m.inverse().unwrap();
assert!((m * inv).is_equal(&Matrix3::IDENTITY, EPS));
let big = Matrix3::IDENTITY * 1e6;
assert!((big * big.inverse().unwrap()).is_equal(&Matrix3::IDENTITY, EPS));
}
#[test]
fn orthonormal_inverse_equals_transpose() {
let axis = Direction::from_coords(2.0, -1.0, 0.5, T).unwrap();
let m = Matrix3::rotation(axis, 1.1);
assert!(m.inverse().unwrap().is_equal(&m.transposed(), EPS));
assert!(!Matrix3::scaling(2.0).is_orthonormal(EPS));
}
#[test]
fn determinant_and_trace() {
let m = Matrix3::scaling_xyz(2.0, 3.0, 4.0);
assert_relative_eq!(m.determinant(), 24.0);
assert_relative_eq!(m.trace(), 9.0);
}
#[test]
fn columns_and_rows_are_bounds_checked() {
let m = Matrix3::from_columns(Vector::X, Vector::Y, Vector::Z);
assert_eq!(m.column(0).unwrap(), Vector::X);
assert_eq!(m.row(1).unwrap(), Vector::Y);
assert!(m.column(3).is_err());
assert!(m.row(3).is_err());
assert!(m.get(0, 3).is_err());
assert!(m.is_equal(&Matrix3::IDENTITY, EPS));
}
#[test]
fn matrix2_rotation_and_inverse() {
let m = Matrix2::rotation(core::f64::consts::FRAC_PI_2);
let v = m * Vector2::X;
assert_relative_eq!(v.x, 0.0, epsilon = 1e-15);
assert_relative_eq!(v.y, 1.0, epsilon = 1e-15);
assert_relative_eq!(m.determinant(), 1.0, epsilon = 1e-15);
assert!((m * m.inverse().unwrap()).is_equal(&Matrix2::IDENTITY, EPS));
assert!(Matrix2::ZERO.inverse().is_err());
}
}