Cleanup: Node Wrangler: capitalize comments in AddReroutes operator
[blender-addons.git] / node_wrangler / operators.py
blobde891d480c536aeca3c23b2f4159f84308faea4e
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_nodes_links, is_viewer_socket, is_viewer_link,
31 get_group_output_node, get_output_location, force_update, get_internal_socket, nw_check,
32 nw_check_space_type, NWBase, get_first_enabled_output, is_visible_socket, viewer_socket_name)
34 class NWLazyMix(Operator, NWBase):
35 """Add a Mix RGB/Shader node by interactively drawing lines between nodes"""
36 bl_idname = "node.nw_lazy_mix"
37 bl_label = "Mix Nodes"
38 bl_options = {'REGISTER', 'UNDO'}
40 def modal(self, context, event):
41 context.area.tag_redraw()
42 nodes, links = get_nodes_links(context)
43 cont = True
45 start_pos = [event.mouse_region_x, event.mouse_region_y]
47 node1 = None
48 if not context.scene.NWBusyDrawing:
49 node1 = node_at_pos(nodes, context, event)
50 if node1:
51 context.scene.NWBusyDrawing = node1.name
52 else:
53 if context.scene.NWBusyDrawing != 'STOP':
54 node1 = nodes[context.scene.NWBusyDrawing]
56 context.scene.NWLazySource = node1.name
57 context.scene.NWLazyTarget = node_at_pos(nodes, context, event).name
59 if event.type == 'MOUSEMOVE':
60 self.mouse_path.append((event.mouse_region_x, event.mouse_region_y))
62 elif event.type == 'RIGHTMOUSE' and event.value == 'RELEASE':
63 end_pos = [event.mouse_region_x, event.mouse_region_y]
64 bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
66 node2 = None
67 node2 = node_at_pos(nodes, context, event)
68 if node2:
69 context.scene.NWBusyDrawing = node2.name
71 if node1 == node2:
72 cont = False
74 if cont:
75 if node1 and node2:
76 for node in nodes:
77 node.select = False
78 node1.select = True
79 node2.select = True
81 bpy.ops.node.nw_merge_nodes(mode="MIX", merge_type="AUTO")
83 context.scene.NWBusyDrawing = ""
84 return {'FINISHED'}
86 elif event.type == 'ESC':
87 print('cancelled')
88 bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
89 return {'CANCELLED'}
91 return {'RUNNING_MODAL'}
93 def invoke(self, context, event):
94 if context.area.type == 'NODE_EDITOR':
95 # the arguments we pass the the callback
96 args = (self, context, 'MIX')
97 # Add the region OpenGL drawing callback
98 # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
99 self._handle = bpy.types.SpaceNodeEditor.draw_handler_add(
100 draw_callback_nodeoutline, args, 'WINDOW', 'POST_PIXEL')
102 self.mouse_path = []
104 context.window_manager.modal_handler_add(self)
105 return {'RUNNING_MODAL'}
106 else:
107 self.report({'WARNING'}, "View3D not found, cannot run operator")
108 return {'CANCELLED'}
111 class NWLazyConnect(Operator, NWBase):
112 """Connect two nodes without clicking a specific socket (automatically determined"""
113 bl_idname = "node.nw_lazy_connect"
114 bl_label = "Lazy Connect"
115 bl_options = {'REGISTER', 'UNDO'}
116 with_menu: BoolProperty()
118 def modal(self, context, event):
119 context.area.tag_redraw()
120 nodes, links = get_nodes_links(context)
121 cont = True
123 start_pos = [event.mouse_region_x, event.mouse_region_y]
125 node1 = None
126 if not context.scene.NWBusyDrawing:
127 node1 = node_at_pos(nodes, context, event)
128 if node1:
129 context.scene.NWBusyDrawing = node1.name
130 else:
131 if context.scene.NWBusyDrawing != 'STOP':
132 node1 = nodes[context.scene.NWBusyDrawing]
134 context.scene.NWLazySource = node1.name
135 context.scene.NWLazyTarget = node_at_pos(nodes, context, event).name
137 if event.type == 'MOUSEMOVE':
138 self.mouse_path.append((event.mouse_region_x, event.mouse_region_y))
140 elif event.type == 'RIGHTMOUSE' and event.value == 'RELEASE':
141 end_pos = [event.mouse_region_x, event.mouse_region_y]
142 bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
144 node2 = None
145 node2 = node_at_pos(nodes, context, event)
146 if node2:
147 context.scene.NWBusyDrawing = node2.name
149 if node1 == node2:
150 cont = False
152 link_success = False
153 if cont:
154 if node1 and node2:
155 original_sel = []
156 original_unsel = []
157 for node in nodes:
158 if node.select:
159 node.select = False
160 original_sel.append(node)
161 else:
162 original_unsel.append(node)
163 node1.select = True
164 node2.select = True
166 # link_success = autolink(node1, node2, links)
167 if self.with_menu:
168 if len(node1.outputs) > 1 and node2.inputs:
169 bpy.ops.wm.call_menu("INVOKE_DEFAULT", name=NWConnectionListOutputs.bl_idname)
170 elif len(node1.outputs) == 1:
171 bpy.ops.node.nw_call_inputs_menu(from_socket=0)
172 else:
173 link_success = autolink(node1, node2, links)
175 for node in original_sel:
176 node.select = True
177 for node in original_unsel:
178 node.select = False
180 if link_success:
181 force_update(context)
182 context.scene.NWBusyDrawing = ""
183 return {'FINISHED'}
185 elif event.type == 'ESC':
186 bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
187 return {'CANCELLED'}
189 return {'RUNNING_MODAL'}
191 def invoke(self, context, event):
192 if context.area.type == 'NODE_EDITOR':
193 nodes, links = get_nodes_links(context)
194 node = node_at_pos(nodes, context, event)
195 if node:
196 context.scene.NWBusyDrawing = node.name
198 # the arguments we pass the the callback
199 mode = "LINK"
200 if self.with_menu:
201 mode = "LINKMENU"
202 args = (self, context, mode)
203 # Add the region OpenGL drawing callback
204 # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
205 self._handle = bpy.types.SpaceNodeEditor.draw_handler_add(
206 draw_callback_nodeoutline, args, 'WINDOW', 'POST_PIXEL')
208 self.mouse_path = []
210 context.window_manager.modal_handler_add(self)
211 return {'RUNNING_MODAL'}
212 else:
213 self.report({'WARNING'}, "View3D not found, cannot run operator")
214 return {'CANCELLED'}
217 class NWDeleteUnused(Operator, NWBase):
218 """Delete all nodes whose output is not used"""
219 bl_idname = 'node.nw_del_unused'
220 bl_label = 'Delete Unused Nodes'
221 bl_options = {'REGISTER', 'UNDO'}
223 delete_muted: BoolProperty(
224 name="Delete Muted",
225 description="Delete (but reconnect, like Ctrl-X) all muted nodes",
226 default=True)
227 delete_frames: BoolProperty(
228 name="Delete Empty Frames",
229 description="Delete all frames that have no nodes inside them",
230 default=True)
232 def is_unused_node(self, node):
233 end_types = ['OUTPUT_MATERIAL', 'OUTPUT', 'VIEWER', 'COMPOSITE',
234 'SPLITVIEWER', 'OUTPUT_FILE', 'LEVELS', 'OUTPUT_LIGHT',
235 'OUTPUT_WORLD', 'GROUP_INPUT', 'GROUP_OUTPUT', 'FRAME']
236 if node.type in end_types:
237 return False
239 for output in node.outputs:
240 if output.links:
241 return False
242 return True
244 @classmethod
245 def poll(cls, context):
246 """Disabled for custom nodes as we do not know which nodes are supported."""
247 return (nw_check(context)
248 and nw_check_space_type(cls, context, 'ShaderNodeTree', 'CompositorNodeTree',
249 'TextureNodeTree', 'GeometryNodeTree')
250 and context.space_data.node_tree.nodes)
252 def execute(self, context):
253 nodes, links = get_nodes_links(context)
255 # Store selection
256 selection = []
257 for node in nodes:
258 if node.select:
259 selection.append(node.name)
261 for node in nodes:
262 node.select = False
264 deleted_nodes = []
265 temp_deleted_nodes = []
266 del_unused_iterations = len(nodes)
267 for it in range(0, del_unused_iterations):
268 temp_deleted_nodes = list(deleted_nodes) # keep record of last iteration
269 for node in nodes:
270 if self.is_unused_node(node):
271 node.select = True
272 deleted_nodes.append(node.name)
273 bpy.ops.node.delete()
275 if temp_deleted_nodes == deleted_nodes: # stop iterations when there are no more nodes to be deleted
276 break
278 if self.delete_frames:
279 repeat = True
280 while repeat:
281 frames_in_use = []
282 frames = []
283 repeat = False
284 for node in nodes:
285 if node.parent:
286 frames_in_use.append(node.parent)
287 for node in nodes:
288 if node.type == 'FRAME' and node not in frames_in_use:
289 frames.append(node)
290 if node.parent:
291 repeat = True # repeat for nested frames
292 for node in frames:
293 if node not in frames_in_use:
294 node.select = True
295 deleted_nodes.append(node.name)
296 bpy.ops.node.delete()
298 if self.delete_muted:
299 for node in nodes:
300 if node.mute:
301 node.select = True
302 deleted_nodes.append(node.name)
303 bpy.ops.node.delete_reconnect()
305 # get unique list of deleted nodes (iterations would count the same node more than once)
306 deleted_nodes = list(set(deleted_nodes))
307 for n in deleted_nodes:
308 self.report({'INFO'}, "Node " + n + " deleted")
309 num_deleted = len(deleted_nodes)
310 n = ' node'
311 if num_deleted > 1:
312 n += 's'
313 if num_deleted:
314 self.report({'INFO'}, "Deleted " + str(num_deleted) + n)
315 else:
316 self.report({'INFO'}, "Nothing deleted")
318 # Restore selection
319 nodes, links = get_nodes_links(context)
320 for node in nodes:
321 if node.name in selection:
322 node.select = True
323 return {'FINISHED'}
325 def invoke(self, context, event):
326 return context.window_manager.invoke_confirm(self, event)
329 class NWSwapLinks(Operator, NWBase):
330 """Swap the output connections of the two selected nodes, or two similar inputs of a single node"""
331 bl_idname = 'node.nw_swap_links'
332 bl_label = 'Swap Links'
333 bl_options = {'REGISTER', 'UNDO'}
335 @classmethod
336 def poll(cls, context):
337 return nw_check(context) and context.selected_nodes and len(context.selected_nodes) <= 2
339 def execute(self, context):
340 nodes, links = get_nodes_links(context)
341 selected_nodes = context.selected_nodes
342 n1 = selected_nodes[0]
344 # Swap outputs
345 if len(selected_nodes) == 2:
346 n2 = selected_nodes[1]
347 if n1.outputs and n2.outputs:
348 n1_outputs = []
349 n2_outputs = []
351 out_index = 0
352 for output in n1.outputs:
353 if output.links:
354 for link in output.links:
355 n1_outputs.append([out_index, link.to_socket])
356 links.remove(link)
357 out_index += 1
359 out_index = 0
360 for output in n2.outputs:
361 if output.links:
362 for link in output.links:
363 n2_outputs.append([out_index, link.to_socket])
364 links.remove(link)
365 out_index += 1
367 for connection in n1_outputs:
368 try:
369 connect_sockets(n2.outputs[connection[0]], connection[1])
370 except:
371 self.report({'WARNING'},
372 "Some connections have been lost due to differing numbers of output sockets")
373 for connection in n2_outputs:
374 try:
375 connect_sockets(n1.outputs[connection[0]], connection[1])
376 except:
377 self.report({'WARNING'},
378 "Some connections have been lost due to differing numbers of output sockets")
379 else:
380 if n1.outputs or n2.outputs:
381 self.report({'WARNING'}, "One of the nodes has no outputs!")
382 else:
383 self.report({'WARNING'}, "Neither of the nodes have outputs!")
385 # Swap Inputs
386 elif len(selected_nodes) == 1:
387 if n1.inputs and n1.inputs[0].is_multi_input:
388 self.report({'WARNING'}, "Can't swap inputs of a multi input socket!")
389 return {'FINISHED'}
390 if n1.inputs:
391 types = []
392 i = 0
393 for i1 in n1.inputs:
394 if i1.is_linked and not i1.is_multi_input:
395 similar_types = 0
396 for i2 in n1.inputs:
397 if i1.type == i2.type and i2.is_linked and not i2.is_multi_input:
398 similar_types += 1
399 types.append([i1, similar_types, i])
400 i += 1
401 types.sort(key=lambda k: k[1], reverse=True)
403 if types:
404 t = types[0]
405 if t[1] == 2:
406 for i2 in n1.inputs:
407 if t[0].type == i2.type == t[0].type and t[0] != i2 and i2.is_linked:
408 pair = [t[0], i2]
409 i1f = pair[0].links[0].from_socket
410 i1t = pair[0].links[0].to_socket
411 i2f = pair[1].links[0].from_socket
412 i2t = pair[1].links[0].to_socket
413 connect_sockets(i1f, i2t)
414 connect_sockets(i2f, i1t)
415 if t[1] == 1:
416 if len(types) == 1:
417 fs = t[0].links[0].from_socket
418 i = t[2]
419 links.remove(t[0].links[0])
420 if i + 1 == len(n1.inputs):
421 i = -1
422 i += 1
423 while n1.inputs[i].is_linked:
424 i += 1
425 connect_sockets(fs, n1.inputs[i])
426 elif len(types) == 2:
427 i1f = types[0][0].links[0].from_socket
428 i1t = types[0][0].links[0].to_socket
429 i2f = types[1][0].links[0].from_socket
430 i2t = types[1][0].links[0].to_socket
431 connect_sockets(i1f, i2t)
432 connect_sockets(i2f, i1t)
434 else:
435 self.report({'WARNING'}, "This node has no input connections to swap!")
436 else:
437 self.report({'WARNING'}, "This node has no inputs to swap!")
439 force_update(context)
440 return {'FINISHED'}
443 class NWResetBG(Operator, NWBase):
444 """Reset the zoom and position of the background image"""
445 bl_idname = 'node.nw_bg_reset'
446 bl_label = 'Reset Backdrop'
447 bl_options = {'REGISTER', 'UNDO'}
449 @classmethod
450 def poll(cls, context):
451 return (nw_check(context)
452 and nw_check_space_type(cls, context, 'CompositorNodeTree'))
454 def execute(self, context):
455 context.space_data.backdrop_zoom = 1
456 context.space_data.backdrop_offset[0] = 0
457 context.space_data.backdrop_offset[1] = 0
458 return {'FINISHED'}
461 class NWAddAttrNode(Operator, NWBase):
462 """Add an Attribute node with this name"""
463 bl_idname = 'node.nw_add_attr_node'
464 bl_label = 'Add UV map'
465 bl_options = {'REGISTER', 'UNDO'}
467 attr_name: StringProperty()
469 @classmethod
470 def poll(cls, context):
471 return (nw_check(context)
472 and nw_check_space_type(cls, context, 'ShaderNodeTree'))
474 def execute(self, context):
475 bpy.ops.node.add_node('INVOKE_DEFAULT', use_transform=True, type="ShaderNodeAttribute")
476 nodes, links = get_nodes_links(context)
477 nodes.active.attribute_name = self.attr_name
478 return {'FINISHED'}
481 class NWPreviewNode(Operator, NWBase):
482 bl_idname = "node.nw_preview_node"
483 bl_label = "Preview Node"
484 bl_description = "Connect active node to the Node Group output or the Material Output"
485 bl_options = {'REGISTER', 'UNDO'}
487 # If false, the operator is not executed if the current node group happens to be a geometry nodes group.
488 # This is needed because geometry nodes has its own viewer node that uses the same shortcut as in the compositor.
489 run_in_geometry_nodes: BoolProperty(default=True)
491 def __init__(self):
492 self.shader_output_type = ""
493 self.shader_output_ident = ""
495 @classmethod
496 def poll(cls, context):
497 """Already implemented natively for compositing nodes."""
498 return (nw_check(context)
499 and nw_check_space_type(cls, context, 'ShaderNodeTree', 'GeometryNodeTree')
500 and (not context.active_node
501 or context.active_node.type not in {"OUTPUT_MATERIAL", "OUTPUT_WORLD"}))
503 @staticmethod
504 def get_output_sockets(node_tree):
505 return [item for item in node_tree.interface.items_tree
506 if item.item_type == 'SOCKET' and item.in_out in {'OUTPUT', 'BOTH'}]
508 def init_shader_variables(self, space, shader_type):
509 if shader_type == 'OBJECT':
510 if space.id in bpy.data.lights.values():
511 self.shader_output_type = "OUTPUT_LIGHT"
512 self.shader_output_ident = "ShaderNodeOutputLight"
513 else:
514 self.shader_output_type = "OUTPUT_MATERIAL"
515 self.shader_output_ident = "ShaderNodeOutputMaterial"
517 elif shader_type == 'WORLD':
518 self.shader_output_type = "OUTPUT_WORLD"
519 self.shader_output_ident = "ShaderNodeOutputWorld"
521 def ensure_viewer_socket(self, node_tree, socket_type, connect_socket=None):
522 """Check if a viewer output already exists in a node group, otherwise create it"""
523 viewer_socket = None
524 output_sockets = self.get_output_sockets(node_tree)
525 if len(output_sockets):
526 for i, socket in enumerate(output_sockets):
527 if is_viewer_socket(socket) and socket.socket_type == socket_type:
528 # If viewer output is already used but leads to the same socket we can still use it
529 is_used = self.is_socket_used_other_mats(socket)
530 if is_used:
531 if connect_socket is None:
532 continue
533 groupout = get_group_output_node(node_tree)
534 groupout_input = groupout.inputs[i]
535 links = groupout_input.links
536 if connect_socket not in [link.from_socket for link in links]:
537 continue
538 viewer_socket = socket
539 break
541 if viewer_socket is None:
542 # Create viewer socket
543 viewer_socket = node_tree.interface.new_socket(
544 viewer_socket_name, in_out='OUTPUT', socket_type=socket_type)
545 viewer_socket.NWViewerSocket = True
546 return viewer_socket
548 @staticmethod
549 def ensure_group_output(node_tree):
550 """Check if a group output node exists, otherwise create it"""
551 groupout = get_group_output_node(node_tree)
552 if groupout is None:
553 groupout = node_tree.nodes.new('NodeGroupOutput')
554 loc_x, loc_y = get_output_location(tree)
555 groupout.location.x = loc_x
556 groupout.location.y = loc_y
557 groupout.select = False
558 # So that we don't keep on adding new group outputs
559 groupout.is_active_output = True
560 return groupout
562 @classmethod
563 def search_sockets(cls, node, sockets, index=None):
564 """Recursively scan nodes for viewer sockets and store them in a list"""
565 for i, input_socket in enumerate(node.inputs):
566 if index and i != index:
567 continue
568 if len(input_socket.links):
569 link = input_socket.links[0]
570 next_node = link.from_node
571 external_socket = link.from_socket
572 if hasattr(next_node, "node_tree"):
573 for socket_index, socket in enumerate(next_node.node_tree.interface.items_tree):
574 if socket.identifier == external_socket.identifier:
575 break
576 if is_viewer_socket(socket) and socket not in sockets:
577 sockets.append(socket)
578 # continue search inside of node group but restrict socket to where we came from
579 groupout = get_group_output_node(next_node.node_tree)
580 cls.search_sockets(groupout, sockets, index=socket_index)
582 @classmethod
583 def scan_nodes(cls, tree, sockets):
584 """Recursively get all viewer sockets in a material tree"""
585 for node in tree.nodes:
586 if hasattr(node, "node_tree"):
587 if node.node_tree is None:
588 continue
589 for socket in cls.get_output_sockets(node.node_tree):
590 if is_viewer_socket(socket) and (socket not in sockets):
591 sockets.append(socket)
592 cls.scan_nodes(node.node_tree, sockets)
594 @staticmethod
595 def remove_socket(tree, socket):
596 interface = tree.interface
597 interface.remove(socket)
598 interface.active_index = min(interface.active_index, len(interface.items_tree) - 1)
600 def link_leads_to_used_socket(self, link):
601 """Return True if link leads to a socket that is already used in this node"""
602 socket = get_internal_socket(link.to_socket)
603 return socket and self.is_socket_used_active_tree(socket)
605 def is_socket_used_active_tree(self, socket):
606 """Ensure used sockets in active node tree is calculated and check given socket"""
607 if not hasattr(self, "used_viewer_sockets_active_mat"):
608 self.used_viewer_sockets_active_mat = []
610 node_tree = bpy.context.space_data.node_tree
611 output_node = None
612 if node_tree.type == 'GEOMETRY':
613 output_node = get_group_output_node(node_tree)
614 elif node_tree.type == 'SHADER':
615 output_node = get_group_output_node(node_tree,
616 output_node_type=self.shader_output_type)
618 if output_node is not None:
619 self.search_sockets(output_node, self.used_viewer_sockets_active_mat)
620 return socket in self.used_viewer_sockets_active_mat
622 def is_socket_used_other_mats(self, socket):
623 """Ensure used sockets in other materials are calculated and check given socket"""
624 if not hasattr(self, "used_viewer_sockets_other_mats"):
625 self.used_viewer_sockets_other_mats = []
626 for mat in bpy.data.materials:
627 if mat.node_tree == bpy.context.space_data.node_tree or not hasattr(mat.node_tree, "nodes"):
628 continue
629 # Get viewer node
630 output_node = get_group_output_node(mat.node_tree,
631 output_node_type=self.shader_output_type)
632 if output_node is not None:
633 self.search_sockets(output_node, self.used_viewer_sockets_other_mats)
634 return socket in self.used_viewer_sockets_other_mats
636 def get_output_index(self, node, output_node, is_base_node_tree, socket_type, check_type=False):
637 """Get the next available output socket in the active node"""
638 out_i = None
639 valid_outputs = []
640 for i, out in enumerate(node.outputs):
641 if is_visible_socket(out) and (not check_type or out.type == socket_type):
642 valid_outputs.append(i)
643 if valid_outputs:
644 out_i = valid_outputs[0] # Start index of node's outputs
645 for i, valid_i in enumerate(valid_outputs):
646 for out_link in node.outputs[valid_i].links:
647 if is_viewer_link(out_link, output_node):
648 if is_base_node_tree or self.link_leads_to_used_socket(out_link):
649 if i < len(valid_outputs) - 1:
650 out_i = valid_outputs[i + 1]
651 else:
652 out_i = valid_outputs[0]
653 return out_i
655 def create_links(self, path, node, active_node_socket_id, socket_type):
656 """Create links at each step in the node group path."""
657 path = list(reversed(path))
658 # Starting from the level of the active node
659 for path_index, path_element in enumerate(path[:-1]):
660 # Ensure there is a viewer node and it has an input
661 tree = path_element.node_tree
662 viewer_socket = self.ensure_viewer_socket(
663 tree, socket_type,
664 connect_socket = node.outputs[active_node_socket_id]
665 if path_index == 0 else None)
666 if viewer_socket in self.delete_sockets:
667 self.delete_sockets.remove(viewer_socket)
669 # Connect the current to its viewer
670 link_start = node.outputs[active_node_socket_id]
671 link_end = self.ensure_group_output(tree).inputs[viewer_socket.identifier]
672 connect_sockets(link_start, link_end)
674 # Go up in the node group hierarchy
675 next_tree = path[path_index + 1].node_tree
676 node = next(n for n in next_tree.nodes
677 if n.type == 'GROUP'
678 and n.node_tree == tree)
679 tree = next_tree
680 active_node_socket_id = viewer_socket.identifier
681 return node.outputs[active_node_socket_id]
683 def cleanup(self):
684 # Delete sockets
685 for socket in self.delete_sockets:
686 if not self.is_socket_used_other_mats(socket):
687 tree = socket.id_data
688 self.remove_socket(tree, socket)
690 def invoke(self, context, event):
691 space = context.space_data
692 # Ignore operator when running in wrong context.
693 if self.run_in_geometry_nodes != (space.tree_type == "GeometryNodeTree"):
694 return {'PASS_THROUGH'}
696 mlocx = event.mouse_region_x
697 mlocy = event.mouse_region_y
698 select_node = bpy.ops.node.select(location=(mlocx, mlocy), extend=False)
699 if 'FINISHED' not in select_node: # only run if mouse click is on a node
700 return {'CANCELLED'}
702 base_node_tree = space.node_tree
703 active_tree = context.space_data.edit_tree
704 path = context.space_data.path
705 nodes = active_tree.nodes
706 active = nodes.active
708 if not active and not any(is_visible_socket(out) for out in active.outputs):
709 return {'CANCELLED'}
711 # Scan through all nodes in tree including nodes inside of groups to find viewer sockets
712 self.delete_sockets = []
713 self.scan_nodes(base_node_tree, self.delete_sockets)
715 if not active.outputs:
716 self.cleanup()
717 return {'CANCELLED'}
719 # For geometry node trees, we just connect to the group output
720 if space.tree_type == "GeometryNodeTree":
721 socket_type = 'NodeSocketGeometry'
723 # Find (or create if needed) the output of this node tree
724 output_node = self.ensure_group_output(base_node_tree)
726 active_node_socket_index = self.get_output_index(
727 active, output_node, base_node_tree == active_tree, 'GEOMETRY', check_type=True
729 # If there is no 'GEOMETRY' output type - We can't preview the node
730 if active_node_socket_index is None:
731 return {'CANCELLED'}
733 # Find an input socket of the output of type geometry
734 output_node_socket_index = None
735 for i, inp in enumerate(output_node.inputs):
736 if inp.type == 'GEOMETRY':
737 output_node_socket_index = i
738 break
739 if output_node_socket_index is None:
740 # Create geometry socket
741 geometry_out_socket = base_node_tree.interface.new_socket(
742 'Geometry', in_out='OUTPUT', socket_type=socket_type
744 output_node_socket_index = geometry_out_socket.index
746 # For shader node trees, we connect to a material output
747 elif space.tree_type == "ShaderNodeTree":
748 socket_type = 'NodeSocketShader'
749 self.init_shader_variables(space, space.shader_type)
751 # Get or create material_output node
752 output_node = get_group_output_node(base_node_tree,
753 output_node_type=self.shader_output_type)
754 if not output_node:
755 output_node = base_node_tree.nodes.new(self.shader_output_ident)
756 output_node.location = get_output_location(base_node_tree)
757 output_node.select = False
759 active_node_socket_index = self.get_output_index(
760 active, output_node, base_node_tree == active_tree, 'SHADER'
763 # Cancel if no socket was found. This can happen for group input
764 # nodes with only a virtual socket output.
765 if active_node_socket_index is None:
766 return {'CANCELLED'}
768 if active.outputs[active_node_socket_index].name == "Volume":
769 output_node_socket_index = 1
770 else:
771 output_node_socket_index = 0
773 # If there are no nested node groups, the link starts at the active node
774 node_output = active.outputs[active_node_socket_index]
775 if len(path) > 1:
776 # Recursively connect inside nested node groups and get the one from base level
777 node_output = self.create_links(path, active, active_node_socket_index, socket_type)
778 output_node_input = output_node.inputs[output_node_socket_index]
780 # Connect at base level
781 connect_sockets(node_output, output_node_input)
783 self.cleanup()
784 nodes.active = active
785 active.select = True
786 force_update(context)
787 return {'FINISHED'}
790 class NWFrameSelected(Operator, NWBase):
791 bl_idname = "node.nw_frame_selected"
792 bl_label = "Frame Selected"
793 bl_description = "Add a frame node and parent the selected nodes to it"
794 bl_options = {'REGISTER', 'UNDO'}
796 label_prop: StringProperty(
797 name='Label',
798 description='The visual name of the frame node',
799 default=' '
801 use_custom_color_prop: BoolProperty(
802 name="Custom Color",
803 description="Use custom color for the frame node",
804 default=False
806 color_prop: FloatVectorProperty(
807 name="Color",
808 description="The color of the frame node",
809 default=(0.604, 0.604, 0.604),
810 min=0, max=1, step=1, precision=3,
811 subtype='COLOR_GAMMA', size=3
814 def draw(self, context):
815 layout = self.layout
816 layout.prop(self, 'label_prop')
817 layout.prop(self, 'use_custom_color_prop')
818 col = layout.column()
819 col.active = self.use_custom_color_prop
820 col.prop(self, 'color_prop', text="")
822 def execute(self, context):
823 nodes, links = get_nodes_links(context)
824 selected = []
825 for node in nodes:
826 if node.select:
827 selected.append(node)
829 bpy.ops.node.add_node(type='NodeFrame')
830 frm = nodes.active
831 frm.label = self.label_prop
832 frm.use_custom_color = self.use_custom_color_prop
833 frm.color = self.color_prop
835 for node in selected:
836 node.parent = frm
838 return {'FINISHED'}
841 class NWReloadImages(Operator):
842 bl_idname = "node.nw_reload_images"
843 bl_label = "Reload Images"
844 bl_description = "Update all the image nodes to match their files on disk"
846 @classmethod
847 def poll(cls, context):
848 return (nw_check(context)
849 and nw_check_space_type(cls, context, 'ShaderNodeTree', 'CompositorNodeTree',
850 'TextureNodeTree', 'GeometryNodeTree')
851 and context.active_node is not None
852 and any(is_visible_socket(out) for out in context.active_node.outputs))
854 def execute(self, context):
855 nodes, links = get_nodes_links(context)
856 image_types = ["IMAGE", "TEX_IMAGE", "TEX_ENVIRONMENT", "TEXTURE"]
857 num_reloaded = 0
858 for node in nodes:
859 if node.type in image_types:
860 if node.type == "TEXTURE":
861 if node.texture: # node has texture assigned
862 if node.texture.type in ['IMAGE', 'ENVIRONMENT_MAP']:
863 if node.texture.image: # texture has image assigned
864 node.texture.image.reload()
865 num_reloaded += 1
866 else:
867 if node.image:
868 node.image.reload()
869 num_reloaded += 1
871 if num_reloaded:
872 self.report({'INFO'}, "Reloaded images")
873 print("Reloaded " + str(num_reloaded) + " images")
874 force_update(context)
875 return {'FINISHED'}
876 else:
877 self.report({'WARNING'}, "No images found to reload in this node tree")
878 return {'CANCELLED'}
881 class NWMergeNodes(Operator, NWBase):
882 bl_idname = "node.nw_merge_nodes"
883 bl_label = "Merge Nodes"
884 bl_description = "Merge Selected Nodes"
885 bl_options = {'REGISTER', 'UNDO'}
887 mode: EnumProperty(
888 name="mode",
889 description="All possible blend types, boolean operations and math operations",
890 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],
892 merge_type: EnumProperty(
893 name="merge type",
894 description="Type of Merge to be used",
895 items=(
896 ('AUTO', 'Auto', 'Automatic Output Type Detection'),
897 ('SHADER', 'Shader', 'Merge using ADD or MIX Shader'),
898 ('GEOMETRY', 'Geometry', 'Merge using Mesh Boolean or Join Geometry Node'),
899 ('MIX', 'Mix Node', 'Merge using Mix Nodes'),
900 ('MATH', 'Math Node', 'Merge using Math Nodes'),
901 ('ZCOMBINE', 'Z-Combine Node', 'Merge using Z-Combine Nodes'),
902 ('ALPHAOVER', 'Alpha Over Node', 'Merge using Alpha Over Nodes'),
906 # Check if the link connects to a node that is in selected_nodes
907 # If not, then check recursively for each link in the nodes outputs.
908 # If yes, return True. If the recursion stops without finding a node
909 # in selected_nodes, it returns False. The depth is used to prevent
910 # getting stuck in a loop because of an already present cycle.
911 @staticmethod
912 def link_creates_cycle(link, selected_nodes, depth=0) -> bool:
913 if depth > 255:
914 # We're stuck in a cycle, but that cycle was already present,
915 # so we return False.
916 # NOTE: The number 255 is arbitrary, but seems to work well.
917 return False
918 node = link.to_node
919 if node in selected_nodes:
920 return True
921 if not node.outputs:
922 return False
923 for output in node.outputs:
924 if output.is_linked:
925 for olink in output.links:
926 if NWMergeNodes.link_creates_cycle(olink, selected_nodes, depth + 1):
927 return True
928 # None of the outputs found a node in selected_nodes, so there is no cycle.
929 return False
931 # Merge the nodes in `nodes_list` with a node of type `node_name` that has a multi_input socket.
932 # The parameters `socket_indices` gives the indices of the node sockets in the order that they should
933 # be connected. The last one is assumed to be a multi input socket.
934 # For convenience the node is returned.
935 @staticmethod
936 def merge_with_multi_input(nodes_list, merge_position, do_hide, loc_x, links, nodes, node_name, socket_indices):
937 # The y-location of the last node
938 loc_y = nodes_list[-1][2]
939 if merge_position == 'CENTER':
940 # Average the y-location
941 for i in range(len(nodes_list) - 1):
942 loc_y += nodes_list[i][2]
943 loc_y = loc_y / len(nodes_list)
944 new_node = nodes.new(node_name)
945 new_node.hide = do_hide
946 new_node.location.x = loc_x
947 new_node.location.y = loc_y
948 selected_nodes = [nodes[node_info[0]] for node_info in nodes_list]
949 prev_links = []
950 outputs_for_multi_input = []
951 for i, node in enumerate(selected_nodes):
952 node.select = False
953 # Search for the first node which had output links that do not create
954 # a cycle, which we can then reconnect afterwards.
955 if prev_links == [] and node.outputs[0].is_linked:
956 prev_links = [
957 link for link in node.outputs[0].links if not NWMergeNodes.link_creates_cycle(
958 link, selected_nodes)]
959 # Get the index of the socket, the last one is a multi input, and is thus used repeatedly
960 # To get the placement to look right we need to reverse the order in which we connect the
961 # outputs to the multi input socket.
962 if i < len(socket_indices) - 1:
963 ind = socket_indices[i]
964 connect_sockets(node.outputs[0], new_node.inputs[ind])
965 else:
966 outputs_for_multi_input.insert(0, node.outputs[0])
967 if outputs_for_multi_input != []:
968 ind = socket_indices[-1]
969 for output in outputs_for_multi_input:
970 connect_sockets(output, new_node.inputs[ind])
971 if prev_links != []:
972 for link in prev_links:
973 connect_sockets(new_node.outputs[0], link.to_node.inputs[0])
974 return new_node
976 @classmethod
977 def poll(cls, context):
978 return (nw_check(context)
979 and nw_check_space_type(cls, context, 'ShaderNodeTree', 'CompositorNodeTree',
980 'TextureNodeTree', 'GeometryNodeTree'))
982 def execute(self, context):
983 settings = context.preferences.addons[__package__].preferences
984 merge_hide = settings.merge_hide
985 merge_position = settings.merge_position # 'center' or 'bottom'
987 do_hide = False
988 do_hide_shader = False
989 if merge_hide == 'ALWAYS':
990 do_hide = True
991 do_hide_shader = True
992 elif merge_hide == 'NON_SHADER':
993 do_hide = True
995 tree_type = context.space_data.node_tree.type
996 if tree_type == 'GEOMETRY':
997 node_type = 'GeometryNode'
998 if tree_type == 'COMPOSITING':
999 node_type = 'CompositorNode'
1000 elif tree_type == 'SHADER':
1001 node_type = 'ShaderNode'
1002 elif tree_type == 'TEXTURE':
1003 node_type = 'TextureNode'
1004 nodes, links = get_nodes_links(context)
1005 mode = self.mode
1006 merge_type = self.merge_type
1007 # Prevent trying to add Z-Combine in not 'COMPOSITING' node tree.
1008 # 'ZCOMBINE' works only if mode == 'MIX'
1009 # Setting mode to None prevents trying to add 'ZCOMBINE' node.
1010 if (merge_type == 'ZCOMBINE' or merge_type == 'ALPHAOVER') and tree_type != 'COMPOSITING':
1011 merge_type = 'MIX'
1012 mode = 'MIX'
1013 if (merge_type != 'MATH' and merge_type != 'GEOMETRY') and tree_type == 'GEOMETRY':
1014 merge_type = 'AUTO'
1015 # The Mix node and math nodes used for geometry nodes are of type 'ShaderNode'
1016 if (merge_type == 'MATH' or merge_type == 'MIX') and tree_type == 'GEOMETRY':
1017 node_type = 'ShaderNode'
1018 selected_mix = [] # entry = [index, loc]
1019 selected_shader = [] # entry = [index, loc]
1020 selected_geometry = [] # entry = [index, loc]
1021 selected_math = [] # entry = [index, loc]
1022 selected_vector = [] # entry = [index, loc]
1023 selected_z = [] # entry = [index, loc]
1024 selected_alphaover = [] # entry = [index, loc]
1026 for i, node in enumerate(nodes):
1027 if node.select and node.outputs:
1028 if merge_type == 'AUTO':
1029 for (type, types_list, dst) in (
1030 ('SHADER', ('MIX', 'ADD'), selected_shader),
1031 ('GEOMETRY', [t[0] for t in geo_combine_operations], selected_geometry),
1032 ('RGBA', [t[0] for t in blend_types], selected_mix),
1033 ('VALUE', [t[0] for t in operations], selected_math),
1034 ('VECTOR', [], selected_vector),
1036 output = get_first_enabled_output(node)
1037 output_type = output.type
1038 valid_mode = mode in types_list
1039 # When mode is 'MIX' we have to cheat since the mix node is not used in
1040 # geometry nodes.
1041 if tree_type == 'GEOMETRY':
1042 if mode == 'MIX':
1043 if output_type == 'VALUE' and type == 'VALUE':
1044 valid_mode = True
1045 elif output_type == 'VECTOR' and type == 'VECTOR':
1046 valid_mode = True
1047 elif type == 'GEOMETRY':
1048 valid_mode = True
1049 # When mode is 'MIX' use mix node for both 'RGBA' and 'VALUE' output types.
1050 # Cheat that output type is 'RGBA',
1051 # and that 'MIX' exists in math operations list.
1052 # This way when selected_mix list is analyzed:
1053 # Node data will be appended even though it doesn't meet requirements.
1054 elif output_type != 'SHADER' and mode == 'MIX':
1055 output_type = 'RGBA'
1056 valid_mode = True
1057 if output_type == type and valid_mode:
1058 dst.append([i, node.location.x, node.location.y, node.dimensions.x, node.hide])
1059 else:
1060 for (type, types_list, dst) in (
1061 ('SHADER', ('MIX', 'ADD'), selected_shader),
1062 ('GEOMETRY', [t[0] for t in geo_combine_operations], selected_geometry),
1063 ('MIX', [t[0] for t in blend_types], selected_mix),
1064 ('MATH', [t[0] for t in operations], selected_math),
1065 ('ZCOMBINE', ('MIX', ), selected_z),
1066 ('ALPHAOVER', ('MIX', ), selected_alphaover),
1068 if merge_type == type and mode in types_list:
1069 dst.append([i, node.location.x, node.location.y, node.dimensions.x, node.hide])
1070 # When nodes with output kinds 'RGBA' and 'VALUE' are selected at the same time
1071 # use only 'Mix' nodes for merging.
1072 # For that we add selected_math list to selected_mix list and clear selected_math.
1073 if selected_mix and selected_math and merge_type == 'AUTO':
1074 selected_mix += selected_math
1075 selected_math = []
1077 # If no nodes are selected, do nothing and pass through.
1078 if not (selected_mix + selected_shader + selected_geometry + selected_math
1079 + selected_vector + selected_z + selected_alphaover):
1080 return {'PASS_THROUGH'}
1082 for nodes_list in [
1083 selected_mix,
1084 selected_shader,
1085 selected_geometry,
1086 selected_math,
1087 selected_vector,
1088 selected_z,
1089 selected_alphaover]:
1090 if not nodes_list:
1091 continue
1092 count_before = len(nodes)
1093 # sort list by loc_x - reversed
1094 nodes_list.sort(key=lambda k: k[1], reverse=True)
1095 # get maximum loc_x
1096 loc_x = nodes_list[0][1] + nodes_list[0][3] + 70
1097 nodes_list.sort(key=lambda k: k[2], reverse=True)
1099 # Change the node type for math nodes in a geometry node tree.
1100 if tree_type == 'GEOMETRY':
1101 if nodes_list is selected_math or nodes_list is selected_vector or nodes_list is selected_mix:
1102 node_type = 'ShaderNode'
1103 if mode == 'MIX':
1104 mode = 'ADD'
1105 else:
1106 node_type = 'GeometryNode'
1107 if merge_position == 'CENTER':
1108 # average yloc of last two nodes (lowest two)
1109 loc_y = ((nodes_list[len(nodes_list) - 1][2]) + (nodes_list[len(nodes_list) - 2][2])) / 2
1110 if nodes_list[len(nodes_list) - 1][-1]: # if last node is hidden, mix should be shifted up a bit
1111 if do_hide:
1112 loc_y += 40
1113 else:
1114 loc_y += 80
1115 else:
1116 loc_y = nodes_list[len(nodes_list) - 1][2]
1117 offset_y = 100
1118 if not do_hide:
1119 offset_y = 200
1120 if nodes_list == selected_shader and not do_hide_shader:
1121 offset_y = 150.0
1122 the_range = len(nodes_list) - 1
1123 if len(nodes_list) == 1:
1124 the_range = 1
1125 was_multi = False
1126 for i in range(the_range):
1127 if nodes_list == selected_mix:
1128 mix_name = 'Mix'
1129 if tree_type == 'COMPOSITING':
1130 mix_name = 'MixRGB'
1131 add_type = node_type + mix_name
1132 add = nodes.new(add_type)
1133 if tree_type != 'COMPOSITING':
1134 add.data_type = 'RGBA'
1135 add.blend_type = mode
1136 if mode != 'MIX':
1137 add.inputs[0].default_value = 1.0
1138 add.show_preview = False
1139 add.hide = do_hide
1140 if do_hide:
1141 loc_y = loc_y - 50
1142 first = 6
1143 second = 7
1144 if tree_type == 'COMPOSITING':
1145 first = 1
1146 second = 2
1147 elif nodes_list == selected_math:
1148 add_type = node_type + 'Math'
1149 add = nodes.new(add_type)
1150 add.operation = mode
1151 add.hide = do_hide
1152 if do_hide:
1153 loc_y = loc_y - 50
1154 first = 0
1155 second = 1
1156 elif nodes_list == selected_shader:
1157 if mode == 'MIX':
1158 add_type = node_type + 'MixShader'
1159 add = nodes.new(add_type)
1160 add.hide = do_hide_shader
1161 if do_hide_shader:
1162 loc_y = loc_y - 50
1163 first = 1
1164 second = 2
1165 elif mode == 'ADD':
1166 add_type = node_type + 'AddShader'
1167 add = nodes.new(add_type)
1168 add.hide = do_hide_shader
1169 if do_hide_shader:
1170 loc_y = loc_y - 50
1171 first = 0
1172 second = 1
1173 elif nodes_list == selected_geometry:
1174 if mode in ('JOIN', 'MIX'):
1175 add_type = node_type + 'JoinGeometry'
1176 add = self.merge_with_multi_input(
1177 nodes_list, merge_position, do_hide, loc_x, links, nodes, add_type, [0])
1178 else:
1179 add_type = node_type + 'MeshBoolean'
1180 indices = [0, 1] if mode == 'DIFFERENCE' else [1]
1181 add = self.merge_with_multi_input(
1182 nodes_list, merge_position, do_hide, loc_x, links, nodes, add_type, indices)
1183 add.operation = mode
1184 was_multi = True
1185 break
1186 elif nodes_list == selected_vector:
1187 add_type = node_type + 'VectorMath'
1188 add = nodes.new(add_type)
1189 add.operation = mode
1190 add.hide = do_hide
1191 if do_hide:
1192 loc_y = loc_y - 50
1193 first = 0
1194 second = 1
1195 elif nodes_list == selected_z:
1196 add = nodes.new('CompositorNodeZcombine')
1197 add.show_preview = False
1198 add.hide = do_hide
1199 if do_hide:
1200 loc_y = loc_y - 50
1201 first = 0
1202 second = 2
1203 elif nodes_list == selected_alphaover:
1204 add = nodes.new('CompositorNodeAlphaOver')
1205 add.show_preview = False
1206 add.hide = do_hide
1207 if do_hide:
1208 loc_y = loc_y - 50
1209 first = 1
1210 second = 2
1211 add.location = loc_x, loc_y
1212 loc_y += offset_y
1213 add.select = True
1215 # This has already been handled separately
1216 if was_multi:
1217 continue
1218 count_adds = i + 1
1219 count_after = len(nodes)
1220 index = count_after - 1
1221 first_selected = nodes[nodes_list[0][0]]
1222 # "last" node has been added as first, so its index is count_before.
1223 last_add = nodes[count_before]
1224 # Create list of invalid indexes.
1225 invalid_nodes = [nodes[n[0]]
1226 for n in (selected_mix + selected_math + selected_shader + selected_z + selected_geometry)]
1228 # Special case:
1229 # Two nodes were selected and first selected has no output links, second selected has output links.
1230 # Then add links from last add to all links 'to_socket' of out links of second selected.
1231 first_selected_output = get_first_enabled_output(first_selected)
1232 if len(nodes_list) == 2:
1233 if not first_selected_output.links:
1234 second_selected = nodes[nodes_list[1][0]]
1235 for ss_link in get_first_enabled_output(second_selected).links:
1236 # Prevent cyclic dependencies when nodes to be merged are linked to one another.
1237 # Link only if "to_node" index not in invalid indexes list.
1238 if not self.link_creates_cycle(ss_link, invalid_nodes):
1239 connect_sockets(get_first_enabled_output(last_add), ss_link.to_socket)
1240 # add links from last_add to all links 'to_socket' of out links of first selected.
1241 for fs_link in first_selected_output.links:
1242 # Link only if "to_node" index not in invalid indexes list.
1243 if not self.link_creates_cycle(fs_link, invalid_nodes):
1244 connect_sockets(get_first_enabled_output(last_add), fs_link.to_socket)
1245 # add link from "first" selected and "first" add node
1246 node_to = nodes[count_after - 1]
1247 connect_sockets(first_selected_output, node_to.inputs[first])
1248 if node_to.type == 'ZCOMBINE':
1249 for fs_out in first_selected.outputs:
1250 if fs_out != first_selected_output and fs_out.name in ('Z', 'Depth'):
1251 connect_sockets(fs_out, node_to.inputs[1])
1252 break
1253 # add links between added ADD nodes and between selected and ADD nodes
1254 for i in range(count_adds):
1255 if i < count_adds - 1:
1256 node_from = nodes[index]
1257 node_to = nodes[index - 1]
1258 node_to_input_i = first
1259 node_to_z_i = 1 # if z combine - link z to first z input
1260 connect_sockets(get_first_enabled_output(node_from), node_to.inputs[node_to_input_i])
1261 if node_to.type == 'ZCOMBINE':
1262 for from_out in node_from.outputs:
1263 if from_out != get_first_enabled_output(node_from) and from_out.name in ('Z', 'Depth'):
1264 connect_sockets(from_out, node_to.inputs[node_to_z_i])
1265 if len(nodes_list) > 1:
1266 node_from = nodes[nodes_list[i + 1][0]]
1267 node_to = nodes[index]
1268 node_to_input_i = second
1269 node_to_z_i = 3 # if z combine - link z to second z input
1270 connect_sockets(get_first_enabled_output(node_from), node_to.inputs[node_to_input_i])
1271 if node_to.type == 'ZCOMBINE':
1272 for from_out in node_from.outputs:
1273 if from_out != get_first_enabled_output(node_from) and from_out.name in ('Z', 'Depth'):
1274 connect_sockets(from_out, node_to.inputs[node_to_z_i])
1275 index -= 1
1276 # set "last" of added nodes as active
1277 nodes.active = last_add
1278 for i, x, y, dx, h in nodes_list:
1279 nodes[i].select = False
1281 return {'FINISHED'}
1284 class NWBatchChangeNodes(Operator, NWBase):
1285 bl_idname = "node.nw_batch_change"
1286 bl_label = "Batch Change"
1287 bl_description = "Batch Change Blend Type and Math Operation"
1288 bl_options = {'REGISTER', 'UNDO'}
1290 blend_type: EnumProperty(
1291 name="Blend Type",
1292 items=blend_types + navs,
1294 operation: EnumProperty(
1295 name="Operation",
1296 items=operations + navs,
1299 @classmethod
1300 def poll(cls, context):
1301 return (nw_check(context)
1302 and nw_check_space_type(cls, context, 'ShaderNodeTree', 'CompositorNodeTree',
1303 'TextureNodeTree', 'GeometryNodeTree'))
1305 def execute(self, context):
1306 blend_type = self.blend_type
1307 operation = self.operation
1308 for node in context.selected_nodes:
1309 if node.type == 'MIX_RGB' or (node.bl_idname == 'ShaderNodeMix' and node.data_type == 'RGBA'):
1310 if blend_type not in [nav[0] for nav in navs]:
1311 node.blend_type = blend_type
1312 else:
1313 if blend_type == 'NEXT':
1314 index = [i for i, entry in enumerate(blend_types) if node.blend_type in entry][0]
1315 # index = blend_types.index(node.blend_type)
1316 if index == len(blend_types) - 1:
1317 node.blend_type = blend_types[0][0]
1318 else:
1319 node.blend_type = blend_types[index + 1][0]
1321 if blend_type == 'PREV':
1322 index = [i for i, entry in enumerate(blend_types) if node.blend_type in entry][0]
1323 if index == 0:
1324 node.blend_type = blend_types[len(blend_types) - 1][0]
1325 else:
1326 node.blend_type = blend_types[index - 1][0]
1328 if node.type == 'MATH' or node.bl_idname == 'ShaderNodeMath':
1329 if operation not in [nav[0] for nav in navs]:
1330 node.operation = operation
1331 else:
1332 if operation == 'NEXT':
1333 index = [i for i, entry in enumerate(operations) if node.operation in entry][0]
1334 # index = operations.index(node.operation)
1335 if index == len(operations) - 1:
1336 node.operation = operations[0][0]
1337 else:
1338 node.operation = operations[index + 1][0]
1340 if operation == 'PREV':
1341 index = [i for i, entry in enumerate(operations) if node.operation in entry][0]
1342 # index = operations.index(node.operation)
1343 if index == 0:
1344 node.operation = operations[len(operations) - 1][0]
1345 else:
1346 node.operation = operations[index - 1][0]
1348 return {'FINISHED'}
1351 class NWChangeMixFactor(Operator, NWBase):
1352 bl_idname = "node.nw_factor"
1353 bl_label = "Change Factor"
1354 bl_description = "Change Factors of Mix Nodes and Mix Shader Nodes"
1355 bl_options = {'REGISTER', 'UNDO'}
1357 # option: Change factor.
1358 # If option is 1.0 or 0.0 - set to 1.0 or 0.0
1359 # Else - change factor by option value.
1360 option: FloatProperty()
1362 def execute(self, context):
1363 nodes, links = get_nodes_links(context)
1364 option = self.option
1365 selected = [] # entry = index
1366 for si, node in enumerate(nodes):
1367 if node.select:
1368 if node.type in {'MIX_RGB', 'MIX_SHADER'} or node.bl_idname == 'ShaderNodeMix':
1369 selected.append(si)
1371 for si in selected:
1372 fac = nodes[si].inputs[0]
1373 nodes[si].hide = False
1374 if option in {0.0, 1.0}:
1375 fac.default_value = option
1376 else:
1377 fac.default_value += option
1379 return {'FINISHED'}
1382 class NWCopySettings(Operator, NWBase):
1383 bl_idname = "node.nw_copy_settings"
1384 bl_label = "Copy Settings"
1385 bl_description = "Copy Settings of Active Node to Selected Nodes"
1386 bl_options = {'REGISTER', 'UNDO'}
1388 @classmethod
1389 def poll(cls, context):
1390 return (nw_check(context)
1391 and context.active_node is not None
1392 and context.active_node.type != 'FRAME')
1394 def execute(self, context):
1395 node_active = context.active_node
1396 node_selected = context.selected_nodes
1398 # Error handling
1399 if not (len(node_selected) > 1):
1400 self.report({'ERROR'}, "2 nodes must be selected at least")
1401 return {'CANCELLED'}
1403 # Check if active node is in the selection
1404 selected_node_names = [n.name for n in node_selected]
1405 if node_active.name not in selected_node_names:
1406 self.report({'ERROR'}, "No active node")
1407 return {'CANCELLED'}
1409 # Get nodes in selection by type
1410 valid_nodes = [n for n in node_selected if n.type == node_active.type]
1412 if not (len(valid_nodes) > 1) and node_active:
1413 self.report({'ERROR'}, "Selected nodes are not of the same type as {}".format(node_active.name))
1414 return {'CANCELLED'}
1416 if len(valid_nodes) != len(node_selected):
1417 # Report nodes that are not valid
1418 valid_node_names = [n.name for n in valid_nodes]
1419 not_valid_names = list(set(selected_node_names) - set(valid_node_names))
1420 self.report(
1421 {'INFO'},
1422 "Ignored {} (not of the same type as {})".format(
1423 ", ".join(not_valid_names),
1424 node_active.name))
1426 # Reference original
1427 orig = node_active
1428 # node_selected_names = [n.name for n in node_selected]
1430 # Output list
1431 success_names = []
1433 # Deselect all nodes
1434 for i in node_selected:
1435 i.select = False
1437 # Code by zeffii from http://blender.stackexchange.com/a/42338/3710
1438 # Run through all other nodes
1439 for node in valid_nodes[1:]:
1441 # Check for frame node
1442 parent = node.parent if node.parent else None
1443 node_loc = [node.location.x, node.location.y]
1445 # Select original to duplicate
1446 orig.select = True
1448 # Duplicate selected node
1449 bpy.ops.node.duplicate()
1450 new_node = context.selected_nodes[0]
1452 # Deselect copy
1453 new_node.select = False
1455 # Properties to copy
1456 node_tree = node.id_data
1457 props_to_copy = 'bl_idname name location height width'.split(' ')
1459 # Input and outputs
1460 reconnections = []
1461 mappings = chain.from_iterable([node.inputs, node.outputs])
1462 for i in (i for i in mappings if i.is_linked):
1463 for L in i.links:
1464 reconnections.append([L.from_socket.path_from_id(), L.to_socket.path_from_id()])
1466 # Properties
1467 props = {j: getattr(node, j) for j in props_to_copy}
1468 props_to_copy.pop(0)
1470 for prop in props_to_copy:
1471 setattr(new_node, prop, props[prop])
1473 # Get the node tree to remove the old node
1474 nodes = node_tree.nodes
1475 nodes.remove(node)
1476 new_node.name = props['name']
1478 if parent:
1479 new_node.parent = parent
1480 new_node.location = node_loc
1482 for str_from, str_to in reconnections:
1483 node_tree.connect_sockets(eval(str_from), eval(str_to))
1485 success_names.append(new_node.name)
1487 orig.select = True
1488 node_tree.nodes.active = orig
1489 self.report(
1490 {'INFO'},
1491 "Successfully copied attributes from {} to: {}".format(
1492 orig.name,
1493 ", ".join(success_names)))
1494 return {'FINISHED'}
1497 class NWCopyLabel(Operator, NWBase):
1498 bl_idname = "node.nw_copy_label"
1499 bl_label = "Copy Label"
1500 bl_options = {'REGISTER', 'UNDO'}
1502 option: EnumProperty(
1503 name="option",
1504 description="Source of name of label",
1505 items=(
1506 ('FROM_ACTIVE', 'from active', 'from active node',),
1507 ('FROM_NODE', 'from node', 'from node linked to selected node'),
1508 ('FROM_SOCKET', 'from socket', 'from socket linked to selected node'),
1512 def execute(self, context):
1513 nodes, links = get_nodes_links(context)
1514 option = self.option
1515 active = nodes.active
1516 if option == 'FROM_ACTIVE':
1517 if active:
1518 src_label = active.label
1519 for node in [n for n in nodes if n.select and nodes.active != n]:
1520 node.label = src_label
1521 elif option == 'FROM_NODE':
1522 selected = [n for n in nodes if n.select]
1523 for node in selected:
1524 for input in node.inputs:
1525 if input.links:
1526 src = input.links[0].from_node
1527 node.label = src.label
1528 break
1529 elif option == 'FROM_SOCKET':
1530 selected = [n for n in nodes if n.select]
1531 for node in selected:
1532 for input in node.inputs:
1533 if input.links:
1534 src = input.links[0].from_socket
1535 node.label = src.name
1536 break
1538 return {'FINISHED'}
1541 class NWClearLabel(Operator, NWBase):
1542 bl_idname = "node.nw_clear_label"
1543 bl_label = "Clear Label"
1544 bl_options = {'REGISTER', 'UNDO'}
1546 option: BoolProperty()
1548 def execute(self, context):
1549 nodes, links = get_nodes_links(context)
1550 for node in [n for n in nodes if n.select]:
1551 node.label = ''
1553 return {'FINISHED'}
1555 def invoke(self, context, event):
1556 if self.option:
1557 return self.execute(context)
1558 else:
1559 return context.window_manager.invoke_confirm(self, event)
1562 class NWModifyLabels(Operator, NWBase):
1563 """Modify Labels of all selected nodes"""
1564 bl_idname = "node.nw_modify_labels"
1565 bl_label = "Modify Labels"
1566 bl_options = {'REGISTER', 'UNDO'}
1568 prepend: StringProperty(
1569 name="Add to Beginning"
1571 append: StringProperty(
1572 name="Add to End"
1574 replace_from: StringProperty(
1575 name="Text to Replace"
1577 replace_to: StringProperty(
1578 name="Replace with"
1581 def execute(self, context):
1582 nodes, links = get_nodes_links(context)
1583 for node in [n for n in nodes if n.select]:
1584 node.label = self.prepend + node.label.replace(self.replace_from, self.replace_to) + self.append
1586 return {'FINISHED'}
1588 def invoke(self, context, event):
1589 self.prepend = ""
1590 self.append = ""
1591 self.remove = ""
1592 return context.window_manager.invoke_props_dialog(self)
1595 class NWAddTextureSetup(Operator, NWBase):
1596 bl_idname = "node.nw_add_texture"
1597 bl_label = "Texture Setup"
1598 bl_description = "Add Texture Node Setup to Selected Shaders"
1599 bl_options = {'REGISTER', 'UNDO'}
1601 add_mapping: BoolProperty(
1602 name="Add Mapping Nodes",
1603 description="Create coordinate and mapping nodes for the texture (ignored for selected texture nodes)",
1604 default=True)
1606 @classmethod
1607 def poll(cls, context):
1608 return (nw_check(context)
1609 and nw_check_space_type(cls, context, 'ShaderNodeTree'))
1611 def execute(self, context):
1612 nodes, links = get_nodes_links(context)
1614 texture_types = get_texture_node_types()
1615 selected_nodes = [n for n in nodes if n.select]
1617 for node in selected_nodes:
1618 if not node.inputs:
1619 continue
1621 input_index = 0
1622 target_input = node.inputs[0]
1623 for input in node.inputs:
1624 if input.enabled:
1625 input_index += 1
1626 if not input.is_linked:
1627 target_input = input
1628 break
1629 else:
1630 self.report({'WARNING'}, "No free inputs for node: " + node.name)
1631 continue
1633 x_offset = 0
1634 padding = 40.0
1635 locx = node.location.x
1636 locy = node.location.y - (input_index * padding)
1638 is_texture_node = node.rna_type.identifier in texture_types
1639 use_environment_texture = node.type == 'BACKGROUND'
1641 # Add an image texture before normal shader nodes.
1642 if not is_texture_node:
1643 image_texture_type = 'ShaderNodeTexEnvironment' if use_environment_texture else 'ShaderNodeTexImage'
1644 image_texture_node = nodes.new(image_texture_type)
1645 x_offset = x_offset + image_texture_node.width + padding
1646 image_texture_node.location = [locx - x_offset, locy]
1647 nodes.active = image_texture_node
1648 connect_sockets(image_texture_node.outputs[0], target_input)
1650 # The mapping setup following this will connect to the first input of this image texture.
1651 target_input = image_texture_node.inputs[0]
1653 node.select = False
1655 if is_texture_node or self.add_mapping:
1656 # Add Mapping node.
1657 mapping_node = nodes.new('ShaderNodeMapping')
1658 x_offset = x_offset + mapping_node.width + padding
1659 mapping_node.location = [locx - x_offset, locy]
1660 connect_sockets(mapping_node.outputs[0], target_input)
1662 # Add Texture Coordinates node.
1663 tex_coord_node = nodes.new('ShaderNodeTexCoord')
1664 x_offset = x_offset + tex_coord_node.width + padding
1665 tex_coord_node.location = [locx - x_offset, locy]
1667 is_procedural_texture = is_texture_node and node.type != 'TEX_IMAGE'
1668 use_generated_coordinates = is_procedural_texture or use_environment_texture
1669 tex_coord_output = tex_coord_node.outputs[0 if use_generated_coordinates else 2]
1670 connect_sockets(tex_coord_output, mapping_node.inputs[0])
1672 return {'FINISHED'}
1675 class NWAddPrincipledSetup(Operator, NWBase, ImportHelper):
1676 bl_idname = "node.nw_add_textures_for_principled"
1677 bl_label = "Principled Texture Setup"
1678 bl_description = "Add Texture Node Setup for Principled BSDF"
1679 bl_options = {'REGISTER', 'UNDO'}
1681 directory: StringProperty(
1682 name='Directory',
1683 subtype='DIR_PATH',
1684 default='',
1685 description='Folder to search in for image files'
1687 files: CollectionProperty(
1688 type=bpy.types.OperatorFileListElement,
1689 options={'HIDDEN', 'SKIP_SAVE'}
1692 relative_path: BoolProperty(
1693 name='Relative Path',
1694 description='Set the file path relative to the blend file, when possible',
1695 default=True
1698 order = [
1699 "filepath",
1700 "files",
1703 def draw(self, context):
1704 layout = self.layout
1705 layout.alignment = 'LEFT'
1707 layout.prop(self, 'relative_path')
1709 @classmethod
1710 def poll(cls, context):
1711 return (nw_check(context)
1712 and nw_check_space_type(cls, context, 'ShaderNodeTree'))
1714 def execute(self, context):
1715 # Check if everything is ok
1716 if not self.directory:
1717 self.report({'INFO'}, 'No Folder Selected')
1718 return {'CANCELLED'}
1719 if not self.files[:]:
1720 self.report({'INFO'}, 'No Files Selected')
1721 return {'CANCELLED'}
1723 nodes, links = get_nodes_links(context)
1724 active_node = nodes.active
1725 if not (active_node and active_node.bl_idname == 'ShaderNodeBsdfPrincipled'):
1726 self.report({'INFO'}, 'Select Principled BSDF')
1727 return {'CANCELLED'}
1729 # Filter textures names for texturetypes in filenames
1730 # [Socket Name, [abbreviations and keyword list], Filename placeholder]
1731 tags = context.preferences.addons[__package__].preferences.principled_tags
1732 normal_abbr = tags.normal.split(' ')
1733 bump_abbr = tags.bump.split(' ')
1734 gloss_abbr = tags.gloss.split(' ')
1735 rough_abbr = tags.rough.split(' ')
1736 socketnames = [
1737 ['Displacement', tags.displacement.split(' '), None],
1738 ['Base Color', tags.base_color.split(' '), None],
1739 ['Metallic', tags.metallic.split(' '), None],
1740 ['Specular IOR Level', tags.specular.split(' '), None],
1741 ['Roughness', rough_abbr + gloss_abbr, None],
1742 ['Bump', bump_abbr, None],
1743 ['Normal', normal_abbr, None],
1744 ['Transmission Weight', tags.transmission.split(' '), None],
1745 ['Emission Color', tags.emission.split(' '), None],
1746 ['Alpha', tags.alpha.split(' '), None],
1747 ['Ambient Occlusion', tags.ambient_occlusion.split(' '), None],
1750 match_files_to_socket_names(self.files, socketnames)
1751 # Remove socketnames without found files
1752 socketnames = [s for s in socketnames if s[2]
1753 and path.exists(self.directory + s[2])]
1754 if not socketnames:
1755 self.report({'INFO'}, 'No matching images found')
1756 print('No matching images found')
1757 return {'CANCELLED'}
1759 # Don't override path earlier as os.path is used to check the absolute path
1760 import_path = self.directory
1761 if self.relative_path:
1762 if bpy.data.filepath:
1763 try:
1764 import_path = bpy.path.relpath(self.directory)
1765 except ValueError:
1766 pass
1768 # Add found images
1769 print('\nMatched Textures:')
1770 texture_nodes = []
1771 disp_texture = None
1772 ao_texture = None
1773 normal_node = None
1774 normal_node_texture = None
1775 bump_node = None
1776 bump_node_texture = None
1777 roughness_node = None
1778 for i, sname in enumerate(socketnames):
1779 print(i, sname[0], sname[2])
1781 # DISPLACEMENT NODES
1782 if sname[0] == 'Displacement':
1783 disp_texture = nodes.new(type='ShaderNodeTexImage')
1784 img = bpy.data.images.load(path.join(import_path, sname[2]))
1785 disp_texture.image = img
1786 disp_texture.label = 'Displacement'
1787 if disp_texture.image:
1788 disp_texture.image.colorspace_settings.is_data = True
1790 # Add displacement offset nodes
1791 disp_node = nodes.new(type='ShaderNodeDisplacement')
1792 # Align the Displacement node under the active Principled BSDF node
1793 disp_node.location = active_node.location + Vector((100, -700))
1794 link = connect_sockets(disp_node.inputs[0], disp_texture.outputs[0])
1796 # TODO Turn on true displacement in the material
1797 # Too complicated for now
1799 # Find output node
1800 output_node = [n for n in nodes if n.bl_idname == 'ShaderNodeOutputMaterial']
1801 if output_node:
1802 if not output_node[0].inputs[2].is_linked:
1803 link = connect_sockets(output_node[0].inputs[2], disp_node.outputs[0])
1805 continue
1807 # BUMP NODES
1808 elif sname[0] == 'Bump':
1809 # Test if new texture node is bump map
1810 fname_components = split_into_components(sname[2])
1811 match_bump = set(bump_abbr).intersection(set(fname_components))
1812 if match_bump:
1813 # If Bump add bump node in between
1814 bump_node_texture = nodes.new(type='ShaderNodeTexImage')
1815 img = bpy.data.images.load(path.join(import_path, sname[2]))
1816 bump_node_texture.image = img
1817 bump_node_texture.label = 'Bump'
1819 # Add bump node
1820 bump_node = nodes.new(type='ShaderNodeBump')
1821 link = connect_sockets(bump_node.inputs[2], bump_node_texture.outputs[0])
1822 link = connect_sockets(active_node.inputs['Normal'], bump_node.outputs[0])
1823 continue
1825 # NORMAL NODES
1826 elif sname[0] == 'Normal':
1827 # Test if new texture node is normal map
1828 fname_components = split_into_components(sname[2])
1829 match_normal = set(normal_abbr).intersection(set(fname_components))
1830 if match_normal:
1831 # If Normal add normal node in between
1832 normal_node_texture = nodes.new(type='ShaderNodeTexImage')
1833 img = bpy.data.images.load(path.join(import_path, sname[2]))
1834 normal_node_texture.image = img
1835 normal_node_texture.label = 'Normal'
1837 # Add normal node
1838 normal_node = nodes.new(type='ShaderNodeNormalMap')
1839 link = connect_sockets(normal_node.inputs[1], normal_node_texture.outputs[0])
1840 # Connect to bump node if it was created before, otherwise to the BSDF
1841 if bump_node is None:
1842 link = connect_sockets(active_node.inputs[sname[0]], normal_node.outputs[0])
1843 else:
1844 link = connect_sockets(bump_node.inputs[sname[0]], normal_node.outputs[sname[0]])
1845 continue
1847 # AMBIENT OCCLUSION TEXTURE
1848 elif sname[0] == 'Ambient Occlusion':
1849 ao_texture = nodes.new(type='ShaderNodeTexImage')
1850 img = bpy.data.images.load(path.join(import_path, sname[2]))
1851 ao_texture.image = img
1852 ao_texture.label = sname[0]
1853 if ao_texture.image:
1854 ao_texture.image.colorspace_settings.is_data = True
1856 continue
1858 if not active_node.inputs[sname[0]].is_linked:
1859 # No texture node connected -> add texture node with new image
1860 texture_node = nodes.new(type='ShaderNodeTexImage')
1861 img = bpy.data.images.load(path.join(import_path, sname[2]))
1862 texture_node.image = img
1864 if sname[0] == 'Roughness':
1865 # Test if glossy or roughness map
1866 fname_components = split_into_components(sname[2])
1867 match_rough = set(rough_abbr).intersection(set(fname_components))
1868 match_gloss = set(gloss_abbr).intersection(set(fname_components))
1870 if match_rough:
1871 # If Roughness nothing to to
1872 link = connect_sockets(active_node.inputs[sname[0]], texture_node.outputs[0])
1874 elif match_gloss:
1875 # If Gloss Map add invert node
1876 invert_node = nodes.new(type='ShaderNodeInvert')
1877 link = connect_sockets(invert_node.inputs[1], texture_node.outputs[0])
1879 link = connect_sockets(active_node.inputs[sname[0]], invert_node.outputs[0])
1880 roughness_node = texture_node
1882 else:
1883 # This is a simple connection Texture --> Input slot
1884 link = connect_sockets(active_node.inputs[sname[0]], texture_node.outputs[0])
1886 # Use non-color except for color inputs
1887 if sname[0] not in ['Base Color', 'Emission Color'] and texture_node.image:
1888 texture_node.image.colorspace_settings.is_data = True
1890 else:
1891 # If already texture connected. add to node list for alignment
1892 texture_node = active_node.inputs[sname[0]].links[0].from_node
1894 # This are all connected texture nodes
1895 texture_nodes.append(texture_node)
1896 texture_node.label = sname[0]
1898 if disp_texture:
1899 texture_nodes.append(disp_texture)
1900 if bump_node_texture:
1901 texture_nodes.append(bump_node_texture)
1902 if normal_node_texture:
1903 texture_nodes.append(normal_node_texture)
1905 if ao_texture:
1906 # We want the ambient occlusion texture to be the top most texture node
1907 texture_nodes.insert(0, ao_texture)
1909 # Alignment
1910 for i, texture_node in enumerate(texture_nodes):
1911 offset = Vector((-550, (i * -280) + 200))
1912 texture_node.location = active_node.location + offset
1914 if normal_node:
1915 # Extra alignment if normal node was added
1916 normal_node.location = normal_node_texture.location + Vector((300, 0))
1918 if bump_node:
1919 # Extra alignment if bump node was added
1920 bump_node.location = bump_node_texture.location + Vector((300, 0))
1922 if roughness_node:
1923 # Alignment of invert node if glossy map
1924 invert_node.location = roughness_node.location + Vector((300, 0))
1926 # Add texture input + mapping
1927 mapping = nodes.new(type='ShaderNodeMapping')
1928 mapping.location = active_node.location + Vector((-1050, 0))
1929 if len(texture_nodes) > 1:
1930 # If more than one texture add reroute node in between
1931 reroute = nodes.new(type='NodeReroute')
1932 texture_nodes.append(reroute)
1933 tex_coords = Vector((texture_nodes[0].location.x,
1934 sum(n.location.y for n in texture_nodes) / len(texture_nodes)))
1935 reroute.location = tex_coords + Vector((-50, -120))
1936 for texture_node in texture_nodes:
1937 link = connect_sockets(texture_node.inputs[0], reroute.outputs[0])
1938 link = connect_sockets(reroute.inputs[0], mapping.outputs[0])
1939 else:
1940 link = connect_sockets(texture_nodes[0].inputs[0], mapping.outputs[0])
1942 # Connect texture_coordinates to mapping node
1943 texture_input = nodes.new(type='ShaderNodeTexCoord')
1944 texture_input.location = mapping.location + Vector((-200, 0))
1945 link = connect_sockets(mapping.inputs[0], texture_input.outputs[2])
1947 # Create frame around tex coords and mapping
1948 frame = nodes.new(type='NodeFrame')
1949 frame.label = 'Mapping'
1950 mapping.parent = frame
1951 texture_input.parent = frame
1952 frame.update()
1954 # Create frame around texture nodes
1955 frame = nodes.new(type='NodeFrame')
1956 frame.label = 'Textures'
1957 for tnode in texture_nodes:
1958 tnode.parent = frame
1959 frame.update()
1961 # Just to be sure
1962 active_node.select = False
1963 nodes.update()
1964 links.update()
1965 force_update(context)
1966 return {'FINISHED'}
1969 class NWAddReroutes(Operator, NWBase):
1970 """Add Reroute Nodes and link them to outputs of selected nodes"""
1971 bl_idname = "node.nw_add_reroutes"
1972 bl_label = "Add Reroutes"
1973 bl_description = "Add Reroutes to Outputs"
1974 bl_options = {'REGISTER', 'UNDO'}
1976 option: EnumProperty(
1977 name="option",
1978 items=[
1979 ('ALL', 'to all', 'Add to all outputs'),
1980 ('LOOSE', 'to loose', 'Add only to loose outputs'),
1981 ('LINKED', 'to linked', 'Add only to linked outputs'),
1985 def execute(self, context):
1986 nodes, _links = get_nodes_links(context)
1987 post_select = [] # Nodes to be selected after execution.
1988 y_offset = -22.0
1990 # Create reroutes and recreate links.
1991 for node in [n for n in nodes if n.select]:
1992 if not node.outputs:
1993 node.select = False
1994 continue
1995 x, y = node.location
1996 width = node.width
1997 # Unhide 'REROUTE' nodes to avoid issues with location.y
1998 if node.type == 'REROUTE':
1999 node.hide = False
2000 # Hack needed to calculate real width.
2001 if node.hide:
2002 bpy.ops.node.select_all(action='DESELECT')
2003 helper = nodes.new('NodeReroute')
2004 helper.select = True
2005 node.select = True
2006 # Resize node and helper to zero. Then check locations to calculate width.
2007 bpy.ops.transform.resize(value=(0.0, 0.0, 0.0))
2008 width = 2.0 * (helper.location.x - node.location.x)
2009 # Restore node location.
2010 node.location = x, y
2011 # Delete helper.
2012 node.select = False
2013 # Only helper is selected now.
2014 bpy.ops.node.delete()
2015 x = node.location.x + width + 20.0
2016 if node.type != 'REROUTE':
2017 y -= 35.0
2019 reroutes_count = 0 # Will be used when aligning reroutes added to hidden nodes.
2020 for out_i, output in enumerate(node.outputs):
2021 if node.type == 'R_LAYERS' and output.name != 'Alpha':
2022 # If 'R_LAYERS' check if output is used in render pass.
2023 # If output is "Alpha", assume it's used. Not available in passes.
2024 node_scene = node.scene
2025 node_layer = node.layer
2026 for rlo in rl_outputs:
2027 # Check entries in global 'rl_outputs' variable.
2028 if output.name in {rlo.output_name, rlo.exr_output_name}:
2029 if not getattr(node_scene.view_layers[node_layer], rlo.render_pass):
2030 node.select = False
2031 continue
2032 # Output is valid when option is 'all' or when 'loose' output has no links.
2033 valid = ((self.option == 'ALL') or
2034 (self.option == 'LOOSE' and not output.links) or
2035 (self.option == 'LINKED' and output.links))
2036 # Add reroutes only if valid, but offset location in all cases.
2037 if valid:
2038 n = nodes.new('NodeReroute')
2039 nodes.active = n
2040 for link in output.links:
2041 connect_sockets(n.outputs[0], link.to_socket)
2042 connect_sockets(output, n.inputs[0])
2043 n.location = x, y
2044 post_select.append(n)
2045 reroutes_count += 1
2046 y += y_offset
2047 # Deselect the node so that after execution of script only newly created nodes are selected.
2048 node.select = False
2050 # Nicer reroutes distribution along y when node.hide.
2051 if node.hide:
2052 y_translate = reroutes_count * y_offset / 2.0 - y_offset - 35.0
2053 for reroute in [r for r in nodes if r.select]:
2054 reroute.location.y -= y_translate
2055 for node in post_select:
2056 node.select = True
2058 return {'FINISHED'}
2061 class NWLinkActiveToSelected(Operator, NWBase):
2062 """Link active node to selected nodes basing on various criteria"""
2063 bl_idname = "node.nw_link_active_to_selected"
2064 bl_label = "Link Active Node to Selected"
2065 bl_options = {'REGISTER', 'UNDO'}
2067 replace: BoolProperty()
2068 use_node_name: BoolProperty()
2069 use_outputs_names: BoolProperty()
2071 @classmethod
2072 def poll(cls, context):
2073 return (nw_check(context)
2074 and context.active_node is not None
2075 and context.active_node.select)
2077 def execute(self, context):
2078 nodes, links = get_nodes_links(context)
2079 replace = self.replace
2080 use_node_name = self.use_node_name
2081 use_outputs_names = self.use_outputs_names
2082 active = nodes.active
2083 selected = [node for node in nodes if node.select and node != active]
2084 outputs = [] # Only usable outputs of active nodes will be stored here.
2085 for out in active.outputs:
2086 if active.type != 'R_LAYERS':
2087 outputs.append(out)
2088 else:
2089 # 'R_LAYERS' node type needs special handling.
2090 # outputs of 'R_LAYERS' are callable even if not seen in UI.
2091 # Only outputs that represent used passes should be taken into account
2092 # Check if pass represented by output is used.
2093 # global 'rl_outputs' list will be used for that
2094 for rlo in rl_outputs:
2095 pass_used = False # initial value. Will be set to True if pass is used
2096 if out.name == 'Alpha':
2097 # Alpha output is always present. Doesn't have representation in render pass. Assume it's used.
2098 pass_used = True
2099 elif out.name in {rlo.output_name, rlo.exr_output_name}:
2100 # example 'render_pass' entry: 'use_pass_uv' Check if True in scene render layers
2101 pass_used = getattr(active.scene.view_layers[active.layer], rlo.render_pass)
2102 break
2103 if pass_used:
2104 outputs.append(out)
2105 doit = True # Will be changed to False when links successfully added to previous output.
2106 for out in outputs:
2107 if doit:
2108 for node in selected:
2109 dst_name = node.name # Will be compared with src_name if needed.
2110 # When node has label - use it as dst_name
2111 if node.label:
2112 dst_name = node.label
2113 valid = True # Initial value. Will be changed to False if names don't match.
2114 src_name = dst_name # If names not used - this assignment will keep valid = True.
2115 if use_node_name:
2116 # Set src_name to source node name or label
2117 src_name = active.name
2118 if active.label:
2119 src_name = active.label
2120 elif use_outputs_names:
2121 src_name = (out.name, )
2122 for rlo in rl_outputs:
2123 if out.name in {rlo.output_name, rlo.exr_output_name}:
2124 src_name = (rlo.output_name, rlo.exr_output_name)
2125 if dst_name not in src_name:
2126 valid = False
2127 if valid:
2128 for input in node.inputs:
2129 if input.type == out.type or node.type == 'REROUTE':
2130 if replace or not input.is_linked:
2131 connect_sockets(out, input)
2132 if not use_node_name and not use_outputs_names:
2133 doit = False
2134 break
2136 return {'FINISHED'}
2139 class NWAlignNodes(Operator, NWBase):
2140 '''Align the selected nodes neatly in a row/column'''
2141 bl_idname = "node.nw_align_nodes"
2142 bl_label = "Align Nodes"
2143 bl_options = {'REGISTER', 'UNDO'}
2144 margin: IntProperty(name='Margin', default=50, description='The amount of space between nodes')
2146 def execute(self, context):
2147 nodes, links = get_nodes_links(context)
2148 margin = self.margin
2150 selection = []
2151 for node in nodes:
2152 if node.select and node.type != 'FRAME':
2153 selection.append(node)
2155 # If no nodes are selected, align all nodes
2156 active_loc = None
2157 if not selection:
2158 selection = nodes
2159 elif nodes.active in selection:
2160 active_loc = copy(nodes.active.location) # make a copy, not a reference
2162 # Check if nodes should be laid out horizontally or vertically
2163 # use dimension to get center of node, not corner
2164 x_locs = [n.location.x + (n.dimensions.x / 2) for n in selection]
2165 y_locs = [n.location.y - (n.dimensions.y / 2) for n in selection]
2166 x_range = max(x_locs) - min(x_locs)
2167 y_range = max(y_locs) - min(y_locs)
2168 mid_x = (max(x_locs) + min(x_locs)) / 2
2169 mid_y = (max(y_locs) + min(y_locs)) / 2
2170 horizontal = x_range > y_range
2172 # Sort selection by location of node mid-point
2173 if horizontal:
2174 selection = sorted(selection, key=lambda n: n.location.x + (n.dimensions.x / 2))
2175 else:
2176 selection = sorted(selection, key=lambda n: n.location.y - (n.dimensions.y / 2), reverse=True)
2178 # Alignment
2179 current_pos = 0
2180 for node in selection:
2181 current_margin = margin
2182 current_margin = current_margin * 0.5 if node.hide else current_margin # use a smaller margin for hidden nodes
2184 if horizontal:
2185 node.location.x = current_pos
2186 current_pos += current_margin + node.dimensions.x
2187 node.location.y = mid_y + (node.dimensions.y / 2)
2188 else:
2189 node.location.y = current_pos
2190 current_pos -= (current_margin * 0.3) + node.dimensions.y # use half-margin for vertical alignment
2191 node.location.x = mid_x - (node.dimensions.x / 2)
2193 # If active node is selected, center nodes around it
2194 if active_loc is not None:
2195 active_loc_diff = active_loc - nodes.active.location
2196 for node in selection:
2197 node.location += active_loc_diff
2198 else: # Position nodes centered around where they used to be
2199 locs = ([n.location.x + (n.dimensions.x / 2) for n in selection]
2200 ) if horizontal else ([n.location.y - (n.dimensions.y / 2) for n in selection])
2201 new_mid = (max(locs) + min(locs)) / 2
2202 for node in selection:
2203 if horizontal:
2204 node.location.x += (mid_x - new_mid)
2205 else:
2206 node.location.y += (mid_y - new_mid)
2208 return {'FINISHED'}
2211 class NWSelectParentChildren(Operator, NWBase):
2212 bl_idname = "node.nw_select_parent_child"
2213 bl_label = "Select Parent or Children"
2214 bl_options = {'REGISTER', 'UNDO'}
2216 option: EnumProperty(
2217 name="option",
2218 items=(
2219 ('PARENT', 'Select Parent', 'Select Parent Frame'),
2220 ('CHILD', 'Select Children', 'Select members of selected frame'),
2224 def execute(self, context):
2225 nodes, links = get_nodes_links(context)
2226 option = self.option
2227 selected = [node for node in nodes if node.select]
2228 if option == 'PARENT':
2229 for sel in selected:
2230 parent = sel.parent
2231 if parent:
2232 parent.select = True
2233 else: # option == 'CHILD'
2234 for sel in selected:
2235 children = [node for node in nodes if node.parent == sel]
2236 for kid in children:
2237 kid.select = True
2239 return {'FINISHED'}
2242 class NWDetachOutputs(Operator, NWBase):
2243 """Detach outputs of selected node leaving inputs linked"""
2244 bl_idname = "node.nw_detach_outputs"
2245 bl_label = "Detach Outputs"
2246 bl_options = {'REGISTER', 'UNDO'}
2248 def execute(self, context):
2249 nodes, links = get_nodes_links(context)
2250 selected = context.selected_nodes
2251 bpy.ops.node.duplicate_move_keep_inputs()
2252 new_nodes = context.selected_nodes
2253 bpy.ops.node.select_all(action="DESELECT")
2254 for node in selected:
2255 node.select = True
2256 bpy.ops.node.delete_reconnect()
2257 for new_node in new_nodes:
2258 new_node.select = True
2259 bpy.ops.transform.translate('INVOKE_DEFAULT')
2261 return {'FINISHED'}
2264 class NWLinkToOutputNode(Operator):
2265 """Link to Composite node or Material Output node"""
2266 bl_idname = "node.nw_link_out"
2267 bl_label = "Connect to Output"
2268 bl_options = {'REGISTER', 'UNDO'}
2270 @classmethod
2271 def poll(cls, context):
2272 """Disabled for custom nodes as we do not know which nodes are outputs."""
2273 return (nw_check(context)
2274 and nw_check_space_type(cls, context, 'ShaderNodeTree', 'CompositorNodeTree',
2275 'TextureNodeTree', 'GeometryNodeTree')
2276 and context.active_node is not None
2277 and any(is_visible_socket(out) for out in context.active_node.outputs))
2279 def execute(self, context):
2280 nodes, links = get_nodes_links(context)
2281 active = nodes.active
2282 output_index = None
2283 tree_type = context.space_data.tree_type
2284 shader_outputs = {'OBJECT': 'ShaderNodeOutputMaterial',
2285 'WORLD': 'ShaderNodeOutputWorld',
2286 'LINESTYLE': 'ShaderNodeOutputLineStyle'}
2287 output_type = {
2288 'ShaderNodeTree': shader_outputs[context.space_data.shader_type],
2289 'CompositorNodeTree': 'CompositorNodeComposite',
2290 'TextureNodeTree': 'TextureNodeOutput',
2291 'GeometryNodeTree': 'NodeGroupOutput',
2292 }[tree_type]
2293 for node in nodes:
2294 # check whether the node is an output node and,
2295 # if supported, whether it's the active one
2296 if node.rna_type.identifier == output_type \
2297 and (node.is_active_output if hasattr(node, 'is_active_output')
2298 else True):
2299 output_node = node
2300 break
2301 else: # No output node exists
2302 bpy.ops.node.select_all(action="DESELECT")
2303 output_node = nodes.new(output_type)
2304 output_node.location.x = active.location.x + active.dimensions.x + 80
2305 output_node.location.y = active.location.y
2307 if active.outputs:
2308 for i, output in enumerate(active.outputs):
2309 if is_visible_socket(output):
2310 output_index = i
2311 break
2312 for i, output in enumerate(active.outputs):
2313 if output.type == output_node.inputs[0].type and is_visible_socket(output):
2314 output_index = i
2315 break
2317 out_input_index = 0
2318 if tree_type == 'ShaderNodeTree':
2319 if active.outputs[output_index].name == 'Volume':
2320 out_input_index = 1
2321 elif active.outputs[output_index].name == 'Displacement':
2322 out_input_index = 2
2323 elif tree_type == 'GeometryNodeTree':
2324 if active.outputs[output_index].type != 'GEOMETRY':
2325 return {'CANCELLED'}
2326 connect_sockets(active.outputs[output_index], output_node.inputs[out_input_index])
2328 force_update(context) # viewport render does not update
2330 return {'FINISHED'}
2333 class NWMakeLink(Operator, NWBase):
2334 """Make a link from one socket to another"""
2335 bl_idname = 'node.nw_make_link'
2336 bl_label = 'Make Link'
2337 bl_options = {'REGISTER', 'UNDO'}
2338 from_socket: IntProperty()
2339 to_socket: IntProperty()
2341 def execute(self, context):
2342 nodes, links = get_nodes_links(context)
2344 n1 = nodes[context.scene.NWLazySource]
2345 n2 = nodes[context.scene.NWLazyTarget]
2347 connect_sockets(n1.outputs[self.from_socket], n2.inputs[self.to_socket])
2349 force_update(context)
2351 return {'FINISHED'}
2354 class NWCallInputsMenu(Operator, NWBase):
2355 """Link from this output"""
2356 bl_idname = 'node.nw_call_inputs_menu'
2357 bl_label = 'Make Link'
2358 bl_options = {'REGISTER', 'UNDO'}
2359 from_socket: IntProperty()
2361 def execute(self, context):
2362 nodes, links = get_nodes_links(context)
2364 context.scene.NWSourceSocket = self.from_socket
2366 n1 = nodes[context.scene.NWLazySource]
2367 n2 = nodes[context.scene.NWLazyTarget]
2368 if len(n2.inputs) > 1:
2369 bpy.ops.wm.call_menu("INVOKE_DEFAULT", name=NWConnectionListInputs.bl_idname)
2370 elif len(n2.inputs) == 1:
2371 connect_sockets(n1.outputs[self.from_socket], n2.inputs[0])
2372 return {'FINISHED'}
2375 class NWAddSequence(Operator, NWBase, ImportHelper):
2376 """Add an Image Sequence"""
2377 bl_idname = 'node.nw_add_sequence'
2378 bl_label = 'Import Image Sequence'
2379 bl_options = {'REGISTER', 'UNDO'}
2381 directory: StringProperty(
2382 subtype="DIR_PATH"
2384 filename: StringProperty(
2385 subtype="FILE_NAME"
2387 files: CollectionProperty(
2388 type=bpy.types.OperatorFileListElement,
2389 options={'HIDDEN', 'SKIP_SAVE'}
2391 relative_path: BoolProperty(
2392 name='Relative Path',
2393 description='Set the file path relative to the blend file, when possible',
2394 default=True
2397 def draw(self, context):
2398 layout = self.layout
2399 layout.alignment = 'LEFT'
2401 layout.prop(self, 'relative_path')
2403 def execute(self, context):
2404 nodes, links = get_nodes_links(context)
2405 directory = self.directory
2406 filename = self.filename
2407 files = self.files
2408 tree = context.space_data.node_tree
2410 # DEBUG
2411 # print ("\nDIR:", directory)
2412 # print ("FN:", filename)
2413 # print ("Fs:", list(f.name for f in files), '\n')
2415 if tree.type == 'SHADER':
2416 node_type = "ShaderNodeTexImage"
2417 elif tree.type == 'COMPOSITING':
2418 node_type = "CompositorNodeImage"
2419 else:
2420 self.report({'ERROR'}, "Unsupported Node Tree type!")
2421 return {'CANCELLED'}
2423 if not files[0].name and not filename:
2424 self.report({'ERROR'}, "No file chosen")
2425 return {'CANCELLED'}
2426 elif files[0].name and (not filename or not path.exists(directory + filename)):
2427 # User has selected multiple files without an active one, or the active one is non-existent
2428 filename = files[0].name
2430 if not path.exists(directory + filename):
2431 self.report({'ERROR'}, filename + " does not exist!")
2432 return {'CANCELLED'}
2434 without_ext = '.'.join(filename.split('.')[:-1])
2436 # if last digit isn't a number, it's not a sequence
2437 if not without_ext[-1].isdigit():
2438 self.report({'ERROR'}, filename + " does not seem to be part of a sequence")
2439 return {'CANCELLED'}
2441 extension = filename.split('.')[-1]
2442 reverse = without_ext[::-1] # reverse string
2444 count_numbers = 0
2445 for char in reverse:
2446 if char.isdigit():
2447 count_numbers += 1
2448 else:
2449 break
2451 without_num = without_ext[:count_numbers * -1]
2453 files = sorted(glob(directory + without_num + "[0-9]" * count_numbers + "." + extension))
2455 num_frames = len(files)
2457 nodes_list = [node for node in nodes]
2458 if nodes_list:
2459 nodes_list.sort(key=lambda k: k.location.x)
2460 xloc = nodes_list[0].location.x - 220 # place new nodes at far left
2461 yloc = 0
2462 for node in nodes:
2463 node.select = False
2464 yloc += node_mid_pt(node, 'y')
2465 yloc = yloc / len(nodes)
2466 else:
2467 xloc = 0
2468 yloc = 0
2470 name_with_hashes = without_num + "#" * count_numbers + '.' + extension
2472 bpy.ops.node.add_node('INVOKE_DEFAULT', use_transform=True, type=node_type)
2473 node = nodes.active
2474 node.label = name_with_hashes
2476 filepath = directory + (without_ext + '.' + extension)
2477 if self.relative_path:
2478 if bpy.data.filepath:
2479 try:
2480 filepath = bpy.path.relpath(filepath)
2481 except ValueError:
2482 pass
2484 img = bpy.data.images.load(filepath)
2485 img.source = 'SEQUENCE'
2486 img.name = name_with_hashes
2487 node.image = img
2488 image_user = node.image_user if tree.type == 'SHADER' else node
2489 # separate the number from the file name of the first file
2490 image_user.frame_offset = int(files[0][len(without_num) + len(directory):-1 * (len(extension) + 1)]) - 1
2491 image_user.frame_duration = num_frames
2493 return {'FINISHED'}
2496 class NWAddMultipleImages(Operator, NWBase, ImportHelper):
2497 """Add multiple images at once"""
2498 bl_idname = 'node.nw_add_multiple_images'
2499 bl_label = 'Open Selected Images'
2500 bl_options = {'REGISTER', 'UNDO'}
2501 directory: StringProperty(
2502 subtype="DIR_PATH"
2504 files: CollectionProperty(
2505 type=bpy.types.OperatorFileListElement,
2506 options={'HIDDEN', 'SKIP_SAVE'}
2509 def execute(self, context):
2510 nodes, links = get_nodes_links(context)
2512 xloc, yloc = context.region.view2d.region_to_view(context.area.width / 2, context.area.height / 2)
2514 if context.space_data.node_tree.type == 'SHADER':
2515 node_type = "ShaderNodeTexImage"
2516 elif context.space_data.node_tree.type == 'COMPOSITING':
2517 node_type = "CompositorNodeImage"
2518 else:
2519 self.report({'ERROR'}, "Unsupported Node Tree type!")
2520 return {'CANCELLED'}
2522 new_nodes = []
2523 for f in self.files:
2524 fname = f.name
2526 node = nodes.new(node_type)
2527 new_nodes.append(node)
2528 node.label = fname
2529 node.hide = True
2530 node.location.x = xloc
2531 node.location.y = yloc
2532 yloc -= 40
2534 img = bpy.data.images.load(self.directory + fname)
2535 node.image = img
2537 # shift new nodes up to center of tree
2538 list_size = new_nodes[0].location.y - new_nodes[-1].location.y
2539 for node in nodes:
2540 if node in new_nodes:
2541 node.select = True
2542 node.location.y += (list_size / 2)
2543 else:
2544 node.select = False
2545 return {'FINISHED'}
2548 class NWViewerFocus(bpy.types.Operator):
2549 """Set the viewer tile center to the mouse position"""
2550 bl_idname = "node.nw_viewer_focus"
2551 bl_label = "Viewer Focus"
2553 x: bpy.props.IntProperty()
2554 y: bpy.props.IntProperty()
2556 @classmethod
2557 def poll(cls, context):
2558 return (nw_check(context)
2559 and nw_check_space_type(cls, context, 'CompositorNodeTree'))
2561 def execute(self, context):
2562 return {'FINISHED'}
2564 def invoke(self, context, event):
2565 render = context.scene.render
2566 space = context.space_data
2567 percent = render.resolution_percentage * 0.01
2569 nodes, links = get_nodes_links(context)
2570 viewers = [n for n in nodes if n.type == 'VIEWER']
2572 if viewers:
2573 mlocx = event.mouse_region_x
2574 mlocy = event.mouse_region_y
2575 select_node = bpy.ops.node.select(location=(mlocx, mlocy), extend=False)
2577 if 'FINISHED' not in select_node: # only run if we're not clicking on a node
2578 region_x = context.region.width
2579 region_y = context.region.height
2581 region_center_x = context.region.width / 2
2582 region_center_y = context.region.height / 2
2584 bd_x = render.resolution_x * percent * space.backdrop_zoom
2585 bd_y = render.resolution_y * percent * space.backdrop_zoom
2587 backdrop_center_x = (bd_x / 2) - space.backdrop_offset[0]
2588 backdrop_center_y = (bd_y / 2) - space.backdrop_offset[1]
2590 margin_x = region_center_x - backdrop_center_x
2591 margin_y = region_center_y - backdrop_center_y
2593 abs_mouse_x = (mlocx - margin_x) / bd_x
2594 abs_mouse_y = (mlocy - margin_y) / bd_y
2596 for node in viewers:
2597 node.center_x = abs_mouse_x
2598 node.center_y = abs_mouse_y
2599 else:
2600 return {'PASS_THROUGH'}
2602 return self.execute(context)
2605 class NWSaveViewer(bpy.types.Operator, ExportHelper):
2606 """Save the current viewer node to an image file"""
2607 bl_idname = "node.nw_save_viewer"
2608 bl_label = "Save This Image"
2609 filepath: StringProperty(subtype="FILE_PATH")
2610 filename_ext: EnumProperty(
2611 name="Format",
2612 description="Choose the file format to save to",
2613 items=(('.bmp', "BMP", ""),
2614 ('.rgb', 'IRIS', ""),
2615 ('.png', 'PNG', ""),
2616 ('.jpg', 'JPEG', ""),
2617 ('.jp2', 'JPEG2000', ""),
2618 ('.tga', 'TARGA', ""),
2619 ('.cin', 'CINEON', ""),
2620 ('.dpx', 'DPX', ""),
2621 ('.exr', 'OPEN_EXR', ""),
2622 ('.hdr', 'HDR', ""),
2623 ('.tif', 'TIFF', "")),
2624 default='.png',
2627 @classmethod
2628 def poll(cls, context):
2629 return (nw_check(context)
2630 and nw_check_space_type(cls, context, 'CompositorNodeTree')
2631 and any(img.source == 'VIEWER'
2632 and img.render_slots == 0
2633 for img in bpy.data.images)
2634 and sum(bpy.data.images["Viewer Node"].size) > 0) # False if not connected or connected but no image
2636 def execute(self, context):
2637 fp = self.filepath
2638 if fp:
2639 formats = {
2640 '.bmp': 'BMP',
2641 '.rgb': 'IRIS',
2642 '.png': 'PNG',
2643 '.jpg': 'JPEG',
2644 '.jpeg': 'JPEG',
2645 '.jp2': 'JPEG2000',
2646 '.tga': 'TARGA',
2647 '.cin': 'CINEON',
2648 '.dpx': 'DPX',
2649 '.exr': 'OPEN_EXR',
2650 '.hdr': 'HDR',
2651 '.tiff': 'TIFF',
2652 '.tif': 'TIFF'}
2653 basename, ext = path.splitext(fp)
2654 old_render_format = context.scene.render.image_settings.file_format
2655 context.scene.render.image_settings.file_format = formats[self.filename_ext]
2656 context.area.type = "IMAGE_EDITOR"
2657 context.area.spaces[0].image = bpy.data.images['Viewer Node']
2658 context.area.spaces[0].image.save_render(fp)
2659 context.area.type = "NODE_EDITOR"
2660 context.scene.render.image_settings.file_format = old_render_format
2661 return {'FINISHED'}
2664 class NWResetNodes(bpy.types.Operator):
2665 """Reset Nodes in Selection"""
2666 bl_idname = "node.nw_reset_nodes"
2667 bl_label = "Reset Nodes"
2668 bl_options = {'REGISTER', 'UNDO'}
2670 @classmethod
2671 def poll(cls, context):
2672 space = context.space_data
2673 return space.type == 'NODE_EDITOR'
2675 def execute(self, context):
2676 node_active = context.active_node
2677 node_selected = context.selected_nodes
2678 node_ignore = ["FRAME", "REROUTE", "GROUP", "SIMULATION_INPUT", "SIMULATION_OUTPUT"]
2680 # Check if one node is selected at least
2681 if not (len(node_selected) > 0):
2682 self.report({'ERROR'}, "1 node must be selected at least")
2683 return {'CANCELLED'}
2685 active_node_name = node_active.name if node_active.select else None
2686 valid_nodes = [n for n in node_selected if n.type not in node_ignore]
2688 # Create output lists
2689 selected_node_names = [n.name for n in node_selected]
2690 success_names = []
2692 # Reset all valid children in a frame
2693 node_active_is_frame = False
2694 if len(node_selected) == 1 and node_active.type == "FRAME":
2695 node_tree = node_active.id_data
2696 children = [n for n in node_tree.nodes if n.parent == node_active]
2697 if children:
2698 valid_nodes = [n for n in children if n.type not in node_ignore]
2699 selected_node_names = [n.name for n in children if n.type not in node_ignore]
2700 node_active_is_frame = True
2702 # Check if valid nodes in selection
2703 if not (len(valid_nodes) > 0):
2704 # Check for frames only
2705 frames_selected = [n for n in node_selected if n.type == "FRAME"]
2706 if (len(frames_selected) > 1 and len(frames_selected) == len(node_selected)):
2707 self.report({'ERROR'}, "Please select only 1 frame to reset")
2708 else:
2709 self.report({'ERROR'}, "No valid node(s) in selection")
2710 return {'CANCELLED'}
2712 # Report nodes that are not valid
2713 if len(valid_nodes) != len(node_selected) and node_active_is_frame is False:
2714 valid_node_names = [n.name for n in valid_nodes]
2715 not_valid_names = list(set(selected_node_names) - set(valid_node_names))
2716 self.report({'INFO'}, "Ignored {}".format(", ".join(not_valid_names)))
2718 # Deselect all nodes
2719 for i in node_selected:
2720 i.select = False
2722 # Run through all valid nodes
2723 for node in valid_nodes:
2725 parent = node.parent if node.parent else None
2726 node_loc = [node.location.x, node.location.y]
2728 node_tree = node.id_data
2729 props_to_copy = 'bl_idname name location height width'.split(' ')
2731 reconnections = []
2732 mappings = chain.from_iterable([node.inputs, node.outputs])
2733 for i in (i for i in mappings if i.is_linked):
2734 for L in i.links:
2735 reconnections.append([L.from_socket.path_from_id(), L.to_socket.path_from_id()])
2737 props = {j: getattr(node, j) for j in props_to_copy}
2739 new_node = node_tree.nodes.new(props['bl_idname'])
2740 props_to_copy.pop(0)
2742 for prop in props_to_copy:
2743 setattr(new_node, prop, props[prop])
2745 nodes = node_tree.nodes
2746 nodes.remove(node)
2747 new_node.name = props['name']
2749 if parent:
2750 new_node.parent = parent
2751 new_node.location = node_loc
2753 for str_from, str_to in reconnections:
2754 connect_sockets(eval(str_from), eval(str_to))
2756 new_node.select = False
2757 success_names.append(new_node.name)
2759 # Reselect all nodes
2760 if selected_node_names and node_active_is_frame is False:
2761 for i in selected_node_names:
2762 node_tree.nodes[i].select = True
2764 if active_node_name is not None:
2765 node_tree.nodes[active_node_name].select = True
2766 node_tree.nodes.active = node_tree.nodes[active_node_name]
2768 self.report({'INFO'}, "Successfully reset {}".format(", ".join(success_names)))
2769 return {'FINISHED'}
2772 classes = (
2773 NWLazyMix,
2774 NWLazyConnect,
2775 NWDeleteUnused,
2776 NWSwapLinks,
2777 NWResetBG,
2778 NWAddAttrNode,
2779 NWPreviewNode,
2780 NWFrameSelected,
2781 NWReloadImages,
2782 NWMergeNodes,
2783 NWBatchChangeNodes,
2784 NWChangeMixFactor,
2785 NWCopySettings,
2786 NWCopyLabel,
2787 NWClearLabel,
2788 NWModifyLabels,
2789 NWAddTextureSetup,
2790 NWAddPrincipledSetup,
2791 NWAddReroutes,
2792 NWLinkActiveToSelected,
2793 NWAlignNodes,
2794 NWSelectParentChildren,
2795 NWDetachOutputs,
2796 NWLinkToOutputNode,
2797 NWMakeLink,
2798 NWCallInputsMenu,
2799 NWAddSequence,
2800 NWAddMultipleImages,
2801 NWViewerFocus,
2802 NWSaveViewer,
2803 NWResetNodes,
2807 def register():
2808 from bpy.utils import register_class
2809 for cls in classes:
2810 register_class(cls)
2813 def unregister():
2814 from bpy.utils import unregister_class
2816 for cls in classes:
2817 unregister_class(cls)