use crate::event::Point;
use crate::tab::Rect;
pub use pdfrum_doc::geom::WidgetRotation as Rotation;
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) struct Plate {
pub(crate) rect: Rect,
pub(crate) rotation: Rotation,
}
impl Plate {
pub(crate) fn new(rect: Rect, rotation: Rotation) -> Plate {
Plate {
rect: normalize(rect),
rotation,
}
}
pub(crate) fn to_widget(self, at: Point) -> Point {
let dx = at.x - self.rect.left;
let dy = at.y - self.rect.bottom;
let (w, h) = (
self.rect.right - self.rect.left,
self.rect.top - self.rect.bottom,
);
match self.rotation {
Rotation::None => Point::new(dx, dy),
Rotation::Quarter => Point::new(dy, w - dx),
Rotation::Half => Point::new(w - dx, h - dy),
Rotation::ThreeQuarter => Point::new(h - dy, dx),
}
}
}
#[cfg(test)]
impl Plate {
pub(crate) fn width(self) -> f32 {
let (w, h) = (
self.rect.right - self.rect.left,
self.rect.top - self.rect.bottom,
);
if self.rotation.swaps_axes() { h } else { w }
}
pub(crate) fn height(self) -> f32 {
let (w, h) = (
self.rect.right - self.rect.left,
self.rect.top - self.rect.bottom,
);
if self.rotation.swaps_axes() { w } else { h }
}
pub(crate) fn to_plate(self, at: Point) -> Point {
let upright = self.to_widget(at);
Point::new(upright.x, self.height() - upright.y)
}
pub(crate) fn to_page(self, at: Point) -> Point {
let (w, h) = (
self.rect.right - self.rect.left,
self.rect.top - self.rect.bottom,
);
let ux = at.x;
let uy = self.height() - at.y;
let (dx, dy) = match self.rotation {
Rotation::None => (ux, uy),
Rotation::Quarter => (w - uy, ux),
Rotation::Half => (w - ux, h - uy),
Rotation::ThreeQuarter => (uy, h - ux),
};
Point::new(self.rect.left + dx, self.rect.bottom + dy)
}
}
pub(crate) fn normalize(rect: Rect) -> Rect {
Rect::new(
rect.left.min(rect.right),
rect.bottom.min(rect.top),
rect.left.max(rect.right),
rect.bottom.max(rect.top),
)
}
#[cfg(test)]
mod tests {
#![allow(clippy::float_cmp)]
use super::*;
fn plate(rotation: Rotation) -> Plate {
Plate::new(Rect::new(20.0, 30.0, 120.0, 80.0), rotation)
}
fn close(a: Point, b: Point) -> bool {
(a.x - b.x).abs() < 1e-3 && (a.y - b.y).abs() < 1e-3
}
#[test]
fn only_the_odd_quadrants_exchange_the_axes() {
assert!(!Rotation::None.swaps_axes());
assert!(Rotation::Quarter.swaps_axes());
assert!(!Rotation::Half.swaps_axes());
assert!(Rotation::ThreeQuarter.swaps_axes());
assert_eq!(plate(Rotation::None).width(), 100.0);
assert_eq!(plate(Rotation::None).height(), 50.0);
assert_eq!(plate(Rotation::Quarter).width(), 50.0);
assert_eq!(plate(Rotation::Quarter).height(), 100.0);
}
#[test]
fn to_widget_reproduces_the_four_inverse_matrices() {
let corners = [
Point::new(20.0, 30.0),
Point::new(120.0, 30.0),
Point::new(120.0, 80.0),
Point::new(20.0, 80.0),
];
let expected = [
(
Rotation::None,
[(0.0, 0.0), (100.0, 0.0), (100.0, 50.0), (0.0, 50.0)],
),
(
Rotation::Quarter,
[(0.0, 100.0), (0.0, 0.0), (50.0, 0.0), (50.0, 100.0)],
),
(
Rotation::Half,
[(100.0, 50.0), (0.0, 50.0), (0.0, 0.0), (100.0, 0.0)],
),
(
Rotation::ThreeQuarter,
[(50.0, 0.0), (50.0, 100.0), (0.0, 100.0), (0.0, 0.0)],
),
];
for (rotation, wanted) in expected {
let p = plate(rotation);
for (corner, (x, y)) in corners.iter().zip(wanted) {
assert!(
close(p.to_widget(*corner), Point::new(x, y)),
"{rotation:?} sends {corner:?} to {:?}, wanted ({x}, {y})",
p.to_widget(*corner)
);
}
}
}
#[test]
fn a_plate_point_is_a_widget_point_flipped() {
for rotation in [
Rotation::None,
Rotation::Quarter,
Rotation::Half,
Rotation::ThreeQuarter,
] {
let p = plate(rotation);
for at in [Point::new(20.0, 30.0), Point::new(70.0, 55.0)] {
let upright = p.to_widget(at);
assert!(close(
p.to_plate(at),
Point::new(upright.x, p.height() - upright.y)
));
}
}
}
#[test]
fn an_upright_plate_is_a_translation_and_a_flip() {
let p = plate(Rotation::None);
assert!(close(
p.to_plate(Point::new(20.0, 80.0)),
Point::new(0.0, 0.0)
));
assert!(close(
p.to_plate(Point::new(120.0, 30.0)),
Point::new(100.0, 50.0)
));
}
#[test]
fn the_two_directions_are_inverses() {
for rotation in [
Rotation::None,
Rotation::Quarter,
Rotation::Half,
Rotation::ThreeQuarter,
] {
let p = plate(rotation);
for (x, y) in [
(20.0, 30.0),
(120.0, 80.0),
(70.0, 55.0),
(25.5, 77.25),
(119.0, 31.0),
] {
let page = Point::new(x, y);
let round_tripped = p.to_page(p.to_plate(page));
assert!(
close(page, round_tripped),
"{rotation:?}: {page:?} became {round_tripped:?}"
);
}
}
}
#[test]
fn every_corner_lands_inside_the_plate() {
for rotation in [
Rotation::None,
Rotation::Quarter,
Rotation::Half,
Rotation::ThreeQuarter,
] {
let p = plate(rotation);
for (x, y) in [(20.0, 30.0), (120.0, 30.0), (20.0, 80.0), (120.0, 80.0)] {
let at = p.to_plate(Point::new(x, y));
assert!(
at.x >= -1e-3 && at.x <= p.width() + 1e-3,
"{rotation:?}: x {} outside 0..{}",
at.x,
p.width()
);
assert!(
at.y >= -1e-3 && at.y <= p.height() + 1e-3,
"{rotation:?}: y {} outside 0..{}",
at.y,
p.height()
);
}
}
}
#[test]
fn an_inverted_rectangle_is_normalized() {
let inverted = Rect::new(100.0, 100.0, 200.0, -130.0);
let p = Plate::new(inverted, Rotation::None);
assert_eq!(p.rect.left, 100.0);
assert_eq!(p.rect.right, 200.0);
assert_eq!(p.rect.bottom, -130.0);
assert_eq!(p.rect.top, 100.0);
assert_eq!(p.width(), 100.0);
assert_eq!(p.height(), 230.0);
}
#[test]
fn a_zero_sized_plate_is_finite() {
let p = Plate::new(Rect::new(50.0, 50.0, 50.0, 50.0), Rotation::None);
assert_eq!(p.width(), 0.0);
assert_eq!(p.height(), 0.0);
let at = p.to_plate(Point::new(50.0, 50.0));
assert!(at.x.is_finite() && at.y.is_finite());
}
#[test]
fn a_quarter_turn_moves_the_origin_corner() {
let p = plate(Rotation::Quarter);
assert!(close(
p.to_plate(Point::new(20.0, 30.0)),
Point::new(0.0, 0.0)
));
}
}