Refactor: Node Wrangler: PreviewNode operator
[blender-addons.git] / node_wrangler / operators.py
blob86676f46c5305eca771f203656a0142bcab30810
1 # SPDX-FileCopyrightText: 2013-2023 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 import bpy
7 from bpy.types import Operator
8 from bpy.props import (
9 FloatProperty,
10 EnumProperty,
11 BoolProperty,
12 IntProperty,
13 StringProperty,
14 FloatVectorProperty,
15 CollectionProperty,
17 from bpy_extras.io_utils import ImportHelper, ExportHelper
18 from bpy_extras.node_utils import connect_sockets
19 from mathutils import Vector
20 from os import path
21 from glob import glob
22 from copy import copy
23 from itertools import chain
25 from .interface import NWConnectionListInputs, NWConnectionListOutputs
27 from .utils.constants import blend_types, geo_combine_operations, operations, navs, get_texture_node_types, rl_outputs
28 from .utils.draw import draw_callback_nodeoutline
29 from .utils.paths import match_files_to_socket_names, split_into_components
30 from .utils.nodes import (node_mid_pt, autolink, node_at_pos, get_active_tree, get_nodes_links, is_viewer_socket,
31 is_viewer_link, get_group_output_node, get_output_location, force_update, get_internal_socket,
32 nw_check, NWBase, get_first_enabled_output, is_visible_socket, viewer_socket_name)
35 class NWLazyMix(Operator, NWBase):
36 """Add a Mix RGB/Shader node by interactively drawing lines between nodes"""
37 bl_idname = "node.nw_lazy_mix"
38 bl_label = "Mix Nodes"
39 bl_options = {'REGISTER', 'UNDO'}
41 def modal(self, context, event):
42 context.area.tag_redraw()
43 nodes, links = get_nodes_links(context)
44 cont = True
46 start_pos = [event.mouse_region_x, event.mouse_region_y]
48 node1 = None
49 if not context.scene.NWBusyDrawing:
50 node1 = node_at_pos(nodes, context, event)
51 if node1:
52 context.scene.NWBusyDrawing = node1.name
53 else:
54 if context.scene.NWBusyDrawing != 'STOP':
55 node1 = nodes[context.scene.NWBusyDrawing]
57 context.scene.NWLazySource = node1.name
58 context.scene.NWLazyTarget = node_at_pos(nodes, context, event).name
60 if event.type == 'MOUSEMOVE':
61 self.mouse_path.append((event.mouse_region_x, event.mouse_region_y))
63 elif event.type == 'RIGHTMOUSE' and event.value == 'RELEASE':
64 end_pos = [event.mouse_region_x, event.mouse_region_y]
65 bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
67 node2 = None
68 node2 = node_at_pos(nodes, context, event)
69 if node2:
70 context.scene.NWBusyDrawing = node2.name
72 if node1 == node2:
73 cont = False
75 if cont:
76 if node1 and node2:
77 for node in nodes:
78 node.select = False
79 node1.select = True
80 node2.select = True
82 bpy.ops.node.nw_merge_nodes(mode="MIX", merge_type="AUTO")
84 context.scene.NWBusyDrawing = ""
85 return {'FINISHED'}
87 elif event.type == 'ESC':
88 print('cancelled')
89 bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
90 return {'CANCELLED'}
92 return {'RUNNING_MODAL'}
94 def invoke(self, context, event):
95 if context.area.type == 'NODE_EDITOR':
96 # the arguments we pass the the callback
97 args = (self, context, 'MIX')
98 # Add the region OpenGL drawing callback
99 # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
100 self._handle = bpy.types.SpaceNodeEditor.draw_handler_add(
101 draw_callback_nodeoutline, args, 'WINDOW', 'POST_PIXEL')
103 self.mouse_path = []
105 context.window_manager.modal_handler_add(self)
106 return {'RUNNING_MODAL'}
107 else:
108 self.report({'WARNING'}, "View3D not found, cannot run operator")
109 return {'CANCELLED'}
112 class NWLazyConnect(Operator, NWBase):
113 """Connect two nodes without clicking a specific socket (automatically determined"""
114 bl_idname = "node.nw_lazy_connect"
115 bl_label = "Lazy Connect"
116 bl_options = {'REGISTER', 'UNDO'}
117 with_menu: BoolProperty()
119 def modal(self, context, event):
120 context.area.tag_redraw()
121 nodes, links = get_nodes_links(context)
122 cont = True
124 start_pos = [event.mouse_region_x, event.mouse_region_y]
126 node1 = None
127 if not context.scene.NWBusyDrawing:
128 node1 = node_at_pos(nodes, context, event)
129 if node1:
130 context.scene.NWBusyDrawing = node1.name
131 else:
132 if context.scene.NWBusyDrawing != 'STOP':
133 node1 = nodes[context.scene.NWBusyDrawing]
135 context.scene.NWLazySource = node1.name
136 context.scene.NWLazyTarget = node_at_pos(nodes, context, event).name
138 if event.type == 'MOUSEMOVE':
139 self.mouse_path.append((event.mouse_region_x, event.mouse_region_y))
141 elif event.type == 'RIGHTMOUSE' and event.value == 'RELEASE':
142 end_pos = [event.mouse_region_x, event.mouse_region_y]
143 bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
145 node2 = None
146 node2 = node_at_pos(nodes, context, event)
147 if node2:
148 context.scene.NWBusyDrawing = node2.name
150 if node1 == node2:
151 cont = False
153 link_success = False
154 if cont:
155 if node1 and node2:
156 original_sel = []
157 original_unsel = []
158 for node in nodes:
159 if node.select:
160 node.select = False
161 original_sel.append(node)
162 else:
163 original_unsel.append(node)
164 node1.select = True
165 node2.select = True
167 # link_success = autolink(node1, node2, links)
168 if self.with_menu:
169 if len(node1.outputs) > 1 and node2.inputs:
170 bpy.ops.wm.call_menu("INVOKE_DEFAULT", name=NWConnectionListOutputs.bl_idname)
171 elif len(node1.outputs) == 1:
172 bpy.ops.node.nw_call_inputs_menu(from_socket=0)
173 else:
174 link_success = autolink(node1, node2, links)
176 for node in original_sel:
177 node.select = True
178 for node in original_unsel:
179 node.select = False
181 if link_success:
182 force_update(context)
183 context.scene.NWBusyDrawing = ""
184 return {'FINISHED'}
186 elif event.type == 'ESC':
187 bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
188 return {'CANCELLED'}
190 return {'RUNNING_MODAL'}
192 def invoke(self, context, event):
193 if context.area.type == 'NODE_EDITOR':
194 nodes, links = get_nodes_links(context)
195 node = node_at_pos(nodes, context, event)
196 if node:
197 context.scene.NWBusyDrawing = node.name
199 # the arguments we pass the the callback
200 mode = "LINK"
201 if self.with_menu:
202 mode = "LINKMENU"
203 args = (self, context, mode)
204 # Add the region OpenGL drawing callback
205 # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
206 self._handle = bpy.types.SpaceNodeEditor.draw_handler_add(
207 draw_callback_nodeoutline, args, 'WINDOW', 'POST_PIXEL')
209 self.mouse_path = []
211 context.window_manager.modal_handler_add(self)
212 return {'RUNNING_MODAL'}
213 else:
214 self.report({'WARNING'}, "View3D not found, cannot run operator")
215 return {'CANCELLED'}
218 class NWDeleteUnused(Operator, NWBase):
219 """Delete all nodes whose output is not used"""
220 bl_idname = 'node.nw_del_unused'
221 bl_label = 'Delete Unused Nodes'
222 bl_options = {'REGISTER', 'UNDO'}
224 delete_muted: BoolProperty(
225 name="Delete Muted",
226 description="Delete (but reconnect, like Ctrl-X) all muted nodes",
227 default=True)
228 delete_frames: BoolProperty(
229 name="Delete Empty Frames",
230 description="Delete all frames that have no nodes inside them",
231 default=True)
233 def is_unused_node(self, node):
234 end_types = ['OUTPUT_MATERIAL', 'OUTPUT', 'VIEWER', 'COMPOSITE',
235 'SPLITVIEWER', 'OUTPUT_FILE', 'LEVELS', 'OUTPUT_LIGHT',
236 'OUTPUT_WORLD', 'GROUP_INPUT', 'GROUP_OUTPUT', 'FRAME']
237 if node.type in end_types:
238 return False
240 for output in node.outputs:
241 if output.links:
242 return False
243 return True
245 @classmethod
246 def poll(cls, context):
247 valid = False
248 if nw_check(context):
249 if context.space_data.node_tree.nodes:
250 valid = True
251 return valid
253 def execute(self, context):
254 nodes, links = get_nodes_links(context)
256 # Store selection
257 selection = []
258 for node in nodes:
259 if node.select:
260 selection.append(node.name)
262 for node in nodes:
263 node.select = False
265 deleted_nodes = []
266 temp_deleted_nodes = []
267 del_unused_iterations = len(nodes)
268 for it in range(0, del_unused_iterations):
269 temp_deleted_nodes = list(deleted_nodes) # keep record of last iteration
270 for node in nodes:
271 if self.is_unused_node(node):
272 node.select = True
273 deleted_nodes.append(node.name)
274 bpy.ops.node.delete()
276 if temp_deleted_nodes == deleted_nodes: # stop iterations when there are no more nodes to be deleted
277 break
279 if self.delete_frames:
280 repeat = True
281 while repeat:
282 frames_in_use = []
283 frames = []
284 repeat = False
285 for node in nodes:
286 if node.parent:
287 frames_in_use.append(node.parent)
288 for node in nodes:
289 if node.type == 'FRAME' and node not in frames_in_use:
290 frames.append(node)
291 if node.parent:
292 repeat = True # repeat for nested frames
293 for node in frames:
294 if node not in frames_in_use:
295 node.select = True
296 deleted_nodes.append(node.name)
297 bpy.ops.node.delete()
299 if self.delete_muted:
300 for node in nodes:
301 if node.mute:
302 node.select = True
303 deleted_nodes.append(node.name)
304 bpy.ops.node.delete_reconnect()
306 # get unique list of deleted nodes (iterations would count the same node more than once)
307 deleted_nodes = list(set(deleted_nodes))
308 for n in deleted_nodes:
309 self.report({'INFO'}, "Node " + n + " deleted")
310 num_deleted = len(deleted_nodes)
311 n = ' node'
312 if num_deleted > 1:
313 n += 's'
314 if num_deleted:
315 self.report({'INFO'}, "Deleted " + str(num_deleted) + n)
316 else:
317 self.report({'INFO'}, "Nothing deleted")
319 # Restore selection
320 nodes, links = get_nodes_links(context)
321 for node in nodes:
322 if node.name in selection:
323 node.select = True
324 return {'FINISHED'}
326 def invoke(self, context, event):
327 return context.window_manager.invoke_confirm(self, event)
330 class NWSwapLinks(Operator, NWBase):
331 """Swap the output connections of the two selected nodes, or two similar inputs of a single node"""
332 bl_idname = 'node.nw_swap_links'
333 bl_label = 'Swap Links'
334 bl_options = {'REGISTER', 'UNDO'}
336 @classmethod
337 def poll(cls, context):
338 valid = False
339 if nw_check(context):
340 if context.selected_nodes:
341 valid = len(context.selected_nodes) <= 2
342 return valid
344 def execute(self, context):
345 nodes, links = get_nodes_links(context)
346 selected_nodes = context.selected_nodes
347 n1 = selected_nodes[0]
349 # Swap outputs
350 if len(selected_nodes) == 2:
351 n2 = selected_nodes[1]
352 if n1.outputs and n2.outputs:
353 n1_outputs = []
354 n2_outputs = []
356 out_index = 0
357 for output in n1.outputs:
358 if output.links:
359 for link in output.links:
360 n1_outputs.append([out_index, link.to_socket])
361 links.remove(link)
362 out_index += 1
364 out_index = 0
365 for output in n2.outputs:
366 if output.links:
367 for link in output.links:
368 n2_outputs.append([out_index, link.to_socket])
369 links.remove(link)
370 out_index += 1
372 for connection in n1_outputs:
373 try:
374 connect_sockets(n2.outputs[connection[0]], connection[1])
375 except:
376 self.report({'WARNING'},
377 "Some connections have been lost due to differing numbers of output sockets")
378 for connection in n2_outputs:
379 try:
380 connect_sockets(n1.outputs[connection[0]], connection[1])
381 except:
382 self.report({'WARNING'},
383 "Some connections have been lost due to differing numbers of output sockets")
384 else:
385 if n1.outputs or n2.outputs:
386 self.report({'WARNING'}, "One of the nodes has no outputs!")
387 else:
388 self.report({'WARNING'}, "Neither of the nodes have outputs!")
390 # Swap Inputs
391 elif len(selected_nodes) == 1:
392 if n1.inputs and n1.inputs[0].is_multi_input:
393 self.report({'WARNING'}, "Can't swap inputs of a multi input socket!")
394 return {'FINISHED'}
395 if n1.inputs:
396 types = []
397 i = 0
398 for i1 in n1.inputs:
399 if i1.is_linked and not i1.is_multi_input:
400 similar_types = 0
401 for i2 in n1.inputs:
402 if i1.type == i2.type and i2.is_linked and not i2.is_multi_input:
403 similar_types += 1
404 types.append([i1, similar_types, i])
405 i += 1
406 types.sort(key=lambda k: k[1], reverse=True)
408 if types:
409 t = types[0]
410 if t[1] == 2:
411 for i2 in n1.inputs:
412 if t[0].type == i2.type == t[0].type and t[0] != i2 and i2.is_linked:
413 pair = [t[0], i2]
414 i1f = pair[0].links[0].from_socket
415 i1t = pair[0].links[0].to_socket
416 i2f = pair[1].links[0].from_socket
417 i2t = pair[1].links[0].to_socket
418 connect_sockets(i1f, i2t)
419 connect_sockets(i2f, i1t)
420 if t[1] == 1:
421 if len(types) == 1:
422 fs = t[0].links[0].from_socket
423 i = t[2]
424 links.remove(t[0].links[0])
425 if i + 1 == len(n1.inputs):
426 i = -1
427 i += 1
428 while n1.inputs[i].is_linked:
429 i += 1
430 connect_sockets(fs, n1.inputs[i])
431 elif len(types) == 2:
432 i1f = types[0][0].links[0].from_socket
433 i1t = types[0][0].links[0].to_socket
434 i2f = types[1][0].links[0].from_socket
435 i2t = types[1][0].links[0].to_socket
436 connect_sockets(i1f, i2t)
437 connect_sockets(i2f, i1t)
439 else:
440 self.report({'WARNING'}, "This node has no input connections to swap!")
441 else:
442 self.report({'WARNING'}, "This node has no inputs to swap!")
444 force_update(context)
445 return {'FINISHED'}
448 class NWResetBG(Operator, NWBase):
449 """Reset the zoom and position of the background image"""
450 bl_idname = 'node.nw_bg_reset'
451 bl_label = 'Reset Backdrop'
452 bl_options = {'REGISTER', 'UNDO'}
454 @classmethod
455 def poll(cls, context):
456 valid = False
457 if nw_check(context):
458 snode = context.space_data
459 valid = snode.tree_type == 'CompositorNodeTree'
460 return valid
462 def execute(self, context):
463 context.space_data.backdrop_zoom = 1
464 context.space_data.backdrop_offset[0] = 0
465 context.space_data.backdrop_offset[1] = 0
466 return {'FINISHED'}
469 class NWAddAttrNode(Operator, NWBase):
470 """Add an Attribute node with this name"""
471 bl_idname = 'node.nw_add_attr_node'
472 bl_label = 'Add UV map'
473 bl_options = {'REGISTER', 'UNDO'}
475 attr_name: StringProperty()
477 def execute(self, context):
478 bpy.ops.node.add_node('INVOKE_DEFAULT', use_transform=True, type="ShaderNodeAttribute")
479 nodes, links = get_nodes_links(context)
480 nodes.active.attribute_name = self.attr_name
481 return {'FINISHED'}
484 class NWPreviewNode(Operator, NWBase):
485 bl_idname = "node.nw_preview_node"
486 bl_label = "Preview Node"
487 bl_description = "Connect active node to the Node Group output or the Material Output"
488 bl_options = {'REGISTER', 'UNDO'}
490 # If false, the operator is not executed if the current node group happens to be a geometry nodes group.
491 # This is needed because geometry nodes has its own viewer node that uses the same shortcut as in the compositor.
492 run_in_geometry_nodes: BoolProperty(default=True)
494 def __init__(self):
495 self.shader_output_type = ""
496 self.shader_output_ident = ""
498 @classmethod
499 def poll(cls, context):
500 if nw_check(context):
501 space = context.space_data
502 if space.tree_type in {'ShaderNodeTree', 'GeometryNodeTree'}:
503 if context.active_node:
504 if context.active_node.type not in {"OUTPUT_MATERIAL", "OUTPUT_WORLD"}:
505 return True
506 else:
507 return True
508 return False
510 @staticmethod
511 def get_output_sockets(node_tree):
512 return [item for item in node_tree.interface.items_tree if item.item_type == 'SOCKET' and item.in_out in {'OUTPUT', 'BOTH'}]
514 def ensure_viewer_socket(self, node, socket_type, connect_socket=None):
515 """Check if a viewer output already exists in a node group, otherwise create it"""
516 if not hasattr(node, "node_tree"):
517 return None
519 viewer_socket = None
520 output_sockets = self.get_output_sockets(node.node_tree)
521 if len(output_sockets):
522 for i, socket in enumerate(output_sockets):
523 if is_viewer_socket(socket) and socket.socket_type == socket_type:
524 # If viewer output is already used but leads to the same socket we can still use it
525 is_used = self.is_socket_used_other_mats(socket)
526 if is_used:
527 if connect_socket is None:
528 continue
529 groupout = get_group_output_node(node.node_tree)
530 groupout_input = groupout.inputs[i]
531 links = groupout_input.links
532 if connect_socket not in [link.from_socket for link in links]:
533 continue
534 viewer_socket = socket
535 break
537 if viewer_socket is None:
538 # Create viewer socket
539 viewer_socket = node.node_tree.interface.new_socket(viewer_socket_name, in_out='OUTPUT', socket_type=socket_type)
540 viewer_socket.NWViewerSocket = True
541 return viewer_socket
543 def init_shader_variables(self, space, shader_type):
544 if shader_type == 'OBJECT':
545 if space.id in bpy.data.lights.values():
546 self.shader_output_type = "OUTPUT_LIGHT"
547 self.shader_output_ident = "ShaderNodeOutputLight"
548 else:
549 self.shader_output_type = "OUTPUT_MATERIAL"
550 self.shader_output_ident = "ShaderNodeOutputMaterial"
552 elif shader_type == 'WORLD':
553 self.shader_output_type = "OUTPUT_WORLD"
554 self.shader_output_ident = "ShaderNodeOutputWorld"
556 @staticmethod
557 def ensure_group_output(tree):
558 """Check if a group output node exists, otherwise create it"""
559 groupout = get_group_output_node(tree)
560 if groupout is None:
561 groupout = tree.nodes.new('NodeGroupOutput')
562 loc_x, loc_y = get_output_location(tree)
563 groupout.location.x = loc_x
564 groupout.location.y = loc_y
565 groupout.select = False
566 # So that we don't keep on adding new group outputs
567 groupout.is_active_output = True
568 return groupout
570 @classmethod
571 def search_sockets(cls, node, sockets, index=None):
572 """Recursively scan nodes for viewer sockets and store them in a list"""
573 for i, input_socket in enumerate(node.inputs):
574 if index and i != index:
575 continue
576 if len(input_socket.links):
577 link = input_socket.links[0]
578 next_node = link.from_node
579 external_socket = link.from_socket
580 if hasattr(next_node, "node_tree"):
581 for socket_index, socket in enumerate(next_node.node_tree.interface.items_tree):
582 if socket.identifier == external_socket.identifier:
583 break
584 if is_viewer_socket(socket) and socket not in sockets:
585 sockets.append(socket)
586 # continue search inside of node group but restrict socket to where we came from
587 groupout = get_group_output_node(next_node.node_tree)
588 cls.search_sockets(groupout, sockets, index=socket_index)
590 @classmethod
591 def scan_nodes(cls, tree, sockets):
592 """Recursively get all viewer sockets in a material tree"""
593 for node in tree.nodes:
594 if hasattr(node, "node_tree"):
595 if node.node_tree is None:
596 continue
597 for socket in cls.get_output_sockets(node.node_tree):
598 if is_viewer_socket(socket) and (socket not in sockets):
599 sockets.append(socket)
600 cls.scan_nodes(node.node_tree, sockets)
602 @staticmethod
603 def remove_socket(tree, socket):
604 interface = tree.interface
605 interface.remove(socket)
606 interface.active_index = min(interface.active_index, len(interface.items_tree) - 1)
608 def link_leads_to_used_socket(self, link):
609 """Return True if link leads to a socket that is already used in this material"""
610 socket = get_internal_socket(link.to_socket)
611 return (socket and self.is_socket_used_active_mat(socket))
613 def is_socket_used_active_mat(self, socket):
614 """Ensure used sockets in active material is calculated and check given socket"""
615 if not hasattr(self, "used_viewer_sockets_active_mat"):
616 self.used_viewer_sockets_active_mat = []
617 output_node = get_group_output_node(bpy.context.space_data.node_tree,
618 output_node_type=self.shader_output_type)
620 if output_node is not None:
621 self.search_sockets(output_node, self.used_viewer_sockets_active_mat)
622 return socket in self.used_viewer_sockets_active_mat
624 def is_socket_used_other_mats(self, socket):
625 """Ensure used sockets in other materials are calculated and check given socket"""
626 if not hasattr(self, "used_viewer_sockets_other_mats"):
627 self.used_viewer_sockets_other_mats = []
628 for mat in bpy.data.materials:
629 if mat.node_tree == bpy.context.space_data.node_tree or not hasattr(mat.node_tree, "nodes"):
630 continue
631 # get viewer node
632 output_node = get_group_output_node(mat.node_tree,
633 output_node_type=self.shader_output_type)
634 if output_node is not None:
635 self.search_sockets(output_node, self.used_viewer_sockets_other_mats)
636 return socket in self.used_viewer_sockets_other_mats
638 def get_output_index(self, base_node_tree, nodes, output_node, socket_type, check_type=False):
639 """Get the next available output socket in the active node"""
640 out_i = None
641 valid_outputs = []
642 for i, out in enumerate(nodes.active.outputs):
643 if is_visible_socket(out) and (not check_type or out.type == socket_type):
644 valid_outputs.append(i)
645 if valid_outputs:
646 out_i = valid_outputs[0] # Start index of node's outputs
647 for i, valid_i in enumerate(valid_outputs):
648 for out_link in nodes.active.outputs[valid_i].links:
649 if is_viewer_link(out_link, output_node):
650 if nodes == base_node_tree.nodes or self.link_leads_to_used_socket(out_link):
651 if i < len(valid_outputs) - 1:
652 out_i = valid_outputs[i + 1]
653 else:
654 out_i = valid_outputs[0]
655 return out_i
657 def create_links(self, tree, link_end, active, out_i, socket_type):
658 """Create links through node groups until we reach the active node"""
659 while tree.nodes.active != active:
660 node = tree.nodes.active
661 viewer_socket = self.ensure_viewer_socket(
662 node, socket_type, connect_socket=active.outputs[out_i] if node.node_tree.nodes.active == active else None)
663 link_start = node.outputs[viewer_socket.identifier]
664 if viewer_socket in self.delete_sockets:
665 self.delete_sockets.remove(viewer_socket)
666 connect_sockets(link_start, link_end)
667 # Iterate
668 link_end = self.ensure_group_output(node.node_tree).inputs[viewer_socket.identifier]
669 tree = tree.nodes.active.node_tree
670 connect_sockets(active.outputs[out_i], link_end)
672 def invoke(self, context, event):
673 space = context.space_data
674 # Ignore operator when running in wrong context.
675 if self.run_in_geometry_nodes != (space.tree_type == "GeometryNodeTree"):
676 return {'PASS_THROUGH'}
678 mlocx = event.mouse_region_x
679 mlocy = event.mouse_region_y
680 select_node = bpy.ops.node.select(location=(mlocx, mlocy), extend=False)
681 if not 'FINISHED' in select_node: # only run if mouse click is on a node
682 return {'CANCELLED'}
684 active_tree, path_to_tree = get_active_tree(context)
685 nodes, links = active_tree.nodes, active_tree.links
686 base_node_tree = space.node_tree
687 active = nodes.active
689 if not active and not any(is_visible_socket(out) for out in active.outputs):
690 return {'CANCELLED'}
692 # Scan through all nodes in tree including nodes inside of groups to find viewer sockets
693 self.delete_sockets = []
694 self.scan_nodes(base_node_tree, self.delete_sockets)
696 # For geometry node trees we just connect to the group output
697 if space.tree_type == "GeometryNodeTree" and active.outputs:
698 socket_type = 'GEOMETRY'
700 # Find (or create if needed) the output of this node tree
701 output_node = self.ensure_group_output(base_node_tree)
703 out_i = self.get_output_index(base_node_tree, nodes, output_node, 'GEOMETRY', check_type=True)
704 # If there is no 'GEOMETRY' output type - We can't preview the node
705 if out_i is None:
706 return {'CANCELLED'}
708 # Find an input socket of the output of type geometry
709 geometry_out_index = None
710 for i, inp in enumerate(output_node.inputs):
711 if inp.type == socket_type:
712 geometry_out_index = i
713 break
714 if geometry_out_index is None:
715 # Create geometry socket
716 geometry_out_socket = base_node_tree.interface.new_socket(
717 'Geometry', in_out='OUTPUT', socket_type='NodeSocketGeometry'
719 geometry_out_index = geometry_out_socket.index
721 output_socket = output_node.inputs[geometry_out_index]
723 self.create_links(base_node_tree, output_socket, active, out_i, 'NodeSocketGeometry')
725 # What follows is code for the shader editor
726 elif space.tree_type == "ShaderNodeTree" and active.outputs:
727 shader_type = space.shader_type
728 self.init_shader_variables(space, shader_type)
729 socket_type = 'NodeSocketShader'
731 # Get or create material_output node
732 output_node = get_group_output_node(base_node_tree,
733 output_node_type=self.shader_output_type)
734 if not output_node:
735 output_node = base_node_tree.nodes.new(self.shader_output_ident)
736 output_node.location = get_output_location(base_node_tree)
737 output_node.select = False
739 out_i = self.get_output_index(base_node_tree, nodes, output_node, 'SHADER')
741 materialout_index = 1 if active.outputs[out_i].name == "Volume" else 0
742 output_socket = output_node.inputs[materialout_index]
744 self.create_links(base_node_tree, output_socket, active, out_i, 'NodeSocketShader')
746 # Delete sockets
747 for socket in self.delete_sockets:
748 if not self.is_socket_used_other_mats(socket):
749 tree = socket.id_data
750 self.remove_socket(tree, socket)
752 nodes.active = active
753 active.select = True
754 force_update(context)
755 return {'FINISHED'}
758 class NWFrameSelected(Operator, NWBase):
759 bl_idname = "node.nw_frame_selected"
760 bl_label = "Frame Selected"
761 bl_description = "Add a frame node and parent the selected nodes to it"
762 bl_options = {'REGISTER', 'UNDO'}
764 label_prop: StringProperty(
765 name='Label',
766 description='The visual name of the frame node',
767 default=' '
769 use_custom_color_prop: BoolProperty(
770 name="Custom Color",
771 description="Use custom color for the frame node",
772 default=False
774 color_prop: FloatVectorProperty(
775 name="Color",
776 description="The color of the frame node",
777 default=(0.604, 0.604, 0.604),
778 min=0, max=1, step=1, precision=3,
779 subtype='COLOR_GAMMA', size=3
782 def draw(self, context):
783 layout = self.layout
784 layout.prop(self, 'label_prop')
785 layout.prop(self, 'use_custom_color_prop')
786 col = layout.column()
787 col.active = self.use_custom_color_prop
788 col.prop(self, 'color_prop', text="")
790 def execute(self, context):
791 nodes, links = get_nodes_links(context)
792 selected = []
793 for node in nodes:
794 if node.select:
795 selected.append(node)
797 bpy.ops.node.add_node(type='NodeFrame')
798 frm = nodes.active
799 frm.label = self.label_prop
800 frm.use_custom_color = self.use_custom_color_prop
801 frm.color = self.color_prop
803 for node in selected:
804 node.parent = frm
806 return {'FINISHED'}
809 class NWReloadImages(Operator):
810 bl_idname = "node.nw_reload_images"
811 bl_label = "Reload Images"
812 bl_description = "Update all the image nodes to match their files on disk"
814 @classmethod
815 def poll(cls, context):
816 valid = False
817 if nw_check(context) and context.space_data.tree_type != 'GeometryNodeTree':
818 if context.active_node is not None:
819 for out in context.active_node.outputs:
820 if is_visible_socket(out):
821 valid = True
822 break
823 return valid
825 def execute(self, context):
826 nodes, links = get_nodes_links(context)
827 image_types = ["IMAGE", "TEX_IMAGE", "TEX_ENVIRONMENT", "TEXTURE"]
828 num_reloaded = 0
829 for node in nodes:
830 if node.type in image_types:
831 if node.type == "TEXTURE":
832 if node.texture: # node has texture assigned
833 if node.texture.type in ['IMAGE', 'ENVIRONMENT_MAP']:
834 if node.texture.image: # texture has image assigned
835 node.texture.image.reload()
836 num_reloaded += 1
837 else:
838 if node.image:
839 node.image.reload()
840 num_reloaded += 1
842 if num_reloaded:
843 self.report({'INFO'}, "Reloaded images")
844 print("Reloaded " + str(num_reloaded) + " images")
845 force_update(context)
846 return {'FINISHED'}
847 else:
848 self.report({'WARNING'}, "No images found to reload in this node tree")
849 return {'CANCELLED'}
852 class NWMergeNodes(Operator, NWBase):
853 bl_idname = "node.nw_merge_nodes"
854 bl_label = "Merge Nodes"
855 bl_description = "Merge Selected Nodes"
856 bl_options = {'REGISTER', 'UNDO'}
858 mode: EnumProperty(
859 name="mode",
860 description="All possible blend types, boolean operations and math operations",
861 items=blend_types + [op for op in geo_combine_operations if op not in blend_types] + [op for op in operations if op not in blend_types],
863 merge_type: EnumProperty(
864 name="merge type",
865 description="Type of Merge to be used",
866 items=(
867 ('AUTO', 'Auto', 'Automatic Output Type Detection'),
868 ('SHADER', 'Shader', 'Merge using ADD or MIX Shader'),
869 ('GEOMETRY', 'Geometry', 'Merge using Mesh Boolean or Join Geometry Node'),
870 ('MIX', 'Mix Node', 'Merge using Mix Nodes'),
871 ('MATH', 'Math Node', 'Merge using Math Nodes'),
872 ('ZCOMBINE', 'Z-Combine Node', 'Merge using Z-Combine Nodes'),
873 ('ALPHAOVER', 'Alpha Over Node', 'Merge using Alpha Over Nodes'),
877 # Check if the link connects to a node that is in selected_nodes
878 # If not, then check recursively for each link in the nodes outputs.
879 # If yes, return True. If the recursion stops without finding a node
880 # in selected_nodes, it returns False. The depth is used to prevent
881 # getting stuck in a loop because of an already present cycle.
882 @staticmethod
883 def link_creates_cycle(link, selected_nodes, depth=0) -> bool:
884 if depth > 255:
885 # We're stuck in a cycle, but that cycle was already present,
886 # so we return False.
887 # NOTE: The number 255 is arbitrary, but seems to work well.
888 return False
889 node = link.to_node
890 if node in selected_nodes:
891 return True
892 if not node.outputs:
893 return False
894 for output in node.outputs:
895 if output.is_linked:
896 for olink in output.links:
897 if NWMergeNodes.link_creates_cycle(olink, selected_nodes, depth + 1):
898 return True
899 # None of the outputs found a node in selected_nodes, so there is no cycle.
900 return False
902 # Merge the nodes in `nodes_list` with a node of type `node_name` that has a multi_input socket.
903 # The parameters `socket_indices` gives the indices of the node sockets in the order that they should
904 # be connected. The last one is assumed to be a multi input socket.
905 # For convenience the node is returned.
906 @staticmethod
907 def merge_with_multi_input(nodes_list, merge_position, do_hide, loc_x, links, nodes, node_name, socket_indices):
908 # The y-location of the last node
909 loc_y = nodes_list[-1][2]
910 if merge_position == 'CENTER':
911 # Average the y-location
912 for i in range(len(nodes_list) - 1):
913 loc_y += nodes_list[i][2]
914 loc_y = loc_y / len(nodes_list)
915 new_node = nodes.new(node_name)
916 new_node.hide = do_hide
917 new_node.location.x = loc_x
918 new_node.location.y = loc_y
919 selected_nodes = [nodes[node_info[0]] for node_info in nodes_list]
920 prev_links = []
921 outputs_for_multi_input = []
922 for i, node in enumerate(selected_nodes):
923 node.select = False
924 # Search for the first node which had output links that do not create
925 # a cycle, which we can then reconnect afterwards.
926 if prev_links == [] and node.outputs[0].is_linked:
927 prev_links = [
928 link for link in node.outputs[0].links if not NWMergeNodes.link_creates_cycle(
929 link, selected_nodes)]
930 # Get the index of the socket, the last one is a multi input, and is thus used repeatedly
931 # To get the placement to look right we need to reverse the order in which we connect the
932 # outputs to the multi input socket.
933 if i < len(socket_indices) - 1:
934 ind = socket_indices[i]
935 connect_sockets(node.outputs[0], new_node.inputs[ind])
936 else:
937 outputs_for_multi_input.insert(0, node.outputs[0])
938 if outputs_for_multi_input != []:
939 ind = socket_indices[-1]
940 for output in outputs_for_multi_input:
941 connect_sockets(output, new_node.inputs[ind])
942 if prev_links != []:
943 for link in prev_links:
944 connect_sockets(new_node.outputs[0], link.to_node.inputs[0])
945 return new_node
947 def execute(self, context):
948 settings = context.preferences.addons[__package__].preferences
949 merge_hide = settings.merge_hide
950 merge_position = settings.merge_position # 'center' or 'bottom'
952 do_hide = False
953 do_hide_shader = False
954 if merge_hide == 'ALWAYS':
955 do_hide = True
956 do_hide_shader = True
957 elif merge_hide == 'NON_SHADER':
958 do_hide = True
960 tree_type = context.space_data.node_tree.type
961 if tree_type == 'GEOMETRY':
962 node_type = 'GeometryNode'
963 if tree_type == 'COMPOSITING':
964 node_type = 'CompositorNode'
965 elif tree_type == 'SHADER':
966 node_type = 'ShaderNode'
967 elif tree_type == 'TEXTURE':
968 node_type = 'TextureNode'
969 nodes, links = get_nodes_links(context)
970 mode = self.mode
971 merge_type = self.merge_type
972 # Prevent trying to add Z-Combine in not 'COMPOSITING' node tree.
973 # 'ZCOMBINE' works only if mode == 'MIX'
974 # Setting mode to None prevents trying to add 'ZCOMBINE' node.
975 if (merge_type == 'ZCOMBINE' or merge_type == 'ALPHAOVER') and tree_type != 'COMPOSITING':
976 merge_type = 'MIX'
977 mode = 'MIX'
978 if (merge_type != 'MATH' and merge_type != 'GEOMETRY') and tree_type == 'GEOMETRY':
979 merge_type = 'AUTO'
980 # The Mix node and math nodes used for geometry nodes are of type 'ShaderNode'
981 if (merge_type == 'MATH' or merge_type == 'MIX') and tree_type == 'GEOMETRY':
982 node_type = 'ShaderNode'
983 selected_mix = [] # entry = [index, loc]
984 selected_shader = [] # entry = [index, loc]
985 selected_geometry = [] # entry = [index, loc]
986 selected_math = [] # entry = [index, loc]
987 selected_vector = [] # entry = [index, loc]
988 selected_z = [] # entry = [index, loc]
989 selected_alphaover = [] # entry = [index, loc]
991 for i, node in enumerate(nodes):
992 if node.select and node.outputs:
993 if merge_type == 'AUTO':
994 for (type, types_list, dst) in (
995 ('SHADER', ('MIX', 'ADD'), selected_shader),
996 ('GEOMETRY', [t[0] for t in geo_combine_operations], selected_geometry),
997 ('RGBA', [t[0] for t in blend_types], selected_mix),
998 ('VALUE', [t[0] for t in operations], selected_math),
999 ('VECTOR', [], selected_vector),
1001 output = get_first_enabled_output(node)
1002 output_type = output.type
1003 valid_mode = mode in types_list
1004 # When mode is 'MIX' we have to cheat since the mix node is not used in
1005 # geometry nodes.
1006 if tree_type == 'GEOMETRY':
1007 if mode == 'MIX':
1008 if output_type == 'VALUE' and type == 'VALUE':
1009 valid_mode = True
1010 elif output_type == 'VECTOR' and type == 'VECTOR':
1011 valid_mode = True
1012 elif type == 'GEOMETRY':
1013 valid_mode = True
1014 # When mode is 'MIX' use mix node for both 'RGBA' and 'VALUE' output types.
1015 # Cheat that output type is 'RGBA',
1016 # and that 'MIX' exists in math operations list.
1017 # This way when selected_mix list is analyzed:
1018 # Node data will be appended even though it doesn't meet requirements.
1019 elif output_type != 'SHADER' and mode == 'MIX':
1020 output_type = 'RGBA'
1021 valid_mode = True
1022 if output_type == type and valid_mode:
1023 dst.append([i, node.location.x, node.location.y, node.dimensions.x, node.hide])
1024 else:
1025 for (type, types_list, dst) in (
1026 ('SHADER', ('MIX', 'ADD'), selected_shader),
1027 ('GEOMETRY', [t[0] for t in geo_combine_operations], selected_geometry),
1028 ('MIX', [t[0] for t in blend_types], selected_mix),
1029 ('MATH', [t[0] for t in operations], selected_math),
1030 ('ZCOMBINE', ('MIX', ), selected_z),
1031 ('ALPHAOVER', ('MIX', ), selected_alphaover),
1033 if merge_type == type and mode in types_list:
1034 dst.append([i, node.location.x, node.location.y, node.dimensions.x, node.hide])
1035 # When nodes with output kinds 'RGBA' and 'VALUE' are selected at the same time
1036 # use only 'Mix' nodes for merging.
1037 # For that we add selected_math list to selected_mix list and clear selected_math.
1038 if selected_mix and selected_math and merge_type == 'AUTO':
1039 selected_mix += selected_math
1040 selected_math = []
1041 for nodes_list in [
1042 selected_mix,
1043 selected_shader,
1044 selected_geometry,
1045 selected_math,
1046 selected_vector,
1047 selected_z,
1048 selected_alphaover]:
1049 if not nodes_list:
1050 continue
1051 count_before = len(nodes)
1052 # sort list by loc_x - reversed
1053 nodes_list.sort(key=lambda k: k[1], reverse=True)
1054 # get maximum loc_x
1055 loc_x = nodes_list[0][1] + nodes_list[0][3] + 70
1056 nodes_list.sort(key=lambda k: k[2], reverse=True)
1058 # Change the node type for math nodes in a geometry node tree.
1059 if tree_type == 'GEOMETRY':
1060 if nodes_list is selected_math or nodes_list is selected_vector or nodes_list is selected_mix:
1061 node_type = 'ShaderNode'
1062 if mode == 'MIX':
1063 mode = 'ADD'
1064 else:
1065 node_type = 'GeometryNode'
1066 if merge_position == 'CENTER':
1067 # average yloc of last two nodes (lowest two)
1068 loc_y = ((nodes_list[len(nodes_list) - 1][2]) + (nodes_list[len(nodes_list) - 2][2])) / 2
1069 if nodes_list[len(nodes_list) - 1][-1]: # if last node is hidden, mix should be shifted up a bit
1070 if do_hide:
1071 loc_y += 40
1072 else:
1073 loc_y += 80
1074 else:
1075 loc_y = nodes_list[len(nodes_list) - 1][2]
1076 offset_y = 100
1077 if not do_hide:
1078 offset_y = 200
1079 if nodes_list == selected_shader and not do_hide_shader:
1080 offset_y = 150.0
1081 the_range = len(nodes_list) - 1
1082 if len(nodes_list) == 1:
1083 the_range = 1
1084 was_multi = False
1085 for i in range(the_range):
1086 if nodes_list == selected_mix:
1087 mix_name = 'Mix'
1088 if tree_type == 'COMPOSITING':
1089 mix_name = 'MixRGB'
1090 add_type = node_type + mix_name
1091 add = nodes.new(add_type)
1092 if tree_type != 'COMPOSITING':
1093 add.data_type = 'RGBA'
1094 add.blend_type = mode
1095 if mode != 'MIX':
1096 add.inputs[0].default_value = 1.0
1097 add.show_preview = False
1098 add.hide = do_hide
1099 if do_hide:
1100 loc_y = loc_y - 50
1101 first = 6
1102 second = 7
1103 if tree_type == 'COMPOSITING':
1104 first = 1
1105 second = 2
1106 elif nodes_list == selected_math:
1107 add_type = node_type + 'Math'
1108 add = nodes.new(add_type)
1109 add.operation = mode
1110 add.hide = do_hide
1111 if do_hide:
1112 loc_y = loc_y - 50
1113 first = 0
1114 second = 1
1115 elif nodes_list == selected_shader:
1116 if mode == 'MIX':
1117 add_type = node_type + 'MixShader'
1118 add = nodes.new(add_type)
1119 add.hide = do_hide_shader
1120 if do_hide_shader:
1121 loc_y = loc_y - 50
1122 first = 1
1123 second = 2
1124 elif mode == 'ADD':
1125 add_type = node_type + 'AddShader'
1126 add = nodes.new(add_type)
1127 add.hide = do_hide_shader
1128 if do_hide_shader:
1129 loc_y = loc_y - 50
1130 first = 0
1131 second = 1
1132 elif nodes_list == selected_geometry:
1133 if mode in ('JOIN', 'MIX'):
1134 add_type = node_type + 'JoinGeometry'
1135 add = self.merge_with_multi_input(
1136 nodes_list, merge_position, do_hide, loc_x, links, nodes, add_type, [0])
1137 else:
1138 add_type = node_type + 'MeshBoolean'
1139 indices = [0, 1] if mode == 'DIFFERENCE' else [1]
1140 add = self.merge_with_multi_input(
1141 nodes_list, merge_position, do_hide, loc_x, links, nodes, add_type, indices)
1142 add.operation = mode
1143 was_multi = True
1144 break
1145 elif nodes_list == selected_vector:
1146 add_type = node_type + 'VectorMath'
1147 add = nodes.new(add_type)
1148 add.operation = mode
1149 add.hide = do_hide
1150 if do_hide:
1151 loc_y = loc_y - 50
1152 first = 0
1153 second = 1
1154 elif nodes_list == selected_z:
1155 add = nodes.new('CompositorNodeZcombine')
1156 add.show_preview = False
1157 add.hide = do_hide
1158 if do_hide:
1159 loc_y = loc_y - 50
1160 first = 0
1161 second = 2
1162 elif nodes_list == selected_alphaover:
1163 add = nodes.new('CompositorNodeAlphaOver')
1164 add.show_preview = False
1165 add.hide = do_hide
1166 if do_hide:
1167 loc_y = loc_y - 50
1168 first = 1
1169 second = 2
1170 add.location = loc_x, loc_y
1171 loc_y += offset_y
1172 add.select = True
1174 # This has already been handled separately
1175 if was_multi:
1176 continue
1177 count_adds = i + 1
1178 count_after = len(nodes)
1179 index = count_after - 1
1180 first_selected = nodes[nodes_list[0][0]]
1181 # "last" node has been added as first, so its index is count_before.
1182 last_add = nodes[count_before]
1183 # Create list of invalid indexes.
1184 invalid_nodes = [nodes[n[0]]
1185 for n in (selected_mix + selected_math + selected_shader + selected_z + selected_geometry)]
1187 # Special case:
1188 # Two nodes were selected and first selected has no output links, second selected has output links.
1189 # Then add links from last add to all links 'to_socket' of out links of second selected.
1190 first_selected_output = get_first_enabled_output(first_selected)
1191 if len(nodes_list) == 2:
1192 if not first_selected_output.links:
1193 second_selected = nodes[nodes_list[1][0]]
1194 for ss_link in get_first_enabled_output(second_selected).links:
1195 # Prevent cyclic dependencies when nodes to be merged are linked to one another.
1196 # Link only if "to_node" index not in invalid indexes list.
1197 if not self.link_creates_cycle(ss_link, invalid_nodes):
1198 connect_sockets(get_first_enabled_output(last_add), ss_link.to_socket)
1199 # add links from last_add to all links 'to_socket' of out links of first selected.
1200 for fs_link in first_selected_output.links:
1201 # Link only if "to_node" index not in invalid indexes list.
1202 if not self.link_creates_cycle(fs_link, invalid_nodes):
1203 connect_sockets(get_first_enabled_output(last_add), fs_link.to_socket)
1204 # add link from "first" selected and "first" add node
1205 node_to = nodes[count_after - 1]
1206 connect_sockets(first_selected_output, node_to.inputs[first])
1207 if node_to.type == 'ZCOMBINE':
1208 for fs_out in first_selected.outputs:
1209 if fs_out != first_selected_output and fs_out.name in ('Z', 'Depth'):
1210 connect_sockets(fs_out, node_to.inputs[1])
1211 break
1212 # add links between added ADD nodes and between selected and ADD nodes
1213 for i in range(count_adds):
1214 if i < count_adds - 1:
1215 node_from = nodes[index]
1216 node_to = nodes[index - 1]
1217 node_to_input_i = first
1218 node_to_z_i = 1 # if z combine - link z to first z input
1219 connect_sockets(get_first_enabled_output(node_from), node_to.inputs[node_to_input_i])
1220 if node_to.type == 'ZCOMBINE':
1221 for from_out in node_from.outputs:
1222 if from_out != get_first_enabled_output(node_from) and from_out.name in ('Z', 'Depth'):
1223 connect_sockets(from_out, node_to.inputs[node_to_z_i])
1224 if len(nodes_list) > 1:
1225 node_from = nodes[nodes_list[i + 1][0]]
1226 node_to = nodes[index]
1227 node_to_input_i = second
1228 node_to_z_i = 3 # if z combine - link z to second z input
1229 connect_sockets(get_first_enabled_output(node_from), node_to.inputs[node_to_input_i])
1230 if node_to.type == 'ZCOMBINE':
1231 for from_out in node_from.outputs:
1232 if from_out != get_first_enabled_output(node_from) and from_out.name in ('Z', 'Depth'):
1233 connect_sockets(from_out, node_to.inputs[node_to_z_i])
1234 index -= 1
1235 # set "last" of added nodes as active
1236 nodes.active = last_add
1237 for i, x, y, dx, h in nodes_list:
1238 nodes[i].select = False
1240 return {'FINISHED'}
1243 class NWBatchChangeNodes(Operator, NWBase):
1244 bl_idname = "node.nw_batch_change"
1245 bl_label = "Batch Change"
1246 bl_description = "Batch Change Blend Type and Math Operation"
1247 bl_options = {'REGISTER', 'UNDO'}
1249 blend_type: EnumProperty(
1250 name="Blend Type",
1251 items=blend_types + navs,
1253 operation: EnumProperty(
1254 name="Operation",
1255 items=operations + navs,
1258 def execute(self, context):
1259 blend_type = self.blend_type
1260 operation = self.operation
1261 for node in context.selected_nodes:
1262 if node.type == 'MIX_RGB' or (node.bl_idname == 'ShaderNodeMix' and node.data_type == 'RGBA'):
1263 if blend_type not in [nav[0] for nav in navs]:
1264 node.blend_type = blend_type
1265 else:
1266 if blend_type == 'NEXT':
1267 index = [i for i, entry in enumerate(blend_types) if node.blend_type in entry][0]
1268 # index = blend_types.index(node.blend_type)
1269 if index == len(blend_types) - 1:
1270 node.blend_type = blend_types[0][0]
1271 else:
1272 node.blend_type = blend_types[index + 1][0]
1274 if blend_type == 'PREV':
1275 index = [i for i, entry in enumerate(blend_types) if node.blend_type in entry][0]
1276 if index == 0:
1277 node.blend_type = blend_types[len(blend_types) - 1][0]
1278 else:
1279 node.blend_type = blend_types[index - 1][0]
1281 if node.type == 'MATH' or node.bl_idname == 'ShaderNodeMath':
1282 if operation not in [nav[0] for nav in navs]:
1283 node.operation = operation
1284 else:
1285 if operation == 'NEXT':
1286 index = [i for i, entry in enumerate(operations) if node.operation in entry][0]
1287 # index = operations.index(node.operation)
1288 if index == len(operations) - 1:
1289 node.operation = operations[0][0]
1290 else:
1291 node.operation = operations[index + 1][0]
1293 if operation == 'PREV':
1294 index = [i for i, entry in enumerate(operations) if node.operation in entry][0]
1295 # index = operations.index(node.operation)
1296 if index == 0:
1297 node.operation = operations[len(operations) - 1][0]
1298 else:
1299 node.operation = operations[index - 1][0]
1301 return {'FINISHED'}
1304 class NWChangeMixFactor(Operator, NWBase):
1305 bl_idname = "node.nw_factor"
1306 bl_label = "Change Factor"
1307 bl_description = "Change Factors of Mix Nodes and Mix Shader Nodes"
1308 bl_options = {'REGISTER', 'UNDO'}
1310 # option: Change factor.
1311 # If option is 1.0 or 0.0 - set to 1.0 or 0.0
1312 # Else - change factor by option value.
1313 option: FloatProperty()
1315 def execute(self, context):
1316 nodes, links = get_nodes_links(context)
1317 option = self.option
1318 selected = [] # entry = index
1319 for si, node in enumerate(nodes):
1320 if node.select:
1321 if node.type in {'MIX_RGB', 'MIX_SHADER'} or node.bl_idname == 'ShaderNodeMix':
1322 selected.append(si)
1324 for si in selected:
1325 fac = nodes[si].inputs[0]
1326 nodes[si].hide = False
1327 if option in {0.0, 1.0}:
1328 fac.default_value = option
1329 else:
1330 fac.default_value += option
1332 return {'FINISHED'}
1335 class NWCopySettings(Operator, NWBase):
1336 bl_idname = "node.nw_copy_settings"
1337 bl_label = "Copy Settings"
1338 bl_description = "Copy Settings of Active Node to Selected Nodes"
1339 bl_options = {'REGISTER', 'UNDO'}
1341 @classmethod
1342 def poll(cls, context):
1343 valid = False
1344 if nw_check(context):
1345 if (
1346 context.active_node is not None and
1347 context.active_node.type != 'FRAME'
1349 valid = True
1350 return valid
1352 def execute(self, context):
1353 node_active = context.active_node
1354 node_selected = context.selected_nodes
1356 # Error handling
1357 if not (len(node_selected) > 1):
1358 self.report({'ERROR'}, "2 nodes must be selected at least")
1359 return {'CANCELLED'}
1361 # Check if active node is in the selection
1362 selected_node_names = [n.name for n in node_selected]
1363 if node_active.name not in selected_node_names:
1364 self.report({'ERROR'}, "No active node")
1365 return {'CANCELLED'}
1367 # Get nodes in selection by type
1368 valid_nodes = [n for n in node_selected if n.type == node_active.type]
1370 if not (len(valid_nodes) > 1) and node_active:
1371 self.report({'ERROR'}, "Selected nodes are not of the same type as {}".format(node_active.name))
1372 return {'CANCELLED'}
1374 if len(valid_nodes) != len(node_selected):
1375 # Report nodes that are not valid
1376 valid_node_names = [n.name for n in valid_nodes]
1377 not_valid_names = list(set(selected_node_names) - set(valid_node_names))
1378 self.report(
1379 {'INFO'},
1380 "Ignored {} (not of the same type as {})".format(
1381 ", ".join(not_valid_names),
1382 node_active.name))
1384 # Reference original
1385 orig = node_active
1386 # node_selected_names = [n.name for n in node_selected]
1388 # Output list
1389 success_names = []
1391 # Deselect all nodes
1392 for i in node_selected:
1393 i.select = False
1395 # Code by zeffii from http://blender.stackexchange.com/a/42338/3710
1396 # Run through all other nodes
1397 for node in valid_nodes[1:]:
1399 # Check for frame node
1400 parent = node.parent if node.parent else None
1401 node_loc = [node.location.x, node.location.y]
1403 # Select original to duplicate
1404 orig.select = True
1406 # Duplicate selected node
1407 bpy.ops.node.duplicate()
1408 new_node = context.selected_nodes[0]
1410 # Deselect copy
1411 new_node.select = False
1413 # Properties to copy
1414 node_tree = node.id_data
1415 props_to_copy = 'bl_idname name location height width'.split(' ')
1417 # Input and outputs
1418 reconnections = []
1419 mappings = chain.from_iterable([node.inputs, node.outputs])
1420 for i in (i for i in mappings if i.is_linked):
1421 for L in i.links:
1422 reconnections.append([L.from_socket.path_from_id(), L.to_socket.path_from_id()])
1424 # Properties
1425 props = {j: getattr(node, j) for j in props_to_copy}
1426 props_to_copy.pop(0)
1428 for prop in props_to_copy:
1429 setattr(new_node, prop, props[prop])
1431 # Get the node tree to remove the old node
1432 nodes = node_tree.nodes
1433 nodes.remove(node)
1434 new_node.name = props['name']
1436 if parent:
1437 new_node.parent = parent
1438 new_node.location = node_loc
1440 for str_from, str_to in reconnections:
1441 node_tree.connect_sockets(eval(str_from), eval(str_to))
1443 success_names.append(new_node.name)
1445 orig.select = True
1446 node_tree.nodes.active = orig
1447 self.report(
1448 {'INFO'},
1449 "Successfully copied attributes from {} to: {}".format(
1450 orig.name,
1451 ", ".join(success_names)))
1452 return {'FINISHED'}
1455 class NWCopyLabel(Operator, NWBase):
1456 bl_idname = "node.nw_copy_label"
1457 bl_label = "Copy Label"
1458 bl_options = {'REGISTER', 'UNDO'}
1460 option: EnumProperty(
1461 name="option",
1462 description="Source of name of label",
1463 items=(
1464 ('FROM_ACTIVE', 'from active', 'from active node',),
1465 ('FROM_NODE', 'from node', 'from node linked to selected node'),
1466 ('FROM_SOCKET', 'from socket', 'from socket linked to selected node'),
1470 def execute(self, context):
1471 nodes, links = get_nodes_links(context)
1472 option = self.option
1473 active = nodes.active
1474 if option == 'FROM_ACTIVE':
1475 if active:
1476 src_label = active.label
1477 for node in [n for n in nodes if n.select and nodes.active != n]:
1478 node.label = src_label
1479 elif option == 'FROM_NODE':
1480 selected = [n for n in nodes if n.select]
1481 for node in selected:
1482 for input in node.inputs:
1483 if input.links:
1484 src = input.links[0].from_node
1485 node.label = src.label
1486 break
1487 elif option == 'FROM_SOCKET':
1488 selected = [n for n in nodes if n.select]
1489 for node in selected:
1490 for input in node.inputs:
1491 if input.links:
1492 src = input.links[0].from_socket
1493 node.label = src.name
1494 break
1496 return {'FINISHED'}
1499 class NWClearLabel(Operator, NWBase):
1500 bl_idname = "node.nw_clear_label"
1501 bl_label = "Clear Label"
1502 bl_options = {'REGISTER', 'UNDO'}
1504 option: BoolProperty()
1506 def execute(self, context):
1507 nodes, links = get_nodes_links(context)
1508 for node in [n for n in nodes if n.select]:
1509 node.label = ''
1511 return {'FINISHED'}
1513 def invoke(self, context, event):
1514 if self.option:
1515 return self.execute(context)
1516 else:
1517 return context.window_manager.invoke_confirm(self, event)
1520 class NWModifyLabels(Operator, NWBase):
1521 """Modify Labels of all selected nodes"""
1522 bl_idname = "node.nw_modify_labels"
1523 bl_label = "Modify Labels"
1524 bl_options = {'REGISTER', 'UNDO'}
1526 prepend: StringProperty(
1527 name="Add to Beginning"
1529 append: StringProperty(
1530 name="Add to End"
1532 replace_from: StringProperty(
1533 name="Text to Replace"
1535 replace_to: StringProperty(
1536 name="Replace with"
1539 def execute(self, context):
1540 nodes, links = get_nodes_links(context)
1541 for node in [n for n in nodes if n.select]:
1542 node.label = self.prepend + node.label.replace(self.replace_from, self.replace_to) + self.append
1544 return {'FINISHED'}
1546 def invoke(self, context, event):
1547 self.prepend = ""
1548 self.append = ""
1549 self.remove = ""
1550 return context.window_manager.invoke_props_dialog(self)
1553 class NWAddTextureSetup(Operator, NWBase):
1554 bl_idname = "node.nw_add_texture"
1555 bl_label = "Texture Setup"
1556 bl_description = "Add Texture Node Setup to Selected Shaders"
1557 bl_options = {'REGISTER', 'UNDO'}
1559 add_mapping: BoolProperty(
1560 name="Add Mapping Nodes",
1561 description="Create coordinate and mapping nodes for the texture (ignored for selected texture nodes)",
1562 default=True)
1564 @classmethod
1565 def poll(cls, context):
1566 if nw_check(context):
1567 space = context.space_data
1568 if space.tree_type == 'ShaderNodeTree':
1569 return True
1570 return False
1572 def execute(self, context):
1573 nodes, links = get_nodes_links(context)
1575 texture_types = get_texture_node_types()
1576 selected_nodes = [n for n in nodes if n.select]
1578 for node in selected_nodes:
1579 if not node.inputs:
1580 continue
1582 input_index = 0
1583 target_input = node.inputs[0]
1584 for input in node.inputs:
1585 if input.enabled:
1586 input_index += 1
1587 if not input.is_linked:
1588 target_input = input
1589 break
1590 else:
1591 self.report({'WARNING'}, "No free inputs for node: " + node.name)
1592 continue
1594 x_offset = 0
1595 padding = 40.0
1596 locx = node.location.x
1597 locy = node.location.y - (input_index * padding)
1599 is_texture_node = node.rna_type.identifier in texture_types
1600 use_environment_texture = node.type == 'BACKGROUND'
1602 # Add an image texture before normal shader nodes.
1603 if not is_texture_node:
1604 image_texture_type = 'ShaderNodeTexEnvironment' if use_environment_texture else 'ShaderNodeTexImage'
1605 image_texture_node = nodes.new(image_texture_type)
1606 x_offset = x_offset + image_texture_node.width + padding
1607 image_texture_node.location = [locx - x_offset, locy]
1608 nodes.active = image_texture_node
1609 connect_sockets(image_texture_node.outputs[0], target_input)
1611 # The mapping setup following this will connect to the first input of this image texture.
1612 target_input = image_texture_node.inputs[0]
1614 node.select = False
1616 if is_texture_node or self.add_mapping:
1617 # Add Mapping node.
1618 mapping_node = nodes.new('ShaderNodeMapping')
1619 x_offset = x_offset + mapping_node.width + padding
1620 mapping_node.location = [locx - x_offset, locy]
1621 connect_sockets(mapping_node.outputs[0], target_input)
1623 # Add Texture Coordinates node.
1624 tex_coord_node = nodes.new('ShaderNodeTexCoord')
1625 x_offset = x_offset + tex_coord_node.width + padding
1626 tex_coord_node.location = [locx - x_offset, locy]
1628 is_procedural_texture = is_texture_node and node.type != 'TEX_IMAGE'
1629 use_generated_coordinates = is_procedural_texture or use_environment_texture
1630 tex_coord_output = tex_coord_node.outputs[0 if use_generated_coordinates else 2]
1631 connect_sockets(tex_coord_output, mapping_node.inputs[0])
1633 return {'FINISHED'}
1636 class NWAddPrincipledSetup(Operator, NWBase, ImportHelper):
1637 bl_idname = "node.nw_add_textures_for_principled"
1638 bl_label = "Principled Texture Setup"
1639 bl_description = "Add Texture Node Setup for Principled BSDF"
1640 bl_options = {'REGISTER', 'UNDO'}
1642 directory: StringProperty(
1643 name='Directory',
1644 subtype='DIR_PATH',
1645 default='',
1646 description='Folder to search in for image files'
1648 files: CollectionProperty(
1649 type=bpy.types.OperatorFileListElement,
1650 options={'HIDDEN', 'SKIP_SAVE'}
1653 relative_path: BoolProperty(
1654 name='Relative Path',
1655 description='Set the file path relative to the blend file, when possible',
1656 default=True
1659 order = [
1660 "filepath",
1661 "files",
1664 def draw(self, context):
1665 layout = self.layout
1666 layout.alignment = 'LEFT'
1668 layout.prop(self, 'relative_path')
1670 @classmethod
1671 def poll(cls, context):
1672 valid = False
1673 if nw_check(context):
1674 space = context.space_data
1675 if space.tree_type == 'ShaderNodeTree':
1676 valid = True
1677 return valid
1679 def execute(self, context):
1680 # Check if everything is ok
1681 if not self.directory:
1682 self.report({'INFO'}, 'No Folder Selected')
1683 return {'CANCELLED'}
1684 if not self.files[:]:
1685 self.report({'INFO'}, 'No Files Selected')
1686 return {'CANCELLED'}
1688 nodes, links = get_nodes_links(context)
1689 active_node = nodes.active
1690 if not (active_node and active_node.bl_idname == 'ShaderNodeBsdfPrincipled'):
1691 self.report({'INFO'}, 'Select Principled BSDF')
1692 return {'CANCELLED'}
1694 # Filter textures names for texturetypes in filenames
1695 # [Socket Name, [abbreviations and keyword list], Filename placeholder]
1696 tags = context.preferences.addons[__package__].preferences.principled_tags
1697 normal_abbr = tags.normal.split(' ')
1698 bump_abbr = tags.bump.split(' ')
1699 gloss_abbr = tags.gloss.split(' ')
1700 rough_abbr = tags.rough.split(' ')
1701 socketnames = [
1702 ['Displacement', tags.displacement.split(' '), None],
1703 ['Base Color', tags.base_color.split(' '), None],
1704 ['Metallic', tags.metallic.split(' '), None],
1705 ['Specular IOR Level', tags.specular.split(' '), None],
1706 ['Roughness', rough_abbr + gloss_abbr, None],
1707 ['Normal', normal_abbr + bump_abbr, None],
1708 ['Transmission Weight', tags.transmission.split(' '), None],
1709 ['Emission Color', tags.emission.split(' '), None],
1710 ['Alpha', tags.alpha.split(' '), None],
1711 ['Ambient Occlusion', tags.ambient_occlusion.split(' '), None],
1714 match_files_to_socket_names(self.files, socketnames)
1715 # Remove socketnames without found files
1716 socketnames = [s for s in socketnames if s[2]
1717 and path.exists(self.directory + s[2])]
1718 if not socketnames:
1719 self.report({'INFO'}, 'No matching images found')
1720 print('No matching images found')
1721 return {'CANCELLED'}
1723 # Don't override path earlier as os.path is used to check the absolute path
1724 import_path = self.directory
1725 if self.relative_path:
1726 if bpy.data.filepath:
1727 try:
1728 import_path = bpy.path.relpath(self.directory)
1729 except ValueError:
1730 pass
1732 # Add found images
1733 print('\nMatched Textures:')
1734 texture_nodes = []
1735 disp_texture = None
1736 ao_texture = None
1737 normal_node = None
1738 roughness_node = None
1739 for i, sname in enumerate(socketnames):
1740 print(i, sname[0], sname[2])
1742 # DISPLACEMENT NODES
1743 if sname[0] == 'Displacement':
1744 disp_texture = nodes.new(type='ShaderNodeTexImage')
1745 img = bpy.data.images.load(path.join(import_path, sname[2]))
1746 disp_texture.image = img
1747 disp_texture.label = 'Displacement'
1748 if disp_texture.image:
1749 disp_texture.image.colorspace_settings.is_data = True
1751 # Add displacement offset nodes
1752 disp_node = nodes.new(type='ShaderNodeDisplacement')
1753 # Align the Displacement node under the active Principled BSDF node
1754 disp_node.location = active_node.location + Vector((100, -700))
1755 link = connect_sockets(disp_node.inputs[0], disp_texture.outputs[0])
1757 # TODO Turn on true displacement in the material
1758 # Too complicated for now
1760 # Find output node
1761 output_node = [n for n in nodes if n.bl_idname == 'ShaderNodeOutputMaterial']
1762 if output_node:
1763 if not output_node[0].inputs[2].is_linked:
1764 link = connect_sockets(output_node[0].inputs[2], disp_node.outputs[0])
1766 continue
1768 # AMBIENT OCCLUSION TEXTURE
1769 if sname[0] == 'Ambient Occlusion':
1770 ao_texture = nodes.new(type='ShaderNodeTexImage')
1771 img = bpy.data.images.load(path.join(import_path, sname[2]))
1772 ao_texture.image = img
1773 ao_texture.label = sname[0]
1774 if ao_texture.image:
1775 ao_texture.image.colorspace_settings.is_data = True
1777 continue
1779 if not active_node.inputs[sname[0]].is_linked:
1780 # No texture node connected -> add texture node with new image
1781 texture_node = nodes.new(type='ShaderNodeTexImage')
1782 img = bpy.data.images.load(path.join(import_path, sname[2]))
1783 texture_node.image = img
1785 # NORMAL NODES
1786 if sname[0] == 'Normal':
1787 # Test if new texture node is normal or bump map
1788 fname_components = split_into_components(sname[2])
1789 match_normal = set(normal_abbr).intersection(set(fname_components))
1790 match_bump = set(bump_abbr).intersection(set(fname_components))
1791 if match_normal:
1792 # If Normal add normal node in between
1793 normal_node = nodes.new(type='ShaderNodeNormalMap')
1794 link = connect_sockets(normal_node.inputs[1], texture_node.outputs[0])
1795 elif match_bump:
1796 # If Bump add bump node in between
1797 normal_node = nodes.new(type='ShaderNodeBump')
1798 link = connect_sockets(normal_node.inputs[2], texture_node.outputs[0])
1800 link = connect_sockets(active_node.inputs[sname[0]], normal_node.outputs[0])
1801 normal_node_texture = texture_node
1803 elif sname[0] == 'Roughness':
1804 # Test if glossy or roughness map
1805 fname_components = split_into_components(sname[2])
1806 match_rough = set(rough_abbr).intersection(set(fname_components))
1807 match_gloss = set(gloss_abbr).intersection(set(fname_components))
1809 if match_rough:
1810 # If Roughness nothing to to
1811 link = connect_sockets(active_node.inputs[sname[0]], texture_node.outputs[0])
1813 elif match_gloss:
1814 # If Gloss Map add invert node
1815 invert_node = nodes.new(type='ShaderNodeInvert')
1816 link = connect_sockets(invert_node.inputs[1], texture_node.outputs[0])
1818 link = connect_sockets(active_node.inputs[sname[0]], invert_node.outputs[0])
1819 roughness_node = texture_node
1821 else:
1822 # This is a simple connection Texture --> Input slot
1823 link = connect_sockets(active_node.inputs[sname[0]], texture_node.outputs[0])
1825 # Use non-color except for color inputs
1826 if sname[0] not in ['Base Color', 'Emission Color'] and texture_node.image:
1827 texture_node.image.colorspace_settings.is_data = True
1829 else:
1830 # If already texture connected. add to node list for alignment
1831 texture_node = active_node.inputs[sname[0]].links[0].from_node
1833 # This are all connected texture nodes
1834 texture_nodes.append(texture_node)
1835 texture_node.label = sname[0]
1837 if disp_texture:
1838 texture_nodes.append(disp_texture)
1840 if ao_texture:
1841 # We want the ambient occlusion texture to be the top most texture node
1842 texture_nodes.insert(0, ao_texture)
1844 # Alignment
1845 for i, texture_node in enumerate(texture_nodes):
1846 offset = Vector((-550, (i * -280) + 200))
1847 texture_node.location = active_node.location + offset
1849 if normal_node:
1850 # Extra alignment if normal node was added
1851 normal_node.location = normal_node_texture.location + Vector((300, 0))
1853 if roughness_node:
1854 # Alignment of invert node if glossy map
1855 invert_node.location = roughness_node.location + Vector((300, 0))
1857 # Add texture input + mapping
1858 mapping = nodes.new(type='ShaderNodeMapping')
1859 mapping.location = active_node.location + Vector((-1050, 0))
1860 if len(texture_nodes) > 1:
1861 # If more than one texture add reroute node in between
1862 reroute = nodes.new(type='NodeReroute')
1863 texture_nodes.append(reroute)
1864 tex_coords = Vector((texture_nodes[0].location.x,
1865 sum(n.location.y for n in texture_nodes) / len(texture_nodes)))
1866 reroute.location = tex_coords + Vector((-50, -120))
1867 for texture_node in texture_nodes:
1868 link = connect_sockets(texture_node.inputs[0], reroute.outputs[0])
1869 link = connect_sockets(reroute.inputs[0], mapping.outputs[0])
1870 else:
1871 link = connect_sockets(texture_nodes[0].inputs[0], mapping.outputs[0])
1873 # Connect texture_coordiantes to mapping node
1874 texture_input = nodes.new(type='ShaderNodeTexCoord')
1875 texture_input.location = mapping.location + Vector((-200, 0))
1876 link = connect_sockets(mapping.inputs[0], texture_input.outputs[2])
1878 # Create frame around tex coords and mapping
1879 frame = nodes.new(type='NodeFrame')
1880 frame.label = 'Mapping'
1881 mapping.parent = frame
1882 texture_input.parent = frame
1883 frame.update()
1885 # Create frame around texture nodes
1886 frame = nodes.new(type='NodeFrame')
1887 frame.label = 'Textures'
1888 for tnode in texture_nodes:
1889 tnode.parent = frame
1890 frame.update()
1892 # Just to be sure
1893 active_node.select = False
1894 nodes.update()
1895 links.update()
1896 force_update(context)
1897 return {'FINISHED'}
1900 class NWAddReroutes(Operator, NWBase):
1901 """Add Reroute Nodes and link them to outputs of selected nodes"""
1902 bl_idname = "node.nw_add_reroutes"
1903 bl_label = "Add Reroutes"
1904 bl_description = "Add Reroutes to Outputs"
1905 bl_options = {'REGISTER', 'UNDO'}
1907 option: EnumProperty(
1908 name="option",
1909 items=[
1910 ('ALL', 'to all', 'Add to all outputs'),
1911 ('LOOSE', 'to loose', 'Add only to loose outputs'),
1912 ('LINKED', 'to linked', 'Add only to linked outputs'),
1916 def execute(self, context):
1917 tree_type = context.space_data.node_tree.type
1918 option = self.option
1919 nodes, links = get_nodes_links(context)
1920 # output valid when option is 'all' or when 'loose' output has no links
1921 valid = False
1922 post_select = [] # nodes to be selected after execution
1923 # create reroutes and recreate links
1924 for node in [n for n in nodes if n.select]:
1925 if node.outputs:
1926 x = node.location.x
1927 y = node.location.y
1928 width = node.width
1929 # unhide 'REROUTE' nodes to avoid issues with location.y
1930 if node.type == 'REROUTE':
1931 node.hide = False
1932 # Hack needed to calculate real width
1933 if node.hide:
1934 bpy.ops.node.select_all(action='DESELECT')
1935 helper = nodes.new('NodeReroute')
1936 helper.select = True
1937 node.select = True
1938 # resize node and helper to zero. Then check locations to calculate width
1939 bpy.ops.transform.resize(value=(0.0, 0.0, 0.0))
1940 width = 2.0 * (helper.location.x - node.location.x)
1941 # restore node location
1942 node.location = x, y
1943 # delete helper
1944 node.select = False
1945 # only helper is selected now
1946 bpy.ops.node.delete()
1947 x = node.location.x + width + 20.0
1948 if node.type != 'REROUTE':
1949 y -= 35.0
1950 y_offset = -22.0
1951 loc = x, y
1952 reroutes_count = 0 # will be used when aligning reroutes added to hidden nodes
1953 for out_i, output in enumerate(node.outputs):
1954 pass_used = False # initial value to be analyzed if 'R_LAYERS'
1955 # if node != 'R_LAYERS' - "pass_used" not needed, so set it to True
1956 if node.type != 'R_LAYERS':
1957 pass_used = True
1958 else: # if 'R_LAYERS' check if output represent used render pass
1959 node_scene = node.scene
1960 node_layer = node.layer
1961 # If output - "Alpha" is analyzed - assume it's used. Not represented in passes.
1962 if output.name == 'Alpha':
1963 pass_used = True
1964 else:
1965 # check entries in global 'rl_outputs' variable
1966 for rlo in rl_outputs:
1967 if output.name in {rlo.output_name, rlo.exr_output_name}:
1968 pass_used = getattr(node_scene.view_layers[node_layer], rlo.render_pass)
1969 break
1970 if pass_used:
1971 valid = ((option == 'ALL') or
1972 (option == 'LOOSE' and not output.links) or
1973 (option == 'LINKED' and output.links))
1974 # Add reroutes only if valid, but offset location in all cases.
1975 if valid:
1976 n = nodes.new('NodeReroute')
1977 nodes.active = n
1978 for link in output.links:
1979 connect_sockets(n.outputs[0], link.to_socket)
1980 connect_sockets(output, n.inputs[0])
1981 n.location = loc
1982 post_select.append(n)
1983 reroutes_count += 1
1984 y += y_offset
1985 loc = x, y
1986 # disselect the node so that after execution of script only newly created nodes are selected
1987 node.select = False
1988 # nicer reroutes distribution along y when node.hide
1989 if node.hide:
1990 y_translate = reroutes_count * y_offset / 2.0 - y_offset - 35.0
1991 for reroute in [r for r in nodes if r.select]:
1992 reroute.location.y -= y_translate
1993 for node in post_select:
1994 node.select = True
1996 return {'FINISHED'}
1999 class NWLinkActiveToSelected(Operator, NWBase):
2000 """Link active node to selected nodes basing on various criteria"""
2001 bl_idname = "node.nw_link_active_to_selected"
2002 bl_label = "Link Active Node to Selected"
2003 bl_options = {'REGISTER', 'UNDO'}
2005 replace: BoolProperty()
2006 use_node_name: BoolProperty()
2007 use_outputs_names: BoolProperty()
2009 @classmethod
2010 def poll(cls, context):
2011 valid = False
2012 if nw_check(context):
2013 if context.active_node is not None:
2014 if context.active_node.select:
2015 valid = True
2016 return valid
2018 def execute(self, context):
2019 nodes, links = get_nodes_links(context)
2020 replace = self.replace
2021 use_node_name = self.use_node_name
2022 use_outputs_names = self.use_outputs_names
2023 active = nodes.active
2024 selected = [node for node in nodes if node.select and node != active]
2025 outputs = [] # Only usable outputs of active nodes will be stored here.
2026 for out in active.outputs:
2027 if active.type != 'R_LAYERS':
2028 outputs.append(out)
2029 else:
2030 # 'R_LAYERS' node type needs special handling.
2031 # outputs of 'R_LAYERS' are callable even if not seen in UI.
2032 # Only outputs that represent used passes should be taken into account
2033 # Check if pass represented by output is used.
2034 # global 'rl_outputs' list will be used for that
2035 for rlo in rl_outputs:
2036 pass_used = False # initial value. Will be set to True if pass is used
2037 if out.name == 'Alpha':
2038 # Alpha output is always present. Doesn't have representation in render pass. Assume it's used.
2039 pass_used = True
2040 elif out.name in {rlo.output_name, rlo.exr_output_name}:
2041 # example 'render_pass' entry: 'use_pass_uv' Check if True in scene render layers
2042 pass_used = getattr(active.scene.view_layers[active.layer], rlo.render_pass)
2043 break
2044 if pass_used:
2045 outputs.append(out)
2046 doit = True # Will be changed to False when links successfully added to previous output.
2047 for out in outputs:
2048 if doit:
2049 for node in selected:
2050 dst_name = node.name # Will be compared with src_name if needed.
2051 # When node has label - use it as dst_name
2052 if node.label:
2053 dst_name = node.label
2054 valid = True # Initial value. Will be changed to False if names don't match.
2055 src_name = dst_name # If names not used - this assignment will keep valid = True.
2056 if use_node_name:
2057 # Set src_name to source node name or label
2058 src_name = active.name
2059 if active.label:
2060 src_name = active.label
2061 elif use_outputs_names:
2062 src_name = (out.name, )
2063 for rlo in rl_outputs:
2064 if out.name in {rlo.output_name, rlo.exr_output_name}:
2065 src_name = (rlo.output_name, rlo.exr_output_name)
2066 if dst_name not in src_name:
2067 valid = False
2068 if valid:
2069 for input in node.inputs:
2070 if input.type == out.type or node.type == 'REROUTE':
2071 if replace or not input.is_linked:
2072 connect_sockets(out, input)
2073 if not use_node_name and not use_outputs_names:
2074 doit = False
2075 break
2077 return {'FINISHED'}
2080 class NWAlignNodes(Operator, NWBase):
2081 '''Align the selected nodes neatly in a row/column'''
2082 bl_idname = "node.nw_align_nodes"
2083 bl_label = "Align Nodes"
2084 bl_options = {'REGISTER', 'UNDO'}
2085 margin: IntProperty(name='Margin', default=50, description='The amount of space between nodes')
2087 def execute(self, context):
2088 nodes, links = get_nodes_links(context)
2089 margin = self.margin
2091 selection = []
2092 for node in nodes:
2093 if node.select and node.type != 'FRAME':
2094 selection.append(node)
2096 # If no nodes are selected, align all nodes
2097 active_loc = None
2098 if not selection:
2099 selection = nodes
2100 elif nodes.active in selection:
2101 active_loc = copy(nodes.active.location) # make a copy, not a reference
2103 # Check if nodes should be laid out horizontally or vertically
2104 # use dimension to get center of node, not corner
2105 x_locs = [n.location.x + (n.dimensions.x / 2) for n in selection]
2106 y_locs = [n.location.y - (n.dimensions.y / 2) for n in selection]
2107 x_range = max(x_locs) - min(x_locs)
2108 y_range = max(y_locs) - min(y_locs)
2109 mid_x = (max(x_locs) + min(x_locs)) / 2
2110 mid_y = (max(y_locs) + min(y_locs)) / 2
2111 horizontal = x_range > y_range
2113 # Sort selection by location of node mid-point
2114 if horizontal:
2115 selection = sorted(selection, key=lambda n: n.location.x + (n.dimensions.x / 2))
2116 else:
2117 selection = sorted(selection, key=lambda n: n.location.y - (n.dimensions.y / 2), reverse=True)
2119 # Alignment
2120 current_pos = 0
2121 for node in selection:
2122 current_margin = margin
2123 current_margin = current_margin * 0.5 if node.hide else current_margin # use a smaller margin for hidden nodes
2125 if horizontal:
2126 node.location.x = current_pos
2127 current_pos += current_margin + node.dimensions.x
2128 node.location.y = mid_y + (node.dimensions.y / 2)
2129 else:
2130 node.location.y = current_pos
2131 current_pos -= (current_margin * 0.3) + node.dimensions.y # use half-margin for vertical alignment
2132 node.location.x = mid_x - (node.dimensions.x / 2)
2134 # If active node is selected, center nodes around it
2135 if active_loc is not None:
2136 active_loc_diff = active_loc - nodes.active.location
2137 for node in selection:
2138 node.location += active_loc_diff
2139 else: # Position nodes centered around where they used to be
2140 locs = ([n.location.x + (n.dimensions.x / 2) for n in selection]
2141 ) if horizontal else ([n.location.y - (n.dimensions.y / 2) for n in selection])
2142 new_mid = (max(locs) + min(locs)) / 2
2143 for node in selection:
2144 if horizontal:
2145 node.location.x += (mid_x - new_mid)
2146 else:
2147 node.location.y += (mid_y - new_mid)
2149 return {'FINISHED'}
2152 class NWSelectParentChildren(Operator, NWBase):
2153 bl_idname = "node.nw_select_parent_child"
2154 bl_label = "Select Parent or Children"
2155 bl_options = {'REGISTER', 'UNDO'}
2157 option: EnumProperty(
2158 name="option",
2159 items=(
2160 ('PARENT', 'Select Parent', 'Select Parent Frame'),
2161 ('CHILD', 'Select Children', 'Select members of selected frame'),
2165 def execute(self, context):
2166 nodes, links = get_nodes_links(context)
2167 option = self.option
2168 selected = [node for node in nodes if node.select]
2169 if option == 'PARENT':
2170 for sel in selected:
2171 parent = sel.parent
2172 if parent:
2173 parent.select = True
2174 else: # option == 'CHILD'
2175 for sel in selected:
2176 children = [node for node in nodes if node.parent == sel]
2177 for kid in children:
2178 kid.select = True
2180 return {'FINISHED'}
2183 class NWDetachOutputs(Operator, NWBase):
2184 """Detach outputs of selected node leaving inputs linked"""
2185 bl_idname = "node.nw_detach_outputs"
2186 bl_label = "Detach Outputs"
2187 bl_options = {'REGISTER', 'UNDO'}
2189 def execute(self, context):
2190 nodes, links = get_nodes_links(context)
2191 selected = context.selected_nodes
2192 bpy.ops.node.duplicate_move_keep_inputs()
2193 new_nodes = context.selected_nodes
2194 bpy.ops.node.select_all(action="DESELECT")
2195 for node in selected:
2196 node.select = True
2197 bpy.ops.node.delete_reconnect()
2198 for new_node in new_nodes:
2199 new_node.select = True
2200 bpy.ops.transform.translate('INVOKE_DEFAULT')
2202 return {'FINISHED'}
2205 class NWLinkToOutputNode(Operator):
2206 """Link to Composite node or Material Output node"""
2207 bl_idname = "node.nw_link_out"
2208 bl_label = "Connect to Output"
2209 bl_options = {'REGISTER', 'UNDO'}
2211 @classmethod
2212 def poll(cls, context):
2213 valid = False
2214 if nw_check(context):
2215 if context.active_node is not None:
2216 for out in context.active_node.outputs:
2217 if is_visible_socket(out):
2218 valid = True
2219 break
2220 return valid
2222 def execute(self, context):
2223 nodes, links = get_nodes_links(context)
2224 active = nodes.active
2225 output_index = None
2226 tree_type = context.space_data.tree_type
2227 shader_outputs = {'OBJECT': 'ShaderNodeOutputMaterial',
2228 'WORLD': 'ShaderNodeOutputWorld',
2229 'LINESTYLE': 'ShaderNodeOutputLineStyle'}
2230 output_type = {
2231 'ShaderNodeTree': shader_outputs[context.space_data.shader_type],
2232 'CompositorNodeTree': 'CompositorNodeComposite',
2233 'TextureNodeTree': 'TextureNodeOutput',
2234 'GeometryNodeTree': 'NodeGroupOutput',
2235 }[tree_type]
2236 for node in nodes:
2237 # check whether the node is an output node and,
2238 # if supported, whether it's the active one
2239 if node.rna_type.identifier == output_type \
2240 and (node.is_active_output if hasattr(node, 'is_active_output')
2241 else True):
2242 output_node = node
2243 break
2244 else: # No output node exists
2245 bpy.ops.node.select_all(action="DESELECT")
2246 output_node = nodes.new(output_type)
2247 output_node.location.x = active.location.x + active.dimensions.x + 80
2248 output_node.location.y = active.location.y
2250 if active.outputs:
2251 for i, output in enumerate(active.outputs):
2252 if is_visible_socket(output):
2253 output_index = i
2254 break
2255 for i, output in enumerate(active.outputs):
2256 if output.type == output_node.inputs[0].type and is_visible_socket(output):
2257 output_index = i
2258 break
2260 out_input_index = 0
2261 if tree_type == 'ShaderNodeTree':
2262 if active.outputs[output_index].name == 'Volume':
2263 out_input_index = 1
2264 elif active.outputs[output_index].name == 'Displacement':
2265 out_input_index = 2
2266 elif tree_type == 'GeometryNodeTree':
2267 if active.outputs[output_index].type != 'GEOMETRY':
2268 return {'CANCELLED'}
2269 connect_sockets(active.outputs[output_index], output_node.inputs[out_input_index])
2271 force_update(context) # viewport render does not update
2273 return {'FINISHED'}
2276 class NWMakeLink(Operator, NWBase):
2277 """Make a link from one socket to another"""
2278 bl_idname = 'node.nw_make_link'
2279 bl_label = 'Make Link'
2280 bl_options = {'REGISTER', 'UNDO'}
2281 from_socket: IntProperty()
2282 to_socket: IntProperty()
2284 def execute(self, context):
2285 nodes, links = get_nodes_links(context)
2287 n1 = nodes[context.scene.NWLazySource]
2288 n2 = nodes[context.scene.NWLazyTarget]
2290 connect_sockets(n1.outputs[self.from_socket], n2.inputs[self.to_socket])
2292 force_update(context)
2294 return {'FINISHED'}
2297 class NWCallInputsMenu(Operator, NWBase):
2298 """Link from this output"""
2299 bl_idname = 'node.nw_call_inputs_menu'
2300 bl_label = 'Make Link'
2301 bl_options = {'REGISTER', 'UNDO'}
2302 from_socket: IntProperty()
2304 def execute(self, context):
2305 nodes, links = get_nodes_links(context)
2307 context.scene.NWSourceSocket = self.from_socket
2309 n1 = nodes[context.scene.NWLazySource]
2310 n2 = nodes[context.scene.NWLazyTarget]
2311 if len(n2.inputs) > 1:
2312 bpy.ops.wm.call_menu("INVOKE_DEFAULT", name=NWConnectionListInputs.bl_idname)
2313 elif len(n2.inputs) == 1:
2314 connect_sockets(n1.outputs[self.from_socket], n2.inputs[0])
2315 return {'FINISHED'}
2318 class NWAddSequence(Operator, NWBase, ImportHelper):
2319 """Add an Image Sequence"""
2320 bl_idname = 'node.nw_add_sequence'
2321 bl_label = 'Import Image Sequence'
2322 bl_options = {'REGISTER', 'UNDO'}
2324 directory: StringProperty(
2325 subtype="DIR_PATH"
2327 filename: StringProperty(
2328 subtype="FILE_NAME"
2330 files: CollectionProperty(
2331 type=bpy.types.OperatorFileListElement,
2332 options={'HIDDEN', 'SKIP_SAVE'}
2334 relative_path: BoolProperty(
2335 name='Relative Path',
2336 description='Set the file path relative to the blend file, when possible',
2337 default=True
2340 def draw(self, context):
2341 layout = self.layout
2342 layout.alignment = 'LEFT'
2344 layout.prop(self, 'relative_path')
2346 def execute(self, context):
2347 nodes, links = get_nodes_links(context)
2348 directory = self.directory
2349 filename = self.filename
2350 files = self.files
2351 tree = context.space_data.node_tree
2353 # DEBUG
2354 # print ("\nDIR:", directory)
2355 # print ("FN:", filename)
2356 # print ("Fs:", list(f.name for f in files), '\n')
2358 if tree.type == 'SHADER':
2359 node_type = "ShaderNodeTexImage"
2360 elif tree.type == 'COMPOSITING':
2361 node_type = "CompositorNodeImage"
2362 else:
2363 self.report({'ERROR'}, "Unsupported Node Tree type!")
2364 return {'CANCELLED'}
2366 if not files[0].name and not filename:
2367 self.report({'ERROR'}, "No file chosen")
2368 return {'CANCELLED'}
2369 elif files[0].name and (not filename or not path.exists(directory + filename)):
2370 # User has selected multiple files without an active one, or the active one is non-existent
2371 filename = files[0].name
2373 if not path.exists(directory + filename):
2374 self.report({'ERROR'}, filename + " does not exist!")
2375 return {'CANCELLED'}
2377 without_ext = '.'.join(filename.split('.')[:-1])
2379 # if last digit isn't a number, it's not a sequence
2380 if not without_ext[-1].isdigit():
2381 self.report({'ERROR'}, filename + " does not seem to be part of a sequence")
2382 return {'CANCELLED'}
2384 extension = filename.split('.')[-1]
2385 reverse = without_ext[::-1] # reverse string
2387 count_numbers = 0
2388 for char in reverse:
2389 if char.isdigit():
2390 count_numbers += 1
2391 else:
2392 break
2394 without_num = without_ext[:count_numbers * -1]
2396 files = sorted(glob(directory + without_num + "[0-9]" * count_numbers + "." + extension))
2398 num_frames = len(files)
2400 nodes_list = [node for node in nodes]
2401 if nodes_list:
2402 nodes_list.sort(key=lambda k: k.location.x)
2403 xloc = nodes_list[0].location.x - 220 # place new nodes at far left
2404 yloc = 0
2405 for node in nodes:
2406 node.select = False
2407 yloc += node_mid_pt(node, 'y')
2408 yloc = yloc / len(nodes)
2409 else:
2410 xloc = 0
2411 yloc = 0
2413 name_with_hashes = without_num + "#" * count_numbers + '.' + extension
2415 bpy.ops.node.add_node('INVOKE_DEFAULT', use_transform=True, type=node_type)
2416 node = nodes.active
2417 node.label = name_with_hashes
2419 filepath = directory + (without_ext + '.' + extension)
2420 if self.relative_path:
2421 if bpy.data.filepath:
2422 try:
2423 filepath = bpy.path.relpath(filepath)
2424 except ValueError:
2425 pass
2427 img = bpy.data.images.load(filepath)
2428 img.source = 'SEQUENCE'
2429 img.name = name_with_hashes
2430 node.image = img
2431 image_user = node.image_user if tree.type == 'SHADER' else node
2432 # separate the number from the file name of the first file
2433 image_user.frame_offset = int(files[0][len(without_num) + len(directory):-1 * (len(extension) + 1)]) - 1
2434 image_user.frame_duration = num_frames
2436 return {'FINISHED'}
2439 class NWAddMultipleImages(Operator, NWBase, ImportHelper):
2440 """Add multiple images at once"""
2441 bl_idname = 'node.nw_add_multiple_images'
2442 bl_label = 'Open Selected Images'
2443 bl_options = {'REGISTER', 'UNDO'}
2444 directory: StringProperty(
2445 subtype="DIR_PATH"
2447 files: CollectionProperty(
2448 type=bpy.types.OperatorFileListElement,
2449 options={'HIDDEN', 'SKIP_SAVE'}
2452 def execute(self, context):
2453 nodes, links = get_nodes_links(context)
2455 xloc, yloc = context.region.view2d.region_to_view(context.area.width / 2, context.area.height / 2)
2457 if context.space_data.node_tree.type == 'SHADER':
2458 node_type = "ShaderNodeTexImage"
2459 elif context.space_data.node_tree.type == 'COMPOSITING':
2460 node_type = "CompositorNodeImage"
2461 else:
2462 self.report({'ERROR'}, "Unsupported Node Tree type!")
2463 return {'CANCELLED'}
2465 new_nodes = []
2466 for f in self.files:
2467 fname = f.name
2469 node = nodes.new(node_type)
2470 new_nodes.append(node)
2471 node.label = fname
2472 node.hide = True
2473 node.location.x = xloc
2474 node.location.y = yloc
2475 yloc -= 40
2477 img = bpy.data.images.load(self.directory + fname)
2478 node.image = img
2480 # shift new nodes up to center of tree
2481 list_size = new_nodes[0].location.y - new_nodes[-1].location.y
2482 for node in nodes:
2483 if node in new_nodes:
2484 node.select = True
2485 node.location.y += (list_size / 2)
2486 else:
2487 node.select = False
2488 return {'FINISHED'}
2491 class NWViewerFocus(bpy.types.Operator):
2492 """Set the viewer tile center to the mouse position"""
2493 bl_idname = "node.nw_viewer_focus"
2494 bl_label = "Viewer Focus"
2496 x: bpy.props.IntProperty()
2497 y: bpy.props.IntProperty()
2499 @classmethod
2500 def poll(cls, context):
2501 return nw_check(context) and context.space_data.tree_type == 'CompositorNodeTree'
2503 def execute(self, context):
2504 return {'FINISHED'}
2506 def invoke(self, context, event):
2507 render = context.scene.render
2508 space = context.space_data
2509 percent = render.resolution_percentage * 0.01
2511 nodes, links = get_nodes_links(context)
2512 viewers = [n for n in nodes if n.type == 'VIEWER']
2514 if viewers:
2515 mlocx = event.mouse_region_x
2516 mlocy = event.mouse_region_y
2517 select_node = bpy.ops.node.select(location=(mlocx, mlocy), extend=False)
2519 if 'FINISHED' not in select_node: # only run if we're not clicking on a node
2520 region_x = context.region.width
2521 region_y = context.region.height
2523 region_center_x = context.region.width / 2
2524 region_center_y = context.region.height / 2
2526 bd_x = render.resolution_x * percent * space.backdrop_zoom
2527 bd_y = render.resolution_y * percent * space.backdrop_zoom
2529 backdrop_center_x = (bd_x / 2) - space.backdrop_offset[0]
2530 backdrop_center_y = (bd_y / 2) - space.backdrop_offset[1]
2532 margin_x = region_center_x - backdrop_center_x
2533 margin_y = region_center_y - backdrop_center_y
2535 abs_mouse_x = (mlocx - margin_x) / bd_x
2536 abs_mouse_y = (mlocy - margin_y) / bd_y
2538 for node in viewers:
2539 node.center_x = abs_mouse_x
2540 node.center_y = abs_mouse_y
2541 else:
2542 return {'PASS_THROUGH'}
2544 return self.execute(context)
2547 class NWSaveViewer(bpy.types.Operator, ExportHelper):
2548 """Save the current viewer node to an image file"""
2549 bl_idname = "node.nw_save_viewer"
2550 bl_label = "Save This Image"
2551 filepath: StringProperty(subtype="FILE_PATH")
2552 filename_ext: EnumProperty(
2553 name="Format",
2554 description="Choose the file format to save to",
2555 items=(('.bmp', "BMP", ""),
2556 ('.rgb', 'IRIS', ""),
2557 ('.png', 'PNG', ""),
2558 ('.jpg', 'JPEG', ""),
2559 ('.jp2', 'JPEG2000', ""),
2560 ('.tga', 'TARGA', ""),
2561 ('.cin', 'CINEON', ""),
2562 ('.dpx', 'DPX', ""),
2563 ('.exr', 'OPEN_EXR', ""),
2564 ('.hdr', 'HDR', ""),
2565 ('.tif', 'TIFF', "")),
2566 default='.png',
2569 @classmethod
2570 def poll(cls, context):
2571 valid = False
2572 if nw_check(context):
2573 if context.space_data.tree_type == 'CompositorNodeTree':
2574 if "Viewer Node" in [i.name for i in bpy.data.images]:
2575 if sum(bpy.data.images["Viewer Node"].size) > 0: # False if not connected or connected but no image
2576 valid = True
2577 return valid
2579 def execute(self, context):
2580 fp = self.filepath
2581 if fp:
2582 formats = {
2583 '.bmp': 'BMP',
2584 '.rgb': 'IRIS',
2585 '.png': 'PNG',
2586 '.jpg': 'JPEG',
2587 '.jpeg': 'JPEG',
2588 '.jp2': 'JPEG2000',
2589 '.tga': 'TARGA',
2590 '.cin': 'CINEON',
2591 '.dpx': 'DPX',
2592 '.exr': 'OPEN_EXR',
2593 '.hdr': 'HDR',
2594 '.tiff': 'TIFF',
2595 '.tif': 'TIFF'}
2596 basename, ext = path.splitext(fp)
2597 old_render_format = context.scene.render.image_settings.file_format
2598 context.scene.render.image_settings.file_format = formats[self.filename_ext]
2599 context.area.type = "IMAGE_EDITOR"
2600 context.area.spaces[0].image = bpy.data.images['Viewer Node']
2601 context.area.spaces[0].image.save_render(fp)
2602 context.area.type = "NODE_EDITOR"
2603 context.scene.render.image_settings.file_format = old_render_format
2604 return {'FINISHED'}
2607 class NWResetNodes(bpy.types.Operator):
2608 """Reset Nodes in Selection"""
2609 bl_idname = "node.nw_reset_nodes"
2610 bl_label = "Reset Nodes"
2611 bl_options = {'REGISTER', 'UNDO'}
2613 @classmethod
2614 def poll(cls, context):
2615 space = context.space_data
2616 return space.type == 'NODE_EDITOR'
2618 def execute(self, context):
2619 node_active = context.active_node
2620 node_selected = context.selected_nodes
2621 node_ignore = ["FRAME", "REROUTE", "GROUP", "SIMULATION_INPUT", "SIMULATION_OUTPUT"]
2623 # Check if one node is selected at least
2624 if not (len(node_selected) > 0):
2625 self.report({'ERROR'}, "1 node must be selected at least")
2626 return {'CANCELLED'}
2628 active_node_name = node_active.name if node_active.select else None
2629 valid_nodes = [n for n in node_selected if n.type not in node_ignore]
2631 # Create output lists
2632 selected_node_names = [n.name for n in node_selected]
2633 success_names = []
2635 # Reset all valid children in a frame
2636 node_active_is_frame = False
2637 if len(node_selected) == 1 and node_active.type == "FRAME":
2638 node_tree = node_active.id_data
2639 children = [n for n in node_tree.nodes if n.parent == node_active]
2640 if children:
2641 valid_nodes = [n for n in children if n.type not in node_ignore]
2642 selected_node_names = [n.name for n in children if n.type not in node_ignore]
2643 node_active_is_frame = True
2645 # Check if valid nodes in selection
2646 if not (len(valid_nodes) > 0):
2647 # Check for frames only
2648 frames_selected = [n for n in node_selected if n.type == "FRAME"]
2649 if (len(frames_selected) > 1 and len(frames_selected) == len(node_selected)):
2650 self.report({'ERROR'}, "Please select only 1 frame to reset")
2651 else:
2652 self.report({'ERROR'}, "No valid node(s) in selection")
2653 return {'CANCELLED'}
2655 # Report nodes that are not valid
2656 if len(valid_nodes) != len(node_selected) and node_active_is_frame is False:
2657 valid_node_names = [n.name for n in valid_nodes]
2658 not_valid_names = list(set(selected_node_names) - set(valid_node_names))
2659 self.report({'INFO'}, "Ignored {}".format(", ".join(not_valid_names)))
2661 # Deselect all nodes
2662 for i in node_selected:
2663 i.select = False
2665 # Run through all valid nodes
2666 for node in valid_nodes:
2668 parent = node.parent if node.parent else None
2669 node_loc = [node.location.x, node.location.y]
2671 node_tree = node.id_data
2672 props_to_copy = 'bl_idname name location height width'.split(' ')
2674 reconnections = []
2675 mappings = chain.from_iterable([node.inputs, node.outputs])
2676 for i in (i for i in mappings if i.is_linked):
2677 for L in i.links:
2678 reconnections.append([L.from_socket.path_from_id(), L.to_socket.path_from_id()])
2680 props = {j: getattr(node, j) for j in props_to_copy}
2682 new_node = node_tree.nodes.new(props['bl_idname'])
2683 props_to_copy.pop(0)
2685 for prop in props_to_copy:
2686 setattr(new_node, prop, props[prop])
2688 nodes = node_tree.nodes
2689 nodes.remove(node)
2690 new_node.name = props['name']
2692 if parent:
2693 new_node.parent = parent
2694 new_node.location = node_loc
2696 for str_from, str_to in reconnections:
2697 connect_sockets(eval(str_from), eval(str_to))
2699 new_node.select = False
2700 success_names.append(new_node.name)
2702 # Reselect all nodes
2703 if selected_node_names and node_active_is_frame is False:
2704 for i in selected_node_names:
2705 node_tree.nodes[i].select = True
2707 if active_node_name is not None:
2708 node_tree.nodes[active_node_name].select = True
2709 node_tree.nodes.active = node_tree.nodes[active_node_name]
2711 self.report({'INFO'}, "Successfully reset {}".format(", ".join(success_names)))
2712 return {'FINISHED'}
2715 classes = (
2716 NWLazyMix,
2717 NWLazyConnect,
2718 NWDeleteUnused,
2719 NWSwapLinks,
2720 NWResetBG,
2721 NWAddAttrNode,
2722 NWPreviewNode,
2723 NWFrameSelected,
2724 NWReloadImages,
2725 NWMergeNodes,
2726 NWBatchChangeNodes,
2727 NWChangeMixFactor,
2728 NWCopySettings,
2729 NWCopyLabel,
2730 NWClearLabel,
2731 NWModifyLabels,
2732 NWAddTextureSetup,
2733 NWAddPrincipledSetup,
2734 NWAddReroutes,
2735 NWLinkActiveToSelected,
2736 NWAlignNodes,
2737 NWSelectParentChildren,
2738 NWDetachOutputs,
2739 NWLinkToOutputNode,
2740 NWMakeLink,
2741 NWCallInputsMenu,
2742 NWAddSequence,
2743 NWAddMultipleImages,
2744 NWViewerFocus,
2745 NWSaveViewer,
2746 NWResetNodes,
2750 def register():
2751 from bpy.utils import register_class
2752 for cls in classes:
2753 register_class(cls)
2756 def unregister():
2757 from bpy.utils import unregister_class
2759 for cls in classes:
2760 unregister_class(cls)