use crate::value::Length;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Transform {
pub a: f32,
pub b: f32,
pub c: f32,
pub d: f32,
pub e: f32,
pub f: f32,
}
impl Default for Transform {
fn default() -> Self {
Self::IDENTITY
}
}
impl Transform {
pub const IDENTITY: Self = Self {
a: 1.0,
b: 0.0,
c: 0.0,
d: 1.0,
e: 0.0,
f: 0.0,
};
#[must_use]
pub fn parse(text: &str) -> Option<Self> {
let mut placement = Self::IDENTITY;
let mut read = 0;
let mut rest = text;
while let Some(open) = rest.find('(') {
let name = rest[..open].trim().trim_start_matches(')').trim();
let Some(close) = rest[open..].find(')') else {
break;
};
let arguments = &rest[open + 1..open + close];
rest = &rest[open + close + 1..];
let numbers: Vec<f32> = arguments
.split([',', ' ', '\t', '\n'])
.filter(|word| !word.is_empty())
.filter_map(number)
.collect();
let Some(step) = step(name, &numbers) else {
continue;
};
placement = placement.then(step);
read += 1;
}
(read > 0).then_some(placement)
}
#[must_use]
pub fn then(self, outer: Self) -> Self {
Self {
a: outer.a * self.a + outer.c * self.b,
b: outer.b * self.a + outer.d * self.b,
c: outer.a * self.c + outer.c * self.d,
d: outer.b * self.c + outer.d * self.d,
e: outer.a * self.e + outer.c * self.f + outer.e,
f: outer.b * self.e + outer.d * self.f + outer.f,
}
}
#[must_use]
pub fn apply(self, (x, y): (f32, f32)) -> (f32, f32) {
(
self.a * x + self.c * y + self.e,
self.b * x + self.d * y + self.f,
)
}
#[must_use]
pub fn is_upright(self) -> bool {
self.b.abs() < 1e-4 && self.c.abs() < 1e-4 && self.a > 0.0 && self.d > 0.0
}
}
fn step(name: &str, numbers: &[f32]) -> Option<Transform> {
let at = |i: usize| numbers.get(i).copied();
Some(match name {
"translate" => Transform {
e: at(0)?,
f: at(1).unwrap_or(0.0),
..Transform::IDENTITY
},
"scale" => {
let x = at(0)?;
Transform {
a: x,
d: at(1).unwrap_or(x),
..Transform::IDENTITY
}
}
"rotate" => {
let (sin, cos) = at(0)?.sin_cos();
Transform {
a: cos,
b: -sin,
c: sin,
d: cos,
..Transform::IDENTITY
}
}
"skewX" => Transform {
c: at(0)?.tan(),
..Transform::IDENTITY
},
"skewY" => Transform {
b: at(0)?.tan(),
..Transform::IDENTITY
},
"matrix" => Transform {
a: at(0)?,
b: at(1)?,
c: at(2)?,
d: at(3)?,
e: at(4)?,
f: at(5)?,
},
_ => return None,
})
}
fn number(word: &str) -> Option<f32> {
Length::parse(word).map_or_else(|| word.parse().ok(), |length| Some(length.points()))
}
#[cfg(test)]
mod tests {
use super::Transform;
fn near(left: (f32, f32), right: (f32, f32)) -> bool {
(left.0 - right.0).abs() < 0.01 && (left.1 - right.1).abs() < 0.01
}
#[test]
fn a_half_turn_puts_the_far_corner_where_the_near_one_was() {
let placed = Transform::parse("rotate (-3.14159265358979) translate (28cm 15.75cm)")
.expect("a readable transform");
let corner = |x: f32, y: f32| {
let (x, y) = placed.apply((x * 28.3465, y * 28.3465));
(x / 28.3465, y / 28.3465)
};
assert!(
near(corner(0.0, 0.0), (28.0, 15.75)),
"{:?}",
corner(0.0, 0.0)
);
assert!(
near(corner(8.834, 15.086), (19.166, 0.664)),
"{:?}",
corner(8.834, 15.086)
);
}
#[test]
fn a_quarter_turn_lays_a_tall_bar_along_the_bottom() {
let placed = Transform::parse("rotate (-1.5707963267949) translate (28cm 15.05cm)")
.expect("a readable transform");
let corner = |x: f32, y: f32| {
let (x, y) = placed.apply((x * 28.3465, y * 28.3465));
(x / 28.3465, y / 28.3465)
};
assert!(
near(corner(0.0, 0.0), (28.0, 15.05)),
"{:?}",
corner(0.0, 0.0)
);
assert!(
near(corner(0.7, 28.0), (0.0, 15.75)),
"{:?}",
corner(0.7, 28.0)
);
}
#[test]
fn a_translation_alone_is_upright_and_a_turn_is_not() {
let moved = Transform::parse("translate (2cm 3cm)").expect("a readable transform");
assert!(moved.is_upright());
assert!(near(moved.apply((0.0, 0.0)), (56.693, 85.039)));
let turned = Transform::parse("rotate (0.5) translate (0cm 0cm)").expect("readable");
assert!(!turned.is_upright());
}
#[test]
fn an_operation_with_no_name_this_knows_leaves_the_others_standing() {
let placed = Transform::parse("wobble (3) translate (1cm 0cm)").expect("the translation");
assert!(near(placed.apply((0.0, 0.0)), (28.3465, 0.0)));
assert!(Transform::parse("wobble (3)").is_none());
}
}