| 
									
										
										
										
											2020-05-11 19:09:35 -07:00
										 |  |  | from typing import List, Tuple, Dict, Optional, Sequence | 
					
						
							| 
									
										
										
										
											2019-05-17 00:41:43 -07:00
										 |  |  | import copy | 
					
						
							| 
									
										
										
										
											2016-03-15 19:12:39 -07:00
										 |  |  | import math | 
					
						
							|  |  |  | import numpy | 
					
						
							|  |  |  | from numpy import pi | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS | 
					
						
							|  |  |  | from .. import PatternError | 
					
						
							| 
									
										
										
										
											2020-05-11 18:39:02 -07:00
										 |  |  | from ..utils import is_scalar, vector2, layer_t | 
					
						
							| 
									
										
										
										
											2016-03-15 19:12:39 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 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. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     The radii define an ellipse; the ring is formed with radii +/- width/2. | 
					
						
							|  |  |  |     The rotation gives the angle from x-axis, counterclockwise, to the first (x) radius. | 
					
						
							| 
									
										
										
										
											2018-08-30 23:06:45 -07:00
										 |  |  |     The start and stop angle are measured counterclockwise from the first (x) radius. | 
					
						
							| 
									
										
										
										
											2016-03-15 19:12:39 -07:00
										 |  |  |     """
 | 
					
						
							| 
									
										
										
										
											2019-05-17 00:37:56 -07:00
										 |  |  |     __slots__ = ('_radii', '_angles', '_width', '_rotation', | 
					
						
							|  |  |  |                  'poly_num_points', 'poly_max_arclen') | 
					
						
							|  |  |  |     _radii: numpy.ndarray | 
					
						
							| 
									
										
										
										
											2020-02-17 21:02:53 -08:00
										 |  |  |     """ Two radii for defining an ellipse """ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     _rotation: float | 
					
						
							|  |  |  |     """ Rotation (ccw, radians) from the x axis to the first radius """ | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-05-17 00:37:56 -07:00
										 |  |  |     _angles: numpy.ndarray | 
					
						
							| 
									
										
										
										
											2020-02-17 21:02:53 -08:00
										 |  |  |     """ Start and stop angles (ccw, radians) for choosing an arc from the ellipse, measured from the first radius """ | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-05-17 00:37:56 -07:00
										 |  |  |     _width: float | 
					
						
							| 
									
										
										
										
											2020-02-17 21:02:53 -08:00
										 |  |  |     """ Width of the arc """ | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											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 | 
					
						
							|  |  |  |     def radii(self) -> numpy.ndarray: | 
					
						
							|  |  |  |         """
 | 
					
						
							| 
									
										
										
										
											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 | 
					
						
							|  |  |  |     def radii(self, val: vector2): | 
					
						
							|  |  |  |         val = numpy.array(val, dtype=float).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: | 
					
						
							| 
									
										
										
										
											2017-04-19 18:54:58 -07:00
										 |  |  |         return self._radii[0] | 
					
						
							| 
									
										
										
										
											2016-03-15 19:12:39 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |     @radius_x.setter | 
					
						
							|  |  |  |     def radius_x(self, val: float): | 
					
						
							|  |  |  |         if not val >= 0: | 
					
						
							|  |  |  |             raise PatternError('Radius must be non-negative') | 
					
						
							| 
									
										
										
										
											2017-04-19 18:54:58 -07:00
										 |  |  |         self._radii[0] = val | 
					
						
							| 
									
										
										
										
											2016-03-15 19:12:39 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |     @property | 
					
						
							|  |  |  |     def radius_y(self) -> float: | 
					
						
							| 
									
										
										
										
											2017-04-19 18:54:58 -07:00
										 |  |  |         return self._radii[1] | 
					
						
							| 
									
										
										
										
											2016-03-15 19:12:39 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |     @radius_y.setter | 
					
						
							|  |  |  |     def radius_y(self, val: float): | 
					
						
							|  |  |  |         if not val >= 0: | 
					
						
							|  |  |  |             raise PatternError('Radius must be non-negative') | 
					
						
							| 
									
										
										
										
											2017-04-19 18:54:58 -07:00
										 |  |  |         self._radii[1] = val | 
					
						
							| 
									
										
										
										
											2016-03-15 19:12:39 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |     # arc start/stop angle properties | 
					
						
							|  |  |  |     @property | 
					
						
							| 
									
										
										
										
											2020-05-11 19:09:35 -07:00
										 |  |  |     def angles(self) -> numpy.ndarray:          #ndarray[float] | 
					
						
							| 
									
										
										
										
											2016-03-15 19:12:39 -07:00
										 |  |  |         """
 | 
					
						
							| 
									
										
										
										
											2020-02-17 21:02:53 -08:00
										 |  |  |         Return the start and stop angles `[a_start, a_stop]`. | 
					
						
							| 
									
										
										
										
											2017-08-29 15:51:00 -07:00
										 |  |  |         Angles are measured from x-axis after rotation | 
					
						
							| 
									
										
										
										
											2016-03-15 19:12:39 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-02-17 21:02:53 -08:00
										 |  |  |         Returns: | 
					
						
							|  |  |  |             `[a_start, a_stop]` | 
					
						
							| 
									
										
										
										
											2016-03-15 19:12:39 -07:00
										 |  |  |         """
 | 
					
						
							|  |  |  |         return self._angles | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @angles.setter | 
					
						
							|  |  |  |     def angles(self, val: vector2): | 
					
						
							|  |  |  |         val = numpy.array(val, dtype=float).flatten() | 
					
						
							|  |  |  |         if not val.size == 2: | 
					
						
							|  |  |  |             raise PatternError('Angles must have length 2') | 
					
						
							| 
									
										
										
										
											2017-04-19 18:54:58 -07:00
										 |  |  |         self._angles = val | 
					
						
							| 
									
										
										
										
											2016-03-15 19:12:39 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |     @property | 
					
						
							|  |  |  |     def start_angle(self) -> float: | 
					
						
							|  |  |  |         return self.angles[0] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @start_angle.setter | 
					
						
							|  |  |  |     def start_angle(self, val: float): | 
					
						
							| 
									
										
										
										
											2017-04-19 18:54:58 -07:00
										 |  |  |         self.angles = (val, self.angles[1]) | 
					
						
							| 
									
										
										
										
											2016-03-15 19:12:39 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |     @property | 
					
						
							|  |  |  |     def stop_angle(self) -> float: | 
					
						
							|  |  |  |         return self.angles[1] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @stop_angle.setter | 
					
						
							|  |  |  |     def stop_angle(self, val: float): | 
					
						
							| 
									
										
										
										
											2017-04-19 18:54:58 -07:00
										 |  |  |         self.angles = (self.angles[0], val) | 
					
						
							| 
									
										
										
										
											2016-03-15 19:12:39 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |     # Rotation property | 
					
						
							|  |  |  |     @property | 
					
						
							|  |  |  |     def rotation(self) -> float: | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Rotation of radius_x from x_axis, counterclockwise, in radians. Stored mod 2*pi | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-02-17 21:02:53 -08:00
										 |  |  |         Returns: | 
					
						
							|  |  |  |             rotation counterclockwise in radians | 
					
						
							| 
									
										
										
										
											2016-03-15 19:12:39 -07:00
										 |  |  |         """
 | 
					
						
							|  |  |  |         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) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # Width | 
					
						
							|  |  |  |     @property | 
					
						
							|  |  |  |     def width(self) -> float: | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Width of the arc (difference between inner and outer radii) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-02-17 21:02:53 -08:00
										 |  |  |         Returns: | 
					
						
							|  |  |  |             width | 
					
						
							| 
									
										
										
										
											2016-03-15 19:12:39 -07:00
										 |  |  |         """
 | 
					
						
							|  |  |  |         return self._width | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @width.setter | 
					
						
							|  |  |  |     def width(self, val: float): | 
					
						
							|  |  |  |         if not is_scalar(val): | 
					
						
							|  |  |  |             raise PatternError('Width must be a scalar') | 
					
						
							|  |  |  |         if not val > 0: | 
					
						
							|  |  |  |             raise PatternError('Width must be positive') | 
					
						
							|  |  |  |         self._width = val | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def __init__(self, | 
					
						
							|  |  |  |                  radii: vector2, | 
					
						
							|  |  |  |                  angles: vector2, | 
					
						
							| 
									
										
										
										
											2017-04-19 18:54:58 -07:00
										 |  |  |                  width: float, | 
					
						
							| 
									
										
										
										
											2020-05-11 19:09:35 -07:00
										 |  |  |                  poly_num_points: Optional[int] = DEFAULT_POLY_NUM_POINTS, | 
					
						
							|  |  |  |                  poly_max_arclen: Optional[float] = None, | 
					
						
							| 
									
										
										
										
											2019-05-17 00:39:46 -07:00
										 |  |  |                  offset: vector2 = (0.0, 0.0), | 
					
						
							|  |  |  |                  rotation: float = 0, | 
					
						
							| 
									
										
										
										
											2020-05-11 19:09:35 -07:00
										 |  |  |                  mirrored: Sequence[bool] = (False, False), | 
					
						
							| 
									
										
										
										
											2020-05-11 18:39:02 -07:00
										 |  |  |                  layer: layer_t = 0, | 
					
						
							| 
									
										
										
										
											2019-12-12 00:38:11 -08:00
										 |  |  |                  dose: float = 1.0, | 
					
						
							|  |  |  |                  locked: bool = False): | 
					
						
							| 
									
										
										
										
											2020-05-11 19:29:00 -07:00
										 |  |  |         object.__setattr__(self, 'locked', False) | 
					
						
							| 
									
										
										
										
											2019-05-17 00:37:56 -07:00
										 |  |  |         self.identifier = () | 
					
						
							| 
									
										
										
										
											2016-03-15 19:12:39 -07:00
										 |  |  |         self.radii = radii | 
					
						
							|  |  |  |         self.angles = angles | 
					
						
							| 
									
										
										
										
											2017-04-19 18:54:58 -07:00
										 |  |  |         self.width = width | 
					
						
							| 
									
										
										
										
											2019-04-20 14:18:25 -07:00
										 |  |  |         self.offset = offset | 
					
						
							| 
									
										
										
										
											2016-03-15 19:12:39 -07:00
										 |  |  |         self.rotation = rotation | 
					
						
							| 
									
										
										
										
											2019-04-20 14:18:25 -07:00
										 |  |  |         [self.mirror(a) for a, do in enumerate(mirrored) if do] | 
					
						
							|  |  |  |         self.layer = layer | 
					
						
							|  |  |  |         self.dose = dose | 
					
						
							| 
									
										
										
										
											2016-03-15 19:12:39 -07:00
										 |  |  |         self.poly_num_points = poly_num_points | 
					
						
							|  |  |  |         self.poly_max_arclen = poly_max_arclen | 
					
						
							| 
									
										
										
										
											2019-12-12 00:38:11 -08:00
										 |  |  |         self.locked = locked | 
					
						
							| 
									
										
										
										
											2016-03-15 19:12:39 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-05-15 00:19:37 -07:00
										 |  |  |     def  __deepcopy__(self, memo: Dict = None) -> 'Arc': | 
					
						
							|  |  |  |         memo = {} if memo is None else memo | 
					
						
							| 
									
										
										
										
											2019-12-13 21:16:43 -08:00
										 |  |  |         new = copy.copy(self).unlock() | 
					
						
							| 
									
										
										
										
											2019-05-15 00:19:37 -07:00
										 |  |  |         new._offset = self._offset.copy() | 
					
						
							|  |  |  |         new._radii = self._radii.copy() | 
					
						
							|  |  |  |         new._angles = self._angles.copy() | 
					
						
							| 
									
										
										
										
											2019-12-13 21:16:43 -08:00
										 |  |  |         new.locked = self.locked | 
					
						
							| 
									
										
										
										
											2019-05-15 00:19:37 -07:00
										 |  |  |         return new | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-05-17 00:39:46 -07:00
										 |  |  |     def to_polygons(self, | 
					
						
							| 
									
										
										
										
											2020-05-11 19:09:35 -07:00
										 |  |  |                     poly_num_points: Optional[int] = None, | 
					
						
							|  |  |  |                     poly_max_arclen: Optional[float] = None, | 
					
						
							| 
									
										
										
										
											2019-05-17 00:39:46 -07:00
										 |  |  |                     ) -> 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('Max number of points and arclength left unspecified' + | 
					
						
							|  |  |  |                                ' (default was also overridden)') | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-04-19 18:54:58 -07:00
										 |  |  |         r0, r1 = self.radii | 
					
						
							| 
									
										
										
										
											2017-08-29 15:51:00 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |         # Convert from polar angle to ellipse parameter (for [rx*cos(t), ry*sin(t)] representation) | 
					
						
							| 
									
										
										
										
											2018-09-02 21:05:18 -07:00
										 |  |  |         a_ranges = self._angles_to_parameters() | 
					
						
							| 
									
										
										
										
											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 | 
					
						
							| 
									
										
										
										
											2018-09-02 21:05:18 -07:00
										 |  |  |         a0, a1 = a_ranges[1]    # use outer arc | 
					
						
							| 
									
										
										
										
											2017-04-19 18:54:58 -07:00
										 |  |  |         h = ((r1 - r0) / (r1 + r0)) ** 2 | 
					
						
							|  |  |  |         ellipse_perimeter = pi * (r1 + r0) * (1 + 3 * h / (10 + math.sqrt(4 - 3 * h))) | 
					
						
							|  |  |  |         perimeter = abs(a0 - a1) / (2 * pi) * ellipse_perimeter         # TODO: make this more accurate | 
					
						
							| 
									
										
										
										
											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] | 
					
						
							| 
									
										
										
										
											2018-09-02 21:05:18 -07:00
										 |  |  |         thetas_inner = numpy.linspace(a_ranges[0][1], a_ranges[0][0], max(n), endpoint=True) | 
					
						
							|  |  |  |         thetas_outer = numpy.linspace(a_ranges[1][0], a_ranges[1][1], max(n), endpoint=True) | 
					
						
							| 
									
										
										
										
											2016-03-15 19:12:39 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-09-02 21:05:18 -07:00
										 |  |  |         sin_th_i, cos_th_i = (numpy.sin(thetas_inner), numpy.cos(thetas_inner)) | 
					
						
							|  |  |  |         sin_th_o, cos_th_o = (numpy.sin(thetas_outer), numpy.cos(thetas_outer)) | 
					
						
							| 
									
										
										
										
											2016-03-15 19:12:39 -07:00
										 |  |  |         wh = self.width / 2.0 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-09-02 21:05:18 -07:00
										 |  |  |         xs1 = (r0 + wh) * cos_th_o | 
					
						
							|  |  |  |         ys1 = (r1 + wh) * sin_th_o | 
					
						
							|  |  |  |         xs2 = (r0 - wh) * cos_th_i | 
					
						
							|  |  |  |         ys2 = (r1 - wh) * sin_th_i | 
					
						
							| 
									
										
										
										
											2016-03-15 19:12:39 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-09-02 21:05:18 -07:00
										 |  |  |         xs = numpy.hstack((xs1, xs2)) | 
					
						
							|  |  |  |         ys = numpy.hstack((ys1, ys2)) | 
					
						
							| 
									
										
										
										
											2016-03-15 19:12:39 -07:00
										 |  |  |         xys = numpy.vstack((xs, ys)).T | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-04-20 14:18:25 -07:00
										 |  |  |         poly = Polygon(xys, dose=self.dose, layer=self.layer, offset=self.offset, rotation=self.rotation) | 
					
						
							| 
									
										
										
										
											2016-03-15 19:12:39 -07:00
										 |  |  |         return [poly] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def get_bounds(self) -> numpy.ndarray: | 
					
						
							| 
									
										
										
										
											2017-08-29 16:55:06 -07:00
										 |  |  |         '''
 | 
					
						
							|  |  |  |         Equation for rotated ellipse is | 
					
						
							| 
									
										
										
										
											2020-02-17 21:02:53 -08:00
										 |  |  |             `x = x0 + a * cos(t) * cos(rot) - b * sin(t) * sin(phi)` | 
					
						
							|  |  |  |             `y = y0 + a * cos(t) * sin(rot) + b * sin(t) * cos(rot)` | 
					
						
							|  |  |  |           where `t` is our parameter. | 
					
						
							| 
									
										
										
										
											2017-08-29 16:55:06 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-02-17 21:02:53 -08:00
										 |  |  |         Differentiating and solving for 0 slope wrt. `t`, we find | 
					
						
							|  |  |  |             `tan(t) = -+ b/a cot(phi)` | 
					
						
							| 
									
										
										
										
											2017-08-29 16:55:06 -07:00
										 |  |  |           where -+ is for x, y cases, so that's where the extrema are. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         If the extrema are innaccessible due to arc constraints, check the arc endpoints instead. | 
					
						
							|  |  |  |         '''
 | 
					
						
							| 
									
										
										
										
											2018-09-02 21:05:18 -07:00
										 |  |  |         a_ranges = self._angles_to_parameters() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-03-15 19:12:39 -07:00
										 |  |  |         mins = [] | 
					
						
							|  |  |  |         maxs = [] | 
					
						
							| 
									
										
										
										
											2018-09-02 21:05:18 -07:00
										 |  |  |         for a, sgn in zip(a_ranges, (-1, +1)): | 
					
						
							| 
									
										
										
										
											2016-03-15 19:12:39 -07:00
										 |  |  |             wh = sgn * self.width/2 | 
					
						
							|  |  |  |             rx = self.radius_x + wh | 
					
						
							|  |  |  |             ry = self.radius_y + wh | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-09-02 21:05:18 -07:00
										 |  |  |             a0, a1 = a | 
					
						
							| 
									
										
										
										
											2017-08-29 15:51:00 -07:00
										 |  |  |             a0_offset = a0 - (a0 % (2 * pi)) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-03-15 19:12:39 -07:00
										 |  |  |             sin_r = numpy.sin(self.rotation) | 
					
						
							|  |  |  |             cos_r = numpy.cos(self.rotation) | 
					
						
							|  |  |  |             sin_a = numpy.sin(a) | 
					
						
							|  |  |  |             cos_a = numpy.cos(a) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-08-29 15:51:00 -07:00
										 |  |  |             # Cutoff angles | 
					
						
							|  |  |  |             xpt = (-self.rotation) % (2 * pi) + a0_offset | 
					
						
							| 
									
										
										
										
											2018-10-28 13:32:04 -07:00
										 |  |  |             ypt = (pi/2 - self.rotation) % (2 * pi) + a0_offset | 
					
						
							| 
									
										
										
										
											2017-08-29 15:51:00 -07:00
										 |  |  |             xnt = (xpt - pi) % (2 * pi) + a0_offset | 
					
						
							|  |  |  |             ynt = (ypt - pi) % (2 * pi) + a0_offset | 
					
						
							| 
									
										
										
										
											2016-03-15 19:12:39 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-08-29 15:51:00 -07:00
										 |  |  |             # Points along coordinate axes | 
					
						
							| 
									
										
										
										
											2018-10-28 13:32:04 -07:00
										 |  |  |             rx2_inv = 1 / (rx * rx) | 
					
						
							|  |  |  |             ry2_inv = 1 / (ry * ry) | 
					
						
							|  |  |  |             xr = numpy.abs(cos_r * cos_r * rx2_inv + sin_r * sin_r * ry2_inv) ** -0.5 | 
					
						
							|  |  |  |             yr = numpy.abs(-sin_r * -sin_r * rx2_inv + cos_r * cos_r * ry2_inv) ** -0.5 | 
					
						
							| 
									
										
										
										
											2016-03-15 19:12:39 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-08-29 15:51:00 -07:00
										 |  |  |             # Arc endpoints | 
					
						
							| 
									
										
										
										
											2016-03-15 19:12:39 -07:00
										 |  |  |             xn, xp = sorted(rx * cos_r * cos_a - ry * sin_r * sin_a) | 
					
						
							| 
									
										
										
										
											2017-08-29 16:55:06 -07:00
										 |  |  |             yn, yp = sorted(rx * sin_r * cos_a + ry * cos_r * sin_a) | 
					
						
							| 
									
										
										
										
											2016-03-15 19:12:39 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-10-28 13:32:04 -07:00
										 |  |  |             # If our arc subtends a coordinate axis, use the extremum along that axis | 
					
						
							|  |  |  |             if a0 < xpt < a1 or a0 < xpt + 2 * pi < a1: | 
					
						
							| 
									
										
										
										
											2016-03-15 19:12:39 -07:00
										 |  |  |                 xp = xr | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-10-28 13:32:04 -07:00
										 |  |  |             if a0 < xnt < a1 or a0 < xnt + 2 * pi < a1: | 
					
						
							| 
									
										
										
										
											2016-03-15 19:12:39 -07:00
										 |  |  |                 xn = -xr | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-10-28 13:32:04 -07:00
										 |  |  |             if a0 < ypt < a1 or a0 < ypt + 2 * pi < a1: | 
					
						
							| 
									
										
										
										
											2016-03-15 19:12:39 -07:00
										 |  |  |                 yp = yr | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-10-28 13:32:04 -07:00
										 |  |  |             if a0 < ynt < a1 or a0 < ynt + 2 * pi < a1: | 
					
						
							| 
									
										
										
										
											2016-03-15 19:12:39 -07:00
										 |  |  |                 yn = -yr | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             mins.append([xn, yn]) | 
					
						
							|  |  |  |             maxs.append([xp, yp]) | 
					
						
							|  |  |  |         return numpy.vstack((numpy.min(mins, axis=0) + self.offset, | 
					
						
							|  |  |  |                              numpy.max(maxs, axis=0) + self.offset)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def rotate(self, theta: float) -> 'Arc': | 
					
						
							|  |  |  |         self.rotation += theta | 
					
						
							|  |  |  |         return self | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-04-14 15:27:56 -07:00
										 |  |  |     def mirror(self, axis: int) -> 'Arc': | 
					
						
							|  |  |  |         self.offset[axis - 1] *= -1 | 
					
						
							|  |  |  |         self.rotation *= -1 | 
					
						
							|  |  |  |         self.angles *= -1 | 
					
						
							|  |  |  |         return self | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-03-15 19:12:39 -07:00
										 |  |  |     def scale_by(self, c: float) -> 'Arc': | 
					
						
							|  |  |  |         self.radii *= c | 
					
						
							|  |  |  |         self.width *= 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 | 
					
						
							|  |  |  |             rotation = self.rotation | 
					
						
							|  |  |  |             angles = self.angles | 
					
						
							|  |  |  |         else:  # rotate by 90 degrees and swap radii | 
					
						
							|  |  |  |             radii = self.radii[::-1] / self.radius_y | 
					
						
							|  |  |  |             scale = self.radius_y | 
					
						
							|  |  |  |             rotation = self.rotation + pi / 2 | 
					
						
							|  |  |  |             angles = self.angles - pi / 2 | 
					
						
							| 
									
										
										
										
											2017-04-19 18:54:58 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-04-20 13:01:31 -07:00
										 |  |  |         delta_angle = angles[1] - angles[0] | 
					
						
							|  |  |  |         start_angle = angles[0] % (2 * pi) | 
					
						
							|  |  |  |         if start_angle >= pi: | 
					
						
							|  |  |  |             start_angle -= pi | 
					
						
							| 
									
										
										
										
											2017-04-19 18:54:58 -07:00
										 |  |  |             rotation += pi | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-04-20 13:05:58 -07:00
										 |  |  |         angles = (start_angle, start_angle + delta_angle) | 
					
						
							| 
									
										
										
										
											2017-04-19 18:54:58 -07:00
										 |  |  |         rotation %= 2 * pi | 
					
						
							|  |  |  |         width = self.width | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-04-20 14:18:52 -07:00
										 |  |  |         return (type(self), radii, angles, width/norm_value, self.layer), \ | 
					
						
							| 
									
										
										
										
											2019-05-17 00:41:26 -07:00
										 |  |  |                (self.offset, scale/norm_value, rotation, False, self.dose), \ | 
					
						
							| 
									
										
										
										
											2019-04-20 14:18:52 -07:00
										 |  |  |                lambda: Arc(radii=radii*norm_value, angles=angles, width=width*norm_value, layer=self.layer) | 
					
						
							| 
									
										
										
										
											2018-09-02 21:05:18 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-09-02 22:40:20 -07:00
										 |  |  |     def get_cap_edges(self) -> numpy.ndarray: | 
					
						
							|  |  |  |         '''
 | 
					
						
							| 
									
										
										
										
											2020-02-17 21:02:53 -08:00
										 |  |  |         Returns: | 
					
						
							|  |  |  |             ``` | 
					
						
							|  |  |  |             [[[x0, y0], [x1, y1]],   array of 4 points, specifying the two cuts which | 
					
						
							|  |  |  |              [[x2, y2], [x3, y3]]],    would create this arc from its corresponding ellipse. | 
					
						
							|  |  |  |             ``` | 
					
						
							| 
									
										
										
										
											2018-09-02 22:40:20 -07:00
										 |  |  |         '''
 | 
					
						
							|  |  |  |         a_ranges = self._angles_to_parameters() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         mins = [] | 
					
						
							|  |  |  |         maxs = [] | 
					
						
							|  |  |  |         for a, sgn in zip(a_ranges, (-1, +1)): | 
					
						
							|  |  |  |             wh = sgn * self.width/2 | 
					
						
							|  |  |  |             rx = self.radius_x + wh | 
					
						
							|  |  |  |             ry = self.radius_y + wh | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             sin_r = numpy.sin(self.rotation) | 
					
						
							|  |  |  |             cos_r = numpy.cos(self.rotation) | 
					
						
							|  |  |  |             sin_a = numpy.sin(a) | 
					
						
							|  |  |  |             cos_a = numpy.cos(a) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             # arc endpoints | 
					
						
							|  |  |  |             xn, xp = sorted(rx * cos_r * cos_a - ry * sin_r * sin_a) | 
					
						
							|  |  |  |             yn, yp = sorted(rx * sin_r * cos_a + ry * cos_r * sin_a) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             mins.append([xn, yn]) | 
					
						
							|  |  |  |             maxs.append([xp, yp]) | 
					
						
							|  |  |  |         return  numpy.array([mins, maxs]) + self.offset | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-09-02 21:05:18 -07:00
										 |  |  |     def _angles_to_parameters(self) -> numpy.ndarray: | 
					
						
							|  |  |  |         '''
 | 
					
						
							| 
									
										
										
										
											2020-02-17 21:02:53 -08:00
										 |  |  |         Returns: | 
					
						
							|  |  |  |             "Eccentric anomaly" parameter ranges for the inner and outer edges, in the form | 
					
						
							|  |  |  |                    `[[a_min_inner, a_max_inner], [a_min_outer, a_max_outer]]` | 
					
						
							| 
									
										
										
										
											2018-09-02 21:05:18 -07:00
										 |  |  |         '''
 | 
					
						
							|  |  |  |         a = [] | 
					
						
							|  |  |  |         for sgn in (-1, +1): | 
					
						
							|  |  |  |             wh = sgn * self.width/2 | 
					
						
							|  |  |  |             rx = self.radius_x + wh | 
					
						
							|  |  |  |             ry = self.radius_y + wh | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             # create paremeter 'a' for parametrized ellipse | 
					
						
							|  |  |  |             a0, a1 = (numpy.arctan2(rx*numpy.sin(a), ry*numpy.cos(a)) for a in self.angles) | 
					
						
							|  |  |  |             sign = numpy.sign(self.angles[1] - self.angles[0]) | 
					
						
							|  |  |  |             if sign != numpy.sign(a1 - a0): | 
					
						
							|  |  |  |                 a1 += sign * 2 * pi | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             a.append((a0, a1)) | 
					
						
							|  |  |  |         return numpy.array(a) | 
					
						
							| 
									
										
										
										
											2020-05-11 19:29:00 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def lock(self) -> 'Arc': | 
					
						
							|  |  |  |         self.radii.flags.writeable = False | 
					
						
							|  |  |  |         self.angles.flags.writeable = False | 
					
						
							|  |  |  |         Shape.lock(self) | 
					
						
							|  |  |  |         return self | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def unlock(self) -> 'Arc': | 
					
						
							|  |  |  |         Shape.unlock(self) | 
					
						
							|  |  |  |         self.radii.flags.writeable = True | 
					
						
							|  |  |  |         self.angles.flags.writeable = True | 
					
						
							|  |  |  |         return self | 
					
						
							| 
									
										
										
										
											2020-05-11 20:31:07 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def __repr__(self) -> str: | 
					
						
							|  |  |  |         angles = f' a°{self.angles*180/pi}' | 
					
						
							|  |  |  |         rotation = f' r°{self.rotation*180/pi:g}' if self.rotation != 0 else '' | 
					
						
							|  |  |  |         dose = f' d{self.dose:g}' if self.dose != 1 else '' | 
					
						
							|  |  |  |         locked = ' L' if self.locked else '' | 
					
						
							|  |  |  |         return f'<Arc l{self.layer} o{self.offset} r{self.radii}{angles} w{self.width:g}{rotation}{dose}{locked}>' |