Fix #104941: Node Wrangler cannot use both bump and normal
[blender-addons.git] / node_wrangler / operators.py
blob6bd7de7ee02941ebb94c6ad5a79d5314a38521ca
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'
762 if active.outputs[active_node_socket_index].name == "Volume":
763 output_node_socket_index = 1
764 else:
765 output_node_socket_index = 0
767 # If there are no nested node groups, the link starts at the active node
768 node_output = active.outputs[active_node_socket_index]
769 if len(path) > 1:
770 # Recursively connect inside nested node groups and get the one from base level
771 node_output = self.create_links(path, active, active_node_socket_index, socket_type)
772 output_node_input = output_node.inputs[output_node_socket_index]
774 # Connect at base level
775 connect_sockets(node_output, output_node_input)
777 self.cleanup()
778 nodes.active = active
779 active.select = True
780 force_update(context)
781 return {'FINISHED'}
784 class NWFrameSelected(Operator, NWBase):
785 bl_idname = "node.nw_frame_selected"
786 bl_label = "Frame Selected"
787 bl_description = "Add a frame node and parent the selected nodes to it"
788 bl_options = {'REGISTER', 'UNDO'}
790 label_prop: StringProperty(
791 name='Label',
792 description='The visual name of the frame node',
793 default=' '
795 use_custom_color_prop: BoolProperty(
796 name="Custom Color",
797 description="Use custom color for the frame node",
798 default=False
800 color_prop: FloatVectorProperty(
801 name="Color",
802 description="The color of the frame node",
803 default=(0.604, 0.604, 0.604),
804 min=0, max=1, step=1, precision=3,
805 subtype='COLOR_GAMMA', size=3
808 def draw(self, context):
809 layout = self.layout
810 layout.prop(self, 'label_prop')
811 layout.prop(self, 'use_custom_color_prop')
812 col = layout.column()
813 col.active = self.use_custom_color_prop
814 col.prop(self, 'color_prop', text="")
816 def execute(self, context):
817 nodes, links = get_nodes_links(context)
818 selected = []
819 for node in nodes:
820 if node.select:
821 selected.append(node)
823 bpy.ops.node.add_node(type='NodeFrame')
824 frm = nodes.active
825 frm.label = self.label_prop
826 frm.use_custom_color = self.use_custom_color_prop
827 frm.color = self.color_prop
829 for node in selected:
830 node.parent = frm
832 return {'FINISHED'}
835 class NWReloadImages(Operator):
836 bl_idname = "node.nw_reload_images"
837 bl_label = "Reload Images"
838 bl_description = "Update all the image nodes to match their files on disk"
840 @classmethod
841 def poll(cls, context):
842 return (nw_check(context)
843 and nw_check_space_type(cls, context, 'ShaderNodeTree', 'CompositorNodeTree',
844 'TextureNodeTree', 'GeometryNodeTree')
845 and context.active_node is not None
846 and any(is_visible_socket(out) for out in context.active_node.outputs))
848 def execute(self, context):
849 nodes, links = get_nodes_links(context)
850 image_types = ["IMAGE", "TEX_IMAGE", "TEX_ENVIRONMENT", "TEXTURE"]
851 num_reloaded = 0
852 for node in nodes:
853 if node.type in image_types:
854 if node.type == "TEXTURE":
855 if node.texture: # node has texture assigned
856 if node.texture.type in ['IMAGE', 'ENVIRONMENT_MAP']:
857 if node.texture.image: # texture has image assigned
858 node.texture.image.reload()
859 num_reloaded += 1
860 else:
861 if node.image:
862 node.image.reload()
863 num_reloaded += 1
865 if num_reloaded:
866 self.report({'INFO'}, "Reloaded images")
867 print("Reloaded " + str(num_reloaded) + " images")
868 force_update(context)
869 return {'FINISHED'}
870 else:
871 self.report({'WARNING'}, "No images found to reload in this node tree")
872 return {'CANCELLED'}
875 class NWMergeNodes(Operator, NWBase):
876 bl_idname = "node.nw_merge_nodes"
877 bl_label = "Merge Nodes"
878 bl_description = "Merge Selected Nodes"
879 bl_options = {'REGISTER', 'UNDO'}
881 mode: EnumProperty(
882 name="mode",
883 description="All possible blend types, boolean operations and math operations",
884 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],
886 merge_type: EnumProperty(
887 name="merge type",
888 description="Type of Merge to be used",
889 items=(
890 ('AUTO', 'Auto', 'Automatic Output Type Detection'),
891 ('SHADER', 'Shader', 'Merge using ADD or MIX Shader'),
892 ('GEOMETRY', 'Geometry', 'Merge using Mesh Boolean or Join Geometry Node'),
893 ('MIX', 'Mix Node', 'Merge using Mix Nodes'),
894 ('MATH', 'Math Node', 'Merge using Math Nodes'),
895 ('ZCOMBINE', 'Z-Combine Node', 'Merge using Z-Combine Nodes'),
896 ('ALPHAOVER', 'Alpha Over Node', 'Merge using Alpha Over Nodes'),
900 # Check if the link connects to a node that is in selected_nodes
901 # If not, then check recursively for each link in the nodes outputs.
902 # If yes, return True. If the recursion stops without finding a node
903 # in selected_nodes, it returns False. The depth is used to prevent
904 # getting stuck in a loop because of an already present cycle.
905 @staticmethod
906 def link_creates_cycle(link, selected_nodes, depth=0) -> bool:
907 if depth > 255:
908 # We're stuck in a cycle, but that cycle was already present,
909 # so we return False.
910 # NOTE: The number 255 is arbitrary, but seems to work well.
911 return False
912 node = link.to_node
913 if node in selected_nodes:
914 return True
915 if not node.outputs:
916 return False
917 for output in node.outputs:
918 if output.is_linked:
919 for olink in output.links:
920 if NWMergeNodes.link_creates_cycle(olink, selected_nodes, depth + 1):
921 return True
922 # None of the outputs found a node in selected_nodes, so there is no cycle.
923 return False
925 # Merge the nodes in `nodes_list` with a node of type `node_name` that has a multi_input socket.
926 # The parameters `socket_indices` gives the indices of the node sockets in the order that they should
927 # be connected. The last one is assumed to be a multi input socket.
928 # For convenience the node is returned.
929 @staticmethod
930 def merge_with_multi_input(nodes_list, merge_position, do_hide, loc_x, links, nodes, node_name, socket_indices):
931 # The y-location of the last node
932 loc_y = nodes_list[-1][2]
933 if merge_position == 'CENTER':
934 # Average the y-location
935 for i in range(len(nodes_list) - 1):
936 loc_y += nodes_list[i][2]
937 loc_y = loc_y / len(nodes_list)
938 new_node = nodes.new(node_name)
939 new_node.hide = do_hide
940 new_node.location.x = loc_x
941 new_node.location.y = loc_y
942 selected_nodes = [nodes[node_info[0]] for node_info in nodes_list]
943 prev_links = []
944 outputs_for_multi_input = []
945 for i, node in enumerate(selected_nodes):
946 node.select = False
947 # Search for the first node which had output links that do not create
948 # a cycle, which we can then reconnect afterwards.
949 if prev_links == [] and node.outputs[0].is_linked:
950 prev_links = [
951 link for link in node.outputs[0].links if not NWMergeNodes.link_creates_cycle(
952 link, selected_nodes)]
953 # Get the index of the socket, the last one is a multi input, and is thus used repeatedly
954 # To get the placement to look right we need to reverse the order in which we connect the
955 # outputs to the multi input socket.
956 if i < len(socket_indices) - 1:
957 ind = socket_indices[i]
958 connect_sockets(node.outputs[0], new_node.inputs[ind])
959 else:
960 outputs_for_multi_input.insert(0, node.outputs[0])
961 if outputs_for_multi_input != []:
962 ind = socket_indices[-1]
963 for output in outputs_for_multi_input:
964 connect_sockets(output, new_node.inputs[ind])
965 if prev_links != []:
966 for link in prev_links:
967 connect_sockets(new_node.outputs[0], link.to_node.inputs[0])
968 return new_node
970 @classmethod
971 def poll(cls, context):
972 return (nw_check(context)
973 and nw_check_space_type(cls, context, 'ShaderNodeTree', 'CompositorNodeTree',
974 'TextureNodeTree', 'GeometryNodeTree'))
976 def execute(self, context):
977 settings = context.preferences.addons[__package__].preferences
978 merge_hide = settings.merge_hide
979 merge_position = settings.merge_position # 'center' or 'bottom'
981 do_hide = False
982 do_hide_shader = False
983 if merge_hide == 'ALWAYS':
984 do_hide = True
985 do_hide_shader = True
986 elif merge_hide == 'NON_SHADER':
987 do_hide = True
989 tree_type = context.space_data.node_tree.type
990 if tree_type == 'GEOMETRY':
991 node_type = 'GeometryNode'
992 if tree_type == 'COMPOSITING':
993 node_type = 'CompositorNode'
994 elif tree_type == 'SHADER':
995 node_type = 'ShaderNode'
996 elif tree_type == 'TEXTURE':
997 node_type = 'TextureNode'
998 nodes, links = get_nodes_links(context)
999 mode = self.mode
1000 merge_type = self.merge_type
1001 # Prevent trying to add Z-Combine in not 'COMPOSITING' node tree.
1002 # 'ZCOMBINE' works only if mode == 'MIX'
1003 # Setting mode to None prevents trying to add 'ZCOMBINE' node.
1004 if (merge_type == 'ZCOMBINE' or merge_type == 'ALPHAOVER') and tree_type != 'COMPOSITING':
1005 merge_type = 'MIX'
1006 mode = 'MIX'
1007 if (merge_type != 'MATH' and merge_type != 'GEOMETRY') and tree_type == 'GEOMETRY':
1008 merge_type = 'AUTO'
1009 # The Mix node and math nodes used for geometry nodes are of type 'ShaderNode'
1010 if (merge_type == 'MATH' or merge_type == 'MIX') and tree_type == 'GEOMETRY':
1011 node_type = 'ShaderNode'
1012 selected_mix = [] # entry = [index, loc]
1013 selected_shader = [] # entry = [index, loc]
1014 selected_geometry = [] # entry = [index, loc]
1015 selected_math = [] # entry = [index, loc]
1016 selected_vector = [] # entry = [index, loc]
1017 selected_z = [] # entry = [index, loc]
1018 selected_alphaover = [] # entry = [index, loc]
1020 for i, node in enumerate(nodes):
1021 if node.select and node.outputs:
1022 if merge_type == 'AUTO':
1023 for (type, types_list, dst) in (
1024 ('SHADER', ('MIX', 'ADD'), selected_shader),
1025 ('GEOMETRY', [t[0] for t in geo_combine_operations], selected_geometry),
1026 ('RGBA', [t[0] for t in blend_types], selected_mix),
1027 ('VALUE', [t[0] for t in operations], selected_math),
1028 ('VECTOR', [], selected_vector),
1030 output = get_first_enabled_output(node)
1031 output_type = output.type
1032 valid_mode = mode in types_list
1033 # When mode is 'MIX' we have to cheat since the mix node is not used in
1034 # geometry nodes.
1035 if tree_type == 'GEOMETRY':
1036 if mode == 'MIX':
1037 if output_type == 'VALUE' and type == 'VALUE':
1038 valid_mode = True
1039 elif output_type == 'VECTOR' and type == 'VECTOR':
1040 valid_mode = True
1041 elif type == 'GEOMETRY':
1042 valid_mode = True
1043 # When mode is 'MIX' use mix node for both 'RGBA' and 'VALUE' output types.
1044 # Cheat that output type is 'RGBA',
1045 # and that 'MIX' exists in math operations list.
1046 # This way when selected_mix list is analyzed:
1047 # Node data will be appended even though it doesn't meet requirements.
1048 elif output_type != 'SHADER' and mode == 'MIX':
1049 output_type = 'RGBA'
1050 valid_mode = True
1051 if output_type == type and valid_mode:
1052 dst.append([i, node.location.x, node.location.y, node.dimensions.x, node.hide])
1053 else:
1054 for (type, types_list, dst) in (
1055 ('SHADER', ('MIX', 'ADD'), selected_shader),
1056 ('GEOMETRY', [t[0] for t in geo_combine_operations], selected_geometry),
1057 ('MIX', [t[0] for t in blend_types], selected_mix),
1058 ('MATH', [t[0] for t in operations], selected_math),
1059 ('ZCOMBINE', ('MIX', ), selected_z),
1060 ('ALPHAOVER', ('MIX', ), selected_alphaover),
1062 if merge_type == type and mode in types_list:
1063 dst.append([i, node.location.x, node.location.y, node.dimensions.x, node.hide])
1064 # When nodes with output kinds 'RGBA' and 'VALUE' are selected at the same time
1065 # use only 'Mix' nodes for merging.
1066 # For that we add selected_math list to selected_mix list and clear selected_math.
1067 if selected_mix and selected_math and merge_type == 'AUTO':
1068 selected_mix += selected_math
1069 selected_math = []
1071 # If no nodes are selected, do nothing and pass through.
1072 if not (selected_mix + selected_shader + selected_geometry + selected_math
1073 + selected_vector + selected_z + selected_alphaover):
1074 return {'PASS_THROUGH'}
1076 for nodes_list in [
1077 selected_mix,
1078 selected_shader,
1079 selected_geometry,
1080 selected_math,
1081 selected_vector,
1082 selected_z,
1083 selected_alphaover]:
1084 if not nodes_list:
1085 continue
1086 count_before = len(nodes)
1087 # sort list by loc_x - reversed
1088 nodes_list.sort(key=lambda k: k[1], reverse=True)
1089 # get maximum loc_x
1090 loc_x = nodes_list[0][1] + nodes_list[0][3] + 70
1091 nodes_list.sort(key=lambda k: k[2], reverse=True)
1093 # Change the node type for math nodes in a geometry node tree.
1094 if tree_type == 'GEOMETRY':
1095 if nodes_list is selected_math or nodes_list is selected_vector or nodes_list is selected_mix:
1096 node_type = 'ShaderNode'
1097 if mode == 'MIX':
1098 mode = 'ADD'
1099 else:
1100 node_type = 'GeometryNode'
1101 if merge_position == 'CENTER':
1102 # average yloc of last two nodes (lowest two)
1103 loc_y = ((nodes_list[len(nodes_list) - 1][2]) + (nodes_list[len(nodes_list) - 2][2])) / 2
1104 if nodes_list[len(nodes_list) - 1][-1]: # if last node is hidden, mix should be shifted up a bit
1105 if do_hide:
1106 loc_y += 40
1107 else:
1108 loc_y += 80
1109 else:
1110 loc_y = nodes_list[len(nodes_list) - 1][2]
1111 offset_y = 100
1112 if not do_hide:
1113 offset_y = 200
1114 if nodes_list == selected_shader and not do_hide_shader:
1115 offset_y = 150.0
1116 the_range = len(nodes_list) - 1
1117 if len(nodes_list) == 1:
1118 the_range = 1
1119 was_multi = False
1120 for i in range(the_range):
1121 if nodes_list == selected_mix:
1122 mix_name = 'Mix'
1123 if tree_type == 'COMPOSITING':
1124 mix_name = 'MixRGB'
1125 add_type = node_type + mix_name
1126 add = nodes.new(add_type)
1127 if tree_type != 'COMPOSITING':
1128 add.data_type = 'RGBA'
1129 add.blend_type = mode
1130 if mode != 'MIX':
1131 add.inputs[0].default_value = 1.0
1132 add.show_preview = False
1133 add.hide = do_hide
1134 if do_hide:
1135 loc_y = loc_y - 50
1136 first = 6
1137 second = 7
1138 if tree_type == 'COMPOSITING':
1139 first = 1
1140 second = 2
1141 elif nodes_list == selected_math:
1142 add_type = node_type + 'Math'
1143 add = nodes.new(add_type)
1144 add.operation = mode
1145 add.hide = do_hide
1146 if do_hide:
1147 loc_y = loc_y - 50
1148 first = 0
1149 second = 1
1150 elif nodes_list == selected_shader:
1151 if mode == 'MIX':
1152 add_type = node_type + 'MixShader'
1153 add = nodes.new(add_type)
1154 add.hide = do_hide_shader
1155 if do_hide_shader:
1156 loc_y = loc_y - 50
1157 first = 1
1158 second = 2
1159 elif mode == 'ADD':
1160 add_type = node_type + 'AddShader'
1161 add = nodes.new(add_type)
1162 add.hide = do_hide_shader
1163 if do_hide_shader:
1164 loc_y = loc_y - 50
1165 first = 0
1166 second = 1
1167 elif nodes_list == selected_geometry:
1168 if mode in ('JOIN', 'MIX'):
1169 add_type = node_type + 'JoinGeometry'
1170 add = self.merge_with_multi_input(
1171 nodes_list, merge_position, do_hide, loc_x, links, nodes, add_type, [0])
1172 else:
1173 add_type = node_type + 'MeshBoolean'
1174 indices = [0, 1] if mode == 'DIFFERENCE' else [1]
1175 add = self.merge_with_multi_input(
1176 nodes_list, merge_position, do_hide, loc_x, links, nodes, add_type, indices)
1177 add.operation = mode
1178 was_multi = True
1179 break
1180 elif nodes_list == selected_vector:
1181 add_type = node_type + 'VectorMath'
1182 add = nodes.new(add_type)
1183 add.operation = mode
1184 add.hide = do_hide
1185 if do_hide:
1186 loc_y = loc_y - 50
1187 first = 0
1188 second = 1
1189 elif nodes_list == selected_z:
1190 add = nodes.new('CompositorNodeZcombine')
1191 add.show_preview = False
1192 add.hide = do_hide
1193 if do_hide:
1194 loc_y = loc_y - 50
1195 first = 0
1196 second = 2
1197 elif nodes_list == selected_alphaover:
1198 add = nodes.new('CompositorNodeAlphaOver')
1199 add.show_preview = False
1200 add.hide = do_hide
1201 if do_hide:
1202 loc_y = loc_y - 50
1203 first = 1
1204 second = 2
1205 add.location = loc_x, loc_y
1206 loc_y += offset_y
1207 add.select = True
1209 # This has already been handled separately
1210 if was_multi:
1211 continue
1212 count_adds = i + 1
1213 count_after = len(nodes)
1214 index = count_after - 1
1215 first_selected = nodes[nodes_list[0][0]]
1216 # "last" node has been added as first, so its index is count_before.
1217 last_add = nodes[count_before]
1218 # Create list of invalid indexes.
1219 invalid_nodes = [nodes[n[0]]
1220 for n in (selected_mix + selected_math + selected_shader + selected_z + selected_geometry)]
1222 # Special case:
1223 # Two nodes were selected and first selected has no output links, second selected has output links.
1224 # Then add links from last add to all links 'to_socket' of out links of second selected.
1225 first_selected_output = get_first_enabled_output(first_selected)
1226 if len(nodes_list) == 2:
1227 if not first_selected_output.links:
1228 second_selected = nodes[nodes_list[1][0]]
1229 for ss_link in get_first_enabled_output(second_selected).links:
1230 # Prevent cyclic dependencies when nodes to be merged are linked to one another.
1231 # Link only if "to_node" index not in invalid indexes list.
1232 if not self.link_creates_cycle(ss_link, invalid_nodes):
1233 connect_sockets(get_first_enabled_output(last_add), ss_link.to_socket)
1234 # add links from last_add to all links 'to_socket' of out links of first selected.
1235 for fs_link in first_selected_output.links:
1236 # Link only if "to_node" index not in invalid indexes list.
1237 if not self.link_creates_cycle(fs_link, invalid_nodes):
1238 connect_sockets(get_first_enabled_output(last_add), fs_link.to_socket)
1239 # add link from "first" selected and "first" add node
1240 node_to = nodes[count_after - 1]
1241 connect_sockets(first_selected_output, node_to.inputs[first])
1242 if node_to.type == 'ZCOMBINE':
1243 for fs_out in first_selected.outputs:
1244 if fs_out != first_selected_output and fs_out.name in ('Z', 'Depth'):
1245 connect_sockets(fs_out, node_to.inputs[1])
1246 break
1247 # add links between added ADD nodes and between selected and ADD nodes
1248 for i in range(count_adds):
1249 if i < count_adds - 1:
1250 node_from = nodes[index]
1251 node_to = nodes[index - 1]
1252 node_to_input_i = first
1253 node_to_z_i = 1 # if z combine - link z to first z input
1254 connect_sockets(get_first_enabled_output(node_from), node_to.inputs[node_to_input_i])
1255 if node_to.type == 'ZCOMBINE':
1256 for from_out in node_from.outputs:
1257 if from_out != get_first_enabled_output(node_from) and from_out.name in ('Z', 'Depth'):
1258 connect_sockets(from_out, node_to.inputs[node_to_z_i])
1259 if len(nodes_list) > 1:
1260 node_from = nodes[nodes_list[i + 1][0]]
1261 node_to = nodes[index]
1262 node_to_input_i = second
1263 node_to_z_i = 3 # if z combine - link z to second z input
1264 connect_sockets(get_first_enabled_output(node_from), node_to.inputs[node_to_input_i])
1265 if node_to.type == 'ZCOMBINE':
1266 for from_out in node_from.outputs:
1267 if from_out != get_first_enabled_output(node_from) and from_out.name in ('Z', 'Depth'):
1268 connect_sockets(from_out, node_to.inputs[node_to_z_i])
1269 index -= 1
1270 # set "last" of added nodes as active
1271 nodes.active = last_add
1272 for i, x, y, dx, h in nodes_list:
1273 nodes[i].select = False
1275 return {'FINISHED'}
1278 class NWBatchChangeNodes(Operator, NWBase):
1279 bl_idname = "node.nw_batch_change"
1280 bl_label = "Batch Change"
1281 bl_description = "Batch Change Blend Type and Math Operation"
1282 bl_options = {'REGISTER', 'UNDO'}
1284 blend_type: EnumProperty(
1285 name="Blend Type",
1286 items=blend_types + navs,
1288 operation: EnumProperty(
1289 name="Operation",
1290 items=operations + navs,
1293 @classmethod
1294 def poll(cls, context):
1295 return (nw_check(context)
1296 and nw_check_space_type(cls, context, 'ShaderNodeTree', 'CompositorNodeTree',
1297 'TextureNodeTree', 'GeometryNodeTree'))
1299 def execute(self, context):
1300 blend_type = self.blend_type
1301 operation = self.operation
1302 for node in context.selected_nodes:
1303 if node.type == 'MIX_RGB' or (node.bl_idname == 'ShaderNodeMix' and node.data_type == 'RGBA'):
1304 if blend_type not in [nav[0] for nav in navs]:
1305 node.blend_type = blend_type
1306 else:
1307 if blend_type == 'NEXT':
1308 index = [i for i, entry in enumerate(blend_types) if node.blend_type in entry][0]
1309 # index = blend_types.index(node.blend_type)
1310 if index == len(blend_types) - 1:
1311 node.blend_type = blend_types[0][0]
1312 else:
1313 node.blend_type = blend_types[index + 1][0]
1315 if blend_type == 'PREV':
1316 index = [i for i, entry in enumerate(blend_types) if node.blend_type in entry][0]
1317 if index == 0:
1318 node.blend_type = blend_types[len(blend_types) - 1][0]
1319 else:
1320 node.blend_type = blend_types[index - 1][0]
1322 if node.type == 'MATH' or node.bl_idname == 'ShaderNodeMath':
1323 if operation not in [nav[0] for nav in navs]:
1324 node.operation = operation
1325 else:
1326 if operation == 'NEXT':
1327 index = [i for i, entry in enumerate(operations) if node.operation in entry][0]
1328 # index = operations.index(node.operation)
1329 if index == len(operations) - 1:
1330 node.operation = operations[0][0]
1331 else:
1332 node.operation = operations[index + 1][0]
1334 if operation == 'PREV':
1335 index = [i for i, entry in enumerate(operations) if node.operation in entry][0]
1336 # index = operations.index(node.operation)
1337 if index == 0:
1338 node.operation = operations[len(operations) - 1][0]
1339 else:
1340 node.operation = operations[index - 1][0]
1342 return {'FINISHED'}
1345 class NWChangeMixFactor(Operator, NWBase):
1346 bl_idname = "node.nw_factor"
1347 bl_label = "Change Factor"
1348 bl_description = "Change Factors of Mix Nodes and Mix Shader Nodes"
1349 bl_options = {'REGISTER', 'UNDO'}
1351 # option: Change factor.
1352 # If option is 1.0 or 0.0 - set to 1.0 or 0.0
1353 # Else - change factor by option value.
1354 option: FloatProperty()
1356 def execute(self, context):
1357 nodes, links = get_nodes_links(context)
1358 option = self.option
1359 selected = [] # entry = index
1360 for si, node in enumerate(nodes):
1361 if node.select:
1362 if node.type in {'MIX_RGB', 'MIX_SHADER'} or node.bl_idname == 'ShaderNodeMix':
1363 selected.append(si)
1365 for si in selected:
1366 fac = nodes[si].inputs[0]
1367 nodes[si].hide = False
1368 if option in {0.0, 1.0}:
1369 fac.default_value = option
1370 else:
1371 fac.default_value += option
1373 return {'FINISHED'}
1376 class NWCopySettings(Operator, NWBase):
1377 bl_idname = "node.nw_copy_settings"
1378 bl_label = "Copy Settings"
1379 bl_description = "Copy Settings of Active Node to Selected Nodes"
1380 bl_options = {'REGISTER', 'UNDO'}
1382 @classmethod
1383 def poll(cls, context):
1384 return (nw_check(context)
1385 and context.active_node is not None
1386 and context.active_node.type != 'FRAME')
1388 def execute(self, context):
1389 node_active = context.active_node
1390 node_selected = context.selected_nodes
1392 # Error handling
1393 if not (len(node_selected) > 1):
1394 self.report({'ERROR'}, "2 nodes must be selected at least")
1395 return {'CANCELLED'}
1397 # Check if active node is in the selection
1398 selected_node_names = [n.name for n in node_selected]
1399 if node_active.name not in selected_node_names:
1400 self.report({'ERROR'}, "No active node")
1401 return {'CANCELLED'}
1403 # Get nodes in selection by type
1404 valid_nodes = [n for n in node_selected if n.type == node_active.type]
1406 if not (len(valid_nodes) > 1) and node_active:
1407 self.report({'ERROR'}, "Selected nodes are not of the same type as {}".format(node_active.name))
1408 return {'CANCELLED'}
1410 if len(valid_nodes) != len(node_selected):
1411 # Report nodes that are not valid
1412 valid_node_names = [n.name for n in valid_nodes]
1413 not_valid_names = list(set(selected_node_names) - set(valid_node_names))
1414 self.report(
1415 {'INFO'},
1416 "Ignored {} (not of the same type as {})".format(
1417 ", ".join(not_valid_names),
1418 node_active.name))
1420 # Reference original
1421 orig = node_active
1422 # node_selected_names = [n.name for n in node_selected]
1424 # Output list
1425 success_names = []
1427 # Deselect all nodes
1428 for i in node_selected:
1429 i.select = False
1431 # Code by zeffii from http://blender.stackexchange.com/a/42338/3710
1432 # Run through all other nodes
1433 for node in valid_nodes[1:]:
1435 # Check for frame node
1436 parent = node.parent if node.parent else None
1437 node_loc = [node.location.x, node.location.y]
1439 # Select original to duplicate
1440 orig.select = True
1442 # Duplicate selected node
1443 bpy.ops.node.duplicate()
1444 new_node = context.selected_nodes[0]
1446 # Deselect copy
1447 new_node.select = False
1449 # Properties to copy
1450 node_tree = node.id_data
1451 props_to_copy = 'bl_idname name location height width'.split(' ')
1453 # Input and outputs
1454 reconnections = []
1455 mappings = chain.from_iterable([node.inputs, node.outputs])
1456 for i in (i for i in mappings if i.is_linked):
1457 for L in i.links:
1458 reconnections.append([L.from_socket.path_from_id(), L.to_socket.path_from_id()])
1460 # Properties
1461 props = {j: getattr(node, j) for j in props_to_copy}
1462 props_to_copy.pop(0)
1464 for prop in props_to_copy:
1465 setattr(new_node, prop, props[prop])
1467 # Get the node tree to remove the old node
1468 nodes = node_tree.nodes
1469 nodes.remove(node)
1470 new_node.name = props['name']
1472 if parent:
1473 new_node.parent = parent
1474 new_node.location = node_loc
1476 for str_from, str_to in reconnections:
1477 node_tree.connect_sockets(eval(str_from), eval(str_to))
1479 success_names.append(new_node.name)
1481 orig.select = True
1482 node_tree.nodes.active = orig
1483 self.report(
1484 {'INFO'},
1485 "Successfully copied attributes from {} to: {}".format(
1486 orig.name,
1487 ", ".join(success_names)))
1488 return {'FINISHED'}
1491 class NWCopyLabel(Operator, NWBase):
1492 bl_idname = "node.nw_copy_label"
1493 bl_label = "Copy Label"
1494 bl_options = {'REGISTER', 'UNDO'}
1496 option: EnumProperty(
1497 name="option",
1498 description="Source of name of label",
1499 items=(
1500 ('FROM_ACTIVE', 'from active', 'from active node',),
1501 ('FROM_NODE', 'from node', 'from node linked to selected node'),
1502 ('FROM_SOCKET', 'from socket', 'from socket linked to selected node'),
1506 def execute(self, context):
1507 nodes, links = get_nodes_links(context)
1508 option = self.option
1509 active = nodes.active
1510 if option == 'FROM_ACTIVE':
1511 if active:
1512 src_label = active.label
1513 for node in [n for n in nodes if n.select and nodes.active != n]:
1514 node.label = src_label
1515 elif option == 'FROM_NODE':
1516 selected = [n for n in nodes if n.select]
1517 for node in selected:
1518 for input in node.inputs:
1519 if input.links:
1520 src = input.links[0].from_node
1521 node.label = src.label
1522 break
1523 elif option == 'FROM_SOCKET':
1524 selected = [n for n in nodes if n.select]
1525 for node in selected:
1526 for input in node.inputs:
1527 if input.links:
1528 src = input.links[0].from_socket
1529 node.label = src.name
1530 break
1532 return {'FINISHED'}
1535 class NWClearLabel(Operator, NWBase):
1536 bl_idname = "node.nw_clear_label"
1537 bl_label = "Clear Label"
1538 bl_options = {'REGISTER', 'UNDO'}
1540 option: BoolProperty()
1542 def execute(self, context):
1543 nodes, links = get_nodes_links(context)
1544 for node in [n for n in nodes if n.select]:
1545 node.label = ''
1547 return {'FINISHED'}
1549 def invoke(self, context, event):
1550 if self.option:
1551 return self.execute(context)
1552 else:
1553 return context.window_manager.invoke_confirm(self, event)
1556 class NWModifyLabels(Operator, NWBase):
1557 """Modify Labels of all selected nodes"""
1558 bl_idname = "node.nw_modify_labels"
1559 bl_label = "Modify Labels"
1560 bl_options = {'REGISTER', 'UNDO'}
1562 prepend: StringProperty(
1563 name="Add to Beginning"
1565 append: StringProperty(
1566 name="Add to End"
1568 replace_from: StringProperty(
1569 name="Text to Replace"
1571 replace_to: StringProperty(
1572 name="Replace with"
1575 def execute(self, context):
1576 nodes, links = get_nodes_links(context)
1577 for node in [n for n in nodes if n.select]:
1578 node.label = self.prepend + node.label.replace(self.replace_from, self.replace_to) + self.append
1580 return {'FINISHED'}
1582 def invoke(self, context, event):
1583 self.prepend = ""
1584 self.append = ""
1585 self.remove = ""
1586 return context.window_manager.invoke_props_dialog(self)
1589 class NWAddTextureSetup(Operator, NWBase):
1590 bl_idname = "node.nw_add_texture"
1591 bl_label = "Texture Setup"
1592 bl_description = "Add Texture Node Setup to Selected Shaders"
1593 bl_options = {'REGISTER', 'UNDO'}
1595 add_mapping: BoolProperty(
1596 name="Add Mapping Nodes",
1597 description="Create coordinate and mapping nodes for the texture (ignored for selected texture nodes)",
1598 default=True)
1600 @classmethod
1601 def poll(cls, context):
1602 return (nw_check(context)
1603 and nw_check_space_type(cls, context, 'ShaderNodeTree'))
1605 def execute(self, context):
1606 nodes, links = get_nodes_links(context)
1608 texture_types = get_texture_node_types()
1609 selected_nodes = [n for n in nodes if n.select]
1611 for node in selected_nodes:
1612 if not node.inputs:
1613 continue
1615 input_index = 0
1616 target_input = node.inputs[0]
1617 for input in node.inputs:
1618 if input.enabled:
1619 input_index += 1
1620 if not input.is_linked:
1621 target_input = input
1622 break
1623 else:
1624 self.report({'WARNING'}, "No free inputs for node: " + node.name)
1625 continue
1627 x_offset = 0
1628 padding = 40.0
1629 locx = node.location.x
1630 locy = node.location.y - (input_index * padding)
1632 is_texture_node = node.rna_type.identifier in texture_types
1633 use_environment_texture = node.type == 'BACKGROUND'
1635 # Add an image texture before normal shader nodes.
1636 if not is_texture_node:
1637 image_texture_type = 'ShaderNodeTexEnvironment' if use_environment_texture else 'ShaderNodeTexImage'
1638 image_texture_node = nodes.new(image_texture_type)
1639 x_offset = x_offset + image_texture_node.width + padding
1640 image_texture_node.location = [locx - x_offset, locy]
1641 nodes.active = image_texture_node
1642 connect_sockets(image_texture_node.outputs[0], target_input)
1644 # The mapping setup following this will connect to the first input of this image texture.
1645 target_input = image_texture_node.inputs[0]
1647 node.select = False
1649 if is_texture_node or self.add_mapping:
1650 # Add Mapping node.
1651 mapping_node = nodes.new('ShaderNodeMapping')
1652 x_offset = x_offset + mapping_node.width + padding
1653 mapping_node.location = [locx - x_offset, locy]
1654 connect_sockets(mapping_node.outputs[0], target_input)
1656 # Add Texture Coordinates node.
1657 tex_coord_node = nodes.new('ShaderNodeTexCoord')
1658 x_offset = x_offset + tex_coord_node.width + padding
1659 tex_coord_node.location = [locx - x_offset, locy]
1661 is_procedural_texture = is_texture_node and node.type != 'TEX_IMAGE'
1662 use_generated_coordinates = is_procedural_texture or use_environment_texture
1663 tex_coord_output = tex_coord_node.outputs[0 if use_generated_coordinates else 2]
1664 connect_sockets(tex_coord_output, mapping_node.inputs[0])
1666 return {'FINISHED'}
1669 class NWAddPrincipledSetup(Operator, NWBase, ImportHelper):
1670 bl_idname = "node.nw_add_textures_for_principled"
1671 bl_label = "Principled Texture Setup"
1672 bl_description = "Add Texture Node Setup for Principled BSDF"
1673 bl_options = {'REGISTER', 'UNDO'}
1675 directory: StringProperty(
1676 name='Directory',
1677 subtype='DIR_PATH',
1678 default='',
1679 description='Folder to search in for image files'
1681 files: CollectionProperty(
1682 type=bpy.types.OperatorFileListElement,
1683 options={'HIDDEN', 'SKIP_SAVE'}
1686 relative_path: BoolProperty(
1687 name='Relative Path',
1688 description='Set the file path relative to the blend file, when possible',
1689 default=True
1692 order = [
1693 "filepath",
1694 "files",
1697 def draw(self, context):
1698 layout = self.layout
1699 layout.alignment = 'LEFT'
1701 layout.prop(self, 'relative_path')
1703 @classmethod
1704 def poll(cls, context):
1705 return (nw_check(context)
1706 and nw_check_space_type(cls, context, 'ShaderNodeTree'))
1708 def execute(self, context):
1709 # Check if everything is ok
1710 if not self.directory:
1711 self.report({'INFO'}, 'No Folder Selected')
1712 return {'CANCELLED'}
1713 if not self.files[:]:
1714 self.report({'INFO'}, 'No Files Selected')
1715 return {'CANCELLED'}
1717 nodes, links = get_nodes_links(context)
1718 active_node = nodes.active
1719 if not (active_node and active_node.bl_idname == 'ShaderNodeBsdfPrincipled'):
1720 self.report({'INFO'}, 'Select Principled BSDF')
1721 return {'CANCELLED'}
1723 # Filter textures names for texturetypes in filenames
1724 # [Socket Name, [abbreviations and keyword list], Filename placeholder]
1725 tags = context.preferences.addons[__package__].preferences.principled_tags
1726 normal_abbr = tags.normal.split(' ')
1727 bump_abbr = tags.bump.split(' ')
1728 gloss_abbr = tags.gloss.split(' ')
1729 rough_abbr = tags.rough.split(' ')
1730 socketnames = [
1731 ['Displacement', tags.displacement.split(' '), None],
1732 ['Base Color', tags.base_color.split(' '), None],
1733 ['Metallic', tags.metallic.split(' '), None],
1734 ['Specular IOR Level', tags.specular.split(' '), None],
1735 ['Roughness', rough_abbr + gloss_abbr, None],
1736 ['Bump', bump_abbr, None],
1737 ['Normal', normal_abbr, None],
1738 ['Transmission Weight', tags.transmission.split(' '), None],
1739 ['Emission Color', tags.emission.split(' '), None],
1740 ['Alpha', tags.alpha.split(' '), None],
1741 ['Ambient Occlusion', tags.ambient_occlusion.split(' '), None],
1744 match_files_to_socket_names(self.files, socketnames)
1745 # Remove socketnames without found files
1746 socketnames = [s for s in socketnames if s[2]
1747 and path.exists(self.directory + s[2])]
1748 if not socketnames:
1749 self.report({'INFO'}, 'No matching images found')
1750 print('No matching images found')
1751 return {'CANCELLED'}
1753 # Don't override path earlier as os.path is used to check the absolute path
1754 import_path = self.directory
1755 if self.relative_path:
1756 if bpy.data.filepath:
1757 try:
1758 import_path = bpy.path.relpath(self.directory)
1759 except ValueError:
1760 pass
1762 # Add found images
1763 print('\nMatched Textures:')
1764 texture_nodes = []
1765 disp_texture = None
1766 ao_texture = None
1767 normal_node = None
1768 normal_node_texture = None
1769 bump_node = None
1770 bump_node_texture = None
1771 roughness_node = None
1772 for i, sname in enumerate(socketnames):
1773 print(i, sname[0], sname[2])
1775 # DISPLACEMENT NODES
1776 if sname[0] == 'Displacement':
1777 disp_texture = nodes.new(type='ShaderNodeTexImage')
1778 img = bpy.data.images.load(path.join(import_path, sname[2]))
1779 disp_texture.image = img
1780 disp_texture.label = 'Displacement'
1781 if disp_texture.image:
1782 disp_texture.image.colorspace_settings.is_data = True
1784 # Add displacement offset nodes
1785 disp_node = nodes.new(type='ShaderNodeDisplacement')
1786 # Align the Displacement node under the active Principled BSDF node
1787 disp_node.location = active_node.location + Vector((100, -700))
1788 link = connect_sockets(disp_node.inputs[0], disp_texture.outputs[0])
1790 # TODO Turn on true displacement in the material
1791 # Too complicated for now
1793 # Find output node
1794 output_node = [n for n in nodes if n.bl_idname == 'ShaderNodeOutputMaterial']
1795 if output_node:
1796 if not output_node[0].inputs[2].is_linked:
1797 link = connect_sockets(output_node[0].inputs[2], disp_node.outputs[0])
1799 continue
1801 # BUMP NODES
1802 elif sname[0] == 'Bump':
1803 # Test if new texture node is bump map
1804 fname_components = split_into_components(sname[2])
1805 match_bump = set(bump_abbr).intersection(set(fname_components))
1806 if match_bump:
1807 # If Bump add bump node in between
1808 bump_node_texture = nodes.new(type='ShaderNodeTexImage')
1809 img = bpy.data.images.load(path.join(import_path, sname[2]))
1810 bump_node_texture.image = img
1811 bump_node_texture.label = 'Bump'
1813 # Add bump node
1814 bump_node = nodes.new(type='ShaderNodeBump')
1815 link = connect_sockets(bump_node.inputs[2], bump_node_texture.outputs[0])
1816 link = connect_sockets(active_node.inputs['Normal'], bump_node.outputs[0])
1817 continue
1819 # NORMAL NODES
1820 elif sname[0] == 'Normal':
1821 # Test if new texture node is normal map
1822 fname_components = split_into_components(sname[2])
1823 match_normal = set(normal_abbr).intersection(set(fname_components))
1824 if match_normal:
1825 # If Normal add normal node in between
1826 normal_node_texture = nodes.new(type='ShaderNodeTexImage')
1827 img = bpy.data.images.load(path.join(import_path, sname[2]))
1828 normal_node_texture.image = img
1829 normal_node_texture.label = 'Normal'
1831 # Add normal node
1832 normal_node = nodes.new(type='ShaderNodeNormalMap')
1833 link = connect_sockets(normal_node.inputs[1], normal_node_texture.outputs[0])
1834 # Connect to bump node if it was created before, otherwise to the BSDF
1835 if bump_node is None:
1836 link = connect_sockets(active_node.inputs[sname[0]], normal_node.outputs[0])
1837 else:
1838 link = connect_sockets(bump_node.inputs[sname[0]], normal_node.outputs[sname[0]])
1839 continue
1841 # AMBIENT OCCLUSION TEXTURE
1842 elif sname[0] == 'Ambient Occlusion':
1843 ao_texture = nodes.new(type='ShaderNodeTexImage')
1844 img = bpy.data.images.load(path.join(import_path, sname[2]))
1845 ao_texture.image = img
1846 ao_texture.label = sname[0]
1847 if ao_texture.image:
1848 ao_texture.image.colorspace_settings.is_data = True
1850 continue
1852 if not active_node.inputs[sname[0]].is_linked:
1853 # No texture node connected -> add texture node with new image
1854 texture_node = nodes.new(type='ShaderNodeTexImage')
1855 img = bpy.data.images.load(path.join(import_path, sname[2]))
1856 texture_node.image = img
1858 if sname[0] == 'Roughness':
1859 # Test if glossy or roughness map
1860 fname_components = split_into_components(sname[2])
1861 match_rough = set(rough_abbr).intersection(set(fname_components))
1862 match_gloss = set(gloss_abbr).intersection(set(fname_components))
1864 if match_rough:
1865 # If Roughness nothing to to
1866 link = connect_sockets(active_node.inputs[sname[0]], texture_node.outputs[0])
1868 elif match_gloss:
1869 # If Gloss Map add invert node
1870 invert_node = nodes.new(type='ShaderNodeInvert')
1871 link = connect_sockets(invert_node.inputs[1], texture_node.outputs[0])
1873 link = connect_sockets(active_node.inputs[sname[0]], invert_node.outputs[0])
1874 roughness_node = texture_node
1876 else:
1877 # This is a simple connection Texture --> Input slot
1878 link = connect_sockets(active_node.inputs[sname[0]], texture_node.outputs[0])
1880 # Use non-color except for color inputs
1881 if sname[0] not in ['Base Color', 'Emission Color'] and texture_node.image:
1882 texture_node.image.colorspace_settings.is_data = True
1884 else:
1885 # If already texture connected. add to node list for alignment
1886 texture_node = active_node.inputs[sname[0]].links[0].from_node
1888 # This are all connected texture nodes
1889 texture_nodes.append(texture_node)
1890 texture_node.label = sname[0]
1892 if disp_texture:
1893 texture_nodes.append(disp_texture)
1894 if bump_node_texture:
1895 texture_nodes.append(bump_node_texture)
1896 if normal_node_texture:
1897 texture_nodes.append(normal_node_texture)
1899 if ao_texture:
1900 # We want the ambient occlusion texture to be the top most texture node
1901 texture_nodes.insert(0, ao_texture)
1903 # Alignment
1904 for i, texture_node in enumerate(texture_nodes):
1905 offset = Vector((-550, (i * -280) + 200))
1906 texture_node.location = active_node.location + offset
1908 if normal_node:
1909 # Extra alignment if normal node was added
1910 normal_node.location = normal_node_texture.location + Vector((300, 0))
1912 if bump_node:
1913 # Extra alignment if bump node was added
1914 bump_node.location = bump_node_texture.location + Vector((300, 0))
1916 if roughness_node:
1917 # Alignment of invert node if glossy map
1918 invert_node.location = roughness_node.location + Vector((300, 0))
1920 # Add texture input + mapping
1921 mapping = nodes.new(type='ShaderNodeMapping')
1922 mapping.location = active_node.location + Vector((-1050, 0))
1923 if len(texture_nodes) > 1:
1924 # If more than one texture add reroute node in between
1925 reroute = nodes.new(type='NodeReroute')
1926 texture_nodes.append(reroute)
1927 tex_coords = Vector((texture_nodes[0].location.x,
1928 sum(n.location.y for n in texture_nodes) / len(texture_nodes)))
1929 reroute.location = tex_coords + Vector((-50, -120))
1930 for texture_node in texture_nodes:
1931 link = connect_sockets(texture_node.inputs[0], reroute.outputs[0])
1932 link = connect_sockets(reroute.inputs[0], mapping.outputs[0])
1933 else:
1934 link = connect_sockets(texture_nodes[0].inputs[0], mapping.outputs[0])
1936 # Connect texture_coordinates to mapping node
1937 texture_input = nodes.new(type='ShaderNodeTexCoord')
1938 texture_input.location = mapping.location + Vector((-200, 0))
1939 link = connect_sockets(mapping.inputs[0], texture_input.outputs[2])
1941 # Create frame around tex coords and mapping
1942 frame = nodes.new(type='NodeFrame')
1943 frame.label = 'Mapping'
1944 mapping.parent = frame
1945 texture_input.parent = frame
1946 frame.update()
1948 # Create frame around texture nodes
1949 frame = nodes.new(type='NodeFrame')
1950 frame.label = 'Textures'
1951 for tnode in texture_nodes:
1952 tnode.parent = frame
1953 frame.update()
1955 # Just to be sure
1956 active_node.select = False
1957 nodes.update()
1958 links.update()
1959 force_update(context)
1960 return {'FINISHED'}
1963 class NWAddReroutes(Operator, NWBase):
1964 """Add Reroute Nodes and link them to outputs of selected nodes"""
1965 bl_idname = "node.nw_add_reroutes"
1966 bl_label = "Add Reroutes"
1967 bl_description = "Add Reroutes to Outputs"
1968 bl_options = {'REGISTER', 'UNDO'}
1970 option: EnumProperty(
1971 name="option",
1972 items=[
1973 ('ALL', 'to all', 'Add to all outputs'),
1974 ('LOOSE', 'to loose', 'Add only to loose outputs'),
1975 ('LINKED', 'to linked', 'Add only to linked outputs'),
1979 def execute(self, context):
1980 tree_type = context.space_data.node_tree.type
1981 option = self.option
1982 nodes, links = get_nodes_links(context)
1983 # output valid when option is 'all' or when 'loose' output has no links
1984 valid = False
1985 post_select = [] # nodes to be selected after execution
1986 # create reroutes and recreate links
1987 for node in [n for n in nodes if n.select]:
1988 if node.outputs:
1989 x = node.location.x
1990 y = node.location.y
1991 width = node.width
1992 # unhide 'REROUTE' nodes to avoid issues with location.y
1993 if node.type == 'REROUTE':
1994 node.hide = False
1995 # Hack needed to calculate real width
1996 if node.hide:
1997 bpy.ops.node.select_all(action='DESELECT')
1998 helper = nodes.new('NodeReroute')
1999 helper.select = True
2000 node.select = True
2001 # resize node and helper to zero. Then check locations to calculate width
2002 bpy.ops.transform.resize(value=(0.0, 0.0, 0.0))
2003 width = 2.0 * (helper.location.x - node.location.x)
2004 # restore node location
2005 node.location = x, y
2006 # delete helper
2007 node.select = False
2008 # only helper is selected now
2009 bpy.ops.node.delete()
2010 x = node.location.x + width + 20.0
2011 if node.type != 'REROUTE':
2012 y -= 35.0
2013 y_offset = -22.0
2014 loc = x, y
2015 reroutes_count = 0 # will be used when aligning reroutes added to hidden nodes
2016 for out_i, output in enumerate(node.outputs):
2017 pass_used = False # initial value to be analyzed if 'R_LAYERS'
2018 # if node != 'R_LAYERS' - "pass_used" not needed, so set it to True
2019 if node.type != 'R_LAYERS':
2020 pass_used = True
2021 else: # if 'R_LAYERS' check if output represent used render pass
2022 node_scene = node.scene
2023 node_layer = node.layer
2024 # If output - "Alpha" is analyzed - assume it's used. Not represented in passes.
2025 if output.name == 'Alpha':
2026 pass_used = True
2027 else:
2028 # check entries in global 'rl_outputs' variable
2029 for rlo in rl_outputs:
2030 if output.name in {rlo.output_name, rlo.exr_output_name}:
2031 pass_used = getattr(node_scene.view_layers[node_layer], rlo.render_pass)
2032 break
2033 if pass_used:
2034 valid = ((option == 'ALL') or
2035 (option == 'LOOSE' and not output.links) or
2036 (option == 'LINKED' and output.links))
2037 # Add reroutes only if valid, but offset location in all cases.
2038 if valid:
2039 n = nodes.new('NodeReroute')
2040 nodes.active = n
2041 for link in output.links:
2042 connect_sockets(n.outputs[0], link.to_socket)
2043 connect_sockets(output, n.inputs[0])
2044 n.location = loc
2045 post_select.append(n)
2046 reroutes_count += 1
2047 y += y_offset
2048 loc = x, y
2049 # disselect the node so that after execution of script only newly created nodes are selected
2050 node.select = False
2051 # nicer reroutes distribution along y when node.hide
2052 if node.hide:
2053 y_translate = reroutes_count * y_offset / 2.0 - y_offset - 35.0
2054 for reroute in [r for r in nodes if r.select]:
2055 reroute.location.y -= y_translate
2056 for node in post_select:
2057 node.select = True
2059 return {'FINISHED'}
2062 class NWLinkActiveToSelected(Operator, NWBase):
2063 """Link active node to selected nodes basing on various criteria"""
2064 bl_idname = "node.nw_link_active_to_selected"
2065 bl_label = "Link Active Node to Selected"
2066 bl_options = {'REGISTER', 'UNDO'}
2068 replace: BoolProperty()
2069 use_node_name: BoolProperty()
2070 use_outputs_names: BoolProperty()
2072 @classmethod
2073 def poll(cls, context):
2074 return (nw_check(context)
2075 and context.active_node is not None
2076 and context.active_node.select)
2078 def execute(self, context):
2079 nodes, links = get_nodes_links(context)
2080 replace = self.replace
2081 use_node_name = self.use_node_name
2082 use_outputs_names = self.use_outputs_names
2083 active = nodes.active
2084 selected = [node for node in nodes if node.select and node != active]
2085 outputs = [] # Only usable outputs of active nodes will be stored here.
2086 for out in active.outputs:
2087 if active.type != 'R_LAYERS':
2088 outputs.append(out)
2089 else:
2090 # 'R_LAYERS' node type needs special handling.
2091 # outputs of 'R_LAYERS' are callable even if not seen in UI.
2092 # Only outputs that represent used passes should be taken into account
2093 # Check if pass represented by output is used.
2094 # global 'rl_outputs' list will be used for that
2095 for rlo in rl_outputs:
2096 pass_used = False # initial value. Will be set to True if pass is used
2097 if out.name == 'Alpha':
2098 # Alpha output is always present. Doesn't have representation in render pass. Assume it's used.
2099 pass_used = True
2100 elif out.name in {rlo.output_name, rlo.exr_output_name}:
2101 # example 'render_pass' entry: 'use_pass_uv' Check if True in scene render layers
2102 pass_used = getattr(active.scene.view_layers[active.layer], rlo.render_pass)
2103 break
2104 if pass_used:
2105 outputs.append(out)
2106 doit = True # Will be changed to False when links successfully added to previous output.
2107 for out in outputs:
2108 if doit:
2109 for node in selected:
2110 dst_name = node.name # Will be compared with src_name if needed.
2111 # When node has label - use it as dst_name
2112 if node.label:
2113 dst_name = node.label
2114 valid = True # Initial value. Will be changed to False if names don't match.
2115 src_name = dst_name # If names not used - this assignment will keep valid = True.
2116 if use_node_name:
2117 # Set src_name to source node name or label
2118 src_name = active.name
2119 if active.label:
2120 src_name = active.label
2121 elif use_outputs_names:
2122 src_name = (out.name, )
2123 for rlo in rl_outputs:
2124 if out.name in {rlo.output_name, rlo.exr_output_name}:
2125 src_name = (rlo.output_name, rlo.exr_output_name)
2126 if dst_name not in src_name:
2127 valid = False
2128 if valid:
2129 for input in node.inputs:
2130 if input.type == out.type or node.type == 'REROUTE':
2131 if replace or not input.is_linked:
2132 connect_sockets(out, input)
2133 if not use_node_name and not use_outputs_names:
2134 doit = False
2135 break
2137 return {'FINISHED'}
2140 class NWAlignNodes(Operator, NWBase):
2141 '''Align the selected nodes neatly in a row/column'''
2142 bl_idname = "node.nw_align_nodes"
2143 bl_label = "Align Nodes"
2144 bl_options = {'REGISTER', 'UNDO'}
2145 margin: IntProperty(name='Margin', default=50, description='The amount of space between nodes')
2147 def execute(self, context):
2148 nodes, links = get_nodes_links(context)
2149 margin = self.margin
2151 selection = []
2152 for node in nodes:
2153 if node.select and node.type != 'FRAME':
2154 selection.append(node)
2156 # If no nodes are selected, align all nodes
2157 active_loc = None
2158 if not selection:
2159 selection = nodes
2160 elif nodes.active in selection:
2161 active_loc = copy(nodes.active.location) # make a copy, not a reference
2163 # Check if nodes should be laid out horizontally or vertically
2164 # use dimension to get center of node, not corner
2165 x_locs = [n.location.x + (n.dimensions.x / 2) for n in selection]
2166 y_locs = [n.location.y - (n.dimensions.y / 2) for n in selection]
2167 x_range = max(x_locs) - min(x_locs)
2168 y_range = max(y_locs) - min(y_locs)
2169 mid_x = (max(x_locs) + min(x_locs)) / 2
2170 mid_y = (max(y_locs) + min(y_locs)) / 2
2171 horizontal = x_range > y_range
2173 # Sort selection by location of node mid-point
2174 if horizontal:
2175 selection = sorted(selection, key=lambda n: n.location.x + (n.dimensions.x / 2))
2176 else:
2177 selection = sorted(selection, key=lambda n: n.location.y - (n.dimensions.y / 2), reverse=True)
2179 # Alignment
2180 current_pos = 0
2181 for node in selection:
2182 current_margin = margin
2183 current_margin = current_margin * 0.5 if node.hide else current_margin # use a smaller margin for hidden nodes
2185 if horizontal:
2186 node.location.x = current_pos
2187 current_pos += current_margin + node.dimensions.x
2188 node.location.y = mid_y + (node.dimensions.y / 2)
2189 else:
2190 node.location.y = current_pos
2191 current_pos -= (current_margin * 0.3) + node.dimensions.y # use half-margin for vertical alignment
2192 node.location.x = mid_x - (node.dimensions.x / 2)
2194 # If active node is selected, center nodes around it
2195 if active_loc is not None:
2196 active_loc_diff = active_loc - nodes.active.location
2197 for node in selection:
2198 node.location += active_loc_diff
2199 else: # Position nodes centered around where they used to be
2200 locs = ([n.location.x + (n.dimensions.x / 2) for n in selection]
2201 ) if horizontal else ([n.location.y - (n.dimensions.y / 2) for n in selection])
2202 new_mid = (max(locs) + min(locs)) / 2
2203 for node in selection:
2204 if horizontal:
2205 node.location.x += (mid_x - new_mid)
2206 else:
2207 node.location.y += (mid_y - new_mid)
2209 return {'FINISHED'}
2212 class NWSelectParentChildren(Operator, NWBase):
2213 bl_idname = "node.nw_select_parent_child"
2214 bl_label = "Select Parent or Children"
2215 bl_options = {'REGISTER', 'UNDO'}
2217 option: EnumProperty(
2218 name="option",
2219 items=(
2220 ('PARENT', 'Select Parent', 'Select Parent Frame'),
2221 ('CHILD', 'Select Children', 'Select members of selected frame'),
2225 def execute(self, context):
2226 nodes, links = get_nodes_links(context)
2227 option = self.option
2228 selected = [node for node in nodes if node.select]
2229 if option == 'PARENT':
2230 for sel in selected:
2231 parent = sel.parent
2232 if parent:
2233 parent.select = True
2234 else: # option == 'CHILD'
2235 for sel in selected:
2236 children = [node for node in nodes if node.parent == sel]
2237 for kid in children:
2238 kid.select = True
2240 return {'FINISHED'}
2243 class NWDetachOutputs(Operator, NWBase):
2244 """Detach outputs of selected node leaving inputs linked"""
2245 bl_idname = "node.nw_detach_outputs"
2246 bl_label = "Detach Outputs"
2247 bl_options = {'REGISTER', 'UNDO'}
2249 def execute(self, context):
2250 nodes, links = get_nodes_links(context)
2251 selected = context.selected_nodes
2252 bpy.ops.node.duplicate_move_keep_inputs()
2253 new_nodes = context.selected_nodes
2254 bpy.ops.node.select_all(action="DESELECT")
2255 for node in selected:
2256 node.select = True
2257 bpy.ops.node.delete_reconnect()
2258 for new_node in new_nodes:
2259 new_node.select = True
2260 bpy.ops.transform.translate('INVOKE_DEFAULT')
2262 return {'FINISHED'}
2265 class NWLinkToOutputNode(Operator):
2266 """Link to Composite node or Material Output node"""
2267 bl_idname = "node.nw_link_out"
2268 bl_label = "Connect to Output"
2269 bl_options = {'REGISTER', 'UNDO'}
2271 @classmethod
2272 def poll(cls, context):
2273 """Disabled for custom nodes as we do not know which nodes are outputs."""
2274 return (nw_check(context)
2275 and nw_check_space_type(cls, context, 'ShaderNodeTree', 'CompositorNodeTree',
2276 'TextureNodeTree', 'GeometryNodeTree')
2277 and context.active_node is not None
2278 and any(is_visible_socket(out) for out in context.active_node.outputs))
2280 def execute(self, context):
2281 nodes, links = get_nodes_links(context)
2282 active = nodes.active
2283 output_index = None
2284 tree_type = context.space_data.tree_type
2285 shader_outputs = {'OBJECT': 'ShaderNodeOutputMaterial',
2286 'WORLD': 'ShaderNodeOutputWorld',
2287 'LINESTYLE': 'ShaderNodeOutputLineStyle'}
2288 output_type = {
2289 'ShaderNodeTree': shader_outputs[context.space_data.shader_type],
2290 'CompositorNodeTree': 'CompositorNodeComposite',
2291 'TextureNodeTree': 'TextureNodeOutput',
2292 'GeometryNodeTree': 'NodeGroupOutput',
2293 }[tree_type]
2294 for node in nodes:
2295 # check whether the node is an output node and,
2296 # if supported, whether it's the active one
2297 if node.rna_type.identifier == output_type \
2298 and (node.is_active_output if hasattr(node, 'is_active_output')
2299 else True):
2300 output_node = node
2301 break
2302 else: # No output node exists
2303 bpy.ops.node.select_all(action="DESELECT")
2304 output_node = nodes.new(output_type)
2305 output_node.location.x = active.location.x + active.dimensions.x + 80
2306 output_node.location.y = active.location.y
2308 if active.outputs:
2309 for i, output in enumerate(active.outputs):
2310 if is_visible_socket(output):
2311 output_index = i
2312 break
2313 for i, output in enumerate(active.outputs):
2314 if output.type == output_node.inputs[0].type and is_visible_socket(output):
2315 output_index = i
2316 break
2318 out_input_index = 0
2319 if tree_type == 'ShaderNodeTree':
2320 if active.outputs[output_index].name == 'Volume':
2321 out_input_index = 1
2322 elif active.outputs[output_index].name == 'Displacement':
2323 out_input_index = 2
2324 elif tree_type == 'GeometryNodeTree':
2325 if active.outputs[output_index].type != 'GEOMETRY':
2326 return {'CANCELLED'}
2327 connect_sockets(active.outputs[output_index], output_node.inputs[out_input_index])
2329 force_update(context) # viewport render does not update
2331 return {'FINISHED'}
2334 class NWMakeLink(Operator, NWBase):
2335 """Make a link from one socket to another"""
2336 bl_idname = 'node.nw_make_link'
2337 bl_label = 'Make Link'
2338 bl_options = {'REGISTER', 'UNDO'}
2339 from_socket: IntProperty()
2340 to_socket: IntProperty()
2342 def execute(self, context):
2343 nodes, links = get_nodes_links(context)
2345 n1 = nodes[context.scene.NWLazySource]
2346 n2 = nodes[context.scene.NWLazyTarget]
2348 connect_sockets(n1.outputs[self.from_socket], n2.inputs[self.to_socket])
2350 force_update(context)
2352 return {'FINISHED'}
2355 class NWCallInputsMenu(Operator, NWBase):
2356 """Link from this output"""
2357 bl_idname = 'node.nw_call_inputs_menu'
2358 bl_label = 'Make Link'
2359 bl_options = {'REGISTER', 'UNDO'}
2360 from_socket: IntProperty()
2362 def execute(self, context):
2363 nodes, links = get_nodes_links(context)
2365 context.scene.NWSourceSocket = self.from_socket
2367 n1 = nodes[context.scene.NWLazySource]
2368 n2 = nodes[context.scene.NWLazyTarget]
2369 if len(n2.inputs) > 1:
2370 bpy.ops.wm.call_menu("INVOKE_DEFAULT", name=NWConnectionListInputs.bl_idname)
2371 elif len(n2.inputs) == 1:
2372 connect_sockets(n1.outputs[self.from_socket], n2.inputs[0])
2373 return {'FINISHED'}
2376 class NWAddSequence(Operator, NWBase, ImportHelper):
2377 """Add an Image Sequence"""
2378 bl_idname = 'node.nw_add_sequence'
2379 bl_label = 'Import Image Sequence'
2380 bl_options = {'REGISTER', 'UNDO'}
2382 directory: StringProperty(
2383 subtype="DIR_PATH"
2385 filename: StringProperty(
2386 subtype="FILE_NAME"
2388 files: CollectionProperty(
2389 type=bpy.types.OperatorFileListElement,
2390 options={'HIDDEN', 'SKIP_SAVE'}
2392 relative_path: BoolProperty(
2393 name='Relative Path',
2394 description='Set the file path relative to the blend file, when possible',
2395 default=True
2398 def draw(self, context):
2399 layout = self.layout
2400 layout.alignment = 'LEFT'
2402 layout.prop(self, 'relative_path')
2404 def execute(self, context):
2405 nodes, links = get_nodes_links(context)
2406 directory = self.directory
2407 filename = self.filename
2408 files = self.files
2409 tree = context.space_data.node_tree
2411 # DEBUG
2412 # print ("\nDIR:", directory)
2413 # print ("FN:", filename)
2414 # print ("Fs:", list(f.name for f in files), '\n')
2416 if tree.type == 'SHADER':
2417 node_type = "ShaderNodeTexImage"
2418 elif tree.type == 'COMPOSITING':
2419 node_type = "CompositorNodeImage"
2420 else:
2421 self.report({'ERROR'}, "Unsupported Node Tree type!")
2422 return {'CANCELLED'}
2424 if not files[0].name and not filename:
2425 self.report({'ERROR'}, "No file chosen")
2426 return {'CANCELLED'}
2427 elif files[0].name and (not filename or not path.exists(directory + filename)):
2428 # User has selected multiple files without an active one, or the active one is non-existent
2429 filename = files[0].name
2431 if not path.exists(directory + filename):
2432 self.report({'ERROR'}, filename + " does not exist!")
2433 return {'CANCELLED'}
2435 without_ext = '.'.join(filename.split('.')[:-1])
2437 # if last digit isn't a number, it's not a sequence
2438 if not without_ext[-1].isdigit():
2439 self.report({'ERROR'}, filename + " does not seem to be part of a sequence")
2440 return {'CANCELLED'}
2442 extension = filename.split('.')[-1]
2443 reverse = without_ext[::-1] # reverse string
2445 count_numbers = 0
2446 for char in reverse:
2447 if char.isdigit():
2448 count_numbers += 1
2449 else:
2450 break
2452 without_num = without_ext[:count_numbers * -1]
2454 files = sorted(glob(directory + without_num + "[0-9]" * count_numbers + "." + extension))
2456 num_frames = len(files)
2458 nodes_list = [node for node in nodes]
2459 if nodes_list:
2460 nodes_list.sort(key=lambda k: k.location.x)
2461 xloc = nodes_list[0].location.x - 220 # place new nodes at far left
2462 yloc = 0
2463 for node in nodes:
2464 node.select = False
2465 yloc += node_mid_pt(node, 'y')
2466 yloc = yloc / len(nodes)
2467 else:
2468 xloc = 0
2469 yloc = 0
2471 name_with_hashes = without_num + "#" * count_numbers + '.' + extension
2473 bpy.ops.node.add_node('INVOKE_DEFAULT', use_transform=True, type=node_type)
2474 node = nodes.active
2475 node.label = name_with_hashes
2477 filepath = directory + (without_ext + '.' + extension)
2478 if self.relative_path:
2479 if bpy.data.filepath:
2480 try:
2481 filepath = bpy.path.relpath(filepath)
2482 except ValueError:
2483 pass
2485 img = bpy.data.images.load(filepath)
2486 img.source = 'SEQUENCE'
2487 img.name = name_with_hashes
2488 node.image = img
2489 image_user = node.image_user if tree.type == 'SHADER' else node
2490 # separate the number from the file name of the first file
2491 image_user.frame_offset = int(files[0][len(without_num) + len(directory):-1 * (len(extension) + 1)]) - 1
2492 image_user.frame_duration = num_frames
2494 return {'FINISHED'}
2497 class NWAddMultipleImages(Operator, NWBase, ImportHelper):
2498 """Add multiple images at once"""
2499 bl_idname = 'node.nw_add_multiple_images'
2500 bl_label = 'Open Selected Images'
2501 bl_options = {'REGISTER', 'UNDO'}
2502 directory: StringProperty(
2503 subtype="DIR_PATH"
2505 files: CollectionProperty(
2506 type=bpy.types.OperatorFileListElement,
2507 options={'HIDDEN', 'SKIP_SAVE'}
2510 def execute(self, context):
2511 nodes, links = get_nodes_links(context)
2513 xloc, yloc = context.region.view2d.region_to_view(context.area.width / 2, context.area.height / 2)
2515 if context.space_data.node_tree.type == 'SHADER':
2516 node_type = "ShaderNodeTexImage"
2517 elif context.space_data.node_tree.type == 'COMPOSITING':
2518 node_type = "CompositorNodeImage"
2519 else:
2520 self.report({'ERROR'}, "Unsupported Node Tree type!")
2521 return {'CANCELLED'}
2523 new_nodes = []
2524 for f in self.files:
2525 fname = f.name
2527 node = nodes.new(node_type)
2528 new_nodes.append(node)
2529 node.label = fname
2530 node.hide = True
2531 node.location.x = xloc
2532 node.location.y = yloc
2533 yloc -= 40
2535 img = bpy.data.images.load(self.directory + fname)
2536 node.image = img
2538 # shift new nodes up to center of tree
2539 list_size = new_nodes[0].location.y - new_nodes[-1].location.y
2540 for node in nodes:
2541 if node in new_nodes:
2542 node.select = True
2543 node.location.y += (list_size / 2)
2544 else:
2545 node.select = False
2546 return {'FINISHED'}
2549 class NWViewerFocus(bpy.types.Operator):
2550 """Set the viewer tile center to the mouse position"""
2551 bl_idname = "node.nw_viewer_focus"
2552 bl_label = "Viewer Focus"
2554 x: bpy.props.IntProperty()
2555 y: bpy.props.IntProperty()
2557 @classmethod
2558 def poll(cls, context):
2559 return (nw_check(context)
2560 and nw_check_space_type(cls, context, 'CompositorNodeTree'))
2562 def execute(self, context):
2563 return {'FINISHED'}
2565 def invoke(self, context, event):
2566 render = context.scene.render
2567 space = context.space_data
2568 percent = render.resolution_percentage * 0.01
2570 nodes, links = get_nodes_links(context)
2571 viewers = [n for n in nodes if n.type == 'VIEWER']
2573 if viewers:
2574 mlocx = event.mouse_region_x
2575 mlocy = event.mouse_region_y
2576 select_node = bpy.ops.node.select(location=(mlocx, mlocy), extend=False)
2578 if 'FINISHED' not in select_node: # only run if we're not clicking on a node
2579 region_x = context.region.width
2580 region_y = context.region.height
2582 region_center_x = context.region.width / 2
2583 region_center_y = context.region.height / 2
2585 bd_x = render.resolution_x * percent * space.backdrop_zoom
2586 bd_y = render.resolution_y * percent * space.backdrop_zoom
2588 backdrop_center_x = (bd_x / 2) - space.backdrop_offset[0]
2589 backdrop_center_y = (bd_y / 2) - space.backdrop_offset[1]
2591 margin_x = region_center_x - backdrop_center_x
2592 margin_y = region_center_y - backdrop_center_y
2594 abs_mouse_x = (mlocx - margin_x) / bd_x
2595 abs_mouse_y = (mlocy - margin_y) / bd_y
2597 for node in viewers:
2598 node.center_x = abs_mouse_x
2599 node.center_y = abs_mouse_y
2600 else:
2601 return {'PASS_THROUGH'}
2603 return self.execute(context)
2606 class NWSaveViewer(bpy.types.Operator, ExportHelper):
2607 """Save the current viewer node to an image file"""
2608 bl_idname = "node.nw_save_viewer"
2609 bl_label = "Save This Image"
2610 filepath: StringProperty(subtype="FILE_PATH")
2611 filename_ext: EnumProperty(
2612 name="Format",
2613 description="Choose the file format to save to",
2614 items=(('.bmp', "BMP", ""),
2615 ('.rgb', 'IRIS', ""),
2616 ('.png', 'PNG', ""),
2617 ('.jpg', 'JPEG', ""),
2618 ('.jp2', 'JPEG2000', ""),
2619 ('.tga', 'TARGA', ""),
2620 ('.cin', 'CINEON', ""),
2621 ('.dpx', 'DPX', ""),
2622 ('.exr', 'OPEN_EXR', ""),
2623 ('.hdr', 'HDR', ""),
2624 ('.tif', 'TIFF', "")),
2625 default='.png',
2628 @classmethod
2629 def poll(cls, context):
2630 return (nw_check(context)
2631 and nw_check_space_type(cls, context, 'CompositorNodeTree')
2632 and any(img.source == 'VIEWER'
2633 and img.render_slots == 0
2634 for img in bpy.data.images)
2635 and sum(bpy.data.images["Viewer Node"].size) > 0) # False if not connected or connected but no image
2637 def execute(self, context):
2638 fp = self.filepath
2639 if fp:
2640 formats = {
2641 '.bmp': 'BMP',
2642 '.rgb': 'IRIS',
2643 '.png': 'PNG',
2644 '.jpg': 'JPEG',
2645 '.jpeg': 'JPEG',
2646 '.jp2': 'JPEG2000',
2647 '.tga': 'TARGA',
2648 '.cin': 'CINEON',
2649 '.dpx': 'DPX',
2650 '.exr': 'OPEN_EXR',
2651 '.hdr': 'HDR',
2652 '.tiff': 'TIFF',
2653 '.tif': 'TIFF'}
2654 basename, ext = path.splitext(fp)
2655 old_render_format = context.scene.render.image_settings.file_format
2656 context.scene.render.image_settings.file_format = formats[self.filename_ext]
2657 context.area.type = "IMAGE_EDITOR"
2658 context.area.spaces[0].image = bpy.data.images['Viewer Node']
2659 context.area.spaces[0].image.save_render(fp)
2660 context.area.type = "NODE_EDITOR"
2661 context.scene.render.image_settings.file_format = old_render_format
2662 return {'FINISHED'}
2665 class NWResetNodes(bpy.types.Operator):
2666 """Reset Nodes in Selection"""
2667 bl_idname = "node.nw_reset_nodes"
2668 bl_label = "Reset Nodes"
2669 bl_options = {'REGISTER', 'UNDO'}
2671 @classmethod
2672 def poll(cls, context):
2673 space = context.space_data
2674 return space.type == 'NODE_EDITOR'
2676 def execute(self, context):
2677 node_active = context.active_node
2678 node_selected = context.selected_nodes
2679 node_ignore = ["FRAME", "REROUTE", "GROUP", "SIMULATION_INPUT", "SIMULATION_OUTPUT"]
2681 # Check if one node is selected at least
2682 if not (len(node_selected) > 0):
2683 self.report({'ERROR'}, "1 node must be selected at least")
2684 return {'CANCELLED'}
2686 active_node_name = node_active.name if node_active.select else None
2687 valid_nodes = [n for n in node_selected if n.type not in node_ignore]
2689 # Create output lists
2690 selected_node_names = [n.name for n in node_selected]
2691 success_names = []
2693 # Reset all valid children in a frame
2694 node_active_is_frame = False
2695 if len(node_selected) == 1 and node_active.type == "FRAME":
2696 node_tree = node_active.id_data
2697 children = [n for n in node_tree.nodes if n.parent == node_active]
2698 if children:
2699 valid_nodes = [n for n in children if n.type not in node_ignore]
2700 selected_node_names = [n.name for n in children if n.type not in node_ignore]
2701 node_active_is_frame = True
2703 # Check if valid nodes in selection
2704 if not (len(valid_nodes) > 0):
2705 # Check for frames only
2706 frames_selected = [n for n in node_selected if n.type == "FRAME"]
2707 if (len(frames_selected) > 1 and len(frames_selected) == len(node_selected)):
2708 self.report({'ERROR'}, "Please select only 1 frame to reset")
2709 else:
2710 self.report({'ERROR'}, "No valid node(s) in selection")
2711 return {'CANCELLED'}
2713 # Report nodes that are not valid
2714 if len(valid_nodes) != len(node_selected) and node_active_is_frame is False:
2715 valid_node_names = [n.name for n in valid_nodes]
2716 not_valid_names = list(set(selected_node_names) - set(valid_node_names))
2717 self.report({'INFO'}, "Ignored {}".format(", ".join(not_valid_names)))
2719 # Deselect all nodes
2720 for i in node_selected:
2721 i.select = False
2723 # Run through all valid nodes
2724 for node in valid_nodes:
2726 parent = node.parent if node.parent else None
2727 node_loc = [node.location.x, node.location.y]
2729 node_tree = node.id_data
2730 props_to_copy = 'bl_idname name location height width'.split(' ')
2732 reconnections = []
2733 mappings = chain.from_iterable([node.inputs, node.outputs])
2734 for i in (i for i in mappings if i.is_linked):
2735 for L in i.links:
2736 reconnections.append([L.from_socket.path_from_id(), L.to_socket.path_from_id()])
2738 props = {j: getattr(node, j) for j in props_to_copy}
2740 new_node = node_tree.nodes.new(props['bl_idname'])
2741 props_to_copy.pop(0)
2743 for prop in props_to_copy:
2744 setattr(new_node, prop, props[prop])
2746 nodes = node_tree.nodes
2747 nodes.remove(node)
2748 new_node.name = props['name']
2750 if parent:
2751 new_node.parent = parent
2752 new_node.location = node_loc
2754 for str_from, str_to in reconnections:
2755 connect_sockets(eval(str_from), eval(str_to))
2757 new_node.select = False
2758 success_names.append(new_node.name)
2760 # Reselect all nodes
2761 if selected_node_names and node_active_is_frame is False:
2762 for i in selected_node_names:
2763 node_tree.nodes[i].select = True
2765 if active_node_name is not None:
2766 node_tree.nodes[active_node_name].select = True
2767 node_tree.nodes.active = node_tree.nodes[active_node_name]
2769 self.report({'INFO'}, "Successfully reset {}".format(", ".join(success_names)))
2770 return {'FINISHED'}
2773 classes = (
2774 NWLazyMix,
2775 NWLazyConnect,
2776 NWDeleteUnused,
2777 NWSwapLinks,
2778 NWResetBG,
2779 NWAddAttrNode,
2780 NWPreviewNode,
2781 NWFrameSelected,
2782 NWReloadImages,
2783 NWMergeNodes,
2784 NWBatchChangeNodes,
2785 NWChangeMixFactor,
2786 NWCopySettings,
2787 NWCopyLabel,
2788 NWClearLabel,
2789 NWModifyLabels,
2790 NWAddTextureSetup,
2791 NWAddPrincipledSetup,
2792 NWAddReroutes,
2793 NWLinkActiveToSelected,
2794 NWAlignNodes,
2795 NWSelectParentChildren,
2796 NWDetachOutputs,
2797 NWLinkToOutputNode,
2798 NWMakeLink,
2799 NWCallInputsMenu,
2800 NWAddSequence,
2801 NWAddMultipleImages,
2802 NWViewerFocus,
2803 NWSaveViewer,
2804 NWResetNodes,
2808 def register():
2809 from bpy.utils import register_class
2810 for cls in classes:
2811 register_class(cls)
2814 def unregister():
2815 from bpy.utils import unregister_class
2817 for cls in classes:
2818 unregister_class(cls)