diff --git a/masque/builder/tools.py b/masque/builder/tools.py index 6542271..ca27c86 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] | Callable[[float], Library], str, str] + straight: tuple[Callable[[float], Pattern], str, str] """ `create_straight(length: float), in_port_name, out_port_name` """ bend: abstract_tuple_t # Assumed to be clockwise @@ -290,16 +290,12 @@ 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='unk' if in_ptype is None else in_ptype) + pat.add_port_pair(names=port_names, ptype=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_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 + straight = tree <= {SINGLE_USE_PREFIX + 'straight': gen_straight(data.straight_length, **kwargs)} pat.plug(straight, {port_names[1]: sport_in}) if data.ccw is not None: bend, bport_in, bport_out = self.bend @@ -409,24 +405,12 @@ 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_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) + straight_pat = gen_straight(straight_length, **kwargs) + if append: + pat.plug(straight_pat, {port_names[1]: sport_in}, append=True) else: - 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) + straight = tree <= {SINGLE_USE_PREFIX + 'straight': straight_pat} + pat.plug(straight, {port_names[1]: sport_in}, append=True) 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 6972cfa..10f5a9a 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,126])') + if not (0 < i < 126): + raise PatternError(f'Annotation key {key} converts to {i} (must be in the range [1,125])') 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 672af25..642ad44 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 = tuple(rep.displacements[0, :]) + offset = rep.displacements[0, :] else: assert rep is None frep = None diff --git a/masque/pattern.py b/masque/pattern.py index 01ddf6a..f77c64f 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_nonempty() + bounds += ref.repetition.get_bounds() else: # Non-manhattan rotation, have to figure out bounds by rotating the pattern diff --git a/masque/ports.py b/masque/ports.py index 8060616..1cc711a 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) -> None: + def rotation(self, val: float) -> None: if val is None: self._rotation = None else: diff --git a/masque/ref.py b/masque/ref.py index b3a684c..09e00b1 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, SupportsBool +from .utils import annotations_t, rotation_matrix_2d, annotations_eq, annotations_lt, rep2key from .repetition import Repetition from .traits import ( PositionableImpl, RotatableImpl, ScalableImpl, @@ -50,11 +50,11 @@ class Ref( # Mirrored property @property - def mirrored(self) -> bool: + def mirrored(self) -> bool: # mypy#3004, setter should be SupportsBool return self._mirrored @mirrored.setter - def mirrored(self, val: SupportsBool) -> None: + def mirrored(self, val: bool) -> None: self._mirrored = bool(val) def __init__( diff --git a/masque/repetition.py b/masque/repetition.py index 5e7a7f0..e6d00fc 100644 --- a/masque/repetition.py +++ b/masque/repetition.py @@ -327,7 +327,7 @@ class Arbitrary(Repetition): """ @property - def displacements(self) -> NDArray[numpy.float64]: + def displacements(self) -> Any: # mypy#3004 NDArray[numpy.float64]: return self._displacements @displacements.setter diff --git a/masque/shapes/arc.py b/masque/shapes/arc.py index 480835e..9fe8b31 100644 --- a/masque/shapes/arc.py +++ b/masque/shapes/arc.py @@ -10,11 +10,10 @@ 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(PositionableImpl, Shape): +class Arc(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. @@ -43,7 +42,7 @@ class Arc(PositionableImpl, Shape): # radius properties @property - def radii(self) -> NDArray[numpy.float64]: + def radii(self) -> Any: # mypy#3004 NDArray[numpy.float64]: """ Return the radii `[rx, ry]` """ @@ -80,7 +79,7 @@ class Arc(PositionableImpl, Shape): # arc start/stop angle properties @property - def angles(self) -> NDArray[numpy.float64]: + def angles(self) -> Any: # mypy#3004 NDArray[numpy.float64]: """ Return the start and stop angles `[a_start, a_stop]`. Angles are measured from x-axis after rotation @@ -413,15 +412,15 @@ class Arc(PositionableImpl, Shape): start_angle -= pi rotation += pi - norm_angles = (start_angle, start_angle + delta_angle) + angles = (start_angle, start_angle + delta_angle) rotation %= 2 * pi width = self.width - return ((type(self), radii, norm_angles, width / norm_value), + return ((type(self), radii, angles, width / norm_value), (self.offset, scale / norm_value, rotation, False), lambda: Arc( radii=radii * norm_value, - angles=norm_angles, + angles=angles, width=width * norm_value, )) diff --git a/masque/shapes/circle.py b/masque/shapes/circle.py index b20a681..0b71198 100644 --- a/masque/shapes/circle.py +++ b/masque/shapes/circle.py @@ -10,11 +10,10 @@ 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(PositionableImpl, Shape): +class Circle(Shape): """ A circle, which has a position and radius. """ diff --git a/masque/shapes/ellipse.py b/masque/shapes/ellipse.py index 6029f2f..56ee73f 100644 --- a/masque/shapes/ellipse.py +++ b/masque/shapes/ellipse.py @@ -11,11 +11,10 @@ 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(PositionableImpl, Shape): +class Ellipse(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. @@ -34,7 +33,7 @@ class Ellipse(PositionableImpl, Shape): # radius properties @property - def radii(self) -> NDArray[numpy.float64]: + def radii(self) -> Any: # mypy#3004 NDArray[numpy.float64]: """ Return the radii `[rx, ry]` """ diff --git a/masque/shapes/path.py b/masque/shapes/path.py index b9d2d4d..700f02f 100644 --- a/masque/shapes/path.py +++ b/masque/shapes/path.py @@ -1,4 +1,4 @@ -from typing import Any, cast, Self +from typing import Any, cast from collections.abc import Sequence import copy import functools @@ -30,7 +30,8 @@ class PathCap(Enum): @functools.total_ordering class Path(Shape): """ - A path, consisting of a bunch of vertices (Nx2 ndarray), a width, and an end-cap shape. + A path, consisting of a bunch of vertices (Nx2 ndarray), a width, an end-cap shape, + and an offset. Note that the setter for `Path.vertices` will create a copy of the passed vertex coordinates. @@ -39,7 +40,7 @@ class Path(Shape): __slots__ = ( '_vertices', '_width', '_cap', '_cap_extensions', # Inherited - '_repetition', '_annotations', + '_offset', '_repetition', '_annotations', ) _vertices: NDArray[numpy.float64] _width: float @@ -86,7 +87,7 @@ class Path(Shape): # cap_extensions property @property - def cap_extensions(self) -> NDArray[numpy.float64] | None: + def cap_extensions(self) -> Any | None: # mypy#3004 NDArray[numpy.float64]]: """ Path end-cap extension @@ -112,7 +113,7 @@ class Path(Shape): # vertices property @property - def vertices(self) -> NDArray[numpy.float64]: + def vertices(self) -> Any: # mypy#3004 NDArray[numpy.float64]]: """ Vertices of the path (Nx2 ndarray: `[[x0, y0], [x1, y1], ...]` @@ -159,28 +160,6 @@ 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, @@ -198,8 +177,10 @@ 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 @@ -207,19 +188,18 @@ 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 - if numpy.any(offset): - self.translate(offset) - if rotation: - self.rotate(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) @@ -229,6 +209,7 @@ 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 @@ -253,6 +234,8 @@ 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) @@ -309,7 +292,7 @@ class Path(Shape): if self.width == 0: verts = numpy.vstack((v, v[::-1])) - return [Polygon(vertices=verts)] + return [Polygon(offset=self.offset, vertices=verts)] perp = dvdir[:, ::-1] * [[1, -1]] * self.width / 2 @@ -360,7 +343,7 @@ class Path(Shape): o1.append(v[-1] - perp[-1]) verts = numpy.vstack((o0, o1[::-1])) - polys = [Polygon(vertices=verts)] + polys = [Polygon(offset=self.offset, vertices=verts)] if self.cap == PathCap.Circle: #for vert in v: # not sure if every vertex, or just ends? @@ -372,8 +355,8 @@ class Path(Shape): def get_bounds_single(self) -> NDArray[numpy.float64]: if self.cap == PathCap.Circle: - bounds = numpy.vstack((numpy.min(self.vertices, axis=0) - self.width / 2, - numpy.max(self.vertices, axis=0) + self.width / 2)) + bounds = self.offset + 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, @@ -407,7 +390,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) + offset = self.vertices.mean(axis=0) + self.offset zeroed_vertices = self.vertices - offset scale = zeroed_vertices.std() @@ -477,5 +460,5 @@ class Path(Shape): return extensions def __repr__(self) -> str: - centroid = self.vertices.mean(axis=0) + centroid = self.offset + self.vertices.mean(axis=0) return f'' diff --git a/masque/shapes/poly_collection.py b/masque/shapes/poly_collection.py index e37d417..bd2c23c 100644 --- a/masque/shapes/poly_collection.py +++ b/masque/shapes/poly_collection.py @@ -10,7 +10,6 @@ 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 @@ -28,7 +27,7 @@ class PolyCollection(Shape): '_vertex_lists', '_vertex_offsets', # Inherited - '_repetition', '_annotations', + '_offset', '_repetition', '_annotations', ) _vertex_lists: NDArray[numpy.float64] @@ -38,14 +37,14 @@ class PolyCollection(Shape): """ 1D NDArray specifying the starting offset for each polygon """ @property - def vertex_lists(self) -> NDArray[numpy.float64]: + def vertex_lists(self) -> Any: # mypy#3004 NDArray[numpy.float64]: """ Vertices of the polygons, ((N+M+...) x 2). Use with `vertex_offsets`. """ return self._vertex_lists @property - def vertex_offsets(self) -> NDArray[numpy.intp]: + def vertex_offsets(self) -> Any: # mypy#3004 NDArray[numpy.intp]: """ Starting offset (in `vertex_lists`) for each polygon """ @@ -68,27 +67,6 @@ 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, @@ -103,23 +81,25 @@ 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) @@ -128,6 +108,7 @@ 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 @@ -153,6 +134,8 @@ 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) @@ -164,13 +147,14 @@ 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((numpy.min(self._vertex_lists, axis=0), - numpy.max(self._vertex_lists, axis=0))) + return numpy.vstack((self.offset + numpy.min(self._vertex_lists, axis=0), + self.offset + numpy.max(self._vertex_lists, axis=0))) def rotate(self, theta: float) -> Self: if theta != 0: @@ -191,7 +175,7 @@ class PolyCollection(Shape): # other shapes meanv = self._vertex_lists.mean(axis=0) zeroed_vertices = self._vertex_lists - [meanv] - offset = meanv + offset = meanv + self.offset scale = zeroed_vertices.std() normed_vertices = zeroed_vertices / scale @@ -219,5 +203,5 @@ class PolyCollection(Shape): ) def __repr__(self) -> str: - centroid = self.vertex_lists.mean(axis=0) + centroid = self.offset + self.vertex_lists.mean(axis=0) return f'' diff --git a/masque/shapes/polygon.py b/masque/shapes/polygon.py index 6b27606..9c228d4 100644 --- a/masque/shapes/polygon.py +++ b/masque/shapes/polygon.py @@ -1,4 +1,4 @@ -from typing import Any, cast, TYPE_CHECKING, Self +from typing import Any, cast, TYPE_CHECKING 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. + implicitly-closed boundary, and an offset. 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 - '_repetition', '_annotations', + '_offset', '_repetition', '_annotations', ) _vertices: NDArray[numpy.float64] @@ -38,7 +38,7 @@ class Polygon(Shape): # vertices property @property - def vertices(self) -> NDArray[numpy.float64]: + def vertices(self) -> Any: # mypy#3004 NDArray[numpy.float64]: """ Vertices of the polygon (Nx2 ndarray: `[[x0, y0], [x1, y1], ...]`) @@ -85,28 +85,6 @@ 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, @@ -121,20 +99,21 @@ 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 @@ -142,6 +121,7 @@ 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) @@ -161,6 +141,8 @@ 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) @@ -266,11 +248,11 @@ class Polygon(Shape): elif xmax is None: assert xmin is not None assert xctr is not None - lx = 2.0 * (xctr - xmin) + lx = 2 * (xctr - xmin) elif xmin is None: assert xctr is not None assert xmax is not None - lx = 2.0 * (xmax - xctr) + lx = 2 * (xmax - xctr) else: raise PatternError('Two of xmin, xctr, xmax, lx must be None!') else: # noqa: PLR5501 @@ -296,11 +278,11 @@ class Polygon(Shape): elif ymax is None: assert ymin is not None assert yctr is not None - ly = 2.0 * (yctr - ymin) + ly = 2 * (yctr - ymin) elif ymin is None: assert yctr is not None assert ymax is not None - ly = 2.0 * (ymax - yctr) + ly = 2 * (ymax - yctr) else: raise PatternError('Two of ymin, yctr, ymax, ly must be None!') else: # noqa: PLR5501 @@ -381,8 +363,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((numpy.min(self.vertices, axis=0), - numpy.max(self.vertices, axis=0))) + return numpy.vstack((self.offset + numpy.min(self.vertices, axis=0), + self.offset + numpy.max(self.vertices, axis=0))) def rotate(self, theta: float) -> 'Polygon': if theta != 0: @@ -402,7 +384,7 @@ class Polygon(Shape): # other shapes meanv = self.vertices.mean(axis=0) zeroed_vertices = self.vertices - meanv - offset = meanv + offset = meanv + self.offset scale = zeroed_vertices.std() normed_vertices = zeroed_vertices / scale @@ -456,5 +438,5 @@ class Polygon(Shape): return self def __repr__(self) -> str: - centroid = self.vertices.mean(axis=0) + centroid = self.offset + self.vertices.mean(axis=0) return f'' diff --git a/masque/shapes/shape.py b/masque/shapes/shape.py index 90bca2b..0a7c86d 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, - Positionable, PivotableImpl, RepeatableImpl, AnnotatableImpl, + PositionableImpl, PivotableImpl, RepeatableImpl, AnnotatableImpl, ) if TYPE_CHECKING: @@ -26,7 +26,7 @@ normalized_shape_tuple = tuple[ DEFAULT_POLY_NUM_VERTICES = 24 -class Shape(Positionable, Rotatable, Mirrorable, Copyable, Scalable, +class Shape(PositionableImpl, Rotatable, Mirrorable, Copyable, Scalable, PivotableImpl, RepeatableImpl, AnnotatableImpl, metaclass=ABCMeta): """ Class specifying functions common to all shapes. @@ -134,7 +134,7 @@ class Shape(Positionable, Rotatable, Mirrorable, Copyable, Scalable, mins, maxs = bounds vertex_lists = [] - p_verts = polygon.vertices + p_verts = polygon.vertices + polygon.offset 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(Positionable, Rotatable, Mirrorable, Copyable, Scalable, offset = (numpy.where(keep_x)[0][0], numpy.where(keep_y)[0][0]) - rastered = float_raster.raster((polygon.vertices).T, gx, gy) + rastered = float_raster.raster((polygon.vertices + polygon.offset).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 78632f6..e8b97ed 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 PositionableImpl, RotatableImpl -from ..utils import is_scalar, get_bit, annotations_t, annotations_lt, annotations_eq, rep2key, SupportsBool +from ..traits import RotatableImpl +from ..utils import is_scalar, get_bit, annotations_t, annotations_lt, annotations_eq, rep2key # 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(PositionableImpl, RotatableImpl, Shape): +class Text(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(PositionableImpl, RotatableImpl, Shape): self._height = val @property - def mirrored(self) -> bool: + def mirrored(self) -> bool: # mypy#3004, should be bool return self._mirrored @mirrored.setter - def mirrored(self, val: SupportsBool) -> None: + def mirrored(self, val: bool) -> 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[NDArray[numpy.float64]], float]: + ) -> tuple[list[list[list[float]]], float]: from freetype import Face # type: ignore from matplotlib.path import Path # type: ignore @@ -276,12 +276,11 @@ 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 = [numpy.asarray(poly) for poly in path.to_polygons()] + polygons = path.to_polygons() return polygons, advance diff --git a/masque/traits/positionable.py b/masque/traits/positionable.py index fd8551b..66e6e7d 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) -> NDArray[numpy.float64]: + def offset(self) -> Any: # mypy#3004 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 += numpy.asarray(offset) + self._offset += offset # type: ignore # NDArray += ArrayLike should be fine?? return self diff --git a/masque/traits/rotatable.py b/masque/traits/rotatable.py index 2fa86c1..04816f1 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) + self.offset = numpy.dot(rotation_matrix_2d(rotation), self.offset) # type: ignore # mypy#3004 cast('Positionable', self).translate(+pivot) return self