use crate::math::Vec2;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Point {
pub x: i16,
pub y: i16,
}
impl Point {
pub const ORIGIN: Point = Point { x: 0, y: 0 };
pub const MIN_COORD: i16 = -16384;
pub const MAX_COORD: i16 = 16383;
#[inline]
pub const fn in_range(self) -> bool {
self.x >= Self::MIN_COORD
&& self.x <= Self::MAX_COORD
&& self.y >= Self::MIN_COORD
&& self.y <= Self::MAX_COORD
}
#[inline]
pub const fn new(x: i16, y: i16) -> Self {
Point { x, y }
}
#[inline]
pub(crate) fn to_vec2(self) -> Vec2 {
Vec2::new(self.x as f32, self.y as f32)
}
#[inline]
pub(crate) fn from_vec2_rounded(v: Vec2) -> Self {
Point {
x: round_to_i16(v.x),
y: round_to_i16(v.y),
}
}
#[inline]
pub fn distance_squared(self, other: Point) -> u32 {
let dx = (self.x as i32 - other.x as i32).unsigned_abs();
let dy = (self.y as i32 - other.y as i32).unsigned_abs();
dx.saturating_mul(dx).saturating_add(dy.saturating_mul(dy))
}
}
#[inline]
fn round_to_i16(v: f32) -> i16 {
if v.is_nan() {
return 0;
}
let r = round_half_away_from_zero(v);
if r <= i16::MIN as f32 {
i16::MIN
} else if r >= i16::MAX as f32 {
i16::MAX
} else {
r as i16
}
}
#[inline]
pub(crate) fn round_half_away_from_zero(v: f32) -> f32 {
if v >= 0.0 {
let t = (v + 0.5) as i32 as f32;
if t - v > 0.5 {
t - 1.0
} else {
t
}
} else {
let t = (v - 0.5) as i32 as f32;
if v - t > 0.5 {
t + 1.0
} else {
t
}
}
}
impl From<(i16, i16)> for Point {
#[inline]
fn from((x, y): (i16, i16)) -> Self {
Point::new(x, y)
}
}
impl From<[i16; 2]> for Point {
#[inline]
fn from([x, y]: [i16; 2]) -> Self {
Point::new(x, y)
}
}
impl From<Point> for (i16, i16) {
#[inline]
fn from(p: Point) -> Self {
(p.x, p.y)
}
}
impl From<Point> for [i16; 2] {
#[inline]
fn from(p: Point) -> Self {
[p.x, p.y]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rounds_to_nearest() {
assert_eq!(round_to_i16(0.4), 0);
assert_eq!(round_to_i16(0.5), 1);
assert_eq!(round_to_i16(0.6), 1);
assert_eq!(round_to_i16(-0.4), 0);
assert_eq!(round_to_i16(-0.5), -1);
assert_eq!(round_to_i16(-0.6), -1);
assert_eq!(round_to_i16(1.5), 2);
assert_eq!(round_to_i16(2.5), 3);
assert_eq!(round_to_i16(-2.5), -3);
}
#[test]
fn rounding_saturates_instead_of_wrapping() {
assert_eq!(round_to_i16(1e9), i16::MAX);
assert_eq!(round_to_i16(-1e9), i16::MIN);
assert_eq!(round_to_i16(f32::INFINITY), i16::MAX);
assert_eq!(round_to_i16(f32::NEG_INFINITY), i16::MIN);
assert_eq!(round_to_i16(f32::NAN), 0);
assert_eq!(round_to_i16(32767.4), i16::MAX);
assert_eq!(round_to_i16(-32768.4), i16::MIN);
}
#[test]
fn coordinate_cap_is_where_the_arithmetic_says() {
assert_eq!(Point::MIN_COORD, -16384);
assert_eq!(Point::MAX_COORD, 16383);
assert!(Point::new(0, 0).in_range());
assert!(Point::new(16383, -16384).in_range());
assert!(!Point::new(16384, 0).in_range());
assert!(!Point::new(0, -16385).in_range());
assert!(!Point::new(i16::MAX, i16::MIN).in_range());
}
#[test]
fn distance_squared_is_exact_across_the_whole_capped_range() {
let d = Point::new(Point::MIN_COORD, Point::MIN_COORD)
.distance_squared(Point::new(Point::MAX_COORD, Point::MAX_COORD));
assert_eq!(d, 2 * 32767 * 32767);
}
#[test]
fn distance_squared_is_exact_in_range() {
assert_eq!(Point::new(0, 0).distance_squared(Point::new(3, 4)), 25);
assert_eq!(Point::new(-3, -4).distance_squared(Point::new(0, 0)), 25);
assert_eq!(Point::new(5, 5).distance_squared(Point::new(5, 5)), 0);
}
#[test]
fn distance_squared_saturates_beyond_the_cap_rather_than_wrapping() {
let d = Point::new(i16::MIN, i16::MIN).distance_squared(Point::new(i16::MAX, i16::MAX));
assert_eq!(d, u32::MAX);
assert!(2u64 * 65535 * 65535 > u32::MAX as u64);
}
#[test]
fn conversions_round_trip() {
let p = Point::new(-7, 12);
assert_eq!(Point::from(<(i16, i16)>::from(p)), p);
assert_eq!(Point::from(<[i16; 2]>::from(p)), p);
}
#[test]
fn to_vec2_is_lossless() {
for v in [i16::MIN, -1, 0, 1, i16::MAX] {
let p = Point::new(v, v);
assert_eq!(Point::from_vec2_rounded(p.to_vec2()), p);
}
}
}