use crate::region::Region;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Calibration {
pub origin_x: f64,
pub origin_y: f64,
pub cell_width: f64,
pub cell_height: f64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct PixelRect {
pub x_coordinate: f64,
pub y_coordinate: f64,
pub width: f64,
pub height: f64,
}
impl Calibration {
pub fn new(origin_x: f64, origin_y: f64, cell_width: f64, cell_height: f64) -> Self {
Self {
origin_x,
origin_y,
cell_width,
cell_height,
}
}
pub fn from_image_size(image_width: u32, image_height: u32, cols: u16, rows: u16) -> Self {
let cell_width = f64::from(image_width) / f64::from(cols);
let cell_height = f64::from(image_height) / f64::from(rows);
Self {
origin_x: 0.0,
origin_y: 0.0,
cell_width,
cell_height,
}
}
pub fn region_to_pixels(&self, region: &Region) -> PixelRect {
PixelRect {
x_coordinate: self.origin_x + f64::from(region.col) * self.cell_width,
y_coordinate: self.origin_y + f64::from(region.row) * self.cell_height,
width: f64::from(region.width) * self.cell_width,
height: f64::from(region.height) * self.cell_height,
}
}
pub fn pixel_to_cell(&self, pixel_x: f64, pixel_y: f64) -> (u16, u16) {
let col = safe_f64_to_u16(((pixel_x - self.origin_x) / self.cell_width).floor());
let row = safe_f64_to_u16(((pixel_y - self.origin_y) / self.cell_height).floor());
(col, row)
}
}
impl PixelRect {
pub fn right(&self) -> f64 {
self.x_coordinate + self.width
}
pub fn bottom(&self) -> f64 {
self.y_coordinate + self.height
}
pub fn to_integer_bounds(&self) -> (u32, u32, u32, u32) {
(
safe_f64_to_u32(self.x_coordinate.round()),
safe_f64_to_u32(self.y_coordinate.round()),
safe_f64_to_u32(self.width.round()),
safe_f64_to_u32(self.height.round()),
)
}
}
#[expect(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "value is clamped to [0.0, 65535.0] before the cast"
)]
fn safe_f64_to_u16(value: f64) -> u16 {
value.clamp(0.0, f64::from(u16::MAX)) as u16
}
#[expect(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "value is clamped to [0.0, u32::MAX as f64] before the cast"
)]
fn safe_f64_to_u32(value: f64) -> u32 {
value.clamp(0.0, f64::from(u32::MAX)) as u32
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn from_image_size_computes_cell_dimensions() {
let calibration = Calibration::from_image_size(800, 480, 80, 24);
assert!((calibration.cell_width - 10.0).abs() < f64::EPSILON);
assert!((calibration.cell_height - 20.0).abs() < f64::EPSILON);
assert!((calibration.origin_x).abs() < f64::EPSILON);
}
#[test]
fn region_to_pixels_maps_correctly() {
let calibration = Calibration::new(0.0, 0.0, 10.0, 20.0);
let region = Region::new(5, 2, 10, 3);
let pixel_rect = calibration.region_to_pixels(®ion);
assert!((pixel_rect.x_coordinate - 50.0).abs() < f64::EPSILON);
assert!((pixel_rect.y_coordinate - 40.0).abs() < f64::EPSILON);
assert!((pixel_rect.width - 100.0).abs() < f64::EPSILON);
assert!((pixel_rect.height - 60.0).abs() < f64::EPSILON);
}
#[test]
fn region_to_pixels_with_origin_offset() {
let calibration = Calibration::new(10.0, 5.0, 8.0, 16.0);
let region = Region::new(0, 0, 1, 1);
let pixel_rect = calibration.region_to_pixels(®ion);
assert!((pixel_rect.x_coordinate - 10.0).abs() < f64::EPSILON);
assert!((pixel_rect.y_coordinate - 5.0).abs() < f64::EPSILON);
}
#[test]
fn pixel_to_cell_round_trips() {
let calibration = Calibration::new(0.0, 0.0, 10.0, 20.0);
let (col, row) = calibration.pixel_to_cell(55.0, 45.0);
assert_eq!(col, 5);
assert_eq!(row, 2);
}
#[test]
fn pixel_rect_to_integer_bounds() {
let rect = PixelRect {
x_coordinate: 10.4,
y_coordinate: 20.6,
width: 100.3,
height: 50.7,
};
let (x_bound, y_bound, width_bound, height_bound) = rect.to_integer_bounds();
assert_eq!(x_bound, 10);
assert_eq!(y_bound, 21);
assert_eq!(width_bound, 100);
assert_eq!(height_bound, 51);
}
}