1 # SPDX-FileCopyrightText: 2013-2023 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
7 from bpy
.types
import Operator
8 from bpy
.props
import (
17 from bpy_extras
.io_utils
import ImportHelper
, ExportHelper
18 from bpy_extras
.node_utils
import connect_sockets
19 from mathutils
import Vector
23 from itertools
import chain
25 from .interface
import NWConnectionListInputs
, NWConnectionListOutputs
27 from .utils
.constants
import blend_types
, geo_combine_operations
, operations
, navs
, get_texture_node_types
, rl_outputs
28 from .utils
.draw
import draw_callback_nodeoutline
29 from .utils
.paths
import match_files_to_socket_names
, split_into_components
30 from .utils
.nodes
import (node_mid_pt
, autolink
, node_at_pos
, get_nodes_links
, is_viewer_socket
, is_viewer_link
,
31 get_group_output_node
, get_output_location
, force_update
, get_internal_socket
, nw_check
,
32 nw_check_space_type
, NWBase
, get_first_enabled_output
, is_visible_socket
, viewer_socket_name
)
34 class NWLazyMix(Operator
, NWBase
):
35 """Add a Mix RGB/Shader node by interactively drawing lines between nodes"""
36 bl_idname
= "node.nw_lazy_mix"
37 bl_label
= "Mix Nodes"
38 bl_options
= {'REGISTER', 'UNDO'}
40 def modal(self
, context
, event
):
41 context
.area
.tag_redraw()
42 nodes
, links
= get_nodes_links(context
)
45 start_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
48 if not context
.scene
.NWBusyDrawing
:
49 node1
= node_at_pos(nodes
, context
, event
)
51 context
.scene
.NWBusyDrawing
= node1
.name
53 if context
.scene
.NWBusyDrawing
!= 'STOP':
54 node1
= nodes
[context
.scene
.NWBusyDrawing
]
56 context
.scene
.NWLazySource
= node1
.name
57 context
.scene
.NWLazyTarget
= node_at_pos(nodes
, context
, event
).name
59 if event
.type == 'MOUSEMOVE':
60 self
.mouse_path
.append((event
.mouse_region_x
, event
.mouse_region_y
))
62 elif event
.type == 'RIGHTMOUSE' and event
.value
== 'RELEASE':
63 end_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
64 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
67 node2
= node_at_pos(nodes
, context
, event
)
69 context
.scene
.NWBusyDrawing
= node2
.name
81 bpy
.ops
.node
.nw_merge_nodes(mode
="MIX", merge_type
="AUTO")
83 context
.scene
.NWBusyDrawing
= ""
86 elif event
.type == 'ESC':
88 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
91 return {'RUNNING_MODAL'}
93 def invoke(self
, context
, event
):
94 if context
.area
.type == 'NODE_EDITOR':
95 # the arguments we pass the the callback
96 args
= (self
, context
, 'MIX')
97 # Add the region OpenGL drawing callback
98 # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
99 self
._handle
= bpy
.types
.SpaceNodeEditor
.draw_handler_add(
100 draw_callback_nodeoutline
, args
, 'WINDOW', 'POST_PIXEL')
104 context
.window_manager
.modal_handler_add(self
)
105 return {'RUNNING_MODAL'}
107 self
.report({'WARNING'}, "View3D not found, cannot run operator")
111 class NWLazyConnect(Operator
, NWBase
):
112 """Connect two nodes without clicking a specific socket (automatically determined"""
113 bl_idname
= "node.nw_lazy_connect"
114 bl_label
= "Lazy Connect"
115 bl_options
= {'REGISTER', 'UNDO'}
116 with_menu
: BoolProperty()
118 def modal(self
, context
, event
):
119 context
.area
.tag_redraw()
120 nodes
, links
= get_nodes_links(context
)
123 start_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
126 if not context
.scene
.NWBusyDrawing
:
127 node1
= node_at_pos(nodes
, context
, event
)
129 context
.scene
.NWBusyDrawing
= node1
.name
131 if context
.scene
.NWBusyDrawing
!= 'STOP':
132 node1
= nodes
[context
.scene
.NWBusyDrawing
]
134 context
.scene
.NWLazySource
= node1
.name
135 context
.scene
.NWLazyTarget
= node_at_pos(nodes
, context
, event
).name
137 if event
.type == 'MOUSEMOVE':
138 self
.mouse_path
.append((event
.mouse_region_x
, event
.mouse_region_y
))
140 elif event
.type == 'RIGHTMOUSE' and event
.value
== 'RELEASE':
141 end_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
142 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
145 node2
= node_at_pos(nodes
, context
, event
)
147 context
.scene
.NWBusyDrawing
= node2
.name
160 original_sel
.append(node
)
162 original_unsel
.append(node
)
166 # link_success = autolink(node1, node2, links)
168 if len(node1
.outputs
) > 1 and node2
.inputs
:
169 bpy
.ops
.wm
.call_menu("INVOKE_DEFAULT", name
=NWConnectionListOutputs
.bl_idname
)
170 elif len(node1
.outputs
) == 1:
171 bpy
.ops
.node
.nw_call_inputs_menu(from_socket
=0)
173 link_success
= autolink(node1
, node2
, links
)
175 for node
in original_sel
:
177 for node
in original_unsel
:
181 force_update(context
)
182 context
.scene
.NWBusyDrawing
= ""
185 elif event
.type == 'ESC':
186 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
189 return {'RUNNING_MODAL'}
191 def invoke(self
, context
, event
):
192 if context
.area
.type == 'NODE_EDITOR':
193 nodes
, links
= get_nodes_links(context
)
194 node
= node_at_pos(nodes
, context
, event
)
196 context
.scene
.NWBusyDrawing
= node
.name
198 # the arguments we pass the the callback
202 args
= (self
, context
, mode
)
203 # Add the region OpenGL drawing callback
204 # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
205 self
._handle
= bpy
.types
.SpaceNodeEditor
.draw_handler_add(
206 draw_callback_nodeoutline
, args
, 'WINDOW', 'POST_PIXEL')
210 context
.window_manager
.modal_handler_add(self
)
211 return {'RUNNING_MODAL'}
213 self
.report({'WARNING'}, "View3D not found, cannot run operator")
217 class NWDeleteUnused(Operator
, NWBase
):
218 """Delete all nodes whose output is not used"""
219 bl_idname
= 'node.nw_del_unused'
220 bl_label
= 'Delete Unused Nodes'
221 bl_options
= {'REGISTER', 'UNDO'}
223 delete_muted
: BoolProperty(
225 description
="Delete (but reconnect, like Ctrl-X) all muted nodes",
227 delete_frames
: BoolProperty(
228 name
="Delete Empty Frames",
229 description
="Delete all frames that have no nodes inside them",
232 def is_unused_node(self
, node
):
233 end_types
= ['OUTPUT_MATERIAL', 'OUTPUT', 'VIEWER', 'COMPOSITE',
234 'SPLITVIEWER', 'OUTPUT_FILE', 'LEVELS', 'OUTPUT_LIGHT',
235 'OUTPUT_WORLD', 'GROUP_INPUT', 'GROUP_OUTPUT', 'FRAME']
236 if node
.type in end_types
:
239 for output
in node
.outputs
:
245 def poll(cls
, context
):
246 """Disabled for custom nodes as we do not know which nodes are supported."""
247 return (nw_check(context
)
248 and nw_check_space_type(cls
, context
, 'ShaderNodeTree', 'CompositorNodeTree',
249 'TextureNodeTree', 'GeometryNodeTree')
250 and context
.space_data
.node_tree
.nodes
)
252 def execute(self
, context
):
253 nodes
, links
= get_nodes_links(context
)
259 selection
.append(node
.name
)
265 temp_deleted_nodes
= []
266 del_unused_iterations
= len(nodes
)
267 for it
in range(0, del_unused_iterations
):
268 temp_deleted_nodes
= list(deleted_nodes
) # keep record of last iteration
270 if self
.is_unused_node(node
):
272 deleted_nodes
.append(node
.name
)
273 bpy
.ops
.node
.delete()
275 if temp_deleted_nodes
== deleted_nodes
: # stop iterations when there are no more nodes to be deleted
278 if self
.delete_frames
:
286 frames_in_use
.append(node
.parent
)
288 if node
.type == 'FRAME' and node
not in frames_in_use
:
291 repeat
= True # repeat for nested frames
293 if node
not in frames_in_use
:
295 deleted_nodes
.append(node
.name
)
296 bpy
.ops
.node
.delete()
298 if self
.delete_muted
:
302 deleted_nodes
.append(node
.name
)
303 bpy
.ops
.node
.delete_reconnect()
305 # get unique list of deleted nodes (iterations would count the same node more than once)
306 deleted_nodes
= list(set(deleted_nodes
))
307 for n
in deleted_nodes
:
308 self
.report({'INFO'}, "Node " + n
+ " deleted")
309 num_deleted
= len(deleted_nodes
)
314 self
.report({'INFO'}, "Deleted " + str(num_deleted
) + n
)
316 self
.report({'INFO'}, "Nothing deleted")
319 nodes
, links
= get_nodes_links(context
)
321 if node
.name
in selection
:
325 def invoke(self
, context
, event
):
326 return context
.window_manager
.invoke_confirm(self
, event
)
329 class NWSwapLinks(Operator
, NWBase
):
330 """Swap the output connections of the two selected nodes, or two similar inputs of a single node"""
331 bl_idname
= 'node.nw_swap_links'
332 bl_label
= 'Swap Links'
333 bl_options
= {'REGISTER', 'UNDO'}
336 def poll(cls
, context
):
337 return nw_check(context
) and context
.selected_nodes
and len(context
.selected_nodes
) <= 2
339 def execute(self
, context
):
340 nodes
, links
= get_nodes_links(context
)
341 selected_nodes
= context
.selected_nodes
342 n1
= selected_nodes
[0]
345 if len(selected_nodes
) == 2:
346 n2
= selected_nodes
[1]
347 if n1
.outputs
and n2
.outputs
:
352 for output
in n1
.outputs
:
354 for link
in output
.links
:
355 n1_outputs
.append([out_index
, link
.to_socket
])
360 for output
in n2
.outputs
:
362 for link
in output
.links
:
363 n2_outputs
.append([out_index
, link
.to_socket
])
367 for connection
in n1_outputs
:
369 connect_sockets(n2
.outputs
[connection
[0]], connection
[1])
371 self
.report({'WARNING'},
372 "Some connections have been lost due to differing numbers of output sockets")
373 for connection
in n2_outputs
:
375 connect_sockets(n1
.outputs
[connection
[0]], connection
[1])
377 self
.report({'WARNING'},
378 "Some connections have been lost due to differing numbers of output sockets")
380 if n1
.outputs
or n2
.outputs
:
381 self
.report({'WARNING'}, "One of the nodes has no outputs!")
383 self
.report({'WARNING'}, "Neither of the nodes have outputs!")
386 elif len(selected_nodes
) == 1:
387 if n1
.inputs
and n1
.inputs
[0].is_multi_input
:
388 self
.report({'WARNING'}, "Can't swap inputs of a multi input socket!")
394 if i1
.is_linked
and not i1
.is_multi_input
:
397 if i1
.type == i2
.type and i2
.is_linked
and not i2
.is_multi_input
:
399 types
.append([i1
, similar_types
, i
])
401 types
.sort(key
=lambda k
: k
[1], reverse
=True)
407 if t
[0].type == i2
.type == t
[0].type and t
[0] != i2
and i2
.is_linked
:
409 i1f
= pair
[0].links
[0].from_socket
410 i1t
= pair
[0].links
[0].to_socket
411 i2f
= pair
[1].links
[0].from_socket
412 i2t
= pair
[1].links
[0].to_socket
413 connect_sockets(i1f
, i2t
)
414 connect_sockets(i2f
, i1t
)
417 fs
= t
[0].links
[0].from_socket
419 links
.remove(t
[0].links
[0])
420 if i
+ 1 == len(n1
.inputs
):
423 while n1
.inputs
[i
].is_linked
:
425 connect_sockets(fs
, n1
.inputs
[i
])
426 elif len(types
) == 2:
427 i1f
= types
[0][0].links
[0].from_socket
428 i1t
= types
[0][0].links
[0].to_socket
429 i2f
= types
[1][0].links
[0].from_socket
430 i2t
= types
[1][0].links
[0].to_socket
431 connect_sockets(i1f
, i2t
)
432 connect_sockets(i2f
, i1t
)
435 self
.report({'WARNING'}, "This node has no input connections to swap!")
437 self
.report({'WARNING'}, "This node has no inputs to swap!")
439 force_update(context
)
443 class NWResetBG(Operator
, NWBase
):
444 """Reset the zoom and position of the background image"""
445 bl_idname
= 'node.nw_bg_reset'
446 bl_label
= 'Reset Backdrop'
447 bl_options
= {'REGISTER', 'UNDO'}
450 def poll(cls
, context
):
451 return (nw_check(context
)
452 and nw_check_space_type(cls
, context
, 'CompositorNodeTree'))
454 def execute(self
, context
):
455 context
.space_data
.backdrop_zoom
= 1
456 context
.space_data
.backdrop_offset
[0] = 0
457 context
.space_data
.backdrop_offset
[1] = 0
461 class NWAddAttrNode(Operator
, NWBase
):
462 """Add an Attribute node with this name"""
463 bl_idname
= 'node.nw_add_attr_node'
464 bl_label
= 'Add UV map'
465 bl_options
= {'REGISTER', 'UNDO'}
467 attr_name
: StringProperty()
470 def poll(cls
, context
):
471 return (nw_check(context
)
472 and nw_check_space_type(cls
, context
, 'ShaderNodeTree'))
474 def execute(self
, context
):
475 bpy
.ops
.node
.add_node('INVOKE_DEFAULT', use_transform
=True, type="ShaderNodeAttribute")
476 nodes
, links
= get_nodes_links(context
)
477 nodes
.active
.attribute_name
= self
.attr_name
481 class NWPreviewNode(Operator
, NWBase
):
482 bl_idname
= "node.nw_preview_node"
483 bl_label
= "Preview Node"
484 bl_description
= "Connect active node to the Node Group output or the Material Output"
485 bl_options
= {'REGISTER', 'UNDO'}
487 # If false, the operator is not executed if the current node group happens to be a geometry nodes group.
488 # This is needed because geometry nodes has its own viewer node that uses the same shortcut as in the compositor.
489 run_in_geometry_nodes
: BoolProperty(default
=True)
492 self
.shader_output_type
= ""
493 self
.shader_output_ident
= ""
496 def poll(cls
, context
):
497 """Already implemented natively for compositing nodes."""
498 return (nw_check(context
)
499 and nw_check_space_type(cls
, context
, 'ShaderNodeTree', 'GeometryNodeTree')
500 and (not context
.active_node
501 or context
.active_node
.type not in {"OUTPUT_MATERIAL", "OUTPUT_WORLD"}))
504 def get_output_sockets(node_tree
):
505 return [item
for item
in node_tree
.interface
.items_tree
506 if item
.item_type
== 'SOCKET' and item
.in_out
in {'OUTPUT', 'BOTH'}]
508 def init_shader_variables(self
, space
, shader_type
):
509 if shader_type
== 'OBJECT':
510 if space
.id in bpy
.data
.lights
.values():
511 self
.shader_output_type
= "OUTPUT_LIGHT"
512 self
.shader_output_ident
= "ShaderNodeOutputLight"
514 self
.shader_output_type
= "OUTPUT_MATERIAL"
515 self
.shader_output_ident
= "ShaderNodeOutputMaterial"
517 elif shader_type
== 'WORLD':
518 self
.shader_output_type
= "OUTPUT_WORLD"
519 self
.shader_output_ident
= "ShaderNodeOutputWorld"
521 def ensure_viewer_socket(self
, node_tree
, socket_type
, connect_socket
=None):
522 """Check if a viewer output already exists in a node group, otherwise create it"""
524 output_sockets
= self
.get_output_sockets(node_tree
)
525 if len(output_sockets
):
526 for i
, socket
in enumerate(output_sockets
):
527 if is_viewer_socket(socket
) and socket
.socket_type
== socket_type
:
528 # If viewer output is already used but leads to the same socket we can still use it
529 is_used
= self
.is_socket_used_other_mats(socket
)
531 if connect_socket
is None:
533 groupout
= get_group_output_node(node_tree
)
534 groupout_input
= groupout
.inputs
[i
]
535 links
= groupout_input
.links
536 if connect_socket
not in [link
.from_socket
for link
in links
]:
538 viewer_socket
= socket
541 if viewer_socket
is None:
542 # Create viewer socket
543 viewer_socket
= node_tree
.interface
.new_socket(
544 viewer_socket_name
, in_out
='OUTPUT', socket_type
=socket_type
)
545 viewer_socket
.NWViewerSocket
= True
549 def ensure_group_output(node_tree
):
550 """Check if a group output node exists, otherwise create it"""
551 groupout
= get_group_output_node(node_tree
)
553 groupout
= node_tree
.nodes
.new('NodeGroupOutput')
554 loc_x
, loc_y
= get_output_location(tree
)
555 groupout
.location
.x
= loc_x
556 groupout
.location
.y
= loc_y
557 groupout
.select
= False
558 # So that we don't keep on adding new group outputs
559 groupout
.is_active_output
= True
563 def search_sockets(cls
, node
, sockets
, index
=None):
564 """Recursively scan nodes for viewer sockets and store them in a list"""
565 for i
, input_socket
in enumerate(node
.inputs
):
566 if index
and i
!= index
:
568 if len(input_socket
.links
):
569 link
= input_socket
.links
[0]
570 next_node
= link
.from_node
571 external_socket
= link
.from_socket
572 if hasattr(next_node
, "node_tree"):
573 for socket_index
, socket
in enumerate(next_node
.node_tree
.interface
.items_tree
):
574 if socket
.identifier
== external_socket
.identifier
:
576 if is_viewer_socket(socket
) and socket
not in sockets
:
577 sockets
.append(socket
)
578 # continue search inside of node group but restrict socket to where we came from
579 groupout
= get_group_output_node(next_node
.node_tree
)
580 cls
.search_sockets(groupout
, sockets
, index
=socket_index
)
583 def scan_nodes(cls
, tree
, sockets
):
584 """Recursively get all viewer sockets in a material tree"""
585 for node
in tree
.nodes
:
586 if hasattr(node
, "node_tree"):
587 if node
.node_tree
is None:
589 for socket
in cls
.get_output_sockets(node
.node_tree
):
590 if is_viewer_socket(socket
) and (socket
not in sockets
):
591 sockets
.append(socket
)
592 cls
.scan_nodes(node
.node_tree
, sockets
)
595 def remove_socket(tree
, socket
):
596 interface
= tree
.interface
597 interface
.remove(socket
)
598 interface
.active_index
= min(interface
.active_index
, len(interface
.items_tree
) - 1)
600 def link_leads_to_used_socket(self
, link
):
601 """Return True if link leads to a socket that is already used in this node"""
602 socket
= get_internal_socket(link
.to_socket
)
603 return socket
and self
.is_socket_used_active_tree(socket
)
605 def is_socket_used_active_tree(self
, socket
):
606 """Ensure used sockets in active node tree is calculated and check given socket"""
607 if not hasattr(self
, "used_viewer_sockets_active_mat"):
608 self
.used_viewer_sockets_active_mat
= []
610 node_tree
= bpy
.context
.space_data
.node_tree
612 if node_tree
.type == 'GEOMETRY':
613 output_node
= get_group_output_node(node_tree
)
614 elif node_tree
.type == 'SHADER':
615 output_node
= get_group_output_node(node_tree
,
616 output_node_type
=self
.shader_output_type
)
618 if output_node
is not None:
619 self
.search_sockets(output_node
, self
.used_viewer_sockets_active_mat
)
620 return socket
in self
.used_viewer_sockets_active_mat
622 def is_socket_used_other_mats(self
, socket
):
623 """Ensure used sockets in other materials are calculated and check given socket"""
624 if not hasattr(self
, "used_viewer_sockets_other_mats"):
625 self
.used_viewer_sockets_other_mats
= []
626 for mat
in bpy
.data
.materials
:
627 if mat
.node_tree
== bpy
.context
.space_data
.node_tree
or not hasattr(mat
.node_tree
, "nodes"):
630 output_node
= get_group_output_node(mat
.node_tree
,
631 output_node_type
=self
.shader_output_type
)
632 if output_node
is not None:
633 self
.search_sockets(output_node
, self
.used_viewer_sockets_other_mats
)
634 return socket
in self
.used_viewer_sockets_other_mats
636 def get_output_index(self
, node
, output_node
, is_base_node_tree
, socket_type
, check_type
=False):
637 """Get the next available output socket in the active node"""
640 for i
, out
in enumerate(node
.outputs
):
641 if is_visible_socket(out
) and (not check_type
or out
.type == socket_type
):
642 valid_outputs
.append(i
)
644 out_i
= valid_outputs
[0] # Start index of node's outputs
645 for i
, valid_i
in enumerate(valid_outputs
):
646 for out_link
in node
.outputs
[valid_i
].links
:
647 if is_viewer_link(out_link
, output_node
):
648 if is_base_node_tree
or self
.link_leads_to_used_socket(out_link
):
649 if i
< len(valid_outputs
) - 1:
650 out_i
= valid_outputs
[i
+ 1]
652 out_i
= valid_outputs
[0]
655 def create_links(self
, path
, node
, active_node_socket_id
, socket_type
):
656 """Create links at each step in the node group path."""
657 path
= list(reversed(path
))
658 # Starting from the level of the active node
659 for path_index
, path_element
in enumerate(path
[:-1]):
660 # Ensure there is a viewer node and it has an input
661 tree
= path_element
.node_tree
662 viewer_socket
= self
.ensure_viewer_socket(
664 connect_socket
= node
.outputs
[active_node_socket_id
]
665 if path_index
== 0 else None)
666 if viewer_socket
in self
.delete_sockets
:
667 self
.delete_sockets
.remove(viewer_socket
)
669 # Connect the current to its viewer
670 link_start
= node
.outputs
[active_node_socket_id
]
671 link_end
= self
.ensure_group_output(tree
).inputs
[viewer_socket
.identifier
]
672 connect_sockets(link_start
, link_end
)
674 # Go up in the node group hierarchy
675 next_tree
= path
[path_index
+ 1].node_tree
676 node
= next(n
for n
in next_tree
.nodes
678 and n
.node_tree
== tree
)
680 active_node_socket_id
= viewer_socket
.identifier
681 return node
.outputs
[active_node_socket_id
]
685 for socket
in self
.delete_sockets
:
686 if not self
.is_socket_used_other_mats(socket
):
687 tree
= socket
.id_data
688 self
.remove_socket(tree
, socket
)
690 def invoke(self
, context
, event
):
691 space
= context
.space_data
692 # Ignore operator when running in wrong context.
693 if self
.run_in_geometry_nodes
!= (space
.tree_type
== "GeometryNodeTree"):
694 return {'PASS_THROUGH'}
696 mlocx
= event
.mouse_region_x
697 mlocy
= event
.mouse_region_y
698 select_node
= bpy
.ops
.node
.select(location
=(mlocx
, mlocy
), extend
=False)
699 if 'FINISHED' not in select_node
: # only run if mouse click is on a node
702 base_node_tree
= space
.node_tree
703 active_tree
= context
.space_data
.edit_tree
704 path
= context
.space_data
.path
705 nodes
= active_tree
.nodes
706 active
= nodes
.active
708 if not active
and not any(is_visible_socket(out
) for out
in active
.outputs
):
711 # Scan through all nodes in tree including nodes inside of groups to find viewer sockets
712 self
.delete_sockets
= []
713 self
.scan_nodes(base_node_tree
, self
.delete_sockets
)
715 if not active
.outputs
:
719 # For geometry node trees, we just connect to the group output
720 if space
.tree_type
== "GeometryNodeTree":
721 socket_type
= 'NodeSocketGeometry'
723 # Find (or create if needed) the output of this node tree
724 output_node
= self
.ensure_group_output(base_node_tree
)
726 active_node_socket_index
= self
.get_output_index(
727 active
, output_node
, base_node_tree
== active_tree
, 'GEOMETRY', check_type
=True
729 # If there is no 'GEOMETRY' output type - We can't preview the node
730 if active_node_socket_index
is None:
733 # Find an input socket of the output of type geometry
734 output_node_socket_index
= None
735 for i
, inp
in enumerate(output_node
.inputs
):
736 if inp
.type == 'GEOMETRY':
737 output_node_socket_index
= i
739 if output_node_socket_index
is None:
740 # Create geometry socket
741 geometry_out_socket
= base_node_tree
.interface
.new_socket(
742 'Geometry', in_out
='OUTPUT', socket_type
=socket_type
744 output_node_socket_index
= geometry_out_socket
.index
746 # For shader node trees, we connect to a material output
747 elif space
.tree_type
== "ShaderNodeTree":
748 socket_type
= 'NodeSocketShader'
749 self
.init_shader_variables(space
, space
.shader_type
)
751 # Get or create material_output node
752 output_node
= get_group_output_node(base_node_tree
,
753 output_node_type
=self
.shader_output_type
)
755 output_node
= base_node_tree
.nodes
.new(self
.shader_output_ident
)
756 output_node
.location
= get_output_location(base_node_tree
)
757 output_node
.select
= False
759 active_node_socket_index
= self
.get_output_index(
760 active
, output_node
, base_node_tree
== active_tree
, 'SHADER'
762 if active
.outputs
[active_node_socket_index
].name
== "Volume":
763 output_node_socket_index
= 1
765 output_node_socket_index
= 0
767 # If there are no nested node groups, the link starts at the active node
768 node_output
= active
.outputs
[active_node_socket_index
]
770 # Recursively connect inside nested node groups and get the one from base level
771 node_output
= self
.create_links(path
, active
, active_node_socket_index
, socket_type
)
772 output_node_input
= output_node
.inputs
[output_node_socket_index
]
774 # Connect at base level
775 connect_sockets(node_output
, output_node_input
)
778 nodes
.active
= active
780 force_update(context
)
784 class NWFrameSelected(Operator
, NWBase
):
785 bl_idname
= "node.nw_frame_selected"
786 bl_label
= "Frame Selected"
787 bl_description
= "Add a frame node and parent the selected nodes to it"
788 bl_options
= {'REGISTER', 'UNDO'}
790 label_prop
: StringProperty(
792 description
='The visual name of the frame node',
795 use_custom_color_prop
: BoolProperty(
797 description
="Use custom color for the frame node",
800 color_prop
: FloatVectorProperty(
802 description
="The color of the frame node",
803 default
=(0.604, 0.604, 0.604),
804 min=0, max=1, step
=1, precision
=3,
805 subtype
='COLOR_GAMMA', size
=3
808 def draw(self
, context
):
810 layout
.prop(self
, 'label_prop')
811 layout
.prop(self
, 'use_custom_color_prop')
812 col
= layout
.column()
813 col
.active
= self
.use_custom_color_prop
814 col
.prop(self
, 'color_prop', text
="")
816 def execute(self
, context
):
817 nodes
, links
= get_nodes_links(context
)
821 selected
.append(node
)
823 bpy
.ops
.node
.add_node(type='NodeFrame')
825 frm
.label
= self
.label_prop
826 frm
.use_custom_color
= self
.use_custom_color_prop
827 frm
.color
= self
.color_prop
829 for node
in selected
:
835 class NWReloadImages(Operator
):
836 bl_idname
= "node.nw_reload_images"
837 bl_label
= "Reload Images"
838 bl_description
= "Update all the image nodes to match their files on disk"
841 def poll(cls
, context
):
842 return (nw_check(context
)
843 and nw_check_space_type(cls
, context
, 'ShaderNodeTree', 'CompositorNodeTree',
844 'TextureNodeTree', 'GeometryNodeTree')
845 and context
.active_node
is not None
846 and any(is_visible_socket(out
) for out
in context
.active_node
.outputs
))
848 def execute(self
, context
):
849 nodes
, links
= get_nodes_links(context
)
850 image_types
= ["IMAGE", "TEX_IMAGE", "TEX_ENVIRONMENT", "TEXTURE"]
853 if node
.type in image_types
:
854 if node
.type == "TEXTURE":
855 if node
.texture
: # node has texture assigned
856 if node
.texture
.type in ['IMAGE', 'ENVIRONMENT_MAP']:
857 if node
.texture
.image
: # texture has image assigned
858 node
.texture
.image
.reload()
866 self
.report({'INFO'}, "Reloaded images")
867 print("Reloaded " + str(num_reloaded
) + " images")
868 force_update(context
)
871 self
.report({'WARNING'}, "No images found to reload in this node tree")
875 class NWMergeNodes(Operator
, NWBase
):
876 bl_idname
= "node.nw_merge_nodes"
877 bl_label
= "Merge Nodes"
878 bl_description
= "Merge Selected Nodes"
879 bl_options
= {'REGISTER', 'UNDO'}
883 description
="All possible blend types, boolean operations and math operations",
884 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
],
886 merge_type
: EnumProperty(
888 description
="Type of Merge to be used",
890 ('AUTO', 'Auto', 'Automatic Output Type Detection'),
891 ('SHADER', 'Shader', 'Merge using ADD or MIX Shader'),
892 ('GEOMETRY', 'Geometry', 'Merge using Mesh Boolean or Join Geometry Node'),
893 ('MIX', 'Mix Node', 'Merge using Mix Nodes'),
894 ('MATH', 'Math Node', 'Merge using Math Nodes'),
895 ('ZCOMBINE', 'Z-Combine Node', 'Merge using Z-Combine Nodes'),
896 ('ALPHAOVER', 'Alpha Over Node', 'Merge using Alpha Over Nodes'),
900 # Check if the link connects to a node that is in selected_nodes
901 # If not, then check recursively for each link in the nodes outputs.
902 # If yes, return True. If the recursion stops without finding a node
903 # in selected_nodes, it returns False. The depth is used to prevent
904 # getting stuck in a loop because of an already present cycle.
906 def link_creates_cycle(link
, selected_nodes
, depth
=0) -> bool:
908 # We're stuck in a cycle, but that cycle was already present,
909 # so we return False.
910 # NOTE: The number 255 is arbitrary, but seems to work well.
913 if node
in selected_nodes
:
917 for output
in node
.outputs
:
919 for olink
in output
.links
:
920 if NWMergeNodes
.link_creates_cycle(olink
, selected_nodes
, depth
+ 1):
922 # None of the outputs found a node in selected_nodes, so there is no cycle.
925 # Merge the nodes in `nodes_list` with a node of type `node_name` that has a multi_input socket.
926 # The parameters `socket_indices` gives the indices of the node sockets in the order that they should
927 # be connected. The last one is assumed to be a multi input socket.
928 # For convenience the node is returned.
930 def merge_with_multi_input(nodes_list
, merge_position
, do_hide
, loc_x
, links
, nodes
, node_name
, socket_indices
):
931 # The y-location of the last node
932 loc_y
= nodes_list
[-1][2]
933 if merge_position
== 'CENTER':
934 # Average the y-location
935 for i
in range(len(nodes_list
) - 1):
936 loc_y
+= nodes_list
[i
][2]
937 loc_y
= loc_y
/ len(nodes_list
)
938 new_node
= nodes
.new(node_name
)
939 new_node
.hide
= do_hide
940 new_node
.location
.x
= loc_x
941 new_node
.location
.y
= loc_y
942 selected_nodes
= [nodes
[node_info
[0]] for node_info
in nodes_list
]
944 outputs_for_multi_input
= []
945 for i
, node
in enumerate(selected_nodes
):
947 # Search for the first node which had output links that do not create
948 # a cycle, which we can then reconnect afterwards.
949 if prev_links
== [] and node
.outputs
[0].is_linked
:
951 link
for link
in node
.outputs
[0].links
if not NWMergeNodes
.link_creates_cycle(
952 link
, selected_nodes
)]
953 # Get the index of the socket, the last one is a multi input, and is thus used repeatedly
954 # To get the placement to look right we need to reverse the order in which we connect the
955 # outputs to the multi input socket.
956 if i
< len(socket_indices
) - 1:
957 ind
= socket_indices
[i
]
958 connect_sockets(node
.outputs
[0], new_node
.inputs
[ind
])
960 outputs_for_multi_input
.insert(0, node
.outputs
[0])
961 if outputs_for_multi_input
!= []:
962 ind
= socket_indices
[-1]
963 for output
in outputs_for_multi_input
:
964 connect_sockets(output
, new_node
.inputs
[ind
])
966 for link
in prev_links
:
967 connect_sockets(new_node
.outputs
[0], link
.to_node
.inputs
[0])
971 def poll(cls
, context
):
972 return (nw_check(context
)
973 and nw_check_space_type(cls
, context
, 'ShaderNodeTree', 'CompositorNodeTree',
974 'TextureNodeTree', 'GeometryNodeTree'))
976 def execute(self
, context
):
977 settings
= context
.preferences
.addons
[__package__
].preferences
978 merge_hide
= settings
.merge_hide
979 merge_position
= settings
.merge_position
# 'center' or 'bottom'
982 do_hide_shader
= False
983 if merge_hide
== 'ALWAYS':
985 do_hide_shader
= True
986 elif merge_hide
== 'NON_SHADER':
989 tree_type
= context
.space_data
.node_tree
.type
990 if tree_type
== 'GEOMETRY':
991 node_type
= 'GeometryNode'
992 if tree_type
== 'COMPOSITING':
993 node_type
= 'CompositorNode'
994 elif tree_type
== 'SHADER':
995 node_type
= 'ShaderNode'
996 elif tree_type
== 'TEXTURE':
997 node_type
= 'TextureNode'
998 nodes
, links
= get_nodes_links(context
)
1000 merge_type
= self
.merge_type
1001 # Prevent trying to add Z-Combine in not 'COMPOSITING' node tree.
1002 # 'ZCOMBINE' works only if mode == 'MIX'
1003 # Setting mode to None prevents trying to add 'ZCOMBINE' node.
1004 if (merge_type
== 'ZCOMBINE' or merge_type
== 'ALPHAOVER') and tree_type
!= 'COMPOSITING':
1007 if (merge_type
!= 'MATH' and merge_type
!= 'GEOMETRY') and tree_type
== 'GEOMETRY':
1009 # The Mix node and math nodes used for geometry nodes are of type 'ShaderNode'
1010 if (merge_type
== 'MATH' or merge_type
== 'MIX') and tree_type
== 'GEOMETRY':
1011 node_type
= 'ShaderNode'
1012 selected_mix
= [] # entry = [index, loc]
1013 selected_shader
= [] # entry = [index, loc]
1014 selected_geometry
= [] # entry = [index, loc]
1015 selected_math
= [] # entry = [index, loc]
1016 selected_vector
= [] # entry = [index, loc]
1017 selected_z
= [] # entry = [index, loc]
1018 selected_alphaover
= [] # entry = [index, loc]
1020 for i
, node
in enumerate(nodes
):
1021 if node
.select
and node
.outputs
:
1022 if merge_type
== 'AUTO':
1023 for (type, types_list
, dst
) in (
1024 ('SHADER', ('MIX', 'ADD'), selected_shader
),
1025 ('GEOMETRY', [t
[0] for t
in geo_combine_operations
], selected_geometry
),
1026 ('RGBA', [t
[0] for t
in blend_types
], selected_mix
),
1027 ('VALUE', [t
[0] for t
in operations
], selected_math
),
1028 ('VECTOR', [], selected_vector
),
1030 output
= get_first_enabled_output(node
)
1031 output_type
= output
.type
1032 valid_mode
= mode
in types_list
1033 # When mode is 'MIX' we have to cheat since the mix node is not used in
1035 if tree_type
== 'GEOMETRY':
1037 if output_type
== 'VALUE' and type == 'VALUE':
1039 elif output_type
== 'VECTOR' and type == 'VECTOR':
1041 elif type == 'GEOMETRY':
1043 # When mode is 'MIX' use mix node for both 'RGBA' and 'VALUE' output types.
1044 # Cheat that output type is 'RGBA',
1045 # and that 'MIX' exists in math operations list.
1046 # This way when selected_mix list is analyzed:
1047 # Node data will be appended even though it doesn't meet requirements.
1048 elif output_type
!= 'SHADER' and mode
== 'MIX':
1049 output_type
= 'RGBA'
1051 if output_type
== type and valid_mode
:
1052 dst
.append([i
, node
.location
.x
, node
.location
.y
, node
.dimensions
.x
, node
.hide
])
1054 for (type, types_list
, dst
) in (
1055 ('SHADER', ('MIX', 'ADD'), selected_shader
),
1056 ('GEOMETRY', [t
[0] for t
in geo_combine_operations
], selected_geometry
),
1057 ('MIX', [t
[0] for t
in blend_types
], selected_mix
),
1058 ('MATH', [t
[0] for t
in operations
], selected_math
),
1059 ('ZCOMBINE', ('MIX', ), selected_z
),
1060 ('ALPHAOVER', ('MIX', ), selected_alphaover
),
1062 if merge_type
== type and mode
in types_list
:
1063 dst
.append([i
, node
.location
.x
, node
.location
.y
, node
.dimensions
.x
, node
.hide
])
1064 # When nodes with output kinds 'RGBA' and 'VALUE' are selected at the same time
1065 # use only 'Mix' nodes for merging.
1066 # For that we add selected_math list to selected_mix list and clear selected_math.
1067 if selected_mix
and selected_math
and merge_type
== 'AUTO':
1068 selected_mix
+= selected_math
1071 # If no nodes are selected, do nothing and pass through.
1072 if not (selected_mix
+ selected_shader
+ selected_geometry
+ selected_math
1073 + selected_vector
+ selected_z
+ selected_alphaover
):
1074 return {'PASS_THROUGH'}
1083 selected_alphaover
]:
1086 count_before
= len(nodes
)
1087 # sort list by loc_x - reversed
1088 nodes_list
.sort(key
=lambda k
: k
[1], reverse
=True)
1090 loc_x
= nodes_list
[0][1] + nodes_list
[0][3] + 70
1091 nodes_list
.sort(key
=lambda k
: k
[2], reverse
=True)
1093 # Change the node type for math nodes in a geometry node tree.
1094 if tree_type
== 'GEOMETRY':
1095 if nodes_list
is selected_math
or nodes_list
is selected_vector
or nodes_list
is selected_mix
:
1096 node_type
= 'ShaderNode'
1100 node_type
= 'GeometryNode'
1101 if merge_position
== 'CENTER':
1102 # average yloc of last two nodes (lowest two)
1103 loc_y
= ((nodes_list
[len(nodes_list
) - 1][2]) + (nodes_list
[len(nodes_list
) - 2][2])) / 2
1104 if nodes_list
[len(nodes_list
) - 1][-1]: # if last node is hidden, mix should be shifted up a bit
1110 loc_y
= nodes_list
[len(nodes_list
) - 1][2]
1114 if nodes_list
== selected_shader
and not do_hide_shader
:
1116 the_range
= len(nodes_list
) - 1
1117 if len(nodes_list
) == 1:
1120 for i
in range(the_range
):
1121 if nodes_list
== selected_mix
:
1123 if tree_type
== 'COMPOSITING':
1125 add_type
= node_type
+ mix_name
1126 add
= nodes
.new(add_type
)
1127 if tree_type
!= 'COMPOSITING':
1128 add
.data_type
= 'RGBA'
1129 add
.blend_type
= mode
1131 add
.inputs
[0].default_value
= 1.0
1132 add
.show_preview
= False
1138 if tree_type
== 'COMPOSITING':
1141 elif nodes_list
== selected_math
:
1142 add_type
= node_type
+ 'Math'
1143 add
= nodes
.new(add_type
)
1144 add
.operation
= mode
1150 elif nodes_list
== selected_shader
:
1152 add_type
= node_type
+ 'MixShader'
1153 add
= nodes
.new(add_type
)
1154 add
.hide
= do_hide_shader
1160 add_type
= node_type
+ 'AddShader'
1161 add
= nodes
.new(add_type
)
1162 add
.hide
= do_hide_shader
1167 elif nodes_list
== selected_geometry
:
1168 if mode
in ('JOIN', 'MIX'):
1169 add_type
= node_type
+ 'JoinGeometry'
1170 add
= self
.merge_with_multi_input(
1171 nodes_list
, merge_position
, do_hide
, loc_x
, links
, nodes
, add_type
, [0])
1173 add_type
= node_type
+ 'MeshBoolean'
1174 indices
= [0, 1] if mode
== 'DIFFERENCE' else [1]
1175 add
= self
.merge_with_multi_input(
1176 nodes_list
, merge_position
, do_hide
, loc_x
, links
, nodes
, add_type
, indices
)
1177 add
.operation
= mode
1180 elif nodes_list
== selected_vector
:
1181 add_type
= node_type
+ 'VectorMath'
1182 add
= nodes
.new(add_type
)
1183 add
.operation
= mode
1189 elif nodes_list
== selected_z
:
1190 add
= nodes
.new('CompositorNodeZcombine')
1191 add
.show_preview
= False
1197 elif nodes_list
== selected_alphaover
:
1198 add
= nodes
.new('CompositorNodeAlphaOver')
1199 add
.show_preview
= False
1205 add
.location
= loc_x
, loc_y
1209 # This has already been handled separately
1213 count_after
= len(nodes
)
1214 index
= count_after
- 1
1215 first_selected
= nodes
[nodes_list
[0][0]]
1216 # "last" node has been added as first, so its index is count_before.
1217 last_add
= nodes
[count_before
]
1218 # Create list of invalid indexes.
1219 invalid_nodes
= [nodes
[n
[0]]
1220 for n
in (selected_mix
+ selected_math
+ selected_shader
+ selected_z
+ selected_geometry
)]
1223 # Two nodes were selected and first selected has no output links, second selected has output links.
1224 # Then add links from last add to all links 'to_socket' of out links of second selected.
1225 first_selected_output
= get_first_enabled_output(first_selected
)
1226 if len(nodes_list
) == 2:
1227 if not first_selected_output
.links
:
1228 second_selected
= nodes
[nodes_list
[1][0]]
1229 for ss_link
in get_first_enabled_output(second_selected
).links
:
1230 # Prevent cyclic dependencies when nodes to be merged are linked to one another.
1231 # Link only if "to_node" index not in invalid indexes list.
1232 if not self
.link_creates_cycle(ss_link
, invalid_nodes
):
1233 connect_sockets(get_first_enabled_output(last_add
), ss_link
.to_socket
)
1234 # add links from last_add to all links 'to_socket' of out links of first selected.
1235 for fs_link
in first_selected_output
.links
:
1236 # Link only if "to_node" index not in invalid indexes list.
1237 if not self
.link_creates_cycle(fs_link
, invalid_nodes
):
1238 connect_sockets(get_first_enabled_output(last_add
), fs_link
.to_socket
)
1239 # add link from "first" selected and "first" add node
1240 node_to
= nodes
[count_after
- 1]
1241 connect_sockets(first_selected_output
, node_to
.inputs
[first
])
1242 if node_to
.type == 'ZCOMBINE':
1243 for fs_out
in first_selected
.outputs
:
1244 if fs_out
!= first_selected_output
and fs_out
.name
in ('Z', 'Depth'):
1245 connect_sockets(fs_out
, node_to
.inputs
[1])
1247 # add links between added ADD nodes and between selected and ADD nodes
1248 for i
in range(count_adds
):
1249 if i
< count_adds
- 1:
1250 node_from
= nodes
[index
]
1251 node_to
= nodes
[index
- 1]
1252 node_to_input_i
= first
1253 node_to_z_i
= 1 # if z combine - link z to first z input
1254 connect_sockets(get_first_enabled_output(node_from
), node_to
.inputs
[node_to_input_i
])
1255 if node_to
.type == 'ZCOMBINE':
1256 for from_out
in node_from
.outputs
:
1257 if from_out
!= get_first_enabled_output(node_from
) and from_out
.name
in ('Z', 'Depth'):
1258 connect_sockets(from_out
, node_to
.inputs
[node_to_z_i
])
1259 if len(nodes_list
) > 1:
1260 node_from
= nodes
[nodes_list
[i
+ 1][0]]
1261 node_to
= nodes
[index
]
1262 node_to_input_i
= second
1263 node_to_z_i
= 3 # if z combine - link z to second z input
1264 connect_sockets(get_first_enabled_output(node_from
), node_to
.inputs
[node_to_input_i
])
1265 if node_to
.type == 'ZCOMBINE':
1266 for from_out
in node_from
.outputs
:
1267 if from_out
!= get_first_enabled_output(node_from
) and from_out
.name
in ('Z', 'Depth'):
1268 connect_sockets(from_out
, node_to
.inputs
[node_to_z_i
])
1270 # set "last" of added nodes as active
1271 nodes
.active
= last_add
1272 for i
, x
, y
, dx
, h
in nodes_list
:
1273 nodes
[i
].select
= False
1278 class NWBatchChangeNodes(Operator
, NWBase
):
1279 bl_idname
= "node.nw_batch_change"
1280 bl_label
= "Batch Change"
1281 bl_description
= "Batch Change Blend Type and Math Operation"
1282 bl_options
= {'REGISTER', 'UNDO'}
1284 blend_type
: EnumProperty(
1286 items
=blend_types
+ navs
,
1288 operation
: EnumProperty(
1290 items
=operations
+ navs
,
1294 def poll(cls
, context
):
1295 return (nw_check(context
)
1296 and nw_check_space_type(cls
, context
, 'ShaderNodeTree', 'CompositorNodeTree',
1297 'TextureNodeTree', 'GeometryNodeTree'))
1299 def execute(self
, context
):
1300 blend_type
= self
.blend_type
1301 operation
= self
.operation
1302 for node
in context
.selected_nodes
:
1303 if node
.type == 'MIX_RGB' or (node
.bl_idname
== 'ShaderNodeMix' and node
.data_type
== 'RGBA'):
1304 if blend_type
not in [nav
[0] for nav
in navs
]:
1305 node
.blend_type
= blend_type
1307 if blend_type
== 'NEXT':
1308 index
= [i
for i
, entry
in enumerate(blend_types
) if node
.blend_type
in entry
][0]
1309 # index = blend_types.index(node.blend_type)
1310 if index
== len(blend_types
) - 1:
1311 node
.blend_type
= blend_types
[0][0]
1313 node
.blend_type
= blend_types
[index
+ 1][0]
1315 if blend_type
== 'PREV':
1316 index
= [i
for i
, entry
in enumerate(blend_types
) if node
.blend_type
in entry
][0]
1318 node
.blend_type
= blend_types
[len(blend_types
) - 1][0]
1320 node
.blend_type
= blend_types
[index
- 1][0]
1322 if node
.type == 'MATH' or node
.bl_idname
== 'ShaderNodeMath':
1323 if operation
not in [nav
[0] for nav
in navs
]:
1324 node
.operation
= operation
1326 if operation
== 'NEXT':
1327 index
= [i
for i
, entry
in enumerate(operations
) if node
.operation
in entry
][0]
1328 # index = operations.index(node.operation)
1329 if index
== len(operations
) - 1:
1330 node
.operation
= operations
[0][0]
1332 node
.operation
= operations
[index
+ 1][0]
1334 if operation
== 'PREV':
1335 index
= [i
for i
, entry
in enumerate(operations
) if node
.operation
in entry
][0]
1336 # index = operations.index(node.operation)
1338 node
.operation
= operations
[len(operations
) - 1][0]
1340 node
.operation
= operations
[index
- 1][0]
1345 class NWChangeMixFactor(Operator
, NWBase
):
1346 bl_idname
= "node.nw_factor"
1347 bl_label
= "Change Factor"
1348 bl_description
= "Change Factors of Mix Nodes and Mix Shader Nodes"
1349 bl_options
= {'REGISTER', 'UNDO'}
1351 # option: Change factor.
1352 # If option is 1.0 or 0.0 - set to 1.0 or 0.0
1353 # Else - change factor by option value.
1354 option
: FloatProperty()
1356 def execute(self
, context
):
1357 nodes
, links
= get_nodes_links(context
)
1358 option
= self
.option
1359 selected
= [] # entry = index
1360 for si
, node
in enumerate(nodes
):
1362 if node
.type in {'MIX_RGB', 'MIX_SHADER'} or node
.bl_idname
== 'ShaderNodeMix':
1366 fac
= nodes
[si
].inputs
[0]
1367 nodes
[si
].hide
= False
1368 if option
in {0.0, 1.0}:
1369 fac
.default_value
= option
1371 fac
.default_value
+= option
1376 class NWCopySettings(Operator
, NWBase
):
1377 bl_idname
= "node.nw_copy_settings"
1378 bl_label
= "Copy Settings"
1379 bl_description
= "Copy Settings of Active Node to Selected Nodes"
1380 bl_options
= {'REGISTER', 'UNDO'}
1383 def poll(cls
, context
):
1384 return (nw_check(context
)
1385 and context
.active_node
is not None
1386 and context
.active_node
.type != 'FRAME')
1388 def execute(self
, context
):
1389 node_active
= context
.active_node
1390 node_selected
= context
.selected_nodes
1393 if not (len(node_selected
) > 1):
1394 self
.report({'ERROR'}, "2 nodes must be selected at least")
1395 return {'CANCELLED'}
1397 # Check if active node is in the selection
1398 selected_node_names
= [n
.name
for n
in node_selected
]
1399 if node_active
.name
not in selected_node_names
:
1400 self
.report({'ERROR'}, "No active node")
1401 return {'CANCELLED'}
1403 # Get nodes in selection by type
1404 valid_nodes
= [n
for n
in node_selected
if n
.type == node_active
.type]
1406 if not (len(valid_nodes
) > 1) and node_active
:
1407 self
.report({'ERROR'}, "Selected nodes are not of the same type as {}".format(node_active
.name
))
1408 return {'CANCELLED'}
1410 if len(valid_nodes
) != len(node_selected
):
1411 # Report nodes that are not valid
1412 valid_node_names
= [n
.name
for n
in valid_nodes
]
1413 not_valid_names
= list(set(selected_node_names
) - set(valid_node_names
))
1416 "Ignored {} (not of the same type as {})".format(
1417 ", ".join(not_valid_names
),
1420 # Reference original
1422 # node_selected_names = [n.name for n in node_selected]
1427 # Deselect all nodes
1428 for i
in node_selected
:
1431 # Code by zeffii from http://blender.stackexchange.com/a/42338/3710
1432 # Run through all other nodes
1433 for node
in valid_nodes
[1:]:
1435 # Check for frame node
1436 parent
= node
.parent
if node
.parent
else None
1437 node_loc
= [node
.location
.x
, node
.location
.y
]
1439 # Select original to duplicate
1442 # Duplicate selected node
1443 bpy
.ops
.node
.duplicate()
1444 new_node
= context
.selected_nodes
[0]
1447 new_node
.select
= False
1449 # Properties to copy
1450 node_tree
= node
.id_data
1451 props_to_copy
= 'bl_idname name location height width'.split(' ')
1455 mappings
= chain
.from_iterable([node
.inputs
, node
.outputs
])
1456 for i
in (i
for i
in mappings
if i
.is_linked
):
1458 reconnections
.append([L
.from_socket
.path_from_id(), L
.to_socket
.path_from_id()])
1461 props
= {j
: getattr(node
, j
) for j
in props_to_copy
}
1462 props_to_copy
.pop(0)
1464 for prop
in props_to_copy
:
1465 setattr(new_node
, prop
, props
[prop
])
1467 # Get the node tree to remove the old node
1468 nodes
= node_tree
.nodes
1470 new_node
.name
= props
['name']
1473 new_node
.parent
= parent
1474 new_node
.location
= node_loc
1476 for str_from
, str_to
in reconnections
:
1477 node_tree
.connect_sockets(eval(str_from
), eval(str_to
))
1479 success_names
.append(new_node
.name
)
1482 node_tree
.nodes
.active
= orig
1485 "Successfully copied attributes from {} to: {}".format(
1487 ", ".join(success_names
)))
1491 class NWCopyLabel(Operator
, NWBase
):
1492 bl_idname
= "node.nw_copy_label"
1493 bl_label
= "Copy Label"
1494 bl_options
= {'REGISTER', 'UNDO'}
1496 option
: EnumProperty(
1498 description
="Source of name of label",
1500 ('FROM_ACTIVE', 'from active', 'from active node',),
1501 ('FROM_NODE', 'from node', 'from node linked to selected node'),
1502 ('FROM_SOCKET', 'from socket', 'from socket linked to selected node'),
1506 def execute(self
, context
):
1507 nodes
, links
= get_nodes_links(context
)
1508 option
= self
.option
1509 active
= nodes
.active
1510 if option
== 'FROM_ACTIVE':
1512 src_label
= active
.label
1513 for node
in [n
for n
in nodes
if n
.select
and nodes
.active
!= n
]:
1514 node
.label
= src_label
1515 elif option
== 'FROM_NODE':
1516 selected
= [n
for n
in nodes
if n
.select
]
1517 for node
in selected
:
1518 for input in node
.inputs
:
1520 src
= input.links
[0].from_node
1521 node
.label
= src
.label
1523 elif option
== 'FROM_SOCKET':
1524 selected
= [n
for n
in nodes
if n
.select
]
1525 for node
in selected
:
1526 for input in node
.inputs
:
1528 src
= input.links
[0].from_socket
1529 node
.label
= src
.name
1535 class NWClearLabel(Operator
, NWBase
):
1536 bl_idname
= "node.nw_clear_label"
1537 bl_label
= "Clear Label"
1538 bl_options
= {'REGISTER', 'UNDO'}
1540 option
: BoolProperty()
1542 def execute(self
, context
):
1543 nodes
, links
= get_nodes_links(context
)
1544 for node
in [n
for n
in nodes
if n
.select
]:
1549 def invoke(self
, context
, event
):
1551 return self
.execute(context
)
1553 return context
.window_manager
.invoke_confirm(self
, event
)
1556 class NWModifyLabels(Operator
, NWBase
):
1557 """Modify Labels of all selected nodes"""
1558 bl_idname
= "node.nw_modify_labels"
1559 bl_label
= "Modify Labels"
1560 bl_options
= {'REGISTER', 'UNDO'}
1562 prepend
: StringProperty(
1563 name
="Add to Beginning"
1565 append
: StringProperty(
1568 replace_from
: StringProperty(
1569 name
="Text to Replace"
1571 replace_to
: StringProperty(
1575 def execute(self
, context
):
1576 nodes
, links
= get_nodes_links(context
)
1577 for node
in [n
for n
in nodes
if n
.select
]:
1578 node
.label
= self
.prepend
+ node
.label
.replace(self
.replace_from
, self
.replace_to
) + self
.append
1582 def invoke(self
, context
, event
):
1586 return context
.window_manager
.invoke_props_dialog(self
)
1589 class NWAddTextureSetup(Operator
, NWBase
):
1590 bl_idname
= "node.nw_add_texture"
1591 bl_label
= "Texture Setup"
1592 bl_description
= "Add Texture Node Setup to Selected Shaders"
1593 bl_options
= {'REGISTER', 'UNDO'}
1595 add_mapping
: BoolProperty(
1596 name
="Add Mapping Nodes",
1597 description
="Create coordinate and mapping nodes for the texture (ignored for selected texture nodes)",
1601 def poll(cls
, context
):
1602 return (nw_check(context
)
1603 and nw_check_space_type(cls
, context
, 'ShaderNodeTree'))
1605 def execute(self
, context
):
1606 nodes
, links
= get_nodes_links(context
)
1608 texture_types
= get_texture_node_types()
1609 selected_nodes
= [n
for n
in nodes
if n
.select
]
1611 for node
in selected_nodes
:
1616 target_input
= node
.inputs
[0]
1617 for input in node
.inputs
:
1620 if not input.is_linked
:
1621 target_input
= input
1624 self
.report({'WARNING'}, "No free inputs for node: " + node
.name
)
1629 locx
= node
.location
.x
1630 locy
= node
.location
.y
- (input_index
* padding
)
1632 is_texture_node
= node
.rna_type
.identifier
in texture_types
1633 use_environment_texture
= node
.type == 'BACKGROUND'
1635 # Add an image texture before normal shader nodes.
1636 if not is_texture_node
:
1637 image_texture_type
= 'ShaderNodeTexEnvironment' if use_environment_texture
else 'ShaderNodeTexImage'
1638 image_texture_node
= nodes
.new(image_texture_type
)
1639 x_offset
= x_offset
+ image_texture_node
.width
+ padding
1640 image_texture_node
.location
= [locx
- x_offset
, locy
]
1641 nodes
.active
= image_texture_node
1642 connect_sockets(image_texture_node
.outputs
[0], target_input
)
1644 # The mapping setup following this will connect to the first input of this image texture.
1645 target_input
= image_texture_node
.inputs
[0]
1649 if is_texture_node
or self
.add_mapping
:
1651 mapping_node
= nodes
.new('ShaderNodeMapping')
1652 x_offset
= x_offset
+ mapping_node
.width
+ padding
1653 mapping_node
.location
= [locx
- x_offset
, locy
]
1654 connect_sockets(mapping_node
.outputs
[0], target_input
)
1656 # Add Texture Coordinates node.
1657 tex_coord_node
= nodes
.new('ShaderNodeTexCoord')
1658 x_offset
= x_offset
+ tex_coord_node
.width
+ padding
1659 tex_coord_node
.location
= [locx
- x_offset
, locy
]
1661 is_procedural_texture
= is_texture_node
and node
.type != 'TEX_IMAGE'
1662 use_generated_coordinates
= is_procedural_texture
or use_environment_texture
1663 tex_coord_output
= tex_coord_node
.outputs
[0 if use_generated_coordinates
else 2]
1664 connect_sockets(tex_coord_output
, mapping_node
.inputs
[0])
1669 class NWAddPrincipledSetup(Operator
, NWBase
, ImportHelper
):
1670 bl_idname
= "node.nw_add_textures_for_principled"
1671 bl_label
= "Principled Texture Setup"
1672 bl_description
= "Add Texture Node Setup for Principled BSDF"
1673 bl_options
= {'REGISTER', 'UNDO'}
1675 directory
: StringProperty(
1679 description
='Folder to search in for image files'
1681 files
: CollectionProperty(
1682 type=bpy
.types
.OperatorFileListElement
,
1683 options
={'HIDDEN', 'SKIP_SAVE'}
1686 relative_path
: BoolProperty(
1687 name
='Relative Path',
1688 description
='Set the file path relative to the blend file, when possible',
1697 def draw(self
, context
):
1698 layout
= self
.layout
1699 layout
.alignment
= 'LEFT'
1701 layout
.prop(self
, 'relative_path')
1704 def poll(cls
, context
):
1705 return (nw_check(context
)
1706 and nw_check_space_type(cls
, context
, 'ShaderNodeTree'))
1708 def execute(self
, context
):
1709 # Check if everything is ok
1710 if not self
.directory
:
1711 self
.report({'INFO'}, 'No Folder Selected')
1712 return {'CANCELLED'}
1713 if not self
.files
[:]:
1714 self
.report({'INFO'}, 'No Files Selected')
1715 return {'CANCELLED'}
1717 nodes
, links
= get_nodes_links(context
)
1718 active_node
= nodes
.active
1719 if not (active_node
and active_node
.bl_idname
== 'ShaderNodeBsdfPrincipled'):
1720 self
.report({'INFO'}, 'Select Principled BSDF')
1721 return {'CANCELLED'}
1723 # Filter textures names for texturetypes in filenames
1724 # [Socket Name, [abbreviations and keyword list], Filename placeholder]
1725 tags
= context
.preferences
.addons
[__package__
].preferences
.principled_tags
1726 normal_abbr
= tags
.normal
.split(' ')
1727 bump_abbr
= tags
.bump
.split(' ')
1728 gloss_abbr
= tags
.gloss
.split(' ')
1729 rough_abbr
= tags
.rough
.split(' ')
1731 ['Displacement', tags
.displacement
.split(' '), None],
1732 ['Base Color', tags
.base_color
.split(' '), None],
1733 ['Metallic', tags
.metallic
.split(' '), None],
1734 ['Specular IOR Level', tags
.specular
.split(' '), None],
1735 ['Roughness', rough_abbr
+ gloss_abbr
, None],
1736 ['Bump', bump_abbr
, None],
1737 ['Normal', normal_abbr
, None],
1738 ['Transmission Weight', tags
.transmission
.split(' '), None],
1739 ['Emission Color', tags
.emission
.split(' '), None],
1740 ['Alpha', tags
.alpha
.split(' '), None],
1741 ['Ambient Occlusion', tags
.ambient_occlusion
.split(' '), None],
1744 match_files_to_socket_names(self
.files
, socketnames
)
1745 # Remove socketnames without found files
1746 socketnames
= [s
for s
in socketnames
if s
[2]
1747 and path
.exists(self
.directory
+ s
[2])]
1749 self
.report({'INFO'}, 'No matching images found')
1750 print('No matching images found')
1751 return {'CANCELLED'}
1753 # Don't override path earlier as os.path is used to check the absolute path
1754 import_path
= self
.directory
1755 if self
.relative_path
:
1756 if bpy
.data
.filepath
:
1758 import_path
= bpy
.path
.relpath(self
.directory
)
1763 print('\nMatched Textures:')
1768 normal_node_texture
= None
1770 bump_node_texture
= None
1771 roughness_node
= None
1772 for i
, sname
in enumerate(socketnames
):
1773 print(i
, sname
[0], sname
[2])
1775 # DISPLACEMENT NODES
1776 if sname
[0] == 'Displacement':
1777 disp_texture
= nodes
.new(type='ShaderNodeTexImage')
1778 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
1779 disp_texture
.image
= img
1780 disp_texture
.label
= 'Displacement'
1781 if disp_texture
.image
:
1782 disp_texture
.image
.colorspace_settings
.is_data
= True
1784 # Add displacement offset nodes
1785 disp_node
= nodes
.new(type='ShaderNodeDisplacement')
1786 # Align the Displacement node under the active Principled BSDF node
1787 disp_node
.location
= active_node
.location
+ Vector((100, -700))
1788 link
= connect_sockets(disp_node
.inputs
[0], disp_texture
.outputs
[0])
1790 # TODO Turn on true displacement in the material
1791 # Too complicated for now
1794 output_node
= [n
for n
in nodes
if n
.bl_idname
== 'ShaderNodeOutputMaterial']
1796 if not output_node
[0].inputs
[2].is_linked
:
1797 link
= connect_sockets(output_node
[0].inputs
[2], disp_node
.outputs
[0])
1802 elif sname
[0] == 'Bump':
1803 # Test if new texture node is bump map
1804 fname_components
= split_into_components(sname
[2])
1805 match_bump
= set(bump_abbr
).intersection(set(fname_components
))
1807 # If Bump add bump node in between
1808 bump_node_texture
= nodes
.new(type='ShaderNodeTexImage')
1809 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
1810 bump_node_texture
.image
= img
1811 bump_node_texture
.label
= 'Bump'
1814 bump_node
= nodes
.new(type='ShaderNodeBump')
1815 link
= connect_sockets(bump_node
.inputs
[2], bump_node_texture
.outputs
[0])
1816 link
= connect_sockets(active_node
.inputs
['Normal'], bump_node
.outputs
[0])
1820 elif sname
[0] == 'Normal':
1821 # Test if new texture node is normal map
1822 fname_components
= split_into_components(sname
[2])
1823 match_normal
= set(normal_abbr
).intersection(set(fname_components
))
1825 # If Normal add normal node in between
1826 normal_node_texture
= nodes
.new(type='ShaderNodeTexImage')
1827 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
1828 normal_node_texture
.image
= img
1829 normal_node_texture
.label
= 'Normal'
1832 normal_node
= nodes
.new(type='ShaderNodeNormalMap')
1833 link
= connect_sockets(normal_node
.inputs
[1], normal_node_texture
.outputs
[0])
1834 # Connect to bump node if it was created before, otherwise to the BSDF
1835 if bump_node
is None:
1836 link
= connect_sockets(active_node
.inputs
[sname
[0]], normal_node
.outputs
[0])
1838 link
= connect_sockets(bump_node
.inputs
[sname
[0]], normal_node
.outputs
[sname
[0]])
1841 # AMBIENT OCCLUSION TEXTURE
1842 elif sname
[0] == 'Ambient Occlusion':
1843 ao_texture
= nodes
.new(type='ShaderNodeTexImage')
1844 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
1845 ao_texture
.image
= img
1846 ao_texture
.label
= sname
[0]
1847 if ao_texture
.image
:
1848 ao_texture
.image
.colorspace_settings
.is_data
= True
1852 if not active_node
.inputs
[sname
[0]].is_linked
:
1853 # No texture node connected -> add texture node with new image
1854 texture_node
= nodes
.new(type='ShaderNodeTexImage')
1855 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
1856 texture_node
.image
= img
1858 if sname
[0] == 'Roughness':
1859 # Test if glossy or roughness map
1860 fname_components
= split_into_components(sname
[2])
1861 match_rough
= set(rough_abbr
).intersection(set(fname_components
))
1862 match_gloss
= set(gloss_abbr
).intersection(set(fname_components
))
1865 # If Roughness nothing to to
1866 link
= connect_sockets(active_node
.inputs
[sname
[0]], texture_node
.outputs
[0])
1869 # If Gloss Map add invert node
1870 invert_node
= nodes
.new(type='ShaderNodeInvert')
1871 link
= connect_sockets(invert_node
.inputs
[1], texture_node
.outputs
[0])
1873 link
= connect_sockets(active_node
.inputs
[sname
[0]], invert_node
.outputs
[0])
1874 roughness_node
= texture_node
1877 # This is a simple connection Texture --> Input slot
1878 link
= connect_sockets(active_node
.inputs
[sname
[0]], texture_node
.outputs
[0])
1880 # Use non-color except for color inputs
1881 if sname
[0] not in ['Base Color', 'Emission Color'] and texture_node
.image
:
1882 texture_node
.image
.colorspace_settings
.is_data
= True
1885 # If already texture connected. add to node list for alignment
1886 texture_node
= active_node
.inputs
[sname
[0]].links
[0].from_node
1888 # This are all connected texture nodes
1889 texture_nodes
.append(texture_node
)
1890 texture_node
.label
= sname
[0]
1893 texture_nodes
.append(disp_texture
)
1894 if bump_node_texture
:
1895 texture_nodes
.append(bump_node_texture
)
1896 if normal_node_texture
:
1897 texture_nodes
.append(normal_node_texture
)
1900 # We want the ambient occlusion texture to be the top most texture node
1901 texture_nodes
.insert(0, ao_texture
)
1904 for i
, texture_node
in enumerate(texture_nodes
):
1905 offset
= Vector((-550, (i
* -280) + 200))
1906 texture_node
.location
= active_node
.location
+ offset
1909 # Extra alignment if normal node was added
1910 normal_node
.location
= normal_node_texture
.location
+ Vector((300, 0))
1913 # Extra alignment if bump node was added
1914 bump_node
.location
= bump_node_texture
.location
+ Vector((300, 0))
1917 # Alignment of invert node if glossy map
1918 invert_node
.location
= roughness_node
.location
+ Vector((300, 0))
1920 # Add texture input + mapping
1921 mapping
= nodes
.new(type='ShaderNodeMapping')
1922 mapping
.location
= active_node
.location
+ Vector((-1050, 0))
1923 if len(texture_nodes
) > 1:
1924 # If more than one texture add reroute node in between
1925 reroute
= nodes
.new(type='NodeReroute')
1926 texture_nodes
.append(reroute
)
1927 tex_coords
= Vector((texture_nodes
[0].location
.x
,
1928 sum(n
.location
.y
for n
in texture_nodes
) / len(texture_nodes
)))
1929 reroute
.location
= tex_coords
+ Vector((-50, -120))
1930 for texture_node
in texture_nodes
:
1931 link
= connect_sockets(texture_node
.inputs
[0], reroute
.outputs
[0])
1932 link
= connect_sockets(reroute
.inputs
[0], mapping
.outputs
[0])
1934 link
= connect_sockets(texture_nodes
[0].inputs
[0], mapping
.outputs
[0])
1936 # Connect texture_coordinates to mapping node
1937 texture_input
= nodes
.new(type='ShaderNodeTexCoord')
1938 texture_input
.location
= mapping
.location
+ Vector((-200, 0))
1939 link
= connect_sockets(mapping
.inputs
[0], texture_input
.outputs
[2])
1941 # Create frame around tex coords and mapping
1942 frame
= nodes
.new(type='NodeFrame')
1943 frame
.label
= 'Mapping'
1944 mapping
.parent
= frame
1945 texture_input
.parent
= frame
1948 # Create frame around texture nodes
1949 frame
= nodes
.new(type='NodeFrame')
1950 frame
.label
= 'Textures'
1951 for tnode
in texture_nodes
:
1952 tnode
.parent
= frame
1956 active_node
.select
= False
1959 force_update(context
)
1963 class NWAddReroutes(Operator
, NWBase
):
1964 """Add Reroute Nodes and link them to outputs of selected nodes"""
1965 bl_idname
= "node.nw_add_reroutes"
1966 bl_label
= "Add Reroutes"
1967 bl_description
= "Add Reroutes to Outputs"
1968 bl_options
= {'REGISTER', 'UNDO'}
1970 option
: EnumProperty(
1973 ('ALL', 'to all', 'Add to all outputs'),
1974 ('LOOSE', 'to loose', 'Add only to loose outputs'),
1975 ('LINKED', 'to linked', 'Add only to linked outputs'),
1979 def execute(self
, context
):
1980 tree_type
= context
.space_data
.node_tree
.type
1981 option
= self
.option
1982 nodes
, links
= get_nodes_links(context
)
1983 # output valid when option is 'all' or when 'loose' output has no links
1985 post_select
= [] # nodes to be selected after execution
1986 # create reroutes and recreate links
1987 for node
in [n
for n
in nodes
if n
.select
]:
1992 # unhide 'REROUTE' nodes to avoid issues with location.y
1993 if node
.type == 'REROUTE':
1995 # Hack needed to calculate real width
1997 bpy
.ops
.node
.select_all(action
='DESELECT')
1998 helper
= nodes
.new('NodeReroute')
1999 helper
.select
= True
2001 # resize node and helper to zero. Then check locations to calculate width
2002 bpy
.ops
.transform
.resize(value
=(0.0, 0.0, 0.0))
2003 width
= 2.0 * (helper
.location
.x
- node
.location
.x
)
2004 # restore node location
2005 node
.location
= x
, y
2008 # only helper is selected now
2009 bpy
.ops
.node
.delete()
2010 x
= node
.location
.x
+ width
+ 20.0
2011 if node
.type != 'REROUTE':
2015 reroutes_count
= 0 # will be used when aligning reroutes added to hidden nodes
2016 for out_i
, output
in enumerate(node
.outputs
):
2017 pass_used
= False # initial value to be analyzed if 'R_LAYERS'
2018 # if node != 'R_LAYERS' - "pass_used" not needed, so set it to True
2019 if node
.type != 'R_LAYERS':
2021 else: # if 'R_LAYERS' check if output represent used render pass
2022 node_scene
= node
.scene
2023 node_layer
= node
.layer
2024 # If output - "Alpha" is analyzed - assume it's used. Not represented in passes.
2025 if output
.name
== 'Alpha':
2028 # check entries in global 'rl_outputs' variable
2029 for rlo
in rl_outputs
:
2030 if output
.name
in {rlo
.output_name
, rlo
.exr_output_name
}:
2031 pass_used
= getattr(node_scene
.view_layers
[node_layer
], rlo
.render_pass
)
2034 valid
= ((option
== 'ALL') or
2035 (option
== 'LOOSE' and not output
.links
) or
2036 (option
== 'LINKED' and output
.links
))
2037 # Add reroutes only if valid, but offset location in all cases.
2039 n
= nodes
.new('NodeReroute')
2041 for link
in output
.links
:
2042 connect_sockets(n
.outputs
[0], link
.to_socket
)
2043 connect_sockets(output
, n
.inputs
[0])
2045 post_select
.append(n
)
2049 # disselect the node so that after execution of script only newly created nodes are selected
2051 # nicer reroutes distribution along y when node.hide
2053 y_translate
= reroutes_count
* y_offset
/ 2.0 - y_offset
- 35.0
2054 for reroute
in [r
for r
in nodes
if r
.select
]:
2055 reroute
.location
.y
-= y_translate
2056 for node
in post_select
:
2062 class NWLinkActiveToSelected(Operator
, NWBase
):
2063 """Link active node to selected nodes basing on various criteria"""
2064 bl_idname
= "node.nw_link_active_to_selected"
2065 bl_label
= "Link Active Node to Selected"
2066 bl_options
= {'REGISTER', 'UNDO'}
2068 replace
: BoolProperty()
2069 use_node_name
: BoolProperty()
2070 use_outputs_names
: BoolProperty()
2073 def poll(cls
, context
):
2074 return (nw_check(context
)
2075 and context
.active_node
is not None
2076 and context
.active_node
.select
)
2078 def execute(self
, context
):
2079 nodes
, links
= get_nodes_links(context
)
2080 replace
= self
.replace
2081 use_node_name
= self
.use_node_name
2082 use_outputs_names
= self
.use_outputs_names
2083 active
= nodes
.active
2084 selected
= [node
for node
in nodes
if node
.select
and node
!= active
]
2085 outputs
= [] # Only usable outputs of active nodes will be stored here.
2086 for out
in active
.outputs
:
2087 if active
.type != 'R_LAYERS':
2090 # 'R_LAYERS' node type needs special handling.
2091 # outputs of 'R_LAYERS' are callable even if not seen in UI.
2092 # Only outputs that represent used passes should be taken into account
2093 # Check if pass represented by output is used.
2094 # global 'rl_outputs' list will be used for that
2095 for rlo
in rl_outputs
:
2096 pass_used
= False # initial value. Will be set to True if pass is used
2097 if out
.name
== 'Alpha':
2098 # Alpha output is always present. Doesn't have representation in render pass. Assume it's used.
2100 elif out
.name
in {rlo
.output_name
, rlo
.exr_output_name
}:
2101 # example 'render_pass' entry: 'use_pass_uv' Check if True in scene render layers
2102 pass_used
= getattr(active
.scene
.view_layers
[active
.layer
], rlo
.render_pass
)
2106 doit
= True # Will be changed to False when links successfully added to previous output.
2109 for node
in selected
:
2110 dst_name
= node
.name
# Will be compared with src_name if needed.
2111 # When node has label - use it as dst_name
2113 dst_name
= node
.label
2114 valid
= True # Initial value. Will be changed to False if names don't match.
2115 src_name
= dst_name
# If names not used - this assignment will keep valid = True.
2117 # Set src_name to source node name or label
2118 src_name
= active
.name
2120 src_name
= active
.label
2121 elif use_outputs_names
:
2122 src_name
= (out
.name
, )
2123 for rlo
in rl_outputs
:
2124 if out
.name
in {rlo
.output_name
, rlo
.exr_output_name
}:
2125 src_name
= (rlo
.output_name
, rlo
.exr_output_name
)
2126 if dst_name
not in src_name
:
2129 for input in node
.inputs
:
2130 if input.type == out
.type or node
.type == 'REROUTE':
2131 if replace
or not input.is_linked
:
2132 connect_sockets(out
, input)
2133 if not use_node_name
and not use_outputs_names
:
2140 class NWAlignNodes(Operator
, NWBase
):
2141 '''Align the selected nodes neatly in a row/column'''
2142 bl_idname
= "node.nw_align_nodes"
2143 bl_label
= "Align Nodes"
2144 bl_options
= {'REGISTER', 'UNDO'}
2145 margin
: IntProperty(name
='Margin', default
=50, description
='The amount of space between nodes')
2147 def execute(self
, context
):
2148 nodes
, links
= get_nodes_links(context
)
2149 margin
= self
.margin
2153 if node
.select
and node
.type != 'FRAME':
2154 selection
.append(node
)
2156 # If no nodes are selected, align all nodes
2160 elif nodes
.active
in selection
:
2161 active_loc
= copy(nodes
.active
.location
) # make a copy, not a reference
2163 # Check if nodes should be laid out horizontally or vertically
2164 # use dimension to get center of node, not corner
2165 x_locs
= [n
.location
.x
+ (n
.dimensions
.x
/ 2) for n
in selection
]
2166 y_locs
= [n
.location
.y
- (n
.dimensions
.y
/ 2) for n
in selection
]
2167 x_range
= max(x_locs
) - min(x_locs
)
2168 y_range
= max(y_locs
) - min(y_locs
)
2169 mid_x
= (max(x_locs
) + min(x_locs
)) / 2
2170 mid_y
= (max(y_locs
) + min(y_locs
)) / 2
2171 horizontal
= x_range
> y_range
2173 # Sort selection by location of node mid-point
2175 selection
= sorted(selection
, key
=lambda n
: n
.location
.x
+ (n
.dimensions
.x
/ 2))
2177 selection
= sorted(selection
, key
=lambda n
: n
.location
.y
- (n
.dimensions
.y
/ 2), reverse
=True)
2181 for node
in selection
:
2182 current_margin
= margin
2183 current_margin
= current_margin
* 0.5 if node
.hide
else current_margin
# use a smaller margin for hidden nodes
2186 node
.location
.x
= current_pos
2187 current_pos
+= current_margin
+ node
.dimensions
.x
2188 node
.location
.y
= mid_y
+ (node
.dimensions
.y
/ 2)
2190 node
.location
.y
= current_pos
2191 current_pos
-= (current_margin
* 0.3) + node
.dimensions
.y
# use half-margin for vertical alignment
2192 node
.location
.x
= mid_x
- (node
.dimensions
.x
/ 2)
2194 # If active node is selected, center nodes around it
2195 if active_loc
is not None:
2196 active_loc_diff
= active_loc
- nodes
.active
.location
2197 for node
in selection
:
2198 node
.location
+= active_loc_diff
2199 else: # Position nodes centered around where they used to be
2200 locs
= ([n
.location
.x
+ (n
.dimensions
.x
/ 2) for n
in selection
]
2201 ) if horizontal
else ([n
.location
.y
- (n
.dimensions
.y
/ 2) for n
in selection
])
2202 new_mid
= (max(locs
) + min(locs
)) / 2
2203 for node
in selection
:
2205 node
.location
.x
+= (mid_x
- new_mid
)
2207 node
.location
.y
+= (mid_y
- new_mid
)
2212 class NWSelectParentChildren(Operator
, NWBase
):
2213 bl_idname
= "node.nw_select_parent_child"
2214 bl_label
= "Select Parent or Children"
2215 bl_options
= {'REGISTER', 'UNDO'}
2217 option
: EnumProperty(
2220 ('PARENT', 'Select Parent', 'Select Parent Frame'),
2221 ('CHILD', 'Select Children', 'Select members of selected frame'),
2225 def execute(self
, context
):
2226 nodes
, links
= get_nodes_links(context
)
2227 option
= self
.option
2228 selected
= [node
for node
in nodes
if node
.select
]
2229 if option
== 'PARENT':
2230 for sel
in selected
:
2233 parent
.select
= True
2234 else: # option == 'CHILD'
2235 for sel
in selected
:
2236 children
= [node
for node
in nodes
if node
.parent
== sel
]
2237 for kid
in children
:
2243 class NWDetachOutputs(Operator
, NWBase
):
2244 """Detach outputs of selected node leaving inputs linked"""
2245 bl_idname
= "node.nw_detach_outputs"
2246 bl_label
= "Detach Outputs"
2247 bl_options
= {'REGISTER', 'UNDO'}
2249 def execute(self
, context
):
2250 nodes
, links
= get_nodes_links(context
)
2251 selected
= context
.selected_nodes
2252 bpy
.ops
.node
.duplicate_move_keep_inputs()
2253 new_nodes
= context
.selected_nodes
2254 bpy
.ops
.node
.select_all(action
="DESELECT")
2255 for node
in selected
:
2257 bpy
.ops
.node
.delete_reconnect()
2258 for new_node
in new_nodes
:
2259 new_node
.select
= True
2260 bpy
.ops
.transform
.translate('INVOKE_DEFAULT')
2265 class NWLinkToOutputNode(Operator
):
2266 """Link to Composite node or Material Output node"""
2267 bl_idname
= "node.nw_link_out"
2268 bl_label
= "Connect to Output"
2269 bl_options
= {'REGISTER', 'UNDO'}
2272 def poll(cls
, context
):
2273 """Disabled for custom nodes as we do not know which nodes are outputs."""
2274 return (nw_check(context
)
2275 and nw_check_space_type(cls
, context
, 'ShaderNodeTree', 'CompositorNodeTree',
2276 'TextureNodeTree', 'GeometryNodeTree')
2277 and context
.active_node
is not None
2278 and any(is_visible_socket(out
) for out
in context
.active_node
.outputs
))
2280 def execute(self
, context
):
2281 nodes
, links
= get_nodes_links(context
)
2282 active
= nodes
.active
2284 tree_type
= context
.space_data
.tree_type
2285 shader_outputs
= {'OBJECT': 'ShaderNodeOutputMaterial',
2286 'WORLD': 'ShaderNodeOutputWorld',
2287 'LINESTYLE': 'ShaderNodeOutputLineStyle'}
2289 'ShaderNodeTree': shader_outputs
[context
.space_data
.shader_type
],
2290 'CompositorNodeTree': 'CompositorNodeComposite',
2291 'TextureNodeTree': 'TextureNodeOutput',
2292 'GeometryNodeTree': 'NodeGroupOutput',
2295 # check whether the node is an output node and,
2296 # if supported, whether it's the active one
2297 if node
.rna_type
.identifier
== output_type \
2298 and (node
.is_active_output
if hasattr(node
, 'is_active_output')
2302 else: # No output node exists
2303 bpy
.ops
.node
.select_all(action
="DESELECT")
2304 output_node
= nodes
.new(output_type
)
2305 output_node
.location
.x
= active
.location
.x
+ active
.dimensions
.x
+ 80
2306 output_node
.location
.y
= active
.location
.y
2309 for i
, output
in enumerate(active
.outputs
):
2310 if is_visible_socket(output
):
2313 for i
, output
in enumerate(active
.outputs
):
2314 if output
.type == output_node
.inputs
[0].type and is_visible_socket(output
):
2319 if tree_type
== 'ShaderNodeTree':
2320 if active
.outputs
[output_index
].name
== 'Volume':
2322 elif active
.outputs
[output_index
].name
== 'Displacement':
2324 elif tree_type
== 'GeometryNodeTree':
2325 if active
.outputs
[output_index
].type != 'GEOMETRY':
2326 return {'CANCELLED'}
2327 connect_sockets(active
.outputs
[output_index
], output_node
.inputs
[out_input_index
])
2329 force_update(context
) # viewport render does not update
2334 class NWMakeLink(Operator
, NWBase
):
2335 """Make a link from one socket to another"""
2336 bl_idname
= 'node.nw_make_link'
2337 bl_label
= 'Make Link'
2338 bl_options
= {'REGISTER', 'UNDO'}
2339 from_socket
: IntProperty()
2340 to_socket
: IntProperty()
2342 def execute(self
, context
):
2343 nodes
, links
= get_nodes_links(context
)
2345 n1
= nodes
[context
.scene
.NWLazySource
]
2346 n2
= nodes
[context
.scene
.NWLazyTarget
]
2348 connect_sockets(n1
.outputs
[self
.from_socket
], n2
.inputs
[self
.to_socket
])
2350 force_update(context
)
2355 class NWCallInputsMenu(Operator
, NWBase
):
2356 """Link from this output"""
2357 bl_idname
= 'node.nw_call_inputs_menu'
2358 bl_label
= 'Make Link'
2359 bl_options
= {'REGISTER', 'UNDO'}
2360 from_socket
: IntProperty()
2362 def execute(self
, context
):
2363 nodes
, links
= get_nodes_links(context
)
2365 context
.scene
.NWSourceSocket
= self
.from_socket
2367 n1
= nodes
[context
.scene
.NWLazySource
]
2368 n2
= nodes
[context
.scene
.NWLazyTarget
]
2369 if len(n2
.inputs
) > 1:
2370 bpy
.ops
.wm
.call_menu("INVOKE_DEFAULT", name
=NWConnectionListInputs
.bl_idname
)
2371 elif len(n2
.inputs
) == 1:
2372 connect_sockets(n1
.outputs
[self
.from_socket
], n2
.inputs
[0])
2376 class NWAddSequence(Operator
, NWBase
, ImportHelper
):
2377 """Add an Image Sequence"""
2378 bl_idname
= 'node.nw_add_sequence'
2379 bl_label
= 'Import Image Sequence'
2380 bl_options
= {'REGISTER', 'UNDO'}
2382 directory
: StringProperty(
2385 filename
: StringProperty(
2388 files
: CollectionProperty(
2389 type=bpy
.types
.OperatorFileListElement
,
2390 options
={'HIDDEN', 'SKIP_SAVE'}
2392 relative_path
: BoolProperty(
2393 name
='Relative Path',
2394 description
='Set the file path relative to the blend file, when possible',
2398 def draw(self
, context
):
2399 layout
= self
.layout
2400 layout
.alignment
= 'LEFT'
2402 layout
.prop(self
, 'relative_path')
2404 def execute(self
, context
):
2405 nodes
, links
= get_nodes_links(context
)
2406 directory
= self
.directory
2407 filename
= self
.filename
2409 tree
= context
.space_data
.node_tree
2412 # print ("\nDIR:", directory)
2413 # print ("FN:", filename)
2414 # print ("Fs:", list(f.name for f in files), '\n')
2416 if tree
.type == 'SHADER':
2417 node_type
= "ShaderNodeTexImage"
2418 elif tree
.type == 'COMPOSITING':
2419 node_type
= "CompositorNodeImage"
2421 self
.report({'ERROR'}, "Unsupported Node Tree type!")
2422 return {'CANCELLED'}
2424 if not files
[0].name
and not filename
:
2425 self
.report({'ERROR'}, "No file chosen")
2426 return {'CANCELLED'}
2427 elif files
[0].name
and (not filename
or not path
.exists(directory
+ filename
)):
2428 # User has selected multiple files without an active one, or the active one is non-existent
2429 filename
= files
[0].name
2431 if not path
.exists(directory
+ filename
):
2432 self
.report({'ERROR'}, filename
+ " does not exist!")
2433 return {'CANCELLED'}
2435 without_ext
= '.'.join(filename
.split('.')[:-1])
2437 # if last digit isn't a number, it's not a sequence
2438 if not without_ext
[-1].isdigit():
2439 self
.report({'ERROR'}, filename
+ " does not seem to be part of a sequence")
2440 return {'CANCELLED'}
2442 extension
= filename
.split('.')[-1]
2443 reverse
= without_ext
[::-1] # reverse string
2446 for char
in reverse
:
2452 without_num
= without_ext
[:count_numbers
* -1]
2454 files
= sorted(glob(directory
+ without_num
+ "[0-9]" * count_numbers
+ "." + extension
))
2456 num_frames
= len(files
)
2458 nodes_list
= [node
for node
in nodes
]
2460 nodes_list
.sort(key
=lambda k
: k
.location
.x
)
2461 xloc
= nodes_list
[0].location
.x
- 220 # place new nodes at far left
2465 yloc
+= node_mid_pt(node
, 'y')
2466 yloc
= yloc
/ len(nodes
)
2471 name_with_hashes
= without_num
+ "#" * count_numbers
+ '.' + extension
2473 bpy
.ops
.node
.add_node('INVOKE_DEFAULT', use_transform
=True, type=node_type
)
2475 node
.label
= name_with_hashes
2477 filepath
= directory
+ (without_ext
+ '.' + extension
)
2478 if self
.relative_path
:
2479 if bpy
.data
.filepath
:
2481 filepath
= bpy
.path
.relpath(filepath
)
2485 img
= bpy
.data
.images
.load(filepath
)
2486 img
.source
= 'SEQUENCE'
2487 img
.name
= name_with_hashes
2489 image_user
= node
.image_user
if tree
.type == 'SHADER' else node
2490 # separate the number from the file name of the first file
2491 image_user
.frame_offset
= int(files
[0][len(without_num
) + len(directory
):-1 * (len(extension
) + 1)]) - 1
2492 image_user
.frame_duration
= num_frames
2497 class NWAddMultipleImages(Operator
, NWBase
, ImportHelper
):
2498 """Add multiple images at once"""
2499 bl_idname
= 'node.nw_add_multiple_images'
2500 bl_label
= 'Open Selected Images'
2501 bl_options
= {'REGISTER', 'UNDO'}
2502 directory
: StringProperty(
2505 files
: CollectionProperty(
2506 type=bpy
.types
.OperatorFileListElement
,
2507 options
={'HIDDEN', 'SKIP_SAVE'}
2510 def execute(self
, context
):
2511 nodes
, links
= get_nodes_links(context
)
2513 xloc
, yloc
= context
.region
.view2d
.region_to_view(context
.area
.width
/ 2, context
.area
.height
/ 2)
2515 if context
.space_data
.node_tree
.type == 'SHADER':
2516 node_type
= "ShaderNodeTexImage"
2517 elif context
.space_data
.node_tree
.type == 'COMPOSITING':
2518 node_type
= "CompositorNodeImage"
2520 self
.report({'ERROR'}, "Unsupported Node Tree type!")
2521 return {'CANCELLED'}
2524 for f
in self
.files
:
2527 node
= nodes
.new(node_type
)
2528 new_nodes
.append(node
)
2531 node
.location
.x
= xloc
2532 node
.location
.y
= yloc
2535 img
= bpy
.data
.images
.load(self
.directory
+ fname
)
2538 # shift new nodes up to center of tree
2539 list_size
= new_nodes
[0].location
.y
- new_nodes
[-1].location
.y
2541 if node
in new_nodes
:
2543 node
.location
.y
+= (list_size
/ 2)
2549 class NWViewerFocus(bpy
.types
.Operator
):
2550 """Set the viewer tile center to the mouse position"""
2551 bl_idname
= "node.nw_viewer_focus"
2552 bl_label
= "Viewer Focus"
2554 x
: bpy
.props
.IntProperty()
2555 y
: bpy
.props
.IntProperty()
2558 def poll(cls
, context
):
2559 return (nw_check(context
)
2560 and nw_check_space_type(cls
, context
, 'CompositorNodeTree'))
2562 def execute(self
, context
):
2565 def invoke(self
, context
, event
):
2566 render
= context
.scene
.render
2567 space
= context
.space_data
2568 percent
= render
.resolution_percentage
* 0.01
2570 nodes
, links
= get_nodes_links(context
)
2571 viewers
= [n
for n
in nodes
if n
.type == 'VIEWER']
2574 mlocx
= event
.mouse_region_x
2575 mlocy
= event
.mouse_region_y
2576 select_node
= bpy
.ops
.node
.select(location
=(mlocx
, mlocy
), extend
=False)
2578 if 'FINISHED' not in select_node
: # only run if we're not clicking on a node
2579 region_x
= context
.region
.width
2580 region_y
= context
.region
.height
2582 region_center_x
= context
.region
.width
/ 2
2583 region_center_y
= context
.region
.height
/ 2
2585 bd_x
= render
.resolution_x
* percent
* space
.backdrop_zoom
2586 bd_y
= render
.resolution_y
* percent
* space
.backdrop_zoom
2588 backdrop_center_x
= (bd_x
/ 2) - space
.backdrop_offset
[0]
2589 backdrop_center_y
= (bd_y
/ 2) - space
.backdrop_offset
[1]
2591 margin_x
= region_center_x
- backdrop_center_x
2592 margin_y
= region_center_y
- backdrop_center_y
2594 abs_mouse_x
= (mlocx
- margin_x
) / bd_x
2595 abs_mouse_y
= (mlocy
- margin_y
) / bd_y
2597 for node
in viewers
:
2598 node
.center_x
= abs_mouse_x
2599 node
.center_y
= abs_mouse_y
2601 return {'PASS_THROUGH'}
2603 return self
.execute(context
)
2606 class NWSaveViewer(bpy
.types
.Operator
, ExportHelper
):
2607 """Save the current viewer node to an image file"""
2608 bl_idname
= "node.nw_save_viewer"
2609 bl_label
= "Save This Image"
2610 filepath
: StringProperty(subtype
="FILE_PATH")
2611 filename_ext
: EnumProperty(
2613 description
="Choose the file format to save to",
2614 items
=(('.bmp', "BMP", ""),
2615 ('.rgb', 'IRIS', ""),
2616 ('.png', 'PNG', ""),
2617 ('.jpg', 'JPEG', ""),
2618 ('.jp2', 'JPEG2000', ""),
2619 ('.tga', 'TARGA', ""),
2620 ('.cin', 'CINEON', ""),
2621 ('.dpx', 'DPX', ""),
2622 ('.exr', 'OPEN_EXR', ""),
2623 ('.hdr', 'HDR', ""),
2624 ('.tif', 'TIFF', "")),
2629 def poll(cls
, context
):
2630 return (nw_check(context
)
2631 and nw_check_space_type(cls
, context
, 'CompositorNodeTree')
2632 and any(img
.source
== 'VIEWER'
2633 and img
.render_slots
== 0
2634 for img
in bpy
.data
.images
)
2635 and sum(bpy
.data
.images
["Viewer Node"].size
) > 0) # False if not connected or connected but no image
2637 def execute(self
, context
):
2654 basename
, ext
= path
.splitext(fp
)
2655 old_render_format
= context
.scene
.render
.image_settings
.file_format
2656 context
.scene
.render
.image_settings
.file_format
= formats
[self
.filename_ext
]
2657 context
.area
.type = "IMAGE_EDITOR"
2658 context
.area
.spaces
[0].image
= bpy
.data
.images
['Viewer Node']
2659 context
.area
.spaces
[0].image
.save_render(fp
)
2660 context
.area
.type = "NODE_EDITOR"
2661 context
.scene
.render
.image_settings
.file_format
= old_render_format
2665 class NWResetNodes(bpy
.types
.Operator
):
2666 """Reset Nodes in Selection"""
2667 bl_idname
= "node.nw_reset_nodes"
2668 bl_label
= "Reset Nodes"
2669 bl_options
= {'REGISTER', 'UNDO'}
2672 def poll(cls
, context
):
2673 space
= context
.space_data
2674 return space
.type == 'NODE_EDITOR'
2676 def execute(self
, context
):
2677 node_active
= context
.active_node
2678 node_selected
= context
.selected_nodes
2679 node_ignore
= ["FRAME", "REROUTE", "GROUP", "SIMULATION_INPUT", "SIMULATION_OUTPUT"]
2681 # Check if one node is selected at least
2682 if not (len(node_selected
) > 0):
2683 self
.report({'ERROR'}, "1 node must be selected at least")
2684 return {'CANCELLED'}
2686 active_node_name
= node_active
.name
if node_active
.select
else None
2687 valid_nodes
= [n
for n
in node_selected
if n
.type not in node_ignore
]
2689 # Create output lists
2690 selected_node_names
= [n
.name
for n
in node_selected
]
2693 # Reset all valid children in a frame
2694 node_active_is_frame
= False
2695 if len(node_selected
) == 1 and node_active
.type == "FRAME":
2696 node_tree
= node_active
.id_data
2697 children
= [n
for n
in node_tree
.nodes
if n
.parent
== node_active
]
2699 valid_nodes
= [n
for n
in children
if n
.type not in node_ignore
]
2700 selected_node_names
= [n
.name
for n
in children
if n
.type not in node_ignore
]
2701 node_active_is_frame
= True
2703 # Check if valid nodes in selection
2704 if not (len(valid_nodes
) > 0):
2705 # Check for frames only
2706 frames_selected
= [n
for n
in node_selected
if n
.type == "FRAME"]
2707 if (len(frames_selected
) > 1 and len(frames_selected
) == len(node_selected
)):
2708 self
.report({'ERROR'}, "Please select only 1 frame to reset")
2710 self
.report({'ERROR'}, "No valid node(s) in selection")
2711 return {'CANCELLED'}
2713 # Report nodes that are not valid
2714 if len(valid_nodes
) != len(node_selected
) and node_active_is_frame
is False:
2715 valid_node_names
= [n
.name
for n
in valid_nodes
]
2716 not_valid_names
= list(set(selected_node_names
) - set(valid_node_names
))
2717 self
.report({'INFO'}, "Ignored {}".format(", ".join(not_valid_names
)))
2719 # Deselect all nodes
2720 for i
in node_selected
:
2723 # Run through all valid nodes
2724 for node
in valid_nodes
:
2726 parent
= node
.parent
if node
.parent
else None
2727 node_loc
= [node
.location
.x
, node
.location
.y
]
2729 node_tree
= node
.id_data
2730 props_to_copy
= 'bl_idname name location height width'.split(' ')
2733 mappings
= chain
.from_iterable([node
.inputs
, node
.outputs
])
2734 for i
in (i
for i
in mappings
if i
.is_linked
):
2736 reconnections
.append([L
.from_socket
.path_from_id(), L
.to_socket
.path_from_id()])
2738 props
= {j
: getattr(node
, j
) for j
in props_to_copy
}
2740 new_node
= node_tree
.nodes
.new(props
['bl_idname'])
2741 props_to_copy
.pop(0)
2743 for prop
in props_to_copy
:
2744 setattr(new_node
, prop
, props
[prop
])
2746 nodes
= node_tree
.nodes
2748 new_node
.name
= props
['name']
2751 new_node
.parent
= parent
2752 new_node
.location
= node_loc
2754 for str_from
, str_to
in reconnections
:
2755 connect_sockets(eval(str_from
), eval(str_to
))
2757 new_node
.select
= False
2758 success_names
.append(new_node
.name
)
2760 # Reselect all nodes
2761 if selected_node_names
and node_active_is_frame
is False:
2762 for i
in selected_node_names
:
2763 node_tree
.nodes
[i
].select
= True
2765 if active_node_name
is not None:
2766 node_tree
.nodes
[active_node_name
].select
= True
2767 node_tree
.nodes
.active
= node_tree
.nodes
[active_node_name
]
2769 self
.report({'INFO'}, "Successfully reset {}".format(", ".join(success_names
)))
2791 NWAddPrincipledSetup
,
2793 NWLinkActiveToSelected
,
2795 NWSelectParentChildren
,
2801 NWAddMultipleImages
,
2809 from bpy
.utils
import register_class
2815 from bpy
.utils
import unregister_class
2818 unregister_class(cls
)