use std::f64::consts::PI;
use crate::{AngleInRadians, Movable, Point, Transformable, Transformation};
#[derive(Clone, Debug, PartialEq)]
pub struct Grid {
origin: Point,
columns: u32,
rows: u32,
spacing_x: Option<Point>,
spacing_y: Option<Point>,
magnification: f64,
angle: AngleInRadians,
x_reflection: bool,
}
impl Grid {
#[allow(clippy::too_many_arguments)]
pub const fn new(
origin: Point,
columns: u32,
rows: u32,
spacing_x: Option<Point>,
spacing_y: Option<Point>,
magnification: f64,
angle: AngleInRadians,
x_reflection: bool,
) -> Self {
Self {
origin,
columns,
rows,
spacing_x,
spacing_y,
magnification,
angle,
x_reflection,
}
}
pub const fn origin(&self) -> Point {
self.origin
}
pub const fn columns(&self) -> u32 {
self.columns
}
pub const fn rows(&self) -> u32 {
self.rows
}
pub const fn spacing_x(&self) -> Option<Point> {
self.spacing_x
}
pub const fn spacing_y(&self) -> Option<Point> {
self.spacing_y
}
pub const fn magnification(&self) -> f64 {
self.magnification
}
pub const fn angle(&self) -> f64 {
self.angle
}
pub const fn x_reflection(&self) -> bool {
self.x_reflection
}
pub const fn set_origin(&mut self, origin: Point) {
self.origin = origin;
}
#[must_use]
pub const fn with_origin(mut self, origin: Point) -> Self {
self.origin = origin;
self
}
pub const fn set_columns(&mut self, columns: u32) {
self.columns = columns;
}
#[must_use]
pub const fn with_columns(mut self, columns: u32) -> Self {
self.columns = columns;
self
}
pub const fn set_rows(&mut self, rows: u32) {
self.rows = rows;
}
#[must_use]
pub const fn with_rows(mut self, rows: u32) -> Self {
self.rows = rows;
self
}
pub const fn set_spacing_x(&mut self, spacing_x: Option<Point>) {
self.spacing_x = spacing_x;
}
#[must_use]
pub const fn with_spacing_x(mut self, spacing_x: Option<Point>) -> Self {
self.spacing_x = spacing_x;
self
}
pub const fn set_spacing_y(&mut self, spacing_y: Option<Point>) {
self.spacing_y = spacing_y;
}
#[must_use]
pub const fn with_spacing_y(mut self, spacing_y: Option<Point>) -> Self {
self.spacing_y = spacing_y;
self
}
pub const fn set_magnification(&mut self, magnification: f64) {
self.magnification = magnification;
}
#[must_use]
pub const fn with_magnification(mut self, magnification: f64) -> Self {
self.magnification = magnification;
self
}
pub const fn set_angle(&mut self, angle: AngleInRadians) {
self.angle = angle;
}
#[must_use]
pub const fn with_angle(mut self, angle: AngleInRadians) -> Self {
self.angle = angle;
self
}
pub const fn set_x_reflection(&mut self, x_reflection: bool) {
self.x_reflection = x_reflection;
}
#[must_use]
pub const fn with_x_reflection(mut self, x_reflection: bool) -> Self {
self.x_reflection = x_reflection;
self
}
#[must_use]
pub fn to_integer_unit(self) -> Self {
Self {
origin: self.origin.to_integer_unit(),
spacing_x: self.spacing_x.as_ref().map(Point::to_integer_unit),
spacing_y: self.spacing_y.as_ref().map(Point::to_integer_unit),
..self
}
}
#[must_use]
pub fn to_float_unit(self) -> Self {
Self {
origin: self.origin.to_float_unit(),
spacing_x: self.spacing_x.as_ref().map(Point::to_float_unit),
spacing_y: self.spacing_y.as_ref().map(Point::to_float_unit),
..self
}
}
}
impl Default for Grid {
fn default() -> Self {
Self::new(
Point::integer(0, 0, 1e-9),
1,
1,
None,
None,
1.0,
0.0,
false,
)
}
}
impl std::fmt::Display for Grid {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let spacing_x_str = self
.spacing_x
.map_or_else(|| "None".to_string(), |p| p.to_string());
let spacing_y_str = self
.spacing_y
.map_or_else(|| "None".to_string(), |p| p.to_string());
write!(
f,
"Grid at {} with {} columns and {} rows, spacing ({}, {}), magnification {:?}, angle {:?}, x_reflection {}",
self.origin,
self.columns,
self.rows,
spacing_x_str,
spacing_y_str,
self.magnification,
self.angle,
self.x_reflection,
)
}
}
impl Transformable for Grid {
fn transform_impl(mut self, transformation: &Transformation) -> Self {
self.origin = transformation.apply_to_point(&self.origin);
self.spacing_x = self.spacing_x.map(|p| transformation.apply_to_point(&p));
self.spacing_y = self.spacing_y.map(|p| transformation.apply_to_point(&p));
if let Some(scale) = &transformation.scale {
self.magnification *= scale.factor();
}
if let Some(rotation) = &transformation.rotation {
self.angle += rotation.angle();
let result = self.angle % (PI * 2.0);
self.angle = if result < 0.0 {
result + PI * 2.0
} else {
result
};
}
if transformation.reflection.is_some() {
self.x_reflection = !self.x_reflection;
}
self
}
}
impl Movable for Grid {
fn move_to(mut self, target: Point) -> Self {
self.origin = target;
self
}
}
#[cfg(test)]
mod tests {
use std::f64::consts::FRAC_PI_2;
use insta::assert_snapshot;
use super::*;
use crate::Point;
fn p(x: i32, y: i32) -> Point {
Point::integer(x, y, 1e-9)
}
fn pf(x: f64, y: f64) -> Point {
Point::float(x, y, 1e-6)
}
fn origin() -> Point {
p(0, 0)
}
fn test_grid() -> Grid {
Grid::new(
p(10, 20),
2,
2,
Some(p(5, 0)),
Some(p(0, 5)),
1.0,
0.0,
false,
)
}
#[test]
fn test_grid_new_and_getters() {
let grid = Grid::new(
p(10, 20),
3,
4,
Some(p(5, 0)),
Some(p(0, 5)),
1.5,
45.0,
true,
);
assert_eq!(grid.origin(), p(10, 20));
assert_eq!(grid.columns(), 3);
assert_eq!(grid.rows(), 4);
assert_eq!(grid.spacing_x(), Some(p(5, 0)));
assert_eq!(grid.spacing_y(), Some(p(0, 5)));
assert_eq!(grid.magnification(), 1.5);
assert_eq!(grid.angle(), 45.0);
assert!(grid.x_reflection());
}
#[test]
fn test_grid_default() {
let grid = Grid::default();
assert_eq!(grid.origin(), p(0, 0));
assert_eq!(grid.columns(), 1);
assert_eq!(grid.rows(), 1);
assert_eq!(grid.spacing_x(), None);
assert_eq!(grid.spacing_y(), None);
assert_eq!(grid.magnification(), 1.0);
assert_eq!(grid.angle(), 0.0);
assert!(!grid.x_reflection());
}
#[test]
fn test_grid_display() {
let grid = Grid::new(
p(10, 20),
2,
3,
Some(p(5, 0)),
Some(p(0, 5)),
1.0,
0.0,
false,
);
assert_snapshot!(format!("{grid}"), @"Grid at Point(10 (1.000e-9), 20 (1.000e-9)) with 2 columns and 3 rows, spacing (Point(5 (1.000e-9), 0 (1.000e-9)), Point(0 (1.000e-9), 5 (1.000e-9))), magnification 1.0, angle 0.0, x_reflection false");
let grid = Grid::new(p(10, 20), 2, 3, Some(p(5, 0)), None, 1.0, 0.0, false);
assert_snapshot!(format!("{grid}"), @"Grid at Point(10 (1.000e-9), 20 (1.000e-9)) with 2 columns and 3 rows, spacing (Point(5 (1.000e-9), 0 (1.000e-9)), None), magnification 1.0, angle 0.0, x_reflection false");
let grid = Grid::new(p(10, 20), 2, 3, None, Some(p(0, 5)), 1.0, 0.0, false);
assert_snapshot!(format!("{grid}"), @"Grid at Point(10 (1.000e-9), 20 (1.000e-9)) with 2 columns and 3 rows, spacing (None, Point(0 (1.000e-9), 5 (1.000e-9))), magnification 1.0, angle 0.0, x_reflection false");
}
#[test]
fn test_grid_setters_and_with_setters() {
let mut grid = test_grid();
grid.set_origin(p(100, 200));
grid.set_columns(5);
grid.set_rows(6);
grid.set_spacing_x(Some(p(10, 0)));
grid.set_spacing_y(Some(p(0, 10)));
grid.set_magnification(2.0);
grid.set_angle(90.0);
grid.set_x_reflection(true);
let grid_with = test_grid()
.with_origin(p(100, 200))
.with_columns(5)
.with_rows(6)
.with_spacing_x(Some(p(10, 0)))
.with_spacing_y(Some(p(0, 10)))
.with_magnification(2.0)
.with_angle(90.0)
.with_x_reflection(true);
assert_eq!(grid, grid_with);
assert_eq!(grid.origin(), p(100, 200));
assert_eq!(grid.columns(), 5);
assert_eq!(grid.rows(), 6);
assert_eq!(grid.spacing_x(), Some(p(10, 0)));
assert_eq!(grid.spacing_y(), Some(p(0, 10)));
assert_eq!(grid.magnification(), 2.0);
assert_eq!(grid.angle(), 90.0);
assert!(grid.x_reflection());
}
#[test]
fn test_grid_transform_with_scale() {
let transformed = test_grid().scale(2.0, origin());
assert_eq!(transformed.magnification, 2.0);
}
#[test]
fn test_grid_transform_with_rotation() {
let transformed = test_grid().rotate(FRAC_PI_2, origin());
assert_eq!(transformed.angle, FRAC_PI_2);
}
#[test]
fn test_grid_transform_with_reflection() {
let transformed = test_grid().reflect(0.0, origin());
assert!(transformed.x_reflection);
}
#[test]
fn test_grid_transform_with_translation() {
let transformed = test_grid().translate(p(5, 5));
assert_eq!(transformed.origin, p(15, 25));
}
#[test]
fn test_grid_move_to() {
let target = p(100, 200);
let moved = test_grid().move_to(target);
assert_eq!(moved.origin, target);
}
#[test]
fn test_grid_angle_normalization() {
let grid = Grid::new(
p(10, 20),
2,
2,
Some(p(5, 0)),
Some(p(0, 5)),
1.0,
FRAC_PI_2,
false,
);
let transformed = grid.rotate(PI * 2.0, origin());
assert!((transformed.angle - FRAC_PI_2).abs() < 0.001);
}
#[test]
fn test_grid_1x1() {
let grid = Grid::new(
p(5, 10),
1,
1,
Some(p(10, 0)),
Some(p(0, 10)),
1.0,
0.0,
false,
);
assert_eq!(grid.columns(), 1);
assert_eq!(grid.rows(), 1);
assert_eq!(grid.origin(), p(5, 10));
}
#[test]
fn test_grid_zero_spacing() {
let grid = Grid::default()
.with_spacing_x(Some(p(0, 0)))
.with_spacing_y(Some(p(0, 0)));
assert_eq!(grid.spacing_x(), Some(p(0, 0)));
assert_eq!(grid.spacing_y(), Some(p(0, 0)));
}
#[test]
fn test_grid_negative_spacing() {
let grid = Grid::default()
.with_spacing_x(Some(p(-10, 0)))
.with_spacing_y(Some(p(0, -10)));
assert_eq!(grid.spacing_x(), Some(p(-10, 0)));
assert_eq!(grid.spacing_y(), Some(p(0, -10)));
}
#[test]
fn test_grid_transform_rotation_then_reflection() {
let transformed = test_grid()
.rotate(FRAC_PI_2, origin())
.reflect(0.0, origin());
assert!((transformed.angle() - FRAC_PI_2).abs() < 0.001);
assert!(transformed.x_reflection());
}
#[test]
fn test_grid_to_integer_unit() {
let grid = Grid::new(
pf(1.5, 2.5),
2,
3,
Some(pf(10.0, 0.0)),
Some(pf(0.0, 10.0)),
1.0,
0.0,
false,
);
let converted = grid.to_integer_unit();
assert_eq!(converted.origin(), pf(1.5, 2.5).to_integer_unit());
assert_eq!(converted.spacing_x(), Some(pf(10.0, 0.0).to_integer_unit()));
assert_eq!(converted.spacing_y(), Some(pf(0.0, 10.0).to_integer_unit()));
assert_eq!(converted.columns(), 2);
assert_eq!(converted.rows(), 3);
}
#[test]
fn test_grid_to_float_unit() {
let grid = Grid::new(
p(10, 20),
2,
3,
Some(p(5, 0)),
Some(p(0, 5)),
1.0,
0.0,
false,
);
let converted = grid.to_float_unit();
assert_eq!(converted.origin(), p(10, 20).to_float_unit());
assert_eq!(converted.spacing_x(), Some(p(5, 0).to_float_unit()));
assert_eq!(converted.spacing_y(), Some(p(0, 5).to_float_unit()));
}
#[test]
fn test_grid_to_integer_unit_none_spacing() {
let grid = Grid::new(pf(1.0, 2.0), 2, 2, None, None, 1.0, 0.0, false);
let converted = grid.to_integer_unit();
assert_eq!(converted.spacing_x(), None);
assert_eq!(converted.spacing_y(), None);
}
#[test]
fn test_grid_zero_dimensions() {
let grid = Grid::default().with_columns(0).with_rows(3);
assert_eq!(grid.columns(), 0);
assert_eq!(grid.rows(), 3);
let grid = Grid::default().with_columns(3).with_rows(0);
assert_eq!(grid.columns(), 3);
assert_eq!(grid.rows(), 0);
let grid = Grid::default().with_columns(0).with_rows(0);
assert_eq!(grid.columns(), 0);
assert_eq!(grid.rows(), 0);
}
#[test]
fn test_grid_edge_magnification() {
let grid = Grid::default().with_magnification(0.0);
assert_eq!(grid.magnification(), 0.0);
let grid = Grid::default().with_magnification(-2.0);
assert_eq!(grid.magnification(), -2.0);
}
#[test]
fn test_grid_transform_edge_magnification_with_scale() {
let grid = test_grid().with_magnification(0.0);
let transformed = grid.scale(2.0, origin());
assert_eq!(transformed.magnification(), 0.0);
let grid = test_grid().with_magnification(-1.0);
let transformed = grid.scale(3.0, origin());
assert_eq!(transformed.magnification(), -3.0);
}
}