diff --git a/masque/builder/tools.py b/masque/builder/tools.py index ca27c86..6542271 100644 --- a/masque/builder/tools.py +++ b/masque/builder/tools.py @@ -251,7 +251,7 @@ class BasicTool(Tool, metaclass=ABCMeta): for generating straight paths, and a table of pre-rendered `transitions` for converting from non-native ptypes. """ - straight: tuple[Callable[[float], Pattern], str, str] + straight: tuple[Callable[[float], Pattern] | Callable[[float], Library], str, str] """ `create_straight(length: float), in_port_name, out_port_name` """ bend: abstract_tuple_t # Assumed to be clockwise @@ -290,12 +290,16 @@ class BasicTool(Tool, metaclass=ABCMeta): gen_straight, sport_in, sport_out = self.straight tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'path') - pat.add_port_pair(names=port_names, ptype=in_ptype) + pat.add_port_pair(names=port_names, ptype='unk' if in_ptype is None else in_ptype) if data.in_transition: ipat, iport_theirs, _iport_ours = data.in_transition pat.plug(ipat, {port_names[1]: iport_theirs}) if not numpy.isclose(data.straight_length, 0): - straight = tree <= {SINGLE_USE_PREFIX + 'straight': gen_straight(data.straight_length, **kwargs)} + straight_pat_or_tree = gen_straight(data.straight_length, **kwargs) + if isinstance(straight_pat_or_tree, Pattern): + straight = tree <= {SINGLE_USE_PREFIX + 'straight': straight_pat_or_tree} + else: + straight = tree <= straight_pat_or_tree pat.plug(straight, {port_names[1]: sport_in}) if data.ccw is not None: bend, bport_in, bport_out = self.bend @@ -405,12 +409,24 @@ class BasicTool(Tool, metaclass=ABCMeta): ipat, iport_theirs, _iport_ours = in_transition pat.plug(ipat, {port_names[1]: iport_theirs}) if not numpy.isclose(straight_length, 0): - straight_pat = gen_straight(straight_length, **kwargs) - if append: - pat.plug(straight_pat, {port_names[1]: sport_in}, append=True) + straight_pat_or_tree = gen_straight(straight_length, **kwargs) + pmap = {port_names[1]: sport_in} + if isinstance(straight_pat_or_tree, Pattern): + straight_pat = straight_pat_or_tree + if append: + pat.plug(straight_pat, pmap, append=True) + else: + straight_name = tree <= {SINGLE_USE_PREFIX + 'straight': straight_pat} + pat.plug(straight_name, pmap) else: - straight = tree <= {SINGLE_USE_PREFIX + 'straight': straight_pat} - pat.plug(straight, {port_names[1]: sport_in}, append=True) + straight_tree = straight_pat_or_tree + if append: + top = straight_tree.top() + straight_tree.flatten(top) + pat.plug(straight_tree[top], pmap, append=True) + else: + straight = tree <= straight_pat_or_tree + pat.plug(straight, pmap) if ccw is not None: bend, bport_in, bport_out = self.bend pat.plug(bend, {port_names[1]: bport_in}, mirrored=bool(ccw)) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index 10f5a9a..6972cfa 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -418,8 +418,8 @@ def _annotations_to_properties(annotations: annotations_t, max_len: int = 126) - i = int(key) except ValueError as err: raise PatternError(f'Annotation key {key} is not convertable to an integer') from err - if not (0 < i < 126): - raise PatternError(f'Annotation key {key} converts to {i} (must be in the range [1,125])') + if not (0 < i <= 126): + raise PatternError(f'Annotation key {key} converts to {i} (must be in the range [1,126])') val_strings = ' '.join(str(val) for val in vals) b = val_strings.encode() diff --git a/masque/file/oasis.py b/masque/file/oasis.py index 642ad44..672af25 100644 --- a/masque/file/oasis.py +++ b/masque/file/oasis.py @@ -661,7 +661,7 @@ def repetition_masq2fata( diffs = numpy.diff(rep.displacements, axis=0) diff_ints = rint_cast(diffs) frep = fatamorgana.ArbitraryRepetition(diff_ints[:, 0], diff_ints[:, 1]) # type: ignore - offset = rep.displacements[0, :] + offset = tuple(rep.displacements[0, :]) else: assert rep is None frep = None diff --git a/masque/pattern.py b/masque/pattern.py index f77c64f..01ddf6a 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -584,7 +584,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable): bounds = numpy.vstack((numpy.min(corners, axis=0), numpy.max(corners, axis=0))) * ref.scale + [ref.offset] if ref.repetition is not None: - bounds += ref.repetition.get_bounds() + bounds += ref.repetition.get_bounds_nonempty() else: # Non-manhattan rotation, have to figure out bounds by rotating the pattern diff --git a/masque/ports.py b/masque/ports.py index 1cc711a..8060616 100644 --- a/masque/ports.py +++ b/masque/ports.py @@ -64,7 +64,7 @@ class Port(PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable): return self._rotation @rotation.setter - def rotation(self, val: float) -> None: + def rotation(self, val: float | None) -> None: if val is None: self._rotation = None else: diff --git a/masque/ref.py b/masque/ref.py index 09e00b1..b3a684c 100644 --- a/masque/ref.py +++ b/masque/ref.py @@ -11,7 +11,7 @@ import numpy from numpy import pi from numpy.typing import NDArray, ArrayLike -from .utils import annotations_t, rotation_matrix_2d, annotations_eq, annotations_lt, rep2key +from .utils import annotations_t, rotation_matrix_2d, annotations_eq, annotations_lt, rep2key, SupportsBool from .repetition import Repetition from .traits import ( PositionableImpl, RotatableImpl, ScalableImpl, @@ -50,11 +50,11 @@ class Ref( # Mirrored property @property - def mirrored(self) -> bool: # mypy#3004, setter should be SupportsBool + def mirrored(self) -> bool: return self._mirrored @mirrored.setter - def mirrored(self, val: bool) -> None: + def mirrored(self, val: SupportsBool) -> None: self._mirrored = bool(val) def __init__( diff --git a/masque/repetition.py b/masque/repetition.py index e6d00fc..5e7a7f0 100644 --- a/masque/repetition.py +++ b/masque/repetition.py @@ -327,7 +327,7 @@ class Arbitrary(Repetition): """ @property - def displacements(self) -> Any: # mypy#3004 NDArray[numpy.float64]: + def displacements(self) -> NDArray[numpy.float64]: return self._displacements @displacements.setter diff --git a/masque/shapes/arc.py b/masque/shapes/arc.py index 9fe8b31..480835e 100644 --- a/masque/shapes/arc.py +++ b/masque/shapes/arc.py @@ -10,10 +10,11 @@ from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_VERTICES from ..error import PatternError from ..repetition import Repetition from ..utils import is_scalar, annotations_t, annotations_lt, annotations_eq, rep2key +from ..traits import PositionableImpl @functools.total_ordering -class Arc(Shape): +class Arc(PositionableImpl, Shape): """ An elliptical arc, formed by cutting off an elliptical ring with two rays which exit from its center. It has a position, two radii, a start and stop angle, a rotation, and a width. @@ -42,7 +43,7 @@ class Arc(Shape): # radius properties @property - def radii(self) -> Any: # mypy#3004 NDArray[numpy.float64]: + def radii(self) -> NDArray[numpy.float64]: """ Return the radii `[rx, ry]` """ @@ -79,7 +80,7 @@ class Arc(Shape): # arc start/stop angle properties @property - def angles(self) -> Any: # mypy#3004 NDArray[numpy.float64]: + def angles(self) -> NDArray[numpy.float64]: """ Return the start and stop angles `[a_start, a_stop]`. Angles are measured from x-axis after rotation @@ -412,15 +413,15 @@ class Arc(Shape): start_angle -= pi rotation += pi - angles = (start_angle, start_angle + delta_angle) + norm_angles = (start_angle, start_angle + delta_angle) rotation %= 2 * pi width = self.width - return ((type(self), radii, angles, width / norm_value), + return ((type(self), radii, norm_angles, width / norm_value), (self.offset, scale / norm_value, rotation, False), lambda: Arc( radii=radii * norm_value, - angles=angles, + angles=norm_angles, width=width * norm_value, )) diff --git a/masque/shapes/circle.py b/masque/shapes/circle.py index 0b71198..b20a681 100644 --- a/masque/shapes/circle.py +++ b/masque/shapes/circle.py @@ -10,10 +10,11 @@ from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_VERTICES from ..error import PatternError from ..repetition import Repetition from ..utils import is_scalar, annotations_t, annotations_lt, annotations_eq, rep2key +from ..traits import PositionableImpl @functools.total_ordering -class Circle(Shape): +class Circle(PositionableImpl, Shape): """ A circle, which has a position and radius. """ diff --git a/masque/shapes/ellipse.py b/masque/shapes/ellipse.py index 56ee73f..6029f2f 100644 --- a/masque/shapes/ellipse.py +++ b/masque/shapes/ellipse.py @@ -11,10 +11,11 @@ from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_VERTICES from ..error import PatternError from ..repetition import Repetition from ..utils import is_scalar, rotation_matrix_2d, annotations_t, annotations_lt, annotations_eq, rep2key +from ..traits import PositionableImpl @functools.total_ordering -class Ellipse(Shape): +class Ellipse(PositionableImpl, Shape): """ An ellipse, which has a position, two radii, and a rotation. The rotation gives the angle from x-axis, counterclockwise, to the first (x) radius. @@ -33,7 +34,7 @@ class Ellipse(Shape): # radius properties @property - def radii(self) -> Any: # mypy#3004 NDArray[numpy.float64]: + def radii(self) -> NDArray[numpy.float64]: """ Return the radii `[rx, ry]` """ diff --git a/masque/shapes/path.py b/masque/shapes/path.py index 700f02f..b9d2d4d 100644 --- a/masque/shapes/path.py +++ b/masque/shapes/path.py @@ -1,4 +1,4 @@ -from typing import Any, cast +from typing import Any, cast, Self from collections.abc import Sequence import copy import functools @@ -30,8 +30,7 @@ class PathCap(Enum): @functools.total_ordering class Path(Shape): """ - A path, consisting of a bunch of vertices (Nx2 ndarray), a width, an end-cap shape, - and an offset. + A path, consisting of a bunch of vertices (Nx2 ndarray), a width, and an end-cap shape. Note that the setter for `Path.vertices` will create a copy of the passed vertex coordinates. @@ -40,7 +39,7 @@ class Path(Shape): __slots__ = ( '_vertices', '_width', '_cap', '_cap_extensions', # Inherited - '_offset', '_repetition', '_annotations', + '_repetition', '_annotations', ) _vertices: NDArray[numpy.float64] _width: float @@ -87,7 +86,7 @@ class Path(Shape): # cap_extensions property @property - def cap_extensions(self) -> Any | None: # mypy#3004 NDArray[numpy.float64]]: + def cap_extensions(self) -> NDArray[numpy.float64] | None: """ Path end-cap extension @@ -113,7 +112,7 @@ class Path(Shape): # vertices property @property - def vertices(self) -> Any: # mypy#3004 NDArray[numpy.float64]]: + def vertices(self) -> NDArray[numpy.float64]: """ Vertices of the path (Nx2 ndarray: `[[x0, y0], [x1, y1], ...]` @@ -160,6 +159,28 @@ class Path(Shape): raise PatternError('Wrong number of vertices') self.vertices[:, 1] = val + # Offset property for `Positionable` + @property + def offset(self) -> NDArray[numpy.float64]: + """ + [x, y] offset + """ + return numpy.zeros(2) + + @offset.setter + def offset(self, val: ArrayLike) -> None: + if numpy.any(val): + raise PatternError('Path offset is forced to (0, 0)') + + def set_offset(self, val: ArrayLike) -> Self: + if numpy.any(val): + raise PatternError('Path offset is forced to (0, 0)') + return self + + def translate(self, offset: ArrayLike) -> Self: + self._vertices += numpy.atleast_2d(offset) + return self + def __init__( self, vertices: ArrayLike, @@ -177,10 +198,8 @@ class Path(Shape): if raw: assert isinstance(vertices, numpy.ndarray) - assert isinstance(offset, numpy.ndarray) assert isinstance(cap_extensions, numpy.ndarray) or cap_extensions is None self._vertices = vertices - self._offset = offset self._repetition = repetition self._annotations = annotations self._width = width @@ -188,18 +207,19 @@ class Path(Shape): self._cap_extensions = cap_extensions else: self.vertices = vertices - self.offset = offset self.repetition = repetition self.annotations = annotations self.width = width self.cap = cap self.cap_extensions = cap_extensions - self.rotate(rotation) + if numpy.any(offset): + self.translate(offset) + if rotation: + self.rotate(rotation) def __deepcopy__(self, memo: dict | None = None) -> 'Path': memo = {} if memo is None else memo new = copy.copy(self) - new._offset = self._offset.copy() new._vertices = self._vertices.copy() new._cap = copy.deepcopy(self._cap, memo) new._cap_extensions = copy.deepcopy(self._cap_extensions, memo) @@ -209,7 +229,6 @@ class Path(Shape): def __eq__(self, other: Any) -> bool: return ( type(self) is type(other) - and numpy.array_equal(self.offset, other.offset) and numpy.array_equal(self.vertices, other.vertices) and self.width == other.width and self.cap == other.cap @@ -234,8 +253,6 @@ class Path(Shape): if self.cap_extensions is None: return True return tuple(self.cap_extensions) < tuple(other.cap_extensions) - if not numpy.array_equal(self.offset, other.offset): - return tuple(self.offset) < tuple(other.offset) if self.repetition != other.repetition: return rep2key(self.repetition) < rep2key(other.repetition) return annotations_lt(self.annotations, other.annotations) @@ -292,7 +309,7 @@ class Path(Shape): if self.width == 0: verts = numpy.vstack((v, v[::-1])) - return [Polygon(offset=self.offset, vertices=verts)] + return [Polygon(vertices=verts)] perp = dvdir[:, ::-1] * [[1, -1]] * self.width / 2 @@ -343,7 +360,7 @@ class Path(Shape): o1.append(v[-1] - perp[-1]) verts = numpy.vstack((o0, o1[::-1])) - polys = [Polygon(offset=self.offset, vertices=verts)] + polys = [Polygon(vertices=verts)] if self.cap == PathCap.Circle: #for vert in v: # not sure if every vertex, or just ends? @@ -355,8 +372,8 @@ class Path(Shape): def get_bounds_single(self) -> NDArray[numpy.float64]: if self.cap == PathCap.Circle: - bounds = self.offset + numpy.vstack((numpy.min(self.vertices, axis=0) - self.width / 2, - numpy.max(self.vertices, axis=0) + self.width / 2)) + bounds = numpy.vstack((numpy.min(self.vertices, axis=0) - self.width / 2, + numpy.max(self.vertices, axis=0) + self.width / 2)) elif self.cap in ( PathCap.Flush, PathCap.Square, @@ -390,7 +407,7 @@ class Path(Shape): def normalized_form(self, norm_value: float) -> normalized_shape_tuple: # Note: this function is going to be pretty slow for many-vertexed paths, relative to # other shapes - offset = self.vertices.mean(axis=0) + self.offset + offset = self.vertices.mean(axis=0) zeroed_vertices = self.vertices - offset scale = zeroed_vertices.std() @@ -460,5 +477,5 @@ class Path(Shape): return extensions def __repr__(self) -> str: - centroid = self.offset + self.vertices.mean(axis=0) + centroid = self.vertices.mean(axis=0) return f'' diff --git a/masque/shapes/poly_collection.py b/masque/shapes/poly_collection.py index bd2c23c..e37d417 100644 --- a/masque/shapes/poly_collection.py +++ b/masque/shapes/poly_collection.py @@ -10,6 +10,7 @@ from numpy.typing import NDArray, ArrayLike from . import Shape, normalized_shape_tuple from .polygon import Polygon +from ..error import PatternError from ..repetition import Repetition from ..utils import rotation_matrix_2d, annotations_lt, annotations_eq, rep2key, annotations_t @@ -27,7 +28,7 @@ class PolyCollection(Shape): '_vertex_lists', '_vertex_offsets', # Inherited - '_offset', '_repetition', '_annotations', + '_repetition', '_annotations', ) _vertex_lists: NDArray[numpy.float64] @@ -37,14 +38,14 @@ class PolyCollection(Shape): """ 1D NDArray specifying the starting offset for each polygon """ @property - def vertex_lists(self) -> Any: # mypy#3004 NDArray[numpy.float64]: + def vertex_lists(self) -> NDArray[numpy.float64]: """ Vertices of the polygons, ((N+M+...) x 2). Use with `vertex_offsets`. """ return self._vertex_lists @property - def vertex_offsets(self) -> Any: # mypy#3004 NDArray[numpy.intp]: + def vertex_offsets(self) -> NDArray[numpy.intp]: """ Starting offset (in `vertex_lists`) for each polygon """ @@ -67,6 +68,27 @@ class PolyCollection(Shape): for slc in self.vertex_slices: yield self._vertex_lists[slc] + # Offset property for `Positionable` + @property + def offset(self) -> NDArray[numpy.float64]: + """ + [x, y] offset + """ + return numpy.zeros(2) + + @offset.setter + def offset(self, val: ArrayLike) -> None: + raise PatternError('PolyCollection offset is forced to (0, 0)') + + def set_offset(self, val: ArrayLike) -> Self: + if numpy.any(val): + raise PatternError('Path offset is forced to (0, 0)') + return self + + def translate(self, offset: ArrayLike) -> Self: + self._vertex_lists += numpy.atleast_2d(offset) + return self + def __init__( self, vertex_lists: ArrayLike, @@ -81,25 +103,23 @@ class PolyCollection(Shape): if raw: assert isinstance(vertex_lists, numpy.ndarray) assert isinstance(vertex_offsets, numpy.ndarray) - assert isinstance(offset, numpy.ndarray) self._vertex_lists = vertex_lists self._vertex_offsets = vertex_offsets - self._offset = offset self._repetition = repetition self._annotations = annotations else: self._vertex_lists = numpy.asarray(vertex_lists, dtype=float) self._vertex_offsets = numpy.asarray(vertex_offsets, dtype=numpy.intp) - self.offset = offset self.repetition = repetition self.annotations = annotations + if numpy.any(offset): + self.translate(offset) if rotation: self.rotate(rotation) 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._vertex_lists = self._vertex_lists.copy() new._vertex_offsets = self._vertex_offsets.copy() new._annotations = copy.deepcopy(self._annotations) @@ -108,7 +128,6 @@ class PolyCollection(Shape): def __eq__(self, other: Any) -> bool: return ( type(self) is type(other) - and numpy.array_equal(self.offset, other.offset) and numpy.array_equal(self._vertex_lists, other._vertex_lists) and numpy.array_equal(self._vertex_offsets, other._vertex_offsets) and self.repetition == other.repetition @@ -134,8 +153,6 @@ class PolyCollection(Shape): return vv.shape[0] < oo.shape[0] if len(self.vertex_lists) != len(other.vertex_lists): return len(self.vertex_lists) < len(other.vertex_lists) - if not numpy.array_equal(self.offset, other.offset): - return tuple(self.offset) < tuple(other.offset) if self.repetition != other.repetition: return rep2key(self.repetition) < rep2key(other.repetition) return annotations_lt(self.annotations, other.annotations) @@ -147,14 +164,13 @@ class PolyCollection(Shape): ) -> list['Polygon']: return [Polygon( vertices = vv, - offset = self.offset, repetition = copy.deepcopy(self.repetition), annotations = copy.deepcopy(self.annotations), ) for vv in self.polygon_vertices] def get_bounds_single(self) -> NDArray[numpy.float64]: # TODO note shape get_bounds doesn't include repetition - return numpy.vstack((self.offset + numpy.min(self._vertex_lists, axis=0), - self.offset + numpy.max(self._vertex_lists, axis=0))) + return numpy.vstack((numpy.min(self._vertex_lists, axis=0), + numpy.max(self._vertex_lists, axis=0))) def rotate(self, theta: float) -> Self: if theta != 0: @@ -175,7 +191,7 @@ class PolyCollection(Shape): # other shapes meanv = self._vertex_lists.mean(axis=0) zeroed_vertices = self._vertex_lists - [meanv] - offset = meanv + self.offset + offset = meanv scale = zeroed_vertices.std() normed_vertices = zeroed_vertices / scale @@ -203,5 +219,5 @@ class PolyCollection(Shape): ) def __repr__(self) -> str: - centroid = self.offset + self.vertex_lists.mean(axis=0) + centroid = self.vertex_lists.mean(axis=0) return f'' diff --git a/masque/shapes/polygon.py b/masque/shapes/polygon.py index 9c228d4..6b27606 100644 --- a/masque/shapes/polygon.py +++ b/masque/shapes/polygon.py @@ -1,4 +1,4 @@ -from typing import Any, cast, TYPE_CHECKING +from typing import Any, cast, TYPE_CHECKING, Self import copy import functools @@ -20,7 +20,7 @@ if TYPE_CHECKING: class Polygon(Shape): """ A polygon, consisting of a bunch of vertices (Nx2 ndarray) which specify an - implicitly-closed boundary, and an offset. + implicitly-closed boundary. Note that the setter for `Polygon.vertices` creates a copy of the passed vertex coordinates. @@ -30,7 +30,7 @@ class Polygon(Shape): __slots__ = ( '_vertices', # Inherited - '_offset', '_repetition', '_annotations', + '_repetition', '_annotations', ) _vertices: NDArray[numpy.float64] @@ -38,7 +38,7 @@ class Polygon(Shape): # vertices property @property - def vertices(self) -> Any: # mypy#3004 NDArray[numpy.float64]: + def vertices(self) -> NDArray[numpy.float64]: """ Vertices of the polygon (Nx2 ndarray: `[[x0, y0], [x1, y1], ...]`) @@ -85,6 +85,28 @@ class Polygon(Shape): raise PatternError('Wrong number of vertices') self.vertices[:, 1] = val + # Offset property for `Positionable` + @property + def offset(self) -> NDArray[numpy.float64]: + """ + [x, y] offset + """ + return numpy.zeros(2) + + @offset.setter + def offset(self, val: ArrayLike) -> None: + if numpy.any(val): + raise PatternError('Path offset is forced to (0, 0)') + + def set_offset(self, val: ArrayLike) -> Self: + if numpy.any(val): + raise PatternError('Path offset is forced to (0, 0)') + return self + + def translate(self, offset: ArrayLike) -> Self: + self._vertices += numpy.atleast_2d(offset) + return self + def __init__( self, vertices: ArrayLike, @@ -99,21 +121,20 @@ class Polygon(Shape): assert isinstance(vertices, numpy.ndarray) assert isinstance(offset, numpy.ndarray) self._vertices = vertices - self._offset = offset self._repetition = repetition self._annotations = annotations else: self.vertices = vertices - self.offset = offset self.repetition = repetition self.annotations = annotations + if numpy.any(offset): + self.translate(offset) if rotation: self.rotate(rotation) def __deepcopy__(self, memo: dict | None = None) -> 'Polygon': memo = {} if memo is None else memo new = copy.copy(self) - new._offset = self._offset.copy() new._vertices = self._vertices.copy() new._annotations = copy.deepcopy(self._annotations) return new @@ -121,7 +142,6 @@ class Polygon(Shape): def __eq__(self, other: Any) -> bool: return ( type(self) is type(other) - and numpy.array_equal(self.offset, other.offset) and numpy.array_equal(self.vertices, other.vertices) and self.repetition == other.repetition and annotations_eq(self.annotations, other.annotations) @@ -141,8 +161,6 @@ class Polygon(Shape): if eq_lt_masked.size > 0: return eq_lt_masked.flat[0] return self.vertices.shape[0] < other.vertices.shape[0] - if not numpy.array_equal(self.offset, other.offset): - return tuple(self.offset) < tuple(other.offset) if self.repetition != other.repetition: return rep2key(self.repetition) < rep2key(other.repetition) return annotations_lt(self.annotations, other.annotations) @@ -248,11 +266,11 @@ class Polygon(Shape): elif xmax is None: assert xmin is not None assert xctr is not None - lx = 2 * (xctr - xmin) + lx = 2.0 * (xctr - xmin) elif xmin is None: assert xctr is not None assert xmax is not None - lx = 2 * (xmax - xctr) + lx = 2.0 * (xmax - xctr) else: raise PatternError('Two of xmin, xctr, xmax, lx must be None!') else: # noqa: PLR5501 @@ -278,11 +296,11 @@ class Polygon(Shape): elif ymax is None: assert ymin is not None assert yctr is not None - ly = 2 * (yctr - ymin) + ly = 2.0 * (yctr - ymin) elif ymin is None: assert yctr is not None assert ymax is not None - ly = 2 * (ymax - yctr) + ly = 2.0 * (ymax - yctr) else: raise PatternError('Two of ymin, yctr, ymax, ly must be None!') else: # noqa: PLR5501 @@ -363,8 +381,8 @@ class Polygon(Shape): return [copy.deepcopy(self)] def get_bounds_single(self) -> NDArray[numpy.float64]: # TODO note shape get_bounds doesn't include repetition - return numpy.vstack((self.offset + numpy.min(self.vertices, axis=0), - self.offset + numpy.max(self.vertices, axis=0))) + return numpy.vstack((numpy.min(self.vertices, axis=0), + numpy.max(self.vertices, axis=0))) def rotate(self, theta: float) -> 'Polygon': if theta != 0: @@ -384,7 +402,7 @@ class Polygon(Shape): # other shapes meanv = self.vertices.mean(axis=0) zeroed_vertices = self.vertices - meanv - offset = meanv + self.offset + offset = meanv scale = zeroed_vertices.std() normed_vertices = zeroed_vertices / scale @@ -438,5 +456,5 @@ class Polygon(Shape): return self def __repr__(self) -> str: - centroid = self.offset + self.vertices.mean(axis=0) + centroid = self.vertices.mean(axis=0) return f'' diff --git a/masque/shapes/shape.py b/masque/shapes/shape.py index 0a7c86d..90bca2b 100644 --- a/masque/shapes/shape.py +++ b/masque/shapes/shape.py @@ -7,7 +7,7 @@ from numpy.typing import NDArray, ArrayLike from ..traits import ( Rotatable, Mirrorable, Copyable, Scalable, - PositionableImpl, PivotableImpl, RepeatableImpl, AnnotatableImpl, + Positionable, PivotableImpl, RepeatableImpl, AnnotatableImpl, ) if TYPE_CHECKING: @@ -26,7 +26,7 @@ normalized_shape_tuple = tuple[ DEFAULT_POLY_NUM_VERTICES = 24 -class Shape(PositionableImpl, Rotatable, Mirrorable, Copyable, Scalable, +class Shape(Positionable, Rotatable, Mirrorable, Copyable, Scalable, PivotableImpl, RepeatableImpl, AnnotatableImpl, metaclass=ABCMeta): """ Class specifying functions common to all shapes. @@ -134,7 +134,7 @@ class Shape(PositionableImpl, Rotatable, Mirrorable, Copyable, Scalable, mins, maxs = bounds vertex_lists = [] - p_verts = polygon.vertices + polygon.offset + p_verts = polygon.vertices for v, v_next in zip(p_verts, numpy.roll(p_verts, -1, axis=0), strict=True): dv = v_next - v @@ -282,7 +282,7 @@ class Shape(PositionableImpl, Rotatable, Mirrorable, Copyable, Scalable, offset = (numpy.where(keep_x)[0][0], numpy.where(keep_y)[0][0]) - rastered = float_raster.raster((polygon.vertices + polygon.offset).T, gx, gy) + rastered = float_raster.raster((polygon.vertices).T, gx, gy) binary_rastered = (numpy.abs(rastered) >= 0.5) supersampled = binary_rastered.repeat(2, axis=0).repeat(2, axis=1) diff --git a/masque/shapes/text.py b/masque/shapes/text.py index e8b97ed..78632f6 100644 --- a/masque/shapes/text.py +++ b/masque/shapes/text.py @@ -9,8 +9,8 @@ from numpy.typing import NDArray, ArrayLike 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, annotations_t, annotations_lt, annotations_eq, rep2key +from ..traits import PositionableImpl, RotatableImpl +from ..utils import is_scalar, get_bit, annotations_t, annotations_lt, annotations_eq, rep2key, SupportsBool # Loaded on use: # from freetype import Face @@ -18,7 +18,7 @@ from ..utils import is_scalar, get_bit, annotations_t, annotations_lt, annotatio @functools.total_ordering -class Text(RotatableImpl, Shape): +class Text(PositionableImpl, RotatableImpl, Shape): """ Text (to be printed e.g. as a set of polygons). This is distinct from non-printed Label objects. @@ -55,11 +55,11 @@ class Text(RotatableImpl, Shape): self._height = val @property - def mirrored(self) -> bool: # mypy#3004, should be bool + def mirrored(self) -> bool: return self._mirrored @mirrored.setter - def mirrored(self, val: bool) -> None: + def mirrored(self, val: SupportsBool) -> None: self._mirrored = bool(val) def __init__( @@ -201,7 +201,7 @@ def get_char_as_polygons( font_path: str, char: str, resolution: float = 48 * 64, - ) -> tuple[list[list[list[float]]], float]: + ) -> tuple[list[NDArray[numpy.float64]], float]: from freetype import Face # type: ignore from matplotlib.path import Path # type: ignore @@ -276,11 +276,12 @@ def get_char_as_polygons( advance = slot.advance.x / resolution + polygons: list[NDArray[numpy.float64]] if len(all_verts) == 0: polygons = [] else: path = Path(all_verts, all_codes) path.should_simplify = False - polygons = path.to_polygons() + polygons = [numpy.asarray(poly) for poly in path.to_polygons()] return polygons, advance diff --git a/masque/traits/positionable.py b/masque/traits/positionable.py index 66e6e7d..fd8551b 100644 --- a/masque/traits/positionable.py +++ b/masque/traits/positionable.py @@ -73,7 +73,7 @@ class PositionableImpl(Positionable, metaclass=ABCMeta): # # offset property @property - def offset(self) -> Any: # mypy#3004 NDArray[numpy.float64]: + def offset(self) -> NDArray[numpy.float64]: """ [x, y] offset """ @@ -95,7 +95,7 @@ class PositionableImpl(Positionable, metaclass=ABCMeta): return self def translate(self, offset: ArrayLike) -> Self: - self._offset += offset # type: ignore # NDArray += ArrayLike should be fine?? + self._offset += numpy.asarray(offset) return self diff --git a/masque/traits/rotatable.py b/masque/traits/rotatable.py index 04816f1..2fa86c1 100644 --- a/masque/traits/rotatable.py +++ b/masque/traits/rotatable.py @@ -116,7 +116,7 @@ class PivotableImpl(Pivotable, metaclass=ABCMeta): pivot = numpy.asarray(pivot, dtype=float) cast('Positionable', self).translate(-pivot) cast('Rotatable', self).rotate(rotation) - self.offset = numpy.dot(rotation_matrix_2d(rotation), self.offset) # type: ignore # mypy#3004 + self.offset = numpy.dot(rotation_matrix_2d(rotation), self.offset) cast('Positionable', self).translate(+pivot) return self