use super::{CoordCube, SQRT3_2, Vec2d};
use crate::{
Direction,
error::HexGridError,
math::{mul_add, round},
};
use core::{
cmp, fmt,
ops::{Add, MulAssign, Sub},
};
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct CoordIJ {
pub i: i32,
pub j: i32,
}
impl CoordIJ {
#[must_use]
pub const fn new(i: i32, j: i32) -> Self {
Self { i, j }
}
}
impl TryFrom<CoordIJ> for CoordIJK {
type Error = HexGridError;
fn try_from(value: CoordIJ) -> Result<Self, Self::Error> {
Self::new(value.i, value.j, 0)
.checked_normalize()
.ok_or_else(|| HexGridError::new("IJ coordinates overflow"))
}
}
impl fmt::Display for CoordIJ {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "({}, {})", self.i, self.j)
}
}
#[derive(Debug, Clone, Default, Copy, Eq, PartialEq)]
pub struct CoordIJK {
i: i32,
j: i32,
k: i32,
}
impl CoordIJK {
pub const fn new(i: i32, j: i32, k: i32) -> Self {
Self { i, j, k }
}
pub const fn i(&self) -> i32 {
self.i
}
pub const fn j(&self) -> i32 {
self.j
}
pub const fn k(&self) -> i32 {
self.k
}
pub fn normalize(mut self) -> Self {
let min = cmp::min(self.i, cmp::min(self.j, self.k));
self.i -= min;
self.j -= min;
self.k -= min;
self
}
fn checked_normalize(mut self) -> Option<Self> {
let min = cmp::min(self.i, cmp::min(self.j, self.k));
self.i = self.i.checked_sub(min)?;
self.j = self.j.checked_sub(min)?;
self.k = self.k.checked_sub(min)?;
Some(self)
}
pub fn distance(&self, other: &Self) -> i32 {
let diff = (*self - *other).normalize();
cmp::max(diff.i.abs(), cmp::max(diff.j.abs(), diff.k.abs()))
}
#[expect(clippy::cast_possible_truncation, reason = "on purpose")]
pub fn up_aperture7<const CCW: bool>(&self) -> Self {
let CoordIJ { i, j } = self.into();
let (i, j) = if CCW {
(f64::from(3 * i - j) / 7., f64::from(i + 2 * j) / 7.)
} else {
(f64::from(2 * i + j) / 7., f64::from(3 * j - i) / 7.)
};
Self::new(round(i) as i32, round(j) as i32, 0).normalize()
}
#[expect(clippy::cast_possible_truncation, reason = "on purpose")]
pub fn checked_up_aperture7<const CCW: bool>(&self) -> Option<Self> {
let CoordIJ { i, j } = self.into();
let (i, j) = if CCW {
(
f64::from(i.checked_mul(3)?.checked_sub(j)?) / 7.,
f64::from(j.checked_mul(2)?.checked_add(i)?) / 7.,
)
} else {
(
f64::from(i.checked_mul(2)?.checked_add(j)?) / 7.,
f64::from(j.checked_mul(3)?.checked_sub(i)?) / 7.,
)
};
Self::new(round(i) as i32, round(j) as i32, 0).checked_normalize()
}
pub fn down_aperture7<const CCW: bool>(&self) -> Self {
let (mut i_vec, mut j_vec, mut k_vec) = if CCW {
(Self::new(3, 0, 1), Self::new(1, 3, 0), Self::new(0, 1, 3))
} else {
(Self::new(3, 1, 0), Self::new(0, 3, 1), Self::new(1, 0, 3))
};
i_vec *= self.i;
j_vec *= self.j;
k_vec *= self.k;
(i_vec + j_vec + k_vec).normalize()
}
pub fn down_aperture3<const CCW: bool>(&self) -> Self {
let (mut i_vec, mut j_vec, mut k_vec) = if CCW {
(Self::new(2, 0, 1), Self::new(1, 2, 0), Self::new(0, 1, 2))
} else {
(Self::new(2, 1, 0), Self::new(0, 2, 1), Self::new(1, 0, 2))
};
i_vec *= self.i;
j_vec *= self.j;
k_vec *= self.k;
(i_vec + j_vec + k_vec).normalize()
}
pub fn neighbor(&self, direction: Direction) -> Self {
(*self + direction.coordinate()).normalize()
}
pub fn rotate60<const CCW: bool>(&self) -> Self {
let (mut i_vec, mut j_vec, mut k_vec) = if CCW {
(Self::new(1, 1, 0), Self::new(0, 1, 1), Self::new(1, 0, 1))
} else {
(Self::new(1, 0, 1), Self::new(1, 1, 0), Self::new(0, 1, 1))
};
i_vec *= self.i;
j_vec *= self.j;
k_vec *= self.k;
(i_vec + j_vec + k_vec).normalize()
}
}
impl Add for CoordIJK {
type Output = Self;
fn add(self, other: Self) -> Self {
Self {
i: self.i + other.i,
j: self.j + other.j,
k: self.k + other.k,
}
}
}
impl Sub for CoordIJK {
type Output = Self;
fn sub(self, other: Self) -> Self {
Self {
i: self.i - other.i,
j: self.j - other.j,
k: self.k - other.k,
}
}
}
impl MulAssign<i32> for CoordIJK {
fn mul_assign(&mut self, rhs: i32) {
self.i *= rhs;
self.j *= rhs;
self.k *= rhs;
}
}
impl From<CoordIJK> for Vec2d {
fn from(value: CoordIJK) -> Self {
let i = f64::from(value.i - value.k);
let j = f64::from(value.j - value.k);
Self::new(mul_add(0.5, -j, i), j * SQRT3_2)
}
}
impl From<&CoordIJK> for CoordIJ {
fn from(value: &CoordIJK) -> Self {
Self::new(value.i - value.k, value.j - value.k)
}
}
impl From<CoordIJK> for CoordCube {
fn from(value: CoordIJK) -> Self {
let i = -value.i + value.k;
let j = value.j - value.k;
let k = -i - j;
Self::new(i, j, k)
}
}
impl TryFrom<CoordIJK> for Direction {
type Error = HexGridError;
fn try_from(value: CoordIJK) -> Result<Self, Self::Error> {
let value = value.normalize();
if (value.i | value.j | value.k) & !1 == 0 {
#[expect(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "cannot truncate thx to check above (unit vector)"
)]
let bits = ((value.i << 2) | (value.j << 1) | value.k) as u8;
Ok(Self::new_unchecked(bits))
} else {
Err(HexGridError::new("non-unit vector in IJK coordinate"))
}
}
}
#[cfg(test)]
#[path = "./ijk_tests.rs"]
mod tests;