Skip to main content

civ_map_generator/grid/hex_grid/
hex.rs

1//! Hexagonal grid coordinate system implementation.
2//!
3
4#![allow(dead_code)]
5
6use crate::grid::*;
7use core::f32::consts::{FRAC_PI_3, FRAC_PI_6};
8use glam::{IVec2, Mat2, Vec2};
9use std::{
10    cmp::{max, min},
11    ops::{Add, Sub},
12};
13
14pub const SQRT_3: f32 = 1.732_050_8_f32;
15
16/// Hexagonal grid coordinate in axial (cube) coordinate system.
17#[derive(Clone, Copy, PartialEq, Eq, Debug)]
18pub struct Hex(IVec2);
19impl Hex {
20    /// Hexagon neighbor coordinates array, following [`HexOrientation::POINTY_EDGE`] or [`HexOrientation::FLAT_EDGE`] order.
21    ///
22    /// These 6 direction vectors represent the offset from a hex to each of its neighbors.
23    /// The order corresponds to clockwise directions starting from East (for pointy-top)
24    /// or NorthEast (for flat-top).
25    ///
26    /// # Direction Mapping
27    ///
28    /// | Index | Pointy-Top | Flat-Top | Vector (Hex)  |
29    /// | :---: | :--------- | :------- | :------------ |
30    /// | 0     | East       | NE       | (1, 0)        |
31    /// | 1     | SE         | SE       | (1, -1)       |
32    /// | 2     | SW         | South    | (0, -1)       |
33    /// | 3     | West       | SW       | (-1, 0)       |
34    /// | 4     | NW         | NW       | (-1, 1)       |
35    /// | 5     | NE         | North    | (0, 1)        |
36    pub const HEX_DIRECTIONS: [Self; 6] = [
37        Self::new(1, 0),
38        Self::new(1, -1),
39        Self::new(0, -1),
40        Self::new(-1, 0),
41        Self::new(-1, 1),
42        Self::new(0, 1),
43    ];
44
45    pub const fn new(x: i32, y: i32) -> Self {
46        Self(IVec2::new(x, y))
47    }
48
49    /// Creates a new [`Hex`] from an [`OffsetCoordinate`].
50    pub const fn from_offset(
51        offset_coordinate: OffsetCoordinate,
52        orientation: HexOrientation,
53        offset: Offset,
54    ) -> Self {
55        let [x, y] = offset_coordinate.to_array();
56
57        let (q, r) = match orientation {
58            HexOrientation::Pointy => (x - (y + offset as i32 * (y & 1)) / 2, y),
59            HexOrientation::Flat => (x, y - (x + offset as i32 * (x & 1)) / 2),
60        };
61        Hex::new(q, r)
62    }
63
64    pub const fn x(self) -> i32 {
65        self.0.x
66    }
67
68    pub const fn y(self) -> i32 {
69        self.0.y
70    }
71
72    pub const fn z(self) -> i32 {
73        -self.0.x - self.0.y
74    }
75
76    pub const fn into_inner(self) -> IVec2 {
77        self.0
78    }
79
80    /// Converts a hex coordinate to an offset coordinate.
81    pub fn to_offset(self, orientation: HexOrientation, offset: Offset) -> OffsetCoordinate {
82        let (col, row) = match orientation {
83            HexOrientation::Pointy => (
84                self.0.x + (self.0.y + offset as i32 * (self.0.y & 1)) / 2,
85                self.0.y,
86            ),
87            HexOrientation::Flat => (
88                self.0.x,
89                self.0.y + (self.0.x + offset as i32 * (self.0.x & 1)) / 2,
90            ),
91        };
92        OffsetCoordinate::new(col, row)
93    }
94
95    /// Get the hex at the given `direction` from `self`, according to the given `orientation` is `HexOrientation::Pointy` or `HexOrientation::Flat`.
96    pub fn neighbor(self, orientation: HexOrientation, direction: Direction) -> Hex {
97        let edge_index = orientation.edge_index(direction);
98        self + Self::HEX_DIRECTIONS[edge_index]
99    }
100
101    #[inline]
102    /// Computes coordinates length as a signed integer.
103    /// The length of a [`Hex`] coordinate is equal to its distance from the origin.
104    pub const fn length(self) -> i32 {
105        /* let [x, y, z] = [self.x.abs(), self.y.abs(), self.z().abs()];
106        if x >= y && x >= z {
107            x
108        } else if y >= x && y >= z {
109            y
110        } else {
111            z
112        } */
113
114        // The following code is equivalent to the commented code above, but it is faster.
115        (self.0.x.abs() + self.0.y.abs() + self.z().abs()) / 2
116    }
117
118    #[inline]
119    /// Computes the distance from `self` to `rhs` in hexagonal space as a signed integer.
120    pub fn distance_to(self, rhs: Self) -> i32 {
121        (self - rhs).length()
122    }
123
124    /// Return a [`Vec<Hex>`] containing all [`Hex`] which are exactly at a given `distance` from `self`.
125    /// If `distance` = 0 the [`Vec<Hex>`] will be empty. \
126    /// The number of returned hexes is equal to `6 * distance`.
127    pub fn hexes_at_distance(self, distance: u32) -> Vec<Hex> {
128        // If distance is 0, return an empty vector
129        if distance == 0 {
130            return Vec::new();
131        }
132
133        let mut hex_list = Vec::with_capacity((6 * distance) as usize);
134        let radius = distance as i32;
135
136        /* for q in -radius..=radius {
137            for r in max(-radius, -q - radius)..=min(radius, -q + radius) {
138                let offset_hex = Hex::from([q, r]);
139                if offset_hex.distance_to(Hex::from([0, 0])) == radius {
140                    let hex = self + offset_hex;
141                    hex_list.push(hex);
142                }
143            }
144        } */
145
146        // The following code is equivalent to the commented code above, but it is faster.
147        let mut hex = Hex(self.0 + Self::HEX_DIRECTIONS[4].0 * radius);
148        for hex_direction in Self::HEX_DIRECTIONS {
149            for _ in 0..radius {
150                hex_list.push(hex);
151                hex = hex + hex_direction;
152            }
153        }
154
155        hex_list
156    }
157
158    /// Return a [`Vec<Hex>`] containing all [`Hex`] around `self` in a given `distance`, including `self`. \
159    /// The number of returned hexes is equal to `3 * distance * (distance + 1) + 1`.
160    pub fn hexes_in_distance(self, distance: u32) -> Vec<Hex> {
161        let mut hex_list = Vec::with_capacity((3 * distance * (distance + 1) + 1) as usize);
162        let radius = distance as i32;
163        for q in -radius..=radius {
164            for r in max(-radius, -q - radius)..=min(radius, -q + radius) {
165                let hex = self + Hex::new(q, r);
166                hex_list.push(hex);
167            }
168        }
169        hex_list
170    }
171
172    /// Rounds floating point coordinates to [`Hex`].
173    #[inline(always)]
174    pub fn round(fractional_hex: Vec2) -> Self {
175        let mut rounded = fractional_hex.round();
176
177        let diff = fractional_hex - rounded;
178
179        if diff.x.abs() >= diff.y.abs() {
180            rounded.x += 0.5_f32.mul_add(diff.y, diff.x).round();
181        } else {
182            rounded.y += 0.5_f32.mul_add(diff.x, diff.y).round();
183        }
184
185        Self(rounded.as_ivec2())
186    }
187}
188
189impl Add for Hex {
190    type Output = Self;
191    fn add(self, rhs: Self) -> Self::Output {
192        Self(self.0 + rhs.0)
193    }
194}
195
196impl Sub for Hex {
197    type Output = Self;
198    fn sub(self, rhs: Self) -> Self::Output {
199        Self(self.0 - rhs.0)
200    }
201}
202
203impl From<[i32; 2]> for Hex {
204    #[inline]
205    fn from(a: [i32; 2]) -> Self {
206        Self(a.into())
207    }
208}
209
210#[derive(PartialEq, Clone, Copy, Debug)]
211pub struct HexLayout {
212    /// The orientation of the hexagonal layout (pointy or flat top).
213    pub orientation: HexOrientation,
214    /// The size of each hex in pixels: [width, height].
215    pub size: [f32; 2],
216    /// The pixel position of hex (0, 0): [x, y].
217    pub origin: [f32; 2],
218}
219
220impl HexLayout {
221    pub fn new(orientation: HexOrientation, size: [f32; 2], origin: [f32; 2]) -> Self {
222        Self {
223            orientation,
224            size,
225            origin,
226        }
227    }
228
229    /// Returns the pixel coordinates of the center of the given hexagonal coordinates.
230    pub fn hex_to_pixel(self, hex: Hex) -> Vec2 {
231        let m = self.orientation.conversion_matrix();
232        let size = Vec2::from(self.size);
233        let origin = Vec2::from(self.origin);
234        let mat2 = m.forward_matrix;
235        let pixel = mat2 * (hex.0.as_vec2()) * size;
236        pixel + origin
237    }
238
239    /// Returns the hexagonal coordinates that contains the given pixel position.
240    pub fn pixel_to_hex(self, pixel: [f32; 2]) -> Hex {
241        let m = self.orientation.conversion_matrix();
242        let size = Vec2::from(self.size);
243        let origin = Vec2::from(self.origin);
244        let pt = (Vec2::from(pixel) - origin) / size;
245        let mat2 = m.inverse_matrix;
246        let fractional_hex = mat2 * pt;
247        Hex::round(fractional_hex)
248    }
249
250    /// Returns the corner pixel coordinates of the given hexagonal coordinates according to corner direction.
251    pub fn corner(self, hex: Hex, direction: Direction) -> [f32; 2] {
252        let center: Vec2 = self.hex_to_pixel(hex);
253        let offset: Vec2 = self.corner_offset(direction);
254        (center + offset).to_array()
255    }
256
257    /// Retrieves all 6 corner pixel coordinates of the given hexagonal coordinates.
258    ///
259    /// The returned array is ordered and usually used to draw a hexagon.
260    pub fn all_corners(self, hex: Hex) -> [[f32; 2]; 6] {
261        self.orientation
262            .corner_direction()
263            .map(|direction| self.corner(hex, direction))
264    }
265
266    #[inline(always)]
267    fn corner_offset(self, direction: Direction) -> Vec2 {
268        let size: Vec2 = Vec2::from(self.size);
269        let angle: f32 = self.orientation.corner_angle(direction);
270        size * Vec2::from_angle(angle)
271    }
272}
273
274#[derive(PartialEq, Eq, Clone, Copy, Debug)]
275pub enum Offset {
276    /// Even offset variant (value = +1)
277    Even = 1,
278    /// Odd offset variant (value = -1)
279    Odd = -1,
280}
281
282/// Conversion matrices for transforming between hex and pixel coordinates.
283///
284/// Contains precomputed forward and inverse matrices for efficient coordinate transformations.
285/// These matrices are orientation-specific (pointy vs flat) and account for hex geometry.
286#[derive(Clone, Copy, Debug)]
287pub struct ConversionMatrix {
288    /// Matrix used to compute hexagonal coordinates to pixel coordinates.
289    pub forward_matrix: Mat2,
290    /// Matrix used to compute pixel coordinates to hexagonal coordinates.
291    pub inverse_matrix: Mat2,
292}
293
294/// Hexagonal grid orientation (pointy-top vs flat-top).
295///
296/// Determines the visual orientation of hexagons and affects coordinate conversions,
297/// neighbor directions, and pixel layout calculations.
298#[repr(u8)]
299#[derive(PartialEq, Eq, Clone, Copy, Debug)]
300pub enum HexOrientation {
301    /// ⬢ Pointy-top orientation: hexagon has pointed top/bottom
302    Pointy,
303    /// ⬣ Flat-top orientation: hexagon has flat top/bottom
304    Flat,
305}
306
307impl HexOrientation {
308    /// Pointy hex edge direction, the directions of the edges of a `Hex` relative to its center
309    ///
310    /// - The number in the Hex-A is the index of the direction of the Hex-A corner in the array of all the corner direction
311    /// - The number outside the Hex-A is the index of the direction of the Hex-A edge in the array of all the edge direction
312    ///
313    /// ```txt
314    ///
315    ///          / \     / \
316    ///         /   \   /   \
317    ///        /     \ /     \
318    ///       |       |       |
319    ///       |   4   |   5   |
320    ///       |       |       |
321    ///      / \     /5\     / \
322    ///     /   \   /   \   /   \
323    ///    /     \ /     \ /     \
324    ///   |       |4     0|       |
325    ///   |   3   | Hex-A |   0   |
326    ///   |       |3     1|       |
327    ///    \     / \     / \     /
328    ///     \   /   \   /   \   /
329    ///      \ /     \2/     \ /
330    ///       |       |       |
331    ///       |   2   |   1   |
332    ///       |       |       |
333    ///        \     / \     /
334    ///         \   /   \   /
335    ///          \ /     \ /
336    ///  ```
337    ///     
338    pub const POINTY_EDGE: [Direction; 6] = [
339        Direction::East,
340        Direction::SouthEast,
341        Direction::SouthWest,
342        Direction::West,
343        Direction::NorthWest,
344        Direction::NorthEast,
345    ];
346
347    /// Pointy hex corner direction, the directions of the corners of a `Hex` relative to its center
348    /// > See [`HexOrientation::POINTY_EDGE`] for more information
349    pub const POINTY_CORNER: [Direction; 6] = [
350        Direction::NorthEast,
351        Direction::SouthEast,
352        Direction::South,
353        Direction::SouthWest,
354        Direction::NorthWest,
355        Direction::North,
356    ];
357
358    /// Flat hex edge direction, the directions of the edges of a `Hex` relative to its center
359    ///  
360    /// - The number in the Hex-A is the index of the direction of the Hex-A corner in the array of all the corner direction
361    /// - The number outside the Hex-A is the index of the direction of the Hex-A edge in the array of all the edge direction
362    ///
363    /// ```txt
364    ///                 ________
365    ///                /        \
366    ///               /          \
367    ///      ________/     5      \________
368    ///     /        \            /        \
369    ///    /          \          /          \
370    ///   /     4      \________/     0      \
371    ///   \            /4      5\            /
372    ///    \          /          \          /
373    ///     \________/3   Hex-A  0\________/
374    ///     /        \            /        \
375    ///    /          \          /          \
376    ///   /     3      \2______1/     1      \
377    ///   \            /        \            /
378    ///    \          /          \          /
379    ///     \________/     2      \________/
380    ///              \            /
381    ///               \          /
382    ///                \________/
383    /// ```
384    ///    
385    pub const FLAT_EDGE: [Direction; 6] = Self::POINTY_CORNER;
386
387    /// Flat hex corner direction, the directions of the corners of a `Hex` relative to its center
388    /// > See [`HexOrientation::FLAT_EDGE`] for more information
389    pub const FLAT_CORNER: [Direction; 6] = Self::POINTY_EDGE;
390
391    #[inline]
392    /// Get the index of the direction of the [`Hex`] corner in the array of all the corner direction
393    /// # Panics
394    /// Panics if the direction is not a valid corner direction for the hexagon orientation
395    pub const fn corner_index(self, direction: Direction) -> usize {
396        match (self, direction) {
397            (HexOrientation::Pointy, Direction::NorthEast) => 0,
398            (HexOrientation::Pointy, Direction::SouthEast) => 1,
399            (HexOrientation::Pointy, Direction::South) => 2,
400            (HexOrientation::Pointy, Direction::SouthWest) => 3,
401            (HexOrientation::Pointy, Direction::NorthWest) => 4,
402            (HexOrientation::Pointy, Direction::North) => 5,
403            (HexOrientation::Pointy, Direction::East | Direction::West) => {
404                panic!("The direction is not a valid corner direction for the hexagon orientation")
405            }
406            (HexOrientation::Flat, Direction::East) => 0,
407            (HexOrientation::Flat, Direction::SouthEast) => 1,
408            (HexOrientation::Flat, Direction::SouthWest) => 2,
409            (HexOrientation::Flat, Direction::West) => 3,
410            (HexOrientation::Flat, Direction::NorthWest) => 4,
411            (HexOrientation::Flat, Direction::NorthEast) => 5,
412            (HexOrientation::Flat, Direction::North | Direction::South) => {
413                panic!("The direction is not a valid corner direction for the hexagon orientation")
414            }
415        }
416    }
417
418    #[inline]
419    /// Get the index of the direction of the `Hex` edge in the array of all the edge direction
420    /// # Panics
421    /// Panics if the direction is not a valid edge direction for the hexagon orientation
422    pub const fn edge_index(self, direction: Direction) -> usize {
423        match (self, direction) {
424            (HexOrientation::Pointy, Direction::East) => 0,
425            (HexOrientation::Pointy, Direction::SouthEast) => 1,
426            (HexOrientation::Pointy, Direction::SouthWest) => 2,
427            (HexOrientation::Pointy, Direction::West) => 3,
428            (HexOrientation::Pointy, Direction::NorthWest) => 4,
429            (HexOrientation::Pointy, Direction::NorthEast) => 5,
430            (HexOrientation::Pointy, Direction::North | Direction::South) => {
431                panic!("The direction is not a valid edge direction for the hexagon orientation")
432            }
433            (HexOrientation::Flat, Direction::NorthEast) => 0,
434            (HexOrientation::Flat, Direction::SouthEast) => 1,
435            (HexOrientation::Flat, Direction::South) => 2,
436            (HexOrientation::Flat, Direction::SouthWest) => 3,
437            (HexOrientation::Flat, Direction::NorthWest) => 4,
438            (HexOrientation::Flat, Direction::North) => 5,
439            (HexOrientation::Flat, Direction::East | Direction::West) => {
440                panic!("The direction is not a valid edge direction for the hexagon orientation")
441            }
442        }
443    }
444
445    /// Returns the next corner direction in clockwise order
446    pub const fn corner_clockwise(self, corner_direction: Direction) -> Direction {
447        let corner_index = self.corner_index(corner_direction);
448        self.corner_direction()[(corner_index + 1) % 6]
449    }
450
451    /// Returns the next edge direction in clockwise order
452    pub const fn edge_clockwise(self, edge_direction: Direction) -> Direction {
453        let edge_index = self.edge_index(edge_direction);
454        self.edge_direction()[(edge_index + 1) % 6]
455    }
456
457    /// Returns the next corner direction in counter clockwise order
458    pub const fn corner_counter_clockwise(self, corner_direction: Direction) -> Direction {
459        let corner_index = self.corner_index(corner_direction);
460        self.corner_direction()[(corner_index + 5) % 6]
461    }
462
463    /// Returns the next edge direction in counter clockwise order
464    pub const fn edge_counter_clockwise(self, edge_direction: Direction) -> Direction {
465        let edge_index = self.edge_index(edge_direction);
466        self.edge_direction()[(edge_index + 5) % 6]
467    }
468
469    #[inline]
470    /// Returns the angle of the `Hex` corner in radians of the given direction for the hexagons
471    pub fn corner_angle(self, direction: Direction) -> f32 {
472        let start_angle = match self {
473            HexOrientation::Pointy => FRAC_PI_6,
474            HexOrientation::Flat => 0.0,
475        };
476        let corner_index = self.corner_index(direction) as f32;
477        //equal to `start_angle - corner_index * FRAC_PI_3`
478        corner_index.mul_add(-FRAC_PI_3, start_angle)
479    }
480
481    #[inline]
482    /// Returns the angle of the `Hex` edge in radians of the given direction for the hexagons
483    pub fn edge_angle(self, direction: Direction) -> f32 {
484        let start_angle = match self {
485            HexOrientation::Pointy => 0.0,
486            HexOrientation::Flat => FRAC_PI_6,
487        };
488        let edge_index = self.edge_index(direction) as f32;
489        //equal to `start_angle - edge_index * FRAC_PI_3`
490        edge_index.mul_add(-FRAC_PI_3, start_angle)
491    }
492
493    const POINTY_CONVERSION_MATRIX: ConversionMatrix = ConversionMatrix {
494        forward_matrix: Mat2::from_cols_array(&[SQRT_3, 0.0, SQRT_3 / 2.0, 3.0 / 2.0]),
495        inverse_matrix: Mat2::from_cols_array(&[SQRT_3 / 3.0, 0.0, -1.0 / 3.0, 2.0 / 3.0]),
496    };
497
498    const FLAT_CONVERSION_MATRIX: ConversionMatrix = ConversionMatrix {
499        forward_matrix: Mat2::from_cols_array(&[3.0 / 2.0, SQRT_3 / 2.0, 0.0, SQRT_3]),
500        inverse_matrix: Mat2::from_cols_array(&[2.0 / 3.0, -1.0 / 3.0, 0.0, SQRT_3 / 3.0]),
501    };
502
503    #[inline]
504    /// Get `ConversionMatrix` for pixel/hex conversion
505    const fn conversion_matrix(self) -> ConversionMatrix {
506        match self {
507            Self::Pointy => Self::POINTY_CONVERSION_MATRIX,
508            Self::Flat => Self::FLAT_CONVERSION_MATRIX,
509        }
510    }
511
512    #[inline]
513    /// Get all the directions of the edges of a `Hex` relative to its center
514    pub const fn edge_direction(&self) -> [Direction; 6] {
515        match self {
516            HexOrientation::Pointy => Self::POINTY_EDGE,
517            HexOrientation::Flat => Self::FLAT_EDGE,
518        }
519    }
520
521    #[inline]
522    /// Get all the directions of the corners of a `Hex` relative to its center
523    pub const fn corner_direction(&self) -> [Direction; 6] {
524        match self {
525            HexOrientation::Pointy => Self::POINTY_CORNER,
526            HexOrientation::Flat => Self::FLAT_CORNER,
527        }
528    }
529}
530
531// Tests
532#[cfg(test)]
533mod tests {
534    use glam::Vec2;
535
536    use super::{Direction, Hex, HexLayout, HexOrientation, Offset, OffsetCoordinate};
537
538    /// Helper function to assert hex equality with descriptive error message
539    fn assert_hex_eq(actual: Hex, expected: Hex, msg: &str) {
540        assert_eq!(
541            actual, expected,
542            "{}: expected {:?}, got {:?}",
543            msg, expected, actual
544        );
545    }
546
547    /// Helper function to assert offset coordinate equality
548    fn assert_offset_eq(actual: OffsetCoordinate, expected: OffsetCoordinate, msg: &str) {
549        assert_eq!(
550            actual, expected,
551            "{}: expected {:?}, got {:?}",
552            msg, expected, actual
553        );
554    }
555
556    #[test]
557    fn test_hex_neighbor_flat_orientation() {
558        // Test flat-top orientation: South neighbor
559        let center = Hex::new(1, -2);
560        let expected = Hex::new(1, -3);
561        let actual = center.neighbor(HexOrientation::Flat, Direction::South);
562        assert_hex_eq(actual, expected, "Flat orientation South neighbor");
563    }
564
565    #[test]
566    fn test_hex_neighbor_pointy_orientation() {
567        // Test pointy-top orientation: SouthWest neighbor
568        let center = Hex::new(1, -2);
569        let expected = Hex::new(1, -3);
570        let actual = center.neighbor(HexOrientation::Pointy, Direction::SouthWest);
571        assert_hex_eq(actual, expected, "Pointy orientation SouthWest neighbor");
572    }
573
574    #[test]
575    fn test_hex_distance_from_origin() {
576        let hex = Hex::new(3, -7);
577        let origin = Hex::new(0, 0);
578        let distance = hex.distance_to(origin);
579        assert_eq!(distance, 7, "Distance from (3,-7) to origin should be 7");
580    }
581
582    #[test]
583    fn test_hex_distance_symmetric() {
584        let a = Hex::new(2, -3);
585        let b = Hex::new(-1, 4);
586        let dist_ab = a.distance_to(b);
587        let dist_ba = b.distance_to(a);
588        assert_eq!(dist_ab, dist_ba, "Distance should be symmetric");
589    }
590
591    #[test]
592    fn test_hex_round_interpolation() {
593        // Test rounding at interpolation midpoint
594        let start = Vec2::ZERO;
595        let end = Vec2::new(10.0, -20.0);
596        let midpoint = start.lerp(end, 0.5);
597        let rounded = Hex::round(midpoint);
598        assert_hex_eq(rounded, Hex::new(5, -10), "Rounding at 0.5 interpolation");
599    }
600
601    #[test]
602    fn test_hex_round_bias_towards_start() {
603        // Values < 0.5 should round towards start
604        let a = Vec2::ZERO;
605        let b = Vec2::new(1.0, -1.0);
606        let biased = a.lerp(b, 0.499);
607        let rounded = Hex::round(biased);
608        assert_hex_eq(rounded, Hex::round(a), "Should bias towards start at 0.499");
609    }
610
611    #[test]
612    fn test_hex_round_bias_towards_end() {
613        // Values > 0.5 should round towards end
614        let a = Vec2::ZERO;
615        let b = Vec2::new(1.0, -1.0);
616        let biased = a.lerp(b, 0.501);
617        let rounded = Hex::round(biased);
618        assert_hex_eq(rounded, Hex::round(b), "Should bias towards end at 0.501");
619    }
620
621    #[test]
622    fn test_hex_round_weighted_average() {
623        // Test rounding with weighted combination
624        let a = Vec2::ZERO;
625        let b = Vec2::new(1.0, -1.0);
626        let c = Vec2::new(0.0, -1.0);
627
628        // More weight on 'a' should round to 'a'
629        let weighted_a = a * 0.4 + b * 0.3 + c * 0.3;
630        assert_hex_eq(
631            Hex::round(weighted_a),
632            Hex::round(a),
633            "Weighted average biased towards a",
634        );
635
636        // More weight on 'c' should round to 'c'
637        let weighted_c = a * 0.3 + b * 0.3 + c * 0.4;
638        assert_hex_eq(
639            Hex::round(weighted_c),
640            Hex::round(c),
641            "Weighted average biased towards c",
642        );
643    }
644
645    #[test]
646    fn test_layout_flat_orientation_roundtrip() {
647        let hex = Hex::new(3, 4);
648        let layout = HexLayout {
649            orientation: HexOrientation::Flat,
650            size: [10.0, 15.0],
651            origin: [35.0, 71.0],
652        };
653
654        // Convert hex → pixel → hex should return original
655        let pixel = layout.hex_to_pixel(hex);
656        let recovered = layout.pixel_to_hex(pixel.to_array());
657        assert_hex_eq(recovered, hex, "Flat layout roundtrip conversion");
658    }
659
660    #[test]
661    fn test_layout_pointy_orientation_roundtrip() {
662        let hex = Hex::new(3, 4);
663        let layout = HexLayout {
664            orientation: HexOrientation::Pointy,
665            size: [10.0, 15.0],
666            origin: [35.0, 71.0],
667        };
668
669        // Convert hex → pixel → hex should return original
670        let pixel = layout.hex_to_pixel(hex);
671        let recovered = layout.pixel_to_hex(pixel.to_array());
672        assert_hex_eq(recovered, hex, "Pointy layout roundtrip conversion");
673    }
674
675    #[test]
676    fn test_offset_conversion_flat_even_roundtrip() {
677        let hex = Hex::new(3, 4);
678        let offset = hex.to_offset(HexOrientation::Flat, Offset::Even);
679        let recovered = Hex::from_offset(offset, HexOrientation::Flat, Offset::Even);
680        assert_hex_eq(recovered, hex, "Flat even-offset roundtrip");
681    }
682
683    #[test]
684    fn test_offset_conversion_flat_odd_roundtrip() {
685        let hex = Hex::new(3, 4);
686        let offset = hex.to_offset(HexOrientation::Flat, Offset::Odd);
687        let recovered = Hex::from_offset(offset, HexOrientation::Flat, Offset::Odd);
688        assert_hex_eq(recovered, hex, "Flat odd-offset roundtrip");
689    }
690
691    #[test]
692    fn test_offset_conversion_pointy_even_roundtrip() {
693        let hex = Hex::new(3, 4);
694        let offset = hex.to_offset(HexOrientation::Pointy, Offset::Even);
695        let recovered = Hex::from_offset(offset, HexOrientation::Pointy, Offset::Even);
696        assert_hex_eq(recovered, hex, "Pointy even-offset roundtrip");
697    }
698
699    #[test]
700    fn test_offset_conversion_pointy_odd_roundtrip() {
701        let hex = Hex::new(3, 4);
702        let offset = hex.to_offset(HexOrientation::Pointy, Offset::Odd);
703        let recovered = Hex::from_offset(offset, HexOrientation::Pointy, Offset::Odd);
704        assert_hex_eq(recovered, hex, "Pointy odd-offset roundtrip");
705    }
706
707    #[test]
708    fn test_offset_coordinate_to_hex_flat_even() {
709        let offset = OffsetCoordinate::new(1, -3);
710        let hex = Hex::from_offset(offset, HexOrientation::Flat, Offset::Even);
711        let recovered = hex.to_offset(HexOrientation::Flat, Offset::Even);
712        assert_offset_eq(recovered, offset, "Offset to hex conversion (flat, even)");
713    }
714
715    #[test]
716    fn test_offset_coordinate_to_hex_flat_odd() {
717        let offset = OffsetCoordinate::new(1, -3);
718        let hex = Hex::from_offset(offset, HexOrientation::Flat, Offset::Odd);
719        let recovered = hex.to_offset(HexOrientation::Flat, Offset::Odd);
720        assert_offset_eq(recovered, offset, "Offset to hex conversion (flat, odd)");
721    }
722
723    #[test]
724    fn test_offset_coordinate_to_hex_pointy_even() {
725        let offset = OffsetCoordinate::new(1, -3);
726        let hex = Hex::from_offset(offset, HexOrientation::Pointy, Offset::Even);
727        let recovered = hex.to_offset(HexOrientation::Pointy, Offset::Even);
728        assert_offset_eq(recovered, offset, "Offset to hex conversion (pointy, even)");
729    }
730
731    #[test]
732    fn test_offset_coordinate_to_hex_pointy_odd() {
733        let offset = OffsetCoordinate::new(1, -3);
734        let hex = Hex::from_offset(offset, HexOrientation::Pointy, Offset::Odd);
735        let recovered = hex.to_offset(HexOrientation::Pointy, Offset::Odd);
736        assert_offset_eq(recovered, offset, "Offset to hex conversion (pointy, odd)");
737    }
738
739    #[test]
740    fn test_hex_to_offset_flat_even() {
741        let hex = Hex::new(1, 2);
742        let expected = OffsetCoordinate::new(1, 3);
743        let actual = hex.to_offset(HexOrientation::Flat, Offset::Even);
744        assert_offset_eq(actual, expected, "Hex to offset (flat, even)");
745    }
746
747    #[test]
748    fn test_hex_to_offset_flat_odd() {
749        let hex = Hex::new(1, 2);
750        let expected = OffsetCoordinate::new(1, 2);
751        let actual = hex.to_offset(HexOrientation::Flat, Offset::Odd);
752        assert_offset_eq(actual, expected, "Hex to offset (flat, odd)");
753    }
754
755    #[test]
756    fn test_offset_to_hex_flat_even() {
757        let offset = OffsetCoordinate::new(1, 3);
758        let expected = Hex::new(1, 2);
759        let actual = Hex::from_offset(offset, HexOrientation::Flat, Offset::Even);
760        assert_hex_eq(actual, expected, "Offset to hex (flat, even)");
761    }
762
763    #[test]
764    fn test_offset_to_hex_flat_odd() {
765        let offset = OffsetCoordinate::new(1, 2);
766        let expected = Hex::new(1, 2);
767        let actual = Hex::from_offset(offset, HexOrientation::Flat, Offset::Odd);
768        assert_hex_eq(actual, expected, "Offset to hex (flat, odd)");
769    }
770
771    #[test]
772    fn test_hex_coordinates_accessors() {
773        let hex = Hex::new(5, -3);
774        assert_eq!(hex.x(), 5, "X coordinate accessor");
775        assert_eq!(hex.y(), -3, "Y coordinate accessor");
776        assert_eq!(hex.z(), -2, "Z coordinate (should be -x-y)");
777    }
778
779    #[test]
780    fn test_hex_addition() {
781        let a = Hex::new(2, -1);
782        let b = Hex::new(-1, 3);
783        let sum = a + b;
784        assert_hex_eq(sum, Hex::new(1, 2), "Hex addition");
785    }
786
787    #[test]
788    fn test_hex_subtraction() {
789        let a = Hex::new(5, -2);
790        let b = Hex::new(2, 1);
791        let diff = a - b;
792        assert_hex_eq(diff, Hex::new(3, -3), "Hex subtraction");
793    }
794
795    #[test]
796    fn test_hex_length() {
797        assert_eq!(Hex::new(0, 0).length(), 0, "Origin length");
798        assert_eq!(Hex::new(1, 0).length(), 1, "Unit X length");
799        assert_eq!(Hex::new(0, 1).length(), 1, "Unit Y length");
800        assert_eq!(Hex::new(3, -4).length(), 4, "Longer distance");
801    }
802
803    #[test]
804    fn test_hex_equality() {
805        let a = Hex::new(2, -3);
806        let b = Hex::new(2, -3);
807        let c = Hex::new(2, -2);
808        assert_eq!(a, b, "Equal hexes");
809        assert_ne!(a, c, "Different hexes");
810    }
811}