111 lines
2.7 KiB
Python
111 lines
2.7 KiB
Python
from __future__ import annotations
|
|
|
|
import numpy
|
|
|
|
|
|
# 1nm snap (0.001 µm)
|
|
GRID_SNAP_UM = 0.001
|
|
|
|
|
|
def snap_nm(value: float) -> float:
|
|
"""
|
|
Snap a coordinate to the nearest 1nm (0.001 um).
|
|
|
|
Args:
|
|
value: Coordinate value to snap.
|
|
|
|
Returns:
|
|
Snapped coordinate value.
|
|
"""
|
|
return round(value / GRID_SNAP_UM) * GRID_SNAP_UM
|
|
|
|
|
|
class Port:
|
|
"""
|
|
A port defined by (x, y, orientation) in micrometers.
|
|
"""
|
|
__slots__ = ('x', 'y', 'orientation')
|
|
|
|
x: float
|
|
""" x-coordinate in micrometers """
|
|
|
|
y: float
|
|
""" y-coordinate in micrometers """
|
|
|
|
orientation: float
|
|
""" Orientation in degrees: 0, 90, 180, 270 """
|
|
|
|
def __init__(
|
|
self,
|
|
x: float,
|
|
y: float,
|
|
orientation: float,
|
|
) -> None:
|
|
"""
|
|
Initialize and snap a Port.
|
|
|
|
Args:
|
|
x: Initial x-coordinate.
|
|
y: Initial y-coordinate.
|
|
orientation: Initial orientation in degrees.
|
|
"""
|
|
# Snap x, y to 1nm
|
|
self.x = snap_nm(x)
|
|
self.y = snap_nm(y)
|
|
|
|
# Ensure orientation is one of {0, 90, 180, 270}
|
|
norm_orientation = int(round(orientation)) % 360
|
|
if norm_orientation not in {0, 90, 180, 270}:
|
|
norm_orientation = (round(norm_orientation / 90) * 90) % 360
|
|
|
|
self.orientation = float(norm_orientation)
|
|
|
|
def __repr__(self) -> str:
|
|
return f'Port(x={self.x}, y={self.y}, orientation={self.orientation})'
|
|
|
|
def __eq__(self, other: object) -> bool:
|
|
if not isinstance(other, Port):
|
|
return False
|
|
return (self.x == other.x and
|
|
self.y == other.y and
|
|
self.orientation == other.orientation)
|
|
|
|
def __hash__(self) -> int:
|
|
return hash((self.x, self.y, self.orientation))
|
|
|
|
|
|
def translate_port(port: Port, dx: float, dy: float) -> Port:
|
|
"""
|
|
Translate a port by (dx, dy).
|
|
|
|
Args:
|
|
port: Port to translate.
|
|
dx: x-offset.
|
|
dy: y-offset.
|
|
|
|
Returns:
|
|
A new translated Port.
|
|
"""
|
|
return Port(port.x + dx, port.y + dy, port.orientation)
|
|
|
|
|
|
def rotate_port(port: Port, angle: float, origin: tuple[float, float] = (0, 0)) -> Port:
|
|
"""
|
|
Rotate a port by a multiple of 90 degrees around an origin.
|
|
|
|
Args:
|
|
port: Port to rotate.
|
|
angle: Angle to rotate by (degrees).
|
|
origin: (x, y) origin to rotate around.
|
|
|
|
Returns:
|
|
A new rotated Port.
|
|
"""
|
|
ox, oy = origin
|
|
px, py = port.x, port.y
|
|
|
|
rad = numpy.radians(angle)
|
|
qx = ox + numpy.cos(rad) * (px - ox) - numpy.sin(rad) * (py - oy)
|
|
qy = oy + numpy.sin(rad) * (px - ox) + numpy.cos(rad) * (py - oy)
|
|
|
|
return Port(qx, qy, port.orientation + angle)
|