from __future__ import annotations class OrthoCamera: """Top-down orthographic camera; world space is tile units with y increasing downward.""" def __init__(self, viewport_w: int, viewport_h: int, tiles_visible_y: float = 12.0): self.x = 0.0 self.y = 0.0 self.viewport_w = viewport_w self.viewport_h = viewport_h self.tiles_visible_y = tiles_visible_y def set_target(self, x: float, y: float) -> None: self.x, self.y = x, y def resize(self, viewport_w: int, viewport_h: int) -> None: self.viewport_w, self.viewport_h = viewport_w, viewport_h def _world_bounds(self) -> tuple[float, float, float, float]: half_h = self.tiles_visible_y / 2 aspect = self.viewport_w / max(self.viewport_h, 1) half_w = half_h * aspect return self.x - half_w, self.x + half_w, self.y - half_h, self.y + half_h def scale_offset(self) -> tuple[tuple[float, float], tuple[float, float]]: """Returns (scale, offset) such that ndc = world * scale + offset.""" left, right, top, bottom = self._world_bounds() scale_x = 2.0 / (right - left) scale_y = -2.0 / (bottom - top) offset_x = -left * scale_x - 1.0 offset_y = 1.0 - top * scale_y return (scale_x, scale_y), (offset_x, offset_y) def screen_to_world(self, px: float, py: float) -> tuple[float, float]: """Unprojects a canvas logical-pixel position (0,0 top-left) into world tile coords.""" left, right, top, bottom = self._world_bounds() wx = left + (px / max(self.viewport_w, 1)) * (right - left) wy = top + (py / max(self.viewport_h, 1)) * (bottom - top) return wx, wy