use crate::{EdgeDirection, Hex};
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "facet", derive(facet::Facet))]
#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
pub struct HexBounds {
pub center: Hex,
pub radius: u32,
}
impl HexBounds {
#[inline]
#[must_use]
pub const fn new(center: Hex, radius: u32) -> Self {
Self { center, radius }
}
#[inline]
#[must_use]
pub const fn from_radius(radius: u32) -> Self {
Self {
center: Hex::ZERO,
radius,
}
}
#[inline]
#[must_use]
pub fn from_min_max(min: Hex, max: Hex) -> Self {
let center = (min + max) / 2;
let radius = center.unsigned_distance_to(max);
Self { center, radius }
}
#[inline]
#[must_use]
#[expect(clippy::cast_possible_wrap)]
pub const fn positive_radius(radius: u32) -> Self {
let center = Hex::splat(radius as i32);
Self { center, radius }
}
#[inline]
#[must_use]
pub const fn is_in_bounds(&self, rhs: Hex) -> bool {
self.center.unsigned_distance_to(rhs) <= self.radius
}
#[must_use]
#[inline]
#[doc(alias = "coords_count")]
#[doc(alias = "len")]
pub const fn hex_count(&self) -> usize {
Hex::range_count(self.radius) as usize
}
#[must_use]
#[inline]
#[doc(alias = "coords_count")]
#[doc(alias = "len32")]
pub const fn hex_count32(&self) -> u32 {
Hex::range_count(self.radius)
}
#[doc(alias = "all_items")]
#[must_use]
pub fn all_coords(self) -> impl ExactSizeIterator<Item = Hex> {
self.center.range(self.radius)
}
pub fn intersecting_with(self, rhs: Self) -> impl Iterator<Item = Hex> {
let [start, end] = if self.radius > rhs.radius {
[rhs, self]
} else {
[self, rhs]
};
start.all_coords().filter(move |h| end.is_in_bounds(*h))
}
#[must_use]
pub fn wrap_local(&self, coord: Hex) -> Hex {
let coord = coord - self.center;
coord.wrap_in_range(self.radius)
}
#[must_use]
pub fn wrap(&self, coord: Hex) -> Hex {
self.wrap_local(coord) + self.center
}
#[must_use]
#[expect(clippy::cast_possible_wrap)]
pub fn corners(&self) -> [Hex; 6] {
EdgeDirection::ALL_DIRECTIONS.map(|dir| self.center.const_add(dir * self.radius as i32))
}
}
impl FromIterator<Hex> for HexBounds {
fn from_iter<T: IntoIterator<Item = Hex>>(iter: T) -> Self {
let mut iter = iter.into_iter();
let Some(first) = iter.next() else {
return Self::from_radius(0);
};
let mut max = first.as_ivec3();
let mut min = max;
for hex in iter {
let hex = hex.as_ivec3();
max = max.max(hex);
min = min.min(hex);
}
let duo_size = (max - min).max_element();
let trio_size = max.element_sum().max(-min.element_sum());
let duo_radius = (duo_size + 1) / 2;
let trio_radius = (trio_size + 2) / 3;
let radius = duo_radius.max(trio_radius);
let center_min = max - radius;
let center_max = min + radius;
let range = center_max - center_min;
let mut center = center_min;
let mut sum = center.element_sum();
if -sum > range.x {
sum += range.x;
center.x += range.x;
if -sum > range.y {
center.y += range.y;
} else {
center.y -= sum;
}
} else {
center.x -= sum;
}
let center = Hex::new(center.x, center.y);
#[expect(clippy::cast_sign_loss)]
let radius = radius as u32;
Self { center, radius }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn in_bounds_work() {
let bounds = HexBounds::new(Hex::new(-4, 23), 34);
for h in bounds.all_coords() {
assert!(bounds.is_in_bounds(h));
}
}
#[test]
fn intersecting_with() {
let ba = HexBounds::new(Hex::ZERO, 3);
let bb = HexBounds::new(Hex::new(4, 0), 3);
let intersection = ba.intersecting_with(bb);
assert_eq!(intersection.count(), 9);
}
#[test]
fn wrapping_works() {
let map = HexBounds::from_radius(3);
assert_eq!(map.wrap(Hex::new(0, 4)), Hex::new(-3, 0));
assert_eq!(map.wrap(Hex::new(4, 0)), Hex::new(-3, 3));
assert_eq!(map.wrap(Hex::new(4, -4)), Hex::new(0, 3));
}
#[test]
fn wrapping_outside_works() {
let map = HexBounds::from_radius(2);
assert_eq!(map.wrap(Hex::new(3, 0)), Hex::new(-2, 2));
assert_eq!(map.wrap(Hex::new(5, 0)), Hex::new(0, 2));
assert_eq!(map.wrap(Hex::new(6, 0)), Hex::new(-1, -1));
assert_eq!(map.wrap(Hex::new(2, 3)), Hex::new(0, 0)); assert_eq!(map.wrap(Hex::new(4, 6)), Hex::new(0, 0));
}
#[test]
fn positive_radius() {
for radius in 0..100_u32 {
let bounds = HexBounds::positive_radius(radius);
let coords = bounds.all_coords();
let fails: Vec<_> = coords.filter(|c| c.x < 0 || c.y < 0).collect();
println!("{fails:#?}");
assert!(fails.is_empty());
}
}
#[test]
fn bounds_hexagon() {
for radius in 0..8 {
let bounds = HexBounds::new(Hex::ZERO, radius);
let coords = bounds.all_coords();
let reconstructed: HexBounds = coords.collect();
assert_eq!(bounds, reconstructed);
let corners = bounds.corners();
let reconstructed: HexBounds = corners.into_iter().collect();
assert_eq!(bounds, reconstructed);
}
for radius in 0..8 {
let bounds = HexBounds::new(Hex::new(15, -19), radius);
let coords = bounds.all_coords();
let reconstructed: HexBounds = coords.collect();
assert_eq!(bounds, reconstructed);
let corners = bounds.corners();
let reconstructed: HexBounds = corners.into_iter().collect();
assert_eq!(bounds, reconstructed);
}
}
#[test]
fn range_works() {
let coords: Vec<_> = Hex::ZERO.range(10).collect();
let bounds = HexBounds::from_iter(coords.clone());
assert_eq!(bounds.center, Hex::ZERO);
assert_eq!(bounds.radius, 10);
for h in coords {
assert!(bounds.is_in_bounds(h));
}
}
#[test]
fn bounds_rhombus() {
for size in 1..10 {
for rotation in 0..3 {
let coords = (0..size * size)
.map(|i| Hex::new(i / size, i % size).rotate_cw(rotation))
.collect::<Vec<_>>();
let reconstructed: HexBounds = coords.iter().copied().collect();
for h in coords {
assert!(reconstructed.is_in_bounds(h));
}
let radius = size as u32 - 1;
assert_eq!(reconstructed.radius, radius);
}
}
}
#[test]
fn bounds_line() {
for direction in 0..6 {
for size in 1..10 {
let coords = Hex::new(0, 0)
.line_to(Hex::new(size, 0))
.map(|h| h.rotate_cw(direction))
.collect::<Vec<_>>();
let reconstructed: HexBounds = coords.iter().copied().collect();
for h in coords {
assert!(reconstructed.is_in_bounds(h));
}
let radius = (size as u32).div_ceil(2);
assert_eq!(reconstructed.radius, radius);
}
}
}
#[test]
fn bounds_edge_cases() {
let mut coords = vec![];
let reconstructed: HexBounds = coords.iter().copied().collect();
assert_eq!(reconstructed.radius, 0);
coords.push(Hex::new(0, 0));
let reconstructed: HexBounds = coords.iter().copied().collect();
assert_eq!(reconstructed, HexBounds::from_radius(0));
}
#[test]
fn bounds_wedge() {
for size in 1..10 {
let coords = Hex::new(0, 0)
.full_wedge(size, crate::VertexDirection(1))
.collect::<Vec<_>>();
let reconstructed: HexBounds = coords.iter().copied().collect();
for h in coords {
assert!(reconstructed.is_in_bounds(h));
}
assert_eq!(reconstructed.radius, 2 * (size + 1) / 3);
let coords = Hex::new(0, 0)
.full_wedge(size, crate::VertexDirection(2))
.collect::<Vec<_>>();
let reconstructed: HexBounds = coords.iter().copied().collect();
for h in coords {
assert!(reconstructed.is_in_bounds(h));
}
assert_eq!(reconstructed.radius, 2 * (size + 1) / 3);
}
}
}