"""``Polygon`` — a normalized corner list (no duplicate or collinear corners). Ports ``geometry/planar/Polygon.java``. Used as the corner normalizer behind :class:`~freeroute.geometry.polygon_shape.PolygonShape`. **Arithmetic model:** corner de-duplication and collinearity removal are **exact** (exact ``Point.side_of``); the winding-number test is approximate (sum of ``angle_approx``), exactly as upstream. """ from __future__ import annotations import math from .point import Point from .side import Side __all__ = ["Polygon"] class Polygon: """A list of corners with no two consecutive equal and no three collinear.""" __slots__ = ("corners",) def __init__(self, points) -> None: corners: list[Point] = list(points) corner_removed = True while corner_removed: corner_removed = False if not corners: break # remove consecutive duplicate points deduped: list[Point] = [corners[0]] for pt in corners[1:]: if pt != deduped[-1]: deduped.append(pt) if len(deduped) != len(corners): corner_removed = True corners = deduped # remove a point collinear with its previous and next point if len(corners) >= 3: for i in range(1, len(corners) - 1): if corners[i].side_of(corners[i - 1], corners[i + 1]) == Side.COLLINEAR: del corners[i] corner_removed = True break self.corners: list[Point] = corners def corner_array(self) -> list[Point]: return list(self.corners) def revert_corners(self) -> Polygon: return Polygon(list(reversed(self.corners))) def winding_number_after_closing(self) -> int: """Winding number of the closed polygon: >0 counter-clockwise, <0 clockwise.""" arr = self.corners if len(arr) < 2: return 0 first_side = arr[1].difference_by(arr[0]) prev_side = first_side corner_count = len(arr) if arr[0] == arr[corner_count - 1]: corner_count -= 1 angle_sum = 0.0 for i in range(1, corner_count + 1): if i == corner_count - 1: next_side = arr[0].difference_by(arr[i]) elif i == corner_count: next_side = first_side else: next_side = arr[i + 1].difference_by(arr[i]) angle_sum += prev_side.angle_approx(next_side) prev_side = next_side angle_sum /= 2.0 * math.pi return round(angle_sum)