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,445 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
A module containing NodeEditor's class for representing Socket and Socket Position Constants.
|
|
4
|
+
"""
|
|
5
|
+
from collections import OrderedDict
|
|
6
|
+
from qtpy.QtCore import QObject
|
|
7
|
+
|
|
8
|
+
from nodeeditor.node_serializable import Serializable
|
|
9
|
+
from nodeeditor.node_graphics_socket import QDMGraphicsSocket
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
from typing import TYPE_CHECKING, List, Optional, Tuple, Any, Callable, TypedDict
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from nodeeditor.node_graphics_view import QDMGraphicsView
|
|
17
|
+
from nodeeditor.node_scene import Scene
|
|
18
|
+
from nodeeditor.node_edge import Edge
|
|
19
|
+
from nodeeditor.node_node import Node
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
LEFT_TOP = 1 #:
|
|
23
|
+
LEFT_CENTER = 2 #:
|
|
24
|
+
LEFT_BOTTOM = 3 #:
|
|
25
|
+
RIGHT_TOP = 4 #:
|
|
26
|
+
RIGHT_CENTER = 5 #:
|
|
27
|
+
RIGHT_BOTTOM = 6 #:
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
DEBUG = False
|
|
31
|
+
DEBUG_REMOVE_WARNINGS = False
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Socket(QObject, Serializable):
|
|
35
|
+
"""Class representing Socket."""
|
|
36
|
+
|
|
37
|
+
Socket_GR_Class = QDMGraphicsSocket
|
|
38
|
+
|
|
39
|
+
"""Class representing Socket."""
|
|
40
|
+
|
|
41
|
+
def __init__(self, node: 'Node', index: int = 0, position: int = LEFT_TOP, socket_type: int = 1, multi_edges: bool = True,
|
|
42
|
+
count_on_this_node_side: int = 1, is_input: bool = False) -> None:
|
|
43
|
+
"""
|
|
44
|
+
:param node: reference to the :class:`~nodeeditor.node_node.Node` containing this `Socket`
|
|
45
|
+
:type node: :class:`~nodeeditor.node_node.Node`
|
|
46
|
+
:param index: Current index of this socket in the position
|
|
47
|
+
:type index: ``int``
|
|
48
|
+
:param position: Socket position. See :ref:`socket-position-constants`
|
|
49
|
+
:param socket_type: Constant defining type(color) of this socket
|
|
50
|
+
:param multi_edges: Can this socket have multiple `Edges` connected?
|
|
51
|
+
:type multi_edges: ``bool``
|
|
52
|
+
:param count_on_this_node_side: number of total sockets on this position
|
|
53
|
+
:type count_on_this_node_side: ``int``
|
|
54
|
+
:param is_input: Is this an input `Socket`?
|
|
55
|
+
:type is_input: ``bool``
|
|
56
|
+
|
|
57
|
+
:Instance Attributes:
|
|
58
|
+
|
|
59
|
+
- **node** - reference to the :class:`~nodeeditor.node_node.Node` containing this `Socket`
|
|
60
|
+
- **edges** - list of `Edges` connected to this `Socket`
|
|
61
|
+
- **grSocket** - reference to the :class:`~nodeeditor.node_graphics_socket.QDMGraphicsSocket`
|
|
62
|
+
- **position** - Socket position. See :ref:`socket-position-constants`
|
|
63
|
+
- **index** - Current index of this socket in the position
|
|
64
|
+
- **socket_type** - Constant defining type(color) of this socket
|
|
65
|
+
- **count_on_this_node_side** - number of sockets on this position
|
|
66
|
+
- **is_multi_edges** - ``True`` if `Socket` can contain multiple `Edges`
|
|
67
|
+
- **is_input** - ``True`` if this socket serves for Input
|
|
68
|
+
- **is_output** - ``True`` if this socket serves for Output
|
|
69
|
+
"""
|
|
70
|
+
super().__init__() # Initialize QObject
|
|
71
|
+
super(Serializable).__init__() # Initialize Serializable
|
|
72
|
+
|
|
73
|
+
self.node = node
|
|
74
|
+
self.position = position
|
|
75
|
+
self.index = index
|
|
76
|
+
self.socket_type = socket_type
|
|
77
|
+
self.count_on_this_node_side = count_on_this_node_side
|
|
78
|
+
self.is_multi_edges = multi_edges
|
|
79
|
+
self.is_input = is_input
|
|
80
|
+
self.is_output = not self.is_input
|
|
81
|
+
|
|
82
|
+
#: name of the shape this socket is drawn with. Set by the owning node
|
|
83
|
+
#: (see :meth:`~nodeeditor.node_node.Node.getSocketShapeName`), so it
|
|
84
|
+
#: is code, never graph file data.
|
|
85
|
+
self.shape = self._resolveShapeName(socket_type, is_input)
|
|
86
|
+
|
|
87
|
+
if DEBUG:
|
|
88
|
+
print("Socket -- creating with", self.index,
|
|
89
|
+
self.position, "for nodeeditor", self.node)
|
|
90
|
+
|
|
91
|
+
self.grSocket = self.__class__.Socket_GR_Class(self)
|
|
92
|
+
|
|
93
|
+
self.setSocketPosition()
|
|
94
|
+
|
|
95
|
+
self.edges: List['Edge'] = []
|
|
96
|
+
|
|
97
|
+
def __str__(self) -> str:
|
|
98
|
+
"""Return a short identifying string of this `Socket`."""
|
|
99
|
+
return "<Socket #%d %s %s..%s>" % (
|
|
100
|
+
self.index, "ME" if self.is_multi_edges else "SE", hex(id(self))[
|
|
101
|
+
2:5], hex(id(self))[-3:]
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
def _resolveShapeName(self, socket_type, is_input: bool) -> str:
|
|
105
|
+
"""Ask the owning node which shape a socket of this type should use.
|
|
106
|
+
|
|
107
|
+
:param socket_type: the socket's type
|
|
108
|
+
:param is_input: ``True`` for an input socket
|
|
109
|
+
:return: the registered shape name
|
|
110
|
+
:rtype: ``str``
|
|
111
|
+
"""
|
|
112
|
+
getter = getattr(self.node, "getSocketShapeName", None)
|
|
113
|
+
if getter is None:
|
|
114
|
+
return "circle"
|
|
115
|
+
try:
|
|
116
|
+
return getter(socket_type, is_input) or "circle"
|
|
117
|
+
except Exception:
|
|
118
|
+
return "circle"
|
|
119
|
+
|
|
120
|
+
def delete(self) -> None:
|
|
121
|
+
"""Delete this `Socket` from graphics scene for sure"""
|
|
122
|
+
self.grSocket.setParentItem(None)
|
|
123
|
+
self.node.scene.grScene.removeItem(self.grSocket)
|
|
124
|
+
del self.grSocket
|
|
125
|
+
|
|
126
|
+
def changeSocketType(self, new_socket_type: int) -> bool:
|
|
127
|
+
"""
|
|
128
|
+
Change the Socket Type
|
|
129
|
+
|
|
130
|
+
:param new_socket_type: new socket type
|
|
131
|
+
:type new_socket_type: ``int``
|
|
132
|
+
:return: Returns ``True`` if the socket type was actually changed
|
|
133
|
+
:rtype: ``bool``
|
|
134
|
+
"""
|
|
135
|
+
if self.socket_type != new_socket_type:
|
|
136
|
+
self.socket_type = new_socket_type
|
|
137
|
+
self.grSocket.changeSocketType()
|
|
138
|
+
# the shape follows the socket type
|
|
139
|
+
self.shape = self._resolveShapeName(
|
|
140
|
+
new_socket_type, self.is_input)
|
|
141
|
+
self.grSocket.setShape(self.shape)
|
|
142
|
+
return True
|
|
143
|
+
return False
|
|
144
|
+
|
|
145
|
+
def setSocketPosition(self) -> None:
|
|
146
|
+
"""Helper function to set `Graphics Socket` position. Exact socket position is calculated
|
|
147
|
+
inside :class:`~nodeeditor.node_node.Node`."""
|
|
148
|
+
self.grSocket.setPos(*self.node.getSocketPosition(self.index,
|
|
149
|
+
self.position, self.count_on_this_node_side))
|
|
150
|
+
|
|
151
|
+
def getSocketPosition(self):
|
|
152
|
+
"""
|
|
153
|
+
:return: Returns this `Socket` position according to the implementation stored in
|
|
154
|
+
:class:`~nodeeditor.node_node.Node`
|
|
155
|
+
:rtype: ``x, y`` position
|
|
156
|
+
"""
|
|
157
|
+
if DEBUG:
|
|
158
|
+
print(" GSP: ", self.index, self.position, "nodeeditor:", self.node)
|
|
159
|
+
res = self.node.getSocketPosition(
|
|
160
|
+
self.index, self.position, self.count_on_this_node_side)
|
|
161
|
+
if DEBUG:
|
|
162
|
+
print(" res", res)
|
|
163
|
+
return res
|
|
164
|
+
|
|
165
|
+
def hasAnyEdge(self) -> bool:
|
|
166
|
+
"""
|
|
167
|
+
Returns ``True`` if any :class:`~nodeeditor.node_edge.Edge` is connected to this socket
|
|
168
|
+
|
|
169
|
+
:return: ``True`` if any :class:`~nodeeditor.node_edge.Edge` is connected to this socket
|
|
170
|
+
:rtype: ``bool``
|
|
171
|
+
"""
|
|
172
|
+
return len(self.edges) > 0
|
|
173
|
+
|
|
174
|
+
def isConnected(self, edge: 'Edge') -> bool:
|
|
175
|
+
"""
|
|
176
|
+
Returns ``True`` if :class:`~nodeeditor.node_edge.Edge` is connected to this `Socket`
|
|
177
|
+
|
|
178
|
+
:param edge: :class:`~nodeeditor.node_edge.Edge` to check if it is connected to this `Socket`
|
|
179
|
+
:type edge: :class:`~nodeeditor.node_edge.Edge`
|
|
180
|
+
:return: ``True`` if `Edge` is connected to this socket
|
|
181
|
+
:rtype: ``bool``
|
|
182
|
+
"""
|
|
183
|
+
return edge in self.edges
|
|
184
|
+
|
|
185
|
+
def addEdge(self, edge: 'Edge', index: Optional[int] = None) -> None:
|
|
186
|
+
"""
|
|
187
|
+
Append an Edge to the list of connected Edges
|
|
188
|
+
|
|
189
|
+
On an **input** socket the `Edge` also gets a *read position* (see
|
|
190
|
+
:attr:`~nodeeditor.node_edge.Edge.input_index`). Pass ``index`` to
|
|
191
|
+
place it at a specific slot; when ``index`` is ``None`` a value
|
|
192
|
+
already set on the `Edge` (``Edge(..., input_index=n)`` or
|
|
193
|
+
deserialization) is used, and otherwise the `Edge` is appended, i.e.
|
|
194
|
+
it is read last. Output sockets are not ordered and ignore ``index``.
|
|
195
|
+
|
|
196
|
+
:param edge: :class:`~nodeeditor.node_edge.Edge` to connect to this `Socket`
|
|
197
|
+
:type edge: :class:`~nodeeditor.node_edge.Edge`
|
|
198
|
+
:param index: 0-based read position on an input socket or ``None`` to
|
|
199
|
+
append at the end (default: order of connection)
|
|
200
|
+
:type index: ``int`` or ``None``
|
|
201
|
+
"""
|
|
202
|
+
if edge in self.edges:
|
|
203
|
+
return
|
|
204
|
+
|
|
205
|
+
if not self.is_input:
|
|
206
|
+
# output sockets keep plain connection order
|
|
207
|
+
self.edges.append(edge)
|
|
208
|
+
return
|
|
209
|
+
|
|
210
|
+
if index is None:
|
|
211
|
+
index = getattr(edge, "input_index", -1)
|
|
212
|
+
|
|
213
|
+
try:
|
|
214
|
+
index = int(index)
|
|
215
|
+
except (TypeError, ValueError):
|
|
216
|
+
index = -1
|
|
217
|
+
|
|
218
|
+
if 0 <= index <= len(self.edges):
|
|
219
|
+
self.edges.insert(index, edge)
|
|
220
|
+
else:
|
|
221
|
+
self.edges.append(edge)
|
|
222
|
+
self._reindexEdges()
|
|
223
|
+
|
|
224
|
+
def removeEdge(self, edge: 'Edge') -> None:
|
|
225
|
+
"""
|
|
226
|
+
Disconnect passed :class:`~nodeeditor.node_edge.Edge` from this `Socket`
|
|
227
|
+
:param edge: :class:`~nodeeditor.node_edge.Edge` to disconnect
|
|
228
|
+
:type edge: :class:`~nodeeditor.node_edge.Edge`
|
|
229
|
+
"""
|
|
230
|
+
if edge in self.edges:
|
|
231
|
+
self.edges.remove(edge)
|
|
232
|
+
if self.is_input:
|
|
233
|
+
# close the gap the removed edge left behind
|
|
234
|
+
self._reindexEdges()
|
|
235
|
+
else:
|
|
236
|
+
if DEBUG_REMOVE_WARNINGS:
|
|
237
|
+
print("!W:", "Socket::removeEdge", "wanna remove edge", edge,
|
|
238
|
+
"from self.edges but it's not in the list!")
|
|
239
|
+
|
|
240
|
+
def removeAllEdges(self, silent: bool = False) -> None:
|
|
241
|
+
"""Disconnect all `Edges` from this `Socket`"""
|
|
242
|
+
while self.edges:
|
|
243
|
+
edge = self.edges.pop(0)
|
|
244
|
+
if silent:
|
|
245
|
+
edge.remove(silent_for_socket=self)
|
|
246
|
+
else:
|
|
247
|
+
edge.remove() # just remove all with notifications
|
|
248
|
+
if self.is_input:
|
|
249
|
+
self._reindexEdges()
|
|
250
|
+
|
|
251
|
+
# ordered input helpers
|
|
252
|
+
|
|
253
|
+
def _reindexEdges(self) -> None:
|
|
254
|
+
"""
|
|
255
|
+
Compact the read order of this **input** `Socket`.
|
|
256
|
+
|
|
257
|
+
Writes ``0..len(edges)-1`` back onto every connected `Edge` so read
|
|
258
|
+
positions are always gap free. No-op for output sockets, whose `Edges`
|
|
259
|
+
carry no meaningful :attr:`~nodeeditor.node_edge.Edge.input_index`.
|
|
260
|
+
"""
|
|
261
|
+
if not self.is_input:
|
|
262
|
+
return
|
|
263
|
+
for position, edge in enumerate(self.edges):
|
|
264
|
+
edge.input_index = position
|
|
265
|
+
|
|
266
|
+
def orderedEdges(self) -> List['Edge']:
|
|
267
|
+
"""
|
|
268
|
+
Connected `Edges` in read order.
|
|
269
|
+
|
|
270
|
+
On an input `Socket` this is the connection order maintained by
|
|
271
|
+
:meth:`addEdge`; on an output `Socket` it is the plain connection list.
|
|
272
|
+
|
|
273
|
+
:return: `Edges` connected to this `Socket`, first read position first
|
|
274
|
+
:rtype: List[:class:`~nodeeditor.node_edge.Edge`]
|
|
275
|
+
"""
|
|
276
|
+
if not self.is_input:
|
|
277
|
+
return list(self.edges)
|
|
278
|
+
# stable sort: edges sharing an input_index keep their relative order
|
|
279
|
+
return sorted(self.edges, key=lambda edge: edge.input_index)
|
|
280
|
+
|
|
281
|
+
def edgeIndex(self, edge: 'Edge') -> int:
|
|
282
|
+
"""
|
|
283
|
+
Read position of ``edge`` on this input `Socket`.
|
|
284
|
+
|
|
285
|
+
:param edge: :class:`~nodeeditor.node_edge.Edge` to look up
|
|
286
|
+
:type edge: :class:`~nodeeditor.node_edge.Edge`
|
|
287
|
+
:return: 0-based read position or ``-1`` if not connected to this `Socket`
|
|
288
|
+
:rtype: ``int``
|
|
289
|
+
"""
|
|
290
|
+
if edge not in self.edges:
|
|
291
|
+
return -1
|
|
292
|
+
return self.orderedEdges().index(edge)
|
|
293
|
+
|
|
294
|
+
def compactEdgeOrder(self) -> bool:
|
|
295
|
+
"""
|
|
296
|
+
Re-sort and renumber this input `Socket`'s `Edges` to a gap free
|
|
297
|
+
``0..n-1`` sequence. Useful after assigning
|
|
298
|
+
:attr:`~nodeeditor.node_edge.Edge.input_index` directly from code.
|
|
299
|
+
|
|
300
|
+
:return: ``True`` if anything actually changed
|
|
301
|
+
:rtype: ``bool``
|
|
302
|
+
"""
|
|
303
|
+
if not self.is_input:
|
|
304
|
+
return False
|
|
305
|
+
ordered = self.orderedEdges()
|
|
306
|
+
changed = ordered != self.edges
|
|
307
|
+
self.edges[:] = ordered
|
|
308
|
+
for position, edge in enumerate(ordered):
|
|
309
|
+
if edge.input_index != position:
|
|
310
|
+
changed = True
|
|
311
|
+
edge.input_index = position
|
|
312
|
+
return changed
|
|
313
|
+
|
|
314
|
+
def setEdgeOrder(self, edges: List['Edge']) -> bool:
|
|
315
|
+
"""
|
|
316
|
+
Set the read order of the `Edges` connected to this input `Socket`.
|
|
317
|
+
|
|
318
|
+
``edges`` may be a partial list; connected `Edges` missing from it are
|
|
319
|
+
appended afterwards keeping their current relative order. Duplicates
|
|
320
|
+
and `Edges` of other sockets are ignored, so passing a stale or
|
|
321
|
+
reordered reference is safe.
|
|
322
|
+
|
|
323
|
+
:param edges: `Edges` of this socket, first read position first
|
|
324
|
+
:type edges: List[:class:`~nodeeditor.node_edge.Edge`]
|
|
325
|
+
:return: ``True`` if the order changed
|
|
326
|
+
:rtype: ``bool``
|
|
327
|
+
"""
|
|
328
|
+
if not self.is_input:
|
|
329
|
+
return False
|
|
330
|
+
|
|
331
|
+
ordered: List['Edge'] = []
|
|
332
|
+
for edge in edges or []:
|
|
333
|
+
# skip foreign edges and duplicates instead of failing, so a stale
|
|
334
|
+
# reference to a disconnected edge cannot corrupt the order
|
|
335
|
+
if edge in self.edges and edge not in ordered:
|
|
336
|
+
ordered.append(edge)
|
|
337
|
+
ordered += [edge for edge in self.orderedEdges() if edge not in ordered]
|
|
338
|
+
|
|
339
|
+
if ordered == self.orderedEdges():
|
|
340
|
+
return False
|
|
341
|
+
self.edges[:] = ordered
|
|
342
|
+
self._reindexEdges()
|
|
343
|
+
return True
|
|
344
|
+
|
|
345
|
+
def moveEdgeTo(self, edge: 'Edge', position: int) -> bool:
|
|
346
|
+
"""
|
|
347
|
+
Move ``edge`` to read position ``position`` on this input `Socket`.
|
|
348
|
+
|
|
349
|
+
:param edge: :class:`~nodeeditor.node_edge.Edge` to move
|
|
350
|
+
:type edge: :class:`~nodeeditor.node_edge.Edge`
|
|
351
|
+
:param position: target read position
|
|
352
|
+
:type position: ``int``
|
|
353
|
+
:return: ``True`` if the order changed
|
|
354
|
+
:rtype: ``bool``
|
|
355
|
+
"""
|
|
356
|
+
if not self.is_input or edge not in self.edges:
|
|
357
|
+
return False
|
|
358
|
+
try:
|
|
359
|
+
position = int(position)
|
|
360
|
+
except (TypeError, ValueError):
|
|
361
|
+
return False
|
|
362
|
+
|
|
363
|
+
ordered = self.orderedEdges()
|
|
364
|
+
if not 0 <= position < len(ordered):
|
|
365
|
+
return False
|
|
366
|
+
ordered.remove(edge)
|
|
367
|
+
ordered.insert(position, edge)
|
|
368
|
+
self.edges[:] = ordered
|
|
369
|
+
self._reindexEdges()
|
|
370
|
+
return True
|
|
371
|
+
|
|
372
|
+
def swapEdges(self, edge_a: 'Edge', edge_b: 'Edge') -> bool:
|
|
373
|
+
"""
|
|
374
|
+
Swap the read positions of two `Edges` of this input `Socket`.
|
|
375
|
+
|
|
376
|
+
:param edge_a: first :class:`~nodeeditor.node_edge.Edge` to swap
|
|
377
|
+
:type edge_a: :class:`~nodeeditor.node_edge.Edge`
|
|
378
|
+
:param edge_b: second :class:`~nodeeditor.node_edge.Edge` to swap
|
|
379
|
+
:type edge_b: :class:`~nodeeditor.node_edge.Edge`
|
|
380
|
+
:return: ``True`` if the order changed
|
|
381
|
+
:rtype: ``bool``
|
|
382
|
+
"""
|
|
383
|
+
if not self.is_input or edge_a not in self.edges or edge_b not in self.edges:
|
|
384
|
+
return False
|
|
385
|
+
ordered = self.orderedEdges()
|
|
386
|
+
first, second = ordered.index(edge_a), ordered.index(edge_b)
|
|
387
|
+
if first == second:
|
|
388
|
+
return False
|
|
389
|
+
ordered[first], ordered[second] = ordered[second], ordered[first]
|
|
390
|
+
self.edges[:] = ordered
|
|
391
|
+
self._reindexEdges()
|
|
392
|
+
return True
|
|
393
|
+
|
|
394
|
+
def determineMultiEdges(self, data: dict) -> bool:
|
|
395
|
+
"""
|
|
396
|
+
Deserialization helper function. In our tutorials we created a new version of graph data format.
|
|
397
|
+
This function is here to help solve the issue of opening older files in the newer format.
|
|
398
|
+
If the 'multi_edges' param is missing in the dictionary, we determine if this `Socket`
|
|
399
|
+
should support multiple `Edges`.
|
|
400
|
+
|
|
401
|
+
:param data: `Socket` data in ``dict`` format for deserialization
|
|
402
|
+
:type data: ``dict``
|
|
403
|
+
:return: ``True`` if this `Socket` should support multi_edges
|
|
404
|
+
"""
|
|
405
|
+
if 'multi_edges' in data:
|
|
406
|
+
return data['multi_edges']
|
|
407
|
+
else:
|
|
408
|
+
# probably older version of file, make RIGHT socket multiedged by default
|
|
409
|
+
return data['position'] in (RIGHT_BOTTOM, RIGHT_TOP)
|
|
410
|
+
|
|
411
|
+
def serialize(self) -> OrderedDict:
|
|
412
|
+
"""
|
|
413
|
+
Serialize this `Socket` into an ``OrderedDict`` for storing in memory or on disk.
|
|
414
|
+
|
|
415
|
+
:return: data serialized in ``OrderedDict``
|
|
416
|
+
:rtype: ``OrderedDict``
|
|
417
|
+
"""
|
|
418
|
+
return OrderedDict([
|
|
419
|
+
('id', self.id),
|
|
420
|
+
('index', self.index),
|
|
421
|
+
('multi_edges', self.is_multi_edges),
|
|
422
|
+
('position', self.position),
|
|
423
|
+
('socket_type', self.socket_type),
|
|
424
|
+
])
|
|
425
|
+
|
|
426
|
+
def deserialize(self, data: dict, hashmap: dict = {}, restore_id: bool = True) -> bool:
|
|
427
|
+
"""
|
|
428
|
+
Deserialize this `Socket` from a ``dict`` of serialized data.
|
|
429
|
+
|
|
430
|
+
:param data: Dictionary containing serialized data
|
|
431
|
+
:type data: ``dict``
|
|
432
|
+
:param hashmap: Helper dictionary containing references (by id == key) to existing objects
|
|
433
|
+
:type hashmap: ``dict``
|
|
434
|
+
:param restore_id: True if we are creating new Sockets. False is useful when loading existing
|
|
435
|
+
Sockets of which we want to keep the existing object's `id`.
|
|
436
|
+
:type restore_id: bool
|
|
437
|
+
:return: ``True`` if deserialization was successful, otherwise ``False``
|
|
438
|
+
:rtype: ``bool``
|
|
439
|
+
"""
|
|
440
|
+
if restore_id:
|
|
441
|
+
self.id = data['id']
|
|
442
|
+
self.is_multi_edges = self.determineMultiEdges(data)
|
|
443
|
+
self.changeSocketType(data['socket_type'])
|
|
444
|
+
hashmap[data['id']] = self
|
|
445
|
+
return True
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
A module containing the Socket Shape system.
|
|
4
|
+
|
|
5
|
+
A socket shape decides two things: how a :class:`~nodeeditor.node_socket.Socket`
|
|
6
|
+
is painted, and which area of it counts as a hit. Four presets ship with the
|
|
7
|
+
library (``circle``, ``square``, ``triangle``, ``chevron``) and applications
|
|
8
|
+
can register their own.
|
|
9
|
+
|
|
10
|
+
Shapes are named per socket type by the owning node (see
|
|
11
|
+
:meth:`~nodeeditor.node_node.Node.getSocketShapeName`), so they are declared in
|
|
12
|
+
code and never written to the graph file.
|
|
13
|
+
"""
|
|
14
|
+
from qtpy.QtCore import QPointF
|
|
15
|
+
from qtpy.QtGui import QPainterPath, QPolygonF
|
|
16
|
+
|
|
17
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
#: Direction handed to a shape for an output socket (right hand side of a node)
|
|
21
|
+
DIRECTION_OUTPUT = 1
|
|
22
|
+
|
|
23
|
+
#: Direction handed to a shape for an input socket (left hand side of a node)
|
|
24
|
+
DIRECTION_INPUT = -1
|
|
25
|
+
|
|
26
|
+
#: Name of the shape every socket falls back to
|
|
27
|
+
DEFAULT_SHAPE = "circle"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SocketShapeContext:
|
|
31
|
+
"""Everything a shape needs while it paints itself."""
|
|
32
|
+
|
|
33
|
+
__slots__ = ("painter", "radius", "center", "brush", "pen", "color",
|
|
34
|
+
"socket", "hovered", "direction")
|
|
35
|
+
|
|
36
|
+
def __init__(self, painter, radius: float, center: QPointF, brush, pen,
|
|
37
|
+
color, socket=None, hovered: bool = False,
|
|
38
|
+
direction: int = DIRECTION_OUTPUT) -> None:
|
|
39
|
+
"""
|
|
40
|
+
:param painter: the `QPainter` the shape draws into
|
|
41
|
+
:param radius: the socket's radius in scene units
|
|
42
|
+
:param center: the socket's center in item coordinates
|
|
43
|
+
:param brush: the fill brush (the socket's type color)
|
|
44
|
+
:param pen: the outline pen (already switched for the hover state)
|
|
45
|
+
:param color: the socket's fill color
|
|
46
|
+
:param socket: the owning :class:`~nodeeditor.node_socket.Socket`
|
|
47
|
+
:param hovered: ``True`` while the mouse hovers the socket
|
|
48
|
+
:param direction: ``+1`` for output sockets, ``-1`` for input sockets
|
|
49
|
+
"""
|
|
50
|
+
self.painter = painter
|
|
51
|
+
self.radius = radius
|
|
52
|
+
self.center = center
|
|
53
|
+
self.brush = brush
|
|
54
|
+
self.pen = pen
|
|
55
|
+
self.color = color
|
|
56
|
+
self.socket = socket
|
|
57
|
+
self.hovered = hovered
|
|
58
|
+
self.direction = direction
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class SocketShape:
|
|
62
|
+
"""Base class for socket shapes.
|
|
63
|
+
|
|
64
|
+
Subclasses describe their outline as a list of points normalized against the
|
|
65
|
+
socket radius, and get the hit-test path and the painting for free.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
#: Mirrors the outline for input sockets when ``True``.
|
|
69
|
+
mirrors_input: bool = True
|
|
70
|
+
|
|
71
|
+
def points(self) -> List[Tuple[float, float]]:
|
|
72
|
+
"""The outline as normalized points, in units of the socket radius.
|
|
73
|
+
|
|
74
|
+
:return: list of ``(x, y)`` pairs
|
|
75
|
+
"""
|
|
76
|
+
raise NotImplementedError
|
|
77
|
+
|
|
78
|
+
def path(self, radius: float, direction: int = DIRECTION_OUTPUT) -> QPainterPath:
|
|
79
|
+
"""The outline as a path, centered on the item origin.
|
|
80
|
+
|
|
81
|
+
:param radius: the socket's radius in scene units
|
|
82
|
+
:param direction: ``+1`` for output sockets, ``-1`` for input sockets
|
|
83
|
+
:return: the outline path
|
|
84
|
+
"""
|
|
85
|
+
pts = self.points()
|
|
86
|
+
if self.mirrors_input and direction < 0:
|
|
87
|
+
pts = [(-x, y) for x, y in pts]
|
|
88
|
+
path = QPainterPath()
|
|
89
|
+
path.addPolygon(QPolygonF(
|
|
90
|
+
[QPointF(x * radius, y * radius) for x, y in pts]))
|
|
91
|
+
path.closeSubpath()
|
|
92
|
+
return path
|
|
93
|
+
|
|
94
|
+
def draw(self, painter, ctx: SocketShapeContext) -> None:
|
|
95
|
+
"""Paint the shape.
|
|
96
|
+
|
|
97
|
+
:param painter: the `QPainter` to draw into
|
|
98
|
+
:param ctx: the :class:`SocketShapeContext` for this paint
|
|
99
|
+
"""
|
|
100
|
+
painter.setBrush(ctx.brush)
|
|
101
|
+
painter.setPen(ctx.pen)
|
|
102
|
+
painter.drawPath(self.path(ctx.radius, ctx.direction))
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class CircleShape(SocketShape):
|
|
106
|
+
"""The classic socket: a filled circle."""
|
|
107
|
+
|
|
108
|
+
def path(self, radius: float, direction: int = DIRECTION_OUTPUT) -> QPainterPath:
|
|
109
|
+
"""The circle as a path, centered on the item origin.
|
|
110
|
+
|
|
111
|
+
:param radius: the socket's radius in scene units
|
|
112
|
+
:param direction: ignored, a circle looks the same on both sides
|
|
113
|
+
:return: the outline path
|
|
114
|
+
"""
|
|
115
|
+
path = QPainterPath()
|
|
116
|
+
path.addEllipse(QPointF(0, 0), radius, radius)
|
|
117
|
+
return path
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class SquareShape(SocketShape):
|
|
121
|
+
"""A square socket.
|
|
122
|
+
|
|
123
|
+
The half side is ``0.85 * radius``, which reads as the same weight as the
|
|
124
|
+
circle next to it.
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
HALF_SIDE = 0.85
|
|
128
|
+
|
|
129
|
+
def points(self) -> List[Tuple[float, float]]:
|
|
130
|
+
"""The square's four corners, in units of the socket radius.
|
|
131
|
+
|
|
132
|
+
:return: list of ``(x, y)`` pairs
|
|
133
|
+
"""
|
|
134
|
+
h = self.HALF_SIDE
|
|
135
|
+
return [(-h, -h), (h, -h), (h, h), (-h, h)]
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class TriangleShape(SocketShape):
|
|
139
|
+
"""A triangular socket.
|
|
140
|
+
|
|
141
|
+
The tip points away from the node: right on an output socket, left on an
|
|
142
|
+
input socket.
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
def points(self) -> List[Tuple[float, float]]:
|
|
146
|
+
"""The triangle's three corners, in units of the socket radius.
|
|
147
|
+
|
|
148
|
+
The tip points right, so the outline mirrors on input sockets.
|
|
149
|
+
|
|
150
|
+
:return: list of ``(x, y)`` pairs
|
|
151
|
+
"""
|
|
152
|
+
return [(-0.8, -0.9), (0.9, 0.0), (-0.8, 0.9)]
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class ChevronShape(SocketShape):
|
|
156
|
+
"""A chevron socket: the concave hexagon, pointing right.
|
|
157
|
+
|
|
158
|
+
The shape never mirrors, on either side of a node. A chevron on an input
|
|
159
|
+
socket aims its tip into the node, so the two sides of a graph read as one
|
|
160
|
+
left-to-right flow: in, processed, out.
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
mirrors_input = False
|
|
164
|
+
|
|
165
|
+
def points(self) -> List[Tuple[float, float]]:
|
|
166
|
+
"""The chevron's six corners, in units of the socket radius.
|
|
167
|
+
|
|
168
|
+
:return: list of ``(x, y)`` pairs
|
|
169
|
+
"""
|
|
170
|
+
return [(0.847, 0.0), (0.284, 1.0), (-0.847, 1.0),
|
|
171
|
+
(-0.331, 0.0), (-0.847, -1.0), (0.284, -1.0)]
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class CallbackShape(SocketShape):
|
|
175
|
+
"""Wraps a plain ``fn(painter, ctx)`` callback as a shape.
|
|
176
|
+
|
|
177
|
+
A callback only knows how to draw, not what its outline is, so the hit
|
|
178
|
+
area falls back to the circle.
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
def __init__(self, func: Callable) -> None:
|
|
182
|
+
"""
|
|
183
|
+
:param func: called as ``func(painter, ctx)`` to paint the socket
|
|
184
|
+
"""
|
|
185
|
+
self.func = func
|
|
186
|
+
|
|
187
|
+
def path(self, radius: float, direction: int = DIRECTION_OUTPUT) -> QPainterPath:
|
|
188
|
+
"""The circle used as the hit area of a callback shape.
|
|
189
|
+
|
|
190
|
+
:param radius: the socket's radius in scene units
|
|
191
|
+
:param direction: ignored, the fallback outline is always a circle
|
|
192
|
+
:return: the outline path
|
|
193
|
+
"""
|
|
194
|
+
path = QPainterPath()
|
|
195
|
+
path.addEllipse(QPointF(0, 0), radius, radius)
|
|
196
|
+
return path
|
|
197
|
+
|
|
198
|
+
def draw(self, painter, ctx: SocketShapeContext) -> None:
|
|
199
|
+
"""Paint the shape by handing the paint over to the wrapped callback.
|
|
200
|
+
|
|
201
|
+
:param painter: the `QPainter` to draw into
|
|
202
|
+
:param ctx: the :class:`SocketShapeContext` for this paint
|
|
203
|
+
"""
|
|
204
|
+
self.func(painter, ctx)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
#: All registered socket shapes, by name
|
|
208
|
+
SOCKET_SHAPES: Dict[str, SocketShape] = {}
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def register_socket_shape(name: str, shape) -> SocketShape:
|
|
212
|
+
"""Register a socket shape under ``name``.
|
|
213
|
+
|
|
214
|
+
``shape`` may be a :class:`SocketShape` instance, a :class:`SocketShape`
|
|
215
|
+
subclass, or a plain callable ``fn(painter, ctx)``. Registering an existing
|
|
216
|
+
name replaces the old shape.
|
|
217
|
+
|
|
218
|
+
:param name: the name nodes use to select this shape
|
|
219
|
+
:param shape: the shape, its class, or a drawing callback
|
|
220
|
+
:return: the registered shape
|
|
221
|
+
"""
|
|
222
|
+
if isinstance(shape, type):
|
|
223
|
+
if not issubclass(shape, SocketShape):
|
|
224
|
+
raise TypeError(
|
|
225
|
+
"socket shape class must inherit from SocketShape, got %r"
|
|
226
|
+
% (shape,))
|
|
227
|
+
shape = shape()
|
|
228
|
+
|
|
229
|
+
if not isinstance(shape, SocketShape):
|
|
230
|
+
if callable(shape):
|
|
231
|
+
shape = CallbackShape(shape)
|
|
232
|
+
else:
|
|
233
|
+
raise TypeError(
|
|
234
|
+
"socket shape must be a SocketShape, a SocketShape subclass or "
|
|
235
|
+
"a callable, got %r" % (shape,))
|
|
236
|
+
|
|
237
|
+
SOCKET_SHAPES[name] = shape
|
|
238
|
+
return shape
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def get_socket_shape(name: Optional[str]) -> SocketShape:
|
|
242
|
+
"""The shape registered under ``name``.
|
|
243
|
+
|
|
244
|
+
Unknown names fall back to the circle rather than raising, so a typo in a
|
|
245
|
+
node's shape map cannot break a graph.
|
|
246
|
+
|
|
247
|
+
:param name: the registered shape name
|
|
248
|
+
:return: the shape to draw with
|
|
249
|
+
"""
|
|
250
|
+
if isinstance(name, str):
|
|
251
|
+
shape = SOCKET_SHAPES.get(name)
|
|
252
|
+
if shape is not None:
|
|
253
|
+
return shape
|
|
254
|
+
return SOCKET_SHAPES[DEFAULT_SHAPE]
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
# the presets
|
|
258
|
+
register_socket_shape(DEFAULT_SHAPE, CircleShape())
|
|
259
|
+
register_socket_shape("square", SquareShape())
|
|
260
|
+
register_socket_shape("triangle", TriangleShape())
|
|
261
|
+
register_socket_shape("chevron", ChevronShape())
|