don't keep track of y-mirroring separately from x

master
jan 1 year ago
parent 9bc8d29b85
commit 91465b7175

@ -17,7 +17,8 @@ def main():
cell_name = 'ellip_grating' cell_name = 'ellip_grating'
pat = masque.Pattern() pat = masque.Pattern()
for rmin in numpy.arange(10, 15, 0.5): for rmin in numpy.arange(10, 15, 0.5):
pat.shapes.append(Arc( layer = (0, 0)
pat.shapes[layer].append(Arc(
radii=(rmin, rmin), radii=(rmin, rmin),
width=0.1, width=0.1,
angles=(0 * -pi/4, pi/4), angles=(0 * -pi/4, pi/4),
@ -35,27 +36,27 @@ def main():
print(f'\nAdded a copy of {cell_name} as {new_name}') print(f'\nAdded a copy of {cell_name} as {new_name}')
pat3 = Pattern() pat3 = Pattern()
pat3.refs = [ pat3.refs[cell_name] = [
Ref(cell_name, offset=(1e5, 3e5), annotations={'4': ['Hello I am the base Ref']}), Ref(offset=(1e5, 3e5), annotations={'4': ['Hello I am the base Ref']}),
Ref(cell_name, offset=(2e5, 3e5), rotation=pi/3), Ref(offset=(2e5, 3e5), rotation=pi/3),
Ref(cell_name, offset=(3e5, 3e5), rotation=pi/2), Ref(offset=(3e5, 3e5), rotation=pi/2),
Ref(cell_name, offset=(4e5, 3e5), rotation=pi), Ref(offset=(4e5, 3e5), rotation=pi),
Ref(cell_name, offset=(5e5, 3e5), rotation=3*pi/2), Ref(offset=(5e5, 3e5), rotation=3*pi/2),
Ref(cell_name, mirrored=(True, False), offset=(1e5, 4e5)), Ref(mirrored=True, offset=(1e5, 4e5)),
Ref(cell_name, mirrored=(True, False), offset=(2e5, 4e5), rotation=pi/3), Ref(mirrored=True, offset=(2e5, 4e5), rotation=pi/3),
Ref(cell_name, mirrored=(True, False), offset=(3e5, 4e5), rotation=pi/2), Ref(mirrored=True, offset=(3e5, 4e5), rotation=pi/2),
Ref(cell_name, mirrored=(True, False), offset=(4e5, 4e5), rotation=pi), Ref(mirrored=True, offset=(4e5, 4e5), rotation=pi),
Ref(cell_name, mirrored=(True, False), offset=(5e5, 4e5), rotation=3*pi/2), Ref(mirrored=True, offset=(5e5, 4e5), rotation=3*pi/2),
Ref(cell_name, mirrored=(False, True), offset=(1e5, 5e5)), Ref(offset=(1e5, 5e5)).mirror_target(1),
Ref(cell_name, mirrored=(False, True), offset=(2e5, 5e5), rotation=pi/3), Ref(offset=(2e5, 5e5), rotation=pi/3).mirror_target(1),
Ref(cell_name, mirrored=(False, True), offset=(3e5, 5e5), rotation=pi/2), Ref(offset=(3e5, 5e5), rotation=pi/2).mirror_target(1),
Ref(cell_name, mirrored=(False, True), offset=(4e5, 5e5), rotation=pi), Ref(offset=(4e5, 5e5), rotation=pi).mirror_target(1),
Ref(cell_name, mirrored=(False, True), offset=(5e5, 5e5), rotation=3*pi/2), Ref(offset=(5e5, 5e5), rotation=3*pi/2).mirror_target(1),
Ref(cell_name, mirrored=(True, True), offset=(1e5, 6e5)), Ref(offset=(1e5, 6e5)).mirror2d_target(True, True),
Ref(cell_name, mirrored=(True, True), offset=(2e5, 6e5), rotation=pi/3), Ref(offset=(2e5, 6e5), rotation=pi/3).mirror2d_target(True, True),
Ref(cell_name, mirrored=(True, True), offset=(3e5, 6e5), rotation=pi/2), Ref(offset=(3e5, 6e5), rotation=pi/2).mirror2d_target(True, True),
Ref(cell_name, mirrored=(True, True), offset=(4e5, 6e5), rotation=pi), Ref(offset=(4e5, 6e5), rotation=pi).mirror2d_target(True, True),
Ref(cell_name, mirrored=(True, True), offset=(5e5, 6e5), rotation=3*pi/2), Ref(offset=(5e5, 6e5), rotation=3*pi/2).mirror2d_target(True, True),
] ]
lib['sref_test'] = pat3 lib['sref_test'] = pat3
@ -70,27 +71,27 @@ def main():
b_count=2, b_count=2,
) )
pat4 = Pattern() pat4 = Pattern()
pat4.refs = [ pat4.refs[cell_name] = [
Ref(cell_name, repetition=rep, offset=(1e5, 3e5)), Ref(repetition=rep, offset=(1e5, 3e5)),
Ref(cell_name, repetition=rep, offset=(2e5, 3e5), rotation=pi/3), Ref(repetition=rep, offset=(2e5, 3e5), rotation=pi/3),
Ref(cell_name, repetition=rep, offset=(3e5, 3e5), rotation=pi/2), Ref(repetition=rep, offset=(3e5, 3e5), rotation=pi/2),
Ref(cell_name, repetition=rep, offset=(4e5, 3e5), rotation=pi), Ref(repetition=rep, offset=(4e5, 3e5), rotation=pi),
Ref(cell_name, repetition=rep, offset=(5e5, 3e5), rotation=3*pi/2), Ref(repetition=rep, offset=(5e5, 3e5), rotation=3*pi/2),
Ref(cell_name, repetition=rep, mirrored=(True, False), offset=(1e5, 4e5)), Ref(repetition=rep, mirrored=True, offset=(1e5, 4e5)),
Ref(cell_name, repetition=rep, mirrored=(True, False), offset=(2e5, 4e5), rotation=pi/3), Ref(repetition=rep, mirrored=True, offset=(2e5, 4e5), rotation=pi/3),
Ref(cell_name, repetition=rep, mirrored=(True, False), offset=(3e5, 4e5), rotation=pi/2), Ref(repetition=rep, mirrored=True, offset=(3e5, 4e5), rotation=pi/2),
Ref(cell_name, repetition=rep, mirrored=(True, False), offset=(4e5, 4e5), rotation=pi), Ref(repetition=rep, mirrored=True, offset=(4e5, 4e5), rotation=pi),
Ref(cell_name, repetition=rep, mirrored=(True, False), offset=(5e5, 4e5), rotation=3*pi/2), Ref(repetition=rep, mirrored=True, offset=(5e5, 4e5), rotation=3*pi/2),
Ref(cell_name, repetition=rep, mirrored=(False, True), offset=(1e5, 5e5)), Ref(repetition=rep, offset=(1e5, 5e5)).mirror_target(1),
Ref(cell_name, repetition=rep, mirrored=(False, True), offset=(2e5, 5e5), rotation=pi/3), Ref(repetition=rep, offset=(2e5, 5e5), rotation=pi/3).mirror_target(1),
Ref(cell_name, repetition=rep, mirrored=(False, True), offset=(3e5, 5e5), rotation=pi/2), Ref(repetition=rep, offset=(3e5, 5e5), rotation=pi/2).mirror_target(1),
Ref(cell_name, repetition=rep, mirrored=(False, True), offset=(4e5, 5e5), rotation=pi), Ref(repetition=rep, offset=(4e5, 5e5), rotation=pi).mirror_target(1),
Ref(cell_name, repetition=rep, mirrored=(False, True), offset=(5e5, 5e5), rotation=3*pi/2), Ref(repetition=rep, offset=(5e5, 5e5), rotation=3*pi/2).mirror_target(1),
Ref(cell_name, repetition=rep, mirrored=(True, True), offset=(1e5, 6e5)), Ref(repetition=rep, offset=(1e5, 6e5)).mirror2d_target(True, True),
Ref(cell_name, repetition=rep, mirrored=(True, True), offset=(2e5, 6e5), rotation=pi/3), Ref(repetition=rep, offset=(2e5, 6e5), rotation=pi/3).mirror2d_target(True, True),
Ref(cell_name, repetition=rep, mirrored=(True, True), offset=(3e5, 6e5), rotation=pi/2), Ref(repetition=rep, offset=(3e5, 6e5), rotation=pi/2).mirror2d_target(True, True),
Ref(cell_name, repetition=rep, mirrored=(True, True), offset=(4e5, 6e5), rotation=pi), Ref(repetition=rep, offset=(4e5, 6e5), rotation=pi).mirror2d_target(True, True),
Ref(cell_name, repetition=rep, mirrored=(True, True), offset=(5e5, 6e5), rotation=3*pi/2), Ref(repetition=rep, offset=(5e5, 6e5), rotation=3*pi/2).mirror2d_target(True, True),
] ]
lib['aref_test'] = pat4 lib['aref_test'] = pat4

@ -7,7 +7,7 @@ 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, normalize_mirror from .utils import rotation_matrix_2d
#if TYPE_CHECKING: #if TYPE_CHECKING:
# from .builder import Builder, Tool # from .builder import Builder, Tool
@ -143,7 +143,7 @@ class Abstract(PortList):
port.rotate(rotation) port.rotate(rotation)
return self return self
def mirror_port_offsets(self, across_axis: int) -> Self: def mirror_port_offsets(self, across_axis: int = 0) -> Self:
""" """
Mirror the offsets of all shapes, labels, and refs across an axis Mirror the offsets of all shapes, labels, and refs across an axis
@ -158,7 +158,7 @@ class Abstract(PortList):
port.offset[across_axis - 1] *= -1 port.offset[across_axis - 1] *= -1
return self return self
def mirror_ports(self, across_axis: int) -> Self: def mirror_ports(self, across_axis: int = 0) -> Self:
""" """
Mirror each port's rotation across an axis, relative to its Mirror each port's rotation across an axis, relative to its
offset offset
@ -174,7 +174,7 @@ class Abstract(PortList):
port.mirror(across_axis) port.mirror(across_axis)
return self return self
def mirror(self, across_axis: int) -> Self: def mirror(self, across_axis: int = 0) -> Self:
""" """
Mirror the Pattern across an axis Mirror the Pattern across an axis
@ -200,11 +200,10 @@ class Abstract(PortList):
Returns: Returns:
self self
""" """
mirrored_across_x, angle = normalize_mirror(ref.mirrored) if ref.mirrored:
if mirrored_across_x: self.mirror()
self.mirror(across_axis=0) self.rotate_ports(ref.rotation)
self.rotate_ports(angle + ref.rotation) self.rotate_port_offsets(ref.rotation)
self.rotate_port_offsets(angle + ref.rotation)
self.translate_ports(ref.offset) self.translate_ports(ref.offset)
return self return self
@ -221,10 +220,9 @@ class Abstract(PortList):
# TODO test undo_ref_transform # TODO test undo_ref_transform
""" """
mirrored_across_x, angle = normalize_mirror(ref.mirrored)
self.translate_ports(-ref.offset) self.translate_ports(-ref.offset)
self.rotate_port_offsets(-angle - ref.rotation) self.rotate_port_offsets(-ref.rotation)
self.rotate_ports(-angle - ref.rotation) self.rotate_ports(-ref.rotation)
if mirrored_across_x: if ref.mirrored:
self.mirror(across_axis=0) self.mirror(0)
return self return self

@ -230,7 +230,7 @@ class Builder(PortList):
# map_in: dict[str, str], # map_in: dict[str, str],
# map_out: dict[str, str | None] | None, # map_out: dict[str, str | None] | None,
# *, # *,
# mirrored: tuple[bool, bool], # mirrored: bool = False,
# inherit_name: bool, # inherit_name: bool,
# set_rotation: bool | None, # set_rotation: bool | None,
# append: bool, # append: bool,
@ -244,7 +244,7 @@ class Builder(PortList):
# map_in: dict[str, str], # map_in: dict[str, str],
# map_out: dict[str, str | None] | None = None, # map_out: dict[str, str | None] | None = None,
# *, # *,
# mirrored: tuple[bool, bool] = (False, False), # mirrored: bool = False,
# inherit_name: bool = True, # inherit_name: bool = True,
# set_rotation: bool | None = None, # set_rotation: bool | None = None,
# append: bool = False, # append: bool = False,
@ -257,7 +257,7 @@ class Builder(PortList):
map_in: dict[str, str], map_in: dict[str, str],
map_out: dict[str, str | None] | None = None, map_out: dict[str, str | None] | None = None,
*, *,
mirrored: tuple[bool, bool] = (False, False), mirrored: bool = False,
inherit_name: bool = True, inherit_name: bool = True,
set_rotation: bool | None = None, set_rotation: bool | None = None,
append: bool = False, append: bool = False,
@ -351,11 +351,9 @@ class Builder(PortList):
if isinstance(other, Pattern): if isinstance(other, Pattern):
assert append assert append
self.place(other, offset=translation, rotation=rotation, pivot=pivot,
mirrored=mirrored, port_map=map_out, skip_port_check=True, append=append) self.place(other, offset=translation, rotation=rotation, pivot=pivot,
else: mirrored=mirrored, port_map=map_out, skip_port_check=True, append=append)
self.place(other, offset=translation, rotation=rotation, pivot=pivot,
mirrored=mirrored, port_map=map_out, skip_port_check=True, append=append)
return self return self
# @overload # @overload
@ -366,7 +364,7 @@ class Builder(PortList):
# offset: ArrayLike, # offset: ArrayLike,
# rotation: float, # rotation: float,
# pivot: ArrayLike, # pivot: ArrayLike,
# mirrored: tuple[bool, bool], # mirrored: bool = False,
# port_map: dict[str, str | None] | None, # port_map: dict[str, str | None] | None,
# skip_port_check: bool, # skip_port_check: bool,
# append: bool, # append: bool,
@ -381,7 +379,7 @@ class Builder(PortList):
# offset: ArrayLike, # offset: ArrayLike,
# rotation: float, # rotation: float,
# pivot: ArrayLike, # pivot: ArrayLike,
# mirrored: tuple[bool, bool], # mirrored: bool = False,
# port_map: dict[str, str | None] | None, # port_map: dict[str, str | None] | None,
# skip_port_check: bool, # skip_port_check: bool,
# append: Literal[True], # append: Literal[True],
@ -395,7 +393,7 @@ class Builder(PortList):
offset: ArrayLike = (0, 0), offset: ArrayLike = (0, 0),
rotation: float = 0, rotation: float = 0,
pivot: ArrayLike = (0, 0), pivot: ArrayLike = (0, 0),
mirrored: tuple[bool, bool] = (False, False), mirrored: bool = False,
port_map: dict[str, str | None] | None = None, port_map: dict[str, str | None] | None = None,
skip_port_check: bool = False, skip_port_check: bool = False,
append: bool = False, append: bool = False,
@ -461,7 +459,8 @@ class Builder(PortList):
for name, port in ports.items(): for name, port in ports.items():
p = port.deepcopy() p = port.deepcopy()
p.mirror2d(mirrored) if mirrored:
p.mirror()
p.rotate_around(pivot, rotation) p.rotate_around(pivot, rotation)
p.translate(offset) p.translate(offset)
self.ports[name] = p self.ports[name] = p
@ -476,7 +475,8 @@ class Builder(PortList):
other_pat = self.library[name] other_pat = self.library[name]
other_copy = other_pat.deepcopy() other_copy = other_pat.deepcopy()
other_copy.ports.clear() other_copy.ports.clear()
other_copy.mirror2d(mirrored) if mirrored:
other_copy.mirror()
other_copy.rotate_around(pivot, rotation) other_copy.rotate_around(pivot, rotation)
other_copy.translate_elements(offset) other_copy.translate_elements(offset)
self.pattern.append(other_copy) self.pattern.append(other_copy)
@ -517,7 +517,7 @@ class Builder(PortList):
port.rotate_around(pivot, angle) port.rotate_around(pivot, angle)
return self return self
def mirror(self, axis: int) -> Self: def mirror(self, axis: int = 0) -> Self:
""" """
Mirror the pattern and all ports across the specified axis. Mirror the pattern and all ports across the specified axis.
@ -528,8 +528,6 @@ class Builder(PortList):
self self
""" """
self.pattern.mirror(axis) self.pattern.mirror(axis)
for p in self.ports.values():
p.mirror(axis)
return self return self
def set_dead(self) -> Self: def set_dead(self) -> Self:

@ -13,7 +13,6 @@ from ..library import ILibrary
from ..error import PortError, BuildError from ..error import PortError, BuildError
from ..ports import PortList, Port from ..ports import PortList, Port
from ..abstract import Abstract from ..abstract import Abstract
from ..utils import rotation_matrix_2d
from ..utils import SupportsBool from ..utils import SupportsBool
from .tools import Tool, RenderStep from .tools import Tool, RenderStep
from .utils import ell from .utils import ell
@ -94,7 +93,6 @@ class RenderPather(PortList):
else: else:
self.tools = dict(tools) self.tools = dict(tools)
@classmethod @classmethod
def interface( def interface(
cls, cls,
@ -200,7 +198,7 @@ class RenderPather(PortList):
map_in: dict[str, str], map_in: dict[str, str],
map_out: dict[str, str | None] | None = None, map_out: dict[str, str | None] | None = None,
*, *,
mirrored: tuple[bool, bool] = (False, False), mirrored: bool = False,
inherit_name: bool = True, inherit_name: bool = True,
set_rotation: bool | None = None, set_rotation: bool | None = None,
) -> Self: ) -> Self:
@ -250,7 +248,7 @@ class RenderPather(PortList):
offset: ArrayLike = (0, 0), offset: ArrayLike = (0, 0),
rotation: float = 0, rotation: float = 0,
pivot: ArrayLike = (0, 0), pivot: ArrayLike = (0, 0),
mirrored: tuple[bool, bool] = (False, False), mirrored: bool = False,
port_map: dict[str, str | None] | None = None, port_map: dict[str, str | None] | None = None,
skip_port_check: bool = False, skip_port_check: bool = False,
) -> Self: ) -> Self:
@ -280,7 +278,8 @@ class RenderPather(PortList):
for name, port in ports.items(): for name, port in ports.items():
p = port.deepcopy() p = port.deepcopy()
p.mirror2d(mirrored) if mirrored:
p.mirror()
p.rotate_around(pivot, rotation) p.rotate_around(pivot, rotation)
p.translate(offset) p.translate(offset)
self.ports[name] = p self.ports[name] = p

