Source code for scene_service.geometry
# SPDX-License-Identifier: MulanPSL-2.0
"""Geometry helpers shared by Scene room rendering and navigation."""
from __future__ import annotations
import math
from collections.abc import Sequence
Point = tuple[float, float]
[docs]
def polygon_centroid(points: Sequence[Point]) -> Point:
"""Return the area-weighted centroid of a non-self-intersecting polygon."""
pts = [(float(x), float(y)) for x, y in points]
if not pts:
return 0.0, 0.0
if len(pts) > 1 and pts[0] == pts[-1]:
pts.pop()
if len(pts) < 3:
return (
sum(x for x, _ in pts) / len(pts),
sum(y for _, y in pts) / len(pts),
)
twice_area = 0.0
cx = 0.0
cy = 0.0
for (x1, y1), (x2, y2) in zip(pts, pts[1:] + pts[:1]):
cross = x1 * y2 - x2 * y1
twice_area += cross
cx += (x1 + x2) * cross
cy += (y1 + y2) * cross
if abs(twice_area) < 1e-9:
return (
sum(x for x, _ in pts) / len(pts),
sum(y for _, y in pts) / len(pts),
)
return cx / (3.0 * twice_area), cy / (3.0 * twice_area)
[docs]
def point_in_polygon(x: float, y: float, points: Sequence[Point]) -> bool:
"""Return whether a point lies inside a polygon using even-odd crossings."""
pts = [(float(px), float(py)) for px, py in points]
if len(pts) > 1 and pts[0] == pts[-1]:
pts.pop()
if len(pts) < 3:
return False
inside = False
for (x1, y1), (x2, y2) in zip(pts, pts[1:] + pts[:1]):
if (y1 > y) != (y2 > y):
crossing_x = (x2 - x1) * (y - y1) / (y2 - y1) + x1
if x < crossing_x:
inside = not inside
return inside
[docs]
def disc_inside_polygon(
x: float,
y: float,
radius: float,
points: Sequence[Point],
samples: int = 16,
) -> bool:
"""Require a circular robot footprint, not just its centre, inside a room."""
if not point_in_polygon(x, y, points):
return False
return all(
point_in_polygon(
x + radius * math.cos(2.0 * math.pi * i / samples),
y + radius * math.sin(2.0 * math.pi * i / samples),
points,
)
for i in range(samples)
)