Skip to content

Commit 3f14d6a

Browse files
committed
[update] math lib to return errors as opposed to panicking, letting the user decide what to do.
1 parent d0abc73 commit 3f14d6a

File tree

7 files changed

+216
-113
lines changed

7 files changed

+216
-113
lines changed

crates/lambda-rs/examples/reflective_room.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,8 @@ impl Component<ComponentResult, String> for ReflectiveRoomExample {
317317
lambda::math::matrix::identity_matrix(4, 4),
318318
[1.0, 0.0, 0.0],
319319
-self.camera_pitch_turns,
320-
);
320+
)
321+
.expect("rotation axis must be a unit axis vector");
321322
let view = rot_x.multiply(&compute_view_matrix(camera.position));
322323
let projection = compute_perspective_projection(
323324
camera.field_of_view_in_turns,
@@ -360,7 +361,8 @@ impl Component<ComponentResult, String> for ReflectiveRoomExample {
360361
model_floor,
361362
[1.0, 0.0, 0.0],
362363
self.floor_tilt_turns,
363-
);
364+
)
365+
.expect("rotation axis must be a unit axis vector");
364366
let mvp_floor = projection.multiply(&view).multiply(&model_floor);
365367

366368
let viewport = ViewportBuilder::new().build(self.width, self.height);

crates/lambda-rs/examples/textured_cube.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -411,12 +411,14 @@ impl Component<ComponentResult, String> for TexturedCubeExample {
411411
model,
412412
[0.0, 1.0, 0.0],
413413
angle_y_turns,
414-
);
414+
)
415+
.expect("rotation axis must be a unit axis vector");
415416
model = lambda::math::matrix::rotate_matrix(
416417
model,
417418
[1.0, 0.0, 0.0],
418419
angle_x_turns,
419-
);
420+
)
421+
.expect("rotation axis must be a unit axis vector");
420422

421423
let view = compute_view_matrix(camera.position);
422424
let projection = compute_perspective_projection(

crates/lambda-rs/src/math/error.rs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
//! Math error types returned from fallible operations.
2+
3+
use std::fmt;
4+
5+
/// Errors returned by fallible math operations in `lambda-rs`.
6+
#[derive(Debug, Clone, PartialEq)]
7+
pub enum MathError {
8+
/// Cross product requires exactly 3 dimensions.
9+
CrossProductDimension { actual: usize },
10+
/// Cross product requires both vectors to have the same dimension.
11+
MismatchedVectorDimensions { left: usize, right: usize },
12+
/// Rotation axis must be a unit axis vector (one of `[1,0,0]`, `[0,1,0]`,
13+
/// `[0,0,1]`). A zero axis (`[0,0,0]`) is treated as "no rotation".
14+
InvalidRotationAxis { axis: [f32; 3] },
15+
/// Rotation requires a 4x4 matrix.
16+
InvalidRotationMatrixSize { rows: usize, cols: usize },
17+
/// Determinant requires a square matrix.
18+
NonSquareMatrix { rows: usize, cols: usize },
19+
/// Determinant cannot be computed for an empty matrix.
20+
EmptyMatrix,
21+
/// Cannot normalize a zero-length vector.
22+
ZeroLengthVector,
23+
}
24+
25+
impl fmt::Display for MathError {
26+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27+
match self {
28+
MathError::CrossProductDimension { actual } => {
29+
return write!(f, "Cross product requires 3D vectors, got {}D", actual);
30+
}
31+
MathError::MismatchedVectorDimensions { left, right } => {
32+
return write!(
33+
f,
34+
"Vectors must have matching dimensions (left {}D, right {}D)",
35+
left, right
36+
);
37+
}
38+
MathError::InvalidRotationAxis { axis } => {
39+
return write!(f, "Rotation axis {:?} is not a unit axis vector", axis);
40+
}
41+
MathError::InvalidRotationMatrixSize { rows, cols } => {
42+
return write!(
43+
f,
44+
"Rotation requires a 4x4 matrix, got {}x{}",
45+
rows, cols
46+
);
47+
}
48+
MathError::NonSquareMatrix { rows, cols } => {
49+
return write!(
50+
f,
51+
"Determinant requires square matrix, got {}x{}",
52+
rows, cols
53+
);
54+
}
55+
MathError::EmptyMatrix => {
56+
return write!(f, "Determinant requires a non-empty matrix");
57+
}
58+
MathError::ZeroLengthVector => {
59+
return write!(f, "Cannot normalize a zero-length vector");
60+
}
61+
}
62+
}
63+
}
64+
65+
impl std::error::Error for MathError {}

