masque/masque/traits/mirrorable.py

101 lines
2.9 KiB
Python

from typing import Self
from abc import ABCMeta, abstractmethod
import numpy
from numpy.typing import NDArray
from ..error import MasqueError
from .positionable import Positionable
from .repeatable import Repeatable
class Mirrorable(metaclass=ABCMeta):
"""
Trait class for all mirrorable entities
"""
__slots__ = ()
@abstractmethod
def mirror(self, axis: int = 0) -> Self:
"""
Mirror the entity across an axis through its origin, ignoring its offset.
Args:
axis: Axis to mirror across (0: x-axis, 1: y-axis).
Returns:
self
"""
pass
def mirror2d(self, across_x: bool = False, across_y: bool = False) -> Self:
"""
Optionally mirror the entity across both axes through its origin.
Args:
across_x: Mirror across x axis (flip y)
across_y: Mirror across y axis (flip x)
Returns:
self
"""
if across_x:
self.mirror(0)
if across_y:
self.mirror(1)
return self
class Flippable(Positionable, metaclass=ABCMeta):
"""
Trait class for entities which can be mirrored relative to an external line.
"""
__slots__ = ()
@staticmethod
def _check_flip_args(axis: int | None = None, *, x: float | None = None, y: float | None = None) -> tuple[int, NDArray[numpy.float64]]:
pivot = numpy.zeros(2)
if axis is not None:
if x is not None or y is not None:
raise MasqueError('Cannot specify both axis and x or y')
return axis, pivot
if x is not None:
if y is not None:
raise MasqueError('Cannot specify both x and y')
return 1, pivot + (x, 0)
if y is not None:
return 0, pivot + (0, y)
raise MasqueError('Must specify one of axis, x, or y')
@abstractmethod
def flip_across(self, axis: int | None = None, *, x: float | None = None, y: float | None = None) -> Self:
"""
Mirror the object across a line.
Args:
axis: Axis to mirror across. 0 mirrors across x=0. 1 mirrors across y=0.
x: Vertical line x=val to mirror across.
y: Horizontal line y=val to mirror across.
Returns:
self
"""
pass
class FlippableImpl(Flippable, Mirrorable, Repeatable, metaclass=ABCMeta):
"""
Implementation of `Flippable` for objects which are `Mirrorable`, `Positionable`,
and `Repeatable`.
"""
__slots__ = ()
def flip_across(self, axis: int | None = None, *, x: float | None = None, y: float | None = None) -> Self:
axis, pivot = self._check_flip_args(axis=axis, x=x, y=y)
self.translate(-pivot)
self.mirror(axis)
if self.repetition is not None:
self.repetition.mirror(axis)
self.offset[1 - axis] *= -1
self.translate(+pivot)
return self