inire/inire/geometry/primitives.py
2026-03-30 23:54:30 -07:00

65 lines
2 KiB
Python

from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING, Self
import numpy
if TYPE_CHECKING:
from numpy.typing import NDArray
def _normalize_angle(angle_deg: int | float) -> int:
angle = int(round(angle_deg)) % 360
if angle % 90 != 0:
raise ValueError(f"Port angle must be Manhattan (multiple of 90), got {angle_deg!r}")
return angle
@dataclass(frozen=True, slots=True)
class Port:
"""
Port represented as a normalized integer (x, y, r) triple.
"""
x: int | float
y: int | float
r: int | float
def __post_init__(self) -> None:
object.__setattr__(self, "x", int(round(self.x)))
object.__setattr__(self, "y", int(round(self.y)))
object.__setattr__(self, "r", _normalize_angle(self.r))
def as_tuple(self) -> tuple[int, int, int]:
return (int(self.x), int(self.y), int(self.r))
def translate(
self,
dx: int | float = 0,
dy: int | float = 0,
rotation: int | float = 0,
) -> Self:
return type(self)(self.x + dx, self.y + dy, self.r + rotation)
def rotated(
self,
angle: int | float,
origin: tuple[int | float, int | float] = (0, 0),
) -> Self:
angle_i = _normalize_angle(angle)
rot = rotation_matrix2(angle_i)
origin_xy = numpy.array((int(round(origin[0])), int(round(origin[1]))), dtype=numpy.int32)
rel = numpy.array((self.x, self.y), dtype=numpy.int32) - origin_xy
rotated = origin_xy + rot @ rel
return type(self)(int(rotated[0]), int(rotated[1]), self.r + angle_i)
ROT2_0 = numpy.array(((1, 0), (0, 1)), dtype=numpy.int32)
ROT2_90 = numpy.array(((0, -1), (1, 0)), dtype=numpy.int32)
ROT2_180 = numpy.array(((-1, 0), (0, -1)), dtype=numpy.int32)
ROT2_270 = numpy.array(((0, 1), (-1, 0)), dtype=numpy.int32)
def rotation_matrix2(rotation_deg: int | float) -> NDArray[numpy.int32]:
quadrant = (_normalize_angle(rotation_deg) // 90) % 4
return (ROT2_0, ROT2_90, ROT2_180, ROT2_270)[quadrant]