1 # Copyright 2023 highstreet technologies GmbH
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
7 # http://www.apache.org/licenses/LICENSE-2.0
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
15 # inspired by http://www.redblobgames.com/grids/hexagons/
19 from __future__ import division, print_function
23 from typing import NamedTuple
25 from network_generation.model.python.geo_location import GeoLocation
26 from network_generation.model.python.point import Point
30 def __init__(self, q: float, r: float, s: float) -> None:
31 if round(q + r + s) != 0:
32 raise ValueError("The sum of q, r, and s must be 0.")
37 def __str__(self) -> str:
38 return f"q: {self.q}, r: {self.r}, s: {self.s}"
41 def hex_add(a: Hex, b: Hex) -> Hex:
42 return Hex(a.q + b.q, a.r + b.r, a.s + b.s)
45 def hex_subtract(a: Hex, b: Hex) -> Hex:
46 return Hex(a.q - b.q, a.r - b.r, a.s - b.s)
49 def hex_scale(a: Hex, k: int) -> Hex:
50 return Hex(a.q * k, a.r * k, a.s * k)
53 def hex_rotate_left(a: Hex) -> Hex:
54 return Hex(-a.s, -a.q, -a.r)
57 def hex_rotate_right(a: Hex) -> Hex:
58 return Hex(-a.r, -a.s, -a.q)
61 hex_directions: list[Hex] = [
71 def hex_direction(direction: int) -> Hex:
72 return hex_directions[direction]
75 def hex_neighbor(hex: Hex, direction: int) -> Hex:
76 return hex_add(hex, hex_direction(direction))
79 hex_diagonals: list[Hex] = [
89 def hex_diagonal_neighbor(hex: Hex, direction: int) -> Hex:
90 return hex_add(hex, hex_diagonals[direction])
93 def hex_length(hex: Hex) -> float:
94 return (abs(hex.q) + abs(hex.r) + abs(hex.s)) // 2
97 def hex_distance(a: Hex, b: Hex) -> float:
98 return hex_length(hex_subtract(a, b))
101 def hex_round(hex: Hex) -> Hex:
105 q_diff = abs(qi - hex.q)
106 r_diff = abs(ri - hex.r)
107 s_diff = abs(si - hex.s)
108 if q_diff > r_diff and q_diff > s_diff:
115 return Hex(qi, ri, si)
118 def hex_lerp(a: Hex, b: Hex, t: float) -> Hex: # linearly interpolation
120 a.q * (1 - t) + b.q * t,
121 a.r * (1 - t) + b.r * t,
122 a.s * (1 - t) + b.s * t,
126 def hex_linedraw(a: Hex, b: Hex) -> list[Hex]:
127 N: float = hex_distance(a, b)
128 a_nudge: Hex = Hex(a.q + 1e-06, a.r + 1e-06, a.s - 2e-06)
129 b_nudge: Hex = Hex(b.q + 1e-06, b.r + 1e-06, b.s - 2e-06)
130 results: list[Hex] = []
131 step: float = 1 / max(N, 1)
132 for i in range(0, int(N) + 1):
133 results.append(hex_round(hex_lerp(a_nudge, b_nudge, step * i)))
137 OffsetCoord = collections.namedtuple("OffsetCoord", ["col", "row"])
143 def qoffset_from_cube(offset: float, hex: Hex) -> OffsetCoord:
145 row = hex.r + (hex.q + offset * (int(hex.q) & 1)) // 2
146 if offset != EVEN and offset != ODD:
147 raise ValueError("offset must be EVEN (+1) or ODD (-1)")
148 return OffsetCoord(col, row)
151 def qoffset_to_cube(offset: int, offsetCoord: OffsetCoord) -> Hex:
155 - (offsetCoord.col + offset * (offsetCoord.col & 1)) // 2
158 if offset != EVEN and offset != ODD:
159 raise ValueError("offset must be EVEN (+1) or ODD (-1)")
163 def roffset_from_cube(offset: float, hex: Hex) -> OffsetCoord:
164 col = hex.q + (hex.r + offset * (int(hex.r) & 1)) // 2
166 if offset != EVEN and offset != ODD:
167 raise ValueError("offset must be EVEN (+1) or ODD (-1)")
168 return OffsetCoord(col, row)
171 def roffset_to_cube(offset: float, hex: OffsetCoord) -> Hex:
172 q = hex.col - (hex.row + offset * (int(hex.row) & 1)) // 2
175 if offset != EVEN and offset != ODD:
176 raise ValueError("offset must be EVEN (+1) or ODD (-1)")
180 DoubledCoord = collections.namedtuple("DoubledCoord", ["col", "row"])
183 def qdoubled_from_cube(hex: Hex) -> DoubledCoord:
185 row = 2 * hex.r + hex.q
186 return DoubledCoord(col, row)
189 def qdoubled_to_cube(doubledCoord: DoubledCoord) -> Hex:
191 r = (doubledCoord.row - doubledCoord.col) // 2
196 def rdoubled_from_cube(hex: Hex) -> DoubledCoord:
197 col = 2 * hex.q + hex.r
199 return DoubledCoord(col, row)
202 def rdoubled_to_cube(doubledCoord: DoubledCoord) -> Hex:
203 q = (doubledCoord.col - doubledCoord.row) // 2
209 Orientation = collections.namedtuple(
211 ["f0", "f1", "f2", "f3", "b0", "b1", "b2", "b3", "start_angle"],
215 # Layout = collections.namedtuple("Layout", ["orientation", "size", "origin"])
216 class Layout(NamedTuple):
217 orientation: Orientation
222 layout_pointy: Orientation = Orientation(
224 math.sqrt(3.0) / 2.0,
227 math.sqrt(3.0) / 3.0,
233 layout_flat: Orientation = Orientation(
236 math.sqrt(3.0) / 2.0,
241 math.sqrt(3.0) / 3.0,
246 def hex_to_pixel(layout: Layout, hex: Hex) -> Point:
247 M = layout.orientation
249 origin = layout.origin
250 x = (M.f0 * hex.q + M.f1 * hex.r) * size.x
251 y = (M.f2 * hex.q + M.f3 * hex.r) * size.y
252 return Point(x + origin.x, y + origin.y)
255 def pixel_to_hex(layout: Layout, p: Point) -> Hex:
256 M = layout.orientation
258 origin = layout.origin
259 pt = Point((p.x - origin.x) / size.x, (p.y - origin.y) / size.y)
260 q = M.b0 * pt.x + M.b1 * pt.y
261 r = M.b2 * pt.x + M.b3 * pt.y
262 return Hex(q, r, -q - r)
265 def hex_corner_offset(layout: Layout, corner: int) -> Point:
266 M = layout.orientation
268 angle = 2.0 * math.pi * (M.start_angle - corner) / 6.0
269 return Point(size.x * math.cos(angle), size.y * math.sin(angle))
272 def polygon_corners(layout: Layout, hex: Hex) -> list[Point]:
273 corners: list[Point] = []
274 center = hex_to_pixel(layout, hex)
275 for i in range(0, 6):
276 offset = hex_corner_offset(layout, i)
277 corners.append(Point(center.x + offset.x, center.y + offset.y))
281 def hex_to_geo_location(
282 layout: Layout, hex: Hex, reference: GeoLocation
284 hexPoint: Point = hex_to_pixel(layout, hex)
285 return reference.point_to_geo_location(hexPoint)