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)