From 2d63e7280279b7a0d5bfcae6e554c55ac6e09a26 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 15 Feb 2026 00:49:34 -0800 Subject: [PATCH] fixup! [Mirrorable / Flippable] Bifurcate mirror into flip (relative to line) vs mirror (relative to own offset/origin) --- masque/abstract.py | 6 +++--- masque/label.py | 6 +++++- masque/ports.py | 23 +++++++++++++++++++++-- masque/ref.py | 5 ++--- masque/shapes/circle.py | 1 - masque/traits/mirrorable.py | 17 ++++++++++------- 6 files changed, 41 insertions(+), 17 deletions(-) diff --git a/masque/abstract.py b/masque/abstract.py index 1266ae6..501e394 100644 --- a/masque/abstract.py +++ b/masque/abstract.py @@ -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 diff --git a/masque/label.py b/masque/label.py index 3f4f80e..4bd2c4a 100644 --- a/masque/label.py +++ b/masque/label.py @@ -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]: diff --git a/masque/ports.py b/masque/ports.py index c0f7ddc..3fc556a 100644 --- a/masque/ports.py +++ b/masque/ports.py @@ -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 diff --git a/masque/ref.py b/masque/ref.py index 75a27a8..70e52db 100644 --- a/masque/ref.py +++ b/masque/ref.py @@ -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: diff --git a/masque/shapes/circle.py b/masque/shapes/circle.py index b20a681..8dad165 100644 --- a/masque/shapes/circle.py +++ b/masque/shapes/circle.py @@ -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': diff --git a/masque/traits/mirrorable.py b/masque/traits/mirrorable.py index fffa9c8..2ff486a 100644 --- a/masque/traits/mirrorable.py +++ b/masque/traits/mirrorable.py @@ -6,8 +6,8 @@ from numpy.typing import ArrayLike, NDArray from ..error import MasqueError -if TYPE_CHECKING: - from .positionable import Positionable +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