Source code for pyhexlib.viewport

#  MIT License
#
#  Copyright (c) 2026 Heiko Sippel
#
#  Permission is hereby granted, free of charge, to any person obtaining a copy
#  of this software and associated documentation files (the "Software"), to deal
#  in the Software without restriction, including without limitation the rights
#  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#  copies of the Software, and to permit persons to whom the Software is
#  furnished to do so, subject to the following conditions:
#
#  The above copyright notice and this permission notice shall be included in all
#  copies or substantial portions of the Software.
#
#  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
#  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
#  SOFTWARE.
#
#
from math import sqrt
from typing import Any, Dict

import pyhexlib
import pyhexlib.graphic as g
from pyhexlib.basic import Hexagon, Bounds
from pyhexlib.graphic import Point
from pyhexlib.hexagons import HexagonalGrid

# ---------------------------------------- Logger ------------------------------------------------

LOGGER = pyhexlib.get_logger(__name__)


# ----------------------------------------- HexGridViewport -----------------------------------------------

[docs] class Viewport(Bounds): def __init__(self, bounds: Bounds, outer_bounds: Bounds) -> None: super().__init__(*bounds) self.outer_bounds = outer_bounds # ------------------------- Developer Representation ----------------------------------- def __repr__(self) -> str: return ( f"<{HexGridViewport.__name__}, bounds: top={self.top}, left={self.left}, bottom={self.bottom}, right={self.right}> " f"outer bounds={self.outer_bounds!r} >")
# ----------------------------------------- Hexagons Cache -----------------------------------------------
[docs] class Hexagons: def __init__(self, hexagons: Dict[Any, Any]) -> None: self._hexagons = hexagons # ----------------------------- Methods for accessing hexagon data -------------------------------
[docs] def center(self, rc: Hexagon) -> Point: """Return cached pixel center for hex coordinate rc (r,c).""" return self._hexagons[rc]["center"]
[docs] def edges(self, rc: Hexagon) -> list[Point]: """Return cached list of six corner points for hex key h (r,c).""" return self._hexagons[rc]["edges"]
# ------------------------- Dictionary-like access to hexagon data ------------------------------- def __iter__(self): return iter(self._hexagons) def __len__(self): return len(self._hexagons) def __contains__(self, item): return item in self._hexagons def __getitem__(self, key): return self._hexagons[key]
[docs] def keys(self): return self._hexagons.keys()
[docs] def items(self): return self._hexagons.items()
[docs] def values(self): return self._hexagons.values()
[docs] def get(self, key, default=None): return self._hexagons.get(key, default)
# ------------------------- Developer Representation ----------------------------------- def __repr__(self): return f"Hexagons(count={len(self)}: {self._hexagons})"
# ----------------------------------------- HexGridViewport -----------------------------------------------
[docs] class HexGridViewport: def __init__(self, hexagonal_grid: HexagonalGrid, radius: int, visible_size: tuple[int, int], origin: Hexagon, offset: Point = Point(0, 0)) -> None: self.hexagonal_grid = hexagonal_grid self.radius = radius self._origin = Hexagon(origin.row, origin.col) self.bounds = self.hexagonal_grid.bounds # The real bounds, including non-visible hexagons self.offset = offset self.rows, self.cols = visible_size self.inner_radius = sqrt(3) * self.radius / 2 _, self.x_dist, self.y_dist = g.hex_dimensions(self.radius) self.hexagons = self._compute_hexagons() # ---------------------------- Methods for accessing hexagon data -------------------------------
[docs] def center(self, rc: Hexagon) -> Point: """Return cached pixel center for hex coordinate rc (r,c).""" return self.hexagons.center(rc)
[docs] def edges(self, rc: Hexagon) -> list[Point]: """Return cached list of six corner points for hex key h (r,c).""" return self.hexagons.edges(rc)
# ---------------------------- Methods dependent from Orientation ------------------------------- def _compute_hex_center(self, r: int, c: int, offset: Point) -> Point: return g.hex_center(r, c, self.x_dist, self.y_dist, self.offset, self.inner_radius, self.radius) - offset def _compute_hex_corners(self, center) -> list[Point]: return g.hex_corners(center, self.radius) # ----------------------------------- Independent methods ----------------------------------- def _compute_hexagons(self) -> Hexagons: hexes = {} offset = self._compute_offset() for (r, c) in self._get_hexagon_within_bounds(): center = self._compute_hex_center(r, c, offset) edges = self._compute_hex_corners(center) hexes[Hexagon(r, c)] = {"center": center, "edges": edges} return Hexagons(hexes) def _compute_offset(self) -> Point: return g.compute_offset(*self.origin, self.x_dist, self.y_dist, self.offset, self.inner_radius, self.radius) def _get_hexagon_within_bounds(self) -> list[Hexagon]: hexagons = [rc for rc in self.hexagonal_grid if self.bounds.contains(rc)] return hexagons def _xy_with_offset(self, p: Point): return p.x + self.offset.x, p.y + self.offset.y # ------------------------- Properties for viewport configuration ------------------------------- @property def origin(self) -> Hexagon: """Return the current origin hex for rendering.""" return self._origin @origin.setter def origin(self, origin: Hexagon) -> None: bounds = Bounds(origin.row, origin.col, origin.row + self.rows, origin.col + self.cols) self.bounds = self.adjust_bounds(bounds) self.hexagons = self._compute_hexagons()
[docs] def adjust_bounds(self, bounds: Bounds) -> Bounds: # Ensure the viewport origin stays within the grid bounds. # `bounds` parameter contains a requested topleft (r_min, c_min) and an # implied size (we treat bottom/right as origin + rows/cols). The # grid bounds give the allowable area for the viewport origin. top, left, bottom, right = self.hexagonal_grid.bounds r_min, c_min, r_max, c_max = bounds # Compute the maximum allowed origin so that origin + (rows, cols) # does not exceed the grid's bottom/right. We use max(..., top) # to handle the case where the grid is smaller than the viewport. max_origin_row = max(top, bottom - self.rows) max_origin_col = max(left, right - self.cols) # Clamp requested origin to [top ... max_origin_row] and [left ... max_origin_col] r_min_temp = min(max(r_min, top), max_origin_row) c_min_temp = min(max(c_min, left), max_origin_col) # Set the adjusted origin and corresponding bounds (origin + size) self._origin = Hexagon(r_min_temp, c_min_temp) self.bounds = Bounds(r_min_temp, c_min_temp, r_min_temp + self.rows, c_min_temp + self.cols) return self.bounds
# ----------------------------------- Moving the Viewport -----------------------------------
[docs] def move_to(self, origin: Hexagon) -> None: """Move the viewport to a new origin hex.""" self.origin = origin
[docs] def move_by(self, rows: int = 0, cols: int = 0) -> None: """Move the viewport by a given amount.""" row, col = self.origin self.origin = Hexagon(row + rows, col + cols)
# ------------------------- Developer Representation ----------------------------------- def __repr__(self) -> str: return (f"<{HexGridViewport.__name__}, origin={self.origin!r}, rows={self.rows!r}, cols = {self.cols!r}, " f"bounds={self.bounds!r} hex_count={len(self.hexagons)}>")