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