2022-02-23 15:47:38 -08:00
|
|
|
from typing import List, Dict, Sequence, Optional, Any
|
2019-05-17 00:41:43 -07:00
|
|
|
import copy
|
2016-03-15 19:12:39 -07:00
|
|
|
import math
|
2020-09-10 20:06:58 -07:00
|
|
|
|
2022-02-23 15:47:38 -08:00
|
|
|
import numpy
|
2016-03-15 19:12:39 -07:00
|
|
|
from numpy import pi
|
2022-02-23 15:47:38 -08:00
|
|
|
from numpy.typing import ArrayLike, NDArray
|
2016-03-15 19:12:39 -07:00
|
|
|
|
|
|
|
from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS
|
|
|
|
from .. import PatternError
|
2020-07-22 21:50:39 -07:00
|
|
|
from ..repetition import Repetition
|
2022-02-23 15:47:38 -08:00
|
|
|
from ..utils import is_scalar, rotation_matrix_2d, layer_t, AutoSlots, annotations_t
|
2016-03-15 19:12:39 -07:00
|
|
|
|
|
|
|
|
2020-07-22 02:45:16 -07:00
|
|
|
class Ellipse(Shape, metaclass=AutoSlots):
|
2016-03-15 19:12:39 -07:00
|
|
|
"""
|
|
|
|
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.
|
|
|
|
"""
|
2023-01-19 22:20:16 -08:00
|
|
|
__slots__ = (
|
|
|
|
'_radii', '_rotation',
|
|
|
|
'poly_num_points', 'poly_max_arclen',
|
|
|
|
# Inherited
|
|
|
|
'_offset', '_layer', '_repetition', '_annotations',
|
|
|
|
)
|
2020-07-22 02:45:16 -07:00
|
|
|
|
2022-02-23 15:47:38 -08:00
|
|
|
_radii: NDArray[numpy.float64]
|
2020-02-17 21:02:53 -08:00
|
|
|
""" Ellipse radii """
|
|
|
|
|
2019-05-17 00:37:56 -07:00
|
|
|
_rotation: float
|
2020-02-17 21:02:53 -08:00
|
|
|
""" Angle from x-axis to first radius (ccw, radians) """
|
|
|
|
|
2020-05-11 19:09:35 -07:00
|
|
|
poly_num_points: Optional[int]
|
2020-02-17 21:02:53 -08:00
|
|
|
""" Sets the default number of points for `.polygonize()` """
|
|
|
|
|
2020-05-11 19:09:35 -07:00
|
|
|
poly_max_arclen: Optional[float]
|
2020-02-17 21:02:53 -08:00
|
|
|
""" Sets the default max segement length for `.polygonize()` """
|
2016-03-15 19:12:39 -07:00
|
|
|
|
|
|
|
# radius properties
|
|
|
|
@property
|
2022-02-23 15:47:38 -08:00
|
|
|
def radii(self) -> Any: #TODO mypy#3004 NDArray[numpy.float64]:
|
2016-03-15 19:12:39 -07:00
|
|
|
"""
|
2020-02-17 21:02:53 -08:00
|
|
|
Return the radii `[rx, ry]`
|
2016-03-15 19:12:39 -07:00
|
|
|
"""
|
2017-04-19 18:54:58 -07:00
|
|
|
return self._radii
|
2016-03-15 19:12:39 -07:00
|
|
|
|
|
|
|
@radii.setter
|
2022-02-23 15:47:38 -08:00
|
|
|
def radii(self, val: ArrayLike) -> None:
|
2016-03-15 19:12:39 -07:00
|
|
|
val = numpy.array(val).flatten()
|
|
|
|
if not val.size == 2:
|
|
|
|
raise PatternError('Radii must have length 2')
|
|
|
|
if not val.min() >= 0:
|
|
|
|
raise PatternError('Radii must be non-negative')
|
2017-04-19 18:54:58 -07:00
|
|
|
self._radii = val
|
2016-03-15 19:12:39 -07:00
|
|
|
|
|
|
|
@property
|
|
|
|
def radius_x(self) -> float:
|
|
|
|
return self.radii[0]
|
|
|
|
|
|
|
|
@radius_x.setter
|
2022-02-23 11:27:11 -08:00
|
|
|
def radius_x(self, val: float) -> None:
|
2016-03-15 19:12:39 -07:00
|
|
|
if not val >= 0:
|
|
|
|
raise PatternError('Radius must be non-negative')
|
|
|
|
self.radii[0] = val
|
|
|
|
|
|
|
|
@property
|
|
|
|
def radius_y(self) -> float:
|
|
|
|
return self.radii[1]
|
|
|
|
|
|
|
|
@radius_y.setter
|
2022-02-23 11:27:11 -08:00
|
|
|
def radius_y(self, val: float) -> None:
|
2016-03-15 19:12:39 -07:00
|
|
|
if not val >= 0:
|
|
|
|
raise PatternError('Radius must be non-negative')
|
|
|
|
self.radii[1] = val
|
|
|
|
|
|
|
|
# Rotation property
|
|
|
|
@property
|
|
|
|
def rotation(self) -> float:
|
|
|
|
"""
|
|
|
|
Rotation of rx from the x axis. Uses the interval [0, pi) in radians (counterclockwise
|
|
|
|
is positive)
|
|
|
|
|
2020-02-17 21:02:53 -08:00
|
|
|
Returns:
|
|
|
|
counterclockwise rotation in radians
|
2016-03-15 19:12:39 -07:00
|
|
|
"""
|
|
|
|
return self._rotation
|
|
|
|
|
|
|
|
@rotation.setter
|
2022-02-23 11:27:11 -08:00
|
|
|
def rotation(self, val: float) -> None:
|
2016-03-15 19:12:39 -07:00
|
|
|
if not is_scalar(val):
|
|
|
|
raise PatternError('Rotation must be a scalar')
|
|
|
|
self._rotation = val % pi
|
|
|
|
|
2022-02-23 11:27:11 -08:00
|
|
|
def __init__(
|
|
|
|
self,
|
2022-02-23 15:47:38 -08:00
|
|
|
radii: ArrayLike,
|
2022-02-23 11:27:11 -08:00
|
|
|
*,
|
|
|
|
poly_num_points: Optional[int] = DEFAULT_POLY_NUM_POINTS,
|
|
|
|
poly_max_arclen: Optional[float] = None,
|
2022-02-23 15:47:38 -08:00
|
|
|
offset: ArrayLike = (0.0, 0.0),
|
2022-02-23 11:27:11 -08:00
|
|
|
rotation: float = 0,
|
|
|
|
mirrored: Sequence[bool] = (False, False),
|
|
|
|
layer: layer_t = 0,
|
|
|
|
repetition: Optional[Repetition] = None,
|
|
|
|
annotations: Optional[annotations_t] = None,
|
|
|
|
raw: bool = False,
|
|
|
|
) -> None:
|
2020-08-11 01:18:29 -07:00
|
|
|
if raw:
|
2022-02-23 15:47:38 -08:00
|
|
|
assert(isinstance(radii, numpy.ndarray))
|
|
|
|
assert(isinstance(offset, numpy.ndarray))
|
2020-08-11 01:18:29 -07:00
|
|
|
self._radii = radii
|
|
|
|
self._offset = offset
|
|
|
|
self._rotation = rotation
|
|
|
|
self._repetition = repetition
|
2020-09-10 20:06:58 -07:00
|
|
|
self._annotations = annotations if annotations is not None else {}
|
2020-08-11 01:18:29 -07:00
|
|
|
self._layer = layer
|
|
|
|
else:
|
|
|
|
self.radii = radii
|
|
|
|
self.offset = offset
|
|
|
|
self.rotation = rotation
|
|
|
|
self.repetition = repetition
|
2020-09-10 20:06:58 -07:00
|
|
|
self.annotations = annotations if annotations is not None else {}
|
2020-08-11 01:18:29 -07:00
|
|
|
self.layer = layer
|
2019-04-20 14:18:25 -07:00
|
|
|
[self.mirror(a) for a, do in enumerate(mirrored) if do]
|
2016-03-15 19:12:39 -07:00
|
|
|
self.poly_num_points = poly_num_points
|
|
|
|
self.poly_max_arclen = poly_max_arclen
|
|
|
|
|
2023-01-13 20:33:14 -08:00
|
|
|
def __deepcopy__(self, memo: Optional[Dict] = None) -> 'Ellipse':
|
2019-05-15 00:19:37 -07:00
|
|
|
memo = {} if memo is None else memo
|
2022-04-17 19:04:13 -07:00
|
|
|
new = copy.copy(self)
|
2019-05-15 00:19:37 -07:00
|
|
|
new._offset = self._offset.copy()
|
|
|
|
new._radii = self._radii.copy()
|
2020-09-10 20:06:58 -07:00
|
|
|
new._annotations = copy.deepcopy(self._annotations)
|
2019-05-15 00:19:37 -07:00
|
|
|
return new
|
|
|
|
|
2022-02-23 11:27:11 -08:00
|
|
|
def to_polygons(
|
|
|
|
self,
|
|
|
|
poly_num_points: Optional[int] = None,
|
|
|
|
poly_max_arclen: Optional[float] = None,
|
|
|
|
) -> List[Polygon]:
|
2016-03-15 19:12:39 -07:00
|
|
|
if poly_num_points is None:
|
|
|
|
poly_num_points = self.poly_num_points
|
|
|
|
if poly_max_arclen is None:
|
|
|
|
poly_max_arclen = self.poly_max_arclen
|
|
|
|
|
|
|
|
if (poly_num_points is None) and (poly_max_arclen is None):
|
|
|
|
raise PatternError('Number of points and arclength left unspecified'
|
|
|
|
' (default was also overridden)')
|
|
|
|
|
2017-04-19 18:54:58 -07:00
|
|
|
r0, r1 = self.radii
|
2016-03-15 19:12:39 -07:00
|
|
|
|
|
|
|
# Approximate perimeter
|
|
|
|
# Ramanujan, S., "Modular Equations and Approximations to ,"
|
|
|
|
# Quart. J. Pure. Appl. Math., vol. 45 (1913-1914), pp. 350-372
|
2017-04-19 18:54:58 -07:00
|
|
|
h = ((r1 - r0) / (r1 + r0)) ** 2
|
|
|
|
perimeter = pi * (r1 + r0) * (1 + 3 * h / (10 + math.sqrt(4 - 3 * h)))
|
2016-03-15 19:12:39 -07:00
|
|
|
|
|
|
|
n = []
|
|
|
|
if poly_num_points is not None:
|
|
|
|
n += [poly_num_points]
|
|
|
|
if poly_max_arclen is not None:
|
|
|
|
n += [perimeter / poly_max_arclen]
|
2020-07-21 20:38:38 -07:00
|
|
|
num_points = int(round(max(n)))
|
|
|
|
thetas = numpy.linspace(2 * pi, 0, num_points, endpoint=False)
|
2016-03-15 19:12:39 -07:00
|
|
|
|
|
|
|
sin_th, cos_th = (numpy.sin(thetas), numpy.cos(thetas))
|
2017-04-19 18:54:58 -07:00
|
|
|
xs = r0 * cos_th
|
|
|
|
ys = r1 * sin_th
|
2016-03-15 19:12:39 -07:00
|
|
|
xys = numpy.vstack((xs, ys)).T
|
|
|
|
|
2023-01-18 18:14:33 -08:00
|
|
|
poly = Polygon(xys, layer=self.layer, offset=self.offset, rotation=self.rotation)
|
2016-03-15 19:12:39 -07:00
|
|
|
return [poly]
|
|
|
|
|
2022-02-23 15:47:38 -08:00
|
|
|
def get_bounds(self) -> NDArray[numpy.float64]:
|
2016-03-15 19:12:39 -07:00
|
|
|
rot_radii = numpy.dot(rotation_matrix_2d(self.rotation), self.radii)
|
|
|
|
return numpy.vstack((self.offset - rot_radii[0],
|
|
|
|
self.offset + rot_radii[1]))
|
|
|
|
|
|
|
|
def rotate(self, theta: float) -> 'Ellipse':
|
|
|
|
self.rotation += theta
|
|
|
|
return self
|
|
|
|
|
2018-04-14 15:27:56 -07:00
|
|
|
def mirror(self, axis: int) -> 'Ellipse':
|
|
|
|
self.offset[axis - 1] *= -1
|
|
|
|
self.rotation *= -1
|
2020-12-05 14:49:57 -08:00
|
|
|
self.rotation += axis * pi
|
2018-04-14 15:27:56 -07:00
|
|
|
return self
|
|
|
|
|
2016-03-15 19:12:39 -07:00
|
|
|
def scale_by(self, c: float) -> 'Ellipse':
|
|
|
|
self.radii *= c
|
|
|
|
return self
|
|
|
|
|
|
|
|
def normalized_form(self, norm_value: float) -> normalized_shape_tuple:
|
|
|
|
if self.radius_x < self.radius_y:
|
|
|
|
radii = self.radii / self.radius_x
|
|
|
|
scale = self.radius_x
|
|
|
|
angle = self.rotation
|
|
|
|
else:
|
|
|
|
radii = self.radii[::-1] / self.radius_y
|
|
|
|
scale = self.radius_y
|
|
|
|
angle = (self.rotation + pi / 2) % pi
|
2020-10-16 19:00:50 -07:00
|
|
|
return ((type(self), radii, self.layer),
|
2023-01-18 18:14:33 -08:00
|
|
|
(self.offset, scale / norm_value, angle, False),
|
2020-10-16 19:00:50 -07:00
|
|
|
lambda: Ellipse(radii=radii * norm_value, layer=self.layer))
|
2016-03-15 19:12:39 -07:00
|
|
|
|
2020-05-11 20:31:07 -07:00
|
|
|
def __repr__(self) -> str:
|
|
|
|
rotation = f' r{self.rotation*180/pi:g}' if self.rotation != 0 else ''
|
2023-01-18 18:14:33 -08:00
|
|
|
return f'<Ellipse l{self.layer} o{self.offset} r{self.radii}{rotation}>'
|