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
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
A module containing the intersecting nodes functionality. If a node gets dragged and dropped on an existing edge
|
|
4
|
+
it will intersect that edge.
|
|
5
|
+
"""
|
|
6
|
+
from qtpy.QtWidgets import QGraphicsView, QGraphicsItem
|
|
7
|
+
from qtpy.QtCore import QRectF, QPointF
|
|
8
|
+
from nodeeditor.node_edge import Edge
|
|
9
|
+
from nodeeditor.node_graphics_edge import QDMGraphicsEdge
|
|
10
|
+
|
|
11
|
+
from typing import TYPE_CHECKING, Union, List
|
|
12
|
+
|
|
13
|
+
from nodeeditor.node_socket import Socket
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from nodeeditor.node_graphics_view import QDMGraphicsView
|
|
18
|
+
from nodeeditor.node_node import Node
|
|
19
|
+
from nodeeditor.node_graphics_scene import QDMGraphicsScene
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class EdgeIntersect:
|
|
23
|
+
"""Class handling the intersection of a dragged node with an existing edge"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, grView: 'QDMGraphicsView') -> None:
|
|
26
|
+
"""
|
|
27
|
+
:param grView: reference to the :class:`~nodeeditor.node_graphics_view.QDMGraphicsView`
|
|
28
|
+
:type grView: :class:`~nodeeditor.node_graphics_view.QDMGraphicsView`
|
|
29
|
+
|
|
30
|
+
:Instance Attributes:
|
|
31
|
+
|
|
32
|
+
- **grView** - reference to the :class:`~nodeeditor.node_graphics_view.QDMGraphicsView`
|
|
33
|
+
- **grScene** - reference to the :class:`~nodeeditor.node_graphics_scene.QDMGraphicsScene`
|
|
34
|
+
- **draggedNode** - the :class:`~nodeeditor.node_node.Node` currently being dragged
|
|
35
|
+
- **hoveredList** - graphics items hovered by the dragged node
|
|
36
|
+
"""
|
|
37
|
+
self.grView = grView
|
|
38
|
+
self.grScene: 'QDMGraphicsScene' = grView.grScene
|
|
39
|
+
self.draggedNode: Node | None = None
|
|
40
|
+
self.hoveredList: List[Union['QDMGraphicsEdge',
|
|
41
|
+
'QGraphicsItem', 'QGraphicsView', 'Socket', 'Node']] = []
|
|
42
|
+
|
|
43
|
+
def enterState(self, node: 'Node') -> None:
|
|
44
|
+
"""
|
|
45
|
+
Initialize when we enter the state
|
|
46
|
+
|
|
47
|
+
:param node: :class:`~nodeeditor.node_node.Node` which we started to drag
|
|
48
|
+
:type node: :class:`~nodeeditor.node_node.Node`
|
|
49
|
+
"""
|
|
50
|
+
self.hoveredList = []
|
|
51
|
+
self.draggedNode = node
|
|
52
|
+
|
|
53
|
+
def leaveState(self, scene_pos_x: float, scene_pos_y: float) -> None:
|
|
54
|
+
"""
|
|
55
|
+
Deinit when we leave this state
|
|
56
|
+
|
|
57
|
+
:param scene_pos_x: scene position x
|
|
58
|
+
:type scene_pos_x: `float`
|
|
59
|
+
:param scene_pos_y: scene position y
|
|
60
|
+
:type scene_pos_y: `float`
|
|
61
|
+
"""
|
|
62
|
+
if self.draggedNode is None:
|
|
63
|
+
print(">>> Warning: self.draggedNode is None")
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
self.dropNode(self.draggedNode, scene_pos_x, scene_pos_y)
|
|
67
|
+
self.draggedNode = None
|
|
68
|
+
self.hoveredList = []
|
|
69
|
+
|
|
70
|
+
def dropNode(self, node: "Node", scene_pos_x: float, scene_pos_y: float) -> None:
|
|
71
|
+
"""
|
|
72
|
+
Code handling the dropping of a node on an existing edge.
|
|
73
|
+
|
|
74
|
+
:param scene_pos_x: scene position x
|
|
75
|
+
:type scene_pos_x: `float`
|
|
76
|
+
:param scene_pos_y: scene position y
|
|
77
|
+
:type scene_pos_y: `float`
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
node_box = self.hotZoneRect(node)
|
|
81
|
+
|
|
82
|
+
# check if the node is dropped on an existing edge
|
|
83
|
+
edge = self.intersect(node_box)
|
|
84
|
+
if edge is None:
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
if self.isConnected(node):
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
# determine the order of start and end
|
|
91
|
+
if edge.start_socket.is_output:
|
|
92
|
+
socket_start = edge.start_socket
|
|
93
|
+
socket_end = edge.end_socket
|
|
94
|
+
else:
|
|
95
|
+
socket_start = edge.end_socket
|
|
96
|
+
socket_end = edge.start_socket
|
|
97
|
+
|
|
98
|
+
new_node_socket_in = node.inputs[0]
|
|
99
|
+
new_node_socket_out = node.outputs[0]
|
|
100
|
+
|
|
101
|
+
# Validate like manual drag does before touching the original edge.
|
|
102
|
+
if not Edge.validateEdge(socket_start, new_node_socket_in):
|
|
103
|
+
return
|
|
104
|
+
if not Edge.validateEdge(new_node_socket_out, socket_end):
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
# Use the Scene's Edge class so subclasses survive splits
|
|
108
|
+
# (hardcoded Edge() would silently downgrade them).
|
|
109
|
+
edge_class = self.grScene.scene.getEdgeClass()
|
|
110
|
+
|
|
111
|
+
# The new edges will have the same edge_type as the intersected edge
|
|
112
|
+
edge_type = edge.edge_type
|
|
113
|
+
edge.remove()
|
|
114
|
+
self.grView.grScene.scene.history.storeHistory(
|
|
115
|
+
'Delete existing edge', setModified=True)
|
|
116
|
+
|
|
117
|
+
e1 = edge_class(self.grScene.scene, socket_start,
|
|
118
|
+
new_node_socket_in, edge_type=edge_type)
|
|
119
|
+
e2 = edge_class(self.grScene.scene, new_node_socket_out,
|
|
120
|
+
socket_end, edge_type=edge_type)
|
|
121
|
+
|
|
122
|
+
# Notify exactly like manual drag (EdgeDragging.edgeDragEnd) so nodes
|
|
123
|
+
# relying on onEdgeConnectionChanged/onInputChanged (dirty marking,
|
|
124
|
+
# re-eval, cached input data) behave identically for auto-splits.
|
|
125
|
+
for socket, new_edge in ((socket_start, e1), (new_node_socket_in, e1),
|
|
126
|
+
(new_node_socket_out, e2), (socket_end, e2)):
|
|
127
|
+
try:
|
|
128
|
+
socket.node.onEdgeConnectionChanged(new_edge)
|
|
129
|
+
if socket.is_input:
|
|
130
|
+
socket.node.onInputChanged(socket)
|
|
131
|
+
except Exception:
|
|
132
|
+
continue
|
|
133
|
+
|
|
134
|
+
self.grView.grScene.scene.history.storeHistory(
|
|
135
|
+
'Created new edges by dropping node', setModified=True)
|
|
136
|
+
|
|
137
|
+
def hotZoneRect(self, node: 'Node') -> 'QRectF':
|
|
138
|
+
"""
|
|
139
|
+
Returns A QRectF of creating a box around a node
|
|
140
|
+
|
|
141
|
+
:param node: :class:`~nodeeditor.node_node.Node` for which we want to get `QRectF` describing its position and area
|
|
142
|
+
:type node: :class:`~nodeeditor.node_node.Node`
|
|
143
|
+
:return: `QRectF` describing node's position and area
|
|
144
|
+
:rtype: `QRectF`
|
|
145
|
+
"""
|
|
146
|
+
if node.grNode is None:
|
|
147
|
+
print(">>> Warning: node is None")
|
|
148
|
+
return QRectF()
|
|
149
|
+
|
|
150
|
+
nodePos: QPointF = node.grNode.scenePos()
|
|
151
|
+
x = nodePos.x()
|
|
152
|
+
y = nodePos.y()
|
|
153
|
+
w = node.grNode.width
|
|
154
|
+
h = node.grNode.height
|
|
155
|
+
return QRectF(x, y, w, h)
|
|
156
|
+
|
|
157
|
+
def update(self, scene_pos_x: float, scene_pos_y: float) -> None:
|
|
158
|
+
"""
|
|
159
|
+
Updating during mouse move when grView is in this state
|
|
160
|
+
|
|
161
|
+
:param scene_pos_x: scene position x
|
|
162
|
+
:type scene_pos_x: `float`
|
|
163
|
+
:param scene_pos_y: scene position y
|
|
164
|
+
:type scene_pos_y: `float`
|
|
165
|
+
"""
|
|
166
|
+
if self.draggedNode is None:
|
|
167
|
+
print(">>> Warning: self.draggedNode is None")
|
|
168
|
+
return
|
|
169
|
+
|
|
170
|
+
rect = self.hotZoneRect(self.draggedNode)
|
|
171
|
+
grItems = self.grScene.items(rect)
|
|
172
|
+
|
|
173
|
+
for grEdge in self.hoveredList:
|
|
174
|
+
if isinstance(grEdge, QDMGraphicsEdge):
|
|
175
|
+
grEdge.hovered = False
|
|
176
|
+
|
|
177
|
+
self.hoveredList = []
|
|
178
|
+
|
|
179
|
+
for grItem in grItems:
|
|
180
|
+
if hasattr(grItem, 'edge') and not self.draggedNode.hasConnectedEdge(grItem.edge):
|
|
181
|
+
self.hoveredList.append(grItem)
|
|
182
|
+
if hasattr(grItem, 'hovered'):
|
|
183
|
+
grItem.hovered = True
|
|
184
|
+
|
|
185
|
+
def intersect(self, node_box: 'QRectF') -> Union['Edge', None]:
|
|
186
|
+
"""
|
|
187
|
+
Checking for intersection of a rectangle (usually a `Node`) with edges in the scene
|
|
188
|
+
|
|
189
|
+
:param node_box: `QRectF` for which we want find intersecting `Edges`
|
|
190
|
+
:type node_box: `QRectF`
|
|
191
|
+
:return: :class:`~nodeeditor.node_edge.Edge` or `None` if the node is being cut by an `Edge`
|
|
192
|
+
:rtype: :class:`~nodeeditor.node_edge.Edge`
|
|
193
|
+
"""
|
|
194
|
+
|
|
195
|
+
if self.draggedNode is None:
|
|
196
|
+
print(">>> Warning: self.draggedNode is None")
|
|
197
|
+
return None
|
|
198
|
+
|
|
199
|
+
# returns the first edge that intersects with the dropped node, ignores the rest
|
|
200
|
+
grItems = self.grScene.items(node_box)
|
|
201
|
+
for grItem in grItems:
|
|
202
|
+
if hasattr(grItem, 'edge') and not self.draggedNode.hasConnectedEdge(grItem.edge):
|
|
203
|
+
return grItem.edge
|
|
204
|
+
return None
|
|
205
|
+
|
|
206
|
+
def isConnected(self, node: 'Node'):
|
|
207
|
+
"""
|
|
208
|
+
Return ``True`` if node got any connections
|
|
209
|
+
|
|
210
|
+
:param node: :class:`~nodeeditor.node_node.Node` which connections to check
|
|
211
|
+
:type node: :class:`~nodeeditor.node_node.Node`
|
|
212
|
+
:return:
|
|
213
|
+
"""
|
|
214
|
+
# Nodes with only inputs or outputs are excluded
|
|
215
|
+
if node.inputs == [] or node.outputs == []:
|
|
216
|
+
return True
|
|
217
|
+
|
|
218
|
+
# Check if the node has edges connected
|
|
219
|
+
return node.getInput() or node.getOutputs()
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
A module containing the Edge Rerouting functionality
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
from typing import TYPE_CHECKING, List, Optional, Tuple, Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from nodeeditor.node_graphics_view import QDMGraphicsView
|
|
12
|
+
from nodeeditor.node_edge import Edge
|
|
13
|
+
from nodeeditor.node_socket import Socket
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
DEBUG_REROUTING = True
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class EdgeRerouting:
|
|
20
|
+
"""Class handling the re-routing of existing edges to another socket"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, grView: 'QDMGraphicsView') -> None:
|
|
23
|
+
"""
|
|
24
|
+
:param grView: reference to the :class:`~nodeeditor.node_graphics_view.QDMGraphicsView`
|
|
25
|
+
:type grView: :class:`~nodeeditor.node_graphics_view.QDMGraphicsView`
|
|
26
|
+
|
|
27
|
+
:Instance Attributes:
|
|
28
|
+
|
|
29
|
+
- **grView** - reference to the :class:`~nodeeditor.node_graphics_view.QDMGraphicsView`
|
|
30
|
+
- **start_socket** - the :class:`~nodeeditor.node_socket.Socket` where the re-routing started
|
|
31
|
+
- **rerouting_edges** - dashed edges representing the re-routing in progress
|
|
32
|
+
- **is_rerouting** - are we currently re-routing?
|
|
33
|
+
- **first_mb_release** - flag for detecting if we already clicked with the rerouting LMB release
|
|
34
|
+
"""
|
|
35
|
+
self.grView = grView
|
|
36
|
+
# store where we started re-routing the edges
|
|
37
|
+
self.start_socket: Optional['Socket'] = None
|
|
38
|
+
# edges representing the re-routing (dashed edges)
|
|
39
|
+
self.rerouting_edges: List['Edge'] = []
|
|
40
|
+
self.is_rerouting: bool = False # are we currently re-routing?
|
|
41
|
+
# flag for detecting if we already clicked with the rerouting LMB release
|
|
42
|
+
self.first_mb_release: bool = False
|
|
43
|
+
|
|
44
|
+
def print(self, *args) -> None:
|
|
45
|
+
"""Helper function to better control debug printing to console for this feature"""
|
|
46
|
+
if DEBUG_REROUTING:
|
|
47
|
+
print("REROUTING:", *args)
|
|
48
|
+
|
|
49
|
+
def getEdgeClass(self):
|
|
50
|
+
"""Helper function to get the Edge class. Using what the Scene class provides"""
|
|
51
|
+
return self.grView.grScene.scene.getEdgeClass()
|
|
52
|
+
|
|
53
|
+
def getAffectedEdges(self) -> list:
|
|
54
|
+
"""
|
|
55
|
+
Get a list of all edges connected to the `self.start_socket` where we started the re-routing
|
|
56
|
+
|
|
57
|
+
:return: List of all edges affected by the rerouting started from this `self.start_socket` :class:`~nodeeditor.node_socket.Socket`
|
|
58
|
+
:rtype: ``list``
|
|
59
|
+
"""
|
|
60
|
+
if self.start_socket is None:
|
|
61
|
+
return [] # no starting socket assigned, so no edges for us
|
|
62
|
+
# return edges connected to the socket
|
|
63
|
+
return self.start_socket.edges.copy()
|
|
64
|
+
|
|
65
|
+
def setAffectedEdgesVisible(self, visibility: bool = True) -> None:
|
|
66
|
+
"""
|
|
67
|
+
Show/Hide all edges connected to the `self.start_socket` where we started the re-routing
|
|
68
|
+
|
|
69
|
+
:param visibility: ``True`` if all the affected :class:`~nodeeditor.node_edge.Edge` (s) should be shown or hidden
|
|
70
|
+
:type visibility: ``bool``
|
|
71
|
+
"""
|
|
72
|
+
for edge in self.getAffectedEdges():
|
|
73
|
+
if visibility:
|
|
74
|
+
edge.grEdge.show()
|
|
75
|
+
else:
|
|
76
|
+
edge.grEdge.hide()
|
|
77
|
+
|
|
78
|
+
def resetRerouting(self) -> None:
|
|
79
|
+
"""Reset to default state. Init this feature internal variables"""
|
|
80
|
+
self.is_rerouting = False
|
|
81
|
+
self.start_socket = None
|
|
82
|
+
self.first_mb_release = False
|
|
83
|
+
# holding all rerouting edges should be empty at this point...
|
|
84
|
+
# self.rerouting_edges = []
|
|
85
|
+
|
|
86
|
+
def clearReroutingEdges(self) -> None:
|
|
87
|
+
"""Remove the helping dashed edges from the :class:`~nodeeditor.node_scene.Scene`"""
|
|
88
|
+
self.print("clean called")
|
|
89
|
+
while self.rerouting_edges != []:
|
|
90
|
+
edge = self.rerouting_edges.pop()
|
|
91
|
+
self.print("\twant to clean:", edge)
|
|
92
|
+
edge.remove()
|
|
93
|
+
|
|
94
|
+
def updateScenePos(self, x: float, y: float) -> None:
|
|
95
|
+
"""
|
|
96
|
+
Update position of all the rerouting edges (dashed ones). Called from mouseMove event to update to new mouse position
|
|
97
|
+
|
|
98
|
+
:param x: new X position
|
|
99
|
+
:type x: ``float``
|
|
100
|
+
:param y: new Y position
|
|
101
|
+
:type y: ``float``
|
|
102
|
+
"""
|
|
103
|
+
if self.is_rerouting:
|
|
104
|
+
for edge in self.rerouting_edges:
|
|
105
|
+
if edge and edge.grEdge:
|
|
106
|
+
edge.grEdge.setDestination(x, y)
|
|
107
|
+
edge.grEdge.update()
|
|
108
|
+
|
|
109
|
+
def startRerouting(self, socket: 'Socket') -> None:
|
|
110
|
+
"""
|
|
111
|
+
Method to start the re-routing. Called from the grView's state machine.
|
|
112
|
+
|
|
113
|
+
:param socket: :class:`~nodeeditor.node_socket.Socket` where we started the re-routing
|
|
114
|
+
:type socket: :class:`~nodeeditor.node_socket.Socket`
|
|
115
|
+
"""
|
|
116
|
+
self.print("startRerouting", socket)
|
|
117
|
+
self.is_rerouting = True
|
|
118
|
+
self.start_socket = socket
|
|
119
|
+
|
|
120
|
+
self.print("numEdges:", len(self.getAffectedEdges()))
|
|
121
|
+
self.setAffectedEdgesVisible(visibility=False)
|
|
122
|
+
|
|
123
|
+
start_position = self.start_socket.node.getSocketScenePosition(
|
|
124
|
+
self.start_socket)
|
|
125
|
+
|
|
126
|
+
for edge in self.getAffectedEdges():
|
|
127
|
+
other_socket = edge.getOtherSocket(self.start_socket)
|
|
128
|
+
|
|
129
|
+
new_edge = self.getEdgeClass()(self.start_socket.node.scene,
|
|
130
|
+
edge_type=edge.edge_type)
|
|
131
|
+
new_edge.start_socket = other_socket
|
|
132
|
+
new_edge.grEdge.setSource(
|
|
133
|
+
*other_socket.node.getSocketScenePosition(other_socket))
|
|
134
|
+
new_edge.grEdge.setDestination(*start_position)
|
|
135
|
+
new_edge.grEdge.update()
|
|
136
|
+
self.rerouting_edges.append(new_edge)
|
|
137
|
+
|
|
138
|
+
def stopRerouting(self, target: 'Socket' = None) -> None:
|
|
139
|
+
"""
|
|
140
|
+
Method for stopping the re-routing
|
|
141
|
+
|
|
142
|
+
:param target: Target where we ended the rerouting (usually released mouse button). Provide ``Socket`` or ``None`` to cancel
|
|
143
|
+
:type target: :class:`~nodeeditor.node_socket.Socket` or ``None``
|
|
144
|
+
"""
|
|
145
|
+
self.print("stopRerouting on:", target,
|
|
146
|
+
"no change" if target == self.start_socket else "")
|
|
147
|
+
|
|
148
|
+
if self.start_socket is None:
|
|
149
|
+
return
|
|
150
|
+
|
|
151
|
+
# reset start socket highlight
|
|
152
|
+
self.start_socket.grSocket.isHighlighted = False
|
|
153
|
+
|
|
154
|
+
# collect all affected (node, edge) tuples in the meantime.. if necessary
|
|
155
|
+
affected_nodes = []
|
|
156
|
+
|
|
157
|
+
if target is None or target == self.start_socket:
|
|
158
|
+
# canceling -> no change
|
|
159
|
+
self.setAffectedEdgesVisible(visibility=True)
|
|
160
|
+
|
|
161
|
+
else:
|
|
162
|
+
# validate edges before doing anything else
|
|
163
|
+
valid_edges, invalid_edges = self.getAffectedEdges(), []
|
|
164
|
+
for edge in self.getAffectedEdges():
|
|
165
|
+
start_sock = edge.getOtherSocket(self.start_socket)
|
|
166
|
+
if not edge.validateEdge(start_sock, target):
|
|
167
|
+
# not valid edge
|
|
168
|
+
self.print("This edge rerouting is not valid!", edge)
|
|
169
|
+
invalid_edges.append(edge)
|
|
170
|
+
|
|
171
|
+
# remove the invalidated edges from the list
|
|
172
|
+
for invalid_edge in invalid_edges:
|
|
173
|
+
valid_edges.remove(invalid_edge)
|
|
174
|
+
|
|
175
|
+
# reconnect to new socket
|
|
176
|
+
self.print("should reconnect from:",
|
|
177
|
+
self.start_socket, "-->", target)
|
|
178
|
+
|
|
179
|
+
self.setAffectedEdgesVisible(visibility=True)
|
|
180
|
+
|
|
181
|
+
for edge in valid_edges:
|
|
182
|
+
for node in [edge.start_socket.node, edge.end_socket.node]:
|
|
183
|
+
if node not in affected_nodes:
|
|
184
|
+
affected_nodes.append((node, edge))
|
|
185
|
+
|
|
186
|
+
if target.is_input and not target.is_multi_edges:
|
|
187
|
+
# single-edged input: the new edge replaces the old one.
|
|
188
|
+
# Multi-edged inputs keep every other edge, and each moved
|
|
189
|
+
# edge lands at the read position it had on the old socket.
|
|
190
|
+
target.removeAllEdges(silent=True)
|
|
191
|
+
|
|
192
|
+
if edge.end_socket == self.start_socket:
|
|
193
|
+
edge.end_socket = target
|
|
194
|
+
else:
|
|
195
|
+
edge.start_socket = target
|
|
196
|
+
|
|
197
|
+
edge.updatePositions()
|
|
198
|
+
|
|
199
|
+
# hide rerouting edges
|
|
200
|
+
self.clearReroutingEdges()
|
|
201
|
+
|
|
202
|
+
# Send notifications for all affected nodes
|
|
203
|
+
for affected_node, edge in affected_nodes:
|
|
204
|
+
affected_node.onEdgeConnectionChanged(edge)
|
|
205
|
+
if edge.start_socket in affected_node.inputs:
|
|
206
|
+
affected_node.onInputChanged(edge.start_socket)
|
|
207
|
+
if edge.end_socket in affected_node.inputs:
|
|
208
|
+
affected_node.onInputChanged(edge.end_socket)
|
|
209
|
+
|
|
210
|
+
# store history stamp only if we have a valid start socket
|
|
211
|
+
if self.start_socket and self.start_socket.node:
|
|
212
|
+
self.start_socket.node.scene.history.storeHistory(
|
|
213
|
+
"Rerouted edges", setModified=True)
|
|
214
|
+
|
|
215
|
+
# reset variables of this rerouting state
|
|
216
|
+
self.resetRerouting()
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
A module containing the Edge Snapping functions which are used in :class:`~nodeeditor.node_graphics_view.QDMGraphicsView` class.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
from qtpy.QtCore import QPointF, QRectF
|
|
8
|
+
from qtpy.QtGui import QMouseEvent
|
|
9
|
+
|
|
10
|
+
from nodeeditor.node_graphics_socket import QDMGraphicsSocket
|
|
11
|
+
|
|
12
|
+
from typing import TYPE_CHECKING, List, Optional, Tuple, Any
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from nodeeditor.node_graphics_view import QDMGraphicsView
|
|
17
|
+
from nodeeditor.node_edge import Edge
|
|
18
|
+
from nodeeditor.node_socket import Socket
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class EdgeSnapping():
|
|
22
|
+
"""Class handling the snapping of a dragged edge to a nearby socket"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, grView: 'QDMGraphicsView', snapping_radius: float = 24) -> None:
|
|
25
|
+
"""
|
|
26
|
+
:param grView: reference to the :class:`~nodeeditor.node_graphics_view.QDMGraphicsView`
|
|
27
|
+
:type grView: :class:`~nodeeditor.node_graphics_view.QDMGraphicsView`
|
|
28
|
+
:param snapping_radius: radius in scene units within which sockets are considered for snapping
|
|
29
|
+
:type snapping_radius: ``float``
|
|
30
|
+
|
|
31
|
+
:Instance Attributes:
|
|
32
|
+
|
|
33
|
+
- **grView** - reference to the :class:`~nodeeditor.node_graphics_view.QDMGraphicsView`
|
|
34
|
+
- **grScene** - reference to the :class:`~nodeeditor.node_graphics_scene.QDMGraphicsScene`
|
|
35
|
+
- **edge_snapping_radius** - radius in scene units within which sockets are considered for snapping
|
|
36
|
+
"""
|
|
37
|
+
self.grView = grView
|
|
38
|
+
self.grScene = self.grView.grScene
|
|
39
|
+
self.edge_snapping_radius = snapping_radius
|
|
40
|
+
|
|
41
|
+
def getSnappedSocketItem(self, event: 'QMouseEvent') -> Optional['QDMGraphicsSocket']:
|
|
42
|
+
"""Returns :class:`~nodeeditor.node_graphics_socket.QDMGraphicsSocket` which we should snap to"""
|
|
43
|
+
scenepos = self.grView.mapToScene(event.pos())
|
|
44
|
+
grSocket, pos = self.getSnappedToSocketPosition(scenepos)
|
|
45
|
+
|
|
46
|
+
if grSocket is None:
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
return grSocket
|
|
50
|
+
|
|
51
|
+
def getSnappedToSocketPosition(self, scenepos: QPointF) -> Tuple[Optional['QDMGraphicsSocket'], QPointF]:
|
|
52
|
+
"""
|
|
53
|
+
Returns grSocket and Scene position to nearest Socket or original position if no nearby Socket found
|
|
54
|
+
|
|
55
|
+
:param scenepos: From which point should I snap?
|
|
56
|
+
:type scenepos: ``QPointF``
|
|
57
|
+
:return: grSocket and Scene postion to nearest socket
|
|
58
|
+
"""
|
|
59
|
+
scanrect = QRectF(
|
|
60
|
+
scenepos.x() - self.edge_snapping_radius,
|
|
61
|
+
scenepos.y() - self.edge_snapping_radius,
|
|
62
|
+
self.edge_snapping_radius * 2,
|
|
63
|
+
self.edge_snapping_radius * 2
|
|
64
|
+
)
|
|
65
|
+
items = self.grScene.items(scanrect)
|
|
66
|
+
socket_items = list(
|
|
67
|
+
filter(lambda x: isinstance(x, QDMGraphicsSocket), items))
|
|
68
|
+
|
|
69
|
+
if not socket_items:
|
|
70
|
+
return None, scenepos
|
|
71
|
+
|
|
72
|
+
selected_item = socket_items[0]
|
|
73
|
+
if len(socket_items) > 1:
|
|
74
|
+
# calculate the nearest socket
|
|
75
|
+
nearest = float('inf')
|
|
76
|
+
for grsock in socket_items:
|
|
77
|
+
if not isinstance(grsock, QDMGraphicsSocket) or not grsock.socket or not grsock.socket.node:
|
|
78
|
+
continue
|
|
79
|
+
|
|
80
|
+
grsock_scenepos = grsock.socket.node.getSocketScenePosition(
|
|
81
|
+
grsock.socket)
|
|
82
|
+
qpdist = QPointF(*grsock_scenepos) - scenepos
|
|
83
|
+
dist = qpdist.x() * qpdist.x() + qpdist.y() * qpdist.y()
|
|
84
|
+
if dist < nearest:
|
|
85
|
+
nearest, selected_item = dist, grsock
|
|
86
|
+
|
|
87
|
+
if not isinstance(selected_item, QDMGraphicsSocket) or not selected_item.socket or not selected_item.socket.node:
|
|
88
|
+
return None, scenepos
|
|
89
|
+
|
|
90
|
+
selected_item.isHighlighted = True
|
|
91
|
+
|
|
92
|
+
calcpos = selected_item.socket.node.getSocketScenePosition(
|
|
93
|
+
selected_item.socket)
|
|
94
|
+
|
|
95
|
+
return selected_item, QPointF(*calcpos)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
A module containing the Edge Validator functions which can be registered as callbacks to
|
|
4
|
+
:class:`~nodeeditor.node_edge.Edge` class.
|
|
5
|
+
|
|
6
|
+
Example of registering Edge Validator callbacks:
|
|
7
|
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
8
|
+
|
|
9
|
+
You can register validation callbacks once for example on the bottom of node_edge.py file or on the
|
|
10
|
+
application start with calling this:
|
|
11
|
+
|
|
12
|
+
.. code-block:: python
|
|
13
|
+
|
|
14
|
+
from nodeeditor.node_edge_validators import *
|
|
15
|
+
|
|
16
|
+
Edge.registerEdgeValidator(edge_validator_debug)
|
|
17
|
+
Edge.registerEdgeValidator(edge_cannot_connect_two_outputs_or_two_inputs)
|
|
18
|
+
Edge.registerEdgeValidator(edge_cannot_connect_input_and_output_of_same_node)
|
|
19
|
+
Edge.registerEdgeValidator(edge_cannot_connect_input_and_output_of_different_type)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from typing import TYPE_CHECKING
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from nodeeditor.node_socket import Socket
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
DEBUG = False
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def print_error(*args) -> None:
|
|
35
|
+
"""Helper method which prints to console if `DEBUG` is set to `True`"""
|
|
36
|
+
if DEBUG:
|
|
37
|
+
print("Edge Validation Error:", *args)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def edge_validator_debug(input: 'Socket', output: 'Socket') -> bool:
|
|
41
|
+
"""This will consider edge always valid, however writes bunch of debug stuff into console"""
|
|
42
|
+
print("VALIDATING:")
|
|
43
|
+
print(input, "input" if input.is_input else "output", "of node", input.node)
|
|
44
|
+
for s in input.node.inputs+input.node.outputs:
|
|
45
|
+
print("\t", s, "input" if s.is_input else "output")
|
|
46
|
+
print(output, "input" if input.is_input else "output", "of node", output.node)
|
|
47
|
+
for s in output.node.inputs+output.node.outputs:
|
|
48
|
+
print("\t", s, "input" if s.is_input else "output")
|
|
49
|
+
|
|
50
|
+
return True
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def edge_cannot_connect_two_outputs_or_two_inputs(input: 'Socket', output: 'Socket') -> bool:
|
|
54
|
+
"""Edge is invalid if it connects 2 output sockets or 2 input sockets"""
|
|
55
|
+
if input.is_output and output.is_output:
|
|
56
|
+
print_error("Connecting 2 outputs")
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
if input.is_input and output.is_input:
|
|
60
|
+
print_error("Connecting 2 inputs")
|
|
61
|
+
return False
|
|
62
|
+
|
|
63
|
+
return True
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def edge_cannot_connect_input_and_output_of_same_node(input: 'Socket', output: 'Socket') -> bool:
|
|
67
|
+
"""Edge is invalid if it connects the same node"""
|
|
68
|
+
if input.node == output.node:
|
|
69
|
+
print_error("Connecting the same node")
|
|
70
|
+
return False
|
|
71
|
+
|
|
72
|
+
return True
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def edge_cannot_connect_input_and_output_of_different_type(input: 'Socket', output: 'Socket') -> bool:
|
|
76
|
+
"""Edge is invalid if it connects sockets with different colors"""
|
|
77
|
+
|
|
78
|
+
if input.socket_type != output.socket_type:
|
|
79
|
+
print_error("Connecting sockets with different colors")
|
|
80
|
+
return False
|
|
81
|
+
|
|
82
|
+
return True
|