@ -125,7 +125,7 @@ class BasicTool(Tool, metaclass=ABCMeta):
bb.plug(straight, {port_names[1]: sport_in}) bb.plug(straight, {port_names[1]: sport_in})
if data.ccw is not None: if data.ccw is not None:
bend, bport_in, bport_out = self.bend bend, bport_in, bport_out = self.bend
bb.plug(bend, {port_names[1]: bport_in}, mirrored=(False, bool(ccw))) bb.plug(bend, {port_names[1]: bport_in}, mirrored=bool(ccw))
if data.out_transition: if data.out_transition:
opat, oport_theirs, oport_ours = data.out_transition opat, oport_theirs, oport_ours = data.out_transition
bb.plug(opat, {port_names[1]: oport_ours}) bb.plug(opat, {port_names[1]: oport_ours})
@ -239,7 +239,7 @@ class BasicTool(Tool, metaclass=ABCMeta):
bb.plug(straight, {port_names[1]: sport_in}, append=True) bb.plug(straight, {port_names[1]: sport_in}, append=True)
if ccw is not None: if ccw is not None:
bend, bport_in, bport_out = self.bend bend, bport_in, bport_out = self.bend
bb.plug(bend, {port_names[1]: bport_in}, mirrored=(False, bool(ccw))) bb.plug(bend, {port_names[1]: bport_in}, mirrored=bool(ccw))
if out_transition: if out_transition:
opat, oport_theirs, oport_ours = out_transition opat, oport_theirs, oport_ours = out_transition
bb.plug(opat, {port_names[1]: oport_ours}) bb.plug(opat, {port_names[1]: oport_ours})
@ -256,7 +256,6 @@ class PathTool(Tool, metaclass=ABCMeta):
#class LData: #class LData:
# dxy: NDArray[numpy.float64] # dxy: NDArray[numpy.float64]
#def __init__(self, layer: layer_t, width: float, ptype: str = 'unk') -> None: #def __init__(self, layer: layer_t, width: float, ptype: str = 'unk') -> None:
# Tool.__init__(self) # Tool.__init__(self)
# self.layer = layer # self.layer = layer

@ -21,7 +21,7 @@ from .. import Pattern, Ref, PatternError, Label
from ..library import ILibraryView, LibraryView, Library from ..library import ILibraryView, LibraryView, Library
from ..shapes import Shape, Polygon, Path from ..shapes import Shape, Polygon, Path
from ..repetition import Grid from ..repetition import Grid
from ..utils import rotation_matrix_2d, layer_t from ..utils import rotation_matrix_2d, layer_t, normalize_mirror
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -258,8 +258,8 @@ def _read_block(block) -> tuple[str, Pattern]:
if abs(xscale) != abs(yscale): if abs(xscale) != abs(yscale):
logger.warning('Masque does not support per-axis scaling; using x-scaling only!') logger.warning('Masque does not support per-axis scaling; using x-scaling only!')
scale = abs(xscale) scale = abs(xscale)
mirrored = (yscale < 0, xscale < 0) mirrored, extra_angle = normalize_mirror((yscale < 0, xscale < 0))
rotation = numpy.deg2rad(attr.get('rotation', 0)) rotation = numpy.deg2rad(attr.get('rotation', 0)) + extra_angle
offset = numpy.array(attr.get('insert', (0, 0, 0)))[:2] offset = numpy.array(attr.get('insert', (0, 0, 0)))[:2]
@ -291,8 +291,8 @@ def _mrefs_to_drefs(
def mk_blockref(encoded_name: str, ref: Ref) -> None: def mk_blockref(encoded_name: str, ref: Ref) -> None:
rotation = numpy.rad2deg(ref.rotation) % 360 rotation = numpy.rad2deg(ref.rotation) % 360
attribs = dict( attribs = dict(
xscale=ref.scale * (-1 if ref.mirrored[1] else 1), xscale=ref.scale,
yscale=ref.scale * (-1 if ref.mirrored[0] else 1), yscale=ref.scale * (-1 if ref.mirrored else 1),
rotation=rotation, rotation=rotation,
) )

@ -37,7 +37,7 @@ from .utils import is_gzipped, tmpfile
from .. import Pattern, Ref, PatternError, LibraryError, Label, Shape from .. import Pattern, Ref, PatternError, LibraryError, Label, Shape
from ..shapes import Polygon, Path from ..shapes import Polygon, Path
from ..repetition import Grid from ..repetition import Grid
from ..utils import layer_t, normalize_mirror, annotations_t from ..utils import layer_t, annotations_t
from ..library import LazyLibrary, Library, ILibrary, ILibraryView from ..library import LazyLibrary, Library, ILibrary, ILibraryView
@ -306,7 +306,7 @@ def _gref_to_mref(ref: klamath.library.Reference) -> tuple[str, Ref]:
offset=offset, offset=offset,
rotation=numpy.deg2rad(ref.angle_deg), rotation=numpy.deg2rad(ref.angle_deg),
scale=ref.mag, scale=ref.mag,
mirrored=(ref.invert_y, False), mirrored=ref.invert_y,
annotations=_properties_to_annotations(ref.properties), annotations=_properties_to_annotations(ref.properties),
repetition=repetition, repetition=repetition,
) )
@ -348,10 +348,9 @@ def _mrefs_to_grefs(refs: dict[str | None, list[Ref]]) -> list[klamath.library.R
continue continue
encoded_name = target.encode('ASCII') encoded_name = target.encode('ASCII')
for ref in rseq: for ref in rseq:
# Note: GDS mirrors first and rotates second # Note: GDS also mirrors first and rotates second
mirror_across_x, extra_angle = normalize_mirror(ref.mirrored)
rep = ref.repetition rep = ref.repetition
angle_deg = numpy.rad2deg(ref.rotation + extra_angle) % 360 angle_deg = numpy.rad2deg(ref.rotation) % 360
properties = _annotations_to_properties(ref.annotations, 512) properties = _annotations_to_properties(ref.annotations, 512)
if isinstance(rep, Grid): if isinstance(rep, Grid):
@ -367,7 +366,7 @@ def _mrefs_to_grefs(refs: dict[str | None, list[Ref]]) -> list[klamath.library.R
xy=rint_cast(xy), xy=rint_cast(xy),
colrow=(numpy.rint(rep.a_count), numpy.rint(rep.b_count)), colrow=(numpy.rint(rep.a_count), numpy.rint(rep.b_count)),
angle_deg=angle_deg, angle_deg=angle_deg,
invert_y=mirror_across_x, invert_y=ref.mirrored,
mag=ref.scale, mag=ref.scale,
properties=properties, properties=properties,
) )
@ -378,7 +377,7 @@ def _mrefs_to_grefs(refs: dict[str | None, list[Ref]]) -> list[klamath.library.R
xy=rint_cast([ref.offset]), xy=rint_cast([ref.offset]),
colrow=None, colrow=None,
angle_deg=angle_deg, angle_deg=angle_deg,
invert_y=mirror_across_x, invert_y=ref.mirrored,
mag=ref.scale, mag=ref.scale,
properties=properties, properties=properties,
) )
@ -390,7 +389,7 @@ def _mrefs_to_grefs(refs: dict[str | None, list[Ref]]) -> list[klamath.library.R
xy=rint_cast([ref.offset + dd]), xy=rint_cast([ref.offset + dd]),
colrow=None, colrow=None,
angle_deg=angle_deg, angle_deg=angle_deg,
invert_y=mirror_across_x, invert_y=ref.mirrored,
mag=ref.scale, mag=ref.scale,
properties=properties, properties=properties,
) )