crates/lambda-rs/src/math/matrix.rs

Lines changed: 89 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
use super::{
44
turns_to_radians,
55
vector::Vector,
6+
MathError,
67
};
78

89
// -------------------------------- MATRIX -------------------------------------
@@ -17,7 +18,10 @@ pub trait Matrix<V: Vector> {
1718
fn transpose(&self) -> Self;
1819
fn inverse(&self) -> Self;
1920
fn transform(&self, other: &V) -> V;
20-
fn determinant(&self) -> f32;
21+
/// Compute the determinant of the matrix.
22+
///
23+
/// Returns an error when the matrix is empty or not square.
24+
fn determinant(&self) -> Result<f32, MathError>;
2125
fn size(&self) -> (usize, usize);
2226
fn row(&self, row: usize) -> &V;
2327
fn at(&self, row: usize, column: usize) -> V::Scalar;
@@ -84,70 +88,67 @@ pub fn translation_matrix<
8488
/// Rotates the input matrix by the given number of turns around the given axis.
8589
/// The axis must be a unit vector and the turns must be in the range [0, 1).
8690
/// The rotation is counter-clockwise when looking down the axis.
91+
///
92+
/// Returns an error when the matrix is not 4x4, or when `axis_to_rotate` is not
93+
/// a unit axis vector (`[1,0,0]`, `[0,1,0]`, `[0,0,1]`). A zero axis (`[0,0,0]`)
94+
/// is treated as "no rotation".
8795
pub fn rotate_matrix<
88-
InputVector: Vector<Scalar = f32>,
89-
ResultingVector: Vector<Scalar = f32>,
90-
OutputMatrix: Matrix<ResultingVector> + Default + Clone,
96+
V: Vector<Scalar = f32>,
97+
MatrixLike: Matrix<V> + Default + Clone,
9198
>(
92-
matrix_to_rotate: OutputMatrix,
93-
axis_to_rotate: InputVector,
99+
matrix_to_rotate: MatrixLike,
100+
axis_to_rotate: [f32; 3],
94101
angle_in_turns: f32,
95-
) -> OutputMatrix {
102+
) -> Result<MatrixLike, MathError> {
96103
let (rows, columns) = matrix_to_rotate.size();
97-
assert_eq!(rows, columns, "Matrix must be square");
98-
assert_eq!(rows, 4, "Matrix must be 4x4");
99-
assert_eq!(
100-
axis_to_rotate.size(),
101-
3,
102-
"Axis vector must have 3 elements (x, y, z)"
103-
);
104+
if rows != columns {
105+
return Err(MathError::NonSquareMatrix {
106+
rows,
107+
cols: columns,
108+
});
109+
}
110+
if rows != 4 {
111+
return Err(MathError::InvalidRotationMatrixSize {
112+
rows,
113+
cols: columns,
114+
});
115+
}
104116

105117
let angle_in_radians = turns_to_radians(angle_in_turns);
106118
let cosine_of_angle = angle_in_radians.cos();
107119
let sin_of_angle = angle_in_radians.sin();
108120

109-
let _t = 1.0 - cosine_of_angle;
110-
let x = axis_to_rotate.at(0);
111-
let y = axis_to_rotate.at(1);
112-
let z = axis_to_rotate.at(2);
113-
114-
let mut rotation_matrix = OutputMatrix::default();
115-
116-
let rotation = match (x as u8, y as u8, z as u8) {
117-
(0, 0, 0) => {
118-
// No rotation
119-
return matrix_to_rotate;
120-
}
121-
(0, 0, 1) => {
122-
// Rotate around z-axis
123-
[
124-
[cosine_of_angle, sin_of_angle, 0.0, 0.0],
125-
[-sin_of_angle, cosine_of_angle, 0.0, 0.0],
126-
[0.0, 0.0, 1.0, 0.0],
127-
[0.0, 0.0, 0.0, 1.0],
128-
]
129-
}
130-
(0, 1, 0) => {
131-
// Rotate around y-axis
132-
[
133-
[cosine_of_angle, 0.0, -sin_of_angle, 0.0],
134-
[0.0, 1.0, 0.0, 0.0],
135-
[sin_of_angle, 0.0, cosine_of_angle, 0.0],
136-
[0.0, 0.0, 0.0, 1.0],
137-
]
138-
}
139-
(1, 0, 0) => {
140-
// Rotate around x-axis
141-
[
142-
[1.0, 0.0, 0.0, 0.0],
143-
[0.0, cosine_of_angle, sin_of_angle, 0.0],
144-
[0.0, -sin_of_angle, cosine_of_angle, 0.0],
145-
[0.0, 0.0, 0.0, 1.0],
146-
]
147-
}
148-
_ => {
149-
panic!("Axis must be a unit vector")
150-
}
121+
let mut rotation_matrix = MatrixLike::default();
122+
let [x, y, z] = axis_to_rotate;
123+
124+
let rotation = if axis_to_rotate == [0.0, 0.0, 0.0] {
125+
return Ok(matrix_to_rotate);
126+
} else if axis_to_rotate == [0.0, 0.0, 1.0] {
127+
// Rotate around z-axis
128+
[
129+
[cosine_of_angle, sin_of_angle, 0.0, 0.0],
130+
[-sin_of_angle, cosine_of_angle, 0.0, 0.0],
131+
[0.0, 0.0, 1.0, 0.0],
132+
[0.0, 0.0, 0.0, 1.0],
133+
]
134+
} else if axis_to_rotate == [0.0, 1.0, 0.0] {
135+
// Rotate around y-axis
136+
[
137+
[cosine_of_angle, 0.0, -sin_of_angle, 0.0],
138+
[0.0, 1.0, 0.0, 0.0],
139+
[sin_of_angle, 0.0, cosine_of_angle, 0.0],
140+
[0.0, 0.0, 0.0, 1.0],
141+
]
142+
} else if axis_to_rotate == [1.0, 0.0, 0.0] {
143+
// Rotate around x-axis
144+
[
145+
[1.0, 0.0, 0.0, 0.0],
146+
[0.0, cosine_of_angle, sin_of_angle, 0.0],
147+
[0.0, -sin_of_angle, cosine_of_angle, 0.0],
148+
[0.0, 0.0, 0.0, 1.0],
149+
]
150+
} else {
151+
return Err(MathError::InvalidRotationAxis { axis: [x, y, z] });
151152
};
152153

153154
for (i, row) in rotation.iter().enumerate().take(rows) {
@@ -156,7 +157,7 @@ pub fn rotate_matrix<
156157
}
157158
}
158159

159-
return matrix_to_rotate.multiply(&rotation_matrix);
160+
return Ok(matrix_to_rotate.multiply(&rotation_matrix));
160161
}
161162

162163
/// Creates a 4x4 perspective matrix given the fov in turns (unit between
@@ -325,40 +326,47 @@ where
325326
}
326327

