from typing import Self import copy import logging import numpy from numpy.typing import ArrayLike from .ref import Ref from .ports import PortList, Port from .utils import rotation_matrix_2d from .traits import Mirrorable logger = logging.getLogger(__name__) class Abstract(PortList, Mirrorable): """ An `Abstract` is a container for a name and associated ports. When snapping a sub-component to an existing pattern, only the name (not contained in a `Pattern` object) and port info is needed, and not the geometry itself. """ # Alternate design option: do we want to store a Ref instead of just a name? then we can translate/rotate/mirror... __slots__ = ('name', '_ports') name: str """ Name of the pattern this device references """ _ports: dict[str, Port] """ Uniquely-named ports which can be used to instances together""" @property def ports(self) -> dict[str, Port]: return self._ports @ports.setter def ports(self, value: dict[str, Port]) -> None: self._ports = value def __init__( self, name: str, ports: dict[str, Port], ) -> None: self.name = name self.ports = copy.deepcopy(ports) def __repr__(self) -> str: s = f' Self: """ Translates all ports by the given offset. Args: offset: (x, y) to translate by Returns: self """ for port in self.ports.values(): port.translate(offset) return self def scale_by(self, c: float) -> Self: """ Scale this Abstract by the given value (all port offsets are scaled) Args: c: factor to scale by Returns: self """ for port in self.ports.values(): port.offset *= c return self def rotate_around(self, pivot: ArrayLike, rotation: float) -> Self: """ Rotate the Abstract around a pivot point. Args: pivot: (x, y) location to rotate around rotation: Angle to rotate by (counter-clockwise, radians) Returns: self """ pivot = numpy.asarray(pivot, dtype=float) self.translate_ports(-pivot) self.rotate_ports(rotation) self.rotate_port_offsets(rotation) self.translate_ports(+pivot) return self def rotate_port_offsets(self, rotation: float) -> Self: """ Rotate the offsets of all ports around (0, 0) Args: rotation: Angle to rotate by (counter-clockwise, radians) Returns: self """ for port in self.ports.values(): port.offset = rotation_matrix_2d(rotation) @ port.offset return self def rotate_ports(self, rotation: float) -> Self: """ Rotate each port around its offset (i.e. in place) Args: rotation: Angle to rotate by (counter-clockwise, radians) Returns: self """ for port in self.ports.values(): port.rotate(rotation) return self def mirror(self, axis: int = 0) -> Self: """ Mirror the Abstract across an axis through its origin. Args: axis: Axis to mirror across (0: x-axis, 1: y-axis). Returns: self """ for port in self.ports.values(): port.flip_across(axis=axis) return self def apply_ref_transform(self, ref: Ref) -> Self: """ Apply the transform from a `Ref` to the ports of this `Abstract`. This changes the port locations to where they would be in the Ref's parent pattern. Args: ref: The ref whose transform should be applied. Returns: self """ if ref.mirrored: self.mirror() self.rotate_ports(ref.rotation) self.rotate_port_offsets(ref.rotation) if ref.scale != 1: self.scale_by(ref.scale) self.translate_ports(ref.offset) return self def undo_ref_transform(self, ref: Ref) -> Self: """ Apply the inverse transform from a `Ref` to the ports of this `Abstract`. This changes the port locations to where they would be in the Ref's target (from the parent). Args: ref: The ref whose (inverse) transform should be applied. Returns: self # TODO test undo_ref_transform """ self.translate_ports(-ref.offset) if ref.scale != 1: self.scale_by(1 / ref.scale) self.rotate_port_offsets(-ref.rotation) self.rotate_ports(-ref.rotation) if ref.mirrored: self.mirror(0) return self