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