65 lines
2 KiB
Python
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]
|