1 /**
2  * 2D hexagon grid math.
3  *
4  * See_Also: https://www.redblobgames.com/grids/hexagons/
5  */
6 module bettercmath.hexagrid2d;
7 
8 import std.algorithm : among;
9 import std.traits : isFloatingPoint;
10 
11 import bettercmath.cmath;
12 import bettercmath.vector;
13 import bettercmath.matrix;
14 import bettercmath.misc;
15 
16 @safe @nogc nothrow:
17 
18 private enum sqrt3 = sqrt(3);
19 
20 version (unittest)
21 {
22     private alias Hexi = Hex!(int);
23     private alias Hexf = Hex!(float);
24 }
25 
26 enum Orientation
27 {
28     pointy,
29     flat,
30 }
31 
32 struct Layout(Orientation orientation, FT = float)
33 if (isFloatingPoint!FT)
34 {
35 pure:
36     private alias Mat2 = Matrix!(FT, 2);
37     private alias Vec2 = Vector!(FT, 2);
38     private alias Vec2i = Vector!(int, 2);
39 
40     alias Hexagon = Hex!(int);
41     alias FractionalHexagon = Hex!(FT);
42 
43     Vec2 origin;
44     Vec2 size;
45 
46     static if (orientation == Orientation.pointy)
47     {
48         enum Directions
49         {
50             East = Hexagon(1, 0),
51             E = East,
52             NorthEast = Hexagon(1, -1),
53             NE = NorthEast,
54             NorthWest = Hexagon(0, -1),
55             NW = NorthWest,
56             West = Hexagon(-1, 0),
57             W = West,
58             SouthWest = Hexagon(-1, 1),
59             SW = SouthWest,
60             SouthEast = Hexagon(0, 1),
61             SE = SouthEast,
62         }
63         private enum toPixelMatrix = Mat2.fromRows(
64             sqrt3, sqrt3 / 2.0,
65             0,     3.0 / 2.0
66         );
67         private enum fromPixelMatrix = Mat2.fromRows(
68             sqrt3 / 3.0, -1.0 / 3.0,
69             0,            2.0 / 3.0
70         );
71         private enum FT[6] angles = [30, 90, 150, 210, 270, 330];
72     }
73     else
74     {
75         enum Directions
76         {
77             SouthEast = Hexagon(1, 0),
78             SE = SouthEast,
79             NorthEast = Hexagon(1, -1),
80             NE = NorthEast,
81             North = Hexagon(0, -1),
82             N = North,
83             NorthWest = Hexagon(-1, 0),
84             NW = NorthWest,
85             SouthWest = Hexagon(-1, 1),
86             SW = SouthWest,
87             South = Hexagon(0, 1),
88             S = South,
89         }
90         private enum toPixelMatrix = Mat2.fromRows(
91             3.0 / 2.0,   0,
92             sqrt3 / 2.0, sqrt3
93         );
94         private enum fromPixelMatrix = Mat2.fromRows(
95             2.0 / 3.0,  0,
96             -1.0 / 3.0, sqrt3 / 3.0
97         );
98         private enum FT[6] angles = [0, 60, 120, 180, 240, 300];
99     }
100 
101     Vec2 toPixel(const Hexagon hex) const
102     {
103         typeof(return) result = toPixelMatrix * cast(Vec2) hex.coordinates;
104         return result * size + origin;
105     }
106 
107     FractionalHexagon fromPixel(const Vec2 originalPoint) const
108     {
109         const Vec2 point = (originalPoint - origin) / size;
110         return typeof(return)(fromPixelMatrix * point);
111     }
112 
113     Vec2[6] corners() const
114     {
115         typeof(return) result = void;
116         foreach (i; 0 .. 6)
117         {
118             FT angle = deg2rad(angles[i]);
119             result[i] = [size.x * cos(angle), size.y * sin(angle)];
120         }
121         return result;
122     }
123 }
124 
125 struct Hex(T = int)
126 {
127 pure:
128     alias ElementType = T;
129     /// Axial coordinates, see https://www.redblobgames.com/grids/hexagons/implementation.html
130     private Vector!(T, 2) _coordinates;
131     @property const(typeof(_coordinates)) coordinates() const
132     {
133         return _coordinates;
134     }
135     
136     @property T q() const
137     {
138         return coordinates[0];
139     }
140     @property T r() const
141     {
142         return coordinates[1];
143     }
144     @property T s() const
145     {
146         return -q -r;
147     }
148 
149     this(T q, T r)
150     {
151         _coordinates = [q, r];
152     }
153     this(T[2] coordinates)
154     {
155         _coordinates = coordinates;
156     }
157 
158     // Operations
159     Hex opBinary(string op)(const Hex other) const
160     if (op.among("+", "-"))
161     {
162         return Hex(this.coordinates.opBinary!op(other.coordinates));
163     }
164 
165     Hex opBinary(string op : "*")(const int scale) const
166     {
167         Hex result;
168         result.coordinates = coordinates * scale;
169         return result;
170     }
171 
172     T magnitude() const
173     {
174         import std.algorithm : sum;
175         return cast(T)((fabs(q) + fabs(r) + fabs(s)) / 2);
176     }
177 
178     T distanceTo(const Hex other) const
179     {
180         Hex vector = this - other;
181         return vector.magnitude();
182     }
183 }
184 
185 Hex!(int) rounded(FT)(const Hex!(FT) hex)
186 if (isFloatingPoint!FT)
187 {
188     import std.algorithm : map;
189     alias Vec3 = Vector!(FT, 3);
190     Vec3 cubic_hex = hex.coordinates ~ hex.s;
191     Vec3 roundedVec = cubic_hex[].map!(round);
192     Vec3 diff = roundedVec - cubic_hex;
193 
194     if (diff[0] > diff[1] && diff[0] > diff[2])
195     {
196         roundedVec[0] = -roundedVec[1] - roundedVec[2];
197     }
198     else if (diff[1] > diff[2])
199     {
200         roundedVec[1] = -roundedVec[0] - roundedVec[2];
201     }
202     return typeof(return)(cast(int) roundedVec[0], cast(int) roundedVec[1]);
203 }
204 unittest
205 {
206     Hexf a = Hexf(2.1, 3.5); // -5.6
207     assert(a.rounded() == Hexi(2, 4));
208 }
209 
210 struct RectangleHexagrid(Orientation orientation, T, uint columns, uint rows)
211 {
212     Layout!(orientation) layout;
213     Hex!(int) hexagons;
214     T[columns][rows] values;
215 }