from __future__ import annotations from math import hypot from typing import Sequence from .models import ActionPayload, ActionType, GridCellModel, GridDescriptor class GridPlanner: """Helper that picks a grid cell using simple heuristics.""" def select_cell( self, descriptor: GridDescriptor, preferred_label: str | None = None ) -> GridCellModel | None: if not descriptor.cells: return None if preferred_label: match = self._match_label(descriptor.cells, preferred_label) if match: return match center_point = self._grid_center(descriptor) return min(descriptor.cells, key=lambda cell: self._distance(self._cell_center(cell), center_point)) def build_payload( self, descriptor: GridDescriptor, action: ActionType = ActionType.CLICK, preferred_label: str | None = None, text: str | None = None, comment: str | None = None, ) -> ActionPayload: target = self.select_cell(descriptor, preferred_label) return ActionPayload( grid_id=descriptor.grid_id, action=action, target_cell=target.cell_id if target else None, text=text, comment=comment, ) def describe(self, descriptor: GridDescriptor) -> str: cell_count = len(descriptor.cells) return ( f"Grid {descriptor.grid_id} is {descriptor.rows}x{descriptor.columns} with {cell_count} cells." ) def _grid_center(self, descriptor: GridDescriptor) -> tuple[float, float]: width = descriptor.metadata.get("width", 0) height = descriptor.metadata.get("height", 0) return (width / 2, height / 2) def _cell_center(self, cell: GridCellModel) -> tuple[float, float]: left, top, right, bottom = cell.bounds return ((left + right) / 2, (top + bottom) / 2) def _distance( self, first: tuple[float, float], second: tuple[float, float] ) -> float: return hypot(first[0] - second[0], first[1] - second[1]) def _match_label( self, cells: Sequence[GridCellModel], label: str ) -> GridCellModel | None: lowered = label.lower() for cell in cells: if cell.label and lowered in cell.label.lower(): return cell return None