/ pdkmaster / technology / geometry.py
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          )