[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:05:53 -08:00
commit 44986bac67
11 changed files with 115 additions and 111 deletions

View file

@ -8,16 +8,13 @@ from numpy.typing import ArrayLike
from .ref import Ref from .ref import Ref
from .ports import PortList, Port from .ports import PortList, Port
from .utils import rotation_matrix_2d from .utils import rotation_matrix_2d
from .traits import Flippable
#if TYPE_CHECKING:
# from .builder import Builder, Tool
# from .library import ILibrary
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class Abstract(PortList): class Abstract(PortList, Flippable):
""" """
An `Abstract` is a container for a name and associated ports. An `Abstract` is a container for a name and associated ports.
@ -131,50 +128,18 @@ class Abstract(PortList):
port.rotate(rotation) port.rotate(rotation)
return self return self
def mirror_port_offsets(self, across_axis: int = 0) -> Self: def mirror(self, axis: int = 0) -> Self:
""" """
Mirror the offsets of all shapes, labels, and refs across an axis Mirror the Abstract across an axis through its origin.
Args: Args:
across_axis: Axis to mirror across axis: Axis to mirror across (0: mirror across x axis, 1: mirror across y axis)
(0: mirror across x axis, 1: mirror across y axis)
Returns: Returns:
self self
""" """
for port in self.ports.values(): for port in self.ports.values():
port.offset[across_axis - 1] *= -1 port.flip_across(axis=axis)
return self
def mirror_ports(self, across_axis: int = 0) -> Self:
"""
Mirror each port's rotation across an axis, relative to its
offset
Args:
across_axis: Axis to mirror across
(0: mirror across x axis, 1: mirror across y axis)
Returns:
self
"""
for port in self.ports.values():
port.mirror(across_axis)
return self
def mirror(self, across_axis: int = 0) -> Self:
"""
Mirror the Pattern across an axis
Args:
axis: Axis to mirror across
(0: mirror across x axis, 1: mirror across y axis)
Returns:
self
"""
self.mirror_ports(across_axis)
self.mirror_port_offsets(across_axis)
return self return self
def apply_ref_transform(self, ref: Ref) -> Self: def apply_ref_transform(self, ref: Ref) -> Self:

View file

@ -7,12 +7,12 @@ from numpy.typing import ArrayLike, NDArray
from .repetition import Repetition from .repetition import Repetition
from .utils import rotation_matrix_2d, annotations_t, annotations_eq, annotations_lt, rep2key from .utils import rotation_matrix_2d, annotations_t, annotations_eq, annotations_lt, rep2key
from .traits import PositionableImpl, Copyable, Pivotable, RepeatableImpl, Bounded from .traits import PositionableImpl, Copyable, Pivotable, RepeatableImpl, Bounded, Flippable
from .traits import AnnotatableImpl from .traits import AnnotatableImpl
@functools.total_ordering @functools.total_ordering
class Label(PositionableImpl, RepeatableImpl, AnnotatableImpl, Bounded, Pivotable, Copyable): class Label(PositionableImpl, RepeatableImpl, AnnotatableImpl, Bounded, Pivotable, Copyable, Flippable):
""" """
A text annotation with a position (but no size; it is not drawn) A text annotation with a position (but no size; it is not drawn)
""" """
@ -102,6 +102,22 @@ class Label(PositionableImpl, RepeatableImpl, AnnotatableImpl, Bounded, Pivotabl
self.translate(+pivot) self.translate(+pivot)
return self 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 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
"""
if self.repetition is not None:
self.repetition.flip_across(axis=axis, x=x, y=y)
return self
def get_bounds_single(self) -> NDArray[numpy.float64]: def get_bounds_single(self) -> NDArray[numpy.float64]:
""" """
Return the bounds of the label. Return the bounds of the label.

View file

@ -733,50 +733,31 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
cast('Rotatable', entry).rotate(rotation) cast('Rotatable', entry).rotate(rotation)
return self return self
def mirror_element_centers(self, across_axis: int = 0) -> Self: def mirror_elements(self, across_axis: int = 0) -> Self:
""" """
Mirror the offsets of all shapes, labels, and refs across an axis Mirror each shape, ref, and port relative to (0,0).
Args: Args:
across_axis: Axis to mirror across across_axis: Axis to mirror across
(0: mirror across x axis, 1: mirror across y axis) (0: mirror across x axis, 1: mirror across y axis)
Returns:
self
"""
return self.flip_across(axis=across_axis)
def mirror(self, axis: int = 0) -> Self:
"""
Mirror the Pattern across an axis through its origin.
Args:
axis: Axis to mirror across (0: x-axis, 1: y-axis).
Returns: Returns:
self self
""" """
for entry in chain(chain_elements(self.shapes, self.refs, self.labels), self.ports.values()): for entry in chain(chain_elements(self.shapes, self.refs, self.labels), self.ports.values()):
cast('Positionable', entry).offset[1 - across_axis] *= -1 cast('Flippable', entry).flip_across(axis=axis)
return self
def mirror_elements(self, across_axis: int = 0) -> Self:
"""
Mirror each shape, ref, and pattern across an axis, relative
to its offset
Args:
across_axis: Axis to mirror across
(0: mirror across x axis, 1: mirror across y axis)
Returns:
self
"""
for entry in chain(chain_elements(self.shapes, self.refs), self.ports.values()):
cast('Mirrorable', entry).mirror(across_axis)
return self
def mirror(self, across_axis: int = 0) -> Self:
"""
Mirror the Pattern across an axis
Args:
across_axis: Axis to mirror across
(0: mirror across x axis, 1: mirror across y axis)
Returns:
self
"""
self.mirror_elements(across_axis)
self.mirror_element_centers(across_axis)
return self return self
def copy(self) -> Self: def copy(self) -> Self:

View file

@ -10,7 +10,7 @@ import numpy
from numpy import pi from numpy import pi
from numpy.typing import ArrayLike, NDArray from numpy.typing import ArrayLike, NDArray
from .traits import PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable from .traits import PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable, Flippable, FlippableImpl
from .utils import rotate_offsets_around, rotation_matrix_2d from .utils import rotate_offsets_around, rotation_matrix_2d
from .error import PortError, format_stacktrace from .error import PortError, format_stacktrace
@ -19,7 +19,7 @@ logger = logging.getLogger(__name__)
@functools.total_ordering @functools.total_ordering
class Port(PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable): class Port(PositionableImpl, Rotatable, PivotableImpl, Copyable, FlippableImpl):
""" """
A point at which a `Device` can be snapped to another `Device`. A point at which a `Device` can be snapped to another `Device`.

View file

@ -16,6 +16,7 @@ from .repetition import Repetition
from .traits import ( from .traits import (
PositionableImpl, RotatableImpl, ScalableImpl, PositionableImpl, RotatableImpl, ScalableImpl,
Mirrorable, PivotableImpl, Copyable, RepeatableImpl, AnnotatableImpl, Mirrorable, PivotableImpl, Copyable, RepeatableImpl, AnnotatableImpl,
Flippable, FlippableImpl,
) )
@ -25,7 +26,7 @@ if TYPE_CHECKING:
@functools.total_ordering @functools.total_ordering
class Ref( class Ref(
PositionableImpl, RotatableImpl, ScalableImpl, Mirrorable, FlippableImpl, PositionableImpl, RotatableImpl, ScalableImpl,
PivotableImpl, Copyable, RepeatableImpl, AnnotatableImpl, PivotableImpl, Copyable, RepeatableImpl, AnnotatableImpl,
): ):
""" """

View file

@ -384,7 +384,6 @@ class Arc(PositionableImpl, Shape):
return self return self
def mirror(self, axis: int = 0) -> 'Arc': def mirror(self, axis: int = 0) -> 'Arc':
self.offset[1 - axis] *= -1
self.rotation *= -1 self.rotation *= -1
self.rotation += axis * pi self.rotation += axis * pi
self.angles *= -1 self.angles *= -1

View file

@ -189,7 +189,6 @@ class Ellipse(PositionableImpl, Shape):
return self return self
def mirror(self, axis: int = 0) -> Self: def mirror(self, axis: int = 0) -> Self:
self.offset[1 - axis] *= -1
self.rotation *= -1 self.rotation *= -1
self.rotation += axis * pi self.rotation += axis * pi
return self return self

View file

@ -6,8 +6,8 @@ import numpy
from numpy.typing import NDArray, ArrayLike from numpy.typing import NDArray, ArrayLike
from ..traits import ( from ..traits import (
Rotatable, Mirrorable, Copyable, Scalable, Rotatable, Mirrorable, Copyable, Scalable, FlippableImpl,
Positionable, PivotableImpl, RepeatableImpl, AnnotatableImpl, Positionable, Pivotable, PivotableImpl, RepeatableImpl, AnnotatableImpl,
) )
if TYPE_CHECKING: if TYPE_CHECKING:
@ -26,8 +26,9 @@ normalized_shape_tuple = tuple[
DEFAULT_POLY_NUM_VERTICES = 24 DEFAULT_POLY_NUM_VERTICES = 24
class Shape(Positionable, Rotatable, Mirrorable, Copyable, Scalable, class Shape(Positionable, Rotatable, FlippableImpl, Copyable, Scalable,
PivotableImpl, RepeatableImpl, AnnotatableImpl, metaclass=ABCMeta): AnnotatableImpl, RepeatableImpl, PivotableImpl, Pivotable,
metaclass=ABCMeta):
""" """
Class specifying functions common to all shapes. Class specifying functions common to all shapes.
""" """

View file

@ -70,6 +70,7 @@ class Text(PositionableImpl, RotatableImpl, Shape):
*, *,
offset: ArrayLike = (0.0, 0.0), offset: ArrayLike = (0.0, 0.0),
rotation: float = 0.0, rotation: float = 0.0,
mirrored: bool = False,
repetition: Repetition | None = None, repetition: Repetition | None = None,
annotations: annotations_t = None, annotations: annotations_t = None,
raw: bool = False, raw: bool = False,
@ -80,6 +81,7 @@ class Text(PositionableImpl, RotatableImpl, Shape):
self._string = string self._string = string
self._height = height self._height = height
self._rotation = rotation self._rotation = rotation
self._mirrored = mirrored
self._repetition = repetition self._repetition = repetition
self._annotations = annotations self._annotations = annotations
else: else:
@ -87,6 +89,7 @@ class Text(PositionableImpl, RotatableImpl, Shape):
self.string = string self.string = string
self.height = height self.height = height
self.rotation = rotation self.rotation = rotation
self.mirrored = mirrored
self.repetition = repetition self.repetition = repetition
self.annotations = annotations self.annotations = annotations
self.font_path = font_path self.font_path = font_path

View file

@ -26,7 +26,11 @@ from .scalable import (
Scalable as Scalable, Scalable as Scalable,
ScalableImpl as ScalableImpl, ScalableImpl as ScalableImpl,
) )
from .mirrorable import Mirrorable as Mirrorable from .mirrorable import (
Mirrorable as Mirrorable,
Flippable as Flippable,
FlippableImpl as FlippableImpl,
)
from .copyable import Copyable as Copyable from .copyable import Copyable as Copyable
from .annotatable import ( from .annotatable import (
Annotatable as Annotatable, Annotatable as Annotatable,

View file

@ -1,6 +1,14 @@
from typing import Self from typing import Self, Any, TYPE_CHECKING, cast
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod
import numpy
from numpy.typing import ArrayLike, NDArray
from ..error import MasqueError
if TYPE_CHECKING:
from .positionable import Positionable
class Mirrorable(metaclass=ABCMeta): class Mirrorable(metaclass=ABCMeta):
""" """
@ -11,10 +19,10 @@ class Mirrorable(metaclass=ABCMeta):
@abstractmethod @abstractmethod
def mirror(self, axis: int = 0) -> Self: def mirror(self, axis: int = 0) -> Self:
""" """
Mirror the entity across an axis. Mirror the entity across an axis through its origin, ignoring its offset.
Args: Args:
axis: Axis to mirror across. axis: Axis to mirror across (0: x-axis, 1: y-axis).
Returns: Returns:
self self
@ -23,10 +31,11 @@ class Mirrorable(metaclass=ABCMeta):
def mirror2d(self, across_x: bool = False, across_y: bool = False) -> Self: def mirror2d(self, across_x: bool = False, across_y: bool = False) -> Self:
""" """
Optionally mirror the entity across both axes Optionally mirror the entity across both axes through its origin.
Args: Args:
axes: (mirror_across_x, mirror_across_y) across_x: Mirror across x axis (flip y)
across_y: Mirror across y axis (flip x)
Returns: Returns:
self self
@ -38,30 +47,56 @@ class Mirrorable(metaclass=ABCMeta):
return self return self
#class MirrorableImpl(Mirrorable, metaclass=ABCMeta): class Flippable(metaclass=ABCMeta):
# """ """
# Simple implementation of `Mirrorable` Trait class for entities which can be mirrored relative to an external line.
# """ """
# __slots__ = () __slots__ = ()
#
# _mirrored: NDArray[numpy.bool] @staticmethod
# """ Whether to mirror the instance across the x and/or y axes. """ def _check_flip_args(axis: int | None = None, *, x: float | None = None, y: float | None = None) -> tuple[int, float]:
# pivot = numpy.zeros(2)
# # if axis is not None:
# # Properties if x is not None or y is not None:
# # raise MasqueError('Cannot specify both axis and x or y')
# # Mirrored property return axis, pivot
# @property if x is not None:
# def mirrored(self) -> NDArray[numpy.bool]: if y is not None:
# """ Whether to mirror across the [x, y] axes, respectively """ raise MasqueError('Cannot specify both x and y')
# return self._mirrored return 0, pivot + (x, 0)
# if y is not None:
# @mirrored.setter return 1, pivot + (0, y)
# def mirrored(self, val: Sequence[bool]) -> None: raise MasqueError('Must specify one of axis, x, or y')
# if is_scalar(val):
# raise MasqueError('Mirrored must be a 2-element list of booleans') @abstractmethod
# self._mirrored = numpy.array(val, dtype=bool) def flip_across(self, axis: int | None = None, *, x: float | None = None, y: float | None = None) -> Self:
# """
# # Mirror the object across a line.
# # Methods
# # 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, metaclass=ABCMeta):
"""
Implementation of `Flippable` for objects which are `Mirrorable` and `Positionable`.
"""
__slots__ = ()
offset: NDArray[numpy.float64]
""" `[x_offset, y_offset]` """
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)
cast('Positionable', self).translate(-pivot)
cast('Mirrorable', self).mirror(axis)
self.offset[1 - axis] *= -1
cast('Positionable', self).translate(+pivot)
return self