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 }