1#![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#[derive(Clone, Copy, PartialEq, Eq, Debug)]
18pub struct Hex(IVec2);
19impl Hex {
20 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 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 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 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 pub const fn length(self) -> i32 {
105 (self.0.x.abs() + self.0.y.abs() + self.z().abs()) / 2
116 }
117
118 #[inline]
119 pub fn distance_to(self, rhs: Self) -> i32 {
121 (self - rhs).length()
122 }
123
124 pub fn hexes_at_distance(self, distance: u32) -> Vec<Hex> {
128 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 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 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 #[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 pub orientation: HexOrientation,
214 pub size: [f32; 2],
216 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 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 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 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 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 = 1,
278 Odd = -1,
280}
281
282#[derive(Clone, Copy, Debug)]
287pub struct ConversionMatrix {
288 pub forward_matrix: Mat2,
290 pub inverse_matrix: Mat2,
292}
293
294#[repr(u8)]
299#[derive(PartialEq, Eq, Clone, Copy, Debug)]
300pub enum HexOrientation {
301 Pointy,
303 Flat,
305}
306
307impl HexOrientation {
308 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 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 pub const FLAT_EDGE: [Direction; 6] = Self::POINTY_CORNER;
386
387 pub const FLAT_CORNER: [Direction; 6] = Self::POINTY_EDGE;
390
391 #[inline]
392 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 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 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 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 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 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 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 corner_index.mul_add(-FRAC_PI_3, start_angle)
479 }
480
481 #[inline]
482 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 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 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 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 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#[cfg(test)]
533mod tests {
534 use glam::Vec2;
535
536 use super::{Direction, Hex, HexLayout, HexOrientation, Offset, OffsetCoordinate};
537
538 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 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 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 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 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 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 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 let a = Vec2::ZERO;
625 let b = Vec2::new(1.0, -1.0);
626 let c = Vec2::new(0.0, -1.0);
627
628 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 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 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 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}