QUELLCODE / PY

mapping.py

src/hardware_box/hid/mapping.py

1# OpenAss: nur privater, experimenteller Gebrauch; auf eigene Verantwortung.2# Niemals Unternehmensrechner, Produktivsysteme, Industrieanlagen oder3# sicherheitskritische Systeme anschließen oder bedienen.4# Haftungshinweis und gesetzliche Grenzen: ../../../DISCLAIMER.txt.5"""Map screenshot pixels to an absolute USB HID desktop pointer.67The capture may downscale the whole desktop. Its pixel dimensions do not reveal8the target's native resolution: target_width/height are operator configuration.9content_rect=(left, top, width, height) is an explicitly calibrated crop that10excludes letterboxing. It must depict the entire controlled desktop. Multiple11monitors, overscan, rotated displays and OS pointer mapping require calibration.12"""1314from dataclasses import dataclass151617class MappingError(ValueError):18    """The capture geometry or requested screenshot pixel is unsafe."""192021def _integer(value: object, name: str, minimum: int = 0) -> int:22    # bool is an int in Python, but is not a valid configured pixel coordinate.23    if type(value) is not int or value < minimum:24        raise MappingError(f"{name} must be an integer >= {minimum}")25    return value262728@dataclass(frozen=True)29class Geometry:30    screenshot_width: int31    screenshot_height: int32    target_width: int33    target_height: int34    content_rect: tuple[int, int, int, int] | None = None3536    def __post_init__(self) -> None:37        for name in ("screenshot_width", "screenshot_height", "target_width", "target_height"):38            _integer(getattr(self, name), name, 2)39        if self.content_rect is not None:40            if not isinstance(self.content_rect, (list, tuple)) or len(self.content_rect) != 4:41                raise MappingError("content_rect must be (left, top, width, height)")42            left, top, width, height = self.content_rect43            _integer(left, "content left")44            _integer(top, "content top")45            _integer(width, "content width", 2)46            _integer(height, "content height", 2)47            if left + width > self.screenshot_width or top + height > self.screenshot_height:48                raise MappingError("content_rect must fit inside the screenshot")49            object.__setattr__(self, "content_rect", tuple(self.content_rect))5051    @property52    def content(self) -> tuple[int, int, int, int]:53        return self.content_rect or (0, 0, self.screenshot_width, self.screenshot_height)5455    def _fractions(self, x: int, y: int) -> tuple[float, float]:56        _integer(x, "x")57        _integer(y, "y")58        left, top, width, height = self.content59        if not (left <= x < left + width and top <= y < top + height):60            raise MappingError("point lies outside calibrated desktop content")61        return (x - left) / (width - 1), (y - top) / (height - 1)6263    def to_hid(self, x: int, y: int) -> tuple[int, int]:64        """Normalize content endpoints exactly to 0..32767; never clamp bars."""65        fx, fy = self._fractions(x, y)66        return round(fx * 32767), round(fy * 32767)6768    def to_absolute(self, x: int, y: int) -> tuple[int, int]:69        """Alias used by the safety layer for the authoritative HID transform."""70        return self.to_hid(x, y)7172    def to_target(self, x: int, y: int) -> tuple[int, int]:73        """Return configured desktop pixels, useful for calibration diagnostics."""74        fx, fy = self._fractions(x, y)75        return round(fx * (self.target_width - 1)), round(fy * (self.target_height - 1))767778def screenshot_to_hid(x: int, y: int, geometry: Geometry) -> tuple[int, int]:79    return geometry.to_hid(x, y)