[remove_colinear_vertices / Path] add preserve_uturns and use it for paths
This commit is contained in:
parent
495babf837
commit
ea93a7ef37
3 changed files with 56 additions and 19 deletions
|
|
@ -323,9 +323,30 @@ class Path(Shape):
|
|||
) -> list['Polygon']:
|
||||
extensions = self._calculate_cap_extensions()
|
||||
|
||||
v = remove_colinear_vertices(self.vertices, closed_path=False)
|
||||
v = remove_colinear_vertices(self.vertices, closed_path=False, preserve_uturns=True)
|
||||
dv = numpy.diff(v, axis=0)
|
||||
dvdir = dv / numpy.sqrt((dv * dv).sum(axis=1))[:, None]
|
||||
norms = numpy.sqrt((dv * dv).sum(axis=1))
|
||||
|
||||
# Filter out zero-length segments if any remained after remove_colinear_vertices
|
||||
valid = (norms > 1e-18)
|
||||
if not numpy.all(valid):
|
||||
# This shouldn't happen much if remove_colinear_vertices is working
|
||||
v = v[numpy.append(valid, True)]
|
||||
dv = numpy.diff(v, axis=0)
|
||||
norms = norms[valid]
|
||||
|
||||
if dv.shape[0] == 0:
|
||||
# All vertices were the same. It's a point.
|
||||
if self.width == 0:
|
||||
return [Polygon(vertices=numpy.zeros((3, 2)))] # Area-less degenerate
|
||||
if self.cap == PathCap.Circle:
|
||||
return Circle(radius=self.width / 2, offset=v[0]).to_polygons(num_vertices=num_vertices, max_arclen=max_arclen)
|
||||
if self.cap == PathCap.Square:
|
||||
return [Polygon.square(side_length=self.width, offset=v[0])]
|
||||
# Flush or CustomSquare
|
||||
return [Polygon(vertices=numpy.zeros((3, 2)))]
|
||||
|
||||
dvdir = dv / norms[:, None]
|
||||
|
||||
if self.width == 0:
|
||||
verts = numpy.vstack((v, v[::-1]))
|
||||
|
|
@ -448,16 +469,11 @@ class Path(Shape):
|
|||
rotated_vertices = numpy.vstack([numpy.dot(rotation_matrix_2d(-rotation), v)
|
||||
for v in normed_vertices])
|
||||
|
||||
# Reorder the vertices so that the one with lowest x, then y, comes first.
|
||||
x_min_val = rotated_vertices[:, 0].min()
|
||||
x_min_inds = numpy.where(rotated_vertices[:, 0] == x_min_val)[0]
|
||||
if x_min_inds.size > 1:
|
||||
y_min_val = rotated_vertices[x_min_inds, 1].min()
|
||||
tie_breaker = numpy.where(rotated_vertices[x_min_inds, 1] == y_min_val)[0][0]
|
||||
start_ind = x_min_inds[tie_breaker]
|
||||
# Canonical ordering for open paths: pick whichever of (v) or (v[::-1]) is smaller
|
||||
if tuple(rotated_vertices.flat) > tuple(rotated_vertices[::-1].flat):
|
||||
reordered_vertices = rotated_vertices[::-1]
|
||||
else:
|
||||
start_ind = x_min_inds[0]
|
||||
reordered_vertices = numpy.roll(rotated_vertices, -start_ind, axis=0)
|
||||
reordered_vertices = rotated_vertices
|
||||
|
||||
width0 = self.width / norm_value
|
||||
|
||||
|
|
@ -496,7 +512,7 @@ class Path(Shape):
|
|||
Returns:
|
||||
self
|
||||
"""
|
||||
self.vertices = remove_colinear_vertices(self.vertices, closed_path=False)
|
||||
self.vertices = remove_colinear_vertices(self.vertices, closed_path=False, preserve_uturns=True)
|
||||
return self
|
||||
|
||||
def _calculate_cap_extensions(self) -> NDArray[numpy.float64]:
|
||||
|
|
|
|||
|
|
@ -29,14 +29,19 @@ def test_remove_colinear_vertices() -> None:
|
|||
def test_remove_colinear_vertices_exhaustive() -> None:
|
||||
# U-turn
|
||||
v = [[0, 0], [10, 0], [0, 0]]
|
||||
v_clean = remove_colinear_vertices(v, closed_path=False)
|
||||
v_clean = remove_colinear_vertices(v, closed_path=False, preserve_uturns=True)
|
||||
# Open path should keep ends. [10,0] is between [0,0] and [0,0]?
|
||||
# Yes, they are all on the same line.
|
||||
assert len(v_clean) == 2
|
||||
# They are colinear, but it's a 180 degree turn.
|
||||
# We preserve 180 degree turns if preserve_uturns is True.
|
||||
assert len(v_clean) == 3
|
||||
|
||||
v_collapsed = remove_colinear_vertices(v, closed_path=False, preserve_uturns=False)
|
||||
# If not preserving u-turns, it should collapse to just the endpoints
|
||||
assert len(v_collapsed) == 2
|
||||
|
||||
# 180 degree U-turn in closed path
|
||||
v = [[0, 0], [10, 0], [5, 0]]
|
||||
v_clean = remove_colinear_vertices(v, closed_path=True)
|
||||
v_clean = remove_colinear_vertices(v, closed_path=True, preserve_uturns=False)
|
||||
assert len(v_clean) == 2
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,11 @@ def remove_duplicate_vertices(vertices: ArrayLike, closed_path: bool = True) ->
|
|||
return result
|
||||
|
||||
|
||||
def remove_colinear_vertices(vertices: ArrayLike, closed_path: bool = True) -> NDArray[numpy.float64]:
|
||||
def remove_colinear_vertices(
|
||||
vertices: ArrayLike,
|
||||
closed_path: bool = True,
|
||||
preserve_uturns: bool = False,
|
||||
) -> NDArray[numpy.float64]:
|
||||
"""
|
||||
Given a list of vertices, remove any superflous vertices (i.e.
|
||||
those which lie along the line formed by their neighbors)
|
||||
|
|
@ -39,6 +43,8 @@ def remove_colinear_vertices(vertices: ArrayLike, closed_path: bool = True) -> N
|
|||
vertices: Nx2 ndarray of vertices
|
||||
closed_path: If `True`, the vertices are assumed to represent an implicitly
|
||||
closed path. If `False`, the path is assumed to be open. Default `True`.
|
||||
preserve_uturns: If `True`, colinear vertices that correspond to a 180 degree
|
||||
turn (a "spike") are preserved. Default `False`.
|
||||
|
||||
Returns:
|
||||
`vertices` with colinear (superflous) vertices removed. May be a view into the original array.
|
||||
|
|
@ -46,14 +52,24 @@ def remove_colinear_vertices(vertices: ArrayLike, closed_path: bool = True) -> N
|
|||
vertices = remove_duplicate_vertices(vertices, closed_path=closed_path)
|
||||
|
||||
# Check for dx0/dy0 == dx1/dy1
|
||||
dv = numpy.roll(vertices, -1, axis=0) - vertices
|
||||
if not closed_path:
|
||||
dv[-1] = 0
|
||||
|
||||
dv = numpy.roll(vertices, -1, axis=0) - vertices # [y1-y0, y2-y1, ...]
|
||||
dxdy = dv * numpy.roll(dv, 1, axis=0)[:, ::-1] # [[dx0*(dy_-1), (dx_-1)*dy0], dx1*dy0, dy1*dx0]]
|
||||
# dxdy[i] is based on dv[i] and dv[i-1]
|
||||
# slopes_equal[i] refers to vertex i
|
||||
dxdy = dv * numpy.roll(dv, 1, axis=0)[:, ::-1]
|
||||
|
||||
dxdy_diff = numpy.abs(numpy.diff(dxdy, axis=1))[:, 0]
|
||||
err_mult = 2 * numpy.abs(dxdy).sum(axis=1) + 1e-40
|
||||
|
||||
slopes_equal = (dxdy_diff / err_mult) < 1e-15
|
||||
|
||||
if preserve_uturns:
|
||||
# Only merge if segments are in the same direction (avoid collapsing u-turns)
|
||||
dot_prod = (dv * numpy.roll(dv, 1, axis=0)).sum(axis=1)
|
||||
slopes_equal &= (dot_prod > 0)
|
||||
|
||||
if not closed_path:
|
||||
slopes_equal[[0, -1]] = False
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue