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

This commit is contained in:
jan 2023-04-14 22:19:56 -07:00
parent 9bc8d29b85
commit 91465b7175
20 changed files with 190 additions and 213 deletions

View File

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

View File

@ -7,7 +7,7 @@ from numpy.typing import ArrayLike
from .ref import Ref
from .ports import PortList, Port
from .utils import rotation_matrix_2d, normalize_mirror
from .utils import rotation_matrix_2d
#if TYPE_CHECKING:
# from .builder import Builder, Tool
@ -143,7 +143,7 @@ class Abstract(PortList):
port.rotate(rotation)
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
@ -158,7 +158,7 @@ class Abstract(PortList):
port.offset[across_axis - 1] *= -1
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
offset
@ -174,7 +174,7 @@ class Abstract(PortList):
port.mirror(across_axis)
return self
def mirror(self, across_axis: int) -> Self:
def mirror(self, across_axis: int = 0) -> Self:
"""
Mirror the Pattern across an axis
@ -200,11 +200,10 @@ class Abstract(PortList):
Returns:
self
"""
mirrored_across_x, angle = normalize_mirror(ref.mirrored)
if mirrored_across_x:
self.mirror(across_axis=0)
self.rotate_ports(angle + ref.rotation)
self.rotate_port_offsets(angle + ref.rotation)
if ref.mirrored:
self.mirror()
self.rotate_ports(ref.rotation)
self.rotate_port_offsets(ref.rotation)
self.translate_ports(ref.offset)
return self
@ -221,10 +220,9 @@ class Abstract(PortList):
# TODO test undo_ref_transform
"""
mirrored_across_x, angle = normalize_mirror(ref.mirrored)
self.translate_ports(-ref.offset)
self.rotate_port_offsets(-angle - ref.rotation)
self.rotate_ports(-angle - ref.rotation)
if mirrored_across_x:
self.mirror(across_axis=0)
self.rotate_port_offsets(-ref.rotation)
self.rotate_ports(-ref.rotation)
if ref.mirrored:
self.mirror(0)
return self

View File

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

View File

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

View File

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

View File

@ -21,7 +21,7 @@ from .. import Pattern, Ref, PatternError, Label
from ..library import ILibraryView, LibraryView, Library
from ..shapes import Shape, Polygon, Path
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__)
@ -258,8 +258,8 @@ def _read_block(block) -> tuple[str, Pattern]:
if abs(xscale) != abs(yscale):
logger.warning('Masque does not support per-axis scaling; using x-scaling only!')
scale = abs(xscale)
mirrored = (yscale < 0, xscale < 0)
rotation = numpy.deg2rad(attr.get('rotation', 0))
mirrored, extra_angle = normalize_mirror((yscale < 0, xscale < 0))
rotation = numpy.deg2rad(attr.get('rotation', 0)) + extra_angle
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:
rotation = numpy.rad2deg(ref.rotation) % 360
attribs = dict(
xscale=ref.scale * (-1 if ref.mirrored[1] else 1),
yscale=ref.scale * (-1 if ref.mirrored[0] else 1),
xscale=ref.scale,
yscale=ref.scale * (-1 if ref.mirrored else 1),
rotation=rotation,
)

View File

@ -37,7 +37,7 @@ from .utils import is_gzipped, tmpfile
from .. import Pattern, Ref, PatternError, LibraryError, Label, Shape
from ..shapes import Polygon, Path
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
@ -306,7 +306,7 @@ def _gref_to_mref(ref: klamath.library.Reference) -> tuple[str, Ref]:
offset=offset,
rotation=numpy.deg2rad(ref.angle_deg),
scale=ref.mag,
mirrored=(ref.invert_y, False),
mirrored=ref.invert_y,
annotations=_properties_to_annotations(ref.properties),
repetition=repetition,
)
@ -348,10 +348,9 @@ def _mrefs_to_grefs(refs: dict[str | None, list[Ref]]) -> list[klamath.library.R
continue
encoded_name = target.encode('ASCII')
for ref in rseq:
# Note: GDS mirrors first and rotates second
mirror_across_x, extra_angle = normalize_mirror(ref.mirrored)
# Note: GDS also mirrors first and rotates second
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)
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),
colrow=(numpy.rint(rep.a_count), numpy.rint(rep.b_count)),
angle_deg=angle_deg,
invert_y=mirror_across_x,
invert_y=ref.mirrored,
mag=ref.scale,
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]),
colrow=None,
angle_deg=angle_deg,
invert_y=mirror_across_x,
invert_y=ref.mirrored,
mag=ref.scale,
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]),
colrow=None,
angle_deg=angle_deg,
invert_y=mirror_across_x,
invert_y=ref.mirrored,
mag=ref.scale,
properties=properties,
)

View File

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

View File

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

View File

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

View File

@ -76,7 +76,7 @@ class Port(PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable):
self.ptype = ptype
return self
def mirror(self, axis: int) -> Self:
def mirror(self, axis: int = 0) -> Self:
self.offset[1 - axis] *= -1
if self.rotation is not None:
self.rotation *= -1
@ -275,7 +275,7 @@ class PortList(metaclass=ABCMeta):
other: 'PortList',
map_in: dict[str, str],
*,
mirrored: tuple[bool, bool] = (False, False),
mirrored: bool = False,
set_rotation: bool | None = None,
) -> tuple[NDArray[numpy.float64], float, NDArray[numpy.float64]]:
"""
@ -286,7 +286,7 @@ class PortList(metaclass=ABCMeta):
other: a device
map_in: dict of `{'self_port': 'other_port'}` mappings, specifying
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.
set_rotation: If the necessary rotation cannot be determined from
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],
map_in: dict[str, str],
*,
mirrored: tuple[bool, bool] = (False, False),
mirrored: bool = False,
set_rotation: bool | None = None,
) -> 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.
map_in: dict of `{'s_port': 'o_port'}` mappings, specifying
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.
set_rotation: If the necessary rotation cannot be determined from
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)
has_rot = s_has_rot & o_has_rot
if mirrored[0]:
if mirrored:
o_offsets[:, 1] *= -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'
for st, ot in zip(s_types, o_types)])

