Fix: Node Wrangler: non-color bump and normal in principled setup
[blender-addons.git] / node_wrangler / operators.py
blob3be14929fc10c82a8596ea04f411d46a87b55fcf
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) and nw_check_not_empty(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.has_socket_other_users(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(node_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 has_socket_other_users(self, socket):
629 """List the other users for this socket (other materials or GN groups)"""
630 if not hasattr(self, "other_viewer_sockets_users"):
631 self.other_viewer_sockets_users = []
632 if socket.socket_type == 'NodeSocketShader':
633 for mat in bpy.data.materials:
634 if mat.node_tree == bpy.context.space_data.node_tree or not hasattr(mat.node_tree, "nodes"):
635 continue
636 # Get viewer node
637 output_node = get_group_output_node(mat.node_tree,
638 output_node_type=self.shader_output_type)
639 if output_node is not None:
640 self.search_sockets(output_node, self.other_viewer_sockets_users)
641 elif socket.socket_type == 'NodeSocketGeometry':
642 for obj in bpy.data.objects:
643 for mod in obj.modifiers:
644 if mod.type != 'NODES' or mod.node_group == bpy.context.space_data.node_tree:
645 continue
646 # Get viewer node
647 output_node = get_group_output_node(mod.node_group)
648 if output_node is not None:
649 self.search_sockets(output_node, self.other_viewer_sockets_users)
650 return socket in self.other_viewer_sockets_users
652 def get_output_index(self, node, output_node, is_base_node_tree, socket_type, check_type=False):
653 """Get the next available output socket in the active node"""
654 out_i = None
655 valid_outputs = []
656 for i, out in enumerate(node.outputs):
657 if is_visible_socket(out) and (not check_type or out.type == socket_type):
658 valid_outputs.append(i)
659 if valid_outputs:
660 out_i = valid_outputs[0] # Start index of node's outputs
661 for i, valid_i in enumerate(valid_outputs):
662 for out_link in node.outputs[valid_i].links:
663 if is_viewer_link(out_link, output_node):
664 if is_base_node_tree or self.link_leads_to_used_socket(out_link):
665 if i < len(valid_outputs) - 1:
666 out_i = valid_outputs[i + 1]
667 else:
668 out_i = valid_outputs[0]
669 return out_i
671 def create_links(self, path, node, active_node_socket_id, socket_type):
672 """Create links at each step in the node group path."""
673 path = list(reversed(path))
674 # Starting from the level of the active node
675 for path_index, path_element in enumerate(path[:-1]):
676 # Ensure there is a viewer node and it has an input
677 tree = path_element.node_tree
678 viewer_socket = self.ensure_viewer_socket(
679 tree, socket_type,
680 connect_socket = node.outputs[active_node_socket_id]
681 if path_index == 0 else None)
682 if viewer_socket in self.delete_sockets:
683 self.delete_sockets.remove(viewer_socket)
685 # Connect the current to its viewer
686 link_start = node.outputs[active_node_socket_id]
687 link_end = self.ensure_group_output(tree).inputs[viewer_socket.identifier]
688 connect_sockets(link_start, link_end)
690 # Go up in the node group hierarchy
691 next_tree = path[path_index + 1].node_tree
692 node = next(n for n in next_tree.nodes
693 if n.type == 'GROUP'
694 and n.node_tree == tree)
695 tree = next_tree
696 active_node_socket_id = viewer_socket.identifier
697 return node.outputs[active_node_socket_id]
699 def cleanup(self):
700 # Delete sockets
701 for socket in self.delete_sockets:
702 if not self.has_socket_other_users(socket):
703 tree = socket.id_data
704 self.remove_socket(tree, socket)
706 def invoke(self, context, event):
707 space = context.space_data
708 # Ignore operator when running in wrong context.
709 if self.run_in_geometry_nodes != (space.tree_type == "GeometryNodeTree"):
710 return {'PASS_THROUGH'}
712 mlocx = event.mouse_region_x
713 mlocy = event.mouse_region_y
714 select_node = bpy.ops.node.select(location=(mlocx, mlocy), extend=False)
715 if 'FINISHED' not in select_node: # only run if mouse click is on a node
716 return {'CANCELLED'}
718 base_node_tree = space.node_tree
719 active_tree = context.space_data.edit_tree
720 path = context.space_data.path
721 nodes = active_tree.nodes
722 active = nodes.active
724 if not active and not any(is_visible_socket(out) for out in active.outputs):
725 return {'CANCELLED'}
727 # Scan through all nodes in tree including nodes inside of groups to find viewer sockets
728 self.delete_sockets = []
729 self.scan_nodes(base_node_tree, self.delete_sockets)
731 if not active.outputs:
732 self.cleanup()
733 return {'CANCELLED'}
735 # For geometry node trees, we just connect to the group output
736 if space.tree_type == "GeometryNodeTree":
737 socket_type = 'NodeSocketGeometry'
739 # Find (or create if needed) the output of this node tree
740 output_node = self.ensure_group_output(base_node_tree)
742 active_node_socket_index = self.get_output_index(
743 active, output_node, base_node_tree == active_tree, 'GEOMETRY', check_type=True
745 # If there is no 'GEOMETRY' output type - We can't preview the node
746 if active_node_socket_index is None:
747 return {'CANCELLED'}
749 # Find an input socket of the output of type geometry
750 output_node_socket_index = None
751 for i, inp in enumerate(output_node.inputs):
752 if inp.type == 'GEOMETRY':
753 output_node_socket_index = i
754 break
755 if output_node_socket_index is None:
756 # Create geometry socket
757 geometry_out_socket = base_node_tree.interface.new_socket(
758 'Geometry', in_out='OUTPUT', socket_type=socket_type
760 output_node_socket_index = geometry_out_socket.index
762 # For shader node trees, we connect to a material output
763 elif space.tree_type == "ShaderNodeTree":
764 socket_type = 'NodeSocketShader'
765 self.init_shader_variables(space, space.shader_type)
767 # Get or create material_output node
768 output_node = get_group_output_node(base_node_tree,
769 output_node_type=self.shader_output_type)
770 if not output_node:
771 output_node = base_node_tree.nodes.new(self.shader_output_ident)
772 output_node.location = get_output_location(base_node_tree)
773 output_node.select = False
775 active_node_socket_index = self.get_output_index(
776 active, output_node, base_node_tree == active_tree, 'SHADER'
779 # Cancel if no socket was found. This can happen for group input
780 # nodes with only a virtual socket output.
781 if active_node_socket_index is None:
782 return {'CANCELLED'}
784 if active.outputs[active_node_socket_index].name == "Volume":
785 output_node_socket_index = 1
786 else:
787 output_node_socket_index = 0
789 # If there are no nested node groups, the link starts at the active node
790 node_output = active.outputs[active_node_socket_index]
791 if len(path) > 1:
792 # Recursively connect inside nested node groups and get the one from base level
793 node_output = self.create_links(path, active, active_node_socket_index, socket_type)
794 output_node_input = output_node.inputs[output_node_socket_index]
796 # Connect at base level
797 connect_sockets(node_output, output_node_input)
799 self.cleanup()
800 nodes.active = active
801 active.select = True
802 force_update(context)
803 return {'FINISHED'}
806 class NWFrameSelected(Operator, NWBase):
807 bl_idname = "node.nw_frame_selected"
808 bl_label = "Frame Selected"
809 bl_description = "Add a frame node and parent the selected nodes to it"
810 bl_options = {'REGISTER', 'UNDO'}
812 label_prop: StringProperty(
813 name='Label',
814 description='The visual name of the frame node',
815 default=' '
817 use_custom_color_prop: BoolProperty(
818 name="Custom Color",
819 description="Use custom color for the frame node",
820 default=False
822 color_prop: FloatVectorProperty(
823 name="Color",
824 description="The color of the frame node",
825 default=(0.604, 0.604, 0.604),
826 min=0, max=1, step=1, precision=3,
827 subtype='COLOR_GAMMA', size=3
830 def draw(self, context):
831 layout = self.layout
832 layout.prop(self, 'label_prop')
833 layout.prop(self, 'use_custom_color_prop')
834 col = layout.column()
835 col.active = self.use_custom_color_prop
836 col.prop(self, 'color_prop', text="")
838 def execute(self, context):
839 nodes, links = get_nodes_links(context)
840 selected = []
841 for node in nodes:
842 if node.select:
843 selected.append(node)
845 bpy.ops.node.add_node(type='NodeFrame')
846 frm = nodes.active
847 frm.label = self.label_prop
848 frm.use_custom_color = self.use_custom_color_prop
849 frm.color = self.color_prop
851 for node in selected:
852 node.parent = frm
854 return {'FINISHED'}
857 class NWReloadImages(Operator):
858 bl_idname = "node.nw_reload_images"
859 bl_label = "Reload Images"
860 bl_description = "Update all the image nodes to match their files on disk"
862 @classmethod
863 def poll(cls, context):
864 """Disabled for custom nodes."""
865 return (nw_check(cls, context)
866 and nw_check_space_type(cls, context, {'ShaderNodeTree', 'CompositorNodeTree',
867 'TextureNodeTree', 'GeometryNodeTree'}))
869 def execute(self, context):
870 nodes, links = get_nodes_links(context)
871 image_types = ["IMAGE", "TEX_IMAGE", "TEX_ENVIRONMENT", "TEXTURE"]
872 num_reloaded = 0
873 for node in nodes:
874 if node.type in image_types:
875 if node.type == "TEXTURE":
876 if node.texture: # node has texture assigned
877 if node.texture.type in ['IMAGE', 'ENVIRONMENT_MAP']:
878 if node.texture.image: # texture has image assigned
879 node.texture.image.reload()
880 num_reloaded += 1
881 else:
882 if node.image:
883 node.image.reload()
884 num_reloaded += 1
886 if num_reloaded:
887 self.report({'INFO'}, "Reloaded images")
888 print("Reloaded " + str(num_reloaded) + " images")
889 force_update(context)
890 return {'FINISHED'}
891 else:
892 self.report({'WARNING'}, "No images found to reload in this node tree")
893 return {'CANCELLED'}
896 class NWMergeNodes(Operator, NWBase):
897 bl_idname = "node.nw_merge_nodes"
898 bl_label = "Merge Nodes"
899 bl_description = "Merge Selected Nodes"
900 bl_options = {'REGISTER', 'UNDO'}
902 mode: EnumProperty(
903 name="mode",
904 description="All possible blend types, boolean operations and math operations",
905 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],
907 merge_type: EnumProperty(
908 name="merge type",
909 description="Type of Merge to be used",
910 items=(
911 ('AUTO', 'Auto', 'Automatic Output Type Detection'),
912 ('SHADER', 'Shader', 'Merge using ADD or MIX Shader'),
913 ('GEOMETRY', 'Geometry', 'Merge using Mesh Boolean or Join Geometry Node'),
914 ('MIX', 'Mix Node', 'Merge using Mix Nodes'),
915 ('MATH', 'Math Node', 'Merge using Math Nodes'),
916 ('ZCOMBINE', 'Z-Combine Node', 'Merge using Z-Combine Nodes'),
917 ('ALPHAOVER', 'Alpha Over Node', 'Merge using Alpha Over Nodes'),
921 # Check if the link connects to a node that is in selected_nodes
922 # If not, then check recursively for each link in the nodes outputs.
923 # If yes, return True. If the recursion stops without finding a node
924 # in selected_nodes, it returns False. The depth is used to prevent
925 # getting stuck in a loop because of an already present cycle.
926 @staticmethod
927 def link_creates_cycle(link, selected_nodes, depth=0) -> bool:
928 if depth > 255:
929 # We're stuck in a cycle, but that cycle was already present,
930 # so we return False.
931 # NOTE: The number 255 is arbitrary, but seems to work well.
932 return False
933 node = link.to_node
934 if node in selected_nodes:
935 return True
936 if not node.outputs:
937 return False
938 for output in node.outputs:
939 if output.is_linked:
940 for olink in output.links:
941 if NWMergeNodes.link_creates_cycle(olink, selected_nodes, depth + 1):
942 return True
943 # None of the outputs found a node in selected_nodes, so there is no cycle.
944 return False
946 # Merge the nodes in `nodes_list` with a node of type `node_name` that has a multi_input socket.
947 # The parameters `socket_indices` gives the indices of the node sockets in the order that they should
948 # be connected. The last one is assumed to be a multi input socket.
949 # For convenience the node is returned.
950 @staticmethod
951 def merge_with_multi_input(nodes_list, merge_position, do_hide, loc_x, links, nodes, node_name, socket_indices):
952 # The y-location of the last node
953 loc_y = nodes_list[-1][2]
954 if merge_position == 'CENTER':
955 # Average the y-location
956 for i in range(len(nodes_list) - 1):
957 loc_y += nodes_list[i][2]
958 loc_y = loc_y / len(nodes_list)
959 new_node = nodes.new(node_name)
960 new_node.hide = do_hide
961 new_node.location.x = loc_x
962 new_node.location.y = loc_y
963 selected_nodes = [nodes[node_info[0]] for node_info in nodes_list]
964 prev_links = []
965 outputs_for_multi_input = []
966 for i, node in enumerate(selected_nodes):
967 node.select = False
968 # Search for the first node which had output links that do not create
969 # a cycle, which we can then reconnect afterwards.
970 if prev_links == [] and node.outputs[0].is_linked:
971 prev_links = [
972 link for link in node.outputs[0].links if not NWMergeNodes.link_creates_cycle(
973 link, selected_nodes)]
974 # Get the index of the socket, the last one is a multi input, and is thus used repeatedly
975 # To get the placement to look right we need to reverse the order in which we connect the
976 # outputs to the multi input socket.
977 if i < len(socket_indices) - 1:
978 ind = socket_indices[i]
979 connect_sockets(node.outputs[0], new_node.inputs[ind])
980 else:
981 outputs_for_multi_input.insert(0, node.outputs[0])
982 if outputs_for_multi_input != []:
983 ind = socket_indices[-1]
984 for output in outputs_for_multi_input:
985 connect_sockets(output, new_node.inputs[ind])
986 if prev_links != []:
987 for link in prev_links:
988 connect_sockets(new_node.outputs[0], link.to_node.inputs[0])
989 return new_node
991 @classmethod
992 def poll(cls, context):
993 return (nw_check(cls, context)
994 and nw_check_space_type(cls, context, {'ShaderNodeTree', 'CompositorNodeTree',
995 'TextureNodeTree', 'GeometryNodeTree'})
996 and nw_check_selected(cls, context))
998 def execute(self, context):
999 settings = context.preferences.addons[__package__].preferences
1000 merge_hide = settings.merge_hide
1001 merge_position = settings.merge_position # 'center' or 'bottom'
1003 do_hide = False
1004 do_hide_shader = False
1005 if merge_hide == 'ALWAYS':
1006 do_hide = True
1007 do_hide_shader = True
1008 elif merge_hide == 'NON_SHADER':
1009 do_hide = True
1011 tree_type = context.space_data.node_tree.type
1012 if tree_type == 'GEOMETRY':
1013 node_type = 'GeometryNode'
1014 if tree_type == 'COMPOSITING':
1015 node_type = 'CompositorNode'
1016 elif tree_type == 'SHADER':
1017 node_type = 'ShaderNode'
1018 elif tree_type == 'TEXTURE':
1019 node_type = 'TextureNode'
1020 nodes, links = get_nodes_links(context)
1021 mode = self.mode
1022 merge_type = self.merge_type
1023 # Prevent trying to add Z-Combine in not 'COMPOSITING' node tree.
1024 # 'ZCOMBINE' works only if mode == 'MIX'
1025 # Setting mode to None prevents trying to add 'ZCOMBINE' node.
1026 if (merge_type == 'ZCOMBINE' or merge_type == 'ALPHAOVER') and tree_type != 'COMPOSITING':
1027 merge_type = 'MIX'
1028 mode = 'MIX'
1029 if (merge_type != 'MATH' and merge_type != 'GEOMETRY') and tree_type == 'GEOMETRY':
1030 merge_type = 'AUTO'
1031 # The Mix node and math nodes used for geometry nodes are of type 'ShaderNode'
1032 if (merge_type == 'MATH' or merge_type == 'MIX') and tree_type == 'GEOMETRY':
1033 node_type = 'ShaderNode'
1034 selected_mix = [] # entry = [index, loc]
1035 selected_shader = [] # entry = [index, loc]
1036 selected_geometry = [] # entry = [index, loc]
1037 selected_math = [] # entry = [index, loc]
1038 selected_vector = [] # entry = [index, loc]
1039 selected_z = [] # entry = [index, loc]
1040 selected_alphaover = [] # entry = [index, loc]
1042 for i, node in enumerate(nodes):
1043 if node.select and node.outputs:
1044 if merge_type == 'AUTO':
1045 for (type, types_list, dst) in (
1046 ('SHADER', ('MIX', 'ADD'), selected_shader),
1047 ('GEOMETRY', [t[0] for t in geo_combine_operations], selected_geometry),
1048 ('RGBA', [t[0] for t in blend_types], selected_mix),
1049 ('VALUE', [t[0] for t in operations], selected_math),
1050 ('VECTOR', [], selected_vector),
1052 output = get_first_enabled_output(node)
1053 output_type = output.type
1054 valid_mode = mode in types_list
1055 # When mode is 'MIX' we have to cheat since the mix node is not used in
1056 # geometry nodes.
1057 if tree_type == 'GEOMETRY':
1058 if mode == 'MIX':
1059 if output_type == 'VALUE' and type == 'VALUE':
1060 valid_mode = True
1061 elif output_type == 'VECTOR' and type == 'VECTOR':
1062 valid_mode = True
1063 elif type == 'GEOMETRY':
1064 valid_mode = True
1065 # When mode is 'MIX' use mix node for both 'RGBA' and 'VALUE' output types.
1066 # Cheat that output type is 'RGBA',
1067 # and that 'MIX' exists in math operations list.
1068 # This way when selected_mix list is analyzed:
1069 # Node data will be appended even though it doesn't meet requirements.
1070 elif output_type != 'SHADER' and mode == 'MIX':
1071 output_type = 'RGBA'
1072 valid_mode = True
1073 if output_type == type and valid_mode:
1074 dst.append([i, node.location.x, node.location.y, node.dimensions.x, node.hide])
1075 else:
1076 for (type, types_list, dst) in (
1077 ('SHADER', ('MIX', 'ADD'), selected_shader),
1078 ('GEOMETRY', [t[0] for t in geo_combine_operations], selected_geometry),
1079 ('MIX', [t[0] for t in blend_types], selected_mix),
1080 ('MATH', [t[0] for t in operations], selected_math),
1081 ('ZCOMBINE', ('MIX', ), selected_z),
1082 ('ALPHAOVER', ('MIX', ), selected_alphaover),
1084 if merge_type == type and mode in types_list:
1085 dst.append([i, node.location.x, node.location.y, node.dimensions.x, node.hide])
1086 # When nodes with output kinds 'RGBA' and 'VALUE' are selected at the same time
1087 # use only 'Mix' nodes for merging.
1088 # For that we add selected_math list to selected_mix list and clear selected_math.
1089 if selected_mix and selected_math and merge_type == 'AUTO':
1090 selected_mix += selected_math
1091 selected_math = []
1093 # If no nodes are selected, do nothing and pass through.
1094 if not (selected_mix + selected_shader + selected_geometry + selected_math
1095 + selected_vector + selected_z + selected_alphaover):
1096 return {'PASS_THROUGH'}
1098 for nodes_list in [
1099 selected_mix,
1100 selected_shader,
1101 selected_geometry,
1102 selected_math,
1103 selected_vector,
1104 selected_z,
1105 selected_alphaover]:
1106 if not nodes_list:
1107 continue
1108 count_before = len(nodes)
1109 # sort list by loc_x - reversed
1110 nodes_list.sort(key=lambda k: k[1], reverse=True)
1111 # get maximum loc_x
1112 loc_x = nodes_list[0][1] + nodes_list[0][3] + 70
1113 nodes_list.sort(key=lambda k: k[2], reverse=True)
1115 # Change the node type for math nodes in a geometry node tree.
1116 if tree_type == 'GEOMETRY':
1117 if nodes_list is selected_math or nodes_list is selected_vector or nodes_list is selected_mix:
1118 node_type = 'ShaderNode'
1119 if mode == 'MIX':
1120 mode = 'ADD'
1121 else:
1122 node_type = 'GeometryNode'
1123 if merge_position == 'CENTER':
1124 # average yloc of last two nodes (lowest two)
1125 loc_y = ((nodes_list[len(nodes_list) - 1][2]) + (nodes_list[len(nodes_list) - 2][2])) / 2
1126 if nodes_list[len(nodes_list) - 1][-1]: # if last node is hidden, mix should be shifted up a bit
1127 if do_hide:
1128 loc_y += 40
1129 else:
1130 loc_y += 80
1131 else:
1132 loc_y = nodes_list[len(nodes_list) - 1][2]
1133 offset_y = 100
1134 if not do_hide:
1135 offset_y = 200
1136 if nodes_list == selected_shader and not do_hide_shader:
1137 offset_y = 150.0
1138 the_range = len(nodes_list) - 1
1139 if len(nodes_list) == 1:
1140 the_range = 1
1141 was_multi = False
1142 for i in range(the_range):
1143 if nodes_list == selected_mix:
1144 mix_name = 'Mix'
1145 if tree_type == 'COMPOSITING':
1146 mix_name = 'MixRGB'
1147 add_type = node_type + mix_name
1148 add = nodes.new(add_type)
1149 if tree_type != 'COMPOSITING':
1150 add.data_type = 'RGBA'
1151 add.blend_type = mode
1152 if mode != 'MIX':
1153 add.inputs[0].default_value = 1.0
1154 add.show_preview = False
1155 add.hide = do_hide
1156 if do_hide:
1157 loc_y = loc_y - 50
1158 first = 6
1159 second = 7
1160 if tree_type == 'COMPOSITING':
1161 first = 1
1162 second = 2
1163 elif nodes_list == selected_math:
1164 add_type = node_type + 'Math'
1165 add = nodes.new(add_type)
1166 add.operation = mode
1167 add.hide = do_hide
1168 if do_hide:
1169 loc_y = loc_y - 50
1170 first = 0
1171 second = 1
1172 elif nodes_list == selected_shader:
1173 if mode == 'MIX':
1174 add_type = node_type + 'MixShader'
1175 add = nodes.new(add_type)
1176 add.hide = do_hide_shader
1177 if do_hide_shader:
1178 loc_y = loc_y - 50
1179 first = 1
1180 second = 2
1181 elif mode == 'ADD':
1182 add_type = node_type + 'AddShader'
1183 add = nodes.new(add_type)
1184 add.hide = do_hide_shader
1185 if do_hide_shader:
1186 loc_y = loc_y - 50
1187 first = 0
1188 second = 1
1189 elif nodes_list == selected_geometry:
1190 if mode in ('JOIN', 'MIX'):
1191 add_type = node_type + 'JoinGeometry'
1192 add = self.merge_with_multi_input(
1193 nodes_list, merge_position, do_hide, loc_x, links, nodes, add_type, [0])
1194 else:
1195 add_type = node_type + 'MeshBoolean'
1196 indices = [0, 1] if mode == 'DIFFERENCE' else [1]
1197 add = self.merge_with_multi_input(
1198 nodes_list, merge_position, do_hide, loc_x, links, nodes, add_type, indices)
1199 add.operation = mode
1200 was_multi = True
1201 break
1202 elif nodes_list == selected_vector:
1203 add_type = node_type + 'VectorMath'
1204 add = nodes.new(add_type)
1205 add.operation = mode
1206 add.hide = do_hide
1207 if do_hide:
1208 loc_y = loc_y - 50
1209 first = 0
1210 second = 1
1211 elif nodes_list == selected_z:
1212 add = nodes.new('CompositorNodeZcombine')
1213 add.show_preview = False
1214 add.hide = do_hide
1215 if do_hide:
1216 loc_y = loc_y - 50
1217 first = 0
1218 second = 2
1219 elif nodes_list == selected_alphaover:
1220 add = nodes.new('CompositorNodeAlphaOver')
1221 add.show_preview = False
1222 add.hide = do_hide
1223 if do_hide:
1224 loc_y = loc_y - 50
1225 first = 1
1226 second = 2
1227 add.location = loc_x, loc_y
1228 loc_y += offset_y
1229 add.select = True
1231 # This has already been handled separately
1232 if was_multi:
1233 continue
1234 count_adds = i + 1
1235 count_after = len(nodes)
1236 index = count_after - 1
1237 first_selected = nodes[nodes_list[0][0]]
1238 # "last" node has been added as first, so its index is count_before.
1239 last_add = nodes[count_before]
1240 # Create list of invalid indexes.
1241 invalid_nodes = [nodes[n[0]]
1242 for n in (selected_mix + selected_math + selected_shader + selected_z + selected_geometry)]
1244 # Special case:
1245 # Two nodes were selected and first selected has no output links, second selected has output links.
1246 # Then add links from last add to all links 'to_socket' of out links of second selected.
1247 first_selected_output = get_first_enabled_output(first_selected)
1248 if len(nodes_list) == 2:
1249 if not first_selected_output.links:
1250 second_selected = nodes[nodes_list[1][0]]
1251 for ss_link in get_first_enabled_output(second_selected).links:
1252 # Prevent cyclic dependencies when nodes to be merged are linked to one another.
1253 # Link only if "to_node" index not in invalid indexes list.
1254 if not self.link_creates_cycle(ss_link, invalid_nodes):
1255 connect_sockets(get_first_enabled_output(last_add), ss_link.to_socket)
1256 # add links from last_add to all links 'to_socket' of out links of first selected.
1257 for fs_link in first_selected_output.links:
1258 # Link only if "to_node" index not in invalid indexes list.
1259 if not self.link_creates_cycle(fs_link, invalid_nodes):
1260 connect_sockets(get_first_enabled_output(last_add), fs_link.to_socket)
1261 # add link from "first" selected and "first" add node
1262 node_to = nodes[count_after - 1]
1263 connect_sockets(first_selected_output, node_to.inputs[first])
1264 if node_to.type == 'ZCOMBINE':
1265 for fs_out in first_selected.outputs:
1266 if fs_out != first_selected_output and fs_out.name in ('Z', 'Depth'):
1267 connect_sockets(fs_out, node_to.inputs[1])
1268 break
1269 # add links between added ADD nodes and between selected and ADD nodes
1270 for i in range(count_adds):
1271 if i < count_adds - 1:
1272 node_from = nodes[index]
1273 node_to = nodes[index - 1]
1274 node_to_input_i = first
1275 node_to_z_i = 1 # if z combine - link z to first 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 if len(nodes_list) > 1:
1282 node_from = nodes[nodes_list[i + 1][0]]
1283 node_to = nodes[index]
1284 node_to_input_i = second
1285 node_to_z_i = 3 # if z combine - link z to second z input
1286 connect_sockets(get_first_enabled_output(node_from), node_to.inputs[node_to_input_i])
1287 if node_to.type == 'ZCOMBINE':
1288 for from_out in node_from.outputs:
1289 if from_out != get_first_enabled_output(node_from) and from_out.name in ('Z', 'Depth'):
1290 connect_sockets(from_out, node_to.inputs[node_to_z_i])
1291 index -= 1
1292 # set "last" of added nodes as active
1293 nodes.active = last_add
1294 for i, x, y, dx, h in nodes_list:
1295 nodes[i].select = False
1297 return {'FINISHED'}
1300 class NWBatchChangeNodes(Operator, NWBase):
1301 bl_idname = "node.nw_batch_change"
1302 bl_label = "Batch Change"
1303 bl_description = "Batch Change Blend Type and Math Operation"
1304 bl_options = {'REGISTER', 'UNDO'}
1306 blend_type: EnumProperty(
1307 name="Blend Type",
1308 items=blend_types + navs,
1310 operation: EnumProperty(
1311 name="Operation",
1312 items=operations + navs,
1315 @classmethod
1316 def poll(cls, context):
1317 return (nw_check(cls, context)
1318 and nw_check_space_type(cls, context, {'ShaderNodeTree', 'CompositorNodeTree',
1319 'TextureNodeTree', 'GeometryNodeTree'})
1320 and nw_check_selected(cls, context))
1322 def execute(self, context):
1323 blend_type = self.blend_type
1324 operation = self.operation
1325 for node in context.selected_nodes:
1326 if node.type == 'MIX_RGB' or (node.bl_idname == 'ShaderNodeMix' and node.data_type == 'RGBA'):
1327 if blend_type not in [nav[0] for nav in navs]:
1328 node.blend_type = blend_type
1329 else:
1330 if blend_type == 'NEXT':
1331 index = [i for i, entry in enumerate(blend_types) if node.blend_type in entry][0]
1332 # index = blend_types.index(node.blend_type)
1333 if index == len(blend_types) - 1:
1334 node.blend_type = blend_types[0][0]
1335 else:
1336 node.blend_type = blend_types[index + 1][0]
1338 if blend_type == 'PREV':
1339 index = [i for i, entry in enumerate(blend_types) if node.blend_type in entry][0]
1340 if index == 0:
1341 node.blend_type = blend_types[len(blend_types) - 1][0]
1342 else:
1343 node.blend_type = blend_types[index - 1][0]
1345 if node.type == 'MATH' or node.bl_idname == 'ShaderNodeMath':
1346 if operation not in [nav[0] for nav in navs]:
1347 node.operation = operation
1348 else:
1349 if operation == 'NEXT':
1350 index = [i for i, entry in enumerate(operations) if node.operation in entry][0]
1351 # index = operations.index(node.operation)
1352 if index == len(operations) - 1:
1353 node.operation = operations[0][0]
1354 else:
1355 node.operation = operations[index + 1][0]
1357 if operation == 'PREV':
1358 index = [i for i, entry in enumerate(operations) if node.operation in entry][0]
1359 # index = operations.index(node.operation)
1360 if index == 0:
1361 node.operation = operations[len(operations) - 1][0]
1362 else:
1363 node.operation = operations[index - 1][0]
1365 return {'FINISHED'}
1368 class NWChangeMixFactor(Operator, NWBase):
1369 bl_idname = "node.nw_factor"
1370 bl_label = "Change Factor"
1371 bl_description = "Change Factors of Mix Nodes and Mix Shader Nodes"
1372 bl_options = {'REGISTER', 'UNDO'}
1374 @classmethod
1375 def poll(cls, context):
1376 return nw_check(cls, context) and nw_check_selected(cls, context)
1378 # option: Change factor.
1379 # If option is 1.0 or 0.0 - set to 1.0 or 0.0
1380 # Else - change factor by option value.
1381 option: FloatProperty()
1383 def execute(self, context):
1384 nodes, links = get_nodes_links(context)
1385 option = self.option
1386 selected = [] # entry = index
1387 for si, node in enumerate(nodes):
1388 if node.select:
1389 if node.type in {'MIX_RGB', 'MIX_SHADER'} or node.bl_idname == 'ShaderNodeMix':
1390 selected.append(si)
1392 for si in selected:
1393 fac = nodes[si].inputs[0]
1394 nodes[si].hide = False
1395 if option in {0.0, 1.0}:
1396 fac.default_value = option
1397 else:
1398 fac.default_value += option
1400 return {'FINISHED'}
1403 class NWCopySettings(Operator, NWBase):
1404 bl_idname = "node.nw_copy_settings"
1405 bl_label = "Copy Settings"
1406 bl_description = "Copy Settings of Active Node to Selected Nodes"
1407 bl_options = {'REGISTER', 'UNDO'}
1409 @classmethod
1410 def poll(cls, context):
1411 return (nw_check(cls, context)
1412 and nw_check_active(cls, context)
1413 and nw_check_selected(cls, context, min=2)
1414 and nw_check_node_type(cls, context, 'FRAME', invert=True))
1416 def execute(self, context):
1417 node_active = context.active_node
1418 node_selected = context.selected_nodes
1419 selected_node_names = [n.name for n in node_selected]
1421 # Get nodes in selection by type
1422 valid_nodes = [n for n in node_selected if n.type == node_active.type]
1424 if not (len(valid_nodes) > 1) and node_active:
1425 self.report({'ERROR'}, "Selected nodes are not of the same type as {}".format(node_active.name))
1426 return {'CANCELLED'}
1428 if len(valid_nodes) != len(node_selected):
1429 # Report nodes that are not valid
1430 valid_node_names = [n.name for n in valid_nodes]
1431 not_valid_names = list(set(selected_node_names) - set(valid_node_names))
1432 self.report(
1433 {'INFO'},
1434 "Ignored {} (not of the same type as {})".format(
1435 ", ".join(not_valid_names),
1436 node_active.name))
1438 # Reference original
1439 orig = node_active
1440 # node_selected_names = [n.name for n in node_selected]
1442 # Output list
1443 success_names = []
1445 # Deselect all nodes
1446 for i in node_selected:
1447 i.select = False
1449 # Code by zeffii from http://blender.stackexchange.com/a/42338/3710
1450 # Run through all other nodes
1451 for node in valid_nodes[1:]:
1453 # Check for frame node
1454 parent = node.parent if node.parent else None
1455 node_loc = [node.location.x, node.location.y]
1457 # Select original to duplicate
1458 orig.select = True
1460 # Duplicate selected node
1461 bpy.ops.node.duplicate()
1462 new_node = context.selected_nodes[0]
1464 # Deselect copy
1465 new_node.select = False
1467 # Properties to copy
1468 node_tree = node.id_data
1469 props_to_copy = 'bl_idname name location height width'.split(' ')
1471 # Input and outputs
1472 reconnections = []
1473 mappings = chain.from_iterable([node.inputs, node.outputs])
1474 for i in (i for i in mappings if i.is_linked):
1475 for L in i.links:
1476 reconnections.append([L.from_socket.path_from_id(), L.to_socket.path_from_id()])
1478 # Properties
1479 props = {j: getattr(node, j) for j in props_to_copy}
1480 props_to_copy.pop(0)
1482 for prop in props_to_copy:
1483 setattr(new_node, prop, props[prop])
1485 # Get the node tree to remove the old node
1486 nodes = node_tree.nodes
1487 nodes.remove(node)
1488 new_node.name = props['name']
1490 if parent:
1491 new_node.parent = parent
1492 new_node.location = node_loc
1494 for str_from, str_to in reconnections:
1495 connect_sockets(eval(str_from), eval(str_to))
1497 success_names.append(new_node.name)
1499 orig.select = True
1500 node_tree.nodes.active = orig
1501 self.report(
1502 {'INFO'},
1503 "Successfully copied attributes from {} to: {}".format(
1504 orig.name,
1505 ", ".join(success_names)))
1506 return {'FINISHED'}
1509 class NWCopyLabel(Operator, NWBase):
1510 bl_idname = "node.nw_copy_label"
1511 bl_label = "Copy Label"
1512 bl_options = {'REGISTER', 'UNDO'}
1513 bl_description = "Copy label from active to selected nodes"
1515 option: EnumProperty(
1516 name="option",
1517 description="Source of name of label",
1518 items=(
1519 ('FROM_ACTIVE', 'from active', 'from active node',),
1520 ('FROM_NODE', 'from node', 'from node linked to selected node'),
1521 ('FROM_SOCKET', 'from socket', 'from socket linked to selected node'),
1525 @classmethod
1526 def poll(cls, context):
1527 return nw_check(cls, context) and nw_check_selected(cls, context, min=2)
1529 def execute(self, context):
1530 nodes, links = get_nodes_links(context)
1531 option = self.option
1532 active = nodes.active
1533 if option == 'FROM_ACTIVE':
1534 if active:
1535 src_label = active.label
1536 for node in [n for n in nodes if n.select and nodes.active != n]:
1537 node.label = src_label
1538 elif option == 'FROM_NODE':
1539 selected = [n for n in nodes if n.select]
1540 for node in selected:
1541 for input in node.inputs:
1542 if input.links:
1543 src = input.links[0].from_node
1544 node.label = src.label
1545 break
1546 elif option == 'FROM_SOCKET':
1547 selected = [n for n in nodes if n.select]
1548 for node in selected:
1549 for input in node.inputs:
1550 if input.links:
1551 src = input.links[0].from_socket
1552 node.label = src.name
1553 break
1555 return {'FINISHED'}
1558 class NWClearLabel(Operator, NWBase):
1559 bl_idname = "node.nw_clear_label"
1560 bl_label = "Clear Label"
1561 bl_options = {'REGISTER', 'UNDO'}
1562 bl_description = "Clear labels on selected nodes"
1564 option: BoolProperty()
1566 @classmethod
1567 def poll(cls, context):
1568 return nw_check(cls, context) and nw_check_selected(cls, context)
1570 def execute(self, context):
1571 nodes, links = get_nodes_links(context)
1572 for node in [n for n in nodes if n.select]:
1573 node.label = ''
1575 return {'FINISHED'}
1577 def invoke(self, context, event):
1578 if self.option:
1579 return self.execute(context)
1580 else:
1581 return context.window_manager.invoke_confirm(self, event)
1584 class NWModifyLabels(Operator, NWBase):
1585 """Modify labels of all selected nodes"""
1586 bl_idname = "node.nw_modify_labels"
1587 bl_label = "Modify Labels"
1588 bl_options = {'REGISTER', 'UNDO'}
1590 prepend: StringProperty(
1591 name="Add to Beginning"
1593 append: StringProperty(
1594 name="Add to End"
1596 replace_from: StringProperty(
1597 name="Text to Replace"
1599 replace_to: StringProperty(
1600 name="Replace with"
1603 @classmethod
1604 def poll(cls, context):
1605 return nw_check(cls, context) and nw_check_selected(cls, context)
1607 def execute(self, context):
1608 nodes, links = get_nodes_links(context)
1609 for node in [n for n in nodes if n.select]:
1610 node.label = self.prepend + node.label.replace(self.replace_from, self.replace_to) + self.append
1612 return {'FINISHED'}
1614 def invoke(self, context, event):
1615 self.prepend = ""
1616 self.append = ""
1617 self.remove = ""
1618 return context.window_manager.invoke_props_dialog(self)
1621 class NWAddTextureSetup(Operator, NWBase):
1622 bl_idname = "node.nw_add_texture"
1623 bl_label = "Texture Setup"
1624 bl_description = "Add Texture Node Setup to Selected Shaders"
1625 bl_options = {'REGISTER', 'UNDO'}
1627 add_mapping: BoolProperty(
1628 name="Add Mapping Nodes",
1629 description="Create coordinate and mapping nodes for the texture (ignored for selected texture nodes)",
1630 default=True)
1632 @classmethod
1633 def poll(cls, context):
1634 return (nw_check(cls, context)
1635 and nw_check_space_type(cls, context, {'ShaderNodeTree'})
1636 and nw_check_selected(cls, context))
1638 def execute(self, context):
1639 nodes, links = get_nodes_links(context)
1641 texture_types = get_texture_node_types()
1642 selected_nodes = [n for n in nodes if n.select]
1644 for node in selected_nodes:
1645 if not node.inputs:
1646 continue
1648 input_index = 0
1649 target_input = node.inputs[0]
1650 for input in node.inputs:
1651 if input.enabled:
1652 input_index += 1
1653 if not input.is_linked:
1654 target_input = input
1655 break
1656 else:
1657 self.report({'WARNING'}, "No free inputs for node: " + node.name)
1658 continue
1660 x_offset = 0
1661 padding = 40.0
1662 locx = node.location.x
1663 locy = node.location.y - (input_index * padding)
1665 is_texture_node = node.rna_type.identifier in texture_types
1666 use_environment_texture = node.type == 'BACKGROUND'
1668 # Add an image texture before normal shader nodes.
1669 if not is_texture_node:
1670 image_texture_type = 'ShaderNodeTexEnvironment' if use_environment_texture else 'ShaderNodeTexImage'
1671 image_texture_node = nodes.new(image_texture_type)
1672 x_offset = x_offset + image_texture_node.width + padding
1673 image_texture_node.location = [locx - x_offset, locy]
1674 nodes.active = image_texture_node
1675 connect_sockets(image_texture_node.outputs[0], target_input)
1677 # The mapping setup following this will connect to the first input of this image texture.
1678 target_input = image_texture_node.inputs[0]
1680 node.select = False
1682 if is_texture_node or self.add_mapping:
1683 # Add Mapping node.
1684 mapping_node = nodes.new('ShaderNodeMapping')
1685 x_offset = x_offset + mapping_node.width + padding
1686 mapping_node.location = [locx - x_offset, locy]
1687 connect_sockets(mapping_node.outputs[0], target_input)
1689 # Add Texture Coordinates node.
1690 tex_coord_node = nodes.new('ShaderNodeTexCoord')
1691 x_offset = x_offset + tex_coord_node.width + padding
1692 tex_coord_node.location = [locx - x_offset, locy]
1694 is_procedural_texture = is_texture_node and node.type != 'TEX_IMAGE'
1695 use_generated_coordinates = is_procedural_texture or use_environment_texture
1696 tex_coord_output = tex_coord_node.outputs[0 if use_generated_coordinates else 2]
1697 connect_sockets(tex_coord_output, mapping_node.inputs[0])
1699 return {'FINISHED'}
1702 class NWAddPrincipledSetup(Operator, NWBase, ImportHelper):
1703 bl_idname = "node.nw_add_textures_for_principled"
1704 bl_label = "Principled Texture Setup"
1705 bl_description = "Add Texture Node Setup for Principled BSDF"
1706 bl_options = {'REGISTER', 'UNDO'}
1708 directory: StringProperty(
1709 name='Directory',
1710 subtype='DIR_PATH',
1711 default='',
1712 description='Folder to search in for image files'
1714 files: CollectionProperty(
1715 type=bpy.types.OperatorFileListElement,
1716 options={'HIDDEN', 'SKIP_SAVE'}
1719 relative_path: BoolProperty(
1720 name='Relative Path',
1721 description='Set the file path relative to the blend file, when possible',
1722 default=True
1725 order = [
1726 "filepath",
1727 "files",
1730 def draw(self, context):
1731 layout = self.layout
1732 layout.alignment = 'LEFT'
1734 layout.prop(self, 'relative_path')
1736 @classmethod
1737 def poll(cls, context):
1738 return (nw_check(cls, context)
1739 and nw_check_active(cls, context)
1740 and nw_check_space_type(cls, context, {'ShaderNodeTree'})
1741 and nw_check_node_type(cls, context, 'BSDF_PRINCIPLED'))
1743 def execute(self, context):
1744 # Check if everything is ok
1745 if not self.directory:
1746 self.report({'INFO'}, 'No folder selected')
1747 return {'CANCELLED'}
1748 if not self.files[:]:
1749 self.report({'INFO'}, 'No files selected')
1750 return {'CANCELLED'}
1752 nodes, links = get_nodes_links(context)
1753 active_node = nodes.active
1755 # Filter textures names for texturetypes in filenames
1756 # [Socket Name, [abbreviations and keyword list], Filename placeholder]
1757 tags = context.preferences.addons[__package__].preferences.principled_tags
1758 normal_abbr = tags.normal.split(' ')
1759 bump_abbr = tags.bump.split(' ')
1760 gloss_abbr = tags.gloss.split(' ')
1761 rough_abbr = tags.rough.split(' ')
1762 socketnames = [
1763 ['Displacement', tags.displacement.split(' '), None],
1764 ['Base Color', tags.base_color.split(' '), None],
1765 ['Metallic', tags.metallic.split(' '), None],
1766 ['Specular IOR Level', tags.specular.split(' '), None],
1767 ['Roughness', rough_abbr + gloss_abbr, None],
1768 ['Bump', bump_abbr, None],
1769 ['Normal', normal_abbr, None],
1770 ['Transmission Weight', tags.transmission.split(' '), None],
1771 ['Emission Color', tags.emission.split(' '), None],
1772 ['Alpha', tags.alpha.split(' '), None],
1773 ['Ambient Occlusion', tags.ambient_occlusion.split(' '), None],
1776 match_files_to_socket_names(self.files, socketnames)
1777 # Remove socketnames without found files
1778 socketnames = [s for s in socketnames if s[2]
1779 and path.exists(self.directory + s[2])]
1780 if not socketnames:
1781 self.report({'INFO'}, 'No matching images found')
1782 print('No matching images found')
1783 return {'CANCELLED'}
1785 # Don't override path earlier as os.path is used to check the absolute path
1786 import_path = self.directory
1787 if self.relative_path:
1788 if bpy.data.filepath:
1789 try:
1790 import_path = bpy.path.relpath(self.directory)
1791 except ValueError:
1792 pass
1794 # Add found images
1795 print('\nMatched Textures:')
1796 texture_nodes = []
1797 disp_texture = None
1798 ao_texture = None
1799 normal_node = None
1800 normal_node_texture = None
1801 bump_node = None
1802 bump_node_texture = None
1803 roughness_node = None
1804 for i, sname in enumerate(socketnames):
1805 print(i, sname[0], sname[2])
1807 # DISPLACEMENT NODES
1808 if sname[0] == 'Displacement':
1809 disp_texture = nodes.new(type='ShaderNodeTexImage')
1810 img = bpy.data.images.load(path.join(import_path, sname[2]))
1811 disp_texture.image = img
1812 disp_texture.label = 'Displacement'
1813 if disp_texture.image:
1814 disp_texture.image.colorspace_settings.is_data = True
1816 # Add displacement offset nodes
1817 disp_node = nodes.new(type='ShaderNodeDisplacement')
1818 # Align the Displacement node under the active Principled BSDF node
1819 disp_node.location = active_node.location + Vector((100, -700))
1820 link = connect_sockets(disp_node.inputs[0], disp_texture.outputs[0])
1822 # TODO Turn on true displacement in the material
1823 # Too complicated for now
1825 # Find output node
1826 output_node = [n for n in nodes if n.bl_idname == 'ShaderNodeOutputMaterial']
1827 if output_node:
1828 if not output_node[0].inputs[2].is_linked:
1829 link = connect_sockets(output_node[0].inputs[2], disp_node.outputs[0])
1831 continue
1833 # BUMP NODES
1834 elif sname[0] == 'Bump':
1835 # Test if new texture node is bump map
1836 fname_components = split_into_components(sname[2])
1837 match_bump = set(bump_abbr).intersection(set(fname_components))
1838 if match_bump:
1839 # If Bump add bump node in between
1840 bump_node_texture = nodes.new(type='ShaderNodeTexImage')
1841 img = bpy.data.images.load(path.join(import_path, sname[2]))
1842 img.colorspace_settings.is_data = True
1843 bump_node_texture.image = img
1844 bump_node_texture.label = 'Bump'
1846 # Add bump node
1847 bump_node = nodes.new(type='ShaderNodeBump')
1848 link = connect_sockets(bump_node.inputs[2], bump_node_texture.outputs[0])
1849 link = connect_sockets(active_node.inputs['Normal'], bump_node.outputs[0])
1850 continue
1852 # NORMAL NODES
1853 elif sname[0] == 'Normal':
1854 # Test if new texture node is normal map
1855 fname_components = split_into_components(sname[2])
1856 match_normal = set(normal_abbr).intersection(set(fname_components))
1857 if match_normal:
1858 # If Normal add normal node in between
1859 normal_node_texture = nodes.new(type='ShaderNodeTexImage')
1860 img = bpy.data.images.load(path.join(import_path, sname[2]))
1861 img.colorspace_settings.is_data = True
1862 normal_node_texture.image = img
1863 normal_node_texture.label = 'Normal'
1865 # Add normal node
1866 normal_node = nodes.new(type='ShaderNodeNormalMap')
1867 link = connect_sockets(normal_node.inputs[1], normal_node_texture.outputs[0])
1868 # Connect to bump node if it was created before, otherwise to the BSDF
1869 if bump_node is None:
1870 link = connect_sockets(active_node.inputs[sname[0]], normal_node.outputs[0])
1871 else:
1872 link = connect_sockets(bump_node.inputs[sname[0]], normal_node.outputs[sname[0]])
1873 continue
1875 # AMBIENT OCCLUSION TEXTURE
1876 elif sname[0] == 'Ambient Occlusion':
1877 ao_texture = nodes.new(type='ShaderNodeTexImage')
1878 img = bpy.data.images.load(path.join(import_path, sname[2]))
1879 ao_texture.image = img
1880 ao_texture.label = sname[0]
1881 if ao_texture.image:
1882 ao_texture.image.colorspace_settings.is_data = True
1884 continue
1886 if not active_node.inputs[sname[0]].is_linked:
1887 # No texture node connected -> add texture node with new image
1888 texture_node = nodes.new(type='ShaderNodeTexImage')
1889 img = bpy.data.images.load(path.join(import_path, sname[2]))
1890 texture_node.image = img
1892 if sname[0] == 'Roughness':
1893 # Test if glossy or roughness map
1894 fname_components = split_into_components(sname[2])
1895 match_rough = set(rough_abbr).intersection(set(fname_components))
1896 match_gloss = set(gloss_abbr).intersection(set(fname_components))
1898 if match_rough:
1899 # If Roughness nothing to to
1900 link = connect_sockets(active_node.inputs[sname[0]], texture_node.outputs[0])
1902 elif match_gloss:
1903 # If Gloss Map add invert node
1904 invert_node = nodes.new(type='ShaderNodeInvert')
1905 link = connect_sockets(invert_node.inputs[1], texture_node.outputs[0])
1907 link = connect_sockets(active_node.inputs[sname[0]], invert_node.outputs[0])
1908 roughness_node = texture_node
1910 else:
1911 # This is a simple connection Texture --> Input slot
1912 link = connect_sockets(active_node.inputs[sname[0]], texture_node.outputs[0])
1914 # Use non-color except for color inputs
1915 if sname[0] not in ['Base Color', 'Emission Color'] and texture_node.image:
1916 texture_node.image.colorspace_settings.is_data = True
1918 else:
1919 # If already texture connected. add to node list for alignment
1920 texture_node = active_node.inputs[sname[0]].links[0].from_node
1922 # This are all connected texture nodes
1923 texture_nodes.append(texture_node)
1924 texture_node.label = sname[0]
1926 if disp_texture:
1927 texture_nodes.append(disp_texture)
1928 if bump_node_texture:
1929 texture_nodes.append(bump_node_texture)
1930 if normal_node_texture:
1931 texture_nodes.append(normal_node_texture)
1933 if ao_texture:
1934 # We want the ambient occlusion texture to be the top most texture node
1935 texture_nodes.insert(0, ao_texture)
1937 # Alignment
1938 for i, texture_node in enumerate(texture_nodes):
1939 offset = Vector((-550, (i * -280) + 200))
1940 texture_node.location = active_node.location + offset
1942 if normal_node:
1943 # Extra alignment if normal node was added
1944 normal_node.location = normal_node_texture.location + Vector((300, 0))
1946 if bump_node:
1947 # Extra alignment if bump node was added
1948 bump_node.location = bump_node_texture.location + Vector((300, 0))
1950 if roughness_node:
1951 # Alignment of invert node if glossy map
1952 invert_node.location = roughness_node.location + Vector((300, 0))
1954 # Add texture input + mapping
1955 mapping = nodes.new(type='ShaderNodeMapping')
1956 mapping.location = active_node.location + Vector((-1050, 0))
1957 if len(texture_nodes) > 1:
1958 # If more than one texture add reroute node in between
1959 reroute = nodes.new(type='NodeReroute')
1960 texture_nodes.append(reroute)
1961 tex_coords = Vector((texture_nodes[0].location.x,
1962 sum(n.location.y for n in texture_nodes) / len(texture_nodes)))
1963 reroute.location = tex_coords + Vector((-50, -120))
1964 for texture_node in texture_nodes:
1965 link = connect_sockets(texture_node.inputs[0], reroute.outputs[0])
1966 link = connect_sockets(reroute.inputs[0], mapping.outputs[0])
1967 else:
1968 link = connect_sockets(texture_nodes[0].inputs[0], mapping.outputs[0])
1970 # Connect texture_coordinates to mapping node
1971 texture_input = nodes.new(type='ShaderNodeTexCoord')
1972 texture_input.location = mapping.location + Vector((-200, 0))
1973 link = connect_sockets(mapping.inputs[0], texture_input.outputs[2])
1975 # Create frame around tex coords and mapping
1976 frame = nodes.new(type='NodeFrame')
1977 frame.label = 'Mapping'
1978 mapping.parent = frame
1979 texture_input.parent = frame
1980 frame.update()
1982 # Create frame around texture nodes
1983 frame = nodes.new(type='NodeFrame')
1984 frame.label = 'Textures'
1985 for tnode in texture_nodes:
1986 tnode.parent = frame
1987 frame.update()
1989 # Just to be sure
1990 active_node.select = False
1991 nodes.update()
1992 links.update()
1993 force_update(context)
1994 return {'FINISHED'}
1997 class NWAddReroutes(Operator, NWBase):
1998 """Add Reroute Nodes and link them to outputs of selected nodes"""
1999 bl_idname = "node.nw_add_reroutes"
2000 bl_label = "Add Reroutes"
2001 bl_description = "Add Reroutes to Outputs"
2002 bl_options = {'REGISTER', 'UNDO'}
2004 option: EnumProperty(
2005 name="option",
2006 items=[
2007 ('ALL', 'to all', 'Add to all outputs'),
2008 ('LOOSE', 'to loose', 'Add only to loose outputs'),
2009 ('LINKED', 'to linked', 'Add only to linked outputs'),
2013 @classmethod
2014 def poll(cls, context):
2015 return nw_check(cls, context) and nw_check_selected(cls, context)
2017 def execute(self, context):
2018 nodes, _links = get_nodes_links(context)
2019 post_select = [] # Nodes to be selected after execution.
2020 y_offset = -22.0
2022 # Create reroutes and recreate links.
2023 for node in [n for n in nodes if n.select]:
2024 if not node.outputs:
2025 continue
2026 x = node.location.x + node.width + 20.0
2027 y = node.location.y
2028 new_node_reroutes = []
2030 # Unhide 'REROUTE' nodes to avoid issues with location.y
2031 if node.type == 'REROUTE':
2032 node.hide = False
2033 else:
2034 y -= 35.0
2036 reroutes_count = 0 # Will be used when aligning reroutes added to hidden nodes.
2037 for out_i, output in enumerate(node.outputs):
2038 if output.is_unavailable:
2039 continue
2040 if node.type == 'R_LAYERS' and output.name != 'Alpha':
2041 # If 'R_LAYERS' check if output is used in render pass.
2042 # If output is "Alpha", assume it's used. Not available in passes.
2043 node_scene = node.scene
2044 node_layer = node.layer
2045 for rlo in rl_outputs:
2046 # Check entries in global 'rl_outputs' variable.
2047 if output.name in {rlo.output_name, rlo.exr_output_name}:
2048 if not getattr(node_scene.view_layers[node_layer], rlo.render_pass):
2049 continue
2050 # Output is valid when option is 'all' or when 'loose' output has no links.
2051 valid = ((self.option == 'ALL') or
2052 (self.option == 'LOOSE' and not output.links) or
2053 (self.option == 'LINKED' and output.links))
2054 if valid:
2055 # Add reroutes only if valid.
2056 n = nodes.new('NodeReroute')
2057 nodes.active = n
2058 for link in output.links:
2059 connect_sockets(n.outputs[0], link.to_socket)
2060 connect_sockets(output, n.inputs[0])
2061 n.location = x, y
2062 new_node_reroutes.append(n)
2063 post_select.append(n)
2064 if valid or not output.hide:
2065 # Offset reroutes for all outputs, except hidden ones.
2066 reroutes_count += 1
2067 y += y_offset
2069 # Nicer reroutes distribution along y when node.hide.
2070 if node.hide:
2071 y_translate = reroutes_count * y_offset / 2.0 - y_offset - 35.0
2072 for reroute in new_node_reroutes:
2073 reroute.location.y -= y_translate
2075 if post_select:
2076 for node in nodes:
2077 # Select only newly created nodes.
2078 node.select = node in post_select
2079 else:
2080 # No new nodes were created.
2081 return {'CANCELLED'}
2083 return {'FINISHED'}
2086 class NWLinkActiveToSelected(Operator, NWBase):
2087 """Link active node to selected nodes basing on various criteria"""
2088 bl_idname = "node.nw_link_active_to_selected"
2089 bl_label = "Link Active Node to Selected"
2090 bl_options = {'REGISTER', 'UNDO'}
2092 replace: BoolProperty()
2093 use_node_name: BoolProperty()
2094 use_outputs_names: BoolProperty()
2096 @classmethod
2097 def poll(cls, context):
2098 return (nw_check(cls, context)
2099 and nw_check_active(cls, context)
2100 and nw_check_selected(cls, context, min=2))
2102 def execute(self, context):
2103 nodes, links = get_nodes_links(context)
2104 replace = self.replace
2105 use_node_name = self.use_node_name
2106 use_outputs_names = self.use_outputs_names
2107 active = nodes.active
2108 selected = [node for node in nodes if node.select and node != active]
2109 outputs = [] # Only usable outputs of active nodes will be stored here.
2110 for out in active.outputs:
2111 if active.type != 'R_LAYERS':
2112 outputs.append(out)
2113 else:
2114 # 'R_LAYERS' node type needs special handling.
2115 # outputs of 'R_LAYERS' are callable even if not seen in UI.
2116 # Only outputs that represent used passes should be taken into account
2117 # Check if pass represented by output is used.
2118 # global 'rl_outputs' list will be used for that
2119 for rlo in rl_outputs:
2120 pass_used = False # initial value. Will be set to True if pass is used
2121 if out.name == 'Alpha':
2122 # Alpha output is always present. Doesn't have representation in render pass. Assume it's used.
2123 pass_used = True
2124 elif out.name in {rlo.output_name, rlo.exr_output_name}:
2125 # example 'render_pass' entry: 'use_pass_uv' Check if True in scene render layers
2126 pass_used = getattr(active.scene.view_layers[active.layer], rlo.render_pass)
2127 break
2128 if pass_used:
2129 outputs.append(out)
2130 doit = True # Will be changed to False when links successfully added to previous output.
2131 for out in outputs:
2132 if doit:
2133 for node in selected:
2134 dst_name = node.name # Will be compared with src_name if needed.
2135 # When node has label - use it as dst_name
2136 if node.label:
2137 dst_name = node.label
2138 valid = True # Initial value. Will be changed to False if names don't match.
2139 src_name = dst_name # If names not used - this assignment will keep valid = True.
2140 if use_node_name:
2141 # Set src_name to source node name or label
2142 src_name = active.name
2143 if active.label:
2144 src_name = active.label
2145 elif use_outputs_names:
2146 src_name = (out.name, )
2147 for rlo in rl_outputs:
2148 if out.name in {rlo.output_name, rlo.exr_output_name}:
2149 src_name = (rlo.output_name, rlo.exr_output_name)
2150 if dst_name not in src_name:
2151 valid = False
2152 if valid:
2153 for input in node.inputs:
2154 if input.type == out.type or node.type == 'REROUTE':
2155 if replace or not input.is_linked:
2156 connect_sockets(out, input)
2157 if not use_node_name and not use_outputs_names:
2158 doit = False
2159 break
2161 return {'FINISHED'}
2164 class NWAlignNodes(Operator, NWBase):
2165 '''Align the selected nodes neatly in a row/column'''
2166 bl_idname = "node.nw_align_nodes"
2167 bl_label = "Align Nodes"
2168 bl_options = {'REGISTER', 'UNDO'}
2169 margin: IntProperty(name='Margin', default=50, description='The amount of space between nodes')
2171 @classmethod
2172 def poll(cls, context):
2173 return nw_check(cls, context) and nw_check_not_empty(cls, context)
2175 def execute(self, context):
2176 nodes, links = get_nodes_links(context)
2177 margin = self.margin
2179 selection = []
2180 for node in nodes:
2181 if node.select and node.type != 'FRAME':
2182 selection.append(node)
2184 # If no nodes are selected, align all nodes
2185 active_loc = None
2186 if not selection:
2187 selection = nodes
2188 elif nodes.active in selection:
2189 active_loc = copy(nodes.active.location) # make a copy, not a reference
2191 # Check if nodes should be laid out horizontally or vertically
2192 # use dimension to get center of node, not corner
2193 x_locs = [n.location.x + (n.dimensions.x / 2) for n in selection]
2194 y_locs = [n.location.y - (n.dimensions.y / 2) for n in selection]
2195 x_range = max(x_locs) - min(x_locs)
2196 y_range = max(y_locs) - min(y_locs)
2197 mid_x = (max(x_locs) + min(x_locs)) / 2
2198 mid_y = (max(y_locs) + min(y_locs)) / 2
2199 horizontal = x_range > y_range
2201 # Sort selection by location of node mid-point
2202 if horizontal:
2203 selection = sorted(selection, key=lambda n: n.location.x + (n.dimensions.x / 2))
2204 else:
2205 selection = sorted(selection, key=lambda n: n.location.y - (n.dimensions.y / 2), reverse=True)
2207 # Alignment
2208 current_pos = 0
2209 for node in selection:
2210 current_margin = margin
2211 current_margin = current_margin * 0.5 if node.hide else current_margin # use a smaller margin for hidden nodes
2213 if horizontal:
2214 node.location.x = current_pos
2215 current_pos += current_margin + node.dimensions.x
2216 node.location.y = mid_y + (node.dimensions.y / 2)
2217 else:
2218 node.location.y = current_pos
2219 current_pos -= (current_margin * 0.3) + node.dimensions.y # use half-margin for vertical alignment
2220 node.location.x = mid_x - (node.dimensions.x / 2)
2222 # If active node is selected, center nodes around it
2223 if active_loc is not None:
2224 active_loc_diff = active_loc - nodes.active.location
2225 for node in selection:
2226 node.location += active_loc_diff
2227 else: # Position nodes centered around where they used to be
2228 locs = ([n.location.x + (n.dimensions.x / 2) for n in selection]
2229 ) if horizontal else ([n.location.y - (n.dimensions.y / 2) for n in selection])
2230 new_mid = (max(locs) + min(locs)) / 2
2231 for node in selection:
2232 if horizontal:
2233 node.location.x += (mid_x - new_mid)
2234 else:
2235 node.location.y += (mid_y - new_mid)
2237 return {'FINISHED'}
2240 class NWSelectParentChildren(Operator, NWBase):
2241 bl_idname = "node.nw_select_parent_child"
2242 bl_label = "Select Parent or Children"
2243 bl_options = {'REGISTER', 'UNDO'}
2245 option: EnumProperty(
2246 name="option",
2247 items=(
2248 ('PARENT', 'Select Parent', 'Select Parent Frame'),
2249 ('CHILD', 'Select Children', 'Select members of selected frame'),
2253 @classmethod
2254 def poll(cls, context):
2255 return nw_check(cls, context) and nw_check_selected(cls, context)
2257 def execute(self, context):
2258 nodes, links = get_nodes_links(context)
2259 option = self.option
2260 selected = [node for node in nodes if node.select]
2261 if option == 'PARENT':
2262 for sel in selected:
2263 parent = sel.parent
2264 if parent:
2265 parent.select = True
2266 else: # option == 'CHILD'
2267 for sel in selected:
2268 children = [node for node in nodes if node.parent == sel]
2269 for kid in children:
2270 kid.select = True
2272 return {'FINISHED'}
2275 class NWDetachOutputs(Operator, NWBase):
2276 """Detach outputs of selected node leaving inputs linked"""
2277 bl_idname = "node.nw_detach_outputs"
2278 bl_label = "Detach Outputs"
2279 bl_options = {'REGISTER', 'UNDO'}
2281 @classmethod
2282 def poll(cls, context):
2283 return nw_check(cls, context) and nw_check_selected(cls, context)
2285 def execute(self, context):
2286 nodes, links = get_nodes_links(context)
2287 selected = context.selected_nodes
2288 bpy.ops.node.duplicate_move_keep_inputs()
2289 new_nodes = context.selected_nodes
2290 bpy.ops.node.select_all(action="DESELECT")
2291 for node in selected:
2292 node.select = True
2293 bpy.ops.node.delete_reconnect()
2294 for new_node in new_nodes:
2295 new_node.select = True
2296 bpy.ops.transform.translate('INVOKE_DEFAULT')
2298 return {'FINISHED'}
2301 class NWLinkToOutputNode(Operator):
2302 """Link to Composite node or Material Output node"""
2303 bl_idname = "node.nw_link_out"
2304 bl_label = "Connect to Output"
2305 bl_options = {'REGISTER', 'UNDO'}
2307 @classmethod
2308 def poll(cls, context):
2309 """Disabled for custom nodes as we do not know which nodes are outputs."""
2310 return (nw_check(cls, context)
2311 and nw_check_space_type(cls, context, {'ShaderNodeTree', 'CompositorNodeTree',
2312 'TextureNodeTree', 'GeometryNodeTree'})
2313 and nw_check_active(cls, context)
2314 and nw_check_visible_outputs(cls, context))
2316 def execute(self, context):
2317 nodes, links = get_nodes_links(context)
2318 active = nodes.active
2319 output_index = None
2320 tree_type = context.space_data.tree_type
2321 shader_outputs = {'OBJECT': 'ShaderNodeOutputMaterial',
2322 'WORLD': 'ShaderNodeOutputWorld',
2323 'LINESTYLE': 'ShaderNodeOutputLineStyle'}
2324 output_type = {
2325 'ShaderNodeTree': shader_outputs[context.space_data.shader_type],
2326 'CompositorNodeTree': 'CompositorNodeComposite',
2327 'TextureNodeTree': 'TextureNodeOutput',
2328 'GeometryNodeTree': 'NodeGroupOutput',
2329 }[tree_type]
2330 for node in nodes:
2331 # check whether the node is an output node and,
2332 # if supported, whether it's the active one
2333 if node.rna_type.identifier == output_type \
2334 and (node.is_active_output if hasattr(node, 'is_active_output')
2335 else True):
2336 output_node = node
2337 break
2338 else: # No output node exists
2339 bpy.ops.node.select_all(action="DESELECT")
2340 output_node = nodes.new(output_type)
2341 output_node.location.x = active.location.x + active.dimensions.x + 80
2342 output_node.location.y = active.location.y
2344 if active.outputs:
2345 for i, output in enumerate(active.outputs):
2346 if is_visible_socket(output):
2347 output_index = i
2348 break
2349 for i, output in enumerate(active.outputs):
2350 if output.type == output_node.inputs[0].type and is_visible_socket(output):
2351 output_index = i
2352 break
2354 out_input_index = 0
2355 if tree_type == 'ShaderNodeTree':
2356 if active.outputs[output_index].name == 'Volume':
2357 out_input_index = 1
2358 elif active.outputs[output_index].name == 'Displacement':
2359 out_input_index = 2
2360 elif tree_type == 'GeometryNodeTree':
2361 if active.outputs[output_index].type != 'GEOMETRY':
2362 return {'CANCELLED'}
2363 connect_sockets(active.outputs[output_index], output_node.inputs[out_input_index])
2365 force_update(context) # viewport render does not update
2367 return {'FINISHED'}
2370 class NWMakeLink(Operator, NWBase):
2371 """Make a link from one socket to another"""
2372 bl_idname = 'node.nw_make_link'
2373 bl_label = 'Make Link'
2374 bl_options = {'REGISTER', 'UNDO'}
2375 from_socket: IntProperty()
2376 to_socket: IntProperty()
2378 def execute(self, context):
2379 nodes, links = get_nodes_links(context)
2381 n1 = nodes[context.scene.NWLazySource]
2382 n2 = nodes[context.scene.NWLazyTarget]
2384 connect_sockets(n1.outputs[self.from_socket], n2.inputs[self.to_socket])
2386 force_update(context)
2388 return {'FINISHED'}
2391 class NWCallInputsMenu(Operator, NWBase):
2392 """Link from this output"""
2393 bl_idname = 'node.nw_call_inputs_menu'
2394 bl_label = 'Make Link'
2395 bl_options = {'REGISTER', 'UNDO'}
2396 from_socket: IntProperty()
2398 def execute(self, context):
2399 nodes, links = get_nodes_links(context)
2401 context.scene.NWSourceSocket = self.from_socket
2403 n1 = nodes[context.scene.NWLazySource]
2404 n2 = nodes[context.scene.NWLazyTarget]
2405 if len(n2.inputs) > 1:
2406 bpy.ops.wm.call_menu("INVOKE_DEFAULT", name=NWConnectionListInputs.bl_idname)
2407 elif len(n2.inputs) == 1:
2408 connect_sockets(n1.outputs[self.from_socket], n2.inputs[0])
2409 return {'FINISHED'}
2412 class NWAddSequence(Operator, NWBase, ImportHelper):
2413 """Add an Image Sequence"""
2414 bl_idname = 'node.nw_add_sequence'
2415 bl_label = 'Import Image Sequence'
2416 bl_options = {'REGISTER', 'UNDO'}
2418 directory: StringProperty(
2419 subtype="DIR_PATH"
2421 filename: StringProperty(
2422 subtype="FILE_NAME"
2424 files: CollectionProperty(
2425 type=bpy.types.OperatorFileListElement,
2426 options={'HIDDEN', 'SKIP_SAVE'}
2428 relative_path: BoolProperty(
2429 name='Relative Path',
2430 description='Set the file path relative to the blend file, when possible',
2431 default=True
2434 @classmethod
2435 def poll(cls, context):
2436 return (nw_check(cls, context)
2437 and nw_check_space_type(cls, context, {'ShaderNodeTree', 'CompositorNodeTree'}))
2439 def draw(self, context):
2440 layout = self.layout
2441 layout.alignment = 'LEFT'
2443 layout.prop(self, 'relative_path')
2445 def execute(self, context):
2446 nodes, links = get_nodes_links(context)
2447 directory = self.directory
2448 filename = self.filename
2449 files = self.files
2450 tree = context.space_data.node_tree
2452 # DEBUG
2453 # print ("\nDIR:", directory)
2454 # print ("FN:", filename)
2455 # print ("Fs:", list(f.name for f in files), '\n')
2457 if tree.type == 'SHADER':
2458 node_type = "ShaderNodeTexImage"
2459 elif tree.type == 'COMPOSITING':
2460 node_type = "CompositorNodeImage"
2461 else:
2462 self.report({'ERROR'}, "Unsupported Node Tree type!")
2463 return {'CANCELLED'}
2465 if not files[0].name and not filename:
2466 self.report({'ERROR'}, "No file chosen")
2467 return {'CANCELLED'}
2468 elif files[0].name and (not filename or not path.exists(directory + filename)):
2469 # User has selected multiple files without an active one, or the active one is non-existent
2470 filename = files[0].name
2472 if not path.exists(directory + filename):
2473 self.report({'ERROR'}, filename + " does not exist!")
2474 return {'CANCELLED'}
2476 without_ext = '.'.join(filename.split('.')[:-1])
2478 # if last digit isn't a number, it's not a sequence
2479 if not without_ext[-1].isdigit():
2480 self.report({'ERROR'}, filename + " does not seem to be part of a sequence")
2481 return {'CANCELLED'}
2483 extension = filename.split('.')[-1]
2484 reverse = without_ext[::-1] # reverse string
2486 count_numbers = 0
2487 for char in reverse:
2488 if char.isdigit():
2489 count_numbers += 1
2490 else:
2491 break
2493 without_num = without_ext[:count_numbers * -1]
2495 files = sorted(glob(directory + without_num + "[0-9]" * count_numbers + "." + extension))
2497 num_frames = len(files)
2499 nodes_list = [node for node in nodes]
2500 if nodes_list:
2501 nodes_list.sort(key=lambda k: k.location.x)
2502 xloc = nodes_list[0].location.x - 220 # place new nodes at far left
2503 yloc = 0
2504 for node in nodes:
2505 node.select = False
2506 yloc += node_mid_pt(node, 'y')
2507 yloc = yloc / len(nodes)
2508 else:
2509 xloc = 0
2510 yloc = 0
2512 name_with_hashes = without_num + "#" * count_numbers + '.' + extension
2514 bpy.ops.node.add_node('INVOKE_DEFAULT', use_transform=True, type=node_type)
2515 node = nodes.active
2516 node.label = name_with_hashes
2518 filepath = directory + (without_ext + '.' + extension)
2519 if self.relative_path:
2520 if bpy.data.filepath:
2521 try:
2522 filepath = bpy.path.relpath(filepath)
2523 except ValueError:
2524 pass
2526 img = bpy.data.images.load(filepath)
2527 img.source = 'SEQUENCE'
2528 img.name = name_with_hashes
2529 node.image = img
2530 image_user = node.image_user if tree.type == 'SHADER' else node
2531 # separate the number from the file name of the first file
2532 image_user.frame_offset = int(files[0][len(without_num) + len(directory):-1 * (len(extension) + 1)]) - 1
2533 image_user.frame_duration = num_frames
2535 return {'FINISHED'}
2538 class NWAddMultipleImages(Operator, NWBase, ImportHelper):
2539 """Add multiple images at once"""
2540 bl_idname = 'node.nw_add_multiple_images'
2541 bl_label = 'Open Selected Images'
2542 bl_options = {'REGISTER', 'UNDO'}
2543 directory: StringProperty(
2544 subtype="DIR_PATH"
2546 files: CollectionProperty(
2547 type=bpy.types.OperatorFileListElement,
2548 options={'HIDDEN', 'SKIP_SAVE'}
2551 @classmethod
2552 def poll(cls, context):
2553 return (nw_check(cls, context)
2554 and nw_check_space_type(cls, context, {'ShaderNodeTree', 'CompositorNodeTree'}))
2556 def execute(self, context):
2557 nodes, links = get_nodes_links(context)
2559 xloc, yloc = context.region.view2d.region_to_view(context.area.width / 2, context.area.height / 2)
2561 if context.space_data.node_tree.type == 'SHADER':
2562 node_type = "ShaderNodeTexImage"
2563 elif context.space_data.node_tree.type == 'COMPOSITING':
2564 node_type = "CompositorNodeImage"
2565 else:
2566 self.report({'ERROR'}, "Unsupported Node Tree type!")
2567 return {'CANCELLED'}
2569 new_nodes = []
2570 for f in self.files:
2571 fname = f.name
2573 node = nodes.new(node_type)
2574 new_nodes.append(node)
2575 node.label = fname
2576 node.hide = True
2577 node.location.x = xloc
2578 node.location.y = yloc
2579 yloc -= 40
2581 img = bpy.data.images.load(self.directory + fname)
2582 node.image = img
2584 # shift new nodes up to center of tree
2585 list_size = new_nodes[0].location.y - new_nodes[-1].location.y
2586 for node in nodes:
2587 if node in new_nodes:
2588 node.select = True
2589 node.location.y += (list_size / 2)
2590 else:
2591 node.select = False
2592 return {'FINISHED'}
2595 class NWViewerFocus(bpy.types.Operator):
2596 """Set the viewer tile center to the mouse position"""
2597 bl_idname = "node.nw_viewer_focus"
2598 bl_label = "Viewer Focus"
2600 x: bpy.props.IntProperty()
2601 y: bpy.props.IntProperty()
2603 @classmethod
2604 def poll(cls, context):
2605 return (nw_check(cls, context)
2606 and nw_check_space_type(cls, context, {'CompositorNodeTree'}))
2608 def execute(self, context):
2609 return {'FINISHED'}
2611 def invoke(self, context, event):
2612 render = context.scene.render
2613 space = context.space_data
2614 percent = render.resolution_percentage * 0.01
2616 nodes, links = get_nodes_links(context)
2617 viewers = [n for n in nodes if n.type == 'VIEWER']
2619 if viewers:
2620 mlocx = event.mouse_region_x
2621 mlocy = event.mouse_region_y
2622 select_node = bpy.ops.node.select(location=(mlocx, mlocy), extend=False)
2624 if 'FINISHED' not in select_node: # only run if we're not clicking on a node
2625 region_x = context.region.width
2626 region_y = context.region.height
2628 region_center_x = context.region.width / 2
2629 region_center_y = context.region.height / 2
2631 bd_x = render.resolution_x * percent * space.backdrop_zoom
2632 bd_y = render.resolution_y * percent * space.backdrop_zoom
2634 backdrop_center_x = (bd_x / 2) - space.backdrop_offset[0]
2635 backdrop_center_y = (bd_y / 2) - space.backdrop_offset[1]
2637 margin_x = region_center_x - backdrop_center_x
2638 margin_y = region_center_y - backdrop_center_y
2640 abs_mouse_x = (mlocx - margin_x) / bd_x
2641 abs_mouse_y = (mlocy - margin_y) / bd_y
2643 for node in viewers:
2644 node.center_x = abs_mouse_x
2645 node.center_y = abs_mouse_y
2646 else:
2647 return {'PASS_THROUGH'}
2649 return self.execute(context)
2652 class NWSaveViewer(bpy.types.Operator, ExportHelper):
2653 """Save the current viewer node to an image file"""
2654 bl_idname = "node.nw_save_viewer"
2655 bl_label = "Save This Image"
2656 filepath: StringProperty(subtype="FILE_PATH")
2657 filename_ext: EnumProperty(
2658 name="Format",
2659 description="Choose the file format to save to",
2660 items=(('.bmp', "BMP", ""),
2661 ('.rgb', 'IRIS', ""),
2662 ('.png', 'PNG', ""),
2663 ('.jpg', 'JPEG', ""),
2664 ('.jp2', 'JPEG2000', ""),
2665 ('.tga', 'TARGA', ""),
2666 ('.cin', 'CINEON', ""),
2667 ('.dpx', 'DPX', ""),
2668 ('.exr', 'OPEN_EXR', ""),
2669 ('.hdr', 'HDR', ""),
2670 ('.tif', 'TIFF', "")),
2671 default='.png',
2674 @classmethod
2675 def poll(cls, context):
2676 return (nw_check(cls, context)
2677 and nw_check_space_type(cls, context, {'CompositorNodeTree'})
2678 and nw_check_viewer_node(cls))
2680 def execute(self, context):
2681 fp = self.filepath
2682 if fp:
2683 formats = {
2684 '.bmp': 'BMP',
2685 '.rgb': 'IRIS',
2686 '.png': 'PNG',
2687 '.jpg': 'JPEG',
2688 '.jpeg': 'JPEG',
2689 '.jp2': 'JPEG2000',
2690 '.tga': 'TARGA',
2691 '.cin': 'CINEON',
2692 '.dpx': 'DPX',
2693 '.exr': 'OPEN_EXR',
2694 '.hdr': 'HDR',
2695 '.tiff': 'TIFF',
2696 '.tif': 'TIFF'}
2697 basename, ext = path.splitext(fp)
2698 old_render_format = context.scene.render.image_settings.file_format
2699 context.scene.render.image_settings.file_format = formats[self.filename_ext]
2700 context.area.type = "IMAGE_EDITOR"
2701 context.area.spaces[0].image = bpy.data.images['Viewer Node']
2702 context.area.spaces[0].image.save_render(fp)
2703 context.area.type = "NODE_EDITOR"
2704 context.scene.render.image_settings.file_format = old_render_format
2705 return {'FINISHED'}
2708 class NWResetNodes(bpy.types.Operator):
2709 """Reset Nodes in Selection"""
2710 bl_idname = "node.nw_reset_nodes"
2711 bl_label = "Reset Nodes"
2712 bl_options = {'REGISTER', 'UNDO'}
2714 @classmethod
2715 def poll(cls, context):
2716 return (nw_check(cls, context)
2717 and nw_check_selected(cls, context)
2718 and nw_check_active(cls, context))
2720 def execute(self, context):
2721 node_active = context.active_node
2722 node_selected = context.selected_nodes
2723 node_ignore = ["FRAME", "REROUTE", "GROUP", "SIMULATION_INPUT", "SIMULATION_OUTPUT"]
2725 active_node_name = node_active.name if node_active.select else None
2726 valid_nodes = [n for n in node_selected if n.type not in node_ignore]
2728 # Create output lists
2729 selected_node_names = [n.name for n in node_selected]
2730 success_names = []
2732 # Reset all valid children in a frame
2733 node_active_is_frame = False
2734 if len(node_selected) == 1 and node_active.type == "FRAME":
2735 node_tree = node_active.id_data
2736 children = [n for n in node_tree.nodes if n.parent == node_active]
2737 if children:
2738 valid_nodes = [n for n in children if n.type not in node_ignore]
2739 selected_node_names = [n.name for n in children if n.type not in node_ignore]
2740 node_active_is_frame = True
2742 # Check if valid nodes in selection
2743 if not (len(valid_nodes) > 0):
2744 # Check for frames only
2745 frames_selected = [n for n in node_selected if n.type == "FRAME"]
2746 if (len(frames_selected) > 1 and len(frames_selected) == len(node_selected)):
2747 self.report({'ERROR'}, "Please select only 1 frame to reset")
2748 else:
2749 self.report({'ERROR'}, "No valid node(s) in selection")
2750 return {'CANCELLED'}
2752 # Report nodes that are not valid
2753 if len(valid_nodes) != len(node_selected) and node_active_is_frame is False:
2754 valid_node_names = [n.name for n in valid_nodes]
2755 not_valid_names = list(set(selected_node_names) - set(valid_node_names))
2756 self.report({'INFO'}, "Ignored {}".format(", ".join(not_valid_names)))
2758 # Deselect all nodes
2759 for i in node_selected:
2760 i.select = False
2762 # Run through all valid nodes
2763 for node in valid_nodes:
2765 parent = node.parent if node.parent else None
2766 node_loc = [node.location.x, node.location.y]
2768 node_tree = node.id_data
2769 props_to_copy = 'bl_idname name location height width'.split(' ')
2771 reconnections = []
2772 mappings = chain.from_iterable([node.inputs, node.outputs])
2773 for i in (i for i in mappings if i.is_linked):
2774 for L in i.links:
2775 reconnections.append([L.from_socket.path_from_id(), L.to_socket.path_from_id()])
2777 props = {j: getattr(node, j) for j in props_to_copy}
2779 new_node = node_tree.nodes.new(props['bl_idname'])
2780 props_to_copy.pop(0)
2782 for prop in props_to_copy:
2783 setattr(new_node, prop, props[prop])
2785 nodes = node_tree.nodes
2786 nodes.remove(node)
2787 new_node.name = props['name']
2789 if parent:
2790 new_node.parent = parent
2791 new_node.location = node_loc
2793 for str_from, str_to in reconnections:
2794 connect_sockets(eval(str_from), eval(str_to))
2796 new_node.select = False
2797 success_names.append(new_node.name)
2799 # Reselect all nodes
2800 if selected_node_names and node_active_is_frame is False:
2801 for i in selected_node_names:
2802 node_tree.nodes[i].select = True
2804 if active_node_name is not None:
2805 node_tree.nodes[active_node_name].select = True
2806 node_tree.nodes.active = node_tree.nodes[active_node_name]
2808 self.report({'INFO'}, "Successfully reset {}".format(", ".join(success_names)))
2809 return {'FINISHED'}
2812 classes = (
2813 NWLazyMix,
2814 NWLazyConnect,
2815 NWDeleteUnused,
2816 NWSwapLinks,
2817 NWResetBG,
2818 NWAddAttrNode,
2819 NWPreviewNode,
2820 NWFrameSelected,
2821 NWReloadImages,
2822 NWMergeNodes,
2823 NWBatchChangeNodes,
2824 NWChangeMixFactor,
2825 NWCopySettings,
2826 NWCopyLabel,
2827 NWClearLabel,
2828 NWModifyLabels,
2829 NWAddTextureSetup,
2830 NWAddPrincipledSetup,
2831 NWAddReroutes,
2832 NWLinkActiveToSelected,
2833 NWAlignNodes,
2834 NWSelectParentChildren,
2835 NWDetachOutputs,
2836 NWLinkToOutputNode,
2837 NWMakeLink,
2838 NWCallInputsMenu,
2839 NWAddSequence,
2840 NWAddMultipleImages,
2841 NWViewerFocus,
2842 NWSaveViewer,
2843 NWResetNodes,
2847 def register():
2848 from bpy.utils import register_class
2849 for cls in classes:
2850 register_class(cls)
2853 def unregister():
2854 from bpy.utils import unregister_class
2856 for cls in classes:
2857 unregister_class(cls)