Cleanup: Node Wrangler: format using autopep8
[blender-addons.git] / node_wrangler / operators.py
blob92a752048405b620590155be521e5bbca66bccd2
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
507 if item.item_type == 'SOCKET' and item.in_out in {'OUTPUT', 'BOTH'}]
509 def ensure_viewer_socket(self, node, socket_type, connect_socket=None):
510 """Check if a viewer output already exists in a node group, otherwise create it"""
511 if not hasattr(node, "node_tree"):
512 return None
514 viewer_socket = None
515 output_sockets = self.get_output_sockets(node.node_tree)
516 if len(output_sockets):
517 for i, socket in enumerate(output_sockets):
518 if is_viewer_socket(socket) and socket.socket_type == socket_type:
519 # If viewer output is already used but leads to the same socket we can still use it
520 is_used = self.is_socket_used_other_mats(socket)
521 if is_used:
522 if connect_socket is None:
523 continue
524 groupout = get_group_output_node(node.node_tree)
525 groupout_input = groupout.inputs[i]
526 links = groupout_input.links
527 if connect_socket not in [link.from_socket for link in links]:
528 continue
529 viewer_socket = socket
530 break
532 if viewer_socket is None:
533 # Create viewer socket
534 viewer_socket = node.node_tree.interface.new_socket(
535 viewer_socket_name, in_out='OUTPUT', socket_type=socket_type)
536 viewer_socket.NWViewerSocket = True
537 return viewer_socket
539 def init_shader_variables(self, space, shader_type):
540 if shader_type == 'OBJECT':
541 if space.id in bpy.data.lights.values():
542 self.shader_output_type = "OUTPUT_LIGHT"
543 self.shader_output_ident = "ShaderNodeOutputLight"
544 else:
545 self.shader_output_type = "OUTPUT_MATERIAL"
546 self.shader_output_ident = "ShaderNodeOutputMaterial"
548 elif shader_type == 'WORLD':
549 self.shader_output_type = "OUTPUT_WORLD"
550 self.shader_output_ident = "ShaderNodeOutputWorld"
552 @staticmethod
553 def ensure_group_output(tree):
554 """Check if a group output node exists, otherwise create it"""
555 groupout = get_group_output_node(tree)
556 if groupout is None:
557 groupout = tree.nodes.new('NodeGroupOutput')
558 loc_x, loc_y = get_output_location(tree)
559 groupout.location.x = loc_x
560 groupout.location.y = loc_y
561 groupout.select = False
562 # So that we don't keep on adding new group outputs
563 groupout.is_active_output = True
564 return groupout
566 @classmethod
567 def search_sockets(cls, node, sockets, index=None):
568 """Recursively scan nodes for viewer sockets and store them in a list"""
569 for i, input_socket in enumerate(node.inputs):
570 if index and i != index:
571 continue
572 if len(input_socket.links):
573 link = input_socket.links[0]
574 next_node = link.from_node
575 external_socket = link.from_socket
576 if hasattr(next_node, "node_tree"):
577 for socket_index, socket in enumerate(next_node.node_tree.interface.items_tree):
578 if socket.identifier == external_socket.identifier:
579 break
580 if is_viewer_socket(socket) and socket not in sockets:
581 sockets.append(socket)
582 # continue search inside of node group but restrict socket to where we came from
583 groupout = get_group_output_node(next_node.node_tree)
584 cls.search_sockets(groupout, sockets, index=socket_index)
586 @classmethod
587 def scan_nodes(cls, tree, sockets):
588 """Recursively get all viewer sockets in a material tree"""
589 for node in tree.nodes:
590 if hasattr(node, "node_tree"):
591 if node.node_tree is None:
592 continue
593 for socket in cls.get_output_sockets(node.node_tree):
594 if is_viewer_socket(socket) and (socket not in sockets):
595 sockets.append(socket)
596 cls.scan_nodes(node.node_tree, sockets)
598 @staticmethod
599 def remove_socket(tree, socket):
600 interface = tree.interface
601 interface.remove(socket)
602 interface.active_index = min(interface.active_index, len(interface.items_tree) - 1)
604 def link_leads_to_used_socket(self, link):
605 """Return True if link leads to a socket that is already used in this material"""
606 socket = get_internal_socket(link.to_socket)
607 return (socket and self.is_socket_used_active_mat(socket))
609 def is_socket_used_active_mat(self, socket):
610 """Ensure used sockets in active material is calculated and check given socket"""
611 if not hasattr(self, "used_viewer_sockets_active_mat"):
612 self.used_viewer_sockets_active_mat = []
613 output_node = get_group_output_node(bpy.context.space_data.node_tree,
614 output_node_type=self.shader_output_type)
616 if output_node is not None:
617 self.search_sockets(output_node, self.used_viewer_sockets_active_mat)
618 return socket in self.used_viewer_sockets_active_mat
620 def is_socket_used_other_mats(self, socket):
621 """Ensure used sockets in other materials are calculated and check given socket"""
622 if not hasattr(self, "used_viewer_sockets_other_mats"):
623 self.used_viewer_sockets_other_mats = []
624 for mat in bpy.data.materials:
625 if mat.node_tree == bpy.context.space_data.node_tree or not hasattr(mat.node_tree, "nodes"):
626 continue
627 # get viewer node
628 output_node = get_group_output_node(mat.node_tree,
629 output_node_type=self.shader_output_type)
630 if output_node is not None:
631 self.search_sockets(output_node, self.used_viewer_sockets_other_mats)
632 return socket in self.used_viewer_sockets_other_mats
634 def get_output_index(self, base_node_tree, nodes, output_node, socket_type, check_type=False):
635 """Get the next available output socket in the active node"""
636 out_i = None
637 valid_outputs = []
638 for i, out in enumerate(nodes.active.outputs):
639 if is_visible_socket(out) and (not check_type or out.type == socket_type):
640 valid_outputs.append(i)
641 if valid_outputs:
642 out_i = valid_outputs[0] # Start index of node's outputs
643 for i, valid_i in enumerate(valid_outputs):
644 for out_link in nodes.active.outputs[valid_i].links:
645 if is_viewer_link(out_link, output_node):
646 if nodes == base_node_tree.nodes or self.link_leads_to_used_socket(out_link):
647 if i < len(valid_outputs) - 1:
648 out_i = valid_outputs[i + 1]
649 else:
650 out_i = valid_outputs[0]
651 return out_i
653 def create_links(self, tree, link_end, active, out_i, socket_type):
654 """Create links through node groups until we reach the active node"""
655 while tree.nodes.active != active:
656 node = tree.nodes.active
657 viewer_socket = self.ensure_viewer_socket(
658 node, socket_type, connect_socket=active.outputs[out_i] if node.node_tree.nodes.active == active else None)
659 link_start = node.outputs[viewer_socket.identifier]
660 if viewer_socket in self.delete_sockets:
661 self.delete_sockets.remove(viewer_socket)
662 connect_sockets(link_start, link_end)
663 # Iterate
664 link_end = self.ensure_group_output(node.node_tree).inputs[viewer_socket.identifier]
665 tree = tree.nodes.active.node_tree
666 connect_sockets(active.outputs[out_i], link_end)
668 def invoke(self, context, event):
669 space = context.space_data
670 # Ignore operator when running in wrong context.
671 if self.run_in_geometry_nodes != (space.tree_type == "GeometryNodeTree"):
672 return {'PASS_THROUGH'}
674 mlocx = event.mouse_region_x
675 mlocy = event.mouse_region_y
676 select_node = bpy.ops.node.select(location=(mlocx, mlocy), extend=False)
677 if 'FINISHED' not in select_node: # only run if mouse click is on a node
678 return {'CANCELLED'}
680 active_tree, path_to_tree = get_active_tree(context)
681 nodes, links = active_tree.nodes, active_tree.links
682 base_node_tree = space.node_tree
683 active = nodes.active
685 if not active and not any(is_visible_socket(out) for out in active.outputs):
686 return {'CANCELLED'}
688 # Scan through all nodes in tree including nodes inside of groups to find viewer sockets
689 self.delete_sockets = []
690 self.scan_nodes(base_node_tree, self.delete_sockets)
692 # For geometry node trees we just connect to the group output
693 if space.tree_type == "GeometryNodeTree" and active.outputs:
694 socket_type = 'GEOMETRY'
696 # Find (or create if needed) the output of this node tree
697 output_node = self.ensure_group_output(base_node_tree)
699 out_i = self.get_output_index(base_node_tree, nodes, output_node, 'GEOMETRY', check_type=True)
700 # If there is no 'GEOMETRY' output type - We can't preview the node
701 if out_i is None:
702 return {'CANCELLED'}
704 # Find an input socket of the output of type geometry
705 geometry_out_index = None
706 for i, inp in enumerate(output_node.inputs):
707 if inp.type == socket_type:
708 geometry_out_index = i
709 break
710 if geometry_out_index is None:
711 # Create geometry socket
712 geometry_out_socket = base_node_tree.interface.new_socket(
713 'Geometry', in_out='OUTPUT', socket_type='NodeSocketGeometry'
715 geometry_out_index = geometry_out_socket.index
717 output_socket = output_node.inputs[geometry_out_index]
719 self.create_links(base_node_tree, output_socket, active, out_i, 'NodeSocketGeometry')
721 # What follows is code for the shader editor
722 elif space.tree_type == "ShaderNodeTree" and active.outputs:
723 shader_type = space.shader_type
724 self.init_shader_variables(space, shader_type)
725 socket_type = 'NodeSocketShader'
727 # Get or create material_output node
728 output_node = get_group_output_node(base_node_tree,
729 output_node_type=self.shader_output_type)
730 if not output_node:
731 output_node = base_node_tree.nodes.new(self.shader_output_ident)
732 output_node.location = get_output_location(base_node_tree)
733 output_node.select = False
735 out_i = self.get_output_index(base_node_tree, nodes, output_node, 'SHADER')
737 materialout_index = 1 if active.outputs[out_i].name == "Volume" else 0
738 output_socket = output_node.inputs[materialout_index]
740 self.create_links(base_node_tree, output_socket, active, out_i, 'NodeSocketShader')
742 # Delete sockets
743 for socket in self.delete_sockets:
744 if not self.is_socket_used_other_mats(socket):
745 tree = socket.id_data
746 self.remove_socket(tree, socket)
748 nodes.active = active
749 active.select = True
750 force_update(context)
751 return {'FINISHED'}
754 class NWFrameSelected(Operator, NWBase):
755 bl_idname = "node.nw_frame_selected"
756 bl_label = "Frame Selected"
757 bl_description = "Add a frame node and parent the selected nodes to it"
758 bl_options = {'REGISTER', 'UNDO'}
760 label_prop: StringProperty(
761 name='Label',
762 description='The visual name of the frame node',
763 default=' '
765 use_custom_color_prop: BoolProperty(
766 name="Custom Color",
767 description="Use custom color for the frame node",
768 default=False
770 color_prop: FloatVectorProperty(
771 name="Color",
772 description="The color of the frame node",
773 default=(0.604, 0.604, 0.604),
774 min=0, max=1, step=1, precision=3,
775 subtype='COLOR_GAMMA', size=3
778 def draw(self, context):
779 layout = self.layout
780 layout.prop(self, 'label_prop')
781 layout.prop(self, 'use_custom_color_prop')
782 col = layout.column()
783 col.active = self.use_custom_color_prop
784 col.prop(self, 'color_prop', text="")
786 def execute(self, context):
787 nodes, links = get_nodes_links(context)
788 selected = []
789 for node in nodes:
790 if node.select:
791 selected.append(node)
793 bpy.ops.node.add_node(type='NodeFrame')
794 frm = nodes.active
795 frm.label = self.label_prop
796 frm.use_custom_color = self.use_custom_color_prop
797 frm.color = self.color_prop
799 for node in selected:
800 node.parent = frm
802 return {'FINISHED'}
805 class NWReloadImages(Operator):
806 bl_idname = "node.nw_reload_images"
807 bl_label = "Reload Images"
808 bl_description = "Update all the image nodes to match their files on disk"
810 @classmethod
811 def poll(cls, context):
812 return (nw_check(context)
813 and nw_check_space_type(cls, context, 'ShaderNodeTree', 'CompositorNodeTree',
814 'TextureNodeTree', 'GeometryNodeTree')
815 and context.active_node is not None
816 and any(is_visible_socket(out) for out in context.active_node.outputs))
818 def execute(self, context):
819 nodes, links = get_nodes_links(context)
820 image_types = ["IMAGE", "TEX_IMAGE", "TEX_ENVIRONMENT", "TEXTURE"]
821 num_reloaded = 0
822 for node in nodes:
823 if node.type in image_types:
824 if node.type == "TEXTURE":
825 if node.texture: # node has texture assigned
826 if node.texture.type in ['IMAGE', 'ENVIRONMENT_MAP']:
827 if node.texture.image: # texture has image assigned
828 node.texture.image.reload()
829 num_reloaded += 1
830 else:
831 if node.image:
832 node.image.reload()
833 num_reloaded += 1
835 if num_reloaded:
836 self.report({'INFO'}, "Reloaded images")
837 print("Reloaded " + str(num_reloaded) + " images")
838 force_update(context)
839 return {'FINISHED'}
840 else:
841 self.report({'WARNING'}, "No images found to reload in this node tree")
842 return {'CANCELLED'}
845 class NWMergeNodes(Operator, NWBase):
846 bl_idname = "node.nw_merge_nodes"
847 bl_label = "Merge Nodes"
848 bl_description = "Merge Selected Nodes"
849 bl_options = {'REGISTER', 'UNDO'}
851 mode: EnumProperty(
852 name="mode",
853 description="All possible blend types, boolean operations and math operations",
854 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],
856 merge_type: EnumProperty(
857 name="merge type",
858 description="Type of Merge to be used",
859 items=(
860 ('AUTO', 'Auto', 'Automatic Output Type Detection'),
861 ('SHADER', 'Shader', 'Merge using ADD or MIX Shader'),
862 ('GEOMETRY', 'Geometry', 'Merge using Mesh Boolean or Join Geometry Node'),
863 ('MIX', 'Mix Node', 'Merge using Mix Nodes'),
864 ('MATH', 'Math Node', 'Merge using Math Nodes'),
865 ('ZCOMBINE', 'Z-Combine Node', 'Merge using Z-Combine Nodes'),
866 ('ALPHAOVER', 'Alpha Over Node', 'Merge using Alpha Over Nodes'),
870 # Check if the link connects to a node that is in selected_nodes
871 # If not, then check recursively for each link in the nodes outputs.
872 # If yes, return True. If the recursion stops without finding a node
873 # in selected_nodes, it returns False. The depth is used to prevent
874 # getting stuck in a loop because of an already present cycle.
875 @staticmethod
876 def link_creates_cycle(link, selected_nodes, depth=0) -> bool:
877 if depth > 255:
878 # We're stuck in a cycle, but that cycle was already present,
879 # so we return False.
880 # NOTE: The number 255 is arbitrary, but seems to work well.
881 return False
882 node = link.to_node
883 if node in selected_nodes:
884 return True
885 if not node.outputs:
886 return False
887 for output in node.outputs:
888 if output.is_linked:
889 for olink in output.links:
890 if NWMergeNodes.link_creates_cycle(olink, selected_nodes, depth + 1):
891 return True
892 # None of the outputs found a node in selected_nodes, so there is no cycle.
893 return False
895 # Merge the nodes in `nodes_list` with a node of type `node_name` that has a multi_input socket.
896 # The parameters `socket_indices` gives the indices of the node sockets in the order that they should
897 # be connected. The last one is assumed to be a multi input socket.
898 # For convenience the node is returned.
899 @staticmethod
900 def merge_with_multi_input(nodes_list, merge_position, do_hide, loc_x, links, nodes, node_name, socket_indices):
901 # The y-location of the last node
902 loc_y = nodes_list[-1][2]
903 if merge_position == 'CENTER':
904 # Average the y-location
905 for i in range(len(nodes_list) - 1):
906 loc_y += nodes_list[i][2]
907 loc_y = loc_y / len(nodes_list)
908 new_node = nodes.new(node_name)
909 new_node.hide = do_hide
910 new_node.location.x = loc_x
911 new_node.location.y = loc_y
912 selected_nodes = [nodes[node_info[0]] for node_info in nodes_list]
913 prev_links = []
914 outputs_for_multi_input = []
915 for i, node in enumerate(selected_nodes):
916 node.select = False
917 # Search for the first node which had output links that do not create
918 # a cycle, which we can then reconnect afterwards.
919 if prev_links == [] and node.outputs[0].is_linked:
920 prev_links = [
921 link for link in node.outputs[0].links if not NWMergeNodes.link_creates_cycle(
922 link, selected_nodes)]
923 # Get the index of the socket, the last one is a multi input, and is thus used repeatedly
924 # To get the placement to look right we need to reverse the order in which we connect the
925 # outputs to the multi input socket.
926 if i < len(socket_indices) - 1:
927 ind = socket_indices[i]
928 connect_sockets(node.outputs[0], new_node.inputs[ind])
929 else:
930 outputs_for_multi_input.insert(0, node.outputs[0])
931 if outputs_for_multi_input != []:
932 ind = socket_indices[-1]
933 for output in outputs_for_multi_input:
934 connect_sockets(output, new_node.inputs[ind])
935 if prev_links != []:
936 for link in prev_links:
937 connect_sockets(new_node.outputs[0], link.to_node.inputs[0])
938 return new_node
940 @classmethod
941 def poll(cls, context):
942 return (nw_check(context)
943 and nw_check_space_type(cls, context, 'ShaderNodeTree', 'CompositorNodeTree',
944 'TextureNodeTree', 'GeometryNodeTree'))
946 def execute(self, context):
947 settings = context.preferences.addons[__package__].preferences
948 merge_hide = settings.merge_hide
949 merge_position = settings.merge_position # 'center' or 'bottom'
951 do_hide = False
952 do_hide_shader = False
953 if merge_hide == 'ALWAYS':
954 do_hide = True
955 do_hide_shader = True
956 elif merge_hide == 'NON_SHADER':
957 do_hide = True
959 tree_type = context.space_data.node_tree.type
960 if tree_type == 'GEOMETRY':
961 node_type = 'GeometryNode'
962 if tree_type == 'COMPOSITING':
963 node_type = 'CompositorNode'
964 elif tree_type == 'SHADER':
965 node_type = 'ShaderNode'
966 elif tree_type == 'TEXTURE':
967 node_type = 'TextureNode'
968 nodes, links = get_nodes_links(context)
969 mode = self.mode
970 merge_type = self.merge_type
971 # Prevent trying to add Z-Combine in not 'COMPOSITING' node tree.
972 # 'ZCOMBINE' works only if mode == 'MIX'
973 # Setting mode to None prevents trying to add 'ZCOMBINE' node.
974 if (merge_type == 'ZCOMBINE' or merge_type == 'ALPHAOVER') and tree_type != 'COMPOSITING':
975 merge_type = 'MIX'
976 mode = 'MIX'
977 if (merge_type != 'MATH' and merge_type != 'GEOMETRY') and tree_type == 'GEOMETRY':
978 merge_type = 'AUTO'
979 # The Mix node and math nodes used for geometry nodes are of type 'ShaderNode'
980 if (merge_type == 'MATH' or merge_type == 'MIX') and tree_type == 'GEOMETRY':
981 node_type = 'ShaderNode'
982 selected_mix = [] # entry = [index, loc]
983 selected_shader = [] # entry = [index, loc]
984 selected_geometry = [] # entry = [index, loc]
985 selected_math = [] # entry = [index, loc]
986 selected_vector = [] # entry = [index, loc]
987 selected_z = [] # entry = [index, loc]
988 selected_alphaover = [] # entry = [index, loc]
990 for i, node in enumerate(nodes):
991 if node.select and node.outputs:
992 if merge_type == 'AUTO':
993 for (type, types_list, dst) in (
994 ('SHADER', ('MIX', 'ADD'), selected_shader),
995 ('GEOMETRY', [t[0] for t in geo_combine_operations], selected_geometry),
996 ('RGBA', [t[0] for t in blend_types], selected_mix),
997 ('VALUE', [t[0] for t in operations], selected_math),
998 ('VECTOR', [], selected_vector),
1000 output = get_first_enabled_output(node)
1001 output_type = output.type
1002 valid_mode = mode in types_list
1003 # When mode is 'MIX' we have to cheat since the mix node is not used in
1004 # geometry nodes.
1005 if tree_type == 'GEOMETRY':
1006 if mode == 'MIX':
1007 if output_type == 'VALUE' and type == 'VALUE':
1008 valid_mode = True
1009 elif output_type == 'VECTOR' and type == 'VECTOR':
1010 valid_mode = True
1011 elif type == 'GEOMETRY':
1012 valid_mode = True
1013 # When mode is 'MIX' use mix node for both 'RGBA' and 'VALUE' output types.
1014 # Cheat that output type is 'RGBA',
1015 # and that 'MIX' exists in math operations list.
1016 # This way when selected_mix list is analyzed:
1017 # Node data will be appended even though it doesn't meet requirements.
1018 elif output_type != 'SHADER' and mode == 'MIX':
1019 output_type = 'RGBA'
1020 valid_mode = True
1021 if output_type == type and valid_mode:
1022 dst.append([i, node.location.x, node.location.y, node.dimensions.x, node.hide])
1023 else:
1024 for (type, types_list, dst) in (
1025 ('SHADER', ('MIX', 'ADD'), selected_shader),
1026 ('GEOMETRY', [t[0] for t in geo_combine_operations], selected_geometry),
1027 ('MIX', [t[0] for t in blend_types], selected_mix),
1028 ('MATH', [t[0] for t in operations], selected_math),
1029 ('ZCOMBINE', ('MIX', ), selected_z),
1030 ('ALPHAOVER', ('MIX', ), selected_alphaover),
1032 if merge_type == type and mode in types_list:
1033 dst.append([i, node.location.x, node.location.y, node.dimensions.x, node.hide])
1034 # When nodes with output kinds 'RGBA' and 'VALUE' are selected at the same time
1035 # use only 'Mix' nodes for merging.
1036 # For that we add selected_math list to selected_mix list and clear selected_math.
1037 if selected_mix and selected_math and merge_type == 'AUTO':
1038 selected_mix += selected_math
1039 selected_math = []
1040 for nodes_list in [
1041 selected_mix,
1042 selected_shader,
1043 selected_geometry,
1044 selected_math,
1045 selected_vector,
1046 selected_z,
1047 selected_alphaover]:
1048 if not nodes_list:
1049 continue
1050 count_before = len(nodes)
1051 # sort list by loc_x - reversed
1052 nodes_list.sort(key=lambda k: k[1], reverse=True)
1053 # get maximum loc_x
1054 loc_x = nodes_list[0][1] + nodes_list[0][3] + 70
1055 nodes_list.sort(key=lambda k: k[2], reverse=True)
1057 # Change the node type for math nodes in a geometry node tree.
1058 if tree_type == 'GEOMETRY':
1059 if nodes_list is selected_math or nodes_list is selected_vector or nodes_list is selected_mix:
1060 node_type = 'ShaderNode'
1061 if mode == 'MIX':
1062 mode = 'ADD'
1063 else:
1064 node_type = 'GeometryNode'
1065 if merge_position == 'CENTER':
1066 # average yloc of last two nodes (lowest two)
1067 loc_y = ((nodes_list[len(nodes_list) - 1][2]) + (nodes_list[len(nodes_list) - 2][2])) / 2
1068 if nodes_list[len(nodes_list) - 1][-1]: # if last node is hidden, mix should be shifted up a bit
1069 if do_hide:
1070 loc_y += 40
1071 else:
1072 loc_y += 80
1073 else:
1074 loc_y = nodes_list[len(nodes_list) - 1][2]
1075 offset_y = 100
1076 if not do_hide:
1077 offset_y = 200
1078 if nodes_list == selected_shader and not do_hide_shader:
1079 offset_y = 150.0
1080 the_range = len(nodes_list) - 1
1081 if len(nodes_list) == 1:
1082 the_range = 1
1083 was_multi = False
1084 for i in range(the_range):
1085 if nodes_list == selected_mix:
1086 mix_name = 'Mix'
1087 if tree_type == 'COMPOSITING':
1088 mix_name = 'MixRGB'
1089 add_type = node_type + mix_name
1090 add = nodes.new(add_type)
1091 if tree_type != 'COMPOSITING':
1092 add.data_type = 'RGBA'
1093 add.blend_type = mode
1094 if mode != 'MIX':
1095 add.inputs[0].default_value = 1.0
1096 add.show_preview = False
1097 add.hide = do_hide
1098 if do_hide:
1099 loc_y = loc_y - 50
1100 first = 6
1101 second = 7
1102 if tree_type == 'COMPOSITING':
1103 first = 1
1104 second = 2
1105 elif nodes_list == selected_math:
1106 add_type = node_type + 'Math'
1107 add = nodes.new(add_type)
1108 add.operation = mode
1109 add.hide = do_hide
1110 if do_hide:
1111 loc_y = loc_y - 50
1112 first = 0
1113 second = 1
1114 elif nodes_list == selected_shader:
1115 if mode == 'MIX':
1116 add_type = node_type + 'MixShader'
1117 add = nodes.new(add_type)
1118 add.hide = do_hide_shader
1119 if do_hide_shader:
1120 loc_y = loc_y - 50
1121 first = 1
1122 second = 2
1123 elif mode == 'ADD':
1124 add_type = node_type + 'AddShader'
1125 add = nodes.new(add_type)
1126 add.hide = do_hide_shader
1127 if do_hide_shader:
1128 loc_y = loc_y - 50
1129 first = 0
1130 second = 1
1131 elif nodes_list == selected_geometry:
1132 if mode in ('JOIN', 'MIX'):
1133 add_type = node_type + 'JoinGeometry'
1134 add = self.merge_with_multi_input(
1135 nodes_list, merge_position, do_hide, loc_x, links, nodes, add_type, [0])
1136 else:
1137 add_type = node_type + 'MeshBoolean'
1138 indices = [0, 1] if mode == 'DIFFERENCE' else [1]
1139 add = self.merge_with_multi_input(
1140 nodes_list, merge_position, do_hide, loc_x, links, nodes, add_type, indices)
1141 add.operation = mode
1142 was_multi = True
1143 break
1144 elif nodes_list == selected_vector:
1145 add_type = node_type + 'VectorMath'
1146 add = nodes.new(add_type)
1147 add.operation = mode
1148 add.hide = do_hide
1149 if do_hide:
1150 loc_y = loc_y - 50
1151 first = 0
1152 second = 1
1153 elif nodes_list == selected_z:
1154 add = nodes.new('CompositorNodeZcombine')
1155 add.show_preview = False
1156 add.hide = do_hide
1157 if do_hide:
1158 loc_y = loc_y - 50
1159 first = 0
1160 second = 2
1161 elif nodes_list == selected_alphaover:
1162 add = nodes.new('CompositorNodeAlphaOver')
1163 add.show_preview = False
1164 add.hide = do_hide
1165 if do_hide:
1166 loc_y = loc_y - 50
1167 first = 1
1168 second = 2
1169 add.location = loc_x, loc_y
1170 loc_y += offset_y
1171 add.select = True
1173 # This has already been handled separately
1174 if was_multi:
1175 continue
1176 count_adds = i + 1
1177 count_after = len(nodes)
1178 index = count_after - 1
1179 first_selected = nodes[nodes_list[0][0]]
1180 # "last" node has been added as first, so its index is count_before.
1181 last_add = nodes[count_before]
1182 # Create list of invalid indexes.
1183 invalid_nodes = [nodes[n[0]]
1184 for n in (selected_mix + selected_math + selected_shader + selected_z + selected_geometry)]
1186 # Special case:
1187 # Two nodes were selected and first selected has no output links, second selected has output links.
1188 # Then add links from last add to all links 'to_socket' of out links of second selected.
1189 first_selected_output = get_first_enabled_output(first_selected)
1190 if len(nodes_list) == 2:
1191 if not first_selected_output.links:
1192 second_selected = nodes[nodes_list[1][0]]
1193 for ss_link in get_first_enabled_output(second_selected).links:
1194 # Prevent cyclic dependencies when nodes to be merged are linked to one another.
1195 # Link only if "to_node" index not in invalid indexes list.
1196 if not self.link_creates_cycle(ss_link, invalid_nodes):
1197 connect_sockets(get_first_enabled_output(last_add), ss_link.to_socket)
1198 # add links from last_add to all links 'to_socket' of out links of first selected.
1199 for fs_link in first_selected_output.links:
1200 # Link only if "to_node" index not in invalid indexes list.
1201 if not self.link_creates_cycle(fs_link, invalid_nodes):
1202 connect_sockets(get_first_enabled_output(last_add), fs_link.to_socket)
1203 # add link from "first" selected and "first" add node
1204 node_to = nodes[count_after - 1]
1205 connect_sockets(first_selected_output, node_to.inputs[first])
1206 if node_to.type == 'ZCOMBINE':
1207 for fs_out in first_selected.outputs:
1208 if fs_out != first_selected_output and fs_out.name in ('Z', 'Depth'):
1209 connect_sockets(fs_out, node_to.inputs[1])
1210 break
1211 # add links between added ADD nodes and between selected and ADD nodes
1212 for i in range(count_adds):
1213 if i < count_adds - 1:
1214 node_from = nodes[index]
1215 node_to = nodes[index - 1]
1216 node_to_input_i = first
1217 node_to_z_i = 1 # if z combine - link z to first z input
1218 connect_sockets(get_first_enabled_output(node_from), node_to.inputs[node_to_input_i])
1219 if node_to.type == 'ZCOMBINE':
1220 for from_out in node_from.outputs:
1221 if from_out != get_first_enabled_output(node_from) and from_out.name in ('Z', 'Depth'):
1222 connect_sockets(from_out, node_to.inputs[node_to_z_i])
1223 if len(nodes_list) > 1:
1224 node_from = nodes[nodes_list[i + 1][0]]
1225 node_to = nodes[index]
1226 node_to_input_i = second
1227 node_to_z_i = 3 # if z combine - link z to second z input
1228 connect_sockets(get_first_enabled_output(node_from), node_to.inputs[node_to_input_i])
1229 if node_to.type == 'ZCOMBINE':
1230 for from_out in node_from.outputs:
1231 if from_out != get_first_enabled_output(node_from) and from_out.name in ('Z', 'Depth'):
1232 connect_sockets(from_out, node_to.inputs[node_to_z_i])
1233 index -= 1
1234 # set "last" of added nodes as active
1235 nodes.active = last_add
1236 for i, x, y, dx, h in nodes_list:
1237 nodes[i].select = False
1239 return {'FINISHED'}
1242 class NWBatchChangeNodes(Operator, NWBase):
1243 bl_idname = "node.nw_batch_change"
1244 bl_label = "Batch Change"
1245 bl_description = "Batch Change Blend Type and Math Operation"
1246 bl_options = {'REGISTER', 'UNDO'}
1248 blend_type: EnumProperty(
1249 name="Blend Type",
1250 items=blend_types + navs,
1252 operation: EnumProperty(
1253 name="Operation",
1254 items=operations + navs,
1257 @classmethod
1258 def poll(cls, context):
1259 return (nw_check(context)
1260 and nw_check_space_type(cls, context, 'ShaderNodeTree', 'CompositorNodeTree',
1261 'TextureNodeTree', 'GeometryNodeTree'))
1263 def execute(self, context):
1264 blend_type = self.blend_type
1265 operation = self.operation
1266 for node in context.selected_nodes:
1267 if node.type == 'MIX_RGB' or (node.bl_idname == 'ShaderNodeMix' and node.data_type == 'RGBA'):
1268 if blend_type not in [nav[0] for nav in navs]:
1269 node.blend_type = blend_type
1270 else:
1271 if blend_type == 'NEXT':
1272 index = [i for i, entry in enumerate(blend_types) if node.blend_type in entry][0]
1273 # index = blend_types.index(node.blend_type)
1274 if index == len(blend_types) - 1:
1275 node.blend_type = blend_types[0][0]
1276 else:
1277 node.blend_type = blend_types[index + 1][0]
1279 if blend_type == 'PREV':
1280 index = [i for i, entry in enumerate(blend_types) if node.blend_type in entry][0]
1281 if index == 0:
1282 node.blend_type = blend_types[len(blend_types) - 1][0]
1283 else:
1284 node.blend_type = blend_types[index - 1][0]
1286 if node.type == 'MATH' or node.bl_idname == 'ShaderNodeMath':
1287 if operation not in [nav[0] for nav in navs]:
1288 node.operation = operation
1289 else:
1290 if operation == 'NEXT':
1291 index = [i for i, entry in enumerate(operations) if node.operation in entry][0]
1292 # index = operations.index(node.operation)
1293 if index == len(operations) - 1:
1294 node.operation = operations[0][0]
1295 else:
1296 node.operation = operations[index + 1][0]
1298 if operation == 'PREV':
1299 index = [i for i, entry in enumerate(operations) if node.operation in entry][0]
1300 # index = operations.index(node.operation)
1301 if index == 0:
1302 node.operation = operations[len(operations) - 1][0]
1303 else:
1304 node.operation = operations[index - 1][0]
1306 return {'FINISHED'}
1309 class NWChangeMixFactor(Operator, NWBase):
1310 bl_idname = "node.nw_factor"
1311 bl_label = "Change Factor"
1312 bl_description = "Change Factors of Mix Nodes and Mix Shader Nodes"
1313 bl_options = {'REGISTER', 'UNDO'}
1315 # option: Change factor.
1316 # If option is 1.0 or 0.0 - set to 1.0 or 0.0
1317 # Else - change factor by option value.
1318 option: FloatProperty()
1320 def execute(self, context):
1321 nodes, links = get_nodes_links(context)
1322 option = self.option
1323 selected = [] # entry = index
1324 for si, node in enumerate(nodes):
1325 if node.select:
1326 if node.type in {'MIX_RGB', 'MIX_SHADER'} or node.bl_idname == 'ShaderNodeMix':
1327 selected.append(si)
1329 for si in selected:
1330 fac = nodes[si].inputs[0]
1331 nodes[si].hide = False
1332 if option in {0.0, 1.0}:
1333 fac.default_value = option
1334 else:
1335 fac.default_value += option
1337 return {'FINISHED'}
1340 class NWCopySettings(Operator, NWBase):
1341 bl_idname = "node.nw_copy_settings"
1342 bl_label = "Copy Settings"
1343 bl_description = "Copy Settings of Active Node to Selected Nodes"
1344 bl_options = {'REGISTER', 'UNDO'}
1346 @classmethod
1347 def poll(cls, context):
1348 return (nw_check(context)
1349 and context.active_node is not None
1350 and context.active_node.type != 'FRAME')
1352 def execute(self, context):
1353 node_active = context.active_node
1354 node_selected = context.selected_nodes
1356 # Error handling
1357 if not (len(node_selected) > 1):
1358 self.report({'ERROR'}, "2 nodes must be selected at least")
1359 return {'CANCELLED'}
1361 # Check if active node is in the selection
1362 selected_node_names = [n.name for n in node_selected]
1363 if node_active.name not in selected_node_names:
1364 self.report({'ERROR'}, "No active node")
1365 return {'CANCELLED'}
1367 # Get nodes in selection by type
1368 valid_nodes = [n for n in node_selected if n.type == node_active.type]
1370 if not (len(valid_nodes) > 1) and node_active:
1371 self.report({'ERROR'}, "Selected nodes are not of the same type as {}".format(node_active.name))
1372 return {'CANCELLED'}
1374 if len(valid_nodes) != len(node_selected):
1375 # Report nodes that are not valid
1376 valid_node_names = [n.name for n in valid_nodes]
1377 not_valid_names = list(set(selected_node_names) - set(valid_node_names))
1378 self.report(
1379 {'INFO'},
1380 "Ignored {} (not of the same type as {})".format(
1381 ", ".join(not_valid_names),
1382 node_active.name))
1384 # Reference original
1385 orig = node_active
1386 # node_selected_names = [n.name for n in node_selected]
1388 # Output list
1389 success_names = []
1391 # Deselect all nodes
1392 for i in node_selected:
1393 i.select = False
1395 # Code by zeffii from http://blender.stackexchange.com/a/42338/3710
1396 # Run through all other nodes
1397 for node in valid_nodes[1:]:
1399 # Check for frame node
1400 parent = node.parent if node.parent else None
1401 node_loc = [node.location.x, node.location.y]
1403 # Select original to duplicate
1404 orig.select = True
1406 # Duplicate selected node
1407 bpy.ops.node.duplicate()
1408 new_node = context.selected_nodes[0]
1410 # Deselect copy
1411 new_node.select = False
1413 # Properties to copy
1414 node_tree = node.id_data
1415 props_to_copy = 'bl_idname name location height width'.split(' ')
1417 # Input and outputs
1418 reconnections = []
1419 mappings = chain.from_iterable([node.inputs, node.outputs])
1420 for i in (i for i in mappings if i.is_linked):
1421 for L in i.links:
1422 reconnections.append([L.from_socket.path_from_id(), L.to_socket.path_from_id()])
1424 # Properties
1425 props = {j: getattr(node, j) for j in props_to_copy}
1426 props_to_copy.pop(0)
1428 for prop in props_to_copy:
1429 setattr(new_node, prop, props[prop])
1431 # Get the node tree to remove the old node
1432 nodes = node_tree.nodes
1433 nodes.remove(node)
1434 new_node.name = props['name']
1436 if parent:
1437 new_node.parent = parent
1438 new_node.location = node_loc
1440 for str_from, str_to in reconnections:
1441 node_tree.connect_sockets(eval(str_from), eval(str_to))
1443 success_names.append(new_node.name)
1445 orig.select = True
1446 node_tree.nodes.active = orig
1447 self.report(
1448 {'INFO'},
1449 "Successfully copied attributes from {} to: {}".format(
1450 orig.name,
1451 ", ".join(success_names)))
1452 return {'FINISHED'}
1455 class NWCopyLabel(Operator, NWBase):
1456 bl_idname = "node.nw_copy_label"
1457 bl_label = "Copy Label"
1458 bl_options = {'REGISTER', 'UNDO'}
1460 option: EnumProperty(
1461 name="option",
1462 description="Source of name of label",
1463 items=(
1464 ('FROM_ACTIVE', 'from active', 'from active node',),
1465 ('FROM_NODE', 'from node', 'from node linked to selected node'),
1466 ('FROM_SOCKET', 'from socket', 'from socket linked to selected node'),
1470 def execute(self, context):
1471 nodes, links = get_nodes_links(context)
1472 option = self.option
1473 active = nodes.active
1474 if option == 'FROM_ACTIVE':
1475 if active:
1476 src_label = active.label
1477 for node in [n for n in nodes if n.select and nodes.active != n]:
1478 node.label = src_label
1479 elif option == 'FROM_NODE':
1480 selected = [n for n in nodes if n.select]
1481 for node in selected:
1482 for input in node.inputs:
1483 if input.links:
1484 src = input.links[0].from_node
1485 node.label = src.label
1486 break
1487 elif option == 'FROM_SOCKET':
1488 selected = [n for n in nodes if n.select]
1489 for node in selected:
1490 for input in node.inputs:
1491 if input.links:
1492 src = input.links[0].from_socket
1493 node.label = src.name
1494 break
1496 return {'FINISHED'}
1499 class NWClearLabel(Operator, NWBase):
1500 bl_idname = "node.nw_clear_label"
1501 bl_label = "Clear Label"
1502 bl_options = {'REGISTER', 'UNDO'}
1504 option: BoolProperty()
1506 def execute(self, context):
1507 nodes, links = get_nodes_links(context)
1508 for node in [n for n in nodes if n.select]:
1509 node.label = ''
1511 return {'FINISHED'}
1513 def invoke(self, context, event):
1514 if self.option:
1515 return self.execute(context)
1516 else:
1517 return context.window_manager.invoke_confirm(self, event)
1520 class NWModifyLabels(Operator, NWBase):
1521 """Modify Labels of all selected nodes"""
1522 bl_idname = "node.nw_modify_labels"
1523 bl_label = "Modify Labels"
1524 bl_options = {'REGISTER', 'UNDO'}
1526 prepend: StringProperty(
1527 name="Add to Beginning"
1529 append: StringProperty(
1530 name="Add to End"
1532 replace_from: StringProperty(
1533 name="Text to Replace"
1535 replace_to: StringProperty(
1536 name="Replace with"
1539 def execute(self, context):
1540 nodes, links = get_nodes_links(context)
1541 for node in [n for n in nodes if n.select]:
1542 node.label = self.prepend + node.label.replace(self.replace_from, self.replace_to) + self.append
1544 return {'FINISHED'}
1546 def invoke(self, context, event):
1547 self.prepend = ""
1548 self.append = ""
1549 self.remove = ""
1550 return context.window_manager.invoke_props_dialog(self)
1553 class NWAddTextureSetup(Operator, NWBase):
1554 bl_idname = "node.nw_add_texture"
1555 bl_label = "Texture Setup"
1556 bl_description = "Add Texture Node Setup to Selected Shaders"
1557 bl_options = {'REGISTER', 'UNDO'}
1559 add_mapping: BoolProperty(
1560 name="Add Mapping Nodes",
1561 description="Create coordinate and mapping nodes for the texture (ignored for selected texture nodes)",
1562 default=True)
1564 @classmethod
1565 def poll(cls, context):
1566 return (nw_check(context)
1567 and nw_check_space_type(cls, context, 'ShaderNodeTree'))
1569 def execute(self, context):
1570 nodes, links = get_nodes_links(context)
1572 texture_types = get_texture_node_types()
1573 selected_nodes = [n for n in nodes if n.select]
1575 for node in selected_nodes:
1576 if not node.inputs:
1577 continue
1579 input_index = 0
1580 target_input = node.inputs[0]
1581 for input in node.inputs:
1582 if input.enabled:
1583 input_index += 1
1584 if not input.is_linked:
1585 target_input = input
1586 break
1587 else:
1588 self.report({'WARNING'}, "No free inputs for node: " + node.name)
1589 continue
1591 x_offset = 0
1592 padding = 40.0
1593 locx = node.location.x
1594 locy = node.location.y - (input_index * padding)
1596 is_texture_node = node.rna_type.identifier in texture_types
1597 use_environment_texture = node.type == 'BACKGROUND'
1599 # Add an image texture before normal shader nodes.
1600 if not is_texture_node:
1601 image_texture_type = 'ShaderNodeTexEnvironment' if use_environment_texture else 'ShaderNodeTexImage'
1602 image_texture_node = nodes.new(image_texture_type)
1603 x_offset = x_offset + image_texture_node.width + padding
1604 image_texture_node.location = [locx - x_offset, locy]
1605 nodes.active = image_texture_node
1606 connect_sockets(image_texture_node.outputs[0], target_input)
1608 # The mapping setup following this will connect to the first input of this image texture.
1609 target_input = image_texture_node.inputs[0]
1611 node.select = False
1613 if is_texture_node or self.add_mapping:
1614 # Add Mapping node.
1615 mapping_node = nodes.new('ShaderNodeMapping')
1616 x_offset = x_offset + mapping_node.width + padding
1617 mapping_node.location = [locx - x_offset, locy]
1618 connect_sockets(mapping_node.outputs[0], target_input)
1620 # Add Texture Coordinates node.
1621 tex_coord_node = nodes.new('ShaderNodeTexCoord')
1622 x_offset = x_offset + tex_coord_node.width + padding
1623 tex_coord_node.location = [locx - x_offset, locy]
1625 is_procedural_texture = is_texture_node and node.type != 'TEX_IMAGE'
1626 use_generated_coordinates = is_procedural_texture or use_environment_texture
1627 tex_coord_output = tex_coord_node.outputs[0 if use_generated_coordinates else 2]
1628 connect_sockets(tex_coord_output, mapping_node.inputs[0])
1630 return {'FINISHED'}
1633 class NWAddPrincipledSetup(Operator, NWBase, ImportHelper):
1634 bl_idname = "node.nw_add_textures_for_principled"
1635 bl_label = "Principled Texture Setup"
1636 bl_description = "Add Texture Node Setup for Principled BSDF"
1637 bl_options = {'REGISTER', 'UNDO'}
1639 directory: StringProperty(
1640 name='Directory',
1641 subtype='DIR_PATH',
1642 default='',
1643 description='Folder to search in for image files'
1645 files: CollectionProperty(
1646 type=bpy.types.OperatorFileListElement,
1647 options={'HIDDEN', 'SKIP_SAVE'}
1650 relative_path: BoolProperty(
1651 name='Relative Path',
1652 description='Set the file path relative to the blend file, when possible',
1653 default=True
1656 order = [
1657 "filepath",
1658 "files",
1661 def draw(self, context):
1662 layout = self.layout
1663 layout.alignment = 'LEFT'
1665 layout.prop(self, 'relative_path')
1667 @classmethod
1668 def poll(cls, context):
1669 return (nw_check(context)
1670 and nw_check_space_type(cls, context, 'ShaderNodeTree'))
1672 def execute(self, context):
1673 # Check if everything is ok
1674 if not self.directory:
1675 self.report({'INFO'}, 'No Folder Selected')
1676 return {'CANCELLED'}
1677 if not self.files[:]:
1678 self.report({'INFO'}, 'No Files Selected')
1679 return {'CANCELLED'}
1681 nodes, links = get_nodes_links(context)
1682 active_node = nodes.active
1683 if not (active_node and active_node.bl_idname == 'ShaderNodeBsdfPrincipled'):
1684 self.report({'INFO'}, 'Select Principled BSDF')
1685 return {'CANCELLED'}
1687 # Filter textures names for texturetypes in filenames
1688 # [Socket Name, [abbreviations and keyword list], Filename placeholder]
1689 tags = context.preferences.addons[__package__].preferences.principled_tags
1690 normal_abbr = tags.normal.split(' ')
1691 bump_abbr = tags.bump.split(' ')
1692 gloss_abbr = tags.gloss.split(' ')
1693 rough_abbr = tags.rough.split(' ')
1694 socketnames = [
1695 ['Displacement', tags.displacement.split(' '), None],
1696 ['Base Color', tags.base_color.split(' '), None],
1697 ['Metallic', tags.metallic.split(' '), None],
1698 ['Specular IOR Level', tags.specular.split(' '), None],
1699 ['Roughness', rough_abbr + gloss_abbr, None],
1700 ['Normal', normal_abbr + bump_abbr, None],
1701 ['Transmission Weight', tags.transmission.split(' '), None],
1702 ['Emission Color', tags.emission.split(' '), None],
1703 ['Alpha', tags.alpha.split(' '), None],
1704 ['Ambient Occlusion', tags.ambient_occlusion.split(' '), None],
1707 match_files_to_socket_names(self.files, socketnames)
1708 # Remove socketnames without found files
1709 socketnames = [s for s in socketnames if s[2]
1710 and path.exists(self.directory + s[2])]
1711 if not socketnames:
1712 self.report({'INFO'}, 'No matching images found')
1713 print('No matching images found')
1714 return {'CANCELLED'}
1716 # Don't override path earlier as os.path is used to check the absolute path
1717 import_path = self.directory
1718 if self.relative_path:
1719 if bpy.data.filepath:
1720 try:
1721 import_path = bpy.path.relpath(self.directory)
1722 except ValueError:
1723 pass
1725 # Add found images
1726 print('\nMatched Textures:')
1727 texture_nodes = []
1728 disp_texture = None
1729 ao_texture = None
1730 normal_node = None
1731 roughness_node = None
1732 for i, sname in enumerate(socketnames):
1733 print(i, sname[0], sname[2])
1735 # DISPLACEMENT NODES
1736 if sname[0] == 'Displacement':
1737 disp_texture = nodes.new(type='ShaderNodeTexImage')
1738 img = bpy.data.images.load(path.join(import_path, sname[2]))
1739 disp_texture.image = img
1740 disp_texture.label = 'Displacement'
1741 if disp_texture.image:
1742 disp_texture.image.colorspace_settings.is_data = True
1744 # Add displacement offset nodes
1745 disp_node = nodes.new(type='ShaderNodeDisplacement')
1746 # Align the Displacement node under the active Principled BSDF node
1747 disp_node.location = active_node.location + Vector((100, -700))
1748 link = connect_sockets(disp_node.inputs[0], disp_texture.outputs[0])
1750 # TODO Turn on true displacement in the material
1751 # Too complicated for now
1753 # Find output node
1754 output_node = [n for n in nodes if n.bl_idname == 'ShaderNodeOutputMaterial']
1755 if output_node:
1756 if not output_node[0].inputs[2].is_linked:
1757 link = connect_sockets(output_node[0].inputs[2], disp_node.outputs[0])
1759 continue
1761 # AMBIENT OCCLUSION TEXTURE
1762 if sname[0] == 'Ambient Occlusion':
1763 ao_texture = nodes.new(type='ShaderNodeTexImage')
1764 img = bpy.data.images.load(path.join(import_path, sname[2]))
1765 ao_texture.image = img
1766 ao_texture.label = sname[0]
1767 if ao_texture.image:
1768 ao_texture.image.colorspace_settings.is_data = True
1770 continue
1772 if not active_node.inputs[sname[0]].is_linked:
1773 # No texture node connected -> add texture node with new image
1774 texture_node = nodes.new(type='ShaderNodeTexImage')
1775 img = bpy.data.images.load(path.join(import_path, sname[2]))
1776 texture_node.image = img
1778 # NORMAL NODES
1779 if sname[0] == 'Normal':
1780 # Test if new texture node is normal or bump map
1781 fname_components = split_into_components(sname[2])
1782 match_normal = set(normal_abbr).intersection(set(fname_components))
1783 match_bump = set(bump_abbr).intersection(set(fname_components))
1784 if match_normal:
1785 # If Normal add normal node in between
1786 normal_node = nodes.new(type='ShaderNodeNormalMap')
1787 link = connect_sockets(normal_node.inputs[1], texture_node.outputs[0])
1788 elif match_bump:
1789 # If Bump add bump node in between
1790 normal_node = nodes.new(type='ShaderNodeBump')
1791 link = connect_sockets(normal_node.inputs[2], texture_node.outputs[0])
1793 link = connect_sockets(active_node.inputs[sname[0]], normal_node.outputs[0])
1794 normal_node_texture = texture_node
1796 elif sname[0] == 'Roughness':
1797 # Test if glossy or roughness map
1798 fname_components = split_into_components(sname[2])
1799 match_rough = set(rough_abbr).intersection(set(fname_components))
1800 match_gloss = set(gloss_abbr).intersection(set(fname_components))
1802 if match_rough:
1803 # If Roughness nothing to to
1804 link = connect_sockets(active_node.inputs[sname[0]], texture_node.outputs[0])
1806 elif match_gloss:
1807 # If Gloss Map add invert node
1808 invert_node = nodes.new(type='ShaderNodeInvert')
1809 link = connect_sockets(invert_node.inputs[1], texture_node.outputs[0])
1811 link = connect_sockets(active_node.inputs[sname[0]], invert_node.outputs[0])
1812 roughness_node = texture_node
1814 else:
1815 # This is a simple connection Texture --> Input slot
1816 link = connect_sockets(active_node.inputs[sname[0]], texture_node.outputs[0])
1818 # Use non-color except for color inputs
1819 if sname[0] not in ['Base Color', 'Emission Color'] and texture_node.image:
1820 texture_node.image.colorspace_settings.is_data = True
1822 else:
1823 # If already texture connected. add to node list for alignment
1824 texture_node = active_node.inputs[sname[0]].links[0].from_node
1826 # This are all connected texture nodes
1827 texture_nodes.append(texture_node)
1828 texture_node.label = sname[0]
1830 if disp_texture:
1831 texture_nodes.append(disp_texture)
1833 if ao_texture:
1834 # We want the ambient occlusion texture to be the top most texture node
1835 texture_nodes.insert(0, ao_texture)
1837 # Alignment
1838 for i, texture_node in enumerate(texture_nodes):
1839 offset = Vector((-550, (i * -280) + 200))
1840 texture_node.location = active_node.location + offset
1842 if normal_node:
1843 # Extra alignment if normal node was added
1844 normal_node.location = normal_node_texture.location + Vector((300, 0))
1846 if roughness_node:
1847 # Alignment of invert node if glossy map
1848 invert_node.location = roughness_node.location + Vector((300, 0))
1850 # Add texture input + mapping
1851 mapping = nodes.new(type='ShaderNodeMapping')
1852 mapping.location = active_node.location + Vector((-1050, 0))
1853 if len(texture_nodes) > 1:
1854 # If more than one texture add reroute node in between
1855 reroute = nodes.new(type='NodeReroute')
1856 texture_nodes.append(reroute)
1857 tex_coords = Vector((texture_nodes[0].location.x,
1858 sum(n.location.y for n in texture_nodes) / len(texture_nodes)))
1859 reroute.location = tex_coords + Vector((-50, -120))
1860 for texture_node in texture_nodes:
1861 link = connect_sockets(texture_node.inputs[0], reroute.outputs[0])
1862 link = connect_sockets(reroute.inputs[0], mapping.outputs[0])
1863 else:
1864 link = connect_sockets(texture_nodes[0].inputs[0], mapping.outputs[0])
1866 # Connect texture_coordiantes to mapping node
1867 texture_input = nodes.new(type='ShaderNodeTexCoord')
1868 texture_input.location = mapping.location + Vector((-200, 0))
1869 link = connect_sockets(mapping.inputs[0], texture_input.outputs[2])
1871 # Create frame around tex coords and mapping
1872 frame = nodes.new(type='NodeFrame')
1873 frame.label = 'Mapping'
1874 mapping.parent = frame
1875 texture_input.parent = frame
1876 frame.update()
1878 # Create frame around texture nodes
1879 frame = nodes.new(type='NodeFrame')
1880 frame.label = 'Textures'
1881 for tnode in texture_nodes:
1882 tnode.parent = frame
1883 frame.update()
1885 # Just to be sure
1886 active_node.select = False
1887 nodes.update()
1888 links.update()
1889 force_update(context)
1890 return {'FINISHED'}
1893 class NWAddReroutes(Operator, NWBase):
1894 """Add Reroute Nodes and link them to outputs of selected nodes"""
1895 bl_idname = "node.nw_add_reroutes"
1896 bl_label = "Add Reroutes"
1897 bl_description = "Add Reroutes to Outputs"
1898 bl_options = {'REGISTER', 'UNDO'}
1900 option: EnumProperty(
1901 name="option",
1902 items=[
1903 ('ALL', 'to all', 'Add to all outputs'),
1904 ('LOOSE', 'to loose', 'Add only to loose outputs'),
1905 ('LINKED', 'to linked', 'Add only to linked outputs'),
1909 def execute(self, context):
1910 tree_type = context.space_data.node_tree.type
1911 option = self.option
1912 nodes, links = get_nodes_links(context)
1913 # output valid when option is 'all' or when 'loose' output has no links
1914 valid = False
1915 post_select = [] # nodes to be selected after execution
1916 # create reroutes and recreate links
1917 for node in [n for n in nodes if n.select]:
1918 if node.outputs:
1919 x = node.location.x
1920 y = node.location.y
1921 width = node.width
1922 # unhide 'REROUTE' nodes to avoid issues with location.y
1923 if node.type == 'REROUTE':
1924 node.hide = False
1925 # Hack needed to calculate real width
1926 if node.hide:
1927 bpy.ops.node.select_all(action='DESELECT')
1928 helper = nodes.new('NodeReroute')
1929 helper.select = True
1930 node.select = True
1931 # resize node and helper to zero. Then check locations to calculate width
1932 bpy.ops.transform.resize(value=(0.0, 0.0, 0.0))
1933 width = 2.0 * (helper.location.x - node.location.x)
1934 # restore node location
1935 node.location = x, y
1936 # delete helper
1937 node.select = False
1938 # only helper is selected now
1939 bpy.ops.node.delete()
1940 x = node.location.x + width + 20.0
1941 if node.type != 'REROUTE':
1942 y -= 35.0
1943 y_offset = -22.0
1944 loc = x, y
1945 reroutes_count = 0 # will be used when aligning reroutes added to hidden nodes
1946 for out_i, output in enumerate(node.outputs):
1947 pass_used = False # initial value to be analyzed if 'R_LAYERS'
1948 # if node != 'R_LAYERS' - "pass_used" not needed, so set it to True
1949 if node.type != 'R_LAYERS':
1950 pass_used = True
1951 else: # if 'R_LAYERS' check if output represent used render pass
1952 node_scene = node.scene
1953 node_layer = node.layer
1954 # If output - "Alpha" is analyzed - assume it's used. Not represented in passes.
1955 if output.name == 'Alpha':
1956 pass_used = True
1957 else:
1958 # check entries in global 'rl_outputs' variable
1959 for rlo in rl_outputs:
1960 if output.name in {rlo.output_name, rlo.exr_output_name}:
1961 pass_used = getattr(node_scene.view_layers[node_layer], rlo.render_pass)
1962 break
1963 if pass_used:
1964 valid = ((option == 'ALL') or
1965 (option == 'LOOSE' and not output.links) or
1966 (option == 'LINKED' and output.links))
1967 # Add reroutes only if valid, but offset location in all cases.
1968 if valid:
1969 n = nodes.new('NodeReroute')
1970 nodes.active = n
1971 for link in output.links:
1972 connect_sockets(n.outputs[0], link.to_socket)
1973 connect_sockets(output, n.inputs[0])
1974 n.location = loc
1975 post_select.append(n)
1976 reroutes_count += 1
1977 y += y_offset
1978 loc = x, y
1979 # disselect the node so that after execution of script only newly created nodes are selected
1980 node.select = False
1981 # nicer reroutes distribution along y when node.hide
1982 if node.hide:
1983 y_translate = reroutes_count * y_offset / 2.0 - y_offset - 35.0
1984 for reroute in [r for r in nodes if r.select]:
1985 reroute.location.y -= y_translate
1986 for node in post_select:
1987 node.select = True
1989 return {'FINISHED'}
1992 class NWLinkActiveToSelected(Operator, NWBase):
1993 """Link active node to selected nodes basing on various criteria"""
1994 bl_idname = "node.nw_link_active_to_selected"
1995 bl_label = "Link Active Node to Selected"
1996 bl_options = {'REGISTER', 'UNDO'}
1998 replace: BoolProperty()
1999 use_node_name: BoolProperty()
2000 use_outputs_names: BoolProperty()
2002 @classmethod
2003 def poll(cls, context):
2004 return (nw_check(context)
2005 and context.active_node is not None
2006 and context.active_node.select)
2008 def execute(self, context):
2009 nodes, links = get_nodes_links(context)
2010 replace = self.replace
2011 use_node_name = self.use_node_name
2012 use_outputs_names = self.use_outputs_names
2013 active = nodes.active
2014 selected = [node for node in nodes if node.select and node != active]
2015 outputs = [] # Only usable outputs of active nodes will be stored here.
2016 for out in active.outputs:
2017 if active.type != 'R_LAYERS':
2018 outputs.append(out)
2019 else:
2020 # 'R_LAYERS' node type needs special handling.
2021 # outputs of 'R_LAYERS' are callable even if not seen in UI.
2022 # Only outputs that represent used passes should be taken into account
2023 # Check if pass represented by output is used.
2024 # global 'rl_outputs' list will be used for that
2025 for rlo in rl_outputs:
2026 pass_used = False # initial value. Will be set to True if pass is used
2027 if out.name == 'Alpha':
2028 # Alpha output is always present. Doesn't have representation in render pass. Assume it's used.
2029 pass_used = True
2030 elif out.name in {rlo.output_name, rlo.exr_output_name}:
2031 # example 'render_pass' entry: 'use_pass_uv' Check if True in scene render layers
2032 pass_used = getattr(active.scene.view_layers[active.layer], rlo.render_pass)
2033 break
2034 if pass_used:
2035 outputs.append(out)
2036 doit = True # Will be changed to False when links successfully added to previous output.
2037 for out in outputs:
2038 if doit:
2039 for node in selected:
2040 dst_name = node.name # Will be compared with src_name if needed.
2041 # When node has label - use it as dst_name
2042 if node.label:
2043 dst_name = node.label
2044 valid = True # Initial value. Will be changed to False if names don't match.
2045 src_name = dst_name # If names not used - this assignment will keep valid = True.
2046 if use_node_name:
2047 # Set src_name to source node name or label
2048 src_name = active.name
2049 if active.label:
2050 src_name = active.label
2051 elif use_outputs_names:
2052 src_name = (out.name, )
2053 for rlo in rl_outputs:
2054 if out.name in {rlo.output_name, rlo.exr_output_name}:
2055 src_name = (rlo.output_name, rlo.exr_output_name)
2056 if dst_name not in src_name:
2057 valid = False
2058 if valid:
2059 for input in node.inputs:
2060 if input.type == out.type or node.type == 'REROUTE':
2061 if replace or not input.is_linked:
2062 connect_sockets(out, input)
2063 if not use_node_name and not use_outputs_names:
2064 doit = False
2065 break
2067 return {'FINISHED'}
2070 class NWAlignNodes(Operator, NWBase):
2071 '''Align the selected nodes neatly in a row/column'''
2072 bl_idname = "node.nw_align_nodes"
2073 bl_label = "Align Nodes"
2074 bl_options = {'REGISTER', 'UNDO'}
2075 margin: IntProperty(name='Margin', default=50, description='The amount of space between nodes')
2077 def execute(self, context):
2078 nodes, links = get_nodes_links(context)
2079 margin = self.margin
2081 selection = []
2082 for node in nodes:
2083 if node.select and node.type != 'FRAME':
2084 selection.append(node)
2086 # If no nodes are selected, align all nodes
2087 active_loc = None
2088 if not selection:
2089 selection = nodes
2090 elif nodes.active in selection:
2091 active_loc = copy(nodes.active.location) # make a copy, not a reference
2093 # Check if nodes should be laid out horizontally or vertically
2094 # use dimension to get center of node, not corner
2095 x_locs = [n.location.x + (n.dimensions.x / 2) for n in selection]
2096 y_locs = [n.location.y - (n.dimensions.y / 2) for n in selection]
2097 x_range = max(x_locs) - min(x_locs)
2098 y_range = max(y_locs) - min(y_locs)
2099 mid_x = (max(x_locs) + min(x_locs)) / 2
2100 mid_y = (max(y_locs) + min(y_locs)) / 2
2101 horizontal = x_range > y_range
2103 # Sort selection by location of node mid-point
2104 if horizontal:
2105 selection = sorted(selection, key=lambda n: n.location.x + (n.dimensions.x / 2))
2106 else:
2107 selection = sorted(selection, key=lambda n: n.location.y - (n.dimensions.y / 2), reverse=True)
2109 # Alignment
2110 current_pos = 0
2111 for node in selection:
2112 current_margin = margin
2113 current_margin = current_margin * 0.5 if node.hide else current_margin # use a smaller margin for hidden nodes
2115 if horizontal:
2116 node.location.x = current_pos
2117 current_pos += current_margin + node.dimensions.x
2118 node.location.y = mid_y + (node.dimensions.y / 2)
2119 else:
2120 node.location.y = current_pos
2121 current_pos -= (current_margin * 0.3) + node.dimensions.y # use half-margin for vertical alignment
2122 node.location.x = mid_x - (node.dimensions.x / 2)
2124 # If active node is selected, center nodes around it
2125 if active_loc is not None:
2126 active_loc_diff = active_loc - nodes.active.location
2127 for node in selection:
2128 node.location += active_loc_diff
2129 else: # Position nodes centered around where they used to be
2130 locs = ([n.location.x + (n.dimensions.x / 2) for n in selection]
2131 ) if horizontal else ([n.location.y - (n.dimensions.y / 2) for n in selection])
2132 new_mid = (max(locs) + min(locs)) / 2
2133 for node in selection:
2134 if horizontal:
2135 node.location.x += (mid_x - new_mid)
2136 else:
2137 node.location.y += (mid_y - new_mid)
2139 return {'FINISHED'}
2142 class NWSelectParentChildren(Operator, NWBase):
2143 bl_idname = "node.nw_select_parent_child"
2144 bl_label = "Select Parent or Children"
2145 bl_options = {'REGISTER', 'UNDO'}
2147 option: EnumProperty(
2148 name="option",
2149 items=(
2150 ('PARENT', 'Select Parent', 'Select Parent Frame'),
2151 ('CHILD', 'Select Children', 'Select members of selected frame'),
2155 def execute(self, context):
2156 nodes, links = get_nodes_links(context)
2157 option = self.option
2158 selected = [node for node in nodes if node.select]
2159 if option == 'PARENT':
2160 for sel in selected:
2161 parent = sel.parent
2162 if parent:
2163 parent.select = True
2164 else: # option == 'CHILD'
2165 for sel in selected:
2166 children = [node for node in nodes if node.parent == sel]
2167 for kid in children:
2168 kid.select = True
2170 return {'FINISHED'}
2173 class NWDetachOutputs(Operator, NWBase):
2174 """Detach outputs of selected node leaving inputs linked"""
2175 bl_idname = "node.nw_detach_outputs"
2176 bl_label = "Detach Outputs"
2177 bl_options = {'REGISTER', 'UNDO'}
2179 def execute(self, context):
2180 nodes, links = get_nodes_links(context)
2181 selected = context.selected_nodes
2182 bpy.ops.node.duplicate_move_keep_inputs()
2183 new_nodes = context.selected_nodes
2184 bpy.ops.node.select_all(action="DESELECT")
2185 for node in selected:
2186 node.select = True
2187 bpy.ops.node.delete_reconnect()
2188 for new_node in new_nodes:
2189 new_node.select = True
2190 bpy.ops.transform.translate('INVOKE_DEFAULT')
2192 return {'FINISHED'}
2195 class NWLinkToOutputNode(Operator):
2196 """Link to Composite node or Material Output node"""
2197 bl_idname = "node.nw_link_out"
2198 bl_label = "Connect to Output"
2199 bl_options = {'REGISTER', 'UNDO'}
2201 @classmethod
2202 def poll(cls, context):
2203 """Disabled for custom nodes as we do not know which nodes are outputs."""
2204 return (nw_check(context)
2205 and nw_check_space_type(cls, context, 'ShaderNodeTree', 'CompositorNodeTree',
2206 'TextureNodeTree', 'GeometryNodeTree')
2207 and context.active_node is not None
2208 and any(is_visible_socket(out) for out in context.active_node.outputs))
2210 def execute(self, context):
2211 nodes, links = get_nodes_links(context)
2212 active = nodes.active
2213 output_index = None
2214 tree_type = context.space_data.tree_type
2215 shader_outputs = {'OBJECT': 'ShaderNodeOutputMaterial',
2216 'WORLD': 'ShaderNodeOutputWorld',
2217 'LINESTYLE': 'ShaderNodeOutputLineStyle'}
2218 output_type = {
2219 'ShaderNodeTree': shader_outputs[context.space_data.shader_type],
2220 'CompositorNodeTree': 'CompositorNodeComposite',
2221 'TextureNodeTree': 'TextureNodeOutput',
2222 'GeometryNodeTree': 'NodeGroupOutput',
2223 }[tree_type]
2224 for node in nodes:
2225 # check whether the node is an output node and,
2226 # if supported, whether it's the active one
2227 if node.rna_type.identifier == output_type \
2228 and (node.is_active_output if hasattr(node, 'is_active_output')
2229 else True):
2230 output_node = node
2231 break
2232 else: # No output node exists
2233 bpy.ops.node.select_all(action="DESELECT")
2234 output_node = nodes.new(output_type)
2235 output_node.location.x = active.location.x + active.dimensions.x + 80
2236 output_node.location.y = active.location.y
2238 if active.outputs:
2239 for i, output in enumerate(active.outputs):
2240 if is_visible_socket(output):
2241 output_index = i
2242 break
2243 for i, output in enumerate(active.outputs):
2244 if output.type == output_node.inputs[0].type and is_visible_socket(output):
2245 output_index = i
2246 break
2248 out_input_index = 0
2249 if tree_type == 'ShaderNodeTree':
2250 if active.outputs[output_index].name == 'Volume':
2251 out_input_index = 1
2252 elif active.outputs[output_index].name == 'Displacement':
2253 out_input_index = 2
2254 elif tree_type == 'GeometryNodeTree':
2255 if active.outputs[output_index].type != 'GEOMETRY':
2256 return {'CANCELLED'}
2257 connect_sockets(active.outputs[output_index], output_node.inputs[out_input_index])
2259 force_update(context) # viewport render does not update
2261 return {'FINISHED'}
2264 class NWMakeLink(Operator, NWBase):
2265 """Make a link from one socket to another"""
2266 bl_idname = 'node.nw_make_link'
2267 bl_label = 'Make Link'
2268 bl_options = {'REGISTER', 'UNDO'}
2269 from_socket: IntProperty()
2270 to_socket: IntProperty()
2272 def execute(self, context):
2273 nodes, links = get_nodes_links(context)
2275 n1 = nodes[context.scene.NWLazySource]
2276 n2 = nodes[context.scene.NWLazyTarget]
2278 connect_sockets(n1.outputs[self.from_socket], n2.inputs[self.to_socket])
2280 force_update(context)
2282 return {'FINISHED'}
2285 class NWCallInputsMenu(Operator, NWBase):
2286 """Link from this output"""
2287 bl_idname = 'node.nw_call_inputs_menu'
2288 bl_label = 'Make Link'
2289 bl_options = {'REGISTER', 'UNDO'}
2290 from_socket: IntProperty()
2292 def execute(self, context):
2293 nodes, links = get_nodes_links(context)
2295 context.scene.NWSourceSocket = self.from_socket
2297 n1 = nodes[context.scene.NWLazySource]
2298 n2 = nodes[context.scene.NWLazyTarget]
2299 if len(n2.inputs) > 1:
2300 bpy.ops.wm.call_menu("INVOKE_DEFAULT", name=NWConnectionListInputs.bl_idname)
2301 elif len(n2.inputs) == 1:
2302 connect_sockets(n1.outputs[self.from_socket], n2.inputs[0])
2303 return {'FINISHED'}
2306 class NWAddSequence(Operator, NWBase, ImportHelper):
2307 """Add an Image Sequence"""
2308 bl_idname = 'node.nw_add_sequence'
2309 bl_label = 'Import Image Sequence'
2310 bl_options = {'REGISTER', 'UNDO'}
2312 directory: StringProperty(
2313 subtype="DIR_PATH"
2315 filename: StringProperty(
2316 subtype="FILE_NAME"
2318 files: CollectionProperty(
2319 type=bpy.types.OperatorFileListElement,
2320 options={'HIDDEN', 'SKIP_SAVE'}
2322 relative_path: BoolProperty(
2323 name='Relative Path',
2324 description='Set the file path relative to the blend file, when possible',
2325 default=True
2328 def draw(self, context):
2329 layout = self.layout
2330 layout.alignment = 'LEFT'
2332 layout.prop(self, 'relative_path')
2334 def execute(self, context):
2335 nodes, links = get_nodes_links(context)
2336 directory = self.directory
2337 filename = self.filename
2338 files = self.files
2339 tree = context.space_data.node_tree
2341 # DEBUG
2342 # print ("\nDIR:", directory)
2343 # print ("FN:", filename)
2344 # print ("Fs:", list(f.name for f in files), '\n')
2346 if tree.type == 'SHADER':
2347 node_type = "ShaderNodeTexImage"
2348 elif tree.type == 'COMPOSITING':
2349 node_type = "CompositorNodeImage"
2350 else:
2351 self.report({'ERROR'}, "Unsupported Node Tree type!")
2352 return {'CANCELLED'}
2354 if not files[0].name and not filename:
2355 self.report({'ERROR'}, "No file chosen")
2356 return {'CANCELLED'}
2357 elif files[0].name and (not filename or not path.exists(directory + filename)):
2358 # User has selected multiple files without an active one, or the active one is non-existent
2359 filename = files[0].name
2361 if not path.exists(directory + filename):
2362 self.report({'ERROR'}, filename + " does not exist!")
2363 return {'CANCELLED'}
2365 without_ext = '.'.join(filename.split('.')[:-1])
2367 # if last digit isn't a number, it's not a sequence
2368 if not without_ext[-1].isdigit():
2369 self.report({'ERROR'}, filename + " does not seem to be part of a sequence")
2370 return {'CANCELLED'}
2372 extension = filename.split('.')[-1]
2373 reverse = without_ext[::-1] # reverse string
2375 count_numbers = 0
2376 for char in reverse:
2377 if char.isdigit():
2378 count_numbers += 1
2379 else:
2380 break
2382 without_num = without_ext[:count_numbers * -1]
2384 files = sorted(glob(directory + without_num + "[0-9]" * count_numbers + "." + extension))
2386 num_frames = len(files)
2388 nodes_list = [node for node in nodes]
2389 if nodes_list:
2390 nodes_list.sort(key=lambda k: k.location.x)
2391 xloc = nodes_list[0].location.x - 220 # place new nodes at far left
2392 yloc = 0
2393 for node in nodes:
2394 node.select = False
2395 yloc += node_mid_pt(node, 'y')
2396 yloc = yloc / len(nodes)
2397 else:
2398 xloc = 0
2399 yloc = 0
2401 name_with_hashes = without_num + "#" * count_numbers + '.' + extension
2403 bpy.ops.node.add_node('INVOKE_DEFAULT', use_transform=True, type=node_type)
2404 node = nodes.active
2405 node.label = name_with_hashes
2407 filepath = directory + (without_ext + '.' + extension)
2408 if self.relative_path:
2409 if bpy.data.filepath:
2410 try:
2411 filepath = bpy.path.relpath(filepath)
2412 except ValueError:
2413 pass
2415 img = bpy.data.images.load(filepath)
2416 img.source = 'SEQUENCE'
2417 img.name = name_with_hashes
2418 node.image = img
2419 image_user = node.image_user if tree.type == 'SHADER' else node
2420 # separate the number from the file name of the first file
2421 image_user.frame_offset = int(files[0][len(without_num) + len(directory):-1 * (len(extension) + 1)]) - 1
2422 image_user.frame_duration = num_frames
2424 return {'FINISHED'}
2427 class NWAddMultipleImages(Operator, NWBase, ImportHelper):
2428 """Add multiple images at once"""
2429 bl_idname = 'node.nw_add_multiple_images'
2430 bl_label = 'Open Selected Images'
2431 bl_options = {'REGISTER', 'UNDO'}
2432 directory: StringProperty(
2433 subtype="DIR_PATH"
2435 files: CollectionProperty(
2436 type=bpy.types.OperatorFileListElement,
2437 options={'HIDDEN', 'SKIP_SAVE'}
2440 def execute(self, context):
2441 nodes, links = get_nodes_links(context)
2443 xloc, yloc = context.region.view2d.region_to_view(context.area.width / 2, context.area.height / 2)
2445 if context.space_data.node_tree.type == 'SHADER':
2446 node_type = "ShaderNodeTexImage"
2447 elif context.space_data.node_tree.type == 'COMPOSITING':
2448 node_type = "CompositorNodeImage"
2449 else:
2450 self.report({'ERROR'}, "Unsupported Node Tree type!")
2451 return {'CANCELLED'}
2453 new_nodes = []
2454 for f in self.files:
2455 fname = f.name
2457 node = nodes.new(node_type)
2458 new_nodes.append(node)
2459 node.label = fname
2460 node.hide = True
2461 node.location.x = xloc
2462 node.location.y = yloc
2463 yloc -= 40
2465 img = bpy.data.images.load(self.directory + fname)
2466 node.image = img
2468 # shift new nodes up to center of tree
2469 list_size = new_nodes[0].location.y - new_nodes[-1].location.y
2470 for node in nodes:
2471 if node in new_nodes:
2472 node.select = True
2473 node.location.y += (list_size / 2)
2474 else:
2475 node.select = False
2476 return {'FINISHED'}
2479 class NWViewerFocus(bpy.types.Operator):
2480 """Set the viewer tile center to the mouse position"""
2481 bl_idname = "node.nw_viewer_focus"
2482 bl_label = "Viewer Focus"
2484 x: bpy.props.IntProperty()
2485 y: bpy.props.IntProperty()
2487 @classmethod
2488 def poll(cls, context):
2489 return (nw_check(context)
2490 and nw_check_space_type(cls, context, 'CompositorNodeTree'))
2492 def execute(self, context):
2493 return {'FINISHED'}
2495 def invoke(self, context, event):
2496 render = context.scene.render
2497 space = context.space_data
2498 percent = render.resolution_percentage * 0.01
2500 nodes, links = get_nodes_links(context)
2501 viewers = [n for n in nodes if n.type == 'VIEWER']
2503 if viewers:
2504 mlocx = event.mouse_region_x
2505 mlocy = event.mouse_region_y
2506 select_node = bpy.ops.node.select(location=(mlocx, mlocy), extend=False)
2508 if 'FINISHED' not in select_node: # only run if we're not clicking on a node
2509 region_x = context.region.width
2510 region_y = context.region.height
2512 region_center_x = context.region.width / 2
2513 region_center_y = context.region.height / 2
2515 bd_x = render.resolution_x * percent * space.backdrop_zoom
2516 bd_y = render.resolution_y * percent * space.backdrop_zoom
2518 backdrop_center_x = (bd_x / 2) - space.backdrop_offset[0]
2519 backdrop_center_y = (bd_y / 2) - space.backdrop_offset[1]
2521 margin_x = region_center_x - backdrop_center_x
2522 margin_y = region_center_y - backdrop_center_y
2524 abs_mouse_x = (mlocx - margin_x) / bd_x
2525 abs_mouse_y = (mlocy - margin_y) / bd_y
2527 for node in viewers:
2528 node.center_x = abs_mouse_x
2529 node.center_y = abs_mouse_y
2530 else:
2531 return {'PASS_THROUGH'}
2533 return self.execute(context)
2536 class NWSaveViewer(bpy.types.Operator, ExportHelper):
2537 """Save the current viewer node to an image file"""
2538 bl_idname = "node.nw_save_viewer"
2539 bl_label = "Save This Image"
2540 filepath: StringProperty(subtype="FILE_PATH")
2541 filename_ext: EnumProperty(
2542 name="Format",
2543 description="Choose the file format to save to",
2544 items=(('.bmp', "BMP", ""),
2545 ('.rgb', 'IRIS', ""),
2546 ('.png', 'PNG', ""),
2547 ('.jpg', 'JPEG', ""),
2548 ('.jp2', 'JPEG2000', ""),
2549 ('.tga', 'TARGA', ""),
2550 ('.cin', 'CINEON', ""),
2551 ('.dpx', 'DPX', ""),
2552 ('.exr', 'OPEN_EXR', ""),
2553 ('.hdr', 'HDR', ""),
2554 ('.tif', 'TIFF', "")),
2555 default='.png',
2558 @classmethod
2559 def poll(cls, context):
2560 return (nw_check(context)
2561 and nw_check_space_type(cls, context, 'CompositorNodeTree')
2562 and any(img.source == 'VIEWER'
2563 and img.render_slots == 0
2564 for img in bpy.data.images)
2565 and sum(bpy.data.images["Viewer Node"].size) > 0) # False if not connected or connected but no image
2567 def execute(self, context):
2568 fp = self.filepath
2569 if fp:
2570 formats = {
2571 '.bmp': 'BMP',
2572 '.rgb': 'IRIS',
2573 '.png': 'PNG',
2574 '.jpg': 'JPEG',
2575 '.jpeg': 'JPEG',
2576 '.jp2': 'JPEG2000',
2577 '.tga': 'TARGA',
2578 '.cin': 'CINEON',
2579 '.dpx': 'DPX',
2580 '.exr': 'OPEN_EXR',
2581 '.hdr': 'HDR',
2582 '.tiff': 'TIFF',
2583 '.tif': 'TIFF'}
2584 basename, ext = path.splitext(fp)
2585 old_render_format = context.scene.render.image_settings.file_format
2586 context.scene.render.image_settings.file_format = formats[self.filename_ext]
2587 context.area.type = "IMAGE_EDITOR"
2588 context.area.spaces[0].image = bpy.data.images['Viewer Node']
2589 context.area.spaces[0].image.save_render(fp)
2590 context.area.type = "NODE_EDITOR"
2591 context.scene.render.image_settings.file_format = old_render_format
2592 return {'FINISHED'}
2595 class NWResetNodes(bpy.types.Operator):
2596 """Reset Nodes in Selection"""
2597 bl_idname = "node.nw_reset_nodes"
2598 bl_label = "Reset Nodes"
2599 bl_options = {'REGISTER', 'UNDO'}
2601 @classmethod
2602 def poll(cls, context):
2603 space = context.space_data
2604 return space.type == 'NODE_EDITOR'
2606 def execute(self, context):
2607 node_active = context.active_node
2608 node_selected = context.selected_nodes
2609 node_ignore = ["FRAME", "REROUTE", "GROUP", "SIMULATION_INPUT", "SIMULATION_OUTPUT"]
2611 # Check if one node is selected at least
2612 if not (len(node_selected) > 0):
2613 self.report({'ERROR'}, "1 node must be selected at least")
2614 return {'CANCELLED'}
2616 active_node_name = node_active.name if node_active.select else None
2617 valid_nodes = [n for n in node_selected if n.type not in node_ignore]
2619 # Create output lists
2620 selected_node_names = [n.name for n in node_selected]
2621 success_names = []
2623 # Reset all valid children in a frame
2624 node_active_is_frame = False
2625 if len(node_selected) == 1 and node_active.type == "FRAME":
2626 node_tree = node_active.id_data
2627 children = [n for n in node_tree.nodes if n.parent == node_active]
2628 if children:
2629 valid_nodes = [n for n in children if n.type not in node_ignore]
2630 selected_node_names = [n.name for n in children if n.type not in node_ignore]
2631 node_active_is_frame = True
2633 # Check if valid nodes in selection
2634 if not (len(valid_nodes) > 0):
2635 # Check for frames only
2636 frames_selected = [n for n in node_selected if n.type == "FRAME"]
2637 if (len(frames_selected) > 1 and len(frames_selected) == len(node_selected)):
2638 self.report({'ERROR'}, "Please select only 1 frame to reset")
2639 else:
2640 self.report({'ERROR'}, "No valid node(s) in selection")
2641 return {'CANCELLED'}
2643 # Report nodes that are not valid
2644 if len(valid_nodes) != len(node_selected) and node_active_is_frame is False:
2645 valid_node_names = [n.name for n in valid_nodes]
2646 not_valid_names = list(set(selected_node_names) - set(valid_node_names))
2647 self.report({'INFO'}, "Ignored {}".format(", ".join(not_valid_names)))
2649 # Deselect all nodes
2650 for i in node_selected:
2651 i.select = False
2653 # Run through all valid nodes
2654 for node in valid_nodes:
2656 parent = node.parent if node.parent else None
2657 node_loc = [node.location.x, node.location.y]
2659 node_tree = node.id_data
2660 props_to_copy = 'bl_idname name location height width'.split(' ')
2662 reconnections = []
2663 mappings = chain.from_iterable([node.inputs, node.outputs])
2664 for i in (i for i in mappings if i.is_linked):
2665 for L in i.links:
2666 reconnections.append([L.from_socket.path_from_id(), L.to_socket.path_from_id()])
2668 props = {j: getattr(node, j) for j in props_to_copy}
2670 new_node = node_tree.nodes.new(props['bl_idname'])
2671 props_to_copy.pop(0)
2673 for prop in props_to_copy:
2674 setattr(new_node, prop, props[prop])
2676 nodes = node_tree.nodes
2677 nodes.remove(node)
2678 new_node.name = props['name']
2680 if parent:
2681 new_node.parent = parent
2682 new_node.location = node_loc
2684 for str_from, str_to in reconnections:
2685 connect_sockets(eval(str_from), eval(str_to))
2687 new_node.select = False
2688 success_names.append(new_node.name)
2690 # Reselect all nodes
2691 if selected_node_names and node_active_is_frame is False:
2692 for i in selected_node_names:
2693 node_tree.nodes[i].select = True
2695 if active_node_name is not None:
2696 node_tree.nodes[active_node_name].select = True
2697 node_tree.nodes.active = node_tree.nodes[active_node_name]
2699 self.report({'INFO'}, "Successfully reset {}".format(", ".join(success_names)))
2700 return {'FINISHED'}
2703 classes = (
2704 NWLazyMix,
2705 NWLazyConnect,
2706 NWDeleteUnused,
2707 NWSwapLinks,
2708 NWResetBG,
2709 NWAddAttrNode,
2710 NWPreviewNode,
2711 NWFrameSelected,
2712 NWReloadImages,
2713 NWMergeNodes,
2714 NWBatchChangeNodes,
2715 NWChangeMixFactor,
2716 NWCopySettings,
2717 NWCopyLabel,
2718 NWClearLabel,
2719 NWModifyLabels,
2720 NWAddTextureSetup,
2721 NWAddPrincipledSetup,
2722 NWAddReroutes,
2723 NWLinkActiveToSelected,
2724 NWAlignNodes,
2725 NWSelectParentChildren,
2726 NWDetachOutputs,
2727 NWLinkToOutputNode,
2728 NWMakeLink,
2729 NWCallInputsMenu,
2730 NWAddSequence,
2731 NWAddMultipleImages,
2732 NWViewerFocus,
2733 NWSaveViewer,
2734 NWResetNodes,
2738 def register():
2739 from bpy.utils import register_class
2740 for cls in classes:
2741 register_class(cls)
2744 def unregister():
2745 from bpy.utils import unregister_class
2747 for cls in classes:
2748 unregister_class(cls)