Ports geometry/planar/Polygon.java (corner de-duplication and collinear removal, winding number) and PolygonShape.java, including the recursive split_to_convex that decomposes a simple polygon into convex Simplex tiles by dividing at concave corners along minimal axis-parallel lines. Orientation and convexity tests are exact; the division-point search is approximate (float line evaluation, split point rounded to an integer corner) as upstream. The concave-corner search starts deterministically at corner 0 rather than a seeded PRNG; this only affects which valid decomposition is produced. Supporting additions: Simplex.from_corners (convex polygon to simplex) and Line.function_value_approx / function_in_y_value_approx. Invariant tests over L, plus, staircase and square polygons: tile areas sum to the polygon area (no gaps, no overlap), a point is in the polygon iff in some tile, and no point is strictly inside more than one tile (interiors disjoint). Also covers Polygon normalization and orientation.
78 lines
2.6 KiB
Python
78 lines
2.6 KiB
Python
"""``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)
|