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_node.py
ADDED
|
@@ -0,0 +1,848 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
A module containing NodeEditor's class for representing `Node`.
|
|
4
|
+
"""
|
|
5
|
+
from collections import OrderedDict
|
|
6
|
+
from qtpy.QtCore import QObject, Signal
|
|
7
|
+
from nodeeditor.node_graphics_node import QDMGraphicsNode
|
|
8
|
+
from nodeeditor.node_content_widget import QDMNodeContentWidget
|
|
9
|
+
from nodeeditor.node_serializable import Serializable
|
|
10
|
+
from nodeeditor.node_socket import Socket, LEFT_BOTTOM, LEFT_CENTER, LEFT_TOP, RIGHT_BOTTOM, RIGHT_CENTER, RIGHT_TOP
|
|
11
|
+
from nodeeditor.node_socket_shapes import SOCKET_SHAPES
|
|
12
|
+
import nodeeditor.node_graph as node_graph
|
|
13
|
+
from nodeeditor.utils_no_qt import dumpException, pp
|
|
14
|
+
|
|
15
|
+
from typing import TYPE_CHECKING, List, Optional, Tuple, Any, Union
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from nodeeditor.node_graphics_view import QDMGraphicsView
|
|
20
|
+
from nodeeditor.node_edge import Edge
|
|
21
|
+
from nodeeditor.node_socket import Socket
|
|
22
|
+
from nodeeditor.node_scene import Scene
|
|
23
|
+
from nodeeditor.node_group import Group
|
|
24
|
+
|
|
25
|
+
DEBUG = False
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Node(QObject, Serializable):
|
|
29
|
+
"""
|
|
30
|
+
Class representing `Node` in the `Scene`.
|
|
31
|
+
"""
|
|
32
|
+
# Signal emitted when node position changes (after user finishes dragging)
|
|
33
|
+
positionChanged = Signal(object) # Emits the Node object
|
|
34
|
+
# Signal emitted when the floating label text is changed by user or code
|
|
35
|
+
labelChanged = Signal(str)
|
|
36
|
+
|
|
37
|
+
GraphicsNode_class = QDMGraphicsNode
|
|
38
|
+
NodeContent_class = QDMNodeContentWidget
|
|
39
|
+
Socket_class = Socket
|
|
40
|
+
|
|
41
|
+
def __init__(self, scene: 'Scene', title: str = "Undefined Node", inputs: list = [], outputs: list = [], input_text: list = [], output_text: list = []) -> None:
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
:param scene: reference to the :class:`~nodeeditor.node_scene.Scene`
|
|
45
|
+
:type scene: :class:`~nodeeditor.node_scene.Scene`
|
|
46
|
+
:param title: Node Title shown in Scene
|
|
47
|
+
:type title: str
|
|
48
|
+
:param inputs: list of :class:`~nodeeditor.node_socket.Socket` types from which the `Sockets` will be auto created
|
|
49
|
+
:param outputs: list of :class:`~nodeeditor.node_socket.Socket` types from which the `Sockets` will be auto created
|
|
50
|
+
|
|
51
|
+
:Instance Attributes:
|
|
52
|
+
|
|
53
|
+
- **scene** - reference to the :class:`~nodeeditor.node_scene.Scene`
|
|
54
|
+
- **grNode** - Instance of :class:`~nodeeditor.node_graphics_node.QDMGraphicsNode` handling graphical representation in the ``QGraphicsScene``. Automatically created in the constructor
|
|
55
|
+
- **content** - Instance of :class:`~nodeeditor.node_graphics_content.QDMGraphicsContent` which is child of ``QWidget`` representing container for all inner widgets inside of the Node. Automatically created in the constructor
|
|
56
|
+
- **inputs** - list containin Input :class:`~nodeeditor.node_socket.Socket` instances
|
|
57
|
+
- **outputs** - list containin Output :class:`~nodeeditor.node_socket.Socket` instances
|
|
58
|
+
|
|
59
|
+
"""
|
|
60
|
+
super().__init__() # Initialize QObject
|
|
61
|
+
super(Serializable).__init__() # Initialize Serializable
|
|
62
|
+
self._title = title
|
|
63
|
+
self.scene = scene
|
|
64
|
+
|
|
65
|
+
# just to be sure, init these variables
|
|
66
|
+
self.content: QDMNodeContentWidget
|
|
67
|
+
self.grNode: QDMGraphicsNode
|
|
68
|
+
|
|
69
|
+
self.initInnerClasses()
|
|
70
|
+
self.initSettings()
|
|
71
|
+
|
|
72
|
+
self.title = title
|
|
73
|
+
|
|
74
|
+
self.scene.addNode(self)
|
|
75
|
+
self.scene.grScene.addItem(self.grNode)
|
|
76
|
+
|
|
77
|
+
# create socket for inputs and outputs
|
|
78
|
+
self.inputs: List['Socket'] = []
|
|
79
|
+
self.outputs: List['Socket'] = []
|
|
80
|
+
self.initSockets(inputs, outputs, input_text, output_text)
|
|
81
|
+
|
|
82
|
+
# dirty and evaluation
|
|
83
|
+
self._is_dirty = False
|
|
84
|
+
self._is_invalid = False
|
|
85
|
+
|
|
86
|
+
# grouping support (single parent Group or None; see nodeeditor.node_group)
|
|
87
|
+
self.parent_group: Optional["Group"] = None
|
|
88
|
+
|
|
89
|
+
# The scene may already be at low zoom (a restore into a zoomed-out
|
|
90
|
+
# view): sockets and the content proxy were built after Qt reported
|
|
91
|
+
# this node joining the scene, so push the current state onto them.
|
|
92
|
+
sync_detail = getattr(self.grNode, "syncDetailVisibility", None)
|
|
93
|
+
if sync_detail is not None:
|
|
94
|
+
sync_detail()
|
|
95
|
+
|
|
96
|
+
def __str__(self) -> str:
|
|
97
|
+
"""Return a short identifying string of this `Node`."""
|
|
98
|
+
return "<%s:%s %s..%s>" % (self.title, self.__class__.__name__, hex(id(self))[2:5], hex(id(self))[-3:])
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def title(self):
|
|
102
|
+
"""
|
|
103
|
+
Title shown in the scene
|
|
104
|
+
|
|
105
|
+
:getter: return current Node title
|
|
106
|
+
:setter: sets Node title and passes it to Graphics Node class
|
|
107
|
+
:type: ``str``
|
|
108
|
+
"""
|
|
109
|
+
return self._title
|
|
110
|
+
|
|
111
|
+
@title.setter
|
|
112
|
+
def title(self, value) -> None:
|
|
113
|
+
self._title = value
|
|
114
|
+
self.grNode.title = self._title
|
|
115
|
+
|
|
116
|
+
def setNodeLabel(self, text: str, store_history: bool = True) -> None:
|
|
117
|
+
"""Set the floating single-line label above the node.
|
|
118
|
+
|
|
119
|
+
:param text: label text; empty string hides the label
|
|
120
|
+
:param store_history: store undo history entry (skip during load/deserialize)
|
|
121
|
+
"""
|
|
122
|
+
if self.grNode is None or not hasattr(self.grNode, 'setLabelText'):
|
|
123
|
+
return
|
|
124
|
+
self.grNode.setLabelText(text)
|
|
125
|
+
try:
|
|
126
|
+
self.labelChanged.emit(self.grNode.labelText())
|
|
127
|
+
except Exception:
|
|
128
|
+
pass
|
|
129
|
+
if store_history:
|
|
130
|
+
try:
|
|
131
|
+
self.scene.history.storeHistory("Node label changed", setModified=True)
|
|
132
|
+
except Exception:
|
|
133
|
+
pass
|
|
134
|
+
|
|
135
|
+
def getNodeLabel(self) -> str:
|
|
136
|
+
"""Return floating label text (``""`` when none)."""
|
|
137
|
+
if self.grNode is None or not hasattr(self.grNode, 'labelText'):
|
|
138
|
+
return ""
|
|
139
|
+
try:
|
|
140
|
+
return self.grNode.labelText()
|
|
141
|
+
except Exception:
|
|
142
|
+
return ""
|
|
143
|
+
|
|
144
|
+
def clearNodeLabel(self, store_history: bool = True) -> None:
|
|
145
|
+
"""Remove the floating label."""
|
|
146
|
+
self.setNodeLabel("", store_history=store_history)
|
|
147
|
+
|
|
148
|
+
def setNodeLabelVisible(self, visible: bool) -> None:
|
|
149
|
+
"""Show or hide the floating label of this `Node`.
|
|
150
|
+
|
|
151
|
+
The label stays hidden while it carries no text.
|
|
152
|
+
|
|
153
|
+
:param visible: ``True`` shows the label, ``False`` hides it
|
|
154
|
+
:type visible: ``bool``
|
|
155
|
+
"""
|
|
156
|
+
if self.grNode is None or not hasattr(self.grNode, 'setLabelVisible'):
|
|
157
|
+
return
|
|
158
|
+
self.grNode.setLabelVisible(visible)
|
|
159
|
+
|
|
160
|
+
def isNodeLabelVisible(self) -> bool:
|
|
161
|
+
"""Is the floating label of this `Node` currently visible?
|
|
162
|
+
|
|
163
|
+
:return: ``True`` if the label is visible
|
|
164
|
+
:rtype: ``bool``
|
|
165
|
+
"""
|
|
166
|
+
if self.grNode is None or not hasattr(self.grNode, 'isLabelVisible'):
|
|
167
|
+
return False
|
|
168
|
+
try:
|
|
169
|
+
return bool(self.grNode.isLabelVisible())
|
|
170
|
+
except Exception:
|
|
171
|
+
return False
|
|
172
|
+
|
|
173
|
+
def setNodeLabelOffset(self, offset: float) -> None:
|
|
174
|
+
"""Set the gap between the top of this `Node` and the bottom of its floating label.
|
|
175
|
+
|
|
176
|
+
:param offset: gap in pixels
|
|
177
|
+
:type offset: ``float``
|
|
178
|
+
"""
|
|
179
|
+
if self.grNode is None or not hasattr(self.grNode, 'setLabelOffset'):
|
|
180
|
+
return
|
|
181
|
+
self.grNode.setLabelOffset(offset)
|
|
182
|
+
|
|
183
|
+
@property
|
|
184
|
+
def pos(self):
|
|
185
|
+
"""
|
|
186
|
+
Retrieve Node's position in the Scene
|
|
187
|
+
|
|
188
|
+
:return: Node position
|
|
189
|
+
:rtype: ``QPointF``
|
|
190
|
+
"""
|
|
191
|
+
return self.grNode.pos() # QPointF
|
|
192
|
+
|
|
193
|
+
def setPos(self, x: float, y: float) -> None:
|
|
194
|
+
"""
|
|
195
|
+
Sets position of the Graphics Node
|
|
196
|
+
|
|
197
|
+
:param x: X `Scene` position
|
|
198
|
+
:param y: Y `Scene` position
|
|
199
|
+
"""
|
|
200
|
+
# Moving the graphics item fires ``itemChange(ItemPositionHasChanged)``,
|
|
201
|
+
# which refreshes the connected edges. The loop that used to live here
|
|
202
|
+
# did the same work again, and called ``calcPath()`` a second time
|
|
203
|
+
# before immediately invalidating that path, so it was pure waste.
|
|
204
|
+
self.grNode.setPos(x, y)
|
|
205
|
+
|
|
206
|
+
def initInnerClasses(self) -> None:
|
|
207
|
+
"""Sets up graphics Node (PyQt) and Content Widget"""
|
|
208
|
+
node_content_class = self.getNodeContentClass()
|
|
209
|
+
graphics_node_class = self.getGraphicsNodeClass()
|
|
210
|
+
if node_content_class is not None:
|
|
211
|
+
self.content = node_content_class(self)
|
|
212
|
+
if graphics_node_class is not None:
|
|
213
|
+
self.grNode = graphics_node_class(self)
|
|
214
|
+
|
|
215
|
+
def getNodeContentClass(self):
|
|
216
|
+
"""Returns class representing nodeeditor content"""
|
|
217
|
+
return self.__class__.NodeContent_class
|
|
218
|
+
|
|
219
|
+
def getGraphicsNodeClass(self):
|
|
220
|
+
"""Returns class representing the graphics node"""
|
|
221
|
+
return self.__class__.GraphicsNode_class
|
|
222
|
+
|
|
223
|
+
def initSettings(self) -> None:
|
|
224
|
+
"""Initialize properties and socket information"""
|
|
225
|
+
self.socket_spacing = 22
|
|
226
|
+
# Todo: set input_multi_edged option.
|
|
227
|
+
self.input_socket_position = LEFT_BOTTOM
|
|
228
|
+
self.output_socket_position = RIGHT_TOP
|
|
229
|
+
self.input_multi_edged = False
|
|
230
|
+
self.output_multi_edged = True
|
|
231
|
+
#: shape used by this node's input sockets, keyed by socket type.
|
|
232
|
+
#: ``'*'`` is the wildcard for every type the map does not name.
|
|
233
|
+
self.input_socket_shapes: dict = {}
|
|
234
|
+
#: shape used by this node's output sockets, keyed by socket type
|
|
235
|
+
self.output_socket_shapes: dict = {}
|
|
236
|
+
self.socket_offsets = {
|
|
237
|
+
LEFT_BOTTOM: -1,
|
|
238
|
+
LEFT_CENTER: -1,
|
|
239
|
+
LEFT_TOP: -1,
|
|
240
|
+
RIGHT_BOTTOM: 1,
|
|
241
|
+
RIGHT_CENTER: 1,
|
|
242
|
+
RIGHT_TOP: 1,
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
def getSocketShapeName(self, socket_type, is_input: bool) -> str:
|
|
246
|
+
"""Name of the shape used by a socket of ``socket_type`` on this node.
|
|
247
|
+
|
|
248
|
+
The side's own map is consulted first, then its ``'*'`` wildcard
|
|
249
|
+
entry, and the circle is the last resort. Types that are not mapped
|
|
250
|
+
keep the classic circle.
|
|
251
|
+
|
|
252
|
+
:param socket_type: the socket's type, as passed to ``initSockets``
|
|
253
|
+
:param is_input: ``True`` for an input socket
|
|
254
|
+
:return: the registered shape name
|
|
255
|
+
:rtype: ``str``
|
|
256
|
+
|
|
257
|
+
Example::
|
|
258
|
+
|
|
259
|
+
class MyNode(Node):
|
|
260
|
+
def initSettings(self):
|
|
261
|
+
super().initSettings()
|
|
262
|
+
self.input_socket_shapes = {1: 'chevron'}
|
|
263
|
+
self.output_socket_shapes = {'*': 'triangle'}
|
|
264
|
+
"""
|
|
265
|
+
if is_input:
|
|
266
|
+
mapping = getattr(self, "input_socket_shapes", None)
|
|
267
|
+
else:
|
|
268
|
+
mapping = getattr(self, "output_socket_shapes", None)
|
|
269
|
+
|
|
270
|
+
name = None
|
|
271
|
+
if isinstance(mapping, dict):
|
|
272
|
+
name = mapping.get(socket_type)
|
|
273
|
+
if name is None:
|
|
274
|
+
name = mapping.get("*")
|
|
275
|
+
# an unregistered name draws as the circle rather than breaking
|
|
276
|
+
if isinstance(name, str) and name in SOCKET_SHAPES:
|
|
277
|
+
return name
|
|
278
|
+
return "circle"
|
|
279
|
+
|
|
280
|
+
def initSockets(self, inputs: list, outputs: list,
|
|
281
|
+
input_text: list = [], output_text: list = [],
|
|
282
|
+
reset: bool = True) -> None:
|
|
283
|
+
"""
|
|
284
|
+
Create sockets for inputs and outputs
|
|
285
|
+
|
|
286
|
+
:param inputs: list of Socket Types (int)
|
|
287
|
+
:type inputs: ``list``
|
|
288
|
+
:param outputs: list of Socket Types (int)
|
|
289
|
+
:type outputs: ``list``
|
|
290
|
+
:param input_text: list of Socket Types (int)
|
|
291
|
+
:type input_text: ``list``
|
|
292
|
+
:param output_text: list of Socket Types (int)
|
|
293
|
+
:type output_text: ``list``
|
|
294
|
+
:param reset: if ``True`` destroys and removes old `Sockets`
|
|
295
|
+
:type reset: ``bool``
|
|
296
|
+
"""
|
|
297
|
+
|
|
298
|
+
# Initialize empty lists if None provided
|
|
299
|
+
input_text = input_text or []
|
|
300
|
+
output_text = output_text or []
|
|
301
|
+
|
|
302
|
+
if reset:
|
|
303
|
+
# clear old sockets
|
|
304
|
+
if hasattr(self, 'inputs') and hasattr(self, 'outputs'):
|
|
305
|
+
# remove grSockets from scene
|
|
306
|
+
for socket in (self.inputs + self.outputs):
|
|
307
|
+
self.scene.grScene.removeItem(socket.grSocket)
|
|
308
|
+
self.inputs = []
|
|
309
|
+
self.outputs = []
|
|
310
|
+
|
|
311
|
+
# create new sockets
|
|
312
|
+
for i, item in enumerate(inputs):
|
|
313
|
+
# Get text if available, otherwise empty string
|
|
314
|
+
text = input_text[i] if i < len(input_text) else ""
|
|
315
|
+
socket = self.__class__.Socket_class(
|
|
316
|
+
node=self,
|
|
317
|
+
index=i,
|
|
318
|
+
position=self.input_socket_position,
|
|
319
|
+
socket_type=item,
|
|
320
|
+
multi_edges=self.input_multi_edged,
|
|
321
|
+
count_on_this_node_side=len(inputs),
|
|
322
|
+
is_input=True
|
|
323
|
+
)
|
|
324
|
+
socket.grSocket.setText(text)
|
|
325
|
+
self.inputs.append(socket)
|
|
326
|
+
|
|
327
|
+
for i, item in enumerate(outputs):
|
|
328
|
+
# Get text if available, otherwise empty string
|
|
329
|
+
text = output_text[i] if i < len(output_text) else ""
|
|
330
|
+
socket = self.__class__.Socket_class(
|
|
331
|
+
node=self,
|
|
332
|
+
index=i,
|
|
333
|
+
position=self.output_socket_position,
|
|
334
|
+
socket_type=item,
|
|
335
|
+
multi_edges=self.output_multi_edged,
|
|
336
|
+
count_on_this_node_side=len(outputs),
|
|
337
|
+
is_input=False
|
|
338
|
+
)
|
|
339
|
+
socket.grSocket.setText(text)
|
|
340
|
+
self.outputs.append(socket)
|
|
341
|
+
|
|
342
|
+
def onEdgeConnectionChanged(self, new_edge: 'Edge') -> None:
|
|
343
|
+
"""
|
|
344
|
+
Event handling that any connection (`Edge`) has changed. Currently not used...
|
|
345
|
+
|
|
346
|
+
:param new_edge: reference to the changed :class:`~nodeeditor.node_edge.Edge`
|
|
347
|
+
:type new_edge: :class:`~nodeeditor.node_edge.Edge`
|
|
348
|
+
"""
|
|
349
|
+
pass
|
|
350
|
+
|
|
351
|
+
def onInputChanged(self, socket: 'Socket') -> None:
|
|
352
|
+
"""Event handling when Node's input Edge has changed. We auto-mark this `Node` to be `Dirty` with all it's
|
|
353
|
+
descendants
|
|
354
|
+
|
|
355
|
+
:param socket: reference to the changed :class:`~nodeeditor.node_socket.Socket`
|
|
356
|
+
:type socket: :class:`~nodeeditor.node_socket.Socket`
|
|
357
|
+
"""
|
|
358
|
+
self.markDirty()
|
|
359
|
+
self.markDescendantsDirty()
|
|
360
|
+
|
|
361
|
+
def onDeserialized(self, data: dict) -> None:
|
|
362
|
+
"""Event manually called when this node was deserialized. Currently called when node is deserialized from scene
|
|
363
|
+
Passing `data` containing the data which have been deserialized """
|
|
364
|
+
pass
|
|
365
|
+
|
|
366
|
+
def onDoubleClicked(self, event) -> None:
|
|
367
|
+
"""Event handling double click on Graphics Node in `Scene`"""
|
|
368
|
+
pass
|
|
369
|
+
|
|
370
|
+
def doSelect(self, new_state: bool = True) -> None:
|
|
371
|
+
"""Shortcut method for selecting/deselecting the `Node`
|
|
372
|
+
|
|
373
|
+
:param new_state: ``True`` if you want to select the `Node`. ``False`` if you want to deselect the `Node`
|
|
374
|
+
:type new_state: ``bool``
|
|
375
|
+
"""
|
|
376
|
+
self.grNode.doSelect(new_state)
|
|
377
|
+
|
|
378
|
+
def isSelected(self):
|
|
379
|
+
"""Returns ``True`` if current `Node` is selected"""
|
|
380
|
+
return self.grNode.isSelected()
|
|
381
|
+
|
|
382
|
+
def hasConnectedEdge(self, edge: 'Edge') -> bool:
|
|
383
|
+
"""Returns ``True`` if edge is connected to any :class:`~nodeeditor.node_socket.Socket` of this `Node`"""
|
|
384
|
+
for socket in (self.inputs + self.outputs):
|
|
385
|
+
if socket.isConnected(edge):
|
|
386
|
+
return True
|
|
387
|
+
return False
|
|
388
|
+
|
|
389
|
+
def getSocketPosition(self, index: int, position: int, num_out_of: int = 1) -> List[float]:
|
|
390
|
+
"""
|
|
391
|
+
Get the relative `x, y` position of a :class:`~nodeeditor.node_socket.Socket`. This is used for placing
|
|
392
|
+
the `Graphics Sockets` on `Graphics Node`.
|
|
393
|
+
|
|
394
|
+
:param index: Order number of the Socket. (0, 1, 2, ...)
|
|
395
|
+
:type index: ``int``
|
|
396
|
+
:param position: `Socket Position Constant` describing where the Socket is located. See :ref:`socket-position-constants`
|
|
397
|
+
:type position: ``int``
|
|
398
|
+
:param num_out_of: Total number of Sockets on this `Socket Position`
|
|
399
|
+
:type num_out_of: ``int``
|
|
400
|
+
:return: Position of described Socket on the `Node`
|
|
401
|
+
:rtype: ``x, y``
|
|
402
|
+
"""
|
|
403
|
+
x = self.socket_offsets[position] if (position in (
|
|
404
|
+
LEFT_TOP, LEFT_CENTER, LEFT_BOTTOM)) else self.grNode.width + self.socket_offsets[position]
|
|
405
|
+
|
|
406
|
+
if position in (LEFT_BOTTOM, RIGHT_BOTTOM):
|
|
407
|
+
# start from bottom
|
|
408
|
+
y = self.grNode.height - self.grNode.edge_roundness - \
|
|
409
|
+
self.grNode.title_vertical_padding - index * self.socket_spacing
|
|
410
|
+
elif position in (LEFT_CENTER, RIGHT_CENTER):
|
|
411
|
+
num_sockets = num_out_of
|
|
412
|
+
node_height = self.grNode.height
|
|
413
|
+
top_offset = self.grNode.title_height + 2 * \
|
|
414
|
+
self.grNode.title_vertical_padding + self.grNode.edge_padding
|
|
415
|
+
available_height = node_height - top_offset
|
|
416
|
+
|
|
417
|
+
total_height_of_all_sockets = num_sockets * self.socket_spacing
|
|
418
|
+
new_top = available_height - total_height_of_all_sockets
|
|
419
|
+
|
|
420
|
+
# y = top_offset + index * self.socket_spacing + new_top / 2
|
|
421
|
+
y = top_offset + available_height / \
|
|
422
|
+
2.0 + (index-0.5)*self.socket_spacing
|
|
423
|
+
if num_sockets > 1:
|
|
424
|
+
y -= self.socket_spacing * (num_sockets-1)/2
|
|
425
|
+
|
|
426
|
+
elif position in (LEFT_TOP, RIGHT_TOP):
|
|
427
|
+
# start from top
|
|
428
|
+
y = self.grNode.title_height + self.grNode.title_vertical_padding + \
|
|
429
|
+
self.grNode.edge_roundness + index * self.socket_spacing
|
|
430
|
+
else:
|
|
431
|
+
# this should never happen
|
|
432
|
+
y = 0
|
|
433
|
+
|
|
434
|
+
return [x, y]
|
|
435
|
+
|
|
436
|
+
def getSocketScenePosition(self, socket: 'Socket') -> Tuple[float, float]:
|
|
437
|
+
"""
|
|
438
|
+
Get absolute Socket position in the Scene
|
|
439
|
+
|
|
440
|
+
:param socket: `Socket` which position we want to know
|
|
441
|
+
:return: (x, y) Socket's scene position
|
|
442
|
+
"""
|
|
443
|
+
nodepos = self.grNode.pos()
|
|
444
|
+
socketpos = self.getSocketPosition(
|
|
445
|
+
socket.index, socket.position, socket.count_on_this_node_side)
|
|
446
|
+
return (nodepos.x() + socketpos[0], nodepos.y() + socketpos[1])
|
|
447
|
+
|
|
448
|
+
def updateConnectedEdges(self) -> None:
|
|
449
|
+
"""Recalculate (Refresh) positions of all connected `Edges`. Used for updating Graphics Edges"""
|
|
450
|
+
for socket in self.inputs + self.outputs:
|
|
451
|
+
# if socket.hasEdge():
|
|
452
|
+
for edge in socket.edges:
|
|
453
|
+
edge.updatePositions()
|
|
454
|
+
|
|
455
|
+
def remove(self) -> None:
|
|
456
|
+
"""
|
|
457
|
+
Safely remove this Node (detaches from parent Group first)
|
|
458
|
+
"""
|
|
459
|
+
# detach from group first so bounds/stubs update without this node
|
|
460
|
+
try:
|
|
461
|
+
grp = getattr(self, 'parent_group', None)
|
|
462
|
+
if grp is not None:
|
|
463
|
+
try:
|
|
464
|
+
grp.removeNode(self, update_bounds=False)
|
|
465
|
+
except Exception:
|
|
466
|
+
try:
|
|
467
|
+
self.parent_group = None
|
|
468
|
+
except Exception:
|
|
469
|
+
pass
|
|
470
|
+
except Exception:
|
|
471
|
+
pass
|
|
472
|
+
if DEBUG:
|
|
473
|
+
print("> Removing Node", self)
|
|
474
|
+
if DEBUG:
|
|
475
|
+
print(" - remove all edges from sockets")
|
|
476
|
+
for socket in (self.inputs+self.outputs):
|
|
477
|
+
# if socket.hasEdge():
|
|
478
|
+
for edge in socket.edges.copy():
|
|
479
|
+
if DEBUG:
|
|
480
|
+
print(" - removing from socket:", socket, "edge:", edge)
|
|
481
|
+
edge.remove()
|
|
482
|
+
if DEBUG:
|
|
483
|
+
print(" - remove grNode")
|
|
484
|
+
try:
|
|
485
|
+
if self.grNode is not None:
|
|
486
|
+
self.scene.grScene.removeItem(self.grNode)
|
|
487
|
+
except Exception:
|
|
488
|
+
pass
|
|
489
|
+
self.grNode = None
|
|
490
|
+
if DEBUG:
|
|
491
|
+
print(" - remove node from the scene")
|
|
492
|
+
self.scene.removeNode(self)
|
|
493
|
+
if DEBUG:
|
|
494
|
+
print(" - everything was done.")
|
|
495
|
+
|
|
496
|
+
# node evaluation stuff
|
|
497
|
+
|
|
498
|
+
def isDirty(self) -> bool:
|
|
499
|
+
"""Is this node marked as `Dirty`
|
|
500
|
+
|
|
501
|
+
:return: ``True`` if `Node` is marked as `Dirty`
|
|
502
|
+
:rtype: ``bool``
|
|
503
|
+
"""
|
|
504
|
+
return self._is_dirty
|
|
505
|
+
|
|
506
|
+
def markDirty(self, new_value: bool = True) -> None:
|
|
507
|
+
"""Mark this `Node` as `Dirty`. See :ref:`evaluation` for more
|
|
508
|
+
|
|
509
|
+
:param new_value: ``True`` if this `Node` should be `Dirty`. ``False`` if you want to un-dirty this `Node`
|
|
510
|
+
:type new_value: ``bool``
|
|
511
|
+
"""
|
|
512
|
+
self._is_dirty = new_value
|
|
513
|
+
if self._is_dirty:
|
|
514
|
+
self.onMarkedDirty()
|
|
515
|
+
|
|
516
|
+
def onMarkedDirty(self) -> None:
|
|
517
|
+
"""Called when this `Node` has been marked as `Dirty`. This method is supposed to be overridden"""
|
|
518
|
+
pass
|
|
519
|
+
|
|
520
|
+
def markChildrenDirty(self, new_value: bool = True) -> None:
|
|
521
|
+
"""Mark all first level children of this `Node` to be `Dirty`. Not this `Node` it self. Not other descendants
|
|
522
|
+
|
|
523
|
+
:param new_value: ``True`` if children should be `Dirty`. ``False`` if you want to un-dirty children
|
|
524
|
+
:type new_value: ``bool``
|
|
525
|
+
"""
|
|
526
|
+
for other_node in self.getChildrenNodes():
|
|
527
|
+
other_node.markDirty(new_value)
|
|
528
|
+
|
|
529
|
+
def markDescendantsDirty(self, new_value: bool = True) -> None:
|
|
530
|
+
"""Mark all children and descendants of this `Node` to be `Dirty`. Not this `Node` it self
|
|
531
|
+
|
|
532
|
+
The traversal is iterative and keeps a visited set, so a graph with a
|
|
533
|
+
cycle terminates instead of exhausting the interpreter's recursion
|
|
534
|
+
limit, and a node reachable through several paths is visited once
|
|
535
|
+
rather than once per path.
|
|
536
|
+
|
|
537
|
+
:param new_value: ``True`` if children and descendants should be `Dirty`. ``False`` if you want to un-dirty children and descendants
|
|
538
|
+
:type new_value: ``bool``
|
|
539
|
+
"""
|
|
540
|
+
for other_node in node_graph.descendants(self.scene, self):
|
|
541
|
+
other_node.markDirty(new_value)
|
|
542
|
+
|
|
543
|
+
def isInvalid(self) -> bool:
|
|
544
|
+
"""Is this node marked as `Invalid`?
|
|
545
|
+
|
|
546
|
+
:return: ``True`` if `Node` is marked as `Invalid`
|
|
547
|
+
:rtype: ``bool``
|
|
548
|
+
"""
|
|
549
|
+
return self._is_invalid
|
|
550
|
+
|
|
551
|
+
def markInvalid(self, new_value: bool = True) -> None:
|
|
552
|
+
"""Mark this `Node` as `Invalid`. See :ref:`evaluation` for more
|
|
553
|
+
|
|
554
|
+
:param new_value: ``True`` if this `Node` should be `Invalid`. ``False`` if you want to make this `Node` valid
|
|
555
|
+
:type new_value: ``bool``
|
|
556
|
+
"""
|
|
557
|
+
self._is_invalid = new_value
|
|
558
|
+
if self._is_invalid:
|
|
559
|
+
self.onMarkedInvalid()
|
|
560
|
+
|
|
561
|
+
def onMarkedInvalid(self) -> None:
|
|
562
|
+
"""Called when this `Node` has been marked as `Invalid`. This method is supposed to be overridden"""
|
|
563
|
+
pass
|
|
564
|
+
|
|
565
|
+
def markChildrenInvalid(self, new_value: bool = True) -> None:
|
|
566
|
+
"""Mark all first level children of this `Node` to be `Invalid`. Not this `Node` it self. Not other descendants
|
|
567
|
+
|
|
568
|
+
:param new_value: ``True`` if children should be `Invalid`. ``False`` if you want to make children valid
|
|
569
|
+
:type new_value: ``bool``
|
|
570
|
+
"""
|
|
571
|
+
for other_node in self.getChildrenNodes():
|
|
572
|
+
other_node.markInvalid(new_value)
|
|
573
|
+
|
|
574
|
+
def markDescendantsInvalid(self, new_value: bool = True) -> None:
|
|
575
|
+
"""Mark all children and descendants of this `Node` to be `Invalid`. Not this `Node` it self
|
|
576
|
+
|
|
577
|
+
The traversal is iterative and keeps a visited set, so a graph with a
|
|
578
|
+
cycle terminates instead of exhausting the interpreter's recursion
|
|
579
|
+
limit, and a node reachable through several paths is visited once
|
|
580
|
+
rather than once per path.
|
|
581
|
+
|
|
582
|
+
:param new_value: ``True`` if children and descendants should be `Invalid`. ``False`` if you want to make children and descendants valid
|
|
583
|
+
:type new_value: ``bool``
|
|
584
|
+
"""
|
|
585
|
+
for other_node in node_graph.descendants(self.scene, self):
|
|
586
|
+
other_node.markInvalid(new_value)
|
|
587
|
+
|
|
588
|
+
def eval(self, index: int = 0) -> int:
|
|
589
|
+
"""Evaluate this `Node`. This is supposed to be overridden. See :ref:`evaluation` for more"""
|
|
590
|
+
self.markDirty(False)
|
|
591
|
+
self.markInvalid(False)
|
|
592
|
+
return 0
|
|
593
|
+
|
|
594
|
+
def evalChildren(self) -> None:
|
|
595
|
+
"""Evaluate all children of this `Node`"""
|
|
596
|
+
for node in self.getChildrenNodes():
|
|
597
|
+
node.eval()
|
|
598
|
+
|
|
599
|
+
# traversing nodes functions
|
|
600
|
+
|
|
601
|
+
def getChildrenNodes(self) -> 'List[Node]':
|
|
602
|
+
"""
|
|
603
|
+
Retreive all first-level children connected to this `Node` `Outputs`
|
|
604
|
+
|
|
605
|
+
:return: list of `Nodes` connected to this `Node` from all `Outputs`
|
|
606
|
+
:rtype: List[:class:`~nodeeditor.node_node.Node`]
|
|
607
|
+
"""
|
|
608
|
+
if self.outputs == []:
|
|
609
|
+
return []
|
|
610
|
+
other_nodes = []
|
|
611
|
+
for ix in range(len(self.outputs)):
|
|
612
|
+
for edge in self.outputs[ix].edges:
|
|
613
|
+
other_node = edge.getOtherSocket(self.outputs[ix]).node
|
|
614
|
+
other_nodes.append(other_node)
|
|
615
|
+
return other_nodes
|
|
616
|
+
|
|
617
|
+
def getInput(self, index: int = 0) -> Optional['Node']:
|
|
618
|
+
"""
|
|
619
|
+
Get the **first** `Node` connected to the Input specified by `index`
|
|
620
|
+
|
|
621
|
+
:param index: Order number of the `Input Socket`
|
|
622
|
+
:type index: ``int``
|
|
623
|
+
:return: :class:`~nodeeditor.node_node.Node` which is connected to the specified `Input` or ``None`` if
|
|
624
|
+
there is no connection or the index is out of range
|
|
625
|
+
:rtype: :class:`~nodeeditor.node_node.Node` or ``None``
|
|
626
|
+
"""
|
|
627
|
+
try:
|
|
628
|
+
input_socket = self.inputs[index]
|
|
629
|
+
if len(input_socket.edges) == 0:
|
|
630
|
+
return None
|
|
631
|
+
connecting_edge = input_socket.edges[0]
|
|
632
|
+
other_socket = connecting_edge.getOtherSocket(self.inputs[index])
|
|
633
|
+
return other_socket.node
|
|
634
|
+
except Exception as e:
|
|
635
|
+
dumpException(e)
|
|
636
|
+
return None
|
|
637
|
+
|
|
638
|
+
def getInputWithSocket(self, index: int = 0) -> Union[Tuple['Node', 'Socket'], Tuple[None, None]]:
|
|
639
|
+
"""
|
|
640
|
+
Get the **first** `Node` connected to the Input specified by `index` and the connection `Socket`
|
|
641
|
+
|
|
642
|
+
:param index: Order number of the `Input Socket`
|
|
643
|
+
:type index: ``int``
|
|
644
|
+
:return: Tuple containing :class:`~nodeeditor.node_node.Node` and :class:`~nodeeditor.node_socket.Socket` which
|
|
645
|
+
is connected to the specified `Input` or ``None`` if there is no connection or the index is out of range
|
|
646
|
+
:rtype: (:class:`~nodeeditor.node_node.Node`, :class:`~nodeeditor.node_socket.Socket`)
|
|
647
|
+
"""
|
|
648
|
+
try:
|
|
649
|
+
input_socket = self.inputs[index]
|
|
650
|
+
if len(input_socket.edges) == 0:
|
|
651
|
+
return None, None
|
|
652
|
+
connecting_edge = input_socket.edges[0]
|
|
653
|
+
other_socket = connecting_edge.getOtherSocket(self.inputs[index])
|
|
654
|
+
return other_socket.node, other_socket
|
|
655
|
+
except Exception as e:
|
|
656
|
+
dumpException(e)
|
|
657
|
+
return None, None
|
|
658
|
+
|
|
659
|
+
def getInputWithSocketIndex(self, index: int = 0) -> Union[Tuple['Node', int], Tuple[None, None]]:
|
|
660
|
+
"""
|
|
661
|
+
Get the **first** `Node` connected to the Input specified by `index` and the connection `Socket`
|
|
662
|
+
|
|
663
|
+
:param index: Order number of the `Input Socket`
|
|
664
|
+
:type index: ``int``
|
|
665
|
+
:return: Tuple containing :class:`~nodeeditor.node_node.Node` and :class:`~nodeeditor.node_socket.Socket` which
|
|
666
|
+
is connected to the specified `Input` or ``None`` if there is no connection or the index is out of range
|
|
667
|
+
:rtype: (:class:`~nodeeditor.node_node.Node`, int)
|
|
668
|
+
"""
|
|
669
|
+
try:
|
|
670
|
+
edge = self.inputs[index].edges[0]
|
|
671
|
+
socket = edge.getOtherSocket(self.inputs[index])
|
|
672
|
+
return socket.node, socket.index
|
|
673
|
+
except IndexError:
|
|
674
|
+
# print("EXC: Trying to get input with socket index %d, but none is attached to" % index, self)
|
|
675
|
+
return None, None
|
|
676
|
+
except Exception as e:
|
|
677
|
+
dumpException(e)
|
|
678
|
+
return None, None
|
|
679
|
+
|
|
680
|
+
def getInputs(self, index: int = 0) -> 'List[Node]':
|
|
681
|
+
"""
|
|
682
|
+
Get **all** `Nodes` connected to the Input specified by `index`
|
|
683
|
+
|
|
684
|
+
:param index: Order number of the `Input Socket`
|
|
685
|
+
:type index: ``int``
|
|
686
|
+
:return: all :class:`~nodeeditor.node_node.Node` instances which are connected to the
|
|
687
|
+
specified `Input` or ``[]`` if there is no connection or the index is out of range
|
|
688
|
+
:rtype: List[:class:`~nodeeditor.node_node.Node`]
|
|
689
|
+
"""
|
|
690
|
+
ins = []
|
|
691
|
+
for edge in self.inputs[index].edges:
|
|
692
|
+
other_socket = edge.getOtherSocket(self.inputs[index])
|
|
693
|
+
ins.append(other_socket.node)
|
|
694
|
+
return ins
|
|
695
|
+
|
|
696
|
+
def getOutputs(self, index: int = 0) -> 'List[Node]':
|
|
697
|
+
"""
|
|
698
|
+
Get **all** `Nodes` connected to the Output specified by `index`
|
|
699
|
+
|
|
700
|
+
:param index: Order number of the `Output Socket`
|
|
701
|
+
:type index: ``int``
|
|
702
|
+
:return: all :class:`~nodeeditor.node_node.Node` instances which are connected to the
|
|
703
|
+
specified `Output` or ``[]`` if there is no connection or the index is out of range
|
|
704
|
+
:rtype: List[:class:`~nodeeditor.node_node.Node`]
|
|
705
|
+
"""
|
|
706
|
+
outs = []
|
|
707
|
+
for edge in self.outputs[index].edges:
|
|
708
|
+
other_socket = edge.getOtherSocket(self.outputs[index])
|
|
709
|
+
outs.append(other_socket.node)
|
|
710
|
+
return outs
|
|
711
|
+
|
|
712
|
+
# serialization functions
|
|
713
|
+
|
|
714
|
+
def serialize(self) -> OrderedDict:
|
|
715
|
+
"""
|
|
716
|
+
Serialize this `Node` into an ``OrderedDict`` for storing in memory or on disk.
|
|
717
|
+
|
|
718
|
+
:return: data serialized in ``OrderedDict``
|
|
719
|
+
:rtype: ``OrderedDict``
|
|
720
|
+
"""
|
|
721
|
+
inputs, outputs = [], []
|
|
722
|
+
for socket in self.inputs:
|
|
723
|
+
inputs.append(socket.serialize())
|
|
724
|
+
for socket in self.outputs:
|
|
725
|
+
outputs.append(socket.serialize())
|
|
726
|
+
ser_content = self.content.serialize() if isinstance(
|
|
727
|
+
self.content, Serializable) else {}
|
|
728
|
+
try:
|
|
729
|
+
label_text = self.getNodeLabel()
|
|
730
|
+
except Exception:
|
|
731
|
+
label_text = ""
|
|
732
|
+
try:
|
|
733
|
+
label_visible = self.isNodeLabelVisible() if label_text else False
|
|
734
|
+
except Exception:
|
|
735
|
+
label_visible = False
|
|
736
|
+
try:
|
|
737
|
+
label_offset = float(self.grNode.labelOffset()) if self.grNode is not None and hasattr(self.grNode, 'labelOffset') else 10.0
|
|
738
|
+
except Exception:
|
|
739
|
+
label_offset = 10.0
|
|
740
|
+
return OrderedDict([
|
|
741
|
+
('id', self.id),
|
|
742
|
+
('title', self.title),
|
|
743
|
+
('pos_x', self.grNode.scenePos().x()),
|
|
744
|
+
('pos_y', self.grNode.scenePos().y()),
|
|
745
|
+
('inputs', inputs),
|
|
746
|
+
('outputs', outputs),
|
|
747
|
+
('content', ser_content),
|
|
748
|
+
('label_text', label_text),
|
|
749
|
+
('label_visible', label_visible),
|
|
750
|
+
('label_offset', label_offset),
|
|
751
|
+
])
|
|
752
|
+
|
|
753
|
+
def deserialize(self, data: dict, hashmap: dict = {}, restore_id: bool = True, *args, **kwargs) -> bool:
|
|
754
|
+
"""
|
|
755
|
+
Deserialize this `Node` from a ``dict`` of serialized data.
|
|
756
|
+
|
|
757
|
+
Sockets missing in the data are created, sockets already present are reused.
|
|
758
|
+
|
|
759
|
+
:param data: Dictionary containing serialized data
|
|
760
|
+
:type data: ``dict``
|
|
761
|
+
:param hashmap: Helper dictionary containing references (by id == key) to existing objects
|
|
762
|
+
:type hashmap: ``dict``
|
|
763
|
+
:param restore_id: True if we are creating new Sockets. False is useful when loading existing
|
|
764
|
+
Sockets of which we want to keep the existing object's `id`.
|
|
765
|
+
:type restore_id: bool
|
|
766
|
+
:return: ``True`` if deserialization was successful, otherwise ``False``
|
|
767
|
+
:rtype: ``bool``
|
|
768
|
+
"""
|
|
769
|
+
try:
|
|
770
|
+
if restore_id:
|
|
771
|
+
self.id = data['id']
|
|
772
|
+
hashmap[data['id']] = self
|
|
773
|
+
|
|
774
|
+
self.setPos(data['pos_x'], data['pos_y'])
|
|
775
|
+
self.title = data['title']
|
|
776
|
+
|
|
777
|
+
# floating label (backward compatible: old files have no keys)
|
|
778
|
+
try:
|
|
779
|
+
if self.grNode is not None and hasattr(self.grNode, 'setLabelText'):
|
|
780
|
+
self.grNode.setLabelOffset(float(data.get('label_offset', 10.0)))
|
|
781
|
+
self.grNode.setLabelText(data.get('label_text', ""))
|
|
782
|
+
self.grNode.setLabelVisible(bool(data.get('label_visible', bool(data.get('label_text', "")))))
|
|
783
|
+
except Exception:
|
|
784
|
+
pass
|
|
785
|
+
|
|
786
|
+
data['inputs'].sort(
|
|
787
|
+
key=lambda socket: socket['index'] + socket['position'] * 10000)
|
|
788
|
+
data['outputs'].sort(
|
|
789
|
+
key=lambda socket: socket['index'] + socket['position'] * 10000)
|
|
790
|
+
num_inputs = len(data['inputs'])
|
|
791
|
+
num_outputs = len(data['outputs'])
|
|
792
|
+
|
|
793
|
+
# print("> deserialize node, num inputs:", num_inputs, "num outputs:", num_outputs)
|
|
794
|
+
# pp(data)
|
|
795
|
+
|
|
796
|
+
# possible way to do it is reuse existing sockets...
|
|
797
|
+
# dont create new ones if not necessary
|
|
798
|
+
|
|
799
|
+
for socket_data in data['inputs']:
|
|
800
|
+
found = None
|
|
801
|
+
for socket in self.inputs:
|
|
802
|
+
# print("\t", socket, socket.index, "=?", socket_data['index'])
|
|
803
|
+
if socket.index == socket_data['index']:
|
|
804
|
+
found = socket
|
|
805
|
+
break
|
|
806
|
+
if found is None:
|
|
807
|
+
# print("deserialization of socket data has not found input socket with index:", socket_data['index'])
|
|
808
|
+
# print("actual socket data:", socket_data)
|
|
809
|
+
# we can create new socket for this
|
|
810
|
+
found = self.__class__.Socket_class(
|
|
811
|
+
node=self, index=socket_data['index'], position=socket_data['position'],
|
|
812
|
+
socket_type=socket_data['socket_type'], count_on_this_node_side=num_inputs,
|
|
813
|
+
is_input=True
|
|
814
|
+
)
|
|
815
|
+
# append newly created input to the list
|
|
816
|
+
self.inputs.append(found)
|
|
817
|
+
found.deserialize(socket_data, hashmap, restore_id)
|
|
818
|
+
|
|
819
|
+
for socket_data in data['outputs']:
|
|
820
|
+
found = None
|
|
821
|
+
for socket in self.outputs:
|
|
822
|
+
# print("\t", socket, socket.index, "=?", socket_data['index'])
|
|
823
|
+
if socket.index == socket_data['index']:
|
|
824
|
+
found = socket
|
|
825
|
+
break
|
|
826
|
+
if found is None:
|
|
827
|
+
# print("deserialization of socket data has not found output socket with index:", socket_data['index'])
|
|
828
|
+
# print("actual socket data:", socket_data)
|
|
829
|
+
# we can create new socket for this
|
|
830
|
+
found = self.__class__.Socket_class(
|
|
831
|
+
node=self, index=socket_data['index'], position=socket_data['position'],
|
|
832
|
+
socket_type=socket_data['socket_type'], count_on_this_node_side=num_outputs,
|
|
833
|
+
is_input=False
|
|
834
|
+
)
|
|
835
|
+
# append newly created output to the list
|
|
836
|
+
self.outputs.append(found)
|
|
837
|
+
found.deserialize(socket_data, hashmap, restore_id)
|
|
838
|
+
|
|
839
|
+
except Exception as e:
|
|
840
|
+
dumpException(e)
|
|
841
|
+
|
|
842
|
+
# also deserialize the content of the node
|
|
843
|
+
# so far the rest was ok, now as last step the content...
|
|
844
|
+
if isinstance(self.content, Serializable):
|
|
845
|
+
res = self.content.deserialize(data['content'], hashmap)
|
|
846
|
+
return res
|
|
847
|
+
|
|
848
|
+
return True
|