fixup! [Mirrorable / Flippable] Bifurcate mirror into flip (relative to line) vs mirror (relative to own offset/origin)

This commit is contained in:
Jan Petykiewicz 2026-02-15 00:49:34 -08:00
commit 2d63e72802
6 changed files with 41 additions and 17 deletions

View file

@ -8,13 +8,13 @@ from numpy.typing import ArrayLike
from .ref import Ref
from .ports import PortList, Port
from .utils import rotation_matrix_2d
from .traits import Flippable
from .traits import Mirrorable
logger = logging.getLogger(__name__)
class Abstract(PortList, Flippable):
class Abstract(PortList, Mirrorable):
"""
An `Abstract` is a container for a name and associated ports.
@ -133,7 +133,7 @@ class Abstract(PortList, Flippable):
Mirror the Abstract across an axis through its origin.
Args:
axis: Axis to mirror across (0: mirror across x axis, 1: mirror across y axis)
axis: Axis to mirror across (0: x-axis, 1: y-axis).
Returns:
self

View file

@ -114,8 +114,12 @@ class Label(PositionableImpl, RepeatableImpl, AnnotatableImpl, Bounded, Pivotabl
Returns:
self
"""
axis, pivot = self._check_flip_args(axis=axis, x=x, y=y)
self.translate(-pivot)
if self.repetition is not None:
self.repetition.flip_across(axis=axis, x=x, y=y)
self.repetition.mirror(axis)
self.offset[1 - axis] *= -1
self.translate(+pivot)
return self
def get_bounds_single(self) -> NDArray[numpy.float64]:

View file

@ -10,7 +10,7 @@ import numpy
from numpy import pi
from numpy.typing import ArrayLike, NDArray
from .traits import PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable, Flippable, FlippableImpl
from .traits import PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable, Flippable
from .utils import rotate_offsets_around, rotation_matrix_2d
from .error import PortError, format_stacktrace
@ -19,7 +19,7 @@ logger = logging.getLogger(__name__)
@functools.total_ordering
class Port(PositionableImpl, Rotatable, PivotableImpl, Copyable, FlippableImpl):
class Port(PositionableImpl, Rotatable, PivotableImpl, Copyable, Flippable):
"""
A point at which a `Device` can be snapped to another `Device`.
@ -99,6 +99,25 @@ class Port(PositionableImpl, Rotatable, PivotableImpl, Copyable, FlippableImpl):
self.ptype = ptype
return self
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 y=0. 1 mirrors across x=0.
x: Vertical line x=val to mirror across.
y: Horizontal line y=val to mirror across.
Returns:
self
"""
axis, pivot = self._check_flip_args(axis=axis, x=x, y=y)
self.translate(-pivot)
self.mirror(axis)
self.offset[1 - axis] *= -1
self.translate(+pivot)
return self
def mirror(self, axis: int = 0) -> Self:
if self.rotation is not None:
self.rotation *= -1

View file

@ -26,8 +26,9 @@ if TYPE_CHECKING:
@functools.total_ordering
class Ref(
FlippableImpl, PositionableImpl, RotatableImpl, ScalableImpl,
PositionableImpl, RotatableImpl, ScalableImpl, Mirrorable,
PivotableImpl, Copyable, RepeatableImpl, AnnotatableImpl,
FlippableImpl, Flippable,
):
"""
`Ref` provides basic support for nesting Pattern objects within each other.
@ -169,8 +170,6 @@ class Ref(
def mirror(self, axis: int = 0) -> Self:
self.mirror_target(axis)
self.rotation *= -1
if self.repetition is not None:
self.repetition.mirror(axis)
return self
def mirror_target(self, axis: int = 0) -> Self:

View file

@ -124,7 +124,6 @@ class Circle(PositionableImpl, Shape):
return self
def mirror(self, axis: int = 0) -> 'Circle': # noqa: ARG002 (axis unused)
self.offset[axis - 1] *= -1
return self
def scale_by(self, c: float) -> 'Circle':

View file

@ -6,8 +6,8 @@ from numpy.typing import ArrayLike, NDArray
from ..error import MasqueError
if TYPE_CHECKING:
from .positionable import Positionable
from .repeatable import Repeatable
class Mirrorable(metaclass=ABCMeta):
@ -54,7 +54,7 @@ class Flippable(metaclass=ABCMeta):
__slots__ = ()
@staticmethod
def _check_flip_args(axis: int | None = None, *, x: float | None = None, y: float | None = None) -> tuple[int, float]:
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:
@ -63,9 +63,9 @@ class Flippable(metaclass=ABCMeta):
if x is not None:
if y is not None:
raise MasqueError('Cannot specify both x and y')
return 0, pivot + (x, 0)
return 1, pivot + (x, 0)
if y is not None:
return 1, pivot + (0, y)
return 0, pivot + (0, y)
raise MasqueError('Must specify one of axis, x, or y')
@abstractmethod
@ -84,9 +84,10 @@ class Flippable(metaclass=ABCMeta):
pass
class FlippableImpl(Flippable, metaclass=ABCMeta):
class FlippableImpl(Flippable, Repeatable, metaclass=ABCMeta):
"""
Implementation of `Flippable` for objects which are `Mirrorable` and `Positionable`.
Implementation of `Flippable` for objects which are `Mirrorable`, `Positionable`,
and `Repeatable`.
"""
__slots__ = ()
@ -97,6 +98,8 @@ class FlippableImpl(Flippable, metaclass=ABCMeta):
axis, pivot = self._check_flip_args(axis=axis, x=x, y=y)
cast('Positionable', self).translate(-pivot)
cast('Mirrorable', self).mirror(axis)
if self.repetition is not None:
self.repetition.mirror(axis)
self.offset[1 - axis] *= -1
cast('Positionable', self).translate(+pivot)
return self