View File

@ -4,15 +4,14 @@
"""
#TODO more top-level documentation
from typing import Sequence, Mapping, TYPE_CHECKING, Any, Self
from typing import Mapping, TYPE_CHECKING, Self
import copy
import numpy
from numpy import pi
from numpy.typing import NDArray, ArrayLike
from .error import PatternError
from .utils import is_scalar, annotations_t
from .utils import annotations_t
from .repetition import Repetition
from .traits import (
PositionableImpl, RotatableImpl, ScalableImpl,
@ -31,6 +30,8 @@ class Ref(
"""
`Ref` provides basic support for nesting Pattern objects within each other, by adding
offset, rotation, scaling, and associated methods.
Note: Order is (mirror, rotate, scale, translate, repeat)
"""
__slots__ = (
'_mirrored',
@ -38,32 +39,41 @@ class Ref(
'_offset', '_rotation', 'scale', '_repetition', '_annotations',
)
_mirrored: NDArray[numpy.bool_]
""" Whether to mirror the instance across the x and/or y axes. """
_mirrored: bool
""" 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__(
self,
*,
offset: ArrayLike = (0.0, 0.0),
rotation: float = 0.0,
mirrored: Sequence[bool] | None = None,
mirrored: bool = False,
scale: float = 1.0,
repetition: Repetition | None = None,
annotations: annotations_t | None = None,
) -> None:
"""
Note: Order is (mirror, rotate, scale, translate, repeat)
Args:
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).
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.
repetition: `Repetition` object, default `None`
"""
self.offset = offset
self.rotation = rotation
self.scale = scale
if mirrored is None:
mirrored = (False, False)
self.mirrored = mirrored
self.repetition = repetition
self.annotations = annotations if annotations is not None else {}
@ -73,7 +83,7 @@ class Ref(
offset=self.offset.copy(),
rotation=self.rotation,
scale=self.scale,
mirrored=self.mirrored.copy(),
mirrored=self.mirrored,
repetition=copy.deepcopy(self.repetition),
annotations=copy.deepcopy(self.annotations),
)
@ -86,17 +96,6 @@ class Ref(
new.annotations = copy.deepcopy(self.annotations, memo)
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(
self,
pattern: 'Pattern',
@ -113,8 +112,8 @@ class Ref(
if self.scale != 1:
pattern.scale_by(self.scale)
if numpy.any(self.mirrored):
pattern.mirror2d(self.mirrored)
if self.mirrored:
pattern.mirror()
if self.rotation % (2 * pi) != 0:
pattern.rotate_around((0.0, 0.0), self.rotation)
if numpy.any(self.offset):
@ -137,13 +136,24 @@ class Ref(
self.repetition.rotate(rotation)
return self
def mirror(self, axis: int) -> Self:
self.mirrored[axis] = not self.mirrored[axis]
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:
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(
self,
pattern: 'Pattern',
@ -169,5 +179,5 @@ class Ref(
def __repr__(self) -> str:
rotation = f' r{numpy.rad2deg(self.rotation):g}' if self.rotation != 0 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}>'

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
from typing import Sequence, Any
from typing import Any, Self
import copy
import math
@ -90,7 +90,6 @@ class Ellipse(Shape):
*,
offset: ArrayLike = (0.0, 0.0),
rotation: float = 0,
mirrored: Sequence[bool] = (False, False),
repetition: Repetition | None = None,
annotations: annotations_t | None = None,
raw: bool = False,
@ -109,9 +108,8 @@ class Ellipse(Shape):
self.rotation = rotation
self.repetition = repetition
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
new = copy.copy(self)
new._offset = self._offset.copy()
@ -157,17 +155,17 @@ class Ellipse(Shape):
return numpy.vstack((self.offset - rot_radii[0],
self.offset + rot_radii[1]))
def rotate(self, theta: float) -> 'Ellipse':
def rotate(self, theta: float) -> Self:
self.rotation += theta
return self
def mirror(self, axis: int) -> 'Ellipse':
def mirror(self, axis: int = 0) -> Self:
self.offset[axis - 1] *= -1
self.rotation *= -1
self.rotation += axis * pi
return self
def scale_by(self, c: float) -> 'Ellipse':
def scale_by(self, c: float) -> Self:
self.radii *= c
return self

View File

@ -153,7 +153,6 @@ class Path(Shape):
cap_extensions: ArrayLike | None = None,
offset: ArrayLike = (0.0, 0.0),
rotation: float = 0,
mirrored: Sequence[bool] = (False, False),
repetition: Repetition | None = None,
annotations: annotations_t | None = None,
raw: bool = False,
@ -180,7 +179,6 @@ class Path(Shape):
self.cap = cap
self.cap_extensions = cap_extensions
self.rotate(rotation)
[self.mirror(a) for a, do in enumerate(mirrored) if do]
def __deepcopy__(self, memo: dict | None = None) -> 'Path':
memo = {} if memo is None else memo
@ -200,7 +198,6 @@ class Path(Shape):
cap_extensions: tuple[float, float] | None = None,
offset: ArrayLike = (0.0, 0.0),
rotation: float = 0,
mirrored: Sequence[bool] = (False, False),
) -> 'Path':
"""
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
offset: Offset, default `(0, 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:
The resulting Path object
@ -233,7 +227,7 @@ class Path(Shape):
verts.append(verts[-1] + direction * distance)
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(
self,
@ -334,7 +328,7 @@ class Path(Shape):
self.vertices = numpy.dot(rotation_matrix_2d(theta), self.vertices.T).T
return self
def mirror(self, axis: int) -> 'Path':
def mirror(self, axis: int = 0) -> 'Path':
self.vertices[:, axis - 1] *= -1
return self

View File

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

View File

@ -1,4 +1,4 @@
from typing import Sequence, Any
from typing import Self
import copy
import numpy
@ -9,8 +9,7 @@ from . import Shape, Polygon, normalized_shape_tuple
from ..error import PatternError
from ..repetition import Repetition
from ..traits import RotatableImpl
from ..utils import is_scalar, get_bit, normalize_mirror
from ..utils import annotations_t
from ..utils import is_scalar, get_bit, annotations_t
# Loaded on use:
# from freetype import Face
@ -30,7 +29,7 @@ class Text(RotatableImpl, Shape):
_string: str
_height: float
_mirrored: NDArray[numpy.bool_]
_mirrored: bool
font_path: str
# vertices property
@ -53,16 +52,13 @@ class Text(RotatableImpl, Shape):
raise PatternError('Height must be a scalar')
self._height = val
# Mirrored property
@property
def mirrored(self) -> Any: # TODO mypy#3004 NDArray[numpy.bool_]:
def mirrored(self) -> bool: # mypy#3004, should be bool
return self._mirrored
@mirrored.setter
def mirrored(self, val: Sequence[bool]) -> 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 mirrored(self, val: bool) -> None:
self._mirrored = bool(val)
def __init__(
self,
@ -72,19 +68,16 @@ class Text(RotatableImpl, Shape):
*,
offset: ArrayLike = (0.0, 0.0),
rotation: float = 0.0,
mirrored: ArrayLike = (False, False),
repetition: Repetition | None = None,
annotations: annotations_t | None = None,
raw: bool = False,
) -> None:
if raw:
assert isinstance(offset, numpy.ndarray)
assert isinstance(mirrored, numpy.ndarray)
self._offset = offset
self._string = string
self._height = height
self._rotation = rotation
self._mirrored = mirrored
self._repetition = repetition
self._annotations = annotations if annotations is not None else {}
else:
@ -92,16 +85,14 @@ class Text(RotatableImpl, Shape):
self.string = string
self.height = height
self.rotation = rotation
self.mirrored = mirrored
self.repetition = repetition
self.annotations = annotations if annotations is not None else {}
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
new = copy.copy(self)
new._offset = self._offset.copy()
new._mirrored = copy.deepcopy(self._mirrored, memo)
new._annotations = copy.deepcopy(self._annotations)
return new
@ -118,7 +109,8 @@ class Text(RotatableImpl, Shape):
# Move these polygons to the right of the previous letter
for xys in raw_polys:
poly = Polygon(xys)
poly.mirror2d(self.mirrored)
if self.mirrored:
poly.mirror()
poly.scale_by(self.height)
poly.offset = self.offset + [total_advance, 0]
poly.rotate_around(self.offset, self.rotation)
@ -129,27 +121,27 @@ class Text(RotatableImpl, Shape):
return all_polygons
def mirror(self, axis: int) -> 'Text':
self.mirrored[axis] = not self.mirrored[axis]
def mirror(self, axis: int = 0) -> Self:
self.mirrored = not self.mirrored
if axis == 1:
self.rotation += pi
return self
def scale_by(self, c: float) -> 'Text':
def scale_by(self, c: float) -> Self:
self.height *= c
return self
def normalized_form(self, norm_value: float) -> normalized_shape_tuple:
mirror_x, rotation = normalize_mirror(self.mirrored)
rotation += self.rotation
rotation %= 2 * pi
rotation = self.rotation % (2 * pi)
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(
string=self.string,
height=self.height * norm_value,
font_path=self.font_path,
rotation=rotation,
mirrored=(mirror_x, False),
))
).mirror2d(across_x=self.mirrored),
)
def get_bounds_single(self) -> NDArray[numpy.float64]:
# 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:
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}>'

View File

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