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