You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
snarled/snarled/poly.py

158 lines
5.0 KiB
Python

"""
Utilities for working with polygons
"""
import numpy
from numpy.typing import NDArray, ArrayLike
def poly_contains_points(
vertices: ArrayLike,
points: ArrayLike,
include_boundary: bool = True,
) -> NDArray[numpy.int_]:
"""
Tests whether the provided points are inside the implicitly closed polygon
described by the provided list of vertices.
Args:
vertices: Nx2 Arraylike of form [[x0, y0], [x1, y1], ...], describing an implicitly-
closed polygon. Note that this should include any offsets.
points: Nx2 ArrayLike of form [[x0, y0], [x1, y1], ...] containing the points to test.
include_boundary: True if points on the boundary should be count as inside the shape.
Default True.
Returns:
ndarray of booleans, [point0_is_in_shape, point1_is_in_shape, ...]
"""
points = numpy.array(points, copy=False)
vertices = numpy.array(vertices, copy=False)
if points.size == 0:
return numpy.zeros(0)
min_bounds = numpy.min(vertices, axis=0)[None, :]
max_bounds = numpy.max(vertices, axis=0)[None, :]
trivially_outside = ((points < min_bounds).any(axis=1)
| (points > max_bounds).any(axis=1))
nontrivial = ~trivially_outside
if trivially_outside.all():
inside = numpy.zeros_like(trivially_outside, dtype=bool)
return inside
ntpts = points[None, nontrivial, :] # nontrivial points, along axis 1 of ndarray
verts = vertices[:, None, :] # vertices, along axis 0
xydiff = ntpts - verts # Expands into (n_vertices, n_ntpts, 2)
y0_le = xydiff[:, :, 1] >= 0 # y_point >= y_vertex (axes 0, 1 for all points & vertices)
y1_le = numpy.roll(y0_le, -1, axis=0) # same thing for next vertex
upward = y0_le & ~y1_le # edge passes point y coord going upwards
downward = ~y0_le & y1_le # edge passes point y coord going downwards
dv = numpy.roll(verts, -1, axis=0) - verts
is_left = (dv[..., 0] * xydiff[..., 1] # >0 if left of dv, <0 if right, 0 if on the line
- dv[..., 1] * xydiff[..., 0])
winding_number = ((upward & (is_left > 0)).sum(axis=0)
- (downward & (is_left < 0)).sum(axis=0))
nontrivial_inside = winding_number != 0 # filter nontrivial points based on winding number
if include_boundary:
nontrivial_inside[(is_left == 0).any(axis=0)] = True # check if point lies on any edge
inside = nontrivial.copy()
inside[nontrivial] = nontrivial_inside
return inside
def intersects(poly_a: ArrayLike, poly_b: ArrayLike) -> bool:
"""
Check if two polygons overlap and/or touch.
Args:
poly_a: List of vertices, implicitly closed: `[[x0, y0], [x1, y1], ...]`
poly_b: List of vertices, implicitly closed: `[[x0, y0], [x1, y1], ...]`
Returns:
`True` if the polygons overlap and/or touch.
"""
poly_a = numpy.array(poly_a, copy=False)
poly_b = numpy.array(poly_b, copy=False)
# Check bounding boxes
min_a = poly_a.min(axis=0)
min_b = poly_b.min(axis=0)
max_a = poly_a.max(axis=0)
max_b = poly_b.max(axis=0)
if (min_a > max_b).any() or (min_b > max_a).any():
return False
#TODO: Check against sorted coords?
#Check if edges intersect
if poly_edges_intersect(poly_a, poly_b):
return True
# Check if either polygon contains the other
if poly_contains_points(poly_b, poly_a).any():
return True
if poly_contains_points(poly_a, poly_b).any():
return True
return False
def poly_edges_intersect(
poly_a: NDArray[numpy.float64],
poly_b: NDArray[numpy.float64],
) -> NDArray[numpy.int_]:
"""
Check if the edges of two polygons intersect.
Args:
poly_a: NDArray of vertices, implicitly closed: `[[x0, y0], [x1, y1], ...]`
poly_b: NDArray of vertices, implicitly closed: `[[x0, y0], [x1, y1], ...]`
Returns:
`True` if the polygons' edges intersect.
"""
a_next = numpy.roll(poly_a, -1, axis=0)
b_next = numpy.roll(poly_b, -1, axis=0)
# Lists of initial/final coordinates for polygon segments
xi1 = poly_a[:, 0, None]
yi1 = poly_a[:, 1, None]
xf1 = a_next[:, 0, None]
yf1 = a_next[:, 1, None]
xi2 = poly_b[None, :, 0]
yi2 = poly_b[None, :, 1]
xf2 = b_next[None, :, 0]
yf2 = b_next[None, :, 1]
# Perform calculation
dxi = xi1 - xi2
dyi = yi1 - yi2
dx1 = xf1 - xi1
dx2 = xf2 - xi2
dy1 = yf1 - yi1
dy2 = yf2 - yi2
numerator_a = dx2 * dyi - dy2 * dxi
numerator_b = dx1 * dyi - dy1 * dxi
denominator = dy2 * dx1 - dx2 * dy1
# Avoid warnings since we may multiply eg. NaN*False
with numpy.errstate(invalid='ignore', divide='ignore'):
u_a = numerator_a / denominator
u_b = numerator_b / denominator
# Find the adjacency matrix
adjacency = numpy.logical_and.reduce((u_a >= 0, u_a <= 1, u_b >= 0, u_b <= 1))
return adjacency.any()