Files
rpmroute/src/freeroute/geometry/polygon.py
T
rsp2k 68ab16a7cf Add PolygonShape.split_to_convex (polygon to convex tiles)
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.
2026-07-11 18:41:07 -06:00

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)