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 material"""
602 socket
= get_internal_socket(link
.to_socket
)
603 return (socket
and self
.is_socket_used_active_mat(socket
))
605 def is_socket_used_active_mat(self
, socket
):
606 """Ensure used sockets in active material is calculated and check given socket"""
607 if not hasattr(self
, "used_viewer_sockets_active_mat"):
608 self
.used_viewer_sockets_active_mat
= []
609 output_node
= get_group_output_node(bpy
.context
.space_data
.node_tree
,
610 output_node_type
=self
.shader_output_type
)
612 if output_node
is not None:
613 self
.search_sockets(output_node
, self
.used_viewer_sockets_active_mat
)
614 return socket
in self
.used_viewer_sockets_active_mat
616 def is_socket_used_other_mats(self
, socket
):
617 """Ensure used sockets in other materials are calculated and check given socket"""
618 if not hasattr(self
, "used_viewer_sockets_other_mats"):
619 self
.used_viewer_sockets_other_mats
= []
620 for mat
in bpy
.data
.materials
:
621 if mat
.node_tree
== bpy
.context
.space_data
.node_tree
or not hasattr(mat
.node_tree
, "nodes"):
624 output_node
= get_group_output_node(mat
.node_tree
,
625 output_node_type
=self
.shader_output_type
)
626 if output_node
is not None:
627 self
.search_sockets(output_node
, self
.used_viewer_sockets_other_mats
)
628 return socket
in self
.used_viewer_sockets_other_mats
630 def get_output_index(self
, node
, output_node
, is_base_node_tree
, socket_type
, check_type
=False):
631 """Get the next available output socket in the active node"""
634 for i
, out
in enumerate(node
.outputs
):
635 if is_visible_socket(out
) and (not check_type
or out
.type == socket_type
):
636 valid_outputs
.append(i
)
638 out_i
= valid_outputs
[0] # Start index of node's outputs
639 for i
, valid_i
in enumerate(valid_outputs
):
640 for out_link
in node
.outputs
[valid_i
].links
:
641 if is_viewer_link(out_link
, output_node
):
642 if is_base_node_tree
or self
.link_leads_to_used_socket(out_link
):
643 if i
< len(valid_outputs
) - 1:
644 out_i
= valid_outputs
[i
+ 1]
646 out_i
= valid_outputs
[0]
649 def create_links(self
, path
, node
, active_node_socket_id
, socket_type
):
650 """Create links at each step in the node group path."""
651 path
= list(reversed(path
))
652 # Starting from the level of the active node
653 for path_index
, path_element
in enumerate(path
[:-1]):
654 # Ensure there is a viewer node and it has an input
655 tree
= path_element
.node_tree
656 viewer_socket
= self
.ensure_viewer_socket(
658 connect_socket
= node
.outputs
[active_node_socket_id
]
659 if path_index
== 0 else None)
660 if viewer_socket
in self
.delete_sockets
:
661 self
.delete_sockets
.remove(viewer_socket
)
663 # Connect the current to its viewer
664 link_start
= node
.outputs
[active_node_socket_id
]
665 link_end
= self
.ensure_group_output(tree
).inputs
[viewer_socket
.identifier
]
666 connect_sockets(link_start
, link_end
)
668 # Go up in the node group hierarchy
669 next_tree
= path
[path_index
+ 1].node_tree
670 node
= next(n
for n
in next_tree
.nodes
672 and n
.node_tree
== tree
)
674 active_node_socket_id
= viewer_socket
.identifier
675 return node
.outputs
[active_node_socket_id
]
679 for socket
in self
.delete_sockets
:
680 if not self
.is_socket_used_other_mats(socket
):
681 tree
= socket
.id_data
682 self
.remove_socket(tree
, socket
)
684 def invoke(self
, context
, event
):
685 space
= context
.space_data
686 # Ignore operator when running in wrong context.
687 if self
.run_in_geometry_nodes
!= (space
.tree_type
== "GeometryNodeTree"):
688 return {'PASS_THROUGH'}
690 mlocx
= event
.mouse_region_x
691 mlocy
= event
.mouse_region_y
692 select_node
= bpy
.ops
.node
.select(location
=(mlocx
, mlocy
), extend
=False)
693 if 'FINISHED' not in select_node
: # only run if mouse click is on a node
696 base_node_tree
= space
.node_tree
697 active_tree
= context
.space_data
.edit_tree
698 path
= context
.space_data
.path
699 nodes
= active_tree
.nodes
700 active
= nodes
.active
702 if not active
and not any(is_visible_socket(out
) for out
in active
.outputs
):
705 # Scan through all nodes in tree including nodes inside of groups to find viewer sockets
706 self
.delete_sockets
= []
707 self
.scan_nodes(base_node_tree
, self
.delete_sockets
)
709 if not active
.outputs
:
713 # For geometry node trees, we just connect to the group output
714 if space
.tree_type
== "GeometryNodeTree":
715 socket_type
= 'NodeSocketGeometry'
717 # Find (or create if needed) the output of this node tree
718 output_node
= self
.ensure_group_output(base_node_tree
)
720 active_node_socket_index
= self
.get_output_index(
721 active
, output_node
, base_node_tree
== active_tree
, 'GEOMETRY', check_type
=True
723 # If there is no 'GEOMETRY' output type - We can't preview the node
724 if active_node_socket_index
is None:
727 # Find an input socket of the output of type geometry
728 output_node_socket_index
= None
729 for i
, inp
in enumerate(output_node
.inputs
):
730 if inp
.type == 'GEOMETRY':
731 output_node_socket_index
= i
733 if output_node_socket_index
is None:
734 # Create geometry socket
735 geometry_out_socket
= base_node_tree
.interface
.new_socket(
736 'Geometry', in_out
='OUTPUT', socket_type
=socket_type
738 output_node_socket_index
= geometry_out_socket
.index
740 # For shader node trees, we connect to a material output
741 elif space
.tree_type
== "ShaderNodeTree":
742 socket_type
= 'NodeSocketShader'
743 self
.init_shader_variables(space
, space
.shader_type
)
745 # Get or create material_output node
746 output_node
= get_group_output_node(base_node_tree
,
747 output_node_type
=self
.shader_output_type
)
749 output_node
= base_node_tree
.nodes
.new(self
.shader_output_ident
)
750 output_node
.location
= get_output_location(base_node_tree
)
751 output_node
.select
= False
753 active_node_socket_index
= self
.get_output_index(
754 active
, output_node
, base_node_tree
== active_tree
, 'SHADER'
756 if active
.outputs
[active_node_socket_index
].name
== "Volume":
757 output_node_socket_index
= 1
759 output_node_socket_index
= 0
761 # If there are no nested node groups, the link starts at the active node
762 node_output
= active
.outputs
[active_node_socket_index
]
764 # Recursively connect inside nested node groups and get the one from base level
765 node_output
= self
.create_links(path
, active
, active_node_socket_index
, socket_type
)
766 output_node_input
= output_node
.inputs
[output_node_socket_index
]
768 # Connect at base level
769 connect_sockets(node_output
, output_node_input
)
772 nodes
.active
= active
774 force_update(context
)
778 class NWFrameSelected(Operator
, NWBase
):
779 bl_idname
= "node.nw_frame_selected"
780 bl_label
= "Frame Selected"
781 bl_description
= "Add a frame node and parent the selected nodes to it"
782 bl_options
= {'REGISTER', 'UNDO'}
784 label_prop
: StringProperty(
786 description
='The visual name of the frame node',
789 use_custom_color_prop
: BoolProperty(
791 description
="Use custom color for the frame node",
794 color_prop
: FloatVectorProperty(
796 description
="The color of the frame node",
797 default
=(0.604, 0.604, 0.604),
798 min=0, max=1, step
=1, precision
=3,
799 subtype
='COLOR_GAMMA', size
=3
802 def draw(self
, context
):
804 layout
.prop(self
, 'label_prop')
805 layout
.prop(self
, 'use_custom_color_prop')
806 col
= layout
.column()
807 col
.active
= self
.use_custom_color_prop
808 col
.prop(self
, 'color_prop', text
="")
810 def execute(self
, context
):
811 nodes
, links
= get_nodes_links(context
)
815 selected
.append(node
)
817 bpy
.ops
.node
.add_node(type='NodeFrame')
819 frm
.label
= self
.label_prop
820 frm
.use_custom_color
= self
.use_custom_color_prop
821 frm
.color
= self
.color_prop
823 for node
in selected
:
829 class NWReloadImages(Operator
):
830 bl_idname
= "node.nw_reload_images"
831 bl_label
= "Reload Images"
832 bl_description
= "Update all the image nodes to match their files on disk"
835 def poll(cls
, context
):
836 return (nw_check(context
)
837 and nw_check_space_type(cls
, context
, 'ShaderNodeTree', 'CompositorNodeTree',
838 'TextureNodeTree', 'GeometryNodeTree')
839 and context
.active_node
is not None
840 and any(is_visible_socket(out
) for out
in context
.active_node
.outputs
))
842 def execute(self
, context
):
843 nodes
, links
= get_nodes_links(context
)
844 image_types
= ["IMAGE", "TEX_IMAGE", "TEX_ENVIRONMENT", "TEXTURE"]
847 if node
.type in image_types
:
848 if node
.type == "TEXTURE":
849 if node
.texture
: # node has texture assigned
850 if node
.texture
.type in ['IMAGE', 'ENVIRONMENT_MAP']:
851 if node
.texture
.image
: # texture has image assigned
852 node
.texture
.image
.reload()
860 self
.report({'INFO'}, "Reloaded images")
861 print("Reloaded " + str(num_reloaded
) + " images")
862 force_update(context
)
865 self
.report({'WARNING'}, "No images found to reload in this node tree")
869 class NWMergeNodes(Operator
, NWBase
):
870 bl_idname
= "node.nw_merge_nodes"
871 bl_label
= "Merge Nodes"
872 bl_description
= "Merge Selected Nodes"
873 bl_options
= {'REGISTER', 'UNDO'}
877 description
="All possible blend types, boolean operations and math operations",
878 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
],
880 merge_type
: EnumProperty(
882 description
="Type of Merge to be used",
884 ('AUTO', 'Auto', 'Automatic Output Type Detection'),
885 ('SHADER', 'Shader', 'Merge using ADD or MIX Shader'),
886 ('GEOMETRY', 'Geometry', 'Merge using Mesh Boolean or Join Geometry Node'),
887 ('MIX', 'Mix Node', 'Merge using Mix Nodes'),
888 ('MATH', 'Math Node', 'Merge using Math Nodes'),
889 ('ZCOMBINE', 'Z-Combine Node', 'Merge using Z-Combine Nodes'),
890 ('ALPHAOVER', 'Alpha Over Node', 'Merge using Alpha Over Nodes'),
894 # Check if the link connects to a node that is in selected_nodes
895 # If not, then check recursively for each link in the nodes outputs.
896 # If yes, return True. If the recursion stops without finding a node
897 # in selected_nodes, it returns False. The depth is used to prevent
898 # getting stuck in a loop because of an already present cycle.
900 def link_creates_cycle(link
, selected_nodes
, depth
=0) -> bool:
902 # We're stuck in a cycle, but that cycle was already present,
903 # so we return False.
904 # NOTE: The number 255 is arbitrary, but seems to work well.
907 if node
in selected_nodes
:
911 for output
in node
.outputs
:
913 for olink
in output
.links
:
914 if NWMergeNodes
.link_creates_cycle(olink
, selected_nodes
, depth
+ 1):
916 # None of the outputs found a node in selected_nodes, so there is no cycle.
919 # Merge the nodes in `nodes_list` with a node of type `node_name` that has a multi_input socket.
920 # The parameters `socket_indices` gives the indices of the node sockets in the order that they should
921 # be connected. The last one is assumed to be a multi input socket.
922 # For convenience the node is returned.
924 def merge_with_multi_input(nodes_list
, merge_position
, do_hide
, loc_x
, links
, nodes
, node_name
, socket_indices
):
925 # The y-location of the last node
926 loc_y
= nodes_list
[-1][2]
927 if merge_position
== 'CENTER':
928 # Average the y-location
929 for i
in range(len(nodes_list
) - 1):
930 loc_y
+= nodes_list
[i
][2]
931 loc_y
= loc_y
/ len(nodes_list
)
932 new_node
= nodes
.new(node_name
)
933 new_node
.hide
= do_hide
934 new_node
.location
.x
= loc_x
935 new_node
.location
.y
= loc_y
936 selected_nodes
= [nodes
[node_info
[0]] for node_info
in nodes_list
]
938 outputs_for_multi_input
= []
939 for i
, node
in enumerate(selected_nodes
):
941 # Search for the first node which had output links that do not create
942 # a cycle, which we can then reconnect afterwards.
943 if prev_links
== [] and node
.outputs
[0].is_linked
:
945 link
for link
in node
.outputs
[0].links
if not NWMergeNodes
.link_creates_cycle(
946 link
, selected_nodes
)]
947 # Get the index of the socket, the last one is a multi input, and is thus used repeatedly
948 # To get the placement to look right we need to reverse the order in which we connect the
949 # outputs to the multi input socket.
950 if i
< len(socket_indices
) - 1:
951 ind
= socket_indices
[i
]
952 connect_sockets(node
.outputs
[0], new_node
.inputs
[ind
])
954 outputs_for_multi_input
.insert(0, node
.outputs
[0])
955 if outputs_for_multi_input
!= []:
956 ind
= socket_indices
[-1]
957 for output
in outputs_for_multi_input
:
958 connect_sockets(output
, new_node
.inputs
[ind
])
960 for link
in prev_links
:
961 connect_sockets(new_node
.outputs
[0], link
.to_node
.inputs
[0])
965 def poll(cls
, context
):
966 return (nw_check(context
)
967 and nw_check_space_type(cls
, context
, 'ShaderNodeTree', 'CompositorNodeTree',
968 'TextureNodeTree', 'GeometryNodeTree'))
970 def execute(self
, context
):
971 settings
= context
.preferences
.addons
[__package__
].preferences
972 merge_hide
= settings
.merge_hide
973 merge_position
= settings
.merge_position
# 'center' or 'bottom'
976 do_hide_shader
= False
977 if merge_hide
== 'ALWAYS':
979 do_hide_shader
= True
980 elif merge_hide
== 'NON_SHADER':
983 tree_type
= context
.space_data
.node_tree
.type
984 if tree_type
== 'GEOMETRY':
985 node_type
= 'GeometryNode'
986 if tree_type
== 'COMPOSITING':
987 node_type
= 'CompositorNode'
988 elif tree_type
== 'SHADER':
989 node_type
= 'ShaderNode'
990 elif tree_type
== 'TEXTURE':
991 node_type
= 'TextureNode'
992 nodes
, links
= get_nodes_links(context
)
994 merge_type
= self
.merge_type
995 # Prevent trying to add Z-Combine in not 'COMPOSITING' node tree.
996 # 'ZCOMBINE' works only if mode == 'MIX'
997 # Setting mode to None prevents trying to add 'ZCOMBINE' node.
998 if (merge_type
== 'ZCOMBINE' or merge_type
== 'ALPHAOVER') and tree_type
!= 'COMPOSITING':
1001 if (merge_type
!= 'MATH' and merge_type
!= 'GEOMETRY') and tree_type
== 'GEOMETRY':
1003 # The Mix node and math nodes used for geometry nodes are of type 'ShaderNode'
1004 if (merge_type
== 'MATH' or merge_type
== 'MIX') and tree_type
== 'GEOMETRY':
1005 node_type
= 'ShaderNode'
1006 selected_mix
= [] # entry = [index, loc]
1007 selected_shader
= [] # entry = [index, loc]
1008 selected_geometry
= [] # entry = [index, loc]
1009 selected_math
= [] # entry = [index, loc]
1010 selected_vector
= [] # entry = [index, loc]
1011 selected_z
= [] # entry = [index, loc]
1012 selected_alphaover
= [] # entry = [index, loc]
1014 for i
, node
in enumerate(nodes
):
1015 if node
.select
and node
.outputs
:
1016 if merge_type
== 'AUTO':
1017 for (type, types_list
, dst
) in (
1018 ('SHADER', ('MIX', 'ADD'), selected_shader
),
1019 ('GEOMETRY', [t
[0] for t
in geo_combine_operations
], selected_geometry
),
1020 ('RGBA', [t
[0] for t
in blend_types
], selected_mix
),
1021 ('VALUE', [t
[0] for t
in operations
], selected_math
),
1022 ('VECTOR', [], selected_vector
),
1024 output
= get_first_enabled_output(node
)
1025 output_type
= output
.type
1026 valid_mode
= mode
in types_list
1027 # When mode is 'MIX' we have to cheat since the mix node is not used in
1029 if tree_type
== 'GEOMETRY':
1031 if output_type
== 'VALUE' and type == 'VALUE':
1033 elif output_type
== 'VECTOR' and type == 'VECTOR':
1035 elif type == 'GEOMETRY':
1037 # When mode is 'MIX' use mix node for both 'RGBA' and 'VALUE' output types.
1038 # Cheat that output type is 'RGBA',
1039 # and that 'MIX' exists in math operations list.
1040 # This way when selected_mix list is analyzed:
1041 # Node data will be appended even though it doesn't meet requirements.
1042 elif output_type
!= 'SHADER' and mode
== 'MIX':
1043 output_type
= 'RGBA'
1045 if output_type
== type and valid_mode
:
1046 dst
.append([i
, node
.location
.x
, node
.location
.y
, node
.dimensions
.x
, node
.hide
])
1048 for (type, types_list
, dst
) in (
1049 ('SHADER', ('MIX', 'ADD'), selected_shader
),
1050 ('GEOMETRY', [t
[0] for t
in geo_combine_operations
], selected_geometry
),
1051 ('MIX', [t
[0] for t
in blend_types
], selected_mix
),
1052 ('MATH', [t
[0] for t
in operations
], selected_math
),
1053 ('ZCOMBINE', ('MIX', ), selected_z
),
1054 ('ALPHAOVER', ('MIX', ), selected_alphaover
),
1056 if merge_type
== type and mode
in types_list
:
1057 dst
.append([i
, node
.location
.x
, node
.location
.y
, node
.dimensions
.x
, node
.hide
])
1058 # When nodes with output kinds 'RGBA' and 'VALUE' are selected at the same time
1059 # use only 'Mix' nodes for merging.
1060 # For that we add selected_math list to selected_mix list and clear selected_math.
1061 if selected_mix
and selected_math
and merge_type
== 'AUTO':
1062 selected_mix
+= selected_math
1065 # If no nodes are selected, do nothing and pass through.
1066 if not (selected_mix
+ selected_shader
+ selected_geometry
+ selected_math
1067 + selected_vector
+ selected_z
+ selected_alphaover
):
1068 return {'PASS_THROUGH'}
1077 selected_alphaover
]:
1080 count_before
= len(nodes
)
1081 # sort list by loc_x - reversed
1082 nodes_list
.sort(key
=lambda k
: k
[1], reverse
=True)
1084 loc_x
= nodes_list
[0][1] + nodes_list
[0][3] + 70
1085 nodes_list
.sort(key
=lambda k
: k
[2], reverse
=True)
1087 # Change the node type for math nodes in a geometry node tree.
1088 if tree_type
== 'GEOMETRY':
1089 if nodes_list
is selected_math
or nodes_list
is selected_vector
or nodes_list
is selected_mix
:
1090 node_type
= 'ShaderNode'
1094 node_type
= 'GeometryNode'
1095 if merge_position
== 'CENTER':
1096 # average yloc of last two nodes (lowest two)
1097 loc_y
= ((nodes_list
[len(nodes_list
) - 1][2]) + (nodes_list
[len(nodes_list
) - 2][2])) / 2
1098 if nodes_list
[len(nodes_list
) - 1][-1]: # if last node is hidden, mix should be shifted up a bit
1104 loc_y
= nodes_list
[len(nodes_list
) - 1][2]
1108 if nodes_list
== selected_shader
and not do_hide_shader
:
1110 the_range
= len(nodes_list
) - 1
1111 if len(nodes_list
) == 1:
1114 for i
in range(the_range
):
1115 if nodes_list
== selected_mix
:
1117 if tree_type
== 'COMPOSITING':
1119 add_type
= node_type
+ mix_name
1120 add
= nodes
.new(add_type
)
1121 if tree_type
!= 'COMPOSITING':
1122 add
.data_type
= 'RGBA'
1123 add
.blend_type
= mode
1125 add
.inputs
[0].default_value
= 1.0
1126 add
.show_preview
= False
1132 if tree_type
== 'COMPOSITING':
1135 elif nodes_list
== selected_math
:
1136 add_type
= node_type
+ 'Math'
1137 add
= nodes
.new(add_type
)
1138 add
.operation
= mode
1144 elif nodes_list
== selected_shader
:
1146 add_type
= node_type
+ 'MixShader'
1147 add
= nodes
.new(add_type
)
1148 add
.hide
= do_hide_shader
1154 add_type
= node_type
+ 'AddShader'
1155 add
= nodes
.new(add_type
)
1156 add
.hide
= do_hide_shader
1161 elif nodes_list
== selected_geometry
:
1162 if mode
in ('JOIN', 'MIX'):
1163 add_type
= node_type
+ 'JoinGeometry'
1164 add
= self
.merge_with_multi_input(
1165 nodes_list
, merge_position
, do_hide
, loc_x
, links
, nodes
, add_type
, [0])
1167 add_type
= node_type
+ 'MeshBoolean'
1168 indices
= [0, 1] if mode
== 'DIFFERENCE' else [1]
1169 add
= self
.merge_with_multi_input(
1170 nodes_list
, merge_position
, do_hide
, loc_x
, links
, nodes
, add_type
, indices
)
1171 add
.operation
= mode
1174 elif nodes_list
== selected_vector
:
1175 add_type
= node_type
+ 'VectorMath'
1176 add
= nodes
.new(add_type
)
1177 add
.operation
= mode
1183 elif nodes_list
== selected_z
:
1184 add
= nodes
.new('CompositorNodeZcombine')
1185 add
.show_preview
= False
1191 elif nodes_list
== selected_alphaover
:
1192 add
= nodes
.new('CompositorNodeAlphaOver')
1193 add
.show_preview
= False
1199 add
.location
= loc_x
, loc_y
1203 # This has already been handled separately
1207 count_after
= len(nodes
)
1208 index
= count_after
- 1
1209 first_selected
= nodes
[nodes_list
[0][0]]
1210 # "last" node has been added as first, so its index is count_before.
1211 last_add
= nodes
[count_before
]
1212 # Create list of invalid indexes.
1213 invalid_nodes
= [nodes
[n
[0]]
1214 for n
in (selected_mix
+ selected_math
+ selected_shader
+ selected_z
+ selected_geometry
)]
1217 # Two nodes were selected and first selected has no output links, second selected has output links.
1218 # Then add links from last add to all links 'to_socket' of out links of second selected.
1219 first_selected_output
= get_first_enabled_output(first_selected
)
1220 if len(nodes_list
) == 2:
1221 if not first_selected_output
.links
:
1222 second_selected
= nodes
[nodes_list
[1][0]]
1223 for ss_link
in get_first_enabled_output(second_selected
).links
:
1224 # Prevent cyclic dependencies when nodes to be merged are linked to one another.
1225 # Link only if "to_node" index not in invalid indexes list.
1226 if not self
.link_creates_cycle(ss_link
, invalid_nodes
):
1227 connect_sockets(get_first_enabled_output(last_add
), ss_link
.to_socket
)
1228 # add links from last_add to all links 'to_socket' of out links of first selected.
1229 for fs_link
in first_selected_output
.links
:
1230 # Link only if "to_node" index not in invalid indexes list.
1231 if not self
.link_creates_cycle(fs_link
, invalid_nodes
):
1232 connect_sockets(get_first_enabled_output(last_add
), fs_link
.to_socket
)
1233 # add link from "first" selected and "first" add node
1234 node_to
= nodes
[count_after
- 1]
1235 connect_sockets(first_selected_output
, node_to
.inputs
[first
])
1236 if node_to
.type == 'ZCOMBINE':
1237 for fs_out
in first_selected
.outputs
:
1238 if fs_out
!= first_selected_output
and fs_out
.name
in ('Z', 'Depth'):
1239 connect_sockets(fs_out
, node_to
.inputs
[1])
1241 # add links between added ADD nodes and between selected and ADD nodes
1242 for i
in range(count_adds
):
1243 if i
< count_adds
- 1:
1244 node_from
= nodes
[index
]
1245 node_to
= nodes
[index
- 1]
1246 node_to_input_i
= first
1247 node_to_z_i
= 1 # if z combine - link z to first z input
1248 connect_sockets(get_first_enabled_output(node_from
), node_to
.inputs
[node_to_input_i
])
1249 if node_to
.type == 'ZCOMBINE':
1250 for from_out
in node_from
.outputs
:
1251 if from_out
!= get_first_enabled_output(node_from
) and from_out
.name
in ('Z', 'Depth'):
1252 connect_sockets(from_out
, node_to
.inputs
[node_to_z_i
])
1253 if len(nodes_list
) > 1:
1254 node_from
= nodes
[nodes_list
[i
+ 1][0]]
1255 node_to
= nodes
[index
]
1256 node_to_input_i
= second
1257 node_to_z_i
= 3 # if z combine - link z to second z input
1258 connect_sockets(get_first_enabled_output(node_from
), node_to
.inputs
[node_to_input_i
])
1259 if node_to
.type == 'ZCOMBINE':
1260 for from_out
in node_from
.outputs
:
1261 if from_out
!= get_first_enabled_output(node_from
) and from_out
.name
in ('Z', 'Depth'):
1262 connect_sockets(from_out
, node_to
.inputs
[node_to_z_i
])
1264 # set "last" of added nodes as active
1265 nodes
.active
= last_add
1266 for i
, x
, y
, dx
, h
in nodes_list
:
1267 nodes
[i
].select
= False
1272 class NWBatchChangeNodes(Operator
, NWBase
):
1273 bl_idname
= "node.nw_batch_change"
1274 bl_label
= "Batch Change"
1275 bl_description
= "Batch Change Blend Type and Math Operation"
1276 bl_options
= {'REGISTER', 'UNDO'}
1278 blend_type
: EnumProperty(
1280 items
=blend_types
+ navs
,
1282 operation
: EnumProperty(
1284 items
=operations
+ navs
,
1288 def poll(cls
, context
):
1289 return (nw_check(context
)
1290 and nw_check_space_type(cls
, context
, 'ShaderNodeTree', 'CompositorNodeTree',
1291 'TextureNodeTree', 'GeometryNodeTree'))
1293 def execute(self
, context
):
1294 blend_type
= self
.blend_type
1295 operation
= self
.operation
1296 for node
in context
.selected_nodes
:
1297 if node
.type == 'MIX_RGB' or (node
.bl_idname
== 'ShaderNodeMix' and node
.data_type
== 'RGBA'):
1298 if blend_type
not in [nav
[0] for nav
in navs
]:
1299 node
.blend_type
= blend_type
1301 if blend_type
== 'NEXT':
1302 index
= [i
for i
, entry
in enumerate(blend_types
) if node
.blend_type
in entry
][0]
1303 # index = blend_types.index(node.blend_type)
1304 if index
== len(blend_types
) - 1:
1305 node
.blend_type
= blend_types
[0][0]
1307 node
.blend_type
= blend_types
[index
+ 1][0]
1309 if blend_type
== 'PREV':
1310 index
= [i
for i
, entry
in enumerate(blend_types
) if node
.blend_type
in entry
][0]
1312 node
.blend_type
= blend_types
[len(blend_types
) - 1][0]
1314 node
.blend_type
= blend_types
[index
- 1][0]
1316 if node
.type == 'MATH' or node
.bl_idname
== 'ShaderNodeMath':
1317 if operation
not in [nav
[0] for nav
in navs
]:
1318 node
.operation
= operation
1320 if operation
== 'NEXT':
1321 index
= [i
for i
, entry
in enumerate(operations
) if node
.operation
in entry
][0]
1322 # index = operations.index(node.operation)
1323 if index
== len(operations
) - 1:
1324 node
.operation
= operations
[0][0]
1326 node
.operation
= operations
[index
+ 1][0]
1328 if operation
== 'PREV':
1329 index
= [i
for i
, entry
in enumerate(operations
) if node
.operation
in entry
][0]
1330 # index = operations.index(node.operation)
1332 node
.operation
= operations
[len(operations
) - 1][0]
1334 node
.operation
= operations
[index
- 1][0]
1339 class NWChangeMixFactor(Operator
, NWBase
):
1340 bl_idname
= "node.nw_factor"
1341 bl_label
= "Change Factor"
1342 bl_description
= "Change Factors of Mix Nodes and Mix Shader Nodes"
1343 bl_options
= {'REGISTER', 'UNDO'}
1345 # option: Change factor.
1346 # If option is 1.0 or 0.0 - set to 1.0 or 0.0
1347 # Else - change factor by option value.
1348 option
: FloatProperty()
1350 def execute(self
, context
):
1351 nodes
, links
= get_nodes_links(context
)
1352 option
= self
.option
1353 selected
= [] # entry = index
1354 for si
, node
in enumerate(nodes
):
1356 if node
.type in {'MIX_RGB', 'MIX_SHADER'} or node
.bl_idname
== 'ShaderNodeMix':
1360 fac
= nodes
[si
].inputs
[0]
1361 nodes
[si
].hide
= False
1362 if option
in {0.0, 1.0}:
1363 fac
.default_value
= option
1365 fac
.default_value
+= option
1370 class NWCopySettings(Operator
, NWBase
):
1371 bl_idname
= "node.nw_copy_settings"
1372 bl_label
= "Copy Settings"
1373 bl_description
= "Copy Settings of Active Node to Selected Nodes"
1374 bl_options
= {'REGISTER', 'UNDO'}
1377 def poll(cls
, context
):
1378 return (nw_check(context
)
1379 and context
.active_node
is not None
1380 and context
.active_node
.type != 'FRAME')
1382 def execute(self
, context
):
1383 node_active
= context
.active_node
1384 node_selected
= context
.selected_nodes
1387 if not (len(node_selected
) > 1):
1388 self
.report({'ERROR'}, "2 nodes must be selected at least")
1389 return {'CANCELLED'}
1391 # Check if active node is in the selection
1392 selected_node_names
= [n
.name
for n
in node_selected
]
1393 if node_active
.name
not in selected_node_names
:
1394 self
.report({'ERROR'}, "No active node")
1395 return {'CANCELLED'}
1397 # Get nodes in selection by type
1398 valid_nodes
= [n
for n
in node_selected
if n
.type == node_active
.type]
1400 if not (len(valid_nodes
) > 1) and node_active
:
1401 self
.report({'ERROR'}, "Selected nodes are not of the same type as {}".format(node_active
.name
))
1402 return {'CANCELLED'}
1404 if len(valid_nodes
) != len(node_selected
):
1405 # Report nodes that are not valid
1406 valid_node_names
= [n
.name
for n
in valid_nodes
]
1407 not_valid_names
= list(set(selected_node_names
) - set(valid_node_names
))
1410 "Ignored {} (not of the same type as {})".format(
1411 ", ".join(not_valid_names
),
1414 # Reference original
1416 # node_selected_names = [n.name for n in node_selected]
1421 # Deselect all nodes
1422 for i
in node_selected
:
1425 # Code by zeffii from http://blender.stackexchange.com/a/42338/3710
1426 # Run through all other nodes
1427 for node
in valid_nodes
[1:]:
1429 # Check for frame node
1430 parent
= node
.parent
if node
.parent
else None
1431 node_loc
= [node
.location
.x
, node
.location
.y
]
1433 # Select original to duplicate
1436 # Duplicate selected node
1437 bpy
.ops
.node
.duplicate()
1438 new_node
= context
.selected_nodes
[0]
1441 new_node
.select
= False
1443 # Properties to copy
1444 node_tree
= node
.id_data
1445 props_to_copy
= 'bl_idname name location height width'.split(' ')
1449 mappings
= chain
.from_iterable([node
.inputs
, node
.outputs
])
1450 for i
in (i
for i
in mappings
if i
.is_linked
):
1452 reconnections
.append([L
.from_socket
.path_from_id(), L
.to_socket
.path_from_id()])
1455 props
= {j
: getattr(node
, j
) for j
in props_to_copy
}
1456 props_to_copy
.pop(0)
1458 for prop
in props_to_copy
:
1459 setattr(new_node
, prop
, props
[prop
])
1461 # Get the node tree to remove the old node
1462 nodes
= node_tree
.nodes
1464 new_node
.name
= props
['name']
1467 new_node
.parent
= parent
1468 new_node
.location
= node_loc
1470 for str_from
, str_to
in reconnections
:
1471 node_tree
.connect_sockets(eval(str_from
), eval(str_to
))
1473 success_names
.append(new_node
.name
)
1476 node_tree
.nodes
.active
= orig
1479 "Successfully copied attributes from {} to: {}".format(
1481 ", ".join(success_names
)))
1485 class NWCopyLabel(Operator
, NWBase
):
1486 bl_idname
= "node.nw_copy_label"
1487 bl_label
= "Copy Label"
1488 bl_options
= {'REGISTER', 'UNDO'}
1490 option
: EnumProperty(
1492 description
="Source of name of label",
1494 ('FROM_ACTIVE', 'from active', 'from active node',),
1495 ('FROM_NODE', 'from node', 'from node linked to selected node'),
1496 ('FROM_SOCKET', 'from socket', 'from socket linked to selected node'),
1500 def execute(self
, context
):
1501 nodes
, links
= get_nodes_links(context
)
1502 option
= self
.option
1503 active
= nodes
.active
1504 if option
== 'FROM_ACTIVE':
1506 src_label
= active
.label
1507 for node
in [n
for n
in nodes
if n
.select
and nodes
.active
!= n
]:
1508 node
.label
= src_label
1509 elif option
== 'FROM_NODE':
1510 selected
= [n
for n
in nodes
if n
.select
]
1511 for node
in selected
:
1512 for input in node
.inputs
:
1514 src
= input.links
[0].from_node
1515 node
.label
= src
.label
1517 elif option
== 'FROM_SOCKET':
1518 selected
= [n
for n
in nodes
if n
.select
]
1519 for node
in selected
:
1520 for input in node
.inputs
:
1522 src
= input.links
[0].from_socket
1523 node
.label
= src
.name
1529 class NWClearLabel(Operator
, NWBase
):
1530 bl_idname
= "node.nw_clear_label"
1531 bl_label
= "Clear Label"
1532 bl_options
= {'REGISTER', 'UNDO'}
1534 option
: BoolProperty()
1536 def execute(self
, context
):
1537 nodes
, links
= get_nodes_links(context
)
1538 for node
in [n
for n
in nodes
if n
.select
]:
1543 def invoke(self
, context
, event
):
1545 return self
.execute(context
)
1547 return context
.window_manager
.invoke_confirm(self
, event
)
1550 class NWModifyLabels(Operator
, NWBase
):
1551 """Modify Labels of all selected nodes"""
1552 bl_idname
= "node.nw_modify_labels"
1553 bl_label
= "Modify Labels"
1554 bl_options
= {'REGISTER', 'UNDO'}
1556 prepend
: StringProperty(
1557 name
="Add to Beginning"
1559 append
: StringProperty(
1562 replace_from
: StringProperty(
1563 name
="Text to Replace"
1565 replace_to
: StringProperty(
1569 def execute(self
, context
):
1570 nodes
, links
= get_nodes_links(context
)
1571 for node
in [n
for n
in nodes
if n
.select
]:
1572 node
.label
= self
.prepend
+ node
.label
.replace(self
.replace_from
, self
.replace_to
) + self
.append
1576 def invoke(self
, context
, event
):
1580 return context
.window_manager
.invoke_props_dialog(self
)
1583 class NWAddTextureSetup(Operator
, NWBase
):
1584 bl_idname
= "node.nw_add_texture"
1585 bl_label
= "Texture Setup"
1586 bl_description
= "Add Texture Node Setup to Selected Shaders"
1587 bl_options
= {'REGISTER', 'UNDO'}
1589 add_mapping
: BoolProperty(
1590 name
="Add Mapping Nodes",
1591 description
="Create coordinate and mapping nodes for the texture (ignored for selected texture nodes)",
1595 def poll(cls
, context
):
1596 return (nw_check(context
)
1597 and nw_check_space_type(cls
, context
, 'ShaderNodeTree'))
1599 def execute(self
, context
):
1600 nodes
, links
= get_nodes_links(context
)
1602 texture_types
= get_texture_node_types()
1603 selected_nodes
= [n
for n
in nodes
if n
.select
]
1605 for node
in selected_nodes
:
1610 target_input
= node
.inputs
[0]
1611 for input in node
.inputs
:
1614 if not input.is_linked
:
1615 target_input
= input
1618 self
.report({'WARNING'}, "No free inputs for node: " + node
.name
)
1623 locx
= node
.location
.x
1624 locy
= node
.location
.y
- (input_index
* padding
)
1626 is_texture_node
= node
.rna_type
.identifier
in texture_types
1627 use_environment_texture
= node
.type == 'BACKGROUND'
1629 # Add an image texture before normal shader nodes.
1630 if not is_texture_node
:
1631 image_texture_type
= 'ShaderNodeTexEnvironment' if use_environment_texture
else 'ShaderNodeTexImage'
1632 image_texture_node
= nodes
.new(image_texture_type
)
1633 x_offset
= x_offset
+ image_texture_node
.width
+ padding
1634 image_texture_node
.location
= [locx
- x_offset
, locy
]
1635 nodes
.active
= image_texture_node
1636 connect_sockets(image_texture_node
.outputs
[0], target_input
)
1638 # The mapping setup following this will connect to the first input of this image texture.
1639 target_input
= image_texture_node
.inputs
[0]
1643 if is_texture_node
or self
.add_mapping
:
1645 mapping_node
= nodes
.new('ShaderNodeMapping')
1646 x_offset
= x_offset
+ mapping_node
.width
+ padding
1647 mapping_node
.location
= [locx
- x_offset
, locy
]
1648 connect_sockets(mapping_node
.outputs
[0], target_input
)
1650 # Add Texture Coordinates node.
1651 tex_coord_node
= nodes
.new('ShaderNodeTexCoord')
1652 x_offset
= x_offset
+ tex_coord_node
.width
+ padding
1653 tex_coord_node
.location
= [locx
- x_offset
, locy
]
1655 is_procedural_texture
= is_texture_node
and node
.type != 'TEX_IMAGE'
1656 use_generated_coordinates
= is_procedural_texture
or use_environment_texture
1657 tex_coord_output
= tex_coord_node
.outputs
[0 if use_generated_coordinates
else 2]
1658 connect_sockets(tex_coord_output
, mapping_node
.inputs
[0])
1663 class NWAddPrincipledSetup(Operator
, NWBase
, ImportHelper
):
1664 bl_idname
= "node.nw_add_textures_for_principled"
1665 bl_label
= "Principled Texture Setup"
1666 bl_description
= "Add Texture Node Setup for Principled BSDF"
1667 bl_options
= {'REGISTER', 'UNDO'}
1669 directory
: StringProperty(
1673 description
='Folder to search in for image files'
1675 files
: CollectionProperty(
1676 type=bpy
.types
.OperatorFileListElement
,
1677 options
={'HIDDEN', 'SKIP_SAVE'}
1680 relative_path
: BoolProperty(
1681 name
='Relative Path',
1682 description
='Set the file path relative to the blend file, when possible',
1691 def draw(self
, context
):
1692 layout
= self
.layout
1693 layout
.alignment
= 'LEFT'
1695 layout
.prop(self
, 'relative_path')
1698 def poll(cls
, context
):
1699 return (nw_check(context
)
1700 and nw_check_space_type(cls
, context
, 'ShaderNodeTree'))
1702 def execute(self
, context
):
1703 # Check if everything is ok
1704 if not self
.directory
:
1705 self
.report({'INFO'}, 'No Folder Selected')
1706 return {'CANCELLED'}
1707 if not self
.files
[:]:
1708 self
.report({'INFO'}, 'No Files Selected')
1709 return {'CANCELLED'}
1711 nodes
, links
= get_nodes_links(context
)
1712 active_node
= nodes
.active
1713 if not (active_node
and active_node
.bl_idname
== 'ShaderNodeBsdfPrincipled'):
1714 self
.report({'INFO'}, 'Select Principled BSDF')
1715 return {'CANCELLED'}
1717 # Filter textures names for texturetypes in filenames
1718 # [Socket Name, [abbreviations and keyword list], Filename placeholder]
1719 tags
= context
.preferences
.addons
[__package__
].preferences
.principled_tags
1720 normal_abbr
= tags
.normal
.split(' ')
1721 bump_abbr
= tags
.bump
.split(' ')
1722 gloss_abbr
= tags
.gloss
.split(' ')
1723 rough_abbr
= tags
.rough
.split(' ')
1725 ['Displacement', tags
.displacement
.split(' '), None],
1726 ['Base Color', tags
.base_color
.split(' '), None],
1727 ['Metallic', tags
.metallic
.split(' '), None],
1728 ['Specular IOR Level', tags
.specular
.split(' '), None],
1729 ['Roughness', rough_abbr
+ gloss_abbr
, None],
1730 ['Normal', normal_abbr
+ bump_abbr
, None],
1731 ['Transmission Weight', tags
.transmission
.split(' '), None],
1732 ['Emission Color', tags
.emission
.split(' '), None],
1733 ['Alpha', tags
.alpha
.split(' '), None],
1734 ['Ambient Occlusion', tags
.ambient_occlusion
.split(' '), None],
1737 match_files_to_socket_names(self
.files
, socketnames
)
1738 # Remove socketnames without found files
1739 socketnames
= [s
for s
in socketnames
if s
[2]
1740 and path
.exists(self
.directory
+ s
[2])]
1742 self
.report({'INFO'}, 'No matching images found')
1743 print('No matching images found')
1744 return {'CANCELLED'}
1746 # Don't override path earlier as os.path is used to check the absolute path
1747 import_path
= self
.directory
1748 if self
.relative_path
:
1749 if bpy
.data
.filepath
:
1751 import_path
= bpy
.path
.relpath(self
.directory
)
1756 print('\nMatched Textures:')
1761 roughness_node
= None
1762 for i
, sname
in enumerate(socketnames
):
1763 print(i
, sname
[0], sname
[2])
1765 # DISPLACEMENT NODES
1766 if sname
[0] == 'Displacement':
1767 disp_texture
= nodes
.new(type='ShaderNodeTexImage')
1768 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
1769 disp_texture
.image
= img
1770 disp_texture
.label
= 'Displacement'
1771 if disp_texture
.image
:
1772 disp_texture
.image
.colorspace_settings
.is_data
= True
1774 # Add displacement offset nodes
1775 disp_node
= nodes
.new(type='ShaderNodeDisplacement')
1776 # Align the Displacement node under the active Principled BSDF node
1777 disp_node
.location
= active_node
.location
+ Vector((100, -700))
1778 link
= connect_sockets(disp_node
.inputs
[0], disp_texture
.outputs
[0])
1780 # TODO Turn on true displacement in the material
1781 # Too complicated for now
1784 output_node
= [n
for n
in nodes
if n
.bl_idname
== 'ShaderNodeOutputMaterial']
1786 if not output_node
[0].inputs
[2].is_linked
:
1787 link
= connect_sockets(output_node
[0].inputs
[2], disp_node
.outputs
[0])
1791 # AMBIENT OCCLUSION TEXTURE
1792 if sname
[0] == 'Ambient Occlusion':
1793 ao_texture
= nodes
.new(type='ShaderNodeTexImage')
1794 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
1795 ao_texture
.image
= img
1796 ao_texture
.label
= sname
[0]
1797 if ao_texture
.image
:
1798 ao_texture
.image
.colorspace_settings
.is_data
= True
1802 if not active_node
.inputs
[sname
[0]].is_linked
:
1803 # No texture node connected -> add texture node with new image
1804 texture_node
= nodes
.new(type='ShaderNodeTexImage')
1805 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
1806 texture_node
.image
= img
1809 if sname
[0] == 'Normal':
1810 # Test if new texture node is normal or bump map
1811 fname_components
= split_into_components(sname
[2])
1812 match_normal
= set(normal_abbr
).intersection(set(fname_components
))
1813 match_bump
= set(bump_abbr
).intersection(set(fname_components
))
1815 # If Normal add normal node in between
1816 normal_node
= nodes
.new(type='ShaderNodeNormalMap')
1817 link
= connect_sockets(normal_node
.inputs
[1], texture_node
.outputs
[0])
1819 # If Bump add bump node in between
1820 normal_node
= nodes
.new(type='ShaderNodeBump')
1821 link
= connect_sockets(normal_node
.inputs
[2], texture_node
.outputs
[0])
1823 link
= connect_sockets(active_node
.inputs
[sname
[0]], normal_node
.outputs
[0])
1824 normal_node_texture
= texture_node
1826 elif sname
[0] == 'Roughness':
1827 # Test if glossy or roughness map
1828 fname_components
= split_into_components(sname
[2])
1829 match_rough
= set(rough_abbr
).intersection(set(fname_components
))
1830 match_gloss
= set(gloss_abbr
).intersection(set(fname_components
))
1833 # If Roughness nothing to to
1834 link
= connect_sockets(active_node
.inputs
[sname
[0]], texture_node
.outputs
[0])
1837 # If Gloss Map add invert node
1838 invert_node
= nodes
.new(type='ShaderNodeInvert')
1839 link
= connect_sockets(invert_node
.inputs
[1], texture_node
.outputs
[0])
1841 link
= connect_sockets(active_node
.inputs
[sname
[0]], invert_node
.outputs
[0])
1842 roughness_node
= texture_node
1845 # This is a simple connection Texture --> Input slot
1846 link
= connect_sockets(active_node
.inputs
[sname
[0]], texture_node
.outputs
[0])
1848 # Use non-color except for color inputs
1849 if sname
[0] not in ['Base Color', 'Emission Color'] and texture_node
.image
:
1850 texture_node
.image
.colorspace_settings
.is_data
= True
1853 # If already texture connected. add to node list for alignment
1854 texture_node
= active_node
.inputs
[sname
[0]].links
[0].from_node
1856 # This are all connected texture nodes
1857 texture_nodes
.append(texture_node
)
1858 texture_node
.label
= sname
[0]
1861 texture_nodes
.append(disp_texture
)
1864 # We want the ambient occlusion texture to be the top most texture node
1865 texture_nodes
.insert(0, ao_texture
)
1868 for i
, texture_node
in enumerate(texture_nodes
):
1869 offset
= Vector((-550, (i
* -280) + 200))
1870 texture_node
.location
= active_node
.location
+ offset
1873 # Extra alignment if normal node was added
1874 normal_node
.location
= normal_node_texture
.location
+ Vector((300, 0))
1877 # Alignment of invert node if glossy map
1878 invert_node
.location
= roughness_node
.location
+ Vector((300, 0))
1880 # Add texture input + mapping
1881 mapping
= nodes
.new(type='ShaderNodeMapping')
1882 mapping
.location
= active_node
.location
+ Vector((-1050, 0))
1883 if len(texture_nodes
) > 1:
1884 # If more than one texture add reroute node in between
1885 reroute
= nodes
.new(type='NodeReroute')
1886 texture_nodes
.append(reroute
)
1887 tex_coords
= Vector((texture_nodes
[0].location
.x
,
1888 sum(n
.location
.y
for n
in texture_nodes
) / len(texture_nodes
)))
1889 reroute
.location
= tex_coords
+ Vector((-50, -120))
1890 for texture_node
in texture_nodes
:
1891 link
= connect_sockets(texture_node
.inputs
[0], reroute
.outputs
[0])
1892 link
= connect_sockets(reroute
.inputs
[0], mapping
.outputs
[0])
1894 link
= connect_sockets(texture_nodes
[0].inputs
[0], mapping
.outputs
[0])
1896 # Connect texture_coordiantes to mapping node
1897 texture_input
= nodes
.new(type='ShaderNodeTexCoord')
1898 texture_input
.location
= mapping
.location
+ Vector((-200, 0))
1899 link
= connect_sockets(mapping
.inputs
[0], texture_input
.outputs
[2])
1901 # Create frame around tex coords and mapping
1902 frame
= nodes
.new(type='NodeFrame')
1903 frame
.label
= 'Mapping'
1904 mapping
.parent
= frame
1905 texture_input
.parent
= frame
1908 # Create frame around texture nodes
1909 frame
= nodes
.new(type='NodeFrame')
1910 frame
.label
= 'Textures'
1911 for tnode
in texture_nodes
:
1912 tnode
.parent
= frame
1916 active_node
.select
= False
1919 force_update(context
)
1923 class NWAddReroutes(Operator
, NWBase
):
1924 """Add Reroute Nodes and link them to outputs of selected nodes"""
1925 bl_idname
= "node.nw_add_reroutes"
1926 bl_label
= "Add Reroutes"
1927 bl_description
= "Add Reroutes to Outputs"
1928 bl_options
= {'REGISTER', 'UNDO'}
1930 option
: EnumProperty(
1933 ('ALL', 'to all', 'Add to all outputs'),
1934 ('LOOSE', 'to loose', 'Add only to loose outputs'),
1935 ('LINKED', 'to linked', 'Add only to linked outputs'),
1939 def execute(self
, context
):
1940 tree_type
= context
.space_data
.node_tree
.type
1941 option
= self
.option
1942 nodes
, links
= get_nodes_links(context
)
1943 # output valid when option is 'all' or when 'loose' output has no links
1945 post_select
= [] # nodes to be selected after execution
1946 # create reroutes and recreate links
1947 for node
in [n
for n
in nodes
if n
.select
]:
1952 # unhide 'REROUTE' nodes to avoid issues with location.y
1953 if node
.type == 'REROUTE':
1955 # Hack needed to calculate real width
1957 bpy
.ops
.node
.select_all(action
='DESELECT')
1958 helper
= nodes
.new('NodeReroute')
1959 helper
.select
= True
1961 # resize node and helper to zero. Then check locations to calculate width
1962 bpy
.ops
.transform
.resize(value
=(0.0, 0.0, 0.0))
1963 width
= 2.0 * (helper
.location
.x
- node
.location
.x
)
1964 # restore node location
1965 node
.location
= x
, y
1968 # only helper is selected now
1969 bpy
.ops
.node
.delete()
1970 x
= node
.location
.x
+ width
+ 20.0
1971 if node
.type != 'REROUTE':
1975 reroutes_count
= 0 # will be used when aligning reroutes added to hidden nodes
1976 for out_i
, output
in enumerate(node
.outputs
):
1977 pass_used
= False # initial value to be analyzed if 'R_LAYERS'
1978 # if node != 'R_LAYERS' - "pass_used" not needed, so set it to True
1979 if node
.type != 'R_LAYERS':
1981 else: # if 'R_LAYERS' check if output represent used render pass
1982 node_scene
= node
.scene
1983 node_layer
= node
.layer
1984 # If output - "Alpha" is analyzed - assume it's used. Not represented in passes.
1985 if output
.name
== 'Alpha':
1988 # check entries in global 'rl_outputs' variable
1989 for rlo
in rl_outputs
:
1990 if output
.name
in {rlo
.output_name
, rlo
.exr_output_name
}:
1991 pass_used
= getattr(node_scene
.view_layers
[node_layer
], rlo
.render_pass
)
1994 valid
= ((option
== 'ALL') or
1995 (option
== 'LOOSE' and not output
.links
) or
1996 (option
== 'LINKED' and output
.links
))
1997 # Add reroutes only if valid, but offset location in all cases.
1999 n
= nodes
.new('NodeReroute')
2001 for link
in output
.links
:
2002 connect_sockets(n
.outputs
[0], link
.to_socket
)
2003 connect_sockets(output
, n
.inputs
[0])
2005 post_select
.append(n
)
2009 # disselect the node so that after execution of script only newly created nodes are selected
2011 # nicer reroutes distribution along y when node.hide
2013 y_translate
= reroutes_count
* y_offset
/ 2.0 - y_offset
- 35.0
2014 for reroute
in [r
for r
in nodes
if r
.select
]:
2015 reroute
.location
.y
-= y_translate
2016 for node
in post_select
:
2022 class NWLinkActiveToSelected(Operator
, NWBase
):
2023 """Link active node to selected nodes basing on various criteria"""
2024 bl_idname
= "node.nw_link_active_to_selected"
2025 bl_label
= "Link Active Node to Selected"
2026 bl_options
= {'REGISTER', 'UNDO'}
2028 replace
: BoolProperty()
2029 use_node_name
: BoolProperty()
2030 use_outputs_names
: BoolProperty()
2033 def poll(cls
, context
):
2034 return (nw_check(context
)
2035 and context
.active_node
is not None
2036 and context
.active_node
.select
)
2038 def execute(self
, context
):
2039 nodes
, links
= get_nodes_links(context
)
2040 replace
= self
.replace
2041 use_node_name
= self
.use_node_name
2042 use_outputs_names
= self
.use_outputs_names
2043 active
= nodes
.active
2044 selected
= [node
for node
in nodes
if node
.select
and node
!= active
]
2045 outputs
= [] # Only usable outputs of active nodes will be stored here.
2046 for out
in active
.outputs
:
2047 if active
.type != 'R_LAYERS':
2050 # 'R_LAYERS' node type needs special handling.
2051 # outputs of 'R_LAYERS' are callable even if not seen in UI.
2052 # Only outputs that represent used passes should be taken into account
2053 # Check if pass represented by output is used.
2054 # global 'rl_outputs' list will be used for that
2055 for rlo
in rl_outputs
:
2056 pass_used
= False # initial value. Will be set to True if pass is used
2057 if out
.name
== 'Alpha':
2058 # Alpha output is always present. Doesn't have representation in render pass. Assume it's used.
2060 elif out
.name
in {rlo
.output_name
, rlo
.exr_output_name
}:
2061 # example 'render_pass' entry: 'use_pass_uv' Check if True in scene render layers
2062 pass_used
= getattr(active
.scene
.view_layers
[active
.layer
], rlo
.render_pass
)
2066 doit
= True # Will be changed to False when links successfully added to previous output.
2069 for node
in selected
:
2070 dst_name
= node
.name
# Will be compared with src_name if needed.
2071 # When node has label - use it as dst_name
2073 dst_name
= node
.label
2074 valid
= True # Initial value. Will be changed to False if names don't match.
2075 src_name
= dst_name
# If names not used - this assignment will keep valid = True.
2077 # Set src_name to source node name or label
2078 src_name
= active
.name
2080 src_name
= active
.label
2081 elif use_outputs_names
:
2082 src_name
= (out
.name
, )
2083 for rlo
in rl_outputs
:
2084 if out
.name
in {rlo
.output_name
, rlo
.exr_output_name
}:
2085 src_name
= (rlo
.output_name
, rlo
.exr_output_name
)
2086 if dst_name
not in src_name
:
2089 for input in node
.inputs
:
2090 if input.type == out
.type or node
.type == 'REROUTE':
2091 if replace
or not input.is_linked
:
2092 connect_sockets(out
, input)
2093 if not use_node_name
and not use_outputs_names
:
2100 class NWAlignNodes(Operator
, NWBase
):
2101 '''Align the selected nodes neatly in a row/column'''
2102 bl_idname
= "node.nw_align_nodes"
2103 bl_label
= "Align Nodes"
2104 bl_options
= {'REGISTER', 'UNDO'}
2105 margin
: IntProperty(name
='Margin', default
=50, description
='The amount of space between nodes')
2107 def execute(self
, context
):
2108 nodes
, links
= get_nodes_links(context
)
2109 margin
= self
.margin
2113 if node
.select
and node
.type != 'FRAME':
2114 selection
.append(node
)
2116 # If no nodes are selected, align all nodes
2120 elif nodes
.active
in selection
:
2121 active_loc
= copy(nodes
.active
.location
) # make a copy, not a reference
2123 # Check if nodes should be laid out horizontally or vertically
2124 # use dimension to get center of node, not corner
2125 x_locs
= [n
.location
.x
+ (n
.dimensions
.x
/ 2) for n
in selection
]
2126 y_locs
= [n
.location
.y
- (n
.dimensions
.y
/ 2) for n
in selection
]
2127 x_range
= max(x_locs
) - min(x_locs
)
2128 y_range
= max(y_locs
) - min(y_locs
)
2129 mid_x
= (max(x_locs
) + min(x_locs
)) / 2
2130 mid_y
= (max(y_locs
) + min(y_locs
)) / 2
2131 horizontal
= x_range
> y_range
2133 # Sort selection by location of node mid-point
2135 selection
= sorted(selection
, key
=lambda n
: n
.location
.x
+ (n
.dimensions
.x
/ 2))
2137 selection
= sorted(selection
, key
=lambda n
: n
.location
.y
- (n
.dimensions
.y
/ 2), reverse
=True)
2141 for node
in selection
:
2142 current_margin
= margin
2143 current_margin
= current_margin
* 0.5 if node
.hide
else current_margin
# use a smaller margin for hidden nodes
2146 node
.location
.x
= current_pos
2147 current_pos
+= current_margin
+ node
.dimensions
.x
2148 node
.location
.y
= mid_y
+ (node
.dimensions
.y
/ 2)
2150 node
.location
.y
= current_pos
2151 current_pos
-= (current_margin
* 0.3) + node
.dimensions
.y
# use half-margin for vertical alignment
2152 node
.location
.x
= mid_x
- (node
.dimensions
.x
/ 2)
2154 # If active node is selected, center nodes around it
2155 if active_loc
is not None:
2156 active_loc_diff
= active_loc
- nodes
.active
.location
2157 for node
in selection
:
2158 node
.location
+= active_loc_diff
2159 else: # Position nodes centered around where they used to be
2160 locs
= ([n
.location
.x
+ (n
.dimensions
.x
/ 2) for n
in selection
]
2161 ) if horizontal
else ([n
.location
.y
- (n
.dimensions
.y
/ 2) for n
in selection
])
2162 new_mid
= (max(locs
) + min(locs
)) / 2
2163 for node
in selection
:
2165 node
.location
.x
+= (mid_x
- new_mid
)
2167 node
.location
.y
+= (mid_y
- new_mid
)
2172 class NWSelectParentChildren(Operator
, NWBase
):
2173 bl_idname
= "node.nw_select_parent_child"
2174 bl_label
= "Select Parent or Children"
2175 bl_options
= {'REGISTER', 'UNDO'}
2177 option
: EnumProperty(
2180 ('PARENT', 'Select Parent', 'Select Parent Frame'),
2181 ('CHILD', 'Select Children', 'Select members of selected frame'),
2185 def execute(self
, context
):
2186 nodes
, links
= get_nodes_links(context
)
2187 option
= self
.option
2188 selected
= [node
for node
in nodes
if node
.select
]
2189 if option
== 'PARENT':
2190 for sel
in selected
:
2193 parent
.select
= True
2194 else: # option == 'CHILD'
2195 for sel
in selected
:
2196 children
= [node
for node
in nodes
if node
.parent
== sel
]
2197 for kid
in children
:
2203 class NWDetachOutputs(Operator
, NWBase
):
2204 """Detach outputs of selected node leaving inputs linked"""
2205 bl_idname
= "node.nw_detach_outputs"
2206 bl_label
= "Detach Outputs"
2207 bl_options
= {'REGISTER', 'UNDO'}
2209 def execute(self
, context
):
2210 nodes
, links
= get_nodes_links(context
)
2211 selected
= context
.selected_nodes
2212 bpy
.ops
.node
.duplicate_move_keep_inputs()
2213 new_nodes
= context
.selected_nodes
2214 bpy
.ops
.node
.select_all(action
="DESELECT")
2215 for node
in selected
:
2217 bpy
.ops
.node
.delete_reconnect()
2218 for new_node
in new_nodes
:
2219 new_node
.select
= True
2220 bpy
.ops
.transform
.translate('INVOKE_DEFAULT')
2225 class NWLinkToOutputNode(Operator
):
2226 """Link to Composite node or Material Output node"""
2227 bl_idname
= "node.nw_link_out"
2228 bl_label
= "Connect to Output"
2229 bl_options
= {'REGISTER', 'UNDO'}
2232 def poll(cls
, context
):
2233 """Disabled for custom nodes as we do not know which nodes are outputs."""
2234 return (nw_check(context
)
2235 and nw_check_space_type(cls
, context
, 'ShaderNodeTree', 'CompositorNodeTree',
2236 'TextureNodeTree', 'GeometryNodeTree')
2237 and context
.active_node
is not None
2238 and any(is_visible_socket(out
) for out
in context
.active_node
.outputs
))
2240 def execute(self
, context
):
2241 nodes
, links
= get_nodes_links(context
)
2242 active
= nodes
.active
2244 tree_type
= context
.space_data
.tree_type
2245 shader_outputs
= {'OBJECT': 'ShaderNodeOutputMaterial',
2246 'WORLD': 'ShaderNodeOutputWorld',
2247 'LINESTYLE': 'ShaderNodeOutputLineStyle'}
2249 'ShaderNodeTree': shader_outputs
[context
.space_data
.shader_type
],
2250 'CompositorNodeTree': 'CompositorNodeComposite',
2251 'TextureNodeTree': 'TextureNodeOutput',
2252 'GeometryNodeTree': 'NodeGroupOutput',
2255 # check whether the node is an output node and,
2256 # if supported, whether it's the active one
2257 if node
.rna_type
.identifier
== output_type \
2258 and (node
.is_active_output
if hasattr(node
, 'is_active_output')
2262 else: # No output node exists
2263 bpy
.ops
.node
.select_all(action
="DESELECT")
2264 output_node
= nodes
.new(output_type
)
2265 output_node
.location
.x
= active
.location
.x
+ active
.dimensions
.x
+ 80
2266 output_node
.location
.y
= active
.location
.y
2269 for i
, output
in enumerate(active
.outputs
):
2270 if is_visible_socket(output
):
2273 for i
, output
in enumerate(active
.outputs
):
2274 if output
.type == output_node
.inputs
[0].type and is_visible_socket(output
):
2279 if tree_type
== 'ShaderNodeTree':
2280 if active
.outputs
[output_index
].name
== 'Volume':
2282 elif active
.outputs
[output_index
].name
== 'Displacement':
2284 elif tree_type
== 'GeometryNodeTree':
2285 if active
.outputs
[output_index
].type != 'GEOMETRY':
2286 return {'CANCELLED'}
2287 connect_sockets(active
.outputs
[output_index
], output_node
.inputs
[out_input_index
])
2289 force_update(context
) # viewport render does not update
2294 class NWMakeLink(Operator
, NWBase
):
2295 """Make a link from one socket to another"""
2296 bl_idname
= 'node.nw_make_link'
2297 bl_label
= 'Make Link'
2298 bl_options
= {'REGISTER', 'UNDO'}
2299 from_socket
: IntProperty()
2300 to_socket
: IntProperty()
2302 def execute(self
, context
):
2303 nodes
, links
= get_nodes_links(context
)
2305 n1
= nodes
[context
.scene
.NWLazySource
]
2306 n2
= nodes
[context
.scene
.NWLazyTarget
]
2308 connect_sockets(n1
.outputs
[self
.from_socket
], n2
.inputs
[self
.to_socket
])
2310 force_update(context
)
2315 class NWCallInputsMenu(Operator
, NWBase
):
2316 """Link from this output"""
2317 bl_idname
= 'node.nw_call_inputs_menu'
2318 bl_label
= 'Make Link'
2319 bl_options
= {'REGISTER', 'UNDO'}
2320 from_socket
: IntProperty()
2322 def execute(self
, context
):
2323 nodes
, links
= get_nodes_links(context
)
2325 context
.scene
.NWSourceSocket
= self
.from_socket
2327 n1
= nodes
[context
.scene
.NWLazySource
]
2328 n2
= nodes
[context
.scene
.NWLazyTarget
]
2329 if len(n2
.inputs
) > 1:
2330 bpy
.ops
.wm
.call_menu("INVOKE_DEFAULT", name
=NWConnectionListInputs
.bl_idname
)
2331 elif len(n2
.inputs
) == 1:
2332 connect_sockets(n1
.outputs
[self
.from_socket
], n2
.inputs
[0])
2336 class NWAddSequence(Operator
, NWBase
, ImportHelper
):
2337 """Add an Image Sequence"""
2338 bl_idname
= 'node.nw_add_sequence'
2339 bl_label
= 'Import Image Sequence'
2340 bl_options
= {'REGISTER', 'UNDO'}
2342 directory
: StringProperty(
2345 filename
: StringProperty(
2348 files
: CollectionProperty(
2349 type=bpy
.types
.OperatorFileListElement
,
2350 options
={'HIDDEN', 'SKIP_SAVE'}
2352 relative_path
: BoolProperty(
2353 name
='Relative Path',
2354 description
='Set the file path relative to the blend file, when possible',
2358 def draw(self
, context
):
2359 layout
= self
.layout
2360 layout
.alignment
= 'LEFT'
2362 layout
.prop(self
, 'relative_path')
2364 def execute(self
, context
):
2365 nodes
, links
= get_nodes_links(context
)
2366 directory
= self
.directory
2367 filename
= self
.filename
2369 tree
= context
.space_data
.node_tree
2372 # print ("\nDIR:", directory)
2373 # print ("FN:", filename)
2374 # print ("Fs:", list(f.name for f in files), '\n')
2376 if tree
.type == 'SHADER':
2377 node_type
= "ShaderNodeTexImage"
2378 elif tree
.type == 'COMPOSITING':
2379 node_type
= "CompositorNodeImage"
2381 self
.report({'ERROR'}, "Unsupported Node Tree type!")
2382 return {'CANCELLED'}
2384 if not files
[0].name
and not filename
:
2385 self
.report({'ERROR'}, "No file chosen")
2386 return {'CANCELLED'}
2387 elif files
[0].name
and (not filename
or not path
.exists(directory
+ filename
)):
2388 # User has selected multiple files without an active one, or the active one is non-existent
2389 filename
= files
[0].name
2391 if not path
.exists(directory
+ filename
):
2392 self
.report({'ERROR'}, filename
+ " does not exist!")
2393 return {'CANCELLED'}
2395 without_ext
= '.'.join(filename
.split('.')[:-1])
2397 # if last digit isn't a number, it's not a sequence
2398 if not without_ext
[-1].isdigit():
2399 self
.report({'ERROR'}, filename
+ " does not seem to be part of a sequence")
2400 return {'CANCELLED'}
2402 extension
= filename
.split('.')[-1]
2403 reverse
= without_ext
[::-1] # reverse string
2406 for char
in reverse
:
2412 without_num
= without_ext
[:count_numbers
* -1]
2414 files
= sorted(glob(directory
+ without_num
+ "[0-9]" * count_numbers
+ "." + extension
))
2416 num_frames
= len(files
)
2418 nodes_list
= [node
for node
in nodes
]
2420 nodes_list
.sort(key
=lambda k
: k
.location
.x
)
2421 xloc
= nodes_list
[0].location
.x
- 220 # place new nodes at far left
2425 yloc
+= node_mid_pt(node
, 'y')
2426 yloc
= yloc
/ len(nodes
)
2431 name_with_hashes
= without_num
+ "#" * count_numbers
+ '.' + extension
2433 bpy
.ops
.node
.add_node('INVOKE_DEFAULT', use_transform
=True, type=node_type
)
2435 node
.label
= name_with_hashes
2437 filepath
= directory
+ (without_ext
+ '.' + extension
)
2438 if self
.relative_path
:
2439 if bpy
.data
.filepath
:
2441 filepath
= bpy
.path
.relpath(filepath
)
2445 img
= bpy
.data
.images
.load(filepath
)
2446 img
.source
= 'SEQUENCE'
2447 img
.name
= name_with_hashes
2449 image_user
= node
.image_user
if tree
.type == 'SHADER' else node
2450 # separate the number from the file name of the first file
2451 image_user
.frame_offset
= int(files
[0][len(without_num
) + len(directory
):-1 * (len(extension
) + 1)]) - 1
2452 image_user
.frame_duration
= num_frames
2457 class NWAddMultipleImages(Operator
, NWBase
, ImportHelper
):
2458 """Add multiple images at once"""
2459 bl_idname
= 'node.nw_add_multiple_images'
2460 bl_label
= 'Open Selected Images'
2461 bl_options
= {'REGISTER', 'UNDO'}
2462 directory
: StringProperty(
2465 files
: CollectionProperty(
2466 type=bpy
.types
.OperatorFileListElement
,
2467 options
={'HIDDEN', 'SKIP_SAVE'}
2470 def execute(self
, context
):
2471 nodes
, links
= get_nodes_links(context
)
2473 xloc
, yloc
= context
.region
.view2d
.region_to_view(context
.area
.width
/ 2, context
.area
.height
/ 2)
2475 if context
.space_data
.node_tree
.type == 'SHADER':
2476 node_type
= "ShaderNodeTexImage"
2477 elif context
.space_data
.node_tree
.type == 'COMPOSITING':
2478 node_type
= "CompositorNodeImage"
2480 self
.report({'ERROR'}, "Unsupported Node Tree type!")
2481 return {'CANCELLED'}
2484 for f
in self
.files
:
2487 node
= nodes
.new(node_type
)
2488 new_nodes
.append(node
)
2491 node
.location
.x
= xloc
2492 node
.location
.y
= yloc
2495 img
= bpy
.data
.images
.load(self
.directory
+ fname
)
2498 # shift new nodes up to center of tree
2499 list_size
= new_nodes
[0].location
.y
- new_nodes
[-1].location
.y
2501 if node
in new_nodes
:
2503 node
.location
.y
+= (list_size
/ 2)
2509 class NWViewerFocus(bpy
.types
.Operator
):
2510 """Set the viewer tile center to the mouse position"""
2511 bl_idname
= "node.nw_viewer_focus"
2512 bl_label
= "Viewer Focus"
2514 x
: bpy
.props
.IntProperty()
2515 y
: bpy
.props
.IntProperty()
2518 def poll(cls
, context
):
2519 return (nw_check(context
)
2520 and nw_check_space_type(cls
, context
, 'CompositorNodeTree'))
2522 def execute(self
, context
):
2525 def invoke(self
, context
, event
):
2526 render
= context
.scene
.render
2527 space
= context
.space_data
2528 percent
= render
.resolution_percentage
* 0.01
2530 nodes
, links
= get_nodes_links(context
)
2531 viewers
= [n
for n
in nodes
if n
.type == 'VIEWER']
2534 mlocx
= event
.mouse_region_x
2535 mlocy
= event
.mouse_region_y
2536 select_node
= bpy
.ops
.node
.select(location
=(mlocx
, mlocy
), extend
=False)
2538 if 'FINISHED' not in select_node
: # only run if we're not clicking on a node
2539 region_x
= context
.region
.width
2540 region_y
= context
.region
.height
2542 region_center_x
= context
.region
.width
/ 2
2543 region_center_y
= context
.region
.height
/ 2
2545 bd_x
= render
.resolution_x
* percent
* space
.backdrop_zoom
2546 bd_y
= render
.resolution_y
* percent
* space
.backdrop_zoom
2548 backdrop_center_x
= (bd_x
/ 2) - space
.backdrop_offset
[0]
2549 backdrop_center_y
= (bd_y
/ 2) - space
.backdrop_offset
[1]
2551 margin_x
= region_center_x
- backdrop_center_x
2552 margin_y
= region_center_y
- backdrop_center_y
2554 abs_mouse_x
= (mlocx
- margin_x
) / bd_x
2555 abs_mouse_y
= (mlocy
- margin_y
) / bd_y
2557 for node
in viewers
:
2558 node
.center_x
= abs_mouse_x
2559 node
.center_y
= abs_mouse_y
2561 return {'PASS_THROUGH'}
2563 return self
.execute(context
)
2566 class NWSaveViewer(bpy
.types
.Operator
, ExportHelper
):
2567 """Save the current viewer node to an image file"""
2568 bl_idname
= "node.nw_save_viewer"
2569 bl_label
= "Save This Image"
2570 filepath
: StringProperty(subtype
="FILE_PATH")
2571 filename_ext
: EnumProperty(
2573 description
="Choose the file format to save to",
2574 items
=(('.bmp', "BMP", ""),
2575 ('.rgb', 'IRIS', ""),
2576 ('.png', 'PNG', ""),
2577 ('.jpg', 'JPEG', ""),
2578 ('.jp2', 'JPEG2000', ""),
2579 ('.tga', 'TARGA', ""),
2580 ('.cin', 'CINEON', ""),
2581 ('.dpx', 'DPX', ""),
2582 ('.exr', 'OPEN_EXR', ""),
2583 ('.hdr', 'HDR', ""),
2584 ('.tif', 'TIFF', "")),
2589 def poll(cls
, context
):
2590 return (nw_check(context
)
2591 and nw_check_space_type(cls
, context
, 'CompositorNodeTree')
2592 and any(img
.source
== 'VIEWER'
2593 and img
.render_slots
== 0
2594 for img
in bpy
.data
.images
)
2595 and sum(bpy
.data
.images
["Viewer Node"].size
) > 0) # False if not connected or connected but no image
2597 def execute(self
, context
):
2614 basename
, ext
= path
.splitext(fp
)
2615 old_render_format
= context
.scene
.render
.image_settings
.file_format
2616 context
.scene
.render
.image_settings
.file_format
= formats
[self
.filename_ext
]
2617 context
.area
.type = "IMAGE_EDITOR"
2618 context
.area
.spaces
[0].image
= bpy
.data
.images
['Viewer Node']
2619 context
.area
.spaces
[0].image
.save_render(fp
)
2620 context
.area
.type = "NODE_EDITOR"
2621 context
.scene
.render
.image_settings
.file_format
= old_render_format
2625 class NWResetNodes(bpy
.types
.Operator
):
2626 """Reset Nodes in Selection"""
2627 bl_idname
= "node.nw_reset_nodes"
2628 bl_label
= "Reset Nodes"
2629 bl_options
= {'REGISTER', 'UNDO'}
2632 def poll(cls
, context
):
2633 space
= context
.space_data
2634 return space
.type == 'NODE_EDITOR'
2636 def execute(self
, context
):
2637 node_active
= context
.active_node
2638 node_selected
= context
.selected_nodes
2639 node_ignore
= ["FRAME", "REROUTE", "GROUP", "SIMULATION_INPUT", "SIMULATION_OUTPUT"]
2641 # Check if one node is selected at least
2642 if not (len(node_selected
) > 0):
2643 self
.report({'ERROR'}, "1 node must be selected at least")
2644 return {'CANCELLED'}
2646 active_node_name
= node_active
.name
if node_active
.select
else None
2647 valid_nodes
= [n
for n
in node_selected
if n
.type not in node_ignore
]
2649 # Create output lists
2650 selected_node_names
= [n
.name
for n
in node_selected
]
2653 # Reset all valid children in a frame
2654 node_active_is_frame
= False
2655 if len(node_selected
) == 1 and node_active
.type == "FRAME":
2656 node_tree
= node_active
.id_data
2657 children
= [n
for n
in node_tree
.nodes
if n
.parent
== node_active
]
2659 valid_nodes
= [n
for n
in children
if n
.type not in node_ignore
]
2660 selected_node_names
= [n
.name
for n
in children
if n
.type not in node_ignore
]
2661 node_active_is_frame
= True
2663 # Check if valid nodes in selection
2664 if not (len(valid_nodes
) > 0):
2665 # Check for frames only
2666 frames_selected
= [n
for n
in node_selected
if n
.type == "FRAME"]
2667 if (len(frames_selected
) > 1 and len(frames_selected
) == len(node_selected
)):
2668 self
.report({'ERROR'}, "Please select only 1 frame to reset")
2670 self
.report({'ERROR'}, "No valid node(s) in selection")
2671 return {'CANCELLED'}
2673 # Report nodes that are not valid
2674 if len(valid_nodes
) != len(node_selected
) and node_active_is_frame
is False:
2675 valid_node_names
= [n
.name
for n
in valid_nodes
]
2676 not_valid_names
= list(set(selected_node_names
) - set(valid_node_names
))
2677 self
.report({'INFO'}, "Ignored {}".format(", ".join(not_valid_names
)))
2679 # Deselect all nodes
2680 for i
in node_selected
:
2683 # Run through all valid nodes
2684 for node
in valid_nodes
:
2686 parent
= node
.parent
if node
.parent
else None
2687 node_loc
= [node
.location
.x
, node
.location
.y
]
2689 node_tree
= node
.id_data
2690 props_to_copy
= 'bl_idname name location height width'.split(' ')
2693 mappings
= chain
.from_iterable([node
.inputs
, node
.outputs
])
2694 for i
in (i
for i
in mappings
if i
.is_linked
):
2696 reconnections
.append([L
.from_socket
.path_from_id(), L
.to_socket
.path_from_id()])
2698 props
= {j
: getattr(node
, j
) for j
in props_to_copy
}
2700 new_node
= node_tree
.nodes
.new(props
['bl_idname'])
2701 props_to_copy
.pop(0)
2703 for prop
in props_to_copy
:
2704 setattr(new_node
, prop
, props
[prop
])
2706 nodes
= node_tree
.nodes
2708 new_node
.name
= props
['name']
2711 new_node
.parent
= parent
2712 new_node
.location
= node_loc
2714 for str_from
, str_to
in reconnections
:
2715 connect_sockets(eval(str_from
), eval(str_to
))
2717 new_node
.select
= False
2718 success_names
.append(new_node
.name
)
2720 # Reselect all nodes
2721 if selected_node_names
and node_active_is_frame
is False:
2722 for i
in selected_node_names
:
2723 node_tree
.nodes
[i
].select
= True
2725 if active_node_name
is not None:
2726 node_tree
.nodes
[active_node_name
].select
= True
2727 node_tree
.nodes
.active
= node_tree
.nodes
[active_node_name
]
2729 self
.report({'INFO'}, "Successfully reset {}".format(", ".join(success_names
)))
2751 NWAddPrincipledSetup
,
2753 NWLinkActiveToSelected
,
2755 NWSelectParentChildren
,
2761 NWAddMultipleImages
,
2769 from bpy
.utils
import register_class
2775 from bpy
.utils
import unregister_class
2778 unregister_class(cls
)