nodeeditor-qtpy 0.8.1__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.
- nodeeditor/__init__.py +93 -0
- nodeeditor/node_colors_config.py +343 -0
- nodeeditor/node_content_widget.py +129 -0
- nodeeditor/node_decorators.py +65 -0
- nodeeditor/node_edge.py +518 -0
- nodeeditor/node_edge_dragging.py +185 -0
- nodeeditor/node_edge_intersect.py +219 -0
- nodeeditor/node_edge_rerouting.py +216 -0
- nodeeditor/node_edge_snapping.py +95 -0
- nodeeditor/node_edge_validators.py +82 -0
- nodeeditor/node_editor_widget.py +257 -0
- nodeeditor/node_editor_window.py +448 -0
- nodeeditor/node_graph.py +583 -0
- nodeeditor/node_graphics_cutline.py +57 -0
- nodeeditor/node_graphics_edge.py +375 -0
- nodeeditor/node_graphics_edge_path.py +226 -0
- nodeeditor/node_graphics_node.py +693 -0
- nodeeditor/node_graphics_scene.py +369 -0
- nodeeditor/node_graphics_socket.py +262 -0
- nodeeditor/node_graphics_view.py +907 -0
- nodeeditor/node_group.py +1286 -0
- nodeeditor/node_group_node.py +5 -0
- nodeeditor/node_icon_content_widget.py +176 -0
- nodeeditor/node_icon_graphics_node.py +524 -0
- nodeeditor/node_multi_input_node.py +450 -0
- nodeeditor/node_node.py +848 -0
- nodeeditor/node_scene.py +1106 -0
- nodeeditor/node_scene_clipboard.py +314 -0
- nodeeditor/node_scene_history.py +556 -0
- nodeeditor/node_serializable.py +43 -0
- nodeeditor/node_socket.py +445 -0
- nodeeditor/node_socket_shapes.py +261 -0
- nodeeditor/py.typed +0 -0
- nodeeditor/qss/nodestyle.qss +3 -0
- nodeeditor/utils.py +108 -0
- nodeeditor/utils_no_qt.py +21 -0
- nodeeditor_qtpy-0.8.1.dist-info/METADATA +165 -0
- nodeeditor_qtpy-0.8.1.dist-info/RECORD +40 -0
- nodeeditor_qtpy-0.8.1.dist-info/WHEEL +4 -0
- nodeeditor_qtpy-0.8.1.dist-info/licenses/LICENCE +23 -0
nodeeditor/node_graph.py
ADDED
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Graph algorithms over a NodeEditor :class:`~nodeeditor.node_scene.Scene`.
|
|
4
|
+
|
|
5
|
+
The data-flow graph of a `Scene` is directed: an
|
|
6
|
+
:class:`~nodeeditor.node_edge.Edge` runs from the `Node` owning its start
|
|
7
|
+
(output) :class:`~nodeeditor.node_socket.Socket` to the `Node` owning its end
|
|
8
|
+
(input) `Socket`. An `Edge` whose endpoints do not form such a pair is
|
|
9
|
+
ignored, as is an `Edge` that is not attached to a `Socket` at all.
|
|
10
|
+
|
|
11
|
+
Every algorithm here is implemented natively, so no third-party graph library
|
|
12
|
+
is required. All traversals are iterative and keep a visited set, which means
|
|
13
|
+
|
|
14
|
+
- a graph containing a cycle terminates instead of exhausting the interpreter's
|
|
15
|
+
recursion limit, and
|
|
16
|
+
- a node reachable through several distinct paths is visited once rather than
|
|
17
|
+
once per path.
|
|
18
|
+
|
|
19
|
+
The graph is derived from the `Scene` on every call, so it always reflects the
|
|
20
|
+
current connections. Nothing is cached and nothing has to be invalidated.
|
|
21
|
+
|
|
22
|
+
Edges internal to a collapsed :class:`~nodeeditor.node_group.Group` are hidden
|
|
23
|
+
on screen but still carry data, so they still count here. Groups are visual
|
|
24
|
+
containers and never change the shape of the graph.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import heapq
|
|
28
|
+
|
|
29
|
+
from collections import deque
|
|
30
|
+
|
|
31
|
+
from typing import (
|
|
32
|
+
TYPE_CHECKING,
|
|
33
|
+
Deque,
|
|
34
|
+
Dict,
|
|
35
|
+
Iterator,
|
|
36
|
+
List,
|
|
37
|
+
Optional,
|
|
38
|
+
Sequence,
|
|
39
|
+
Set,
|
|
40
|
+
Tuple,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
if TYPE_CHECKING:
|
|
45
|
+
from nodeeditor.node_edge import Edge
|
|
46
|
+
from nodeeditor.node_node import Node
|
|
47
|
+
from nodeeditor.node_scene import Scene
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
#: One step of a path: the `Node` reached and the `Edge` used to reach it.
|
|
51
|
+
#: The first step of a path carries ``None`` as its edge, because nothing
|
|
52
|
+
#: leads into the first node.
|
|
53
|
+
PathStep = Tuple["Node", Optional["Edge"]]
|
|
54
|
+
|
|
55
|
+
#: An iterator over the ``(neighbour, edge)`` pairs leaving a `Node`.
|
|
56
|
+
StepIterator = Iterator[Tuple["Node", "Edge"]]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class GraphCycleError(Exception):
|
|
60
|
+
"""Raised when a graph that has to be acyclic contains a cycle.
|
|
61
|
+
|
|
62
|
+
:Instance Attributes:
|
|
63
|
+
|
|
64
|
+
- **cycles** - the cycles found, each a list of
|
|
65
|
+
:class:`~nodeeditor.node_edge.Edge` in traversal order
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(self, cycles: List[List["Edge"]]) -> None:
|
|
69
|
+
"""
|
|
70
|
+
:param cycles: the cycles found in the graph
|
|
71
|
+
:type cycles: ``list`` of ``list`` of :class:`~nodeeditor.node_edge.Edge`
|
|
72
|
+
"""
|
|
73
|
+
self.cycles = cycles
|
|
74
|
+
count = len(cycles)
|
|
75
|
+
super().__init__(
|
|
76
|
+
"graph contains %d cycle%s" % (count, "" if count == 1 else "s"))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
# internal helpers
|
|
81
|
+
# ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
def _edge_endpoints(edge: "Edge") -> Tuple[Optional["Node"], Optional["Node"]]:
|
|
84
|
+
"""``(source, target)`` of a data-flow `Edge`.
|
|
85
|
+
|
|
86
|
+
Returns ``(None, None)`` for an `Edge` that is detached, or whose sockets
|
|
87
|
+
do not form an output-to-input pair.
|
|
88
|
+
"""
|
|
89
|
+
start = edge.start_socket
|
|
90
|
+
end = edge.end_socket
|
|
91
|
+
if start is None or end is None:
|
|
92
|
+
return None, None
|
|
93
|
+
if not (start.is_output and end.is_input):
|
|
94
|
+
return None, None
|
|
95
|
+
return start.node, end.node
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _downstream_steps(node: "Node") -> List[Tuple["Node", "Edge"]]:
|
|
99
|
+
"""``(neighbour, edge)`` pairs reachable from `node` through its outputs."""
|
|
100
|
+
steps: List[Tuple["Node", "Edge"]] = []
|
|
101
|
+
for socket in getattr(node, "outputs", []):
|
|
102
|
+
for edge in list(getattr(socket, "edges", [])):
|
|
103
|
+
# only follow the edge in the direction data flows
|
|
104
|
+
if edge.start_socket is not socket:
|
|
105
|
+
continue
|
|
106
|
+
target = edge.end_socket
|
|
107
|
+
if target is None:
|
|
108
|
+
continue
|
|
109
|
+
neighbour = target.node
|
|
110
|
+
if neighbour is None:
|
|
111
|
+
continue
|
|
112
|
+
steps.append((neighbour, edge))
|
|
113
|
+
return steps
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _upstream_steps(node: "Node") -> List[Tuple["Node", "Edge"]]:
|
|
117
|
+
"""``(neighbour, edge)`` pairs feeding `node` through its inputs."""
|
|
118
|
+
steps: List[Tuple["Node", "Edge"]] = []
|
|
119
|
+
for socket in getattr(node, "inputs", []):
|
|
120
|
+
for edge in list(getattr(socket, "edges", [])):
|
|
121
|
+
if edge.end_socket is not socket:
|
|
122
|
+
continue
|
|
123
|
+
source = edge.start_socket
|
|
124
|
+
if source is None:
|
|
125
|
+
continue
|
|
126
|
+
neighbour = source.node
|
|
127
|
+
if neighbour is None:
|
|
128
|
+
continue
|
|
129
|
+
steps.append((neighbour, edge))
|
|
130
|
+
return steps
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _successor_map(scene: "Scene") -> Dict["Node", List[Tuple["Node", "Edge"]]]:
|
|
134
|
+
"""``{node: [(successor, edge), ...]}`` for the data-flow graph."""
|
|
135
|
+
successors: Dict["Node", List[Tuple["Node", "Edge"]]] = {
|
|
136
|
+
node: [] for node in scene.nodes}
|
|
137
|
+
for edge in scene.edges:
|
|
138
|
+
source, target = _edge_endpoints(edge)
|
|
139
|
+
if source is None or target is None:
|
|
140
|
+
continue
|
|
141
|
+
if source not in successors or target not in successors:
|
|
142
|
+
continue
|
|
143
|
+
successors[source].append((target, edge))
|
|
144
|
+
return successors
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# ---------------------------------------------------------------------------
|
|
148
|
+
# reachability
|
|
149
|
+
# ---------------------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
def descendants(scene: "Scene", node: "Node") -> List["Node"]:
|
|
152
|
+
"""Every `Node` reachable downstream of `node`, excluding `node` itself.
|
|
153
|
+
|
|
154
|
+
The result is ordered breadth-first, so the nearest nodes come first. A
|
|
155
|
+
node reachable through several paths appears once.
|
|
156
|
+
|
|
157
|
+
:param scene: the :class:`~nodeeditor.node_scene.Scene` to walk
|
|
158
|
+
:param node: the `Node` to start from
|
|
159
|
+
:return: the downstream `Nodes`
|
|
160
|
+
:rtype: ``list`` of :class:`~nodeeditor.node_node.Node`
|
|
161
|
+
"""
|
|
162
|
+
if node is None:
|
|
163
|
+
return []
|
|
164
|
+
|
|
165
|
+
members = set(scene.nodes)
|
|
166
|
+
seen: Set["Node"] = {node}
|
|
167
|
+
order: List["Node"] = []
|
|
168
|
+
queue: Deque["Node"] = deque([node])
|
|
169
|
+
|
|
170
|
+
while queue:
|
|
171
|
+
current = queue.popleft()
|
|
172
|
+
for neighbour, _edge in _downstream_steps(current):
|
|
173
|
+
if neighbour in seen or neighbour not in members:
|
|
174
|
+
continue
|
|
175
|
+
seen.add(neighbour)
|
|
176
|
+
order.append(neighbour)
|
|
177
|
+
queue.append(neighbour)
|
|
178
|
+
|
|
179
|
+
return order
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def ancestors(scene: "Scene", node: "Node") -> List["Node"]:
|
|
183
|
+
"""Every `Node` feeding `node`, excluding `node` itself.
|
|
184
|
+
|
|
185
|
+
The result is ordered breadth-first, so the nearest nodes come first. A
|
|
186
|
+
node feeding through several paths appears once.
|
|
187
|
+
|
|
188
|
+
:param scene: the :class:`~nodeeditor.node_scene.Scene` to walk
|
|
189
|
+
:param node: the `Node` to start from
|
|
190
|
+
:return: the upstream `Nodes`
|
|
191
|
+
:rtype: ``list`` of :class:`~nodeeditor.node_node.Node`
|
|
192
|
+
"""
|
|
193
|
+
if node is None:
|
|
194
|
+
return []
|
|
195
|
+
|
|
196
|
+
members = set(scene.nodes)
|
|
197
|
+
seen: Set["Node"] = {node}
|
|
198
|
+
order: List["Node"] = []
|
|
199
|
+
queue: Deque["Node"] = deque([node])
|
|
200
|
+
|
|
201
|
+
while queue:
|
|
202
|
+
current = queue.popleft()
|
|
203
|
+
for neighbour, _edge in _upstream_steps(current):
|
|
204
|
+
if neighbour in seen or neighbour not in members:
|
|
205
|
+
continue
|
|
206
|
+
seen.add(neighbour)
|
|
207
|
+
order.append(neighbour)
|
|
208
|
+
queue.append(neighbour)
|
|
209
|
+
|
|
210
|
+
return order
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# ---------------------------------------------------------------------------
|
|
214
|
+
# components
|
|
215
|
+
# ---------------------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
def components(scene: "Scene") -> List[List["Node"]]:
|
|
218
|
+
"""The disconnected subgraphs of `scene`, the "flocks" of nodes.
|
|
219
|
+
|
|
220
|
+
Two `Nodes` belong to the same component when a chain of `Edges` connects
|
|
221
|
+
them, regardless of direction. A `Node` without any connection forms a
|
|
222
|
+
component of its own.
|
|
223
|
+
|
|
224
|
+
The components come in scene order, and the `Nodes` inside a component come
|
|
225
|
+
in scene order too, so the result is stable for a given graph.
|
|
226
|
+
|
|
227
|
+
:param scene: the :class:`~nodeeditor.node_scene.Scene` to inspect
|
|
228
|
+
:return: one list of `Nodes` per component
|
|
229
|
+
:rtype: ``list`` of ``list`` of :class:`~nodeeditor.node_node.Node`
|
|
230
|
+
"""
|
|
231
|
+
nodes = list(scene.nodes)
|
|
232
|
+
order_of = {node: index for index, node in enumerate(nodes)}
|
|
233
|
+
parent = {node: node for node in nodes}
|
|
234
|
+
|
|
235
|
+
def find(node: "Node") -> "Node":
|
|
236
|
+
root = node
|
|
237
|
+
while parent[root] is not root:
|
|
238
|
+
root = parent[root]
|
|
239
|
+
# path compression
|
|
240
|
+
while parent[node] is not root:
|
|
241
|
+
parent[node], node = root, parent[node]
|
|
242
|
+
return root
|
|
243
|
+
|
|
244
|
+
for edge in scene.edges:
|
|
245
|
+
source, target = _edge_endpoints(edge)
|
|
246
|
+
if source is None or target is None:
|
|
247
|
+
continue
|
|
248
|
+
if source not in parent or target not in parent:
|
|
249
|
+
continue
|
|
250
|
+
root_source = find(source)
|
|
251
|
+
root_target = find(target)
|
|
252
|
+
if root_source is root_target:
|
|
253
|
+
continue
|
|
254
|
+
# keep the earliest node of the component as its root, so the
|
|
255
|
+
# components come out in scene order
|
|
256
|
+
if order_of[root_source] <= order_of[root_target]:
|
|
257
|
+
parent[root_target] = root_source
|
|
258
|
+
else:
|
|
259
|
+
parent[root_source] = root_target
|
|
260
|
+
|
|
261
|
+
groups: Dict["Node", List["Node"]] = {}
|
|
262
|
+
for node in nodes:
|
|
263
|
+
groups.setdefault(find(node), []).append(node)
|
|
264
|
+
|
|
265
|
+
return list(groups.values())
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
# ---------------------------------------------------------------------------
|
|
269
|
+
# ordering
|
|
270
|
+
# ---------------------------------------------------------------------------
|
|
271
|
+
|
|
272
|
+
def topological_order(scene: "Scene") -> List["Node"]:
|
|
273
|
+
"""The `Nodes` of `scene` in dependency order.
|
|
274
|
+
|
|
275
|
+
Every `Node` comes after the `Nodes` feeding it, so a `Node` can be
|
|
276
|
+
evaluated once every input it needs has been evaluated. `Nodes` that do
|
|
277
|
+
not depend on each other keep their scene order, which makes the result
|
|
278
|
+
reproducible for a given graph.
|
|
279
|
+
|
|
280
|
+
:param scene: the :class:`~nodeeditor.node_scene.Scene` to order
|
|
281
|
+
:return: the `Nodes` in dependency order
|
|
282
|
+
:rtype: ``list`` of :class:`~nodeeditor.node_node.Node`
|
|
283
|
+
:raises :class:`GraphCycleError` if the graph contains a cycle
|
|
284
|
+
"""
|
|
285
|
+
nodes = list(scene.nodes)
|
|
286
|
+
order_of = {node: index for index, node in enumerate(nodes)}
|
|
287
|
+
successors: Dict["Node", List["Node"]] = {node: [] for node in nodes}
|
|
288
|
+
indegree: Dict["Node", int] = {node: 0 for node in nodes}
|
|
289
|
+
|
|
290
|
+
for edge in scene.edges:
|
|
291
|
+
source, target = _edge_endpoints(edge)
|
|
292
|
+
if source is None or target is None:
|
|
293
|
+
continue
|
|
294
|
+
if source not in successors or target not in successors:
|
|
295
|
+
continue
|
|
296
|
+
successors[source].append(target)
|
|
297
|
+
indegree[target] += 1
|
|
298
|
+
|
|
299
|
+
ready = [(order_of[node], node) for node in nodes if indegree[node] == 0]
|
|
300
|
+
heapq.heapify(ready)
|
|
301
|
+
|
|
302
|
+
result: List["Node"] = []
|
|
303
|
+
while ready:
|
|
304
|
+
_position, node = heapq.heappop(ready)
|
|
305
|
+
result.append(node)
|
|
306
|
+
for successor in successors[node]:
|
|
307
|
+
indegree[successor] -= 1
|
|
308
|
+
if indegree[successor] == 0:
|
|
309
|
+
heapq.heappush(ready, (order_of[successor], successor))
|
|
310
|
+
|
|
311
|
+
if len(result) != len(nodes):
|
|
312
|
+
# whatever is left sits on a cycle
|
|
313
|
+
raise GraphCycleError(cycles(scene))
|
|
314
|
+
|
|
315
|
+
return result
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
# ---------------------------------------------------------------------------
|
|
319
|
+
# cycles
|
|
320
|
+
# ---------------------------------------------------------------------------
|
|
321
|
+
|
|
322
|
+
def _strongly_connected_components(
|
|
323
|
+
nodes: Sequence["Node"],
|
|
324
|
+
successors: Dict["Node", List[Tuple["Node", "Edge"]]],
|
|
325
|
+
) -> List[List["Node"]]:
|
|
326
|
+
"""Kosaraju's algorithm, iterative.
|
|
327
|
+
|
|
328
|
+
:return: one list of `Nodes` per strongly connected component
|
|
329
|
+
"""
|
|
330
|
+
visited: Set["Node"] = set()
|
|
331
|
+
finish_order: List["Node"] = []
|
|
332
|
+
|
|
333
|
+
# first pass: finishing order over the forward graph
|
|
334
|
+
for root in nodes:
|
|
335
|
+
if root in visited:
|
|
336
|
+
continue
|
|
337
|
+
visited.add(root)
|
|
338
|
+
stack: List[Tuple["Node", StepIterator]] = [
|
|
339
|
+
(root, iter(successors.get(root, ())))]
|
|
340
|
+
while stack:
|
|
341
|
+
node, iterator = stack[-1]
|
|
342
|
+
advanced = False
|
|
343
|
+
for successor, _edge in iterator:
|
|
344
|
+
if successor in visited:
|
|
345
|
+
continue
|
|
346
|
+
visited.add(successor)
|
|
347
|
+
stack.append(
|
|
348
|
+
(successor, iter(successors.get(successor, ()))))
|
|
349
|
+
advanced = True
|
|
350
|
+
break
|
|
351
|
+
if not advanced:
|
|
352
|
+
stack.pop()
|
|
353
|
+
finish_order.append(node)
|
|
354
|
+
|
|
355
|
+
# second pass: components over the reversed graph
|
|
356
|
+
reverse: Dict["Node", List["Node"]] = {node: [] for node in nodes}
|
|
357
|
+
for node, pairs in successors.items():
|
|
358
|
+
for successor, _edge in pairs:
|
|
359
|
+
if successor in reverse:
|
|
360
|
+
reverse[successor].append(node)
|
|
361
|
+
|
|
362
|
+
assigned: Set["Node"] = set()
|
|
363
|
+
components_found: List[List["Node"]] = []
|
|
364
|
+
for root in reversed(finish_order):
|
|
365
|
+
if root in assigned:
|
|
366
|
+
continue
|
|
367
|
+
assigned.add(root)
|
|
368
|
+
stack = [root]
|
|
369
|
+
component: List["Node"] = []
|
|
370
|
+
while stack:
|
|
371
|
+
node = stack.pop()
|
|
372
|
+
component.append(node)
|
|
373
|
+
for predecessor in reverse.get(node, ()):
|
|
374
|
+
if predecessor in assigned:
|
|
375
|
+
continue
|
|
376
|
+
assigned.add(predecessor)
|
|
377
|
+
stack.append(predecessor)
|
|
378
|
+
components_found.append(component)
|
|
379
|
+
|
|
380
|
+
return components_found
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def _cycle_within(
|
|
384
|
+
component: Sequence["Node"],
|
|
385
|
+
successors: Dict["Node", List[Tuple["Node", "Edge"]]],
|
|
386
|
+
) -> List["Edge"]:
|
|
387
|
+
"""One cycle inside a strongly connected component, as a list of `Edges`.
|
|
388
|
+
|
|
389
|
+
The last `Edge` of the returned list leads back to the node the first
|
|
390
|
+
`Edge` started from. An empty list means the component holds no cycle.
|
|
391
|
+
"""
|
|
392
|
+
members = set(component)
|
|
393
|
+
if not component:
|
|
394
|
+
return []
|
|
395
|
+
|
|
396
|
+
if len(component) == 1:
|
|
397
|
+
only = component[0]
|
|
398
|
+
for successor, edge in successors.get(only, ()):
|
|
399
|
+
if successor is only:
|
|
400
|
+
return [edge]
|
|
401
|
+
return []
|
|
402
|
+
|
|
403
|
+
white, grey, black = 0, 1, 2
|
|
404
|
+
color: Dict["Node", int] = {node: white for node in component}
|
|
405
|
+
|
|
406
|
+
root = component[0]
|
|
407
|
+
color[root] = grey
|
|
408
|
+
path_nodes: List["Node"] = [root]
|
|
409
|
+
path_edges: List["Edge"] = []
|
|
410
|
+
stack: List[Tuple["Node", StepIterator]] = [
|
|
411
|
+
(root, iter(successors.get(root, ())))]
|
|
412
|
+
|
|
413
|
+
while stack:
|
|
414
|
+
node, iterator = stack[-1]
|
|
415
|
+
advanced = False
|
|
416
|
+
for successor, edge in iterator:
|
|
417
|
+
if successor not in members:
|
|
418
|
+
continue
|
|
419
|
+
state = color.get(successor, white)
|
|
420
|
+
if state == grey:
|
|
421
|
+
# back edge: the cycle closes on a node of the current path
|
|
422
|
+
index = path_nodes.index(successor)
|
|
423
|
+
return path_edges[index:] + [edge]
|
|
424
|
+
if state == white:
|
|
425
|
+
color[successor] = grey
|
|
426
|
+
path_nodes.append(successor)
|
|
427
|
+
path_edges.append(edge)
|
|
428
|
+
stack.append(
|
|
429
|
+
(successor, iter(successors.get(successor, ()))))
|
|
430
|
+
advanced = True
|
|
431
|
+
break
|
|
432
|
+
if not advanced:
|
|
433
|
+
stack.pop()
|
|
434
|
+
color[node] = black
|
|
435
|
+
if len(path_nodes) > 1:
|
|
436
|
+
path_nodes.pop()
|
|
437
|
+
path_edges.pop()
|
|
438
|
+
|
|
439
|
+
return []
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def cycles(scene: "Scene") -> List[List["Edge"]]:
|
|
443
|
+
"""The cycles of `scene`.
|
|
444
|
+
|
|
445
|
+
One representative cycle is returned per cyclic region, so a graph with
|
|
446
|
+
two separate loops yields two cycles. Within a cycle, the `Edges` are
|
|
447
|
+
listed in traversal order and the last one leads back to the node the
|
|
448
|
+
first one started from.
|
|
449
|
+
|
|
450
|
+
:param scene: the :class:`~nodeeditor.node_scene.Scene` to inspect
|
|
451
|
+
:return: the cycles, each a list of :class:`~nodeeditor.node_edge.Edge`
|
|
452
|
+
:rtype: ``list`` of ``list`` of :class:`~nodeeditor.node_edge.Edge`
|
|
453
|
+
"""
|
|
454
|
+
nodes = list(scene.nodes)
|
|
455
|
+
successors = _successor_map(scene)
|
|
456
|
+
|
|
457
|
+
found: List[List["Edge"]] = []
|
|
458
|
+
for component in _strongly_connected_components(nodes, successors):
|
|
459
|
+
cycle = _cycle_within(component, successors)
|
|
460
|
+
if cycle:
|
|
461
|
+
found.append(cycle)
|
|
462
|
+
|
|
463
|
+
return found
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
# ---------------------------------------------------------------------------
|
|
467
|
+
# paths
|
|
468
|
+
# ---------------------------------------------------------------------------
|
|
469
|
+
|
|
470
|
+
def paths(scene: "Scene", source: "Node", target: "Node") -> List[List[PathStep]]:
|
|
471
|
+
"""Every simple path from `source` to `target`.
|
|
472
|
+
|
|
473
|
+
A path is a list of ``(node, edge)`` steps. The `edge` of a step is the
|
|
474
|
+
`Edge` that led into its `Node`; the first step carries ``None`` because
|
|
475
|
+
nothing leads into the first node. Two `Edges` between the same pair of
|
|
476
|
+
`Nodes` yield two different paths, because each `Edge` is its own thing.
|
|
477
|
+
|
|
478
|
+
:param scene: the :class:`~nodeeditor.node_scene.Scene` to search
|
|
479
|
+
:param source: the `Node` to start from
|
|
480
|
+
:param target: the `Node` to reach
|
|
481
|
+
:return: the paths found
|
|
482
|
+
:rtype: ``list`` of ``list`` of ``(Node, Edge)``
|
|
483
|
+
"""
|
|
484
|
+
if source is None or target is None:
|
|
485
|
+
return []
|
|
486
|
+
if source is target:
|
|
487
|
+
return [[(source, None)]]
|
|
488
|
+
|
|
489
|
+
successors = _successor_map(scene)
|
|
490
|
+
members = set(scene.nodes)
|
|
491
|
+
if source not in members or target not in members:
|
|
492
|
+
return []
|
|
493
|
+
|
|
494
|
+
results: List[List[PathStep]] = []
|
|
495
|
+
visited: Set["Node"] = {source}
|
|
496
|
+
path_nodes: List["Node"] = [source]
|
|
497
|
+
path_edges: List[Optional["Edge"]] = [None]
|
|
498
|
+
stack: List[StepIterator] = [iter(successors.get(source, ()))]
|
|
499
|
+
|
|
500
|
+
while stack:
|
|
501
|
+
iterator = stack[-1]
|
|
502
|
+
advanced = False
|
|
503
|
+
for successor, edge in iterator:
|
|
504
|
+
if successor in visited:
|
|
505
|
+
continue
|
|
506
|
+
visited.add(successor)
|
|
507
|
+
path_nodes.append(successor)
|
|
508
|
+
path_edges.append(edge)
|
|
509
|
+
if successor is target:
|
|
510
|
+
results.append(list(zip(path_nodes, path_edges)))
|
|
511
|
+
visited.discard(successor)
|
|
512
|
+
path_nodes.pop()
|
|
513
|
+
path_edges.pop()
|
|
514
|
+
continue
|
|
515
|
+
stack.append(iter(successors.get(successor, ())))
|
|
516
|
+
advanced = True
|
|
517
|
+
break
|
|
518
|
+
if not advanced:
|
|
519
|
+
stack.pop()
|
|
520
|
+
if len(path_nodes) > 1:
|
|
521
|
+
visited.discard(path_nodes.pop())
|
|
522
|
+
path_edges.pop()
|
|
523
|
+
|
|
524
|
+
return results
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def shortest_path(
|
|
528
|
+
scene: "Scene", source: "Node", target: "Node") -> Optional[List[PathStep]]:
|
|
529
|
+
"""The shortest path from `source` to `target`, counted in `Edges`.
|
|
530
|
+
|
|
531
|
+
The path is a list of ``(node, edge)`` steps, the same shape
|
|
532
|
+
:func:`paths` returns. When several shortest paths exist, the one found
|
|
533
|
+
first is returned, which makes the result reproducible.
|
|
534
|
+
|
|
535
|
+
:param scene: the :class:`~nodeeditor.node_scene.Scene` to search
|
|
536
|
+
:param source: the `Node` to start from
|
|
537
|
+
:param target: the `Node` to reach
|
|
538
|
+
:return: the shortest path, or ``None`` when `target` is unreachable
|
|
539
|
+
:rtype: ``list`` of ``(Node, Edge)`` or ``None``
|
|
540
|
+
"""
|
|
541
|
+
if source is None or target is None:
|
|
542
|
+
return None
|
|
543
|
+
if source is target:
|
|
544
|
+
return [(source, None)]
|
|
545
|
+
|
|
546
|
+
successors = _successor_map(scene)
|
|
547
|
+
members = set(scene.nodes)
|
|
548
|
+
if source not in members or target not in members:
|
|
549
|
+
return None
|
|
550
|
+
|
|
551
|
+
previous: Dict["Node", Tuple["Node", "Edge"]] = {}
|
|
552
|
+
visited: Set["Node"] = {source}
|
|
553
|
+
queue: Deque["Node"] = deque([source])
|
|
554
|
+
|
|
555
|
+
while queue:
|
|
556
|
+
node = queue.popleft()
|
|
557
|
+
for successor, edge in successors.get(node, ()):
|
|
558
|
+
if successor in visited:
|
|
559
|
+
continue
|
|
560
|
+
visited.add(successor)
|
|
561
|
+
previous[successor] = (node, edge)
|
|
562
|
+
if successor is target:
|
|
563
|
+
return _rebuild_path(previous, source, target)
|
|
564
|
+
queue.append(successor)
|
|
565
|
+
|
|
566
|
+
return None
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def _rebuild_path(
|
|
570
|
+
previous: Dict["Node", Tuple["Node", "Edge"]],
|
|
571
|
+
source: "Node",
|
|
572
|
+
target: "Node",
|
|
573
|
+
) -> List[PathStep]:
|
|
574
|
+
"""Turn a BFS predecessor map into a list of ``(node, edge)`` steps."""
|
|
575
|
+
steps: List[PathStep] = []
|
|
576
|
+
node = target
|
|
577
|
+
while node is not source:
|
|
578
|
+
predecessor, edge = previous[node]
|
|
579
|
+
steps.append((node, edge))
|
|
580
|
+
node = predecessor
|
|
581
|
+
steps.append((source, None))
|
|
582
|
+
steps.reverse()
|
|
583
|
+
return steps
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
A module containing the class for Cutting Line
|
|
4
|
+
"""
|
|
5
|
+
from qtpy.QtGui import QPen, QPainterPath, QPolygonF, QPainter
|
|
6
|
+
from qtpy.QtWidgets import QGraphicsItem, QWidget
|
|
7
|
+
from qtpy.QtCore import Qt, QRectF, QPointF
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class QDMCutLine(QGraphicsItem):
|
|
11
|
+
"""Class representing Cutting Line used for cutting multiple `Edges` with one stroke"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, parent: QGraphicsItem = None) -> None:
|
|
14
|
+
"""
|
|
15
|
+
:param parent: parent widget
|
|
16
|
+
:type parent: ``QWidget``
|
|
17
|
+
"""
|
|
18
|
+
super().__init__(parent)
|
|
19
|
+
|
|
20
|
+
self.line_points: list = []
|
|
21
|
+
|
|
22
|
+
self._pen = QPen(Qt.GlobalColor.white)
|
|
23
|
+
self._pen.setWidthF(2.0)
|
|
24
|
+
self._pen.setDashPattern([3, 3])
|
|
25
|
+
|
|
26
|
+
self.setZValue(2)
|
|
27
|
+
|
|
28
|
+
def boundingRect(self) -> QRectF:
|
|
29
|
+
"""Defining Qt' bounding rectangle"""
|
|
30
|
+
return self.shape().boundingRect()
|
|
31
|
+
|
|
32
|
+
def shape(self) -> QPainterPath:
|
|
33
|
+
"""Calculate the QPainterPath object from list of line points
|
|
34
|
+
|
|
35
|
+
:return: shape function returning ``QPainterPath`` representation of Cutting Line
|
|
36
|
+
:rtype: ``QPainterPath``
|
|
37
|
+
"""
|
|
38
|
+
poly = QPolygonF(self.line_points)
|
|
39
|
+
|
|
40
|
+
if len(self.line_points) > 1:
|
|
41
|
+
path = QPainterPath(self.line_points[0])
|
|
42
|
+
for pt in self.line_points[1:]:
|
|
43
|
+
path.lineTo(pt)
|
|
44
|
+
else:
|
|
45
|
+
path = QPainterPath(QPointF(0, 0))
|
|
46
|
+
path.lineTo(QPointF(1, 1))
|
|
47
|
+
|
|
48
|
+
return path
|
|
49
|
+
|
|
50
|
+
def paint(self, painter, QStyleOptionGraphicsItem, widget=None) -> None:
|
|
51
|
+
"""Paint the Cutting Line"""
|
|
52
|
+
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
|
53
|
+
painter.setBrush(Qt.BrushStyle.NoBrush)
|
|
54
|
+
painter.setPen(self._pen)
|
|
55
|
+
|
|
56
|
+
poly = QPolygonF(self.line_points)
|
|
57
|
+
painter.drawPolyline(poly)
|