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.
Files changed (94) hide show
  1. topologicpy/Aperture.py +72 -72
  2. topologicpy/Cell.py +2169 -2169
  3. topologicpy/CellComplex.py +1137 -1137
  4. topologicpy/Cluster.py +1288 -1280
  5. topologicpy/Color.py +423 -423
  6. topologicpy/Context.py +79 -79
  7. topologicpy/DGL.py +3213 -3240
  8. topologicpy/Dictionary.py +698 -698
  9. topologicpy/Edge.py +1187 -1187
  10. topologicpy/EnergyModel.py +1180 -1152
  11. topologicpy/Face.py +2141 -2141
  12. topologicpy/Graph.py +7768 -7768
  13. topologicpy/Grid.py +353 -353
  14. topologicpy/Helper.py +507 -507
  15. topologicpy/Honeybee.py +461 -461
  16. topologicpy/Matrix.py +271 -271
  17. topologicpy/Neo4j.py +521 -521
  18. topologicpy/Plotly.py +2 -2
  19. topologicpy/Polyskel.py +541 -541
  20. topologicpy/Shell.py +1768 -1768
  21. topologicpy/Speckle.py +508 -508
  22. topologicpy/Topology.py +7060 -7002
  23. topologicpy/Vector.py +905 -905
  24. topologicpy/Vertex.py +1585 -1585
  25. topologicpy/Wire.py +3050 -3050
  26. topologicpy/__init__.py +22 -38
  27. topologicpy/version.py +1 -0
  28. {topologicpy-0.5.9.dist-info → topologicpy-6.0.0.dist-info}/LICENSE +661 -704
  29. topologicpy-6.0.0.dist-info/METADATA +751 -0
  30. topologicpy-6.0.0.dist-info/RECORD +32 -0
  31. topologicpy/bin/linux/topologic/__init__.py +0 -2
  32. topologicpy/bin/linux/topologic/libTKBO-6bdf205d.so.7.7.0 +0 -0
  33. topologicpy/bin/linux/topologic/libTKBRep-2960a069.so.7.7.0 +0 -0
  34. topologicpy/bin/linux/topologic/libTKBool-c44b74bd.so.7.7.0 +0 -0
  35. topologicpy/bin/linux/topologic/libTKFillet-9a670ba0.so.7.7.0 +0 -0
  36. topologicpy/bin/linux/topologic/libTKG2d-8f31849e.so.7.7.0 +0 -0
  37. topologicpy/bin/linux/topologic/libTKG3d-4c6bce57.so.7.7.0 +0 -0
  38. topologicpy/bin/linux/topologic/libTKGeomAlgo-26066fd9.so.7.7.0 +0 -0
  39. topologicpy/bin/linux/topologic/libTKGeomBase-2116cabe.so.7.7.0 +0 -0
  40. topologicpy/bin/linux/topologic/libTKMath-72572fa8.so.7.7.0 +0 -0
  41. topologicpy/bin/linux/topologic/libTKMesh-2a060427.so.7.7.0 +0 -0
  42. topologicpy/bin/linux/topologic/libTKOffset-6cab68ff.so.7.7.0 +0 -0
  43. topologicpy/bin/linux/topologic/libTKPrim-eb1262b3.so.7.7.0 +0 -0
  44. topologicpy/bin/linux/topologic/libTKShHealing-e67e5cc7.so.7.7.0 +0 -0
  45. topologicpy/bin/linux/topologic/libTKTopAlgo-e4c96c33.so.7.7.0 +0 -0
  46. topologicpy/bin/linux/topologic/libTKernel-fb7fe3b7.so.7.7.0 +0 -0
  47. topologicpy/bin/linux/topologic/libgcc_s-32c1665e.so.1 +0 -0
  48. topologicpy/bin/linux/topologic/libstdc++-672d7b41.so.6.0.30 +0 -0
  49. topologicpy/bin/linux/topologic/topologic.cpython-310-x86_64-linux-gnu.so +0 -0
  50. topologicpy/bin/linux/topologic/topologic.cpython-311-x86_64-linux-gnu.so +0 -0
  51. topologicpy/bin/linux/topologic/topologic.cpython-38-x86_64-linux-gnu.so +0 -0
  52. topologicpy/bin/linux/topologic/topologic.cpython-39-x86_64-linux-gnu.so +0 -0
  53. topologicpy/bin/linux/topologic.libs/libTKBO-6bdf205d.so.7.7.0 +0 -0
  54. topologicpy/bin/linux/topologic.libs/libTKBRep-2960a069.so.7.7.0 +0 -0
  55. topologicpy/bin/linux/topologic.libs/libTKBool-c44b74bd.so.7.7.0 +0 -0
  56. topologicpy/bin/linux/topologic.libs/libTKFillet-9a670ba0.so.7.7.0 +0 -0
  57. topologicpy/bin/linux/topologic.libs/libTKG2d-8f31849e.so.7.7.0 +0 -0
  58. topologicpy/bin/linux/topologic.libs/libTKG3d-4c6bce57.so.7.7.0 +0 -0
  59. topologicpy/bin/linux/topologic.libs/libTKGeomAlgo-26066fd9.so.7.7.0 +0 -0
  60. topologicpy/bin/linux/topologic.libs/libTKGeomBase-2116cabe.so.7.7.0 +0 -0
  61. topologicpy/bin/linux/topologic.libs/libTKMath-72572fa8.so.7.7.0 +0 -0
  62. topologicpy/bin/linux/topologic.libs/libTKMesh-2a060427.so.7.7.0 +0 -0
  63. topologicpy/bin/linux/topologic.libs/libTKOffset-6cab68ff.so.7.7.0 +0 -0
  64. topologicpy/bin/linux/topologic.libs/libTKPrim-eb1262b3.so.7.7.0 +0 -0
  65. topologicpy/bin/linux/topologic.libs/libTKShHealing-e67e5cc7.so.7.7.0 +0 -0
  66. topologicpy/bin/linux/topologic.libs/libTKTopAlgo-e4c96c33.so.7.7.0 +0 -0
  67. topologicpy/bin/linux/topologic.libs/libTKernel-fb7fe3b7.so.7.7.0 +0 -0
  68. topologicpy/bin/linux/topologic.libs/libgcc_s-32c1665e.so.1 +0 -0
  69. topologicpy/bin/linux/topologic.libs/libstdc++-672d7b41.so.6.0.30 +0 -0
  70. topologicpy/bin/macos/topologic/__init__.py +0 -2
  71. topologicpy/bin/windows/topologic/TKBO-f6b191de.dll +0 -0
  72. topologicpy/bin/windows/topologic/TKBRep-e56a600e.dll +0 -0
  73. topologicpy/bin/windows/topologic/TKBool-7b8d47ae.dll +0 -0
  74. topologicpy/bin/windows/topologic/TKFillet-0ddbf0a8.dll +0 -0
  75. topologicpy/bin/windows/topologic/TKG2d-2e2dee3d.dll +0 -0
  76. topologicpy/bin/windows/topologic/TKG3d-6674513d.dll +0 -0
  77. topologicpy/bin/windows/topologic/TKGeomAlgo-d240e370.dll +0 -0
  78. topologicpy/bin/windows/topologic/TKGeomBase-df87aba5.dll +0 -0
  79. topologicpy/bin/windows/topologic/TKMath-45bd625a.dll +0 -0
  80. topologicpy/bin/windows/topologic/TKMesh-d6e826b1.dll +0 -0
  81. topologicpy/bin/windows/topologic/TKOffset-79b9cc94.dll +0 -0
  82. topologicpy/bin/windows/topologic/TKPrim-aa430a86.dll +0 -0
  83. topologicpy/bin/windows/topologic/TKShHealing-bb48be89.dll +0 -0
  84. topologicpy/bin/windows/topologic/TKTopAlgo-7d0d1e22.dll +0 -0
  85. topologicpy/bin/windows/topologic/TKernel-08c8cfbb.dll +0 -0
  86. topologicpy/bin/windows/topologic/__init__.py +0 -2
  87. topologicpy/bin/windows/topologic/topologic.cp310-win_amd64.pyd +0 -0
  88. topologicpy/bin/windows/topologic/topologic.cp311-win_amd64.pyd +0 -0
  89. topologicpy/bin/windows/topologic/topologic.cp38-win_amd64.pyd +0 -0
  90. topologicpy/bin/windows/topologic/topologic.cp39-win_amd64.pyd +0 -0
  91. topologicpy-0.5.9.dist-info/METADATA +0 -86
  92. topologicpy-0.5.9.dist-info/RECORD +0 -91
  93. {topologicpy-0.5.9.dist-info → topologicpy-6.0.0.dist-info}/WHEEL +0 -0
  94. {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