#![allow(clippy::inline_always)]
mod convert;
mod euclidean;
#[cfg(feature = "grid")]
pub mod grid;
mod impls;
mod iter;
mod rings;
mod siwzzle;
#[cfg(test)]
mod tests;
pub(crate) use iter::ExactSizeHexIterator;
pub use iter::HexIterExt;
use crate::{DirectionWay, EdgeDirection, VertexDirection};
use glam::{IVec2, IVec3, Vec2};
#[cfg(feature = "grid")]
pub use grid::{GridEdge, GridVertex};
use std::{
cmp::{max, min},
fmt::Debug,
};
#[derive(Copy, Clone, Default, Eq, PartialEq)]
#[cfg_attr(not(target_arch = "spirv"), derive(Hash))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "facet", derive(facet::Facet))]
#[cfg_attr(feature = "packed", repr(C))]
#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
#[cfg_attr(feature = "bevy_ecs", derive(bevy_ecs::component::Component))]
pub struct Hex {
pub x: i32,
pub y: i32,
}
#[inline(always)]
#[must_use]
pub const fn hex(x: i32, y: i32) -> Hex {
Hex::new(x, y)
}
impl Hex {
pub const ORIGIN: Self = Self::ZERO;
pub const ZERO: Self = Self::new(0, 0);
pub const ONE: Self = Self::new(1, 1);
pub const NEG_ONE: Self = Self::new(-1, -1);
pub const X: Self = Self::new(1, 0);
pub const NEG_X: Self = Self::new(-1, 0);
pub const Y: Self = Self::new(0, 1);
pub const NEG_Y: Self = Self::new(0, -1);
pub const INCR_X: [Self; 2] = [Self::new(1, 0), Self::new(1, -1)];
pub const INCR_Y: [Self; 2] = [Self::new(0, 1), Self::new(-1, 1)];
pub const INCR_Z: [Self; 2] = [Self::new(-1, 0), Self::new(0, -1)];
pub const DECR_X: [Self; 2] = [Self::new(-1, 0), Self::new(-1, 1)];
pub const DECR_Y: [Self; 2] = [Self::new(0, -1), Self::new(1, -1)];
pub const DECR_Z: [Self; 2] = [Self::new(1, 0), Self::new(0, 1)];
pub const NEIGHBORS_COORDS: [Self; 6] = [
Self::new(1, 0),
Self::new(0, 1),
Self::new(-1, 1),
Self::new(-1, 0),
Self::new(0, -1),
Self::new(1, -1),
];
pub const DIAGONAL_COORDS: [Self; 6] = [
Self::new(2, -1),
Self::new(1, 1),
Self::new(-1, 2),
Self::new(-2, 1),
Self::new(-1, -1),
Self::new(1, -2),
];
#[inline(always)]
#[must_use]
pub const fn new(x: i32, y: i32) -> Self {
Self { x, y }
}
#[inline]
#[must_use]
pub const fn splat(v: i32) -> Self {
Self { x: v, y: v }
}
#[inline]
#[must_use]
pub const fn new_cubic(x: i32, y: i32, z: i32) -> Self {
assert!(x + y + z == 0);
Self { x, y }
}
#[inline]
#[must_use]
#[doc(alias = "q")]
pub const fn x(self) -> i32 {
self.x
}
#[inline]
#[must_use]
#[doc(alias = "r")]
pub const fn y(self) -> i32 {
self.y
}
#[inline]
#[must_use]
#[doc(alias = "s")]
pub const fn z(self) -> i32 {
-self.x - self.y
}
#[inline]
#[must_use]
pub const fn from_array([x, y]: [i32; 2]) -> Self {
Self::new(x, y)
}
#[inline]
#[must_use]
pub const fn to_array(self) -> [i32; 2] {
[self.x, self.y]
}
#[inline]
#[must_use]
#[expect(clippy::cast_precision_loss)]
pub const fn to_array_f32(self) -> [f32; 2] {
[self.x as f32, self.y as f32]
}
#[inline]
#[must_use]
pub const fn to_cubic_array(self) -> [i32; 3] {
[self.x, self.y, self.z()]
}
#[inline]
#[must_use]
#[expect(clippy::cast_precision_loss)]
pub const fn to_cubic_array_f32(self) -> [f32; 3] {
[self.x as f32, self.y as f32, self.z() as f32]
}
#[inline]
#[must_use]
pub const fn from_slice(slice: &[i32]) -> Self {
Self::new(slice[0], slice[1])
}
#[inline]
pub fn write_to_slice(self, slice: &mut [i32]) {
slice[0] = self.x;
slice[1] = self.y;
}
#[must_use]
#[inline]
pub const fn as_ivec2(self) -> IVec2 {
IVec2 {
x: self.x,
y: self.y,
}
}
#[must_use]
#[inline]
#[doc(alias = "as_cubic")]
pub const fn as_ivec3(self) -> IVec3 {
IVec3 {
x: self.x,
y: self.y,
z: self.z(),
}
}
#[expect(clippy::cast_precision_loss)]
#[must_use]
#[inline]
pub const fn as_vec2(self) -> Vec2 {
Vec2 {
x: self.x as f32,
y: self.y as f32,
}
}
#[inline]
#[must_use]
pub const fn const_neg(self) -> Self {
Self {
x: -self.x,
y: -self.y,
}
}
#[inline]
#[must_use]
pub const fn const_add(self, other: Self) -> Self {
Self {
x: self.x + other.x,
y: self.y + other.y,
}
}
#[inline]
#[must_use]
pub const fn const_sub(self, rhs: Self) -> Self {
Self {
x: self.x - rhs.x,
y: self.y - rhs.y,
}
}
#[inline]
#[must_use]
#[expect(clippy::cast_possible_truncation)]
pub fn round([mut x, mut y]: [f32; 2]) -> Self {
let [mut x_r, mut y_r] = [x.round(), y.round()];
x -= x_r;
y -= y_r;
if x.abs() >= y.abs() {
x_r += 0.5_f32.mul_add(y, x).round();
} else {
y_r += 0.5_f32.mul_add(x, y).round();
}
Self::new(x_r as i32, y_r as i32)
}
#[inline]
#[must_use]
pub const fn abs(self) -> Self {
Self {
x: self.x.abs(),
y: self.y.abs(),
}
}
#[inline]
#[must_use]
pub fn min(self, rhs: Self) -> Self {
Self {
x: self.x.min(rhs.x),
y: self.y.min(rhs.y),
}
}
#[inline]
#[must_use]
pub fn max(self, rhs: Self) -> Self {
Self {
x: self.x.max(rhs.x),
y: self.y.max(rhs.y),
}
}
#[inline]
#[must_use]
pub const fn dot(self, rhs: Self) -> i32 {
(self.x * rhs.x) + (self.y * rhs.y)
}
#[inline]
#[must_use]
pub const fn signum(self) -> Self {
Self {
x: self.x.signum(),
y: self.y.signum(),
}
}
#[inline]
#[must_use]
#[doc(alias = "magnitude")]
pub const fn length(self) -> i32 {
let [x, y, z] = [self.x.abs(), self.y.abs(), self.z().abs()];
if x >= y && x >= z {
x
} else if y >= x && y >= z {
y
} else {
z
}
}
#[inline]
#[must_use]
#[doc(alias = "unsigned_length")]
pub const fn ulength(self) -> u32 {
let [x, y, z] = [
self.x.unsigned_abs(),
self.y.unsigned_abs(),
self.z().unsigned_abs(),
];
if x >= y && x >= z {
x
} else if y >= x && y >= z {
y
} else {
z
}
}
#[inline]
#[must_use]
pub const fn distance_to(self, rhs: Self) -> i32 {
self.const_sub(rhs).length()
}
#[inline]
#[must_use]
pub const fn unsigned_distance_to(self, rhs: Self) -> u32 {
self.const_sub(rhs).ulength()
}
#[inline(always)]
#[must_use]
pub const fn neighbor_coord(direction: EdgeDirection) -> Self {
direction.into_hex()
}
#[inline(always)]
#[must_use]
pub const fn diagonal_neighbor_coord(direction: VertexDirection) -> Self {
direction.into_hex()
}
pub(crate) const fn add_dir(self, direction: EdgeDirection) -> Self {
self.const_add(Self::neighbor_coord(direction))
}
pub(crate) const fn add_diag_dir(self, direction: VertexDirection) -> Self {
self.const_add(Self::diagonal_neighbor_coord(direction))
}
#[inline]
#[must_use]
pub const fn neighbor(self, direction: EdgeDirection) -> Self {
self.const_add(Self::neighbor_coord(direction))
}
#[inline]
#[must_use]
pub const fn diagonal_neighbor(self, direction: VertexDirection) -> Self {
self.const_add(Self::diagonal_neighbor_coord(direction))
}
#[inline]
#[must_use]
pub fn neighbor_direction(self, other: Self) -> Option<EdgeDirection> {
EdgeDirection::iter().find(|&dir| self.neighbor(dir) == other)
}
#[must_use]
pub fn main_diagonal_to(self, rhs: Self) -> VertexDirection {
self.diagonal_way_to(rhs).unwrap()
}
#[must_use]
pub fn diagonal_way_to(self, rhs: Self) -> DirectionWay<VertexDirection> {
let [x, y, z] = (rhs - self).to_cubic_array();
let [xa, ya, za] = [x.abs(), y.abs(), z.abs()];
match xa.max(ya).max(za) {
v if v == xa => {
DirectionWay::way_from(x < 0, xa == ya, xa == za, VertexDirection::FLAT_RIGHT)
}
v if v == ya => {
DirectionWay::way_from(y < 0, ya == za, ya == xa, VertexDirection::FLAT_BOTTOM_LEFT)
}
_ => DirectionWay::way_from(z < 0, za == xa, za == ya, VertexDirection::FLAT_TOP_LEFT),
}
}
#[must_use]
pub fn main_direction_to(self, rhs: Self) -> EdgeDirection {
self.way_to(rhs).unwrap()
}
#[must_use]
pub fn way_to(self, rhs: Self) -> DirectionWay<EdgeDirection> {
let [x, y, z] = (rhs - self).to_cubic_array();
let [x, y, z] = [y - x, z - y, x - z];
let [xa, ya, za] = [x.abs(), y.abs(), z.abs()];
match xa.max(ya).max(za) {
v if v == xa => {
DirectionWay::way_from(x < 0, xa == ya, xa == za, EdgeDirection::FLAT_BOTTOM_LEFT)
}
v if v == ya => {
DirectionWay::way_from(y < 0, ya == za, ya == xa, EdgeDirection::FLAT_TOP)
}
_ => {
DirectionWay::way_from(z < 0, za == xa, za == ya, EdgeDirection::FLAT_BOTTOM_RIGHT)
}
}
}
#[inline]
#[must_use]
pub fn all_neighbors(self) -> [Self; 6] {
Self::NEIGHBORS_COORDS.map(|n| self.const_add(n))
}
#[inline]
#[must_use]
pub fn all_diagonals(self) -> [Self; 6] {
Self::DIAGONAL_COORDS.map(|n| self.const_add(n))
}
#[inline]
#[must_use]
#[doc(alias = "ccw")]
pub const fn counter_clockwise(self) -> Self {
Self::new(-self.z(), -self.x)
}
#[inline]
#[must_use]
pub const fn ccw_around(self, center: Self) -> Self {
self.const_sub(center).counter_clockwise().const_add(center)
}
#[inline]
#[must_use]
pub const fn rotate_ccw(self, m: u32) -> Self {
match m % 6 {
1 => self.counter_clockwise(),
2 => self.counter_clockwise().counter_clockwise(),
3 => self.const_neg(),
4 => self.clockwise().clockwise(),
5 => self.clockwise(),
_ => self,
}
}
#[inline]
#[must_use]
pub const fn rotate_ccw_around(self, center: Self, m: u32) -> Self {
self.const_sub(center).rotate_ccw(m).const_add(center)
}
#[inline]
#[must_use]
#[doc(alias = "cw")]
pub const fn clockwise(self) -> Self {
Self::new(-self.y, -self.z())
}
#[inline]
#[must_use]
pub const fn cw_around(self, center: Self) -> Self {
self.const_sub(center).clockwise().const_add(center)
}
#[inline]
#[must_use]
pub const fn rotate_cw(self, m: u32) -> Self {
match m % 6 {
1 => self.clockwise(),
2 => self.clockwise().clockwise(),
3 => self.const_neg(),
4 => self.counter_clockwise().counter_clockwise(),
5 => self.counter_clockwise(),
_ => self,
}
}
#[inline]
#[must_use]
pub const fn rotate_cw_around(self, center: Self, m: u32) -> Self {
self.const_sub(center).rotate_cw(m).const_add(center)
}
#[inline(always)]
#[must_use]
#[doc(alias = "reflect_q")]
pub const fn reflect_x(self) -> Self {
Self::new(self.x, self.z())
}
#[inline(always)]
#[must_use]
#[doc(alias = "reflect_r")]
pub const fn reflect_y(self) -> Self {
Self::new(self.z(), self.y)
}
#[inline(always)]
#[must_use]
#[doc(alias = "reflect_s")]
pub const fn reflect_z(self) -> Self {
Self::new(self.y, self.x)
}
#[expect(clippy::cast_precision_loss)]
#[must_use]
pub fn line_to(self, other: Self) -> impl ExactSizeIterator<Item = Self> {
let distance = self.unsigned_distance_to(other);
let dist = distance.max(1) as f32;
let [a, b]: [Vec2; 2] = [self.as_vec2(), other.as_vec2()];
ExactSizeHexIterator {
iter: (0..=distance).map(move |step| a.lerp(b, step as f32 / dist).into()),
count: distance as usize + 1,
}
}
#[expect(clippy::cast_sign_loss)]
#[must_use]
pub fn rectiline_to(self, other: Self, clockwise: bool) -> impl ExactSizeIterator<Item = Self> {
let delta = other.const_sub(self);
let count = delta.length();
let mut dirs = self.main_diagonal_to(other).edge_directions();
if !clockwise {
dirs.rotate_left(1);
}
let [dir_a, dir_b] = dirs;
let proj_b = dir_b * count;
let ca = proj_b.distance_to(delta);
let iter = std::iter::once(self).chain((0..count).scan(self, move |p, i| {
if i < ca {
*p += dir_a;
} else {
*p += dir_b;
}
Some(*p)
}));
ExactSizeHexIterator {
iter,
count: (count + 1) as usize,
}
}
#[doc(alias = "mix")]
#[inline]
#[must_use]
pub fn lerp(self, rhs: Self, s: f32) -> Self {
let [start, end]: [Vec2; 2] = [self.as_vec2(), rhs.as_vec2()];
start.lerp(end, s).into()
}
#[expect(clippy::cast_possible_wrap)]
#[must_use]
pub fn range(self, range: u32) -> impl ExactSizeIterator<Item = Self> {
let radius = range as i32;
ExactSizeHexIterator {
iter: (-radius..=radius).flat_map(move |x| {
let y_min = max(-radius, -x - radius);
let y_max = min(radius, radius - x);
(y_min..=y_max).map(move |y| self.const_add(Self::new(x, y)))
}),
count: Self::range_count(range) as usize,
}
}
#[doc(alias = "excluding_range")]
#[must_use]
pub fn xrange(self, range: u32) -> impl ExactSizeIterator<Item = Self> {
let iter = self.range(range);
ExactSizeHexIterator {
count: iter.len().saturating_sub(1),
iter: iter.filter(move |h| *h != self),
}
}
#[must_use]
#[expect(
clippy::cast_possible_wrap,
clippy::cast_precision_loss,
clippy::cast_possible_truncation
)]
#[doc(alias = "downscale")]
pub fn to_lower_res(self, radius: u32) -> Self {
let [x, y, z] = self.to_cubic_array();
let area = Self::range_count(radius) as f32;
let shift = Self::shift(radius) as i32;
let [x, y, z] = [
((y + shift * x) as f32 / area).floor() as i32,
((z + shift * y) as f32 / area).floor() as i32,
((x + shift * z) as f32 / area).floor() as i32,
];
let [x, y] = [
((1 + x - y) as f32 / 3.0).floor() as i32,
((1 + y - z) as f32 / 3.0).floor() as i32,
];
Self::new(x, y)
}
#[must_use]
#[expect(clippy::cast_possible_wrap)]
#[doc(alias = "upscale")]
pub const fn to_higher_res(self, radius: u32) -> Self {
let range = radius as i32;
let [x, y, z] = self.to_cubic_array();
Self::new(x * (range + 1) - range * z, y * (range + 1) - range * x)
}
#[must_use]
pub fn to_local(self, radius: u32) -> Self {
let upscale = self.to_lower_res(radius);
let center = upscale.to_higher_res(radius);
self.const_sub(center)
}
#[inline(always)]
#[must_use]
pub const fn range_count(range: u32) -> u32 {
3 * range * (range + 1) + 1
}
#[inline(always)]
#[must_use]
pub(crate) const fn shift(range: u32) -> u32 {
3 * range + 2
}
#[must_use]
#[inline(always)]
pub fn wrap_in_range(self, range: u32) -> Self {
self.to_local(range)
}
}
#[cfg(not(target_arch = "spirv"))]
impl Debug for Hex {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Hex")
.field("x", &self.x)
.field("y", &self.y)
.field("z", &self.z())
.finish()
}
}