forked from miklo/svg2ild
166 lines
5.6 KiB
Python
166 lines
5.6 KiB
Python
# ilda/utils.py
|
|
from dataclasses import dataclass
|
|
from typing import List, Tuple, Optional
|
|
import math
|
|
|
|
Point = Tuple[float, float]
|
|
PathPoints = List[Point]
|
|
|
|
|
|
@dataclass
|
|
class PathOrderResult:
|
|
"""
|
|
Container for ordered paths result.
|
|
|
|
Attributes:
|
|
paths: List of ordered polylines (each polyline is a list of (x,y) tuples).
|
|
reversed_flags: Parallel list of bools, True if corresponding original path was reversed.
|
|
"""
|
|
paths: List[PathPoints]
|
|
reversed_flags: List[bool]
|
|
|
|
|
|
def _dist(a: Point, b: Point) -> float:
|
|
"""Euclidean distance between two points."""
|
|
return math.hypot(a[0] - b[0], a[1] - b[1])
|
|
|
|
|
|
def order_paths_greedy_with_2opt(paths: List[PathPoints],
|
|
start_point: Optional[Point] = None,
|
|
do_2opt: bool = True) -> PathOrderResult:
|
|
"""
|
|
Order polylines to reduce dead (blanked) travel between them.
|
|
|
|
Parameters
|
|
----------
|
|
paths :
|
|
List of polylines (each polyline must have >= 2 points).
|
|
start_point :
|
|
Optional coordinate (x,y) to start the sequence from. If None, starts from first path.
|
|
do_2opt :
|
|
Whether to run a local 2-opt improvement after greedy construction.
|
|
|
|
Returns
|
|
-------
|
|
PathOrderResult
|
|
Ordered paths and flags indicating if each path was reversed relative to original.
|
|
"""
|
|
n = len(paths)
|
|
if n == 0:
|
|
return PathOrderResult([], [])
|
|
|
|
# Precompute endpoints (start, end) for each path
|
|
endpoints: List[Tuple[Point, Point]] = [(p[0], p[-1]) for p in paths]
|
|
|
|
used = [False] * n
|
|
order: List[int] = []
|
|
rev_flags: List[bool] = []
|
|
|
|
# choose initial index
|
|
if start_point is None:
|
|
cur_idx = 0
|
|
cur_rev = False
|
|
used[cur_idx] = True
|
|
order.append(cur_idx)
|
|
rev_flags.append(cur_rev)
|
|
cur_pt = endpoints[cur_idx][1] if not cur_rev else endpoints[cur_idx][0]
|
|
else:
|
|
# pick closest endpoint among all paths
|
|
best_idx = -1
|
|
best_rev = False
|
|
best_d = float('inf')
|
|
for i, (s, e) in enumerate(endpoints):
|
|
d_s = _dist(start_point, s)
|
|
if d_s < best_d:
|
|
best_d = d_s; best_idx = i; best_rev = False
|
|
d_e = _dist(start_point, e)
|
|
if d_e < best_d:
|
|
best_d = d_e; best_idx = i; best_rev = True
|
|
cur_idx = best_idx
|
|
cur_rev = best_rev
|
|
used[cur_idx] = True
|
|
order.append(cur_idx)
|
|
rev_flags.append(cur_rev)
|
|
cur_pt = endpoints[cur_idx][1] if not cur_rev else endpoints[cur_idx][0]
|
|
|
|
# greedy nearest neighbor choosing orientation
|
|
for _ in range(1, n):
|
|
best_j = -1
|
|
best_j_rev = False
|
|
best_d = float('inf')
|
|
for j in range(n):
|
|
if used[j]:
|
|
continue
|
|
s, e = endpoints[j]
|
|
d1 = _dist(cur_pt, s) # not reversed
|
|
if d1 < best_d:
|
|
best_d = d1; best_j = j; best_j_rev = False
|
|
d2 = _dist(cur_pt, e) # reversed
|
|
if d2 < best_d:
|
|
best_d = d2; best_j = j; best_j_rev = True
|
|
if best_j == -1:
|
|
break
|
|
used[best_j] = True
|
|
order.append(best_j)
|
|
rev_flags.append(best_j_rev)
|
|
s, e = endpoints[best_j]
|
|
cur_pt = e if not best_j_rev else s
|
|
|
|
# Build ordered path list (apply reversal where needed)
|
|
ordered_paths: List[PathPoints] = []
|
|
for idx, rev in zip(order, rev_flags):
|
|
p = paths[idx]
|
|
ordered_paths.append(list(reversed(p)) if rev else list(p))
|
|
|
|
# Optional 2-opt improvement (operates on ordered_paths)
|
|
if do_2opt and len(ordered_paths) > 2:
|
|
def connection_cost(a: PathPoints, b: PathPoints) -> float:
|
|
return _dist(a[-1], b[0])
|
|
|
|
improved = True
|
|
seq = ordered_paths
|
|
while improved:
|
|
improved = False
|
|
m = len(seq)
|
|
for i in range(0, m - 2):
|
|
for j in range(i + 2, m):
|
|
# cost affected at edges (i -> i+1) and (j -> j+1)
|
|
old_cost = connection_cost(seq[i], seq[i+1])
|
|
if j + 1 < m:
|
|
old_cost += connection_cost(seq[j], seq[j+1])
|
|
# form new sequence by reversing subsequence (i+1 .. j) and reversing each subpath
|
|
new_seq = seq[:i+1] + [list(reversed(seg)) for seg in seq[i+1:j+1]][::-1] + seq[j+1:]
|
|
new_cost = connection_cost(new_seq[i], new_seq[i+1])
|
|
if j + 1 < m:
|
|
new_cost += connection_cost(new_seq[j], new_seq[j+1])
|
|
if new_cost + 1e-12 < old_cost:
|
|
seq = new_seq
|
|
improved = True
|
|
break
|
|
if improved:
|
|
break
|
|
ordered_paths = seq
|
|
|
|
# Reconstruct reversed_flags relative to originals
|
|
final_flags: List[bool] = []
|
|
# Create mapping of original endpoints for approximate identification
|
|
for p in ordered_paths:
|
|
if not p:
|
|
final_flags.append(False)
|
|
continue
|
|
first = p[0]
|
|
found = False
|
|
for orig in paths:
|
|
if abs(orig[0][0] - first[0]) < 1e-9 and abs(orig[0][1] - first[1]) < 1e-9:
|
|
final_flags.append(False)
|
|
found = True
|
|
break
|
|
if abs(orig[-1][0] - first[0]) < 1e-9 and abs(orig[-1][1] - first[1]) < 1e-9:
|
|
final_flags.append(True)
|
|
found = True
|
|
break
|
|
if not found:
|
|
final_flags.append(False)
|
|
|
|
return PathOrderResult(paths=ordered_paths, reversed_flags=final_flags)
|