use crate::error::{Error, Result};
use crate::math::{Mat4, Vec3};
use crate::param::{ParameterSet, Scalar};
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq)]
pub enum TransformOp {
Translate([Scalar; 3]),
RotateOrigin([Scalar; 3], Scalar),
RotateX(Scalar),
RotateY(Scalar),
RotateZ(Scalar),
Scale(Scalar),
Scale3([Scalar; 3]),
Matrix(Box<[Scalar; 16]>),
}
impl TransformOp {
pub fn name(&self) -> &'static str {
match self {
TransformOp::Translate(_) => "Translate",
TransformOp::RotateOrigin(..) => "Rotate_Origin",
TransformOp::RotateX(_) => "Rotate_X",
TransformOp::RotateY(_) => "Rotate_Y",
TransformOp::RotateZ(_) => "Rotate_Z",
TransformOp::Scale(_) => "Scale",
TransformOp::Scale3(_) => "Scale3",
TransformOp::Matrix(_) => "Matrix",
}
}
pub fn args(&self) -> impl Iterator<Item = &Scalar> + '_ {
let (main, extra): (&[Scalar], Option<&Scalar>) = match self {
TransformOp::Translate(v) | TransformOp::Scale3(v) => (v, None),
TransformOp::RotateOrigin(v, a) => (v, Some(a)),
TransformOp::RotateX(a)
| TransformOp::RotateY(a)
| TransformOp::RotateZ(a)
| TransformOp::Scale(a) => (core::slice::from_ref(a), None),
TransformOp::Matrix(m) => (&m[..], None),
};
main.iter().chain(extra)
}
pub fn from_name_args(
name: &str,
args: impl IntoIterator<Item = Scalar>,
) -> Result<TransformOp> {
let args: Vec<Scalar> = args.into_iter().collect();
let n = args.len();
let bad = || Error::Format(format!("transformation {name} with {n} argument(s)"));
let mut it = args.into_iter();
let mut next = || it.next().ok_or_else(bad);
Ok(match (name, n) {
("Translate", 3) => TransformOp::Translate([next()?, next()?, next()?]),
("Rotate_Origin", 4) => TransformOp::RotateOrigin([next()?, next()?, next()?], next()?),
("Rotate_X", 1) => TransformOp::RotateX(next()?),
("Rotate_Y", 1) => TransformOp::RotateY(next()?),
("Rotate_Z", 1) => TransformOp::RotateZ(next()?),
("Scale" | "Scale3", 1) => TransformOp::Scale(next()?),
("Scale" | "Scale3", 3) => TransformOp::Scale3([next()?, next()?, next()?]),
("Matrix", 16) => {
let mut m: [Scalar; 16] = Default::default();
for s in &mut m {
*s = next()?;
}
TransformOp::Matrix(Box::new(m))
}
_ => return Err(bad()),
})
}
fn update(&mut self, p: &ParameterSet) -> Result<()> {
match self {
TransformOp::Translate(v) | TransformOp::Scale3(v) => {
v.iter_mut().try_for_each(|s| s.update(p))
}
TransformOp::RotateOrigin(v, a) => {
v.iter_mut().try_for_each(|s| s.update(p))?;
a.update(p)
}
TransformOp::RotateX(a)
| TransformOp::RotateY(a)
| TransformOp::RotateZ(a)
| TransformOp::Scale(a) => a.update(p),
TransformOp::Matrix(m) => m.iter_mut().try_for_each(|s| s.update(p)),
}
}
fn matrix(&self, deg: bool) -> Mat4 {
let ang = |a: &Scalar| {
if deg {
a.value().to_radians()
} else {
a.value()
}
};
let v3 = |v: &[Scalar; 3]| Vec3::new(v[0].value(), v[1].value(), v[2].value());
match self {
TransformOp::Translate(v) => Mat4::translation(v3(v)),
TransformOp::RotateOrigin(v, a) => Mat4::rotation(v3(v), ang(a)),
TransformOp::RotateX(a) => Mat4::rotation(Vec3::unit(0), ang(a)),
TransformOp::RotateY(a) => Mat4::rotation(Vec3::unit(1), ang(a)),
TransformOp::RotateZ(a) => Mat4::rotation(Vec3::unit(2), ang(a)),
TransformOp::Scale(s) => {
let s = s.value();
Mat4::scaling(Vec3::new(s, s, s))
}
TransformOp::Scale3(v) => Mat4::scaling(v3(v)),
TransformOp::Matrix(m) => Mat4::from_row_major(&core::array::from_fn(|i| m[i].value())),
}
}
pub fn to_mat4(&self, degrees: bool) -> Mat4 {
self.matrix(degrees)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Transform {
ops: Vec<TransformOp>,
degrees: bool,
matrix: Mat4,
inverse: Mat4,
invertible: bool,
}
impl Default for Transform {
fn default() -> Self {
Transform {
ops: Vec::new(),
degrees: false,
matrix: Mat4::IDENTITY,
inverse: Mat4::IDENTITY,
invertible: true,
}
}
}
impl Transform {
pub fn new() -> Self {
Self::default()
}
pub fn angles_in_degrees(&self) -> bool {
self.degrees
}
pub fn set_angles_in_degrees(&mut self, deg: bool) {
self.degrees = deg;
self.rebuild();
}
pub fn ops(&self) -> &[TransformOp] {
&self.ops
}
pub fn is_empty(&self) -> bool {
self.ops.is_empty()
}
pub fn push(&mut self, op: TransformOp) -> &mut Self {
self.ops.push(op);
self.rebuild();
self
}
pub fn translate(&mut self, v: Vec3) -> &mut Self {
self.push(TransformOp::Translate(v.0.map(Scalar::new)))
}
pub fn rotate(&mut self, axis: Vec3, angle: f64) -> &mut Self {
self.push(TransformOp::RotateOrigin(
axis.0.map(Scalar::new),
angle.into(),
))
}
pub fn rotate_axis(&mut self, dir: usize, angle: f64) -> &mut Self {
let a = Scalar::new(angle);
self.push(match dir {
0 => TransformOp::RotateX(a),
1 => TransformOp::RotateY(a),
_ => TransformOp::RotateZ(a),
})
}
pub fn scale(&mut self, s: f64) -> &mut Self {
self.push(TransformOp::Scale(s.into()))
}
pub fn push_matrix(&mut self, m: &Mat4) -> &mut Self {
self.push(TransformOp::Matrix(Box::new(
m.to_row_major().map(Scalar::new),
)))
}
pub fn clear(&mut self) {
self.ops.clear();
self.rebuild();
}
pub fn update(&mut self, p: &ParameterSet) -> Result<()> {
self.ops.iter_mut().try_for_each(|o| o.update(p))?;
self.rebuild();
if !self.invertible {
return Err(Error::Invalid("singular transformation".into()));
}
Ok(())
}
fn rebuild(&mut self) {
self.matrix = self
.ops
.iter()
.fold(Mat4::IDENTITY, |acc, op| op.matrix(self.degrees) * acc);
match self.matrix.inverse() {
Some(inv) => {
self.inverse = inv;
self.invertible = true;
}
None => {
self.inverse = Mat4::IDENTITY;
self.invertible = false;
}
}
}
pub fn is_invertible(&self) -> bool {
self.invertible
}
pub fn matrix(&self) -> &Mat4 {
&self.matrix
}
pub fn inverse(&self) -> &Mat4 {
&self.inverse
}
#[inline]
pub fn apply(&self, p: Vec3) -> Vec3 {
self.matrix.transform_point(p)
}
#[inline]
pub fn apply_inverse(&self, p: Vec3) -> Vec3 {
self.inverse.transform_point(p)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn matrix_convention() {
let col = [
1.0, 0.0, 0.0, 5.0, 0.0, 1.0, 0.0, 6.0, 0.0, 0.0, 1.0, 7.0, 0.0, 0.0, 0.0, 1.0,
];
let op = TransformOp::from_name_args("Matrix", col.map(Scalar::new).to_vec()).unwrap();
assert_eq!(
op.to_mat4(false).transform_point(Vec3::ZERO),
Vec3::new(5.0, 6.0, 7.0)
);
let rot = [
0.0, -1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
];
let op = TransformOp::from_name_args("Matrix", rot.map(Scalar::new).to_vec()).unwrap();
assert!((op.to_mat4(false).transform_point(Vec3::unit(0)) - Vec3::unit(1)).norm() < 1e-12);
let r = Mat4::rotation(Vec3::unit(2), core::f64::consts::FRAC_PI_2);
let mut t = Transform::new();
t.push_matrix(&r);
assert!((t.apply(Vec3::unit(0)) - Vec3::unit(1)).norm() < 1e-12);
let mut t2 = Transform::new();
t2.push_matrix(&Mat4::translation(Vec3::new(1.0, 2.0, 3.0)));
assert_eq!(t2.apply(Vec3::ZERO), Vec3::new(1.0, 2.0, 3.0));
let s3 = TransformOp::from_name_args("Scale3", vec![1.0.into(), 2.0.into(), 3.0.into()])
.unwrap();
assert_eq!(s3.name(), "Scale3");
assert!(
TransformOp::from_name_args("Scale", vec![1.0.into(), 2.0.into(), 3.0.into()]).is_ok()
);
}
use core::f64::consts::FRAC_PI_2;
#[test]
fn order() {
let mut t = Transform::new();
t.translate(Vec3::new(1.0, 0.0, 0.0))
.rotate_axis(2, FRAC_PI_2);
let p = t.apply(Vec3::ZERO);
assert!((p - Vec3::new(0.0, 1.0, 0.0)).norm() < 1e-12);
assert!((t.apply_inverse(p)).norm() < 1e-12);
}
#[test]
fn degrees_and_params() {
let mut t = Transform::new();
t.set_angles_in_degrees(true);
t.push(TransformOp::RotateZ(Scalar::expr("ang")));
let mut ps = ParameterSet::new();
ps.set("ang", 90.0);
t.update(&ps).unwrap();
assert!((t.apply(Vec3::unit(0)) - Vec3::unit(1)).norm() < 1e-12);
assert!(TransformOp::from_name_args("Translate", vec![1.0.into()]).is_err());
let mut z = Transform::new();
z.scale(0.0);
assert!(!z.is_invertible());
assert!(Transform::new().is_invertible());
let op = TransformOp::from_name_args(
"Rotate_Origin",
vec![0.0.into(), 0.0.into(), 1.0.into(), 2.0.into()],
)
.unwrap();
assert_eq!(op.args().count(), 4);
}
}