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_active_tree
, get_nodes_links
, is_viewer_socket
,
31 is_viewer_link
, get_group_output_node
, get_output_location
, force_update
, get_internal_socket
,
32 nw_check
, nw_check_space_type
, NWBase
, get_first_enabled_output
, is_visible_socket
,
35 class NWLazyMix(Operator
, NWBase
):
36 """Add a Mix RGB/Shader node by interactively drawing lines between nodes"""
37 bl_idname
= "node.nw_lazy_mix"
38 bl_label
= "Mix Nodes"
39 bl_options
= {'REGISTER', 'UNDO'}
41 def modal(self
, context
, event
):
42 context
.area
.tag_redraw()
43 nodes
, links
= get_nodes_links(context
)
46 start_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
49 if not context
.scene
.NWBusyDrawing
:
50 node1
= node_at_pos(nodes
, context
, event
)
52 context
.scene
.NWBusyDrawing
= node1
.name
54 if context
.scene
.NWBusyDrawing
!= 'STOP':
55 node1
= nodes
[context
.scene
.NWBusyDrawing
]
57 context
.scene
.NWLazySource
= node1
.name
58 context
.scene
.NWLazyTarget
= node_at_pos(nodes
, context
, event
).name
60 if event
.type == 'MOUSEMOVE':
61 self
.mouse_path
.append((event
.mouse_region_x
, event
.mouse_region_y
))
63 elif event
.type == 'RIGHTMOUSE' and event
.value
== 'RELEASE':
64 end_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
65 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
68 node2
= node_at_pos(nodes
, context
, event
)
70 context
.scene
.NWBusyDrawing
= node2
.name
82 bpy
.ops
.node
.nw_merge_nodes(mode
="MIX", merge_type
="AUTO")
84 context
.scene
.NWBusyDrawing
= ""
87 elif event
.type == 'ESC':
89 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
92 return {'RUNNING_MODAL'}
94 def invoke(self
, context
, event
):
95 if context
.area
.type == 'NODE_EDITOR':
96 # the arguments we pass the the callback
97 args
= (self
, context
, 'MIX')
98 # Add the region OpenGL drawing callback
99 # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
100 self
._handle
= bpy
.types
.SpaceNodeEditor
.draw_handler_add(
101 draw_callback_nodeoutline
, args
, 'WINDOW', 'POST_PIXEL')
105 context
.window_manager
.modal_handler_add(self
)
106 return {'RUNNING_MODAL'}
108 self
.report({'WARNING'}, "View3D not found, cannot run operator")
112 class NWLazyConnect(Operator
, NWBase
):
113 """Connect two nodes without clicking a specific socket (automatically determined"""
114 bl_idname
= "node.nw_lazy_connect"
115 bl_label
= "Lazy Connect"
116 bl_options
= {'REGISTER', 'UNDO'}
117 with_menu
: BoolProperty()
119 def modal(self
, context
, event
):
120 context
.area
.tag_redraw()
121 nodes
, links
= get_nodes_links(context
)
124 start_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
127 if not context
.scene
.NWBusyDrawing
:
128 node1
= node_at_pos(nodes
, context
, event
)
130 context
.scene
.NWBusyDrawing
= node1
.name
132 if context
.scene
.NWBusyDrawing
!= 'STOP':
133 node1
= nodes
[context
.scene
.NWBusyDrawing
]
135 context
.scene
.NWLazySource
= node1
.name
136 context
.scene
.NWLazyTarget
= node_at_pos(nodes
, context
, event
).name
138 if event
.type == 'MOUSEMOVE':
139 self
.mouse_path
.append((event
.mouse_region_x
, event
.mouse_region_y
))
141 elif event
.type == 'RIGHTMOUSE' and event
.value
== 'RELEASE':
142 end_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
143 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
146 node2
= node_at_pos(nodes
, context
, event
)
148 context
.scene
.NWBusyDrawing
= node2
.name
161 original_sel
.append(node
)
163 original_unsel
.append(node
)
167 # link_success = autolink(node1, node2, links)
169 if len(node1
.outputs
) > 1 and node2
.inputs
:
170 bpy
.ops
.wm
.call_menu("INVOKE_DEFAULT", name
=NWConnectionListOutputs
.bl_idname
)
171 elif len(node1
.outputs
) == 1:
172 bpy
.ops
.node
.nw_call_inputs_menu(from_socket
=0)
174 link_success
= autolink(node1
, node2
, links
)
176 for node
in original_sel
:
178 for node
in original_unsel
:
182 force_update(context
)
183 context
.scene
.NWBusyDrawing
= ""
186 elif event
.type == 'ESC':
187 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
190 return {'RUNNING_MODAL'}
192 def invoke(self
, context
, event
):
193 if context
.area
.type == 'NODE_EDITOR':
194 nodes
, links
= get_nodes_links(context
)
195 node
= node_at_pos(nodes
, context
, event
)
197 context
.scene
.NWBusyDrawing
= node
.name
199 # the arguments we pass the the callback
203 args
= (self
, context
, mode
)
204 # Add the region OpenGL drawing callback
205 # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
206 self
._handle
= bpy
.types
.SpaceNodeEditor
.draw_handler_add(
207 draw_callback_nodeoutline
, args
, 'WINDOW', 'POST_PIXEL')
211 context
.window_manager
.modal_handler_add(self
)
212 return {'RUNNING_MODAL'}
214 self
.report({'WARNING'}, "View3D not found, cannot run operator")
218 class NWDeleteUnused(Operator
, NWBase
):
219 """Delete all nodes whose output is not used"""
220 bl_idname
= 'node.nw_del_unused'
221 bl_label
= 'Delete Unused Nodes'
222 bl_options
= {'REGISTER', 'UNDO'}
224 delete_muted
: BoolProperty(
226 description
="Delete (but reconnect, like Ctrl-X) all muted nodes",
228 delete_frames
: BoolProperty(
229 name
="Delete Empty Frames",
230 description
="Delete all frames that have no nodes inside them",
233 def is_unused_node(self
, node
):
234 end_types
= ['OUTPUT_MATERIAL', 'OUTPUT', 'VIEWER', 'COMPOSITE',
235 'SPLITVIEWER', 'OUTPUT_FILE', 'LEVELS', 'OUTPUT_LIGHT',
236 'OUTPUT_WORLD', 'GROUP_INPUT', 'GROUP_OUTPUT', 'FRAME']
237 if node
.type in end_types
:
240 for output
in node
.outputs
:
246 def poll(cls
, context
):
247 """Disabled for custom nodes as we do not know which nodes are supported."""
248 return (nw_check(context
)
249 and nw_check_space_type(cls
, context
, 'ShaderNodeTree', 'CompositorNodeTree',
250 'TextureNodeTree', 'GeometryNodeTree')
251 and context
.space_data
.node_tree
.nodes
)
253 def execute(self
, context
):
254 nodes
, links
= get_nodes_links(context
)
260 selection
.append(node
.name
)
266 temp_deleted_nodes
= []
267 del_unused_iterations
= len(nodes
)
268 for it
in range(0, del_unused_iterations
):
269 temp_deleted_nodes
= list(deleted_nodes
) # keep record of last iteration
271 if self
.is_unused_node(node
):
273 deleted_nodes
.append(node
.name
)
274 bpy
.ops
.node
.delete()
276 if temp_deleted_nodes
== deleted_nodes
: # stop iterations when there are no more nodes to be deleted
279 if self
.delete_frames
:
287 frames_in_use
.append(node
.parent
)
289 if node
.type == 'FRAME' and node
not in frames_in_use
:
292 repeat
= True # repeat for nested frames
294 if node
not in frames_in_use
:
296 deleted_nodes
.append(node
.name
)
297 bpy
.ops
.node
.delete()
299 if self
.delete_muted
:
303 deleted_nodes
.append(node
.name
)
304 bpy
.ops
.node
.delete_reconnect()
306 # get unique list of deleted nodes (iterations would count the same node more than once)
307 deleted_nodes
= list(set(deleted_nodes
))
308 for n
in deleted_nodes
:
309 self
.report({'INFO'}, "Node " + n
+ " deleted")
310 num_deleted
= len(deleted_nodes
)
315 self
.report({'INFO'}, "Deleted " + str(num_deleted
) + n
)
317 self
.report({'INFO'}, "Nothing deleted")
320 nodes
, links
= get_nodes_links(context
)
322 if node
.name
in selection
:
326 def invoke(self
, context
, event
):
327 return context
.window_manager
.invoke_confirm(self
, event
)
330 class NWSwapLinks(Operator
, NWBase
):
331 """Swap the output connections of the two selected nodes, or two similar inputs of a single node"""
332 bl_idname
= 'node.nw_swap_links'
333 bl_label
= 'Swap Links'
334 bl_options
= {'REGISTER', 'UNDO'}
337 def poll(cls
, context
):
338 return nw_check(context
) and context
.selected_nodes
and len(context
.selected_nodes
) <= 2
340 def execute(self
, context
):
341 nodes
, links
= get_nodes_links(context
)
342 selected_nodes
= context
.selected_nodes
343 n1
= selected_nodes
[0]
346 if len(selected_nodes
) == 2:
347 n2
= selected_nodes
[1]
348 if n1
.outputs
and n2
.outputs
:
353 for output
in n1
.outputs
:
355 for link
in output
.links
:
356 n1_outputs
.append([out_index
, link
.to_socket
])
361 for output
in n2
.outputs
:
363 for link
in output
.links
:
364 n2_outputs
.append([out_index
, link
.to_socket
])
368 for connection
in n1_outputs
:
370 connect_sockets(n2
.outputs
[connection
[0]], connection
[1])
372 self
.report({'WARNING'},
373 "Some connections have been lost due to differing numbers of output sockets")
374 for connection
in n2_outputs
:
376 connect_sockets(n1
.outputs
[connection
[0]], connection
[1])
378 self
.report({'WARNING'},
379 "Some connections have been lost due to differing numbers of output sockets")
381 if n1
.outputs
or n2
.outputs
:
382 self
.report({'WARNING'}, "One of the nodes has no outputs!")
384 self
.report({'WARNING'}, "Neither of the nodes have outputs!")
387 elif len(selected_nodes
) == 1:
388 if n1
.inputs
and n1
.inputs
[0].is_multi_input
:
389 self
.report({'WARNING'}, "Can't swap inputs of a multi input socket!")
395 if i1
.is_linked
and not i1
.is_multi_input
:
398 if i1
.type == i2
.type and i2
.is_linked
and not i2
.is_multi_input
:
400 types
.append([i1
, similar_types
, i
])
402 types
.sort(key
=lambda k
: k
[1], reverse
=True)
408 if t
[0].type == i2
.type == t
[0].type and t
[0] != i2
and i2
.is_linked
:
410 i1f
= pair
[0].links
[0].from_socket
411 i1t
= pair
[0].links
[0].to_socket
412 i2f
= pair
[1].links
[0].from_socket
413 i2t
= pair
[1].links
[0].to_socket
414 connect_sockets(i1f
, i2t
)
415 connect_sockets(i2f
, i1t
)
418 fs
= t
[0].links
[0].from_socket
420 links
.remove(t
[0].links
[0])
421 if i
+ 1 == len(n1
.inputs
):
424 while n1
.inputs
[i
].is_linked
:
426 connect_sockets(fs
, n1
.inputs
[i
])
427 elif len(types
) == 2:
428 i1f
= types
[0][0].links
[0].from_socket
429 i1t
= types
[0][0].links
[0].to_socket
430 i2f
= types
[1][0].links
[0].from_socket
431 i2t
= types
[1][0].links
[0].to_socket
432 connect_sockets(i1f
, i2t
)
433 connect_sockets(i2f
, i1t
)
436 self
.report({'WARNING'}, "This node has no input connections to swap!")
438 self
.report({'WARNING'}, "This node has no inputs to swap!")
440 force_update(context
)
444 class NWResetBG(Operator
, NWBase
):
445 """Reset the zoom and position of the background image"""
446 bl_idname
= 'node.nw_bg_reset'
447 bl_label
= 'Reset Backdrop'
448 bl_options
= {'REGISTER', 'UNDO'}
451 def poll(cls
, context
):
452 return (nw_check(context
)
453 and nw_check_space_type(cls
, context
, 'CompositorNodeTree'))
455 def execute(self
, context
):
456 context
.space_data
.backdrop_zoom
= 1
457 context
.space_data
.backdrop_offset
[0] = 0
458 context
.space_data
.backdrop_offset
[1] = 0
462 class NWAddAttrNode(Operator
, NWBase
):
463 """Add an Attribute node with this name"""
464 bl_idname
= 'node.nw_add_attr_node'
465 bl_label
= 'Add UV map'
466 bl_options
= {'REGISTER', 'UNDO'}
468 attr_name
: StringProperty()
471 def poll(cls
, context
):
472 return (nw_check(context
)
473 and nw_check_space_type(cls
, context
, 'ShaderNodeTree'))
475 def execute(self
, context
):
476 bpy
.ops
.node
.add_node('INVOKE_DEFAULT', use_transform
=True, type="ShaderNodeAttribute")
477 nodes
, links
= get_nodes_links(context
)
478 nodes
.active
.attribute_name
= self
.attr_name
482 class NWPreviewNode(Operator
, NWBase
):
483 bl_idname
= "node.nw_preview_node"
484 bl_label
= "Preview Node"
485 bl_description
= "Connect active node to the Node Group output or the Material Output"
486 bl_options
= {'REGISTER', 'UNDO'}
488 # If false, the operator is not executed if the current node group happens to be a geometry nodes group.
489 # This is needed because geometry nodes has its own viewer node that uses the same shortcut as in the compositor.
490 run_in_geometry_nodes
: BoolProperty(default
=True)
493 self
.shader_output_type
= ""
494 self
.shader_output_ident
= ""
497 def poll(cls
, context
):
498 """Already implemented natively for compositing nodes."""
499 return (nw_check(context
)
500 and nw_check_space_type(cls
, context
, 'ShaderNodeTree', 'GeometryNodeTree')
501 and (not context
.active_node
502 or context
.active_node
.type not in {"OUTPUT_MATERIAL", "OUTPUT_WORLD"}))
505 def get_output_sockets(node_tree
):
506 return [item
for item
in node_tree
.interface
.items_tree
507 if item
.item_type
== 'SOCKET' and item
.in_out
in {'OUTPUT', 'BOTH'}]
509 def ensure_viewer_socket(self
, node
, socket_type
, connect_socket
=None):
510 """Check if a viewer output already exists in a node group, otherwise create it"""
511 if not hasattr(node
, "node_tree"):
515 output_sockets
= self
.get_output_sockets(node
.node_tree
)
516 if len(output_sockets
):
517 for i
, socket
in enumerate(output_sockets
):
518 if is_viewer_socket(socket
) and socket
.socket_type
== socket_type
:
519 # If viewer output is already used but leads to the same socket we can still use it
520 is_used
= self
.is_socket_used_other_mats(socket
)
522 if connect_socket
is None:
524 groupout
= get_group_output_node(node
.node_tree
)
525 groupout_input
= groupout
.inputs
[i
]
526 links
= groupout_input
.links
527 if connect_socket
not in [link
.from_socket
for link
in links
]:
529 viewer_socket
= socket
532 if viewer_socket
is None:
533 # Create viewer socket
534 viewer_socket
= node
.node_tree
.interface
.new_socket(
535 viewer_socket_name
, in_out
='OUTPUT', socket_type
=socket_type
)
536 viewer_socket
.NWViewerSocket
= True
539 def init_shader_variables(self
, space
, shader_type
):
540 if shader_type
== 'OBJECT':
541 if space
.id in bpy
.data
.lights
.values():
542 self
.shader_output_type
= "OUTPUT_LIGHT"
543 self
.shader_output_ident
= "ShaderNodeOutputLight"
545 self
.shader_output_type
= "OUTPUT_MATERIAL"
546 self
.shader_output_ident
= "ShaderNodeOutputMaterial"
548 elif shader_type
== 'WORLD':
549 self
.shader_output_type
= "OUTPUT_WORLD"
550 self
.shader_output_ident
= "ShaderNodeOutputWorld"
553 def ensure_group_output(tree
):
554 """Check if a group output node exists, otherwise create it"""
555 groupout
= get_group_output_node(tree
)
557 groupout
= tree
.nodes
.new('NodeGroupOutput')
558 loc_x
, loc_y
= get_output_location(tree
)
559 groupout
.location
.x
= loc_x
560 groupout
.location
.y
= loc_y
561 groupout
.select
= False
562 # So that we don't keep on adding new group outputs
563 groupout
.is_active_output
= True
567 def search_sockets(cls
, node
, sockets
, index
=None):
568 """Recursively scan nodes for viewer sockets and store them in a list"""
569 for i
, input_socket
in enumerate(node
.inputs
):
570 if index
and i
!= index
:
572 if len(input_socket
.links
):
573 link
= input_socket
.links
[0]
574 next_node
= link
.from_node
575 external_socket
= link
.from_socket
576 if hasattr(next_node
, "node_tree"):
577 for socket_index
, socket
in enumerate(next_node
.node_tree
.interface
.items_tree
):
578 if socket
.identifier
== external_socket
.identifier
:
580 if is_viewer_socket(socket
) and socket
not in sockets
:
581 sockets
.append(socket
)
582 # continue search inside of node group but restrict socket to where we came from
583 groupout
= get_group_output_node(next_node
.node_tree
)
584 cls
.search_sockets(groupout
, sockets
, index
=socket_index
)
587 def scan_nodes(cls
, tree
, sockets
):
588 """Recursively get all viewer sockets in a material tree"""
589 for node
in tree
.nodes
:
590 if hasattr(node
, "node_tree"):
591 if node
.node_tree
is None:
593 for socket
in cls
.get_output_sockets(node
.node_tree
):
594 if is_viewer_socket(socket
) and (socket
not in sockets
):
595 sockets
.append(socket
)
596 cls
.scan_nodes(node
.node_tree
, sockets
)
599 def remove_socket(tree
, socket
):
600 interface
= tree
.interface
601 interface
.remove(socket
)
602 interface
.active_index
= min(interface
.active_index
, len(interface
.items_tree
) - 1)
604 def link_leads_to_used_socket(self
, link
):
605 """Return True if link leads to a socket that is already used in this material"""
606 socket
= get_internal_socket(link
.to_socket
)
607 return (socket
and self
.is_socket_used_active_mat(socket
))
609 def is_socket_used_active_mat(self
, socket
):
610 """Ensure used sockets in active material is calculated and check given socket"""
611 if not hasattr(self
, "used_viewer_sockets_active_mat"):
612 self
.used_viewer_sockets_active_mat
= []
613 output_node
= get_group_output_node(bpy
.context
.space_data
.node_tree
,
614 output_node_type
=self
.shader_output_type
)
616 if output_node
is not None:
617 self
.search_sockets(output_node
, self
.used_viewer_sockets_active_mat
)
618 return socket
in self
.used_viewer_sockets_active_mat
620 def is_socket_used_other_mats(self
, socket
):
621 """Ensure used sockets in other materials are calculated and check given socket"""
622 if not hasattr(self
, "used_viewer_sockets_other_mats"):
623 self
.used_viewer_sockets_other_mats
= []
624 for mat
in bpy
.data
.materials
:
625 if mat
.node_tree
== bpy
.context
.space_data
.node_tree
or not hasattr(mat
.node_tree
, "nodes"):
628 output_node
= get_group_output_node(mat
.node_tree
,
629 output_node_type
=self
.shader_output_type
)
630 if output_node
is not None:
631 self
.search_sockets(output_node
, self
.used_viewer_sockets_other_mats
)
632 return socket
in self
.used_viewer_sockets_other_mats
634 def get_output_index(self
, base_node_tree
, nodes
, output_node
, socket_type
, check_type
=False):
635 """Get the next available output socket in the active node"""
638 for i
, out
in enumerate(nodes
.active
.outputs
):
639 if is_visible_socket(out
) and (not check_type
or out
.type == socket_type
):
640 valid_outputs
.append(i
)
642 out_i
= valid_outputs
[0] # Start index of node's outputs
643 for i
, valid_i
in enumerate(valid_outputs
):
644 for out_link
in nodes
.active
.outputs
[valid_i
].links
:
645 if is_viewer_link(out_link
, output_node
):
646 if nodes
== base_node_tree
.nodes
or self
.link_leads_to_used_socket(out_link
):
647 if i
< len(valid_outputs
) - 1:
648 out_i
= valid_outputs
[i
+ 1]
650 out_i
= valid_outputs
[0]
653 def create_links(self
, tree
, link_end
, active
, out_i
, socket_type
):
654 """Create links through node groups until we reach the active node"""
655 while tree
.nodes
.active
!= active
:
656 node
= tree
.nodes
.active
657 viewer_socket
= self
.ensure_viewer_socket(
658 node
, socket_type
, connect_socket
=active
.outputs
[out_i
] if node
.node_tree
.nodes
.active
== active
else None)
659 link_start
= node
.outputs
[viewer_socket
.identifier
]
660 if viewer_socket
in self
.delete_sockets
:
661 self
.delete_sockets
.remove(viewer_socket
)
662 connect_sockets(link_start
, link_end
)
664 link_end
= self
.ensure_group_output(node
.node_tree
).inputs
[viewer_socket
.identifier
]
665 tree
= tree
.nodes
.active
.node_tree
666 connect_sockets(active
.outputs
[out_i
], link_end
)
668 def invoke(self
, context
, event
):
669 space
= context
.space_data
670 # Ignore operator when running in wrong context.
671 if self
.run_in_geometry_nodes
!= (space
.tree_type
== "GeometryNodeTree"):
672 return {'PASS_THROUGH'}
674 mlocx
= event
.mouse_region_x
675 mlocy
= event
.mouse_region_y
676 select_node
= bpy
.ops
.node
.select(location
=(mlocx
, mlocy
), extend
=False)
677 if 'FINISHED' not in select_node
: # only run if mouse click is on a node
680 active_tree
, path_to_tree
= get_active_tree(context
)
681 nodes
, links
= active_tree
.nodes
, active_tree
.links
682 base_node_tree
= space
.node_tree
683 active
= nodes
.active
685 if not active
and not any(is_visible_socket(out
) for out
in active
.outputs
):
688 # Scan through all nodes in tree including nodes inside of groups to find viewer sockets
689 self
.delete_sockets
= []
690 self
.scan_nodes(base_node_tree
, self
.delete_sockets
)
692 # For geometry node trees we just connect to the group output
693 if space
.tree_type
== "GeometryNodeTree" and active
.outputs
:
694 socket_type
= 'GEOMETRY'
696 # Find (or create if needed) the output of this node tree
697 output_node
= self
.ensure_group_output(base_node_tree
)
699 out_i
= self
.get_output_index(base_node_tree
, nodes
, output_node
, 'GEOMETRY', check_type
=True)
700 # If there is no 'GEOMETRY' output type - We can't preview the node
704 # Find an input socket of the output of type geometry
705 geometry_out_index
= None
706 for i
, inp
in enumerate(output_node
.inputs
):
707 if inp
.type == socket_type
:
708 geometry_out_index
= i
710 if geometry_out_index
is None:
711 # Create geometry socket
712 geometry_out_socket
= base_node_tree
.interface
.new_socket(
713 'Geometry', in_out
='OUTPUT', socket_type
='NodeSocketGeometry'
715 geometry_out_index
= geometry_out_socket
.index
717 output_socket
= output_node
.inputs
[geometry_out_index
]
719 self
.create_links(base_node_tree
, output_socket
, active
, out_i
, 'NodeSocketGeometry')
721 # What follows is code for the shader editor
722 elif space
.tree_type
== "ShaderNodeTree" and active
.outputs
:
723 shader_type
= space
.shader_type
724 self
.init_shader_variables(space
, shader_type
)
725 socket_type
= 'NodeSocketShader'
727 # Get or create material_output node
728 output_node
= get_group_output_node(base_node_tree
,
729 output_node_type
=self
.shader_output_type
)
731 output_node
= base_node_tree
.nodes
.new(self
.shader_output_ident
)
732 output_node
.location
= get_output_location(base_node_tree
)
733 output_node
.select
= False
735 out_i
= self
.get_output_index(base_node_tree
, nodes
, output_node
, 'SHADER')
737 materialout_index
= 1 if active
.outputs
[out_i
].name
== "Volume" else 0
738 output_socket
= output_node
.inputs
[materialout_index
]
740 self
.create_links(base_node_tree
, output_socket
, active
, out_i
, 'NodeSocketShader')
743 for socket
in self
.delete_sockets
:
744 if not self
.is_socket_used_other_mats(socket
):
745 tree
= socket
.id_data
746 self
.remove_socket(tree
, socket
)
748 nodes
.active
= active
750 force_update(context
)
754 class NWFrameSelected(Operator
, NWBase
):
755 bl_idname
= "node.nw_frame_selected"
756 bl_label
= "Frame Selected"
757 bl_description
= "Add a frame node and parent the selected nodes to it"
758 bl_options
= {'REGISTER', 'UNDO'}
760 label_prop
: StringProperty(
762 description
='The visual name of the frame node',
765 use_custom_color_prop
: BoolProperty(
767 description
="Use custom color for the frame node",
770 color_prop
: FloatVectorProperty(
772 description
="The color of the frame node",
773 default
=(0.604, 0.604, 0.604),
774 min=0, max=1, step
=1, precision
=3,
775 subtype
='COLOR_GAMMA', size
=3
778 def draw(self
, context
):
780 layout
.prop(self
, 'label_prop')
781 layout
.prop(self
, 'use_custom_color_prop')
782 col
= layout
.column()
783 col
.active
= self
.use_custom_color_prop
784 col
.prop(self
, 'color_prop', text
="")
786 def execute(self
, context
):
787 nodes
, links
= get_nodes_links(context
)
791 selected
.append(node
)
793 bpy
.ops
.node
.add_node(type='NodeFrame')
795 frm
.label
= self
.label_prop
796 frm
.use_custom_color
= self
.use_custom_color_prop
797 frm
.color
= self
.color_prop
799 for node
in selected
:
805 class NWReloadImages(Operator
):
806 bl_idname
= "node.nw_reload_images"
807 bl_label
= "Reload Images"
808 bl_description
= "Update all the image nodes to match their files on disk"
811 def poll(cls
, context
):
812 return (nw_check(context
)
813 and nw_check_space_type(cls
, context
, 'ShaderNodeTree', 'CompositorNodeTree',
814 'TextureNodeTree', 'GeometryNodeTree')
815 and context
.active_node
is not None
816 and any(is_visible_socket(out
) for out
in context
.active_node
.outputs
))
818 def execute(self
, context
):
819 nodes
, links
= get_nodes_links(context
)
820 image_types
= ["IMAGE", "TEX_IMAGE", "TEX_ENVIRONMENT", "TEXTURE"]
823 if node
.type in image_types
:
824 if node
.type == "TEXTURE":
825 if node
.texture
: # node has texture assigned
826 if node
.texture
.type in ['IMAGE', 'ENVIRONMENT_MAP']:
827 if node
.texture
.image
: # texture has image assigned
828 node
.texture
.image
.reload()
836 self
.report({'INFO'}, "Reloaded images")
837 print("Reloaded " + str(num_reloaded
) + " images")
838 force_update(context
)
841 self
.report({'WARNING'}, "No images found to reload in this node tree")
845 class NWMergeNodes(Operator
, NWBase
):
846 bl_idname
= "node.nw_merge_nodes"
847 bl_label
= "Merge Nodes"
848 bl_description
= "Merge Selected Nodes"
849 bl_options
= {'REGISTER', 'UNDO'}
853 description
="All possible blend types, boolean operations and math operations",
854 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
],
856 merge_type
: EnumProperty(
858 description
="Type of Merge to be used",
860 ('AUTO', 'Auto', 'Automatic Output Type Detection'),
861 ('SHADER', 'Shader', 'Merge using ADD or MIX Shader'),
862 ('GEOMETRY', 'Geometry', 'Merge using Mesh Boolean or Join Geometry Node'),
863 ('MIX', 'Mix Node', 'Merge using Mix Nodes'),
864 ('MATH', 'Math Node', 'Merge using Math Nodes'),
865 ('ZCOMBINE', 'Z-Combine Node', 'Merge using Z-Combine Nodes'),
866 ('ALPHAOVER', 'Alpha Over Node', 'Merge using Alpha Over Nodes'),
870 # Check if the link connects to a node that is in selected_nodes
871 # If not, then check recursively for each link in the nodes outputs.
872 # If yes, return True. If the recursion stops without finding a node
873 # in selected_nodes, it returns False. The depth is used to prevent
874 # getting stuck in a loop because of an already present cycle.
876 def link_creates_cycle(link
, selected_nodes
, depth
=0) -> bool:
878 # We're stuck in a cycle, but that cycle was already present,
879 # so we return False.
880 # NOTE: The number 255 is arbitrary, but seems to work well.
883 if node
in selected_nodes
:
887 for output
in node
.outputs
:
889 for olink
in output
.links
:
890 if NWMergeNodes
.link_creates_cycle(olink
, selected_nodes
, depth
+ 1):
892 # None of the outputs found a node in selected_nodes, so there is no cycle.
895 # Merge the nodes in `nodes_list` with a node of type `node_name` that has a multi_input socket.
896 # The parameters `socket_indices` gives the indices of the node sockets in the order that they should
897 # be connected. The last one is assumed to be a multi input socket.
898 # For convenience the node is returned.
900 def merge_with_multi_input(nodes_list
, merge_position
, do_hide
, loc_x
, links
, nodes
, node_name
, socket_indices
):
901 # The y-location of the last node
902 loc_y
= nodes_list
[-1][2]
903 if merge_position
== 'CENTER':
904 # Average the y-location
905 for i
in range(len(nodes_list
) - 1):
906 loc_y
+= nodes_list
[i
][2]
907 loc_y
= loc_y
/ len(nodes_list
)
908 new_node
= nodes
.new(node_name
)
909 new_node
.hide
= do_hide
910 new_node
.location
.x
= loc_x
911 new_node
.location
.y
= loc_y
912 selected_nodes
= [nodes
[node_info
[0]] for node_info
in nodes_list
]
914 outputs_for_multi_input
= []
915 for i
, node
in enumerate(selected_nodes
):
917 # Search for the first node which had output links that do not create
918 # a cycle, which we can then reconnect afterwards.
919 if prev_links
== [] and node
.outputs
[0].is_linked
:
921 link
for link
in node
.outputs
[0].links
if not NWMergeNodes
.link_creates_cycle(
922 link
, selected_nodes
)]
923 # Get the index of the socket, the last one is a multi input, and is thus used repeatedly
924 # To get the placement to look right we need to reverse the order in which we connect the
925 # outputs to the multi input socket.
926 if i
< len(socket_indices
) - 1:
927 ind
= socket_indices
[i
]
928 connect_sockets(node
.outputs
[0], new_node
.inputs
[ind
])
930 outputs_for_multi_input
.insert(0, node
.outputs
[0])
931 if outputs_for_multi_input
!= []:
932 ind
= socket_indices
[-1]
933 for output
in outputs_for_multi_input
:
934 connect_sockets(output
, new_node
.inputs
[ind
])
936 for link
in prev_links
:
937 connect_sockets(new_node
.outputs
[0], link
.to_node
.inputs
[0])
941 def poll(cls
, context
):
942 return (nw_check(context
)
943 and nw_check_space_type(cls
, context
, 'ShaderNodeTree', 'CompositorNodeTree',
944 'TextureNodeTree', 'GeometryNodeTree'))
946 def execute(self
, context
):
947 settings
= context
.preferences
.addons
[__package__
].preferences
948 merge_hide
= settings
.merge_hide
949 merge_position
= settings
.merge_position
# 'center' or 'bottom'
952 do_hide_shader
= False
953 if merge_hide
== 'ALWAYS':
955 do_hide_shader
= True
956 elif merge_hide
== 'NON_SHADER':
959 tree_type
= context
.space_data
.node_tree
.type
960 if tree_type
== 'GEOMETRY':
961 node_type
= 'GeometryNode'
962 if tree_type
== 'COMPOSITING':
963 node_type
= 'CompositorNode'
964 elif tree_type
== 'SHADER':
965 node_type
= 'ShaderNode'
966 elif tree_type
== 'TEXTURE':
967 node_type
= 'TextureNode'
968 nodes
, links
= get_nodes_links(context
)
970 merge_type
= self
.merge_type
971 # Prevent trying to add Z-Combine in not 'COMPOSITING' node tree.
972 # 'ZCOMBINE' works only if mode == 'MIX'
973 # Setting mode to None prevents trying to add 'ZCOMBINE' node.
974 if (merge_type
== 'ZCOMBINE' or merge_type
== 'ALPHAOVER') and tree_type
!= 'COMPOSITING':
977 if (merge_type
!= 'MATH' and merge_type
!= 'GEOMETRY') and tree_type
== 'GEOMETRY':
979 # The Mix node and math nodes used for geometry nodes are of type 'ShaderNode'
980 if (merge_type
== 'MATH' or merge_type
== 'MIX') and tree_type
== 'GEOMETRY':
981 node_type
= 'ShaderNode'
982 selected_mix
= [] # entry = [index, loc]
983 selected_shader
= [] # entry = [index, loc]
984 selected_geometry
= [] # entry = [index, loc]
985 selected_math
= [] # entry = [index, loc]
986 selected_vector
= [] # entry = [index, loc]
987 selected_z
= [] # entry = [index, loc]
988 selected_alphaover
= [] # entry = [index, loc]
990 for i
, node
in enumerate(nodes
):
991 if node
.select
and node
.outputs
:
992 if merge_type
== 'AUTO':
993 for (type, types_list
, dst
) in (
994 ('SHADER', ('MIX', 'ADD'), selected_shader
),
995 ('GEOMETRY', [t
[0] for t
in geo_combine_operations
], selected_geometry
),
996 ('RGBA', [t
[0] for t
in blend_types
], selected_mix
),
997 ('VALUE', [t
[0] for t
in operations
], selected_math
),
998 ('VECTOR', [], selected_vector
),
1000 output
= get_first_enabled_output(node
)
1001 output_type
= output
.type
1002 valid_mode
= mode
in types_list
1003 # When mode is 'MIX' we have to cheat since the mix node is not used in
1005 if tree_type
== 'GEOMETRY':
1007 if output_type
== 'VALUE' and type == 'VALUE':
1009 elif output_type
== 'VECTOR' and type == 'VECTOR':
1011 elif type == 'GEOMETRY':
1013 # When mode is 'MIX' use mix node for both 'RGBA' and 'VALUE' output types.
1014 # Cheat that output type is 'RGBA',
1015 # and that 'MIX' exists in math operations list.
1016 # This way when selected_mix list is analyzed:
1017 # Node data will be appended even though it doesn't meet requirements.
1018 elif output_type
!= 'SHADER' and mode
== 'MIX':
1019 output_type
= 'RGBA'
1021 if output_type
== type and valid_mode
:
1022 dst
.append([i
, node
.location
.x
, node
.location
.y
, node
.dimensions
.x
, node
.hide
])
1024 for (type, types_list
, dst
) in (
1025 ('SHADER', ('MIX', 'ADD'), selected_shader
),
1026 ('GEOMETRY', [t
[0] for t
in geo_combine_operations
], selected_geometry
),
1027 ('MIX', [t
[0] for t
in blend_types
], selected_mix
),
1028 ('MATH', [t
[0] for t
in operations
], selected_math
),
1029 ('ZCOMBINE', ('MIX', ), selected_z
),
1030 ('ALPHAOVER', ('MIX', ), selected_alphaover
),
1032 if merge_type
== type and mode
in types_list
:
1033 dst
.append([i
, node
.location
.x
, node
.location
.y
, node
.dimensions
.x
, node
.hide
])
1034 # When nodes with output kinds 'RGBA' and 'VALUE' are selected at the same time
1035 # use only 'Mix' nodes for merging.
1036 # For that we add selected_math list to selected_mix list and clear selected_math.
1037 if selected_mix
and selected_math
and merge_type
== 'AUTO':
1038 selected_mix
+= selected_math
1047 selected_alphaover
]:
1050 count_before
= len(nodes
)
1051 # sort list by loc_x - reversed
1052 nodes_list
.sort(key
=lambda k
: k
[1], reverse
=True)
1054 loc_x
= nodes_list
[0][1] + nodes_list
[0][3] + 70
1055 nodes_list
.sort(key
=lambda k
: k
[2], reverse
=True)
1057 # Change the node type for math nodes in a geometry node tree.
1058 if tree_type
== 'GEOMETRY':
1059 if nodes_list
is selected_math
or nodes_list
is selected_vector
or nodes_list
is selected_mix
:
1060 node_type
= 'ShaderNode'
1064 node_type
= 'GeometryNode'
1065 if merge_position
== 'CENTER':
1066 # average yloc of last two nodes (lowest two)
1067 loc_y
= ((nodes_list
[len(nodes_list
) - 1][2]) + (nodes_list
[len(nodes_list
) - 2][2])) / 2
1068 if nodes_list
[len(nodes_list
) - 1][-1]: # if last node is hidden, mix should be shifted up a bit
1074 loc_y
= nodes_list
[len(nodes_list
) - 1][2]
1078 if nodes_list
== selected_shader
and not do_hide_shader
:
1080 the_range
= len(nodes_list
) - 1
1081 if len(nodes_list
) == 1:
1084 for i
in range(the_range
):
1085 if nodes_list
== selected_mix
:
1087 if tree_type
== 'COMPOSITING':
1089 add_type
= node_type
+ mix_name
1090 add
= nodes
.new(add_type
)
1091 if tree_type
!= 'COMPOSITING':
1092 add
.data_type
= 'RGBA'
1093 add
.blend_type
= mode
1095 add
.inputs
[0].default_value
= 1.0
1096 add
.show_preview
= False
1102 if tree_type
== 'COMPOSITING':
1105 elif nodes_list
== selected_math
:
1106 add_type
= node_type
+ 'Math'
1107 add
= nodes
.new(add_type
)
1108 add
.operation
= mode
1114 elif nodes_list
== selected_shader
:
1116 add_type
= node_type
+ 'MixShader'
1117 add
= nodes
.new(add_type
)
1118 add
.hide
= do_hide_shader
1124 add_type
= node_type
+ 'AddShader'
1125 add
= nodes
.new(add_type
)
1126 add
.hide
= do_hide_shader
1131 elif nodes_list
== selected_geometry
:
1132 if mode
in ('JOIN', 'MIX'):
1133 add_type
= node_type
+ 'JoinGeometry'
1134 add
= self
.merge_with_multi_input(
1135 nodes_list
, merge_position
, do_hide
, loc_x
, links
, nodes
, add_type
, [0])
1137 add_type
= node_type
+ 'MeshBoolean'
1138 indices
= [0, 1] if mode
== 'DIFFERENCE' else [1]
1139 add
= self
.merge_with_multi_input(
1140 nodes_list
, merge_position
, do_hide
, loc_x
, links
, nodes
, add_type
, indices
)
1141 add
.operation
= mode
1144 elif nodes_list
== selected_vector
:
1145 add_type
= node_type
+ 'VectorMath'
1146 add
= nodes
.new(add_type
)
1147 add
.operation
= mode
1153 elif nodes_list
== selected_z
:
1154 add
= nodes
.new('CompositorNodeZcombine')
1155 add
.show_preview
= False
1161 elif nodes_list
== selected_alphaover
:
1162 add
= nodes
.new('CompositorNodeAlphaOver')
1163 add
.show_preview
= False
1169 add
.location
= loc_x
, loc_y
1173 # This has already been handled separately
1177 count_after
= len(nodes
)
1178 index
= count_after
- 1
1179 first_selected
= nodes
[nodes_list
[0][0]]
1180 # "last" node has been added as first, so its index is count_before.
1181 last_add
= nodes
[count_before
]
1182 # Create list of invalid indexes.
1183 invalid_nodes
= [nodes
[n
[0]]
1184 for n
in (selected_mix
+ selected_math
+ selected_shader
+ selected_z
+ selected_geometry
)]
1187 # Two nodes were selected and first selected has no output links, second selected has output links.
1188 # Then add links from last add to all links 'to_socket' of out links of second selected.
1189 first_selected_output
= get_first_enabled_output(first_selected
)
1190 if len(nodes_list
) == 2:
1191 if not first_selected_output
.links
:
1192 second_selected
= nodes
[nodes_list
[1][0]]
1193 for ss_link
in get_first_enabled_output(second_selected
).links
:
1194 # Prevent cyclic dependencies when nodes to be merged are linked to one another.
1195 # Link only if "to_node" index not in invalid indexes list.
1196 if not self
.link_creates_cycle(ss_link
, invalid_nodes
):
1197 connect_sockets(get_first_enabled_output(last_add
), ss_link
.to_socket
)
1198 # add links from last_add to all links 'to_socket' of out links of first selected.
1199 for fs_link
in first_selected_output
.links
:
1200 # Link only if "to_node" index not in invalid indexes list.
1201 if not self
.link_creates_cycle(fs_link
, invalid_nodes
):
1202 connect_sockets(get_first_enabled_output(last_add
), fs_link
.to_socket
)
1203 # add link from "first" selected and "first" add node
1204 node_to
= nodes
[count_after
- 1]
1205 connect_sockets(first_selected_output
, node_to
.inputs
[first
])
1206 if node_to
.type == 'ZCOMBINE':
1207 for fs_out
in first_selected
.outputs
:
1208 if fs_out
!= first_selected_output
and fs_out
.name
in ('Z', 'Depth'):
1209 connect_sockets(fs_out
, node_to
.inputs
[1])
1211 # add links between added ADD nodes and between selected and ADD nodes
1212 for i
in range(count_adds
):
1213 if i
< count_adds
- 1:
1214 node_from
= nodes
[index
]
1215 node_to
= nodes
[index
- 1]
1216 node_to_input_i
= first
1217 node_to_z_i
= 1 # if z combine - link z to first z input
1218 connect_sockets(get_first_enabled_output(node_from
), node_to
.inputs
[node_to_input_i
])
1219 if node_to
.type == 'ZCOMBINE':
1220 for from_out
in node_from
.outputs
:
1221 if from_out
!= get_first_enabled_output(node_from
) and from_out
.name
in ('Z', 'Depth'):
1222 connect_sockets(from_out
, node_to
.inputs
[node_to_z_i
])
1223 if len(nodes_list
) > 1:
1224 node_from
= nodes
[nodes_list
[i
+ 1][0]]
1225 node_to
= nodes
[index
]
1226 node_to_input_i
= second
1227 node_to_z_i
= 3 # if z combine - link z to second z input
1228 connect_sockets(get_first_enabled_output(node_from
), node_to
.inputs
[node_to_input_i
])
1229 if node_to
.type == 'ZCOMBINE':
1230 for from_out
in node_from
.outputs
:
1231 if from_out
!= get_first_enabled_output(node_from
) and from_out
.name
in ('Z', 'Depth'):
1232 connect_sockets(from_out
, node_to
.inputs
[node_to_z_i
])
1234 # set "last" of added nodes as active
1235 nodes
.active
= last_add
1236 for i
, x
, y
, dx
, h
in nodes_list
:
1237 nodes
[i
].select
= False
1242 class NWBatchChangeNodes(Operator
, NWBase
):
1243 bl_idname
= "node.nw_batch_change"
1244 bl_label
= "Batch Change"
1245 bl_description
= "Batch Change Blend Type and Math Operation"
1246 bl_options
= {'REGISTER', 'UNDO'}
1248 blend_type
: EnumProperty(
1250 items
=blend_types
+ navs
,
1252 operation
: EnumProperty(
1254 items
=operations
+ navs
,
1258 def poll(cls
, context
):
1259 return (nw_check(context
)
1260 and nw_check_space_type(cls
, context
, 'ShaderNodeTree', 'CompositorNodeTree',
1261 'TextureNodeTree', 'GeometryNodeTree'))
1263 def execute(self
, context
):
1264 blend_type
= self
.blend_type
1265 operation
= self
.operation
1266 for node
in context
.selected_nodes
:
1267 if node
.type == 'MIX_RGB' or (node
.bl_idname
== 'ShaderNodeMix' and node
.data_type
== 'RGBA'):
1268 if blend_type
not in [nav
[0] for nav
in navs
]:
1269 node
.blend_type
= blend_type
1271 if blend_type
== 'NEXT':
1272 index
= [i
for i
, entry
in enumerate(blend_types
) if node
.blend_type
in entry
][0]
1273 # index = blend_types.index(node.blend_type)
1274 if index
== len(blend_types
) - 1:
1275 node
.blend_type
= blend_types
[0][0]
1277 node
.blend_type
= blend_types
[index
+ 1][0]
1279 if blend_type
== 'PREV':
1280 index
= [i
for i
, entry
in enumerate(blend_types
) if node
.blend_type
in entry
][0]
1282 node
.blend_type
= blend_types
[len(blend_types
) - 1][0]
1284 node
.blend_type
= blend_types
[index
- 1][0]
1286 if node
.type == 'MATH' or node
.bl_idname
== 'ShaderNodeMath':
1287 if operation
not in [nav
[0] for nav
in navs
]:
1288 node
.operation
= operation
1290 if operation
== 'NEXT':
1291 index
= [i
for i
, entry
in enumerate(operations
) if node
.operation
in entry
][0]
1292 # index = operations.index(node.operation)
1293 if index
== len(operations
) - 1:
1294 node
.operation
= operations
[0][0]
1296 node
.operation
= operations
[index
+ 1][0]
1298 if operation
== 'PREV':
1299 index
= [i
for i
, entry
in enumerate(operations
) if node
.operation
in entry
][0]
1300 # index = operations.index(node.operation)
1302 node
.operation
= operations
[len(operations
) - 1][0]
1304 node
.operation
= operations
[index
- 1][0]
1309 class NWChangeMixFactor(Operator
, NWBase
):
1310 bl_idname
= "node.nw_factor"
1311 bl_label
= "Change Factor"
1312 bl_description
= "Change Factors of Mix Nodes and Mix Shader Nodes"
1313 bl_options
= {'REGISTER', 'UNDO'}
1315 # option: Change factor.
1316 # If option is 1.0 or 0.0 - set to 1.0 or 0.0
1317 # Else - change factor by option value.
1318 option
: FloatProperty()
1320 def execute(self
, context
):
1321 nodes
, links
= get_nodes_links(context
)
1322 option
= self
.option
1323 selected
= [] # entry = index
1324 for si
, node
in enumerate(nodes
):
1326 if node
.type in {'MIX_RGB', 'MIX_SHADER'} or node
.bl_idname
== 'ShaderNodeMix':
1330 fac
= nodes
[si
].inputs
[0]
1331 nodes
[si
].hide
= False
1332 if option
in {0.0, 1.0}:
1333 fac
.default_value
= option
1335 fac
.default_value
+= option
1340 class NWCopySettings(Operator
, NWBase
):
1341 bl_idname
= "node.nw_copy_settings"
1342 bl_label
= "Copy Settings"
1343 bl_description
= "Copy Settings of Active Node to Selected Nodes"
1344 bl_options
= {'REGISTER', 'UNDO'}
1347 def poll(cls
, context
):
1348 return (nw_check(context
)
1349 and context
.active_node
is not None
1350 and context
.active_node
.type != 'FRAME')
1352 def execute(self
, context
):
1353 node_active
= context
.active_node
1354 node_selected
= context
.selected_nodes
1357 if not (len(node_selected
) > 1):
1358 self
.report({'ERROR'}, "2 nodes must be selected at least")
1359 return {'CANCELLED'}
1361 # Check if active node is in the selection
1362 selected_node_names
= [n
.name
for n
in node_selected
]
1363 if node_active
.name
not in selected_node_names
:
1364 self
.report({'ERROR'}, "No active node")
1365 return {'CANCELLED'}
1367 # Get nodes in selection by type
1368 valid_nodes
= [n
for n
in node_selected
if n
.type == node_active
.type]
1370 if not (len(valid_nodes
) > 1) and node_active
:
1371 self
.report({'ERROR'}, "Selected nodes are not of the same type as {}".format(node_active
.name
))
1372 return {'CANCELLED'}
1374 if len(valid_nodes
) != len(node_selected
):
1375 # Report nodes that are not valid
1376 valid_node_names
= [n
.name
for n
in valid_nodes
]
1377 not_valid_names
= list(set(selected_node_names
) - set(valid_node_names
))
1380 "Ignored {} (not of the same type as {})".format(
1381 ", ".join(not_valid_names
),
1384 # Reference original
1386 # node_selected_names = [n.name for n in node_selected]
1391 # Deselect all nodes
1392 for i
in node_selected
:
1395 # Code by zeffii from http://blender.stackexchange.com/a/42338/3710
1396 # Run through all other nodes
1397 for node
in valid_nodes
[1:]:
1399 # Check for frame node
1400 parent
= node
.parent
if node
.parent
else None
1401 node_loc
= [node
.location
.x
, node
.location
.y
]
1403 # Select original to duplicate
1406 # Duplicate selected node
1407 bpy
.ops
.node
.duplicate()
1408 new_node
= context
.selected_nodes
[0]
1411 new_node
.select
= False
1413 # Properties to copy
1414 node_tree
= node
.id_data
1415 props_to_copy
= 'bl_idname name location height width'.split(' ')
1419 mappings
= chain
.from_iterable([node
.inputs
, node
.outputs
])
1420 for i
in (i
for i
in mappings
if i
.is_linked
):
1422 reconnections
.append([L
.from_socket
.path_from_id(), L
.to_socket
.path_from_id()])
1425 props
= {j
: getattr(node
, j
) for j
in props_to_copy
}
1426 props_to_copy
.pop(0)
1428 for prop
in props_to_copy
:
1429 setattr(new_node
, prop
, props
[prop
])
1431 # Get the node tree to remove the old node
1432 nodes
= node_tree
.nodes
1434 new_node
.name
= props
['name']
1437 new_node
.parent
= parent
1438 new_node
.location
= node_loc
1440 for str_from
, str_to
in reconnections
:
1441 node_tree
.connect_sockets(eval(str_from
), eval(str_to
))
1443 success_names
.append(new_node
.name
)
1446 node_tree
.nodes
.active
= orig
1449 "Successfully copied attributes from {} to: {}".format(
1451 ", ".join(success_names
)))
1455 class NWCopyLabel(Operator
, NWBase
):
1456 bl_idname
= "node.nw_copy_label"
1457 bl_label
= "Copy Label"
1458 bl_options
= {'REGISTER', 'UNDO'}
1460 option
: EnumProperty(
1462 description
="Source of name of label",
1464 ('FROM_ACTIVE', 'from active', 'from active node',),
1465 ('FROM_NODE', 'from node', 'from node linked to selected node'),
1466 ('FROM_SOCKET', 'from socket', 'from socket linked to selected node'),
1470 def execute(self
, context
):
1471 nodes
, links
= get_nodes_links(context
)
1472 option
= self
.option
1473 active
= nodes
.active
1474 if option
== 'FROM_ACTIVE':
1476 src_label
= active
.label
1477 for node
in [n
for n
in nodes
if n
.select
and nodes
.active
!= n
]:
1478 node
.label
= src_label
1479 elif option
== 'FROM_NODE':
1480 selected
= [n
for n
in nodes
if n
.select
]
1481 for node
in selected
:
1482 for input in node
.inputs
:
1484 src
= input.links
[0].from_node
1485 node
.label
= src
.label
1487 elif option
== 'FROM_SOCKET':
1488 selected
= [n
for n
in nodes
if n
.select
]
1489 for node
in selected
:
1490 for input in node
.inputs
:
1492 src
= input.links
[0].from_socket
1493 node
.label
= src
.name
1499 class NWClearLabel(Operator
, NWBase
):
1500 bl_idname
= "node.nw_clear_label"
1501 bl_label
= "Clear Label"
1502 bl_options
= {'REGISTER', 'UNDO'}
1504 option
: BoolProperty()
1506 def execute(self
, context
):
1507 nodes
, links
= get_nodes_links(context
)
1508 for node
in [n
for n
in nodes
if n
.select
]:
1513 def invoke(self
, context
, event
):
1515 return self
.execute(context
)
1517 return context
.window_manager
.invoke_confirm(self
, event
)
1520 class NWModifyLabels(Operator
, NWBase
):
1521 """Modify Labels of all selected nodes"""
1522 bl_idname
= "node.nw_modify_labels"
1523 bl_label
= "Modify Labels"
1524 bl_options
= {'REGISTER', 'UNDO'}
1526 prepend
: StringProperty(
1527 name
="Add to Beginning"
1529 append
: StringProperty(
1532 replace_from
: StringProperty(
1533 name
="Text to Replace"
1535 replace_to
: StringProperty(
1539 def execute(self
, context
):
1540 nodes
, links
= get_nodes_links(context
)
1541 for node
in [n
for n
in nodes
if n
.select
]:
1542 node
.label
= self
.prepend
+ node
.label
.replace(self
.replace_from
, self
.replace_to
) + self
.append
1546 def invoke(self
, context
, event
):
1550 return context
.window_manager
.invoke_props_dialog(self
)
1553 class NWAddTextureSetup(Operator
, NWBase
):
1554 bl_idname
= "node.nw_add_texture"
1555 bl_label
= "Texture Setup"
1556 bl_description
= "Add Texture Node Setup to Selected Shaders"
1557 bl_options
= {'REGISTER', 'UNDO'}
1559 add_mapping
: BoolProperty(
1560 name
="Add Mapping Nodes",
1561 description
="Create coordinate and mapping nodes for the texture (ignored for selected texture nodes)",
1565 def poll(cls
, context
):
1566 return (nw_check(context
)
1567 and nw_check_space_type(cls
, context
, 'ShaderNodeTree'))
1569 def execute(self
, context
):
1570 nodes
, links
= get_nodes_links(context
)
1572 texture_types
= get_texture_node_types()
1573 selected_nodes
= [n
for n
in nodes
if n
.select
]
1575 for node
in selected_nodes
:
1580 target_input
= node
.inputs
[0]
1581 for input in node
.inputs
:
1584 if not input.is_linked
:
1585 target_input
= input
1588 self
.report({'WARNING'}, "No free inputs for node: " + node
.name
)
1593 locx
= node
.location
.x
1594 locy
= node
.location
.y
- (input_index
* padding
)
1596 is_texture_node
= node
.rna_type
.identifier
in texture_types
1597 use_environment_texture
= node
.type == 'BACKGROUND'
1599 # Add an image texture before normal shader nodes.
1600 if not is_texture_node
:
1601 image_texture_type
= 'ShaderNodeTexEnvironment' if use_environment_texture
else 'ShaderNodeTexImage'
1602 image_texture_node
= nodes
.new(image_texture_type
)
1603 x_offset
= x_offset
+ image_texture_node
.width
+ padding
1604 image_texture_node
.location
= [locx
- x_offset
, locy
]
1605 nodes
.active
= image_texture_node
1606 connect_sockets(image_texture_node
.outputs
[0], target_input
)
1608 # The mapping setup following this will connect to the first input of this image texture.
1609 target_input
= image_texture_node
.inputs
[0]
1613 if is_texture_node
or self
.add_mapping
:
1615 mapping_node
= nodes
.new('ShaderNodeMapping')
1616 x_offset
= x_offset
+ mapping_node
.width
+ padding
1617 mapping_node
.location
= [locx
- x_offset
, locy
]
1618 connect_sockets(mapping_node
.outputs
[0], target_input
)
1620 # Add Texture Coordinates node.
1621 tex_coord_node
= nodes
.new('ShaderNodeTexCoord')
1622 x_offset
= x_offset
+ tex_coord_node
.width
+ padding
1623 tex_coord_node
.location
= [locx
- x_offset
, locy
]
1625 is_procedural_texture
= is_texture_node
and node
.type != 'TEX_IMAGE'
1626 use_generated_coordinates
= is_procedural_texture
or use_environment_texture
1627 tex_coord_output
= tex_coord_node
.outputs
[0 if use_generated_coordinates
else 2]
1628 connect_sockets(tex_coord_output
, mapping_node
.inputs
[0])
1633 class NWAddPrincipledSetup(Operator
, NWBase
, ImportHelper
):
1634 bl_idname
= "node.nw_add_textures_for_principled"
1635 bl_label
= "Principled Texture Setup"
1636 bl_description
= "Add Texture Node Setup for Principled BSDF"
1637 bl_options
= {'REGISTER', 'UNDO'}
1639 directory
: StringProperty(
1643 description
='Folder to search in for image files'
1645 files
: CollectionProperty(
1646 type=bpy
.types
.OperatorFileListElement
,
1647 options
={'HIDDEN', 'SKIP_SAVE'}
1650 relative_path
: BoolProperty(
1651 name
='Relative Path',
1652 description
='Set the file path relative to the blend file, when possible',
1661 def draw(self
, context
):
1662 layout
= self
.layout
1663 layout
.alignment
= 'LEFT'
1665 layout
.prop(self
, 'relative_path')
1668 def poll(cls
, context
):
1669 return (nw_check(context
)
1670 and nw_check_space_type(cls
, context
, 'ShaderNodeTree'))
1672 def execute(self
, context
):
1673 # Check if everything is ok
1674 if not self
.directory
:
1675 self
.report({'INFO'}, 'No Folder Selected')
1676 return {'CANCELLED'}
1677 if not self
.files
[:]:
1678 self
.report({'INFO'}, 'No Files Selected')
1679 return {'CANCELLED'}
1681 nodes
, links
= get_nodes_links(context
)
1682 active_node
= nodes
.active
1683 if not (active_node
and active_node
.bl_idname
== 'ShaderNodeBsdfPrincipled'):
1684 self
.report({'INFO'}, 'Select Principled BSDF')
1685 return {'CANCELLED'}
1687 # Filter textures names for texturetypes in filenames
1688 # [Socket Name, [abbreviations and keyword list], Filename placeholder]
1689 tags
= context
.preferences
.addons
[__package__
].preferences
.principled_tags
1690 normal_abbr
= tags
.normal
.split(' ')
1691 bump_abbr
= tags
.bump
.split(' ')
1692 gloss_abbr
= tags
.gloss
.split(' ')
1693 rough_abbr
= tags
.rough
.split(' ')
1695 ['Displacement', tags
.displacement
.split(' '), None],
1696 ['Base Color', tags
.base_color
.split(' '), None],
1697 ['Metallic', tags
.metallic
.split(' '), None],
1698 ['Specular IOR Level', tags
.specular
.split(' '), None],
1699 ['Roughness', rough_abbr
+ gloss_abbr
, None],
1700 ['Normal', normal_abbr
+ bump_abbr
, None],
1701 ['Transmission Weight', tags
.transmission
.split(' '), None],
1702 ['Emission Color', tags
.emission
.split(' '), None],
1703 ['Alpha', tags
.alpha
.split(' '), None],
1704 ['Ambient Occlusion', tags
.ambient_occlusion
.split(' '), None],
1707 match_files_to_socket_names(self
.files
, socketnames
)
1708 # Remove socketnames without found files
1709 socketnames
= [s
for s
in socketnames
if s
[2]
1710 and path
.exists(self
.directory
+ s
[2])]
1712 self
.report({'INFO'}, 'No matching images found')
1713 print('No matching images found')
1714 return {'CANCELLED'}
1716 # Don't override path earlier as os.path is used to check the absolute path
1717 import_path
= self
.directory
1718 if self
.relative_path
:
1719 if bpy
.data
.filepath
:
1721 import_path
= bpy
.path
.relpath(self
.directory
)
1726 print('\nMatched Textures:')
1731 roughness_node
= None
1732 for i
, sname
in enumerate(socketnames
):
1733 print(i
, sname
[0], sname
[2])
1735 # DISPLACEMENT NODES
1736 if sname
[0] == 'Displacement':
1737 disp_texture
= nodes
.new(type='ShaderNodeTexImage')
1738 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
1739 disp_texture
.image
= img
1740 disp_texture
.label
= 'Displacement'
1741 if disp_texture
.image
:
1742 disp_texture
.image
.colorspace_settings
.is_data
= True
1744 # Add displacement offset nodes
1745 disp_node
= nodes
.new(type='ShaderNodeDisplacement')
1746 # Align the Displacement node under the active Principled BSDF node
1747 disp_node
.location
= active_node
.location
+ Vector((100, -700))
1748 link
= connect_sockets(disp_node
.inputs
[0], disp_texture
.outputs
[0])
1750 # TODO Turn on true displacement in the material
1751 # Too complicated for now
1754 output_node
= [n
for n
in nodes
if n
.bl_idname
== 'ShaderNodeOutputMaterial']
1756 if not output_node
[0].inputs
[2].is_linked
:
1757 link
= connect_sockets(output_node
[0].inputs
[2], disp_node
.outputs
[0])
1761 # AMBIENT OCCLUSION TEXTURE
1762 if sname
[0] == 'Ambient Occlusion':
1763 ao_texture
= nodes
.new(type='ShaderNodeTexImage')
1764 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
1765 ao_texture
.image
= img
1766 ao_texture
.label
= sname
[0]
1767 if ao_texture
.image
:
1768 ao_texture
.image
.colorspace_settings
.is_data
= True
1772 if not active_node
.inputs
[sname
[0]].is_linked
:
1773 # No texture node connected -> add texture node with new image
1774 texture_node
= nodes
.new(type='ShaderNodeTexImage')
1775 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
1776 texture_node
.image
= img
1779 if sname
[0] == 'Normal':
1780 # Test if new texture node is normal or bump map
1781 fname_components
= split_into_components(sname
[2])
1782 match_normal
= set(normal_abbr
).intersection(set(fname_components
))
1783 match_bump
= set(bump_abbr
).intersection(set(fname_components
))
1785 # If Normal add normal node in between
1786 normal_node
= nodes
.new(type='ShaderNodeNormalMap')
1787 link
= connect_sockets(normal_node
.inputs
[1], texture_node
.outputs
[0])
1789 # If Bump add bump node in between
1790 normal_node
= nodes
.new(type='ShaderNodeBump')
1791 link
= connect_sockets(normal_node
.inputs
[2], texture_node
.outputs
[0])
1793 link
= connect_sockets(active_node
.inputs
[sname
[0]], normal_node
.outputs
[0])
1794 normal_node_texture
= texture_node
1796 elif sname
[0] == 'Roughness':
1797 # Test if glossy or roughness map
1798 fname_components
= split_into_components(sname
[2])
1799 match_rough
= set(rough_abbr
).intersection(set(fname_components
))
1800 match_gloss
= set(gloss_abbr
).intersection(set(fname_components
))
1803 # If Roughness nothing to to
1804 link
= connect_sockets(active_node
.inputs
[sname
[0]], texture_node
.outputs
[0])
1807 # If Gloss Map add invert node
1808 invert_node
= nodes
.new(type='ShaderNodeInvert')
1809 link
= connect_sockets(invert_node
.inputs
[1], texture_node
.outputs
[0])
1811 link
= connect_sockets(active_node
.inputs
[sname
[0]], invert_node
.outputs
[0])
1812 roughness_node
= texture_node
1815 # This is a simple connection Texture --> Input slot
1816 link
= connect_sockets(active_node
.inputs
[sname
[0]], texture_node
.outputs
[0])
1818 # Use non-color except for color inputs
1819 if sname
[0] not in ['Base Color', 'Emission Color'] and texture_node
.image
:
1820 texture_node
.image
.colorspace_settings
.is_data
= True
1823 # If already texture connected. add to node list for alignment
1824 texture_node
= active_node
.inputs
[sname
[0]].links
[0].from_node
1826 # This are all connected texture nodes
1827 texture_nodes
.append(texture_node
)
1828 texture_node
.label
= sname
[0]
1831 texture_nodes
.append(disp_texture
)
1834 # We want the ambient occlusion texture to be the top most texture node
1835 texture_nodes
.insert(0, ao_texture
)
1838 for i
, texture_node
in enumerate(texture_nodes
):
1839 offset
= Vector((-550, (i
* -280) + 200))
1840 texture_node
.location
= active_node
.location
+ offset
1843 # Extra alignment if normal node was added
1844 normal_node
.location
= normal_node_texture
.location
+ Vector((300, 0))
1847 # Alignment of invert node if glossy map
1848 invert_node
.location
= roughness_node
.location
+ Vector((300, 0))
1850 # Add texture input + mapping
1851 mapping
= nodes
.new(type='ShaderNodeMapping')
1852 mapping
.location
= active_node
.location
+ Vector((-1050, 0))
1853 if len(texture_nodes
) > 1:
1854 # If more than one texture add reroute node in between
1855 reroute
= nodes
.new(type='NodeReroute')
1856 texture_nodes
.append(reroute
)
1857 tex_coords
= Vector((texture_nodes
[0].location
.x
,
1858 sum(n
.location
.y
for n
in texture_nodes
) / len(texture_nodes
)))
1859 reroute
.location
= tex_coords
+ Vector((-50, -120))
1860 for texture_node
in texture_nodes
:
1861 link
= connect_sockets(texture_node
.inputs
[0], reroute
.outputs
[0])
1862 link
= connect_sockets(reroute
.inputs
[0], mapping
.outputs
[0])
1864 link
= connect_sockets(texture_nodes
[0].inputs
[0], mapping
.outputs
[0])
1866 # Connect texture_coordiantes to mapping node
1867 texture_input
= nodes
.new(type='ShaderNodeTexCoord')
1868 texture_input
.location
= mapping
.location
+ Vector((-200, 0))
1869 link
= connect_sockets(mapping
.inputs
[0], texture_input
.outputs
[2])
1871 # Create frame around tex coords and mapping
1872 frame
= nodes
.new(type='NodeFrame')
1873 frame
.label
= 'Mapping'
1874 mapping
.parent
= frame
1875 texture_input
.parent
= frame
1878 # Create frame around texture nodes
1879 frame
= nodes
.new(type='NodeFrame')
1880 frame
.label
= 'Textures'
1881 for tnode
in texture_nodes
:
1882 tnode
.parent
= frame
1886 active_node
.select
= False
1889 force_update(context
)
1893 class NWAddReroutes(Operator
, NWBase
):
1894 """Add Reroute Nodes and link them to outputs of selected nodes"""
1895 bl_idname
= "node.nw_add_reroutes"
1896 bl_label
= "Add Reroutes"
1897 bl_description
= "Add Reroutes to Outputs"
1898 bl_options
= {'REGISTER', 'UNDO'}
1900 option
: EnumProperty(
1903 ('ALL', 'to all', 'Add to all outputs'),
1904 ('LOOSE', 'to loose', 'Add only to loose outputs'),
1905 ('LINKED', 'to linked', 'Add only to linked outputs'),
1909 def execute(self
, context
):
1910 tree_type
= context
.space_data
.node_tree
.type
1911 option
= self
.option
1912 nodes
, links
= get_nodes_links(context
)
1913 # output valid when option is 'all' or when 'loose' output has no links
1915 post_select
= [] # nodes to be selected after execution
1916 # create reroutes and recreate links
1917 for node
in [n
for n
in nodes
if n
.select
]:
1922 # unhide 'REROUTE' nodes to avoid issues with location.y
1923 if node
.type == 'REROUTE':
1925 # Hack needed to calculate real width
1927 bpy
.ops
.node
.select_all(action
='DESELECT')
1928 helper
= nodes
.new('NodeReroute')
1929 helper
.select
= True
1931 # resize node and helper to zero. Then check locations to calculate width
1932 bpy
.ops
.transform
.resize(value
=(0.0, 0.0, 0.0))
1933 width
= 2.0 * (helper
.location
.x
- node
.location
.x
)
1934 # restore node location
1935 node
.location
= x
, y
1938 # only helper is selected now
1939 bpy
.ops
.node
.delete()
1940 x
= node
.location
.x
+ width
+ 20.0
1941 if node
.type != 'REROUTE':
1945 reroutes_count
= 0 # will be used when aligning reroutes added to hidden nodes
1946 for out_i
, output
in enumerate(node
.outputs
):
1947 pass_used
= False # initial value to be analyzed if 'R_LAYERS'
1948 # if node != 'R_LAYERS' - "pass_used" not needed, so set it to True
1949 if node
.type != 'R_LAYERS':
1951 else: # if 'R_LAYERS' check if output represent used render pass
1952 node_scene
= node
.scene
1953 node_layer
= node
.layer
1954 # If output - "Alpha" is analyzed - assume it's used. Not represented in passes.
1955 if output
.name
== 'Alpha':
1958 # check entries in global 'rl_outputs' variable
1959 for rlo
in rl_outputs
:
1960 if output
.name
in {rlo
.output_name
, rlo
.exr_output_name
}:
1961 pass_used
= getattr(node_scene
.view_layers
[node_layer
], rlo
.render_pass
)
1964 valid
= ((option
== 'ALL') or
1965 (option
== 'LOOSE' and not output
.links
) or
1966 (option
== 'LINKED' and output
.links
))
1967 # Add reroutes only if valid, but offset location in all cases.
1969 n
= nodes
.new('NodeReroute')
1971 for link
in output
.links
:
1972 connect_sockets(n
.outputs
[0], link
.to_socket
)
1973 connect_sockets(output
, n
.inputs
[0])
1975 post_select
.append(n
)
1979 # disselect the node so that after execution of script only newly created nodes are selected
1981 # nicer reroutes distribution along y when node.hide
1983 y_translate
= reroutes_count
* y_offset
/ 2.0 - y_offset
- 35.0
1984 for reroute
in [r
for r
in nodes
if r
.select
]:
1985 reroute
.location
.y
-= y_translate
1986 for node
in post_select
:
1992 class NWLinkActiveToSelected(Operator
, NWBase
):
1993 """Link active node to selected nodes basing on various criteria"""
1994 bl_idname
= "node.nw_link_active_to_selected"
1995 bl_label
= "Link Active Node to Selected"
1996 bl_options
= {'REGISTER', 'UNDO'}
1998 replace
: BoolProperty()
1999 use_node_name
: BoolProperty()
2000 use_outputs_names
: BoolProperty()
2003 def poll(cls
, context
):
2004 return (nw_check(context
)
2005 and context
.active_node
is not None
2006 and context
.active_node
.select
)
2008 def execute(self
, context
):
2009 nodes
, links
= get_nodes_links(context
)
2010 replace
= self
.replace
2011 use_node_name
= self
.use_node_name
2012 use_outputs_names
= self
.use_outputs_names
2013 active
= nodes
.active
2014 selected
= [node
for node
in nodes
if node
.select
and node
!= active
]
2015 outputs
= [] # Only usable outputs of active nodes will be stored here.
2016 for out
in active
.outputs
:
2017 if active
.type != 'R_LAYERS':
2020 # 'R_LAYERS' node type needs special handling.
2021 # outputs of 'R_LAYERS' are callable even if not seen in UI.
2022 # Only outputs that represent used passes should be taken into account
2023 # Check if pass represented by output is used.
2024 # global 'rl_outputs' list will be used for that
2025 for rlo
in rl_outputs
:
2026 pass_used
= False # initial value. Will be set to True if pass is used
2027 if out
.name
== 'Alpha':
2028 # Alpha output is always present. Doesn't have representation in render pass. Assume it's used.
2030 elif out
.name
in {rlo
.output_name
, rlo
.exr_output_name
}:
2031 # example 'render_pass' entry: 'use_pass_uv' Check if True in scene render layers
2032 pass_used
= getattr(active
.scene
.view_layers
[active
.layer
], rlo
.render_pass
)
2036 doit
= True # Will be changed to False when links successfully added to previous output.
2039 for node
in selected
:
2040 dst_name
= node
.name
# Will be compared with src_name if needed.
2041 # When node has label - use it as dst_name
2043 dst_name
= node
.label
2044 valid
= True # Initial value. Will be changed to False if names don't match.
2045 src_name
= dst_name
# If names not used - this assignment will keep valid = True.
2047 # Set src_name to source node name or label
2048 src_name
= active
.name
2050 src_name
= active
.label
2051 elif use_outputs_names
:
2052 src_name
= (out
.name
, )
2053 for rlo
in rl_outputs
:
2054 if out
.name
in {rlo
.output_name
, rlo
.exr_output_name
}:
2055 src_name
= (rlo
.output_name
, rlo
.exr_output_name
)
2056 if dst_name
not in src_name
:
2059 for input in node
.inputs
:
2060 if input.type == out
.type or node
.type == 'REROUTE':
2061 if replace
or not input.is_linked
:
2062 connect_sockets(out
, input)
2063 if not use_node_name
and not use_outputs_names
:
2070 class NWAlignNodes(Operator
, NWBase
):
2071 '''Align the selected nodes neatly in a row/column'''
2072 bl_idname
= "node.nw_align_nodes"
2073 bl_label
= "Align Nodes"
2074 bl_options
= {'REGISTER', 'UNDO'}
2075 margin
: IntProperty(name
='Margin', default
=50, description
='The amount of space between nodes')
2077 def execute(self
, context
):
2078 nodes
, links
= get_nodes_links(context
)
2079 margin
= self
.margin
2083 if node
.select
and node
.type != 'FRAME':
2084 selection
.append(node
)
2086 # If no nodes are selected, align all nodes
2090 elif nodes
.active
in selection
:
2091 active_loc
= copy(nodes
.active
.location
) # make a copy, not a reference
2093 # Check if nodes should be laid out horizontally or vertically
2094 # use dimension to get center of node, not corner
2095 x_locs
= [n
.location
.x
+ (n
.dimensions
.x
/ 2) for n
in selection
]
2096 y_locs
= [n
.location
.y
- (n
.dimensions
.y
/ 2) for n
in selection
]
2097 x_range
= max(x_locs
) - min(x_locs
)
2098 y_range
= max(y_locs
) - min(y_locs
)
2099 mid_x
= (max(x_locs
) + min(x_locs
)) / 2
2100 mid_y
= (max(y_locs
) + min(y_locs
)) / 2
2101 horizontal
= x_range
> y_range
2103 # Sort selection by location of node mid-point
2105 selection
= sorted(selection
, key
=lambda n
: n
.location
.x
+ (n
.dimensions
.x
/ 2))
2107 selection
= sorted(selection
, key
=lambda n
: n
.location
.y
- (n
.dimensions
.y
/ 2), reverse
=True)
2111 for node
in selection
:
2112 current_margin
= margin
2113 current_margin
= current_margin
* 0.5 if node
.hide
else current_margin
# use a smaller margin for hidden nodes
2116 node
.location
.x
= current_pos
2117 current_pos
+= current_margin
+ node
.dimensions
.x
2118 node
.location
.y
= mid_y
+ (node
.dimensions
.y
/ 2)
2120 node
.location
.y
= current_pos
2121 current_pos
-= (current_margin
* 0.3) + node
.dimensions
.y
# use half-margin for vertical alignment
2122 node
.location
.x
= mid_x
- (node
.dimensions
.x
/ 2)
2124 # If active node is selected, center nodes around it
2125 if active_loc
is not None:
2126 active_loc_diff
= active_loc
- nodes
.active
.location
2127 for node
in selection
:
2128 node
.location
+= active_loc_diff
2129 else: # Position nodes centered around where they used to be
2130 locs
= ([n
.location
.x
+ (n
.dimensions
.x
/ 2) for n
in selection
]
2131 ) if horizontal
else ([n
.location
.y
- (n
.dimensions
.y
/ 2) for n
in selection
])
2132 new_mid
= (max(locs
) + min(locs
)) / 2
2133 for node
in selection
:
2135 node
.location
.x
+= (mid_x
- new_mid
)
2137 node
.location
.y
+= (mid_y
- new_mid
)
2142 class NWSelectParentChildren(Operator
, NWBase
):
2143 bl_idname
= "node.nw_select_parent_child"
2144 bl_label
= "Select Parent or Children"
2145 bl_options
= {'REGISTER', 'UNDO'}
2147 option
: EnumProperty(
2150 ('PARENT', 'Select Parent', 'Select Parent Frame'),
2151 ('CHILD', 'Select Children', 'Select members of selected frame'),
2155 def execute(self
, context
):
2156 nodes
, links
= get_nodes_links(context
)
2157 option
= self
.option
2158 selected
= [node
for node
in nodes
if node
.select
]
2159 if option
== 'PARENT':
2160 for sel
in selected
:
2163 parent
.select
= True
2164 else: # option == 'CHILD'
2165 for sel
in selected
:
2166 children
= [node
for node
in nodes
if node
.parent
== sel
]
2167 for kid
in children
:
2173 class NWDetachOutputs(Operator
, NWBase
):
2174 """Detach outputs of selected node leaving inputs linked"""
2175 bl_idname
= "node.nw_detach_outputs"
2176 bl_label
= "Detach Outputs"
2177 bl_options
= {'REGISTER', 'UNDO'}
2179 def execute(self
, context
):
2180 nodes
, links
= get_nodes_links(context
)
2181 selected
= context
.selected_nodes
2182 bpy
.ops
.node
.duplicate_move_keep_inputs()
2183 new_nodes
= context
.selected_nodes
2184 bpy
.ops
.node
.select_all(action
="DESELECT")
2185 for node
in selected
:
2187 bpy
.ops
.node
.delete_reconnect()
2188 for new_node
in new_nodes
:
2189 new_node
.select
= True
2190 bpy
.ops
.transform
.translate('INVOKE_DEFAULT')
2195 class NWLinkToOutputNode(Operator
):
2196 """Link to Composite node or Material Output node"""
2197 bl_idname
= "node.nw_link_out"
2198 bl_label
= "Connect to Output"
2199 bl_options
= {'REGISTER', 'UNDO'}
2202 def poll(cls
, context
):
2203 """Disabled for custom nodes as we do not know which nodes are outputs."""
2204 return (nw_check(context
)
2205 and nw_check_space_type(cls
, context
, 'ShaderNodeTree', 'CompositorNodeTree',
2206 'TextureNodeTree', 'GeometryNodeTree')
2207 and context
.active_node
is not None
2208 and any(is_visible_socket(out
) for out
in context
.active_node
.outputs
))
2210 def execute(self
, context
):
2211 nodes
, links
= get_nodes_links(context
)
2212 active
= nodes
.active
2214 tree_type
= context
.space_data
.tree_type
2215 shader_outputs
= {'OBJECT': 'ShaderNodeOutputMaterial',
2216 'WORLD': 'ShaderNodeOutputWorld',
2217 'LINESTYLE': 'ShaderNodeOutputLineStyle'}
2219 'ShaderNodeTree': shader_outputs
[context
.space_data
.shader_type
],
2220 'CompositorNodeTree': 'CompositorNodeComposite',
2221 'TextureNodeTree': 'TextureNodeOutput',
2222 'GeometryNodeTree': 'NodeGroupOutput',
2225 # check whether the node is an output node and,
2226 # if supported, whether it's the active one
2227 if node
.rna_type
.identifier
== output_type \
2228 and (node
.is_active_output
if hasattr(node
, 'is_active_output')
2232 else: # No output node exists
2233 bpy
.ops
.node
.select_all(action
="DESELECT")
2234 output_node
= nodes
.new(output_type
)
2235 output_node
.location
.x
= active
.location
.x
+ active
.dimensions
.x
+ 80
2236 output_node
.location
.y
= active
.location
.y
2239 for i
, output
in enumerate(active
.outputs
):
2240 if is_visible_socket(output
):
2243 for i
, output
in enumerate(active
.outputs
):
2244 if output
.type == output_node
.inputs
[0].type and is_visible_socket(output
):
2249 if tree_type
== 'ShaderNodeTree':
2250 if active
.outputs
[output_index
].name
== 'Volume':
2252 elif active
.outputs
[output_index
].name
== 'Displacement':
2254 elif tree_type
== 'GeometryNodeTree':
2255 if active
.outputs
[output_index
].type != 'GEOMETRY':
2256 return {'CANCELLED'}
2257 connect_sockets(active
.outputs
[output_index
], output_node
.inputs
[out_input_index
])
2259 force_update(context
) # viewport render does not update
2264 class NWMakeLink(Operator
, NWBase
):
2265 """Make a link from one socket to another"""
2266 bl_idname
= 'node.nw_make_link'
2267 bl_label
= 'Make Link'
2268 bl_options
= {'REGISTER', 'UNDO'}
2269 from_socket
: IntProperty()
2270 to_socket
: IntProperty()
2272 def execute(self
, context
):
2273 nodes
, links
= get_nodes_links(context
)
2275 n1
= nodes
[context
.scene
.NWLazySource
]
2276 n2
= nodes
[context
.scene
.NWLazyTarget
]
2278 connect_sockets(n1
.outputs
[self
.from_socket
], n2
.inputs
[self
.to_socket
])
2280 force_update(context
)
2285 class NWCallInputsMenu(Operator
, NWBase
):
2286 """Link from this output"""
2287 bl_idname
= 'node.nw_call_inputs_menu'
2288 bl_label
= 'Make Link'
2289 bl_options
= {'REGISTER', 'UNDO'}
2290 from_socket
: IntProperty()
2292 def execute(self
, context
):
2293 nodes
, links
= get_nodes_links(context
)
2295 context
.scene
.NWSourceSocket
= self
.from_socket
2297 n1
= nodes
[context
.scene
.NWLazySource
]
2298 n2
= nodes
[context
.scene
.NWLazyTarget
]
2299 if len(n2
.inputs
) > 1:
2300 bpy
.ops
.wm
.call_menu("INVOKE_DEFAULT", name
=NWConnectionListInputs
.bl_idname
)
2301 elif len(n2
.inputs
) == 1:
2302 connect_sockets(n1
.outputs
[self
.from_socket
], n2
.inputs
[0])
2306 class NWAddSequence(Operator
, NWBase
, ImportHelper
):
2307 """Add an Image Sequence"""
2308 bl_idname
= 'node.nw_add_sequence'
2309 bl_label
= 'Import Image Sequence'
2310 bl_options
= {'REGISTER', 'UNDO'}
2312 directory
: StringProperty(
2315 filename
: StringProperty(
2318 files
: CollectionProperty(
2319 type=bpy
.types
.OperatorFileListElement
,
2320 options
={'HIDDEN', 'SKIP_SAVE'}
2322 relative_path
: BoolProperty(
2323 name
='Relative Path',
2324 description
='Set the file path relative to the blend file, when possible',
2328 def draw(self
, context
):
2329 layout
= self
.layout
2330 layout
.alignment
= 'LEFT'
2332 layout
.prop(self
, 'relative_path')
2334 def execute(self
, context
):
2335 nodes
, links
= get_nodes_links(context
)
2336 directory
= self
.directory
2337 filename
= self
.filename
2339 tree
= context
.space_data
.node_tree
2342 # print ("\nDIR:", directory)
2343 # print ("FN:", filename)
2344 # print ("Fs:", list(f.name for f in files), '\n')
2346 if tree
.type == 'SHADER':
2347 node_type
= "ShaderNodeTexImage"
2348 elif tree
.type == 'COMPOSITING':
2349 node_type
= "CompositorNodeImage"
2351 self
.report({'ERROR'}, "Unsupported Node Tree type!")
2352 return {'CANCELLED'}
2354 if not files
[0].name
and not filename
:
2355 self
.report({'ERROR'}, "No file chosen")
2356 return {'CANCELLED'}
2357 elif files
[0].name
and (not filename
or not path
.exists(directory
+ filename
)):
2358 # User has selected multiple files without an active one, or the active one is non-existent
2359 filename
= files
[0].name
2361 if not path
.exists(directory
+ filename
):
2362 self
.report({'ERROR'}, filename
+ " does not exist!")
2363 return {'CANCELLED'}
2365 without_ext
= '.'.join(filename
.split('.')[:-1])
2367 # if last digit isn't a number, it's not a sequence
2368 if not without_ext
[-1].isdigit():
2369 self
.report({'ERROR'}, filename
+ " does not seem to be part of a sequence")
2370 return {'CANCELLED'}
2372 extension
= filename
.split('.')[-1]
2373 reverse
= without_ext
[::-1] # reverse string
2376 for char
in reverse
:
2382 without_num
= without_ext
[:count_numbers
* -1]
2384 files
= sorted(glob(directory
+ without_num
+ "[0-9]" * count_numbers
+ "." + extension
))
2386 num_frames
= len(files
)
2388 nodes_list
= [node
for node
in nodes
]
2390 nodes_list
.sort(key
=lambda k
: k
.location
.x
)
2391 xloc
= nodes_list
[0].location
.x
- 220 # place new nodes at far left
2395 yloc
+= node_mid_pt(node
, 'y')
2396 yloc
= yloc
/ len(nodes
)
2401 name_with_hashes
= without_num
+ "#" * count_numbers
+ '.' + extension
2403 bpy
.ops
.node
.add_node('INVOKE_DEFAULT', use_transform
=True, type=node_type
)
2405 node
.label
= name_with_hashes
2407 filepath
= directory
+ (without_ext
+ '.' + extension
)
2408 if self
.relative_path
:
2409 if bpy
.data
.filepath
:
2411 filepath
= bpy
.path
.relpath(filepath
)
2415 img
= bpy
.data
.images
.load(filepath
)
2416 img
.source
= 'SEQUENCE'
2417 img
.name
= name_with_hashes
2419 image_user
= node
.image_user
if tree
.type == 'SHADER' else node
2420 # separate the number from the file name of the first file
2421 image_user
.frame_offset
= int(files
[0][len(without_num
) + len(directory
):-1 * (len(extension
) + 1)]) - 1
2422 image_user
.frame_duration
= num_frames
2427 class NWAddMultipleImages(Operator
, NWBase
, ImportHelper
):
2428 """Add multiple images at once"""
2429 bl_idname
= 'node.nw_add_multiple_images'
2430 bl_label
= 'Open Selected Images'
2431 bl_options
= {'REGISTER', 'UNDO'}
2432 directory
: StringProperty(
2435 files
: CollectionProperty(
2436 type=bpy
.types
.OperatorFileListElement
,
2437 options
={'HIDDEN', 'SKIP_SAVE'}
2440 def execute(self
, context
):
2441 nodes
, links
= get_nodes_links(context
)
2443 xloc
, yloc
= context
.region
.view2d
.region_to_view(context
.area
.width
/ 2, context
.area
.height
/ 2)
2445 if context
.space_data
.node_tree
.type == 'SHADER':
2446 node_type
= "ShaderNodeTexImage"
2447 elif context
.space_data
.node_tree
.type == 'COMPOSITING':
2448 node_type
= "CompositorNodeImage"
2450 self
.report({'ERROR'}, "Unsupported Node Tree type!")
2451 return {'CANCELLED'}
2454 for f
in self
.files
:
2457 node
= nodes
.new(node_type
)
2458 new_nodes
.append(node
)
2461 node
.location
.x
= xloc
2462 node
.location
.y
= yloc
2465 img
= bpy
.data
.images
.load(self
.directory
+ fname
)
2468 # shift new nodes up to center of tree
2469 list_size
= new_nodes
[0].location
.y
- new_nodes
[-1].location
.y
2471 if node
in new_nodes
:
2473 node
.location
.y
+= (list_size
/ 2)
2479 class NWViewerFocus(bpy
.types
.Operator
):
2480 """Set the viewer tile center to the mouse position"""
2481 bl_idname
= "node.nw_viewer_focus"
2482 bl_label
= "Viewer Focus"
2484 x
: bpy
.props
.IntProperty()
2485 y
: bpy
.props
.IntProperty()
2488 def poll(cls
, context
):
2489 return (nw_check(context
)
2490 and nw_check_space_type(cls
, context
, 'CompositorNodeTree'))
2492 def execute(self
, context
):
2495 def invoke(self
, context
, event
):
2496 render
= context
.scene
.render
2497 space
= context
.space_data
2498 percent
= render
.resolution_percentage
* 0.01
2500 nodes
, links
= get_nodes_links(context
)
2501 viewers
= [n
for n
in nodes
if n
.type == 'VIEWER']
2504 mlocx
= event
.mouse_region_x
2505 mlocy
= event
.mouse_region_y
2506 select_node
= bpy
.ops
.node
.select(location
=(mlocx
, mlocy
), extend
=False)
2508 if 'FINISHED' not in select_node
: # only run if we're not clicking on a node
2509 region_x
= context
.region
.width
2510 region_y
= context
.region
.height
2512 region_center_x
= context
.region
.width
/ 2
2513 region_center_y
= context
.region
.height
/ 2
2515 bd_x
= render
.resolution_x
* percent
* space
.backdrop_zoom
2516 bd_y
= render
.resolution_y
* percent
* space
.backdrop_zoom
2518 backdrop_center_x
= (bd_x
/ 2) - space
.backdrop_offset
[0]
2519 backdrop_center_y
= (bd_y
/ 2) - space
.backdrop_offset
[1]
2521 margin_x
= region_center_x
- backdrop_center_x
2522 margin_y
= region_center_y
- backdrop_center_y
2524 abs_mouse_x
= (mlocx
- margin_x
) / bd_x
2525 abs_mouse_y
= (mlocy
- margin_y
) / bd_y
2527 for node
in viewers
:
2528 node
.center_x
= abs_mouse_x
2529 node
.center_y
= abs_mouse_y
2531 return {'PASS_THROUGH'}
2533 return self
.execute(context
)
2536 class NWSaveViewer(bpy
.types
.Operator
, ExportHelper
):
2537 """Save the current viewer node to an image file"""
2538 bl_idname
= "node.nw_save_viewer"
2539 bl_label
= "Save This Image"
2540 filepath
: StringProperty(subtype
="FILE_PATH")
2541 filename_ext
: EnumProperty(
2543 description
="Choose the file format to save to",
2544 items
=(('.bmp', "BMP", ""),
2545 ('.rgb', 'IRIS', ""),
2546 ('.png', 'PNG', ""),
2547 ('.jpg', 'JPEG', ""),
2548 ('.jp2', 'JPEG2000', ""),
2549 ('.tga', 'TARGA', ""),
2550 ('.cin', 'CINEON', ""),
2551 ('.dpx', 'DPX', ""),
2552 ('.exr', 'OPEN_EXR', ""),
2553 ('.hdr', 'HDR', ""),
2554 ('.tif', 'TIFF', "")),
2559 def poll(cls
, context
):
2560 return (nw_check(context
)
2561 and nw_check_space_type(cls
, context
, 'CompositorNodeTree')
2562 and any(img
.source
== 'VIEWER'
2563 and img
.render_slots
== 0
2564 for img
in bpy
.data
.images
)
2565 and sum(bpy
.data
.images
["Viewer Node"].size
) > 0) # False if not connected or connected but no image
2567 def execute(self
, context
):
2584 basename
, ext
= path
.splitext(fp
)
2585 old_render_format
= context
.scene
.render
.image_settings
.file_format
2586 context
.scene
.render
.image_settings
.file_format
= formats
[self
.filename_ext
]
2587 context
.area
.type = "IMAGE_EDITOR"
2588 context
.area
.spaces
[0].image
= bpy
.data
.images
['Viewer Node']
2589 context
.area
.spaces
[0].image
.save_render(fp
)
2590 context
.area
.type = "NODE_EDITOR"
2591 context
.scene
.render
.image_settings
.file_format
= old_render_format
2595 class NWResetNodes(bpy
.types
.Operator
):
2596 """Reset Nodes in Selection"""
2597 bl_idname
= "node.nw_reset_nodes"
2598 bl_label
= "Reset Nodes"
2599 bl_options
= {'REGISTER', 'UNDO'}
2602 def poll(cls
, context
):
2603 space
= context
.space_data
2604 return space
.type == 'NODE_EDITOR'
2606 def execute(self
, context
):
2607 node_active
= context
.active_node
2608 node_selected
= context
.selected_nodes
2609 node_ignore
= ["FRAME", "REROUTE", "GROUP", "SIMULATION_INPUT", "SIMULATION_OUTPUT"]
2611 # Check if one node is selected at least
2612 if not (len(node_selected
) > 0):
2613 self
.report({'ERROR'}, "1 node must be selected at least")
2614 return {'CANCELLED'}
2616 active_node_name
= node_active
.name
if node_active
.select
else None
2617 valid_nodes
= [n
for n
in node_selected
if n
.type not in node_ignore
]
2619 # Create output lists
2620 selected_node_names
= [n
.name
for n
in node_selected
]
2623 # Reset all valid children in a frame
2624 node_active_is_frame
= False
2625 if len(node_selected
) == 1 and node_active
.type == "FRAME":
2626 node_tree
= node_active
.id_data
2627 children
= [n
for n
in node_tree
.nodes
if n
.parent
== node_active
]
2629 valid_nodes
= [n
for n
in children
if n
.type not in node_ignore
]
2630 selected_node_names
= [n
.name
for n
in children
if n
.type not in node_ignore
]
2631 node_active_is_frame
= True
2633 # Check if valid nodes in selection
2634 if not (len(valid_nodes
) > 0):
2635 # Check for frames only
2636 frames_selected
= [n
for n
in node_selected
if n
.type == "FRAME"]
2637 if (len(frames_selected
) > 1 and len(frames_selected
) == len(node_selected
)):
2638 self
.report({'ERROR'}, "Please select only 1 frame to reset")
2640 self
.report({'ERROR'}, "No valid node(s) in selection")
2641 return {'CANCELLED'}
2643 # Report nodes that are not valid
2644 if len(valid_nodes
) != len(node_selected
) and node_active_is_frame
is False:
2645 valid_node_names
= [n
.name
for n
in valid_nodes
]
2646 not_valid_names
= list(set(selected_node_names
) - set(valid_node_names
))
2647 self
.report({'INFO'}, "Ignored {}".format(", ".join(not_valid_names
)))
2649 # Deselect all nodes
2650 for i
in node_selected
:
2653 # Run through all valid nodes
2654 for node
in valid_nodes
:
2656 parent
= node
.parent
if node
.parent
else None
2657 node_loc
= [node
.location
.x
, node
.location
.y
]
2659 node_tree
= node
.id_data
2660 props_to_copy
= 'bl_idname name location height width'.split(' ')
2663 mappings
= chain
.from_iterable([node
.inputs
, node
.outputs
])
2664 for i
in (i
for i
in mappings
if i
.is_linked
):
2666 reconnections
.append([L
.from_socket
.path_from_id(), L
.to_socket
.path_from_id()])
2668 props
= {j
: getattr(node
, j
) for j
in props_to_copy
}
2670 new_node
= node_tree
.nodes
.new(props
['bl_idname'])
2671 props_to_copy
.pop(0)
2673 for prop
in props_to_copy
:
2674 setattr(new_node
, prop
, props
[prop
])
2676 nodes
= node_tree
.nodes
2678 new_node
.name
= props
['name']
2681 new_node
.parent
= parent
2682 new_node
.location
= node_loc
2684 for str_from
, str_to
in reconnections
:
2685 connect_sockets(eval(str_from
), eval(str_to
))
2687 new_node
.select
= False
2688 success_names
.append(new_node
.name
)
2690 # Reselect all nodes
2691 if selected_node_names
and node_active_is_frame
is False:
2692 for i
in selected_node_names
:
2693 node_tree
.nodes
[i
].select
= True
2695 if active_node_name
is not None:
2696 node_tree
.nodes
[active_node_name
].select
= True
2697 node_tree
.nodes
.active
= node_tree
.nodes
[active_node_name
]
2699 self
.report({'INFO'}, "Successfully reset {}".format(", ".join(success_names
)))
2721 NWAddPrincipledSetup
,
2723 NWLinkActiveToSelected
,
2725 NWSelectParentChildren
,
2731 NWAddMultipleImages
,
2739 from bpy
.utils
import register_class
2745 from bpy
.utils
import unregister_class
2748 unregister_class(cls
)