[path] Implement Ramer-Douglas-Peucker line simplification

Implement an iterative version of the Ramer-Douglas-Peucker line
simplification algorithm
(https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm),
which reduces line complexity to a limited linear deviation from the
original polyline. The ability to reason about linear deflection is the
key improvement over the previous linear implementation.

Worst case complexity is O(n^2), but expected complexity for typical
cases is O(n log n). A potentially faster alternative would be to call
out to libclipper, treating the line as a closed polygon. However, in
practice, performance of this implementation seems good enough. A
complex 3d surface operation optimizes in a few seconds, and reduces
output gcode size from about 220MB with the previous implementation to
10MB.
This commit is contained in:
Gabriel Wicke
2020-06-17 20:10:18 -07:00
parent f05253f882
commit 6b3815a766
2 changed files with 42 additions and 17 deletions

View File

@@ -871,3 +871,41 @@ class depth_params(object):
return depths
else:
return [stop] + depths
def simplify3dLine(line, tolerance=1e-4):
"""Simplify a line defined by a list of App.Vectors, while keeping the
maximum deviation from the original line within the defined tolerance.
Implementation of
https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm"""
stack = [(0, len(line) - 1)]
results = []
def processRange(start, end):
"""Internal worker. Process a range of Vector indices within the
line."""
if end - start < 2:
results.extend(line[start:end])
return
# Find point with maximum distance
maxIndex, maxDistance = 0, 0.0
startPoint, endPoint = (line[start], line[end])
for i in range(start + 1, end):
v = line[i]
distance = v.distanceToLineSegment(startPoint, endPoint).Length
if distance > maxDistance:
maxDistance = distance
maxIndex = i
if maxDistance > tolerance:
# Push second branch first, to be executed last
stack.append((maxIndex, end))
stack.append((start, maxIndex))
else:
results.append(line[start])
while len(stack):
processRange(*stack.pop())
# Each segment only appended its start point to the final result, so fill in
# the last point.
results.append(line[-1])
return results