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