diff --git a/masque/label.py b/masque/label.py index 9a6d326..72b7266 100644 --- a/masque/label.py +++ b/masque/label.py @@ -1,14 +1,16 @@ -from typing import List, Tuple, Dict +from typing import List, Tuple, Dict, Optional import copy import numpy from numpy import pi +from .repetition import Repetition from .error import PatternError, PatternLockedError from .utils import is_scalar, vector2, rotation_matrix_2d, layer_t, AutoSlots -from .traits import PositionableImpl, LayerableImpl, Copyable, Pivotable, LockableImpl +from .traits import PositionableImpl, LayerableImpl, Copyable, Pivotable, LockableImpl, RepeatableImpl -class Label(PositionableImpl, LayerableImpl, LockableImpl, Pivotable, Copyable, metaclass=AutoSlots): +class Label(PositionableImpl, LayerableImpl, LockableImpl, RepeatableImpl, + Pivotable, Copyable, metaclass=AutoSlots): """ A text annotation with a position and layer (but no size; it is not drawn) """ @@ -39,18 +41,21 @@ class Label(PositionableImpl, LayerableImpl, LockableImpl, Pivotable, Copyable, string: str, offset: vector2 = (0.0, 0.0), layer: layer_t = 0, + repetition: Optional[Repetition] = None, locked: bool = False): object.__setattr__(self, 'locked', False) self.identifier = () self.string = string self.offset = numpy.array(offset, dtype=float, copy=True) self.layer = layer + self.repetition = repetition self.locked = locked def __copy__(self) -> 'Label': return Label(string=self.string, offset=self.offset.copy(), layer=self.layer, + repetition=self.repetition, locked=self.locked) def __deepcopy__(self, memo: Dict = None) -> 'Label': diff --git a/masque/pattern.py b/masque/pattern.py index 2e57464..9c45749 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -575,6 +575,52 @@ class Pattern: self.append(p) return self + def wrap_repeated_shapes(self, + name_func: Callable[['Pattern', Union[Shape, Label]], str] = lambda p, s: '_repetition', + recursive: bool = True, + ) -> 'Pattern': + """ + Wraps all shapes and labels with a non-`None` `repetition` attribute + into a `SubPattern`/`Pattern` combination, and applies the `repetition` + to each `SubPattern` instead of its contained shape. + + Args: + name_func: Function f(this_pattern, shape) which generates a name for the + wrapping pattern. Default always returns '_repetition'. + recursive: If `True`, this function is also applied to all referenced patterns + recursively. Default `True`. + + Returns: + self + """ + def do_wrap(pat: Optional[Pattern]) -> Optional[Pattern]: + if pat is None: + return pat + + new_subpatterns = [] + for shape in pat.shapes: + if shape.repetition is None: + continue + new_subpatterns.append(SubPattern(Pattern(name_func(pat, shape), shapes=[shape]))) + shape.repetition = None + + for label in self.labels: + if label.repetition is None: + continue + new_subpatterns.append(SubPattern(Pattern(name_func(pat, shape), labels=[label]))) + label.repetition = None + + pat.subpatterns += new_subpatterns + return pat + + if recursive: + self.apply(do_wrap) + else: + do_wrap(self) + + return self + + def translate_elements(self, offset: vector2) -> 'Pattern': """ Translates all shapes, label, and subpatterns by the given offset. diff --git a/masque/shapes/arc.py b/masque/shapes/arc.py index 7de8f76..a33d0d7 100644 --- a/masque/shapes/arc.py +++ b/masque/shapes/arc.py @@ -6,6 +6,7 @@ from numpy import pi from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS from .. import PatternError +from ..repetition import Repetition from ..utils import is_scalar, vector2, layer_t, AutoSlots @@ -158,6 +159,7 @@ class Arc(Shape, metaclass=AutoSlots): mirrored: Sequence[bool] = (False, False), layer: layer_t = 0, dose: float = 1.0, + repetition: Optional[Repetition] = None, locked: bool = False): object.__setattr__(self, 'locked', False) self.identifier = () @@ -171,6 +173,7 @@ class Arc(Shape, metaclass=AutoSlots): self.dose = dose self.poly_num_points = poly_num_points self.poly_max_arclen = poly_max_arclen + self.repetition = repetition self.locked = locked def __deepcopy__(self, memo: Dict = None) -> 'Arc': diff --git a/masque/shapes/circle.py b/masque/shapes/circle.py index 1090588..2834b2a 100644 --- a/masque/shapes/circle.py +++ b/masque/shapes/circle.py @@ -5,6 +5,7 @@ from numpy import pi from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS from .. import PatternError +from ..repetition import Repetition from ..utils import is_scalar, vector2, layer_t, AutoSlots @@ -46,6 +47,7 @@ class Circle(Shape, metaclass=AutoSlots): offset: vector2 = (0.0, 0.0), layer: layer_t = 0, dose: float = 1.0, + repetition: Optional[Repetition] = None, locked: bool = False): object.__setattr__(self, 'locked', False) self.identifier = () @@ -55,6 +57,7 @@ class Circle(Shape, metaclass=AutoSlots): self.radius = radius self.poly_num_points = poly_num_points self.poly_max_arclen = poly_max_arclen + self.repetition = repetition self.locked = locked def __deepcopy__(self, memo: Dict = None) -> 'Circle': diff --git a/masque/shapes/ellipse.py b/masque/shapes/ellipse.py index 45c1ea1..f4cc683 100644 --- a/masque/shapes/ellipse.py +++ b/masque/shapes/ellipse.py @@ -6,6 +6,7 @@ from numpy import pi from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS from .. import PatternError +from ..repetition import Repetition from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t, AutoSlots @@ -93,6 +94,7 @@ class Ellipse(Shape, metaclass=AutoSlots): mirrored: Sequence[bool] = (False, False), layer: layer_t = 0, dose: float = 1.0, + repetition: Optional[Repetition] = None, locked: bool = False): object.__setattr__(self, 'locked', False) self.identifier = () @@ -104,6 +106,7 @@ class Ellipse(Shape, metaclass=AutoSlots): self.dose = dose self.poly_num_points = poly_num_points self.poly_max_arclen = poly_max_arclen + self.repetition = repetition self.locked = locked def __deepcopy__(self, memo: Dict = None) -> 'Ellipse': diff --git a/masque/shapes/path.py b/masque/shapes/path.py index a092356..543b77a 100644 --- a/masque/shapes/path.py +++ b/masque/shapes/path.py @@ -6,6 +6,7 @@ from numpy import pi, inf from . import Shape, normalized_shape_tuple, Polygon, Circle from .. import PatternError +from ..repetition import Repetition from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t, AutoSlots from ..utils import remove_colinear_vertices, remove_duplicate_vertices @@ -147,6 +148,7 @@ class Path(Shape, metaclass=AutoSlots): mirrored: Sequence[bool] = (False, False), layer: layer_t = 0, dose: float = 1.0, + repetition: Optional[Repetition] = None, locked: bool = False, ): object.__setattr__(self, 'locked', False) @@ -163,6 +165,7 @@ class Path(Shape, metaclass=AutoSlots): self.cap_extensions = cap_extensions self.rotate(rotation) [self.mirror(a) for a, do in enumerate(mirrored) if do] + self.repetition = repetition self.locked = locked def __deepcopy__(self, memo: Dict = None) -> 'Path': diff --git a/masque/shapes/polygon.py b/masque/shapes/polygon.py index 5bbd7b0..41ca642 100644 --- a/masque/shapes/polygon.py +++ b/masque/shapes/polygon.py @@ -5,6 +5,7 @@ from numpy import pi from . import Shape, normalized_shape_tuple from .. import PatternError +from ..repetition import Repetition from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t, AutoSlots from ..utils import remove_colinear_vertices, remove_duplicate_vertices @@ -75,6 +76,7 @@ class Polygon(Shape, metaclass=AutoSlots): mirrored: Sequence[bool] = (False, False), layer: layer_t = 0, dose: float = 1.0, + repetition: Optional[Repetition] = None, locked: bool = False, ): object.__setattr__(self, 'locked', False) @@ -85,6 +87,7 @@ class Polygon(Shape, metaclass=AutoSlots): self.offset = offset self.rotate(rotation) [self.mirror(a) for a, do in enumerate(mirrored) if do] + self.repetition = repetition self.locked = locked def __deepcopy__(self, memo: Optional[Dict] = None) -> 'Polygon': diff --git a/masque/shapes/shape.py b/masque/shapes/shape.py index 759942f..c6a0c63 100644 --- a/masque/shapes/shape.py +++ b/masque/shapes/shape.py @@ -7,7 +7,7 @@ from ..error import PatternError, PatternLockedError from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t from ..traits import (PositionableImpl, LayerableImpl, DoseableImpl, Rotatable, Mirrorable, Copyable, Scalable, - PivotableImpl, LockableImpl) + PivotableImpl, LockableImpl, RepeatableImpl) if TYPE_CHECKING: from . import Polygon @@ -26,7 +26,8 @@ DEFAULT_POLY_NUM_POINTS = 24 T = TypeVar('T', bound='Shape') -class Shape(PositionableImpl, LayerableImpl, DoseableImpl, Rotatable, Mirrorable, Copyable, Scalable, PivotableImpl, LockableImpl, metaclass=ABCMeta): +class Shape(PositionableImpl, LayerableImpl, DoseableImpl, Rotatable, Mirrorable, Copyable, Scalable, + PivotableImpl, RepeatableImpl, LockableImpl, metaclass=ABCMeta): """ Abstract class specifying functions common to all shapes. """ diff --git a/masque/shapes/text.py b/masque/shapes/text.py index 9b00161..d796511 100644 --- a/masque/shapes/text.py +++ b/masque/shapes/text.py @@ -5,6 +5,7 @@ from numpy import pi, inf from . import Shape, Polygon, normalized_shape_tuple from .. import PatternError +from ..repetition import Repetition from ..traits import RotatableImpl from ..utils import is_scalar, vector2, get_bit, normalize_mirror, layer_t, AutoSlots @@ -65,6 +66,7 @@ class Text(RotatableImpl, Shape, metaclass=AutoSlots): mirrored: Tuple[bool, bool] = (False, False), layer: layer_t = 0, dose: float = 1.0, + repetition: Optional[Repetition] = None, locked: bool = False, ): object.__setattr__(self, 'locked', False) @@ -77,6 +79,7 @@ class Text(RotatableImpl, Shape, metaclass=AutoSlots): self.rotation = rotation self.font_path = font_path self.mirrored = mirrored + self.repetition = repetition self.locked = locked def __deepcopy__(self, memo: Dict = None) -> 'Text':