topologicpy 0.5.9__py3-none-any.whl → 6.0.0__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/Aperture.py +72 -72
- topologicpy/Cell.py +2169 -2169
- topologicpy/CellComplex.py +1137 -1137
- topologicpy/Cluster.py +1288 -1280
- topologicpy/Color.py +423 -423
- topologicpy/Context.py +79 -79
- topologicpy/DGL.py +3213 -3240
- topologicpy/Dictionary.py +698 -698
- topologicpy/Edge.py +1187 -1187
- topologicpy/EnergyModel.py +1180 -1152
- topologicpy/Face.py +2141 -2141
- topologicpy/Graph.py +7768 -7768
- topologicpy/Grid.py +353 -353
- topologicpy/Helper.py +507 -507
- topologicpy/Honeybee.py +461 -461
- topologicpy/Matrix.py +271 -271
- topologicpy/Neo4j.py +521 -521
- topologicpy/Plotly.py +2 -2
- topologicpy/Polyskel.py +541 -541
- topologicpy/Shell.py +1768 -1768
- topologicpy/Speckle.py +508 -508
- topologicpy/Topology.py +7060 -7002
- topologicpy/Vector.py +905 -905
- topologicpy/Vertex.py +1585 -1585
- topologicpy/Wire.py +3050 -3050
- topologicpy/__init__.py +22 -38
- topologicpy/version.py +1 -0
- {topologicpy-0.5.9.dist-info → topologicpy-6.0.0.dist-info}/LICENSE +661 -704
- topologicpy-6.0.0.dist-info/METADATA +751 -0
- topologicpy-6.0.0.dist-info/RECORD +32 -0
- topologicpy/bin/linux/topologic/__init__.py +0 -2
- topologicpy/bin/linux/topologic/libTKBO-6bdf205d.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic/libTKBRep-2960a069.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic/libTKBool-c44b74bd.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic/libTKFillet-9a670ba0.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic/libTKG2d-8f31849e.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic/libTKG3d-4c6bce57.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic/libTKGeomAlgo-26066fd9.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic/libTKGeomBase-2116cabe.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic/libTKMath-72572fa8.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic/libTKMesh-2a060427.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic/libTKOffset-6cab68ff.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic/libTKPrim-eb1262b3.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic/libTKShHealing-e67e5cc7.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic/libTKTopAlgo-e4c96c33.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic/libTKernel-fb7fe3b7.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic/libgcc_s-32c1665e.so.1 +0 -0
- topologicpy/bin/linux/topologic/libstdc++-672d7b41.so.6.0.30 +0 -0
- topologicpy/bin/linux/topologic/topologic.cpython-310-x86_64-linux-gnu.so +0 -0
- topologicpy/bin/linux/topologic/topologic.cpython-311-x86_64-linux-gnu.so +0 -0
- topologicpy/bin/linux/topologic/topologic.cpython-38-x86_64-linux-gnu.so +0 -0
- topologicpy/bin/linux/topologic/topologic.cpython-39-x86_64-linux-gnu.so +0 -0
- topologicpy/bin/linux/topologic.libs/libTKBO-6bdf205d.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic.libs/libTKBRep-2960a069.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic.libs/libTKBool-c44b74bd.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic.libs/libTKFillet-9a670ba0.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic.libs/libTKG2d-8f31849e.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic.libs/libTKG3d-4c6bce57.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic.libs/libTKGeomAlgo-26066fd9.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic.libs/libTKGeomBase-2116cabe.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic.libs/libTKMath-72572fa8.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic.libs/libTKMesh-2a060427.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic.libs/libTKOffset-6cab68ff.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic.libs/libTKPrim-eb1262b3.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic.libs/libTKShHealing-e67e5cc7.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic.libs/libTKTopAlgo-e4c96c33.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic.libs/libTKernel-fb7fe3b7.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic.libs/libgcc_s-32c1665e.so.1 +0 -0
- topologicpy/bin/linux/topologic.libs/libstdc++-672d7b41.so.6.0.30 +0 -0
- topologicpy/bin/macos/topologic/__init__.py +0 -2
- topologicpy/bin/windows/topologic/TKBO-f6b191de.dll +0 -0
- topologicpy/bin/windows/topologic/TKBRep-e56a600e.dll +0 -0
- topologicpy/bin/windows/topologic/TKBool-7b8d47ae.dll +0 -0
- topologicpy/bin/windows/topologic/TKFillet-0ddbf0a8.dll +0 -0
- topologicpy/bin/windows/topologic/TKG2d-2e2dee3d.dll +0 -0
- topologicpy/bin/windows/topologic/TKG3d-6674513d.dll +0 -0
- topologicpy/bin/windows/topologic/TKGeomAlgo-d240e370.dll +0 -0
- topologicpy/bin/windows/topologic/TKGeomBase-df87aba5.dll +0 -0
- topologicpy/bin/windows/topologic/TKMath-45bd625a.dll +0 -0
- topologicpy/bin/windows/topologic/TKMesh-d6e826b1.dll +0 -0
- topologicpy/bin/windows/topologic/TKOffset-79b9cc94.dll +0 -0
- topologicpy/bin/windows/topologic/TKPrim-aa430a86.dll +0 -0
- topologicpy/bin/windows/topologic/TKShHealing-bb48be89.dll +0 -0
- topologicpy/bin/windows/topologic/TKTopAlgo-7d0d1e22.dll +0 -0
- topologicpy/bin/windows/topologic/TKernel-08c8cfbb.dll +0 -0
- topologicpy/bin/windows/topologic/__init__.py +0 -2
- topologicpy/bin/windows/topologic/topologic.cp310-win_amd64.pyd +0 -0
- topologicpy/bin/windows/topologic/topologic.cp311-win_amd64.pyd +0 -0
- topologicpy/bin/windows/topologic/topologic.cp38-win_amd64.pyd +0 -0
- topologicpy/bin/windows/topologic/topologic.cp39-win_amd64.pyd +0 -0
- topologicpy-0.5.9.dist-info/METADATA +0 -86
- topologicpy-0.5.9.dist-info/RECORD +0 -91
- {topologicpy-0.5.9.dist-info → topologicpy-6.0.0.dist-info}/WHEEL +0 -0
- {topologicpy-0.5.9.dist-info → topologicpy-6.0.0.dist-info}/top_level.txt +0 -0
topologicpy/Polyskel.py
CHANGED
@@ -1,541 +1,541 @@
|
|
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
|
-
import heapq
|
20
|
-
from itertools import *
|
21
|
-
from collections import namedtuple
|
22
|
-
import os
|
23
|
-
import warnings
|
24
|
-
|
25
|
-
try:
|
26
|
-
from euclid3 import *
|
27
|
-
except:
|
28
|
-
print("Polyskel - Installing required euclid3 library.")
|
29
|
-
try:
|
30
|
-
os.system("pip install euclid3")
|
31
|
-
except:
|
32
|
-
os.system("pip install euclid3 --user")
|
33
|
-
try:
|
34
|
-
from euclid3 import *
|
35
|
-
except:
|
36
|
-
warnings.warn("Polyskel - ERROR: Could not import euclid3.")
|
37
|
-
|
38
|
-
log = logging.getLogger("__name__")
|
39
|
-
|
40
|
-
EPSILON = 0.00001
|
41
|
-
|
42
|
-
class Debug:
|
43
|
-
def __init__(self, image):
|
44
|
-
if image is not None:
|
45
|
-
self.im = image[0]
|
46
|
-
self.draw = image[1]
|
47
|
-
self.do = True
|
48
|
-
else:
|
49
|
-
self.do = False
|
50
|
-
|
51
|
-
def line(self, *args, **kwargs):
|
52
|
-
if self.do:
|
53
|
-
self.draw.line(*args, **kwargs)
|
54
|
-
|
55
|
-
def rectangle(self, *args, **kwargs):
|
56
|
-
if self.do:
|
57
|
-
self.draw.rectangle(*args, **kwargs)
|
58
|
-
|
59
|
-
def show(self):
|
60
|
-
if self.do:
|
61
|
-
self.im.show()
|
62
|
-
|
63
|
-
|
64
|
-
_debug = Debug(None)
|
65
|
-
|
66
|
-
|
67
|
-
def set_debug(image):
|
68
|
-
global _debug
|
69
|
-
_debug = Debug(image)
|
70
|
-
|
71
|
-
|
72
|
-
def _window(lst):
|
73
|
-
prevs, items, nexts = tee(lst, 3)
|
74
|
-
prevs = islice(cycle(prevs), len(lst) - 1, None)
|
75
|
-
nexts = islice(cycle(nexts), 1, None)
|
76
|
-
return zip(prevs, items, nexts)
|
77
|
-
|
78
|
-
|
79
|
-
def _cross(a, b):
|
80
|
-
res = a.x * b.y - b.x * a.y
|
81
|
-
return res
|
82
|
-
|
83
|
-
|
84
|
-
def _approximately_equals(a, b):
|
85
|
-
return a == b or (abs(a - b) <= max(abs(a), abs(b)) * 0.001)
|
86
|
-
|
87
|
-
|
88
|
-
def _approximately_same(point_a, point_b):
|
89
|
-
return _approximately_equals(point_a.x, point_b.x) and _approximately_equals(point_a.y, point_b.y)
|
90
|
-
|
91
|
-
|
92
|
-
def _normalize_contour(contour):
|
93
|
-
contour = [Point2(float(x), float(y)) for (x, y) in contour]
|
94
|
-
return [point for prev, point, next in _window(contour) if not (point == next or (point-prev).normalized() == (next - point).normalized())]
|
95
|
-
|
96
|
-
|
97
|
-
class _SplitEvent(namedtuple("_SplitEvent", "distance, intersection_point, vertex, opposite_edge")):
|
98
|
-
__slots__ = ()
|
99
|
-
|
100
|
-
def __lt__(self, other):
|
101
|
-
return self.distance < other.distance
|
102
|
-
|
103
|
-
def __str__(self):
|
104
|
-
return "{} Split event @ {} from {} to {}".format(self.distance, self.intersection_point, self.vertex, self.opposite_edge)
|
105
|
-
|
106
|
-
|
107
|
-
class _EdgeEvent(namedtuple("_EdgeEvent", "distance intersection_point vertex_a vertex_b")):
|
108
|
-
__slots__ = ()
|
109
|
-
|
110
|
-
def __lt__(self, other):
|
111
|
-
return self.distance < other.distance
|
112
|
-
|
113
|
-
def __str__(self):
|
114
|
-
return "{} Edge event @ {} between {} and {}".format(self.distance, self.intersection_point, self.vertex_a, self.vertex_b)
|
115
|
-
|
116
|
-
|
117
|
-
_OriginalEdge = namedtuple("_OriginalEdge", "edge bisector_left, bisector_right")
|
118
|
-
|
119
|
-
Subtree = namedtuple("Subtree", "source, height, sinks")
|
120
|
-
|
121
|
-
|
122
|
-
class _LAVertex:
|
123
|
-
def __init__(self, point, edge_left, edge_right, direction_vectors=None):
|
124
|
-
self.point = point
|
125
|
-
self.edge_left = edge_left
|
126
|
-
self.edge_right = edge_right
|
127
|
-
self.prev = None
|
128
|
-
self.next = None
|
129
|
-
self.lav = None
|
130
|
-
self._valid = True # TODO this might be handled better. Maybe membership in lav implies validity?
|
131
|
-
|
132
|
-
creator_vectors = (edge_left.v.normalized() * -1, edge_right.v.normalized())
|
133
|
-
if direction_vectors is None:
|
134
|
-
direction_vectors = creator_vectors
|
135
|
-
|
136
|
-
self._is_reflex = (_cross(*direction_vectors)) < 0
|
137
|
-
self._bisector = Ray2(self.point, operator.add(*creator_vectors) * (-1 if self.is_reflex else 1))
|
138
|
-
log.info("Created vertex %s", self.__repr__())
|
139
|
-
_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")
|
140
|
-
|
141
|
-
@property
|
142
|
-
def bisector(self):
|
143
|
-
return self._bisector
|
144
|
-
|
145
|
-
@property
|
146
|
-
def is_reflex(self):
|
147
|
-
return self._is_reflex
|
148
|
-
|
149
|
-
@property
|
150
|
-
def original_edges(self):
|
151
|
-
return self.lav._slav._original_edges
|
152
|
-
|
153
|
-
def next_event(self):
|
154
|
-
events = []
|
155
|
-
if self.is_reflex:
|
156
|
-
# a reflex vertex may generate a split event
|
157
|
-
# split events happen when a vertex hits an opposite edge, splitting the polygon in two.
|
158
|
-
log.debug("looking for split candidates for vertex %s", self)
|
159
|
-
for edge in self.original_edges:
|
160
|
-
if edge.edge == self.edge_left or edge.edge == self.edge_right:
|
161
|
-
continue
|
162
|
-
|
163
|
-
log.debug("\tconsidering EDGE %s", edge)
|
164
|
-
|
165
|
-
# a potential b is at the intersection of between our own bisector and the bisector of the
|
166
|
-
# angle between the tested edge and any one of our own edges.
|
167
|
-
|
168
|
-
# we choose the "less parallel" edge (in order to exclude a potentially parallel edge)
|
169
|
-
leftdot = abs(self.edge_left.v.normalized().dot(edge.edge.v.normalized()))
|
170
|
-
rightdot = abs(self.edge_right.v.normalized().dot(edge.edge.v.normalized()))
|
171
|
-
selfedge = self.edge_left if leftdot < rightdot else self.edge_right
|
172
|
-
otheredge = self.edge_left if leftdot > rightdot else self.edge_right
|
173
|
-
|
174
|
-
i = Line2(selfedge).intersect(Line2(edge.edge))
|
175
|
-
if i is not None and not _approximately_equals(i, self.point):
|
176
|
-
# locate candidate b
|
177
|
-
linvec = (self.point - i).normalized()
|
178
|
-
edvec = edge.edge.v.normalized()
|
179
|
-
if linvec.dot(edvec) < 0:
|
180
|
-
edvec = -edvec
|
181
|
-
|
182
|
-
bisecvec = edvec + linvec
|
183
|
-
if abs(bisecvec) == 0:
|
184
|
-
continue
|
185
|
-
bisector = Line2(i, bisecvec)
|
186
|
-
b = bisector.intersect(self.bisector)
|
187
|
-
|
188
|
-
if b is None:
|
189
|
-
continue
|
190
|
-
|
191
|
-
# check eligibility of b
|
192
|
-
# a valid b should lie within the area limited by the edge and the bisectors of its two vertices:
|
193
|
-
xleft = _cross(edge.bisector_left.v.normalized(), (b - edge.bisector_left.p).normalized()) > -EPSILON
|
194
|
-
xright = _cross(edge.bisector_right.v.normalized(), (b - edge.bisector_right.p).normalized()) < EPSILON
|
195
|
-
xedge = _cross(edge.edge.v.normalized(), (b - edge.edge.p).normalized()) < EPSILON
|
196
|
-
|
197
|
-
if not (xleft and xright and xedge):
|
198
|
-
log.debug("\t\tDiscarded candidate %s (%s-%s-%s)", b, xleft, xright, xedge)
|
199
|
-
continue
|
200
|
-
|
201
|
-
log.debug("\t\tFound valid candidate %s", b)
|
202
|
-
events.append(_SplitEvent(Line2(edge.edge).distance(b), b, self, edge.edge))
|
203
|
-
|
204
|
-
i_prev = self.bisector.intersect(self.prev.bisector)
|
205
|
-
i_next = self.bisector.intersect(self.next.bisector)
|
206
|
-
|
207
|
-
if i_prev is not None:
|
208
|
-
events.append(_EdgeEvent(Line2(self.edge_left).distance(i_prev), i_prev, self.prev, self))
|
209
|
-
if i_next is not None:
|
210
|
-
events.append(_EdgeEvent(Line2(self.edge_right).distance(i_next), i_next, self, self.next))
|
211
|
-
|
212
|
-
if not events:
|
213
|
-
return None
|
214
|
-
|
215
|
-
ev = min(events, key=lambda event: self.point.distance(event.intersection_point))
|
216
|
-
|
217
|
-
log.info("Generated new event for %s: %s", self, ev)
|
218
|
-
return ev
|
219
|
-
|
220
|
-
def invalidate(self):
|
221
|
-
if self.lav is not None:
|
222
|
-
self.lav.invalidate(self)
|
223
|
-
else:
|
224
|
-
self._valid = False
|
225
|
-
|
226
|
-
@property
|
227
|
-
def is_valid(self):
|
228
|
-
return self._valid
|
229
|
-
|
230
|
-
def __str__(self):
|
231
|
-
return "Vertex ({:.2f};{:.2f})".format(self.point.x, self.point.y)
|
232
|
-
|
233
|
-
def __repr__(self):
|
234
|
-
return "Vertex ({}) ({:.2f};{:.2f}), bisector {}, edges {} {}".format("reflex" if self.is_reflex else "convex",
|
235
|
-
self.point.x, self.point.y, self.bisector,
|
236
|
-
self.edge_left, self.edge_right)
|
237
|
-
|
238
|
-
|
239
|
-
class _SLAV:
|
240
|
-
def __init__(self, polygon, holes):
|
241
|
-
contours = [_normalize_contour(polygon)]
|
242
|
-
contours.extend([_normalize_contour(hole) for hole in holes])
|
243
|
-
|
244
|
-
self._lavs = [_LAV.from_polygon(contour, self) for contour in contours]
|
245
|
-
|
246
|
-
# store original polygon edges for calculating split events
|
247
|
-
self._original_edges = [
|
248
|
-
_OriginalEdge(LineSegment2(vertex.prev.point, vertex.point), vertex.prev.bisector, vertex.bisector)
|
249
|
-
for vertex in chain.from_iterable(self._lavs)
|
250
|
-
]
|
251
|
-
|
252
|
-
def __iter__(self):
|
253
|
-
for lav in self._lavs:
|
254
|
-
yield lav
|
255
|
-
|
256
|
-
def __len__(self):
|
257
|
-
return len(self._lavs)
|
258
|
-
|
259
|
-
def empty(self):
|
260
|
-
return len(self._lavs) == 0
|
261
|
-
|
262
|
-
def handle_edge_event(self, event):
|
263
|
-
sinks = []
|
264
|
-
events = []
|
265
|
-
|
266
|
-
lav = event.vertex_a.lav
|
267
|
-
if event.vertex_a.prev == event.vertex_b.next:
|
268
|
-
log.info("%.2f Peak event at intersection %s from <%s,%s,%s> in %s", event.distance,
|
269
|
-
event.intersection_point, event.vertex_a, event.vertex_b, event.vertex_a.prev, lav)
|
270
|
-
self._lavs.remove(lav)
|
271
|
-
for vertex in list(lav):
|
272
|
-
sinks.append(vertex.point)
|
273
|
-
vertex.invalidate()
|
274
|
-
else:
|
275
|
-
log.info("%.2f Edge event at intersection %s from <%s,%s> in %s", event.distance, event.intersection_point,
|
276
|
-
event.vertex_a, event.vertex_b, lav)
|
277
|
-
new_vertex = lav.unify(event.vertex_a, event.vertex_b, event.intersection_point)
|
278
|
-
if lav.head in (event.vertex_a, event.vertex_b):
|
279
|
-
lav.head = new_vertex
|
280
|
-
sinks.extend((event.vertex_a.point, event.vertex_b.point))
|
281
|
-
next_event = new_vertex.next_event()
|
282
|
-
if next_event is not None:
|
283
|
-
events.append(next_event)
|
284
|
-
|
285
|
-
return (Subtree(event.intersection_point, event.distance, sinks), events)
|
286
|
-
|
287
|
-
def handle_split_event(self, event):
|
288
|
-
lav = event.vertex.lav
|
289
|
-
log.info("%.2f Split event at intersection %s from vertex %s, for edge %s in %s", event.distance,
|
290
|
-
event.intersection_point, event.vertex, event.opposite_edge, lav)
|
291
|
-
|
292
|
-
sinks = [event.vertex.point]
|
293
|
-
vertices = []
|
294
|
-
x = None # right vertex
|
295
|
-
y = None # left vertex
|
296
|
-
norm = event.opposite_edge.v.normalized()
|
297
|
-
for v in chain.from_iterable(self._lavs):
|
298
|
-
log.debug("%s in %s", v, v.lav)
|
299
|
-
if norm == v.edge_left.v.normalized() and event.opposite_edge.p == v.edge_left.p:
|
300
|
-
x = v
|
301
|
-
y = x.prev
|
302
|
-
elif norm == v.edge_right.v.normalized() and event.opposite_edge.p == v.edge_right.p:
|
303
|
-
y = v
|
304
|
-
x = y.next
|
305
|
-
|
306
|
-
if x:
|
307
|
-
xleft = _cross(y.bisector.v.normalized(), (event.intersection_point - y.point).normalized()) >= -EPSILON
|
308
|
-
xright = _cross(x.bisector.v.normalized(), (event.intersection_point - x.point).normalized()) <= EPSILON
|
309
|
-
log.debug("Vertex %s holds edge as %s edge (%s, %s)", v, ("left" if x == v else "right"), xleft, xright)
|
310
|
-
|
311
|
-
if xleft and xright:
|
312
|
-
break
|
313
|
-
else:
|
314
|
-
x = None
|
315
|
-
y = None
|
316
|
-
|
317
|
-
if x is None:
|
318
|
-
log.info("Failed split event %s (equivalent edge event is expected to follow)", event)
|
319
|
-
return (None, [])
|
320
|
-
|
321
|
-
v1 = _LAVertex(event.intersection_point, event.vertex.edge_left, event.opposite_edge)
|
322
|
-
v2 = _LAVertex(event.intersection_point, event.opposite_edge, event.vertex.edge_right)
|
323
|
-
|
324
|
-
v1.prev = event.vertex.prev
|
325
|
-
v1.next = x
|
326
|
-
event.vertex.prev.next = v1
|
327
|
-
x.prev = v1
|
328
|
-
|
329
|
-
v2.prev = y
|
330
|
-
v2.next = event.vertex.next
|
331
|
-
event.vertex.next.prev = v2
|
332
|
-
y.next = v2
|
333
|
-
|
334
|
-
new_lavs = None
|
335
|
-
self._lavs.remove(lav)
|
336
|
-
if lav != x.lav:
|
337
|
-
# the split event actually merges two lavs
|
338
|
-
self._lavs.remove(x.lav)
|
339
|
-
new_lavs = [_LAV.from_chain(v1, self)]
|
340
|
-
else:
|
341
|
-
new_lavs = [_LAV.from_chain(v1, self), _LAV.from_chain(v2, self)]
|
342
|
-
|
343
|
-
for l in new_lavs:
|
344
|
-
log.debug(l)
|
345
|
-
if len(l) > 2:
|
346
|
-
self._lavs.append(l)
|
347
|
-
vertices.append(l.head)
|
348
|
-
else:
|
349
|
-
log.info("LAV %s has collapsed into the line %s--%s", l, l.head.point, l.head.next.point)
|
350
|
-
sinks.append(l.head.next.point)
|
351
|
-
for v in list(l):
|
352
|
-
v.invalidate()
|
353
|
-
|
354
|
-
events = []
|
355
|
-
for vertex in vertices:
|
356
|
-
next_event = vertex.next_event()
|
357
|
-
if next_event is not None:
|
358
|
-
events.append(next_event)
|
359
|
-
|
360
|
-
event.vertex.invalidate()
|
361
|
-
return (Subtree(event.intersection_point, event.distance, sinks), events)
|
362
|
-
|
363
|
-
|
364
|
-
class _LAV:
|
365
|
-
def __init__(self, slav):
|
366
|
-
self.head = None
|
367
|
-
self._slav = slav
|
368
|
-
self._len = 0
|
369
|
-
log.debug("Created LAV %s", self)
|
370
|
-
|
371
|
-
@classmethod
|
372
|
-
def from_polygon(cls, polygon, slav):
|
373
|
-
lav = cls(slav)
|
374
|
-
for prev, point, next in _window(polygon):
|
375
|
-
lav._len += 1
|
376
|
-
vertex = _LAVertex(point, LineSegment2(prev, point), LineSegment2(point, next))
|
377
|
-
vertex.lav = lav
|
378
|
-
if lav.head is None:
|
379
|
-
lav.head = vertex
|
380
|
-
vertex.prev = vertex.next = vertex
|
381
|
-
else:
|
382
|
-
vertex.next = lav.head
|
383
|
-
vertex.prev = lav.head.prev
|
384
|
-
vertex.prev.next = vertex
|
385
|
-
lav.head.prev = vertex
|
386
|
-
return lav
|
387
|
-
|
388
|
-
@classmethod
|
389
|
-
def from_chain(cls, head, slav):
|
390
|
-
lav = cls(slav)
|
391
|
-
lav.head = head
|
392
|
-
for vertex in lav:
|
393
|
-
lav._len += 1
|
394
|
-
vertex.lav = lav
|
395
|
-
return lav
|
396
|
-
|
397
|
-
def invalidate(self, vertex):
|
398
|
-
assert vertex.lav is self, "Tried to invalidate a vertex that's not mine"
|
399
|
-
log.debug("Invalidating %s", vertex)
|
400
|
-
vertex._valid = False
|
401
|
-
if self.head == vertex:
|
402
|
-
self.head = self.head.next
|
403
|
-
vertex.lav = None
|
404
|
-
|
405
|
-
def unify(self, vertex_a, vertex_b, point):
|
406
|
-
replacement = _LAVertex(point, vertex_a.edge_left, vertex_b.edge_right,
|
407
|
-
(vertex_b.bisector.v.normalized(), vertex_a.bisector.v.normalized()))
|
408
|
-
replacement.lav = self
|
409
|
-
|
410
|
-
if self.head in [vertex_a, vertex_b]:
|
411
|
-
self.head = replacement
|
412
|
-
|
413
|
-
vertex_a.prev.next = replacement
|
414
|
-
vertex_b.next.prev = replacement
|
415
|
-
replacement.prev = vertex_a.prev
|
416
|
-
replacement.next = vertex_b.next
|
417
|
-
|
418
|
-
vertex_a.invalidate()
|
419
|
-
vertex_b.invalidate()
|
420
|
-
|
421
|
-
self._len -= 1
|
422
|
-
return replacement
|
423
|
-
|
424
|
-
def __str__(self):
|
425
|
-
return "LAV {}".format(id(self))
|
426
|
-
|
427
|
-
def __repr__(self):
|
428
|
-
return "{} = {}".format(str(self), [vertex for vertex in self])
|
429
|
-
|
430
|
-
def __len__(self):
|
431
|
-
return self._len
|
432
|
-
|
433
|
-
def __iter__(self):
|
434
|
-
cur = self.head
|
435
|
-
while True:
|
436
|
-
yield cur
|
437
|
-
cur = cur.next
|
438
|
-
if cur == self.head:
|
439
|
-
return
|
440
|
-
|
441
|
-
def _show(self):
|
442
|
-
cur = self.head
|
443
|
-
while True:
|
444
|
-
print(cur.__repr__())
|
445
|
-
cur = cur.next
|
446
|
-
if cur == self.head:
|
447
|
-
break
|
448
|
-
|
449
|
-
|
450
|
-
class _EventQueue:
|
451
|
-
def __init__(self):
|
452
|
-
self.__data = []
|
453
|
-
|
454
|
-
def put(self, item):
|
455
|
-
if item is not None:
|
456
|
-
heapq.heappush(self.__data, item)
|
457
|
-
|
458
|
-
def put_all(self, iterable):
|
459
|
-
for item in iterable:
|
460
|
-
heapq.heappush(self.__data, item)
|
461
|
-
|
462
|
-
def get(self):
|
463
|
-
return heapq.heappop(self.__data)
|
464
|
-
|
465
|
-
def empty(self):
|
466
|
-
return len(self.__data) == 0
|
467
|
-
|
468
|
-
def peek(self):
|
469
|
-
return self.__data[0]
|
470
|
-
|
471
|
-
def show(self):
|
472
|
-
for item in self.__data:
|
473
|
-
print(item)
|
474
|
-
|
475
|
-
def _merge_sources(skeleton):
|
476
|
-
"""
|
477
|
-
In highly symmetrical shapes with reflex vertices multiple sources may share the same
|
478
|
-
location. This function merges those sources.
|
479
|
-
"""
|
480
|
-
sources = {}
|
481
|
-
to_remove = []
|
482
|
-
for i, p in enumerate(skeleton):
|
483
|
-
source = tuple(i for i in p.source)
|
484
|
-
if source in sources:
|
485
|
-
source_index = sources[source]
|
486
|
-
# source exists, merge sinks
|
487
|
-
for sink in p.sinks:
|
488
|
-
if sink not in skeleton[source_index].sinks:
|
489
|
-
skeleton[source_index].sinks.append(sink)
|
490
|
-
to_remove.append(i)
|
491
|
-
else:
|
492
|
-
sources[source] = i
|
493
|
-
for i in reversed(to_remove):
|
494
|
-
skeleton.pop(i)
|
495
|
-
|
496
|
-
|
497
|
-
def skeletonize(polygon, holes=None):
|
498
|
-
"""
|
499
|
-
Compute the straight skeleton of a polygon.
|
500
|
-
|
501
|
-
The polygon should be given as a list of vertices in counter-clockwise order.
|
502
|
-
Holes is a list of the contours of the holes, the vertices of which should be in clockwise order.
|
503
|
-
|
504
|
-
Please note that the y-axis goes downwards as far as polyskel is concerned, so specify your ordering accordingly.
|
505
|
-
|
506
|
-
Returns the straight skeleton as a list of "subtrees", which are in the form of (source, height, sinks),
|
507
|
-
where source is the highest points, height is its height, and sinks are the point connected to the source.
|
508
|
-
"""
|
509
|
-
slav = _SLAV(polygon, holes)
|
510
|
-
output = []
|
511
|
-
prioque = _EventQueue()
|
512
|
-
|
513
|
-
for lav in slav:
|
514
|
-
for vertex in lav:
|
515
|
-
prioque.put(vertex.next_event())
|
516
|
-
|
517
|
-
while not (prioque.empty() or slav.empty()):
|
518
|
-
log.debug("SLAV is %s", [repr(lav) for lav in slav])
|
519
|
-
i = prioque.get()
|
520
|
-
if isinstance(i, _EdgeEvent):
|
521
|
-
if not i.vertex_a.is_valid or not i.vertex_b.is_valid:
|
522
|
-
log.info("%.2f Discarded outdated edge event %s", i.distance, i)
|
523
|
-
continue
|
524
|
-
|
525
|
-
(arc, events) = slav.handle_edge_event(i)
|
526
|
-
elif isinstance(i, _SplitEvent):
|
527
|
-
if not i.vertex.is_valid:
|
528
|
-
log.info("%.2f Discarded outdated split event %s", i.distance, i)
|
529
|
-
continue
|
530
|
-
(arc, events) = slav.handle_split_event(i)
|
531
|
-
|
532
|
-
prioque.put_all(events)
|
533
|
-
|
534
|
-
if arc is not None:
|
535
|
-
output.append(arc)
|
536
|
-
for sink in arc.sinks:
|
537
|
-
_debug.line((arc.source.x, arc.source.y, sink.x, sink.y), fill="red")
|
538
|
-
|
539
|
-
_debug.show()
|
540
|
-
_merge_sources(output)
|
541
|
-
return output
|
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
|
+
import heapq
|
20
|
+
from itertools import *
|
21
|
+
from collections import namedtuple
|
22
|
+
import os
|
23
|
+
import warnings
|
24
|
+
|
25
|
+
try:
|
26
|
+
from euclid3 import *
|
27
|
+
except:
|
28
|
+
print("Polyskel - Installing required euclid3 library.")
|
29
|
+
try:
|
30
|
+
os.system("pip install euclid3")
|
31
|
+
except:
|
32
|
+
os.system("pip install euclid3 --user")
|
33
|
+
try:
|
34
|
+
from euclid3 import *
|
35
|
+
except:
|
36
|
+
warnings.warn("Polyskel - ERROR: Could not import euclid3.")
|
37
|
+
|
38
|
+
log = logging.getLogger("__name__")
|
39
|
+
|
40
|
+
EPSILON = 0.00001
|
41
|
+
|
42
|
+
class Debug:
|
43
|
+
def __init__(self, image):
|
44
|
+
if image is not None:
|
45
|
+
self.im = image[0]
|
46
|
+
self.draw = image[1]
|
47
|
+
self.do = True
|
48
|
+
else:
|
49
|
+
self.do = False
|
50
|
+
|
51
|
+
def line(self, *args, **kwargs):
|
52
|
+
if self.do:
|
53
|
+
self.draw.line(*args, **kwargs)
|
54
|
+
|
55
|
+
def rectangle(self, *args, **kwargs):
|
56
|
+
if self.do:
|
57
|
+
self.draw.rectangle(*args, **kwargs)
|
58
|
+
|
59
|
+
def show(self):
|
60
|
+
if self.do:
|
61
|
+
self.im.show()
|
62
|
+
|
63
|
+
|
64
|
+
_debug = Debug(None)
|
65
|
+
|
66
|
+
|
67
|
+
def set_debug(image):
|
68
|
+
global _debug
|
69
|
+
_debug = Debug(image)
|
70
|
+
|
71
|
+
|
72
|
+
def _window(lst):
|
73
|
+
prevs, items, nexts = tee(lst, 3)
|
74
|
+
prevs = islice(cycle(prevs), len(lst) - 1, None)
|
75
|
+
nexts = islice(cycle(nexts), 1, None)
|
76
|
+
return zip(prevs, items, nexts)
|
77
|
+
|
78
|
+
|
79
|
+
def _cross(a, b):
|
80
|
+
res = a.x * b.y - b.x * a.y
|
81
|
+
return res
|
82
|
+
|
83
|
+
|
84
|
+
def _approximately_equals(a, b):
|
85
|
+
return a == b or (abs(a - b) <= max(abs(a), abs(b)) * 0.001)
|
86
|
+
|
87
|
+
|
88
|
+
def _approximately_same(point_a, point_b):
|
89
|
+
return _approximately_equals(point_a.x, point_b.x) and _approximately_equals(point_a.y, point_b.y)
|
90
|
+
|
91
|
+
|
92
|
+
def _normalize_contour(contour):
|
93
|
+
contour = [Point2(float(x), float(y)) for (x, y) in contour]
|
94
|
+
return [point for prev, point, next in _window(contour) if not (point == next or (point-prev).normalized() == (next - point).normalized())]
|
95
|
+
|
96
|
+
|
97
|
+
class _SplitEvent(namedtuple("_SplitEvent", "distance, intersection_point, vertex, opposite_edge")):
|
98
|
+
__slots__ = ()
|
99
|
+
|
100
|
+
def __lt__(self, other):
|
101
|
+
return self.distance < other.distance
|
102
|
+
|
103
|
+
def __str__(self):
|
104
|
+
return "{} Split event @ {} from {} to {}".format(self.distance, self.intersection_point, self.vertex, self.opposite_edge)
|
105
|
+
|
106
|
+
|
107
|
+
class _EdgeEvent(namedtuple("_EdgeEvent", "distance intersection_point vertex_a vertex_b")):
|
108
|
+
__slots__ = ()
|
109
|
+
|
110
|
+
def __lt__(self, other):
|
111
|
+
return self.distance < other.distance
|
112
|
+
|
113
|
+
def __str__(self):
|
114
|
+
return "{} Edge event @ {} between {} and {}".format(self.distance, self.intersection_point, self.vertex_a, self.vertex_b)
|
115
|
+
|
116
|
+
|
117
|
+
_OriginalEdge = namedtuple("_OriginalEdge", "edge bisector_left, bisector_right")
|
118
|
+
|
119
|
+
Subtree = namedtuple("Subtree", "source, height, sinks")
|
120
|
+
|
121
|
+
|
122
|
+
class _LAVertex:
|
123
|
+
def __init__(self, point, edge_left, edge_right, direction_vectors=None):
|
124
|
+
self.point = point
|
125
|
+
self.edge_left = edge_left
|
126
|
+
self.edge_right = edge_right
|
127
|
+
self.prev = None
|
128
|
+
self.next = None
|
129
|
+
self.lav = None
|
130
|
+
self._valid = True # TODO this might be handled better. Maybe membership in lav implies validity?
|
131
|
+
|
132
|
+
creator_vectors = (edge_left.v.normalized() * -1, edge_right.v.normalized())
|
133
|
+
if direction_vectors is None:
|
134
|
+
direction_vectors = creator_vectors
|
135
|
+
|
136
|
+
self._is_reflex = (_cross(*direction_vectors)) < 0
|
137
|
+
self._bisector = Ray2(self.point, operator.add(*creator_vectors) * (-1 if self.is_reflex else 1))
|
138
|
+
log.info("Created vertex %s", self.__repr__())
|
139
|
+
_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")
|
140
|
+
|
141
|
+
@property
|
142
|
+
def bisector(self):
|
143
|
+
return self._bisector
|
144
|
+
|
145
|
+
@property
|
146
|
+
def is_reflex(self):
|
147
|
+
return self._is_reflex
|
148
|
+
|
149
|
+
@property
|
150
|
+
def original_edges(self):
|
151
|
+
return self.lav._slav._original_edges
|
152
|
+
|
153
|
+
def next_event(self):
|
154
|
+
events = []
|
155
|
+
if self.is_reflex:
|
156
|
+
# a reflex vertex may generate a split event
|
157
|
+
# split events happen when a vertex hits an opposite edge, splitting the polygon in two.
|
158
|
+
log.debug("looking for split candidates for vertex %s", self)
|
159
|
+
for edge in self.original_edges:
|
160
|
+
if edge.edge == self.edge_left or edge.edge == self.edge_right:
|
161
|
+
continue
|
162
|
+
|
163
|
+
log.debug("\tconsidering EDGE %s", edge)
|
164
|
+
|
165
|
+
# a potential b is at the intersection of between our own bisector and the bisector of the
|
166
|
+
# angle between the tested edge and any one of our own edges.
|
167
|
+
|
168
|
+
# we choose the "less parallel" edge (in order to exclude a potentially parallel edge)
|
169
|
+
leftdot = abs(self.edge_left.v.normalized().dot(edge.edge.v.normalized()))
|
170
|
+
rightdot = abs(self.edge_right.v.normalized().dot(edge.edge.v.normalized()))
|
171
|
+
selfedge = self.edge_left if leftdot < rightdot else self.edge_right
|
172
|
+
otheredge = self.edge_left if leftdot > rightdot else self.edge_right
|
173
|
+
|
174
|
+
i = Line2(selfedge).intersect(Line2(edge.edge))
|
175
|
+
if i is not None and not _approximately_equals(i, self.point):
|
176
|
+
# locate candidate b
|
177
|
+
linvec = (self.point - i).normalized()
|
178
|
+
edvec = edge.edge.v.normalized()
|
179
|
+
if linvec.dot(edvec) < 0:
|
180
|
+
edvec = -edvec
|
181
|
+
|
182
|
+
bisecvec = edvec + linvec
|
183
|
+
if abs(bisecvec) == 0:
|
184
|
+
continue
|
185
|
+
bisector = Line2(i, bisecvec)
|
186
|
+
b = bisector.intersect(self.bisector)
|
187
|
+
|
188
|
+
if b is None:
|
189
|
+
continue
|
190
|
+
|
191
|
+
# check eligibility of b
|
192
|
+
# a valid b should lie within the area limited by the edge and the bisectors of its two vertices:
|
193
|
+
xleft = _cross(edge.bisector_left.v.normalized(), (b - edge.bisector_left.p).normalized()) > -EPSILON
|
194
|
+
xright = _cross(edge.bisector_right.v.normalized(), (b - edge.bisector_right.p).normalized()) < EPSILON
|
195
|
+
xedge = _cross(edge.edge.v.normalized(), (b - edge.edge.p).normalized()) < EPSILON
|
196
|
+
|
197
|
+
if not (xleft and xright and xedge):
|
198
|
+
log.debug("\t\tDiscarded candidate %s (%s-%s-%s)", b, xleft, xright, xedge)
|
199
|
+
continue
|
200
|
+
|
201
|
+
log.debug("\t\tFound valid candidate %s", b)
|
202
|
+
events.append(_SplitEvent(Line2(edge.edge).distance(b), b, self, edge.edge))
|
203
|
+
|
204
|
+
i_prev = self.bisector.intersect(self.prev.bisector)
|
205
|
+
i_next = self.bisector.intersect(self.next.bisector)
|
206
|
+
|
207
|
+
if i_prev is not None:
|
208
|
+
events.append(_EdgeEvent(Line2(self.edge_left).distance(i_prev), i_prev, self.prev, self))
|
209
|
+
if i_next is not None:
|
210
|
+
events.append(_EdgeEvent(Line2(self.edge_right).distance(i_next), i_next, self, self.next))
|
211
|
+
|
212
|
+
if not events:
|
213
|
+
return None
|
214
|
+
|
215
|
+
ev = min(events, key=lambda event: self.point.distance(event.intersection_point))
|
216
|
+
|
217
|
+
log.info("Generated new event for %s: %s", self, ev)
|
218
|
+
return ev
|
219
|
+
|
220
|
+
def invalidate(self):
|
221
|
+
if self.lav is not None:
|
222
|
+
self.lav.invalidate(self)
|
223
|
+
else:
|
224
|
+
self._valid = False
|
225
|
+
|
226
|
+
@property
|
227
|
+
def is_valid(self):
|
228
|
+
return self._valid
|
229
|
+
|
230
|
+
def __str__(self):
|
231
|
+
return "Vertex ({:.2f};{:.2f})".format(self.point.x, self.point.y)
|
232
|
+
|
233
|
+
def __repr__(self):
|
234
|
+
return "Vertex ({}) ({:.2f};{:.2f}), bisector {}, edges {} {}".format("reflex" if self.is_reflex else "convex",
|
235
|
+
self.point.x, self.point.y, self.bisector,
|
236
|
+
self.edge_left, self.edge_right)
|
237
|
+
|
238
|
+
|
239
|
+
class _SLAV:
|
240
|
+
def __init__(self, polygon, holes):
|
241
|
+
contours = [_normalize_contour(polygon)]
|
242
|
+
contours.extend([_normalize_contour(hole) for hole in holes])
|
243
|
+
|
244
|
+
self._lavs = [_LAV.from_polygon(contour, self) for contour in contours]
|
245
|
+
|
246
|
+
# store original polygon edges for calculating split events
|
247
|
+
self._original_edges = [
|
248
|
+
_OriginalEdge(LineSegment2(vertex.prev.point, vertex.point), vertex.prev.bisector, vertex.bisector)
|
249
|
+
for vertex in chain.from_iterable(self._lavs)
|
250
|
+
]
|
251
|
+
|
252
|
+
def __iter__(self):
|
253
|
+
for lav in self._lavs:
|
254
|
+
yield lav
|
255
|
+
|
256
|
+
def __len__(self):
|
257
|
+
return len(self._lavs)
|
258
|
+
|
259
|
+
def empty(self):
|
260
|
+
return len(self._lavs) == 0
|
261
|
+
|
262
|
+
def handle_edge_event(self, event):
|
263
|
+
sinks = []
|
264
|
+
events = []
|
265
|
+
|
266
|
+
lav = event.vertex_a.lav
|
267
|
+
if event.vertex_a.prev == event.vertex_b.next:
|
268
|
+
log.info("%.2f Peak event at intersection %s from <%s,%s,%s> in %s", event.distance,
|
269
|
+
event.intersection_point, event.vertex_a, event.vertex_b, event.vertex_a.prev, lav)
|
270
|
+
self._lavs.remove(lav)
|
271
|
+
for vertex in list(lav):
|
272
|
+
sinks.append(vertex.point)
|
273
|
+
vertex.invalidate()
|
274
|
+
else:
|
275
|
+
log.info("%.2f Edge event at intersection %s from <%s,%s> in %s", event.distance, event.intersection_point,
|
276
|
+
event.vertex_a, event.vertex_b, lav)
|
277
|
+
new_vertex = lav.unify(event.vertex_a, event.vertex_b, event.intersection_point)
|
278
|
+
if lav.head in (event.vertex_a, event.vertex_b):
|
279
|
+
lav.head = new_vertex
|
280
|
+
sinks.extend((event.vertex_a.point, event.vertex_b.point))
|
281
|
+
next_event = new_vertex.next_event()
|
282
|
+
if next_event is not None:
|
283
|
+
events.append(next_event)
|
284
|
+
|
285
|
+
return (Subtree(event.intersection_point, event.distance, sinks), events)
|
286
|
+
|
287
|
+
def handle_split_event(self, event):
|
288
|
+
lav = event.vertex.lav
|
289
|
+
log.info("%.2f Split event at intersection %s from vertex %s, for edge %s in %s", event.distance,
|
290
|
+
event.intersection_point, event.vertex, event.opposite_edge, lav)
|
291
|
+
|
292
|
+
sinks = [event.vertex.point]
|
293
|
+
vertices = []
|
294
|
+
x = None # right vertex
|
295
|
+
y = None # left vertex
|
296
|
+
norm = event.opposite_edge.v.normalized()
|
297
|
+
for v in chain.from_iterable(self._lavs):
|
298
|
+
log.debug("%s in %s", v, v.lav)
|
299
|
+
if norm == v.edge_left.v.normalized() and event.opposite_edge.p == v.edge_left.p:
|
300
|
+
x = v
|
301
|
+
y = x.prev
|
302
|
+
elif norm == v.edge_right.v.normalized() and event.opposite_edge.p == v.edge_right.p:
|
303
|
+
y = v
|
304
|
+
x = y.next
|
305
|
+
|
306
|
+
if x:
|
307
|
+
xleft = _cross(y.bisector.v.normalized(), (event.intersection_point - y.point).normalized()) >= -EPSILON
|
308
|
+
xright = _cross(x.bisector.v.normalized(), (event.intersection_point - x.point).normalized()) <= EPSILON
|
309
|
+
log.debug("Vertex %s holds edge as %s edge (%s, %s)", v, ("left" if x == v else "right"), xleft, xright)
|
310
|
+
|
311
|
+
if xleft and xright:
|
312
|
+
break
|
313
|
+
else:
|
314
|
+
x = None
|
315
|
+
y = None
|
316
|
+
|
317
|
+
if x is None:
|
318
|
+
log.info("Failed split event %s (equivalent edge event is expected to follow)", event)
|
319
|
+
return (None, [])
|
320
|
+
|
321
|
+
v1 = _LAVertex(event.intersection_point, event.vertex.edge_left, event.opposite_edge)
|
322
|
+
v2 = _LAVertex(event.intersection_point, event.opposite_edge, event.vertex.edge_right)
|
323
|
+
|
324
|
+
v1.prev = event.vertex.prev
|
325
|
+
v1.next = x
|
326
|
+
event.vertex.prev.next = v1
|
327
|
+
x.prev = v1
|
328
|
+
|
329
|
+
v2.prev = y
|
330
|
+
v2.next = event.vertex.next
|
331
|
+
event.vertex.next.prev = v2
|
332
|
+
y.next = v2
|
333
|
+
|
334
|
+
new_lavs = None
|
335
|
+
self._lavs.remove(lav)
|
336
|
+
if lav != x.lav:
|
337
|
+
# the split event actually merges two lavs
|
338
|
+
self._lavs.remove(x.lav)
|
339
|
+
new_lavs = [_LAV.from_chain(v1, self)]
|
340
|
+
else:
|
341
|
+
new_lavs = [_LAV.from_chain(v1, self), _LAV.from_chain(v2, self)]
|
342
|
+
|
343
|
+
for l in new_lavs:
|
344
|
+
log.debug(l)
|
345
|
+
if len(l) > 2:
|
346
|
+
self._lavs.append(l)
|
347
|
+
vertices.append(l.head)
|
348
|
+
else:
|
349
|
+
log.info("LAV %s has collapsed into the line %s--%s", l, l.head.point, l.head.next.point)
|
350
|
+
sinks.append(l.head.next.point)
|
351
|
+
for v in list(l):
|
352
|
+
v.invalidate()
|
353
|
+
|
354
|
+
events = []
|
355
|
+
for vertex in vertices:
|
356
|
+
next_event = vertex.next_event()
|
357
|
+
if next_event is not None:
|
358
|
+
events.append(next_event)
|
359
|
+
|
360
|
+
event.vertex.invalidate()
|
361
|
+
return (Subtree(event.intersection_point, event.distance, sinks), events)
|
362
|
+
|
363
|
+
|
364
|
+
class _LAV:
|
365
|
+
def __init__(self, slav):
|
366
|
+
self.head = None
|
367
|
+
self._slav = slav
|
368
|
+
self._len = 0
|
369
|
+
log.debug("Created LAV %s", self)
|
370
|
+
|
371
|
+
@classmethod
|
372
|
+
def from_polygon(cls, polygon, slav):
|
373
|
+
lav = cls(slav)
|
374
|
+
for prev, point, next in _window(polygon):
|
375
|
+
lav._len += 1
|
376
|
+
vertex = _LAVertex(point, LineSegment2(prev, point), LineSegment2(point, next))
|
377
|
+
vertex.lav = lav
|
378
|
+
if lav.head is None:
|
379
|
+
lav.head = vertex
|
380
|
+
vertex.prev = vertex.next = vertex
|
381
|
+
else:
|
382
|
+
vertex.next = lav.head
|
383
|
+
vertex.prev = lav.head.prev
|
384
|
+
vertex.prev.next = vertex
|
385
|
+
lav.head.prev = vertex
|
386
|
+
return lav
|
387
|
+
|
388
|
+
@classmethod
|
389
|
+
def from_chain(cls, head, slav):
|
390
|
+
lav = cls(slav)
|
391
|
+
lav.head = head
|
392
|
+
for vertex in lav:
|
393
|
+
lav._len += 1
|
394
|
+
vertex.lav = lav
|
395
|
+
return lav
|
396
|
+
|
397
|
+
def invalidate(self, vertex):
|
398
|
+
assert vertex.lav is self, "Tried to invalidate a vertex that's not mine"
|
399
|
+
log.debug("Invalidating %s", vertex)
|
400
|
+
vertex._valid = False
|
401
|
+
if self.head == vertex:
|
402
|
+
self.head = self.head.next
|
403
|
+
vertex.lav = None
|
404
|
+
|
405
|
+
def unify(self, vertex_a, vertex_b, point):
|
406
|
+
replacement = _LAVertex(point, vertex_a.edge_left, vertex_b.edge_right,
|
407
|
+
(vertex_b.bisector.v.normalized(), vertex_a.bisector.v.normalized()))
|
408
|
+
replacement.lav = self
|
409
|
+
|
410
|
+
if self.head in [vertex_a, vertex_b]:
|
411
|
+
self.head = replacement
|
412
|
+
|
413
|
+
vertex_a.prev.next = replacement
|
414
|
+
vertex_b.next.prev = replacement
|
415
|
+
replacement.prev = vertex_a.prev
|
416
|
+
replacement.next = vertex_b.next
|
417
|
+
|
418
|
+
vertex_a.invalidate()
|
419
|
+
vertex_b.invalidate()
|
420
|
+
|
421
|
+
self._len -= 1
|
422
|
+
return replacement
|
423
|
+
|
424
|
+
def __str__(self):
|
425
|
+
return "LAV {}".format(id(self))
|
426
|
+
|
427
|
+
def __repr__(self):
|
428
|
+
return "{} = {}".format(str(self), [vertex for vertex in self])
|
429
|
+
|
430
|
+
def __len__(self):
|
431
|
+
return self._len
|
432
|
+
|
433
|
+
def __iter__(self):
|
434
|
+
cur = self.head
|
435
|
+
while True:
|
436
|
+
yield cur
|
437
|
+
cur = cur.next
|
438
|
+
if cur == self.head:
|
439
|
+
return
|
440
|
+
|
441
|
+
def _show(self):
|
442
|
+
cur = self.head
|
443
|
+
while True:
|
444
|
+
print(cur.__repr__())
|
445
|
+
cur = cur.next
|
446
|
+
if cur == self.head:
|
447
|
+
break
|
448
|
+
|
449
|
+
|
450
|
+
class _EventQueue:
|
451
|
+
def __init__(self):
|
452
|
+
self.__data = []
|
453
|
+
|
454
|
+
def put(self, item):
|
455
|
+
if item is not None:
|
456
|
+
heapq.heappush(self.__data, item)
|
457
|
+
|
458
|
+
def put_all(self, iterable):
|
459
|
+
for item in iterable:
|
460
|
+
heapq.heappush(self.__data, item)
|
461
|
+
|
462
|
+
def get(self):
|
463
|
+
return heapq.heappop(self.__data)
|
464
|
+
|
465
|
+
def empty(self):
|
466
|
+
return len(self.__data) == 0
|
467
|
+
|
468
|
+
def peek(self):
|
469
|
+
return self.__data[0]
|
470
|
+
|
471
|
+
def show(self):
|
472
|
+
for item in self.__data:
|
473
|
+
print(item)
|
474
|
+
|
475
|
+
def _merge_sources(skeleton):
|
476
|
+
"""
|
477
|
+
In highly symmetrical shapes with reflex vertices multiple sources may share the same
|
478
|
+
location. This function merges those sources.
|
479
|
+
"""
|
480
|
+
sources = {}
|
481
|
+
to_remove = []
|
482
|
+
for i, p in enumerate(skeleton):
|
483
|
+
source = tuple(i for i in p.source)
|
484
|
+
if source in sources:
|
485
|
+
source_index = sources[source]
|
486
|
+
# source exists, merge sinks
|
487
|
+
for sink in p.sinks:
|
488
|
+
if sink not in skeleton[source_index].sinks:
|
489
|
+
skeleton[source_index].sinks.append(sink)
|
490
|
+
to_remove.append(i)
|
491
|
+
else:
|
492
|
+
sources[source] = i
|
493
|
+
for i in reversed(to_remove):
|
494
|
+
skeleton.pop(i)
|
495
|
+
|
496
|
+
|
497
|
+
def skeletonize(polygon, holes=None):
|
498
|
+
"""
|
499
|
+
Compute the straight skeleton of a polygon.
|
500
|
+
|
501
|
+
The polygon should be given as a list of vertices in counter-clockwise order.
|
502
|
+
Holes is a list of the contours of the holes, the vertices of which should be in clockwise order.
|
503
|
+
|
504
|
+
Please note that the y-axis goes downwards as far as polyskel is concerned, so specify your ordering accordingly.
|
505
|
+
|
506
|
+
Returns the straight skeleton as a list of "subtrees", which are in the form of (source, height, sinks),
|
507
|
+
where source is the highest points, height is its height, and sinks are the point connected to the source.
|
508
|
+
"""
|
509
|
+
slav = _SLAV(polygon, holes)
|
510
|
+
output = []
|
511
|
+
prioque = _EventQueue()
|
512
|
+
|
513
|
+
for lav in slav:
|
514
|
+
for vertex in lav:
|
515
|
+
prioque.put(vertex.next_event())
|
516
|
+
|
517
|
+
while not (prioque.empty() or slav.empty()):
|
518
|
+
log.debug("SLAV is %s", [repr(lav) for lav in slav])
|
519
|
+
i = prioque.get()
|
520
|
+
if isinstance(i, _EdgeEvent):
|
521
|
+
if not i.vertex_a.is_valid or not i.vertex_b.is_valid:
|
522
|
+
log.info("%.2f Discarded outdated edge event %s", i.distance, i)
|
523
|
+
continue
|
524
|
+
|
525
|
+
(arc, events) = slav.handle_edge_event(i)
|
526
|
+
elif isinstance(i, _SplitEvent):
|
527
|
+
if not i.vertex.is_valid:
|
528
|
+
log.info("%.2f Discarded outdated split event %s", i.distance, i)
|
529
|
+
continue
|
530
|
+
(arc, events) = slav.handle_split_event(i)
|
531
|
+
|
532
|
+
prioque.put_all(events)
|
533
|
+
|
534
|
+
if arc is not None:
|
535
|
+
output.append(arc)
|
536
|
+
for sink in arc.sinks:
|
537
|
+
_debug.line((arc.source.x, arc.source.y, sink.x, sink.y), fill="red")
|
538
|
+
|
539
|
+
_debug.show()
|
540
|
+
_merge_sources(output)
|
541
|
+
return output
|