327328
/// Computes the determinant of any square matrix using Laplace expansion.
328-
fn determinant(&self) -> f32 {
329-
let (width, height) =
330-
(self.as_ref()[0].as_ref().len(), self.as_ref().len());
329+
fn determinant(&self) -> Result<f32, MathError> {
330+
let rows = self.as_ref().len();
331+
if rows == 0 {
332+
return Err(MathError::EmptyMatrix);
333+
}
334+
335+
let cols = self.as_ref()[0].as_ref().len();
336+
if cols == 0 {
337+
return Err(MathError::EmptyMatrix);
338+
}
331339

332-
if width != height {
333-
panic!("Cannot compute determinant of non-square matrix");
340+
if cols != rows {
341+
return Err(MathError::NonSquareMatrix { rows, cols });
334342
}
335343

336-
return match height {
337-
1 => self.as_ref()[0].as_ref()[0],
344+
return match rows {
345+
1 => Ok(self.as_ref()[0].as_ref()[0]),
338346
2 => {
339347
let a = self.at(0, 0);
340348
let b = self.at(0, 1);
341349
let c = self.at(1, 0);
342350
let d = self.at(1, 1);
343-
a * d - b * c
351+
return Ok(a * d - b * c);
344352
}
345353
_ => {
346354
let mut result = 0.0;
347-
for i in 0..height {
348-
let mut submatrix: Vec<Vec<f32>> = Vec::with_capacity(height - 1);
349-
for j in 1..height {
355+
for i in 0..rows {
356+
let mut submatrix: Vec<Vec<f32>> = Vec::with_capacity(rows - 1);
357+
for j in 1..rows {
350358
let mut row = Vec::new();
351-
for k in 0..height {
359+
for k in 0..rows {
352360
if k != i {
353361
row.push(self.at(j, k));
354362
}
355363
}
356364
submatrix.push(row);
357365
}
358-
result +=
359-
self.at(0, i) * submatrix.determinant() * (-1.0_f32).powi(i as i32);
366+
let sub_determinant = submatrix.determinant()?;
367+
result += self.at(0, i) * sub_determinant * (-1.0_f32).powi(i as i32);
360368
}
361-
result
369+
return Ok(result);
362370
}
363371
};
364372
}
@@ -397,6 +405,7 @@ mod tests {
397405
use crate::math::{
398406
matrix::translation_matrix,
399407
turns_to_radians,
408+
MathError,
400409
};
401410

402411
#[test]
@@ -438,17 +447,17 @@ mod tests {
438447
#[test]
439448
fn square_matrix_determinant() {
440449
let m = [[3.0, 8.0], [4.0, 6.0]];
441-
assert_eq!(m.determinant(), -14.0);
450+
assert_eq!(m.determinant(), Ok(-14.0));
442451

443452
let m2 = [[6.0, 1.0, 1.0], [4.0, -2.0, 5.0], [2.0, 8.0, 7.0]];
444-
assert_eq!(m2.determinant(), -306.0);
453+
assert_eq!(m2.determinant(), Ok(-306.0));
445454
}
446455

447456
#[test]
448457
fn non_square_matrix_determinant() {
449458
let m = [[3.0, 8.0], [4.0, 6.0], [0.0, 1.0]];
450-
let result = std::panic::catch_unwind(|| m.determinant());
451-
assert!(result.is_err());
459+
let result = m.determinant();
460+
assert_eq!(result, Err(MathError::NonSquareMatrix { rows: 3, cols: 2 }));
452461
}
453462

454463
#[test]
@@ -503,7 +512,8 @@ mod tests {
503512
fn rotate_matrices() {
504513
// Test a zero turn rotation.
505514
let matrix: [[f32; 4]; 4] = filled_matrix(4, 4, 1.0);
506-
let rotated_matrix = rotate_matrix(matrix, [0.0, 0.0, 1.0], 0.0);
515+
let rotated_matrix =
516+
rotate_matrix(matrix, [0.0, 0.0, 1.0], 0.0).expect("valid axis");
507517
assert_eq!(rotated_matrix, matrix);
508518

509519
// Test a 90 degree rotation.
@@ -513,7 +523,8 @@ mod tests {
513523
[9.0, 10.0, 11.0, 12.0],
514524
[13.0, 14.0, 15.0, 16.0],
515525
];
516-
let rotated = rotate_matrix(matrix, [0.0, 1.0, 0.0], 0.25);
526+
let rotated =
527+
rotate_matrix(matrix, [0.0, 1.0, 0.0], 0.25).expect("valid axis");
517528
let expected = [
518529
[3.0, 1.9999999, -1.0000001, 4.0],
519530
[7.0, 5.9999995, -5.0000005, 8.0],

crates/lambda-rs/src/math/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
//! Lambda Math Types and operations
22
3+
pub mod error;
34
pub mod matrix;
45
pub mod vector;
56

7+
pub use error::MathError;
8+
69
/// Angle units used by conversion helpers and matrix transforms.
710
///
811
/// Prefer `Angle::Turns` for ergonomic quarter/half rotations when building

0 commit comments

Comments
 (0)