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/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
- from itertools import *
2
+ import logging
3
+ import math
4
+ from itertools import tee, islice, cycle, chain
21
5
  from collections import namedtuple
22
- import os
23
- import warnings
24
-
25
- # try:
26
- # from euclid import *
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
- EPSILON = 0.00001
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
- import math
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
- class Point2:
47
- def __init__(self, x, y):
48
- self.x = x
49
- self.y = y
50
-
51
- def __sub__(self, other):
52
- return Point2(self.x - other.x, self.y - other.y)
53
-
54
- def __add__(self, other):
55
- return Point2(self.x + other.x, self.y + other.y)
56
-
57
- def __mul__(self, scalar):
58
- return Point2(self.x * scalar, self.y * scalar)
59
-
60
- def __neg__(self):
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.x
71
- yield self.y
72
-
73
- def cross(self, other):
74
- """
75
- Computes the cross product of this point with another point.
76
-
77
- Args:
78
- other (Point2): The other point to compute the cross product with.
79
-
80
- Returns:
81
- float: The cross product value.
82
- """
83
- return self.x * other.y - self.y * other.x
84
-
85
- def normalized(self):
86
- length = abs(self)
87
- return Point2(self.x / length, self.y / length)
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
- # Calculate the intersection point using vector algebra
118
- denom = self.v.cross(other.v)
119
- if not denom == 0:
120
- t = (other.p - self.p).cross(other.v) / self.v.cross(other.v)
121
- else:
122
- return None
123
- if t >= 0:
124
- return self.p + self.v * t # Intersection point
125
- else:
126
- return None # Rays do not intersect or intersection point lies behind self
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
- class Line2:
129
- def __init__(self, p1, p2=None):
130
- if p2 is None:
131
- self.p = p1.p
132
- self.v = p1.v
133
- else:
134
- self.p = p1
135
- self.v = (p2 - p1).normalized()
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
- class _EventQueue:
580
- def __init__(self):
581
- self.__data = []
582
-
583
- def put(self, item):
584
- if item is not None:
585
- heapq.heappush(self.__data, item)
586
-
587
- def put_all(self, iterable):
588
- for item in iterable:
589
- heapq.heappush(self.__data, item)
590
-
591
- def get(self):
592
- return heapq.heappop(self.__data)
593
-
594
- def empty(self):
595
- return len(self.__data) == 0
596
-
597
- def peek(self):
598
- return self.__data[0]
599
-
600
- def show(self):
601
- for item in self.__data:
602
- print(item)
603
-
604
- def _merge_sources(skeleton):
605
- """
606
- In highly symmetrical shapes with reflex vertices multiple sources may share the same
607
- location. This function merges those sources.
608
- """
609
- sources = {}
610
- to_remove = []
611
- for i, p in enumerate(skeleton):
612
- source = tuple(i for i in p.source)
613
- if source in sources:
614
- source_index = sources[source]
615
- # source exists, merge sinks
616
- for sink in p.sinks:
617
- if sink not in skeleton[source_index].sinks:
618
- skeleton[source_index].sinks.append(sink)
619
- to_remove.append(i)
620
- else:
621
- sources[source] = i
622
- for i in reversed(to_remove):
623
- skeleton.pop(i)
624
-
625
-
626
- def skeletonize(polygon, holes=None):
627
- """
628
- Compute the straight skeleton of a polygon.
629
-
630
- The polygon should be given as a list of vertices in counter-clockwise order.
631
- Holes is a list of the contours of the holes, the vertices of which should be in clockwise order.
632
-
633
- Please note that the y-axis goes downwards as far as polyskel is concerned, so specify your ordering accordingly.
634
-
635
- Returns the straight skeleton as a list of "subtrees", which are in the form of (source, height, sinks),
636
- where source is the highest points, height is its height, and sinks are the point connected to the source.
637
- """
638
- slav = _SLAV(polygon, holes)
639
- output = []
640
- prioque = _EventQueue()
641
-
642
- for lav in slav:
643
- for vertex in lav:
644
- prioque.put(vertex.next_event())
645
-
646
- while not (prioque.empty() or slav.empty()):
647
- log.debug("SLAV is %s", [repr(lav) for lav in slav])
648
- i = prioque.get()
649
- if isinstance(i, _EdgeEvent):
650
- if not i.vertex_a.is_valid or not i.vertex_b.is_valid:
651
- log.info("%.2f Discarded outdated edge event %s", i.distance, i)
652
- continue
653
-
654
- (arc, events) = slav.handle_edge_event(i)
655
- elif isinstance(i, _SplitEvent):
656
- if not i.vertex.is_valid:
657
- log.info("%.2f Discarded outdated split event %s", i.distance, i)
658
- continue
659
- (arc, events) = slav.handle_split_event(i)
660
-
661
- prioque.put_all(events)
662
-
663
- if arc is not None:
664
- output.append(arc)
665
- for sink in arc.sinks:
666
- _debug.line((arc.source.x, arc.source.y, sink.x, sink.y), fill="red")
667
-
668
- _debug.show()
669
- _merge_sources(output)
670
- return output
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