gui_pb/transform.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158
//! Transforms between coordinate systems (such as grid/logical <=> screen pixels).
use core_pb::grid::computed_grid::Wall;
use eframe::egui::Pos2;
use nalgebra::Point2;
/// A 2D transform consisting of per-axis scale and translation.
#[derive(Copy, Clone)]
pub struct Transform {
scale_x: f32,
scale_y: f32,
offset_x: f32,
offset_y: f32,
/// If true, x and y swap positions as part of the transform
flipped: bool,
}
impl Transform {
/// Creates a new `Transform` that maps the rect `(src_p1, src_p2)` inside `(dst_p1, dst_p2)`,
/// adding padding/letterboxing so that the src rect fits inside the dst rect while preserving
/// its aspect ratio.
pub fn new_letterboxed(
src_p1: Pos2,
src_p2: Pos2,
dst_p1: Pos2,
dst_p2: Pos2,
flipped: bool,
) -> Self {
// Compare the aspect ratios to determine the letterboxing direction.
let src_width = (src_p1.x - src_p2.x).abs();
let src_height = (src_p1.y - src_p2.y).abs();
let dst_width = (dst_p1.x - dst_p2.x).abs();
let dst_height = (dst_p1.y - dst_p2.y).abs();
if src_height * dst_width > dst_height * src_width {
// The src rectangle's aspect ratio is "taller" than the dst rectangle's; add horizontal padding.
Self::new_horizontal_padded(src_p1, src_p2, dst_p1, dst_p2, flipped)
} else {
// The src rectangle's aspect ratio is "wider" than the dst rectangle's; add vertical padding.
fn tr(p: Pos2) -> Pos2 {
Pos2::new(p.y, p.x)
}
Self::new_horizontal_padded(tr(src_p1), tr(src_p2), tr(dst_p1), tr(dst_p2), flipped)
.transpose()
}
}
/// Creates a new `Transform` that maps the rect `(src_p1, src_p2)` inside `(dst_p1, dst_p2)`, adding horizontal padding/letterboxing.
fn new_horizontal_padded(
src_p1: Pos2,
src_p2: Pos2,
dst_p1: Pos2,
dst_p2: Pos2,
flipped: bool,
) -> Self {
let scale_y = (dst_p1.y - dst_p2.y) / (src_p1.y - src_p2.y);
let offset_y = dst_p1.y - src_p1.y * scale_y;
let scale_x = scale_y.copysign((src_p2.x - src_p1.x) * (dst_p2.x - dst_p1.x));
let src_x_middle = (src_p1.x + src_p2.x) / 2.0;
let dst_x_middle = (dst_p1.x + dst_p2.x) / 2.0;
let offset_x = dst_x_middle - src_x_middle * scale_x;
Self {
scale_x,
scale_y,
offset_x,
offset_y,
flipped,
}
}
/// Swaps the X and Y components of this `Transform`.
pub fn transpose(&self) -> Self {
Self {
scale_x: self.scale_y,
scale_y: self.scale_x,
offset_x: self.offset_y,
offset_y: self.offset_x,
flipped: self.flipped,
}
}
/// Returns the inverse `Transform`.
/// Panics if the transformation is not invertible.
pub fn inverse(&self) -> Self {
assert!(self.scale_x != 0.0);
assert!(self.scale_y != 0.0);
if self.flipped {
Self {
scale_x: self.scale_x.recip(),
scale_y: self.scale_y.recip(),
offset_x: -self.offset_x / self.scale_x,
offset_y: -self.offset_y / self.scale_y,
flipped: self.flipped,
}
} else {
Self {
scale_x: self.scale_y.recip(),
scale_y: self.scale_x.recip(),
offset_x: -self.offset_y / self.scale_y,
offset_y: -self.offset_x / self.scale_x,
flipped: self.flipped,
}
}
}
/// Applies the transformation to a point.
pub fn map_point(&self, p: Pos2) -> Pos2 {
if self.flipped {
Pos2::new(
p.x * self.scale_x + self.offset_x,
p.y * self.scale_y + self.offset_y,
)
} else {
Pos2::new(
p.y * self.scale_y + self.offset_y,
p.x * self.scale_x + self.offset_x,
)
}
}
/// Applies the transformation to a Point<f32>
pub fn map_point2(&self, p: Point2<f32>) -> Pos2 {
self.map_point(Pos2::new(p.x, p.y))
}
/// Applies a scalar transformation
pub fn map_dist(&self, x: f32) -> f32 {
(x * self.scale_x).abs() * x.signum()
}
/// Returns the coordinates of the top left and bottom right corners of the [`Wall`] in screen coordinates.
///
/// # Examples
///
/// ```
/// use rapier2d::na::Point2;
/// use eframe::egui::Pos2;
/// use mdrc_pacbot_util::grid::{IntLocation, Wall};
/// use mdrc_pacbot_util::gui::transforms::Transform;
///
/// let world_to_screen = Transform::new_letterboxed(
/// Pos2::new(-1.0, -1.0),
/// Pos2::new(32.0, 32.0),
/// Pos2::new(0.0, 0.0),
/// Pos2::new(330.0, 330.0),
/// );
/// let wall = Wall {
/// top_left: IntLocation::new(1, 2),
/// bottom_right: IntLocation::new(2, 2),
/// };
/// let (top_left, bottom_right) = world_to_screen.map_wall(&wall);
/// assert_eq!(top_left, Pos2::new(30.0, 20.0));
/// ```
pub fn map_wall(&self, wall: &Wall) -> (Pos2, Pos2) {
let top_left = Pos2::new(wall.top_left.x as f32, wall.top_left.y as f32);
let bottom_right = Pos2::new(wall.bottom_right.x as f32, wall.bottom_right.y as f32);
(self.map_point(top_left), self.map_point(bottom_right))
}
}