hex_grid.pyΒΆ
Part of Bloom.
1"""Hex grid: pointy-top axial-coordinate math, viewport-parametrized.
2
3Adapted from the dependency-free hex math used across SimVX (cube/axial
4coordinates, neighbours, distance, ranges). The layout helpers here take an
5explicit ``origin`` and ``size`` so the board can rescale to any viewport while
6render and hit-test stay provably consistent (they share one ``(origin, size)``).
7"""
8
9import math
10from dataclasses import dataclass
11
12from simvx.core import Vec2
13
14SQRT3 = math.sqrt(3)
15
16
17@dataclass(frozen=True, slots=True)
18class Hex:
19 """Cube coordinate hex cell (q + r + s == 0)."""
20
21 q: int
22 r: int
23
24 @property
25 def s(self) -> int:
26 return -self.q - self.r
27
28 def __add__(self, other: Hex) -> Hex:
29 return Hex(self.q + other.q, self.r + other.r)
30
31 def __sub__(self, other: Hex) -> Hex:
32 return Hex(self.q - other.q, self.r - other.r)
33
34 def __neg__(self) -> Hex:
35 return Hex(-self.q, -self.r)
36
37 def __hash__(self) -> int:
38 return hash((self.q, self.r))
39
40
41# Six neighbour direction vectors (pointy-top).
42DIRECTIONS = [
43 Hex(1, 0),
44 Hex(1, -1),
45 Hex(0, -1),
46 Hex(-1, 0),
47 Hex(-1, 1),
48 Hex(0, 1),
49]
50
51
52def neighbours(h: Hex) -> list[Hex]:
53 return [h + d for d in DIRECTIONS]
54
55
56def hex_distance(a: Hex, b: Hex) -> int:
57 d = a - b
58 return max(abs(d.q), abs(d.r), abs(d.s))
59
60
61def hex_in_range(centre: Hex, radius: int) -> set[Hex]:
62 """All hexes within ``radius`` distance of ``centre`` (inclusive)."""
63 results: set[Hex] = set()
64 for q in range(-radius, radius + 1):
65 r1 = max(-radius, -q - radius)
66 r2 = min(radius, -q + radius)
67 for r in range(r1, r2 + 1):
68 results.add(Hex(centre.q + q, centre.r + r))
69 return results
70
71
72def hex_round(fq: float, fr: float) -> Hex:
73 """Round fractional cube coordinates to the nearest hex."""
74 fs = -fq - fr
75 rq, rr, rs = round(fq), round(fr), round(fs)
76 dq, dr, ds = abs(rq - fq), abs(rr - fr), abs(rs - fs)
77 if dq > dr and dq > ds:
78 rq = -rr - rs
79 elif dr > ds:
80 rr = -rq - rs
81 return Hex(rq, rr)
82
83
84def hex_to_pixel(h: Hex, origin: Vec2, size: float) -> Vec2:
85 """Cube hex coordinate -> pixel centre (pointy-top), scaled by ``size``."""
86 x = size * (SQRT3 * h.q + SQRT3 / 2 * h.r)
87 y = size * (3.0 / 2 * h.r)
88 return Vec2(x + origin.x, y + origin.y)
89
90
91def pixel_to_hex(px: float, py: float, origin: Vec2, size: float) -> Hex:
92 """Pixel position -> nearest hex (pointy-top), scaled by ``size``."""
93 x, y = px - origin.x, py - origin.y
94 fq = (SQRT3 / 3 * x - 1.0 / 3 * y) / size
95 fr = (2.0 / 3 * y) / size
96 return hex_round(fq, fr)
97
98
99def hex_corners(centre: Vec2, size: float) -> list[Vec2]:
100 """Six corner positions of a pointy-top hex centred at ``centre``."""
101 corners = []
102 for i in range(6):
103 angle = math.radians(60 * i - 30)
104 corners.append(Vec2(centre.x + size * math.cos(angle), centre.y + size * math.sin(angle)))
105 return corners