topologicpy 0.8.14__py3-none-any.whl → 0.8.17__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- topologicpy/Cell.py +1 -1
- topologicpy/Dictionary.py +24 -0
- topologicpy/Face.py +48 -12
- topologicpy/Matrix.py +77 -0
- topologicpy/Polyskel.py +832 -620
- topologicpy/Topology.py +381 -125
- topologicpy/Vertex.py +4 -4
- topologicpy/Wire.py +6 -6
- topologicpy/version.py +1 -1
- {topologicpy-0.8.14.dist-info → topologicpy-0.8.17.dist-info}/METADATA +1 -1
- {topologicpy-0.8.14.dist-info → topologicpy-0.8.17.dist-info}/RECORD +14 -14
- {topologicpy-0.8.14.dist-info → topologicpy-0.8.17.dist-info}/WHEEL +1 -1
- {topologicpy-0.8.14.dist-info → topologicpy-0.8.17.dist-info}/LICENSE +0 -0
- {topologicpy-0.8.14.dist-info → topologicpy-0.8.17.dist-info}/top_level.txt +0 -0
topologicpy/Polyskel.py
CHANGED
@@ -1,178 +1,184 @@
|
|
1
|
-
# Copyright (C) 2024
|
2
|
-
# Wassim Jabi <wassim.jabi@gmail.com>
|
3
|
-
#
|
4
|
-
# This program is free software: you can redistribute it and/or modify it under
|
5
|
-
# the terms of the GNU Affero General Public License as published by the Free Software
|
6
|
-
# Foundation, either version 3 of the License, or (at your option) any later
|
7
|
-
# version.
|
8
|
-
#
|
9
|
-
# This program is distributed in the hope that it will be useful, but WITHOUT
|
10
|
-
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
11
|
-
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
12
|
-
# details.
|
13
|
-
#
|
14
|
-
# You should have received a copy of the GNU Affero General Public License along with
|
15
|
-
# this program. If not, see <https://www.gnu.org/licenses/>.
|
16
|
-
|
17
|
-
# -*- coding: utf-8 -*-
|
18
|
-
import logging
|
19
1
|
import heapq
|
20
|
-
|
2
|
+
import logging
|
3
|
+
import math
|
4
|
+
from itertools import tee, islice, cycle, chain
|
21
5
|
from collections import namedtuple
|
22
|
-
import
|
23
|
-
import
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
# except:
|
28
|
-
# print("Polyskel - Installing required euclid library.")
|
29
|
-
# try:
|
30
|
-
# os.system("pip install euclid")
|
31
|
-
# except:
|
32
|
-
# os.system("pip install euclid --user")
|
33
|
-
# try:
|
34
|
-
# from euclid import *
|
35
|
-
# except:
|
36
|
-
# warnings.warn("Polyskel - ERROR: Could not import euclid.")
|
6
|
+
from topologicpy.Vertex import Vertex
|
7
|
+
from topologicpy.Edge import Edge
|
8
|
+
from topologicpy.Wire import Wire
|
9
|
+
from topologicpy.Cluster import Cluster
|
10
|
+
from topologicpy.Topology import Topology
|
37
11
|
|
38
12
|
log = logging.getLogger("__name__")
|
13
|
+
EPSILON = 1e-5
|
14
|
+
|
15
|
+
# Define event classes with ordering
|
16
|
+
class _SplitEvent:
|
17
|
+
def __init__(self, distance, intersection_point, vertex, opposite_edge):
|
18
|
+
self.distance = distance
|
19
|
+
self.intersection_point = intersection_point
|
20
|
+
self.vertex = vertex
|
21
|
+
self.opposite_edge = opposite_edge
|
22
|
+
|
23
|
+
def __lt__(self, other):
|
24
|
+
return self.distance < other.distance
|
25
|
+
|
26
|
+
class _EdgeEvent:
|
27
|
+
def __init__(self, distance, intersection_point, vertex_a, vertex_b):
|
28
|
+
self.distance = distance
|
29
|
+
self.intersection_point = intersection_point
|
30
|
+
self.vertex_a = vertex_a
|
31
|
+
self.vertex_b = vertex_b
|
32
|
+
|
33
|
+
def __lt__(self, other):
|
34
|
+
return self.distance < other.distance
|
35
|
+
|
36
|
+
_OriginalEdge = namedtuple("_OriginalEdge", "edge, bisector")
|
37
|
+
Subtree = namedtuple("Subtree", "source, height, sinks")
|
39
38
|
|
40
|
-
|
39
|
+
def distance(v1, v2):
|
40
|
+
return math.sqrt((v1.X() - v2.X())**2 + (v1.Y() - v2.Y())**2)
|
41
41
|
|
42
|
+
def bisector(v1, v2, v3):
|
43
|
+
"""Compute the bisector direction at vertex v2 formed by edges (v1-v2) and (v2-v3)."""
|
44
|
+
e1 = Vertex.ByCoordinates(v1.X() - v2.X(), v1.Y() - v2.Y(), 0)
|
45
|
+
e2 = Vertex.ByCoordinates(v3.X() - v2.X(), v3.Y() - v2.Y(), 0)
|
46
|
+
bisector = Vertex.ByCoordinates(e1.X() + e2.X(), e1.Y() + e2.Y(), 0)
|
47
|
+
return normalize(bisector)
|
42
48
|
|
49
|
+
def normalize(v):
|
50
|
+
length = math.sqrt(v.X()**2 + v.Y()**2)
|
51
|
+
return Vertex.ByCoordinates(v.X()/length, v.Y()/length, 0) if length > 0 else v
|
43
52
|
|
44
|
-
|
53
|
+
class _EventQueue:
|
54
|
+
def __init__(self):
|
55
|
+
self.__data = []
|
56
|
+
def put(self, item):
|
57
|
+
if item is not None:
|
58
|
+
heapq.heappush(self.__data, item)
|
59
|
+
def put_all(self, iterable):
|
60
|
+
for item in iterable:
|
61
|
+
heapq.heappush(self.__data, item)
|
62
|
+
def get(self):
|
63
|
+
return heapq.heappop(self.__data)
|
64
|
+
def empty(self):
|
65
|
+
return len(self.__data) == 0
|
45
66
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
def
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
return Point2(-self.x, -self.y)
|
62
|
-
|
63
|
-
def __eq__(self, other):
|
64
|
-
return self.x == other.x and self.y == other.y
|
65
|
-
|
66
|
-
def __abs__(self):
|
67
|
-
return math.sqrt(self.x ** 2 + self.y ** 2)
|
68
|
-
|
67
|
+
def _window(lst):
|
68
|
+
prevs, items, nexts = tee(lst, 3)
|
69
|
+
prevs = islice(cycle(prevs), len(lst) - 1, None)
|
70
|
+
nexts = islice(cycle(nexts), 1, None)
|
71
|
+
return zip(prevs, items, nexts)
|
72
|
+
|
73
|
+
def _normalize_contour(contour):
|
74
|
+
contour = [Vertex.ByCoordinates(float(x), float(y), 0) for x, y in contour]
|
75
|
+
return [point for prev, point, next in _window(contour) if not (point == next or (prev.X() == point.X() and prev.Y() == point.Y()))]
|
76
|
+
|
77
|
+
class _SLAV:
|
78
|
+
def __init__(self, edges):
|
79
|
+
self.edges = set(edges)
|
80
|
+
self.active_vertices = {v for e in edges for v in Edge.Vertices(e)}
|
81
|
+
|
69
82
|
def __iter__(self):
|
70
|
-
yield self.
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
return
|
84
|
-
|
85
|
-
def
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
def dot(self, other):
|
90
|
-
return self.x * other.x + self.y * other.y
|
91
|
-
|
92
|
-
def distance(self, other):
|
93
|
-
return abs(self - other)
|
94
|
-
|
95
|
-
class Ray2:
|
96
|
-
def __init__(self, p, v):
|
97
|
-
self.p = p
|
98
|
-
self.v = v.normalized()
|
99
|
-
|
100
|
-
def __str__(self):
|
101
|
-
return f"Ray2({self.p}, {self.v})"
|
102
|
-
|
103
|
-
def intersect(self, other):
|
104
|
-
"""
|
105
|
-
Intersects this ray with another ray.
|
106
|
-
|
107
|
-
Args:
|
108
|
-
other (Ray2): The other ray to intersect with.
|
109
|
-
|
110
|
-
Returns:
|
111
|
-
Point2 or None: The intersection point if it exists, or None if the rays do not intersect.
|
112
|
-
"""
|
113
|
-
# Check if the rays are parallel
|
114
|
-
if abs(self.v.dot(other.v)) == 1:
|
115
|
-
return None # Rays are parallel and do not intersect
|
83
|
+
yield from self.edges
|
84
|
+
|
85
|
+
def empty(self):
|
86
|
+
return len(self.active_vertices) == 0
|
87
|
+
|
88
|
+
def generate_initial_events(self):
|
89
|
+
events = []
|
90
|
+
for edge in self.edges:
|
91
|
+
v1, v2 = Edge.Vertices(edge)
|
92
|
+
if v1 == v2:
|
93
|
+
continue # Ignore degenerate edges
|
94
|
+
mid = Vertex.ByCoordinates((v1.X() + v2.X()) / 2, (v1.Y() + v2.Y()) / 2, 0)
|
95
|
+
events.append(_EdgeEvent(distance(v1, v2) / 2, mid, v1, v2))
|
96
|
+
return events
|
97
|
+
|
98
|
+
def handle_edge_event(self, event):
|
99
|
+
if event.vertex_a == event.vertex_b:
|
100
|
+
return None, [] # Ignore redundant events
|
116
101
|
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
102
|
+
new_vertex = event.intersection_point
|
103
|
+
new_edge = Edge.ByVertices([event.vertex_a, new_vertex])
|
104
|
+
|
105
|
+
self.edges.discard(Edge.ByVertices([event.vertex_a, event.vertex_b]))
|
106
|
+
self.edges.add(new_edge)
|
107
|
+
|
108
|
+
self.active_vertices.discard(event.vertex_a)
|
109
|
+
self.active_vertices.discard(event.vertex_b)
|
110
|
+
self.active_vertices.add(new_vertex)
|
111
|
+
|
112
|
+
sinks = [event.vertex_a, event.vertex_b]
|
113
|
+
return Subtree(new_vertex, event.distance, sinks), []
|
114
|
+
|
115
|
+
def handle_split_event(self, event):
|
116
|
+
if event.vertex == event.opposite_edge:
|
117
|
+
return None, [] # Ignore invalid splits
|
118
|
+
|
119
|
+
new_vertex = event.intersection_point
|
120
|
+
new_edge = Edge.ByVertices([event.vertex, new_vertex])
|
121
|
+
|
122
|
+
self.edges.discard(event.opposite_edge)
|
123
|
+
self.edges.add(new_edge)
|
124
|
+
|
125
|
+
self.active_vertices.discard(event.vertex)
|
126
|
+
self.active_vertices.add(new_vertex)
|
127
|
+
|
128
|
+
sinks = [event.vertex]
|
129
|
+
return Subtree(new_vertex, event.distance, sinks), []
|
127
130
|
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
131
|
+
def skeletonize(polygon, holes=None):
|
132
|
+
"""
|
133
|
+
Compute the straight skeleton of a polygon using TopologicPy.
|
134
|
+
|
135
|
+
polygon: List of (x, y) tuples defining the outer boundary in CCW order.
|
136
|
+
holes: List of inner boundaries (holes), each defined in CW order.
|
137
|
+
|
138
|
+
Returns: List of Subtrees representing the skeleton.
|
139
|
+
"""
|
140
|
+
polygon = _normalize_contour(polygon)
|
141
|
+
holes = [_normalize_contour(hole) for hole in (holes or [])]
|
142
|
+
|
143
|
+
outer_wire = Wire.ByVertices(polygon)
|
144
|
+
inner_wires = [Wire.ByVertices(hole) for hole in holes]
|
145
|
+
edges = set(Wire.Edges(outer_wire) + sum([Wire.Edges(w) for w in inner_wires], []))
|
146
|
+
|
147
|
+
slav = _SLAV(edges)
|
148
|
+
output = []
|
149
|
+
event_queue = _EventQueue()
|
150
|
+
|
151
|
+
event_queue.put_all(slav.generate_initial_events())
|
152
|
+
|
153
|
+
while not event_queue.empty() and not slav.empty():
|
154
|
+
event = event_queue.get()
|
155
|
+
if isinstance(event, _EdgeEvent):
|
156
|
+
arc, events = slav.handle_edge_event(event)
|
157
|
+
elif isinstance(event, _SplitEvent):
|
158
|
+
arc, events = slav.handle_split_event(event)
|
159
|
+
event_queue.put_all(events)
|
160
|
+
if arc is not None:
|
161
|
+
output.append(arc)
|
162
|
+
# skeleton_edges = []
|
163
|
+
# for i in range(len(output)):
|
164
|
+
# v1 = polygon[i]
|
165
|
+
# v2 = polygon[(i + 1) % len(polygon)]
|
166
|
+
# mid = Vertex.ByCoordinates((v1.X() + v2.X()) / 2, (v1.Y() + v2.Y()) / 2, 0)
|
167
|
+
# skeleton_edges.append(Edge.ByVertices([v1, mid]))
|
168
|
+
|
169
|
+
# skeleton_wire = Topology.SelfMerge(Cluster.ByTopologies(skeleton_edges))
|
170
|
+
# Topology.Show(skeleton_wire, outer_wire, vertexSize=8)
|
171
|
+
return output
|
172
|
+
|
136
173
|
|
137
|
-
def intersect(self, other):
|
138
|
-
# Line intersection formula
|
139
|
-
det = self.v.x * other.v.y - self.v.y * other.v.x
|
140
|
-
if abs(det) <= EPSILON:
|
141
|
-
return None # Lines are parallel
|
142
174
|
|
143
|
-
dx = other.p.x - self.p.x
|
144
|
-
dy = other.p.y - self.p.y
|
145
|
-
t = (dx * other.v.y - dy * other.v.x) / det
|
146
|
-
return Point2(self.p.x + t * self.v.x, self.p.y + t * self.v.y)
|
147
175
|
|
148
|
-
def distance(self, point):
|
149
|
-
# Perpendicular distance from a point to the line
|
150
|
-
return abs((point.x - self.p.x) * self.v.y - (point.y - self.p.y) * self.v.x) / abs(self.v)
|
151
176
|
|
152
|
-
def __str__(self):
|
153
|
-
return f"Line2({self.p}, {self.v})"
|
154
177
|
|
155
|
-
class LineSegment2(Line2):
|
156
|
-
def __init__(self, p1, p2):
|
157
|
-
super().__init__(p1, p2)
|
158
|
-
self.p2 = p2
|
159
178
|
|
160
|
-
def intersect(self, other):
|
161
|
-
# Check if intersection point lies on both line segments
|
162
|
-
inter = super().intersect(other)
|
163
|
-
if inter is None:
|
164
|
-
return None
|
165
179
|
|
166
|
-
if self._on_segment(inter) and other._on_segment(inter):
|
167
|
-
return inter
|
168
|
-
return None
|
169
180
|
|
170
|
-
def _on_segment(self, point):
|
171
|
-
return (min(self.p.x, self.p2.x) - EPSILON <= point.x <= max(self.p.x, self.p2.x) + EPSILON and
|
172
|
-
min(self.p.y, self.p2.y) - EPSILON <= point.y <= max(self.p.y, self.p2.y) + EPSILON)
|
173
181
|
|
174
|
-
def __str__(self):
|
175
|
-
return f"LineSegment2({self.p}, {self.p2})"
|
176
182
|
|
177
183
|
|
178
184
|
|
@@ -181,490 +187,696 @@ class LineSegment2(Line2):
|
|
181
187
|
|
182
188
|
|
183
189
|
|
184
|
-
class Debug:
|
185
|
-
def __init__(self, image):
|
186
|
-
if image is not None:
|
187
|
-
self.im = image[0]
|
188
|
-
self.draw = image[1]
|
189
|
-
self.do = True
|
190
|
-
else:
|
191
|
-
self.do = False
|
192
190
|
|
193
|
-
def line(self, *args, **kwargs):
|
194
|
-
if self.do:
|
195
|
-
self.draw.line(*args, **kwargs)
|
196
191
|
|
197
|
-
def rectangle(self, *args, **kwargs):
|
198
|
-
if self.do:
|
199
|
-
self.draw.rectangle(*args, **kwargs)
|
200
192
|
|
201
|
-
def show(self):
|
202
|
-
if self.do:
|
203
|
-
self.im.show()
|
204
193
|
|
205
|
-
_debug = Debug(None)
|
206
194
|
|
207
|
-
def set_debug(image):
|
208
|
-
global _debug
|
209
|
-
_debug = Debug(image)
|
210
195
|
|
211
|
-
def _window(lst):
|
212
|
-
prevs, items, nexts = tee(lst, 3)
|
213
|
-
prevs = islice(cycle(prevs), len(lst) - 1, None)
|
214
|
-
nexts = islice(cycle(nexts), 1, None)
|
215
|
-
return zip(prevs, items, nexts)
|
216
196
|
|
217
|
-
def _cross(a, b):
|
218
|
-
res = a.x * b.y - b.x * a.y
|
219
|
-
return res
|
220
197
|
|
221
|
-
def _approximately_equals(a, b):
|
222
|
-
return a == b or (abs(a - b) <= max(abs(a), abs(b)) * 0.001)
|
223
198
|
|
224
|
-
def _approximately_same(point_a, point_b):
|
225
|
-
return _approximately_equals(point_a.x, point_b.x) and _approximately_equals(point_a.y, point_b.y)
|
226
199
|
|
227
|
-
def _normalize_contour(contour):
|
228
|
-
contour = [Point2(float(x), float(y)) for (x, y) in contour]
|
229
|
-
return [point for prev, point, next in _window(contour) if not (point == next or (point-prev).normalized() == (next - point).normalized())]
|
230
200
|
|
231
|
-
class _SplitEvent(namedtuple("_SplitEvent", "distance, intersection_point, vertex, opposite_edge")):
|
232
|
-
__slots__ = ()
|
233
201
|
|
234
|
-
def __lt__(self, other):
|
235
|
-
return self.distance < other.distance
|
236
202
|
|
237
|
-
def __str__(self):
|
238
|
-
return "{} Split event @ {} from {} to {}".format(self.distance, self.intersection_point, self.vertex, self.opposite_edge)
|
239
203
|
|
240
|
-
class _EdgeEvent(namedtuple("_EdgeEvent", "distance intersection_point vertex_a vertex_b")):
|
241
|
-
__slots__ = ()
|
242
204
|
|
243
|
-
def __lt__(self, other):
|
244
|
-
return self.distance < other.distance
|
245
205
|
|
246
|
-
def __str__(self):
|
247
|
-
return "{} Edge event @ {} between {} and {}".format(self.distance, self.intersection_point, self.vertex_a, self.vertex_b)
|
248
206
|
|
249
|
-
_OriginalEdge = namedtuple("_OriginalEdge", "edge bisector_left, bisector_right")
|
250
207
|
|
251
|
-
Subtree = namedtuple("Subtree", "source, height, sinks")
|
252
208
|
|
253
|
-
class _LAVertex:
|
254
|
-
def __init__(self, point, edge_left, edge_right, direction_vectors=None):
|
255
|
-
self.point = point
|
256
|
-
self.edge_left = edge_left
|
257
|
-
self.edge_right = edge_right
|
258
|
-
self.prev = None
|
259
|
-
self.next = None
|
260
|
-
self.lav = None
|
261
|
-
self._valid = True # TODO this might be handled better. Maybe membership in lav implies validity?
|
262
|
-
|
263
|
-
import operator
|
264
|
-
creator_vectors = (edge_left.v.normalized() * -1, edge_right.v.normalized())
|
265
|
-
if direction_vectors is None:
|
266
|
-
direction_vectors = creator_vectors
|
267
|
-
|
268
|
-
self._is_reflex = (_cross(*direction_vectors)) < 0
|
269
|
-
self._bisector = Ray2(self.point, operator.add(*creator_vectors) * (-1 if self.is_reflex else 1))
|
270
|
-
log.info("Created vertex %s", self.__repr__())
|
271
|
-
_debug.line((self.bisector.p.x, self.bisector.p.y, self.bisector.p.x + self.bisector.v.x * 100, self.bisector.p.y + self.bisector.v.y * 100), fill="blue")
|
272
|
-
|
273
|
-
@property
|
274
|
-
def bisector(self):
|
275
|
-
return self._bisector
|
276
|
-
|
277
|
-
@property
|
278
|
-
def is_reflex(self):
|
279
|
-
return self._is_reflex
|
280
|
-
|
281
|
-
@property
|
282
|
-
def original_edges(self):
|
283
|
-
return self.lav._slav._original_edges
|
284
|
-
|
285
|
-
def next_event(self):
|
286
|
-
events = []
|
287
|
-
if self.is_reflex:
|
288
|
-
# a reflex vertex may generate a split event
|
289
|
-
# split events happen when a vertex hits an opposite edge, splitting the polygon in two.
|
290
|
-
log.debug("looking for split candidates for vertex %s", self)
|
291
|
-
for edge in self.original_edges:
|
292
|
-
if edge.edge == self.edge_left or edge.edge == self.edge_right:
|
293
|
-
continue
|
294
|
-
|
295
|
-
log.debug("\tconsidering EDGE %s", edge)
|
296
|
-
|
297
|
-
# a potential b is at the intersection of between our own bisector and the bisector of the
|
298
|
-
# angle between the tested edge and any one of our own edges.
|
299
|
-
|
300
|
-
# we choose the "less parallel" edge (in order to exclude a potentially parallel edge)
|
301
|
-
leftdot = abs(self.edge_left.v.normalized().dot(edge.edge.v.normalized()))
|
302
|
-
rightdot = abs(self.edge_right.v.normalized().dot(edge.edge.v.normalized()))
|
303
|
-
selfedge = self.edge_left if leftdot < rightdot else self.edge_right
|
304
|
-
otheredge = self.edge_left if leftdot > rightdot else self.edge_right
|
305
|
-
|
306
|
-
i = Line2(selfedge).intersect(Line2(edge.edge))
|
307
|
-
if i is not None and not _approximately_equals(i, self.point):
|
308
|
-
# locate candidate b
|
309
|
-
linvec = (self.point - i).normalized()
|
310
|
-
edvec = edge.edge.v.normalized()
|
311
|
-
if linvec.dot(edvec) < 0:
|
312
|
-
edvec = -edvec
|
313
|
-
|
314
|
-
bisecvec = edvec + linvec
|
315
|
-
if abs(bisecvec) == 0:
|
316
|
-
continue
|
317
|
-
bisector = Line2(i, bisecvec)
|
318
|
-
b = bisector.intersect(self.bisector)
|
319
|
-
|
320
|
-
if b is None:
|
321
|
-
continue
|
322
|
-
|
323
|
-
# check eligibility of b
|
324
|
-
# a valid b should lie within the area limited by the edge and the bisectors of its two vertices:
|
325
|
-
xleft = _cross(edge.bisector_left.v.normalized(), (b - edge.bisector_left.p).normalized()) > -EPSILON
|
326
|
-
xright = _cross(edge.bisector_right.v.normalized(), (b - edge.bisector_right.p).normalized()) <= EPSILON
|
327
|
-
xedge = _cross(edge.edge.v.normalized(), (b - edge.edge.p).normalized()) <= EPSILON
|
328
|
-
|
329
|
-
if not (xleft and xright and xedge):
|
330
|
-
log.debug("\t\tDiscarded candidate %s (%s-%s-%s)", b, xleft, xright, xedge)
|
331
|
-
continue
|
332
|
-
|
333
|
-
log.debug("\t\tFound valid candidate %s", b)
|
334
|
-
events.append(_SplitEvent(Line2(edge.edge).distance(b), b, self, edge.edge))
|
335
|
-
|
336
|
-
i_prev = self.bisector.intersect(self.prev.bisector)
|
337
|
-
i_next = self.bisector.intersect(self.next.bisector)
|
338
|
-
|
339
|
-
if i_prev is not None:
|
340
|
-
events.append(_EdgeEvent(Line2(self.edge_left).distance(i_prev), i_prev, self.prev, self))
|
341
|
-
if i_next is not None:
|
342
|
-
events.append(_EdgeEvent(Line2(self.edge_right).distance(i_next), i_next, self, self.next))
|
343
|
-
|
344
|
-
if not events:
|
345
|
-
return None
|
346
|
-
|
347
|
-
ev = min(events, key=lambda event: self.point.distance(event.intersection_point))
|
348
|
-
|
349
|
-
log.info("Generated new event for %s: %s", self, ev)
|
350
|
-
return ev
|
351
|
-
|
352
|
-
def invalidate(self):
|
353
|
-
if self.lav is not None:
|
354
|
-
self.lav.invalidate(self)
|
355
|
-
else:
|
356
|
-
self._valid = False
|
357
|
-
|
358
|
-
@property
|
359
|
-
def is_valid(self):
|
360
|
-
return self._valid
|
361
|
-
|
362
|
-
def __str__(self):
|
363
|
-
return "Vertex ({:.2f};{:.2f})".format(self.point.x, self.point.y)
|
364
|
-
|
365
|
-
def __repr__(self):
|
366
|
-
return "Vertex ({}) ({:.2f};{:.2f}), bisector {}, edges {} {}".format("reflex" if self.is_reflex else "convex",
|
367
|
-
self.point.x, self.point.y, self.bisector,
|
368
|
-
self.edge_left, self.edge_right)
|
369
209
|
|
370
|
-
class _SLAV:
|
371
|
-
def __init__(self, polygon, holes):
|
372
|
-
contours = [_normalize_contour(polygon)]
|
373
|
-
contours.extend([_normalize_contour(hole) for hole in holes])
|
374
|
-
|
375
|
-
self._lavs = [_LAV.from_polygon(contour, self) for contour in contours]
|
376
|
-
|
377
|
-
# store original polygon edges for calculating split events
|
378
|
-
self._original_edges = [
|
379
|
-
_OriginalEdge(LineSegment2(vertex.prev.point, vertex.point), vertex.prev.bisector, vertex.bisector)
|
380
|
-
for vertex in chain.from_iterable(self._lavs)
|
381
|
-
]
|
382
|
-
|
383
|
-
def __iter__(self):
|
384
|
-
for lav in self._lavs:
|
385
|
-
yield lav
|
386
|
-
|
387
|
-
def __len__(self):
|
388
|
-
return len(self._lavs)
|
389
|
-
|
390
|
-
def empty(self):
|
391
|
-
return len(self._lavs) == 0
|
392
|
-
|
393
|
-
def handle_edge_event(self, event):
|
394
|
-
sinks = []
|
395
|
-
events = []
|
396
|
-
|
397
|
-
lav = event.vertex_a.lav
|
398
|
-
if event.vertex_a.prev == event.vertex_b.next:
|
399
|
-
log.info("%.2f Peak event at intersection %s from <%s,%s,%s> in %s", event.distance,
|
400
|
-
event.intersection_point, event.vertex_a, event.vertex_b, event.vertex_a.prev, lav)
|
401
|
-
self._lavs.remove(lav)
|
402
|
-
for vertex in list(lav):
|
403
|
-
sinks.append(vertex.point)
|
404
|
-
vertex.invalidate()
|
405
|
-
else:
|
406
|
-
log.info("%.2f Edge event at intersection %s from <%s,%s> in %s", event.distance, event.intersection_point,
|
407
|
-
event.vertex_a, event.vertex_b, lav)
|
408
|
-
new_vertex = lav.unify(event.vertex_a, event.vertex_b, event.intersection_point)
|
409
|
-
if lav.head in (event.vertex_a, event.vertex_b):
|
410
|
-
lav.head = new_vertex
|
411
|
-
sinks.extend((event.vertex_a.point, event.vertex_b.point))
|
412
|
-
next_event = new_vertex.next_event()
|
413
|
-
if next_event is not None:
|
414
|
-
events.append(next_event)
|
415
|
-
|
416
|
-
return (Subtree(event.intersection_point, event.distance, sinks), events)
|
417
|
-
|
418
|
-
def handle_split_event(self, event):
|
419
|
-
lav = event.vertex.lav
|
420
|
-
log.info("%.2f Split event at intersection %s from vertex %s, for edge %s in %s", event.distance,
|
421
|
-
event.intersection_point, event.vertex, event.opposite_edge, lav)
|
422
|
-
|
423
|
-
sinks = [event.vertex.point]
|
424
|
-
vertices = []
|
425
|
-
x = None # right vertex
|
426
|
-
y = None # left vertex
|
427
|
-
norm = event.opposite_edge.v.normalized()
|
428
|
-
for v in chain.from_iterable(self._lavs):
|
429
|
-
log.debug("%s in %s", v, v.lav)
|
430
|
-
if norm == v.edge_left.v.normalized() and event.opposite_edge.p == v.edge_left.p:
|
431
|
-
x = v
|
432
|
-
y = x.prev
|
433
|
-
elif norm == v.edge_right.v.normalized() and event.opposite_edge.p == v.edge_right.p:
|
434
|
-
y = v
|
435
|
-
x = y.next
|
436
|
-
|
437
|
-
if x:
|
438
|
-
xleft = _cross(y.bisector.v.normalized(), (event.intersection_point - y.point).normalized()) >= -EPSILON
|
439
|
-
xright = _cross(x.bisector.v.normalized(), (event.intersection_point - x.point).normalized()) <= EPSILON
|
440
|
-
log.debug("Vertex %s holds edge as %s edge (%s, %s)", v, ("left" if x == v else "right"), xleft, xright)
|
441
|
-
|
442
|
-
if xleft and xright:
|
443
|
-
break
|
444
|
-
else:
|
445
|
-
x = None
|
446
|
-
y = None
|
447
|
-
|
448
|
-
if x is None:
|
449
|
-
log.info("Failed split event %s (equivalent edge event is expected to follow)", event)
|
450
|
-
return (None, [])
|
451
|
-
|
452
|
-
v1 = _LAVertex(event.intersection_point, event.vertex.edge_left, event.opposite_edge)
|
453
|
-
v2 = _LAVertex(event.intersection_point, event.opposite_edge, event.vertex.edge_right)
|
454
|
-
|
455
|
-
v1.prev = event.vertex.prev
|
456
|
-
v1.next = x
|
457
|
-
event.vertex.prev.next = v1
|
458
|
-
x.prev = v1
|
459
|
-
|
460
|
-
v2.prev = y
|
461
|
-
v2.next = event.vertex.next
|
462
|
-
event.vertex.next.prev = v2
|
463
|
-
y.next = v2
|
464
|
-
|
465
|
-
new_lavs = None
|
466
|
-
self._lavs.remove(lav)
|
467
|
-
if lav != x.lav:
|
468
|
-
# the split event actually merges two lavs
|
469
|
-
self._lavs.remove(x.lav)
|
470
|
-
new_lavs = [_LAV.from_chain(v1, self)]
|
471
|
-
else:
|
472
|
-
new_lavs = [_LAV.from_chain(v1, self), _LAV.from_chain(v2, self)]
|
473
|
-
|
474
|
-
for l in new_lavs:
|
475
|
-
log.debug(l)
|
476
|
-
if len(l) > 2:
|
477
|
-
self._lavs.append(l)
|
478
|
-
vertices.append(l.head)
|
479
|
-
else:
|
480
|
-
log.info("LAV %s has collapsed into the line %s--%s", l, l.head.point, l.head.next.point)
|
481
|
-
sinks.append(l.head.next.point)
|
482
|
-
for v in list(l):
|
483
|
-
v.invalidate()
|
484
|
-
|
485
|
-
events = []
|
486
|
-
for vertex in vertices:
|
487
|
-
next_event = vertex.next_event()
|
488
|
-
if next_event is not None:
|
489
|
-
events.append(next_event)
|
490
|
-
|
491
|
-
event.vertex.invalidate()
|
492
|
-
return (Subtree(event.intersection_point, event.distance, sinks), events)
|
493
|
-
|
494
|
-
class _LAV:
|
495
|
-
def __init__(self, slav):
|
496
|
-
self.head = None
|
497
|
-
self._slav = slav
|
498
|
-
self._len = 0
|
499
|
-
log.debug("Created LAV %s", self)
|
500
|
-
|
501
|
-
@classmethod
|
502
|
-
def from_polygon(cls, polygon, slav):
|
503
|
-
lav = cls(slav)
|
504
|
-
for prev, point, next in _window(polygon):
|
505
|
-
lav._len += 1
|
506
|
-
vertex = _LAVertex(point, LineSegment2(prev, point), LineSegment2(point, next))
|
507
|
-
vertex.lav = lav
|
508
|
-
if lav.head is None:
|
509
|
-
lav.head = vertex
|
510
|
-
vertex.prev = vertex.next = vertex
|
511
|
-
else:
|
512
|
-
vertex.next = lav.head
|
513
|
-
vertex.prev = lav.head.prev
|
514
|
-
vertex.prev.next = vertex
|
515
|
-
lav.head.prev = vertex
|
516
|
-
return lav
|
517
|
-
|
518
|
-
@classmethod
|
519
|
-
def from_chain(cls, head, slav):
|
520
|
-
lav = cls(slav)
|
521
|
-
lav.head = head
|
522
|
-
for vertex in lav:
|
523
|
-
lav._len += 1
|
524
|
-
vertex.lav = lav
|
525
|
-
return lav
|
526
|
-
|
527
|
-
def invalidate(self, vertex):
|
528
|
-
assert vertex.lav is self, "Tried to invalidate a vertex that's not mine"
|
529
|
-
log.debug("Invalidating %s", vertex)
|
530
|
-
vertex._valid = False
|
531
|
-
if self.head == vertex:
|
532
|
-
self.head = self.head.next
|
533
|
-
vertex.lav = None
|
534
|
-
|
535
|
-
def unify(self, vertex_a, vertex_b, point):
|
536
|
-
replacement = _LAVertex(point, vertex_a.edge_left, vertex_b.edge_right,
|
537
|
-
(vertex_b.bisector.v.normalized(), vertex_a.bisector.v.normalized()))
|
538
|
-
replacement.lav = self
|
539
|
-
|
540
|
-
if self.head in [vertex_a, vertex_b]:
|
541
|
-
self.head = replacement
|
542
|
-
|
543
|
-
vertex_a.prev.next = replacement
|
544
|
-
vertex_b.next.prev = replacement
|
545
|
-
replacement.prev = vertex_a.prev
|
546
|
-
replacement.next = vertex_b.next
|
547
|
-
|
548
|
-
vertex_a.invalidate()
|
549
|
-
vertex_b.invalidate()
|
550
|
-
|
551
|
-
self._len -= 1
|
552
|
-
return replacement
|
553
|
-
|
554
|
-
def __str__(self):
|
555
|
-
return "LAV {}".format(id(self))
|
556
|
-
|
557
|
-
def __repr__(self):
|
558
|
-
return "{} = {}".format(str(self), [vertex for vertex in self])
|
559
|
-
|
560
|
-
def __len__(self):
|
561
|
-
return self._len
|
562
|
-
|
563
|
-
def __iter__(self):
|
564
|
-
cur = self.head
|
565
|
-
while True:
|
566
|
-
yield cur
|
567
|
-
cur = cur.next
|
568
|
-
if cur == self.head:
|
569
|
-
return
|
570
|
-
|
571
|
-
def _show(self):
|
572
|
-
cur = self.head
|
573
|
-
while True:
|
574
|
-
print(cur.__repr__())
|
575
|
-
cur = cur.next
|
576
|
-
if cur == self.head:
|
577
|
-
break
|
578
210
|
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
|
596
|
-
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
|
605
|
-
|
606
|
-
|
607
|
-
|
608
|
-
|
609
|
-
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
|
624
|
-
|
625
|
-
|
626
|
-
|
627
|
-
|
628
|
-
|
629
|
-
|
630
|
-
|
631
|
-
|
632
|
-
|
633
|
-
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
|
643
|
-
|
644
|
-
|
645
|
-
|
646
|
-
|
647
|
-
|
648
|
-
|
649
|
-
|
650
|
-
|
651
|
-
|
652
|
-
|
653
|
-
|
654
|
-
|
655
|
-
|
656
|
-
|
657
|
-
|
658
|
-
|
659
|
-
|
660
|
-
|
661
|
-
|
662
|
-
|
663
|
-
|
664
|
-
|
665
|
-
|
666
|
-
|
667
|
-
|
668
|
-
|
669
|
-
|
670
|
-
|
211
|
+
|
212
|
+
|
213
|
+
# # Copyright (C) 2024
|
214
|
+
# # Wassim Jabi <wassim.jabi@gmail.com>
|
215
|
+
# #
|
216
|
+
# # This program is free software: you can redistribute it and/or modify it under
|
217
|
+
# # the terms of the GNU Affero General Public License as published by the Free Software
|
218
|
+
# # Foundation, either version 3 of the License, or (at your option) any later
|
219
|
+
# # version.
|
220
|
+
# #
|
221
|
+
# # This program is distributed in the hope that it will be useful, but WITHOUT
|
222
|
+
# # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
223
|
+
# # FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
224
|
+
# # details.
|
225
|
+
# #
|
226
|
+
# # You should have received a copy of the GNU Affero General Public License along with
|
227
|
+
# # this program. If not, see <https://www.gnu.org/licenses/>.
|
228
|
+
|
229
|
+
# # -*- coding: utf-8 -*-
|
230
|
+
# import logging
|
231
|
+
# import heapq
|
232
|
+
# from itertools import *
|
233
|
+
# from collections import namedtuple
|
234
|
+
# import os
|
235
|
+
# import warnings
|
236
|
+
|
237
|
+
# # try:
|
238
|
+
# # from euclid import *
|
239
|
+
# # except:
|
240
|
+
# # print("Polyskel - Installing required euclid library.")
|
241
|
+
# # try:
|
242
|
+
# # os.system("pip install euclid")
|
243
|
+
# # except:
|
244
|
+
# # os.system("pip install euclid --user")
|
245
|
+
# # try:
|
246
|
+
# # from euclid import *
|
247
|
+
# # except:
|
248
|
+
# # warnings.warn("Polyskel - ERROR: Could not import euclid.")
|
249
|
+
|
250
|
+
# log = logging.getLogger("__name__")
|
251
|
+
|
252
|
+
# EPSILON = 0.00001
|
253
|
+
|
254
|
+
|
255
|
+
|
256
|
+
# import math
|
257
|
+
|
258
|
+
# class Point2:
|
259
|
+
# def __init__(self, x, y):
|
260
|
+
# self.x = x
|
261
|
+
# self.y = y
|
262
|
+
|
263
|
+
# def __sub__(self, other):
|
264
|
+
# return Point2(self.x - other.x, self.y - other.y)
|
265
|
+
|
266
|
+
# def __add__(self, other):
|
267
|
+
# return Point2(self.x + other.x, self.y + other.y)
|
268
|
+
|
269
|
+
# def __mul__(self, scalar):
|
270
|
+
# return Point2(self.x * scalar, self.y * scalar)
|
271
|
+
|
272
|
+
# def __neg__(self):
|
273
|
+
# return Point2(-self.x, -self.y)
|
274
|
+
|
275
|
+
# def __eq__(self, other):
|
276
|
+
# return self.x == other.x and self.y == other.y
|
277
|
+
|
278
|
+
# def __abs__(self):
|
279
|
+
# return math.sqrt(self.x ** 2 + self.y ** 2)
|
280
|
+
|
281
|
+
# def __iter__(self):
|
282
|
+
# yield self.x
|
283
|
+
# yield self.y
|
284
|
+
|
285
|
+
# def cross(self, other):
|
286
|
+
# """
|
287
|
+
# Computes the cross product of this point with another point.
|
288
|
+
|
289
|
+
# Args:
|
290
|
+
# other (Point2): The other point to compute the cross product with.
|
291
|
+
|
292
|
+
# Returns:
|
293
|
+
# float: The cross product value.
|
294
|
+
# """
|
295
|
+
# return self.x * other.y - self.y * other.x
|
296
|
+
|
297
|
+
# def normalized(self):
|
298
|
+
# length = abs(self)
|
299
|
+
# return Point2(self.x / length, self.y / length)
|
300
|
+
|
301
|
+
# def dot(self, other):
|
302
|
+
# return self.x * other.x + self.y * other.y
|
303
|
+
|
304
|
+
# def distance(self, other):
|
305
|
+
# return abs(self - other)
|
306
|
+
|
307
|
+
# class Ray2:
|
308
|
+
# def __init__(self, p, v):
|
309
|
+
# self.p = p
|
310
|
+
# self.v = v.normalized()
|
311
|
+
|
312
|
+
# def __str__(self):
|
313
|
+
# return f"Ray2({self.p}, {self.v})"
|
314
|
+
|
315
|
+
# def intersect(self, other):
|
316
|
+
# """
|
317
|
+
# Intersects this ray with another ray.
|
318
|
+
|
319
|
+
# Args:
|
320
|
+
# other (Ray2): The other ray to intersect with.
|
321
|
+
|
322
|
+
# Returns:
|
323
|
+
# Point2 or None: The intersection point if it exists, or None if the rays do not intersect.
|
324
|
+
# """
|
325
|
+
# # Check if the rays are parallel
|
326
|
+
# if abs(self.v.dot(other.v)) == 1:
|
327
|
+
# return None # Rays are parallel and do not intersect
|
328
|
+
|
329
|
+
# # Calculate the intersection point using vector algebra
|
330
|
+
# denom = self.v.cross(other.v)
|
331
|
+
# if not denom == 0:
|
332
|
+
# t = (other.p - self.p).cross(other.v) / self.v.cross(other.v)
|
333
|
+
# else:
|
334
|
+
# return None
|
335
|
+
# if t >= 0:
|
336
|
+
# return self.p + self.v * t # Intersection point
|
337
|
+
# else:
|
338
|
+
# return None # Rays do not intersect or intersection point lies behind self
|
339
|
+
|
340
|
+
# class Line2:
|
341
|
+
# def __init__(self, p1, p2=None):
|
342
|
+
# if p2 is None:
|
343
|
+
# self.p = p1.p
|
344
|
+
# self.v = p1.v
|
345
|
+
# else:
|
346
|
+
# self.p = p1
|
347
|
+
# self.v = (p2 - p1).normalized()
|
348
|
+
|
349
|
+
# def intersect(self, other):
|
350
|
+
# # Line intersection formula
|
351
|
+
# det = self.v.x * other.v.y - self.v.y * other.v.x
|
352
|
+
# if abs(det) <= EPSILON:
|
353
|
+
# return None # Lines are parallel
|
354
|
+
|
355
|
+
# dx = other.p.x - self.p.x
|
356
|
+
# dy = other.p.y - self.p.y
|
357
|
+
# t = (dx * other.v.y - dy * other.v.x) / det
|
358
|
+
# return Point2(self.p.x + t * self.v.x, self.p.y + t * self.v.y)
|
359
|
+
|
360
|
+
# def distance(self, point):
|
361
|
+
# # Perpendicular distance from a point to the line
|
362
|
+
# return abs((point.x - self.p.x) * self.v.y - (point.y - self.p.y) * self.v.x) / abs(self.v)
|
363
|
+
|
364
|
+
# def __str__(self):
|
365
|
+
# return f"Line2({self.p}, {self.v})"
|
366
|
+
|
367
|
+
# class LineSegment2(Line2):
|
368
|
+
# def __init__(self, p1, p2):
|
369
|
+
# super().__init__(p1, p2)
|
370
|
+
# self.p2 = p2
|
371
|
+
|
372
|
+
# def intersect(self, other):
|
373
|
+
# # Check if intersection point lies on both line segments
|
374
|
+
# inter = super().intersect(other)
|
375
|
+
# if inter is None:
|
376
|
+
# return None
|
377
|
+
|
378
|
+
# if self._on_segment(inter) and other._on_segment(inter):
|
379
|
+
# return inter
|
380
|
+
# return None
|
381
|
+
|
382
|
+
# def _on_segment(self, point):
|
383
|
+
# return (min(self.p.x, self.p2.x) - EPSILON <= point.x <= max(self.p.x, self.p2.x) + EPSILON and
|
384
|
+
# min(self.p.y, self.p2.y) - EPSILON <= point.y <= max(self.p.y, self.p2.y) + EPSILON)
|
385
|
+
|
386
|
+
# def __str__(self):
|
387
|
+
# return f"LineSegment2({self.p}, {self.p2})"
|
388
|
+
|
389
|
+
|
390
|
+
|
391
|
+
|
392
|
+
|
393
|
+
|
394
|
+
|
395
|
+
|
396
|
+
# class Debug:
|
397
|
+
# def __init__(self, image):
|
398
|
+
# if image is not None:
|
399
|
+
# self.im = image[0]
|
400
|
+
# self.draw = image[1]
|
401
|
+
# self.do = True
|
402
|
+
# else:
|
403
|
+
# self.do = False
|
404
|
+
|
405
|
+
# def line(self, *args, **kwargs):
|
406
|
+
# if self.do:
|
407
|
+
# self.draw.line(*args, **kwargs)
|
408
|
+
|
409
|
+
# def rectangle(self, *args, **kwargs):
|
410
|
+
# if self.do:
|
411
|
+
# self.draw.rectangle(*args, **kwargs)
|
412
|
+
|
413
|
+
# def show(self):
|
414
|
+
# if self.do:
|
415
|
+
# self.im.show()
|
416
|
+
|
417
|
+
# _debug = Debug(None)
|
418
|
+
|
419
|
+
# def set_debug(image):
|
420
|
+
# global _debug
|
421
|
+
# _debug = Debug(image)
|
422
|
+
|
423
|
+
# def _window(lst):
|
424
|
+
# prevs, items, nexts = tee(lst, 3)
|
425
|
+
# prevs = islice(cycle(prevs), len(lst) - 1, None)
|
426
|
+
# nexts = islice(cycle(nexts), 1, None)
|
427
|
+
# return zip(prevs, items, nexts)
|
428
|
+
|
429
|
+
# def _cross(a, b):
|
430
|
+
# res = a.x * b.y - b.x * a.y
|
431
|
+
# return res
|
432
|
+
|
433
|
+
# def _approximately_equals(a, b):
|
434
|
+
# return a == b or (abs(a - b) <= max(abs(a), abs(b)) * 0.001)
|
435
|
+
|
436
|
+
# def _approximately_same(point_a, point_b):
|
437
|
+
# return _approximately_equals(point_a.x, point_b.x) and _approximately_equals(point_a.y, point_b.y)
|
438
|
+
|
439
|
+
# def _normalize_contour(contour):
|
440
|
+
# contour = [Point2(float(x), float(y)) for (x, y) in contour]
|
441
|
+
# return [point for prev, point, next in _window(contour) if not (point == next or (point-prev).normalized() == (next - point).normalized())]
|
442
|
+
|
443
|
+
# class _SplitEvent(namedtuple("_SplitEvent", "distance, intersection_point, vertex, opposite_edge")):
|
444
|
+
# __slots__ = ()
|
445
|
+
|
446
|
+
# def __lt__(self, other):
|
447
|
+
# return self.distance < other.distance
|
448
|
+
|
449
|
+
# def __str__(self):
|
450
|
+
# return "{} Split event @ {} from {} to {}".format(self.distance, self.intersection_point, self.vertex, self.opposite_edge)
|
451
|
+
|
452
|
+
# class _EdgeEvent(namedtuple("_EdgeEvent", "distance intersection_point vertex_a vertex_b")):
|
453
|
+
# __slots__ = ()
|
454
|
+
|
455
|
+
# def __lt__(self, other):
|
456
|
+
# return self.distance < other.distance
|
457
|
+
|
458
|
+
# def __str__(self):
|
459
|
+
# return "{} Edge event @ {} between {} and {}".format(self.distance, self.intersection_point, self.vertex_a, self.vertex_b)
|
460
|
+
|
461
|
+
# _OriginalEdge = namedtuple("_OriginalEdge", "edge bisector_left, bisector_right")
|
462
|
+
|
463
|
+
# Subtree = namedtuple("Subtree", "source, height, sinks")
|
464
|
+
|
465
|
+
# class _LAVertex:
|
466
|
+
# def __init__(self, point, edge_left, edge_right, direction_vectors=None):
|
467
|
+
# self.point = point
|
468
|
+
# self.edge_left = edge_left
|
469
|
+
# self.edge_right = edge_right
|
470
|
+
# self.prev = None
|
471
|
+
# self.next = None
|
472
|
+
# self.lav = None
|
473
|
+
# self._valid = True # TODO this might be handled better. Maybe membership in lav implies validity?
|
474
|
+
|
475
|
+
# import operator
|
476
|
+
# creator_vectors = (edge_left.v.normalized() * -1, edge_right.v.normalized())
|
477
|
+
# if direction_vectors is None:
|
478
|
+
# direction_vectors = creator_vectors
|
479
|
+
|
480
|
+
# self._is_reflex = (_cross(*direction_vectors)) < 0
|
481
|
+
# self._bisector = Ray2(self.point, operator.add(*creator_vectors) * (-1 if self.is_reflex else 1))
|
482
|
+
# log.info("Created vertex %s", self.__repr__())
|
483
|
+
# _debug.line((self.bisector.p.x, self.bisector.p.y, self.bisector.p.x + self.bisector.v.x * 100, self.bisector.p.y + self.bisector.v.y * 100), fill="blue")
|
484
|
+
|
485
|
+
# @property
|
486
|
+
# def bisector(self):
|
487
|
+
# return self._bisector
|
488
|
+
|
489
|
+
# @property
|
490
|
+
# def is_reflex(self):
|
491
|
+
# return self._is_reflex
|
492
|
+
|
493
|
+
# @property
|
494
|
+
# def original_edges(self):
|
495
|
+
# return self.lav._slav._original_edges
|
496
|
+
|
497
|
+
# def next_event(self):
|
498
|
+
# events = []
|
499
|
+
# if self.is_reflex:
|
500
|
+
# # a reflex vertex may generate a split event
|
501
|
+
# # split events happen when a vertex hits an opposite edge, splitting the polygon in two.
|
502
|
+
# log.debug("looking for split candidates for vertex %s", self)
|
503
|
+
# for edge in self.original_edges:
|
504
|
+
# if edge.edge == self.edge_left or edge.edge == self.edge_right:
|
505
|
+
# continue
|
506
|
+
|
507
|
+
# log.debug("\tconsidering EDGE %s", edge)
|
508
|
+
|
509
|
+
# # a potential b is at the intersection of between our own bisector and the bisector of the
|
510
|
+
# # angle between the tested edge and any one of our own edges.
|
511
|
+
|
512
|
+
# # we choose the "less parallel" edge (in order to exclude a potentially parallel edge)
|
513
|
+
# leftdot = abs(self.edge_left.v.normalized().dot(edge.edge.v.normalized()))
|
514
|
+
# rightdot = abs(self.edge_right.v.normalized().dot(edge.edge.v.normalized()))
|
515
|
+
# selfedge = self.edge_left if leftdot < rightdot else self.edge_right
|
516
|
+
# otheredge = self.edge_left if leftdot > rightdot else self.edge_right
|
517
|
+
|
518
|
+
# i = Line2(selfedge).intersect(Line2(edge.edge))
|
519
|
+
# if i is not None and not _approximately_equals(i, self.point):
|
520
|
+
# # locate candidate b
|
521
|
+
# linvec = (self.point - i).normalized()
|
522
|
+
# edvec = edge.edge.v.normalized()
|
523
|
+
# if linvec.dot(edvec) < 0:
|
524
|
+
# edvec = -edvec
|
525
|
+
|
526
|
+
# bisecvec = edvec + linvec
|
527
|
+
# if abs(bisecvec) == 0:
|
528
|
+
# continue
|
529
|
+
# bisector = Line2(i, bisecvec)
|
530
|
+
# b = bisector.intersect(self.bisector)
|
531
|
+
|
532
|
+
# if b is None:
|
533
|
+
# continue
|
534
|
+
|
535
|
+
# # check eligibility of b
|
536
|
+
# # a valid b should lie within the area limited by the edge and the bisectors of its two vertices:
|
537
|
+
# xleft = _cross(edge.bisector_left.v.normalized(), (b - edge.bisector_left.p).normalized()) > -EPSILON
|
538
|
+
# xright = _cross(edge.bisector_right.v.normalized(), (b - edge.bisector_right.p).normalized()) <= EPSILON
|
539
|
+
# xedge = _cross(edge.edge.v.normalized(), (b - edge.edge.p).normalized()) <= EPSILON
|
540
|
+
|
541
|
+
# if not (xleft and xright and xedge):
|
542
|
+
# log.debug("\t\tDiscarded candidate %s (%s-%s-%s)", b, xleft, xright, xedge)
|
543
|
+
# continue
|
544
|
+
|
545
|
+
# log.debug("\t\tFound valid candidate %s", b)
|
546
|
+
# events.append(_SplitEvent(Line2(edge.edge).distance(b), b, self, edge.edge))
|
547
|
+
|
548
|
+
# i_prev = self.bisector.intersect(self.prev.bisector)
|
549
|
+
# i_next = self.bisector.intersect(self.next.bisector)
|
550
|
+
|
551
|
+
# if i_prev is not None:
|
552
|
+
# events.append(_EdgeEvent(Line2(self.edge_left).distance(i_prev), i_prev, self.prev, self))
|
553
|
+
# if i_next is not None:
|
554
|
+
# events.append(_EdgeEvent(Line2(self.edge_right).distance(i_next), i_next, self, self.next))
|
555
|
+
|
556
|
+
# if not events:
|
557
|
+
# return None
|
558
|
+
|
559
|
+
# ev = min(events, key=lambda event: self.point.distance(event.intersection_point))
|
560
|
+
|
561
|
+
# log.info("Generated new event for %s: %s", self, ev)
|
562
|
+
# return ev
|
563
|
+
|
564
|
+
# def invalidate(self):
|
565
|
+
# if self.lav is not None:
|
566
|
+
# self.lav.invalidate(self)
|
567
|
+
# else:
|
568
|
+
# self._valid = False
|
569
|
+
|
570
|
+
# @property
|
571
|
+
# def is_valid(self):
|
572
|
+
# return self._valid
|
573
|
+
|
574
|
+
# def __str__(self):
|
575
|
+
# return "Vertex ({:.2f};{:.2f})".format(self.point.x, self.point.y)
|
576
|
+
|
577
|
+
# def __repr__(self):
|
578
|
+
# return "Vertex ({}) ({:.2f};{:.2f}), bisector {}, edges {} {}".format("reflex" if self.is_reflex else "convex",
|
579
|
+
# self.point.x, self.point.y, self.bisector,
|
580
|
+
# self.edge_left, self.edge_right)
|
581
|
+
|
582
|
+
# class _SLAV:
|
583
|
+
# def __init__(self, polygon, holes):
|
584
|
+
# contours = [_normalize_contour(polygon)]
|
585
|
+
# contours.extend([_normalize_contour(hole) for hole in holes])
|
586
|
+
|
587
|
+
# self._lavs = [_LAV.from_polygon(contour, self) for contour in contours]
|
588
|
+
|
589
|
+
# # store original polygon edges for calculating split events
|
590
|
+
# self._original_edges = [
|
591
|
+
# _OriginalEdge(LineSegment2(vertex.prev.point, vertex.point), vertex.prev.bisector, vertex.bisector)
|
592
|
+
# for vertex in chain.from_iterable(self._lavs)
|
593
|
+
# ]
|
594
|
+
|
595
|
+
# def __iter__(self):
|
596
|
+
# for lav in self._lavs:
|
597
|
+
# yield lav
|
598
|
+
|
599
|
+
# def __len__(self):
|
600
|
+
# return len(self._lavs)
|
601
|
+
|
602
|
+
# def empty(self):
|
603
|
+
# return len(self._lavs) == 0
|
604
|
+
|
605
|
+
# def handle_edge_event(self, event):
|
606
|
+
# sinks = []
|
607
|
+
# events = []
|
608
|
+
|
609
|
+
# lav = event.vertex_a.lav
|
610
|
+
# if event.vertex_a.prev == event.vertex_b.next:
|
611
|
+
# log.info("%.2f Peak event at intersection %s from <%s,%s,%s> in %s", event.distance,
|
612
|
+
# event.intersection_point, event.vertex_a, event.vertex_b, event.vertex_a.prev, lav)
|
613
|
+
# self._lavs.remove(lav)
|
614
|
+
# for vertex in list(lav):
|
615
|
+
# sinks.append(vertex.point)
|
616
|
+
# vertex.invalidate()
|
617
|
+
# else:
|
618
|
+
# log.info("%.2f Edge event at intersection %s from <%s,%s> in %s", event.distance, event.intersection_point,
|
619
|
+
# event.vertex_a, event.vertex_b, lav)
|
620
|
+
# new_vertex = lav.unify(event.vertex_a, event.vertex_b, event.intersection_point)
|
621
|
+
# if lav.head in (event.vertex_a, event.vertex_b):
|
622
|
+
# lav.head = new_vertex
|
623
|
+
# sinks.extend((event.vertex_a.point, event.vertex_b.point))
|
624
|
+
# next_event = new_vertex.next_event()
|
625
|
+
# if next_event is not None:
|
626
|
+
# events.append(next_event)
|
627
|
+
|
628
|
+
# return (Subtree(event.intersection_point, event.distance, sinks), events)
|
629
|
+
|
630
|
+
# def handle_split_event(self, event):
|
631
|
+
# lav = event.vertex.lav
|
632
|
+
# log.info("%.2f Split event at intersection %s from vertex %s, for edge %s in %s", event.distance,
|
633
|
+
# event.intersection_point, event.vertex, event.opposite_edge, lav)
|
634
|
+
|
635
|
+
# sinks = [event.vertex.point]
|
636
|
+
# vertices = []
|
637
|
+
# x = None # right vertex
|
638
|
+
# y = None # left vertex
|
639
|
+
# norm = event.opposite_edge.v.normalized()
|
640
|
+
# for v in chain.from_iterable(self._lavs):
|
641
|
+
# log.debug("%s in %s", v, v.lav)
|
642
|
+
# if norm == v.edge_left.v.normalized() and event.opposite_edge.p == v.edge_left.p:
|
643
|
+
# x = v
|
644
|
+
# y = x.prev
|
645
|
+
# elif norm == v.edge_right.v.normalized() and event.opposite_edge.p == v.edge_right.p:
|
646
|
+
# y = v
|
647
|
+
# x = y.next
|
648
|
+
|
649
|
+
# if x:
|
650
|
+
# xleft = _cross(y.bisector.v.normalized(), (event.intersection_point - y.point).normalized()) >= -EPSILON
|
651
|
+
# xright = _cross(x.bisector.v.normalized(), (event.intersection_point - x.point).normalized()) <= EPSILON
|
652
|
+
# log.debug("Vertex %s holds edge as %s edge (%s, %s)", v, ("left" if x == v else "right"), xleft, xright)
|
653
|
+
|
654
|
+
# if xleft and xright:
|
655
|
+
# break
|
656
|
+
# else:
|
657
|
+
# x = None
|
658
|
+
# y = None
|
659
|
+
|
660
|
+
# if x is None:
|
661
|
+
# log.info("Failed split event %s (equivalent edge event is expected to follow)", event)
|
662
|
+
# return (None, [])
|
663
|
+
|
664
|
+
# v1 = _LAVertex(event.intersection_point, event.vertex.edge_left, event.opposite_edge)
|
665
|
+
# v2 = _LAVertex(event.intersection_point, event.opposite_edge, event.vertex.edge_right)
|
666
|
+
|
667
|
+
# v1.prev = event.vertex.prev
|
668
|
+
# v1.next = x
|
669
|
+
# event.vertex.prev.next = v1
|
670
|
+
# x.prev = v1
|
671
|
+
|
672
|
+
# v2.prev = y
|
673
|
+
# v2.next = event.vertex.next
|
674
|
+
# event.vertex.next.prev = v2
|
675
|
+
# y.next = v2
|
676
|
+
|
677
|
+
# new_lavs = None
|
678
|
+
# self._lavs.remove(lav)
|
679
|
+
# if lav != x.lav:
|
680
|
+
# # the split event actually merges two lavs
|
681
|
+
# self._lavs.remove(x.lav)
|
682
|
+
# new_lavs = [_LAV.from_chain(v1, self)]
|
683
|
+
# else:
|
684
|
+
# new_lavs = [_LAV.from_chain(v1, self), _LAV.from_chain(v2, self)]
|
685
|
+
|
686
|
+
# for l in new_lavs:
|
687
|
+
# log.debug(l)
|
688
|
+
# if len(l) > 2:
|
689
|
+
# self._lavs.append(l)
|
690
|
+
# vertices.append(l.head)
|
691
|
+
# else:
|
692
|
+
# log.info("LAV %s has collapsed into the line %s--%s", l, l.head.point, l.head.next.point)
|
693
|
+
# sinks.append(l.head.next.point)
|
694
|
+
# for v in list(l):
|
695
|
+
# v.invalidate()
|
696
|
+
|
697
|
+
# events = []
|
698
|
+
# for vertex in vertices:
|
699
|
+
# next_event = vertex.next_event()
|
700
|
+
# if next_event is not None:
|
701
|
+
# events.append(next_event)
|
702
|
+
|
703
|
+
# event.vertex.invalidate()
|
704
|
+
# return (Subtree(event.intersection_point, event.distance, sinks), events)
|
705
|
+
|
706
|
+
# class _LAV:
|
707
|
+
# def __init__(self, slav):
|
708
|
+
# self.head = None
|
709
|
+
# self._slav = slav
|
710
|
+
# self._len = 0
|
711
|
+
# log.debug("Created LAV %s", self)
|
712
|
+
|
713
|
+
# @classmethod
|
714
|
+
# def from_polygon(cls, polygon, slav):
|
715
|
+
# lav = cls(slav)
|
716
|
+
# for prev, point, next in _window(polygon):
|
717
|
+
# lav._len += 1
|
718
|
+
# vertex = _LAVertex(point, LineSegment2(prev, point), LineSegment2(point, next))
|
719
|
+
# vertex.lav = lav
|
720
|
+
# if lav.head is None:
|
721
|
+
# lav.head = vertex
|
722
|
+
# vertex.prev = vertex.next = vertex
|
723
|
+
# else:
|
724
|
+
# vertex.next = lav.head
|
725
|
+
# vertex.prev = lav.head.prev
|
726
|
+
# vertex.prev.next = vertex
|
727
|
+
# lav.head.prev = vertex
|
728
|
+
# return lav
|
729
|
+
|
730
|
+
# @classmethod
|
731
|
+
# def from_chain(cls, head, slav):
|
732
|
+
# lav = cls(slav)
|
733
|
+
# lav.head = head
|
734
|
+
# for vertex in lav:
|
735
|
+
# lav._len += 1
|
736
|
+
# vertex.lav = lav
|
737
|
+
# return lav
|
738
|
+
|
739
|
+
# def invalidate(self, vertex):
|
740
|
+
# assert vertex.lav is self, "Tried to invalidate a vertex that's not mine"
|
741
|
+
# log.debug("Invalidating %s", vertex)
|
742
|
+
# vertex._valid = False
|
743
|
+
# if self.head == vertex:
|
744
|
+
# self.head = self.head.next
|
745
|
+
# vertex.lav = None
|
746
|
+
|
747
|
+
# def unify(self, vertex_a, vertex_b, point):
|
748
|
+
# replacement = _LAVertex(point, vertex_a.edge_left, vertex_b.edge_right,
|
749
|
+
# (vertex_b.bisector.v.normalized(), vertex_a.bisector.v.normalized()))
|
750
|
+
# replacement.lav = self
|
751
|
+
|
752
|
+
# if self.head in [vertex_a, vertex_b]:
|
753
|
+
# self.head = replacement
|
754
|
+
|
755
|
+
# vertex_a.prev.next = replacement
|
756
|
+
# vertex_b.next.prev = replacement
|
757
|
+
# replacement.prev = vertex_a.prev
|
758
|
+
# replacement.next = vertex_b.next
|
759
|
+
|
760
|
+
# vertex_a.invalidate()
|
761
|
+
# vertex_b.invalidate()
|
762
|
+
|
763
|
+
# self._len -= 1
|
764
|
+
# return replacement
|
765
|
+
|
766
|
+
# def __str__(self):
|
767
|
+
# return "LAV {}".format(id(self))
|
768
|
+
|
769
|
+
# def __repr__(self):
|
770
|
+
# return "{} = {}".format(str(self), [vertex for vertex in self])
|
771
|
+
|
772
|
+
# def __len__(self):
|
773
|
+
# return self._len
|
774
|
+
|
775
|
+
# def __iter__(self):
|
776
|
+
# cur = self.head
|
777
|
+
# while True:
|
778
|
+
# yield cur
|
779
|
+
# cur = cur.next
|
780
|
+
# if cur == self.head:
|
781
|
+
# return
|
782
|
+
|
783
|
+
# def _show(self):
|
784
|
+
# cur = self.head
|
785
|
+
# while True:
|
786
|
+
# print(cur.__repr__())
|
787
|
+
# cur = cur.next
|
788
|
+
# if cur == self.head:
|
789
|
+
# break
|
790
|
+
|
791
|
+
# class _EventQueue:
|
792
|
+
# def __init__(self):
|
793
|
+
# self.__data = []
|
794
|
+
|
795
|
+
# def put(self, item):
|
796
|
+
# if item is not None:
|
797
|
+
# heapq.heappush(self.__data, item)
|
798
|
+
|
799
|
+
# def put_all(self, iterable):
|
800
|
+
# for item in iterable:
|
801
|
+
# heapq.heappush(self.__data, item)
|
802
|
+
|
803
|
+
# def get(self):
|
804
|
+
# return heapq.heappop(self.__data)
|
805
|
+
|
806
|
+
# def empty(self):
|
807
|
+
# return len(self.__data) == 0
|
808
|
+
|
809
|
+
# def peek(self):
|
810
|
+
# return self.__data[0]
|
811
|
+
|
812
|
+
# def show(self):
|
813
|
+
# for item in self.__data:
|
814
|
+
# print(item)
|
815
|
+
|
816
|
+
# def _merge_sources(skeleton):
|
817
|
+
# """
|
818
|
+
# In highly symmetrical shapes with reflex vertices multiple sources may share the same
|
819
|
+
# location. This function merges those sources.
|
820
|
+
# """
|
821
|
+
# sources = {}
|
822
|
+
# to_remove = []
|
823
|
+
# for i, p in enumerate(skeleton):
|
824
|
+
# source = tuple(i for i in p.source)
|
825
|
+
# if source in sources:
|
826
|
+
# source_index = sources[source]
|
827
|
+
# # source exists, merge sinks
|
828
|
+
# for sink in p.sinks:
|
829
|
+
# if sink not in skeleton[source_index].sinks:
|
830
|
+
# skeleton[source_index].sinks.append(sink)
|
831
|
+
# to_remove.append(i)
|
832
|
+
# else:
|
833
|
+
# sources[source] = i
|
834
|
+
# for i in reversed(to_remove):
|
835
|
+
# skeleton.pop(i)
|
836
|
+
|
837
|
+
|
838
|
+
# def skeletonize(polygon, holes=None):
|
839
|
+
# """
|
840
|
+
# Compute the straight skeleton of a polygon.
|
841
|
+
|
842
|
+
# The polygon should be given as a list of vertices in counter-clockwise order.
|
843
|
+
# Holes is a list of the contours of the holes, the vertices of which should be in clockwise order.
|
844
|
+
|
845
|
+
# Please note that the y-axis goes downwards as far as polyskel is concerned, so specify your ordering accordingly.
|
846
|
+
|
847
|
+
# Returns the straight skeleton as a list of "subtrees", which are in the form of (source, height, sinks),
|
848
|
+
# where source is the highest points, height is its height, and sinks are the point connected to the source.
|
849
|
+
# """
|
850
|
+
# slav = _SLAV(polygon, holes)
|
851
|
+
# output = []
|
852
|
+
# prioque = _EventQueue()
|
853
|
+
|
854
|
+
# for lav in slav:
|
855
|
+
# for vertex in lav:
|
856
|
+
# prioque.put(vertex.next_event())
|
857
|
+
|
858
|
+
# while not (prioque.empty() or slav.empty()):
|
859
|
+
# log.debug("SLAV is %s", [repr(lav) for lav in slav])
|
860
|
+
# i = prioque.get()
|
861
|
+
# if isinstance(i, _EdgeEvent):
|
862
|
+
# if not i.vertex_a.is_valid or not i.vertex_b.is_valid:
|
863
|
+
# log.info("%.2f Discarded outdated edge event %s", i.distance, i)
|
864
|
+
# continue
|
865
|
+
|
866
|
+
# (arc, events) = slav.handle_edge_event(i)
|
867
|
+
# elif isinstance(i, _SplitEvent):
|
868
|
+
# if not i.vertex.is_valid:
|
869
|
+
# log.info("%.2f Discarded outdated split event %s", i.distance, i)
|
870
|
+
# continue
|
871
|
+
# (arc, events) = slav.handle_split_event(i)
|
872
|
+
|
873
|
+
# prioque.put_all(events)
|
874
|
+
|
875
|
+
# if arc is not None:
|
876
|
+
# output.append(arc)
|
877
|
+
# for sink in arc.sinks:
|
878
|
+
# _debug.line((arc.source.x, arc.source.y, sink.x, sink.y), fill="red")
|
879
|
+
|
880
|
+
# _debug.show()
|
881
|
+
# _merge_sources(output)
|
882
|
+
# return output
|