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.
Files changed (40) hide show
  1. nodeeditor/__init__.py +93 -0
  2. nodeeditor/node_colors_config.py +343 -0
  3. nodeeditor/node_content_widget.py +129 -0
  4. nodeeditor/node_decorators.py +65 -0
  5. nodeeditor/node_edge.py +518 -0
  6. nodeeditor/node_edge_dragging.py +185 -0
  7. nodeeditor/node_edge_intersect.py +219 -0
  8. nodeeditor/node_edge_rerouting.py +216 -0
  9. nodeeditor/node_edge_snapping.py +95 -0
  10. nodeeditor/node_edge_validators.py +82 -0
  11. nodeeditor/node_editor_widget.py +257 -0
  12. nodeeditor/node_editor_window.py +448 -0
  13. nodeeditor/node_graph.py +583 -0
  14. nodeeditor/node_graphics_cutline.py +57 -0
  15. nodeeditor/node_graphics_edge.py +375 -0
  16. nodeeditor/node_graphics_edge_path.py +226 -0
  17. nodeeditor/node_graphics_node.py +693 -0
  18. nodeeditor/node_graphics_scene.py +369 -0
  19. nodeeditor/node_graphics_socket.py +262 -0
  20. nodeeditor/node_graphics_view.py +907 -0
  21. nodeeditor/node_group.py +1286 -0
  22. nodeeditor/node_group_node.py +5 -0
  23. nodeeditor/node_icon_content_widget.py +176 -0
  24. nodeeditor/node_icon_graphics_node.py +524 -0
  25. nodeeditor/node_multi_input_node.py +450 -0
  26. nodeeditor/node_node.py +848 -0
  27. nodeeditor/node_scene.py +1106 -0
  28. nodeeditor/node_scene_clipboard.py +314 -0
  29. nodeeditor/node_scene_history.py +556 -0
  30. nodeeditor/node_serializable.py +43 -0
  31. nodeeditor/node_socket.py +445 -0
  32. nodeeditor/node_socket_shapes.py +261 -0
  33. nodeeditor/py.typed +0 -0
  34. nodeeditor/qss/nodestyle.qss +3 -0
  35. nodeeditor/utils.py +108 -0
  36. nodeeditor/utils_no_qt.py +21 -0
  37. nodeeditor_qtpy-0.8.1.dist-info/METADATA +165 -0
  38. nodeeditor_qtpy-0.8.1.dist-info/RECORD +40 -0
  39. nodeeditor_qtpy-0.8.1.dist-info/WHEEL +4 -0
  40. nodeeditor_qtpy-0.8.1.dist-info/licenses/LICENCE +23 -0
@@ -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)