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,314 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
A module containing all code for working with Clipboard
|
|
4
|
+
"""
|
|
5
|
+
from collections import OrderedDict
|
|
6
|
+
from nodeeditor.node_graphics_edge import QDMGraphicsEdge
|
|
7
|
+
from nodeeditor.node_edge import Edge
|
|
8
|
+
|
|
9
|
+
from typing import TYPE_CHECKING, List, Optional, Tuple, Any, Callable
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from nodeeditor.node_graphics_view import QDMGraphicsView
|
|
14
|
+
from nodeeditor.node_socket import Socket
|
|
15
|
+
from nodeeditor.node_scene import Scene
|
|
16
|
+
from nodeeditor.node_node import Node
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
DEBUG = False
|
|
20
|
+
DEBUG_PASTING = False
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SceneClipboard():
|
|
24
|
+
"""
|
|
25
|
+
Class contains all the code for serialization/deserialization from Clipboard
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, scene: 'Scene') -> None:
|
|
29
|
+
"""
|
|
30
|
+
:param scene: Reference to the :class:`~nodeeditor.node_scene.Scene`
|
|
31
|
+
:type scene: :class:`~nodeeditor.node_scene.Scene`
|
|
32
|
+
|
|
33
|
+
:Instance Attributes:
|
|
34
|
+
|
|
35
|
+
- **scene** - reference to the :class:`~nodeeditor.node_scene.Scene`
|
|
36
|
+
"""
|
|
37
|
+
self.scene = scene
|
|
38
|
+
|
|
39
|
+
def serializeSelected(self, delete: bool = False) -> OrderedDict:
|
|
40
|
+
"""
|
|
41
|
+
Serializes selected items in the Scene into ``OrderedDict``
|
|
42
|
+
|
|
43
|
+
:param delete: True if you want to delete selected items after serialization. Useful for Cut operation
|
|
44
|
+
:type delete: ``bool``
|
|
45
|
+
:return: Serialized data of current selection in NodeEditor :class:`~nodeeditor.node_scene.Scene`
|
|
46
|
+
"""
|
|
47
|
+
if DEBUG:
|
|
48
|
+
print("-- COPY TO CLIPBOARD ---")
|
|
49
|
+
|
|
50
|
+
sel_nodes, sel_edges, sel_sockets = [], [], {}
|
|
51
|
+
sel_node_ids = set()
|
|
52
|
+
sel_groups = []
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
from nodeeditor.node_group import Group
|
|
56
|
+
except Exception:
|
|
57
|
+
Group = None # type: ignore
|
|
58
|
+
|
|
59
|
+
# sort edges, nodes, and groups (Group has neither .node nor .edge)
|
|
60
|
+
for item in self.scene.grScene.selectedItems():
|
|
61
|
+
try:
|
|
62
|
+
if Group is not None and isinstance(item, Group):
|
|
63
|
+
if item not in sel_groups:
|
|
64
|
+
sel_groups.append(item)
|
|
65
|
+
continue
|
|
66
|
+
except Exception:
|
|
67
|
+
pass
|
|
68
|
+
if hasattr(item, 'node'):
|
|
69
|
+
try:
|
|
70
|
+
if getattr(item.node, 'id', None) in sel_node_ids:
|
|
71
|
+
continue
|
|
72
|
+
sel_nodes.append(item.node.serialize())
|
|
73
|
+
sel_node_ids.add(item.node.id)
|
|
74
|
+
for socket in (item.node.inputs + item.node.outputs):
|
|
75
|
+
sel_sockets[socket.id] = socket
|
|
76
|
+
except Exception:
|
|
77
|
+
continue
|
|
78
|
+
elif isinstance(item, QDMGraphicsEdge):
|
|
79
|
+
sel_edges.append(item.edge)
|
|
80
|
+
|
|
81
|
+
# wholesale: header-selected group pulls in all children + internal edges
|
|
82
|
+
try:
|
|
83
|
+
for grp in list(sel_groups):
|
|
84
|
+
try:
|
|
85
|
+
for node in list(getattr(grp, 'child_nodes', [])):
|
|
86
|
+
try:
|
|
87
|
+
if getattr(node, 'id', None) in sel_node_ids:
|
|
88
|
+
continue
|
|
89
|
+
sel_nodes.append(node.serialize())
|
|
90
|
+
sel_node_ids.add(node.id)
|
|
91
|
+
for socket in (node.inputs + node.outputs):
|
|
92
|
+
sel_sockets[socket.id] = socket
|
|
93
|
+
except Exception:
|
|
94
|
+
continue
|
|
95
|
+
except Exception:
|
|
96
|
+
continue
|
|
97
|
+
# auto-include internal edges of selected groups (both ends now present)
|
|
98
|
+
seen_edge_ids = set(id(e) for e in sel_edges)
|
|
99
|
+
for grp in list(sel_groups):
|
|
100
|
+
try:
|
|
101
|
+
_, _, internal = grp._classifyEdges()
|
|
102
|
+
except Exception:
|
|
103
|
+
internal = []
|
|
104
|
+
for e in list(internal or []):
|
|
105
|
+
try:
|
|
106
|
+
if id(e) not in seen_edge_ids:
|
|
107
|
+
sel_edges.append(e)
|
|
108
|
+
seen_edge_ids.add(id(e))
|
|
109
|
+
except Exception:
|
|
110
|
+
continue
|
|
111
|
+
except Exception:
|
|
112
|
+
pass
|
|
113
|
+
|
|
114
|
+
# debug
|
|
115
|
+
if DEBUG:
|
|
116
|
+
print(" NODES\n ", sel_nodes)
|
|
117
|
+
print(" EDGES\n ", sel_edges)
|
|
118
|
+
print(" SOCKETS\n ", sel_sockets)
|
|
119
|
+
|
|
120
|
+
# remove all edges which are not connected to a nodeeditor in our list
|
|
121
|
+
edges_to_remove = []
|
|
122
|
+
for edge in sel_edges:
|
|
123
|
+
if edge.start_socket.id in sel_sockets and edge.end_socket.id in sel_sockets:
|
|
124
|
+
# if DEBUG: print(" edge is ok, connected with both sides")
|
|
125
|
+
pass
|
|
126
|
+
else:
|
|
127
|
+
if DEBUG:
|
|
128
|
+
print("edge", edge, "is not connected with both sides")
|
|
129
|
+
edges_to_remove.append(edge)
|
|
130
|
+
for edge in edges_to_remove:
|
|
131
|
+
sel_edges.remove(edge)
|
|
132
|
+
|
|
133
|
+
# make final list of edges
|
|
134
|
+
edges_final = []
|
|
135
|
+
for edge in sel_edges:
|
|
136
|
+
edges_final.append(edge.serialize())
|
|
137
|
+
|
|
138
|
+
if DEBUG:
|
|
139
|
+
print("our final edge list:", edges_final)
|
|
140
|
+
|
|
141
|
+
# groups: header-selected groups always included (children auto-pulled above);
|
|
142
|
+
# otherwise include a group iff ALL its children are selected (rubber-band case).
|
|
143
|
+
groups_final = []
|
|
144
|
+
try:
|
|
145
|
+
included = set()
|
|
146
|
+
for grp in list(sel_groups):
|
|
147
|
+
try:
|
|
148
|
+
groups_final.append(grp.serialize())
|
|
149
|
+
included.add(id(grp))
|
|
150
|
+
except Exception:
|
|
151
|
+
continue
|
|
152
|
+
for grp in list(getattr(self.scene, 'groups', [])):
|
|
153
|
+
try:
|
|
154
|
+
if id(grp) in included:
|
|
155
|
+
continue
|
|
156
|
+
children = list(getattr(grp, 'child_nodes', []))
|
|
157
|
+
if not children:
|
|
158
|
+
continue
|
|
159
|
+
if all(getattr(n, 'id', None) in sel_node_ids for n in children):
|
|
160
|
+
groups_final.append(grp.serialize())
|
|
161
|
+
except Exception:
|
|
162
|
+
continue
|
|
163
|
+
except Exception:
|
|
164
|
+
pass
|
|
165
|
+
|
|
166
|
+
data = OrderedDict([
|
|
167
|
+
('nodes', sel_nodes),
|
|
168
|
+
('edges', edges_final),
|
|
169
|
+
('groups', groups_final),
|
|
170
|
+
])
|
|
171
|
+
|
|
172
|
+
# if CUT (aka delete) remove selected items
|
|
173
|
+
if delete:
|
|
174
|
+
self.scene.getView().deleteSelected()
|
|
175
|
+
# store our history
|
|
176
|
+
self.scene.history.storeHistory(
|
|
177
|
+
"Cut out elements from scene", setModified=True)
|
|
178
|
+
|
|
179
|
+
return data
|
|
180
|
+
|
|
181
|
+
def deserializeFromClipboard(self, data: dict, *args, **kwargs) -> List['Node']:
|
|
182
|
+
"""
|
|
183
|
+
Deserializes data from Clipboard.
|
|
184
|
+
|
|
185
|
+
:param data: ``dict`` data for deserialization to the :class:`nodeeditor.node_scene.Scene`.
|
|
186
|
+
:type data: ``dict``
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
hashmap: dict = {}
|
|
190
|
+
|
|
191
|
+
# calculate mouse pointer - scene position
|
|
192
|
+
view = self.scene.getView()
|
|
193
|
+
mouse_scene_pos = view.last_scene_mouse_position
|
|
194
|
+
|
|
195
|
+
# calculate selected objects bbox and center
|
|
196
|
+
minx, max_x, min_y, max_y = 10000000, -10000000, 10000000, -10000000
|
|
197
|
+
for node_data in data['nodes']:
|
|
198
|
+
if 'pos_x' in node_data and 'pos_y' in node_data:
|
|
199
|
+
x, y = node_data['pos_x'], node_data['pos_y']
|
|
200
|
+
else:
|
|
201
|
+
# added support if node pos serializes into `pos` instead of `pos_x` and `pos_y`
|
|
202
|
+
x, y = node_data['pos']
|
|
203
|
+
if x < minx:
|
|
204
|
+
minx = x
|
|
205
|
+
if x > max_x:
|
|
206
|
+
max_x = x
|
|
207
|
+
if y < min_y:
|
|
208
|
+
min_y = y
|
|
209
|
+
if y > max_y:
|
|
210
|
+
max_y = y
|
|
211
|
+
|
|
212
|
+
# add width and height of a node
|
|
213
|
+
max_x -= 180
|
|
214
|
+
max_y += 100
|
|
215
|
+
|
|
216
|
+
relbboxcenterx = (minx + max_x) / 2 - minx
|
|
217
|
+
relbboxcentery = (min_y + max_y) / 2 - min_y
|
|
218
|
+
|
|
219
|
+
if DEBUG_PASTING:
|
|
220
|
+
print(" *** PASTA:")
|
|
221
|
+
print("Copied boudaries:\n\tX:", minx,
|
|
222
|
+
max_x, " Y:", min_y, max_y)
|
|
223
|
+
print("\tbbox_center:", relbboxcenterx, relbboxcentery)
|
|
224
|
+
|
|
225
|
+
# calculate the offset of the newly creating nodes
|
|
226
|
+
mouse_x, mouse_y = mouse_scene_pos.x(), mouse_scene_pos.y()
|
|
227
|
+
|
|
228
|
+
# create each node
|
|
229
|
+
created_nodes: List['Node'] = []
|
|
230
|
+
|
|
231
|
+
self.scene.setSilentSelectionEvents()
|
|
232
|
+
|
|
233
|
+
self.scene.doDeselectItems()
|
|
234
|
+
|
|
235
|
+
node_id_map = {} # old node id -> new Node (for group relink)
|
|
236
|
+
for node_data in data.get('nodes', []):
|
|
237
|
+
try:
|
|
238
|
+
old_id = node_data.get('id', None)
|
|
239
|
+
except Exception:
|
|
240
|
+
old_id = None
|
|
241
|
+
new_node = self.scene.getNodeClassFromData(node_data)(self.scene)
|
|
242
|
+
new_node.deserialize(node_data, hashmap, False, *args, **kwargs)
|
|
243
|
+
created_nodes.append(new_node)
|
|
244
|
+
if old_id is not None:
|
|
245
|
+
try:
|
|
246
|
+
node_id_map[old_id] = new_node
|
|
247
|
+
except Exception:
|
|
248
|
+
pass
|
|
249
|
+
|
|
250
|
+
# readjust the new nodeeditor's position
|
|
251
|
+
|
|
252
|
+
# new node's current position
|
|
253
|
+
pos_x, pos_y = new_node.pos.x(), new_node.pos.y()
|
|
254
|
+
new_x, new_y = mouse_x + pos_x - minx, mouse_y + pos_y - min_y
|
|
255
|
+
|
|
256
|
+
new_node.setPos(new_x, new_y)
|
|
257
|
+
|
|
258
|
+
new_node.doSelect()
|
|
259
|
+
|
|
260
|
+
if DEBUG_PASTING:
|
|
261
|
+
print("** PASTA SUM:")
|
|
262
|
+
print("\tMouse pos:", mouse_x, mouse_y)
|
|
263
|
+
print("\tnew node pos:", pos_x, pos_y)
|
|
264
|
+
print("\tFINAL:", new_x, new_y)
|
|
265
|
+
|
|
266
|
+
# create each edge
|
|
267
|
+
if 'edges' in data:
|
|
268
|
+
for edge_data in data['edges']:
|
|
269
|
+
new_edge = Edge(self.scene)
|
|
270
|
+
# kwargs_copy = kwargs.copy()
|
|
271
|
+
# if 'restore_id' in kwargs_copy:
|
|
272
|
+
# del kwargs_copy['restore_id']
|
|
273
|
+
new_edge.deserialize(edge_data, hashmap,
|
|
274
|
+
False, *args, **kwargs)
|
|
275
|
+
|
|
276
|
+
# create each group (v2 "children", old "child_node_ids" best-effort)
|
|
277
|
+
if 'groups' in data:
|
|
278
|
+
try:
|
|
279
|
+
from nodeeditor.node_group import Group
|
|
280
|
+
for group_data in list(data.get('groups', []) or []):
|
|
281
|
+
try:
|
|
282
|
+
title = group_data.get('title', 'Group')
|
|
283
|
+
grp = Group(self.scene, title=title)
|
|
284
|
+
# link remapped children
|
|
285
|
+
child_ids = group_data.get('children', group_data.get('child_node_ids', []))
|
|
286
|
+
for cid in list(child_ids or []):
|
|
287
|
+
nn = node_id_map.get(cid, None)
|
|
288
|
+
if nn is not None:
|
|
289
|
+
grp.addNode(nn)
|
|
290
|
+
grp.updateBounds()
|
|
291
|
+
# preserve collapsed flag (old payloads forced expanded in Group.deserialize,
|
|
292
|
+
# but clipboard groups are always v2 so collapsed is honored)
|
|
293
|
+
try:
|
|
294
|
+
want_collapsed = bool(group_data.get('collapsed', group_data.get('is_collapsed', False)))
|
|
295
|
+
except Exception:
|
|
296
|
+
want_collapsed = False
|
|
297
|
+
if want_collapsed and grp.getChildNodes():
|
|
298
|
+
grp.collapse()
|
|
299
|
+
try:
|
|
300
|
+
grp.setSelected(True)
|
|
301
|
+
except Exception:
|
|
302
|
+
pass
|
|
303
|
+
except Exception:
|
|
304
|
+
continue
|
|
305
|
+
except Exception:
|
|
306
|
+
pass
|
|
307
|
+
|
|
308
|
+
self.scene.setSilentSelectionEvents(False)
|
|
309
|
+
|
|
310
|
+
# store history
|
|
311
|
+
self.scene.history.storeHistory(
|
|
312
|
+
"Pasted elements in scene", setModified=True)
|
|
313
|
+
|
|
314
|
+
return created_nodes
|