geometry.py
1 # SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-or-later OR CERN-OHL-S-2.0+ OR Apache-2.0 2 """The pdkmaster.design.geometry module provides classes to represent shapes drawn in 3 a DesignMask of a technology. 4 5 Attributes: 6 epsilon: value under which two coordinate values are considered equal. 7 Default is 1e-6; as coordinates are assumed to be in µm this 8 corresponds with 1 fm. 9 origin: (0.0, 0.0) 10 """ 11 import abc, enum 12 from itertools import product 13 from math import floor 14 from typing import ( 15 Any, Dict, Iterable, Iterator, Collection, Tuple, List, 16 Optional, Union, TypeVar, cast, overload 17 ) 18 19 from .. import _util 20 from ..typing import MultiT, cast_MultiT, OptMultiT, cast_OptMultiT 21 from . import property_ as _prp, mask as _msk 22 23 24 __all__ = [ 25 "epsilon", 26 "Rotation", "FloatPoint", 27 "RotationContext", "MoveContext", 28 "ShapeT", "RectangularT", "PointsShapeT", 29 "Point", "origin", "Line", "Polygon", "Rect", "MultiPath", "Ring", "RectRing", 30 "MultiPartShape", "MultiShape", 31 "RepeatedShape", "ArrayShape", 32 "MaskShape", "MaskShapes", 33 "Start", "SetWidth", "GoLeft", "GoDown", "GoRight", "GoUp", "Knot", "NoStart", 34 ] 35 36 37 epsilon: float = 1e-6 38 def _eq(v1: float, v2: float): 39 """Compare if two floats have a difference smaller than epsilon 40 41 API Notes: 42 This function may only be used inside this module 43 """ 44 return (abs(v1 - v2) < epsilon) 45 46 47 _shape_childclass = TypeVar("_shape_childclass", bound="_Shape") 48 49 50 class Rotation(enum.Enum): 51 """Enum type to represent supported `_Shape` rotations 52 """ 53 No = "no" 54 R0 = "no" # alias 55 R90 = "90" 56 R180 = "180" 57 R270 = "270" 58 MX = "mirrorx" 59 MX90 = "mirrorx&90" 60 MY = "mirrory" 61 MY90 = "mirrory&90" 62 63 @staticmethod 64 def from_name(rot: str) -> "Rotation": 65 """Helper function to convert a rotation string representation to 66 a `Rotation` value. 67 68 Arguments: 69 rot: string r of the rotation; supported values: 70 ("no", "90", "180", "270", "mirrorx", "mirrorx&90", "mirrory", 71 "mirrory&90") 72 73 Returns: 74 Corresponding `Rotation` value 75 """ 76 lookup = { 77 "no": Rotation.No, 78 "90": Rotation.R90, 79 "180": Rotation.R180, 80 "270": Rotation.R270, 81 "mirrorx": Rotation.MX, 82 "mirrorx&90": Rotation.MX90, 83 "mirrory": Rotation.MY, 84 "mirrory&90": Rotation.MY90, 85 } 86 assert rot in lookup 87 return lookup[rot] 88 89 @overload 90 def __mul__(self, shape: "Rotation") -> "Rotation": 91 ... # pragma: no cover 92 @overload 93 def __mul__(self, shape: _shape_childclass) -> _shape_childclass: 94 ... # pragma: no cover 95 @overload 96 def __mul__(self, shape: "MaskShape") -> "MaskShape": 97 ... # pragma: no cover 98 @overload 99 def __mul__(self, shape: "MaskShapes") -> "MaskShapes": 100 ... # pragma: no cover 101 def __mul__(self, shape) -> Union["Rotation", "ShapeT", "MaskShape", "MaskShapes"]: 102 if isinstance(shape, Rotation): 103 lookup: Dict["Rotation", Dict["Rotation", "Rotation"]] = { 104 Rotation.R0: { 105 Rotation.R0: Rotation.R0, 106 Rotation.R90: Rotation.R90, 107 Rotation.R180: Rotation.R180, 108 Rotation.R270: Rotation.R270, 109 Rotation.MX: Rotation.MX, 110 Rotation.MX90: Rotation.MX90, 111 Rotation.MY: Rotation.MY, 112 Rotation.MY90: Rotation.MY90, 113 }, 114 Rotation.R90: { 115 Rotation.R0: Rotation.R90, 116 Rotation.R90: Rotation.R180, 117 Rotation.R180: Rotation.R270, 118 Rotation.R270: Rotation.R0, 119 Rotation.MX: Rotation.MY90, 120 Rotation.MX90: Rotation.R270, 121 Rotation.MY: Rotation.MX90, 122 Rotation.MY90: Rotation.MX, 123 }, 124 Rotation.R180: { 125 Rotation.R0: Rotation.R180, 126 Rotation.R90: Rotation.R270, 127 Rotation.R180: Rotation.R0, 128 Rotation.R270: Rotation.R90, 129 Rotation.MX: Rotation.MY, 130 Rotation.MX90: Rotation.MY90, 131 Rotation.MY: Rotation.MX, 132 Rotation.MY90: Rotation.MX90, 133 }, 134 Rotation.R270: { 135 Rotation.R0: Rotation.R270, 136 Rotation.R90: Rotation.R0, 137 Rotation.R180: Rotation.R90, 138 Rotation.R270: Rotation.R180, 139 Rotation.MX: Rotation.MY90, 140 Rotation.MX90: Rotation.MX, 141 Rotation.MY: Rotation.MX90, 142 Rotation.MY90: Rotation.MY, 143 }, 144 Rotation.MX: { 145 Rotation.R0: Rotation.MX, 146 Rotation.R90: Rotation.MX90, 147 Rotation.R180: Rotation.MY, 148 Rotation.R270: Rotation.MY90, 149 Rotation.MX: Rotation.R0, 150 Rotation.MX90: Rotation.R90, 151 Rotation.MY: Rotation.R180, 152 Rotation.MY90: Rotation.R270, 153 }, 154 Rotation.MX90: { 155 Rotation.R0: Rotation.MX90, 156 Rotation.R90: Rotation.MY, 157 Rotation.R180: Rotation.MY90, 158 Rotation.R270: Rotation.MX, 159 Rotation.MX: Rotation.R270, 160 Rotation.MX90: Rotation.R0, 161 Rotation.MY: Rotation.R90, 162 Rotation.MY90: Rotation.R180, 163 }, 164 Rotation.MY: { 165 Rotation.R0: Rotation.MY, 166 Rotation.R90: Rotation.MY90, 167 Rotation.R180: Rotation.MX, 168 Rotation.R270: Rotation.MX90, 169 Rotation.MX: Rotation.R180, 170 Rotation.MX90: Rotation.R270, 171 Rotation.MY: Rotation.R0, 172 Rotation.MY90: Rotation.R90, 173 }, 174 Rotation.MY90: { 175 Rotation.R0: Rotation.MY90, 176 Rotation.R90: Rotation.MX, 177 Rotation.R180: Rotation.MX90, 178 Rotation.R270: Rotation.MY, 179 Rotation.MX: Rotation.R90, 180 Rotation.MX90: Rotation.R180, 181 Rotation.MY: Rotation.R270, 182 Rotation.MY90: Rotation.R0, 183 }, 184 } 185 186 return lookup[self][shape] 187 elif isinstance(shape, (_Shape, MaskShape, MaskShapes)): 188 if self == Rotation.R0: 189 return shape 190 else: 191 return shape.rotated(rotation=self) 192 else: 193 raise TypeError( 194 "unsupported operand type(s) for *: " 195 f"'{self.__class__.__name__}' and '{shape.__class__.__name__}'" 196 ) 197 __rmul__ = __mul__ 198 199 200 class RotationContext: 201 """Context for rotate operations that are considered to belong together. 202 203 Currently it will cache rotated MultiPartShape and link part to the rotated parts. 204 205 API Notes: 206 * API of `RotationContext` is not fixed yet. No backwards compatible guarantees 207 are given. User code using the class may need to be adapted in the future. 208 see [#76](https://gitlab.com/Chips4Makers/PDKMaster/-/issues/76) 209 """ 210 def __init__(self): 211 self._rotation: Optional[Rotation] = None 212 self._mps_cache: Dict["MultiPartShape", "MultiPartShape"] = {} 213 214 def _rotate_part(self, *, 215 part: "MultiPartShape._Part", rotation: Rotation, 216 ) -> "MultiPartShape._Part": 217 if self._rotation is None: 218 self._rotation = rotation 219 else: 220 assert self._rotation == rotation 221 222 mps = part.multipartshape 223 idx = mps.parts.index(part) 224 if mps in self._mps_cache: 225 mps2 = self._mps_cache[mps] 226 else: 227 mps2 = mps.rotated(rotation=rotation) 228 self._mps_cache[mps] = mps2 229 return mps2.parts[idx] 230 231 232 class MoveContext: 233 """Context for move operations that are considered to be part of one move. 234 235 Currently it will cache moved MultiPartShape and link part to the moved parts. 236 237 API Notes: 238 * API of `MoveContext` is not fixed yet. No backwards compatible guarantees 239 are given. User code using the class may need to be adapted in the future. 240 see [#76](https://gitlab.com/Chips4Makers/PDKMaster/-/issues/76) 241 """ 242 def __init__(self): 243 self._dxy: Optional["Point"] = None 244 self._mps_cache: Dict["MultiPartShape", "MultiPartShape"] = {} 245 246 def _move_part(self, *, part: "MultiPartShape._Part", dxy: "Point") -> "MultiPartShape._Part": 247 if self._dxy is None: 248 self._dxy = dxy 249 else: 250 assert self._dxy == dxy 251 mps = part.multipartshape 252 idx = mps.parts.index(part) 253 try: 254 mps2 = self._mps_cache[mps] 255 except KeyError: 256 mps2 = mps.moved(dxy=dxy) 257 self._mps_cache[mps] = mps2 258 return mps2.parts[idx] 259 260 261 class _Shape(abc.ABC): 262 """The base class for representing shapes 263 264 API Notes: 265 * _Shape objects need to be immutable objects. They need to implement 266 __hash__() and __eq__() 267 """ 268 @abc.abstractmethod 269 def __init__(self): 270 pass 271 272 @property 273 @abc.abstractmethod 274 def pointsshapes(self) -> Iterable["PointsShapeT"]: 275 raise NotImplementedError 276 277 @property 278 @abc.abstractmethod 279 def bounds(self) -> "RectangularT": 280 raise NotImplementedError 281 282 @abc.abstractmethod 283 def moved( 284 self: "_shape_childclass", *, 285 dxy: "Point", context: Optional[MoveContext]=None 286 ) -> "_shape_childclass": 287 """Move a _Shape object by a given vector 288 289 This method is called moved() to represent the fact the _Shape objects are 290 immutable and a new object is created by the moved() method. 291 """ 292 raise NotImplementedError 293 294 def repeat(self, *, 295 offset0: "Point", 296 n: int, n_dxy: "Point", m: int=1, m_dxy: Optional["Point"]=None, 297 ) -> "RepeatedShape": 298 return RepeatedShape( 299 shape=self, offset0=offset0, 300 n=n, n_dxy=n_dxy, m=m, m_dxy=m_dxy, 301 ) 302 303 @abc.abstractmethod 304 def rotated( 305 self: "_shape_childclass", *, 306 rotation: Rotation, context: Optional[RotationContext]=None, 307 ) -> "_shape_childclass": 308 """Rotate a _Shape object by a given vector 309 310 This method is called rotated() to represent the fact the _Shape objects are 311 immutable and a new object is created by the rotated() method. 312 """ 313 raise NotImplementedError 314 315 @property 316 @abc.abstractmethod 317 def area(self) -> float: 318 raise NotImplementedError 319 320 @abc.abstractmethod 321 def __eq__(self, o: object) -> bool: 322 raise NotImplementedError 323 324 @abc.abstractmethod 325 def __hash__(self) -> int: 326 raise NotImplementedError 327 ShapeT = _Shape 328 329 330 class _Rectangular(_Shape): 331 """Mixin base class rectangular shapes 332 333 API Notes: 334 * This is private class for this module and is not exported by default. 335 It should only be used as mixing inside this module. 336 """ 337 @property 338 @abc.abstractmethod 339 def left(self) -> float: 340 raise NotImplementedError 341 @property 342 @abc.abstractmethod 343 def bottom(self) -> float: 344 raise NotImplementedError 345 @property 346 @abc.abstractmethod 347 def right(self) -> float: 348 raise NotImplementedError 349 @property 350 @abc.abstractmethod 351 def top(self) -> float: 352 raise NotImplementedError 353 354 # Computed properties 355 @property 356 def width(self) -> float: 357 return self.right - self.left 358 @property 359 def height(self) -> float: 360 return self.top - self.bottom 361 @property 362 def center(self) -> "Point": 363 return Point( 364 x=0.5*(self.left + self.right), 365 y=0.5*(self.bottom + self.top), 366 ) 367 RectangularT = _Rectangular 368 369 370 class _PointsShape(_Shape): 371 """base class for single shape that can be described 372 as a list of points 373 374 API Notes: 375 * This is private class for this module and is not exported by default. 376 It should only be used as mixing inside this module. 377 """ 378 @property 379 @abc.abstractmethod 380 def points(self) -> Iterable["Point"]: 381 raise NotImplementedError 382 383 def __eq__(self, o: object) -> bool: 384 if not isinstance(o, _PointsShape): 385 return False 386 p_it1 = iter(self.points) 387 p_it2 = iter(o.points) 388 while True: 389 try: 390 p1 = next(p_it1) 391 except StopIteration: 392 try: 393 p2 = next(p_it2) 394 except StopIteration: 395 # All points the same 396 return True 397 else: 398 return False 399 else: 400 try: 401 p2 = next(p_it2) 402 except StopIteration: 403 # Different number of points 404 return False 405 if p1 != p2: 406 # Non-equal point 407 return False 408 409 def __hash__(self) -> int: 410 return hash(tuple(self.points)) 411 PointsShapeT = _PointsShape 412 413 414 FloatPoint = Union[Tuple[float, float], List[float]] 415 class Point(_PointsShape, _Rectangular): 416 """A point object 417 418 Arguments: 419 x: X-coordinate 420 y: Y-coordinate 421 422 API Notes: 423 * Point objects are immutable, x and y coordinates may not be changed 424 after object creation. 425 * Point is a final class, no backwards compatibility is guaranteed for 426 subclassing this class. 427 """ 428 def __init__(self, *, x: float, y: float): 429 self._x = x 430 self._y = y 431 432 @staticmethod 433 def from_float(*, point: FloatPoint) -> "Point": 434 assert len(point) == 2 435 return Point(x=point[0], y=point[1]) 436 437 @staticmethod 438 def from_point( 439 *, point: "Point", x: Optional[float]=None, y: Optional[float]=None, 440 ) -> "Point": 441 if x is None: 442 x = point.x 443 if y is None: 444 y = point.y 445 return Point(x=x, y=y) 446 447 @property 448 def x(self) -> float: 449 """X-coordinate""" 450 return self._x 451 @property 452 def y(self) -> float: 453 """Y-coordinate""" 454 return self._y 455 456 # _Shape base class abstract methods 457 @property 458 def pointsshapes(self) -> Iterable[PointsShapeT]: 459 return (self,) 460 @property 461 def bounds(self) -> RectangularT: 462 return self 463 464 def moved(self, *, dxy: "Point", context: Optional[MoveContext]=None) -> "Point": 465 x = self.x + dxy.x 466 y = self.y + dxy.y 467 468 return Point(x=x, y=y) 469 470 def rotated(self, *, rotation: Rotation, context: Optional[RotationContext]=None) -> "Point": 471 x = self.x 472 y = self.y 473 tx, ty = { 474 Rotation.No: (x, y), 475 Rotation.R90: (-y, x), 476 Rotation.R180: (-x, -y), 477 Rotation.R270: (y, -x), 478 Rotation.MX: (x, -y), 479 Rotation.MX90: (y, x), 480 Rotation.MY: (-x, y), 481 Rotation.MY90: (-y, -x), 482 }[rotation] 483 484 return Point(x=tx, y=ty) 485 486 # _PointsShape base class abstract methods 487 @property 488 def points(self) -> Iterable["Point"]: 489 return (self,) 490 491 # _Rectangular mixin abstract methods 492 @property 493 def left(self) -> float: 494 return self._x 495 @property 496 def bottom(self) -> float: 497 return self._y 498 @property 499 def right(self) -> float: 500 return self._x 501 @property 502 def top(self) -> float: 503 return self._y 504 505 def __neg__(self) -> "Point": 506 return Point(x=-self.x, y=-self.y) 507 508 @property 509 def area(self): 510 return 0.0 511 512 def __eq__(self, o: object) -> bool: 513 if not isinstance(o, Point): 514 return False 515 else: 516 return _eq(self.x, o.x) and _eq(self.y, o.y) 517 518 def __hash__(self) -> int: 519 return hash((self.x, self.y)) 520 521 @overload 522 def __add__(self, shape: _shape_childclass) -> _shape_childclass: 523 ... # pragma: no cover 524 @overload 525 def __add__(self, shape: "MaskShape") -> "MaskShape": 526 ... # pragma: no cover 527 @overload 528 def __add__(self, shape: "MaskShapes") -> "MaskShapes": 529 ... # pragma: no cover 530 def __add__(self, shape) -> Union[_Shape, "MaskShape", "MaskShapes"]: 531 """The + operation with a Point. 532 533 The + operation on a (mask)shape will move that shape with the given 534 point as vector. 535 536 Returns 537 Shape shifted by the point as vector 538 """ 539 if isinstance(shape, (_Shape, MaskShape, MaskShapes)): 540 return shape.moved(dxy=self) 541 else: 542 raise TypeError( 543 "unsupported operand type(s) for +: " 544 f"'{self.__class__.__name__}' and '{shape.__class__.__name__}'" 545 ) 546 __radd__ = __add__ 547 548 @overload 549 def __rsub__(self, shape: _shape_childclass) -> _shape_childclass: 550 ... # pragma: no cover 551 @overload 552 def __rsub__(self, shape: "MaskShape") -> "MaskShape": 553 ... # pragma: no cover 554 @overload 555 def __rsub__(self, shape: "MaskShapes") -> "MaskShapes": 556 ... # pragma: no cover 557 def __rsub__(self, shape) -> Union[_Shape, "MaskShape", "MaskShapes"]: 558 """Operation shape - `Point` 559 560 Returns 561 Shape shifted by the negative of the point as vector 562 """ 563 if isinstance(shape, (_Shape, MaskShape, MaskShapes)): 564 return shape.moved(dxy=-self) 565 else: 566 raise TypeError( 567 "unsupported operand type(s) for -: " 568 f"'{shape.__class__.__name__}' and '{self.__class__.__name__}'" 569 ) 570 571 # Point - Point is not handled by __rsub__ 572 def __sub__(self, point: "Point") -> "Point": 573 if isinstance(point, Point): 574 return self.moved(dxy=-point) 575 else: 576 raise TypeError( 577 "unsupported operand type(s) for -: " 578 f"'{self.__class__.__name__}' and '{point.__class__.__name__}'" 579 ) 580 581 def __mul__(self, m: Union[float, Rotation]) -> "Point": 582 if isinstance(m, (int, float)): 583 return Point(x=m*self.x, y=m*self.y) 584 elif isinstance(m, Rotation): 585 return self.rotated(rotation=m) 586 else: 587 raise TypeError( 588 f"unsupported operand type(s) for *: " 589 f"'{self.__class__.__name__}' and '{m.__class__.__name__}'" 590 ) 591 __rmul__ = __mul__ 592 593 def __str__(self) -> str: 594 return f"({self.x:.6},{self.y:.6})" 595 596 def __repr__(self) -> str: 597 return f"Point(x={self.x:.6},y={self.y:.6})" 598 599 600 origin: Point = Point(x=0.0, y=0.0) 601 602 603 class Line(_PointsShape, _Rectangular): 604 """A line shape 605 606 A line consist of a start point and an end point. It is considered 607 to be directional so two lines with start en and point exchanged 608 are not considered equal. 609 """ 610 def __init__(self, *, point1: Point, point2: Point): 611 self._point1 = point1 612 self._point2 = point2 613 614 @property 615 def point1(self) -> Point: 616 return self._point1 617 @property 618 def point2(self) -> Point: 619 return self._point2 620 621 # _Shape base class abstraxt methods 622 @property 623 def pointsshapes(self) -> Iterable[PointsShapeT]: 624 return (self,) 625 @property 626 def bounds(self) -> RectangularT: 627 return self 628 629 def moved(self, *, dxy: Point, context: Optional[MoveContext]=None) -> "Line": 630 return Line( 631 point1=self._point1.moved(dxy=dxy, context=context), 632 point2=self._point2.moved(dxy=dxy, context=context), 633 ) 634 635 def rotated(self, *, rotation: Rotation, context: Optional[RotationContext]=None) -> "Line": 636 return Line( 637 point1=self.point1.rotated(rotation=rotation, context=context), 638 point2=self.point2.rotated(rotation=rotation, context=context), 639 ) 640 641 # _PointsShape mixin abstract methods 642 @property 643 def points(self) -> Iterable[Point]: 644 return (self._point1, self._point2) 645 646 # _Rectangular mixin abstract methods 647 @property 648 def left(self) -> float: 649 return min(self._point1.left, self._point2.left) 650 @property 651 def bottom(self) -> float: 652 return min(self._point1.bottom, self._point2.bottom) 653 @property 654 def right(self) -> float: 655 return max(self._point1.right, self._point2.right) 656 @property 657 def top(self) -> float: 658 return max(self._point1.top, self._point2.top) 659 660 @property 661 def area(self): 662 return 0.0 663 664 def __str__(self) -> str: 665 return f"{self.point1}-{self.point2}" 666 667 def __repr__(self) -> str: 668 return f"Line(point1={self.point1!r},point2={self.point2!r})" 669 670 671 class Polygon(_PointsShape): 672 def __init__(self, *, points: Iterable["Point"]): 673 self._points = points = tuple(points) 674 if points[0] != points[-1]: 675 raise ValueError("Last point has to be the same as the first point") 676 677 left = min(point.x for point in points) 678 bottom = min(point.y for point in points) 679 right = max(point.x for point in points) 680 top = max(point.y for point in points) 681 if _eq(left, right) or _eq(bottom, top): 682 raise ValueError("Polygon with only colinear points not allowed") 683 self._bounds: Rect = Rect(left=left, bottom=bottom, right=right, top=top) 684 685 @classmethod 686 def from_floats( 687 cls, *, points: Iterable[FloatPoint], 688 ) -> "Polygon": 689 """ 690 API Notes: 691 * This method is only meant to be called as Outline.from_floats 692 not as obj.__class__.from_floats(). This means that subclasses 693 may overload this method with incompatible call signature. 694 """ 695 return cls(points=(Point(x=x, y=y) for x, y in points)) 696 697 # _Shape base class abstraxt methods 698 @property 699 def pointsshapes(self) -> Iterable[PointsShapeT]: 700 yield self 701 @property 702 def bounds(self) -> RectangularT: 703 return self._bounds 704 705 def moved(self, *, dxy: Point, context: Optional[MoveContext]=None) -> "Polygon": 706 return Polygon(points=(point + dxy for point in self.points)) 707 708 def rotated(self, *, 709 rotation: Rotation, context: Optional[RotationContext]=None, 710 ) -> "Polygon": 711 return Polygon(points=( 712 point.rotated(rotation=rotation, context=context) 713 for point in self.points 714 )) 715 716 # _PointsShape mixin abstract methods 717 @property 718 def points(self) -> Iterable[Point]: 719 return self._points 720 721 @property 722 def area(self) -> float: 723 raise NotImplementedError 724 725 def __str__(self) -> str: 726 s = "=".join(f"{str(p)}" for p in self.points) 727 return f"{{{s}}}" 728 729 def __repr__(self) -> str: 730 s = ",".join(f"{repr(p)}" for p in self.points) 731 return f"Polygon(points=({s}))" 732 733 734 class Rect(Polygon, _Rectangular): 735 """A rectangular shape object 736 737 Arguments: 738 left, bottom, right, top: 739 Edge coordinates of the rectangle; left, bottom have to be smaller 740 than resp. right, top. 741 742 API Notes: 743 * Rect objects are immutable, dimensions may not be changed after creation. 744 * This class is final. No backwards guarantess given for subclasses in 745 user code 746 """ 747 def __init__(self, *, left: float, bottom: float, right: float, top: float): 748 assert (left < right) and (bottom < top) 749 750 self._left = left 751 self._bottom = bottom 752 self._right = right 753 self._top = top 754 755 @staticmethod 756 # type: ignore[override] 757 def from_floats(*, values: Tuple[float, float, float, float]) -> "Rect": 758 left, bottom, right, top = values 759 return Rect(left=left, bottom=bottom, right=right, top=top) 760 761 @staticmethod 762 def from_rect( 763 *, rect: "_Rectangular", 764 left: Optional[float]=None, bottom: Optional[float]=None, 765 right: Optional[float]=None, top: Optional[float]=None, 766 bias: Union[float, _prp.Enclosure]=0.0, 767 ) -> "Rect": 768 if not isinstance(bias, _prp.Enclosure): 769 bias = _prp.Enclosure(bias) 770 hbias = bias.first 771 vbias = bias.second 772 if left is None: 773 left = rect.left 774 left -= hbias 775 if bottom is None: 776 bottom = rect.bottom 777 bottom -= vbias 778 if right is None: 779 right = rect.right 780 right += hbias 781 if top is None: 782 top = rect.top 783 top += vbias 784 return Rect(left=left, bottom=bottom, right=right, top=top) 785 786 @staticmethod 787 def from_corners(*, corner1: Point, corner2: Point) -> "Rect": 788 left = min(corner1.x, corner2.x) 789 bottom = min(corner1.y, corner2.y) 790 right = max(corner1.x, corner2.x) 791 top = max(corner1.y, corner2.y) 792 793 return Rect(left=left, bottom=bottom, right=right, top=top) 794 795 @staticmethod 796 def from_float_corners(*, corners: Tuple[FloatPoint, FloatPoint]) -> "Rect": 797 return Rect.from_corners( 798 corner1=Point.from_float(point=corners[0]), 799 corner2=Point.from_float(point=corners[1]), 800 ) 801 802 @staticmethod 803 def from_size( 804 *, center: Point=Point(x=0, y=0), width: float, height: float, 805 ) -> "Rect": 806 assert (width > 0) and (height > 0) 807 x = center.x 808 y = center.y 809 left = x - 0.5*width 810 bottom = y - 0.5*height 811 right = x + 0.5*width 812 top = y + 0.5*height 813 814 return Rect(left=left, bottom=bottom, right=right, top=top) 815 816 @property 817 def left(self) -> float: 818 return self._left 819 @property 820 def bottom(self) -> float: 821 return self._bottom 822 @property 823 def right(self) -> float: 824 return self._right 825 @property 826 def top(self) -> float: 827 return self._top 828 829 @property 830 def bounds(self) -> RectangularT: 831 return self 832 833 # overloaded _Shape base class abstract methods 834 def moved(self, *, dxy: Point, context: Optional[MoveContext]=None) -> "Rect": 835 left = self.left + dxy.x 836 bottom = self.bottom + dxy.y 837 right = self.right + dxy.x 838 top = self.top + dxy.y 839 840 return Rect(left=left, bottom=bottom, right=right, top=top) 841 842 def rotated(self, *, 843 rotation: Rotation, context: Optional[RotationContext]=None, 844 ) -> "Rect": 845 if rotation in (Rotation.No, Rotation.R180, Rotation.MX, Rotation.MY): 846 width = self.width 847 height = self.height 848 elif rotation in (Rotation.R90, Rotation.R270, Rotation.MX90, Rotation.MY90): 849 width = self.height 850 height = self.width 851 else: 852 raise RuntimeError( 853 f"Internal error: unsupported rotation '{rotation}'" 854 ) 855 856 return Rect.from_size( 857 center=self.center.rotated(rotation=rotation, context=context), 858 width=width, height=height, 859 ) 860 861 # overloaded _PointsShape mixin abstract methods 862 @property 863 def points(self) -> Iterable[Point]: 864 return ( 865 Point(x=self.left, y=self.bottom), 866 Point(x=self.left, y=self.top), 867 Point(x=self.right, y=self.top), 868 Point(x=self.right, y=self.bottom), 869 Point(x=self.left, y=self.bottom), 870 ) 871 872 def __str__(self) -> str: 873 p1 = Point(x=self.left, y=self.bottom) 874 p2 = Point(x=self.right, y=self.top) 875 return f"[{str(p1)}-{str(p2)}]" 876 877 def __repr__(self) -> str: 878 return ( 879 f"{self.__class__.__name__}(" 880 f"left={self.left:.6},bottom={self.bottom:.6}," 881 f"right={self.right:.6},top={self.top:.6})" 882 ) 883 884 @property 885 def area(self) -> float: 886 return self.width*self.height 887 888 def __eq__(self, o: object) -> bool: 889 if not isinstance(o, Rect): 890 return False 891 return ( 892 _eq(self.left, o.left) and _eq(self.bottom, o.bottom) 893 and _eq(self.right, o.right) and _eq(self.top, o.top) 894 ) 895 896 def __hash__(self) -> int: 897 return hash((self.left, self.bottom, self.right, self.top)) 898 899 900 class MultiPath(Polygon): 901 """A shape consisting of one or more paths. A single path consist of 902 manhattan connections of varying width between points. 903 904 A ``MultiPath`` object is created specifying a list of instructions that 905 build the ``MultiPath``. The first instruction has to be the Start 906 instructions and then follow by a list of other instructions. If a ``Knot`` 907 instruction is included it has to be only once in the list as the last 908 instruction. 909 """ 910 class _Instruction: 911 pass 912 913 class Start(_Instruction): 914 """Indicates the start of a MultiPath""" 915 def __init__(self, *, point: Point, width: float): 916 if width < -epsilon: 917 raise ValueError( 918 f"width has to be a positive value not '{width}'" 919 ) 920 self._point = point 921 self._width = width 922 923 def __eq__(self, obj: object) -> bool: 924 return ( 925 False if not isinstance(obj, Start) 926 else ( 927 (self._point == obj._point) 928 and (abs(self._width - obj._width) < epsilon) 929 ) 930 ) 931 932 class SetWidth(_Instruction): 933 """Set the width for the next segment(s)""" 934 def __init__(self, width: float): 935 if width < -epsilon: 936 raise ValueError( 937 f"width has to be a positive value not '{width}'" 938 ) 939 self._width = width 940 941 def __eq__(self, obj: object) -> bool: 942 return ( 943 False if not isinstance(obj, SetWidth) 944 else abs(self._width - obj._width) < epsilon 945 ) 946 947 class _Go(_Instruction): 948 """Base class for drawing a segment with a certain distance""" 949 def __init__(self, dist: float): 950 if dist < -epsilon: 951 raise ValueError( 952 f"dist has to be a positive value not '{dist}'" 953 ) 954 self._dist = dist 955 956 def __eq__(self, obj: object) -> bool: 957 if self.__class__ != obj.__class__: 958 # _Go objects are final and have to be the same class, not just subclasses 959 return False 960 else: 961 assert isinstance(obj, MultiPath._Go) 962 return abs(self._dist - obj._dist) < epsilon 963 964 class GoLeft(_Go): 965 """Go left from the current location 966 967 API Notes: 968 This class is final, subclassing may cause backwards compatibility problems. 969 """ 970 pass 971 class GoDown(_Go): 972 """Go down from the current location 973 974 API Notes: 975 This class is final, subclassing may cause backwards compatibility problems. 976 """ 977 pass 978 class GoRight(_Go): 979 """Go right from the current location 980 981 API Notes: 982 This class is final, subclassing may cause backwards compatibility problems. 983 """ 984 pass 985 class GoUp(_Go): 986 """Go up from the current location 987 988 API Notes: 989 This class is final, subclassing may cause backwards compatibility problems. 990 """ 991 pass 992 993 class Knot(_Instruction): 994 """A node is a point where different subpaths start from. 995 996 Arguments: 997 left, down, right, up: instructions for each subpath starting from 998 the current location. 999 1000 At least two directions need to be specified. The first instruction 1001 in a direction that is not ``SetWidth`` may not be another ``Knot`` 1002 instruction. 1003 1004 The direction in conflict with last ``_Go`` instruction may not be 1005 specified; e.g. if last instruction was ``GoUp``, down may not be 1006 specified. 1007 1008 API Notes: 1009 This class is final, subclassing may cause backwards compatibility problems. 1010 """ 1011 def __init__(self, *, 1012 left: OptMultiT["MultiPath.NoKnot"]=None, 1013 down: OptMultiT["MultiPath.NoKnot"]=None, 1014 right: OptMultiT["MultiPath.NoKnot"]=None, 1015 up: OptMultiT["MultiPath.NoKnot"]=None, 1016 ): 1017 n_dirs = sum(ins is not None for ins in (left, down, right, up)) 1018 if n_dirs < 2: 1019 raise TypeError("At least two directions need instuctions for 'Knot'") 1020 1021 self.left = cast_OptMultiT(left) 1022 self.down = cast_OptMultiT(down) 1023 self.right = cast_OptMultiT(right) 1024 self.up = cast_OptMultiT(up) 1025 1026 # All instructions except Start; for typing only 1027 NoStart = Union[SetWidth, _Go, Knot] 1028 # All instructions except Start & Knot; for typing only 1029 NoKnot = Union[SetWidth, _Go] 1030 1031 class _PointsBuilder: 1032 def __init__(self, *, first: "MultiPath.Start"): 1033 self.location = first._point 1034 self.width = first._width 1035 self.prevwidth = first._width 1036 self.previnstr: MultiPath._Instruction = first 1037 self.prevdirtype: type = type(first) 1038 1039 self.clkwcoords: List[Point] = [] 1040 self.cclkwcoords: List[Point] = [] 1041 1042 def do_go(self, instr: "MultiPath._Go"): 1043 clkwcoords = self.clkwcoords 1044 cclkwcoords = self.cclkwcoords 1045 1046 width = self.width 1047 prevwidth = self.prevwidth 1048 location = self.location 1049 prevdirtype = self.prevdirtype 1050 1051 instrtype = type(instr) 1052 1053 if prevdirtype == MultiPath.Start: 1054 if instrtype == MultiPath.GoLeft: 1055 dxy = Point(x=0.0, y=0.5*width) 1056 clkwcoords.append(location - dxy) 1057 cclkwcoords.append(location - dxy) 1058 cclkwcoords.append(location + dxy) 1059 elif instrtype == MultiPath.GoDown: 1060 dxy = Point(x=0.5*width, y=0.0) 1061 clkwcoords.append(location + dxy) 1062 cclkwcoords.append(location + dxy) 1063 cclkwcoords.append(location - dxy) 1064 elif instrtype == MultiPath.GoRight: 1065 dxy = Point(x=0.0, y=0.5*width) 1066 clkwcoords.append(location + dxy) 1067 cclkwcoords.append(location + dxy) 1068 cclkwcoords.append(location - dxy) 1069 elif instrtype == MultiPath.GoUp: 1070 dxy = Point(x=0.5*width, y=0.0) 1071 clkwcoords.append(location - dxy) 1072 cclkwcoords.append(location - dxy) 1073 cclkwcoords.append(location + dxy) 1074 else: # pragma: no cover 1075 raise RuntimeError( 1076 f"Internal error: unknown instruction type '{instrtype}'" 1077 ) 1078 elif prevdirtype == MultiPath.GoLeft: 1079 if instrtype == MultiPath.GoLeft: 1080 dxy1 = Point(x=0.0, y=0.5*self.prevwidth) 1081 dxy2 = Point(x=0.0, y=0.5*width) 1082 clkwcoords.extend((location - dxy1, location - dxy2)) 1083 cclkwcoords.extend((location + dxy1, location + dxy2)) 1084 elif instrtype == MultiPath.GoDown: 1085 dxy = Point(x=0.5*width, y=-0.5*prevwidth) 1086 clkwcoords.append(location + dxy) 1087 cclkwcoords.append(location - dxy) 1088 elif instrtype == MultiPath.GoRight: 1089 raise ValueError( 1090 "GoRight instruction after GoLeft not allowed" 1091 ) 1092 elif instrtype == MultiPath.GoUp: 1093 dxy = Point(x=-0.5*width, y=-0.5*prevwidth) 1094 clkwcoords.append(location + dxy) 1095 cclkwcoords.append(location - dxy) 1096 else: # pragma: no cover 1097 raise RuntimeError( 1098 f"Internal error: unknown instruction type '{instrtype}'" 1099 ) 1100 elif prevdirtype == MultiPath.GoDown: 1101 if instrtype == MultiPath.GoLeft: 1102 dxy = Point(x=0.5*prevwidth, y=-0.5*width) 1103 clkwcoords.append(location + dxy) 1104 cclkwcoords.append(location - dxy) 1105 elif instrtype == MultiPath.GoDown: 1106 dxy1 = Point(x=0.5*prevwidth, y=0.0) 1107 dxy2 = Point(x=0.5*width, y=0.0) 1108 clkwcoords.extend((location + dxy1, location + dxy2)) 1109 cclkwcoords.extend((location - dxy1, location - dxy2)) 1110 elif instrtype == MultiPath.GoRight: 1111 dxy = Point(x=0.5*prevwidth, y=0.5*width) 1112 clkwcoords.append(location + dxy) 1113 cclkwcoords.append(location - dxy) 1114 elif instrtype == MultiPath.GoUp: 1115 raise ValueError( 1116 "GoUp instruction after GoDown not allowed" 1117 ) 1118 else: # pragma: no cover 1119 raise RuntimeError( 1120 f"Internal error: unknown instruction type '{instrtype}'" 1121 ) 1122 elif prevdirtype == MultiPath.GoRight: 1123 if instrtype == MultiPath.GoLeft: 1124 raise ValueError( 1125 "GoLeft instruction after GoRight not allowed" 1126 ) 1127 elif instrtype == MultiPath.GoDown: 1128 dxy = Point(x=0.5*width, y=0.5*prevwidth) 1129 clkwcoords.append(location + dxy) 1130 cclkwcoords.append(location - dxy) 1131 elif instrtype == MultiPath.GoRight: 1132 dxy1 = Point(x=0.0, y=0.5*prevwidth) 1133 dxy2 = Point(x=0.0, y=0.5*width) 1134 clkwcoords.extend((location + dxy1, location + dxy2)) 1135 cclkwcoords.extend((location - dxy1, location - dxy2)) 1136 elif instrtype == MultiPath.GoUp: 1137 dxy = Point(x=-0.5*width, y=0.5*prevwidth) 1138 clkwcoords.append(location + dxy) 1139 cclkwcoords.append(location - dxy) 1140 else: # pragma: no cover 1141 raise RuntimeError( 1142 f"Internal error: unknown instruction type '{instrtype}'" 1143 ) 1144 elif prevdirtype == MultiPath.GoUp: 1145 if instrtype == MultiPath.GoLeft: 1146 dxy = Point(x=-0.5*prevwidth, y=-0.5*width) 1147 clkwcoords.append(location + dxy) 1148 cclkwcoords.append(location - dxy) 1149 elif instrtype == MultiPath.GoDown: 1150 raise ValueError( 1151 "GoDown instruction after GoUp not allowed" 1152 ) 1153 elif instrtype == MultiPath.GoRight: 1154 dxy = Point(x=-0.5*prevwidth, y=0.5*width) 1155 clkwcoords.append(location + dxy) 1156 cclkwcoords.append(location - dxy) 1157 elif instrtype == MultiPath.GoUp: 1158 dxy1 = Point(x=0.5*prevwidth, y=0.0) 1159 dxy2 = Point(x=0.5*width, y=0.0) 1160 clkwcoords.extend((location - dxy1, location - dxy2)) 1161 cclkwcoords.extend((location + dxy1, location + dxy2)) 1162 else: # pragma: no cover 1163 raise RuntimeError( 1164 f"Internal error: unknown instruction type '{instrtype}'" 1165 ) 1166 else: # pragma: no cover 1167 raise RuntimeError( 1168 f"Internal error: unknown instruction type '{instrtype}'" 1169 ) 1170 1171 # Update location 1172 if instrtype == MultiPath.GoLeft: 1173 self.location += Point(x=-cast(MultiPath.GoLeft, instr)._dist, y=0.0) 1174 elif instrtype == MultiPath.GoDown: 1175 self.location += Point(x=0.0, y=-cast(MultiPath.GoDown, instr)._dist) 1176 elif instrtype == MultiPath.GoRight: 1177 self.location += Point(x=cast(MultiPath.GoRight, instr)._dist, y=0.0) 1178 elif instrtype == MultiPath.GoUp: 1179 self.location += Point(x=0.0, y=cast(MultiPath.GoDown, instr)._dist) 1180 else: # pragma: no cover 1181 raise RuntimeError( 1182 f"Internal error: unknown `_Go` instruction type '{instrtype}'" 1183 ) 1184 1185 def _knot_builder(self, *, 1186 instrs: Optional[Tuple["MultiPath.NoStart", ...]] 1187 ) -> Optional["MultiPath._PointsBuilder"]: 1188 if instrs is None: 1189 return None 1190 else: 1191 first = instrs[0] 1192 if isinstance(first, MultiPath.SetWidth): 1193 start = MultiPath.Start(point=self.location, width=first._width) 1194 instrs = instrs[1:] 1195 else: 1196 start = MultiPath.Start(point=self.location, width=self.width) 1197 builder = MultiPath._PointsBuilder(first=start) 1198 for instr2 in instrs: 1199 builder.do_instr(instr2) 1200 builder.finalize() 1201 1202 return builder 1203 1204 def do_knot(self, instr: "MultiPath.Knot"): 1205 prevdirtype = self.prevdirtype 1206 1207 left_builder = self._knot_builder(instrs=instr.left) 1208 up_builder = self._knot_builder(instrs=instr.up) 1209 right_builder = self._knot_builder(instrs=instr.right) 1210 down_builder = self._knot_builder(instrs=instr.down) 1211 1212 def conn_ends(*, end: Point, start: Point): 1213 end_sw = (end.x < self.location.x) and (end.y < self.location.y) 1214 end_se = (end.x > self.location.x) and (end.y < self.location.y) 1215 end_nw = (end.x < self.location.x) and (end.y > self.location.y) 1216 end_ne = (end.x > self.location.x) and (end.y > self.location.y) 1217 1218 start_sw = (start.x < self.location.x) and (start.y < self.location.y) 1219 start_se = (start.x > self.location.x) and (start.y < self.location.y) 1220 start_nw = (start.x < self.location.x) and (start.y > self.location.y) 1221 start_ne = (start.x > self.location.x) and (start.y > self.location.y) 1222 1223 if (end_sw or end_ne) and (start_sw or start_ne): 1224 self.clkwcoords.append(Point(x=end.x, y=start.y)) 1225 elif (end_se or end_nw) and (start_se or start_nw): 1226 self.clkwcoords.append(Point(x=start.x, y=end.y)) 1227 elif (end_sw and start_nw) or (end_ne and start_se): 1228 if abs(end.x - start.x) > epsilon: 1229 self.clkwcoords.extend(( 1230 Point(x=end.x, y=self.location.y), 1231 Point(x=start.x, y=self.location.y) 1232 )) 1233 elif (end_nw and start_ne) or (end_se and start_sw): 1234 if abs(end.y - start.y) > epsilon: 1235 self.clkwcoords.extend(( 1236 Point(x=self.location.x, y=end.y), 1237 Point(x=self.location.x, y=start.y), 1238 )) 1239 else: # pragma: no cover 1240 raise RuntimeError("Internal error") 1241 1242 if prevdirtype == MultiPath.GoUp: 1243 assert down_builder is None 1244 builders = (left_builder, up_builder, right_builder) 1245 elif prevdirtype == MultiPath.GoLeft: 1246 assert right_builder is None 1247 builders = (down_builder, left_builder, up_builder) 1248 elif prevdirtype == MultiPath.GoDown: 1249 assert up_builder is None 1250 builders = (right_builder, down_builder, left_builder) 1251 elif prevdirtype == MultiPath.GoRight: 1252 assert left_builder is None 1253 builders = (up_builder, right_builder, down_builder) 1254 else: # pragma: no cover 1255 raise RuntimeError("Internal error") 1256 1257 for builder in builders: 1258 if builder is not None: 1259 conn_ends(end=self.clkwcoords[-1], start=builder.clkwcoords[1]) 1260 self.clkwcoords.extend(( 1261 *builder.clkwcoords[1:], 1262 *reversed(builder.cclkwcoords[2:]), 1263 )) 1264 conn_ends(end=self.clkwcoords[-1], start=self.cclkwcoords[-1]) 1265 1266 def do_instr(self, instr: "MultiPath.NoStart"): 1267 prevtype: type = type(self.previnstr) 1268 if issubclass(prevtype, (MultiPath._Go, MultiPath.Knot)): 1269 self.prevdirtype = prevtype 1270 instrtype: type = type(instr) 1271 1272 prevdirtype = self.prevdirtype 1273 1274 if instrtype == prevtype: 1275 raise ValueError( 1276 "Two instructions of same type after each other is not allowed " 1277 ) 1278 elif instrtype == MultiPath.Start: 1279 raise ValueError("No 'Start' instruction allowed after the first one") 1280 1281 if ( 1282 (instrtype == MultiPath.SetWidth) 1283 and (self.prevdirtype == MultiPath.Start) 1284 ): 1285 raise ValueError( 1286 "First instruction after 'Start' may not be 'SetWidth'", 1287 ) 1288 1289 if prevdirtype == MultiPath.Knot: 1290 raise ValueError( 1291 "No instuction allowed after 'Knot' instruction", 1292 ) 1293 1294 # First instruction after Start needs to be handled differently 1295 if isinstance(instr, MultiPath._Go): 1296 self.do_go(instr) 1297 elif isinstance(instr, MultiPath.Knot): 1298 self.do_knot(instr) 1299 elif not isinstance(instr, MultiPath.SetWidth): # pragma: no cover 1300 raise NotImplementedError(f"instuction type '{instrtype}'") 1301 1302 # Update width 1303 newwidth = instr._width if isinstance(instr, MultiPath.SetWidth) else self.width 1304 self.prevwidth = self.width 1305 self.width = newwidth 1306 1307 self.previnstr = instr 1308 1309 def finalize(self): 1310 location = self.location 1311 width = self.width 1312 clkwcoords = self.clkwcoords 1313 cclkwcoords = self.cclkwcoords 1314 1315 # Complete the last instruction 1316 prevtype = type(self.previnstr) 1317 if prevtype == MultiPath.SetWidth: 1318 raise ValueError( 1319 f"SetWidth may not be the last instruction" 1320 ) 1321 elif prevtype == MultiPath.GoLeft: 1322 dxy = Point(x=0.0, y=0.5*width) 1323 clkwcoords.append(location - dxy) 1324 cclkwcoords.append(location + dxy) 1325 elif prevtype == MultiPath.GoDown: 1326 dxy = Point(x=0.5*width, y=0.0) 1327 clkwcoords.append(location + dxy) 1328 cclkwcoords.append(location - dxy) 1329 elif prevtype == MultiPath.GoRight: 1330 dxy = Point(x=0.0, y=0.5*width) 1331 clkwcoords.append(location + dxy) 1332 cclkwcoords.append(location - dxy) 1333 elif prevtype == MultiPath.GoUp: 1334 dxy = Point(x=0.5*width, y=0.0) 1335 clkwcoords.append(location - dxy) 1336 cclkwcoords.append(location + dxy) 1337 elif prevtype == MultiPath.Knot: 1338 pass 1339 else: # pragma: no cover 1340 raise RuntimeError( 1341 f"Internal error: unknown instruction type '{prevtype}'", 1342 ) 1343 1344 assert len(clkwcoords) > 0 1345 assert len(cclkwcoords) > 0 1346 1347 def __init__(self, first: Start, *instrs: "MultiPath.NoStart"): 1348 if len(instrs) == 0: 1349 raise ValueError("At least one instruction needed after 'Start'") 1350 self._first: MultiPath.Start = first 1351 self._instrs = instrs 1352 1353 # Build the coordinates 1354 builder = MultiPath._PointsBuilder(first=first) 1355 1356 for instr in instrs: 1357 builder.do_instr(instr) 1358 1359 builder.finalize() 1360 1361 super().__init__(points=( 1362 *builder.clkwcoords, *reversed(builder.cclkwcoords), 1363 )) 1364 1365 @property 1366 def first(self) -> Start: 1367 return self._first 1368 @property 1369 def instrs(self) -> Tuple[_Instruction, ...]: 1370 return self._instrs 1371 # Instruction aliases 1372 Start = MultiPath.Start 1373 SetWidth = MultiPath.SetWidth 1374 GoLeft = MultiPath.GoLeft 1375 GoDown = MultiPath.GoDown 1376 GoRight = MultiPath.GoRight 1377 GoUp = MultiPath.GoUp 1378 Knot = MultiPath.Knot 1379 NoStart = MultiPath.NoStart # For typing only 1380 1381 1382 class Ring(MultiPath): 1383 """A shape representating a ring shape polygon 1384 1385 Arguments: 1386 outer_bound: the outer edge of the shape 1387 ring_width: the width of the ring, it has to be smaller than 1388 half the width or height of the outer edge. 1389 """ 1390 def __init__(self, *, outer_bound: Rect, ring_width: float): 1391 if (ring_width + epsilon) > outer_bound.width/2.0: 1392 raise ValueError( 1393 f"ring_width '{ring_width}' is bigger than half outer bound width" 1394 f" '{outer_bound.width}'", 1395 ) 1396 if (ring_width + epsilon) > outer_bound.height/2.0: 1397 raise ValueError( 1398 f"ring_width '{ring_width}' is bigger than half outer bound height" 1399 f" '{outer_bound.height}'", 1400 ) 1401 1402 oleft = outer_bound.left 1403 obottom = outer_bound.bottom 1404 oright = outer_bound.right 1405 otop = outer_bound.top 1406 1407 oheight = otop - obottom 1408 owidth = oright - oleft 1409 mleft = oleft + 0.5*ring_width 1410 1411 instrs = ( 1412 Start(point=Point(x=mleft, y=obottom), width=ring_width), 1413 GoUp(oheight - 0.5*ring_width), 1414 GoRight(owidth - ring_width), 1415 GoDown(oheight - ring_width), 1416 GoLeft(owidth - 1.5*ring_width), 1417 ) 1418 super().__init__(*instrs) 1419 1420 self.outer_bound = outer_bound 1421 self.ring_width = ring_width 1422 1423 1424 class RectRing(_Shape): 1425 """A `RectRing` object is a shape that consists of a ring of `Rect` objects. 1426 1427 An exception will be raised when there is not enough room to put the four corner 1428 rects. 1429 If the 'Rect' objects needs to be on a grid all dimensions specified for this 1430 object - including outer bound placement, width & height - have to be double that 1431 grid number. 1432 1433 Arguments: 1434 outer_bound: the outer bound of the ring; e.g. the generated rect shapes 1435 will be inside and touching the bound. 1436 rect_width: the width of the generated rect objects. 1437 rect_height: the height of the generated rect objects; by default it will 1438 be the same as rect_width. 1439 min_rect_space: the minimum space between two rect structures. 1440 """ 1441 # TODO: Describe rules to get shapes on grid 1442 def __init__(self, *, 1443 outer_bound: Rect, 1444 rect_width: float, rect_height: Optional[float]=None, 1445 min_rect_space: float, 1446 ): 1447 if rect_height is None: 1448 rect_height = rect_width 1449 if (outer_bound.width + epsilon) < (2*rect_width + min_rect_space): 1450 raise ValueError( 1451 "outer_bound width not big enough to fit two rects in" 1452 ) 1453 if (outer_bound.height + epsilon) < (2*rect_height + min_rect_space): 1454 raise ValueError( 1455 "outer_bound height not big enough to fit two rects in" 1456 ) 1457 1458 self._outer_bound = outer_bound 1459 self._rect_width = float(rect_width) 1460 self._rect_height = float(rect_height) 1461 self._min_rect_space = float(min_rect_space) 1462 1463 pitch_x = rect_width + min_rect_space 1464 # Rects in horizontal direction besides corners 1465 self._n_x = floor( 1466 ( 1467 self.outer_bound.width 1468 - (2*self.rect_width + self._min_rect_space) 1469 + epsilon 1470 )/pitch_x 1471 ) 1472 assert self._n_x >= 0, "Internal error" 1473 1474 pitch_y = rect_height + min_rect_space 1475 # Rects in vertical direction besides corners 1476 self._n_y = floor( 1477 ( 1478 self.outer_bound.height 1479 - (2*self.rect_height + self._min_rect_space) 1480 + epsilon 1481 )/pitch_y 1482 ) 1483 assert self._n_y >= 0, "Internal error" 1484 1485 @property 1486 def outer_bound(self) -> Rect: 1487 return self._outer_bound 1488 @property 1489 def rect_width(self) -> float: 1490 return self._rect_width 1491 @property 1492 def rect_height(self) -> float: 1493 return self._rect_height 1494 @property 1495 def min_rect_space(self) -> float: 1496 return self._min_rect_space 1497 1498 def moved(self, *, 1499 dxy: "Point", context: Optional[MoveContext]=None, 1500 ) -> "RectRing": 1501 return RectRing( 1502 outer_bound=self.outer_bound.moved(dxy=dxy, context=context), 1503 rect_width=self.rect_width, rect_height=self.rect_height, 1504 min_rect_space=self.min_rect_space, 1505 ) 1506 1507 def rotated(self, *, 1508 rotation: Rotation, context: Optional[RotationContext]=None, 1509 ) -> "RectRing": 1510 return RectRing( 1511 outer_bound=self.outer_bound.rotated(rotation=rotation, context=context), 1512 rect_width=self.rect_width, rect_height=self.rect_height, 1513 min_rect_space=self.min_rect_space, 1514 ) 1515 1516 @property 1517 def pointsshapes(self) -> Iterable["PointsShapeT"]: 1518 rect = Rect.from_size(width=self.rect_width, height=self.rect_height) 1519 left = self.outer_bound.left 1520 bottom = self.outer_bound.bottom 1521 right = self.outer_bound.right 1522 top = self.outer_bound.top 1523 1524 left_x = left + 0.5*self.rect_width 1525 right_x = right - 0.5*self.rect_width 1526 mid_x = self.outer_bound.center.x 1527 bottom_y = bottom + 0.5*self.rect_height 1528 top_y = top - 0.5*self.rect_height 1529 mid_y = self.outer_bound.center.y 1530 1531 pitch_x = self.rect_width + self.min_rect_space 1532 pitch_y = self.rect_height + self.min_rect_space 1533 1534 # corners 1535 yield rect + Point(x=left_x, y=bottom_y) 1536 yield rect + Point(x=left_x, y=top_y) 1537 yield rect + Point(x=right_x, y=bottom_y) 1538 yield rect + Point(x=right_x, y=top_y) 1539 1540 # bottom and top 1541 left_x2 = mid_x - 0.5*(self._n_x - 1)*pitch_x 1542 for n in range(self._n_x): 1543 x = left_x2 + n*pitch_x 1544 yield rect + Point(x=x, y=bottom_y) 1545 yield rect + Point(x=x, y=top_y) 1546 1547 # left and right 1548 bottom_y2 = mid_y - 0.5*(self._n_y - 1)*pitch_y 1549 for n in range(self._n_y): 1550 y = bottom_y2 + n*pitch_y 1551 yield rect + Point(x=left_x, y=y) 1552 yield rect + Point(x=right_x, y=y) 1553 1554 @property 1555 def bounds(self) -> RectangularT: 1556 return self.outer_bound 1557 1558 @property 1559 def area(self) -> float: 1560 return (4 + 2*self._n_x + 2*self._n_y)*self.rect_width*self.rect_height 1561 1562 def __eq__(self, o: object) -> bool: 1563 if not isinstance(o, RectRing): 1564 return False 1565 else: 1566 return all(( 1567 self.outer_bound == o.outer_bound, 1568 abs(self.rect_width - o.rect_width) <= epsilon, 1569 abs(self.rect_height - o.rect_height) <= epsilon, 1570 abs(self.min_rect_space - o.min_rect_space) <= epsilon, 1571 )) 1572 1573 def __hash__(self) -> int: 1574 return hash( 1575 (self.outer_bound, self.rect_width, self.rect_height, self.min_rect_space), 1576 ) 1577 1578 def __repr__(self) -> str: 1579 s_args = ",".join(( 1580 f"outer_bound={self.outer_bound!r}", 1581 f"rect_width={self.rect_width!r}", 1582 f"rect_height={self.rect_height!r}", 1583 f"min_rect_space={self.min_rect_space!r}", 1584 )) 1585 return f"RingRect({s_args})" 1586 1587 1588 class Label(_Shape): 1589 """This shape represent a text label. 1590 1591 Arguments: 1592 origin: location of the label. 1593 Currently no support for any other property than the origin 1594 (like rotation, font, etc.) is supported. 1595 text: the text of the label 1596 """ 1597 def __init__(self, origin: Point, text: str): 1598 super().__init__() 1599 self._origin = origin 1600 self._text = text 1601 1602 @property 1603 def origin(self) -> Point: 1604 return self._origin 1605 @property 1606 def text(self) -> str: 1607 return self._text 1608 1609 @property 1610 def pointsshapes(self) -> Iterable[PointsShapeT]: 1611 return self.origin.pointsshapes 1612 @property 1613 def bounds(self) -> RectangularT: 1614 return self.origin.bounds 1615 @property 1616 def area(self) -> float: 1617 return self.origin.area 1618 1619 def __eq__(self, o: object) -> bool: 1620 if not isinstance(o, Label): 1621 return False 1622 else: 1623 return (self.origin == o.origin) and (self.text == o.text) 1624 1625 def __hash__(self) -> int: 1626 return hash((self.origin, self.text)) 1627 1628 def moved(self, *, dxy: Point, context: Optional[MoveContext] = None) -> "Label": 1629 return self.__class__(origin=origin.moved(dxy=dxy, context=context), text=self.text) 1630 1631 def rotated(self, *, rotation: Rotation, context: Optional[RotationContext]=None) -> "Label": 1632 return self 1633 1634 1635 class MultiPartShape(Polygon): 1636 """This shape represents a single polygon shape that consist of 1637 a build up of touching parts. 1638 1639 Main use case is to represent a shape where parts are on a different 1640 net as is typically the case for a WaferWire. 1641 1642 Arguments: 1643 fullshape: The full shape 1644 parts: The subshapes 1645 The subshapes should be touching shapes and joined should form the 1646 fullshape shape. Currently it is only checked if the areas match, 1647 in better checking may be implemented. 1648 1649 The subshapes will be converted to MultiPartShape._Part objects before 1650 becoming member of the parts property 1651 """ 1652 # TODO: merging of shapes is not complete. standard cell library still seems to 1653 # generate geometries that are no properly merged. 1654 class _Part(Polygon): 1655 """A shape representing one part of a MultiPartShape 1656 1657 This object keeps reference to the MultiPartShape so the parts can be added 1658 to nets in layout and the shapes still being able to know to which 1659 MultiPartShape object they belong. 1660 """ 1661 def __init__(self, *, partshape: Polygon, multipartshape: "MultiPartShape"): 1662 self._partshape = partshape 1663 self._multipartshape = multipartshape 1664 1665 @property 1666 def partshape(self) -> Polygon: 1667 return self._partshape 1668 @property 1669 def multipartshape(self) -> "MultiPartShape": 1670 return self._multipartshape 1671 1672 @property 1673 def pointsshapes(self) -> Iterable[PointsShapeT]: 1674 yield self 1675 @property 1676 def bounds(self) -> RectangularT: 1677 return self.partshape.bounds 1678 1679 def moved(self, *, 1680 dxy: Point, context: Optional[MoveContext]=None, 1681 ) -> "MultiPartShape._Part": 1682 if context is None: 1683 idx = self.multipartshape.parts.index(self) 1684 return self.multipartshape.moved(dxy=dxy).parts[idx] 1685 else: 1686 return context._move_part(part=self, dxy=dxy) 1687 1688 def rotated(self, *, 1689 rotation: Rotation, context: Optional[RotationContext]=None, 1690 ) -> "MultiPartShape._Part": 1691 if context is None: 1692 idx = self.multipartshape.parts.index(self) 1693 return self.multipartshape.rotated(rotation=rotation).parts[idx] 1694 else: 1695 return context._rotate_part(part=self, rotation=rotation) 1696 1697 # _PointsShape mixin abstract methods 1698 @property 1699 def points(self) -> Iterable[Point]: 1700 return self.partshape.points 1701 1702 @property 1703 def area(self) -> float: 1704 return self.partshape.area 1705 1706 def __str__(self) -> str: 1707 return f"<<{str(self.partshape)}>>" 1708 1709 def __repr__(self) -> str: 1710 ps = self.partshape 1711 return f"MultiPartShape._Part(partshape={ps!r})" 1712 1713 def __hash__(self) -> int: 1714 return hash((self.partshape, self.multipartshape)) 1715 1716 def __eq__(self, other: Any) -> bool: 1717 if not isinstance(other, MultiPartShape._Part): 1718 return False 1719 else: 1720 return ( 1721 (self.partshape == other.partshape) 1722 and (self.multipartshape == other.multipartshape) 1723 ) 1724 1725 def __init__(self, fullshape: Polygon, parts: Iterable[Polygon]): 1726 # TODO: check if shape is actually build up of the parts 1727 self._fullshape = fullshape 1728 self._parts = tuple( 1729 MultiPartShape._Part(partshape=part, multipartshape=self) 1730 for part in parts 1731 ) 1732 1733 @property 1734 def fullshape(self) -> Polygon: 1735 return self._fullshape 1736 @property 1737 def parts(self) -> Tuple["MultiPartShape._Part", ...]: 1738 return self._parts 1739 1740 @property 1741 def pointsshapes(self) -> Iterable[PointsShapeT]: 1742 return self.fullshape.pointsshapes 1743 @property 1744 def bounds(self) -> RectangularT: 1745 return self.fullshape.bounds 1746 1747 def moved(self, *, dxy: Point, context: Optional[MoveContext]=None) -> "MultiPartShape": 1748 return MultiPartShape( 1749 fullshape=self.fullshape.moved(dxy=dxy), 1750 parts=(part.partshape.moved(dxy=dxy) for part in self.parts) 1751 ) 1752 1753 def rotated(self, *, 1754 rotation: Rotation, context: Optional[RotationContext]=None, 1755 ) -> "MultiPartShape": 1756 return MultiPartShape( 1757 fullshape=self.fullshape.rotated(rotation=rotation, context=context), 1758 parts=( 1759 part.partshape.rotated(rotation=rotation, context=context) 1760 for part in self.parts 1761 ) 1762 ) 1763 1764 # _PointsShape mixin abstract methods 1765 @property 1766 def points(self) -> Iterable[Point]: 1767 return self.fullshape.points 1768 1769 @property 1770 def area(self) -> float: 1771 return self.fullshape.area 1772 1773 def __hash__(self) -> int: 1774 return hash(self.fullshape) 1775 1776 def __eq__(self, other: Any) -> bool: 1777 if not isinstance(other, MultiPartShape): 1778 return False 1779 else: 1780 return ( 1781 {part.partshape for part in self.parts} 1782 == {part.partshape for part in other.parts} 1783 ) 1784 1785 def __str__(self) -> str: 1786 s = "|".join(str(p.partshape) for p in self._parts) 1787 return f"({s})" 1788 1789 def __repr__(self) -> str: 1790 s1 = repr(self.fullshape) 1791 s2 = ",".join(repr(p.partshape) for p in self._parts) 1792 return f"MultiPartShape(fullshape={s1},parts=({s2}))" 1793 1794 1795 class MultiShape(_Shape, Collection[_Shape]): 1796 """A shape representing a group of shapes 1797 1798 Arguments: 1799 shapes: the sub shapes. 1800 Subshapes may or may not overlap. The object will fail to create if only one unique 1801 shape is provided including if the same shape is provided multiple times without 1802 another shape. 1803 1804 MultiShape objects part of the provided shapes will be flattened and it's children will 1805 be joined with the other shapes. 1806 """ 1807 def __init__(self, *, shapes: Iterable[_Shape]): 1808 def iterate_shapes(ss: Iterable[_Shape]) -> Iterable[_Shape]: 1809 for shape in ss: 1810 if isinstance(shape, MultiShape): 1811 yield from iterate_shapes(shape.shapes) 1812 else: 1813 yield shape 1814 self._shapes = shapes = frozenset(iterate_shapes(shapes)) 1815 if len(shapes) < 2: 1816 raise ValueError("MultiShape has to consist of more than one shape") 1817 1818 @property 1819 def shapes(self) -> Iterable[ShapeT]: 1820 return self._shapes 1821 1822 # _Shape base class abstract methods 1823 @property 1824 def pointsshapes(self) -> Iterable[PointsShapeT]: 1825 for shape in self._shapes: 1826 yield from shape.pointsshapes 1827 @property 1828 def bounds(self) -> RectangularT: 1829 boundss = tuple(shape.bounds for shape in self.shapes) 1830 left = min(bounds.left for bounds in boundss) 1831 bottom = min(bounds.bottom for bounds in boundss) 1832 right = max(bounds.right for bounds in boundss) 1833 top = max(bounds.top for bounds in boundss) 1834 1835 # It should be impossible to create a MultiShape where bounds 1836 # corresponds with a point. 1837 assert (left != right) or (bottom != top), "Internal error" 1838 if (left == right) or (bottom == top): 1839 return Line( 1840 point1=Point(x=left, y=bottom), 1841 point2=Point(x=right, y=top), 1842 ) 1843 else: 1844 return Rect(left=left, bottom=bottom, right=right, top=top) 1845 1846 def moved(self, *, dxy: Point, context: Optional[MoveContext]=None) -> "MultiShape": 1847 # Avoid generating different MultiPartShape for parts from the same MultiPartShape 1848 if context is None: 1849 context=MoveContext() 1850 1851 return MultiShape( 1852 shapes=( 1853 polygon.moved(dxy=dxy, context=context) 1854 for polygon in self.pointsshapes 1855 ), 1856 ) 1857 1858 def rotated(self, *, 1859 rotation: Rotation, context: Optional[RotationContext]=None, 1860 ) -> "MultiShape": 1861 return MultiShape( 1862 shapes=( 1863 polygon.rotated(rotation=rotation, context=context) 1864 for polygon in self.pointsshapes 1865 ) 1866 ) 1867 1868 # Collection mixin abstract methods 1869 def __iter__(self) -> Iterator[_Shape]: 1870 return iter(self.shapes) 1871 1872 def __len__(self) -> int: 1873 return len(self._shapes) 1874 1875 def __contains__(self, shape: object) -> bool: 1876 return shape in self._shapes 1877 1878 @property 1879 def area(self) -> float: 1880 # TODO: guarantee non overlapping shapes 1881 return sum(shape.area for shape in self._shapes) 1882 1883 def __eq__(self, o: object) -> bool: 1884 if not isinstance(o, MultiShape): 1885 return False 1886 else: 1887 return self._shapes == o._shapes 1888 1889 def __hash__(self) -> int: 1890 return hash(self.shapes) 1891 1892 def __str__(self) -> str: 1893 # substrings are sorted to get reproducable order independent str representation 1894 return "(" + ",".join(sorted(str(shape) for shape in self.shapes)) + ")" 1895 1896 def __repr__(self) -> str: 1897 # substrings are sorted to get reproducable order independent str representation 1898 return ( 1899 "MultiShape(shapes=(" 1900 + ",".join(sorted(repr(shape) for shape in self.shapes)) 1901 + "))" 1902 ) 1903 1904 1905 class RepeatedShape(_Shape): 1906 """A repetition of a shape allowing easy generation of array of objects. 1907 Implementation is generic so that one can represent any repetition with 1908 one or two vector that don't need to be manhattan. 1909 1910 API Notes: 1911 * The current implementation assumes repeated shapes don't overlap. If they 1912 do area property will give wrong value. 1913 """ 1914 # TODO: decide if repeated shapes may overlap, if not can we check it ? 1915 def __init__(self, *, 1916 shape: ShapeT, offset0: Point, 1917 n: int, n_dxy: Point, m: int=1, m_dxy: Optional[Point]=None, 1918 ): 1919 if n < 2: 1920 raise ValueError(f"n has to be equal to or higher than 2, not '{n}'") 1921 if m < 1: 1922 raise ValueError(f"m has to be equal to or higher than 1, not '{m}'") 1923 if (m > 1) and (m_dxy is None): 1924 raise ValueError("m_dxy may not be None if m > 1") 1925 self._shape = shape 1926 self._offset0 = offset0 1927 self._n = n 1928 self._n_dxy = n_dxy 1929 self._m = m 1930 self._m_dxy = m_dxy 1931 1932 self._hash = None 1933 1934 @property 1935 def shape(self) -> ShapeT: 1936 return self._shape 1937 @property 1938 def offset0(self) -> Point: 1939 return self._offset0 1940 @property 1941 def n(self) -> int: 1942 return self._n 1943 @property 1944 def n_dxy(self) -> Point: 1945 return self._n_dxy 1946 @property 1947 def m(self) -> int: 1948 return self._m 1949 @property 1950 def m_dxy(self) -> Optional[Point]: 1951 return self._m_dxy 1952 1953 def moved( 1954 self: "RepeatedShape", *, dxy: "Point", context: Optional[MoveContext]=None, 1955 ) -> "RepeatedShape": 1956 return RepeatedShape( 1957 shape=self.shape, offset0=(self.offset0 + dxy), 1958 n=self.n, n_dxy=self.n_dxy, m=self.m, m_dxy=self.m_dxy, 1959 ) 1960 1961 @property 1962 def pointsshapes(self) -> Iterable[PointsShapeT]: 1963 if self.m <= 1: 1964 for i_n in range(self.n): 1965 dxy = self.offset0 + i_n*self.n_dxy 1966 yield from (polygon + dxy for polygon in self.shape.pointsshapes) 1967 else: 1968 assert self.m_dxy is not None 1969 for i_n, i_m in product(range(self.n), range(self.m)): 1970 dxy = self.offset0 + i_n*self.n_dxy + i_m*self.m_dxy 1971 yield from (polygon + dxy for polygon in self.shape.pointsshapes) 1972 1973 @property 1974 def bounds(self) -> RectangularT: 1975 b0 = self.shape.bounds 1976 b1 = b0 + self.offset0 1977 if self.m <= 1: 1978 b2 = b0 + (self.offset0 + (self.n - 1)*self.n_dxy) 1979 else: 1980 assert self.m_dxy is not None 1981 b2 = b0 + ( 1982 self.offset0 + (self.n - 1)*self.n_dxy + (self.m - 1)*self.m_dxy 1983 ) 1984 return Rect( 1985 left=min(b1.left, b2.left), right=max(b1.right, b2.right), 1986 bottom=min(b1.bottom, b2.bottom), top=max(b1.top, b2.top), 1987 ) 1988 1989 def rotated(self, *, 1990 rotation: Rotation, context: Optional[RotationContext]=None, 1991 ) -> "RepeatedShape": 1992 return RepeatedShape( 1993 shape=self.shape.rotated(rotation=rotation, context=context), 1994 offset0=self.offset0.rotated(rotation=rotation, context=context), 1995 n=self.n, n_dxy=self.n_dxy.rotated(rotation=rotation, context=context), 1996 m=self.m, m_dxy=( 1997 None if self.m_dxy is None 1998 else self.m_dxy.rotated(rotation=rotation, context=context) 1999 ) 2000 ) 2001 2002 @property 2003 def area(self) -> float: 2004 # TODO: Support case with overlapping shapes ? 2005 return self.n*self.m*self.shape.area 2006 2007 def __eq__(self, o: object) -> bool: 2008 if not isinstance(o, RepeatedShape): 2009 return False 2010 elif (self.shape != o.shape) or (self.offset0 != o.offset0): 2011 return False 2012 elif self.m == 1: 2013 return ( 2014 (self.n == o.n) and (self.n_dxy == o.n_dxy) 2015 and (o.m == 1) 2016 ) 2017 elif self.n == self.m: 2018 return ( 2019 (self.n == o.n == o.m) 2020 # dxy value may be exchanged => compare sets 2021 and ({self.n_dxy, self.m_dxy} == {o.n_dxy, o.m_dxy}) 2022 ) 2023 else: # (self.n != self.m) and (self.m > 1) 2024 return ( 2025 ( 2026 (self.n == o.n) and (self.n_dxy == o.n_dxy) 2027 and (self.m == o.m) and (self.m_dxy == o.m_dxy) 2028 ) 2029 or 2030 ( 2031 (self.n == o.m) and (self.n_dxy == o.m_dxy) 2032 and (self.m == o.n) and (self.m_dxy == o.n_dxy) 2033 ) 2034 ) 2035 2036 def __hash__(self) -> int: 2037 if self._hash is None: 2038 if self.m == 1: 2039 self._hash = hash(frozenset(( 2040 self.shape, self.offset0, self.n, self.n_dxy, 2041 ))) 2042 else: 2043 self._hash = hash(frozenset(( 2044 self.shape, self.offset0, self.n, self.n_dxy, self.m, self.m_dxy, 2045 ))) 2046 return self._hash 2047 2048 def __repr__(self) -> str: 2049 s_args = ",".join(( 2050 f"shape={self.shape!r}", 2051 f"offset0={self.offset0!r}", 2052 f"n={self.n}", f"n_dxy={self.n_dxy!r}", 2053 f"m={self.m}", f"m_dxy={self.m_dxy!r}", 2054 )) 2055 return f"RepeatedShape({s_args})" 2056 2057 2058 class ArrayShape(RepeatedShape): 2059 """Object representing a manhattan repeared shape. 2060 2061 This is a RepeatedShape subclass with repeat vectors either a horizontal and/or a 2062 vertical one. 2063 2064 Arguments: 2065 shape: The object to repeat 2066 offset0: The placement of the first shape 2067 rows, columns: The number of rows and columns 2068 Both have to be equal or higher than 1 and either rows or columns has to 2069 be higher than 1. 2070 pitch_y, pitch_x: The displacement for resp. the rows and the columns. 2071 """ 2072 def __init__(self, *, 2073 shape: _Shape, offset0: Point, 2074 rows: int, columns: int, 2075 pitch_y: Optional[float]=None, pitch_x: Optional[float]=None, 2076 ): 2077 if (rows <= 0) or (columns <= 0): 2078 raise ValueError( 2079 f"rows ({rows}) and columns ({columns}) need to be integers greater than zero" 2080 ) 2081 if (rows == 1) and (columns == 1): 2082 raise ValueError( 2083 "either rows or columns or both have to be greater than 1" 2084 ) 2085 if (rows > 1) and (pitch_y is None): 2086 raise ValueError( 2087 "pitch_y not given for rows > 1" 2088 ) 2089 if (columns > 1) and (pitch_x is None): 2090 raise ValueError( 2091 "pitch_x not given for columns > 1" 2092 ) 2093 self._rows = rows 2094 self._columns = columns 2095 self._pitch_x = pitch_x 2096 self._pitch_y = pitch_y 2097 2098 if rows == 1: 2099 n = columns 2100 n_dxy = Point(x=cast(float, pitch_x), y=0.0) 2101 m = 1 2102 m_dxy = None 2103 else: 2104 n = rows 2105 n_dxy = Point(x=0.0, y=cast(float, pitch_y)) 2106 m = columns 2107 m_dxy = None if pitch_x is None else Point(x=pitch_x, y=0.0) 2108 super().__init__( 2109 shape=shape, offset0=offset0, n=n, n_dxy=n_dxy, m=m, m_dxy=m_dxy, 2110 ) 2111 2112 @property 2113 def rows(self) -> int: 2114 return self._rows 2115 @property 2116 def columns(self) -> int: 2117 return self._columns 2118 @property 2119 def pitch_x(self) -> Optional[float]: 2120 return self._pitch_x 2121 @property 2122 def pitch_y(self) -> Optional[float]: 2123 return self._pitch_y 2124 2125 2126 class MaskShape: 2127 def __init__(self, *, mask: _msk.DesignMask, shape: ShapeT): 2128 self._mask = mask 2129 self._shape = shape 2130 # TODO: Check grid 2131 2132 @property 2133 def mask(self) -> _msk.DesignMask: 2134 return self._mask 2135 @property 2136 def shape(self) -> ShapeT: 2137 return self._shape 2138 2139 def moved(self, *, dxy: Point, context: Optional[MoveContext]=None) -> "MaskShape": 2140 return MaskShape(mask=self.mask, shape=self.shape.moved(dxy=dxy, context=context)) 2141 2142 def rotated(self, *, 2143 rotation: Rotation, context: Optional[RotationContext]=None, 2144 ) -> "MaskShape": 2145 return MaskShape( 2146 mask=self.mask, 2147 shape=self.shape.rotated(rotation=rotation, context=context), 2148 ) 2149 2150 @property 2151 def area(self) -> float: 2152 return self.shape.area 2153 2154 def __eq__(self, o: object) -> bool: 2155 if not isinstance(o, MaskShape): 2156 return False 2157 return (self.mask == o.mask) and (self.shape == o.shape) 2158 2159 def __hash__(self) -> int: 2160 return hash((self.mask, self.shape)) 2161 2162 def __repr__(self) -> str: 2163 return f"MaskShape=(mask={self.mask!r},shape={self.shape!r})" 2164 2165 @property 2166 def bounds(self) -> RectangularT: 2167 return self.shape.bounds 2168 2169 2170 class MaskShapes(_util.ExtendedListMapping[MaskShape, _msk.DesignMask]): 2171 """A TypedListMapping of MaskShape objects. 2172 2173 API Notes: 2174 Contrary to other classes a MaskShapes object is mutable if not frozen. 2175 """ 2176 @property 2177 def _index_attribute_(self): 2178 return "mask" 2179 2180 def __init__(self, iterable: MultiT[MaskShape]): 2181 shapes = cast_MultiT(iterable) 2182 2183 def join_shapes() -> Iterable[MaskShape]: 2184 masks = [] 2185 for shape in shapes: 2186 mask = shape.mask 2187 if mask not in masks: 2188 shapes2 = tuple(filter(lambda ms: ms.mask == mask, shapes)) 2189 if len(shapes2) == 1: 2190 yield shapes2[0] 2191 else: 2192 yield MaskShape( 2193 mask=mask, 2194 shape=MultiShape(shapes=(ms.shape for ms in shapes2)) 2195 ) 2196 masks.append(mask) 2197 2198 super().__init__(join_shapes()) 2199 2200 def __iadd__(self, shape: MultiT[MaskShape]) -> "MaskShapes": 2201 for s in cast_MultiT(shape): 2202 mask = s.mask 2203 try: 2204 ms = self[mask] 2205 except KeyError: 2206 super().__iadd__(s) 2207 except: # pragma: no cover 2208 raise 2209 else: 2210 if ms.shape != s.shape: 2211 ms2 = MaskShape( 2212 mask=mask, shape=MultiShape(shapes=(ms.shape, s.shape)), 2213 ) 2214 self[mask] = ms2 2215 2216 return self 2217 2218 def move(self, *, dxy: Point, context: Optional[MoveContext]=None) -> None: 2219 if context is None: 2220 context = MoveContext() 2221 if self._frozen_: 2222 raise TypeError(f"moving frozen '{self.__class__.__name__}' object not allowed") 2223 for i in range(len(self)): 2224 self[i] = self[i].moved(dxy=dxy, context=context) 2225 2226 def moved(self, *, dxy: Point, context: Optional[MoveContext]=None) -> "MaskShapes": 2227 """Moved MaskShapes object will not be frozen""" 2228 if context is None: 2229 context = MoveContext() 2230 return MaskShapes(ms.moved(dxy=dxy, context=context) for ms in self) 2231 2232 def rotate(self, *, 2233 rotation: Rotation, context: Optional[RotationContext]=None, 2234 ) -> None: 2235 if self._frozen_: 2236 raise TypeError(f"rotating frozen '{self.__class__.__name__}' object not allowed") 2237 if context is None: 2238 context = RotationContext() 2239 for i in range(len(self)): 2240 self[i] = self[i].rotated(rotation=rotation, context=context) 2241 2242 def rotated(self, *, 2243 rotation: Rotation, context: Optional[RotationContext]=None, 2244 ) -> "MaskShapes": 2245 """Rotated MaskShapes object will not be frozen""" 2246 if context is None: 2247 context = RotationContext() 2248 return MaskShapes( 2249 ms.rotated(rotation=rotation, context=context) 2250 for ms in self 2251 )