@ -32,7 +32,7 @@ from .. import Pattern, Ref, PatternError, LibraryError, Label, Shape
from ..library import Library, ILibrary from ..library import Library, ILibrary
from ..shapes import Path, Circle from ..shapes import Path, Circle
from ..repetition import Grid, Arbitrary, Repetition from ..repetition import Grid, Arbitrary, Repetition
from ..utils import layer_t, normalize_mirror, annotations_t from ..utils import layer_t, annotations_t
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -494,7 +494,7 @@ def _placement_to_ref(placement: fatrec.Placement, lib: fatamorgana.OasisLayout)
rotation = numpy.deg2rad(float(placement.angle)) rotation = numpy.deg2rad(float(placement.angle))
ref = Ref( ref = Ref(
offset=xy, offset=xy,
mirrored=(placement.flip, False), mirrored=placement.flip,
rotation=rotation, rotation=rotation,
scale=float(mag), scale=float(mag),
repetition=repetition_fata2masq(placement.repetition), repetition=repetition_fata2masq(placement.repetition),
@ -511,15 +511,14 @@ def _refs_to_placements(
if target is None: if target is None:
continue continue
for ref in rseq: for ref in rseq:
# Note: OASIS mirrors first and rotates second # Note: OASIS also mirrors first and rotates second
mirror_across_x, extra_angle = normalize_mirror(ref.mirrored)
frep, rep_offset = repetition_masq2fata(ref.repetition) frep, rep_offset = repetition_masq2fata(ref.repetition)
offset = rint_cast(ref.offset + rep_offset) offset = rint_cast(ref.offset + rep_offset)
angle = numpy.rad2deg(ref.rotation + extra_angle) % 360 angle = numpy.rad2deg(ref.rotation) % 360
placement = fatrec.Placement( placement = fatrec.Placement(
name=target, name=target,
flip=mirror_across_x, flip=ref.mirrored,
angle=angle, angle=angle,
magnification=ref.scale, magnification=ref.scale,
properties=annotations_to_properties(ref.annotations), properties=annotations_to_properties(ref.annotations),

@ -20,7 +20,7 @@ import numpy
from numpy.typing import ArrayLike from numpy.typing import ArrayLike
from .error import LibraryError, PatternError from .error import LibraryError, PatternError
from .utils import rotation_matrix_2d, normalize_mirror from .utils import rotation_matrix_2d
from .shapes import Shape, Polygon from .shapes import Shape, Polygon
from .label import Label from .label import Label
from .abstract import Abstract from .abstract import Abstract
@ -410,9 +410,7 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
if transform[3]: if transform[3]:
sign[1] = -1 sign[1] = -1
xy = numpy.dot(rotation_matrix_2d(transform[2]), ref.offset * sign) xy = numpy.dot(rotation_matrix_2d(transform[2]), ref.offset * sign)
mirror_x, angle = normalize_mirror(ref.mirrored) ref_transform = transform + (xy[0], xy[1], ref.rotation, ref.mirrored)
angle += ref.rotation
ref_transform = transform + (xy[0], xy[1], angle, mirror_x)
ref_transform[3] %= 2 ref_transform[3] %= 2
else: else:
ref_transform = False ref_transform = False

@ -15,7 +15,7 @@ from numpy.typing import NDArray, ArrayLike
from .ref import Ref from .ref import Ref
from .shapes import Shape, Polygon, Path, DEFAULT_POLY_NUM_VERTICES from .shapes import Shape, Polygon, Path, DEFAULT_POLY_NUM_VERTICES
from .label import Label from .label import Label
from .utils import rotation_matrix_2d, annotations_t, layer_t, normalize_mirror from .utils import rotation_matrix_2d, annotations_t, layer_t
from .error import PatternError from .error import PatternError
from .traits import AnnotatableImpl, Scalable, Mirrorable, Rotatable, Positionable, Repeatable, Bounded from .traits import AnnotatableImpl, Scalable, Mirrorable, Rotatable, Positionable, Repeatable, Bounded
from .ports import Port, PortList from .ports import Port, PortList
@ -370,12 +370,10 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
bounds = None bounds = None
else: else:
ubounds = unrot_bounds.copy() ubounds = unrot_bounds.copy()
mirr_x, rot2 = normalize_mirror(ref.mirrored) if ref.mirrored:
if mirr_x:
ubounds[:, 1] *= -1 ubounds[:, 1] *= -1
# note: rounding fixes up sin/cos inaccuracy, probably unnecessary corners = (rotation_matrix_2d(ref.rotation) @ ubounds.T).T
corners = (numpy.round(rotation_matrix_2d(ref.rotation + rot2)) @ ubounds.T).T
bounds = numpy.vstack((numpy.min(corners, axis=0), bounds = numpy.vstack((numpy.min(corners, axis=0),
numpy.max(corners, axis=0))) * ref.scale + [ref.offset] numpy.max(corners, axis=0))) * ref.scale + [ref.offset]
@ -518,7 +516,7 @@ 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) -> Self: def mirror_element_centers(self, across_axis: int = 0) -> Self:
""" """
Mirror the offsets of all shapes, labels, and refs across an axis Mirror the offsets of all shapes, labels, and refs across an axis
@ -533,7 +531,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
cast(Positionable, entry).offset[across_axis - 1] *= -1 cast(Positionable, entry).offset[across_axis - 1] *= -1
return self return self
def mirror_elements(self, across_axis: int) -> Self: def mirror_elements(self, across_axis: int = 0) -> Self:
""" """
Mirror each shape, ref, and pattern across an axis, relative Mirror each shape, ref, and pattern across an axis, relative
to its offset to its offset
@ -549,7 +547,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
cast(Mirrorable, entry).mirror(across_axis) cast(Mirrorable, entry).mirror(across_axis)
return self return self
def mirror(self, across_axis: int) -> Self: def mirror(self, across_axis: int = 0) -> Self:
""" """
Mirror the Pattern across an axis Mirror the Pattern across an axis

@ -76,7 +76,7 @@ class Port(PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable):
self.ptype = ptype self.ptype = ptype
return self return self
def mirror(self, axis: int) -> Self: def mirror(self, axis: int = 0) -> Self:
self.offset[1 - axis] *= -1 self.offset[1 - axis] *= -1
if self.rotation is not None: if self.rotation is not None:
self.rotation *= -1 self.rotation *= -1
@ -275,7 +275,7 @@ class PortList(metaclass=ABCMeta):
other: 'PortList', other: 'PortList',
map_in: dict[str, str], map_in: dict[str, str],
*, *,
mirrored: tuple[bool, bool] = (False, False), mirrored: bool = False,
set_rotation: bool | None = None, set_rotation: bool | None = None,
) -> tuple[NDArray[numpy.float64], float, NDArray[numpy.float64]]: ) -> tuple[NDArray[numpy.float64], float, NDArray[numpy.float64]]:
""" """
@ -286,7 +286,7 @@ class PortList(metaclass=ABCMeta):
other: a device other: a device
map_in: dict of `{'self_port': 'other_port'}` mappings, specifying map_in: dict of `{'self_port': 'other_port'}` mappings, specifying
port connections between the two devices. port connections between the two devices.
mirrored: Mirrors `other` across the x or y axes prior to mirrored: Mirrors `other` across the x axis prior to
connecting any ports. connecting any ports.
set_rotation: If the necessary rotation cannot be determined from set_rotation: If the necessary rotation cannot be determined from
the ports being connected (i.e. all pairs have at least one the ports being connected (i.e. all pairs have at least one
@ -317,7 +317,7 @@ class PortList(metaclass=ABCMeta):
o_ports: Mapping[str, Port], o_ports: Mapping[str, Port],
map_in: dict[str, str], map_in: dict[str, str],
*, *,
mirrored: tuple[bool, bool] = (False, False), mirrored: bool = False,
set_rotation: bool | None = None, set_rotation: bool | None = None,
) -> tuple[NDArray[numpy.float64], float, NDArray[numpy.float64]]: ) -> tuple[NDArray[numpy.float64], float, NDArray[numpy.float64]]:
""" """
@ -330,7 +330,7 @@ class PortList(metaclass=ABCMeta):
o_ports: A list of ports which are to be moved/mirrored. o_ports: A list of ports which are to be moved/mirrored.
map_in: dict of `{'s_port': 'o_port'}` mappings, specifying map_in: dict of `{'s_port': 'o_port'}` mappings, specifying
port connections. port connections.
mirrored: Mirrors `o_ports` across the x or y axes prior to mirrored: Mirrors `o_ports` across the x axis prior to
connecting any ports. connecting any ports.
set_rotation: If the necessary rotation cannot be determined from set_rotation: If the necessary rotation cannot be determined from
the ports being connected (i.e. all pairs have at least one the ports being connected (i.e. all pairs have at least one
@ -356,13 +356,9 @@ class PortList(metaclass=ABCMeta):
o_has_rot = numpy.array([p.rotation is not None for p in o_ports.values()], dtype=bool) o_has_rot = numpy.array([p.rotation is not None for p in o_ports.values()], dtype=bool)
has_rot = s_has_rot & o_has_rot has_rot = s_has_rot & o_has_rot
if mirrored[0]: if mirrored:
o_offsets[:, 1] *= -1 o_offsets[:, 1] *= -1
o_rotations *= -1 o_rotations *= -1
if mirrored[1]:
o_offsets[:, 0] *= -1
o_rotations *= -1
o_rotations += pi
type_conflicts = numpy.array([st != ot and st != 'unk' and ot != 'unk' type_conflicts = numpy.array([st != ot and st != 'unk' and ot != 'unk'
for st, ot in zip(s_types, o_types)]) for st, ot in zip(s_types, o_types)])

@ -4,15 +4,14 @@
""" """
#TODO more top-level documentation #TODO more top-level documentation
from typing import Sequence, Mapping, TYPE_CHECKING, Any, Self from typing import Mapping, TYPE_CHECKING, Self
import copy import copy
import numpy import numpy
from numpy import pi from numpy import pi
from numpy.typing import NDArray, ArrayLike from numpy.typing import NDArray, ArrayLike
from .error import PatternError from .utils import annotations_t
from .utils import is_scalar, annotations_t
from .repetition import Repetition from .repetition import Repetition
from .traits import ( from .traits import (
PositionableImpl, RotatableImpl, ScalableImpl, PositionableImpl, RotatableImpl, ScalableImpl,
@ -31,6 +30,8 @@ class Ref(
""" """
`Ref` provides basic support for nesting Pattern objects within each other, by adding `Ref` provides basic support for nesting Pattern objects within each other, by adding
offset, rotation, scaling, and associated methods. offset, rotation, scaling, and associated methods.
Note: Order is (mirror, rotate, scale, translate, repeat)
""" """
__slots__ = ( __slots__ = (
'_mirrored', '_mirrored',
@ -38,32 +39,41 @@ class Ref(
'_offset', '_rotation', 'scale', '_repetition', '_annotations', '_offset', '_rotation', 'scale', '_repetition', '_annotations',
) )
_mirrored: NDArray[numpy.bool_] _mirrored: bool
""" Whether to mirror the instance across the x and/or y axes. """ """ Whether to mirror the instance across the x axis (new_y = -old_y)ubefore rotating. """
# Mirrored property
@property
def mirrored(self) -> bool: # mypy#3004, setter should be SupportsBool
return self._mirrored
@mirrored.setter
def mirrored(self, val: bool) -> None:
self._mirrored = bool(val)
def __init__( def __init__(
self, self,
*, *,
offset: ArrayLike = (0.0, 0.0), offset: ArrayLike = (0.0, 0.0),
rotation: float = 0.0, rotation: float = 0.0,
mirrored: Sequence[bool] | None = None, mirrored: bool = False,
scale: float = 1.0, scale: float = 1.0,
repetition: Repetition | None = None, repetition: Repetition | None = None,
annotations: annotations_t | None = None, annotations: annotations_t | None = None,
) -> None: ) -> None:
""" """
Note: Order is (mirror, rotate, scale, translate, repeat)
Args: Args:
offset: (x, y) offset applied to the referenced pattern. Not affected by rotation etc. offset: (x, y) offset applied to the referenced pattern. Not affected by rotation etc.
rotation: Rotation (radians, counterclockwise) relative to the referenced pattern's (0, 0). rotation: Rotation (radians, counterclockwise) relative to the referenced pattern's (0, 0).
mirrored: Whether to mirror the referenced pattern across its x and y axes. mirrored: Whether to mirror the referenced pattern across its x axis before rotating.
scale: Scaling factor applied to the pattern's geometry. scale: Scaling factor applied to the pattern's geometry.
repetition: `Repetition` object, default `None` repetition: `Repetition` object, default `None`
""" """
self.offset = offset self.offset = offset
self.rotation = rotation self.rotation = rotation
self.scale = scale self.scale = scale
if mirrored is None:
mirrored = (False, False)
self.mirrored = mirrored self.mirrored = mirrored
self.repetition = repetition self.repetition = repetition
self.annotations = annotations if annotations is not None else {} self.annotations = annotations if annotations is not None else {}
@ -73,7 +83,7 @@ class Ref(
offset=self.offset.copy(), offset=self.offset.copy(),
rotation=self.rotation, rotation=self.rotation,
scale=self.scale, scale=self.scale,
mirrored=self.mirrored.copy(), mirrored=self.mirrored,
repetition=copy.deepcopy(self.repetition), repetition=copy.deepcopy(self.repetition),
annotations=copy.deepcopy(self.annotations), annotations=copy.deepcopy(self.annotations),
) )
@ -86,17 +96,6 @@ class Ref(
new.annotations = copy.deepcopy(self.annotations, memo) new.annotations = copy.deepcopy(self.annotations, memo)
return new return new
# Mirrored property
@property
def mirrored(self) -> Any: # TODO mypy#3004 NDArray[numpy.bool_]:
return self._mirrored
@mirrored.setter
def mirrored(self, val: ArrayLike) -> None:
if is_scalar(val):
raise PatternError('Mirrored must be a 2-element list of booleans')
self._mirrored = numpy.array(val, dtype=bool, copy=True)
def as_pattern( def as_pattern(
self, self,
pattern: 'Pattern', pattern: 'Pattern',
@ -113,8 +112,8 @@ class Ref(
if self.scale != 1: if self.scale != 1:
pattern.scale_by(self.scale) pattern.scale_by(self.scale)
if numpy.any(self.mirrored): if self.mirrored:
pattern.mirror2d(self.mirrored) pattern.mirror()
if self.rotation % (2 * pi) != 0: if self.rotation % (2 * pi) != 0:
pattern.rotate_around((0.0, 0.0), self.rotation) pattern.rotate_around((0.0, 0.0), self.rotation)
if numpy.any(self.offset): if numpy.any(self.offset):
@ -137,13 +136,24 @@ class Ref(
self.repetition.rotate(rotation) self.repetition.rotate(rotation)
return self return self
def mirror(self, axis: int) -> Self: def mirror(self, axis: int = 0) -> Self:
self.mirrored[axis] = not self.mirrored[axis] self.mirror_target(axis)
self.rotation *= -1 self.rotation *= -1
if self.repetition is not None: if self.repetition is not None:
self.repetition.mirror(axis) self.repetition.mirror(axis)
return self return self
def mirror_target(self, axis: int = 0) -> Self:
self.mirrored = not self.mirrored
self.rotation += axis * pi
return self
def mirror2d_target(self, across_x: bool = False, across_y: bool = False) -> Self:
self.mirrored = bool((self.mirrored + across_x + across_y) % 2)
if across_y:
self.rotation += pi
return self
def get_bounds_single( def get_bounds_single(
self, self,
pattern: 'Pattern', pattern: 'Pattern',
@ -169,5 +179,5 @@ class Ref(
def __repr__(self) -> str: def __repr__(self) -> str:
rotation = f' r{numpy.rad2deg(self.rotation):g}' if self.rotation != 0 else '' rotation = f' r{numpy.rad2deg(self.rotation):g}' if self.rotation != 0 else ''
scale = f' d{self.scale:g}' if self.scale != 1 else '' scale = f' d{self.scale:g}' if self.scale != 1 else ''
mirrored = ' m{:d}{:d}'.format(*self.mirrored) if self.mirrored.any() else '' mirrored = ' m' if self.mirrored else ''
return f'<Ref {self.offset}{rotation}{scale}{mirrored}>' return f'<Ref {self.offset}{rotation}{scale}{mirrored}>'

@ -2,8 +2,7 @@
Repetitions provide support for efficiently representing multiple identical Repetitions provide support for efficiently representing multiple identical
instances of an object . instances of an object .
""" """
from typing import Any, Type, Self, TypeVar
from typing import Any, Type
import copy import copy
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod
@ -15,6 +14,9 @@ from .error import PatternError
from .utils import rotation_matrix_2d from .utils import rotation_matrix_2d
GG = TypeVar('GG', bound='Grid')
class Repetition(Copyable, Rotatable, Mirrorable, Scalable, Bounded, metaclass=ABCMeta): class Repetition(Copyable, Rotatable, Mirrorable, Scalable, Bounded, metaclass=ABCMeta):
""" """
Interface common to all objects which specify repetitions Interface common to all objects which specify repetitions
@ -104,12 +106,12 @@ class Grid(Repetition):
@classmethod @classmethod
def aligned( def aligned(
cls: Type, cls: Type[GG],
x: float, x: float,
y: float, y: float,
x_count: int, x_count: int,
y_count: int, y_count: int,
) -> 'Grid': ) -> GG:
""" """
Simple constructor for an axis-aligned 2D grid Simple constructor for an axis-aligned 2D grid
@ -133,7 +135,7 @@ class Grid(Repetition):
) )
return new return new
def __deepcopy__(self, memo: dict | None = None) -> 'Grid': def __deepcopy__(self, memo: dict | None = None) -> Self:
memo = {} if memo is None else memo memo = {} if memo is None else memo
new = copy.copy(self) new = copy.copy(self)
return new return new
@ -197,7 +199,7 @@ class Grid(Repetition):
return (aa.flatten()[:, None] * self.a_vector[None, :] return (aa.flatten()[:, None] * self.a_vector[None, :]
+ bb.flatten()[:, None] * self.b_vector[None, :]) # noqa + bb.flatten()[:, None] * self.b_vector[None, :]) # noqa
def rotate(self, rotation: float) -> 'Grid': def rotate(self, rotation: float) -> Self:
""" """
Rotate lattice vectors (around (0, 0)) Rotate lattice vectors (around (0, 0))
@ -212,7 +214,7 @@ class Grid(Repetition):
self.b_vector = numpy.dot(rotation_matrix_2d(rotation), self.b_vector) self.b_vector = numpy.dot(rotation_matrix_2d(rotation), self.b_vector)
return self return self
def mirror(self, axis: int) -> 'Grid': def mirror(self, axis: int = 0) -> Self:
""" """
Mirror the Grid across an axis. Mirror the Grid across an axis.
@ -248,7 +250,7 @@ class Grid(Repetition):
xy_max = numpy.max(corners, axis=0) xy_max = numpy.max(corners, axis=0)
return numpy.array((xy_min, xy_max)) return numpy.array((xy_min, xy_max))
def scale_by(self, c: float) -> 'Grid': def scale_by(self, c: float) -> Self:
""" """
Scale the Grid by a factor Scale the Grid by a factor
@ -327,7 +329,7 @@ class Arbitrary(Repetition):
return False return False
return numpy.array_equal(self.displacements, other.displacements) return numpy.array_equal(self.displacements, other.displacements)
def rotate(self, rotation: float) -> 'Arbitrary': def rotate(self, rotation: float) -> Self:
""" """
Rotate dispacements (around (0, 0)) Rotate dispacements (around (0, 0))
@ -340,7 +342,7 @@ class Arbitrary(Repetition):
self.displacements = numpy.dot(rotation_matrix_2d(rotation), self.displacements.T).T self.displacements = numpy.dot(rotation_matrix_2d(rotation), self.displacements.T).T
return self return self
def mirror(self, axis: int) -> 'Arbitrary': def mirror(self, axis: int = 0) -> Self:
""" """
Mirror the displacements across an axis. Mirror the displacements across an axis.
@ -366,7 +368,7 @@ class Arbitrary(Repetition):
xy_max = numpy.max(self.displacements, axis=0) xy_max = numpy.max(self.displacements, axis=0)
return numpy.array((xy_min, xy_max)) return numpy.array((xy_min, xy_max))
def scale_by(self, c: float) -> 'Arbitrary': def scale_by(self, c: float) -> Self:
""" """
Scale the displacements by a factor Scale the displacements by a factor

@ -1,4 +1,4 @@
from typing import Sequence, Any from typing import Any
import copy import copy
import math import math
@ -155,7 +155,6 @@ class Arc(Shape):
*, *,
offset: ArrayLike = (0.0, 0.0), offset: ArrayLike = (0.0, 0.0),
rotation: float = 0, rotation: float = 0,
mirrored: Sequence[bool] = (False, False),
repetition: Repetition | None = None, repetition: Repetition | None = None,
annotations: annotations_t | None = None, annotations: annotations_t | None = None,
raw: bool = False, raw: bool = False,
@ -179,7 +178,6 @@ class Arc(Shape):
self.rotation = rotation self.rotation = rotation
self.repetition = repetition self.repetition = repetition
self.annotations = annotations if annotations is not None else {} self.annotations = annotations if annotations is not None else {}
[self.mirror(a) for a, do in enumerate(mirrored) if do]
def __deepcopy__(self, memo: dict | None = None) -> 'Arc': def __deepcopy__(self, memo: dict | None = None) -> 'Arc':
memo = {} if memo is None else memo memo = {} if memo is None else memo
@ -315,7 +313,7 @@ class Arc(Shape):
self.rotation += theta self.rotation += theta
return self return self
def mirror(self, axis: int) -> 'Arc': def mirror(self, axis: int = 0) -> 'Arc':
self.offset[axis - 1] *= -1 self.offset[axis - 1] *= -1
self.rotation *= -1 self.rotation *= -1
self.rotation += axis * pi self.rotation += axis * pi

@ -96,7 +96,7 @@ class Circle(Shape):
def rotate(self, theta: float) -> 'Circle': def rotate(self, theta: float) -> 'Circle':
return self return self
def mirror(self, axis: int) -> 'Circle': def mirror(self, axis: int = 0) -> 'Circle':
self.offset *= -1 self.offset *= -1
return self return self

@ -1,4 +1,4 @@
from typing import Sequence, Any from typing import Any, Self
import copy import copy
import math import math
@ -90,7 +90,6 @@ class Ellipse(Shape):
*, *,
offset: ArrayLike = (0.0, 0.0), offset: ArrayLike = (0.0, 0.0),
rotation: float = 0, rotation: float = 0,
mirrored: Sequence[bool] = (False, False),
repetition: Repetition | None = None, repetition: Repetition | None = None,
annotations: annotations_t | None = None, annotations: annotations_t | None = None,
raw: bool = False, raw: bool = False,
@ -109,9 +108,8 @@ class Ellipse(Shape):
self.rotation = rotation self.rotation = rotation
self.repetition = repetition self.repetition = repetition
self.annotations = annotations if annotations is not None else {} self.annotations = annotations if annotations is not None else {}
[self.mirror(a) for a, do in enumerate(mirrored) if do]
def __deepcopy__(self, memo: dict | None = None) -> 'Ellipse': def __deepcopy__(self, memo: dict | None = None) -> Self:
memo = {} if memo is None else memo memo = {} if memo is None else memo
new = copy.copy(self) new = copy.copy(self)
new._offset = self._offset.copy() new._offset = self._offset.copy()
@ -157,17 +155,17 @@ class Ellipse(Shape):
return numpy.vstack((self.offset - rot_radii[0], return numpy.vstack((self.offset - rot_radii[0],
self.offset + rot_radii[1])) self.offset + rot_radii[1]))
def rotate(self, theta: float) -> 'Ellipse': def rotate(self, theta: float) -> Self:
self.rotation += theta self.rotation += theta
return self return self
def mirror(self, axis: int) -> 'Ellipse': def mirror(self, axis: int = 0) -> Self:
self.offset[axis - 1] *= -1 self.offset[axis - 1] *= -1
self.rotation *= -1 self.rotation *= -1
self.rotation += axis * pi self.rotation += axis * pi
return self return self
def scale_by(self, c: float) -> 'Ellipse': def scale_by(self, c: float) -> Self:
self.radii *= c self.radii *= c
return self return self

@ -153,7 +153,6 @@ class Path(Shape):
cap_extensions: ArrayLike | None = None, cap_extensions: ArrayLike | None = None,
offset: ArrayLike = (0.0, 0.0), offset: ArrayLike = (0.0, 0.0),
rotation: float = 0, rotation: float = 0,
mirrored: Sequence[bool] = (False, False),
repetition: Repetition | None = None, repetition: Repetition | None = None,
annotations: annotations_t | None = None, annotations: annotations_t | None = None,
raw: bool = False, raw: bool = False,
@ -180,7 +179,6 @@ class Path(Shape):
self.cap = cap self.cap = cap
self.cap_extensions = cap_extensions self.cap_extensions = cap_extensions
self.rotate(rotation) self.rotate(rotation)
[self.mirror(a) for a, do in enumerate(mirrored) if do]
def __deepcopy__(self, memo: dict | None = None) -> 'Path': def __deepcopy__(self, memo: dict | None = None) -> 'Path':
memo = {} if memo is None else memo memo = {} if memo is None else memo
@ -200,7 +198,6 @@ class Path(Shape):
cap_extensions: tuple[float, float] | None = None, cap_extensions: tuple[float, float] | None = None,
offset: ArrayLike = (0.0, 0.0), offset: ArrayLike = (0.0, 0.0),
rotation: float = 0, rotation: float = 0,
mirrored: Sequence[bool] = (False, False),
) -> 'Path': ) -> 'Path':
""" """
Build a path by specifying the turn angles and travel distances Build a path by specifying the turn angles and travel distances
@ -217,9 +214,6 @@ class Path(Shape):
Default `(0, 0)` or `None`, depending on cap type Default `(0, 0)` or `None`, depending on cap type
offset: Offset, default `(0, 0)` offset: Offset, default `(0, 0)`
rotation: Rotation counterclockwise, in radians. Default `0` rotation: Rotation counterclockwise, in radians. Default `0`
mirrored: Whether to mirror across the x or y axes. For example,
`mirrored=(True, False)` results in a reflection across the x-axis,
multiplying the path's y-coordinates by -1. Default `(False, False)`
Returns: Returns:
The resulting Path object The resulting Path object
@ -233,7 +227,7 @@ class Path(Shape):
verts.append(verts[-1] + direction * distance) verts.append(verts[-1] + direction * distance)
return Path(vertices=verts, width=width, cap=cap, cap_extensions=cap_extensions, return Path(vertices=verts, width=width, cap=cap, cap_extensions=cap_extensions,
offset=offset, rotation=rotation, mirrored=mirrored) offset=offset, rotation=rotation)
def to_polygons( def to_polygons(
self, self,
@ -334,7 +328,7 @@ class Path(Shape):
self.vertices = numpy.dot(rotation_matrix_2d(theta), self.vertices.T).T self.vertices = numpy.dot(rotation_matrix_2d(theta), self.vertices.T).T
return self return self
def mirror(self, axis: int) -> 'Path': def mirror(self, axis: int = 0) -> 'Path':
self.vertices[:, axis - 1] *= -1 self.vertices[:, axis - 1] *= -1
return self return self

@ -81,7 +81,6 @@ class Polygon(Shape):
*, *,
offset: ArrayLike = (0.0, 0.0), offset: ArrayLike = (0.0, 0.0),
rotation: float = 0.0, rotation: float = 0.0,
mirrored: Sequence[bool] = (False, False),
repetition: Repetition | None = None, repetition: Repetition | None = None,
annotations: annotations_t | None = None, annotations: annotations_t | None = None,
raw: bool = False, raw: bool = False,
@ -99,7 +98,6 @@ class Polygon(Shape):
self.repetition = repetition self.repetition = repetition
self.annotations = annotations if annotations is not None else {} self.annotations = annotations if annotations is not None else {}
self.rotate(rotation) self.rotate(rotation)
[self.mirror(a) for a, do in enumerate(mirrored) if do]
def __deepcopy__(self, memo: dict | None = None) -> 'Polygon': def __deepcopy__(self, memo: dict | None = None) -> 'Polygon':
memo = {} if memo is None else memo memo = {} if memo is None else memo
@ -336,7 +334,7 @@ class Polygon(Shape):
self.vertices = numpy.dot(rotation_matrix_2d(theta), self.vertices.T).T self.vertices = numpy.dot(rotation_matrix_2d(theta), self.vertices.T).T
return self return self
def mirror(self, axis: int) -> 'Polygon': def mirror(self, axis: int = 0) -> 'Polygon':
self.vertices[:, axis - 1] *= -1 self.vertices[:, axis - 1] *= -1
return self return self

@ -1,4 +1,4 @@
from typing import Sequence, Any from typing import Self
import copy import copy
import numpy import numpy
@ -9,8 +9,7 @@ from . import Shape, Polygon, normalized_shape_tuple
from ..error import PatternError from ..error import PatternError
from ..repetition import Repetition from ..repetition import Repetition
from ..traits import RotatableImpl from ..traits import RotatableImpl
from ..utils import is_scalar, get_bit, normalize_mirror from ..utils import is_scalar, get_bit, annotations_t
from ..utils import annotations_t
# Loaded on use: # Loaded on use:
# from freetype import Face # from freetype import Face
@ -30,7 +29,7 @@ class Text(RotatableImpl, Shape):
_string: str _string: str
_height: float _height: float
_mirrored: NDArray[numpy.bool_] _mirrored: bool
font_path: str font_path: str
# vertices property # vertices property
@ -53,16 +52,13 @@ class Text(RotatableImpl, Shape):
raise PatternError('Height must be a scalar') raise PatternError('Height must be a scalar')
self._height = val self._height = val
# Mirrored property
@property @property
def mirrored(self) -> Any: # TODO mypy#3004 NDArray[numpy.bool_]: def mirrored(self) -> bool: # mypy#3004, should be bool
return self._mirrored return self._mirrored
@mirrored.setter @mirrored.setter
def mirrored(self, val: Sequence[bool]) -> None: def mirrored(self, val: bool) -> None:
if is_scalar(val): self._mirrored = bool(val)
raise PatternError('Mirrored must be a 2-element list of booleans')
self._mirrored = numpy.array(val, dtype=bool, copy=True)
def __init__( def __init__(
self, self,
@ -72,19 +68,16 @@ class Text(RotatableImpl, Shape):
*, *,
offset: ArrayLike = (0.0, 0.0), offset: ArrayLike = (0.0, 0.0),
rotation: float = 0.0, rotation: float = 0.0,
mirrored: ArrayLike = (False, False),
repetition: Repetition | None = None, repetition: Repetition | None = None,
annotations: annotations_t | None = None, annotations: annotations_t | None = None,
raw: bool = False, raw: bool = False,
) -> None: ) -> None:
if raw: if raw:
assert isinstance(offset, numpy.ndarray) assert isinstance(offset, numpy.ndarray)
assert isinstance(mirrored, numpy.ndarray)
self._offset = offset self._offset = offset
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 if annotations is not None else {} self._annotations = annotations if annotations is not None else {}
else: else:
@ -92,16 +85,14 @@ class Text(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 if annotations is not None else {} self.annotations = annotations if annotations is not None else {}
self.font_path = font_path self.font_path = font_path
def __deepcopy__(self, memo: dict | None = None) -> 'Text': def __deepcopy__(self, memo: dict | None = None) -> Self:
memo = {} if memo is None else memo memo = {} if memo is None else memo
new = copy.copy(self) new = copy.copy(self)
new._offset = self._offset.copy() new._offset = self._offset.copy()
new._mirrored = copy.deepcopy(self._mirrored, memo)
new._annotations = copy.deepcopy(self._annotations) new._annotations = copy.deepcopy(self._annotations)
return new return new
@ -118,7 +109,8 @@ class Text(RotatableImpl, Shape):
# Move these polygons to the right of the previous letter # Move these polygons to the right of the previous letter
for xys in raw_polys: for xys in raw_polys:
poly = Polygon(xys) poly = Polygon(xys)
poly.mirror2d(self.mirrored) if self.mirrored:
poly.mirror()
poly.scale_by(self.height) poly.scale_by(self.height)
poly.offset = self.offset + [total_advance, 0] poly.offset = self.offset + [total_advance, 0]
poly.rotate_around(self.offset, self.rotation) poly.rotate_around(self.offset, self.rotation)
@ -129,27 +121,27 @@ class Text(RotatableImpl, Shape):
return all_polygons return all_polygons
def mirror(self, axis: int) -> 'Text': def mirror(self, axis: int = 0) -> Self:
self.mirrored[axis] = not self.mirrored[axis] self.mirrored = not self.mirrored
if axis == 1:
self.rotation += pi
return self return self
def scale_by(self, c: float) -> 'Text': def scale_by(self, c: float) -> Self:
self.height *= c self.height *= c
return self return self
def normalized_form(self, norm_value: float) -> normalized_shape_tuple: def normalized_form(self, norm_value: float) -> normalized_shape_tuple:
mirror_x, rotation = normalize_mirror(self.mirrored) rotation = self.rotation % (2 * pi)
rotation += self.rotation
rotation %= 2 * pi
return ((type(self), self.string, self.font_path), return ((type(self), self.string, self.font_path),
(self.offset, self.height / norm_value, rotation, mirror_x), (self.offset, self.height / norm_value, rotation, bool(self.mirrored)),
lambda: Text( lambda: Text(
string=self.string, string=self.string,
height=self.height * norm_value, height=self.height * norm_value,
font_path=self.font_path, font_path=self.font_path,
rotation=rotation, rotation=rotation,
mirrored=(mirror_x, False), ).mirror2d(across_x=self.mirrored),
)) )
def get_bounds_single(self) -> NDArray[numpy.float64]: def get_bounds_single(self) -> NDArray[numpy.float64]:
# rotation makes this a huge pain when using slot.advance and glyph.bbox(), so # rotation makes this a huge pain when using slot.advance and glyph.bbox(), so
@ -258,5 +250,5 @@ def get_char_as_polygons(
def __repr__(self) -> str: def __repr__(self) -> str:
rotation = f'{numpy.rad2deg(self.rotation):g}' if self.rotation != 0 else '' rotation = f'{numpy.rad2deg(self.rotation):g}' if self.rotation != 0 else ''
mirrored = ' m{:d}{:d}'.format(*self.mirrored) if self.mirrored.any() else '' mirrored = ' m{:d}' if self.mirrored else ''
return f'<TextShape "{self.string}" o{self.offset} h{self.height:g}{rotation}{mirrored}>' return f'<TextShape "{self.string}" o{self.offset} h{self.height:g}{rotation}{mirrored}>'

@ -9,7 +9,7 @@ class Mirrorable(metaclass=ABCMeta):
__slots__ = () __slots__ = ()
@abstractmethod @abstractmethod
def mirror(self, axis: int) -> Self: def mirror(self, axis: int = 0) -> Self:
""" """
Mirror the entity across an axis. Mirror the entity across an axis.
@ -21,7 +21,7 @@ class Mirrorable(metaclass=ABCMeta):
""" """
pass pass
def mirror2d(self, axes: tuple[bool, bool]) -> 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
@ -31,9 +31,9 @@ class Mirrorable(metaclass=ABCMeta):
Returns: Returns:
self self
""" """
if axes[0]: if across_x:
self.mirror(0) self.mirror(0)
if axes[1]: if across_y:
self.mirror(1) self.mirror(1)
return self return self

Loading…
Cancel
Save