use crate::error::Error;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Size {
pub width: usize,
pub height: usize,
}
impl Size {
#[inline]
pub fn new(width: usize, height: usize) -> Self {
Self { width, height }
}
#[inline]
pub fn area(&self) -> usize {
self.width
.checked_mul(self.height)
.expect("Size::area: width * height overflows usize")
}
#[inline]
pub fn checked_area(&self) -> Option<usize> {
self.width.checked_mul(self.height)
}
}
impl From<(usize, usize)> for Size {
#[inline]
fn from(value: (usize, usize)) -> Self {
Self::new(value.0, value.1)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Coordinate {
pub x: usize,
pub y: usize,
}
impl Coordinate {
#[inline]
pub fn new(x: usize, y: usize) -> Self {
Self { x, y }
}
#[inline]
#[must_use]
pub const fn checked_add(self, offset: Offset) -> Option<Self> {
match (
self.x.checked_add_signed(offset.dx as isize),
self.y.checked_add_signed(offset.dy as isize),
) {
(Some(x), Some(y)) => Some(Self { x, y }),
_ => None,
}
}
#[inline]
#[must_use]
pub fn offset_to(self, other: Self) -> Offset {
#[inline]
fn delta(from: usize, to: usize) -> i32 {
if to >= from {
i32::try_from(to - from).unwrap_or(i32::MAX)
} else {
i32::try_from(from - to).map_or(i32::MIN, |d| -d)
}
}
Offset::new(delta(self.x, other.x), delta(self.y, other.y))
}
}
impl From<(usize, usize)> for Coordinate {
#[inline]
fn from(value: (usize, usize)) -> Self {
Self::new(value.0, value.1)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct CoordinateF64 {
pub x: f64,
pub y: f64,
}
impl CoordinateF64 {
#[inline]
pub fn new(x: f64, y: f64) -> Self {
Self { x, y }
}
}
impl From<(f64, f64)> for CoordinateF64 {
#[inline]
fn from(value: (f64, f64)) -> Self {
Self::new(value.0, value.1)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct CoordinateI32 {
pub x: i32,
pub y: i32,
}
impl CoordinateI32 {
#[inline]
#[must_use]
pub const fn new(x: i32, y: i32) -> Self {
Self { x, y }
}
}
impl From<(i32, i32)> for CoordinateI32 {
#[inline]
fn from(value: (i32, i32)) -> Self {
Self::new(value.0, value.1)
}
}
impl From<&(i32, i32)> for CoordinateI32 {
#[inline]
fn from(value: &(i32, i32)) -> Self {
Self::new(value.0, value.1)
}
}
impl From<&CoordinateI32> for CoordinateI32 {
#[inline]
fn from(value: &CoordinateI32) -> Self {
*value
}
}
impl TryFrom<Coordinate> for CoordinateI32 {
type Error = Error;
fn try_from(value: Coordinate) -> Result<Self, Error> {
match (i32::try_from(value.x), i32::try_from(value.y)) {
(Ok(x), Ok(y)) => Ok(Self { x, y }),
_ => Err(Error::InvalidParameter(format!(
"coordinate ({}, {}) does not fit a signed 32-bit position",
value.x, value.y
))),
}
}
}
impl From<Coordinate> for CoordinateF64 {
#[inline]
fn from(value: Coordinate) -> Self {
Self::new(value.x as f64, value.y as f64)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Offset {
pub dx: i32,
pub dy: i32,
}
impl Offset {
pub const ZERO: Self = Self::new(0, 0);
#[inline]
#[must_use]
pub const fn new(dx: i32, dy: i32) -> Self {
Self { dx, dy }
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Rectangle {
pub offset: Coordinate,
pub size: Size,
}
impl Rectangle {
#[inline]
pub fn new(offset: impl Into<Coordinate>, size: impl Into<Size>) -> Self {
Self {
offset: offset.into(),
size: size.into(),
}
}
#[inline]
pub fn area(&self) -> usize {
self.size.area()
}
#[inline]
pub fn left(&self) -> usize {
self.offset.x
}
#[inline]
pub fn right(&self) -> usize {
self.offset
.x
.checked_add(self.size.width)
.expect("Rectangle::right: offset.x + size.width overflows usize")
}
#[inline]
pub fn top(&self) -> usize {
self.offset.y
}
#[inline]
pub fn bottom(&self) -> usize {
self.offset
.y
.checked_add(self.size.height)
.expect("Rectangle::bottom: offset.y + size.height overflows usize")
}
#[inline]
pub fn checked_right(&self) -> Option<usize> {
self.offset.x.checked_add(self.size.width)
}
#[inline]
pub fn checked_bottom(&self) -> Option<usize> {
self.offset.y.checked_add(self.size.height)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Stride(Size);
impl Stride {
#[inline]
pub fn new(horizontal: usize, vertical: usize) -> Self {
Self(Size::new(horizontal, vertical))
}
#[inline]
pub fn one() -> Self {
Self(Size::new(1, 1))
}
#[inline]
pub fn horizontal(&self) -> usize {
self.0.width
}
#[inline]
pub fn vertical(&self) -> usize {
self.0.height
}
#[inline]
pub fn as_size(&self) -> Size {
self.0
}
}
impl From<Size> for Stride {
#[inline]
fn from(size: Size) -> Self {
Self(size)
}
}
impl From<(usize, usize)> for Stride {
#[inline]
fn from(value: (usize, usize)) -> Self {
Self(Size::new(value.0, value.1))
}
}
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
#[repr(transparent)]
pub struct Sigma(f32);
impl Sigma {
#[must_use]
pub const fn new(value: f32) -> Option<Self> {
if value.is_finite() && value > 0.0 {
Some(Self(value))
} else {
None
}
}
pub fn try_new(value: f32) -> Result<Self, Error> {
if value.is_finite() && value > 0.0 {
Ok(Self(value))
} else {
Err(Error::InvalidParameter(format!(
"sigma must be finite and strictly positive, got {value}"
)))
}
}
#[must_use]
#[inline]
pub const fn get(self) -> f32 {
self.0
}
}
#[macro_export]
macro_rules! sigma {
($value:expr) => {
const { $crate::Sigma::new($value).expect("sigma must be finite and strictly positive") }
};
}
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
#[repr(transparent)]
pub struct PixelDistance(f64);
impl PixelDistance {
#[must_use]
pub const fn new(value: f64) -> Option<Self> {
if value.is_finite() && value > 0.0 {
Some(Self(value))
} else {
None
}
}
pub fn try_new(value: f64) -> Result<Self, Error> {
if value.is_finite() && value > 0.0 {
Ok(Self(value))
} else {
Err(Error::InvalidParameter(format!(
"pixel distance must be finite and strictly positive, got {value}"
)))
}
}
#[must_use]
#[inline]
pub const fn get(self) -> f64 {
self.0
}
}
#[macro_export]
macro_rules! pixel_distance {
($value:expr) => {
const {
$crate::PixelDistance::new($value)
.expect("pixel distance must be finite and strictly positive")
}
};
}
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
#[repr(transparent)]
pub struct Tolerance(f64);
impl Tolerance {
#[must_use]
pub const fn new(value: f64) -> Option<Self> {
if value.is_finite() && value >= 0.0 {
Some(Self(value))
} else {
None
}
}
pub fn try_new(value: f64) -> Result<Self, Error> {
if value.is_finite() && value >= 0.0 {
Ok(Self(value))
} else {
Err(Error::InvalidParameter(format!(
"tolerance must be finite and non-negative, got {value}"
)))
}
}
#[must_use]
#[inline]
pub const fn get(self) -> f64 {
self.0
}
}
#[macro_export]
macro_rules! tolerance {
($value:expr) => {
const { $crate::Tolerance::new($value).expect("tolerance must be finite and non-negative") }
};
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(transparent)]
pub struct OddWindowSide(usize);
impl OddWindowSide {
#[must_use]
pub const fn new(side: usize) -> Option<Self> {
if side != 0 && side % 2 == 1 {
Some(Self(side))
} else {
None
}
}
pub fn try_new(side: usize) -> Result<Self, Error> {
if side != 0 && side % 2 == 1 {
Ok(Self(side))
} else {
Err(Error::InvalidParameter(format!(
"window side must be odd and non-zero, got {side}"
)))
}
}
#[must_use]
#[inline]
pub const fn get(self) -> usize {
self.0
}
#[must_use]
pub const fn radius(self) -> usize {
self.0 / 2
}
}
#[macro_export]
macro_rules! window {
($side:expr) => {
const { $crate::OddWindowSide::new($side).expect("window side must be odd and non-zero") }
};
}
fn wrap_two_pi(radians: f32) -> f32 {
const PI: f32 = core::f32::consts::PI;
let wrapped = radians.rem_euclid(2.0 * PI); if wrapped > PI {
wrapped - 2.0 * PI
} else {
wrapped
}
}
fn wrap_pi(radians: f64) -> f64 {
const PI: f64 = core::f64::consts::PI;
let wrapped = radians.rem_euclid(PI); if wrapped > PI / 2.0 {
wrapped - PI
} else {
wrapped
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Extremum {
Maximum,
Minimum,
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[repr(transparent)]
pub struct Orientation(f32);
impl Orientation {
pub fn from_radians(radians: f32) -> Result<Self, Error> {
if radians.is_finite() {
Ok(Self(wrap_two_pi(radians)))
} else {
Err(Error::InvalidParameter(format!(
"orientation must be a finite angle in radians, got {radians}"
)))
}
}
#[must_use]
#[inline]
pub fn from_atan2(y: f32, x: f32) -> Self {
let angle = y.atan2(x);
if angle == -core::f32::consts::PI {
Self(core::f32::consts::PI)
} else {
Self(angle)
}
}
#[must_use]
#[inline]
pub const fn radians(self) -> f32 {
self.0
}
#[must_use]
#[inline]
pub fn signed_difference(self, other: Self) -> f32 {
wrap_two_pi(self.0 - other.0)
}
#[must_use]
#[inline]
pub fn to_axial(self) -> AxialOrientation {
AxialOrientation(wrap_pi(f64::from(self.0)))
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[repr(transparent)]
pub struct AxialOrientation(f64);
impl AxialOrientation {
pub fn from_radians(radians: f64) -> Result<Self, Error> {
if radians.is_finite() {
Ok(Self(wrap_pi(radians)))
} else {
Err(Error::InvalidParameter(format!(
"axis orientation must be a finite angle in radians, got {radians}"
)))
}
}
#[must_use]
#[inline]
pub fn from_half_atan2(y: f64, x: f64) -> Self {
let axis = 0.5 * y.atan2(x);
if axis == -core::f64::consts::FRAC_PI_2 {
Self(core::f64::consts::FRAC_PI_2)
} else {
Self(axis)
}
}
#[must_use]
#[inline]
pub const fn radians(self) -> f64 {
self.0
}
#[must_use]
#[inline]
pub fn signed_difference(self, other: Self) -> f64 {
wrap_pi(self.0 - other.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sigma_valid_values_round_trip() {
assert_eq!(sigma!(1.4).get(), 1.4);
assert_eq!(Sigma::try_new(0.5).unwrap().get(), 0.5);
const S: Sigma = sigma!(2.0);
assert_eq!(S.get(), 2.0);
}
#[test]
fn sigma_try_new_rejects_invalid_values() {
for value in [0.0, -1.0, f32::NAN, f32::INFINITY] {
let err = Sigma::try_new(value).unwrap_err();
match err {
Error::InvalidParameter(reason) => assert!(
reason.contains("sigma"),
"reason {reason:?} does not mention sigma"
),
other => panic!("expected InvalidParameter, got {other:?}"),
}
}
}
#[test]
fn sigma_new_rejects_an_invalid_value() {
assert!(Sigma::new(-1.5).is_none());
assert!(Sigma::new(0.0).is_none());
assert!(Sigma::new(f32::NAN).is_none());
assert!(Sigma::new(f32::INFINITY).is_none());
}
#[test]
fn pixel_distance_valid_values_round_trip() {
assert_eq!(pixel_distance!(2.0).get(), 2.0);
assert_eq!(PixelDistance::try_new(0.5).unwrap().get(), 0.5);
const D: PixelDistance = pixel_distance!(0.5);
assert_eq!(D.get(), 0.5);
}
#[test]
fn pixel_distance_try_new_rejects_invalid_values() {
for value in [0.0, -2.0, f64::NAN, f64::INFINITY] {
let err = PixelDistance::try_new(value).unwrap_err();
match err {
Error::InvalidParameter(reason) => assert!(
reason.contains("pixel distance"),
"reason {reason:?} does not mention pixel distance"
),
other => panic!("expected InvalidParameter, got {other:?}"),
}
}
}
#[test]
fn pixel_distance_new_rejects_an_invalid_value() {
assert!(PixelDistance::new(0.0).is_none());
assert!(PixelDistance::new(-2.0).is_none());
assert!(PixelDistance::new(f64::NAN).is_none());
}
#[test]
fn odd_window_side_valid_values_round_trip() {
assert_eq!(window!(31).get(), 31);
assert_eq!(OddWindowSide::try_new(3).unwrap().get(), 3);
const W: OddWindowSide = window!(15);
assert_eq!(W.get(), 15);
assert_eq!(window!(1).radius(), 0);
}
#[test]
fn odd_window_side_try_new_rejects_even_and_zero() {
for side in [0, 2, 4, 100] {
let err = OddWindowSide::try_new(side).unwrap_err();
match err {
Error::InvalidParameter(reason) => assert!(
reason.contains("odd and non-zero"),
"reason {reason:?} does not name the invariant"
),
other => panic!("expected InvalidParameter, got {other:?}"),
}
}
}
#[test]
fn odd_window_side_new_rejects_an_invalid_side() {
assert!(OddWindowSide::new(8).is_none());
assert!(OddWindowSide::new(0).is_none());
}
#[test]
fn odd_window_side_radius_is_the_exact_half_width() {
for side in [1, 3, 5, 31, 101] {
let w = OddWindowSide::new(side).unwrap();
assert_eq!(w.radius(), side / 2);
assert_eq!(2 * w.radius() + 1, side);
}
}
const PI32: f32 = core::f32::consts::PI;
const PI64: f64 = core::f64::consts::PI;
#[test]
fn orientation_canonicalizes_into_half_open_range() {
assert_eq!(Orientation::from_radians(PI32).unwrap().radians(), PI32);
assert_eq!(Orientation::from_radians(-PI32).unwrap().radians(), PI32);
assert_eq!(Orientation::from_radians(0.0).unwrap().radians(), 0.0);
for turns in [-3.0, -1.5, -0.25, 0.0, 0.75, 2.0, 5.5] {
let a = Orientation::from_radians(turns * PI32).unwrap();
assert!(
a.radians() > -PI32 && a.radians() <= PI32,
"{turns} turns → {}",
a.radians()
);
}
}
#[test]
fn orientation_wraps_full_turns_to_the_same_direction() {
let zero = Orientation::from_radians(0.0).unwrap();
let full = Orientation::from_radians(2.0 * PI32).unwrap();
assert_eq!(zero, full);
let three_halves = Orientation::from_radians(3.0 * PI32).unwrap();
let half = Orientation::from_radians(PI32).unwrap();
assert!(three_halves.signed_difference(half).abs() < 1e-6);
}
#[test]
fn orientation_from_radians_rejects_non_finite() {
for value in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
let err = Orientation::from_radians(value).unwrap_err();
match err {
Error::InvalidParameter(reason) => assert!(
reason.contains("orientation"),
"reason {reason:?} does not mention orientation"
),
other => panic!("expected InvalidParameter, got {other:?}"),
}
}
}
#[test]
fn orientation_from_atan2_is_total_and_canonical() {
assert_eq!(Orientation::from_atan2(0.0, 1.0).radians(), 0.0);
assert_eq!(Orientation::from_atan2(1.0, 0.0).radians(), PI32 / 2.0);
assert_eq!(Orientation::from_atan2(0.0, -1.0).radians(), PI32);
assert_eq!(Orientation::from_atan2(0.0, 0.0).radians(), 0.0);
assert_eq!(Orientation::from_atan2(-0.0, -1.0).radians(), PI32);
assert!(Orientation::from_atan2(-0.0, 1.0).radians() == 0.0);
}
#[test]
fn orientation_signed_difference_crosses_the_seam() {
let east = Orientation::from_radians(179_f32.to_radians()).unwrap();
let west = Orientation::from_radians((-179_f32).to_radians()).unwrap();
let delta = east.signed_difference(west).to_degrees();
assert!((delta.abs() - 2.0).abs() < 1e-3, "got {delta}");
}
#[test]
fn orientation_signed_difference_is_signed_and_directed() {
let a = Orientation::from_radians(0.5).unwrap();
let b = Orientation::from_radians(0.2).unwrap();
assert!((a.signed_difference(b) - 0.3).abs() < 1e-6);
assert!((b.signed_difference(a) + 0.3).abs() < 1e-6);
assert_eq!(a.signed_difference(a), 0.0);
}
#[test]
fn orientation_signed_difference_never_exceeds_pi() {
for degrees in [0.0_f32, 45.0, 90.0, 179.0, 181.0, 270.0, 359.0] {
let a = Orientation::from_radians(degrees.to_radians()).unwrap();
let b = Orientation::from_radians(0.0).unwrap();
assert!(
a.signed_difference(b).abs() <= PI32 + 1e-6,
"{degrees}° → {}",
a.signed_difference(b)
);
}
}
#[test]
fn orientation_to_axial_collapses_opposite_directions() {
let north = Orientation::from_radians(PI32 / 2.0).unwrap();
let south = Orientation::from_radians(-PI32 / 2.0).unwrap();
assert!(north.to_axial().signed_difference(south.to_axial()).abs() < 1e-6);
let east = Orientation::from_radians(0.0).unwrap();
let west = Orientation::from_radians(PI32).unwrap();
assert!(east.to_axial().signed_difference(west.to_axial()).abs() < 1e-6);
let apart = north.to_axial().signed_difference(east.to_axial()).abs();
assert!((apart - PI64 / 2.0).abs() < 1e-6, "got {apart}");
}
#[test]
fn orientation_is_copy_and_debug() {
let a = Orientation::from_radians(1.0).unwrap();
let b = a; assert_eq!(a, b);
assert!(format!("{a:?}").contains("Orientation"));
}
#[test]
fn axial_orientation_canonicalizes_into_quarter_turn_range() {
let half_pi = PI64 / 2.0;
assert_eq!(
AxialOrientation::from_radians(half_pi).unwrap().radians(),
half_pi
);
assert_eq!(
AxialOrientation::from_radians(-half_pi).unwrap().radians(),
half_pi
);
for turns in [-2.0, -0.75, 0.0, 0.3, 1.0, 3.5] {
let a = AxialOrientation::from_radians(turns * PI64).unwrap();
assert!(
a.radians() > -half_pi && a.radians() <= half_pi,
"{turns}·π → {}",
a.radians()
);
}
}
#[test]
fn axial_orientation_treats_opposite_angles_as_one_axis() {
let a = AxialOrientation::from_radians(0.4).unwrap();
let b = AxialOrientation::from_radians(0.4 + PI64).unwrap();
assert!(a.signed_difference(b).abs() < 1e-12);
let c = AxialOrientation::from_radians(80_f64.to_radians()).unwrap();
let d = AxialOrientation::from_radians((-100_f64).to_radians()).unwrap();
assert!(c.signed_difference(d).abs() < 1e-12);
}
#[test]
fn axial_orientation_from_radians_rejects_non_finite() {
for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
let err = AxialOrientation::from_radians(value).unwrap_err();
match err {
Error::InvalidParameter(reason) => assert!(
reason.contains("axis orientation"),
"reason {reason:?} does not mention axis orientation"
),
other => panic!("expected InvalidParameter, got {other:?}"),
}
}
}
#[test]
fn axial_orientation_from_half_atan2_is_total_and_canonical() {
assert_eq!(AxialOrientation::from_half_atan2(0.0, 1.0).radians(), 0.0);
assert_eq!(
AxialOrientation::from_half_atan2(0.0, -1.0).radians(),
PI64 / 2.0
);
assert_eq!(AxialOrientation::from_half_atan2(0.0, 0.0).radians(), 0.0);
assert_eq!(
AxialOrientation::from_half_atan2(-0.0, -1.0).radians(),
PI64 / 2.0
);
}
#[test]
fn axial_orientation_difference_never_exceeds_a_quarter_turn() {
let a = AxialOrientation::from_radians(80_f64.to_radians()).unwrap();
let b = AxialOrientation::from_radians((-80_f64).to_radians()).unwrap();
let delta = a.signed_difference(b).to_degrees();
assert!((delta.abs() - 20.0).abs() < 1e-9, "got {delta}");
for degrees in [0.0_f64, 10.0, 89.0, 91.0, 170.0, 269.0] {
let x = AxialOrientation::from_radians(degrees.to_radians()).unwrap();
let y = AxialOrientation::from_radians(0.0).unwrap();
assert!(
x.signed_difference(y).abs() <= PI64 / 2.0 + 1e-12,
"{degrees}° → {}",
x.signed_difference(y)
);
}
}
#[test]
fn axial_orientation_is_copy_and_debug() {
let a = AxialOrientation::from_radians(1.0).unwrap();
let b = a; assert_eq!(a, b);
assert!(format!("{a:?}").contains("AxialOrientation"));
}
#[test]
fn test_size_new() {
let size = Size::new(640, 480);
assert_eq!(size.width, 640);
assert_eq!(size.height, 480);
}
#[test]
fn test_size_area() {
let size = Size::new(10, 20);
assert_eq!(size.area(), 200);
}
#[test]
fn test_size_from_tuple() {
let size = Size::from((100, 200));
assert_eq!(size.width, 100);
assert_eq!(size.height, 200);
}
#[test]
fn test_size_clone() {
let size1 = Size::new(50, 60);
let size2 = size1;
assert_eq!(size1, size2);
}
#[test]
fn test_size_copy() {
let size1 = Size::new(50, 60);
let size2 = size1; assert_eq!(size1, size2); assert_eq!(size1.width, 50);
assert_eq!(size1.height, 60);
}
#[test]
fn test_coordinate_new() {
let coord = Coordinate::new(10, 20);
assert_eq!(coord.x, 10);
assert_eq!(coord.y, 20);
}
#[test]
fn test_coordinate_from_tuple() {
let coord = Coordinate::from((15, 25));
assert_eq!(coord.x, 15);
assert_eq!(coord.y, 25);
}
#[test]
fn test_coordinate_copy() {
let coord1 = Coordinate::new(5, 10);
let coord2 = coord1;
assert_eq!(coord1, coord2);
}
#[test]
fn test_rectangle_new() {
let rect = Rectangle::new((10, 20), (100, 50));
assert_eq!(rect.offset.x, 10);
assert_eq!(rect.offset.y, 20);
assert_eq!(rect.size.width, 100);
assert_eq!(rect.size.height, 50);
}
#[test]
fn test_rectangle_new_with_coordinate_and_size() {
let rect = Rectangle::new(Coordinate::new(5, 15), Size::new(200, 100));
assert_eq!(rect.offset.x, 5);
assert_eq!(rect.offset.y, 15);
assert_eq!(rect.size.width, 200);
assert_eq!(rect.size.height, 100);
}
#[test]
fn test_rectangle_area() {
let rect = Rectangle::new((0, 0), (10, 20));
assert_eq!(rect.area(), 200);
}
#[test]
fn test_rectangle_left() {
let rect = Rectangle::new((10, 20), (100, 50));
assert_eq!(rect.left(), 10);
}
#[test]
fn test_rectangle_right() {
let rect = Rectangle::new((10, 20), (100, 50));
assert_eq!(rect.right(), 110);
}
#[test]
fn test_rectangle_top() {
let rect = Rectangle::new((10, 20), (100, 50));
assert_eq!(rect.top(), 20);
}
#[test]
fn test_rectangle_bottom() {
let rect = Rectangle::new((10, 20), (100, 50));
assert_eq!(rect.bottom(), 70);
}
#[test]
fn test_rectangle_clone() {
let rect1 = Rectangle::new((5, 10), (50, 60));
let rect2 = rect1;
assert_eq!(rect1, rect2);
}
#[test]
fn test_rectangle_copy() {
let rect1 = Rectangle::new((5, 10), (50, 60));
let rect2 = rect1; assert_eq!(rect1, rect2); assert_eq!(rect1.offset.x, 5);
assert_eq!(rect1.size.width, 50);
}
#[test]
fn test_rectangle_zero_area() {
let rect = Rectangle::new((0, 0), (0, 0));
assert_eq!(rect.area(), 0);
}
#[test]
fn test_stride_new() {
let s = Stride::new(3, 5);
assert_eq!(s.horizontal(), 3);
assert_eq!(s.vertical(), 5);
}
#[test]
fn test_stride_one() {
let s = Stride::one();
assert_eq!(s.horizontal(), 1);
assert_eq!(s.vertical(), 1);
}
#[test]
fn test_stride_as_size() {
let s = Stride::new(4, 7);
let sz = s.as_size();
assert_eq!(sz.width, 4);
assert_eq!(sz.height, 7);
}
#[test]
fn test_stride_from_size() {
let sz = Size::new(2, 3);
let s = Stride::from(sz);
assert_eq!(s.horizontal(), 2);
assert_eq!(s.vertical(), 3);
}
#[test]
fn test_stride_from_tuple() {
let s = Stride::from((10, 20));
assert_eq!(s.horizontal(), 10);
assert_eq!(s.vertical(), 20);
}
#[test]
fn test_stride_copy() {
let s1 = Stride::new(5, 6);
let s2 = s1; assert_eq!(s1, s2);
assert_eq!(s1.horizontal(), 5);
}
#[test]
fn test_stride_clone() {
let s1 = Stride::new(8, 9);
let s2 = s1;
assert_eq!(s1, s2);
}
#[test]
fn test_stride_debug() {
let s = Stride::new(1, 2);
let dbg = format!("{:?}", s);
assert!(dbg.contains("Stride"));
}
#[test]
fn test_stride_eq() {
assert_eq!(Stride::new(3, 3), Stride::new(3, 3));
assert_ne!(Stride::new(3, 3), Stride::new(3, 4));
assert_ne!(Stride::new(3, 3), Stride::new(4, 3));
}
#[test]
fn size_checked_area_returns_some_for_normal_values() {
assert_eq!(Size::new(10, 20).checked_area(), Some(200));
assert_eq!(Size::new(0, usize::MAX).checked_area(), Some(0));
}
#[test]
fn size_checked_area_returns_none_on_overflow() {
let huge = Size::new(usize::MAX, 2);
assert!(huge.checked_area().is_none());
}
#[test]
#[should_panic(expected = "overflow")]
fn size_area_panics_on_overflow() {
let huge = Size::new(usize::MAX, 2);
let _ = huge.area();
}
#[test]
fn rectangle_checked_right_and_bottom() {
let r = Rectangle::new((10, 20), (100, 50));
assert_eq!(r.checked_right(), Some(110));
assert_eq!(r.checked_bottom(), Some(70));
let r2 = Rectangle::new((usize::MAX - 1, 0), (10, 1));
assert!(r2.checked_right().is_none());
let r3 = Rectangle::new((0, usize::MAX - 1), (1, 10));
assert!(r3.checked_bottom().is_none());
}
#[test]
#[should_panic(expected = "overflow")]
fn rectangle_right_panics_on_overflow() {
let r = Rectangle::new((usize::MAX - 1, 0), (10, 1));
let _ = r.right();
}
#[test]
#[should_panic(expected = "overflow")]
fn rectangle_bottom_panics_on_overflow() {
let r = Rectangle::new((0, usize::MAX - 1), (1, 10));
let _ = r.bottom();
}
#[test]
fn checked_add_rejects_only_the_negative_half() {
let c = Coordinate::new(0, 0);
assert_eq!(c.checked_add(Offset::ZERO), Some(c));
assert_eq!(c.checked_add(Offset::new(-1, 0)), None);
assert_eq!(c.checked_add(Offset::new(0, -1)), None);
assert_eq!(
c.checked_add(Offset::new(3, 4)),
Some(Coordinate::new(3, 4))
);
assert_eq!(
Coordinate::new(usize::MAX - 1, 0).checked_add(Offset::new(1, 0)),
Some(Coordinate::new(usize::MAX, 0))
);
assert_eq!(
Coordinate::new(usize::MAX, 0).checked_add(Offset::new(1, 0)),
None
);
}
#[test]
fn offset_to_inverts_checked_add() {
let corners = [
Coordinate::new(0, 0),
Coordinate::new(7, 0),
Coordinate::new(0, 5),
Coordinate::new(7, 5),
];
for &a in &corners {
for &b in &corners {
assert_eq!(a.checked_add(a.offset_to(b)), Some(b), "{a:?} -> {b:?}");
}
}
}
#[test]
fn offset_to_saturates_rather_than_wrapping() {
let far = Coordinate::new(usize::MAX, 0).offset_to(Coordinate::new(0, usize::MAX));
assert_eq!(far, Offset::new(i32::MIN, i32::MAX));
}
#[test]
fn an_offset_is_transposition_sensitive() {
assert_ne!(Offset::new(1, -1), Offset::new(-1, 1));
}
}