Spaces:
Sleeping
Sleeping
File size: 3,346 Bytes
9f084b2 bbbfba8 9f084b2 bbbfba8 9f084b2 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 | """Polygon operations — conversions and validation for polygon geometry.
Polygons are represented as list[tuple[float, float]] — ordered (x, y) vertices.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from src.app.geometry.bbox import BBoxTuple
PolygonPoints = list[tuple[float, float]]
def polygon_to_bbox(polygon: PolygonPoints) -> BBoxTuple:
"""Compute the axis-aligned bounding box enclosing a polygon.
Returns (x, y, width, height) in canonical convention.
Raises ValueError if the polygon has fewer than 3 points.
"""
if len(polygon) < 3:
raise ValueError(f"A polygon needs at least 3 points, got {len(polygon)}")
xs = [p[0] for p in polygon]
ys = [p[1] for p in polygon]
min_x = min(xs)
min_y = min(ys)
max_x = max(xs)
max_y = max(ys)
return (min_x, min_y, max_x - min_x, max_y - min_y)
def bbox_to_polygon(bbox: BBoxTuple) -> PolygonPoints:
"""Convert a bbox to a 4-point polygon (clockwise from top-left)."""
x, y, w, h = bbox
return [
(x, y),
(x + w, y),
(x + w, y + h),
(x, y + h),
]
def polygon_area(polygon: PolygonPoints) -> float:
"""Compute area using the shoelace formula. Always returns a positive value."""
n = len(polygon)
if n < 3:
return 0.0
total = 0.0
for i in range(n):
j = (i + 1) % n
total += polygon[i][0] * polygon[j][1]
total -= polygon[j][0] * polygon[i][1]
return abs(total) / 2.0
def polygon_centroid(polygon: PolygonPoints) -> tuple[float, float]:
"""Compute the centroid of a polygon.
Raises ValueError if the polygon has fewer than 3 points.
"""
if len(polygon) < 3:
raise ValueError(f"A polygon needs at least 3 points, got {len(polygon)}")
n = len(polygon)
cx = sum(p[0] for p in polygon) / n
cy = sum(p[1] for p in polygon) / n
return (cx, cy)
def validate_polygon(polygon: PolygonPoints) -> list[str]:
"""Return a list of warnings about the polygon. Empty list = valid."""
warnings: list[str] = []
if len(polygon) < 3:
warnings.append(f"Polygon has fewer than 3 points ({len(polygon)})")
return warnings
# Check for duplicate consecutive points
for i in range(len(polygon)):
j = (i + 1) % len(polygon)
if polygon[i] == polygon[j]:
warnings.append(f"Duplicate consecutive points at index {i} and {j}")
# Check for zero area
if polygon_area(polygon) == 0.0:
warnings.append("Polygon has zero area (degenerate)")
# Check for negative coordinates
for i, (x, y) in enumerate(polygon):
if x < 0 or y < 0:
warnings.append(f"Negative coordinate at point {i}: ({x}, {y})")
return warnings
def is_clockwise(polygon: PolygonPoints) -> bool:
"""Check if polygon vertices are ordered clockwise."""
n = len(polygon)
if n < 3:
return False
total = 0.0
for i in range(n):
j = (i + 1) % n
total += (polygon[j][0] - polygon[i][0]) * (polygon[j][1] + polygon[i][1])
return total > 0
def ensure_clockwise(polygon: PolygonPoints) -> PolygonPoints:
"""Return the polygon in clockwise order."""
if not is_clockwise(polygon):
return list(reversed(polygon))
return polygon
|