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