Add repetitions and split up code into traits
This commit is contained in:
parent
d4fbdd8d27
commit
bab40474a0
27 changed files with 1183 additions and 929 deletions
|
|
@ -6,10 +6,10 @@ from numpy import pi
|
|||
|
||||
from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS
|
||||
from .. import PatternError
|
||||
from ..utils import is_scalar, vector2, layer_t
|
||||
from ..utils import is_scalar, vector2, layer_t, AutoSlots
|
||||
|
||||
|
||||
class Arc(Shape):
|
||||
class Arc(Shape, metaclass=AutoSlots):
|
||||
"""
|
||||
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.
|
||||
|
|
@ -20,6 +20,7 @@ class Arc(Shape):
|
|||
"""
|
||||
__slots__ = ('_radii', '_angles', '_width', '_rotation',
|
||||
'poly_num_points', 'poly_max_arclen')
|
||||
|
||||
_radii: numpy.ndarray
|
||||
""" Two radii for defining an ellipse """
|
||||
|
||||
|
|
|
|||
|
|
@ -5,14 +5,15 @@ from numpy import pi
|
|||
|
||||
from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS
|
||||
from .. import PatternError
|
||||
from ..utils import is_scalar, vector2, layer_t
|
||||
from ..utils import is_scalar, vector2, layer_t, AutoSlots
|
||||
|
||||
|
||||
class Circle(Shape):
|
||||
class Circle(Shape, metaclass=AutoSlots):
|
||||
"""
|
||||
A circle, which has a position and radius.
|
||||
"""
|
||||
__slots__ = ('_radius', 'poly_num_points', 'poly_max_arclen')
|
||||
|
||||
_radius: float
|
||||
""" Circle radius """
|
||||
|
||||
|
|
|
|||
|
|
@ -6,16 +6,17 @@ from numpy import pi
|
|||
|
||||
from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS
|
||||
from .. import PatternError
|
||||
from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t
|
||||
from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t, AutoSlots
|
||||
|
||||
|
||||
class Ellipse(Shape):
|
||||
class Ellipse(Shape, metaclass=AutoSlots):
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
__slots__ = ('_radii', '_rotation',
|
||||
'poly_num_points', 'poly_max_arclen')
|
||||
|
||||
_radii: numpy.ndarray
|
||||
""" Ellipse radii """
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ from numpy import pi, inf
|
|||
|
||||
from . import Shape, normalized_shape_tuple, Polygon, Circle
|
||||
from .. import PatternError
|
||||
from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t
|
||||
from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t, AutoSlots
|
||||
from ..utils import remove_colinear_vertices, remove_duplicate_vertices
|
||||
|
||||
|
||||
|
|
@ -18,7 +18,7 @@ class PathCap(Enum):
|
|||
# defined by path.cap_extensions
|
||||
|
||||
|
||||
class Path(Shape):
|
||||
class Path(Shape, metaclass=AutoSlots):
|
||||
"""
|
||||
A path, consisting of a bunch of vertices (Nx2 ndarray), a width, an end-cap shape,
|
||||
and an offset.
|
||||
|
|
|
|||
|
|
@ -5,11 +5,11 @@ from numpy import pi
|
|||
|
||||
from . import Shape, normalized_shape_tuple
|
||||
from .. import PatternError
|
||||
from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t
|
||||
from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t, AutoSlots
|
||||
from ..utils import remove_colinear_vertices, remove_duplicate_vertices
|
||||
|
||||
|
||||
class Polygon(Shape):
|
||||
class Polygon(Shape, metaclass=AutoSlots):
|
||||
"""
|
||||
A polygon, consisting of a bunch of vertices (Nx2 ndarray) which specify an
|
||||
implicitly-closed boundary, and an offset.
|
||||
|
|
@ -17,6 +17,7 @@ class Polygon(Shape):
|
|||
A `normalized_form(...)` is available, but can be quite slow with lots of vertices.
|
||||
"""
|
||||
__slots__ = ('_vertices',)
|
||||
|
||||
_vertices: numpy.ndarray
|
||||
""" Nx2 ndarray of vertices `[[x0, y0], [x1, y1], ...]` """
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@ import numpy
|
|||
|
||||
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)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import Polygon
|
||||
|
|
@ -23,38 +26,20 @@ DEFAULT_POLY_NUM_POINTS = 24
|
|||
T = TypeVar('T', bound='Shape')
|
||||
|
||||
|
||||
class Shape(metaclass=ABCMeta):
|
||||
class Shape(PositionableImpl, LayerableImpl, DoseableImpl, Rotatable, Mirrorable, Copyable, Scalable, PivotableImpl, LockableImpl, metaclass=ABCMeta):
|
||||
"""
|
||||
Abstract class specifying functions common to all shapes.
|
||||
"""
|
||||
__slots__ = ('_offset', '_layer', '_dose', 'identifier', 'locked')
|
||||
|
||||
_offset: numpy.ndarray
|
||||
""" `[x_offset, y_offset]` """
|
||||
|
||||
_layer: layer_t
|
||||
""" Layer (integer >= 0 or tuple) """
|
||||
|
||||
_dose: float
|
||||
""" Dose """
|
||||
|
||||
identifier: Tuple
|
||||
""" An arbitrary identifier for the shape, usually empty but used by `Pattern.flatten()` """
|
||||
|
||||
locked: bool
|
||||
""" If `True`, any changes to the shape will raise a `PatternLockedError` """
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
if self.locked and name != 'locked':
|
||||
raise PatternLockedError()
|
||||
object.__setattr__(self, name, value)
|
||||
|
||||
def __copy__(self) -> 'Shape':
|
||||
cls = self.__class__
|
||||
new = cls.__new__(cls)
|
||||
for name in Shape.__slots__ + self.__slots__:
|
||||
object.__setattr__(new, name, getattr(self, name))
|
||||
return new
|
||||
# def __copy__(self) -> 'Shape':
|
||||
# cls = self.__class__
|
||||
# new = cls.__new__(cls)
|
||||
# for name in Shape.__slots__ + self.__slots__:
|
||||
# object.__setattr__(new, name, getattr(self, name))
|
||||
# return new
|
||||
|
||||
'''
|
||||
--- Abstract methods
|
||||
|
|
@ -79,53 +64,6 @@ class Shape(metaclass=ABCMeta):
|
|||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_bounds(self) -> numpy.ndarray:
|
||||
"""
|
||||
Returns `[[x_min, y_min], [x_max, y_max]]` which specify a minimal bounding box for the shape.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def rotate(self: T, theta: float) -> T:
|
||||
"""
|
||||
Rotate the shape around its origin (0, 0), ignoring its offset.
|
||||
|
||||
Args:
|
||||
theta: Angle to rotate by (counterclockwise, radians)
|
||||
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def mirror(self: T, axis: int) -> T:
|
||||
"""
|
||||
Mirror the shape across an axis.
|
||||
|
||||
Args:
|
||||
axis: Axis to mirror across.
|
||||
(0: mirror across x axis, 1: mirror across y axis)
|
||||
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def scale_by(self: T, c: float) -> T:
|
||||
"""
|
||||
Scale the shape's size (eg. radius, for a circle) by a constant factor.
|
||||
|
||||
Args:
|
||||
c: Factor to scale by
|
||||
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def normalized_form(self: T, norm_value: int) -> normalized_shape_tuple:
|
||||
"""
|
||||
|
|
@ -150,97 +88,9 @@ class Shape(metaclass=ABCMeta):
|
|||
"""
|
||||
pass
|
||||
|
||||
'''
|
||||
---- Non-abstract properties
|
||||
'''
|
||||
# offset property
|
||||
@property
|
||||
def offset(self) -> numpy.ndarray:
|
||||
"""
|
||||
[x, y] offset
|
||||
"""
|
||||
return self._offset
|
||||
|
||||
@offset.setter
|
||||
def offset(self, val: vector2):
|
||||
if not isinstance(val, numpy.ndarray):
|
||||
val = numpy.array(val, dtype=float)
|
||||
|
||||
if val.size != 2:
|
||||
raise PatternError('Offset must be convertible to size-2 ndarray')
|
||||
self._offset = val.flatten()
|
||||
|
||||
# layer property
|
||||
@property
|
||||
def layer(self) -> layer_t:
|
||||
"""
|
||||
Layer number or name (int, tuple of ints, or string)
|
||||
"""
|
||||
return self._layer
|
||||
|
||||
@layer.setter
|
||||
def layer(self, val: layer_t):
|
||||
self._layer = val
|
||||
|
||||
# dose property
|
||||
@property
|
||||
def dose(self) -> float:
|
||||
"""
|
||||
Dose (float >= 0)
|
||||
"""
|
||||
return self._dose
|
||||
|
||||
@dose.setter
|
||||
def dose(self, val: float):
|
||||
if not is_scalar(val):
|
||||
raise PatternError('Dose must be a scalar')
|
||||
if not val >= 0:
|
||||
raise PatternError('Dose must be non-negative')
|
||||
self._dose = val
|
||||
|
||||
'''
|
||||
---- Non-abstract methods
|
||||
'''
|
||||
def copy(self: T) -> T:
|
||||
"""
|
||||
Returns a deep copy of the shape.
|
||||
|
||||
Returns:
|
||||
copy.deepcopy(self)
|
||||
"""
|
||||
return copy.deepcopy(self)
|
||||
|
||||
def translate(self: T, offset: vector2) -> T:
|
||||
"""
|
||||
Translate the shape by the given offset
|
||||
|
||||
Args:
|
||||
offset: [x_offset, y,offset]
|
||||
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
self.offset += offset
|
||||
return self
|
||||
|
||||
def rotate_around(self: T, pivot: vector2, rotation: float) -> T:
|
||||
"""
|
||||
Rotate the shape around a point.
|
||||
|
||||
Args:
|
||||
pivot: Point (x, y) to rotate around
|
||||
rotation: Angle to rotate by (counterclockwise, radians)
|
||||
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
pivot = numpy.array(pivot, dtype=float)
|
||||
self.translate(-pivot)
|
||||
self.rotate(rotation)
|
||||
self.offset = numpy.dot(rotation_matrix_2d(rotation), self.offset)
|
||||
self.translate(+pivot)
|
||||
return self
|
||||
|
||||
def manhattanize_fast(self,
|
||||
grid_x: numpy.ndarray,
|
||||
grid_y: numpy.ndarray,
|
||||
|
|
@ -442,37 +292,12 @@ class Shape(metaclass=ABCMeta):
|
|||
|
||||
return manhattan_polygons
|
||||
|
||||
def set_layer(self: T, layer: layer_t) -> T:
|
||||
"""
|
||||
Chainable method for changing the layer.
|
||||
|
||||
Args:
|
||||
layer: new value for self.layer
|
||||
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
self.layer = layer
|
||||
return self
|
||||
|
||||
def lock(self: T) -> T:
|
||||
"""
|
||||
Lock the Shape, disallowing further changes
|
||||
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
self.offset.flags.writeable = False
|
||||
object.__setattr__(self, 'locked', True)
|
||||
PositionableImpl._lock(self)
|
||||
LockableImpl.lock(self)
|
||||
return self
|
||||
|
||||
def unlock(self: T) -> T:
|
||||
"""
|
||||
Unlock the Shape
|
||||
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
object.__setattr__(self, 'locked', False)
|
||||
self.offset.flags.writeable = True
|
||||
LockableImpl.unlock(self)
|
||||
PositionableImpl._unlock(self)
|
||||
return self
|
||||
|
|
|
|||
|
|
@ -5,22 +5,23 @@ from numpy import pi, inf
|
|||
|
||||
from . import Shape, Polygon, normalized_shape_tuple
|
||||
from .. import PatternError
|
||||
from ..utils import is_scalar, vector2, get_bit, normalize_mirror, layer_t
|
||||
from ..traits import RotatableImpl
|
||||
from ..utils import is_scalar, vector2, get_bit, normalize_mirror, layer_t, AutoSlots
|
||||
|
||||
# Loaded on use:
|
||||
# from freetype import Face
|
||||
# from matplotlib.path import Path
|
||||
|
||||
|
||||
class Text(Shape):
|
||||
class Text(RotatableImpl, Shape, metaclass=AutoSlots):
|
||||
"""
|
||||
Text (to be printed e.g. as a set of polygons).
|
||||
This is distinct from non-printed Label objects.
|
||||
"""
|
||||
__slots__ = ('_string', '_height', '_rotation', '_mirrored', 'font_path')
|
||||
__slots__ = ('_string', '_height', '_mirrored', 'font_path')
|
||||
|
||||
_string: str
|
||||
_height: float
|
||||
_rotation: float
|
||||
_mirrored: numpy.ndarray #ndarray[bool]
|
||||
font_path: str
|
||||
|
||||
|
|
@ -33,17 +34,6 @@ class Text(Shape):
|
|||
def string(self, val: str):
|
||||
self._string = val
|
||||
|
||||
# Rotation property
|
||||
@property
|
||||
def rotation(self) -> float:
|
||||
return self._rotation
|
||||
|
||||
@rotation.setter
|
||||
def rotation(self, val: float):
|
||||
if not is_scalar(val):
|
||||
raise PatternError('Rotation must be a scalar')
|
||||
self._rotation = val % (2 * pi)
|
||||
|
||||
# Height property
|
||||
@property
|
||||
def height(self) -> float:
|
||||
|
|
@ -120,10 +110,6 @@ class Text(Shape):
|
|||
|
||||
return all_polygons
|
||||
|
||||
def rotate(self, theta: float) -> 'Text':
|
||||
self.rotation += theta
|
||||
return self
|
||||
|
||||
def mirror(self, axis: int) -> 'Text':
|
||||
self.mirrored[axis] = not self.mirrored[axis]
|
||||
return self
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue