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'
763 # Cancel if no socket was found. This can happen for group input
764 # nodes with only a virtual socket output.
765 if active_node_socket_index
is None:
768 if active
.outputs
[active_node_socket_index
].name
== "Volume":
769 output_node_socket_index
= 1
771 output_node_socket_index
= 0
773 # If there are no nested node groups, the link starts at the active node
774 node_output
= active
.outputs
[active_node_socket_index
]
776 # Recursively connect inside nested node groups and get the one from base level
777 node_output
= self
.create_links(path
, active
, active_node_socket_index
, socket_type
)
778 output_node_input
= output_node
.inputs
[output_node_socket_index
]
780 # Connect at base level
781 connect_sockets(node_output
, output_node_input
)
784 nodes
.active
= active
786 force_update(context
)
790 class NWFrameSelected(Operator
, NWBase
):
791 bl_idname
= "node.nw_frame_selected"
792 bl_label
= "Frame Selected"
793 bl_description
= "Add a frame node and parent the selected nodes to it"
794 bl_options
= {'REGISTER', 'UNDO'}
796 label_prop
: StringProperty(
798 description
='The visual name of the frame node',
801 use_custom_color_prop
: BoolProperty(
803 description
="Use custom color for the frame node",
806 color_prop
: FloatVectorProperty(
808 description
="The color of the frame node",
809 default
=(0.604, 0.604, 0.604),
810 min=0, max=1, step
=1, precision
=3,
811 subtype
='COLOR_GAMMA', size
=3
814 def draw(self
, context
):
816 layout
.prop(self
, 'label_prop')
817 layout
.prop(self
, 'use_custom_color_prop')
818 col
= layout
.column()
819 col
.active
= self
.use_custom_color_prop
820 col
.prop(self
, 'color_prop', text
="")
822 def execute(self
, context
):
823 nodes
, links
= get_nodes_links(context
)
827 selected
.append(node
)
829 bpy
.ops
.node
.add_node(type='NodeFrame')
831 frm
.label
= self
.label_prop
832 frm
.use_custom_color
= self
.use_custom_color_prop
833 frm
.color
= self
.color_prop
835 for node
in selected
:
841 class NWReloadImages(Operator
):
842 bl_idname
= "node.nw_reload_images"
843 bl_label
= "Reload Images"
844 bl_description
= "Update all the image nodes to match their files on disk"
847 def poll(cls
, context
):
848 return (nw_check(context
)
849 and nw_check_space_type(cls
, context
, 'ShaderNodeTree', 'CompositorNodeTree',
850 'TextureNodeTree', 'GeometryNodeTree')
851 and context
.active_node
is not None
852 and any(is_visible_socket(out
) for out
in context
.active_node
.outputs
))
854 def execute(self
, context
):
855 nodes
, links
= get_nodes_links(context
)
856 image_types
= ["IMAGE", "TEX_IMAGE", "TEX_ENVIRONMENT", "TEXTURE"]
859 if node
.type in image_types
:
860 if node
.type == "TEXTURE":
861 if node
.texture
: # node has texture assigned
862 if node
.texture
.type in ['IMAGE', 'ENVIRONMENT_MAP']:
863 if node
.texture
.image
: # texture has image assigned
864 node
.texture
.image
.reload()
872 self
.report({'INFO'}, "Reloaded images")
873 print("Reloaded " + str(num_reloaded
) + " images")
874 force_update(context
)
877 self
.report({'WARNING'}, "No images found to reload in this node tree")
881 class NWMergeNodes(Operator
, NWBase
):
882 bl_idname
= "node.nw_merge_nodes"
883 bl_label
= "Merge Nodes"
884 bl_description
= "Merge Selected Nodes"
885 bl_options
= {'REGISTER', 'UNDO'}
889 description
="All possible blend types, boolean operations and math operations",
890 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
],
892 merge_type
: EnumProperty(
894 description
="Type of Merge to be used",
896 ('AUTO', 'Auto', 'Automatic Output Type Detection'),
897 ('SHADER', 'Shader', 'Merge using ADD or MIX Shader'),
898 ('GEOMETRY', 'Geometry', 'Merge using Mesh Boolean or Join Geometry Node'),
899 ('MIX', 'Mix Node', 'Merge using Mix Nodes'),
900 ('MATH', 'Math Node', 'Merge using Math Nodes'),
901 ('ZCOMBINE', 'Z-Combine Node', 'Merge using Z-Combine Nodes'),
902 ('ALPHAOVER', 'Alpha Over Node', 'Merge using Alpha Over Nodes'),
906 # Check if the link connects to a node that is in selected_nodes
907 # If not, then check recursively for each link in the nodes outputs.
908 # If yes, return True. If the recursion stops without finding a node
909 # in selected_nodes, it returns False. The depth is used to prevent
910 # getting stuck in a loop because of an already present cycle.
912 def link_creates_cycle(link
, selected_nodes
, depth
=0) -> bool:
914 # We're stuck in a cycle, but that cycle was already present,
915 # so we return False.
916 # NOTE: The number 255 is arbitrary, but seems to work well.
919 if node
in selected_nodes
:
923 for output
in node
.outputs
:
925 for olink
in output
.links
:
926 if NWMergeNodes
.link_creates_cycle(olink
, selected_nodes
, depth
+ 1):
928 # None of the outputs found a node in selected_nodes, so there is no cycle.
931 # Merge the nodes in `nodes_list` with a node of type `node_name` that has a multi_input socket.
932 # The parameters `socket_indices` gives the indices of the node sockets in the order that they should
933 # be connected. The last one is assumed to be a multi input socket.
934 # For convenience the node is returned.
936 def merge_with_multi_input(nodes_list
, merge_position
, do_hide
, loc_x
, links
, nodes
, node_name
, socket_indices
):
937 # The y-location of the last node
938 loc_y
= nodes_list
[-1][2]
939 if merge_position
== 'CENTER':
940 # Average the y-location
941 for i
in range(len(nodes_list
) - 1):
942 loc_y
+= nodes_list
[i
][2]
943 loc_y
= loc_y
/ len(nodes_list
)
944 new_node
= nodes
.new(node_name
)
945 new_node
.hide
= do_hide
946 new_node
.location
.x
= loc_x
947 new_node
.location
.y
= loc_y
948 selected_nodes
= [nodes
[node_info
[0]] for node_info
in nodes_list
]
950 outputs_for_multi_input
= []
951 for i
, node
in enumerate(selected_nodes
):
953 # Search for the first node which had output links that do not create
954 # a cycle, which we can then reconnect afterwards.
955 if prev_links
== [] and node
.outputs
[0].is_linked
:
957 link
for link
in node
.outputs
[0].links
if not NWMergeNodes
.link_creates_cycle(
958 link
, selected_nodes
)]
959 # Get the index of the socket, the last one is a multi input, and is thus used repeatedly
960 # To get the placement to look right we need to reverse the order in which we connect the
961 # outputs to the multi input socket.
962 if i
< len(socket_indices
) - 1:
963 ind
= socket_indices
[i
]
964 connect_sockets(node
.outputs
[0], new_node
.inputs
[ind
])
966 outputs_for_multi_input
.insert(0, node
.outputs
[0])
967 if outputs_for_multi_input
!= []:
968 ind
= socket_indices
[-1]
969 for output
in outputs_for_multi_input
:
970 connect_sockets(output
, new_node
.inputs
[ind
])
972 for link
in prev_links
:
973 connect_sockets(new_node
.outputs
[0], link
.to_node
.inputs
[0])
977 def poll(cls
, context
):
978 return (nw_check(context
)
979 and nw_check_space_type(cls
, context
, 'ShaderNodeTree', 'CompositorNodeTree',
980 'TextureNodeTree', 'GeometryNodeTree'))
982 def execute(self
, context
):
983 settings
= context
.preferences
.addons
[__package__
].preferences
984 merge_hide
= settings
.merge_hide
985 merge_position
= settings
.merge_position
# 'center' or 'bottom'
988 do_hide_shader
= False
989 if merge_hide
== 'ALWAYS':
991 do_hide_shader
= True
992 elif merge_hide
== 'NON_SHADER':
995 tree_type
= context
.space_data
.node_tree
.type
996 if tree_type
== 'GEOMETRY':
997 node_type
= 'GeometryNode'
998 if tree_type
== 'COMPOSITING':
999 node_type
= 'CompositorNode'
1000 elif tree_type
== 'SHADER':
1001 node_type
= 'ShaderNode'
1002 elif tree_type
== 'TEXTURE':
1003 node_type
= 'TextureNode'
1004 nodes
, links
= get_nodes_links(context
)
1006 merge_type
= self
.merge_type
1007 # Prevent trying to add Z-Combine in not 'COMPOSITING' node tree.
1008 # 'ZCOMBINE' works only if mode == 'MIX'
1009 # Setting mode to None prevents trying to add 'ZCOMBINE' node.
1010 if (merge_type
== 'ZCOMBINE' or merge_type
== 'ALPHAOVER') and tree_type
!= 'COMPOSITING':
1013 if (merge_type
!= 'MATH' and merge_type
!= 'GEOMETRY') and tree_type
== 'GEOMETRY':
1015 # The Mix node and math nodes used for geometry nodes are of type 'ShaderNode'
1016 if (merge_type
== 'MATH' or merge_type
== 'MIX') and tree_type
== 'GEOMETRY':
1017 node_type
= 'ShaderNode'
1018 selected_mix
= [] # entry = [index, loc]
1019 selected_shader
= [] # entry = [index, loc]
1020 selected_geometry
= [] # entry = [index, loc]
1021 selected_math
= [] # entry = [index, loc]
1022 selected_vector
= [] # entry = [index, loc]
1023 selected_z
= [] # entry = [index, loc]
1024 selected_alphaover
= [] # entry = [index, loc]
1026 for i
, node
in enumerate(nodes
):
1027 if node
.select
and node
.outputs
:
1028 if merge_type
== 'AUTO':
1029 for (type, types_list
, dst
) in (
1030 ('SHADER', ('MIX', 'ADD'), selected_shader
),
1031 ('GEOMETRY', [t
[0] for t
in geo_combine_operations
], selected_geometry
),
1032 ('RGBA', [t
[0] for t
in blend_types
], selected_mix
),
1033 ('VALUE', [t
[0] for t
in operations
], selected_math
),
1034 ('VECTOR', [], selected_vector
),
1036 output
= get_first_enabled_output(node
)
1037 output_type
= output
.type
1038 valid_mode
= mode
in types_list
1039 # When mode is 'MIX' we have to cheat since the mix node is not used in
1041 if tree_type
== 'GEOMETRY':
1043 if output_type
== 'VALUE' and type == 'VALUE':
1045 elif output_type
== 'VECTOR' and type == 'VECTOR':
1047 elif type == 'GEOMETRY':
1049 # When mode is 'MIX' use mix node for both 'RGBA' and 'VALUE' output types.
1050 # Cheat that output type is 'RGBA',
1051 # and that 'MIX' exists in math operations list.
1052 # This way when selected_mix list is analyzed:
1053 # Node data will be appended even though it doesn't meet requirements.
1054 elif output_type
!= 'SHADER' and mode
== 'MIX':
1055 output_type
= 'RGBA'
1057 if output_type
== type and valid_mode
:
1058 dst
.append([i
, node
.location
.x
, node
.location
.y
, node
.dimensions
.x
, node
.hide
])
1060 for (type, types_list
, dst
) in (
1061 ('SHADER', ('MIX', 'ADD'), selected_shader
),
1062 ('GEOMETRY', [t
[0] for t
in geo_combine_operations
], selected_geometry
),
1063 ('MIX', [t
[0] for t
in blend_types
], selected_mix
),
1064 ('MATH', [t
[0] for t
in operations
], selected_math
),
1065 ('ZCOMBINE', ('MIX', ), selected_z
),
1066 ('ALPHAOVER', ('MIX', ), selected_alphaover
),
1068 if merge_type
== type and mode
in types_list
:
1069 dst
.append([i
, node
.location
.x
, node
.location
.y
, node
.dimensions
.x
, node
.hide
])
1070 # When nodes with output kinds 'RGBA' and 'VALUE' are selected at the same time
1071 # use only 'Mix' nodes for merging.
1072 # For that we add selected_math list to selected_mix list and clear selected_math.
1073 if selected_mix
and selected_math
and merge_type
== 'AUTO':
1074 selected_mix
+= selected_math
1077 # If no nodes are selected, do nothing and pass through.
1078 if not (selected_mix
+ selected_shader
+ selected_geometry
+ selected_math
1079 + selected_vector
+ selected_z
+ selected_alphaover
):
1080 return {'PASS_THROUGH'}
1089 selected_alphaover
]:
1092 count_before
= len(nodes
)
1093 # sort list by loc_x - reversed
1094 nodes_list
.sort(key
=lambda k
: k
[1], reverse
=True)
1096 loc_x
= nodes_list
[0][1] + nodes_list
[0][3] + 70
1097 nodes_list
.sort(key
=lambda k
: k
[2], reverse
=True)
1099 # Change the node type for math nodes in a geometry node tree.
1100 if tree_type
== 'GEOMETRY':
1101 if nodes_list
is selected_math
or nodes_list
is selected_vector
or nodes_list
is selected_mix
:
1102 node_type
= 'ShaderNode'
1106 node_type
= 'GeometryNode'
1107 if merge_position
== 'CENTER':
1108 # average yloc of last two nodes (lowest two)
1109 loc_y
= ((nodes_list
[len(nodes_list
) - 1][2]) + (nodes_list
[len(nodes_list
) - 2][2])) / 2
1110 if nodes_list
[len(nodes_list
) - 1][-1]: # if last node is hidden, mix should be shifted up a bit
1116 loc_y
= nodes_list
[len(nodes_list
) - 1][2]
1120 if nodes_list
== selected_shader
and not do_hide_shader
:
1122 the_range
= len(nodes_list
) - 1
1123 if len(nodes_list
) == 1:
1126 for i
in range(the_range
):
1127 if nodes_list
== selected_mix
:
1129 if tree_type
== 'COMPOSITING':
1131 add_type
= node_type
+ mix_name
1132 add
= nodes
.new(add_type
)
1133 if tree_type
!= 'COMPOSITING':
1134 add
.data_type
= 'RGBA'
1135 add
.blend_type
= mode
1137 add
.inputs
[0].default_value
= 1.0
1138 add
.show_preview
= False
1144 if tree_type
== 'COMPOSITING':
1147 elif nodes_list
== selected_math
:
1148 add_type
= node_type
+ 'Math'
1149 add
= nodes
.new(add_type
)
1150 add
.operation
= mode
1156 elif nodes_list
== selected_shader
:
1158 add_type
= node_type
+ 'MixShader'
1159 add
= nodes
.new(add_type
)
1160 add
.hide
= do_hide_shader
1166 add_type
= node_type
+ 'AddShader'
1167 add
= nodes
.new(add_type
)
1168 add
.hide
= do_hide_shader
1173 elif nodes_list
== selected_geometry
:
1174 if mode
in ('JOIN', 'MIX'):
1175 add_type
= node_type
+ 'JoinGeometry'
1176 add
= self
.merge_with_multi_input(
1177 nodes_list
, merge_position
, do_hide
, loc_x
, links
, nodes
, add_type
, [0])
1179 add_type
= node_type
+ 'MeshBoolean'
1180 indices
= [0, 1] if mode
== 'DIFFERENCE' else [1]
1181 add
= self
.merge_with_multi_input(
1182 nodes_list
, merge_position
, do_hide
, loc_x
, links
, nodes
, add_type
, indices
)
1183 add
.operation
= mode
1186 elif nodes_list
== selected_vector
:
1187 add_type
= node_type
+ 'VectorMath'
1188 add
= nodes
.new(add_type
)
1189 add
.operation
= mode
1195 elif nodes_list
== selected_z
:
1196 add
= nodes
.new('CompositorNodeZcombine')
1197 add
.show_preview
= False
1203 elif nodes_list
== selected_alphaover
:
1204 add
= nodes
.new('CompositorNodeAlphaOver')
1205 add
.show_preview
= False
1211 add
.location
= loc_x
, loc_y
1215 # This has already been handled separately
1219 count_after
= len(nodes
)
1220 index
= count_after
- 1
1221 first_selected
= nodes
[nodes_list
[0][0]]
1222 # "last" node has been added as first, so its index is count_before.
1223 last_add
= nodes
[count_before
]
1224 # Create list of invalid indexes.
1225 invalid_nodes
= [nodes
[n
[0]]
1226 for n
in (selected_mix
+ selected_math
+ selected_shader
+ selected_z
+ selected_geometry
)]
1229 # Two nodes were selected and first selected has no output links, second selected has output links.
1230 # Then add links from last add to all links 'to_socket' of out links of second selected.
1231 first_selected_output
= get_first_enabled_output(first_selected
)
1232 if len(nodes_list
) == 2:
1233 if not first_selected_output
.links
:
1234 second_selected
= nodes
[nodes_list
[1][0]]
1235 for ss_link
in get_first_enabled_output(second_selected
).links
:
1236 # Prevent cyclic dependencies when nodes to be merged are linked to one another.
1237 # Link only if "to_node" index not in invalid indexes list.
1238 if not self
.link_creates_cycle(ss_link
, invalid_nodes
):
1239 connect_sockets(get_first_enabled_output(last_add
), ss_link
.to_socket
)
1240 # add links from last_add to all links 'to_socket' of out links of first selected.
1241 for fs_link
in first_selected_output
.links
:
1242 # Link only if "to_node" index not in invalid indexes list.
1243 if not self
.link_creates_cycle(fs_link
, invalid_nodes
):
1244 connect_sockets(get_first_enabled_output(last_add
), fs_link
.to_socket
)
1245 # add link from "first" selected and "first" add node
1246 node_to
= nodes
[count_after
- 1]
1247 connect_sockets(first_selected_output
, node_to
.inputs
[first
])
1248 if node_to
.type == 'ZCOMBINE':
1249 for fs_out
in first_selected
.outputs
:
1250 if fs_out
!= first_selected_output
and fs_out
.name
in ('Z', 'Depth'):
1251 connect_sockets(fs_out
, node_to
.inputs
[1])
1253 # add links between added ADD nodes and between selected and ADD nodes
1254 for i
in range(count_adds
):
1255 if i
< count_adds
- 1:
1256 node_from
= nodes
[index
]
1257 node_to
= nodes
[index
- 1]
1258 node_to_input_i
= first
1259 node_to_z_i
= 1 # if z combine - link z to first z input
1260 connect_sockets(get_first_enabled_output(node_from
), node_to
.inputs
[node_to_input_i
])
1261 if node_to
.type == 'ZCOMBINE':
1262 for from_out
in node_from
.outputs
:
1263 if from_out
!= get_first_enabled_output(node_from
) and from_out
.name
in ('Z', 'Depth'):
1264 connect_sockets(from_out
, node_to
.inputs
[node_to_z_i
])
1265 if len(nodes_list
) > 1:
1266 node_from
= nodes
[nodes_list
[i
+ 1][0]]
1267 node_to
= nodes
[index
]
1268 node_to_input_i
= second
1269 node_to_z_i
= 3 # if z combine - link z to second z input
1270 connect_sockets(get_first_enabled_output(node_from
), node_to
.inputs
[node_to_input_i
])
1271 if node_to
.type == 'ZCOMBINE':
1272 for from_out
in node_from
.outputs
:
1273 if from_out
!= get_first_enabled_output(node_from
) and from_out
.name
in ('Z', 'Depth'):
1274 connect_sockets(from_out
, node_to
.inputs
[node_to_z_i
])
1276 # set "last" of added nodes as active
1277 nodes
.active
= last_add
1278 for i
, x
, y
, dx
, h
in nodes_list
:
1279 nodes
[i
].select
= False
1284 class NWBatchChangeNodes(Operator
, NWBase
):
1285 bl_idname
= "node.nw_batch_change"
1286 bl_label
= "Batch Change"
1287 bl_description
= "Batch Change Blend Type and Math Operation"
1288 bl_options
= {'REGISTER', 'UNDO'}
1290 blend_type
: EnumProperty(
1292 items
=blend_types
+ navs
,
1294 operation
: EnumProperty(
1296 items
=operations
+ navs
,
1300 def poll(cls
, context
):
1301 return (nw_check(context
)
1302 and nw_check_space_type(cls
, context
, 'ShaderNodeTree', 'CompositorNodeTree',
1303 'TextureNodeTree', 'GeometryNodeTree'))
1305 def execute(self
, context
):
1306 blend_type
= self
.blend_type
1307 operation
= self
.operation
1308 for node
in context
.selected_nodes
:
1309 if node
.type == 'MIX_RGB' or (node
.bl_idname
== 'ShaderNodeMix' and node
.data_type
== 'RGBA'):
1310 if blend_type
not in [nav
[0] for nav
in navs
]:
1311 node
.blend_type
= blend_type
1313 if blend_type
== 'NEXT':
1314 index
= [i
for i
, entry
in enumerate(blend_types
) if node
.blend_type
in entry
][0]
1315 # index = blend_types.index(node.blend_type)
1316 if index
== len(blend_types
) - 1:
1317 node
.blend_type
= blend_types
[0][0]
1319 node
.blend_type
= blend_types
[index
+ 1][0]
1321 if blend_type
== 'PREV':
1322 index
= [i
for i
, entry
in enumerate(blend_types
) if node
.blend_type
in entry
][0]
1324 node
.blend_type
= blend_types
[len(blend_types
) - 1][0]
1326 node
.blend_type
= blend_types
[index
- 1][0]
1328 if node
.type == 'MATH' or node
.bl_idname
== 'ShaderNodeMath':
1329 if operation
not in [nav
[0] for nav
in navs
]:
1330 node
.operation
= operation
1332 if operation
== 'NEXT':
1333 index
= [i
for i
, entry
in enumerate(operations
) if node
.operation
in entry
][0]
1334 # index = operations.index(node.operation)
1335 if index
== len(operations
) - 1:
1336 node
.operation
= operations
[0][0]
1338 node
.operation
= operations
[index
+ 1][0]
1340 if operation
== 'PREV':
1341 index
= [i
for i
, entry
in enumerate(operations
) if node
.operation
in entry
][0]
1342 # index = operations.index(node.operation)
1344 node
.operation
= operations
[len(operations
) - 1][0]
1346 node
.operation
= operations
[index
- 1][0]
1351 class NWChangeMixFactor(Operator
, NWBase
):
1352 bl_idname
= "node.nw_factor"
1353 bl_label
= "Change Factor"
1354 bl_description
= "Change Factors of Mix Nodes and Mix Shader Nodes"
1355 bl_options
= {'REGISTER', 'UNDO'}
1357 # option: Change factor.
1358 # If option is 1.0 or 0.0 - set to 1.0 or 0.0
1359 # Else - change factor by option value.
1360 option
: FloatProperty()
1362 def execute(self
, context
):
1363 nodes
, links
= get_nodes_links(context
)
1364 option
= self
.option
1365 selected
= [] # entry = index
1366 for si
, node
in enumerate(nodes
):
1368 if node
.type in {'MIX_RGB', 'MIX_SHADER'} or node
.bl_idname
== 'ShaderNodeMix':
1372 fac
= nodes
[si
].inputs
[0]
1373 nodes
[si
].hide
= False
1374 if option
in {0.0, 1.0}:
1375 fac
.default_value
= option
1377 fac
.default_value
+= option
1382 class NWCopySettings(Operator
, NWBase
):
1383 bl_idname
= "node.nw_copy_settings"
1384 bl_label
= "Copy Settings"
1385 bl_description
= "Copy Settings of Active Node to Selected Nodes"
1386 bl_options
= {'REGISTER', 'UNDO'}
1389 def poll(cls
, context
):
1390 return (nw_check(context
)
1391 and context
.active_node
is not None
1392 and context
.active_node
.type != 'FRAME')
1394 def execute(self
, context
):
1395 node_active
= context
.active_node
1396 node_selected
= context
.selected_nodes
1399 if not (len(node_selected
) > 1):
1400 self
.report({'ERROR'}, "2 nodes must be selected at least")
1401 return {'CANCELLED'}
1403 # Check if active node is in the selection
1404 selected_node_names
= [n
.name
for n
in node_selected
]
1405 if node_active
.name
not in selected_node_names
:
1406 self
.report({'ERROR'}, "No active node")
1407 return {'CANCELLED'}
1409 # Get nodes in selection by type
1410 valid_nodes
= [n
for n
in node_selected
if n
.type == node_active
.type]
1412 if not (len(valid_nodes
) > 1) and node_active
:
1413 self
.report({'ERROR'}, "Selected nodes are not of the same type as {}".format(node_active
.name
))
1414 return {'CANCELLED'}
1416 if len(valid_nodes
) != len(node_selected
):
1417 # Report nodes that are not valid
1418 valid_node_names
= [n
.name
for n
in valid_nodes
]
1419 not_valid_names
= list(set(selected_node_names
) - set(valid_node_names
))
1422 "Ignored {} (not of the same type as {})".format(
1423 ", ".join(not_valid_names
),
1426 # Reference original
1428 # node_selected_names = [n.name for n in node_selected]
1433 # Deselect all nodes
1434 for i
in node_selected
:
1437 # Code by zeffii from http://blender.stackexchange.com/a/42338/3710
1438 # Run through all other nodes
1439 for node
in valid_nodes
[1:]:
1441 # Check for frame node
1442 parent
= node
.parent
if node
.parent
else None
1443 node_loc
= [node
.location
.x
, node
.location
.y
]
1445 # Select original to duplicate
1448 # Duplicate selected node
1449 bpy
.ops
.node
.duplicate()
1450 new_node
= context
.selected_nodes
[0]
1453 new_node
.select
= False
1455 # Properties to copy
1456 node_tree
= node
.id_data
1457 props_to_copy
= 'bl_idname name location height width'.split(' ')
1461 mappings
= chain
.from_iterable([node
.inputs
, node
.outputs
])
1462 for i
in (i
for i
in mappings
if i
.is_linked
):
1464 reconnections
.append([L
.from_socket
.path_from_id(), L
.to_socket
.path_from_id()])
1467 props
= {j
: getattr(node
, j
) for j
in props_to_copy
}
1468 props_to_copy
.pop(0)
1470 for prop
in props_to_copy
:
1471 setattr(new_node
, prop
, props
[prop
])
1473 # Get the node tree to remove the old node
1474 nodes
= node_tree
.nodes
1476 new_node
.name
= props
['name']
1479 new_node
.parent
= parent
1480 new_node
.location
= node_loc
1482 for str_from
, str_to
in reconnections
:
1483 node_tree
.connect_sockets(eval(str_from
), eval(str_to
))
1485 success_names
.append(new_node
.name
)
1488 node_tree
.nodes
.active
= orig
1491 "Successfully copied attributes from {} to: {}".format(
1493 ", ".join(success_names
)))
1497 class NWCopyLabel(Operator
, NWBase
):
1498 bl_idname
= "node.nw_copy_label"
1499 bl_label
= "Copy Label"
1500 bl_options
= {'REGISTER', 'UNDO'}
1502 option
: EnumProperty(
1504 description
="Source of name of label",
1506 ('FROM_ACTIVE', 'from active', 'from active node',),
1507 ('FROM_NODE', 'from node', 'from node linked to selected node'),
1508 ('FROM_SOCKET', 'from socket', 'from socket linked to selected node'),
1512 def execute(self
, context
):
1513 nodes
, links
= get_nodes_links(context
)
1514 option
= self
.option
1515 active
= nodes
.active
1516 if option
== 'FROM_ACTIVE':
1518 src_label
= active
.label
1519 for node
in [n
for n
in nodes
if n
.select
and nodes
.active
!= n
]:
1520 node
.label
= src_label
1521 elif option
== 'FROM_NODE':
1522 selected
= [n
for n
in nodes
if n
.select
]
1523 for node
in selected
:
1524 for input in node
.inputs
:
1526 src
= input.links
[0].from_node
1527 node
.label
= src
.label
1529 elif option
== 'FROM_SOCKET':
1530 selected
= [n
for n
in nodes
if n
.select
]
1531 for node
in selected
:
1532 for input in node
.inputs
:
1534 src
= input.links
[0].from_socket
1535 node
.label
= src
.name
1541 class NWClearLabel(Operator
, NWBase
):
1542 bl_idname
= "node.nw_clear_label"
1543 bl_label
= "Clear Label"
1544 bl_options
= {'REGISTER', 'UNDO'}
1546 option
: BoolProperty()
1548 def execute(self
, context
):
1549 nodes
, links
= get_nodes_links(context
)
1550 for node
in [n
for n
in nodes
if n
.select
]:
1555 def invoke(self
, context
, event
):
1557 return self
.execute(context
)
1559 return context
.window_manager
.invoke_confirm(self
, event
)
1562 class NWModifyLabels(Operator
, NWBase
):
1563 """Modify Labels of all selected nodes"""
1564 bl_idname
= "node.nw_modify_labels"
1565 bl_label
= "Modify Labels"
1566 bl_options
= {'REGISTER', 'UNDO'}
1568 prepend
: StringProperty(
1569 name
="Add to Beginning"
1571 append
: StringProperty(
1574 replace_from
: StringProperty(
1575 name
="Text to Replace"
1577 replace_to
: StringProperty(
1581 def execute(self
, context
):
1582 nodes
, links
= get_nodes_links(context
)
1583 for node
in [n
for n
in nodes
if n
.select
]:
1584 node
.label
= self
.prepend
+ node
.label
.replace(self
.replace_from
, self
.replace_to
) + self
.append
1588 def invoke(self
, context
, event
):
1592 return context
.window_manager
.invoke_props_dialog(self
)
1595 class NWAddTextureSetup(Operator
, NWBase
):
1596 bl_idname
= "node.nw_add_texture"
1597 bl_label
= "Texture Setup"
1598 bl_description
= "Add Texture Node Setup to Selected Shaders"
1599 bl_options
= {'REGISTER', 'UNDO'}
1601 add_mapping
: BoolProperty(
1602 name
="Add Mapping Nodes",
1603 description
="Create coordinate and mapping nodes for the texture (ignored for selected texture nodes)",
1607 def poll(cls
, context
):
1608 return (nw_check(context
)
1609 and nw_check_space_type(cls
, context
, 'ShaderNodeTree'))
1611 def execute(self
, context
):
1612 nodes
, links
= get_nodes_links(context
)
1614 texture_types
= get_texture_node_types()
1615 selected_nodes
= [n
for n
in nodes
if n
.select
]
1617 for node
in selected_nodes
:
1622 target_input
= node
.inputs
[0]
1623 for input in node
.inputs
:
1626 if not input.is_linked
:
1627 target_input
= input
1630 self
.report({'WARNING'}, "No free inputs for node: " + node
.name
)
1635 locx
= node
.location
.x
1636 locy
= node
.location
.y
- (input_index
* padding
)
1638 is_texture_node
= node
.rna_type
.identifier
in texture_types
1639 use_environment_texture
= node
.type == 'BACKGROUND'
1641 # Add an image texture before normal shader nodes.
1642 if not is_texture_node
:
1643 image_texture_type
= 'ShaderNodeTexEnvironment' if use_environment_texture
else 'ShaderNodeTexImage'
1644 image_texture_node
= nodes
.new(image_texture_type
)
1645 x_offset
= x_offset
+ image_texture_node
.width
+ padding
1646 image_texture_node
.location
= [locx
- x_offset
, locy
]
1647 nodes
.active
= image_texture_node
1648 connect_sockets(image_texture_node
.outputs
[0], target_input
)
1650 # The mapping setup following this will connect to the first input of this image texture.
1651 target_input
= image_texture_node
.inputs
[0]
1655 if is_texture_node
or self
.add_mapping
:
1657 mapping_node
= nodes
.new('ShaderNodeMapping')
1658 x_offset
= x_offset
+ mapping_node
.width
+ padding
1659 mapping_node
.location
= [locx
- x_offset
, locy
]
1660 connect_sockets(mapping_node
.outputs
[0], target_input
)
1662 # Add Texture Coordinates node.
1663 tex_coord_node
= nodes
.new('ShaderNodeTexCoord')
1664 x_offset
= x_offset
+ tex_coord_node
.width
+ padding
1665 tex_coord_node
.location
= [locx
- x_offset
, locy
]
1667 is_procedural_texture
= is_texture_node
and node
.type != 'TEX_IMAGE'
1668 use_generated_coordinates
= is_procedural_texture
or use_environment_texture
1669 tex_coord_output
= tex_coord_node
.outputs
[0 if use_generated_coordinates
else 2]
1670 connect_sockets(tex_coord_output
, mapping_node
.inputs
[0])
1675 class NWAddPrincipledSetup(Operator
, NWBase
, ImportHelper
):
1676 bl_idname
= "node.nw_add_textures_for_principled"
1677 bl_label
= "Principled Texture Setup"
1678 bl_description
= "Add Texture Node Setup for Principled BSDF"
1679 bl_options
= {'REGISTER', 'UNDO'}
1681 directory
: StringProperty(
1685 description
='Folder to search in for image files'
1687 files
: CollectionProperty(
1688 type=bpy
.types
.OperatorFileListElement
,
1689 options
={'HIDDEN', 'SKIP_SAVE'}
1692 relative_path
: BoolProperty(
1693 name
='Relative Path',
1694 description
='Set the file path relative to the blend file, when possible',
1703 def draw(self
, context
):
1704 layout
= self
.layout
1705 layout
.alignment
= 'LEFT'
1707 layout
.prop(self
, 'relative_path')
1710 def poll(cls
, context
):
1711 return (nw_check(context
)
1712 and nw_check_space_type(cls
, context
, 'ShaderNodeTree'))
1714 def execute(self
, context
):
1715 # Check if everything is ok
1716 if not self
.directory
:
1717 self
.report({'INFO'}, 'No Folder Selected')
1718 return {'CANCELLED'}
1719 if not self
.files
[:]:
1720 self
.report({'INFO'}, 'No Files Selected')
1721 return {'CANCELLED'}
1723 nodes
, links
= get_nodes_links(context
)
1724 active_node
= nodes
.active
1725 if not (active_node
and active_node
.bl_idname
== 'ShaderNodeBsdfPrincipled'):
1726 self
.report({'INFO'}, 'Select Principled BSDF')
1727 return {'CANCELLED'}
1729 # Filter textures names for texturetypes in filenames
1730 # [Socket Name, [abbreviations and keyword list], Filename placeholder]
1731 tags
= context
.preferences
.addons
[__package__
].preferences
.principled_tags
1732 normal_abbr
= tags
.normal
.split(' ')
1733 bump_abbr
= tags
.bump
.split(' ')
1734 gloss_abbr
= tags
.gloss
.split(' ')
1735 rough_abbr
= tags
.rough
.split(' ')
1737 ['Displacement', tags
.displacement
.split(' '), None],
1738 ['Base Color', tags
.base_color
.split(' '), None],
1739 ['Metallic', tags
.metallic
.split(' '), None],
1740 ['Specular IOR Level', tags
.specular
.split(' '), None],
1741 ['Roughness', rough_abbr
+ gloss_abbr
, None],
1742 ['Bump', bump_abbr
, None],
1743 ['Normal', normal_abbr
, None],
1744 ['Transmission Weight', tags
.transmission
.split(' '), None],
1745 ['Emission Color', tags
.emission
.split(' '), None],
1746 ['Alpha', tags
.alpha
.split(' '), None],
1747 ['Ambient Occlusion', tags
.ambient_occlusion
.split(' '), None],
1750 match_files_to_socket_names(self
.files
, socketnames
)
1751 # Remove socketnames without found files
1752 socketnames
= [s
for s
in socketnames
if s
[2]
1753 and path
.exists(self
.directory
+ s
[2])]
1755 self
.report({'INFO'}, 'No matching images found')
1756 print('No matching images found')
1757 return {'CANCELLED'}
1759 # Don't override path earlier as os.path is used to check the absolute path
1760 import_path
= self
.directory
1761 if self
.relative_path
:
1762 if bpy
.data
.filepath
:
1764 import_path
= bpy
.path
.relpath(self
.directory
)
1769 print('\nMatched Textures:')
1774 normal_node_texture
= None
1776 bump_node_texture
= None
1777 roughness_node
= None
1778 for i
, sname
in enumerate(socketnames
):
1779 print(i
, sname
[0], sname
[2])
1781 # DISPLACEMENT NODES
1782 if sname
[0] == 'Displacement':
1783 disp_texture
= nodes
.new(type='ShaderNodeTexImage')
1784 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
1785 disp_texture
.image
= img
1786 disp_texture
.label
= 'Displacement'
1787 if disp_texture
.image
:
1788 disp_texture
.image
.colorspace_settings
.is_data
= True
1790 # Add displacement offset nodes
1791 disp_node
= nodes
.new(type='ShaderNodeDisplacement')
1792 # Align the Displacement node under the active Principled BSDF node
1793 disp_node
.location
= active_node
.location
+ Vector((100, -700))
1794 link
= connect_sockets(disp_node
.inputs
[0], disp_texture
.outputs
[0])
1796 # TODO Turn on true displacement in the material
1797 # Too complicated for now
1800 output_node
= [n
for n
in nodes
if n
.bl_idname
== 'ShaderNodeOutputMaterial']
1802 if not output_node
[0].inputs
[2].is_linked
:
1803 link
= connect_sockets(output_node
[0].inputs
[2], disp_node
.outputs
[0])
1808 elif sname
[0] == 'Bump':
1809 # Test if new texture node is bump map
1810 fname_components
= split_into_components(sname
[2])
1811 match_bump
= set(bump_abbr
).intersection(set(fname_components
))
1813 # If Bump add bump node in between
1814 bump_node_texture
= nodes
.new(type='ShaderNodeTexImage')
1815 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
1816 bump_node_texture
.image
= img
1817 bump_node_texture
.label
= 'Bump'
1820 bump_node
= nodes
.new(type='ShaderNodeBump')
1821 link
= connect_sockets(bump_node
.inputs
[2], bump_node_texture
.outputs
[0])
1822 link
= connect_sockets(active_node
.inputs
['Normal'], bump_node
.outputs
[0])
1826 elif sname
[0] == 'Normal':
1827 # Test if new texture node is normal map
1828 fname_components
= split_into_components(sname
[2])
1829 match_normal
= set(normal_abbr
).intersection(set(fname_components
))
1831 # If Normal add normal node in between
1832 normal_node_texture
= nodes
.new(type='ShaderNodeTexImage')
1833 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
1834 normal_node_texture
.image
= img
1835 normal_node_texture
.label
= 'Normal'
1838 normal_node
= nodes
.new(type='ShaderNodeNormalMap')
1839 link
= connect_sockets(normal_node
.inputs
[1], normal_node_texture
.outputs
[0])
1840 # Connect to bump node if it was created before, otherwise to the BSDF
1841 if bump_node
is None:
1842 link
= connect_sockets(active_node
.inputs
[sname
[0]], normal_node
.outputs
[0])
1844 link
= connect_sockets(bump_node
.inputs
[sname
[0]], normal_node
.outputs
[sname
[0]])
1847 # AMBIENT OCCLUSION TEXTURE
1848 elif sname
[0] == 'Ambient Occlusion':
1849 ao_texture
= nodes
.new(type='ShaderNodeTexImage')
1850 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
1851 ao_texture
.image
= img
1852 ao_texture
.label
= sname
[0]
1853 if ao_texture
.image
:
1854 ao_texture
.image
.colorspace_settings
.is_data
= True
1858 if not active_node
.inputs
[sname
[0]].is_linked
:
1859 # No texture node connected -> add texture node with new image
1860 texture_node
= nodes
.new(type='ShaderNodeTexImage')
1861 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
1862 texture_node
.image
= img
1864 if sname
[0] == 'Roughness':
1865 # Test if glossy or roughness map
1866 fname_components
= split_into_components(sname
[2])
1867 match_rough
= set(rough_abbr
).intersection(set(fname_components
))
1868 match_gloss
= set(gloss_abbr
).intersection(set(fname_components
))
1871 # If Roughness nothing to to
1872 link
= connect_sockets(active_node
.inputs
[sname
[0]], texture_node
.outputs
[0])
1875 # If Gloss Map add invert node
1876 invert_node
= nodes
.new(type='ShaderNodeInvert')
1877 link
= connect_sockets(invert_node
.inputs
[1], texture_node
.outputs
[0])
1879 link
= connect_sockets(active_node
.inputs
[sname
[0]], invert_node
.outputs
[0])
1880 roughness_node
= texture_node
1883 # This is a simple connection Texture --> Input slot
1884 link
= connect_sockets(active_node
.inputs
[sname
[0]], texture_node
.outputs
[0])
1886 # Use non-color except for color inputs
1887 if sname
[0] not in ['Base Color', 'Emission Color'] and texture_node
.image
:
1888 texture_node
.image
.colorspace_settings
.is_data
= True
1891 # If already texture connected. add to node list for alignment
1892 texture_node
= active_node
.inputs
[sname
[0]].links
[0].from_node
1894 # This are all connected texture nodes
1895 texture_nodes
.append(texture_node
)
1896 texture_node
.label
= sname
[0]
1899 texture_nodes
.append(disp_texture
)
1900 if bump_node_texture
:
1901 texture_nodes
.append(bump_node_texture
)
1902 if normal_node_texture
:
1903 texture_nodes
.append(normal_node_texture
)
1906 # We want the ambient occlusion texture to be the top most texture node
1907 texture_nodes
.insert(0, ao_texture
)
1910 for i
, texture_node
in enumerate(texture_nodes
):
1911 offset
= Vector((-550, (i
* -280) + 200))
1912 texture_node
.location
= active_node
.location
+ offset
1915 # Extra alignment if normal node was added
1916 normal_node
.location
= normal_node_texture
.location
+ Vector((300, 0))
1919 # Extra alignment if bump node was added
1920 bump_node
.location
= bump_node_texture
.location
+ Vector((300, 0))
1923 # Alignment of invert node if glossy map
1924 invert_node
.location
= roughness_node
.location
+ Vector((300, 0))
1926 # Add texture input + mapping
1927 mapping
= nodes
.new(type='ShaderNodeMapping')
1928 mapping
.location
= active_node
.location
+ Vector((-1050, 0))
1929 if len(texture_nodes
) > 1:
1930 # If more than one texture add reroute node in between
1931 reroute
= nodes
.new(type='NodeReroute')
1932 texture_nodes
.append(reroute
)
1933 tex_coords
= Vector((texture_nodes
[0].location
.x
,
1934 sum(n
.location
.y
for n
in texture_nodes
) / len(texture_nodes
)))
1935 reroute
.location
= tex_coords
+ Vector((-50, -120))
1936 for texture_node
in texture_nodes
:
1937 link
= connect_sockets(texture_node
.inputs
[0], reroute
.outputs
[0])
1938 link
= connect_sockets(reroute
.inputs
[0], mapping
.outputs
[0])
1940 link
= connect_sockets(texture_nodes
[0].inputs
[0], mapping
.outputs
[0])
1942 # Connect texture_coordinates to mapping node
1943 texture_input
= nodes
.new(type='ShaderNodeTexCoord')
1944 texture_input
.location
= mapping
.location
+ Vector((-200, 0))
1945 link
= connect_sockets(mapping
.inputs
[0], texture_input
.outputs
[2])
1947 # Create frame around tex coords and mapping
1948 frame
= nodes
.new(type='NodeFrame')
1949 frame
.label
= 'Mapping'
1950 mapping
.parent
= frame
1951 texture_input
.parent
= frame
1954 # Create frame around texture nodes
1955 frame
= nodes
.new(type='NodeFrame')
1956 frame
.label
= 'Textures'
1957 for tnode
in texture_nodes
:
1958 tnode
.parent
= frame
1962 active_node
.select
= False
1965 force_update(context
)
1969 class NWAddReroutes(Operator
, NWBase
):
1970 """Add Reroute Nodes and link them to outputs of selected nodes"""
1971 bl_idname
= "node.nw_add_reroutes"
1972 bl_label
= "Add Reroutes"
1973 bl_description
= "Add Reroutes to Outputs"
1974 bl_options
= {'REGISTER', 'UNDO'}
1976 option
: EnumProperty(
1979 ('ALL', 'to all', 'Add to all outputs'),
1980 ('LOOSE', 'to loose', 'Add only to loose outputs'),
1981 ('LINKED', 'to linked', 'Add only to linked outputs'),
1985 def execute(self
, context
):
1986 nodes
, _links
= get_nodes_links(context
)
1987 post_select
= [] # Nodes to be selected after execution.
1990 # Create reroutes and recreate links.
1991 for node
in [n
for n
in nodes
if n
.select
]:
1992 if not node
.outputs
:
1995 x
, y
= node
.location
1997 # Unhide 'REROUTE' nodes to avoid issues with location.y
1998 if node
.type == 'REROUTE':
2000 # Hack needed to calculate real width.
2002 bpy
.ops
.node
.select_all(action
='DESELECT')
2003 helper
= nodes
.new('NodeReroute')
2004 helper
.select
= True
2006 # Resize node and helper to zero. Then check locations to calculate width.
2007 bpy
.ops
.transform
.resize(value
=(0.0, 0.0, 0.0))
2008 width
= 2.0 * (helper
.location
.x
- node
.location
.x
)
2009 # Restore node location.
2010 node
.location
= x
, y
2013 # Only helper is selected now.
2014 bpy
.ops
.node
.delete()
2015 x
= node
.location
.x
+ width
+ 20.0
2016 if node
.type != 'REROUTE':
2019 reroutes_count
= 0 # Will be used when aligning reroutes added to hidden nodes.
2020 for out_i
, output
in enumerate(node
.outputs
):
2021 if node
.type == 'R_LAYERS' and output
.name
!= 'Alpha':
2022 # If 'R_LAYERS' check if output is used in render pass.
2023 # If output is "Alpha", assume it's used. Not available in passes.
2024 node_scene
= node
.scene
2025 node_layer
= node
.layer
2026 for rlo
in rl_outputs
:
2027 # Check entries in global 'rl_outputs' variable.
2028 if output
.name
in {rlo
.output_name
, rlo
.exr_output_name
}:
2029 if not getattr(node_scene
.view_layers
[node_layer
], rlo
.render_pass
):
2032 # Output is valid when option is 'all' or when 'loose' output has no links.
2033 valid
= ((self
.option
== 'ALL') or
2034 (self
.option
== 'LOOSE' and not output
.links
) or
2035 (self
.option
== 'LINKED' and output
.links
))
2036 # Add reroutes only if valid, but offset location in all cases.
2038 n
= nodes
.new('NodeReroute')
2040 for link
in output
.links
:
2041 connect_sockets(n
.outputs
[0], link
.to_socket
)
2042 connect_sockets(output
, n
.inputs
[0])
2044 post_select
.append(n
)
2047 # Deselect the node so that after execution of script only newly created nodes are selected.
2050 # Nicer reroutes distribution along y when node.hide.
2052 y_translate
= reroutes_count
* y_offset
/ 2.0 - y_offset
- 35.0
2053 for reroute
in [r
for r
in nodes
if r
.select
]:
2054 reroute
.location
.y
-= y_translate
2055 for node
in post_select
:
2061 class NWLinkActiveToSelected(Operator
, NWBase
):
2062 """Link active node to selected nodes basing on various criteria"""
2063 bl_idname
= "node.nw_link_active_to_selected"
2064 bl_label
= "Link Active Node to Selected"
2065 bl_options
= {'REGISTER', 'UNDO'}
2067 replace
: BoolProperty()
2068 use_node_name
: BoolProperty()
2069 use_outputs_names
: BoolProperty()
2072 def poll(cls
, context
):
2073 return (nw_check(context
)
2074 and context
.active_node
is not None
2075 and context
.active_node
.select
)
2077 def execute(self
, context
):
2078 nodes
, links
= get_nodes_links(context
)
2079 replace
= self
.replace
2080 use_node_name
= self
.use_node_name
2081 use_outputs_names
= self
.use_outputs_names
2082 active
= nodes
.active
2083 selected
= [node
for node
in nodes
if node
.select
and node
!= active
]
2084 outputs
= [] # Only usable outputs of active nodes will be stored here.
2085 for out
in active
.outputs
:
2086 if active
.type != 'R_LAYERS':
2089 # 'R_LAYERS' node type needs special handling.
2090 # outputs of 'R_LAYERS' are callable even if not seen in UI.
2091 # Only outputs that represent used passes should be taken into account
2092 # Check if pass represented by output is used.
2093 # global 'rl_outputs' list will be used for that
2094 for rlo
in rl_outputs
:
2095 pass_used
= False # initial value. Will be set to True if pass is used
2096 if out
.name
== 'Alpha':
2097 # Alpha output is always present. Doesn't have representation in render pass. Assume it's used.
2099 elif out
.name
in {rlo
.output_name
, rlo
.exr_output_name
}:
2100 # example 'render_pass' entry: 'use_pass_uv' Check if True in scene render layers
2101 pass_used
= getattr(active
.scene
.view_layers
[active
.layer
], rlo
.render_pass
)
2105 doit
= True # Will be changed to False when links successfully added to previous output.
2108 for node
in selected
:
2109 dst_name
= node
.name
# Will be compared with src_name if needed.
2110 # When node has label - use it as dst_name
2112 dst_name
= node
.label
2113 valid
= True # Initial value. Will be changed to False if names don't match.
2114 src_name
= dst_name
# If names not used - this assignment will keep valid = True.
2116 # Set src_name to source node name or label
2117 src_name
= active
.name
2119 src_name
= active
.label
2120 elif use_outputs_names
:
2121 src_name
= (out
.name
, )
2122 for rlo
in rl_outputs
:
2123 if out
.name
in {rlo
.output_name
, rlo
.exr_output_name
}:
2124 src_name
= (rlo
.output_name
, rlo
.exr_output_name
)
2125 if dst_name
not in src_name
:
2128 for input in node
.inputs
:
2129 if input.type == out
.type or node
.type == 'REROUTE':
2130 if replace
or not input.is_linked
:
2131 connect_sockets(out
, input)
2132 if not use_node_name
and not use_outputs_names
:
2139 class NWAlignNodes(Operator
, NWBase
):
2140 '''Align the selected nodes neatly in a row/column'''
2141 bl_idname
= "node.nw_align_nodes"
2142 bl_label
= "Align Nodes"
2143 bl_options
= {'REGISTER', 'UNDO'}
2144 margin
: IntProperty(name
='Margin', default
=50, description
='The amount of space between nodes')
2146 def execute(self
, context
):
2147 nodes
, links
= get_nodes_links(context
)
2148 margin
= self
.margin
2152 if node
.select
and node
.type != 'FRAME':
2153 selection
.append(node
)
2155 # If no nodes are selected, align all nodes
2159 elif nodes
.active
in selection
:
2160 active_loc
= copy(nodes
.active
.location
) # make a copy, not a reference
2162 # Check if nodes should be laid out horizontally or vertically
2163 # use dimension to get center of node, not corner
2164 x_locs
= [n
.location
.x
+ (n
.dimensions
.x
/ 2) for n
in selection
]
2165 y_locs
= [n
.location
.y
- (n
.dimensions
.y
/ 2) for n
in selection
]
2166 x_range
= max(x_locs
) - min(x_locs
)
2167 y_range
= max(y_locs
) - min(y_locs
)
2168 mid_x
= (max(x_locs
) + min(x_locs
)) / 2
2169 mid_y
= (max(y_locs
) + min(y_locs
)) / 2
2170 horizontal
= x_range
> y_range
2172 # Sort selection by location of node mid-point
2174 selection
= sorted(selection
, key
=lambda n
: n
.location
.x
+ (n
.dimensions
.x
/ 2))
2176 selection
= sorted(selection
, key
=lambda n
: n
.location
.y
- (n
.dimensions
.y
/ 2), reverse
=True)
2180 for node
in selection
:
2181 current_margin
= margin
2182 current_margin
= current_margin
* 0.5 if node
.hide
else current_margin
# use a smaller margin for hidden nodes
2185 node
.location
.x
= current_pos
2186 current_pos
+= current_margin
+ node
.dimensions
.x
2187 node
.location
.y
= mid_y
+ (node
.dimensions
.y
/ 2)
2189 node
.location
.y
= current_pos
2190 current_pos
-= (current_margin
* 0.3) + node
.dimensions
.y
# use half-margin for vertical alignment
2191 node
.location
.x
= mid_x
- (node
.dimensions
.x
/ 2)
2193 # If active node is selected, center nodes around it
2194 if active_loc
is not None:
2195 active_loc_diff
= active_loc
- nodes
.active
.location
2196 for node
in selection
:
2197 node
.location
+= active_loc_diff
2198 else: # Position nodes centered around where they used to be
2199 locs
= ([n
.location
.x
+ (n
.dimensions
.x
/ 2) for n
in selection
]
2200 ) if horizontal
else ([n
.location
.y
- (n
.dimensions
.y
/ 2) for n
in selection
])
2201 new_mid
= (max(locs
) + min(locs
)) / 2
2202 for node
in selection
:
2204 node
.location
.x
+= (mid_x
- new_mid
)
2206 node
.location
.y
+= (mid_y
- new_mid
)
2211 class NWSelectParentChildren(Operator
, NWBase
):
2212 bl_idname
= "node.nw_select_parent_child"
2213 bl_label
= "Select Parent or Children"
2214 bl_options
= {'REGISTER', 'UNDO'}
2216 option
: EnumProperty(
2219 ('PARENT', 'Select Parent', 'Select Parent Frame'),
2220 ('CHILD', 'Select Children', 'Select members of selected frame'),
2224 def execute(self
, context
):
2225 nodes
, links
= get_nodes_links(context
)
2226 option
= self
.option
2227 selected
= [node
for node
in nodes
if node
.select
]
2228 if option
== 'PARENT':
2229 for sel
in selected
:
2232 parent
.select
= True
2233 else: # option == 'CHILD'
2234 for sel
in selected
:
2235 children
= [node
for node
in nodes
if node
.parent
== sel
]
2236 for kid
in children
:
2242 class NWDetachOutputs(Operator
, NWBase
):
2243 """Detach outputs of selected node leaving inputs linked"""
2244 bl_idname
= "node.nw_detach_outputs"
2245 bl_label
= "Detach Outputs"
2246 bl_options
= {'REGISTER', 'UNDO'}
2248 def execute(self
, context
):
2249 nodes
, links
= get_nodes_links(context
)
2250 selected
= context
.selected_nodes
2251 bpy
.ops
.node
.duplicate_move_keep_inputs()
2252 new_nodes
= context
.selected_nodes
2253 bpy
.ops
.node
.select_all(action
="DESELECT")
2254 for node
in selected
:
2256 bpy
.ops
.node
.delete_reconnect()
2257 for new_node
in new_nodes
:
2258 new_node
.select
= True
2259 bpy
.ops
.transform
.translate('INVOKE_DEFAULT')
2264 class NWLinkToOutputNode(Operator
):
2265 """Link to Composite node or Material Output node"""
2266 bl_idname
= "node.nw_link_out"
2267 bl_label
= "Connect to Output"
2268 bl_options
= {'REGISTER', 'UNDO'}
2271 def poll(cls
, context
):
2272 """Disabled for custom nodes as we do not know which nodes are outputs."""
2273 return (nw_check(context
)
2274 and nw_check_space_type(cls
, context
, 'ShaderNodeTree', 'CompositorNodeTree',
2275 'TextureNodeTree', 'GeometryNodeTree')
2276 and context
.active_node
is not None
2277 and any(is_visible_socket(out
) for out
in context
.active_node
.outputs
))
2279 def execute(self
, context
):
2280 nodes
, links
= get_nodes_links(context
)
2281 active
= nodes
.active
2283 tree_type
= context
.space_data
.tree_type
2284 shader_outputs
= {'OBJECT': 'ShaderNodeOutputMaterial',
2285 'WORLD': 'ShaderNodeOutputWorld',
2286 'LINESTYLE': 'ShaderNodeOutputLineStyle'}
2288 'ShaderNodeTree': shader_outputs
[context
.space_data
.shader_type
],
2289 'CompositorNodeTree': 'CompositorNodeComposite',
2290 'TextureNodeTree': 'TextureNodeOutput',
2291 'GeometryNodeTree': 'NodeGroupOutput',
2294 # check whether the node is an output node and,
2295 # if supported, whether it's the active one
2296 if node
.rna_type
.identifier
== output_type \
2297 and (node
.is_active_output
if hasattr(node
, 'is_active_output')
2301 else: # No output node exists
2302 bpy
.ops
.node
.select_all(action
="DESELECT")
2303 output_node
= nodes
.new(output_type
)
2304 output_node
.location
.x
= active
.location
.x
+ active
.dimensions
.x
+ 80
2305 output_node
.location
.y
= active
.location
.y
2308 for i
, output
in enumerate(active
.outputs
):
2309 if is_visible_socket(output
):
2312 for i
, output
in enumerate(active
.outputs
):
2313 if output
.type == output_node
.inputs
[0].type and is_visible_socket(output
):
2318 if tree_type
== 'ShaderNodeTree':
2319 if active
.outputs
[output_index
].name
== 'Volume':
2321 elif active
.outputs
[output_index
].name
== 'Displacement':
2323 elif tree_type
== 'GeometryNodeTree':
2324 if active
.outputs
[output_index
].type != 'GEOMETRY':
2325 return {'CANCELLED'}
2326 connect_sockets(active
.outputs
[output_index
], output_node
.inputs
[out_input_index
])
2328 force_update(context
) # viewport render does not update
2333 class NWMakeLink(Operator
, NWBase
):
2334 """Make a link from one socket to another"""
2335 bl_idname
= 'node.nw_make_link'
2336 bl_label
= 'Make Link'
2337 bl_options
= {'REGISTER', 'UNDO'}
2338 from_socket
: IntProperty()
2339 to_socket
: IntProperty()
2341 def execute(self
, context
):
2342 nodes
, links
= get_nodes_links(context
)
2344 n1
= nodes
[context
.scene
.NWLazySource
]
2345 n2
= nodes
[context
.scene
.NWLazyTarget
]
2347 connect_sockets(n1
.outputs
[self
.from_socket
], n2
.inputs
[self
.to_socket
])
2349 force_update(context
)
2354 class NWCallInputsMenu(Operator
, NWBase
):
2355 """Link from this output"""
2356 bl_idname
= 'node.nw_call_inputs_menu'
2357 bl_label
= 'Make Link'
2358 bl_options
= {'REGISTER', 'UNDO'}
2359 from_socket
: IntProperty()
2361 def execute(self
, context
):
2362 nodes
, links
= get_nodes_links(context
)
2364 context
.scene
.NWSourceSocket
= self
.from_socket
2366 n1
= nodes
[context
.scene
.NWLazySource
]
2367 n2
= nodes
[context
.scene
.NWLazyTarget
]
2368 if len(n2
.inputs
) > 1:
2369 bpy
.ops
.wm
.call_menu("INVOKE_DEFAULT", name
=NWConnectionListInputs
.bl_idname
)
2370 elif len(n2
.inputs
) == 1:
2371 connect_sockets(n1
.outputs
[self
.from_socket
], n2
.inputs
[0])
2375 class NWAddSequence(Operator
, NWBase
, ImportHelper
):
2376 """Add an Image Sequence"""
2377 bl_idname
= 'node.nw_add_sequence'
2378 bl_label
= 'Import Image Sequence'
2379 bl_options
= {'REGISTER', 'UNDO'}
2381 directory
: StringProperty(
2384 filename
: StringProperty(
2387 files
: CollectionProperty(
2388 type=bpy
.types
.OperatorFileListElement
,
2389 options
={'HIDDEN', 'SKIP_SAVE'}
2391 relative_path
: BoolProperty(
2392 name
='Relative Path',
2393 description
='Set the file path relative to the blend file, when possible',
2397 def draw(self
, context
):
2398 layout
= self
.layout
2399 layout
.alignment
= 'LEFT'
2401 layout
.prop(self
, 'relative_path')
2403 def execute(self
, context
):
2404 nodes
, links
= get_nodes_links(context
)
2405 directory
= self
.directory
2406 filename
= self
.filename
2408 tree
= context
.space_data
.node_tree
2411 # print ("\nDIR:", directory)
2412 # print ("FN:", filename)
2413 # print ("Fs:", list(f.name for f in files), '\n')
2415 if tree
.type == 'SHADER':
2416 node_type
= "ShaderNodeTexImage"
2417 elif tree
.type == 'COMPOSITING':
2418 node_type
= "CompositorNodeImage"
2420 self
.report({'ERROR'}, "Unsupported Node Tree type!")
2421 return {'CANCELLED'}
2423 if not files
[0].name
and not filename
:
2424 self
.report({'ERROR'}, "No file chosen")
2425 return {'CANCELLED'}
2426 elif files
[0].name
and (not filename
or not path
.exists(directory
+ filename
)):
2427 # User has selected multiple files without an active one, or the active one is non-existent
2428 filename
= files
[0].name
2430 if not path
.exists(directory
+ filename
):
2431 self
.report({'ERROR'}, filename
+ " does not exist!")
2432 return {'CANCELLED'}
2434 without_ext
= '.'.join(filename
.split('.')[:-1])
2436 # if last digit isn't a number, it's not a sequence
2437 if not without_ext
[-1].isdigit():
2438 self
.report({'ERROR'}, filename
+ " does not seem to be part of a sequence")
2439 return {'CANCELLED'}
2441 extension
= filename
.split('.')[-1]
2442 reverse
= without_ext
[::-1] # reverse string
2445 for char
in reverse
:
2451 without_num
= without_ext
[:count_numbers
* -1]
2453 files
= sorted(glob(directory
+ without_num
+ "[0-9]" * count_numbers
+ "." + extension
))
2455 num_frames
= len(files
)
2457 nodes_list
= [node
for node
in nodes
]
2459 nodes_list
.sort(key
=lambda k
: k
.location
.x
)
2460 xloc
= nodes_list
[0].location
.x
- 220 # place new nodes at far left
2464 yloc
+= node_mid_pt(node
, 'y')
2465 yloc
= yloc
/ len(nodes
)
2470 name_with_hashes
= without_num
+ "#" * count_numbers
+ '.' + extension
2472 bpy
.ops
.node
.add_node('INVOKE_DEFAULT', use_transform
=True, type=node_type
)
2474 node
.label
= name_with_hashes
2476 filepath
= directory
+ (without_ext
+ '.' + extension
)
2477 if self
.relative_path
:
2478 if bpy
.data
.filepath
:
2480 filepath
= bpy
.path
.relpath(filepath
)
2484 img
= bpy
.data
.images
.load(filepath
)
2485 img
.source
= 'SEQUENCE'
2486 img
.name
= name_with_hashes
2488 image_user
= node
.image_user
if tree
.type == 'SHADER' else node
2489 # separate the number from the file name of the first file
2490 image_user
.frame_offset
= int(files
[0][len(without_num
) + len(directory
):-1 * (len(extension
) + 1)]) - 1
2491 image_user
.frame_duration
= num_frames
2496 class NWAddMultipleImages(Operator
, NWBase
, ImportHelper
):
2497 """Add multiple images at once"""
2498 bl_idname
= 'node.nw_add_multiple_images'
2499 bl_label
= 'Open Selected Images'
2500 bl_options
= {'REGISTER', 'UNDO'}
2501 directory
: StringProperty(
2504 files
: CollectionProperty(
2505 type=bpy
.types
.OperatorFileListElement
,
2506 options
={'HIDDEN', 'SKIP_SAVE'}
2509 def execute(self
, context
):
2510 nodes
, links
= get_nodes_links(context
)
2512 xloc
, yloc
= context
.region
.view2d
.region_to_view(context
.area
.width
/ 2, context
.area
.height
/ 2)
2514 if context
.space_data
.node_tree
.type == 'SHADER':
2515 node_type
= "ShaderNodeTexImage"
2516 elif context
.space_data
.node_tree
.type == 'COMPOSITING':
2517 node_type
= "CompositorNodeImage"
2519 self
.report({'ERROR'}, "Unsupported Node Tree type!")
2520 return {'CANCELLED'}
2523 for f
in self
.files
:
2526 node
= nodes
.new(node_type
)
2527 new_nodes
.append(node
)
2530 node
.location
.x
= xloc
2531 node
.location
.y
= yloc
2534 img
= bpy
.data
.images
.load(self
.directory
+ fname
)
2537 # shift new nodes up to center of tree
2538 list_size
= new_nodes
[0].location
.y
- new_nodes
[-1].location
.y
2540 if node
in new_nodes
:
2542 node
.location
.y
+= (list_size
/ 2)
2548 class NWViewerFocus(bpy
.types
.Operator
):
2549 """Set the viewer tile center to the mouse position"""
2550 bl_idname
= "node.nw_viewer_focus"
2551 bl_label
= "Viewer Focus"
2553 x
: bpy
.props
.IntProperty()
2554 y
: bpy
.props
.IntProperty()
2557 def poll(cls
, context
):
2558 return (nw_check(context
)
2559 and nw_check_space_type(cls
, context
, 'CompositorNodeTree'))
2561 def execute(self
, context
):
2564 def invoke(self
, context
, event
):
2565 render
= context
.scene
.render
2566 space
= context
.space_data
2567 percent
= render
.resolution_percentage
* 0.01
2569 nodes
, links
= get_nodes_links(context
)
2570 viewers
= [n
for n
in nodes
if n
.type == 'VIEWER']
2573 mlocx
= event
.mouse_region_x
2574 mlocy
= event
.mouse_region_y
2575 select_node
= bpy
.ops
.node
.select(location
=(mlocx
, mlocy
), extend
=False)
2577 if 'FINISHED' not in select_node
: # only run if we're not clicking on a node
2578 region_x
= context
.region
.width
2579 region_y
= context
.region
.height
2581 region_center_x
= context
.region
.width
/ 2
2582 region_center_y
= context
.region
.height
/ 2
2584 bd_x
= render
.resolution_x
* percent
* space
.backdrop_zoom
2585 bd_y
= render
.resolution_y
* percent
* space
.backdrop_zoom
2587 backdrop_center_x
= (bd_x
/ 2) - space
.backdrop_offset
[0]
2588 backdrop_center_y
= (bd_y
/ 2) - space
.backdrop_offset
[1]
2590 margin_x
= region_center_x
- backdrop_center_x
2591 margin_y
= region_center_y
- backdrop_center_y
2593 abs_mouse_x
= (mlocx
- margin_x
) / bd_x
2594 abs_mouse_y
= (mlocy
- margin_y
) / bd_y
2596 for node
in viewers
:
2597 node
.center_x
= abs_mouse_x
2598 node
.center_y
= abs_mouse_y
2600 return {'PASS_THROUGH'}
2602 return self
.execute(context
)
2605 class NWSaveViewer(bpy
.types
.Operator
, ExportHelper
):
2606 """Save the current viewer node to an image file"""
2607 bl_idname
= "node.nw_save_viewer"
2608 bl_label
= "Save This Image"
2609 filepath
: StringProperty(subtype
="FILE_PATH")
2610 filename_ext
: EnumProperty(
2612 description
="Choose the file format to save to",
2613 items
=(('.bmp', "BMP", ""),
2614 ('.rgb', 'IRIS', ""),
2615 ('.png', 'PNG', ""),
2616 ('.jpg', 'JPEG', ""),
2617 ('.jp2', 'JPEG2000', ""),
2618 ('.tga', 'TARGA', ""),
2619 ('.cin', 'CINEON', ""),
2620 ('.dpx', 'DPX', ""),
2621 ('.exr', 'OPEN_EXR', ""),
2622 ('.hdr', 'HDR', ""),
2623 ('.tif', 'TIFF', "")),
2628 def poll(cls
, context
):
2629 return (nw_check(context
)
2630 and nw_check_space_type(cls
, context
, 'CompositorNodeTree')
2631 and any(img
.source
== 'VIEWER'
2632 and img
.render_slots
== 0
2633 for img
in bpy
.data
.images
)
2634 and sum(bpy
.data
.images
["Viewer Node"].size
) > 0) # False if not connected or connected but no image
2636 def execute(self
, context
):
2653 basename
, ext
= path
.splitext(fp
)
2654 old_render_format
= context
.scene
.render
.image_settings
.file_format
2655 context
.scene
.render
.image_settings
.file_format
= formats
[self
.filename_ext
]
2656 context
.area
.type = "IMAGE_EDITOR"
2657 context
.area
.spaces
[0].image
= bpy
.data
.images
['Viewer Node']
2658 context
.area
.spaces
[0].image
.save_render(fp
)
2659 context
.area
.type = "NODE_EDITOR"
2660 context
.scene
.render
.image_settings
.file_format
= old_render_format
2664 class NWResetNodes(bpy
.types
.Operator
):
2665 """Reset Nodes in Selection"""
2666 bl_idname
= "node.nw_reset_nodes"
2667 bl_label
= "Reset Nodes"
2668 bl_options
= {'REGISTER', 'UNDO'}
2671 def poll(cls
, context
):
2672 space
= context
.space_data
2673 return space
.type == 'NODE_EDITOR'
2675 def execute(self
, context
):
2676 node_active
= context
.active_node
2677 node_selected
= context
.selected_nodes
2678 node_ignore
= ["FRAME", "REROUTE", "GROUP", "SIMULATION_INPUT", "SIMULATION_OUTPUT"]
2680 # Check if one node is selected at least
2681 if not (len(node_selected
) > 0):
2682 self
.report({'ERROR'}, "1 node must be selected at least")
2683 return {'CANCELLED'}
2685 active_node_name
= node_active
.name
if node_active
.select
else None
2686 valid_nodes
= [n
for n
in node_selected
if n
.type not in node_ignore
]
2688 # Create output lists
2689 selected_node_names
= [n
.name
for n
in node_selected
]
2692 # Reset all valid children in a frame
2693 node_active_is_frame
= False
2694 if len(node_selected
) == 1 and node_active
.type == "FRAME":
2695 node_tree
= node_active
.id_data
2696 children
= [n
for n
in node_tree
.nodes
if n
.parent
== node_active
]
2698 valid_nodes
= [n
for n
in children
if n
.type not in node_ignore
]
2699 selected_node_names
= [n
.name
for n
in children
if n
.type not in node_ignore
]
2700 node_active_is_frame
= True
2702 # Check if valid nodes in selection
2703 if not (len(valid_nodes
) > 0):
2704 # Check for frames only
2705 frames_selected
= [n
for n
in node_selected
if n
.type == "FRAME"]
2706 if (len(frames_selected
) > 1 and len(frames_selected
) == len(node_selected
)):
2707 self
.report({'ERROR'}, "Please select only 1 frame to reset")
2709 self
.report({'ERROR'}, "No valid node(s) in selection")
2710 return {'CANCELLED'}
2712 # Report nodes that are not valid
2713 if len(valid_nodes
) != len(node_selected
) and node_active_is_frame
is False:
2714 valid_node_names
= [n
.name
for n
in valid_nodes
]
2715 not_valid_names
= list(set(selected_node_names
) - set(valid_node_names
))
2716 self
.report({'INFO'}, "Ignored {}".format(", ".join(not_valid_names
)))
2718 # Deselect all nodes
2719 for i
in node_selected
:
2722 # Run through all valid nodes
2723 for node
in valid_nodes
:
2725 parent
= node
.parent
if node
.parent
else None
2726 node_loc
= [node
.location
.x
, node
.location
.y
]
2728 node_tree
= node
.id_data
2729 props_to_copy
= 'bl_idname name location height width'.split(' ')
2732 mappings
= chain
.from_iterable([node
.inputs
, node
.outputs
])
2733 for i
in (i
for i
in mappings
if i
.is_linked
):
2735 reconnections
.append([L
.from_socket
.path_from_id(), L
.to_socket
.path_from_id()])
2737 props
= {j
: getattr(node
, j
) for j
in props_to_copy
}
2739 new_node
= node_tree
.nodes
.new(props
['bl_idname'])
2740 props_to_copy
.pop(0)
2742 for prop
in props_to_copy
:
2743 setattr(new_node
, prop
, props
[prop
])
2745 nodes
= node_tree
.nodes
2747 new_node
.name
= props
['name']
2750 new_node
.parent
= parent
2751 new_node
.location
= node_loc
2753 for str_from
, str_to
in reconnections
:
2754 connect_sockets(eval(str_from
), eval(str_to
))
2756 new_node
.select
= False
2757 success_names
.append(new_node
.name
)
2759 # Reselect all nodes
2760 if selected_node_names
and node_active_is_frame
is False:
2761 for i
in selected_node_names
:
2762 node_tree
.nodes
[i
].select
= True
2764 if active_node_name
is not None:
2765 node_tree
.nodes
[active_node_name
].select
= True
2766 node_tree
.nodes
.active
= node_tree
.nodes
[active_node_name
]
2768 self
.report({'INFO'}, "Successfully reset {}".format(", ".join(success_names
)))
2790 NWAddPrincipledSetup
,
2792 NWLinkActiveToSelected
,
2794 NWSelectParentChildren
,
2800 NWAddMultipleImages
,
2808 from bpy
.utils
import register_class
2814 from bpy
.utils
import unregister_class
2817 unregister_class(cls
)