You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
masque/masque/ref.py

201 lines
6.7 KiB
Python

"""
Ref provides basic support for nesting Pattern objects within each other.
It carries offset, rotation, mirroring, and scaling data for each individual instance.
"""
from typing import Mapping, TYPE_CHECKING, Self
import copy
import numpy
from numpy import pi
from numpy.typing import NDArray, ArrayLike
8 months ago
from .utils import annotations_t, rotation_matrix_2d
from .repetition import Repetition
from .traits import (
PositionableImpl, RotatableImpl, ScalableImpl,
Mirrorable, PivotableImpl, Copyable, RepeatableImpl, AnnotatableImpl,
)
if TYPE_CHECKING:
from . import Pattern
1 year ago
class Ref(
PositionableImpl, RotatableImpl, ScalableImpl, Mirrorable,
PivotableImpl, Copyable, RepeatableImpl, AnnotatableImpl,
):
"""
`Ref` provides basic support for nesting Pattern objects within each other.
It containts the transformation (mirror, rotation, scale, offset, repetition)
and annotations for a single instantiation of a `Pattern`.
Note that the target (i.e. which pattern a `Ref` instantiates) is not stored within the
`Ref` itself, but is specified by the containing `Pattern`.
Order of operations is (mirror, rotate, scale, translate, repeat).
"""
1 year ago
__slots__ = (
'_mirrored',
1 year ago
# inherited
'_offset', '_rotation', 'scale', '_repetition', '_annotations',
)
_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: 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 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
self.mirrored = mirrored
self.repetition = repetition
self.annotations = annotations if annotations is not None else {}
1 year ago
def __copy__(self) -> 'Ref':
new = Ref(
offset=self.offset.copy(),
rotation=self.rotation,
scale=self.scale,
mirrored=self.mirrored,
repetition=copy.deepcopy(self.repetition),
annotations=copy.deepcopy(self.annotations),
)
return new
def __deepcopy__(self, memo: dict | None = None) -> 'Ref':
memo = {} if memo is None else memo
new = copy.copy(self)
#new.repetition = copy.deepcopy(self.repetition, memo)
#new.annotations = copy.deepcopy(self.annotations, memo)
return new
def as_pattern(
self,
pattern: 'Pattern',
) -> 'Pattern':
"""
Args:
pattern: Pattern object to transform
Returns:
A copy of the referenced Pattern which has been scaled, rotated, etc.
1 year ago
according to this `Ref`'s properties.
"""
pattern = pattern.deepcopy()
if self.scale != 1:
pattern.scale_by(self.scale)
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):
pattern.translate_elements(self.offset)
if self.repetition is not None:
combined = type(pattern)()
for dd in self.repetition.displacements:
temp_pat = pattern.deepcopy()
temp_pat.ports = {}
temp_pat.translate_elements(dd)
combined.append(temp_pat)
pattern = combined
return pattern
1 year ago
def rotate(self, rotation: float) -> Self:
4 years ago
self.rotation += rotation
if self.repetition is not None:
self.repetition.rotate(rotation)
return self
def mirror(self, axis: int = 0) -> Self:
self.mirror_target(axis)
self.rotation *= -1
4 years ago
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',
*,
library: Mapping[str, 'Pattern'] | None = None,
) -> NDArray[numpy.float64] | None:
"""
Return a `numpy.ndarray` containing `[[x_min, y_min], [x_max, y_max]]`, corresponding to the
1 year ago
extent of the `Ref` in each dimension.
Returns `None` if the contained `Pattern` is empty.
Args:
library: Name-to-Pattern mapping for resul
Returns:
`[[x_min, y_min], [x_max, y_max]]` or `None`
"""
if pattern.is_empty():
1 year ago
# no need to run as_pattern()
return None
# if rotation is manhattan, can take pattern's bounds and transform them
if numpy.isclose(self.rotation % (pi / 2), 0):
unrot_bounds = pattern.get_bounds(library)
if unrot_bounds is None:
return None
if self.mirrored:
unrot_bounds[:, 1] *= -1
corners = (rotation_matrix_2d(self.rotation) @ unrot_bounds.T).T
bounds = numpy.vstack((numpy.min(corners, axis=0),
numpy.max(corners, axis=0))) * self.scale + [self.offset]
return bounds
return self.as_pattern(pattern=pattern).get_bounds(library)
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' if self.mirrored else ''
return f'<Ref {self.offset}{rotation}{scale}{mirrored}>'