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