use crate::point::{Vec2D, Unit, WorldUnit, ScreenUnit, Unittless};
use crate::geom::Angle;
fn multiply(t2: &Transform, t1: &Transform) -> Transform {
Transform {
xx: t1.xx * t2.xx + t1.yx * t2.xy,
yx: t1.xx * t2.yx + t1.yx * t2.yy,
xy: t1.xy * t2.xx + t1.yy * t2.xy,
yy: t1.xy * t2.yx + t1.yy * t2.yy,
x0: t1.x0 * t2.xx + t1.y0 * t2.xy + t2.x0,
y0: t1.x0 * t2.yx + t1.y0 * t2.yy + t2.y0,
}
}
#[derive(Debug, Copy, Clone)]
pub struct Transform {
pub xx: f64,
pub yx: f64,
pub xy: f64,
pub yy: f64,
pub x0: f64,
pub y0: f64,
}
impl Default for Transform {
fn default() -> Transform {
Transform {
xx: 1.0,
yx: 0.0,
xy: 0.0,
yy: 1.0,
x0: 0.0,
y0: 0.0,
}
}
}
impl Transform {
fn new(xx: f64, xy: f64, x0: f64, yx: f64, yy: f64, y0: f64) -> Transform {
Transform {
xx, yx, xy, yy, x0, y0,
}
}
pub fn new_scale(factor: f64) -> Transform {
Transform::new(
factor, 0.0, 0.0,
0.0, factor, 0.0,
)
}
pub fn new_scale_x(factor: f64) -> Transform {
Transform::new(
factor, 0.0, 0.0,
0.0, 1.0, 0.0,
)
}
pub fn new_scale_y(factor: f64) -> Transform {
Transform::new(
1.0, 0.0, 0.0,
0.0, factor, 0.0,
)
}
pub fn new_translate(vector: Vec2D<Unittless>) -> Transform {
Transform::new(
1.0, 0.0, vector.x.val(),
0.0, 1.0, vector.y.val(),
)
}
pub fn new_rotate(angle: Angle) -> Transform {
let theta = angle.radians();
Transform::new(
theta.cos(), -theta.sin(), 0.0,
theta.sin(), theta.cos(), 0.0,
)
}
pub fn new_shear_x(factor: f64) -> Transform {
Transform::new(
1.0, factor, 0.0,
0.0, 1.0, 0.0,
)
}
pub fn default_for_viewport(viewport_dimensions: Vec2D<ScreenUnit>) -> Transform {
Transform::new_translate(Vec2D::new_unitless(viewport_dimensions.x.val(), viewport_dimensions.y.val()) * 0.5)
}
pub fn scale(&self, factor: f64) -> Transform {
multiply(&Transform::new_scale(factor), self)
}
pub fn scale_x(&self, factor: f64) -> Transform {
multiply(&Transform::new_scale_x(factor), self)
}
pub fn scale_y(&self, factor: f64) -> Transform {
multiply(&Transform::new_scale_y(factor), self)
}
pub fn translate(&self, delta: Vec2D<Unittless>) -> Transform {
multiply(&Transform::new_translate(delta), self)
}
pub fn rotate(&self, angle: Angle) -> Transform {
multiply(&Transform::new_rotate(angle), self)
}
pub fn shear_x(&self, factor: f64) -> Transform {
multiply(&Transform::new_shear_x(factor), self)
}
pub fn zoom(&self, factor: f64, fixed: Vec2D<ScreenUnit>) -> Transform {
let t = fixed * -1.0;
let t1 = fixed;
self.translate(t.to_vec2d()).scale(factor).translate(t1.to_vec2d())
}
pub fn turn(&self, angle: Angle, fixed: Vec2D<ScreenUnit>) -> Transform {
let t = fixed * -1.0;
let t1 = fixed;
self.translate(t.to_vec2d()).rotate(angle).translate(t1.to_vec2d())
}
pub fn r#move(&self, delta: Vec2D<ScreenUnit>) -> Transform {
self.translate(delta.to_vec2d())
}
#[inline]
fn transform_distance(&self, dx: f64, dy: f64) -> Vec2D<Unittless> {
Vec2D::new_unitless(dx * self.xx + dy * self.xy, dx * self.yx + dy * self.yy)
}
#[inline]
fn transform_point(&self, px: f64, py: f64) -> (f64, f64) {
let Vec2D {x, y} = self.transform_distance(px, py);
(x.val() + self.x0, y.val() + self.y0)
}
#[inline]
fn determinant(&self) -> f64 {
self.xx * self.yy - self.xy * self.yx
}
pub fn invert(&self) -> Self {
let det = self.determinant();
let inv_det = 1.0 / det;
Transform {
xx: inv_det * self.yy,
yx: inv_det * (-self.yx),
xy: inv_det * (-self.xy),
yy: inv_det * self.xx,
x0: inv_det * (self.xy * self.y0 - self.yy * self.x0),
y0: inv_det * (self.yx * self.x0 - self.xx * self.y0),
}
}
pub fn apply(&self, p: Vec2D<Unittless>) -> Vec2D<Unittless> {
self.transform_point(p.x.val(), p.y.val()).into()
}
pub fn to_screen_coordinates(&self, p: Vec2D<WorldUnit>) -> Vec2D<ScreenUnit> {
self.transform_point(p.x.val(), p.y.val()).into()
}
pub fn to_world_coordinates(&self, p: Vec2D<ScreenUnit>) -> Vec2D<WorldUnit> {
self.invert().transform_point(p.x.val(), p.y.val()).into()
}
pub fn to_world_units(&self, length: ScreenUnit) -> WorldUnit {
let vec: Vec2D<WorldUnit> = self.invert().transform_distance(length.val(), 0.0).into();
vec.magnitude()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_viewport_transform_centers_the_origin() {
let dimensions = Vec2D::new_screen(200.0, 100.0);
let t = Transform::default_for_viewport(dimensions);
let center = dimensions / 2.0;
let transformed = t.to_world_coordinates(center);
assert_eq!(transformed, Vec2D::new_world(0.0, 0.0));
}
#[test]
fn screen_center_is_drawing_origin() {
let dimensions = Vec2D::new_screen(200.0, 100.0);
let t = Transform::default_for_viewport(dimensions);
let center = Vec2D::new_screen(100.0, 50.0);
let transformed = t.to_world_coordinates(center);
assert_eq!(transformed, Vec2D::new_world(0.0, 0.0));
}
#[test]
fn drawing_origin_is_screen_center() {
let dimensions = (200.0, 100.0).into();
let t = Transform::default_for_viewport(dimensions);
let origin = Vec2D::new_world(0.0, 0.0);
let transformed = t.to_screen_coordinates(origin);
assert_eq!(transformed, Vec2D::new_screen(100.0, 50.0));
}
#[test]
fn zoom_in_scales_as_expected() {
let dimensions = (200.0, 100.0).into();
let a = Vec2D::new_world(0.0, 0.0);
let b = Vec2D::new_world(3.0, 4.0);
let t = Transform::default_for_viewport(dimensions);
let t1 = t.zoom(2.0, dimensions);
let a1 = t1.to_screen_coordinates(a);
let b1 = t1.to_screen_coordinates(b);
assert_eq!(a1.distance(b1), 10.0.into());
}
#[test]
fn zoom_in_preserves_center_point() {
let dimensions = Vec2D::new_screen(200.0, 100.0);
let center = dimensions * 0.5;
let t = Transform::default_for_viewport(dimensions);
let fixed_point = t.to_world_coordinates(center);
let t1 = t.zoom(2.0, center);
let zoomed_in = t1.to_world_coordinates(center);
assert_eq!(fixed_point, zoomed_in);
}
#[test]
fn zoom_out_preserves_center_point() {
let dimensions = Vec2D::new_screen(200.0, 100.0);
let t = Transform::default_for_viewport(dimensions);
let center = dimensions * 0.5;
let fixed_point = t.to_world_coordinates(center);
let t = t.zoom(0.5, center);
let zoomed_in = t.to_world_coordinates(center);
assert_eq!(fixed_point, zoomed_in);
}
#[test]
fn translated_screen_point_preserves_drawing_coordinates() {
let dimensions = Vec2D::new_screen(200.0, 100.0);
let t = Transform::default_for_viewport(dimensions);
let screen_point = Vec2D::new_screen(20.0, 30.0);
let drawing_point = t.to_world_coordinates(screen_point);
let t = t.r#move(Vec2D::new_screen(10.0, 5.0));
let translated_screen_point = Vec2D::new_screen(30.0, 35.0);
let translated_drawing_point = t.to_world_coordinates(translated_screen_point);
assert_eq!(drawing_point, translated_drawing_point);
}
#[test]
fn units_in_screen_coordinates_can_be_converted_to_world() {
let dimensions = Vec2D::new_screen(200.0, 100.0);
let mut t = Transform::default_for_viewport(dimensions);
assert_eq!(t.to_world_units(1.0.into()), 1.0.into());
t = t.zoom(2.0, dimensions);
assert_eq!(t.to_world_units(1.0.into()), 0.5.into());
t = t.zoom(0.5, dimensions);
assert_eq!(t.to_world_units(1.0.into()), 1.0.into());
t = t.zoom(0.5, dimensions);
assert_eq!(t.to_world_units(1.0.into()), 2.0.into());
}
#[test]
fn zoom_fixes_world_origin() {
let dimensions = Vec2D::new_screen(200.0, 100.0);
let center = dimensions / 2.0;
let mut t = Transform::default_for_viewport(dimensions);
assert_eq!(t.to_screen_coordinates(Vec2D::new_world(0.0, 0.0)), Vec2D::new_screen(100.0, 50.0));
t = t.zoom(2.0, center);
assert_eq!(t.to_screen_coordinates(Vec2D::new_world(0.0, 0.0)), Vec2D::new_screen(100.0, 50.0));
t = t.zoom(2.0, center);
assert_eq!(t.to_screen_coordinates(Vec2D::new_world(0.0, 0.0)), Vec2D::new_screen(100.0, 50.0));
}
#[test]
fn zoom_fixes_viewport_center_even_when_translated() {
let dimensions = Vec2D::new_screen(200.0, 100.0);
let screen_center = dimensions / 2.0;
let mut t = Transform::default_for_viewport(dimensions);
assert_eq!(t.to_world_coordinates(screen_center), Vec2D::new_world(0.0, 0.0));
t = t.zoom(2.0, screen_center);
assert_eq!(t.to_world_coordinates(screen_center), Vec2D::new_world(0.0, 0.0));
t = t.r#move(Vec2D::new_screen(5.0, 0.0));
let fixed = t.to_world_coordinates(screen_center);
assert_ne!(fixed, Vec2D::new_world(0.0, 0.0));
t = t.zoom(0.5, screen_center);
assert_eq!(t.to_world_coordinates(screen_center), fixed);
}
}