/ test / unit / 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  # type: ignore
   3  from itertools import product
   4  import unittest
   5  from typing import Iterable
   6  
   7  from pdkmaster import _util
   8  from pdkmaster.technology import mask as _msk, geometry as _geo
   9  
  10  class GeometryTest(unittest.TestCase):
  11      def test_rotation(self):
  12          self.assertEqual(_geo.Rotation.No, _geo.Rotation.R0)
  13          for name, rot in (
  14              ("no", _geo.Rotation.R0),
  15              ("90", _geo.Rotation.R90),
  16              ("180", _geo.Rotation.R180),
  17              ("270", _geo.Rotation.R270),
  18              ("mirrorx", _geo.Rotation.MX),
  19              ("mirrorx&90", _geo.Rotation.MX90),
  20              ("mirrory", _geo.Rotation.MY),
  21              ("mirrory&90", _geo.Rotation.MY90),
  22          ):
  23              self.assertEqual(_geo.Rotation.from_name(name), rot)
  24  
  25          rot = _geo.Rotation.MX
  26          with self.assertRaisesRegex(
  27              TypeError, (
  28                  "unsupported operand type\(s\) for \*\: "
  29                  f"'{rot.__class__.__name__}' and 'int'"
  30              )
  31          ):
  32              rot * 2
  33  
  34          self.assertEqual(
  35              _geo.Rotation.R180*_geo.Rotation.R180,
  36              _geo.Rotation.R0,
  37          )
  38          self.assertEqual(
  39              _geo.Rotation.MX90*_geo.Rotation.R90,
  40              _geo.Rotation.MY,
  41          )
  42          self.assertEqual(
  43              _geo.Rotation.MY90*_geo.Rotation.R180,
  44              _geo.Rotation.MX90,
  45          )
  46  
  47          r0 = _geo.Rotation.R0
  48  
  49          p = _geo.Point(x=1.0, y=-2.0)
  50          rot = _geo.Rotation.MY
  51          self.assertEqual(p*r0, p)
  52          self.assertEqual(rot*p, p.rotated(rotation=rot))
  53  
  54          m = _msk.DesignMask(name="mask")
  55          r = _geo.Rect.from_size(width=1.0, height=3.0)
  56          ms = _geo.MaskShape(mask=m, shape=r)
  57          mss = _geo.MaskShapes(ms)
  58          rot = _geo.Rotation.R270
  59  
  60          r_r = rot*r
  61          r_ms = ms*rot
  62          r_mss = rot*mss
  63          self.assertEqual(r0*r, r)
  64          self.assertEqual(r0*ms, ms)
  65          self.assertEqual(mss*r0, mss)
  66          self.assertEqual(r_r, r_ms.shape)
  67          self.assertEqual(r_ms, r_mss[0])
  68  
  69      # RotationContext and MoveContext are tested from from the layout
  70      # unit tests.
  71  
  72      def test_abstract(self):
  73          with self.assertRaisesRegex(
  74              TypeError, "^Can't instantiate abstract class _Shape",
  75          ):
  76              _geo._Shape()
  77          with self.assertRaisesRegex(
  78              TypeError, "^Can't instantiate abstract class _Rectangular",
  79          ):
  80              _geo._Rectangular()
  81          with self.assertRaisesRegex(
  82              TypeError, "^Can't instantiate abstract class _PointsShape",
  83          ):
  84              _geo._PointsShape()
  85  
  86      def test_pointsshape(self): # Also test _Shape
  87          class ShapeTest(_geo._PointsShape):
  88              def __init__(self):
  89                  super().__init__()
  90  
  91              @property
  92              def pointsshapes(self) -> Iterable[_geo._PointsShape]:
  93                  return super().pointsshapes
  94  
  95              @property
  96              def bounds(self) -> _geo._Rectangular:
  97                  return super().bounds
  98  
  99              def moved(self, *, dxy: _geo.Point):
 100                  return super().moved(dxy=dxy)
 101              
 102              def rotated(self, *, rotation: _geo.Rotation) -> _geo._Shape:
 103                  return super().rotated(rotation=rotation)
 104  
 105              @property
 106              def area(self) -> float:
 107                  return super().area
 108  
 109              def __eq__(self, o: object) -> bool:
 110                  return super().__eq__(o)
 111  
 112              @property
 113              def points(self) -> Iterable[_geo.Point]:
 114                  return super().points
 115  
 116          t = ShapeTest()
 117          with self.assertRaises(NotImplementedError):
 118              t.pointsshapes
 119          with self.assertRaises(NotImplementedError):
 120              t.bounds
 121          with self.assertRaises(NotImplementedError):
 122              t.moved(dxy=_geo.origin)
 123          with self.assertRaises(NotImplementedError):
 124              t.rotated(rotation=_geo.Rotation.R0)
 125          with self.assertRaises(NotImplementedError):
 126              t.area
 127          with self.assertRaises(NotImplementedError):
 128              _geo._Shape.__eq__(t, None)
 129          with self.assertRaises(NotImplementedError):
 130              t.points
 131          with self.assertRaises(NotImplementedError):
 132              _geo._Shape.__hash__(t)
 133  
 134          with self.assertRaisesRegex(
 135              TypeError,
 136              f"unsupported operand type\(s\) for \+: "
 137              f"'{t.__class__.__name__}' and '{int.__name__}'"
 138          ):
 139              t + 1
 140          with self.assertRaisesRegex(
 141              TypeError,
 142              f"unsupported operand type\(s\) for \-: "
 143              f"'{t.__class__.__name__}' and '{int.__name__}'"
 144          ):
 145              t - 1
 146  
 147          self.assertNotEqual(t, 1)
 148  
 149      def test_rectangular(self):
 150          class RectangularTest(_geo._Rectangular):
 151              def __init__(self):
 152                  super().__init__()
 153  
 154              # _Shape abstract methods
 155              @property
 156              def pointsshapes(self) -> Iterable[_geo._PointsShape]:
 157                  return super().pointsshapes
 158              @property
 159              def bounds(self) -> _geo._Rectangular:
 160                  return super().bounds
 161              def moved(self, *, dxy: _geo.Point):
 162                  return super().moved(dxy)
 163              def rotated(self, *, rotation: _geo.Rotation) -> _geo._Shape:
 164                  return super().rotated(rotation)
 165              @property
 166              def area(self) -> float:
 167                  return super().area
 168              def __eq__(self, o: object) -> bool:
 169                  return super().__eq__(o)
 170  
 171              @property
 172              def left(self) -> float:
 173                  return super().left
 174              @property
 175              def bottom(self) -> float:
 176                  return super().bottom
 177              @property
 178              def right(self) -> float:
 179                  return super().right
 180              @property
 181              def top(self) -> float:
 182                  return super().top
 183  
 184          t = RectangularTest()
 185          with self.assertRaises(NotImplementedError):
 186              t.left
 187          with self.assertRaises(NotImplementedError):
 188              t.bottom
 189          with self.assertRaises(NotImplementedError):
 190              t.right
 191          with self.assertRaises(NotImplementedError):
 192              t.top
 193          with self.assertRaises(NotImplementedError):
 194              self.assertNotEqual(t, 1)
 195  
 196      def test_point(self):
 197          p = _geo.Point(x=0.0, y=0.0)
 198          self.assertTrue((abs(p.x) < _geo.epsilon) and (abs(p.y) < _geo.epsilon))
 199          self.assertEqual(p.area, 0.0)
 200          self.assertNotEqual(p, 1)
 201  
 202          p += _geo.Point.from_float(point=(1.0, 2.0))
 203          self.assertEqual(p, _geo.Point(x=1.0, y=2.0))
 204  
 205          p = _geo.Point.from_point(point=p, x=-1.0)
 206          self.assertEqual(p, _geo.Point(x=-1.0, y=2.0))
 207  
 208          p = _geo.Point.from_point(point=p, y=-p.y)
 209          self.assertEqual(p, _geo.Point(x=-1.0, y=-2.0))
 210  
 211          first = True
 212          for p2 in p.pointsshapes:
 213              self.assertTrue(first)
 214              first = False
 215              self.assertEqual(p2, p)
 216          first = True
 217          for p2 in p.points:
 218              self.assertTrue(first)
 219              first = False
 220              self.assertEqual(p2, p)
 221  
 222          self.assertEqual(p - p, _geo.Point(x=0.0, y=0.0))
 223          with self.assertRaisesRegex(
 224              TypeError,
 225              "unsupported operand type\(s\) for \+: "
 226              f"'{p.__class__.__name__}' and 'str'",
 227          ):
 228              p + "a"
 229          with self.assertRaisesRegex(
 230              TypeError,
 231              "unsupported operand type\(s\) for \-: "
 232              f"'float' and '{p.__class__.__name__}'",
 233          ):
 234              3.14 - p
 235          with self.assertRaisesRegex(
 236              TypeError,
 237              "unsupported operand type\(s\) for \-: "
 238              f"'{p.__class__.__name__}' and 'float'",
 239          ):
 240              p - 3.14
 241  
 242          self.assertEqual(-2*p, _geo.Point(x=2.0, y=4.0))
 243          with self.assertRaisesRegex(
 244              TypeError,
 245              f"unsupported operand type\(s\) for \*: "
 246              f"'{p.__class__.__name__}' and '{p.__class__.__name__}'",
 247          ):
 248              p*p
 249  
 250          p2 = p.rotated(rotation=_geo.Rotation.R0)
 251          self.assertEqual(p, _geo.Point(x=-1.0, y=-2.0))
 252          self.assertEqual(p, p2)
 253          p2 = p.rotated(rotation=_geo.Rotation.R90)
 254          self.assertEqual(p, _geo.Point(x=-1.0, y=-2.0))
 255          self.assertEqual(p2, _geo.Point(x=2.0, y=-1.0))
 256          p2 = p.rotated(rotation=_geo.Rotation.R180)
 257          self.assertEqual(p, _geo.Point(x=-1.0, y=-2.0))
 258          self.assertEqual(p2, _geo.Point(x=1.0, y=2.0))
 259          p2 = p.rotated(rotation=_geo.Rotation.R270)
 260          self.assertEqual(p, _geo.Point(x=-1.0, y=-2.0))
 261          self.assertEqual(p2, _geo.Point(x=-2.0, y=1.0))
 262          p2 = p.rotated(rotation=_geo.Rotation.MX)
 263          self.assertEqual(p, _geo.Point(x=-1.0, y=-2.0))
 264          self.assertEqual(p2, _geo.Point(x=-1.0, y=2.0))
 265          p2 = p.rotated(rotation=_geo.Rotation.MX90)
 266          self.assertEqual(p, _geo.Point(x=-1.0, y=-2.0))
 267          self.assertEqual(p2, _geo.Point(x=-2.0, y=-1.0))
 268          p2 = p.rotated(rotation=_geo.Rotation.MY)
 269          self.assertEqual(p, _geo.Point(x=-1.0, y=-2.0))
 270          self.assertEqual(p2, _geo.Point(x=1.0, y=-2.0))
 271          p2 = p.rotated(rotation=_geo.Rotation.MY90)
 272          self.assertEqual(p, _geo.Point(x=-1.0, y=-2.0))
 273          self.assertEqual(p2, _geo.Point(x=2.0, y=1.0))
 274          rot = _geo.Rotation.MY90
 275          self.assertEqual(p*rot, rot*p)
 276  
 277          self.assertEqual(str(p), f"({str(p.x)},{str(p.y)})")
 278          self.assertEqual(repr(p), f"Point(x={p.x},y={p.y})")
 279  
 280      def test_line(self):
 281          p1 = _geo.Point(x=0.0, y=0.0)
 282          p2 = _geo.Point(x=1.0, y=-1.0)
 283          l = _geo.Line(point1=p1, point2=p2)
 284  
 285          self.assertEqual(l.point1, p1)
 286          self.assertEqual(l.point2, p2)
 287          self.assertEqual(l.area, 0.0)
 288  
 289          first = True
 290          for l2 in l.pointsshapes:
 291              self.assertTrue(first)
 292              first = False
 293              self.assertEqual(l, l2)
 294          ps = l.points
 295          self.assertEqual(len(ps), 2)
 296          self.assertEqual(ps[0], p1)
 297          self.assertEqual(ps[1], p2)
 298  
 299          self.assertEqual(l.bounds, l)
 300  
 301          self.assertEqual(
 302              l.rotated(rotation=_geo.Rotation.R90),
 303              _geo.Line(
 304                  point1=p1.rotated(rotation=_geo.Rotation.R90),
 305                  point2=p2.rotated(rotation=_geo.Rotation.R90),
 306              ),
 307          )
 308  
 309          dxy = _geo.Point(x=1.0, y=1.0)
 310          self.assertEqual(
 311              l.moved(dxy=dxy),
 312              _geo.Line(point1=p1.moved(dxy=dxy), point2=p2.moved(dxy=dxy)),
 313          )
 314          self.assertEqual(-dxy + l, l - dxy)
 315  
 316          self.assertEqual(str(l), f"{p1}-{p2}")
 317          self.assertEqual(repr(l), f"Line(point1={p1!r},point2={p2!r})")
 318  
 319      def test_polygon(self):
 320          with self.assertRaisesRegex(
 321              ValueError, "Last point has to be the same as the first point"
 322          ):
 323              _geo.Polygon(points=(
 324                  _geo.Point(x=0.0, y=0.0), _geo.Point(x=0.0, y=1.0),
 325                  _geo.Point(x=1.0, y=1.0), _geo.Point(x=1.0, y=0.0),
 326              ))
 327          with self.assertRaisesRegex(
 328              ValueError, "Polygon with only colinear points not allowed"
 329          ):
 330              _geo.Polygon(points=(
 331                  _geo.Point(x=0.0, y=0.0), _geo.Point(x=0.0, y=1.0),
 332                  _geo.Point(x=0.0, y=0.5), _geo.Point(x=0.0, y=0.0),
 333              ))
 334              
 335          poly1 = _geo.Polygon(points=(
 336              _geo.Point(x=0.0, y=0.0), _geo.Point(x=0.0, y=1.0),
 337              _geo.Point(x=1.0, y=1.0), _geo.Point(x=1.0, y=0.0),
 338              _geo.Point(x=0.0, y=0.0),
 339          ))
 340          poly2_points = ((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0))
 341          poly2 = _geo.Polygon.from_floats(points=poly2_points)
 342          line1 = _geo.Line(
 343              point1=_util.get_first_of(poly1.points),
 344              point2=_util.get_nth_of(poly1.points, n=1),
 345          )
 346          line2 = _geo.Line(
 347              point1=_util.get_first_of(poly1.points),
 348              point2=_util.get_nth_of(poly1.points, n=2),
 349          )
 350  
 351          with self.assertRaises(NotImplementedError):
 352              poly1.area
 353          with self.assertRaisesRegex(
 354              TypeError, (
 355                  "unsupported operand type\(s\) for \+: "
 356                  f"'{poly1.__class__.__name__}' and '{poly2.__class__.__name__}'"
 357              )
 358          ):
 359              poly3 = poly1 + poly2
 360  
 361          self.assertEqual(poly1, poly2)
 362          self.assertEqual(str(poly2), f"""{{{
 363              "=".join(str(_geo.Point.from_float(point=p)) for p in poly2_points)
 364          }}}""")
 365          self.assertEqual(repr(poly2), f"""Polygon(points=({
 366              ",".join(repr(_geo.Point.from_float(point=p)) for p in poly2_points)
 367          }))""")
 368          self.assertNotEqual(poly1, line1)
 369          self.assertNotEqual(poly1, line2)
 370          self.assertEqual(
 371              poly1.bounds,
 372              _geo.Rect(left=0.0, bottom=0.0, right=1.0, top=1.0),
 373          )
 374          self.assertNotEqual(line1, poly1)
 375          self.assertEqual(
 376              poly1.moved(dxy=_geo.Point(x=1.0, y=1.0)),
 377              _geo.Polygon.from_floats(points=(
 378                  (1.0, 1.0), (1.0, 2.0), (2.0, 2.0), (2.0, 1.0), (1.0, 1.0),
 379              ))
 380          )
 381          self.assertEqual(
 382              poly1.rotated(rotation=_geo.Rotation.R90),
 383              _geo.Polygon.from_floats(points=(
 384                  (0.0, 0.0), (-1.0, 0.0), (-1.0, 1.0), (0.0, 1.0), (0.0, 0.0),
 385              ))
 386          )
 387  
 388      def test_rect(self):
 389          with self.assertRaises(AssertionError):
 390              _geo.Rect(left=0.0, bottom=0.0, right=0.0, top=1.0)
 391          with self.assertRaises(AssertionError):
 392              _geo.Rect.from_size(width=1.0, height=-1.0)
 393  
 394          rect1 = _geo.Rect(left=-1.0, bottom=-1.0, right=1.0, top=1.0)
 395          rect2 = _geo.Rect.from_size(width=2.0, height=2.0)
 396          rect3 = _geo.Rect.from_corners(
 397              corner1=_geo.Point(x=0.0, y=0.0), corner2=_geo.Point(x=2.0, y=2.0),
 398          )
 399          rect4 = _geo.Rect.from_floats(values=(0.0, 0.0, 2.0, 2.0))
 400          rect5 = _geo.Rect.from_rect(rect=rect1, bias=1.0)
 401          rect6 = _geo.Rect.from_rect(rect=rect3, left=-2.0, bottom=-2.0)
 402          rect7 = _geo.Rect.from_float_corners(corners=((-2.0, -2.0), (2.0, 2.0)))
 403  
 404          with self.assertRaisesRegex(
 405              RuntimeError,
 406              f"Internal error: unsupported rotation 'None'"
 407          ):
 408              rect1.rotated(rotation=None)
 409  
 410          self.assertEqual(
 411              str(rect1),
 412              f"[{str(_geo.Point(x=-1.0, y=-1.0))}-{str(_geo.Point(x=1.0, y=1.0))}]",
 413          )
 414          self.assertEqual(
 415              repr(rect1), "Rect(left=-1.0,bottom=-1.0,right=1.0,top=1.0)",
 416          )
 417          self.assertEqual(
 418              _util.get_nth_of(rect1.points, n=1),
 419              _geo.Point(x=rect1.left, y=rect1.top),
 420          )
 421          self.assertEqual(rect1, rect2)
 422          self.assertNotEqual(rect1, 1)
 423          self.assertEqual(rect1, rect1.rotated(rotation=_geo.Rotation.MX90))
 424          self.assertEqual(round(rect1.area, 6), 4.0)
 425          self.assertEqual(rect3, rect4)
 426          self.assertEqual(rect1.moved(dxy=_geo.Point(x=1.0, y=1.0)), rect3)
 427          self.assertEqual(rect5, rect6)
 428          self.assertEqual(rect5, rect7)
 429  
 430      def test_ring(self):
 431          rect1 = _geo.Rect(left=0.0, bottom=0.0, right=3.0, top=3.0)
 432          rect2 = _geo.Rect(left=0.0, bottom=0.0, right=2.0, top=3.0)
 433          rect3 = _geo.Rect(left=0.0, bottom=0.0, right=3.0, top=2.0)
 434  
 435          with self.assertRaises(ValueError):
 436              _geo.Ring(outer_bound=rect2, ring_width=1.0)
 437          with self.assertRaises(ValueError):
 438              _geo.Ring(outer_bound=rect3, ring_width=1.0)
 439  
 440          ring = _geo.Ring(outer_bound=rect1, ring_width=1.0)
 441          polygon = _geo.Polygon.from_floats(points=(
 442              (0.0, 0.0),
 443              (0.0, 3.0),
 444              (3.0, 3.0),
 445              (3.0, 0.0),
 446              (1.0, 0.0),
 447              (1.0, 1.0),
 448              (2.0, 1.0),
 449              (2.0, 2.0),
 450              (1.0, 2.0),
 451              (1.0, 0.0),
 452              (0.0, 0.0),
 453          ))
 454          self.assertEqual(ring, polygon)
 455  
 456      def test_rectring(self):
 457          bb = _geo.Rect.from_size(center=_geo.Point(x=1.0, y=1.5), width=2, height=3)
 458          bb2 = _geo.Rect.from_size(center=_geo.Point(x=1.5, y=1.0), width=3, height=2)
 459          dxy = _geo.Point(x=1.0, y=1.0)
 460  
 461          with self.assertRaises(ValueError):
 462              _geo.RectRing(outer_bound=bb, rect_width=1.0, min_rect_space=1.0)
 463          with self.assertRaises(ValueError):
 464              _geo.RectRing(outer_bound=bb2, rect_width=1.0, min_rect_space=1.0)
 465  
 466          ring1 = _geo.RectRing(outer_bound=bb, rect_width=0.5, min_rect_space=0.5)
 467          ring2 = _geo.RectRing(
 468              outer_bound=bb, rect_width=0.5, rect_height=0.5, min_rect_space=0.5,
 469          )
 470          ring3 = _geo.RectRing(outer_bound=bb, rect_width=0.5, min_rect_space=1.0)
 471          ring4 = _geo.RectRing(outer_bound=bb2, rect_width=0.5, min_rect_space=0.5)
 472          ring5 = _geo.RectRing(outer_bound=(bb + dxy), rect_width=0.5, min_rect_space=0.5)
 473          ring6 = _geo.RectRing(
 474              outer_bound=_geo.Rotation.MX*bb, rect_width=0.5, min_rect_space=0.5,
 475          )
 476  
 477          self.assertEqual(ring1, ring2)
 478          self.assertNotEqual(ring1, ring3)
 479          self.assertNotEqual(ring1, ring4)
 480  
 481          self.assertNotEqual(ring1, 1.0)
 482  
 483          self.assertEqual(hash(ring1), hash(ring2))
 484          self.assertEqual(repr(ring1), repr(ring2))
 485          self.assertNotEqual(hash(ring1), hash(ring3))
 486  
 487          self.assertEqual(ring1.bounds, bb)
 488  
 489          self.assertEqual(ring1 + dxy, ring5)
 490          self.assertEqual(_geo.Rotation.MX*ring1, ring6)
 491  
 492          rect = _geo.Rect.from_size(width=0.5, height=0.5)
 493          self.assertEqual(
 494              set(ring1.pointsshapes),
 495              set(
 496                  rect + p for p in (
 497                      _geo.Point(x=0.25, y=0.25),
 498                      _geo.Point(x=0.25, y=1.50),
 499                      _geo.Point(x=0.25, y=2.75),
 500                      _geo.Point(x=1.75, y=0.25),
 501                      _geo.Point(x=1.75, y=1.50),
 502                      _geo.Point(x=1.75, y=2.75),
 503                  )
 504              )
 505          )
 506          self.assertEqual(
 507              set(ring4.pointsshapes),
 508              set(
 509                  rect + p for p in (
 510                      _geo.Point(x=0.25, y=0.25),
 511                      _geo.Point(x=0.25, y=1.75),
 512                      _geo.Point(x=1.50, y=0.25),
 513                      _geo.Point(x=1.50, y=1.75),
 514                      _geo.Point(x=2.75, y=0.25),
 515                      _geo.Point(x=2.75, y=1.75),
 516                  )
 517              )
 518          )
 519          self.assertEqual(ring1.area, 6*rect.area)
 520  
 521      def test_label(self):
 522          p = _geo.Point(x=0.0, y=1.0)
 523          lbl1 = _geo.Label(origin=_geo.origin, text="lbl1")
 524          lbl1bis = _geo.Label(origin=_geo.origin, text="lbl1")
 525          lbl2 = _geo.Label(origin=_geo.origin, text="lbl2")
 526          lbl3 = _geo.Label(origin=p, text="lbl1")
 527  
 528          self.assertNotEqual(lbl1, _geo.origin)
 529          self.assertEqual(lbl1, lbl1bis)
 530          self.assertNotEqual(lbl1, lbl2)
 531          self.assertNotEqual(lbl1, lbl3)
 532          self.assertNotEqual(lbl1, lbl1.moved(dxy=p))
 533          self.assertEqual(lbl1.moved(dxy=p), lbl3)
 534          self.assertEqual(lbl1, lbl1.rotated(rotation=_geo.Rotation.MX90))
 535  
 536          self.assertEqual(hash(lbl1), hash((lbl1.origin, lbl1.text)))
 537  
 538          self.assertEqual(lbl1.pointsshapes, lbl1.origin.pointsshapes)
 539          self.assertEqual(lbl1.bounds, lbl1.origin.bounds)
 540          self.assertEqual(lbl1.area, lbl1.origin.area)
 541  
 542      def test_multipartshape(self):
 543          r_all = _geo.Rect(left=-2.0, bottom=-1.0, right=1.0, top=1.0)
 544          r_left = _geo.Rect(left=-2.0, bottom=-1.0, right=0.0, top=1.0)
 545          r_right = _geo.Rect(left=0.0, bottom=-1.0, right=1.0, top=1.0)
 546          mps = _geo.MultiPartShape(fullshape=r_all, parts=(r_left, r_right))
 547          r2_all = _geo.Rect(left=0.0, bottom=-1.0, right=2.0, top=1.0)
 548          r2_left = r_right
 549          r2_right = _geo.Rect(left=1.0, bottom=-1.0, right=2.0, top=1.0)
 550          mps2 = _geo.MultiPartShape(fullshape=r2_all, parts=(r2_left, r2_right))
 551  
 552          self.assertEqual(_util.get_first_of(mps.pointsshapes), r_all)
 553          self.assertEqual(mps.bounds, r_all)
 554  
 555          part0 = _util.get_first_of(mps.parts)
 556          part1 = _util.get_nth_of(mps.parts, n=1)
 557          part0_2 = _util.get_first_of(mps2.parts)
 558          part1_2 = _util.get_nth_of(mps2.parts, n=1)
 559  
 560          self.assertNotEqual(part0, 3.14)
 561          self.assertNotEqual(mps, "")
 562          self.assertEqual(part0.partshape, r_left)
 563          self.assertEqual(tuple(part0.points), tuple(r_left.points))
 564          self.assertEqual(part0.area, r_left.area)
 565          self.assertEqual(part1.multipartshape, mps)
 566          self.assertEqual(tuple(part1.pointsshapes), (part1,))
 567          self.assertEqual(part1.bounds, r_right)
 568          self.assertEqual(mps.points, mps.fullshape.points)
 569          self.assertEqual(mps.area, part0.area + part1.area)
 570          self.assertEqual(part1.partshape, part0_2.partshape)
 571          self.assertNotEqual(part1, part0_2)
 572  
 573          self.assertEqual(str(part0), f"<<{str(part0.partshape)}>>")
 574          self.assertEqual(
 575              repr(part0),
 576              f"MultiPartShape._Part(partshape={repr(part0.partshape)})",
 577          )
 578          s = "|".join(str(p.partshape) for p in mps.parts)
 579          self.assertEqual(str(mps), f"({s})")
 580          s = repr(mps.fullshape)
 581          s2 = ",".join(repr(p.partshape) for p in mps.parts)
 582          self.assertEqual(repr(mps), f"MultiPartShape(fullshape={s},parts=({s2}))")
 583  
 584          self.assertEqual({part0, part1}, {part1, part0})
 585  
 586          p = _geo.Point(x=-2.0, y=3.5)
 587          part0_moved = part0 + p
 588          self.assertEqual(mps + p, mps.moved(dxy=p))
 589          self.assertEqual(part0_moved.multipartshape, p + mps)
 590  
 591          rot = _geo.Rotation.MX90
 592          part1_rotated = rot*part1
 593          self.assertEqual(rot * mps, mps.rotated(rotation=rot))
 594          self.assertEqual(part1_rotated.multipartshape, mps*rot)
 595  
 596      def test_multipath_errors(self):
 597          # Negative with/distance values
 598          with self.assertRaises(ValueError):
 599              _geo.Start(point=_geo.origin, width=-1.0)
 600          with self.assertRaises(ValueError):
 601              _geo.SetWidth(-1.0)
 602          with self.assertRaises(ValueError):
 603              _geo.GoLeft(-1.0)
 604  
 605          # Only Start
 606          with self.assertRaises(ValueError):
 607              _geo.MultiPath(_geo.Start(point=_geo.origin, width=1.0))
 608  
 609          # Start not at start
 610          with self.assertRaises(ValueError):
 611              _geo.MultiPath(
 612                  _geo.Start(point=_geo.origin, width=1.0),
 613                  _geo.Start(point=_geo.origin, width=1.0),
 614              )
 615          with self.assertRaises(ValueError):
 616              _geo.MultiPath(
 617                  _geo.Start(point=_geo.origin, width=1.0),
 618                  _geo.GoLeft(2.0),
 619                  _geo.Start(point=_geo.origin, width=1.0),
 620              )
 621  
 622          # SetWidth right after Start
 623          with self.assertRaises(ValueError):
 624              _geo.MultiPath(
 625                  _geo.Start(point=_geo.origin, width=1.0),
 626                  _geo.SetWidth(width=2.0),
 627              )
 628  
 629          # SetWidth as last instruction
 630          with self.assertRaises(ValueError):
 631              _geo.MultiPath(
 632                  _geo.Start(point=_geo.origin, width=1.0),
 633                  _geo.GoLeft(2.0),
 634                  _geo.SetWidth(2.0),
 635              )
 636          with self.assertRaises(ValueError):
 637              _geo.MultiPath(
 638                  _geo.Start(point=_geo.origin, width=1.0),
 639                  _geo.GoLeft(2.0),
 640                  _geo.SetWidth(2.0),
 641                  _geo.GoLeft(2.0),
 642                  _geo.SetWidth(2.0),
 643              )
 644  
 645          # Repeated instruction
 646          with self.assertRaises(ValueError):
 647              _geo.MultiPath(
 648                  _geo.Start(point=_geo.origin, width=1.0),
 649                  _geo.GoLeft(2.0),
 650                  _geo.GoLeft(2.0),
 651              )
 652  
 653          # Only one direction for Knot
 654          with self.assertRaises(TypeError):
 655              _geo.Knot(left=(_geo.GoLeft(1.0),))
 656  
 657          # Instruction after Knot
 658          with self.assertRaises(ValueError):
 659              _geo.MultiPath(
 660                  _geo.Start(point=_geo.origin, width=1.0),
 661                  _geo.GoUp(2.0),
 662                  _geo.Knot(up=_geo.GoUp(2.0), right=_geo.GoRight(2.0)),
 663                  _geo.GoUp(2.0),
 664              )
 665  
 666      def test_multipath_compare(self):
 667          # Start
 668          self.assertEqual(
 669              _geo.Start(point=_geo.origin, width=1.0),
 670              _geo.Start(point=_geo.origin, width=1.0),
 671          )
 672          self.assertNotEqual(
 673              _geo.Start(point=_geo.origin, width=1.0),
 674              1,
 675          )
 676          # SetWidth
 677          self.assertEqual(
 678              _geo.SetWidth(width=1.0),
 679              _geo.SetWidth(width=1.0),
 680          )
 681          self.assertNotEqual(
 682              _geo.SetWidth(width=1.0),
 683              1,
 684          )
 685          # _Go
 686          self.assertEqual(
 687              _geo.GoLeft(1.0),
 688              _geo.GoLeft(1.0),
 689          )
 690          self.assertNotEqual(
 691              _geo.GoLeft(1.0),
 692              _geo.GoRight(1.0),
 693          )
 694          self.assertNotEqual(
 695              _geo.GoLeft(1.0),
 696              1,
 697          )
 698  
 699      def test_multipath_properties(self):
 700          s = _geo.Start(point=_geo.origin, width=1.0)
 701          is_ = (
 702              _geo.GoLeft(1.0),
 703          )
 704          mp = _geo.MultiPath(s, *is_)
 705          self.assertEqual(s, mp.first)
 706          self.assertEqual(is_, mp.instrs)
 707  
 708      def test_multipath_instrs(self):
 709          # GoLeft
 710          self.assertEqual(
 711              _geo.MultiPath(
 712                  _geo.Start(point=_geo.origin, width=2.0),
 713                  _geo.GoLeft(1.0),
 714              ),
 715              _geo.Polygon.from_floats(points=(
 716                  (0.0, -1.0),
 717                  (-1.0, -1.0),
 718                  (-1.0, 1.0),
 719                  (0.0, 1.0),
 720                  (0.0, -1.0),
 721              ))
 722          )
 723          # GoDown
 724          self.assertEqual(
 725              _geo.MultiPath(
 726                  _geo.Start(point=_geo.origin, width=2.0),
 727                  _geo.GoDown(1.0),
 728              ),
 729              _geo.Polygon.from_floats(points=(
 730                  (1.0, 0.0),
 731                  (1.0, -1.0),
 732                  (-1.0, -1.0),
 733                  (-1.0, 0.0),
 734                  (1.0, 0.0),
 735              ))
 736          )
 737          # GoRight
 738          self.assertEqual(
 739              _geo.MultiPath(
 740                  _geo.Start(point=_geo.origin, width=2.0),
 741                  _geo.GoRight(1.0),
 742              ),
 743              _geo.Polygon.from_floats(points=(
 744                  (0.0, 1.0),
 745                  (1.0, 1.0),
 746                  (1.0, -1.0),
 747                  (0.0, -1.0),
 748                  (0.0, 1.0),
 749              ))
 750          )
 751          # GoUp
 752          self.assertEqual(
 753              _geo.MultiPath(
 754                  _geo.Start(point=_geo.origin, width=2.0),
 755                  _geo.GoUp(1.0),
 756              ),
 757              _geo.Polygon.from_floats(points=(
 758                  (-1.0, 0.0),
 759                  (-1.0, 1.0),
 760                  (1.0, 1.0),
 761                  (1.0, 0.0),
 762                  (-1.0, 0.0),
 763              ))
 764          )
 765  
 766          # GoLeft, SetWidth, GoLeft
 767          self.assertEqual(
 768              _geo.MultiPath(
 769                  _geo.Start(point=_geo.origin, width=2.0),
 770                  _geo.GoLeft(1.0),
 771                  _geo.SetWidth(4.0),
 772                  _geo.GoLeft(1.0),
 773              ),
 774              _geo.Polygon.from_floats(points=(
 775                  (0.0, -1.0),
 776                  (-1.0, -1.0),
 777                  (-1.0, -2.0),
 778                  (-2.0, -2.0),
 779                  (-2.0, 2.0),
 780                  (-1.0, 2.0),
 781                  (-1.0, 1.0),
 782                  (0.0, 1.0),
 783                  (0.0, -1.0),
 784              )),
 785          )
 786          # GoLeft, GoDown
 787          self.assertEqual(
 788              _geo.MultiPath(
 789                  _geo.Start(point=_geo.origin, width=2.0),
 790                  _geo.GoLeft(2.0),
 791                  _geo.GoDown(2.0),
 792              ),
 793              _geo.Polygon.from_floats(points=(
 794                  (0.0, -1.0),
 795                  (-1.0, -1.0),
 796                  (-1.0, -2.0),
 797                  (-3.0, -2.0),
 798                  (-3.0, 1.0),
 799                  (0.0, 1.0),
 800                  (0.0, -1.0),
 801              )),
 802          )
 803          # GoLeft, SetWidth, GoDown
 804          self.assertEqual(
 805              _geo.MultiPath(
 806                  _geo.Start(point=_geo.origin, width=2.0),
 807                  _geo.GoLeft(3.0),
 808                  _geo.SetWidth(4.0),
 809                  _geo.GoDown(2.0),
 810              ),
 811              _geo.Polygon.from_floats(points=(
 812                  (0.0, -1.0),
 813                  (-1.0, -1.0),
 814                  (-1.0, -2.0),
 815                  (-5.0, -2.0),
 816                  (-5.0, 1.0),
 817                  (0.0, 1.0),
 818                  (0.0, -1.0),
 819              )),
 820          )
 821          # GoLeft, GoRight
 822          with self.assertRaises(ValueError):
 823              _geo.MultiPath(
 824                  _geo.Start(point=_geo.origin, width=2.0),
 825                  _geo.GoLeft(2.0),
 826                  _geo.GoRight(2.0),
 827              ),
 828          # GoLeft, GoUp
 829          self.assertEqual(
 830              _geo.MultiPath(
 831                  _geo.Start(point=_geo.origin, width=2.0),
 832                  _geo.GoLeft(2.0),
 833                  _geo.GoUp(2.0),
 834              ),
 835              _geo.Polygon.from_floats(points=(
 836                  (0.0, -1.0),
 837                  (-3.0, -1.0),
 838                  (-3.0, 2.0),
 839                  (-1.0, 2.0),
 840                  (-1.0, 1.0),
 841                  (0.0, 1.0),
 842                  (0.0, -1.0),
 843              )),
 844          )
 845          # GoLeft, SetWidth, GoUp
 846          self.assertEqual(
 847              _geo.MultiPath(
 848                  _geo.Start(point=_geo.origin, width=2.0),
 849                  _geo.GoLeft(3.0),
 850                  _geo.SetWidth(4.0),
 851                  _geo.GoUp(2.0),
 852              ),
 853              _geo.Polygon.from_floats(points=(
 854                  (0.0, -1.0),
 855                  (-5.0, -1.0),
 856                  (-5.0, 2.0),
 857                  (-1.0, 2.0),
 858                  (-1.0, 1.0),
 859                  (0.0, 1.0),
 860                  (0.0, -1.0),
 861              )),
 862          )
 863  
 864          # GoDown, GoLeft
 865          self.assertEqual(
 866              _geo.MultiPath(
 867                  _geo.Start(point=_geo.origin, width=2.0),
 868                  _geo.GoDown(2.0),
 869                  _geo.GoLeft(2.0),
 870              ),
 871              _geo.Polygon.from_floats(points=(
 872                  (1.0, 0.0),
 873                  (1.0, -3.0),
 874                  (-2.0, -3.0),
 875                  (-2.0, -1.0),
 876                  (-1.0, -1.0),
 877                  (-1.0, 0.0),
 878                  (1.0, 0.0),
 879              )),
 880          )
 881          # GoDown, SetWidth, GoLeft
 882          self.assertEqual(
 883              _geo.MultiPath(
 884                  _geo.Start(point=_geo.origin, width=2.0),
 885                  _geo.GoDown(3.0),
 886                  _geo.SetWidth(4.0),
 887                  _geo.GoLeft(2.0),
 888              ),
 889              _geo.Polygon.from_floats(points=(
 890                  (1.0, 0.0),
 891                  (1.0, -5.0),
 892                  (-2.0, -5.0),
 893                  (-2.0, -1.0),
 894                  (-1.0, -1.0),
 895                  (-1.0, 0.0),
 896                  (1.0, 0.0),
 897              )),
 898          )
 899          # GoDown, SetWidth, GoDown
 900          self.assertEqual(
 901              _geo.MultiPath(
 902                  _geo.Start(point=_geo.origin, width=2.0),
 903                  _geo.GoDown(1.0),
 904                  _geo.SetWidth(4.0),
 905                  _geo.GoDown(1.0),
 906              ),
 907              _geo.Polygon.from_floats(points=(
 908                  (1.0, 0.0),
 909                  (1.0, -1.0),
 910                  (2.0, -1.0),
 911                  (2.0, -2.0),
 912                  (-2.0, -2.0),
 913                  (-2.0, -1.0),
 914                  (-1.0, -1.0),
 915                  (-1.0, 0.0),
 916                  (1.0, 0.0),
 917              )),
 918          )
 919          # GoDown, GoRight
 920          self.assertEqual(
 921              _geo.MultiPath(
 922                  _geo.Start(point=_geo.origin, width=2.0),
 923                  _geo.GoDown(2.0),
 924                  _geo.GoRight(2.0),
 925              ),
 926              _geo.Polygon.from_floats(points=(
 927                  (1.0, 0.0),
 928                  (1.0, -1.0),
 929                  (2.0, -1.0),
 930                  (2.0, -3.0),
 931                  (-1.0, -3.0),
 932                  (-1.0, 0.0),
 933                  (1.0, 0.0),
 934              )),
 935          )
 936          # GoDown, SetWidth, GoRight
 937          self.assertEqual(
 938              _geo.MultiPath(
 939                  _geo.Start(point=_geo.origin, width=2.0),
 940                  _geo.GoDown(3.0),
 941                  _geo.SetWidth(4.0),
 942                  _geo.GoRight(2.0),
 943              ),
 944              _geo.Polygon.from_floats(points=(
 945                  (1.0, 0.0),
 946                  (1.0, -1.0),
 947                  (2.0, -1.0),
 948                  (2.0, -5.0),
 949                  (-1.0, -5.0),
 950                  (-1.0, 0.0),
 951                  (1.0, 0.0),
 952              )),
 953          )
 954          # GoDown, GoUp
 955          with self.assertRaises(ValueError):
 956              _geo.MultiPath(
 957                  _geo.Start(point=_geo.origin, width=2.0),
 958                  _geo.GoDown(2.0),
 959                  _geo.GoUp(2.0),
 960              ),
 961  
 962          # GoRight, GoLeft
 963          with self.assertRaises(ValueError):
 964              _geo.MultiPath(
 965                  _geo.Start(point=_geo.origin, width=2.0),
 966                  _geo.GoRight(2.0),
 967                  _geo.GoLeft(2.0),
 968              ),
 969          # GoRight, GoDown
 970          self.assertEqual(
 971              _geo.MultiPath(
 972                  _geo.Start(point=_geo.origin, width=2.0),
 973                  _geo.GoRight(2.0),
 974                  _geo.GoDown(2.0),
 975              ),
 976              _geo.Polygon.from_floats(points=(
 977                  (0.0, 1.0),
 978                  (3.0, 1.0),
 979                  (3.0, -2.0),
 980                  (1.0, -2.0),
 981                  (1.0, -1.0),
 982                  (0.0, -1.0),
 983                  (0.0, 1.0),
 984              )),
 985          )
 986          # GoRight, SetWidth, GoDown
 987          self.assertEqual(
 988              _geo.MultiPath(
 989                  _geo.Start(point=_geo.origin, width=2.0),
 990                  _geo.GoRight(3.0),
 991                  _geo.SetWidth(4.0),
 992                  _geo.GoDown(2.0),
 993              ),
 994              _geo.Polygon.from_floats(points=(
 995                  (0.0, 1.0),
 996                  (5.0, 1.0),
 997                  (5.0, -2.0),
 998                  (1.0, -2.0),
 999                  (1.0, -1.0),
1000                  (0.0, -1.0),
1001                  (0.0, 1.0),
1002              )),
1003          )
1004          # GoRight, SetWidth, GoRight
1005          self.assertEqual(
1006              _geo.MultiPath(
1007                  _geo.Start(point=_geo.origin, width=2.0),
1008                  _geo.GoRight(1.0),
1009                  _geo.SetWidth(4.0),
1010                  _geo.GoRight(1.0),
1011              ),
1012              _geo.Polygon.from_floats(points=(
1013                  (0.0, 1.0),
1014                  (1.0, 1.0),
1015                  (1.0, 2.0),
1016                  (2.0, 2.0),
1017                  (2.0, -2.0),
1018                  (1.0, -2.0),
1019                  (1.0, -1.0),
1020                  (0.0, -1.0),
1021                  (0.0, 1.0),
1022              )),
1023          )
1024          # GoRight, GoUp
1025          self.assertEqual(
1026              _geo.MultiPath(
1027                  _geo.Start(point=_geo.origin, width=2.0),
1028                  _geo.GoRight(2.0),
1029                  _geo.GoUp(2.0),
1030              ),
1031              _geo.Polygon.from_floats(points=(
1032                  (0.0, 1.0),
1033                  (1.0, 1.0),
1034                  (1.0, 2.0),
1035                  (3.0, 2.0),
1036                  (3.0, -1.0),
1037                  (0.0, -1.0),
1038                  (0.0, 1.0),
1039              )),
1040          )
1041          # GoRight, SetWidth, GoUp
1042          self.assertEqual(
1043              _geo.MultiPath(
1044                  _geo.Start(point=_geo.origin, width=2.0),
1045                  _geo.GoRight(3.0),
1046                  _geo.SetWidth(4.0),
1047                  _geo.GoUp(2.0),
1048              ),
1049              _geo.Polygon.from_floats(points=(
1050                  (0.0, 1.0),
1051                  (1.0, 1.0),
1052                  (1.0, 2.0),
1053                  (5.0, 2.0),
1054                  (5.0, -1.0),
1055                  (0.0, -1.0),
1056                  (0.0, 1.0),
1057              )),
1058          )
1059  
1060          # GoUp, GoLeft
1061          self.assertEqual(
1062              _geo.MultiPath(
1063                  _geo.Start(point=_geo.origin, width=2.0),
1064                  _geo.GoUp(2.0),
1065                  _geo.GoLeft(2.0),
1066              ),
1067              _geo.Polygon.from_floats(points=(
1068                  (-1.0, 0.0),
1069                  (-1.0, 1.0),
1070                  (-2.0, 1.0),
1071                  (-2.0, 3.0),
1072                  (1.0, 3.0),
1073                  (1.0, 0.0),
1074                  (-1.0, 0.0),
1075              )),
1076          )
1077          # GoUp, SetWidth, GoLeft
1078          self.assertEqual(
1079              _geo.MultiPath(
1080                  _geo.Start(point=_geo.origin, width=2.0),
1081                  _geo.GoUp(3.0),
1082                  _geo.SetWidth(4.0),
1083                  _geo.GoLeft(2.0),
1084              ),
1085              _geo.Polygon.from_floats(points=(
1086                  (-1.0, 0.0),
1087                  (-1.0, 1.0),
1088                  (-2.0, 1.0),
1089                  (-2.0, 5.0),
1090                  (1.0, 5.0),
1091                  (1.0, 0.0),
1092                  (-1.0, 0.0),
1093              )),
1094          )
1095          # GoUp, GoDown
1096          with self.assertRaises(ValueError):
1097              _geo.MultiPath(
1098                  _geo.Start(point=_geo.origin, width=2.0),
1099                  _geo.GoUp(2.0),
1100                  _geo.GoDown(2.0),
1101              ),
1102          # GoUp, GoRight
1103          self.assertEqual(
1104              _geo.MultiPath(
1105                  _geo.Start(point=_geo.origin, width=2.0),
1106                  _geo.GoUp(2.0),
1107                  _geo.GoRight(2.0),
1108              ),
1109              _geo.Polygon.from_floats(points=(
1110                  (-1.0, 0.0),
1111                  (-1.0, 3.0),
1112                  (2.0, 3.0),
1113                  (2.0, 1.0),
1114                  (1.0, 1.0),
1115                  (1.0, 0.0),
1116                  (-1.0, 0.0),
1117              )),
1118          )
1119          # GoUp, SetWidth, GoRight
1120          self.assertEqual(
1121              _geo.MultiPath(
1122                  _geo.Start(point=_geo.origin, width=2.0),
1123                  _geo.GoUp(3.0),
1124                  _geo.SetWidth(4.0),
1125                  _geo.GoRight(2.0),
1126              ),
1127              _geo.Polygon.from_floats(points=(
1128                  (-1.0, 0.0),
1129                  (-1.0, 5.0),
1130                  (2.0, 5.0),
1131                  (2.0, 1.0),
1132                  (1.0, 1.0),
1133                  (1.0, 0.0),
1134                  (-1.0, 0.0),
1135              )),
1136          )
1137          # GoUp, SetWidth, GoUp
1138          self.assertEqual(
1139              _geo.MultiPath(
1140                  _geo.Start(point=_geo.origin, width=2.0),
1141                  _geo.GoUp(1.0),
1142                  _geo.SetWidth(4.0),
1143                  _geo.GoUp(1.0),
1144              ),
1145              _geo.Polygon.from_floats(points=(
1146                  (-1.0, 0.0),
1147                  (-1.0, 1.0),
1148                  (-2.0, 1.0),
1149                  (-2.0, 2.0),
1150                  (2.0, 2.0),
1151                  (2.0, 1.0),
1152                  (1.0, 1.0),
1153                  (1.0, 0.0),
1154                  (-1.0, 0.0),
1155              )),
1156          )
1157  
1158      def test_multipath_knot(self):
1159          # Different Knot implementations covering different corner cases.
1160          # A set of shapes for each previous _Go direction
1161  
1162          ### After GoUp
1163  
1164          self.assertEqual(
1165              _geo.MultiPath(
1166                  _geo.Start(point=_geo.origin, width=2.0),
1167                  _geo.GoUp(5.0),
1168                  _geo.Knot(
1169                      left=(
1170                          _geo.SetWidth(3.0),
1171                          _geo.GoLeft(2.0),
1172                      ),
1173                      right=(
1174                          _geo.SetWidth(3.0),
1175                          _geo.GoRight(2.0),
1176                      ),
1177                  )
1178              ),
1179              _geo.Polygon.from_floats(points=(
1180                  (-1.0, 0.0),
1181                  (-1.0, 3.5),
1182                  (-2.0, 3.5),
1183                  (-2.0, 6.5),
1184                  (2.0, 6.5),
1185                  (2.0, 3.5),
1186                  (1.0, 3.5),
1187                  (1.0, 0.0),
1188                  (-1.0, 0.0),
1189              )),
1190          )
1191  
1192          self.assertEqual(
1193              _geo.MultiPath(
1194                  _geo.Start(point=_geo.origin, width=2.0),
1195                  _geo.GoUp(5.0),
1196                  _geo.Knot(
1197                      left=(
1198                          _geo.SetWidth(3.0),
1199                          _geo.GoLeft(2.0),
1200                      ),
1201                      right=(
1202                          _geo.SetWidth(4.0),
1203                          _geo.GoRight(2.0),
1204                      ),
1205                  )
1206              ),
1207              _geo.Polygon.from_floats(points=(
1208                  (-1.0, 0.0),
1209                  (-1.0, 3.5),
1210                  (-2.0, 3.5),
1211                  (-2.0, 6.5),
1212                  (0.0, 6.5),
1213                  (0.0, 7.0),
1214                  (2.0, 7.0),
1215                  (2.0, 3.0),
1216                  (1.0, 3.0),
1217                  (1.0, 0.0),
1218                  (-1.0, 0.0),
1219              )),
1220          )
1221  
1222          self.assertEqual(
1223              _geo.MultiPath(
1224                  _geo.Start(point=_geo.origin, width=2.0),
1225                  _geo.GoUp(5.0),
1226                  _geo.Knot(
1227                      left=(
1228                          _geo.SetWidth(3.0),
1229                          _geo.GoLeft(2.0),
1230                      ),
1231                      up=_geo.GoUp(5.0),
1232                      right=(
1233                          _geo.SetWidth(3.0),
1234                          _geo.GoRight(2.0),
1235                      ),
1236                  )
1237              ),
1238              _geo.Polygon.from_floats(points=(
1239                  (-1.0, 0.0),
1240                  (-1.0, 3.5),
1241                  (-2.0, 3.5),
1242                  (-2.0, 6.5),
1243                  (-1.0, 6.5),
1244                  (-1.0, 10.0),
1245                  (1.0, 10.0),
1246                  (1.0, 6.5),
1247                  (2.0, 6.5),
1248                  (2.0, 3.5),
1249                  (1.0, 3.5),
1250                  (1.0, 0.0),
1251                  (-1.0, 0.0),
1252              )),
1253          )
1254  
1255          self.assertEqual(
1256              _geo.MultiPath(
1257                  _geo.Start(point=_geo.origin, width=2.0),
1258                  _geo.GoUp(5.0),
1259                  _geo.Knot(
1260                      left=(
1261                          _geo.SetWidth(3.0),
1262                          _geo.GoLeft(3.0),
1263                      ),
1264                      up=_geo.GoUp(1.0),
1265                      right=(
1266                          _geo.SetWidth(3.0),
1267                          _geo.GoRight(3.0),
1268                      ),
1269                  )
1270              ),
1271              _geo.Polygon.from_floats(points=(
1272                  (-1.0, 0.0),
1273                  (-1.0, 3.5),
1274                  (-3.0, 3.5),
1275                  (-3.0, 6.5),
1276                  (-1.0, 6.5),
1277                  (-1.0, 6.0),
1278                  (1.0, 6.0),
1279                  (1.0, 6.5),
1280                  (3.0, 6.5),
1281                  (3.0, 3.5),
1282                  (1.0, 3.5),
1283                  (1.0, 0.0),
1284                  (-1.0, 0.0),
1285              )),
1286          )
1287  
1288          self.assertEqual(
1289              _geo.MultiPath(
1290                  _geo.Start(point=_geo.origin, width=2.0),
1291                  _geo.GoUp(5.0),
1292                  _geo.Knot(
1293                      up=(
1294                          _geo.GoUp(5.0),
1295                      ),
1296                      right=(
1297                          _geo.SetWidth(3.0),
1298                          _geo.GoRight(3.0),
1299                      ),
1300                  )
1301              ),
1302              _geo.Polygon.from_floats(points=(
1303                  (-1.0, 0.0),
1304                  (-1.0, 10.0),
1305                  (1.0, 10.0),
1306                  (1.0, 6.5),
1307                  (3.0, 6.5),
1308                  (3.0, 3.5),
1309                  (1.0, 3.5),
1310                  (1.0, 0.0),
1311                  (-1.0, 0.0),
1312              )),
1313          )
1314  
1315          self.assertEqual(
1316              _geo.MultiPath(
1317                  _geo.Start(point=_geo.origin, width=2.0),
1318                  _geo.GoUp(5.0),
1319                  _geo.Knot(
1320                      up=(
1321                          _geo.SetWidth(1.0),
1322                          _geo.GoUp(5.0),
1323                      ),
1324                      right=(
1325                          _geo.SetWidth(3.0),
1326                          _geo.GoRight(3.0),
1327                      ),
1328                  )
1329              ),
1330              _geo.Polygon.from_floats(points=(
1331                  (-1.0, 0.0),
1332                  (-1.0, 5.0),
1333                  (-0.5, 5.0),
1334                  (-0.5, 10.0),
1335                  (0.5, 10.0),
1336                  (0.5, 6.5),
1337                  (3.0, 6.5),
1338                  (3.0, 3.5),
1339                  (1.0, 3.5),
1340                  (1.0, 0.0),
1341                  (-1.0, 0.0),
1342              )),
1343          )
1344  
1345          ### After GoLeft
1346  
1347          self.assertEqual(
1348              _geo.MultiPath(
1349                  _geo.Start(point=_geo.origin, width=2.0),
1350                  _geo.GoLeft(5.0),
1351                  _geo.Knot(
1352                      down=(
1353                          _geo.SetWidth(3.0),
1354                          _geo.GoDown(2.0),
1355                      ),
1356                      up=(
1357                          _geo.SetWidth(3.0),
1358                          _geo.GoUp(2.0),
1359                      ),
1360                  )
1361              ),
1362              _geo.Polygon.from_floats(points=(
1363                  (0.0, -1.0),
1364                  (-3.5, -1.0),
1365                  (-3.5, -2.0),
1366                  (-6.5, -2.0),
1367                  (-6.5, 2.0),
1368                  (-3.5, 2.0),
1369                  (-3.5, 1.0),
1370                  (0.0, 1.0),
1371                  (0.0, -1.0),
1372              )),
1373          )
1374  
1375          ### After GoDown
1376  
1377          self.assertEqual(
1378              _geo.MultiPath(
1379                  _geo.Start(point=_geo.origin, width=2.0),
1380                  _geo.GoDown(5.0),
1381                  _geo.Knot(
1382                      left=(
1383                          _geo.SetWidth(3.0),
1384                          _geo.GoLeft(2.0),
1385                      ),
1386                      right=(
1387                          _geo.SetWidth(3.0),
1388                          _geo.GoRight(2.0),
1389                      ),
1390                  )
1391              ),
1392              _geo.Polygon.from_floats(points=(
1393                  (1.0, 0.0),
1394                  (1.0, -3.5),
1395                  (2.0, -3.5),
1396                  (2.0, -6.5),
1397                  (-2.0, -6.5),
1398                  (-2.0, -3.5),
1399                  (-1.0, -3.5),
1400                  (-1.0, 0.0),
1401                  (1.0, 0.0),
1402              )),
1403          )
1404  
1405          ### After GoRight
1406  
1407          self.assertEqual(
1408              _geo.MultiPath(
1409                  _geo.Start(point=_geo.origin, width=2.0),
1410                  _geo.GoRight(5.0),
1411                  _geo.Knot(
1412                      down=(
1413                          _geo.SetWidth(3.0),
1414                          _geo.GoDown(2.0),
1415                      ),
1416                      up=(
1417                          _geo.SetWidth(3.0),
1418                          _geo.GoUp(2.0),
1419                      ),
1420                  )
1421              ),
1422              _geo.Polygon.from_floats(points=(
1423                  (0.0, 1.0),
1424                  (3.5, 1.0),
1425                  (3.5, 2.0),
1426                  (6.5, 2.0),
1427                  (6.5, -2.0),
1428                  (3.5, -2.0),
1429                  (3.5, -1.0),
1430                  (0.0, -1.0),
1431                  (0.0, 1.0),
1432              )),
1433          )
1434  
1435          # Two nested Knot instructions
1436  
1437          self.assertEqual(
1438              _geo.MultiPath(
1439                  _geo.Start(point=_geo.origin, width=2.0),
1440                  _geo.GoUp(5.0),
1441                  _geo.Knot(
1442                      left=(
1443                          _geo.GoLeft(5.0),
1444                          _geo.Knot(
1445                              down=_geo.GoDown(5.0),
1446                              left=_geo.GoLeft(5.0),
1447                              up=_geo.GoUp(5.0),
1448                          )
1449                      ),
1450                      up=_geo.GoUp(5.0),
1451                      right=_geo.GoRight(5.0),
1452                  )
1453              ),
1454              _geo.Polygon.from_floats(points=(
1455                  (-1.0, 0.0),
1456                  (-1.0, 4.0),
1457                  (-4.0, 4.0),
1458                  (-4.0, 0.0),
1459                  (-6.0, 0.0),
1460                  (-6.0, 4.0),
1461                  (-10.0, 4.0),
1462                  (-10.0, 6.0),
1463                  (-6.0, 6.0),
1464                  (-6.0, 10.0),
1465                  (-4.0, 10.0),
1466                  (-4.0, 6.0),
1467                  (-1.0, 6.0),
1468                  (-1.0, 10.0),
1469                  (1.0, 10.0),
1470                  (1.0, 6.0),
1471                  (5.0, 6.0),
1472                  (5.0, 4.0),
1473                  (1.0, 4.0),
1474                  (1.0, 0.0),
1475                  (-1.0, 0.0),
1476              )),
1477          )
1478  
1479      def test_multishape(self):
1480          p = _geo.Point(x=1.0, y=-1.0)
1481          p2 = _geo.Point(x=1.0, y=1.0)
1482          l = _geo.Line(point1=_geo.Point(x=0.0, y=0.0), point2=_geo.Point(x=1.0, y=1.0))
1483          r = _geo.Rect(left=-2.0, bottom=-3.0, right=2.0, top=-2.0)
1484          
1485          with self.assertRaisesRegex(
1486              ValueError, "MultiShape has to consist of more than one shape",
1487          ):
1488              _geo.MultiShape(shapes=(p,))
1489  
1490          ms1 = _geo.MultiShape(shapes=(p, l, r))
1491          ms2 = _geo.MultiShape(shapes=(l, r, p))
1492          ms3 = _geo.MultiShape(shapes=(l, _geo.MultiShape(shapes=(r, p))))
1493          ms4 = _geo.MultiShape(shapes=(p, l))
1494          ms5 = _geo.MultiShape(shapes=(p, p2))
1495  
1496          self.assertNotEqual(ms1, "")
1497          self.assertEqual(ms1, ms2)
1498          self.assertEqual(ms1, ms3)
1499          self.assertEqual(hash(ms1), hash(ms2))
1500          self.assertEqual(len(ms1), 3)
1501          self.assertTrue(l in ms2)
1502          self.assertAlmostEqual(ms1.area, 4.0, 6)
1503          self.assertNotEqual(ms1, ms4)
1504          self.assertEqual(set(ms1), {p, l, r})
1505          self.assertEqual(
1506              ms1.moved(dxy=p),
1507              _geo.MultiShape(shapes=(r + p, l + p, 2*p)),
1508          )
1509          rot = _geo.Rotation.MY
1510          self.assertEqual(
1511              ms1.rotated(rotation=rot),
1512              _geo.MultiShape(shapes=(
1513                  r.rotated(rotation=rot), l.rotated(rotation=rot),
1514                  p.rotated(rotation=rot),
1515              )),
1516          )
1517          self.assertEqual(
1518              ms1.bounds,
1519              _geo.Rect(left=-2.0, bottom=-3.0, right=2.0, top=1.0),
1520          )
1521          self.assertEqual(ms5.bounds, _geo.Line(point1=p, point2=p2))
1522          self.assertEqual(str(ms1), f"({str(l)},{str(p)},{str(r)})")
1523          self.assertEqual(
1524              repr(ms1),
1525              "MultiShape(shapes=(Line(point1=Point(x=0.0,y=0.0),point2=Point(x=1.0,y=1.0)),Point(x=1.0,y=-1.0),Rect(left=-2.0,bottom=-3.0,right=2.0,top=-2.0)))"
1526          )
1527  
1528      def test_repeatedshape(self):
1529          s = _geo.Rect.from_size(width=2.0, height=2.0)
1530          dxy1 = _geo.Point(x=5.0, y=0.0)
1531          dxy2 = _geo.Point(x=0.0, y=5.0)
1532          p = _geo.Point(x=0.0, y=1.0)
1533  
1534          with self.assertRaisesRegex(
1535              ValueError, "n has to be equal to or higher than 2, not '1'"
1536          ):
1537              _geo.RepeatedShape(shape=s, offset0=_geo.origin, n=1, n_dxy=dxy1)
1538          with self.assertRaisesRegex(
1539              ValueError, "m has to be equal to or higher than 1, not '0'"
1540          ):
1541              _geo.RepeatedShape(shape=s, offset0=_geo.origin, n=2, n_dxy=dxy1, m=0)
1542          with self.assertRaisesRegex(
1543              ValueError, "m_dxy may not be None if m > 1"
1544          ):
1545              _geo.RepeatedShape(shape=s, offset0=_geo.origin, n=2, n_dxy=dxy1, m=2)
1546          
1547          rp1 = _geo.RepeatedShape(
1548              shape=s, offset0=_geo.origin, n=2, n_dxy=dxy1,
1549          )
1550          rp2 = s.repeat(
1551              offset0=_geo.origin, n=2, n_dxy=dxy1,
1552          )
1553          rp3 = _geo.RepeatedShape(
1554              shape=s, offset0=p, n=2, n_dxy=dxy1,
1555          )
1556          rp4 = _geo.RepeatedShape(
1557              shape=s, offset0=_geo.origin, n=2, n_dxy=dxy1, m=2, m_dxy=dxy2,
1558          )
1559          rp5 = _geo.RepeatedShape(
1560              shape=s, offset0=_geo.origin, n=2, n_dxy=dxy2, m=2, m_dxy=dxy1,
1561          )
1562          rp6 = _geo.RepeatedShape(
1563              shape=s, offset0=_geo.origin, n=2, n_dxy=dxy1, m=3, m_dxy=dxy2,
1564          )
1565          rp7 = _geo.RepeatedShape(
1566              shape=s, offset0=_geo.origin, n=3, n_dxy=dxy2, m=2, m_dxy=dxy1,
1567          )
1568          rp8 = _geo.RepeatedShape(
1569              shape=s, offset0=_geo.origin, n=2, n_dxy=dxy2, m=3, m_dxy=dxy1,
1570          )
1571  
1572          self.assertAlmostEqual(rp1.area, 2*s.area, 6)
1573          self.assertNotEqual(rp1, False)
1574          self.assertEqual(rp1, rp2)
1575          self.assertEqual(hash(rp1), hash(rp2))
1576          self.assertNotEqual(rp1, rp3)
1577          self.assertEqual(rp1.moved(dxy=p), rp3)
1578          self.assertNotEqual(rp1, rp4)
1579          self.assertEqual(rp4, rp5)
1580          self.assertEqual(rp6, rp7)
1581          self.assertEqual(hash(rp6), hash(rp7))
1582          self.assertNotEqual(rp6, rp8)
1583  
1584          ms1 = _geo.MultiShape(shapes=(s, s+dxy1))
1585          ms2 = _geo.MultiShape(shapes=rp1.pointsshapes)
1586          ms3 = _geo.MultiShape(shapes=(
1587              s + i*dxy1 + j*dxy2 for i, j in product(range(2), range(2))
1588          ))
1589          ms4 = _geo.MultiShape(shapes=rp4.pointsshapes)
1590          rot = _geo.Rotation.MY90
1591          ms5 = _geo.MultiShape(shapes=rp1.rotated(rotation=rot).pointsshapes)
1592  
1593          self.assertEqual(ms1, ms2)
1594          self.assertEqual(rp1.bounds, ms1.bounds)
1595          self.assertEqual(ms3, ms4)
1596          self.assertEqual(ms5, ms2.rotated(rotation=rot))
1597          self.assertEqual(rp4.bounds, ms4.bounds)
1598  
1599          self.assertIsInstance(repr(rp1), str) # __repr__ coverage
1600  
1601      def test_arrayshape(self):
1602          via = _geo.Rect.from_size(width=1.0, height=1.0)
1603          orig = _geo.Point(x=-1.0, y=-1.0)
1604          dx = 2.0
1605          dxy_x = _geo.Point(x=dx, y=0.0)
1606          dy = 3.0
1607          dxy_y = _geo.Point(x=0.0, y=dy)
1608  
1609          with self.assertRaises(ValueError):
1610              _geo.ArrayShape(shape=via, offset0=orig, rows=-1, columns=4)
1611          with self.assertRaises(ValueError):
1612              _geo.ArrayShape(shape=via, offset0=orig, rows=1, columns=1)
1613          with self.assertRaises(ValueError):
1614              _geo.ArrayShape(shape=via, offset0=orig, rows=2, columns=1)
1615          with self.assertRaises(ValueError):
1616              _geo.ArrayShape(shape=via, offset0=orig, rows=1, columns=2)
1617  
1618          self.assertEqual(
1619              _geo.ArrayShape(shape=via, offset0=orig, rows=3, columns=1, pitch_x=dx, pitch_y=dy),
1620              _geo.RepeatedShape(shape=via, offset0=orig, n=3, n_dxy=dxy_y),
1621          )
1622          self.assertEqual(
1623              _geo.ArrayShape(shape=via, offset0=orig, rows=1, columns=2, pitch_x=dx, pitch_y=dy),
1624              _geo.RepeatedShape(shape=via, offset0=orig, n=2, n_dxy=dxy_x),
1625          )
1626          self.assertEqual(
1627              _geo.ArrayShape(shape=via, offset0=orig, rows=3, columns=2, pitch_x=dx, pitch_y=dy),
1628              _geo.RepeatedShape(shape=via, offset0=orig, n=3, n_dxy=dxy_y, m=2, m_dxy=dxy_x),
1629          )
1630  
1631          ar = _geo.ArrayShape(shape=via, offset0=orig, rows=3, columns=4, pitch_x=dx, pitch_y=dy)
1632          self.assertEqual(ar.rows, 3)
1633          self.assertEqual(ar.columns, 4)
1634          self.assertEqual(ar.pitch_x, dx)
1635          self.assertEqual(ar.pitch_y, dy)
1636  
1637      def test_maskshape(self):
1638          p = _geo.Point(x=0.0, y=1.0)
1639          l = _geo.Line(point1=_geo.origin, point2=p)
1640          r1 = _geo.Rect.from_size(width=2.0, height=2.0)
1641          r2 = _geo.Rect.from_size(width=2.0, height=2.0) # Same r1 to test equality
1642          m1 = _msk.DesignMask(name="mask1")
1643          m2 = _msk.DesignMask(name="mask2")
1644          ms1 = _geo.MaskShape(mask=m1, shape=r1)
1645          ms2 = _geo.MaskShape(mask=m2, shape=r1)
1646          ms3 = _geo.MaskShape(mask=m1, shape=r2)
1647          ms4 = _geo.MaskShape(mask=m1, shape=l)
1648          ms5 = _geo.MaskShape(mask=m1, shape=(r1 + p))
1649          rot = _geo.Rotation.R270
1650          ms6 = _geo.MaskShape(mask=m1, shape=l.rotated(rotation=rot))
1651  
1652          self.assertEqual(ms1.mask, m1)
1653          self.assertEqual(ms1.shape, r1)
1654          self.assertEqual(ms1.bounds, r1)
1655          self.assertNotEqual(ms1, [])
1656          self.assertIsInstance(repr(ms1), str) # coverage of __repr__()
1657          self.assertAlmostEqual(ms1.area, r1.area, 6)
1658          self.assertNotEqual(ms1, ms2)
1659          self.assertEqual(ms1, ms3)
1660          self.assertEqual(hash(ms1), hash(ms3))
1661          self.assertNotEqual(ms1, ms4)
1662          self.assertEqual(ms1.moved(dxy=p), ms5)
1663          self.assertEqual(ms4.rotated(rotation=rot), ms6)
1664  
1665      def test_maskshapes(self):
1666          m1 = _msk.DesignMask(name="mask1")
1667          m2 = _msk.DesignMask(name="mask2")
1668          p = _geo.Point(x=3.0, y=-2.0)
1669          rot = _geo.Rotation.R90
1670          r1 = _geo.Rect(left=-3.0, bottom=-1.0, right=-1.0, top=1.0)
1671          r2 = _geo.Rect(left=1.0, bottom=-1.0, right=3.0, top=1.0)
1672          ms1 = _geo.MaskShape(mask=m1, shape=r1)
1673          ms2 = _geo.MaskShape(mask=m2, shape=r2)
1674          ms3 = _geo.MaskShape(mask=m1, shape=r2)
1675          ms4 = _geo.MaskShape(mask=m1, shape=_geo.MultiShape(shapes=(r1, r2)))
1676          mss1 = _geo.MaskShapes(ms1)
1677          mss2 = _geo.MaskShapes((ms1, ms2))
1678          mss3 = _geo.MaskShapes((ms1, ms2))
1679          mss3.move(dxy=p)
1680          mss4 = _geo.MaskShapes(ms1)
1681          mss4.rotate(rotation=rot)
1682          mss5 = _geo.MaskShapes(ms1)
1683          mss5 += ms3
1684          mss6 = _geo.MaskShapes(ms4)
1685          mss7 = _geo.MaskShapes((ms1, ms3))
1686  
1687          self.assertEqual(_util.get_first_of(mss1), ms1)
1688          self.assertEqual(_util.get_last_of(mss2), ms2)
1689          self.assertEqual(mss5, mss6)
1690          self.assertEqual(mss5, mss7)
1691          self.assertEqual(mss2.moved(dxy=p), mss3)
1692          self.assertEqual(mss1.rotated(rotation=rot), mss4)
1693  
1694          mss1 += ms2
1695  
1696          self.assertEqual(mss1, mss2)
1697  
1698          mss2._freeze_()
1699  
1700          with self.assertRaises(TypeError):
1701              mss2.move(dxy=p)
1702          with self.assertRaises(TypeError):
1703              mss2.rotate(rotation=rot)